From 169b7fc3f4c66956d7fe5967d3fff2e5c49460ae Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 6 Oct 2026 12:29:50 -0500 Subject: [PATCH] fix: resolve feedback discovery and submission review findings Co-Authored-By: Paperclip --- .../generated/capability/capabilities.yaml | 6 ++-- .../capability/capability-contract.md | 2 +- .../scripts/generate-capability-contract.mjs | 1 + .../src/semantic-tools/discovery.ts | 2 ++ .../src/semantic-tools/semantic-tools.test.ts | 14 ++++++++ .../src/semantic-tools/types.ts | 2 ++ .../scripts/verify-agent-commentary-live.ts | 34 +++++++++++-------- .../agent-commentary.integration.test.ts | 2 ++ .../scripts/submit-agent-commentary.py | 2 ++ 9 files changed, 46 insertions(+), 19 deletions(-) diff --git a/packages/paperclip-runner/generated/capability/capabilities.yaml b/packages/paperclip-runner/generated/capability/capabilities.yaml index dc28db77f85..d03d9dff269 100644 --- a/packages/paperclip-runner/generated/capability/capabilities.yaml +++ b/packages/paperclip-runner/generated/capability/capabilities.yaml @@ -294,9 +294,9 @@ "kind": "skill_heading", "sourceAnchor": "skills/paperclip/SKILL.md#L754:incidental-feedback", "heading": "Incidental feedback", - "primaryDisposition": "control_plane_owned", - "semanticOperation": "runtime_reconciliation", - "expectedMockState": "runtime_decision_record" + "primaryDisposition": "optional_agent_tool", + "semanticOperation": "scoped_discovery", + "expectedMockState": "operation_result" }, { "id": "skill:skills/paperclip/references/api-reference.md:1", diff --git a/packages/paperclip-runner/generated/capability/capability-contract.md b/packages/paperclip-runner/generated/capability/capability-contract.md index 9bab92ef929..4bebf5099e6 100644 --- a/packages/paperclip-runner/generated/capability/capability-contract.md +++ b/packages/paperclip-runner/generated/capability/capability-contract.md @@ -5,6 +5,6 @@ Generated by `scripts/generate-capability-contract.mjs`; do not edit generated f - Skill/reference headings: 163 - Legacy MCP tools: 42 - Eval cases: 106 across 16 groups -- Deterministic content SHA-256: `464784a4473655cd5adee194d8b72964b2200848db6e19ae6b4e3f0cb0b4fe35` +- Deterministic content SHA-256: `3f4939bc27abd5311caf243044f0b2d4ec36a8d27d4eaf34fed7a0738c734c1d` Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation. diff --git a/packages/paperclip-runner/scripts/generate-capability-contract.mjs b/packages/paperclip-runner/scripts/generate-capability-contract.mjs index 7903efd7ca1..82cab38b2f8 100644 --- a/packages/paperclip-runner/scripts/generate-capability-contract.mjs +++ b/packages/paperclip-runner/scripts/generate-capability-contract.mjs @@ -26,6 +26,7 @@ function sourceAnchor(path, line, heading) { function classifyHeading(path, heading) { const normalized = heading.toLowerCase(); if (normalized === "conversational confirmation answers") return "always_agent_tool"; + if (normalized === "incidental feedback") return "optional_agent_tool"; if (/(authentication|identity|checkout|budget|error|wake|heartbeat|approval follow-up|activity|audit|release|terminology)/.test(normalized)) { return "control_plane_owned"; } diff --git a/packages/paperclip-runner/src/semantic-tools/discovery.ts b/packages/paperclip-runner/src/semantic-tools/discovery.ts index fd5b075dbc6..9ad3907a154 100644 --- a/packages/paperclip-runner/src/semantic-tools/discovery.ts +++ b/packages/paperclip-runner/src/semantic-tools/discovery.ts @@ -39,6 +39,7 @@ const NAMESPACE: Readonly> = Objec decide_approval: "governance", comment_on_approval: "governance", get_workspace_runtime: "workspace", control_workspace_service: "workspace", schedule_wake: "continuation", generic_api_request: "test_infrastructure", + submit_complaint: "feedback", submit_suggestion: "feedback", }); export const CAPABILITY_DISCOVERY_NAMESPACES = Object.freeze([ @@ -48,6 +49,7 @@ export const CAPABILITY_DISCOVERY_NAMESPACES = Object.freeze([ { name: "governance", description: "Read, request, discuss, and decide approvals." }, { name: "workspace", description: "Inspect and control active-task workspace services." }, { name: "continuation", description: "Schedule a bounded continuation wake." }, + { name: "feedback", description: "Submit internal complaints and improvement suggestions." }, ]); export const CAPABILITY_DISCOVERY_GATEWAY_DEFINITIONS = Object.freeze([{ diff --git a/packages/paperclip-runner/src/semantic-tools/semantic-tools.test.ts b/packages/paperclip-runner/src/semantic-tools/semantic-tools.test.ts index 2686839b3d2..85d6829cb01 100644 --- a/packages/paperclip-runner/src/semantic-tools/semantic-tools.test.ts +++ b/packages/paperclip-runner/src/semantic-tools/semantic-tools.test.ts @@ -165,6 +165,20 @@ describe("Capability semantic catalog and authorization", () => { expect(JSON.stringify(found)).not.toContain("list_approvals"); }); + it("supports unnamespaced discovery and finds both feedback tools", async () => { + const claims = ["discovery:agents:read"]; + const adapter = await running(claims); + const dispatcher = new CapabilitySemanticDispatcher(adapter, { + scenario: { id: "feedback-discovery", claims }, explicitClaims: claims, + }); + const agents = dispatcher.discoverTools(OPEN.identity.runId, "find company agents"); + expect(agents.operations.map((tool) => tool.name)).toEqual(expect.arrayContaining(["list_agents", "get_agent"])); + for (const options of [{}, { namespace: "feedback" }]) { + const feedback = dispatcher.discoverTools(OPEN.identity.runId, "feedback", options); + expect(feedback.operations.map((tool) => tool.name)).toEqual(["submit_complaint", "submit_suggestion"]); + } + }); + it("does not disclose optional tools that current authority cannot invoke", async () => { const adapter = await running(); const dispatcher = new CapabilitySemanticDispatcher(adapter); diff --git a/packages/paperclip-runner/src/semantic-tools/types.ts b/packages/paperclip-runner/src/semantic-tools/types.ts index 343410f1501..5c1d0a7e66f 100644 --- a/packages/paperclip-runner/src/semantic-tools/types.ts +++ b/packages/paperclip-runner/src/semantic-tools/types.ts @@ -56,6 +56,8 @@ export type CapabilitySemanticOperationId = | "decide_approval" | "comment_on_approval" | "schedule_wake" + | "submit_complaint" + | "submit_suggestion" | "generic_api_request"; export interface CapabilityJsonSchema { diff --git a/server/scripts/verify-agent-commentary-live.ts b/server/scripts/verify-agent-commentary-live.ts index 6cf420e22b4..40317a7bf01 100644 --- a/server/scripts/verify-agent-commentary-live.ts +++ b/server/scripts/verify-agent-commentary-live.ts @@ -20,18 +20,7 @@ import { ensureNativeCompletionContract } from "../src/services/native-runtime/c import { redactSensitiveText } from "../src/redaction.js"; const root = await mkdtemp(join(tmpdir(), "paperclip-commentary-live-")); -const authSource = process.env.PAPERCLIP_LIVE_CODEX_HOME ?? process.env.CODEX_HOME ?? join(homedir(), ".codex"); -const codexHome = join(root, "codex"); -await mkdir(codexHome, { mode: 0o700 }); -await copyFile(join(authSource, "auth.json"), join(codexHome, "auth.json")); -await writeFile(join(codexHome, "config.toml"), 'model_reasoning_effort = "low"\n'); -process.env.PAPERCLIP_HOME = join(root, "instance"); -process.env.PAPERCLIP_AGENT_JWT_SECRET = randomUUID(); -process.env.PAPERCLIP_CODEX_AUTH_CACHE = "false"; -process.env.PAPERCLIP_TELEMETRY_DISABLED = "1"; -const server = await startRunnerApiTestServer(); -process.env.PAPERCLIP_API_URL = server.apiUrl; -const evidence: unknown[] = []; +let server: Awaited> | undefined; async function makeRemovable(path: string): Promise { const stat = await lstat(path); if (stat.isSymbolicLink()) return; @@ -41,6 +30,18 @@ async function makeRemovable(path: string): Promise { } } try { + const authSource = process.env.PAPERCLIP_LIVE_CODEX_HOME ?? process.env.CODEX_HOME ?? join(homedir(), ".codex"); + const codexHome = join(root, "codex"); + await mkdir(codexHome, { mode: 0o700 }); + await copyFile(join(authSource, "auth.json"), join(codexHome, "auth.json")); + await writeFile(join(codexHome, "config.toml"), 'model_reasoning_effort = "low"\n'); + process.env.PAPERCLIP_HOME = join(root, "instance"); + process.env.PAPERCLIP_AGENT_JWT_SECRET = randomUUID(); + process.env.PAPERCLIP_CODEX_AUTH_CACHE = "false"; + process.env.PAPERCLIP_TELEMETRY_DISABLED = "1"; + server = await startRunnerApiTestServer(); + process.env.PAPERCLIP_API_URL = server.apiUrl; + const evidence: unknown[] = []; for (const mode of ["legacy", "native"] as const) { if (process.argv[2] && process.argv[2] !== mode) continue; const f = await server.fixture({ disableWakeOnDemand: true }); @@ -104,7 +105,10 @@ try { } console.log(JSON.stringify({ verifiedAt: new Date().toISOString(), evidence }, null, 2)); } finally { - await server.close(); - await makeRemovable(root); - await rm(root, { recursive: true, force: true }); + try { + await server?.close(); + } finally { + await makeRemovable(root); + await rm(root, { recursive: true, force: true }); + } } diff --git a/server/src/__tests__/agent-commentary.integration.test.ts b/server/src/__tests__/agent-commentary.integration.test.ts index 5c6742f7781..175f158e2eb 100644 --- a/server/src/__tests__/agent-commentary.integration.test.ts +++ b/server/src/__tests__/agent-commentary.integration.test.ts @@ -226,6 +226,8 @@ describe("internal agent commentary through both transports", () => { }); } expect(await run(server.apiUrl)).toEqual({ code: 0, out: "Feedback stored.\n", err: "" }); + expect(await run(server.apiUrl + "/api/")).toEqual({ code: 0, out: "Feedback stored.\n", err: "" }); + expect(await rows(f)).toHaveLength(1); expect((await rows(f))[0].body).toBe(body); const failed = await run("http://127.0.0.1:1"); expect(failed).toMatchObject({ code: 0, out: "" }); diff --git a/skills/paperclip/scripts/submit-agent-commentary.py b/skills/paperclip/scripts/submit-agent-commentary.py index 34eb04ed9a5..fc881319c00 100644 --- a/skills/paperclip/scripts/submit-agent-commentary.py +++ b/skills/paperclip/scripts/submit-agent-commentary.py @@ -18,6 +18,8 @@ def main(): if len(raw) > 4 * 524288 or not body.strip() or len(body.encode("utf-16-le")) // 2 > 524288: raise ValueError() api_url = os.environ["PAPERCLIP_API_URL"].rstrip("/") + if api_url.endswith("/api"): + api_url = api_url[:-4] company_id = str(uuid.UUID(os.environ["PAPERCLIP_COMPANY_ID"])) run_id = str(uuid.UUID(os.environ["PAPERCLIP_RUN_ID"])) key = sys.argv[2] if len(sys.argv) > 2 else str(uuid.uuid4())