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

This commit is contained in:
Protocol Zero 2026-03-12 02:57:14 +08:00 committed by GitHub
parent 10682353e9
commit 25db9470cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 157 additions and 0 deletions

View file

@ -0,0 +1,72 @@
import { glob } from 'glob';
import path from 'path';
import fs from 'fs/promises';
import { parse as parseYaml } from 'yaml';
import { ContextDefinitionSchema, type ContextDefinition, type LoadedContext } from './types.js';
export interface DiscoveryOptions {
rootDir?: string;
contextDir?: string;
}
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;
}
}

View file

@ -0,0 +1,35 @@
import { z } from 'zod';
export const ContextTypeSchema = z.enum(['file', 'url', 'api', 'snippet']);
export const ContextDefinitionSchema = z.object({
id: z.string(),
type: ContextTypeSchema,
path: z.string(),
description: z.string().optional(),
priority: z.number().optional(),
metadata: z.record(z.unknown()).optional(),
});
export type ContextDefinition = z.infer<typeof ContextDefinitionSchema>;
export interface LoadedContext {
definition: ContextDefinition;
content: string;
source: string;
}
export const SkillPermissionSchema = z.object({
skill: z.string(),
tools: z.array(z.string()).optional(),
resources: z.array(z.string()).optional(),
description: z.string().optional(),
});
export const AgentPermissionsSchema = z.object({
agent: z.string(),
permissions: z.array(SkillPermissionSchema),
});
export type SkillPermission = z.infer<typeof SkillPermissionSchema>;
export type AgentPermissions = z.infer<typeof AgentPermissionsSchema>;

View file

@ -24,6 +24,11 @@ export { loadAbilities, loadAbility } from './loader/index.js'
// Validator
export { validateAbility, validateInputs } from './validator/index.js'
export { PermissionValidator } from './validator/permissions.js'
// Context Discovery
export { ContextDiscovery } from './context/discovery.js'
export type { ContextDefinition, LoadedContext, AgentPermissions } from './context/types.js'
// Executor
export { executeAbility, formatExecutionResult } from './executor/index.js'

View file

@ -0,0 +1,45 @@
import { AgentPermissionsSchema, type AgentPermissions, type SkillPermission } from '../context/types.js';
export interface PermissionValidationResult {
valid: boolean;
errors: string[];
}
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;
}
}