mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
feat(runner): expose canonical instruction revision tools
Add native read, update, history, and restore operations with server-bound identity and revision compare-and-swap safety. Register the default catalog and API discovery paths; cover real authority authorization and exact revision restore. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7dfb327bb7
commit
5b8dd74c08
20 files changed
+725
-12
No files matched your search
@@ -162,3 +162,28 @@ conflicts/loss evidence, and arrange bounded recovery. They must not write share
|
||||
instruction caches or call the old filesystem `writeFile` for an entry.
|
||||
The next run should read the canonical snapshot and materialize it when disk is
|
||||
needed. Runtime cleanup parity is not established by these service/editor tests.
|
||||
|
||||
## Dedicated native tools
|
||||
|
||||
Ordinary native runs expose `read_agent_instructions`,
|
||||
`update_agent_instructions`, `get_agent_instruction_history`, and
|
||||
`restore_agent_instructions` by default. `targetAgentId` is optional and defaults
|
||||
to the calling agent; another target must be in the same company. Read and
|
||||
history are available in all task modes. Update and restore are available in
|
||||
standard and planning modes, subject to the canonical service's current
|
||||
responsible-user permissions, containment, and consent requirements.
|
||||
|
||||
Read returns the configured `entryFile`, exact content, and current revision.
|
||||
Retain that entry filename and revision ID when preparing an update. Both writes
|
||||
require an explicit `baseRevisionId`; only update accepts null for a genuinely
|
||||
new entry. Restore requires the existing current revision ID.
|
||||
Historical reads take both `entryFile` and `revisionId`. History pages contain at
|
||||
most 100 entries and include a continuation cursor. Restore appends a new
|
||||
revision; it does not delete history. Tool JSON cannot supply company, caller,
|
||||
run, responsible user, or source attribution. The native authority binds those
|
||||
values from the active run, and the canonical service reloads authorization.
|
||||
|
||||
A tool commit is immediately durable. If a run-local working copy still contains
|
||||
older content, cleanup must preserve its conflict candidate rather than replace
|
||||
the newer tool commit. A 409 response requires explicit conflict resolution;
|
||||
never silently retry an old candidate against the latest revision.
|
||||
@@ -7,10 +7,86 @@
|
||||
"src/scenarios/scenario-plan.ts"
|
||||
],
|
||||
"counts": {
|
||||
"actions": 48,
|
||||
"actions": 52,
|
||||
"legacyRequirements": 106
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"id": "read_agent_instructions",
|
||||
"ownership": "always_agent_tool",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"legacyAliases": [],
|
||||
"contractCase": "protocol-action:read_agent_instructions",
|
||||
"contractOwner": "src/catalog/protocol-action-contracts.test.ts::read_agent_instructions has a schema-valid canonical example and every declared projection",
|
||||
"legacyBehavioralCases": [],
|
||||
"deterministicCases": [
|
||||
"protocol-action:read_agent_instructions"
|
||||
],
|
||||
"legacyRequirementCases": [],
|
||||
"deterministicOwners": [
|
||||
"src/catalog/protocol-action-contracts.test.ts::read_agent_instructions has a schema-valid canonical example and every declared projection",
|
||||
"src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "update_agent_instructions",
|
||||
"ownership": "always_agent_tool",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"legacyAliases": [],
|
||||
"contractCase": "protocol-action:update_agent_instructions",
|
||||
"contractOwner": "src/catalog/protocol-action-contracts.test.ts::update_agent_instructions has a schema-valid canonical example and every declared projection",
|
||||
"legacyBehavioralCases": [],
|
||||
"deterministicCases": [
|
||||
"protocol-action:update_agent_instructions"
|
||||
],
|
||||
"legacyRequirementCases": [],
|
||||
"deterministicOwners": [
|
||||
"src/catalog/protocol-action-contracts.test.ts::update_agent_instructions has a schema-valid canonical example and every declared projection",
|
||||
"src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "get_agent_instruction_history",
|
||||
"ownership": "always_agent_tool",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"legacyAliases": [],
|
||||
"contractCase": "protocol-action:get_agent_instruction_history",
|
||||
"contractOwner": "src/catalog/protocol-action-contracts.test.ts::get_agent_instruction_history has a schema-valid canonical example and every declared projection",
|
||||
"legacyBehavioralCases": [],
|
||||
"deterministicCases": [
|
||||
"protocol-action:get_agent_instruction_history"
|
||||
],
|
||||
"legacyRequirementCases": [],
|
||||
"deterministicOwners": [
|
||||
"src/catalog/protocol-action-contracts.test.ts::get_agent_instruction_history has a schema-valid canonical example and every declared projection",
|
||||
"src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "restore_agent_instructions",
|
||||
"ownership": "always_agent_tool",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"legacyAliases": [],
|
||||
"contractCase": "protocol-action:restore_agent_instructions",
|
||||
"contractOwner": "src/catalog/protocol-action-contracts.test.ts::restore_agent_instructions has a schema-valid canonical example and every declared projection",
|
||||
"legacyBehavioralCases": [],
|
||||
"deterministicCases": [
|
||||
"protocol-action:restore_agent_instructions"
|
||||
],
|
||||
"legacyRequirementCases": [],
|
||||
"deterministicOwners": [
|
||||
"src/catalog/protocol-action-contracts.test.ts::restore_agent_instructions has a schema-valid canonical example and every declared projection",
|
||||
"src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "create_skill",
|
||||
"ownership": "optional_agent_tool",
|
||||
|
||||
@@ -20,21 +20,25 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
it("pins the reconciled op-set relationship between the two catalogs", () => {
|
||||
const summary = capabilityCatalogReconciliation();
|
||||
expect(summary.scenarioCount).toBe(39);
|
||||
expect(summary.liveCount).toBe(36);
|
||||
expect(summary.liveCount).toBe(40);
|
||||
expect(summary.sharedCount).toBe(27);
|
||||
expect(summary.unionCount).toBe(48);
|
||||
expect(summary.unionCount).toBe(52);
|
||||
// Any operation added to or removed from either catalog without a
|
||||
// reconciliation decision changes these exact sets and fails the gate.
|
||||
expect(summary.liveOnly).toEqual([
|
||||
"call_api",
|
||||
"create_project",
|
||||
"get_agent",
|
||||
"get_agent_instruction_history",
|
||||
"get_approval",
|
||||
"get_approval_context",
|
||||
"hire_agent",
|
||||
"list_project_repositories",
|
||||
"read_agent_instructions",
|
||||
"restore_agent_instructions",
|
||||
"schedule_wake",
|
||||
"search_api",
|
||||
"update_agent_instructions",
|
||||
]);
|
||||
expect(summary.scenarioOnly).toEqual([
|
||||
"administer_company",
|
||||
@@ -53,7 +57,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
});
|
||||
|
||||
it("is the single source both catalogs derive their operation set from", () => {
|
||||
expect(CAPABILITY_CANONICAL_OPERATIONS).toHaveLength(48);
|
||||
expect(CAPABILITY_CANONICAL_OPERATIONS).toHaveLength(52);
|
||||
const canonicalIds = new Set(CAPABILITY_CANONICAL_OPERATIONS.map((operation) => operation.operationId));
|
||||
// Neither catalog may contain an operation absent from the canonical source.
|
||||
for (const tool of SCENARIO_CATALOG) expect(canonicalIds.has(tool.operationId)).toBe(true);
|
||||
@@ -87,7 +91,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
});
|
||||
|
||||
it("names placement, claims, task modes, side-effect class, idempotency, redaction, mock mapping, real binding status, and PRP evidence for every operation", () => {
|
||||
expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(48);
|
||||
expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(52);
|
||||
for (const operation of CAPABILITY_CANONICAL_CATALOG) {
|
||||
expect(operation.placement).toMatch(/^(always|optional)_agent_tool$/);
|
||||
expect(Array.isArray(operation.requiredClaims)).toBe(true);
|
||||
@@ -113,7 +117,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
it("classifies real binding status so generic_api_request is never product coverage", () => {
|
||||
const summary = capabilityCatalogReconciliation();
|
||||
expect(summary.byRealBindingStatus).toEqual({
|
||||
live_codex: 35,
|
||||
live_codex: 39,
|
||||
scenario_mock: 12,
|
||||
test_only: 1,
|
||||
});
|
||||
|
||||
@@ -466,13 +466,13 @@ describe("workflow reports and stress traceability", () => {
|
||||
candidateFailures: 36,
|
||||
});
|
||||
expect(report.coverage).toMatchObject({
|
||||
canonicalOperations: 48,
|
||||
canonicalOperations: 52,
|
||||
capabilityCases: 106,
|
||||
workflows: 12,
|
||||
stressFindings: 44,
|
||||
stressExclusions: 1,
|
||||
});
|
||||
expect(report.coverage.operations).toHaveLength(48);
|
||||
expect(report.coverage.operations).toHaveLength(52);
|
||||
expect(report.coverage.composedWorkflows).toHaveLength(12);
|
||||
expect(
|
||||
report.coverage.operations.find(
|
||||
@@ -480,7 +480,7 @@ describe("workflow reports and stress traceability", () => {
|
||||
)?.workflowIds.length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(renderRunnerWorkflowMarkdown(report)).toContain(
|
||||
"48 operations · 106 capability cases · 12 workflows",
|
||||
"52 operations · 106 capability cases · 12 workflows",
|
||||
);
|
||||
expect(renderRunnerWorkflowJUnit(report)).toContain(
|
||||
'tests="36" failures="36" skipped="0"',
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { readAgentInstructionsAction } from "./read-agent-instructions.js";
|
||||
import { updateAgentInstructionsAction } from "./update-agent-instructions.js";
|
||||
import { getAgentInstructionHistoryAction } from "./get-agent-instruction-history.js";
|
||||
import { restoreAgentInstructionsAction } from "./restore-agent-instructions.js";
|
||||
import { answerStatusQuestionAction } from "./answer-status-question.js";
|
||||
import { blockTaskAction } from "./block-task.js";
|
||||
import { finishTaskAction } from "./finish-task.js";
|
||||
@@ -16,6 +20,11 @@ import { deepFreezeProtocolAction } from "./freeze.js";
|
||||
|
||||
/** Core actions that are always present in an authorized runner projection. */
|
||||
export const PAPERCLIP_CORE_PROTOCOL_ACTIONS = deepFreezeProtocolAction([
|
||||
readAgentInstructionsAction,
|
||||
updateAgentInstructionsAction,
|
||||
getAgentInstructionHistoryAction,
|
||||
restoreAgentInstructionsAction,
|
||||
|
||||
answerStatusQuestionAction,
|
||||
blockTaskAction,
|
||||
finishTaskAction,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/** Canonical definition for `get_agent_instruction_history`. */
|
||||
export const getAgentInstructionHistoryAction = {
|
||||
"id": "get_agent_instruction_history",
|
||||
"canonical": {
|
||||
"operationId": "get_agent_instruction_history",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"placement": "always_agent_tool",
|
||||
"optionalGroup": null,
|
||||
"requiredClaims": [],
|
||||
"taskModes": [
|
||||
"standard",
|
||||
"ask",
|
||||
"planning",
|
||||
"skill_test"
|
||||
],
|
||||
"sideEffectClass": "read",
|
||||
"idempotency": "none",
|
||||
"disabledByDefault": false,
|
||||
"realBindingStatus": "live_codex",
|
||||
"realServiceBinding": "agentInstructionRevisionService",
|
||||
"prpEvidence": "tool-result item event plus canonical revision receipt for writes; no scenario mock coverage",
|
||||
"prpBindingStatus": "bound",
|
||||
"legacyAliases": [],
|
||||
"note": "Live canonical instruction service; company scope and current responsible-user authorization are checked at invocation."
|
||||
},
|
||||
"documentation": {
|
||||
"title": "Get agent instruction history",
|
||||
"description": "List bounded canonical instruction revision metadata, including actor, source run, and restore origin. Use read_agent_instructions with entryFile and revisionId to inspect exact historical content.",
|
||||
"note": null
|
||||
},
|
||||
"examples": {
|
||||
"call": {
|
||||
"operationId": "get_agent_instruction_history",
|
||||
"input": {
|
||||
"entryFile": "AGENTS.md"
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
"ok": true,
|
||||
"operationId": "get_agent_instruction_history",
|
||||
"result": {}
|
||||
}
|
||||
},
|
||||
"live": {
|
||||
"order": 42,
|
||||
"descriptor": {
|
||||
"schema": "paperclip.semantic-tool.v1",
|
||||
"operationId": "get_agent_instruction_history",
|
||||
"version": 1,
|
||||
"title": "Get agent instruction history",
|
||||
"description": "List bounded canonical instruction revision metadata, including actor, source run, and restore origin. Use read_agent_instructions with entryFile and revisionId to inspect exact historical content.",
|
||||
"exposure": "always",
|
||||
"requiredClaims": [],
|
||||
"allowedModes": [
|
||||
"standard",
|
||||
"ask",
|
||||
"planning",
|
||||
"skill_test"
|
||||
],
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targetAgentId": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
||||
"description": "Same-company target agent. Omit to use the calling agent."
|
||||
},
|
||||
"entryFile": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 4096,
|
||||
"description": "Configured relative entry filename returned by read_agent_instructions; retain it with the revision."
|
||||
},
|
||||
"cursor": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 2048
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entryFile"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"scenario": null
|
||||
} as const;
|
||||
@@ -1,3 +1,7 @@
|
||||
import { readAgentInstructionsAction } from "./read-agent-instructions.js";
|
||||
import { updateAgentInstructionsAction } from "./update-agent-instructions.js";
|
||||
import { getAgentInstructionHistoryAction } from "./get-agent-instruction-history.js";
|
||||
import { restoreAgentInstructionsAction } from "./restore-agent-instructions.js";
|
||||
import { reassignTaskAction } from "./reassign-task.js";
|
||||
import { createSkillAction } from "./create-skill.js";
|
||||
import { createProjectAction } from "./create-project.js";
|
||||
@@ -49,6 +53,11 @@ import { writeDocumentAction } from "./write-document.js";
|
||||
import { deepFreezeProtocolAction } from "./freeze.js";
|
||||
|
||||
export const PAPERCLIP_PROTOCOL_ACTIONS = deepFreezeProtocolAction([
|
||||
readAgentInstructionsAction,
|
||||
updateAgentInstructionsAction,
|
||||
getAgentInstructionHistoryAction,
|
||||
restoreAgentInstructionsAction,
|
||||
|
||||
createSkillAction,
|
||||
createProjectAction,
|
||||
listProjectRepositoriesAction,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/** Canonical definition for `read_agent_instructions`. */
|
||||
export const readAgentInstructionsAction = {
|
||||
"id": "read_agent_instructions",
|
||||
"canonical": {
|
||||
"operationId": "read_agent_instructions",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"placement": "always_agent_tool",
|
||||
"optionalGroup": null,
|
||||
"requiredClaims": [],
|
||||
"taskModes": [
|
||||
"standard",
|
||||
"ask",
|
||||
"planning",
|
||||
"skill_test"
|
||||
],
|
||||
"sideEffectClass": "read",
|
||||
"idempotency": "none",
|
||||
"disabledByDefault": false,
|
||||
"realBindingStatus": "live_codex",
|
||||
"realServiceBinding": "agentInstructionRevisionService",
|
||||
"prpEvidence": "tool-result item event plus canonical revision receipt for writes; no scenario mock coverage",
|
||||
"prpBindingStatus": "bound",
|
||||
"legacyAliases": [],
|
||||
"note": "Live canonical instruction service; company scope and current responsible-user authorization are checked at invocation."
|
||||
},
|
||||
"documentation": {
|
||||
"title": "Read canonical agent instructions",
|
||||
"description": "Read the current canonical instruction entry and its revision, or inspect a historical revision by supplying both entryFile and revisionId. Read before updating; do not edit shared instruction caches.",
|
||||
"note": null
|
||||
},
|
||||
"examples": {
|
||||
"call": {
|
||||
"operationId": "read_agent_instructions",
|
||||
"input": {}
|
||||
},
|
||||
"success": {
|
||||
"ok": true,
|
||||
"operationId": "read_agent_instructions",
|
||||
"result": {}
|
||||
}
|
||||
},
|
||||
"live": {
|
||||
"order": 40,
|
||||
"descriptor": {
|
||||
"schema": "paperclip.semantic-tool.v1",
|
||||
"operationId": "read_agent_instructions",
|
||||
"version": 1,
|
||||
"title": "Read canonical agent instructions",
|
||||
"description": "Read the current canonical instruction entry and its revision, or inspect a historical revision by supplying both entryFile and revisionId. Read before updating; do not edit shared instruction caches.",
|
||||
"exposure": "always",
|
||||
"requiredClaims": [],
|
||||
"allowedModes": [
|
||||
"standard",
|
||||
"ask",
|
||||
"planning",
|
||||
"skill_test"
|
||||
],
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targetAgentId": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
||||
"description": "Same-company target agent. Omit to use the calling agent."
|
||||
},
|
||||
"entryFile": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 4096,
|
||||
"description": "Configured relative entry filename returned by read_agent_instructions; retain it with the revision."
|
||||
},
|
||||
"revisionId": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
||||
"description": "Historical revision to inspect; also provide its entryFile."
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"scenario": null
|
||||
} as const;
|
||||
@@ -0,0 +1,99 @@
|
||||
/** Canonical definition for `restore_agent_instructions`. */
|
||||
export const restoreAgentInstructionsAction = {
|
||||
"id": "restore_agent_instructions",
|
||||
"canonical": {
|
||||
"operationId": "restore_agent_instructions",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"placement": "always_agent_tool",
|
||||
"optionalGroup": null,
|
||||
"requiredClaims": [],
|
||||
"taskModes": [
|
||||
"standard",
|
||||
"planning"
|
||||
],
|
||||
"sideEffectClass": "company_write",
|
||||
"idempotency": "recommended",
|
||||
"disabledByDefault": false,
|
||||
"realBindingStatus": "live_codex",
|
||||
"realServiceBinding": "agentInstructionRevisionService",
|
||||
"prpEvidence": "tool-result item event plus canonical revision receipt for writes; no scenario mock coverage",
|
||||
"prpBindingStatus": "bound",
|
||||
"legacyAliases": [],
|
||||
"note": "Live canonical instruction service; company scope and current responsible-user authorization are checked at invocation."
|
||||
},
|
||||
"documentation": {
|
||||
"title": "Restore canonical agent instructions",
|
||||
"description": "Append a historical instruction revision as the current content using the current baseRevisionId. Requires the responsible user\u2019s current target edit permission. History remains intact; conflicts require an explicit resolution.",
|
||||
"note": null
|
||||
},
|
||||
"examples": {
|
||||
"call": {
|
||||
"operationId": "restore_agent_instructions",
|
||||
"input": {
|
||||
"entryFile": "AGENTS.md",
|
||||
"revisionId": "00000000-0000-4000-8000-000000000001",
|
||||
"baseRevisionId": "00000000-0000-4000-8000-000000000002"
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
"ok": true,
|
||||
"operationId": "restore_agent_instructions",
|
||||
"result": {}
|
||||
}
|
||||
},
|
||||
"live": {
|
||||
"order": 43,
|
||||
"descriptor": {
|
||||
"schema": "paperclip.semantic-tool.v1",
|
||||
"operationId": "restore_agent_instructions",
|
||||
"version": 1,
|
||||
"title": "Restore canonical agent instructions",
|
||||
"description": "Append a historical instruction revision as the current content using the current baseRevisionId. Requires the responsible user\u2019s current target edit permission. History remains intact; conflicts require an explicit resolution.",
|
||||
"exposure": "always",
|
||||
"requiredClaims": [],
|
||||
"allowedModes": [
|
||||
"standard",
|
||||
"planning"
|
||||
],
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targetAgentId": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
||||
"description": "Same-company target agent. Omit to use the calling agent."
|
||||
},
|
||||
"entryFile": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 4096,
|
||||
"description": "Configured relative entry filename returned by read_agent_instructions; retain it with the revision."
|
||||
},
|
||||
"revisionId": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
||||
"description": "Historical revision whose exact content should be restored."
|
||||
},
|
||||
"baseRevisionId": {
|
||||
"type": "string",
|
||||
"description": "Current revision read before restoring. Never replace a stale base silently.",
|
||||
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entryFile",
|
||||
"revisionId",
|
||||
"baseRevisionId"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"scenario": null
|
||||
} as const;
|
||||
@@ -0,0 +1,102 @@
|
||||
/** Canonical definition for `update_agent_instructions`. */
|
||||
export const updateAgentInstructionsAction = {
|
||||
"id": "update_agent_instructions",
|
||||
"canonical": {
|
||||
"operationId": "update_agent_instructions",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"placement": "always_agent_tool",
|
||||
"optionalGroup": null,
|
||||
"requiredClaims": [],
|
||||
"taskModes": [
|
||||
"standard",
|
||||
"planning"
|
||||
],
|
||||
"sideEffectClass": "company_write",
|
||||
"idempotency": "recommended",
|
||||
"disabledByDefault": false,
|
||||
"realBindingStatus": "live_codex",
|
||||
"realServiceBinding": "agentInstructionRevisionService",
|
||||
"prpEvidence": "tool-result item event plus canonical revision receipt for writes; no scenario mock coverage",
|
||||
"prpBindingStatus": "bound",
|
||||
"legacyAliases": [],
|
||||
"note": "Live canonical instruction service; company scope and current responsible-user authorization are checked at invocation."
|
||||
},
|
||||
"documentation": {
|
||||
"title": "Update canonical agent instructions",
|
||||
"description": "Commit instruction content with the exact entryFile and baseRevisionId from a prior read. Requires the responsible user\u2019s current target edit permission. On conflict, preserve your candidate and explicitly resolve it; never overwrite the newer head.",
|
||||
"note": null
|
||||
},
|
||||
"examples": {
|
||||
"call": {
|
||||
"operationId": "update_agent_instructions",
|
||||
"input": {
|
||||
"entryFile": "AGENTS.md",
|
||||
"content": "# Instructions\n",
|
||||
"baseRevisionId": null
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
"ok": true,
|
||||
"operationId": "update_agent_instructions",
|
||||
"result": {}
|
||||
}
|
||||
},
|
||||
"live": {
|
||||
"order": 41,
|
||||
"descriptor": {
|
||||
"schema": "paperclip.semantic-tool.v1",
|
||||
"operationId": "update_agent_instructions",
|
||||
"version": 1,
|
||||
"title": "Update canonical agent instructions",
|
||||
"description": "Commit instruction content with the exact entryFile and baseRevisionId from a prior read. Requires the responsible user\u2019s current target edit permission. On conflict, preserve your candidate and explicitly resolve it; never overwrite the newer head.",
|
||||
"exposure": "always",
|
||||
"requiredClaims": [],
|
||||
"allowedModes": [
|
||||
"standard",
|
||||
"planning"
|
||||
],
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targetAgentId": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
||||
"description": "Same-company target agent. Omit to use the calling agent."
|
||||
},
|
||||
"entryFile": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 4096,
|
||||
"description": "Configured relative entry filename returned by read_agent_instructions; retain it with the revision."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"maxLength": 1048576,
|
||||
"description": "Complete UTF-8 instruction content, at most 1 MiB; empty content is valid."
|
||||
},
|
||||
"baseRevisionId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Revision read before editing; null only when no canonical entry exists. Never replace a stale base silently.",
|
||||
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entryFile",
|
||||
"content",
|
||||
"baseRevisionId"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"scenario": null
|
||||
} as const;
|
||||
@@ -27,7 +27,12 @@ const NAMESPACE: Readonly<Record<CapabilitySemanticOperationId, string>> = Objec
|
||||
report_progress: "active_task", answer_status_question: "active_task", write_document: "documents",
|
||||
request_human_input: "documents", register_deliverable: "documents", finish_task: "active_task",
|
||||
block_task: "active_task", request_review: "active_task", search_tasks: "discovery",
|
||||
list_agents: "discovery", get_agent: "discovery", create_skill: "skills", create_task: "delegation", create_project: "projects", list_project_repositories: "projects", list_projects: "projects",
|
||||
list_agents: "discovery", get_agent: "discovery",
|
||||
read_agent_instructions: "agent_instructions",
|
||||
update_agent_instructions: "agent_instructions",
|
||||
get_agent_instruction_history: "agent_instructions",
|
||||
restore_agent_instructions: "agent_instructions",
|
||||
create_skill: "skills", create_task: "delegation", create_project: "projects", list_project_repositories: "projects", list_projects: "projects",
|
||||
set_dependencies: "delegation", reassign_task: "delegation", list_approvals: "governance", get_approval: "governance",
|
||||
get_approval_context: "governance", request_approval: "governance",
|
||||
decide_approval: "governance", comment_on_approval: "governance",
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("Capability semantic catalog and authorization", () => {
|
||||
it("publishes a stable narrow catalog without credentials or control-plane-owned tools", () => {
|
||||
const names = CAPABILITY_SEMANTIC_TOOL_CATALOG.map((tool) => tool.operationId);
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
expect(names).toHaveLength(36);
|
||||
expect(names).toHaveLength(40);
|
||||
expect(names).toContain("get_task_context");
|
||||
expect(names).toContain("finish_task");
|
||||
expect(names).not.toContain("checkout_task");
|
||||
|
||||
@@ -33,6 +33,10 @@ export type CapabilitySemanticOperationId =
|
||||
| "request_review"
|
||||
| "list_agents"
|
||||
| "get_agent"
|
||||
| "read_agent_instructions"
|
||||
| "update_agent_instructions"
|
||||
| "get_agent_instruction_history"
|
||||
| "restore_agent_instructions"
|
||||
| "search_tasks"
|
||||
| "list_approvals"
|
||||
| "get_approval"
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { agents, authUsers, companies, companyMemberships, principalPermissionGrants, heartbeatRuns, issues, agentInstructionRevisions, createDb } from "@paperclipai/db";
|
||||
import { startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
|
||||
import { resolveManagedInstructionsRoot } from "../agent-instructions.js";
|
||||
import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js";
|
||||
|
||||
describe("canonical instruction tools through native authority", () => {
|
||||
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
|
||||
let db: ReturnType<typeof createDb>;
|
||||
let home: string;
|
||||
const previousHome = process.env.PAPERCLIP_HOME;
|
||||
let companyId: string, agentId: string, targetAgentId: string, userId: string, runId: string, issueId: string, root: string;
|
||||
let authority: PaperclipRunnerToolAuthority;
|
||||
const entryFile = "policy/ENTRY.md";
|
||||
const original = "\uFEFF# Original\r\n\0☃\n";
|
||||
beforeAll(async () => {
|
||||
home = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "instruction-tool-home-")));
|
||||
process.env.PAPERCLIP_HOME = home;
|
||||
database = await startEmbeddedPostgresTestDatabase("instruction-tool-db-");
|
||||
db = createDb(database.connectionString);
|
||||
}, 90_000);
|
||||
afterAll(async () => {
|
||||
if (previousHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = previousHome;
|
||||
await database?.cleanup();
|
||||
if (home) await fs.rm(home, { recursive: true, force: true });
|
||||
});
|
||||
beforeEach(async () => {
|
||||
[companyId, agentId, targetAgentId, userId, runId, issueId] = Array.from({ length: 6 }, () => randomUUID());
|
||||
await db.insert(companies).values({ id: companyId, name: "Tool scope", issuePrefix: randomUUID().slice(0, 8) });
|
||||
await db.insert(authUsers).values({ id: userId, name: "Editor", email: `${userId}@example.test`, createdAt: new Date(), updatedAt: new Date() });
|
||||
root = resolveManagedInstructionsRoot({ id: targetAgentId, companyId, name: "Target", adapterConfig: {} });
|
||||
await db.insert(agents).values([
|
||||
{ id: agentId, companyId, name: "Caller", status: "active" },
|
||||
{ id: targetAgentId, companyId, name: "Target", adapterConfig: { instructionsBundleMode: "managed", instructionsRootPath: root, instructionsEntryFile: entryFile } },
|
||||
]);
|
||||
await db.insert(companyMemberships).values([
|
||||
{ companyId, principalType: "user", principalId: userId, membershipRole: "operator" },
|
||||
{ companyId, principalType: "agent", principalId: agentId, membershipRole: "member" },
|
||||
]);
|
||||
await db.insert(principalPermissionGrants).values({ companyId, principalType: "user", principalId: userId, permissionKey: "agents:configure", scope: { agentIds: [targetAgentId] } });
|
||||
await db.insert(issues).values({ id: issueId, companyId, title: "Maintain instructions", status: "in_progress", workMode: "standard", assigneeAgentId: agentId });
|
||||
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, status: "running", runtimeMode: "native", nativeIssueId: issueId, invocationSource: "on_demand", responsibleUserId: userId });
|
||||
await db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, issueId));
|
||||
authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
|
||||
await fs.mkdir(path.dirname(path.join(root, entryFile)), { recursive: true });
|
||||
await fs.writeFile(path.join(root, entryFile), original);
|
||||
});
|
||||
const call = (tool: string, args: Record<string, unknown> = {}) => authority.execute({ tool, callId: randomUUID(), arguments: { targetAgentId, ...args } }) as Promise<any>;
|
||||
it("reads, commits, lists, inspects, and restores exact content with server provenance", async () => {
|
||||
const first = await call("read_agent_instructions");
|
||||
expect(first).toMatchObject({ entryFile, content: original });
|
||||
const changed = await call("update_agent_instructions", { entryFile, content: "changed\r\n", baseRevisionId: first.revision.id });
|
||||
expect(changed.revision).toMatchObject({ source: "tool", responsibleUserId: userId, actorAgentId: agentId, sourceRunId: runId });
|
||||
const history = await call("get_agent_instruction_history", { entryFile, limit: 1 });
|
||||
expect(history.revisions.map((revision: { id: string }) => revision.id)).toEqual([changed.revision.id]);
|
||||
expect(history.nextCursor).toEqual(expect.any(String));
|
||||
expect(await call("read_agent_instructions", { entryFile, revisionId: first.revision.id })).toMatchObject({ content: original });
|
||||
const restored = await call("restore_agent_instructions", { entryFile, revisionId: first.revision.id, baseRevisionId: changed.revision.id });
|
||||
expect(restored.revision).toMatchObject({ source: "restore", restoredFromRevisionId: first.revision.id });
|
||||
expect(await fs.readFile(path.join(root, entryFile), "utf8")).toBe(original);
|
||||
await expect(call("update_agent_instructions", { entryFile, content: "stale", baseRevisionId: changed.revision.id })).rejects.toMatchObject({ status: 409 });
|
||||
expect((await call("read_agent_instructions")).content).toBe(original);
|
||||
});
|
||||
it("rejects supplied identity and missing CAS bases before changing content", async () => {
|
||||
const first = await call("read_agent_instructions");
|
||||
for (const identity of ["companyId", "agentId", "runId", "responsibleUserId", "onBehalfOfUserId", "actor"]) {
|
||||
await expect(call("update_agent_instructions", { entryFile, content: "forged", baseRevisionId: first.revision.id, [identity]: randomUUID() })).rejects.toMatchObject({ status: 400 });
|
||||
}
|
||||
await expect(call("update_agent_instructions", { entryFile, content: "missing base" })).rejects.toMatchObject({ status: 400 });
|
||||
expect((await call("read_agent_instructions")).content).toBe(original);
|
||||
expect(await db.select().from(agentInstructionRevisions).where(eq(agentInstructionRevisions.agentId, targetAgentId))).toHaveLength(1);
|
||||
});
|
||||
it("rechecks responsible-user access and rejects a cross-company target", async () => {
|
||||
const first = await call("read_agent_instructions");
|
||||
await db.delete(principalPermissionGrants).where(eq(principalPermissionGrants.companyId, companyId));
|
||||
await expect(call("update_agent_instructions", { entryFile, content: "revoked", baseRevisionId: first.revision.id })).rejects.toMatchObject({ status: 403 });
|
||||
const foreignCompanyId = randomUUID(), foreignAgentId = randomUUID();
|
||||
await db.insert(companies).values({ id: foreignCompanyId, name: "Foreign", issuePrefix: randomUUID().slice(0, 8) });
|
||||
await db.insert(agents).values({ id: foreignAgentId, companyId: foreignCompanyId, name: "Foreign" });
|
||||
await expect(call("read_agent_instructions", { targetAgentId: foreignAgentId })).rejects.toMatchObject({ status: 404 });
|
||||
await expect(call("update_agent_instructions", { targetAgentId: foreignAgentId, entryFile, content: "foreign", baseRevisionId: null })).rejects.toMatchObject({ status: 404 });
|
||||
await db.update(heartbeatRuns).set({ status: "cancelled" }).where(eq(heartbeatRuns.id, runId));
|
||||
await expect(call("read_agent_instructions")).rejects.toThrow("paperclip_runner_tool_binding_not_authorized");
|
||||
expect(await fs.readFile(path.join(root, entryFile), "utf8")).toBe(original);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js";
|
||||
|
||||
const binding = { companyId: "00000000-0000-4000-8000-000000000001", agentId: "00000000-0000-4000-8000-000000000002", issueId: "00000000-0000-4000-8000-000000000003", runId: "00000000-0000-4000-8000-000000000004" };
|
||||
|
||||
describe("dedicated canonical instruction tools", () => {
|
||||
it("advertises read, CAS update, history, and restore on ordinary native runs", () => {
|
||||
const authority = new PaperclipRunnerToolAuthority({} as Db, binding);
|
||||
const tools = authority.definitions();
|
||||
expect(tools.map((tool) => tool.name)).toEqual(expect.arrayContaining([
|
||||
"read_agent_instructions", "update_agent_instructions", "get_agent_instruction_history", "restore_agent_instructions",
|
||||
]));
|
||||
for (const name of ["update_agent_instructions", "restore_agent_instructions"]) {
|
||||
const tool = tools.find((candidate) => candidate.name === name)!;
|
||||
expect(tool.inputSchema).toMatchObject({ required: expect.arrayContaining(["baseRevisionId"]), additionalProperties: false });
|
||||
expect(tool.inputSchema).not.toHaveProperty("properties.responsibleUserId");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { z } from "zod";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { badRequest, notFound } from "../../errors.js";
|
||||
import { agentInstructionRevisionService } from "../agent-instruction-revisions.js";
|
||||
import { deriveBundleState } from "../agent-instructions.js";
|
||||
import { agentService } from "../agents.js";
|
||||
import type { AuthorizationActor } from "../authorization.js";
|
||||
|
||||
const target = { targetAgentId: z.string().uuid().optional() };
|
||||
const entryFile = z.string().min(1).max(4096);
|
||||
const baseRevisionId = z.string().uuid().nullable();
|
||||
const schemas = {
|
||||
read_agent_instructions: z.object({ ...target, entryFile: entryFile.optional(), revisionId: z.string().uuid().optional() }).strict()
|
||||
.refine((input) => Boolean(input.entryFile) === Boolean(input.revisionId), "Historical reads require both entryFile and revisionId"),
|
||||
update_agent_instructions: z.object({ ...target, entryFile, content: z.string().max(1024 * 1024), baseRevisionId }).strict(),
|
||||
get_agent_instruction_history: z.object({ ...target, entryFile, cursor: z.string().min(1).max(2048).optional(), limit: z.number().int().min(1).max(100).optional() }).strict(),
|
||||
restore_agent_instructions: z.object({ ...target, entryFile, revisionId: z.string().uuid(), baseRevisionId: z.string().uuid() }).strict(),
|
||||
};
|
||||
|
||||
export type AgentInstructionToolName = keyof typeof schemas;
|
||||
|
||||
/** Called only after the native authority validates its server-bound active run. */
|
||||
export async function executeAgentInstructionTool(input: {
|
||||
db: Db;
|
||||
binding: { companyId: string; agentId: string; runId: string };
|
||||
tool: AgentInstructionToolName;
|
||||
arguments: unknown;
|
||||
}) {
|
||||
// Never accept an actor, responsible user, company, or source run from tool JSON.
|
||||
const actor: AuthorizationActor = { type: "agent", source: "agent_jwt", companyId: input.binding.companyId, agentId: input.binding.agentId, runId: input.binding.runId };
|
||||
const service = agentInstructionRevisionService(input.db);
|
||||
const parse = <T>(schema: z.ZodType<T>): T => {
|
||||
const result = schema.safeParse(input.arguments);
|
||||
if (!result.success) throw badRequest("Invalid instruction tool arguments", result.error.flatten());
|
||||
return result.data;
|
||||
};
|
||||
const scope = (targetAgentId?: string) => ({ companyId: input.binding.companyId, agentId: targetAgentId ?? input.binding.agentId });
|
||||
switch (input.tool) {
|
||||
case "read_agent_instructions": {
|
||||
const args = parse(schemas.read_agent_instructions);
|
||||
const target = scope(args.targetAgentId);
|
||||
if (args.entryFile && args.revisionId) return service.readRevision({ ...target, entryFile: args.entryFile, revisionId: args.revisionId }, actor);
|
||||
const snapshot = await service.readCurrent(target, actor);
|
||||
if (snapshot) return { ...snapshot, entryFile: snapshot.revision.entryFile };
|
||||
// An unseeded empty entry still needs its configured filename for the first CAS.
|
||||
const agent = await agentService(input.db).getById(target.agentId);
|
||||
if (!agent || agent.companyId !== target.companyId) throw notFound("Agent not found");
|
||||
return { entryFile: deriveBundleState(agent).entryFile, revision: null, content: null };
|
||||
}
|
||||
case "update_agent_instructions": {
|
||||
const args = parse(schemas.update_agent_instructions);
|
||||
return service.commit({ ...scope(args.targetAgentId), entryFile: args.entryFile, content: args.content, baseRevisionId: args.baseRevisionId, source: "tool" }, actor);
|
||||
}
|
||||
case "get_agent_instruction_history": {
|
||||
const args = parse(schemas.get_agent_instruction_history);
|
||||
return service.history({ ...scope(args.targetAgentId), entryFile: args.entryFile, cursor: args.cursor, limit: args.limit }, actor);
|
||||
}
|
||||
case "restore_agent_instructions": {
|
||||
const args = parse(schemas.restore_agent_instructions);
|
||||
return service.restore({ ...scope(args.targetAgentId), entryFile: args.entryFile, revisionId: args.revisionId, baseRevisionId: args.baseRevisionId }, actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
issueId,
|
||||
runId,
|
||||
});
|
||||
expect(authority.definitions()).toHaveLength(30);
|
||||
expect(authority.definitions()).toHaveLength(34);
|
||||
const questions = authority.definitions().find(tool => tool.name === "request_human_input")!;
|
||||
expect(questions.description).toContain("ask only the next unanswered question");
|
||||
expect(questions.description).toContain("Never infer answers");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { executeAgentInstructionTool } from "./agent-instruction-tools.js";
|
||||
import { publicChatTaskUrl } from "../chat-task-url.js";
|
||||
import type { createAssignedMcpTools } from "./assigned-mcp-tools.js";
|
||||
import { assertAssignableAgent } from "../agent-assignability.js";
|
||||
@@ -79,6 +80,7 @@ import {
|
||||
} from "./chat-attachment-read.js";
|
||||
|
||||
const IMPLEMENTED_OPERATIONS = new Set([
|
||||
"read_agent_instructions", "update_agent_instructions", "get_agent_instruction_history", "restore_agent_instructions",
|
||||
"search_api", "call_api", "hire_agent",
|
||||
"get_task_context", "get_task_history", "search_tasks", "report_progress",
|
||||
"request_human_input",
|
||||
@@ -364,6 +366,13 @@ export class PaperclipRunnerToolAuthority {
|
||||
throw new Error("paperclip_runner_tool_mode_denied");
|
||||
}
|
||||
switch (call.tool) {
|
||||
case "read_agent_instructions":
|
||||
case "update_agent_instructions":
|
||||
case "get_agent_instruction_history":
|
||||
case "restore_agent_instructions":
|
||||
return executeAgentInstructionTool({ db: this.db, binding: {
|
||||
companyId: this.binding.companyId, agentId: this.binding.agentId, runId: this.binding.runId,
|
||||
}, tool: call.tool, arguments: call.arguments });
|
||||
case "create_skill": {
|
||||
const apiUrl = this.binding.apiUrl ?? process.env.PAPERCLIP_API_URL;
|
||||
const token = createLocalAgentJwt(this.binding.agentId, this.binding.companyId, context.actor.adapterType, this.binding.runId, context.run.responsibleUserId);
|
||||
|
||||
@@ -42,6 +42,10 @@ function words(text: string): string[] {
|
||||
}
|
||||
|
||||
function dedicatedTools(method: string, path: string): string[] {
|
||||
if (/\/agents\/\{[^}]+\}\/instructions-bundle\/file$/.test(path)) return method === "GET" ? ["read_agent_instructions"] : method === "PUT" ? ["update_agent_instructions"] : [];
|
||||
if (/\/agents\/\{[^}]+\}\/instructions-bundle\/history$/.test(path) && method === "GET") return ["get_agent_instruction_history"];
|
||||
if (/\/agents\/\{[^}]+\}\/instructions-bundle\/revision\/\{[^}]+\}$/.test(path) && method === "GET") return ["read_agent_instructions"];
|
||||
if (/\/agents\/\{[^}]+\}\/instructions-bundle\/restore$/.test(path) && method === "POST") return ["restore_agent_instructions"];
|
||||
if (/\/companies\/\{[^}]+\}\/skills$/.test(path) && method === "POST") return ["create_skill"];
|
||||
if (/\/companies\/\{[^}]+\}\/agent-hires$/.test(path) && method === "POST") return ["hire_agent"];
|
||||
if (/\/projects$/.test(path)) return method === "GET" ? ["list_projects"] : method === "POST" ? ["create_project"] : [];
|
||||
|
||||
@@ -52,6 +52,10 @@ describe("runner API catalog", () => {
|
||||
expect(runnerApiOperation(createProject).dedicatedTools).toEqual(["create_project"]);
|
||||
expect(runnerApiOperation(projects).dedicatedTools).toEqual(["list_projects"]);
|
||||
expect(runnerApiOperation("GET /api/companies/{companyId}/project-repositories").dedicatedTools).toEqual(["list_project_repositories"]);
|
||||
expect(runnerApiOperation("GET /api/agents/{id}/instructions-bundle/file").dedicatedTools).toEqual(["read_agent_instructions"]);
|
||||
expect(runnerApiOperation("PUT /api/agents/{id}/instructions-bundle/file").dedicatedTools).toEqual(["update_agent_instructions"]);
|
||||
expect(runnerApiOperation("GET /api/agents/{id}/instructions-bundle/history").dedicatedTools).toEqual(["get_agent_instruction_history"]);
|
||||
expect(runnerApiOperation("POST /api/agents/{id}/instructions-bundle/restore").dedicatedTools).toEqual(["restore_agent_instructions"]);
|
||||
expect(runnerApiOperation("POST /api/companies/{companyId}/agent-hires").dedicatedTools).toEqual(["hire_agent"]);
|
||||
expect(runnerApiOperation("POST /api/companies/{companyId}/agent-hires").dedicatedToolGuidance).toContain("inherits the caller's native runtime");
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user