Skip to content

feat(context): implement runtime discovery and permission validation#238

Open
Protocol-zero-0 wants to merge 1 commit intodarrenhinde:mainfrom
Protocol-zero-0:main
Open

feat(context): implement runtime discovery and permission validation#238
Protocol-zero-0 wants to merge 1 commit intodarrenhinde:mainfrom
Protocol-zero-0:main

Conversation

@Protocol-zero-0
Copy link

Description

Implements the core runtime context discovery and permission validation system requested in #140.

Features

  1. Context Discovery: Scans .opencode/context for YAML/JSON definitions.
  2. Permission Validation: Adds PermissionValidator to check agent access to skills/tools.
  3. Type Definitions: Adds Zod schemas for Context and Permissions.

Implementation Details

  • Added packages/plugin-abilities/src/context/discovery.ts
  • Added packages/plugin-abilities/src/validator/permissions.ts
  • Exports new modules in index.ts

Usage

const discovery = new ContextDiscovery();
const contexts = await discovery.discover();

const validator = new PermissionValidator();
const isValid = validator.checkSkillAccess(agentPerms, 'git', 'commit');

Closes #140

Copilot AI review requested due to automatic review settings February 22, 2026 21:14
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the core runtime context discovery and permission validation system as requested in issue #140. It adds the foundational infrastructure for discovering YAML/JSON context definitions from .opencode/context and validating agent skill/tool permissions.

Changes:

  • Adds Zod schemas and TypeScript types for context definitions and agent permissions
  • Implements ContextDiscovery class to scan and load context files from the filesystem
  • Implements PermissionValidator class to validate agent permissions and check skill/tool access

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 14 comments.

File Description
packages/plugin-abilities/src/context/types.ts Defines Zod schemas and TypeScript types for ContextDefinition, LoadedContext, and AgentPermissions
packages/plugin-abilities/src/context/discovery.ts Implements ContextDiscovery class for scanning .opencode/context directory and loading context files
packages/plugin-abilities/src/validator/permissions.ts Implements PermissionValidator class for validating agent permissions and checking skill/tool access
packages/plugin-abilities/src/index.ts Exports new context discovery and permission validation modules

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +35
checkSkillAccess(agentPermissions: AgentPermissions, skillName: string, toolName: string): boolean {
const permission = agentPermissions.permissions.find(p => p.skill === skillName);

if (!permission) {
// Default deny if no explicit permission for skill
return false;
}

if (!permission.tools) {
// If tools not specified, assume strict/deny or allow all?
// Security best practice: Default deny.
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Security best practice: Default deny" explains the rationale but remains as a comment instead of being removed. According to the issue description, this feature implements "Skill permission validation in agent frontmatter" with "Tool access restrictions per skill". The behavior of undefined tools should be explicitly documented in the schema or type definition, not just in implementation comments, to ensure API consumers understand the security model.

Suggested change
checkSkillAccess(agentPermissions: AgentPermissions, skillName: string, toolName: string): boolean {
const permission = agentPermissions.permissions.find(p => p.skill === skillName);
if (!permission) {
// Default deny if no explicit permission for skill
return false;
}
if (!permission.tools) {
// If tools not specified, assume strict/deny or allow all?
// Security best practice: Default deny.
/**
* Determines whether the given {@link skillName} is allowed to use {@link toolName}
* according to the provided {@link AgentPermissions}.
*
* Security model:
* - If there is no permission entry for the skill, access is denied (default deny).
* - If the permission entry does not define a `tools` array, access is denied (default deny).
* - If `tools` contains `"*"`, all tools are allowed for that skill.
* - Otherwise, only tools explicitly listed in `tools` are allowed.
*/
checkSkillAccess(agentPermissions: AgentPermissions, skillName: string, toolName: string): boolean {
const permission = agentPermissions.permissions.find(p => p.skill === skillName);
if (!permission) {
// No explicit permission for this skill -> deny access (default deny).
return false;
}
if (!permission.tools) {
// Tools list omitted for this skill -> deny access (default deny).

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +45
export class PermissionValidator {
validateAgentPermissions(data: unknown): PermissionValidationResult {
const result = AgentPermissionsSchema.safeParse(data);

if (!result.success) {
return {
valid: false,
errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`),
};
}

return {
valid: true,
errors: [],
};
}

checkSkillAccess(agentPermissions: AgentPermissions, skillName: string, toolName: string): boolean {
const permission = agentPermissions.permissions.find(p => p.skill === skillName);

if (!permission) {
// Default deny if no explicit permission for skill
return false;
}

if (!permission.tools) {
// If tools not specified, assume strict/deny or allow all?
// Security best practice: Default deny.
return false;
}

if (permission.tools.includes('*') || permission.tools.includes(toolName)) {
return true;
}

return false;
}
}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new PermissionValidator class. The codebase has comprehensive test coverage (87 tests across 7 test files as mentioned in README.md), including validator.test.ts for validation logic. The new permission validation functionality should have corresponding tests to verify: 1) validateAgentPermissions correctly validates schema, 2) checkSkillAccess handles wildcard permissions, 3) checkSkillAccess handles missing skills, 4) checkSkillAccess handles missing tools array, and 5) checkSkillAccess handles specific tool names.

Copilot uses AI. Check for mistakes.
return null;
}
}
// TODO: Handle URL and API types
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loadContext method only implements the 'file' type and returns null for 'url' and 'api' types with a TODO comment. According to the ContextTypeSchema in types.ts, these are valid context types. This creates an incomplete implementation where valid context definitions will silently fail to load. Consider either: 1) throwing an error for unsupported types to make the limitation explicit, 2) filtering these types during discovery, or 3) implementing basic support for these types before merging.

Suggested change
// TODO: Handle URL and API types
if (definition.type === 'url' || definition.type === 'api') {
console.warn(
`Context type '${definition.type}' is currently not supported by loadContext and will not be loaded.`,
);
return null;
}
console.warn(
`Unknown or unsupported context type '${(definition as any).type}' encountered in loadContext; skipping.`,
);

Copilot uses AI. Check for mistakes.
source: filePath,
};
} catch (error) {
console.warn(`Failed to read context file ${definition.path}:`, error);
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loadContext method uses console.warn for file read failures. The existing loader pattern (loader/index.ts:76) uses console.error with a prefix for file loading failures. For consistency, this should use console.error with an appropriate prefix like '[context]'.

Suggested change
console.warn(`Failed to read context file ${definition.path}:`, error);
console.error(`[context] Failed to read context file ${definition.path}:`, error);

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +72
export class ContextDiscovery {
private rootDir: string;
private contextDir: string;

constructor(options: DiscoveryOptions = {}) {
this.rootDir = options.rootDir || process.cwd();
this.contextDir = options.contextDir || path.join(this.rootDir, '.opencode', 'context');
}

async discover(): Promise<ContextDefinition[]> {
const files = await glob('**/*.{yaml,yml,json}', {
cwd: this.contextDir,
ignore: ['node_modules/**'],
});

const definitions: ContextDefinition[] = [];

for (const file of files) {
const filePath = path.join(this.contextDir, file);
try {
const content = await fs.readFile(filePath, 'utf-8');
const parsed = file.endsWith('.json') ? JSON.parse(content) : parseYaml(content);

// Handle array of definitions or single definition
const items = Array.isArray(parsed) ? parsed : [parsed];

for (const item of items) {
const result = ContextDefinitionSchema.safeParse(item);
if (result.success) {
definitions.push(result.data);
} else {
console.warn(`Invalid context definition in ${file}:`, result.error.format());
}
}
} catch (error) {
console.warn(`Failed to load context file ${file}:`, error);
}
}

return definitions;
}

async loadContext(definition: ContextDefinition): Promise<LoadedContext | null> {
if (definition.type === 'file') {
const filePath = path.resolve(this.rootDir, definition.path);
try {
const content = await fs.readFile(filePath, 'utf-8');
return {
definition,
content,
source: filePath,
};
} catch (error) {
console.warn(`Failed to read context file ${definition.path}:`, error);
return null;
}
}
// TODO: Handle URL and API types
return null;
}
}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new ContextDiscovery class. The codebase has comprehensive test coverage (87 tests across 7 test files as mentioned in README.md). The new context discovery functionality should have corresponding tests to verify: 1) discover method finds YAML and JSON files, 2) discover method handles multiple definitions in one file, 3) discover method validates schemas correctly, 4) discover method handles missing directories gracefully, 5) loadContext method loads file types correctly, and 6) error handling for invalid files.

Copilot uses AI. Check for mistakes.

async loadContext(definition: ContextDefinition): Promise<LoadedContext | null> {
if (definition.type === 'file') {
const filePath = path.resolve(this.rootDir, definition.path);
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential path traversal vulnerability in loadContext. The method uses path.resolve(this.rootDir, definition.path) which could allow loading files outside the intended directory if definition.path contains path traversal sequences like '../../../etc/passwd'. Since ContextDefinition comes from user-controlled YAML/JSON files in .opencode/context, this creates a security risk. Consider validating that the resolved path is within the allowed directory using path normalization and checking if the resolved path starts with rootDir.

Suggested change
const filePath = path.resolve(this.rootDir, definition.path);
const rootDir = path.resolve(this.rootDir);
const filePath = path.resolve(rootDir, definition.path);
// Ensure the resolved path is within the allowed root directory
if (filePath !== rootDir && !filePath.startsWith(rootDir + path.sep)) {
console.warn(
`Refusing to load context file outside of root directory: ${definition.path} -> ${filePath}`,
);
return null;
}

Copilot uses AI. Check for mistakes.
content: string;
source: string;
}

Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SkillPermission schema makes the tools field optional. However, according to the permission validation logic in permissions.ts (lines 33-36), when tools is undefined, access is denied by default. This important security behavior is not documented in the schema. Consider adding a JSDoc comment to the SkillPermissionSchema explaining that omitting tools results in default-deny behavior, or making tools required with an empty array default to make the security model more explicit.

Suggested change
/**
* Schema for skill-level permissions.
*
* NOTE: The `tools` field is optional here, but the permission validation logic
* (see `permissions.ts`) treats an omitted/undefined `tools` value as
* "deny all tools" (default-deny). To explicitly allow no tools, you may
* still provide an empty array; to allow specific tools, list them here.
*/

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,35 @@
import { z } from 'zod';

export const ContextTypeSchema = z.enum(['file', 'url', 'api', 'snippet']);
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ContextTypeSchema defines 'snippet' as a valid context type, but there is no implementation or documentation explaining what a snippet context is or how it differs from a file context. This creates ambiguity for API consumers. Consider either: 1) implementing snippet support in loadContext, 2) documenting what snippet means and when to use it vs file, or 3) removing it if not needed for the initial implementation.

Suggested change
export const ContextTypeSchema = z.enum(['file', 'url', 'api', 'snippet']);
// Supported context types; additional types must be implemented and documented before being added here.
export const ContextTypeSchema = z.enum(['file', 'url', 'api']);

Copilot uses AI. Check for mistakes.
if (!result.success) {
return {
valid: false,
errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`),
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message formatting in validateAgentPermissions concatenates the error path with dots. However, when the path is empty (which can happen for root-level validation errors), this will produce error messages starting with a colon like ": Required". Consider handling empty paths specially to avoid confusing error messages.

Suggested change
errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`),
errors: result.error.errors.map(e =>
e.path.length > 0 ? `${e.path.join('.')}: ${e.message}` : e.message
),

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +35
// If tools not specified, assume strict/deny or allow all?
// Security best practice: Default deny.
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on lines 34-35 introduces ambiguity about the intended behavior when tools are not specified. While the implementation chooses default deny (security best practice), this decision should be documented in the type definition or through comprehensive JSDoc comments. This is critical for API consumers to understand the permission model. Consider adding JSDoc to the SkillPermission type in types.ts explaining that omitting the tools field denies all access.

Suggested change
// If tools not specified, assume strict/deny or allow all?
// Security best practice: Default deny.
// If tools are not specified for a skill, deny access to all tools (default-deny).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement runtime context discovery and skill permission validation

2 participants