feat(context): implement runtime discovery and permission validation (#238)
This commit is contained in:
parent
10682353e9
commit
25db9470cc
4 changed files with 157 additions and 0 deletions
72
packages/plugin-abilities/src/context/discovery.ts
Normal file
72
packages/plugin-abilities/src/context/discovery.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
35
packages/plugin-abilities/src/context/types.ts
Normal file
35
packages/plugin-abilities/src/context/types.ts
Normal 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>;
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
45
packages/plugin-abilities/src/validator/permissions.ts
Normal file
45
packages/plugin-abilities/src/validator/permissions.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue