mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
fix: resolve feedback discovery and submission review findings
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3dafe3a9a4
commit
169b7fc3f4
9 files changed
+46
-19
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ const NAMESPACE: Readonly<Record<CapabilitySemanticOperationId, string>> = 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([{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -56,6 +56,8 @@ export type CapabilitySemanticOperationId =
|
||||
| "decide_approval"
|
||||
| "comment_on_approval"
|
||||
| "schedule_wake"
|
||||
| "submit_complaint"
|
||||
| "submit_suggestion"
|
||||
| "generic_api_request";
|
||||
|
||||
export interface CapabilityJsonSchema {
|
||||
|
||||
@@ -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<ReturnType<typeof startRunnerApiTestServer>> | undefined;
|
||||
async function makeRemovable(path: string): Promise<void> {
|
||||
const stat = await lstat(path);
|
||||
if (stat.isSymbolicLink()) return;
|
||||
@@ -41,6 +30,18 @@ async function makeRemovable(path: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -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: "" });
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in new issue
Block a user