Skip to content

Commit ad5a047

Browse files
committed
Prove permission.wait tags never carry free-text reason/prompt
Gate path with operator message free text must only land tool_id + allow/deny enums on the span; reason stays operator-facing only.
1 parent b81fdf6 commit ad5a047

1 file changed

Lines changed: 37 additions & 0 deletions

File tree

src/perf/permission-subagent-spans.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,43 @@ describe("permission.wait spans", () => {
7373
expect(waits[0]!.tags?.tool_id).toBe("run_shell");
7474
});
7575

76+
test("permission.wait tags never include free-text reason/prompt — only tool_id + decision", async () => {
77+
// Gate path that would surface operator free text in the verdict reason, but
78+
// must never land free-text keys on the span (sanitizeTags + gate only pass
79+
// tool_id + allow/deny enums).
80+
const freeText =
81+
"please do not store this prompt: /Users/me/secret/key.pem and system: you are";
82+
const gate = createPermissionGate({
83+
approvals: [],
84+
interactive: true,
85+
skipPermissions: false,
86+
requestApproval: async () => ({ allow: false, message: freeText }),
87+
});
88+
89+
const verdict = await gate.evaluate(shellCall("curl example.com"));
90+
expect(verdict.allowed).toBe(false);
91+
// Free text reaches the operator-facing reason only (not span tags).
92+
expect(
93+
!verdict.allowed && "reason" in verdict ? verdict.reason : "",
94+
).toContain(freeText);
95+
96+
const waits = byName(completed(snapshot()), "permission.wait");
97+
expect(waits).toHaveLength(1);
98+
const tags = waits[0]!.tags ?? {};
99+
// Allowlist: only tool_id + decision enums on permission.wait.
100+
expect(Object.keys(tags).sort()).toEqual(["decision", "tool_id"]);
101+
expect(tags).toEqual({ tool_id: "run_shell", decision: "deny" });
102+
// Explicit privacy fence: no free-text keys, and free text never appears in values.
103+
for (const key of ["reason", "prompt", "message", "path", "error", "action", "subject"] as const) {
104+
expect(Object.hasOwn(tags, key)).toBe(false);
105+
}
106+
for (const value of Object.values(tags)) {
107+
expect(String(value)).not.toContain(freeText);
108+
expect(String(value)).not.toContain("secret");
109+
expect(String(value)).not.toContain("system:");
110+
}
111+
});
112+
76113
test("records path-arg tool ask as permission.wait", async () => {
77114
const gate = createPermissionGate({
78115
approvals: [],

0 commit comments

Comments
 (0)