From b649edfda13643dc0ec76e98e13ebc373dfa9d87 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Mon, 9 Feb 2026 11:17:45 +0500 Subject: [PATCH 1/2] feat: agent JSON Schema validation + agent lint CLI Add agent definition schema (packages/core/schemas/agent.schema.json) with validateAgent, lintAgent, lintAgentsDir functions and `agentplane agent lint` CLI command using CommandSpec pattern. Co-Authored-By: Claude Opus 4.6 --- .../agentplane/src/cli/run-cli/registry.ts | 4 + .../src/commands/agent/lint.command.ts | 17 +++ .../agentplane/src/commands/agent/lint.ts | 35 +++++ packages/core/schemas/agent.schema.json | 35 +++++ packages/core/src/agents/agent-schema.test.ts | 121 +++++++++++++++ packages/core/src/agents/agent-schema.ts | 141 ++++++++++++++++++ packages/core/src/index.ts | 8 + packages/spec/examples/agent.json | 12 ++ packages/spec/schemas/agent.schema.json | 35 +++++ 9 files changed, 408 insertions(+) create mode 100644 packages/agentplane/src/commands/agent/lint.command.ts create mode 100644 packages/agentplane/src/commands/agent/lint.ts create mode 100644 packages/core/schemas/agent.schema.json create mode 100644 packages/core/src/agents/agent-schema.test.ts create mode 100644 packages/core/src/agents/agent-schema.ts create mode 100644 packages/spec/examples/agent.json create mode 100644 packages/spec/schemas/agent.schema.json diff --git a/packages/agentplane/src/cli/run-cli/registry.ts b/packages/agentplane/src/cli/run-cli/registry.ts index a753aa80..e23be187 100644 --- a/packages/agentplane/src/cli/run-cli/registry.ts +++ b/packages/agentplane/src/cli/run-cli/registry.ts @@ -173,6 +173,8 @@ import { } from "../../commands/guard/suggest-allow.command.js"; import { guardCommitSpec, makeRunGuardCommitHandler } from "../../commands/guard/commit.command.js"; +import { agentLintSpec, runAgentLint } from "../../commands/agent/lint.command.js"; + import type { CommandContext } from "../../commands/shared/task-backend.js"; const helpNoop = () => Promise.resolve(0); @@ -184,6 +186,7 @@ export function buildHelpFastRegistry(): CommandRegistry { registry.register(quickstartSpec, helpNoop); registry.register(roleSpec, helpNoop); registry.register(agentsSpec, helpNoop); + registry.register(agentLintSpec, helpNoop); registry.register(configShowSpec, helpNoop); registry.register(configSetSpec, helpNoop); registry.register(modeGetSpec, helpNoop); @@ -277,6 +280,7 @@ export function buildRegistry( registry.register(quickstartSpec, runQuickstart); registry.register(roleSpec, runRole); registry.register(agentsSpec, runAgents); + registry.register(agentLintSpec, runAgentLint); registry.register(configShowSpec, runConfigShow); registry.register(configSetSpec, runConfigSet); registry.register(modeGetSpec, runModeGet); diff --git a/packages/agentplane/src/commands/agent/lint.command.ts b/packages/agentplane/src/commands/agent/lint.command.ts new file mode 100644 index 00000000..7056e011 --- /dev/null +++ b/packages/agentplane/src/commands/agent/lint.command.ts @@ -0,0 +1,17 @@ +import type { CommandCtx, CommandSpec } from "../../cli/spec/spec.js"; + +import { cmdAgentLint } from "./lint.js"; + +export type AgentLintParsed = Record; + +export const agentLintSpec: CommandSpec = { + id: ["agent", "lint"], + group: "Agent", + summary: "Lint agent JSON definitions (schema + invariants).", + examples: [{ cmd: "agentplane agent lint", why: "Validate all agent definitions." }], + parse: () => ({}), +}; + +export async function runAgentLint(ctx: CommandCtx): Promise { + return await cmdAgentLint({ cwd: ctx.cwd, rootOverride: ctx.rootOverride }); +} diff --git a/packages/agentplane/src/commands/agent/lint.ts b/packages/agentplane/src/commands/agent/lint.ts new file mode 100644 index 00000000..3a6e0210 --- /dev/null +++ b/packages/agentplane/src/commands/agent/lint.ts @@ -0,0 +1,35 @@ +import { lintAgentsDir, loadConfig, resolveProject } from "@agentplaneorg/core"; +import path from "node:path"; + +import { mapCoreError } from "../../cli/error-map.js"; +import { CliError } from "../../shared/errors.js"; + +export async function cmdAgentLint(opts: { + cwd: string; + rootOverride?: string; +}): Promise { + try { + const resolved = await resolveProject({ + cwd: opts.cwd, + rootOverride: opts.rootOverride ?? null, + }); + const loaded = await loadConfig(resolved.agentplaneDir); + const agentsDir = path.join(resolved.gitRoot, loaded.config.paths.agents_dir); + const result = await lintAgentsDir(agentsDir); + if (result.errors.length > 0) { + throw new CliError({ + exitCode: 3, + code: "E_VALIDATION", + message: result.errors.join("\n"), + }); + } + for (const w of result.warnings) { + process.stderr.write(`warn: ${w}\n`); + } + process.stdout.write("OK\n"); + return 0; + } catch (err) { + if (err instanceof CliError) throw err; + throw mapCoreError(err, { command: "agent lint", root: opts.rootOverride ?? null }); + } +} diff --git a/packages/core/schemas/agent.schema.json b/packages/core/schemas/agent.schema.json new file mode 100644 index 00000000..6ea21785 --- /dev/null +++ b/packages/core/schemas/agent.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://agentplane.dev/schemas/agent.schema.json", + "title": "agentplane agent definition", + "type": "object", + "additionalProperties": true, + "required": ["id", "role", "description", "permissions", "workflow"], + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]*$", + "minLength": 1 + }, + "role": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 }, + "inputs": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "outputs": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "permissions": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + }, + "workflow": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + } + } +} diff --git a/packages/core/src/agents/agent-schema.test.ts b/packages/core/src/agents/agent-schema.test.ts new file mode 100644 index 00000000..0f2afa59 --- /dev/null +++ b/packages/core/src/agents/agent-schema.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from "vitest"; +import os from "node:os"; +import path from "node:path"; +import { mkdtemp, writeFile, mkdir } from "node:fs/promises"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import { validateAgent, lintAgent, lintAgentsDir } from "../index.js"; + +const VALID_AGENT = { + id: "TEST_AGENT", + role: "Test role.", + description: "Test description.", + permissions: ["read"], + workflow: ["Do things."], +}; + +describe("validateAgent", () => { + it("accepts a valid agent", () => { + expect(() => validateAgent(VALID_AGENT)).not.toThrow(); + }); + + it("spec example agent validates at runtime", () => { + const exampleUrl = new URL("../../../spec/examples/agent.json", import.meta.url); + const text = readFileSync(fileURLToPath(exampleUrl), "utf8"); + const parsed = JSON.parse(text) as unknown; + expect(() => validateAgent(parsed)).not.toThrow(); + }); + + it("rejects missing required fields", () => { + expect(() => validateAgent({})).toThrow(/agent/); + }); + + it("rejects invalid id pattern", () => { + expect(() => validateAgent({ ...VALID_AGENT, id: "lowercase" })).toThrow(/agent/); + }); + + it("rejects empty permissions", () => { + expect(() => validateAgent({ ...VALID_AGENT, permissions: [] })).toThrow(/agent/); + }); +}); + +describe("lintAgent", () => { + it("returns no errors for valid agent", () => { + const result = lintAgent(VALID_AGENT, "TEST_AGENT.json"); + expect(result.errors).toHaveLength(0); + }); + + it("detects filename mismatch", () => { + const result = lintAgent(VALID_AGENT, "WRONG.json"); + expect(result.errors.some((e) => e.includes("does not match"))).toBe(true); + }); + + it("returns schema errors for invalid input", () => { + const result = lintAgent({ id: "bad" }); + expect(result.errors.length).toBeGreaterThan(0); + }); +}); + +describe("lintAgentsDir", () => { + it("lints a directory of valid agents", async () => { + const tmp = await mkdtemp(path.join(os.tmpdir(), "agentplane-agents-lint-")); + const agentsDir = path.join(tmp, "agents"); + await mkdir(agentsDir, { recursive: true }); + + await writeFile( + path.join(agentsDir, "ALPHA.json"), + JSON.stringify({ ...VALID_AGENT, id: "ALPHA" }), + ); + await writeFile( + path.join(agentsDir, "BETA.json"), + JSON.stringify({ ...VALID_AGENT, id: "BETA" }), + ); + + const result = await lintAgentsDir(agentsDir); + expect(result.errors).toHaveLength(0); + }); + + it("detects duplicate agent IDs", async () => { + const tmp = await mkdtemp(path.join(os.tmpdir(), "agentplane-agents-dup-")); + const agentsDir = path.join(tmp, "agents"); + await mkdir(agentsDir, { recursive: true }); + + await writeFile( + path.join(agentsDir, "A.json"), + JSON.stringify({ ...VALID_AGENT, id: "SAME" }), + ); + await writeFile( + path.join(agentsDir, "B.json"), + JSON.stringify({ ...VALID_AGENT, id: "SAME" }), + ); + + const result = await lintAgentsDir(agentsDir); + expect(result.errors.some((e) => e.includes("duplicate"))).toBe(true); + }); + + it("reports invalid JSON gracefully", async () => { + const tmp = await mkdtemp(path.join(os.tmpdir(), "agentplane-agents-bad-")); + const agentsDir = path.join(tmp, "agents"); + await mkdir(agentsDir, { recursive: true }); + + await writeFile(path.join(agentsDir, "BAD.json"), "not json{"); + + const result = await lintAgentsDir(agentsDir); + expect(result.errors.some((e) => e.includes("invalid JSON"))).toBe(true); + }); + + it("warns on empty directory", async () => { + const tmp = await mkdtemp(path.join(os.tmpdir(), "agentplane-agents-empty-")); + const agentsDir = path.join(tmp, "agents"); + await mkdir(agentsDir, { recursive: true }); + + const result = await lintAgentsDir(agentsDir); + expect(result.warnings.some((w) => w.includes("no agent JSON"))).toBe(true); + }); + + it("reports missing directory", async () => { + const result = await lintAgentsDir("/tmp/nonexistent-agents-dir-xyz"); + expect(result.errors.some((e) => e.includes("cannot read"))).toBe(true); + }); +}); diff --git a/packages/core/src/agents/agent-schema.ts b/packages/core/src/agents/agent-schema.ts new file mode 100644 index 00000000..37daca6f --- /dev/null +++ b/packages/core/src/agents/agent-schema.ts @@ -0,0 +1,141 @@ +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import type { ErrorObject, Options, ValidateFunction } from "ajv"; +import AjvModule from "ajv"; + +export type AgentDefinition = { + id: string; + role: string; + description: string; + inputs?: string[]; + outputs?: string[]; + permissions: string[]; + workflow: string[]; +}; + +export type AgentLintResult = { + errors: string[]; + warnings: string[]; +}; + +const AGENT_SCHEMA_URL = new URL("../../schemas/agent.schema.json", import.meta.url); +const AGENT_SCHEMA = JSON.parse( + readFileSync(fileURLToPath(AGENT_SCHEMA_URL), "utf8"), +) as Record; + +type AjvInstance = { + compile: (schema: unknown) => ValidateFunction; + errorsText: (errors?: ErrorObject[] | null, opts?: { dataVar?: string }) => string; +}; + +type AjvConstructor = new (opts?: Options) => AjvInstance; + +const Ajv = + (AjvModule as unknown as { default?: AjvConstructor }).default ?? + (AjvModule as unknown as AjvConstructor); + +const AJV = new Ajv({ + allErrors: true, + allowUnionTypes: true, + strict: false, +}); + +const validateSchema = AJV.compile(AGENT_SCHEMA); + +function formatSchemaErrors(errors: ErrorObject[] | null | undefined): string { + if (!errors || errors.length === 0) return "agent schema validation failed"; + return AJV.errorsText(errors, { dataVar: "agent" }); +} + +export function validateAgent(raw: unknown): AgentDefinition { + const candidate = raw && typeof raw === "object" ? structuredClone(raw) : raw; + if (!validateSchema(candidate)) { + throw new Error(formatSchemaErrors(validateSchema.errors)); + } + return candidate; +} + +export function lintAgent(raw: unknown, fileName?: string): AgentLintResult { + const errors: string[] = []; + const warnings: string[] = []; + + // Schema validation + const candidate = raw && typeof raw === "object" ? structuredClone(raw) : raw; + if (!validateSchema(candidate)) { + const msg = formatSchemaErrors(validateSchema.errors); + errors.push(msg); + return { errors, warnings }; + } + + const agent = candidate as AgentDefinition; + + // Filename must match id + if (fileName) { + const expected = `${agent.id}.json`; + if (fileName !== expected) { + errors.push( + `filename "${fileName}" does not match agent id "${agent.id}" (expected "${expected}")`, + ); + } + } + + return { errors, warnings }; +} + +export async function lintAgentsDir(agentsDir: string): Promise { + const errors: string[] = []; + const warnings: string[] = []; + + let entries: string[]; + try { + entries = await readdir(agentsDir); + } catch { + errors.push(`cannot read agents directory: ${agentsDir}`); + return { errors, warnings }; + } + + const jsonFiles = entries.filter((f) => f.endsWith(".json")).sort(); + if (jsonFiles.length === 0) { + warnings.push("no agent JSON files found"); + return { errors, warnings }; + } + + const seenIds = new Map(); + + for (const file of jsonFiles) { + const filePath = path.join(agentsDir, file); + let raw: unknown; + try { + const text = await readFile(filePath, "utf8"); + raw = JSON.parse(text); + } catch (err) { + errors.push(`${file}: invalid JSON – ${(err as Error).message}`); + continue; + } + + const result = lintAgent(raw, file); + for (const e of result.errors) errors.push(`${file}: ${e}`); + for (const w of result.warnings) warnings.push(`${file}: ${w}`); + + // Check duplicate IDs + if ( + raw && + typeof raw === "object" && + "id" in raw && + typeof (raw as Record).id === "string" + ) { + const id = (raw as Record).id as string; + const prev = seenIds.get(id); + if (prev) { + errors.push(`duplicate agent id "${id}" in ${prev} and ${file}`); + } else { + seenIds.set(id, file); + } + } + } + + return { errors, warnings }; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a0fffddd..e9fddabf 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -92,3 +92,11 @@ export { } from "./commit/commit-policy.js"; export { getStagedFiles, getUnstagedFiles, getUnstagedTrackedFiles } from "./git/git-utils.js"; + +export { + validateAgent, + lintAgent, + lintAgentsDir, + type AgentDefinition, + type AgentLintResult, +} from "./agents/agent-schema.js"; diff --git a/packages/spec/examples/agent.json b/packages/spec/examples/agent.json new file mode 100644 index 00000000..44ca872e --- /dev/null +++ b/packages/spec/examples/agent.json @@ -0,0 +1,12 @@ +{ + "id": "EXAMPLE_AGENT", + "role": "Demonstrate the agent definition schema.", + "description": "A minimal agent definition used as a reference example in tests and documentation.", + "inputs": ["A task ID or free-form user request."], + "outputs": ["A structured summary of work performed."], + "permissions": ["Project files: read-only."], + "workflow": [ + "Follow shared workflow rules in AGENTS.md.", + "Inspect relevant files and report findings." + ] +} diff --git a/packages/spec/schemas/agent.schema.json b/packages/spec/schemas/agent.schema.json new file mode 100644 index 00000000..6ea21785 --- /dev/null +++ b/packages/spec/schemas/agent.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://agentplane.dev/schemas/agent.schema.json", + "title": "agentplane agent definition", + "type": "object", + "additionalProperties": true, + "required": ["id", "role", "description", "permissions", "workflow"], + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]*$", + "minLength": 1 + }, + "role": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 }, + "inputs": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "outputs": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "permissions": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + }, + "workflow": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + } + } +} From 5cfd3ea371c59f3751e91c80ef3e73e3a23a0622 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Mon, 9 Feb 2026 11:26:03 +0500 Subject: [PATCH 2/2] feat: agent tool-boundary fields (allowed_tools, denied_tools, model_preference) Extend agent schema and validation with tool-boundary fields: - allowed_tools/denied_tools arrays with overlap detection in lintAgent - model_preference enum (fast/balanced/powerful) - Add fields to all 22 agent definitions (.agentplane + assets) Co-Authored-By: Claude Opus 4.6 --- .agentplane/agents/CODER.json | 5 ++++- .agentplane/agents/CREATOR.json | 5 ++++- .agentplane/agents/DOCS.json | 5 ++++- .agentplane/agents/INTEGRATOR.json | 5 ++++- .agentplane/agents/ORCHESTRATOR.json | 5 ++++- .agentplane/agents/PLANNER.json | 5 ++++- .agentplane/agents/REDMINE.json | 5 ++++- .agentplane/agents/REVIEWER.json | 5 ++++- .agentplane/agents/TESTER.json | 5 ++++- .agentplane/agents/UPDATER.json | 5 ++++- .agentplane/agents/UPGRADER.json | 5 ++++- packages/agentplane/assets/agents/CODER.json | 5 ++++- .../agentplane/assets/agents/CREATOR.json | 5 ++++- packages/agentplane/assets/agents/DOCS.json | 5 ++++- .../agentplane/assets/agents/INTEGRATOR.json | 5 ++++- .../assets/agents/ORCHESTRATOR.json | 5 ++++- .../agentplane/assets/agents/PLANNER.json | 5 ++++- .../agentplane/assets/agents/REDMINE.json | 5 ++++- .../agentplane/assets/agents/REVIEWER.json | 5 ++++- packages/agentplane/assets/agents/TESTER.json | 5 ++++- .../agentplane/assets/agents/UPDATER.json | 5 ++++- .../agentplane/assets/agents/UPGRADER.json | 5 ++++- packages/core/schemas/agent.schema.json | 14 ++++++++++++ packages/core/src/agents/agent-schema.test.ts | 22 +++++++++++++++++++ packages/core/src/agents/agent-schema.ts | 12 ++++++++++ packages/spec/examples/agent.json | 5 ++++- packages/spec/schemas/agent.schema.json | 14 ++++++++++++ 27 files changed, 154 insertions(+), 23 deletions(-) diff --git a/.agentplane/agents/CODER.json b/.agentplane/agents/CODER.json index 6842af52..79379953 100644 --- a/.agentplane/agents/CODER.json +++ b/.agentplane/agents/CODER.json @@ -25,5 +25,8 @@ "Prefer declared verify commands; record ad-hoc results via PR notes or request PLANNER to update verify lists.", "Coordinate handoffs to TESTER/REVIEWER/DOCS with task ID, changed files, and expected behavior.", "Avoid task closure in branch_pr; keep commits task-scoped and update status via `node packages/agentplane/bin/agentplane.js`." - ] + ], + "allowed_tools": ["read", "write", "bash", "task_manage"], + "denied_tools": ["git_push"], + "model_preference": "powerful" } diff --git a/.agentplane/agents/CREATOR.json b/.agentplane/agents/CREATOR.json index 060287ab..f470125c 100644 --- a/.agentplane/agents/CREATOR.json +++ b/.agentplane/agents/CREATOR.json @@ -21,5 +21,8 @@ "Create the new agent JSON with uppercase snake case ID and crisp IO/permissions/workflow.", "Update AGENTS.md registry guidance as needed and refresh any derived lists per spec.", "Validate JSON and update task status via `node packages/agentplane/bin/agentplane.js`; commit via `node packages/agentplane/bin/agentplane.js`." - ] + ], + "allowed_tools": ["read", "write", "bash", "task_manage"], + "denied_tools": ["git_push"], + "model_preference": "powerful" } diff --git a/.agentplane/agents/DOCS.json b/.agentplane/agents/DOCS.json index e1d760a1..9c7f252f 100644 --- a/.agentplane/agents/DOCS.json +++ b/.agentplane/agents/DOCS.json @@ -21,5 +21,8 @@ "Update user/developer docs minimally to reflect behavior and match existing tone.", "Keep PR artifact docs and notes current when required; add handoff notes for INTEGRATOR.", "Avoid extra commits unless the task is doc-only." - ] + ], + "allowed_tools": ["read", "write", "task_manage"], + "denied_tools": ["bash", "git_push"], + "model_preference": "balanced" } diff --git a/.agentplane/agents/INTEGRATOR.json b/.agentplane/agents/INTEGRATOR.json index 211acc19..b2d184a9 100644 --- a/.agentplane/agents/INTEGRATOR.json +++ b/.agentplane/agents/INTEGRATOR.json @@ -24,5 +24,8 @@ "When writing verification notes (and any other approval/verification notes that include timestamps), use an ISO 8601 UTC timestamp with time, e.g. 2026-02-07T16:20:02.717Z; avoid date-only values like 2026-02-07.", "When closing multiple tasks, use batch finish so the same commit metadata and verification note apply.", "Check `closure_commit_requires_approval` in .agentplane/config.json; ask for user approval before the final closure commit when true, otherwise proceed without confirmation. Optionally clean task branches/worktrees after closure." - ] + ], + "allowed_tools": ["read", "write", "bash", "git_commit", "git_push", "task_manage"], + "denied_tools": [], + "model_preference": "powerful" } diff --git a/.agentplane/agents/ORCHESTRATOR.json b/.agentplane/agents/ORCHESTRATOR.json index 55fe274b..d8c6fdc1 100644 --- a/.agentplane/agents/ORCHESTRATOR.json +++ b/.agentplane/agents/ORCHESTRATOR.json @@ -28,5 +28,8 @@ "Execute step by step and summarize task IDs plus commit hashes after each major step.", "If the user opts out of task creation, track progress against the approved plan in replies.", "Before any final task-closing commit, check `closure_commit_requires_approval` in .agentplane/config.json; request user approval when true, otherwise proceed without confirmation. Finalize with a concise summary and next steps." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["write", "bash", "git_commit", "git_push"], + "model_preference": "fast" } diff --git a/.agentplane/agents/PLANNER.json b/.agentplane/agents/PLANNER.json index e51a504a..5bd93b68 100644 --- a/.agentplane/agents/PLANNER.json +++ b/.agentplane/agents/PLANNER.json @@ -25,5 +25,8 @@ "Create new tasks via task new (reserve task add for pre-existing IDs); include at least one tag and keep tags minimal.", "Scaffold task artifacts via `node packages/agentplane/bin/agentplane.js` when creating tasks; return a structured summary of touched IDs and status changes.", "Provide a numbered plan in replies when work spans multiple steps." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["write", "bash", "git_commit", "git_push"], + "model_preference": "balanced" } diff --git a/.agentplane/agents/REDMINE.json b/.agentplane/agents/REDMINE.json index 67c4424f..7d34ceeb 100644 --- a/.agentplane/agents/REDMINE.json +++ b/.agentplane/agents/REDMINE.json @@ -22,5 +22,8 @@ "Inspect/update tasks and docs via `node packages/agentplane/bin/agentplane.js`; avoid direct API calls.", "Do not change assignee if already set; preserve configured custom field IDs.", "Push updates via `node packages/agentplane/bin/agentplane.js` sync redmine --direction push when required; add handoff notes if needed." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["write", "bash", "git_commit", "git_push"], + "model_preference": "fast" } diff --git a/.agentplane/agents/REVIEWER.json b/.agentplane/agents/REVIEWER.json index 8c1d8602..1d7b738f 100644 --- a/.agentplane/agents/REVIEWER.json +++ b/.agentplane/agents/REVIEWER.json @@ -18,5 +18,8 @@ "Prefer PR artifact review when present (README completeness, diffstat, verify log).", "Report findings ordered by severity with file/line references and testing notes.", "Record handoff notes via `node packages/agentplane/bin/agentplane.js`; do not integrate or finish tasks." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["write", "git_commit", "git_push"], + "model_preference": "balanced" } diff --git a/.agentplane/agents/TESTER.json b/.agentplane/agents/TESTER.json index b9ccd509..e6a80d81 100644 --- a/.agentplane/agents/TESTER.json +++ b/.agentplane/agents/TESTER.json @@ -24,5 +24,8 @@ "Run targeted tests first and summarize only the key output lines.", "When writing verification notes (and any other approval/verification notes that include timestamps), use an ISO 8601 UTC timestamp with time, e.g. 2026-02-07T16:20:02.717Z; avoid date-only values like 2026-02-07.", "If test infrastructure is missing, document the blocker and request a PLANNER task." - ] + ], + "allowed_tools": ["read", "bash", "task_manage"], + "denied_tools": ["write", "git_commit", "git_push"], + "model_preference": "balanced" } diff --git a/.agentplane/agents/UPDATER.json b/.agentplane/agents/UPDATER.json index 374481f4..07b64978 100644 --- a/.agentplane/agents/UPDATER.json +++ b/.agentplane/agents/UPDATER.json @@ -19,5 +19,8 @@ "Confirm the user explicitly summoned UPDATER; otherwise hand control back to ORCHESTRATOR.", "Audit AGENTS.md and .agentplane/agents/*.json plus relevant repo files, citing exact paths.", "Return a prioritized optimization plan and any required validation commands." - ] + ], + "allowed_tools": ["read", "write", "bash", "task_manage"], + "denied_tools": ["git_push"], + "model_preference": "balanced" } diff --git a/.agentplane/agents/UPGRADER.json b/.agentplane/agents/UPGRADER.json index c0d33984..b0bc50fc 100644 --- a/.agentplane/agents/UPGRADER.json +++ b/.agentplane/agents/UPGRADER.json @@ -23,5 +23,8 @@ "Invoke `node packages/agentplane/bin/agentplane.js upgrade --force` when a user explicitly requests an update, or rely on the stale-date logic otherwise.", "Require a clean working tree and run the upgrade from the pinned base branch (default `main`).", "After a successful upgrade, update `framework.last_update` to `datetime.now(UTC).date()` and document the run in the owning task/pr artifacts." - ] + ], + "allowed_tools": ["read", "write", "bash", "git_commit", "task_manage"], + "denied_tools": ["git_push"], + "model_preference": "powerful" } diff --git a/packages/agentplane/assets/agents/CODER.json b/packages/agentplane/assets/agents/CODER.json index e08a2ec3..d77f79ad 100644 --- a/packages/agentplane/assets/agents/CODER.json +++ b/packages/agentplane/assets/agents/CODER.json @@ -25,5 +25,8 @@ "Prefer declared verify commands; record ad-hoc results via PR notes or request PLANNER to update verify lists.", "Coordinate handoffs to TESTER/REVIEWER/DOCS with task ID, changed files, and expected behavior.", "Avoid task closure in branch_pr; keep commits task-scoped and update status via agentplane." - ] + ], + "allowed_tools": ["read", "write", "bash", "task_manage"], + "denied_tools": ["git_push"], + "model_preference": "powerful" } diff --git a/packages/agentplane/assets/agents/CREATOR.json b/packages/agentplane/assets/agents/CREATOR.json index e84d67d2..baf8fe71 100644 --- a/packages/agentplane/assets/agents/CREATOR.json +++ b/packages/agentplane/assets/agents/CREATOR.json @@ -21,5 +21,8 @@ "Create the new agent JSON with uppercase snake case ID and crisp IO/permissions/workflow.", "Update AGENTS.md registry guidance as needed and refresh any derived lists per spec.", "Validate JSON and update task status via agentplane; commit via agentplane." - ] + ], + "allowed_tools": ["read", "write", "bash", "task_manage"], + "denied_tools": ["git_push"], + "model_preference": "powerful" } diff --git a/packages/agentplane/assets/agents/DOCS.json b/packages/agentplane/assets/agents/DOCS.json index 04deef76..8783190f 100644 --- a/packages/agentplane/assets/agents/DOCS.json +++ b/packages/agentplane/assets/agents/DOCS.json @@ -21,5 +21,8 @@ "Update user/developer docs minimally to reflect behavior and match existing tone.", "Keep PR artifact docs and notes current when required; add handoff notes for INTEGRATOR.", "Avoid extra commits unless the task is doc-only." - ] + ], + "allowed_tools": ["read", "write", "task_manage"], + "denied_tools": ["bash", "git_push"], + "model_preference": "balanced" } diff --git a/packages/agentplane/assets/agents/INTEGRATOR.json b/packages/agentplane/assets/agents/INTEGRATOR.json index 4412d375..5222b4da 100644 --- a/packages/agentplane/assets/agents/INTEGRATOR.json +++ b/packages/agentplane/assets/agents/INTEGRATOR.json @@ -24,5 +24,8 @@ "When writing verification notes (and any other approval/verification notes that include timestamps), use an ISO 8601 UTC timestamp with time, e.g. 2026-02-07T16:20:02.717Z; avoid date-only values like 2026-02-07.", "When closing multiple tasks, use batch finish so the same commit metadata and verification note apply.", "Check `closure_commit_requires_approval` in .agentplane/config.json; ask for user approval before the final closure commit when true, otherwise proceed without confirmation. Optionally clean task branches/worktrees after closure." - ] + ], + "allowed_tools": ["read", "write", "bash", "git_commit", "git_push", "task_manage"], + "denied_tools": [], + "model_preference": "powerful" } diff --git a/packages/agentplane/assets/agents/ORCHESTRATOR.json b/packages/agentplane/assets/agents/ORCHESTRATOR.json index f14d8fe5..4cccb2bb 100644 --- a/packages/agentplane/assets/agents/ORCHESTRATOR.json +++ b/packages/agentplane/assets/agents/ORCHESTRATOR.json @@ -28,5 +28,8 @@ "Execute step by step and summarize task IDs plus commit hashes after each major step.", "If the user opts out of task creation, track progress against the approved plan in replies.", "Before any final task-closing commit, check `closure_commit_requires_approval` in .agentplane/config.json; request user approval when true, otherwise proceed without confirmation. Finalize with a concise summary and next steps." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["write", "bash", "git_commit", "git_push"], + "model_preference": "fast" } diff --git a/packages/agentplane/assets/agents/PLANNER.json b/packages/agentplane/assets/agents/PLANNER.json index 1a36bd96..e7a1ec79 100644 --- a/packages/agentplane/assets/agents/PLANNER.json +++ b/packages/agentplane/assets/agents/PLANNER.json @@ -25,5 +25,8 @@ "Create new tasks via task new (reserve task add for pre-existing IDs); include at least one tag and keep tags minimal.", "Scaffold task artifacts via agentplane when creating tasks; return a structured summary of touched IDs and status changes.", "Provide a numbered plan in replies when work spans multiple steps." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["write", "bash", "git_commit", "git_push"], + "model_preference": "balanced" } diff --git a/packages/agentplane/assets/agents/REDMINE.json b/packages/agentplane/assets/agents/REDMINE.json index c9b6fb22..2caa35ca 100644 --- a/packages/agentplane/assets/agents/REDMINE.json +++ b/packages/agentplane/assets/agents/REDMINE.json @@ -22,5 +22,8 @@ "Inspect/update tasks and docs via agentplane; avoid direct API calls.", "Do not change assignee if already set; preserve configured custom field IDs.", "Push updates via agentplane sync redmine --direction push when required; add handoff notes if needed." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["write", "bash", "git_commit", "git_push"], + "model_preference": "fast" } diff --git a/packages/agentplane/assets/agents/REVIEWER.json b/packages/agentplane/assets/agents/REVIEWER.json index d044f911..c59a6400 100644 --- a/packages/agentplane/assets/agents/REVIEWER.json +++ b/packages/agentplane/assets/agents/REVIEWER.json @@ -18,5 +18,8 @@ "Prefer PR artifact review when present (README completeness, diffstat, verify log).", "Report findings ordered by severity with file/line references and testing notes.", "Record handoff notes via agentplane; do not integrate or finish tasks." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["write", "git_commit", "git_push"], + "model_preference": "balanced" } diff --git a/packages/agentplane/assets/agents/TESTER.json b/packages/agentplane/assets/agents/TESTER.json index 0f691f9a..e657ecea 100644 --- a/packages/agentplane/assets/agents/TESTER.json +++ b/packages/agentplane/assets/agents/TESTER.json @@ -24,5 +24,8 @@ "Run targeted tests first and summarize only the key output lines.", "When writing verification notes (and any other approval/verification notes that include timestamps), use an ISO 8601 UTC timestamp with time, e.g. 2026-02-07T16:20:02.717Z; avoid date-only values like 2026-02-07.", "If test infrastructure is missing, document the blocker and request a PLANNER task." - ] + ], + "allowed_tools": ["read", "bash", "task_manage"], + "denied_tools": ["write", "git_commit", "git_push"], + "model_preference": "balanced" } diff --git a/packages/agentplane/assets/agents/UPDATER.json b/packages/agentplane/assets/agents/UPDATER.json index c8256b44..76b55b17 100644 --- a/packages/agentplane/assets/agents/UPDATER.json +++ b/packages/agentplane/assets/agents/UPDATER.json @@ -19,5 +19,8 @@ "Confirm the user explicitly summoned UPDATER; otherwise hand control back to ORCHESTRATOR.", "Audit AGENTS.md and .agentplane/agents/*.json plus relevant repo files, citing exact paths.", "Return a prioritized optimization plan and any required validation commands." - ] + ], + "allowed_tools": ["read", "write", "bash", "task_manage"], + "denied_tools": ["git_push"], + "model_preference": "balanced" } diff --git a/packages/agentplane/assets/agents/UPGRADER.json b/packages/agentplane/assets/agents/UPGRADER.json index ca34f246..fa570259 100644 --- a/packages/agentplane/assets/agents/UPGRADER.json +++ b/packages/agentplane/assets/agents/UPGRADER.json @@ -23,5 +23,8 @@ "Invoke `agentplane upgrade --force` when a user explicitly requests an update, or rely on the stale-date logic otherwise.", "Require a clean working tree and run the upgrade from the pinned base branch (or current branch when unpinned in branch_pr).", "After a successful upgrade, update `framework.last_update` to `datetime.now(UTC).date()` and document the run in the owning task/pr artifacts." - ] + ], + "allowed_tools": ["read", "write", "bash", "git_commit", "task_manage"], + "denied_tools": ["git_push"], + "model_preference": "powerful" } diff --git a/packages/core/schemas/agent.schema.json b/packages/core/schemas/agent.schema.json index 6ea21785..99f38a47 100644 --- a/packages/core/schemas/agent.schema.json +++ b/packages/core/schemas/agent.schema.json @@ -30,6 +30,20 @@ "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1 + }, + "allowed_tools": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "denied_tools": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "model_preference": { + "type": "string", + "enum": ["fast", "balanced", "powerful"] } } } diff --git a/packages/core/src/agents/agent-schema.test.ts b/packages/core/src/agents/agent-schema.test.ts index 0f2afa59..bf23e01e 100644 --- a/packages/core/src/agents/agent-schema.test.ts +++ b/packages/core/src/agents/agent-schema.test.ts @@ -38,6 +38,22 @@ describe("validateAgent", () => { it("rejects empty permissions", () => { expect(() => validateAgent({ ...VALID_AGENT, permissions: [] })).toThrow(/agent/); }); + + it("accepts agent with tool restrictions", () => { + const agent = { + ...VALID_AGENT, + allowed_tools: ["read", "write"], + denied_tools: ["git_commit"], + model_preference: "fast" as const, + }; + expect(() => validateAgent(agent)).not.toThrow(); + }); + + it("rejects invalid model_preference", () => { + expect(() => + validateAgent({ ...VALID_AGENT, model_preference: "turbo" }), + ).toThrow(/agent/); + }); }); describe("lintAgent", () => { @@ -51,6 +67,12 @@ describe("lintAgent", () => { expect(result.errors.some((e) => e.includes("does not match"))).toBe(true); }); + it("detects allowed/denied overlap", () => { + const agent = { ...VALID_AGENT, allowed_tools: ["read", "write"], denied_tools: ["write"] }; + const result = lintAgent(agent); + expect(result.errors.some((e) => e.includes("overlap"))).toBe(true); + }); + it("returns schema errors for invalid input", () => { const result = lintAgent({ id: "bad" }); expect(result.errors.length).toBeGreaterThan(0); diff --git a/packages/core/src/agents/agent-schema.ts b/packages/core/src/agents/agent-schema.ts index 37daca6f..4b0cb3b5 100644 --- a/packages/core/src/agents/agent-schema.ts +++ b/packages/core/src/agents/agent-schema.ts @@ -14,6 +14,9 @@ export type AgentDefinition = { outputs?: string[]; permissions: string[]; workflow: string[]; + allowed_tools?: string[]; + denied_tools?: string[]; + model_preference?: "fast" | "balanced" | "powerful"; }; export type AgentLintResult = { @@ -82,6 +85,15 @@ export function lintAgent(raw: unknown, fileName?: string): AgentLintResult { } } + // Check allowed_tools / denied_tools overlap + if (agent.allowed_tools && agent.denied_tools) { + const allowed = new Set(agent.allowed_tools); + const overlap = agent.denied_tools.filter((t) => allowed.has(t)); + if (overlap.length > 0) { + errors.push(`allowed_tools and denied_tools overlap: ${overlap.join(", ")}`); + } + } + return { errors, warnings }; } diff --git a/packages/spec/examples/agent.json b/packages/spec/examples/agent.json index 44ca872e..a2c9045b 100644 --- a/packages/spec/examples/agent.json +++ b/packages/spec/examples/agent.json @@ -8,5 +8,8 @@ "workflow": [ "Follow shared workflow rules in AGENTS.md.", "Inspect relevant files and report findings." - ] + ], + "allowed_tools": ["read", "task_manage"], + "denied_tools": ["git_commit"], + "model_preference": "balanced" } diff --git a/packages/spec/schemas/agent.schema.json b/packages/spec/schemas/agent.schema.json index 6ea21785..99f38a47 100644 --- a/packages/spec/schemas/agent.schema.json +++ b/packages/spec/schemas/agent.schema.json @@ -30,6 +30,20 @@ "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1 + }, + "allowed_tools": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "denied_tools": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "model_preference": { + "type": "string", + "enum": ["fast", "balanced", "powerful"] } } }