@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,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifier-lambda payload. Cluster table and args parse live in
|
|
3
|
+
* harness-model. This module keeps the unknown[] shape the remainder
|
|
4
|
+
* ports apply.
|
|
5
|
+
*/
|
|
6
|
+
import { clusterOf, fileQuestion } from "@inhuman.tools/harness-model";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Strings the agent controls are JSON-encoded here: the judge prompt places them in a
|
|
10
|
+
* labelled "untrusted" block, and encoding keeps a newline-and-label from forging a new section.
|
|
11
|
+
*/
|
|
12
|
+
/** Arguments for the matching `classifier/*` lambda, or undefined if the payload is missing. */
|
|
13
|
+
export function classifierArgs(name: string, args: Readonly<Record<string, unknown>> | null): unknown[] | undefined {
|
|
14
|
+
switch (clusterOf(name)) {
|
|
15
|
+
case "bash": {
|
|
16
|
+
if (typeof args?.command !== "string" || args.command === "") return undefined;
|
|
17
|
+
const cwd = typeof args.cwd === "string" && args.cwd !== "" && args.cwd !== "." ? args.cwd : undefined;
|
|
18
|
+
return cwd === undefined
|
|
19
|
+
? [JSON.stringify(args.command)]
|
|
20
|
+
: [JSON.stringify(args.command), `cwd ${JSON.stringify(cwd)}`];
|
|
21
|
+
}
|
|
22
|
+
case "read":
|
|
23
|
+
return [JSON.stringify(fileQuestion(name, args) ?? ".")];
|
|
24
|
+
case "write": {
|
|
25
|
+
const question = fileQuestion(name, args);
|
|
26
|
+
return question == null ? undefined : [JSON.stringify(question)];
|
|
27
|
+
}
|
|
28
|
+
case "mcp":
|
|
29
|
+
return [JSON.stringify(name), JSON.stringify(args ?? {})];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifier request cuts from the job's owning turn. Nested jobs keep
|
|
3
|
+
* that turn; a caller-supplied turn would be the wrong one. Privileged
|
|
4
|
+
* is user prompts on the owning turn and its tail ancestry — continue
|
|
5
|
+
* mints `prompt: ""` with the authorization on the tail. Unattended /
|
|
6
|
+
* eval with no user text: owning ProgrammaticJob expr (approval.md
|
|
7
|
+
* EVIDENCE). The bag itself is not a remainder subject — only inner
|
|
8
|
+
* ToolCallJob leaves.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
clusterOf,
|
|
12
|
+
fileQuestion,
|
|
13
|
+
ProgrammaticJob,
|
|
14
|
+
reactChunks,
|
|
15
|
+
SessionTurn,
|
|
16
|
+
tapeCompletion,
|
|
17
|
+
ToolCallJob,
|
|
18
|
+
} from "@inhuman.tools/harness-model";
|
|
19
|
+
import { parseTirithEvidence, tirithPolicyCut } from "@inhuman.tools/policy-tirith";
|
|
20
|
+
|
|
21
|
+
export type EvidenceCuts = {
|
|
22
|
+
/** Oldest-first user prompts on the owning turn and its tails (the eval expression when there are none). */
|
|
23
|
+
readonly userMessages?: readonly string[];
|
|
24
|
+
/** The agent's last completed answer before the most recent user message. Round 2 of the soft-deny judge only. */
|
|
25
|
+
readonly agentAnswer?: string;
|
|
26
|
+
readonly privileged: string;
|
|
27
|
+
readonly priorTurn: string;
|
|
28
|
+
readonly quarantined: string;
|
|
29
|
+
readonly policy?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function evidenceCuts(job: ToolCallJob): EvidenceCuts {
|
|
33
|
+
const turn = owningTurn(job);
|
|
34
|
+
const program = owningProgram(job);
|
|
35
|
+
if (turn == null) {
|
|
36
|
+
const expr = program?.expr.trim() || "";
|
|
37
|
+
return {
|
|
38
|
+
userMessages: expr === "" ? [] : [expr],
|
|
39
|
+
agentAnswer: "",
|
|
40
|
+
privileged: expr,
|
|
41
|
+
priorTurn: "",
|
|
42
|
+
quarantined: "",
|
|
43
|
+
policy: policyPipCut(job.policyEvidenceJson),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const prior: string[] = [];
|
|
47
|
+
const quarantined: string[] = [];
|
|
48
|
+
for (const chunk of reactChunks(turn)) {
|
|
49
|
+
const completion = tapeCompletion(chunk);
|
|
50
|
+
if (completion != null) {
|
|
51
|
+
if (!completion.isDone || underJob(completion, job)) continue;
|
|
52
|
+
if (completion.responseText !== "") prior.push(completion.responseText);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!(chunk instanceof ToolCallJob) || chunk === job) continue;
|
|
56
|
+
if (chunk.sealedOutcomeJson() == null) continue;
|
|
57
|
+
const text = observationText(chunk.observationPayload());
|
|
58
|
+
if (text === "") continue;
|
|
59
|
+
quarantined.push(`${chunk.name} → ${text}`);
|
|
60
|
+
}
|
|
61
|
+
const messages = userPrompts(turn, program);
|
|
62
|
+
return {
|
|
63
|
+
userMessages: messages,
|
|
64
|
+
agentAnswer: agentAnswerBefore(turn),
|
|
65
|
+
privileged: messages.join("\n"),
|
|
66
|
+
priorTurn: prior.join("\n"),
|
|
67
|
+
quarantined: quarantined.join("\n"),
|
|
68
|
+
policy: policyPipCut(job.policyEvidenceJson),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function policyPipCut(json: string | null): string {
|
|
73
|
+
if (json == null || json === "") return "";
|
|
74
|
+
const report = parseTirithEvidence(json);
|
|
75
|
+
if (report != null) return tirithPolicyCut(report);
|
|
76
|
+
return json;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Oldest-first user prompts on `turn` then tails. Expr only when none exist. */
|
|
80
|
+
function userPrompts(turn: SessionTurn, program: ProgrammaticJob | null): string[] {
|
|
81
|
+
const seen = new Set<SessionTurn>();
|
|
82
|
+
const prompts: string[] = [];
|
|
83
|
+
let cell: SessionTurn | null = turn;
|
|
84
|
+
while (cell != null && !seen.has(cell)) {
|
|
85
|
+
seen.add(cell);
|
|
86
|
+
const text = cell.prompt.trim();
|
|
87
|
+
if (text !== "") prompts.push(text);
|
|
88
|
+
cell = cell.turnTail instanceof SessionTurn ? cell.turnTail : null;
|
|
89
|
+
}
|
|
90
|
+
prompts.reverse();
|
|
91
|
+
if (prompts.length > 0) return prompts;
|
|
92
|
+
const expr = program?.expr.trim() || "";
|
|
93
|
+
return expr === "" ? [] : [expr];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The agent's last completed answer that precedes the most recent user message: the proposal a short
|
|
98
|
+
* reply ("let's do it") refers to. Not the current turn's text, never tool output. Empty when there is
|
|
99
|
+
* none (the most recent user message opens the conversation).
|
|
100
|
+
*/
|
|
101
|
+
function agentAnswerBefore(turn: SessionTurn): string {
|
|
102
|
+
const seen = new Set<SessionTurn>();
|
|
103
|
+
let cell: SessionTurn | null = turn;
|
|
104
|
+
while (cell != null && !seen.has(cell) && cell.prompt.trim() === "") {
|
|
105
|
+
seen.add(cell);
|
|
106
|
+
cell = cell.turnTail instanceof SessionTurn ? cell.turnTail : null;
|
|
107
|
+
}
|
|
108
|
+
// `cell` carries the most recent user prompt. Its tails hold the answers that came before it.
|
|
109
|
+
let earlier: SessionTurn | null = cell != null && cell.turnTail instanceof SessionTurn ? cell.turnTail : null;
|
|
110
|
+
while (earlier != null && !seen.has(earlier)) {
|
|
111
|
+
seen.add(earlier);
|
|
112
|
+
const chunks = reactChunks(earlier);
|
|
113
|
+
for (const chunk of chunks.toReversed()) {
|
|
114
|
+
const completion = tapeCompletion(chunk);
|
|
115
|
+
if (completion != null && completion.isDone && completion.responseText !== "") return completion.responseText;
|
|
116
|
+
}
|
|
117
|
+
earlier = earlier.turnTail instanceof SessionTurn ? earlier.turnTail : null;
|
|
118
|
+
}
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function evidenceIdentity(cuts: EvidenceCuts): string {
|
|
123
|
+
return JSON.stringify([cuts.privileged, cuts.priorTurn, cuts.quarantined, cuts.policy ?? "", cuts.agentAnswer ?? ""]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Cluster+payload remainder skip key. */
|
|
127
|
+
export function evidencePayloadKey(job: ToolCallJob): string | null {
|
|
128
|
+
const payload = canonicalPayload(job);
|
|
129
|
+
if (payload == null || payload === "") return null;
|
|
130
|
+
// The tier and classes are part of the key: a payload that was an ask under one policy and a
|
|
131
|
+
// soft-deny under another must not share a vote.
|
|
132
|
+
return JSON.stringify([clusterOf(job.name), payload, job.policyResult, job.policyClassesJson ?? ""]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function owningTurn(job: ToolCallJob): SessionTurn | null {
|
|
136
|
+
return ancestor(job, (parent): parent is SessionTurn => parent instanceof SessionTurn);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function owningProgram(job: ToolCallJob): ProgrammaticJob | null {
|
|
140
|
+
return ancestor(job, (parent): parent is ProgrammaticJob => parent instanceof ProgrammaticJob);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ancestor<T>(job: { parent: unknown }, match: (parent: unknown) => parent is T): T | null {
|
|
144
|
+
const seen = new Set<object>();
|
|
145
|
+
let node: { parent: unknown } | null = job;
|
|
146
|
+
while (node != null) {
|
|
147
|
+
if (seen.has(node)) return null;
|
|
148
|
+
seen.add(node);
|
|
149
|
+
const parent: unknown = node.parent;
|
|
150
|
+
if (match(parent)) return parent;
|
|
151
|
+
if (parent instanceof SessionTurn) return null;
|
|
152
|
+
node = parent !== null && typeof parent === "object" && "parent" in parent ? (parent as { parent: unknown }) : null;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function underJob(node: { parent: unknown }, job: ToolCallJob): boolean {
|
|
158
|
+
const seen = new Set<object>();
|
|
159
|
+
let cur: { parent: unknown } | null = node;
|
|
160
|
+
while (cur != null) {
|
|
161
|
+
if (seen.has(cur)) return false;
|
|
162
|
+
seen.add(cur);
|
|
163
|
+
if (cur === job) return true;
|
|
164
|
+
const parent: unknown = cur.parent;
|
|
165
|
+
if (parent instanceof SessionTurn) return false;
|
|
166
|
+
cur = parent !== null && typeof parent === "object" && "parent" in parent ? (parent as { parent: unknown }) : null;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function observationText(value: unknown): string {
|
|
172
|
+
if (typeof value === "string") return value;
|
|
173
|
+
if (value === undefined) return "";
|
|
174
|
+
try {
|
|
175
|
+
const json = JSON.stringify(value);
|
|
176
|
+
return typeof json === "string" ? json : "";
|
|
177
|
+
} catch {
|
|
178
|
+
return String(value);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function canonicalPayload(job: ToolCallJob): string | null {
|
|
183
|
+
const args = job.args;
|
|
184
|
+
switch (clusterOf(job.name)) {
|
|
185
|
+
case "bash": {
|
|
186
|
+
const command = args?.command;
|
|
187
|
+
if (typeof command !== "string" || command === "") return null;
|
|
188
|
+
const cwd = args?.cwd;
|
|
189
|
+
if (typeof cwd === "string" && cwd !== "" && cwd !== ".") return `${command}\ncwd ${cwd}`;
|
|
190
|
+
return command;
|
|
191
|
+
}
|
|
192
|
+
case "read":
|
|
193
|
+
return fileQuestion(job.name, args) ?? ".";
|
|
194
|
+
case "write":
|
|
195
|
+
return fileQuestion(job.name, args);
|
|
196
|
+
case "mcp":
|
|
197
|
+
return `${job.name}\n${stableJson(args ?? {})}`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sortKeys(value: unknown): unknown {
|
|
202
|
+
if (value === null || typeof value !== "object") return value;
|
|
203
|
+
if (Array.isArray(value)) return value.map((item) => sortKeys(item));
|
|
204
|
+
const rec = value as Record<string, unknown>;
|
|
205
|
+
return Object.fromEntries(
|
|
206
|
+
Object.keys(rec)
|
|
207
|
+
.toSorted()
|
|
208
|
+
.map((key) => [key, sortKeys(rec[key])]),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function stableJson(value: unknown): string {
|
|
213
|
+
return JSON.stringify(sortKeys(value));
|
|
214
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "What would happen to this call right now?" The same floor, grant and resolution code as a real
|
|
3
|
+
* call, with no judge and no effect, so the answer cannot drift from the real path.
|
|
4
|
+
*/
|
|
5
|
+
import nodePath from "node:path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
activeAcks,
|
|
9
|
+
clusterOf,
|
|
10
|
+
collectAckFlags,
|
|
11
|
+
coveringGrants,
|
|
12
|
+
type GrantRow,
|
|
13
|
+
type GrantSubject,
|
|
14
|
+
grantOptionsFor,
|
|
15
|
+
isShellHatch,
|
|
16
|
+
layersOf,
|
|
17
|
+
matchAckFlags,
|
|
18
|
+
readRootList,
|
|
19
|
+
resolveAcks,
|
|
20
|
+
resolveCall,
|
|
21
|
+
ruleStampFor,
|
|
22
|
+
type CallSubject,
|
|
23
|
+
type PermissionMode,
|
|
24
|
+
} from "@inhuman.tools/harness-model";
|
|
25
|
+
import { CLASSES, classesOf, join, cast, type ClassId, type FloorVerdict } from "@inhuman.tools/policy/floor";
|
|
26
|
+
import { tirithClass, tirithDisposition, tirithFailure } from "@inhuman.tools/policy-tirith";
|
|
27
|
+
|
|
28
|
+
import { floorVerdict, withRealRoots, type LicenseScope } from "./scope.js";
|
|
29
|
+
|
|
30
|
+
export type ExplainProbe =
|
|
31
|
+
| { readonly kind: "shell"; readonly command: string; readonly cwd?: string }
|
|
32
|
+
| { readonly kind: "path"; readonly cluster: "read" | "write"; readonly path: string }
|
|
33
|
+
| { readonly kind: "mcp"; readonly server: string; readonly account?: string; readonly tool: string }
|
|
34
|
+
| { readonly kind: "tool"; readonly name: string; readonly args?: Readonly<Record<string, unknown>> };
|
|
35
|
+
|
|
36
|
+
export type Tier = "block" | "soft-deny" | "ask" | "allow";
|
|
37
|
+
|
|
38
|
+
export type Explanation = {
|
|
39
|
+
readonly tier: Tier;
|
|
40
|
+
readonly classes: Array<{ readonly id: ClassId; readonly label: string }>;
|
|
41
|
+
readonly votes: Array<{
|
|
42
|
+
readonly tier: Tier;
|
|
43
|
+
readonly class: ClassId;
|
|
44
|
+
readonly ruleId: string;
|
|
45
|
+
readonly lift?: string;
|
|
46
|
+
}>;
|
|
47
|
+
readonly resolver: {
|
|
48
|
+
readonly kind: "allow" | "judge" | "human" | "deny";
|
|
49
|
+
readonly layer: "dist" | "managed" | "user" | "project" | "session";
|
|
50
|
+
readonly onDeny?: "human" | "deny";
|
|
51
|
+
readonly onFail?: "human" | "deny";
|
|
52
|
+
} | null;
|
|
53
|
+
readonly grant: GrantRow | null;
|
|
54
|
+
readonly grantable: GrantSubject[];
|
|
55
|
+
/** Acknowledgement flags that cover the call in this mode and the stored ack that satisfies them. */
|
|
56
|
+
readonly ack: {
|
|
57
|
+
readonly flags: Array<{ readonly label: string; readonly why?: string; readonly layer: string }>;
|
|
58
|
+
readonly satisfiedBy: GrantRow | null;
|
|
59
|
+
} | null;
|
|
60
|
+
readonly outcome: "runs" | "judge-ask" | "judge-soft-deny" | "prompts-you" | "denied" | "blocked";
|
|
61
|
+
readonly reason: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const TIER: Readonly<Record<string, Tier>> = { block: "block", deny: "soft-deny", ask: "ask", policyApprove: "allow" };
|
|
65
|
+
|
|
66
|
+
function nameAndArgs(probe: ExplainProbe): { name: string; args: Record<string, unknown>; mcpName: string } {
|
|
67
|
+
switch (probe.kind) {
|
|
68
|
+
case "shell":
|
|
69
|
+
return {
|
|
70
|
+
name: "shell",
|
|
71
|
+
args: { command: probe.command, ...(probe.cwd === undefined ? {} : { cwd: probe.cwd }) },
|
|
72
|
+
mcpName: "shell",
|
|
73
|
+
};
|
|
74
|
+
case "path":
|
|
75
|
+
return { name: probe.cluster === "read" ? "read_file" : "write_file", args: { path: probe.path }, mcpName: "" };
|
|
76
|
+
case "mcp":
|
|
77
|
+
return { name: `mcp__${probe.server}__${probe.tool}`, args: {}, mcpName: probe.tool };
|
|
78
|
+
case "tool":
|
|
79
|
+
return { name: probe.name, args: { ...probe.args }, mcpName: probe.name };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function explainCall(
|
|
84
|
+
scope: LicenseScope,
|
|
85
|
+
probe: ExplainProbe,
|
|
86
|
+
options: { readonly mode?: PermissionMode; readonly unattended?: boolean } = {},
|
|
87
|
+
): Promise<Explanation> {
|
|
88
|
+
const root = scope.root;
|
|
89
|
+
const { name, args, mcpName } = nameAndArgs(probe);
|
|
90
|
+
const source = root.policyProgram.trim() === "" ? scope.distSource() : root.policyProgram;
|
|
91
|
+
let verdict: FloorVerdict;
|
|
92
|
+
const layers = layersOf({
|
|
93
|
+
user: root.authorizationUserJson,
|
|
94
|
+
project: root.authorizationProjectJson,
|
|
95
|
+
session: root.authorizationOverlayJson,
|
|
96
|
+
});
|
|
97
|
+
const ruleMcp =
|
|
98
|
+
probe.kind === "mcp"
|
|
99
|
+
? { server: probe.server, account: probe.account ?? probe.server, tool: probe.tool }
|
|
100
|
+
: undefined;
|
|
101
|
+
const rule =
|
|
102
|
+
clusterOf(name) === "mcp"
|
|
103
|
+
? ruleStampFor(layers, { tool: name, ...(ruleMcp === undefined ? {} : { mcp: ruleMcp }) })
|
|
104
|
+
: {};
|
|
105
|
+
try {
|
|
106
|
+
const policy = await scope.compiledPolicy(source, await withRealRoots(readRootList(root.readRoots).toSorted()));
|
|
107
|
+
verdict = await floorVerdict(policy, name, args, root.path, mcpName, rule);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
verdict = cast("block", "policy-fault", "host:policy-invalid");
|
|
110
|
+
return finish(
|
|
111
|
+
verdict,
|
|
112
|
+
null,
|
|
113
|
+
null,
|
|
114
|
+
[],
|
|
115
|
+
`the policy is invalid, so every call is blocked: ${error instanceof Error ? error.message : String(error)}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (clusterOf(name) === "bash" && typeof args.command === "string") {
|
|
119
|
+
try {
|
|
120
|
+
const report = await scope.checkShell(args.command, {
|
|
121
|
+
cwd: nodePath.resolve(root.path, typeof args.cwd === "string" ? args.cwd : "."),
|
|
122
|
+
});
|
|
123
|
+
verdict = join([
|
|
124
|
+
verdict,
|
|
125
|
+
cast(
|
|
126
|
+
tirithDisposition(report),
|
|
127
|
+
tirithClass(report),
|
|
128
|
+
`tirith:${report.findings.map((f) => f.rule_id).join("+")}`,
|
|
129
|
+
),
|
|
130
|
+
]);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const report = tirithFailure(error);
|
|
133
|
+
verdict = join([verdict, cast(tirithDisposition(report), tirithClass(report), "tirith:error")]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const tierVotes = verdict.votes.filter((vote) => vote.disposition === verdict.disposition);
|
|
137
|
+
const classes = classesOf(verdict);
|
|
138
|
+
const cluster = clusterOf(name);
|
|
139
|
+
const mcp = ruleMcp;
|
|
140
|
+
const hatch = typeof args.command === "string" && isShellHatch(args.command);
|
|
141
|
+
const call = {
|
|
142
|
+
tier: verdict.disposition,
|
|
143
|
+
cluster,
|
|
144
|
+
hatch,
|
|
145
|
+
votes: tierVotes,
|
|
146
|
+
mcp,
|
|
147
|
+
toolName: name,
|
|
148
|
+
} as const;
|
|
149
|
+
const grant = verdict.disposition === "ask" ? (coveringGrants(scope.visibleGrants(), call)?.[0] ?? null) : null;
|
|
150
|
+
const grantable = grantOptionsFor(call).map((option) => option.subject);
|
|
151
|
+
const subject: CallSubject = {
|
|
152
|
+
tool: name,
|
|
153
|
+
units: tierVotes.flatMap((vote) => (vote.unit === undefined ? [] : [vote.unit])),
|
|
154
|
+
paths: tierVotes.flatMap((vote) => (vote.path === undefined ? [] : [vote.path])),
|
|
155
|
+
...(mcp === undefined ? {} : { mcp }),
|
|
156
|
+
};
|
|
157
|
+
const resolution =
|
|
158
|
+
verdict.disposition === "ask" || verdict.disposition === "deny"
|
|
159
|
+
? resolveCall(layers, {
|
|
160
|
+
mode: options.mode ?? root.permissionMode,
|
|
161
|
+
cluster,
|
|
162
|
+
tier: verdict.disposition,
|
|
163
|
+
classes,
|
|
164
|
+
unattended: options.unattended ?? false,
|
|
165
|
+
subject,
|
|
166
|
+
})
|
|
167
|
+
: null;
|
|
168
|
+
const mode = options.mode ?? root.permissionMode;
|
|
169
|
+
const ackCall = {
|
|
170
|
+
classes,
|
|
171
|
+
...(mcp === undefined ? {} : { mcp }),
|
|
172
|
+
tool: name,
|
|
173
|
+
units: [...(verdict.seen ?? []), ...verdict.votes.flatMap((vote) => (vote.unit === undefined ? [] : [vote.unit]))],
|
|
174
|
+
};
|
|
175
|
+
const active =
|
|
176
|
+
verdict.disposition === "block" ? [] : activeAcks(matchAckFlags(collectAckFlags(layers), ackCall), mode);
|
|
177
|
+
const acks = resolveAcks(active, scope.visibleAcks(), ackCall);
|
|
178
|
+
const ack =
|
|
179
|
+
active.length === 0
|
|
180
|
+
? null
|
|
181
|
+
: {
|
|
182
|
+
flags: active.map((flag) => ({
|
|
183
|
+
label: flag.label,
|
|
184
|
+
...(flag.why === undefined ? {} : { why: flag.why }),
|
|
185
|
+
layer: flag.layer,
|
|
186
|
+
})),
|
|
187
|
+
satisfiedBy:
|
|
188
|
+
acks.state === "satisfied" ? (scope.visibleAcks().find((row) => row.id === acks.ackedBy[0]) ?? null) : null,
|
|
189
|
+
};
|
|
190
|
+
return withAck(
|
|
191
|
+
finish(verdict, resolution, grant, grantable, undefined, options.unattended ?? false),
|
|
192
|
+
ack,
|
|
193
|
+
options.unattended ?? false,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* An unsatisfied flag stops a call that would run, and follows one that waits for a judge or a person.
|
|
199
|
+
* `block` and denials are untouched: an acknowledgement never lifts anything.
|
|
200
|
+
*/
|
|
201
|
+
function withAck(base: Explanation, ack: Explanation["ack"], unattended: boolean): Explanation {
|
|
202
|
+
if (ack === null || ack.satisfiedBy !== null) return { ...base, ack };
|
|
203
|
+
const what = ack.flags.map((flag) => flag.label).join("; ");
|
|
204
|
+
if (base.outcome === "blocked" || base.outcome === "denied") return { ...base, ack };
|
|
205
|
+
if (unattended) {
|
|
206
|
+
return {
|
|
207
|
+
...base,
|
|
208
|
+
ack,
|
|
209
|
+
outcome: "denied",
|
|
210
|
+
reason: `${base.reason} It also needs your acknowledgement (${what}); this run is unattended, so it is denied as interaction_required and the run continues.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (base.outcome === "runs") {
|
|
214
|
+
return {
|
|
215
|
+
...base,
|
|
216
|
+
ack,
|
|
217
|
+
outcome: "prompts-you",
|
|
218
|
+
reason: `${base.reason} It is flagged to need your acknowledgement (${what}), so you are asked before it runs.`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { ...base, ack, reason: `${base.reason} If it is allowed, you are still asked to acknowledge it (${what}).` };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function finish(
|
|
225
|
+
verdict: FloorVerdict,
|
|
226
|
+
resolution: ReturnType<typeof resolveCall> | null,
|
|
227
|
+
grant: GrantRow | null,
|
|
228
|
+
grantable: GrantSubject[],
|
|
229
|
+
forced?: string,
|
|
230
|
+
unattended = false,
|
|
231
|
+
): Explanation {
|
|
232
|
+
const tier = TIER[verdict.disposition] ?? "ask";
|
|
233
|
+
const classes = classesOf(verdict);
|
|
234
|
+
const base = {
|
|
235
|
+
tier,
|
|
236
|
+
classes: classes.map((id) => ({ id, label: CLASSES[id].label })),
|
|
237
|
+
votes: verdict.votes.map((vote) => ({
|
|
238
|
+
tier: TIER[vote.disposition] ?? "ask",
|
|
239
|
+
class: vote.class,
|
|
240
|
+
ruleId: vote.ruleId,
|
|
241
|
+
...(vote.lift === undefined ? {} : { lift: vote.lift }),
|
|
242
|
+
})),
|
|
243
|
+
grant,
|
|
244
|
+
grantable,
|
|
245
|
+
ack: null,
|
|
246
|
+
};
|
|
247
|
+
const what = classes.map((id) => CLASSES[id].label.toLowerCase()).join(", ");
|
|
248
|
+
if (verdict.disposition === "block") {
|
|
249
|
+
return {
|
|
250
|
+
...base,
|
|
251
|
+
resolver: null,
|
|
252
|
+
outcome: "blocked",
|
|
253
|
+
reason: forced ?? `Blocked (${what}). Nothing lifts a block: no mode, grant, judge or answer.`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (resolution === null) {
|
|
257
|
+
return { ...base, resolver: null, outcome: "runs", reason: "The floor allows it." };
|
|
258
|
+
}
|
|
259
|
+
const { resolver, layer, interaction } = resolution;
|
|
260
|
+
const cell = {
|
|
261
|
+
kind: resolver.kind,
|
|
262
|
+
layer,
|
|
263
|
+
...(resolver.kind === "judge" ? { onDeny: resolver.onDeny, onFail: resolver.onFail } : {}),
|
|
264
|
+
} as const;
|
|
265
|
+
const tierName = tier === "soft-deny" ? "Soft-deny" : "Ask";
|
|
266
|
+
if (grant !== null) {
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
resolver: cell,
|
|
270
|
+
outcome: "runs",
|
|
271
|
+
reason: `${tierName} (${what}), covered by a stored grant: ${grant.label}.`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
switch (resolver.kind) {
|
|
275
|
+
case "allow":
|
|
276
|
+
return {
|
|
277
|
+
...base,
|
|
278
|
+
resolver: cell,
|
|
279
|
+
outcome: "runs",
|
|
280
|
+
reason: `${tierName} (${what}); the ${layer} table allows it in this mode.`,
|
|
281
|
+
};
|
|
282
|
+
case "deny":
|
|
283
|
+
return {
|
|
284
|
+
...base,
|
|
285
|
+
resolver: cell,
|
|
286
|
+
outcome: "denied",
|
|
287
|
+
reason:
|
|
288
|
+
interaction.resolver || (unattended && interaction.onDeny)
|
|
289
|
+
? `${tierName} (${what}) needs a person, and this run is unattended: denied as interaction_required.`
|
|
290
|
+
: `${tierName} (${what}); the ${layer} table denies it.`,
|
|
291
|
+
};
|
|
292
|
+
case "human":
|
|
293
|
+
return {
|
|
294
|
+
...base,
|
|
295
|
+
resolver: cell,
|
|
296
|
+
outcome: "prompts-you",
|
|
297
|
+
reason: `${tierName} (${what}): you are asked${tier === "soft-deny" ? " every time; nothing is remembered" : ""}.`,
|
|
298
|
+
};
|
|
299
|
+
case "judge":
|
|
300
|
+
return {
|
|
301
|
+
...base,
|
|
302
|
+
resolver: cell,
|
|
303
|
+
outcome: tier === "soft-deny" ? "judge-soft-deny" : "judge-ask",
|
|
304
|
+
reason:
|
|
305
|
+
tier === "soft-deny"
|
|
306
|
+
? `Soft-deny (${what}): the judge decides whether you explicitly asked for exactly this; a "no" ${resolver.onDeny === "human" ? "asks you" : "denies it"}. Grants do not apply.`
|
|
307
|
+
: `Ask (${what}): the judge decides; a "no" ${resolver.onDeny === "human" ? "asks you" : "denies it"}.`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monadic license. Admits the leaf onto the session intern, waits until
|
|
3
|
+
* LICENSE WHEN is a boolean, returns process-or-not. Floor and residual
|
|
4
|
+
* kicks live on `LicenseScope`. Does not write `approved`,
|
|
5
|
+
* `orchestrationResult`, or `regenerateable`. Nothing is remembered from a
|
|
6
|
+
* human allow here: a stored grant is a separate, explicit answer
|
|
7
|
+
* (`ask/answer` with `store`), and a soft-deny is licensed per call.
|
|
8
|
+
* Scope close mid-wait is not-process (false), not a throw. A boolean
|
|
9
|
+
* LICENSE WHEN still wins — Allow is a durable vote and must not lose to dispose.
|
|
10
|
+
*/
|
|
11
|
+
import { JOB_STOP_TOOL_NAME, type SessionRoot, type SessionTurn, type ToolCallJob } from "@inhuman.tools/harness-model";
|
|
12
|
+
import { when } from "mobx";
|
|
13
|
+
|
|
14
|
+
import { scopes } from "./scope.js";
|
|
15
|
+
import { noteLicense } from "./unattended-guard.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A denial the host decided before the license wait (an already-stamped block or deny). It still
|
|
19
|
+
* counts toward the unattended run's ledger.
|
|
20
|
+
*/
|
|
21
|
+
export async function noteDenied(job: ToolCallJob, root: SessionRoot): Promise<void> {
|
|
22
|
+
await noteLicense(scopes.get(root), job, false);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A tool's `isSideChannel` skips the floor. `triggersReactLoop: false` is
|
|
27
|
+
* loop control its author chose, not a license. `job_stop` skips too: the
|
|
28
|
+
* body only cancels jobs this session already spawned.
|
|
29
|
+
*/
|
|
30
|
+
export async function license(job: ToolCallJob, _turn: SessionTurn | null, root: SessionRoot): Promise<boolean> {
|
|
31
|
+
if (job.commFirst || job.isSideChannel || job.name === JOB_STOP_TOOL_NAME) return true;
|
|
32
|
+
const scope = scopes.get(root);
|
|
33
|
+
scope.admit(job);
|
|
34
|
+
try {
|
|
35
|
+
await when(() => scope.closed || job.cancelled || job.completed || job.licenseDecision !== null);
|
|
36
|
+
if (job.cancelled || job.completed) return false;
|
|
37
|
+
const decision = job.licenseDecision;
|
|
38
|
+
if (decision === null) return false;
|
|
39
|
+
// The unattended watch counts the call; a denial may be the one that trips bypass detection.
|
|
40
|
+
await noteLicense(scope, job, decision);
|
|
41
|
+
return decision;
|
|
42
|
+
} finally {
|
|
43
|
+
scope.release(job);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { peelShellCwd } from "@inhuman.tools/policy-bash-disassembler";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of file-path policy. The floor is pure and lexical; the tool opens whatever the
|
|
3
|
+
* filesystem says. A link inside the root that points at `.env` is named `notes.txt` to the floor
|
|
4
|
+
* and opens `.env` for the tool. This resolves symlinks (the deepest existing ancestor, so a
|
|
5
|
+
* write to a not-yet-created file under a linked directory resolves too) and re-expresses the
|
|
6
|
+
* result against the real root, so both spellings can be judged and the stricter verdict kept.
|
|
7
|
+
*/
|
|
8
|
+
import { realpath } from "node:fs/promises";
|
|
9
|
+
import nodePath from "node:path";
|
|
10
|
+
|
|
11
|
+
import { workspacePath } from "@inhuman.tools/harness-model";
|
|
12
|
+
|
|
13
|
+
export async function realpathDeepest(absolute: string): Promise<string> {
|
|
14
|
+
const rest: string[] = [];
|
|
15
|
+
let current = absolute;
|
|
16
|
+
for (;;) {
|
|
17
|
+
try {
|
|
18
|
+
const real = await realpath(current);
|
|
19
|
+
return rest.length === 0 ? real : nodePath.join(real, ...rest.toReversed());
|
|
20
|
+
} catch {
|
|
21
|
+
const parent = nodePath.dirname(current);
|
|
22
|
+
if (parent === current) return absolute;
|
|
23
|
+
rest.push(nodePath.basename(current));
|
|
24
|
+
current = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Workspace-relative spelling of the file the OS would open for `raw`, or undefined when it
|
|
31
|
+
* is the same as the lexical spelling (nothing to add).
|
|
32
|
+
*/
|
|
33
|
+
export async function resolvedWorkspacePath(rootPath: string, raw: string): Promise<string | undefined> {
|
|
34
|
+
if (rootPath.trim() === "" || !nodePath.isAbsolute(rootPath)) return undefined;
|
|
35
|
+
const lexical = workspacePath(rootPath, raw);
|
|
36
|
+
const absolute = nodePath.resolve(rootPath, raw.trim() === "" ? "." : raw);
|
|
37
|
+
const realRoot = await realpathDeepest(rootPath);
|
|
38
|
+
const realFile = await realpathDeepest(absolute);
|
|
39
|
+
const resolved = workspacePath(realRoot, realFile);
|
|
40
|
+
return resolved === lexical ? undefined : resolved;
|
|
41
|
+
}
|