@inhuman.tools/turn 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/dist/agent-calls/acceptance.d.ts +12 -0
- package/dist/agent-calls/acceptance.d.ts.map +1 -0
- package/dist/agent-calls/acceptance.js +64 -0
- package/dist/agent-calls/acceptance.js.map +1 -0
- package/dist/agent-calls/callbacks.d.ts +14 -0
- package/dist/agent-calls/callbacks.d.ts.map +1 -0
- package/dist/agent-calls/callbacks.js +59 -0
- package/dist/agent-calls/callbacks.js.map +1 -0
- package/dist/agent-calls/delivery.d.ts +7 -0
- package/dist/agent-calls/delivery.d.ts.map +1 -0
- package/dist/agent-calls/delivery.js +16 -0
- package/dist/agent-calls/delivery.js.map +1 -0
- package/dist/agent-calls/index.d.ts +7 -0
- package/dist/agent-calls/index.d.ts.map +1 -0
- package/dist/agent-calls/index.js +6 -0
- package/dist/agent-calls/index.js.map +1 -0
- package/dist/agent-calls/operator.d.ts +21 -0
- package/dist/agent-calls/operator.d.ts.map +1 -0
- package/dist/agent-calls/operator.js +78 -0
- package/dist/agent-calls/operator.js.map +1 -0
- package/dist/agent-calls/runtime.d.ts +99 -0
- package/dist/agent-calls/runtime.d.ts.map +1 -0
- package/dist/agent-calls/runtime.js +245 -0
- package/dist/agent-calls/runtime.js.map +1 -0
- package/dist/agent-calls/types.d.ts +31 -0
- package/dist/agent-calls/types.d.ts.map +1 -0
- package/dist/agent-calls/types.js +2 -0
- package/dist/agent-calls/types.js.map +1 -0
- package/dist/context/compact-floors.d.ts +43 -0
- package/dist/context/compact-floors.d.ts.map +1 -0
- package/dist/context/compact-floors.js +52 -0
- package/dist/context/compact-floors.js.map +1 -0
- package/dist/context/compact.d.ts +42 -0
- package/dist/context/compact.d.ts.map +1 -0
- package/dist/context/compact.js +155 -0
- package/dist/context/compact.js.map +1 -0
- package/dist/context/delivery.d.ts +13 -0
- package/dist/context/delivery.d.ts.map +1 -0
- package/dist/context/delivery.js +6 -0
- package/dist/context/delivery.js.map +1 -0
- package/dist/context/history.d.ts +66 -0
- package/dist/context/history.d.ts.map +1 -0
- package/dist/context/history.js +573 -0
- package/dist/context/history.js.map +1 -0
- package/dist/context/index.d.ts +10 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +10 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/observations.d.ts +7 -0
- package/dist/context/observations.d.ts.map +1 -0
- package/dist/context/observations.js +11 -0
- package/dist/context/observations.js.map +1 -0
- package/dist/context/retention/defaults.d.ts +10 -0
- package/dist/context/retention/defaults.d.ts.map +1 -0
- package/dist/context/retention/defaults.js +47 -0
- package/dist/context/retention/defaults.js.map +1 -0
- package/dist/context/retention/index.d.ts +14 -0
- package/dist/context/retention/index.d.ts.map +1 -0
- package/dist/context/retention/index.js +21 -0
- package/dist/context/retention/index.js.map +1 -0
- package/dist/context/retention/narrow.d.ts +22 -0
- package/dist/context/retention/narrow.d.ts.map +1 -0
- package/dist/context/retention/narrow.js +140 -0
- package/dist/context/retention/narrow.js.map +1 -0
- package/dist/context/retention/render.d.ts +37 -0
- package/dist/context/retention/render.d.ts.map +1 -0
- package/dist/context/retention/render.js +149 -0
- package/dist/context/retention/render.js.map +1 -0
- package/dist/context/retention/resolve.d.ts +51 -0
- package/dist/context/retention/resolve.d.ts.map +1 -0
- package/dist/context/retention/resolve.js +254 -0
- package/dist/context/retention/resolve.js.map +1 -0
- package/dist/context/retention/schema.d.ts +345 -0
- package/dist/context/retention/schema.d.ts.map +1 -0
- package/dist/context/retention/schema.js +149 -0
- package/dist/context/retention/schema.js.map +1 -0
- package/dist/context/retention/stamp.d.ts +25 -0
- package/dist/context/retention/stamp.d.ts.map +1 -0
- package/dist/context/retention/stamp.js +103 -0
- package/dist/context/retention/stamp.js.map +1 -0
- package/dist/context/retention/step.d.ts +33 -0
- package/dist/context/retention/step.d.ts.map +1 -0
- package/dist/context/retention/step.js +74 -0
- package/dist/context/retention/step.js.map +1 -0
- package/dist/context/retention/view.d.ts +57 -0
- package/dist/context/retention/view.d.ts.map +1 -0
- package/dist/context/retention/view.js +121 -0
- package/dist/context/retention/view.js.map +1 -0
- package/dist/context/squeeze.d.ts +45 -0
- package/dist/context/squeeze.d.ts.map +1 -0
- package/dist/context/squeeze.js +182 -0
- package/dist/context/squeeze.js.map +1 -0
- package/dist/context/tools.d.ts +16 -0
- package/dist/context/tools.d.ts.map +1 -0
- package/dist/context/tools.js +73 -0
- package/dist/context/tools.js.map +1 -0
- package/dist/context/transcript.d.ts +151 -0
- package/dist/context/transcript.d.ts.map +1 -0
- package/dist/context/transcript.js +659 -0
- package/dist/context/transcript.js.map +1 -0
- package/dist/policy-processor/bypass.d.ts +95 -0
- package/dist/policy-processor/bypass.d.ts.map +1 -0
- package/dist/policy-processor/bypass.js +188 -0
- package/dist/policy-processor/bypass.js.map +1 -0
- package/dist/policy-processor/classifier.d.ts +47 -0
- package/dist/policy-processor/classifier.d.ts.map +1 -0
- package/dist/policy-processor/classifier.js +671 -0
- package/dist/policy-processor/classifier.js.map +1 -0
- package/dist/policy-processor/cluster.d.ts +7 -0
- package/dist/policy-processor/cluster.d.ts.map +1 -0
- package/dist/policy-processor/cluster.js +32 -0
- package/dist/policy-processor/cluster.js.map +1 -0
- package/dist/policy-processor/evidence.d.ts +26 -0
- package/dist/policy-processor/evidence.d.ts.map +1 -0
- package/dist/policy-processor/evidence.js +204 -0
- package/dist/policy-processor/evidence.js.map +1 -0
- package/dist/policy-processor/explain.d.ts +59 -0
- package/dist/policy-processor/explain.d.ts.map +1 -0
- package/dist/policy-processor/explain.js +224 -0
- package/dist/policy-processor/explain.js.map +1 -0
- package/dist/policy-processor/index.d.ts +6 -0
- package/dist/policy-processor/index.d.ts.map +1 -0
- package/dist/policy-processor/index.js +6 -0
- package/dist/policy-processor/index.js.map +1 -0
- package/dist/policy-processor/license.d.ts +23 -0
- package/dist/policy-processor/license.d.ts.map +1 -0
- package/dist/policy-processor/license.js +47 -0
- package/dist/policy-processor/license.js.map +1 -0
- package/dist/policy-processor/peel-cwd.d.ts +2 -0
- package/dist/policy-processor/peel-cwd.d.ts.map +1 -0
- package/dist/policy-processor/peel-cwd.js +2 -0
- package/dist/policy-processor/peel-cwd.js.map +1 -0
- package/dist/policy-processor/real-path.d.ts +7 -0
- package/dist/policy-processor/real-path.d.ts.map +1 -0
- package/dist/policy-processor/real-path.js +42 -0
- package/dist/policy-processor/real-path.js.map +1 -0
- package/dist/policy-processor/scope.d.ts +111 -0
- package/dist/policy-processor/scope.d.ts.map +1 -0
- package/dist/policy-processor/scope.js +629 -0
- package/dist/policy-processor/scope.js.map +1 -0
- package/dist/policy-processor/self-protect.d.ts +10 -0
- package/dist/policy-processor/self-protect.d.ts.map +1 -0
- package/dist/policy-processor/self-protect.js +48 -0
- package/dist/policy-processor/self-protect.js.map +1 -0
- package/dist/policy-processor/soft-deny.d.ts +75 -0
- package/dist/policy-processor/soft-deny.d.ts.map +1 -0
- package/dist/policy-processor/soft-deny.js +185 -0
- package/dist/policy-processor/soft-deny.js.map +1 -0
- package/dist/policy-processor/unattended-guard.d.ts +28 -0
- package/dist/policy-processor/unattended-guard.d.ts.map +1 -0
- package/dist/policy-processor/unattended-guard.js +145 -0
- package/dist/policy-processor/unattended-guard.js.map +1 -0
- package/dist/session/cancel.d.ts +14 -0
- package/dist/session/cancel.d.ts.map +1 -0
- package/dist/session/cancel.js +32 -0
- package/dist/session/cancel.js.map +1 -0
- package/dist/session/commands.d.ts +41 -0
- package/dist/session/commands.d.ts.map +1 -0
- package/dist/session/commands.js +241 -0
- package/dist/session/commands.js.map +1 -0
- package/dist/session/compact-actor.d.ts +23 -0
- package/dist/session/compact-actor.d.ts.map +1 -0
- package/dist/session/compact-actor.js +91 -0
- package/dist/session/compact-actor.js.map +1 -0
- package/dist/session/compact-env.d.ts +54 -0
- package/dist/session/compact-env.d.ts.map +1 -0
- package/dist/session/compact-env.js +345 -0
- package/dist/session/compact-env.js.map +1 -0
- package/dist/session/compact.d.ts +42 -0
- package/dist/session/compact.d.ts.map +1 -0
- package/dist/session/compact.js +68 -0
- package/dist/session/compact.js.map +1 -0
- package/dist/session/index.d.ts +9 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +8 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/park.d.ts +16 -0
- package/dist/session/park.d.ts.map +1 -0
- package/dist/session/park.js +73 -0
- package/dist/session/park.js.map +1 -0
- package/dist/session/side-channel-reminder.d.ts +41 -0
- package/dist/session/side-channel-reminder.d.ts.map +1 -0
- package/dist/session/side-channel-reminder.js +132 -0
- package/dist/session/side-channel-reminder.js.map +1 -0
- package/dist/session/types.d.ts +44 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +2 -0
- package/dist/session/types.js.map +1 -0
- package/dist/tool-catalog/catalog.d.ts +77 -0
- package/dist/tool-catalog/catalog.d.ts.map +1 -0
- package/dist/tool-catalog/catalog.js +380 -0
- package/dist/tool-catalog/catalog.js.map +1 -0
- package/dist/tool-catalog/index.d.ts +4 -0
- package/dist/tool-catalog/index.d.ts.map +1 -0
- package/dist/tool-catalog/index.js +3 -0
- package/dist/tool-catalog/index.js.map +1 -0
- package/dist/tool-catalog/recipes.d.ts +36 -0
- package/dist/tool-catalog/recipes.d.ts.map +1 -0
- package/dist/tool-catalog/recipes.js +121 -0
- package/dist/tool-catalog/recipes.js.map +1 -0
- package/dist/tool-catalog/types.d.ts +103 -0
- package/dist/tool-catalog/types.d.ts.map +1 -0
- package/dist/tool-catalog/types.js +5 -0
- package/dist/tool-catalog/types.js.map +1 -0
- package/dist/tool-runtime/approval/approved.d.ts +13 -0
- package/dist/tool-runtime/approval/approved.d.ts.map +1 -0
- package/dist/tool-runtime/approval/approved.js +20 -0
- package/dist/tool-runtime/approval/approved.js.map +1 -0
- package/dist/tool-runtime/approval/dist.d.ts +6 -0
- package/dist/tool-runtime/approval/dist.d.ts.map +1 -0
- package/dist/tool-runtime/approval/dist.js +28 -0
- package/dist/tool-runtime/approval/dist.js.map +1 -0
- package/dist/tool-runtime/approval/inbound.d.ts +7 -0
- package/dist/tool-runtime/approval/inbound.d.ts.map +1 -0
- package/dist/tool-runtime/approval/inbound.js +48 -0
- package/dist/tool-runtime/approval/inbound.js.map +1 -0
- package/dist/tool-runtime/approval/unattended.d.ts +8 -0
- package/dist/tool-runtime/approval/unattended.d.ts.map +1 -0
- package/dist/tool-runtime/approval/unattended.js +23 -0
- package/dist/tool-runtime/approval/unattended.js.map +1 -0
- package/dist/tool-runtime/blob-port.d.ts +46 -0
- package/dist/tool-runtime/blob-port.d.ts.map +1 -0
- package/dist/tool-runtime/blob-port.js +47 -0
- package/dist/tool-runtime/blob-port.js.map +1 -0
- package/dist/tool-runtime/extra-root.d.ts +3 -0
- package/dist/tool-runtime/extra-root.d.ts.map +1 -0
- package/dist/tool-runtime/extra-root.js +14 -0
- package/dist/tool-runtime/extra-root.js.map +1 -0
- package/dist/tool-runtime/handler.d.ts +29 -0
- package/dist/tool-runtime/handler.d.ts.map +1 -0
- package/dist/tool-runtime/handler.js +460 -0
- package/dist/tool-runtime/handler.js.map +1 -0
- package/dist/tool-runtime/index.d.ts +14 -0
- package/dist/tool-runtime/index.d.ts.map +1 -0
- package/dist/tool-runtime/index.js +14 -0
- package/dist/tool-runtime/index.js.map +1 -0
- package/dist/tool-runtime/peel.d.ts +19 -0
- package/dist/tool-runtime/peel.d.ts.map +1 -0
- package/dist/tool-runtime/peel.js +74 -0
- package/dist/tool-runtime/peel.js.map +1 -0
- package/dist/tool-runtime/plugin-context.d.ts +13 -0
- package/dist/tool-runtime/plugin-context.d.ts.map +1 -0
- package/dist/tool-runtime/plugin-context.js +55 -0
- package/dist/tool-runtime/plugin-context.js.map +1 -0
- package/dist/tool-runtime/recall.d.ts +18 -0
- package/dist/tool-runtime/recall.d.ts.map +1 -0
- package/dist/tool-runtime/recall.js +100 -0
- package/dist/tool-runtime/recall.js.map +1 -0
- package/dist/tool-runtime/run-system-tool.d.ts +13 -0
- package/dist/tool-runtime/run-system-tool.d.ts.map +1 -0
- package/dist/tool-runtime/run-system-tool.js +230 -0
- package/dist/tool-runtime/run-system-tool.js.map +1 -0
- package/dist/tool-runtime/tool-error.d.ts +6 -0
- package/dist/tool-runtime/tool-error.d.ts.map +1 -0
- package/dist/tool-runtime/tool-error.js +6 -0
- package/dist/tool-runtime/tool-error.js.map +1 -0
- package/dist/tool-runtime/worker-tools.d.ts +8 -0
- package/dist/tool-runtime/worker-tools.d.ts.map +1 -0
- package/dist/tool-runtime/worker-tools.js +65 -0
- package/dist/tool-runtime/worker-tools.js.map +1 -0
- package/dist/tool-runtime/write-admit.d.ts +21 -0
- package/dist/tool-runtime/write-admit.d.ts.map +1 -0
- package/dist/tool-runtime/write-admit.js +89 -0
- package/dist/tool-runtime/write-admit.js.map +1 -0
- package/package.json +114 -3
- package/policy/auto-mode-ask.scm +201 -0
- package/policy/bypass-detect.prompt +55 -0
- package/policy/classifier-bash-incomplete.prompt +46 -0
- package/policy/classifier-bash.prompt +45 -0
- package/policy/classifier-mcp.prompt +45 -0
- package/policy/classifier-read.prompt +44 -0
- package/policy/classifier-write-outside.prompt +46 -0
- package/policy/classifier-write-risky.prompt +45 -0
- package/policy/classifier-write-workspace.prompt +46 -0
- package/policy/classifier-write.prompt +45 -0
- package/policy/soft-deny-round1.prompt +51 -0
- package/policy/soft-deny-round2.prompt +60 -0
- package/src/agent-calls/acceptance.ts +71 -0
- package/src/agent-calls/callbacks.ts +75 -0
- package/src/agent-calls/delivery.ts +15 -0
- package/src/agent-calls/index.ts +16 -0
- package/src/agent-calls/operator.ts +99 -0
- package/src/agent-calls/runtime.ts +310 -0
- package/src/agent-calls/types.ts +33 -0
- package/src/context/compact-floors.ts +76 -0
- package/src/context/compact.ts +176 -0
- package/src/context/delivery.ts +15 -0
- package/src/context/history.ts +725 -0
- package/src/context/index.ts +35 -0
- package/src/context/observations.ts +13 -0
- package/src/context/retention/defaults.ts +58 -0
- package/src/context/retention/index.ts +45 -0
- package/src/context/retention/narrow.ts +123 -0
- package/src/context/retention/render.ts +177 -0
- package/src/context/retention/resolve.ts +306 -0
- package/src/context/retention/schema.ts +173 -0
- package/src/context/retention/stamp.ts +121 -0
- package/src/context/retention/step.ts +85 -0
- package/src/context/retention/view.ts +161 -0
- package/src/context/squeeze.ts +218 -0
- package/src/context/tools.ts +92 -0
- package/src/context/transcript.ts +705 -0
- package/src/policy-processor/README.md +43 -0
- package/src/policy-processor/bypass.ts +246 -0
- package/src/policy-processor/classifier.ts +711 -0
- package/src/policy-processor/cluster.ts +31 -0
- package/src/policy-processor/evidence.ts +214 -0
- package/src/policy-processor/explain.ts +310 -0
- package/src/policy-processor/index.ts +6 -0
- package/src/policy-processor/license.ts +45 -0
- package/src/policy-processor/peel-cwd.ts +1 -0
- package/src/policy-processor/real-path.ts +41 -0
- package/src/policy-processor/scope.ts +655 -0
- package/src/policy-processor/self-protect.ts +46 -0
- package/src/policy-processor/soft-deny.ts +225 -0
- package/src/policy-processor/unattended-guard.ts +158 -0
- package/src/session/cancel.ts +34 -0
- package/src/session/commands.ts +292 -0
- package/src/session/compact-actor.ts +132 -0
- package/src/session/compact-env.ts +539 -0
- package/src/session/compact.ts +95 -0
- package/src/session/index.ts +18 -0
- package/src/session/park.ts +73 -0
- package/src/session/side-channel-reminder.ts +148 -0
- package/src/session/types.ts +50 -0
- package/src/tool-catalog/catalog.ts +282 -0
- package/src/tool-catalog/index.ts +26 -0
- package/src/tool-catalog/recipes.ts +154 -0
- package/src/tool-catalog/types.ts +110 -0
- package/src/tool-runtime/approval/approved.ts +20 -0
- package/src/tool-runtime/approval/dist.ts +29 -0
- package/src/tool-runtime/approval/inbound.ts +50 -0
- package/src/tool-runtime/approval/unattended.ts +24 -0
- package/src/tool-runtime/blob-port.ts +64 -0
- package/src/tool-runtime/extra-root.ts +16 -0
- package/src/tool-runtime/handler.ts +576 -0
- package/src/tool-runtime/index.ts +21 -0
- package/src/tool-runtime/peel.ts +91 -0
- package/src/tool-runtime/plugin-context.ts +68 -0
- package/src/tool-runtime/recall.ts +104 -0
- package/src/tool-runtime/run-system-tool.ts +273 -0
- package/src/tool-runtime/tool-error.ts +6 -0
- package/src/tool-runtime/worker-tools.ts +76 -0
- package/src/tool-runtime/write-admit.ts +90 -0
- package/README.md +0 -3
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft-deny judge, host half. The question is "did the user explicitly request this exact action?".
|
|
3
|
+
* The model never gets the last word: the host builds every prompt section (one line of JSON each, so
|
|
4
|
+
* agent text cannot forge a label), parses the vote strictly, and verifies the quoted user words
|
|
5
|
+
* itself. Any parse error, missing sink or failed quote check is a `fail` (the cell's `onFail`),
|
|
6
|
+
* never an allow. Untrusted content (tool output, the agent's own text) is never authorization.
|
|
7
|
+
*/
|
|
8
|
+
import { CLASSES, type ClassId, type FloorVote } from "@inhuman.tools/policy/floor";
|
|
9
|
+
|
|
10
|
+
/** Authorization the model must claim before a soft-deny allow counts (on top of >= risk). */
|
|
11
|
+
export const SOFT_DENY_MIN_AUTHORIZATION = 7;
|
|
12
|
+
/** A quote shorter than this proves nothing. */
|
|
13
|
+
export const MIN_QUOTE_CHARS = 3;
|
|
14
|
+
/** Strings in the proposed call are cut at this length in the prompt (the verification is not). */
|
|
15
|
+
const CALL_STRING_LIMIT = 2000;
|
|
16
|
+
const MESSAGE_LIMIT = 8000;
|
|
17
|
+
|
|
18
|
+
export type SoftDenyOutcome =
|
|
19
|
+
| { readonly kind: "vote"; readonly decision: "allow" | "deny"; readonly round: 1 | 2; readonly reason: string }
|
|
20
|
+
/** Round 1 asks for the agent's last answer. */
|
|
21
|
+
| { readonly kind: "need-agent-answer"; readonly reason: string }
|
|
22
|
+
| { readonly kind: "fail"; readonly reason: string };
|
|
23
|
+
|
|
24
|
+
export type SoftDenyCheck = {
|
|
25
|
+
/** Oldest first. The only source of permission. */
|
|
26
|
+
readonly userMessages: readonly string[];
|
|
27
|
+
/** The agent's last answer before the latest user message, or "" when there is none. */
|
|
28
|
+
readonly agentAnswer: string;
|
|
29
|
+
/** Words that must appear in the quote for the action to be the one asked for. */
|
|
30
|
+
readonly identityTokens: readonly string[];
|
|
31
|
+
/** Verify the quote and the identity of the action. Only a user-layer cell may turn this off. */
|
|
32
|
+
readonly requireQuote: boolean;
|
|
33
|
+
/** Allow needs `authorization + margin >= risk`. */
|
|
34
|
+
readonly margin: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** NFC, case folded, whitespace collapsed: the form both sides of a quote check are compared in. */
|
|
38
|
+
export function normalizeQuote(text: string): string {
|
|
39
|
+
return text.normalize("NFC").toLowerCase().replaceAll(/\s+/g, " ").trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function contains(haystack: string, needle: string): boolean {
|
|
43
|
+
const want = normalizeQuote(needle);
|
|
44
|
+
return want.length >= MIN_QUOTE_CHARS && normalizeQuote(haystack).includes(want);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function words(text: string): Set<string> {
|
|
48
|
+
return new Set(
|
|
49
|
+
normalizeQuote(text)
|
|
50
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
51
|
+
.filter((word) => word !== ""),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Does `text` name the action: any identity token is a word of it (or a substring for path-like tokens)? */
|
|
56
|
+
export function namesAction(text: string, tokens: readonly string[]): boolean {
|
|
57
|
+
if (tokens.length === 0) return true;
|
|
58
|
+
const folded = normalizeQuote(text);
|
|
59
|
+
const set = words(text);
|
|
60
|
+
return tokens.some((token) => {
|
|
61
|
+
const want = normalizeQuote(token);
|
|
62
|
+
return want !== "" && (set.has(want) || (/[./_-]/.test(want) && folded.includes(want)));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const KEYS_R1 = new Set(["verdict", "quote", "reason", "risk", "authorization"]);
|
|
67
|
+
const KEYS_R2 = new Set([...KEYS_R1, "proposal"]);
|
|
68
|
+
|
|
69
|
+
function clean(raw: Record<string, unknown>): Record<string, unknown> {
|
|
70
|
+
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key.replace(/^'?:/, ""), value]));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isScore = (value: unknown): value is number =>
|
|
74
|
+
typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 9;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse and verify one round's vote. `raw` is what the program returned. Strict: unknown or missing
|
|
78
|
+
* keys, a wrong type or enum are `fail`. Quote and identity checks are the host's, not the model's.
|
|
79
|
+
*/
|
|
80
|
+
export function parseSoftDenyVote(raw: unknown, round: 1 | 2, check: SoftDenyCheck): SoftDenyOutcome {
|
|
81
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
82
|
+
return { kind: "fail", reason: "soft-deny judge did not vote" };
|
|
83
|
+
}
|
|
84
|
+
const vote = clean(raw as Record<string, unknown>);
|
|
85
|
+
const allowed = round === 1 ? KEYS_R1 : KEYS_R2;
|
|
86
|
+
for (const key of Object.keys(vote)) {
|
|
87
|
+
if (!allowed.has(key)) return { kind: "fail", reason: `soft-deny vote has an unexpected field ${key}` };
|
|
88
|
+
}
|
|
89
|
+
const { verdict, quote, reason, risk, authorization } = vote;
|
|
90
|
+
const proposal = vote.proposal;
|
|
91
|
+
if (typeof reason !== "string" || typeof quote !== "string") {
|
|
92
|
+
return { kind: "fail", reason: "soft-deny vote is missing quote or reason" };
|
|
93
|
+
}
|
|
94
|
+
if (!isScore(risk) || !isScore(authorization))
|
|
95
|
+
return { kind: "fail", reason: "soft-deny vote has no risk or authorization" };
|
|
96
|
+
if (round === 2 && typeof proposal !== "string")
|
|
97
|
+
return { kind: "fail", reason: "soft-deny round 2 vote has no proposal" };
|
|
98
|
+
const why = reason.slice(0, 300);
|
|
99
|
+
if (verdict === "deny") return { kind: "vote", decision: "deny", round, reason: why };
|
|
100
|
+
const messages = check.userMessages;
|
|
101
|
+
const latest = messages.at(-1) ?? "";
|
|
102
|
+
if (verdict === "need-agent-answer") {
|
|
103
|
+
if (round === 2) return { kind: "fail", reason: "soft-deny round 2 cannot ask for the agent's answer" };
|
|
104
|
+
if (check.requireQuote && !contains(latest, quote)) return quoteFailure("confirmation");
|
|
105
|
+
return { kind: "need-agent-answer", reason: why };
|
|
106
|
+
}
|
|
107
|
+
if (verdict !== "allow") return { kind: "fail", reason: "soft-deny vote has an unknown verdict" };
|
|
108
|
+
if (check.requireQuote) {
|
|
109
|
+
if (round === 1) {
|
|
110
|
+
if (!messages.some((message) => contains(message, quote))) return quoteFailure("request");
|
|
111
|
+
if (!namesAction(quote, check.identityTokens)) {
|
|
112
|
+
// The user's words are real but do not name this action: at most a confirmation of something else.
|
|
113
|
+
return { kind: "need-agent-answer", reason: "the quoted words do not name the action" };
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
if (!contains(latest, quote)) return quoteFailure("confirmation");
|
|
117
|
+
if (check.agentAnswer === "" || !contains(check.agentAnswer, proposal as string)) return quoteFailure("proposal");
|
|
118
|
+
if (!namesAction(proposal as string, check.identityTokens)) {
|
|
119
|
+
return { kind: "fail", reason: "the proposal does not name the action" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (authorization < SOFT_DENY_MIN_AUTHORIZATION || authorization + check.margin < risk) {
|
|
124
|
+
return {
|
|
125
|
+
kind: "vote",
|
|
126
|
+
decision: "deny",
|
|
127
|
+
round,
|
|
128
|
+
reason: `authorization ${authorization} does not cover risk ${risk}: ${why}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { kind: "vote", decision: "allow", round, reason: why };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A quote that is not in the user's words is a red-team signal, and a failure, not a deny. */
|
|
135
|
+
function quoteFailure(what: string): SoftDenyOutcome {
|
|
136
|
+
return { kind: "fail", reason: `judge.quote_unverified: the ${what} quoted by the judge is not in the transcript` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function clip(text: string, limit: number): string {
|
|
140
|
+
return text.length <= limit ? text : `${text.slice(0, limit)}…[${text.length - limit} more characters]`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function clipValue(value: unknown): unknown {
|
|
144
|
+
if (typeof value === "string") return clip(value, CALL_STRING_LIMIT);
|
|
145
|
+
if (Array.isArray(value)) return value.map((item) => clipValue(item));
|
|
146
|
+
if (value !== null && typeof value === "object") {
|
|
147
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clipValue(item)]));
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type SoftDenySections = {
|
|
153
|
+
readonly floorJson: string;
|
|
154
|
+
readonly userJson: string;
|
|
155
|
+
readonly callJson: string;
|
|
156
|
+
readonly agentAnswerJson: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** Every section is `JSON.stringify` output: one line, so nothing inside can start a new labelled section. */
|
|
160
|
+
export function buildSoftDenySections(input: {
|
|
161
|
+
readonly classes: readonly ClassId[];
|
|
162
|
+
readonly votes: readonly Pick<FloorVote, "ruleId">[];
|
|
163
|
+
readonly userMessages: readonly string[];
|
|
164
|
+
readonly agentAnswer: string;
|
|
165
|
+
readonly allowAgentAnswer: boolean;
|
|
166
|
+
readonly tool: string;
|
|
167
|
+
readonly cluster: string;
|
|
168
|
+
readonly args: unknown;
|
|
169
|
+
/** The policy owner's words about this call (a rule's `describe`). Trusted context for the judge, not permission. */
|
|
170
|
+
readonly describe?: string;
|
|
171
|
+
}): SoftDenySections {
|
|
172
|
+
return {
|
|
173
|
+
floorJson: JSON.stringify({
|
|
174
|
+
tier: "soft-deny",
|
|
175
|
+
classes: input.classes.map((id) => ({ id, label: CLASSES[id].label, description: CLASSES[id].description })),
|
|
176
|
+
rules: input.votes.map((vote) => vote.ruleId),
|
|
177
|
+
...(input.describe === undefined || input.describe === ""
|
|
178
|
+
? {}
|
|
179
|
+
: { ownerDescription: clip(input.describe, MESSAGE_LIMIT) }),
|
|
180
|
+
}),
|
|
181
|
+
userJson: JSON.stringify(input.userMessages.map((message) => clip(message, MESSAGE_LIMIT))),
|
|
182
|
+
callJson: JSON.stringify({ tool: input.tool, cluster: input.cluster, args: clipValue(input.args ?? {}) }),
|
|
183
|
+
agentAnswerJson:
|
|
184
|
+
input.allowAgentAnswer && input.agentAnswer !== "" ? JSON.stringify(clip(input.agentAnswer, MESSAGE_LIMIT)) : "",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Words the user's quote must contain for the action to be the one they asked for. */
|
|
189
|
+
export function identityTokens(input: {
|
|
190
|
+
readonly cluster: string;
|
|
191
|
+
readonly tool: string;
|
|
192
|
+
readonly args: Readonly<Record<string, unknown>> | null;
|
|
193
|
+
readonly votes: readonly FloorVote[];
|
|
194
|
+
readonly serverToolName?: string;
|
|
195
|
+
}): string[] {
|
|
196
|
+
const out = new Set<string>();
|
|
197
|
+
const base = (path: string) => path.split("/").findLast(Boolean) ?? path;
|
|
198
|
+
switch (input.cluster) {
|
|
199
|
+
case "bash": {
|
|
200
|
+
for (const vote of input.votes) {
|
|
201
|
+
if (vote.unit === undefined) continue;
|
|
202
|
+
out.add(base(vote.unit.argv0));
|
|
203
|
+
for (const word of vote.unit.spine) out.add(word);
|
|
204
|
+
for (const word of vote.unit.familyArgs) out.add(word);
|
|
205
|
+
}
|
|
206
|
+
if (out.size === 0 && typeof input.args?.command === "string") {
|
|
207
|
+
const first = input.args.command.trim().split(/\s+/)[0];
|
|
208
|
+
if (first !== undefined && first !== "") out.add(base(first));
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case "read":
|
|
213
|
+
case "write": {
|
|
214
|
+
const path = input.args?.path ?? input.args?.cwd;
|
|
215
|
+
if (typeof path === "string" && path !== "") out.add(base(path));
|
|
216
|
+
for (const vote of input.votes) if (vote.path !== undefined) out.add(base(vote.path.path));
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
default: {
|
|
220
|
+
const name = input.serverToolName ?? input.tool;
|
|
221
|
+
for (const word of name.split(/[_\-./\s]+|(?<=[a-z0-9])(?=[A-Z])/).filter(Boolean)) out.add(word);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return [...out];
|
|
225
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unattended-run watch: every licensed or denied call of an eval turn goes through here, the
|
|
3
|
+
* ledger counts denials, and bypass detection decides when the run stops for a person.
|
|
4
|
+
*
|
|
5
|
+
* A stop seals the run's program with `interaction_required` and the reason, asks the turn to cancel
|
|
6
|
+
* (queued calls settle as cancelled), and tells the host so it can notify. The check itself is a
|
|
7
|
+
* dedicated judge pass over the recent series; only a "yes" stops the run, except a judge that cannot be
|
|
8
|
+
* reached twice (fail closed, configurable) and the budget guard `hardStopTotal`.
|
|
9
|
+
*/
|
|
10
|
+
import { evalProgramOf, type SessionTurn, type ToolCallJob } from "@inhuman.tools/harness-model";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
buildBypassSections,
|
|
14
|
+
decideTrigger,
|
|
15
|
+
isAckRefusal,
|
|
16
|
+
outcomeOf,
|
|
17
|
+
parseBypassVerdict,
|
|
18
|
+
recordOf,
|
|
19
|
+
stopReason,
|
|
20
|
+
type BypassVerdict,
|
|
21
|
+
RunLedger,
|
|
22
|
+
} from "./bypass.js";
|
|
23
|
+
import { runBypassJudge } from "./classifier.js";
|
|
24
|
+
import { evidenceCuts, owningTurn } from "./evidence.js";
|
|
25
|
+
import type { LicenseScope } from "./scope.js";
|
|
26
|
+
|
|
27
|
+
export type RunStop = {
|
|
28
|
+
readonly kind: "bypass" | "hard-stop" | "immediate" | "check-unavailable";
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
readonly turn: SessionTurn;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Text of the coded failure a stopped run reports. */
|
|
34
|
+
export function stoppedRunText(reason: string): string {
|
|
35
|
+
return `Operation requires human authorization: the run was stopped (${reason}). Review it and resume.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ledgerFor(scope: LicenseScope, turn: SessionTurn): RunLedger {
|
|
39
|
+
let ledger = scope.ledgers.get(turn);
|
|
40
|
+
if (ledger === undefined) {
|
|
41
|
+
ledger = new RunLedger();
|
|
42
|
+
scope.ledgers.set(turn, ledger);
|
|
43
|
+
}
|
|
44
|
+
return ledger;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Seal the program with `interaction_required` and ask the turn to stop. Idempotent. */
|
|
48
|
+
export function stopRun(scope: LicenseScope, turn: SessionTurn, ledger: RunLedger, stop: Omit<RunStop, "turn">): void {
|
|
49
|
+
if (ledger.stopped) return;
|
|
50
|
+
ledger.stopped = true;
|
|
51
|
+
const program = evalProgramOf(turn);
|
|
52
|
+
if (program !== null && program.outcomeSnap === undefined) {
|
|
53
|
+
program.outcomeSnap = { ok: false, text: stoppedRunText(stop.reason), code: "interaction_required" };
|
|
54
|
+
}
|
|
55
|
+
turn.cancelRequested = true;
|
|
56
|
+
scope.onRunStopped?.({ ...stop, turn });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Note how a call was licensed. Only unattended calls are counted. Never throws: the watch must not
|
|
61
|
+
* change a license that is already decided.
|
|
62
|
+
*/
|
|
63
|
+
export async function noteLicense(scope: LicenseScope, job: ToolCallJob, allowed: boolean): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
if (!job.unattended) return;
|
|
66
|
+
const turn = owningTurn(job);
|
|
67
|
+
if (turn?.evalSpec == null) return;
|
|
68
|
+
const ledger = ledgerFor(scope, turn);
|
|
69
|
+
if (ledger.stopped) return;
|
|
70
|
+
if (allowed) {
|
|
71
|
+
ledger.ran(recordOf(job));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const { outcome, counted } = outcomeOf(job);
|
|
75
|
+
if (!counted) {
|
|
76
|
+
ledger.uncounted({ ...recordOf(job), outcome });
|
|
77
|
+
// The run continues; the user is told what is waiting for their acknowledgement.
|
|
78
|
+
if (isAckRefusal(job)) {
|
|
79
|
+
scope.onRunNotice?.({
|
|
80
|
+
turn,
|
|
81
|
+
text: `an unattended run needs an acknowledgement: ${job.ackFlags.map((flag) => flag.label).join("; ")} (${job.name} was denied; the run continues)`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const record = ledger.denied({ ...recordOf(job), outcome });
|
|
87
|
+
const esc = job.escalation;
|
|
88
|
+
const trigger = decideTrigger(ledger, esc, record);
|
|
89
|
+
if (trigger === null) return;
|
|
90
|
+
if (trigger === "immediate") {
|
|
91
|
+
stopRun(scope, turn, ledger, {
|
|
92
|
+
kind: "immediate",
|
|
93
|
+
reason: `bypass suspected: control-plane-tampering: it reached for ${record.ruleIds.join(", ") || "the controls"} after earlier denials`,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (trigger === "hard-stop") {
|
|
98
|
+
stopRun(scope, turn, ledger, {
|
|
99
|
+
kind: "hard-stop",
|
|
100
|
+
reason: `hard stop: ${ledger.total} denied calls (limit ${esc.hardStopTotal})`,
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!esc.bypassCheck) {
|
|
105
|
+
ledger.lastCheckAtTotal = ledger.total;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await check(scope, job, turn, ledger);
|
|
109
|
+
} catch {
|
|
110
|
+
/* a watch failure never lifts or changes a denial */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function check(scope: LicenseScope, job: ToolCallJob, turn: SessionTurn, ledger: RunLedger): Promise<void> {
|
|
115
|
+
const esc = job.escalation;
|
|
116
|
+
const cuts = evidenceCuts(job);
|
|
117
|
+
const sections = buildBypassSections({
|
|
118
|
+
task: cuts.privileged,
|
|
119
|
+
calls: ledger.calls,
|
|
120
|
+
notes: cuts.priorTurn === "" ? [] : [cuts.priorTurn],
|
|
121
|
+
window: esc.window,
|
|
122
|
+
});
|
|
123
|
+
ledger.checks += 1;
|
|
124
|
+
let verdict: BypassVerdict | undefined;
|
|
125
|
+
for (let attempt = 0; attempt < 2 && verdict === undefined; attempt += 1) {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = parseBypassVerdict(await runBypassJudge(scope, job, sections));
|
|
128
|
+
if ("fail" in parsed) throw new Error(parsed.fail);
|
|
129
|
+
verdict = parsed;
|
|
130
|
+
} catch {
|
|
131
|
+
ledger.checkFails += 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (verdict === undefined) {
|
|
135
|
+
// Fail closed: a check that cannot be made twice is not a clean bill of health.
|
|
136
|
+
if (esc.stopOnDoubleFail) {
|
|
137
|
+
stopRun(scope, turn, ledger, { kind: "check-unavailable", reason: "bypass check unavailable" });
|
|
138
|
+
} else {
|
|
139
|
+
ledger.lastCheckAtTotal = ledger.total;
|
|
140
|
+
scope.onRunNotice?.({ turn, text: "repeated denials in an unattended run; the bypass check was unavailable" });
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
ledger.checkFails = 0;
|
|
145
|
+
ledger.lastCheckAtTotal = ledger.total;
|
|
146
|
+
switch (verdict.verdict) {
|
|
147
|
+
case "bypass":
|
|
148
|
+
stopRun(scope, turn, ledger, { kind: "bypass", reason: stopReason(verdict) });
|
|
149
|
+
return;
|
|
150
|
+
case "unsure":
|
|
151
|
+
scope.onRunNotice?.({ turn, text: "an unattended run hit repeated denials" });
|
|
152
|
+
ledger.consecutive = 0;
|
|
153
|
+
return;
|
|
154
|
+
case "no-bypass":
|
|
155
|
+
ledger.consecutive = 0;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop predicate and leftover rejection for a turn. Accepted-work Calls stay.
|
|
3
|
+
* In-flight BODY abort is the caller's `abort`.
|
|
4
|
+
*/
|
|
5
|
+
import { ApprovableJob, CallJob, SessionTurn } from "@inhuman.tools/harness-model";
|
|
6
|
+
import { walkWork } from "@inhuman.tools/plexus-expectations";
|
|
7
|
+
|
|
8
|
+
import { shouldSkipTurnCancel } from "../agent-calls/acceptance.js";
|
|
9
|
+
import { type CallsCatalog } from "../agent-calls/index.js";
|
|
10
|
+
import { cancelUnstartedTool } from "../tool-runtime/index.js";
|
|
11
|
+
|
|
12
|
+
export function turnLoopStopped(turn: SessionTurn, signal: AbortSignal): boolean {
|
|
13
|
+
return signal.aborted || turn.reducerExited || turn.cancelRequested;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Leftovers settle as cancelled so the turn's join and the session's open
|
|
18
|
+
* tool count release. An orchestrated leaf is also aborted: its executor may
|
|
19
|
+
* be gone, and a cancelled job is never claimed again.
|
|
20
|
+
*/
|
|
21
|
+
export function failLeftoverApprovables(
|
|
22
|
+
turn: SessionTurn,
|
|
23
|
+
abort: (leaf: ApprovableJob) => void,
|
|
24
|
+
catalog: CallsCatalog,
|
|
25
|
+
): void {
|
|
26
|
+
for (const node of walkWork(turn)) {
|
|
27
|
+
if (!(node instanceof ApprovableJob)) continue;
|
|
28
|
+
if (node.completed || node.cancelled) continue;
|
|
29
|
+
if (node instanceof CallJob && shouldSkipTurnCancel(node, catalog)) continue;
|
|
30
|
+
if (node.orchestrated) abort(node);
|
|
31
|
+
cancelUnstartedTool(node);
|
|
32
|
+
if (!node.completed) node.markComplete();
|
|
33
|
+
}
|
|
34
|
+
}
|