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
|
// Validator
|
||||||
export { validateAbility, validateInputs } from './validator/index.js'
|
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
|
// Executor
|
||||||
export { executeAbility, formatExecutionResult } from './executor/index.js'
|
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