@inhuman.tools/turn 0.0.0-stage → 0.2.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,711 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remainder-classifier handle. Auto ask (floor ask + hatch). Cache is
|
|
3
|
+
* remainder skip. Empty connection or model gates the classifier, not
|
|
4
|
+
* identity — `dotprompt/run` requires both, and a run plan with one
|
|
5
|
+
* missing retries the invariant forever.
|
|
6
|
+
* `start` owns the run-plan reaction; minting does not begin I/O.
|
|
7
|
+
* Eligibility lives on the scope key set — this handle re-runs when the
|
|
8
|
+
* plan inputs change. Timer ticks are events (backoff). Floor `block`
|
|
9
|
+
* never mints these. Cluster ports share this handle. Classifier
|
|
10
|
+
* prompt files load on first require, not at import.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
disposeRunContext,
|
|
18
|
+
execState,
|
|
19
|
+
jsToScheme,
|
|
20
|
+
makeCallCtx,
|
|
21
|
+
parse,
|
|
22
|
+
toJS,
|
|
23
|
+
type RunContext,
|
|
24
|
+
type SchemeValue,
|
|
25
|
+
} from "@here.build/arrival";
|
|
26
|
+
import { applyCallback } from "@here.build/arrival/host-internals";
|
|
27
|
+
import { ASymbol } from "@here.build/arrival/reflect-internals";
|
|
28
|
+
import { loaderFromResolver } from "@here.build/arrival-modules";
|
|
29
|
+
import { DefaultedMap } from "@here.build/collections";
|
|
30
|
+
import { promptIsolatedCapability } from "@inhuman.tools/env-capability-module-prompt";
|
|
31
|
+
import {
|
|
32
|
+
fileQuestion,
|
|
33
|
+
isJsonSchemaUnsupported,
|
|
34
|
+
JUDGE_FAILS_FINAL,
|
|
35
|
+
MCPToolJob,
|
|
36
|
+
type ToolCallJob,
|
|
37
|
+
clusterOf,
|
|
38
|
+
ruleStampFor,
|
|
39
|
+
workspacePath,
|
|
40
|
+
} from "@inhuman.tools/harness-model";
|
|
41
|
+
import { classifierResources, policyEnvCapability } from "@inhuman.tools/policy/floor";
|
|
42
|
+
import { parseTirithEvidence, tirithCoverage } from "@inhuman.tools/policy-tirith";
|
|
43
|
+
import { action, comparer, type IReactionDisposer, observable, reaction, runInAction } from "mobx";
|
|
44
|
+
|
|
45
|
+
import type { BypassSections } from "./bypass.js";
|
|
46
|
+
import { classifierArgs } from "./cluster.js";
|
|
47
|
+
import { evidenceCuts, evidenceIdentity } from "./evidence.js";
|
|
48
|
+
import type { LicenseScope } from "./scope.js";
|
|
49
|
+
import {
|
|
50
|
+
buildSoftDenySections,
|
|
51
|
+
identityTokens,
|
|
52
|
+
parseSoftDenyVote,
|
|
53
|
+
type SoftDenyCheck,
|
|
54
|
+
type SoftDenyOutcome,
|
|
55
|
+
type SoftDenySections,
|
|
56
|
+
} from "./soft-deny.js";
|
|
57
|
+
|
|
58
|
+
function trace(name: string, message: string): void {
|
|
59
|
+
const debug = process.env.DEBUG;
|
|
60
|
+
if (debug == null) return;
|
|
61
|
+
const parts = debug.trim() === "" ? ["*"] : debug.split(/[\s,]+/).filter((part) => part !== "");
|
|
62
|
+
const on = parts.some((part) => {
|
|
63
|
+
if (part.startsWith("-")) return false;
|
|
64
|
+
if (part === "*" || part === name) return true;
|
|
65
|
+
if (part.endsWith("*")) return name.startsWith(part.slice(0, -1));
|
|
66
|
+
return name.startsWith(`${part}:`);
|
|
67
|
+
});
|
|
68
|
+
if (!on) return;
|
|
69
|
+
process.stderr.write(`${name} ${message}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Every remainder attempt is deadline-bounded. The first is short because the
|
|
74
|
+
* first vote is what the human is waiting on — a miss must open the approval
|
|
75
|
+
* window fast. Retries get longer: a slow-but-live model may still deliver a
|
|
76
|
+
* late allow, and the loop never gives up (overnight runs rely on that). But no
|
|
77
|
+
* attempt may run unbounded — with `scope.cap` a stuck classifier squatting a slot
|
|
78
|
+
* starves every job behind it, which is the hang.
|
|
79
|
+
*
|
|
80
|
+
* Below the stall timer on purpose: a live stream that trickles a token every
|
|
81
|
+
* few minutes resets the stall timer forever, so the wall clock is the only
|
|
82
|
+
* bound that fires.
|
|
83
|
+
*/
|
|
84
|
+
export const FIRST_ATTEMPT_MS = 12_000;
|
|
85
|
+
export const RETRY_ATTEMPT_MS = 5 * 60_000;
|
|
86
|
+
|
|
87
|
+
type ClassifierVote =
|
|
88
|
+
{ readonly decision: "allow" | "deny"; readonly reason?: string; readonly round?: 1 | 2 } | { readonly fail: string };
|
|
89
|
+
|
|
90
|
+
function voteOf(value: unknown): ClassifierVote {
|
|
91
|
+
switch (typeof value) {
|
|
92
|
+
case "string":
|
|
93
|
+
switch (value) {
|
|
94
|
+
case "allow":
|
|
95
|
+
case "deny":
|
|
96
|
+
return { decision: value };
|
|
97
|
+
default:
|
|
98
|
+
return value.trim() === "" ? { fail: "classifier did not vote" } : { fail: value };
|
|
99
|
+
}
|
|
100
|
+
case "object": {
|
|
101
|
+
if (value === null || Array.isArray(value)) return { fail: "classifier did not vote" };
|
|
102
|
+
const rec = value as Record<string, unknown>;
|
|
103
|
+
const decision = rec.decision ?? rec[":decision"];
|
|
104
|
+
switch (decision) {
|
|
105
|
+
case "allow":
|
|
106
|
+
case "deny":
|
|
107
|
+
return { decision };
|
|
108
|
+
default:
|
|
109
|
+
return { fail: "classifier did not vote" };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
return { fail: "classifier did not vote" };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function applyClassifier(
|
|
118
|
+
runCtx: RunContext,
|
|
119
|
+
fn: Parameters<typeof applyCallback>[0],
|
|
120
|
+
args: unknown[],
|
|
121
|
+
): Promise<unknown> {
|
|
122
|
+
const boxed = applyCallback(
|
|
123
|
+
fn,
|
|
124
|
+
args.map((value) => jsToScheme(runCtx, value) as SchemeValue),
|
|
125
|
+
makeCallCtx(runCtx),
|
|
126
|
+
);
|
|
127
|
+
const signal = runCtx.signal;
|
|
128
|
+
if (signal === undefined || signal.aborted) return toJS(await boxed);
|
|
129
|
+
const gate = new AbortController();
|
|
130
|
+
const winner = AbortSignal.any([signal, gate.signal]);
|
|
131
|
+
const abort = new Promise<never>((_resolve, reject) => {
|
|
132
|
+
winner.addEventListener(
|
|
133
|
+
"abort",
|
|
134
|
+
() => {
|
|
135
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("classifier aborted"));
|
|
136
|
+
},
|
|
137
|
+
{ once: true },
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
return toJS(await Promise.race([Promise.resolve(boxed), abort]));
|
|
142
|
+
} finally {
|
|
143
|
+
gate.abort();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
148
|
+
const classifierCache = new DefaultedMap((p: string): string => {
|
|
149
|
+
switch (p) {
|
|
150
|
+
case "classifier-bash.prompt":
|
|
151
|
+
return readFileSync(path.join(here, "../../policy/classifier-bash.prompt"), "utf8");
|
|
152
|
+
case "classifier-read.prompt":
|
|
153
|
+
return readFileSync(path.join(here, "../../policy/classifier-read.prompt"), "utf8");
|
|
154
|
+
case "classifier-write.prompt":
|
|
155
|
+
return readFileSync(path.join(here, "../../policy/classifier-write.prompt"), "utf8");
|
|
156
|
+
case "classifier-write-outside.prompt":
|
|
157
|
+
return readFileSync(path.join(here, "../../policy/classifier-write-outside.prompt"), "utf8");
|
|
158
|
+
case "classifier-write-workspace.prompt":
|
|
159
|
+
return readFileSync(path.join(here, "../../policy/classifier-write-workspace.prompt"), "utf8");
|
|
160
|
+
case "classifier-write-risky.prompt":
|
|
161
|
+
return readFileSync(path.join(here, "../../policy/classifier-write-risky.prompt"), "utf8");
|
|
162
|
+
case "classifier-mcp.prompt":
|
|
163
|
+
return readFileSync(path.join(here, "../../policy/classifier-mcp.prompt"), "utf8");
|
|
164
|
+
case "classifier-bash-incomplete.prompt":
|
|
165
|
+
return readFileSync(path.join(here, "../../policy/classifier-bash-incomplete.prompt"), "utf8");
|
|
166
|
+
case "soft-deny-round1.prompt":
|
|
167
|
+
return readFileSync(path.join(here, "../../policy/soft-deny-round1.prompt"), "utf8");
|
|
168
|
+
case "soft-deny-round2.prompt":
|
|
169
|
+
return readFileSync(path.join(here, "../../policy/soft-deny-round2.prompt"), "utf8");
|
|
170
|
+
case "bypass-detect.prompt":
|
|
171
|
+
return readFileSync(path.join(here, "../../policy/bypass-detect.prompt"), "utf8");
|
|
172
|
+
default:
|
|
173
|
+
throw new Error(`no such file: ${p}`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const classifierFiles = loaderFromResolver((p) => classifierCache.get(p));
|
|
178
|
+
|
|
179
|
+
export type ClassifierRouteOpts = {
|
|
180
|
+
/** Kernel last-choice. Session mint is a snapshot; this overlay is live. */
|
|
181
|
+
readonly fallback?: Readonly<Record<string, string>>;
|
|
182
|
+
/** Connection `settings.model` when the judge wire is named but `judgeModel` is empty. */
|
|
183
|
+
readonly configuredModel?: (connectionId: string) => string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export function classifierRoute(
|
|
187
|
+
kvs: Readonly<Record<string, string>>,
|
|
188
|
+
opts?: ClassifierRouteOpts,
|
|
189
|
+
): {
|
|
190
|
+
readonly connection: string;
|
|
191
|
+
readonly model: string;
|
|
192
|
+
readonly key: string;
|
|
193
|
+
} {
|
|
194
|
+
const fallback = opts?.fallback ?? {};
|
|
195
|
+
const connection = (kvs.judgeConnection || fallback.judgeConnection || kvs.connection || "").trim();
|
|
196
|
+
const judgeModel = (kvs.judgeModel || fallback.judgeModel || "").trim();
|
|
197
|
+
const configured = connection !== "" && opts?.configuredModel != null ? opts.configuredModel(connection).trim() : "";
|
|
198
|
+
const model = (judgeModel || configured || kvs.model || "").trim();
|
|
199
|
+
return { connection, model, key: `${connection}\n${model}` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type ClassifierPlan =
|
|
203
|
+
| { readonly kind: "cache"; readonly decision: "allow" | "deny"; readonly attempt: number }
|
|
204
|
+
| {
|
|
205
|
+
readonly kind: "literal";
|
|
206
|
+
readonly decision: "allow" | "deny";
|
|
207
|
+
readonly identity: string;
|
|
208
|
+
readonly attempt: number;
|
|
209
|
+
}
|
|
210
|
+
| { readonly kind: "verdict"; readonly decision: "allow" | "deny"; readonly reason: string; readonly attempt: number }
|
|
211
|
+
| { readonly kind: "noprogram"; readonly attempt: number }
|
|
212
|
+
| { readonly kind: "halt"; readonly attempt: number }
|
|
213
|
+
| { readonly kind: "noconnect"; readonly attempt: number }
|
|
214
|
+
| { readonly kind: "empty"; readonly attempt: number }
|
|
215
|
+
| { readonly kind: "unread"; readonly source: string; readonly attempt: number }
|
|
216
|
+
| {
|
|
217
|
+
readonly kind: "run";
|
|
218
|
+
readonly source: string;
|
|
219
|
+
readonly connection: string;
|
|
220
|
+
readonly model: string;
|
|
221
|
+
readonly attempt: number;
|
|
222
|
+
readonly identity: string;
|
|
223
|
+
readonly privileged: string;
|
|
224
|
+
readonly priorTurn: string;
|
|
225
|
+
readonly quarantined: string;
|
|
226
|
+
readonly policy: string;
|
|
227
|
+
readonly margin: number;
|
|
228
|
+
/** Set for the soft-deny judge: the host drives its rounds and verifies the vote. */
|
|
229
|
+
readonly soft?: SoftDenyPlan;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
type SoftDenyPlan = {
|
|
233
|
+
readonly sections: SoftDenySections;
|
|
234
|
+
readonly check: SoftDenyCheck;
|
|
235
|
+
readonly rounds: 1 | 2;
|
|
236
|
+
readonly unattended: boolean;
|
|
237
|
+
readonly hasAgentAnswer: boolean;
|
|
238
|
+
readonly callTier: string;
|
|
239
|
+
readonly callClasses: string;
|
|
240
|
+
readonly callRules: string;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
type ProgramRead = { readonly state: "forms"; readonly forms: readonly SchemeValue[] } | { readonly state: "broken" };
|
|
244
|
+
|
|
245
|
+
const programReads = observable.map<string, ProgramRead>({}, { deep: false });
|
|
246
|
+
const programReadsStarted = new Set<string>();
|
|
247
|
+
|
|
248
|
+
function startProgramRead(source: string): void {
|
|
249
|
+
if (programReadsStarted.has(source)) return;
|
|
250
|
+
programReadsStarted.add(source);
|
|
251
|
+
void parse(source).then(
|
|
252
|
+
(forms) => runInAction(() => programReads.set(source, { state: "forms", forms })),
|
|
253
|
+
() => runInAction(() => programReads.set(source, { state: "broken" })),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** One symbol. The reader has already dropped comments. A string `"allow"` is not this. */
|
|
258
|
+
function literalOf(forms: readonly SchemeValue[]): "allow" | "deny" | undefined {
|
|
259
|
+
const form = forms.length === 1 ? forms[0] : undefined;
|
|
260
|
+
if (!(form instanceof ASymbol)) return undefined;
|
|
261
|
+
return form.__name__ === "allow" || form.__name__ === "deny" ? form.__name__ : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The owner's `describe` for an MCP call, from the session's layers (a project file's words are not included). */
|
|
265
|
+
function ownerDescription(job: ToolCallJob): string | undefined {
|
|
266
|
+
if (job.authCluster !== "mcp") return undefined;
|
|
267
|
+
return ruleStampFor(job.authorizationLayers, job.callSubject).describe;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function remainderPlan(scope: LicenseScope, job: ToolCallJob, attempt: number): ClassifierPlan {
|
|
271
|
+
const source = scope.root.judgeAskProgram;
|
|
272
|
+
const route = classifierRoute(scope.root.kvs, {
|
|
273
|
+
fallback: scope.judgeFallbackKvs,
|
|
274
|
+
configuredModel: scope.judgeConfiguredModel,
|
|
275
|
+
});
|
|
276
|
+
const cuts = evidenceCuts(job);
|
|
277
|
+
const identity = evidenceIdentity(cuts);
|
|
278
|
+
const cached = scope.remainderHit(job, identity);
|
|
279
|
+
if (cached !== null) return { kind: "cache", decision: cached, attempt };
|
|
280
|
+
const soft = job.judgeKind === "soft-deny";
|
|
281
|
+
const resolver = job.resolution?.resolver;
|
|
282
|
+
const params = resolver?.kind === "judge" ? resolver.params : {};
|
|
283
|
+
// No user words, no explicit request: nothing to ask a model.
|
|
284
|
+
if (soft && (cuts.userMessages ?? []).length === 0) {
|
|
285
|
+
return { kind: "verdict", decision: "deny", reason: "no user message asks for this", attempt };
|
|
286
|
+
}
|
|
287
|
+
const read = programReads.get(source);
|
|
288
|
+
if (read === undefined) return { kind: "unread", source, attempt };
|
|
289
|
+
if (read.state === "forms") {
|
|
290
|
+
const decision = literalOf(read.forms);
|
|
291
|
+
// A literal program is the ask judge's shortcut. It is never a soft-deny license: the host must
|
|
292
|
+
// see the user's words, so only a literal deny applies here.
|
|
293
|
+
if (decision !== undefined && (!soft || decision === "deny"))
|
|
294
|
+
return { kind: "literal", decision, identity, attempt };
|
|
295
|
+
if (decision !== undefined) return { kind: "noprogram", attempt };
|
|
296
|
+
// No forms is the empty result exec returns. A top-level define is #<void>, not empty.
|
|
297
|
+
if (read.forms.length === 0) return { kind: "empty", attempt };
|
|
298
|
+
}
|
|
299
|
+
if (scope.schemaHaltKeys.has(route.key)) return { kind: "halt", attempt };
|
|
300
|
+
if (route.connection === "" || route.model === "") return { kind: "noconnect", attempt };
|
|
301
|
+
if (source.trim() === "") return { kind: "empty", attempt };
|
|
302
|
+
const base = {
|
|
303
|
+
kind: "run" as const,
|
|
304
|
+
source,
|
|
305
|
+
connection: route.connection,
|
|
306
|
+
model: route.model,
|
|
307
|
+
attempt,
|
|
308
|
+
identity,
|
|
309
|
+
privileged: cuts.privileged,
|
|
310
|
+
priorTurn: cuts.priorTurn,
|
|
311
|
+
quarantined: cuts.quarantined,
|
|
312
|
+
policy: cuts.policy ?? "",
|
|
313
|
+
margin: params.margin ?? 0,
|
|
314
|
+
};
|
|
315
|
+
if (!soft) return base;
|
|
316
|
+
const layer = job.resolution?.layer;
|
|
317
|
+
const rounds = (params.rounds ?? 2) as 1 | 2;
|
|
318
|
+
const unattended = job.unattended;
|
|
319
|
+
const userMessages = cuts.userMessages ?? [];
|
|
320
|
+
const agentAnswer = cuts.agentAnswer ?? "";
|
|
321
|
+
const votes = job.policyVotes as unknown as Parameters<typeof identityTokens>[0]["votes"];
|
|
322
|
+
const cluster = clusterOf(job.name);
|
|
323
|
+
const sections = buildSoftDenySections({
|
|
324
|
+
classes: job.policyClasses as Parameters<typeof buildSoftDenySections>[0]["classes"],
|
|
325
|
+
votes,
|
|
326
|
+
userMessages,
|
|
327
|
+
agentAnswer,
|
|
328
|
+
allowAgentAnswer: rounds === 2 && !unattended,
|
|
329
|
+
tool: job.name,
|
|
330
|
+
cluster,
|
|
331
|
+
args: job.args,
|
|
332
|
+
describe: ownerDescription(job),
|
|
333
|
+
});
|
|
334
|
+
const tokens = identityTokens({
|
|
335
|
+
cluster,
|
|
336
|
+
tool: job.name,
|
|
337
|
+
args: job.args as Readonly<Record<string, unknown>> | null,
|
|
338
|
+
votes,
|
|
339
|
+
serverToolName: job instanceof MCPToolJob ? job.serverToolName : undefined,
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
...base,
|
|
343
|
+
soft: {
|
|
344
|
+
sections,
|
|
345
|
+
rounds,
|
|
346
|
+
unattended,
|
|
347
|
+
hasAgentAnswer: sections.agentAnswerJson !== "",
|
|
348
|
+
callTier: "soft-deny",
|
|
349
|
+
callClasses: job.policyClasses.join(","),
|
|
350
|
+
callRules: votes.map((vote) => vote.ruleId).join(","),
|
|
351
|
+
check: {
|
|
352
|
+
userMessages,
|
|
353
|
+
agentAnswer,
|
|
354
|
+
identityTokens: tokens,
|
|
355
|
+
// Only a layer the human wrote may turn the quote check off.
|
|
356
|
+
requireQuote: !(
|
|
357
|
+
params.requireQuote === false &&
|
|
358
|
+
(layer === "user" || layer === "session" || layer === "managed")
|
|
359
|
+
),
|
|
360
|
+
margin: params.margin ?? 0,
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* One bypass-check attempt: the program's `bypass/judge` sink, applied once, under the per-attempt
|
|
368
|
+
* deadline. Throws on any failure (no connection, no sink, timeout, a crash); the caller decides what
|
|
369
|
+
* a failed check means. The parent job is the denied call that triggered the check, which is still open.
|
|
370
|
+
*/
|
|
371
|
+
export async function runBypassJudge(
|
|
372
|
+
scope: LicenseScope,
|
|
373
|
+
job: ToolCallJob,
|
|
374
|
+
sections: BypassSections,
|
|
375
|
+
): Promise<unknown> {
|
|
376
|
+
const source = scope.root.judgeAskProgram;
|
|
377
|
+
const route = classifierRoute(scope.root.kvs, {
|
|
378
|
+
fallback: scope.judgeFallbackKvs,
|
|
379
|
+
configuredModel: scope.judgeConfiguredModel,
|
|
380
|
+
});
|
|
381
|
+
if (route.connection === "" || route.model === "") throw new Error("no model connection");
|
|
382
|
+
if (scope.schemaHaltKeys.has(route.key)) throw new Error("json_schema unsupported");
|
|
383
|
+
if (source.trim() === "") throw new Error("auto-mode classifier program is empty");
|
|
384
|
+
const attemptMs = scope.judgeDeadlineMs(0);
|
|
385
|
+
const abort = new AbortController();
|
|
386
|
+
const deadline = setTimeout(() => abort.abort(new Error(`bypass check timed out after ${attemptMs}ms`)), attemptMs);
|
|
387
|
+
deadline.unref();
|
|
388
|
+
try {
|
|
389
|
+
const cuts = evidenceCuts(job);
|
|
390
|
+
const state = await execState(source, {
|
|
391
|
+
capabilities: [policyEnvCapability, promptIsolatedCapability],
|
|
392
|
+
signal: abort.signal,
|
|
393
|
+
config: {
|
|
394
|
+
privileged: cuts.privileged,
|
|
395
|
+
taskJson: sections.taskJson,
|
|
396
|
+
restrictionsJson: sections.restrictionsJson,
|
|
397
|
+
callsJson: sections.callsJson,
|
|
398
|
+
hintsJson: sections.hintsJson,
|
|
399
|
+
notesJson: sections.notesJson,
|
|
400
|
+
loader: classifierFiles,
|
|
401
|
+
job,
|
|
402
|
+
connectionId: route.connection,
|
|
403
|
+
completionModel: route.model,
|
|
404
|
+
complete: scope.complete,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
try {
|
|
408
|
+
const fn = classifierResources(state.runCtx).bypass;
|
|
409
|
+
if (fn === undefined) throw new Error("no bypass/judge sink in the classifier program");
|
|
410
|
+
return await applyClassifier(state.runCtx, fn, []);
|
|
411
|
+
} finally {
|
|
412
|
+
await disposeRunContext(state.runCtx);
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
416
|
+
if (isJsonSchemaUnsupported(text)) scope.schemaHaltKeys.add(route.key);
|
|
417
|
+
throw error;
|
|
418
|
+
} finally {
|
|
419
|
+
clearTimeout(deadline);
|
|
420
|
+
if (!abort.signal.aborted) abort.abort();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export class ClassifierHandle {
|
|
425
|
+
@observable accessor attempt = 0;
|
|
426
|
+
#timer: ReturnType<typeof setTimeout> | undefined;
|
|
427
|
+
#attemptAbort: AbortController | undefined;
|
|
428
|
+
#stop: IReactionDisposer | undefined;
|
|
429
|
+
#seq = 0;
|
|
430
|
+
|
|
431
|
+
constructor(
|
|
432
|
+
readonly job: ToolCallJob,
|
|
433
|
+
readonly scope: LicenseScope,
|
|
434
|
+
) {}
|
|
435
|
+
|
|
436
|
+
async #classifierApplyArgs(): Promise<unknown[] | undefined> {
|
|
437
|
+
const args = classifierArgs(this.job.name, this.job.args);
|
|
438
|
+
if (args === undefined) return undefined;
|
|
439
|
+
const cluster = clusterOf(this.job.name);
|
|
440
|
+
if (cluster === "write") {
|
|
441
|
+
const question = fileQuestion(this.job.name, this.job.args ?? undefined);
|
|
442
|
+
if (question == null) return undefined;
|
|
443
|
+
const source =
|
|
444
|
+
this.scope.root.policyProgram.trim() === "" ? this.scope.distSource() : this.scope.root.policyProgram;
|
|
445
|
+
const policy = await this.scope.compiledPolicy(source);
|
|
446
|
+
// A floor deny names its remainder kind; an ordinary in-tree ask is judged on the task.
|
|
447
|
+
const kind =
|
|
448
|
+
policy.writeKind(workspacePath(this.scope.root.path, question)) ??
|
|
449
|
+
(this.job.policyResult === "ask" ? "workspace" : "outside");
|
|
450
|
+
return [...args, kind];
|
|
451
|
+
}
|
|
452
|
+
if (cluster === "mcp") {
|
|
453
|
+
const describe = ownerDescription(this.job);
|
|
454
|
+
if (describe === undefined) return args;
|
|
455
|
+
// The program appends the second string under the operation: a labelled owner note, JSON-encoded.
|
|
456
|
+
return [
|
|
457
|
+
args[0],
|
|
458
|
+
`${String(args[1])}\nOWNER NOTE (written by the policy owner, not the agent): ${JSON.stringify(describe)}`,
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
if (cluster === "bash") {
|
|
462
|
+
const report = parseTirithEvidence(this.job.policyEvidenceJson);
|
|
463
|
+
if (report != null && tirithCoverage(report) === "incomplete") return [...args, "incomplete"];
|
|
464
|
+
}
|
|
465
|
+
return args;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
@action
|
|
469
|
+
start(): void {
|
|
470
|
+
if (this.#stop !== undefined || this.scope.closed) return;
|
|
471
|
+
this.#stop = reaction(
|
|
472
|
+
(): ClassifierPlan => remainderPlan(this.scope, this.job, this.attempt),
|
|
473
|
+
async (plan) => {
|
|
474
|
+
this.#clearTimer();
|
|
475
|
+
const seq = ++this.#seq;
|
|
476
|
+
const jobId = this.job.toolCallId || this.job.name;
|
|
477
|
+
trace(
|
|
478
|
+
"harness:judge",
|
|
479
|
+
`plan ${plan.kind} job=${jobId} tool=${this.job.name} attempt=${plan.attempt}${plan.kind === "run" ? ` model=${plan.model}` : ""}`,
|
|
480
|
+
);
|
|
481
|
+
switch (plan.kind) {
|
|
482
|
+
case "cache":
|
|
483
|
+
runInAction(() => {
|
|
484
|
+
if (this.job.classifierResult !== null) return;
|
|
485
|
+
this.job.classifierResult = plan.decision;
|
|
486
|
+
if (plan.decision === "deny" && this.job.judgeKind === "soft-deny") {
|
|
487
|
+
this.job.judgeReason = "already denied this turn";
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
case "verdict":
|
|
492
|
+
runInAction(() => {
|
|
493
|
+
if (this.job.classifierResult !== null) return;
|
|
494
|
+
this.job.classifierResult = plan.decision;
|
|
495
|
+
this.job.judgeReason = plan.reason;
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
case "noprogram":
|
|
499
|
+
this.writeFailSticky(
|
|
500
|
+
"auto-mode classifier program has no soft-deny sink (soft-deny/<cluster>); a person decides",
|
|
501
|
+
);
|
|
502
|
+
return;
|
|
503
|
+
case "literal":
|
|
504
|
+
runInAction(() => {
|
|
505
|
+
if (this.job.classifierResult !== null) return;
|
|
506
|
+
this.job.classifierResult = plan.decision;
|
|
507
|
+
this.scope.remainderStore(this.job, plan.identity, plan.decision);
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
case "halt":
|
|
511
|
+
this.writeFailSticky("json_schema unsupported: auto mode needs a model that supports structured output");
|
|
512
|
+
return;
|
|
513
|
+
case "noconnect":
|
|
514
|
+
this.writeFail("no model connection");
|
|
515
|
+
return;
|
|
516
|
+
case "empty":
|
|
517
|
+
// An empty program can never vote. Abstain so the call reaches the human
|
|
518
|
+
// cover (and the diagnostic is shown) instead of waiting forever.
|
|
519
|
+
this.writeFailSticky(
|
|
520
|
+
"auto-mode classifier program is empty or unreadable (check autoModeClassifier in config.json)",
|
|
521
|
+
);
|
|
522
|
+
return;
|
|
523
|
+
case "unread":
|
|
524
|
+
startProgramRead(plan.source);
|
|
525
|
+
return;
|
|
526
|
+
case "run":
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
const started = Date.now();
|
|
530
|
+
const attemptMs = this.scope.judgeDeadlineMs(plan.attempt);
|
|
531
|
+
this.#attemptAbort = new AbortController();
|
|
532
|
+
const attemptAbort = this.#attemptAbort;
|
|
533
|
+
const deadline = setTimeout(() => {
|
|
534
|
+
attemptAbort.abort(new Error(`classifier timed out after ${attemptMs}ms`));
|
|
535
|
+
}, attemptMs);
|
|
536
|
+
deadline.unref();
|
|
537
|
+
try {
|
|
538
|
+
let vote: ClassifierVote = { fail: "classifier did not vote" };
|
|
539
|
+
let state;
|
|
540
|
+
try {
|
|
541
|
+
state = await execState(plan.source, {
|
|
542
|
+
capabilities: [policyEnvCapability, promptIsolatedCapability],
|
|
543
|
+
signal: attemptAbort.signal,
|
|
544
|
+
config: {
|
|
545
|
+
privileged: plan.privileged,
|
|
546
|
+
priorTurn: plan.priorTurn,
|
|
547
|
+
quarantined: plan.quarantined,
|
|
548
|
+
policy: plan.policy,
|
|
549
|
+
riskMargin: plan.margin,
|
|
550
|
+
...(plan.soft === undefined
|
|
551
|
+
? {}
|
|
552
|
+
: {
|
|
553
|
+
floorJson: plan.soft.sections.floorJson,
|
|
554
|
+
userJson: plan.soft.sections.userJson,
|
|
555
|
+
callJson: plan.soft.sections.callJson,
|
|
556
|
+
agentAnswerJson: plan.soft.sections.agentAnswerJson,
|
|
557
|
+
callTier: plan.soft.callTier,
|
|
558
|
+
callClasses: plan.soft.callClasses,
|
|
559
|
+
callRules: plan.soft.callRules,
|
|
560
|
+
}),
|
|
561
|
+
loader: classifierFiles,
|
|
562
|
+
job: this.job,
|
|
563
|
+
connectionId: plan.connection,
|
|
564
|
+
completionModel: plan.model,
|
|
565
|
+
complete: this.scope.complete,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
} catch (error) {
|
|
569
|
+
vote = { fail: error instanceof Error ? error.message : String(error) };
|
|
570
|
+
}
|
|
571
|
+
if (state !== undefined) {
|
|
572
|
+
try {
|
|
573
|
+
const cluster = clusterOf(this.job.name);
|
|
574
|
+
if (plan.soft !== undefined) {
|
|
575
|
+
vote = await this.#softDeny(state.runCtx, cluster, plan.soft);
|
|
576
|
+
} else {
|
|
577
|
+
const fn = classifierResources(state.runCtx)[cluster];
|
|
578
|
+
if (fn === undefined) {
|
|
579
|
+
vote = { fail: `no classifier/${cluster}` };
|
|
580
|
+
} else {
|
|
581
|
+
const args = await this.#classifierApplyArgs();
|
|
582
|
+
vote =
|
|
583
|
+
args === undefined
|
|
584
|
+
? { fail: "classifier did not vote" }
|
|
585
|
+
: voteOf(await applyClassifier(state.runCtx, fn, args));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} finally {
|
|
589
|
+
await disposeRunContext(state.runCtx);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (seq !== this.#seq) {
|
|
593
|
+
trace("harness:judge", `stale job=${jobId} ms=${Date.now() - started}`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
runInAction(() => {
|
|
597
|
+
if (this.job.classifierResult !== null) return;
|
|
598
|
+
if ("fail" in vote) {
|
|
599
|
+
trace("harness:judge", `fail job=${jobId} ms=${Date.now() - started} ${vote.fail}`);
|
|
600
|
+
if (isJsonSchemaUnsupported(vote.fail)) {
|
|
601
|
+
this.scope.schemaHaltKeys.add(`${plan.connection}\n${plan.model}`);
|
|
602
|
+
this.writeFailSticky(vote.fail);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
this.writeFail(vote.fail);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
trace("harness:judge", `vote ${vote.decision} job=${jobId} ms=${Date.now() - started}`);
|
|
609
|
+
this.job.classifierResult = vote.decision;
|
|
610
|
+
if (vote.reason !== undefined) this.job.judgeReason = vote.reason;
|
|
611
|
+
if (vote.round !== undefined) this.job.softDenyRound = vote.round;
|
|
612
|
+
this.scope.remainderStore(this.job, plan.identity, vote.decision);
|
|
613
|
+
});
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (seq !== this.#seq) return;
|
|
616
|
+
trace(
|
|
617
|
+
"harness:judge",
|
|
618
|
+
`throw job=${jobId} ms=${Date.now() - started} ${error instanceof Error ? error.message : String(error)}`,
|
|
619
|
+
);
|
|
620
|
+
runInAction(() => {
|
|
621
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
622
|
+
if (isJsonSchemaUnsupported(text)) {
|
|
623
|
+
this.scope.schemaHaltKeys.add(`${plan.connection}\n${plan.model}`);
|
|
624
|
+
this.writeFailSticky(text);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this.writeFail(text);
|
|
628
|
+
});
|
|
629
|
+
} finally {
|
|
630
|
+
clearTimeout(deadline);
|
|
631
|
+
if (!attemptAbort.signal.aborted) attemptAbort.abort();
|
|
632
|
+
if (this.#attemptAbort === attemptAbort) this.#attemptAbort = undefined;
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
{ equals: comparer.structural, fireImmediately: true },
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
dispose(): void {
|
|
640
|
+
this.#seq += 1;
|
|
641
|
+
this.#clearTimer();
|
|
642
|
+
this.#attemptAbort?.abort();
|
|
643
|
+
this.#attemptAbort = undefined;
|
|
644
|
+
this.#stop?.();
|
|
645
|
+
this.#stop = undefined;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* The soft-deny rounds. The program's sink is applied with the round; the host parses and verifies
|
|
650
|
+
* each vote (quote, identity of the action, risk x authorization). Round 2 runs only when round 1
|
|
651
|
+
* asked for the agent's last answer, the cell allows two rounds, one exists, and a person is watching.
|
|
652
|
+
*/
|
|
653
|
+
async #softDeny(
|
|
654
|
+
runCtx: RunContext,
|
|
655
|
+
cluster: ReturnType<typeof clusterOf>,
|
|
656
|
+
plan: SoftDenyPlan,
|
|
657
|
+
): Promise<ClassifierVote> {
|
|
658
|
+
const fn = classifierResources(runCtx).softDeny[cluster];
|
|
659
|
+
if (fn === undefined) return { fail: `no soft-deny/${cluster}` };
|
|
660
|
+
const first = parseSoftDenyVote(await this.#applySoft(runCtx, fn, 1), 1, plan.check);
|
|
661
|
+
const settle = (outcome: SoftDenyOutcome, round: 1 | 2): ClassifierVote => {
|
|
662
|
+
if (outcome.kind === "fail") return { fail: outcome.reason };
|
|
663
|
+
if (outcome.kind === "need-agent-answer") return { decision: "deny", reason: outcome.reason, round };
|
|
664
|
+
return { decision: outcome.decision, reason: outcome.reason, round: outcome.round };
|
|
665
|
+
};
|
|
666
|
+
if (first.kind !== "need-agent-answer") return settle(first, 1);
|
|
667
|
+
if (plan.rounds === 1 || plan.unattended || !plan.hasAgentAnswer) {
|
|
668
|
+
return {
|
|
669
|
+
decision: "deny",
|
|
670
|
+
reason: plan.hasAgentAnswer ? first.reason : "confirmation with no proposal to refer to",
|
|
671
|
+
round: 1,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return settle(parseSoftDenyVote(await this.#applySoft(runCtx, fn, 2), 2, plan.check), 2);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async #applySoft(runCtx: RunContext, fn: Parameters<typeof applyCallback>[0], round: 1 | 2): Promise<unknown> {
|
|
678
|
+
return await applyClassifier(runCtx, fn, [round]);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
@action
|
|
682
|
+
writeFail(text: string): void {
|
|
683
|
+
if (this.job.classifierResult !== null) return;
|
|
684
|
+
if (text.trim() !== "") this.job.classifierAbstain = text;
|
|
685
|
+
this.job.judgeFails += 1;
|
|
686
|
+
if (this.scope.closed) return;
|
|
687
|
+
this.#clearTimer();
|
|
688
|
+
const n = this.attempt + 1;
|
|
689
|
+
this.#timer = setTimeout(
|
|
690
|
+
action("judgeBackoff", () => {
|
|
691
|
+
this.#timer = undefined;
|
|
692
|
+
this.attempt = n;
|
|
693
|
+
}),
|
|
694
|
+
this.scope.backoffMs(n),
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
@action
|
|
699
|
+
writeFailSticky(text: string): void {
|
|
700
|
+
if (this.job.classifierResult !== null) return;
|
|
701
|
+
if (text.trim() !== "") this.job.classifierAbstain = text;
|
|
702
|
+
this.job.judgeFails = JUDGE_FAILS_FINAL;
|
|
703
|
+
this.#clearTimer();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
#clearTimer(): void {
|
|
707
|
+
if (this.#timer === undefined) return;
|
|
708
|
+
clearTimeout(this.#timer);
|
|
709
|
+
this.#timer = undefined;
|
|
710
|
+
}
|
|
711
|
+
}
|