@sema-agent/core 7.17.7 → 7.19.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/CHANGELOG.md +233 -13
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/agents/teacher.js +1 -0
- package/dist/agents/verify.js +3 -0
- package/dist/brain/anthropic.js +6 -3
- package/dist/brain/open-responses.js +10 -4
- package/dist/brain/openai.js +5 -2
- package/dist/brain/reasoning.d.ts +13 -13
- package/dist/brain/reasoning.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +18 -0
- package/dist/brain/route-adjudicator.js +16 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/brain/stream-shared.d.ts +3 -0
- package/dist/brain/stream-shared.js +1 -0
- package/dist/core/agent-definition.d.ts +10 -0
- package/dist/core/artifact-host.d.ts +41 -4
- package/dist/core/artifact-host.js +18 -2
- package/dist/core/ask-origin.d.ts +16 -0
- package/dist/core/ask-origin.js +8 -0
- package/dist/core/auto-mode-arming.d.ts +6 -0
- package/dist/core/auto-mode-arming.js +5 -5
- package/dist/core/auto-mode-defaults.d.ts +43 -15
- package/dist/core/auto-mode-defaults.js +4 -1
- package/dist/core/auto-mode.d.ts +184 -59
- package/dist/core/auto-mode.js +188 -13
- package/dist/core/checkpoint-store.d.ts +23 -2
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/engine-notice.d.ts +77 -1
- package/dist/core/engine-notice.js +7 -0
- package/dist/core/gate-fold.js +5 -1
- package/dist/core/gate-lanes.js +9 -8
- package/dist/core/gate-outcome.d.ts +42 -2
- package/dist/core/gate-outcome.js +23 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hook-seats.d.ts +66 -0
- package/dist/core/hook-seats.js +37 -0
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/read-root-candidate.d.ts +12 -0
- package/dist/core/read-root-candidate.js +24 -0
- package/dist/core/roles.d.ts +9 -1
- package/dist/core/roles.js +4 -0
- package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
- package/dist/core/runner/assemble-result.d.ts +2 -1
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/classifier-ladder.d.ts +227 -0
- package/dist/core/runner/classifier-ladder.js +214 -0
- package/dist/core/runner/contracts.d.ts +71 -13
- package/dist/core/runner/decide-continuation.d.ts +1 -3
- package/dist/core/runner/decide-continuation.js +1 -7
- package/dist/core/runner/deferral-census.d.ts +52 -0
- package/dist/core/runner/deferral-census.js +44 -0
- package/dist/core/runner/denial-limit-arms.js +2 -2
- package/dist/core/runner/derived-route-fallback.d.ts +8 -4
- package/dist/core/runner/derived-route-fallback.js +8 -3
- package/dist/core/runner/gate-exit.d.ts +12 -0
- package/dist/core/runner/gate-exit.js +6 -3
- package/dist/core/runner/prepare-artifact.d.ts +10 -2
- package/dist/core/runner/prepare-artifact.js +5 -4
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
- package/dist/core/runner/prepare-config-doors.d.ts +7 -1
- package/dist/core/runner/prepare-config-doors.js +13 -13
- package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
- package/dist/core/runner/prepare-defer-classify.js +8 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
- package/dist/core/runner/prepare-hands-readface.js +60 -24
- package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
- package/dist/core/runner/prepare-inherited-gate.js +10 -2
- package/dist/core/runner/prepare-lsp.d.ts +2 -2
- package/dist/core/runner/prepare-lsp.js +1 -1
- package/dist/core/runner/prepare-policy-chain.js +2 -0
- package/dist/core/runner/prepare-project-context.d.ts +5 -1
- package/dist/core/runner/prepare-project-context.js +7 -4
- package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
- package/dist/core/runner/prepare-prompt-inputs.js +4 -1
- package/dist/core/runner/prepare-run-refs.d.ts +9 -1
- package/dist/core/runner/prepare-run-refs.js +3 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
- package/dist/core/runner/prepare-suspend-saga.js +5 -2
- package/dist/core/runner/prepare-task.js +19 -14
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
- package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
- package/dist/core/runner/prepare-turn-wiring.js +3 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
- package/dist/core/runner/prepare-wiring-manifest.js +5 -2
- package/dist/core/runner/resume-apply.js +3 -3
- package/dist/core/runner/resume-checkpoint-screen.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +4 -1
- package/dist/core/runner/run-identity-wiring.js +6 -2
- package/dist/core/runner/run-leg.d.ts +2 -0
- package/dist/core/runner/run-leg.js +45 -45
- package/dist/core/runner/run-notification-lane.js +55 -27
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +16 -13
- package/dist/core/runner/run-turn-boundary.js +15 -5
- package/dist/core/runner/runtask.d.ts +6 -5
- package/dist/core/runner/runtask.js +26 -14
- package/dist/core/runner/skill-base-dir.d.ts +54 -0
- package/dist/core/runner/skill-base-dir.js +42 -0
- package/dist/core/runner/synthetic-tools.d.ts +83 -48
- package/dist/core/runner/synthetic-tools.js +69 -32
- package/dist/core/runner/tool-defer-gate.d.ts +22 -9
- package/dist/core/runner/tool-defer-gate.js +15 -5
- package/dist/core/runner/tool-disclosure.d.ts +5 -0
- package/dist/core/runner/tool-disclosure.js +2 -0
- package/dist/core/runner/tool-progress-lane.d.ts +42 -0
- package/dist/core/runner/tool-progress-lane.js +61 -0
- package/dist/core/runner/turn-attachments.d.ts +43 -12
- package/dist/core/runner/turn-attachments.js +58 -14
- package/dist/core/runner-deps.d.ts +48 -7
- package/dist/core/sensitive-path-policy.d.ts +58 -17
- package/dist/core/sensitive-path-policy.js +33 -5
- package/dist/core/skill-listing-budget.d.ts +112 -0
- package/dist/core/skill-listing-budget.js +58 -0
- package/dist/core/swappable-deps.d.ts +9 -7
- package/dist/core/swappable-deps.js +7 -3
- package/dist/core/task-event.d.ts +96 -29
- package/dist/core/task-notification.d.ts +142 -16
- package/dist/core/task-notification.js +106 -0
- package/dist/core/task-result.d.ts +9 -1
- package/dist/core/task-spec.d.ts +46 -3
- package/dist/core/tighten-task-spec.js +7 -5
- package/dist/core/tool-model-gate.d.ts +1 -1
- package/dist/core/tool-policy.d.ts +19 -4
- package/dist/core/tool-policy.js +6 -7
- package/dist/core/tool-search-setting.d.ts +78 -0
- package/dist/core/tool-search-setting.js +90 -0
- package/dist/core/tool-spec.d.ts +45 -4
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +46 -0
- package/dist/core/wiring-manifest.js +9 -1
- package/dist/core/write-protect.d.ts +8 -3
- package/dist/core/write-protect.js +8 -5
- package/dist/engine/execution-env/node-execution-env.js +10 -4
- package/dist/engine/harness/agent-harness.d.ts +42 -0
- package/dist/engine/harness/agent-harness.js +61 -29
- package/dist/engine/loop/agent-loop.d.ts +68 -3
- package/dist/engine/loop/agent-loop.js +109 -16
- package/dist/index.d.ts +13 -8
- package/dist/index.js +11 -7
- package/dist/internal/harness.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +3 -0
- package/dist/orchestration/run-workflow-tool.js +6 -1
- package/dist/orchestration/workflow-governance.d.ts +7 -1
- package/dist/orchestration/workflow-governance.js +9 -5
- package/dist/orchestration/workflow-primitives.d.ts +3 -0
- package/dist/orchestration/workflow-primitives.js +6 -1
- package/dist/orchestration/workflow-types.d.ts +48 -13
- package/dist/orchestration/workflow-types.js +1 -1
- package/dist/orchestration/workflow.js +45 -17
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/section-usage.d.ts +26 -0
- package/dist/prompt-assembly/section-usage.js +10 -0
- package/dist/prompt-assembly/types.d.ts +8 -6
- package/dist/prompts/default.d.ts +13 -0
- package/dist/prompts/default.js +16 -8
- package/dist/tools/artifact/artifact-text.d.ts +10 -1
- package/dist/tools/artifact/artifact-text.js +6 -3
- package/dist/tools/artifact/artifact-tool.d.ts +12 -2
- package/dist/tools/artifact/artifact-tool.js +203 -70
- package/dist/tools/artifact/conformance.d.ts +56 -0
- package/dist/tools/artifact/conformance.js +349 -0
- package/dist/tools/artifact/local-stub.js +1 -1
- package/dist/tools/artifact/reject-breaker.d.ts +28 -0
- package/dist/tools/artifact/reject-breaker.js +42 -0
- package/dist/tools/fs/bash-lexer.d.ts +132 -0
- package/dist/tools/fs/bash-lexer.js +563 -0
- package/dist/tools/fs/bash-program-position.d.ts +142 -0
- package/dist/tools/fs/bash-program-position.js +125 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
- package/dist/tools/fs/bash-readonly-classifier.js +180 -558
- package/dist/tools/fs/containment-face.d.ts +89 -0
- package/dist/tools/fs/containment-face.js +81 -0
- package/dist/tools/fs/fs-bash.d.ts +46 -1
- package/dist/tools/fs/fs-bash.js +165 -25
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +24 -5
- package/dist/tools/fs/index.js +18 -6
- package/dist/tools/fs/read-deny.d.ts +4 -3
- package/dist/tools/fs/repo-map.d.ts +2 -2
- package/dist/tools/fs/safety.d.ts +18 -3
- package/dist/tools/fs/safety.js +11 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +99 -13
- package/wire-changes/core@7.17.7.json +49 -0
- package/wire-changes/core@7.18.0.json +1273 -0
- package/wire-changes/core@7.19.0.json +457 -0
- package/dist/tools/fs/read-face.d.ts +0 -68
- package/dist/tools/fs/read-face.js +0 -47
package/dist/core/auto-mode.js
CHANGED
|
@@ -1,29 +1,151 @@
|
|
|
1
1
|
import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS } from "./auto-mode-defaults.js";
|
|
2
|
+
import { describeRejectedValue } from "./tool-search-setting.js";
|
|
2
3
|
export const AUTO_MODE_UNAVAILABLE_CAUSES = ["error", "timeout"];
|
|
3
4
|
const AUTO_MODE_UNAVAILABLE_CAUSE_SET = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
|
|
4
5
|
export function isAutoModeUnavailableCause(v) {
|
|
5
6
|
return AUTO_MODE_UNAVAILABLE_CAUSE_SET.has(v);
|
|
6
7
|
}
|
|
7
|
-
export
|
|
8
|
+
export const AUTO_MODE_PARSE_FAILURES = ["missing", "conflict"];
|
|
9
|
+
const AUTO_MODE_PARSE_FAILURE_SET = new Set(AUTO_MODE_PARSE_FAILURES);
|
|
10
|
+
export function isAutoModeParseFailure(v) {
|
|
11
|
+
return AUTO_MODE_PARSE_FAILURE_SET.has(v);
|
|
12
|
+
}
|
|
13
|
+
export const AUTO_MODE_FALLBACK_CAUSES = ["error", "timeout", "parse_error"];
|
|
14
|
+
const AUTO_MODE_FALLBACK_CAUSE_SET = new Set(AUTO_MODE_FALLBACK_CAUSES);
|
|
15
|
+
export function isAutoModeFallbackCause(v) {
|
|
16
|
+
return AUTO_MODE_FALLBACK_CAUSE_SET.has(v);
|
|
17
|
+
}
|
|
18
|
+
export const AUTO_MODE_CLASSIFIER_FALLBACKS = ["main", "none"];
|
|
19
|
+
const AUTO_MODE_CLASSIFIER_FALLBACK_SET = new Set(AUTO_MODE_CLASSIFIER_FALLBACKS);
|
|
20
|
+
export function isAutoModeClassifierFallback(v) {
|
|
21
|
+
return AUTO_MODE_CLASSIFIER_FALLBACK_SET.has(v);
|
|
22
|
+
}
|
|
23
|
+
export function autoModeClassifierFallbackOf(v) {
|
|
24
|
+
if (v === undefined)
|
|
25
|
+
return "main";
|
|
26
|
+
if (isAutoModeClassifierFallback(v))
|
|
27
|
+
return v;
|
|
28
|
+
const e = new Error(`RunnerDeps.autoMode.classifierFallback must be one of ${AUTO_MODE_CLASSIFIER_FALLBACKS.map((x) => `"${x}"`).join(" | ")} ` +
|
|
29
|
+
`(got ${describeRejectedValue(v)}) — omit it for the default "main"`);
|
|
30
|
+
e.code = "config.auto_mode_classifier_fallback_invalid";
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
export const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked";
|
|
34
|
+
export function autoModeRoundNotAsked(reason) {
|
|
35
|
+
let detail;
|
|
36
|
+
try {
|
|
37
|
+
const m = reason.message;
|
|
38
|
+
if (typeof m === "string")
|
|
39
|
+
detail = m.slice(0, 200);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
}
|
|
43
|
+
const e = new Error(`the auto-mode classification could not be assembled: ${detail ?? describeRejectedValue(reason)}`, { cause: reason });
|
|
44
|
+
e.code = AUTO_MODE_ROUND_NOT_ASKED_CODE;
|
|
45
|
+
return e;
|
|
46
|
+
}
|
|
47
|
+
function isAutoModeRoundNotAsked(e) {
|
|
48
|
+
try {
|
|
49
|
+
return typeof e === "object" && e !== null && e.code === AUTO_MODE_ROUND_NOT_ASKED_CODE;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function rungDidNotRule(v) {
|
|
56
|
+
if (v.kind === "unavailable")
|
|
57
|
+
return v.cause;
|
|
58
|
+
if (v.kind === "parse_error")
|
|
59
|
+
return "parse_error";
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
|
|
8
63
|
const parenthetical = cause === "timeout" ? " (timed out)" : "";
|
|
64
|
+
const fb = attribution?.fallback;
|
|
65
|
+
const rootCause = fb === undefined ? "" : `The classifier model "${fb.from}" did not answer (${fb.cause}) and neither did the model it fell back to, "${fb.to}" (${cause}). `;
|
|
9
66
|
return (`The auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode cannot determine the safety of ${toolName} right now. ` +
|
|
67
|
+
rootCause +
|
|
10
68
|
"Wait a moment and then try this action again. " +
|
|
11
69
|
"If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
|
|
12
70
|
"Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
|
|
13
71
|
}
|
|
14
|
-
export
|
|
15
|
-
|
|
72
|
+
export function classifierParseFailureDenyMessage(attribution) {
|
|
73
|
+
const stem = "Auto mode could not evaluate this action and is blocking it for safety";
|
|
74
|
+
return attribution === undefined ? `${stem} — the classifier's reply carried no verdict.` : `${stem} — the classifier model "${attribution.modelUsed}" replied without a verdict.`;
|
|
75
|
+
}
|
|
76
|
+
function stripThinking(text) {
|
|
77
|
+
return text.replace(/<(thinking|think)>[\s\S]*?<\/\1>/gi, "").replace(/<(thinking|think)>[\s\S]*$/i, "");
|
|
78
|
+
}
|
|
79
|
+
const BARE_ANSWER_WRAPPERS = [
|
|
80
|
+
["**", "**"],
|
|
81
|
+
["__", "__"],
|
|
82
|
+
["*", "*"],
|
|
83
|
+
["_", "_"],
|
|
84
|
+
["`", "`"],
|
|
85
|
+
['"', '"'],
|
|
86
|
+
["'", "'"],
|
|
87
|
+
["«", "»"],
|
|
88
|
+
];
|
|
89
|
+
function unwrapBareAnswer(text) {
|
|
90
|
+
let s = text;
|
|
91
|
+
for (;;) {
|
|
92
|
+
const before = s;
|
|
93
|
+
s = s.trim().replace(/[.!]+$/, "").trim();
|
|
94
|
+
for (const [open, close] of BARE_ANSWER_WRAPPERS) {
|
|
95
|
+
if (s.length > open.length + close.length && s.startsWith(open) && s.endsWith(close)) {
|
|
96
|
+
s = s.slice(open.length, s.length - close.length);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (s === before)
|
|
101
|
+
return s;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function bareAnswerOf(text) {
|
|
105
|
+
const visible = text.replace(/[\u200b-\u200d\ufeff]/g, "").trim();
|
|
106
|
+
if (visible.includes("```") || visible.includes("\n"))
|
|
107
|
+
return undefined;
|
|
108
|
+
let s = unwrapBareAnswer(visible);
|
|
109
|
+
const labelled = labelledAnswer(s);
|
|
110
|
+
if (labelled !== undefined)
|
|
111
|
+
s = unwrapBareAnswer(labelled);
|
|
112
|
+
const word = /^(yes|no)$/i.exec(s)?.[1]?.toLowerCase();
|
|
113
|
+
return word === "yes" || word === "no" ? word : undefined;
|
|
114
|
+
}
|
|
115
|
+
function labelledAnswer(s) {
|
|
116
|
+
const label = "(?:final[ \\t]+)?(?:block|answer|verdict|decision)";
|
|
117
|
+
for (const [open, close] of [["", ""], ...BARE_ANSWER_WRAPPERS]) {
|
|
118
|
+
const re = new RegExp(`^${escapeRe(open)}${label}${escapeRe(close)}[ \\t]*[:=][ \\t]*(.*)$`, "i");
|
|
119
|
+
const m = re.exec(s);
|
|
120
|
+
if (m !== null)
|
|
121
|
+
return m[1];
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function escapeRe(s) {
|
|
126
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
127
|
+
}
|
|
128
|
+
export function parseAutoModeResponse(text, options) {
|
|
16
129
|
const rawAnswers = new Set([...text.matchAll(/<block>(yes|no)\b/gi)].map((m) => m[1].toLowerCase()));
|
|
17
130
|
if (rawAnswers.size > 1)
|
|
18
|
-
return { kind: "parse_error", raw: text };
|
|
19
|
-
const stripped = text
|
|
131
|
+
return { kind: "parse_error", raw: text, form: "conflict" };
|
|
132
|
+
const stripped = stripThinking(text);
|
|
20
133
|
const hits = [...stripped.matchAll(/<block>(yes|no)\b(<\/block>)?/gi)];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
134
|
+
let answer;
|
|
135
|
+
if (hits.length > 0) {
|
|
136
|
+
const answers = new Set(hits.map((m) => m[1].toLowerCase()));
|
|
137
|
+
if (answers.size !== 1)
|
|
138
|
+
return { kind: "parse_error", raw: text, form: "conflict" };
|
|
139
|
+
answer = answers.has("yes") ? "yes" : "no";
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
answer = options?.bareAnswer === false ? undefined : bareAnswerOf(stripped);
|
|
143
|
+
if (answer === undefined)
|
|
144
|
+
return { kind: "parse_error", raw: text, form: "missing" };
|
|
145
|
+
if (rawAnswers.size > 0 && !rawAnswers.has(answer))
|
|
146
|
+
return { kind: "parse_error", raw: text, form: "conflict" };
|
|
147
|
+
}
|
|
148
|
+
if (answer === "no")
|
|
27
149
|
return { kind: "allow" };
|
|
28
150
|
const category = /<category>([\s\S]*?)<\/category>/i.exec(stripped)?.[1]?.trim() ?? "";
|
|
29
151
|
const reason = /<reason>([\s\S]*?)<\/reason>/.exec(stripped)?.[1]?.trim() ?? "";
|
|
@@ -31,6 +153,15 @@ export function parseAutoModeResponse(text) {
|
|
|
31
153
|
}
|
|
32
154
|
export function createAutoModeDecider(opts) {
|
|
33
155
|
const timeoutMs = opts.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS;
|
|
156
|
+
if (opts.candidates !== undefined) {
|
|
157
|
+
if (opts.candidates.length === 0) {
|
|
158
|
+
throw new Error("AutoModeDeciderOptions.candidates must name at least one model — omit the member for the leg's own model, never an empty ladder");
|
|
159
|
+
}
|
|
160
|
+
const bad = opts.candidates.findIndex((m) => typeof m !== "string" || m.trim() === "");
|
|
161
|
+
if (bad >= 0)
|
|
162
|
+
throw new Error(`AutoModeDeciderOptions.candidates[${bad}] must be a non-empty model id (got ${JSON.stringify(opts.candidates[bad])})`);
|
|
163
|
+
}
|
|
164
|
+
const ladder = opts.candidates ?? [undefined];
|
|
34
165
|
const decide = async (input, signal) => {
|
|
35
166
|
const startedAt = performance.now();
|
|
36
167
|
const verdict = await decideUnreported(input, signal);
|
|
@@ -41,6 +172,7 @@ export function createAutoModeDecider(opts) {
|
|
|
41
172
|
ms: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
42
173
|
verdict: verdict.kind,
|
|
43
174
|
...(verdict.kind === "unavailable" ? { cause: verdict.cause } : {}),
|
|
175
|
+
...(verdict.classifier !== undefined ? { classifier: verdict.classifier } : {}),
|
|
44
176
|
});
|
|
45
177
|
}
|
|
46
178
|
catch {
|
|
@@ -50,6 +182,37 @@ export function createAutoModeDecider(opts) {
|
|
|
50
182
|
};
|
|
51
183
|
return { decide };
|
|
52
184
|
async function decideUnreported(input, signal) {
|
|
185
|
+
const causes = [];
|
|
186
|
+
const round = { notAsked: false };
|
|
187
|
+
let verdict = { kind: "unavailable", cause: "error" };
|
|
188
|
+
let i = 0;
|
|
189
|
+
for (; i < ladder.length; i++) {
|
|
190
|
+
const name = ladder[i];
|
|
191
|
+
verdict = await askRung(input, signal, name !== undefined ? { index: i, model: name } : undefined, round);
|
|
192
|
+
const cause = rungDidNotRule(verdict);
|
|
193
|
+
if (cause === undefined)
|
|
194
|
+
break;
|
|
195
|
+
causes.push(cause);
|
|
196
|
+
if (round.notAsked || signal?.aborted)
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const landed = round.notAsked ? 0 : Math.min(i, ladder.length - 1);
|
|
200
|
+
const requested = ladder[0];
|
|
201
|
+
const used = ladder[landed];
|
|
202
|
+
if (requested === undefined || used === undefined)
|
|
203
|
+
return verdict;
|
|
204
|
+
const from = landed > 0 ? ladder[landed - 1] : undefined;
|
|
205
|
+
const movedBy = landed > 0 ? causes[landed - 1] : undefined;
|
|
206
|
+
return {
|
|
207
|
+
...verdict,
|
|
208
|
+
classifier: Object.freeze({
|
|
209
|
+
modelRequested: requested,
|
|
210
|
+
modelUsed: used,
|
|
211
|
+
...(from !== undefined && movedBy !== undefined ? { fallback: Object.freeze({ from, to: used, cause: movedBy }) } : {}),
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async function askRung(input, signal, rung, round) {
|
|
53
216
|
let timer;
|
|
54
217
|
const inner = new AbortController();
|
|
55
218
|
const onOuterAbort = () => inner.abort();
|
|
@@ -69,7 +232,7 @@ export function createAutoModeDecider(opts) {
|
|
|
69
232
|
reject(new AutoModeTimeout());
|
|
70
233
|
}
|
|
71
234
|
}, timeoutMs);
|
|
72
|
-
opts.classify(input, inner.signal).then((v) => {
|
|
235
|
+
opts.classify(input, inner.signal, rung).then((v) => {
|
|
73
236
|
if (!settled) {
|
|
74
237
|
settled = true;
|
|
75
238
|
resolve(v);
|
|
@@ -84,9 +247,13 @@ export function createAutoModeDecider(opts) {
|
|
|
84
247
|
return parseAutoModeResponse(raw);
|
|
85
248
|
}
|
|
86
249
|
catch (e) {
|
|
250
|
+
if (isAutoModeRoundNotAsked(e)) {
|
|
251
|
+
round.notAsked = true;
|
|
252
|
+
return { kind: "unavailable", cause: "error" };
|
|
253
|
+
}
|
|
87
254
|
if (signal?.aborted)
|
|
88
255
|
return { kind: "unavailable", cause: "error" };
|
|
89
|
-
return { kind: "unavailable", cause: e
|
|
256
|
+
return { kind: "unavailable", cause: isAutoModeTimeout(e) ? "timeout" : "error" };
|
|
90
257
|
}
|
|
91
258
|
finally {
|
|
92
259
|
if (timer !== undefined)
|
|
@@ -101,6 +268,14 @@ class AutoModeTimeout extends Error {
|
|
|
101
268
|
super("auto-mode classify timeout");
|
|
102
269
|
}
|
|
103
270
|
}
|
|
271
|
+
function isAutoModeTimeout(e) {
|
|
272
|
+
try {
|
|
273
|
+
return e instanceof AutoModeTimeout;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
104
279
|
export function readDenialLimitFallback(v) {
|
|
105
280
|
if (typeof v !== "object" || v === null)
|
|
106
281
|
return undefined;
|
|
@@ -369,6 +369,13 @@ export interface PendingSteerInput {
|
|
|
369
369
|
inputId?: string;
|
|
370
370
|
priority?: SystemInjectionPriority;
|
|
371
371
|
}
|
|
372
|
+
/** True ONLY for a PLAIN data record (not null / array / class-instance / proxy-with-odd-prototype) — the only
|
|
373
|
+
* shape we safely digest. Adversarial args (a proxy, a getter that throws) must never crash the mint, so this +
|
|
374
|
+
* {@link safeDataValue}/{@link safeKeys} make {@link buildRiskDescriptor} fully TOTAL. */
|
|
375
|
+
export declare function isPlainRecord(x: unknown): x is Record<string, unknown>;
|
|
376
|
+
/** Read `obj[key]` as a DATA property only — NEVER invokes a getter/accessor (so a throwing getter can't escape
|
|
377
|
+
* AND a non-deterministic getter can't vary the digest); a trapped descriptor read ⇒ undefined. */
|
|
378
|
+
export declare function safeDataValue(obj: Record<string, unknown>, key: string): unknown;
|
|
372
379
|
/**
|
|
373
380
|
* design/80 §D-E: build the DETERMINISTIC, REDACTED {@link RiskDescriptor} for an escalation checkpoint at
|
|
374
381
|
* MINT. Pure — a function ONLY of (`toolName`, `args`, the D-2 `safety` axis, the `shellGated` flag); it reads
|
|
@@ -969,6 +976,16 @@ export interface CheckpointState {
|
|
|
969
976
|
* scope for any in-row invariant). Absent = the row was minted ungoverned. */
|
|
970
977
|
realApproval?: true;
|
|
971
978
|
};
|
|
979
|
+
/**
|
|
980
|
+
* The WRITE-face containment state at suspend: the RESOLVED write face, written ONLY when it is
|
|
981
|
+
* non-default ("open"). An absent section IS "roots" for every reader — which is why it carries NO
|
|
982
|
+
* version stamp: losing it can only make a resume STRICTER. Resume folds STRICTER-WINS, the read seat's
|
|
983
|
+
* law: roots unless the section says open AND the live resolution says open. No deny half and no
|
|
984
|
+
* governance marker.
|
|
985
|
+
*/
|
|
986
|
+
writeFace?: {
|
|
987
|
+
face: "open" | "roots";
|
|
988
|
+
};
|
|
972
989
|
/**
|
|
973
990
|
* design/319 (A ticket) — the session's reminder provenance mark
|
|
974
991
|
* ({@link import("./reminder-mint.js").mintReminderMark}), persisted so a durable resume keeps
|
|
@@ -1961,10 +1978,14 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
|
|
|
1961
1978
|
* parks a steer in the retired pre-queue single seat — terminal for the row's bytes; no worker version
|
|
1962
1979
|
* reads that seat, the host decides the parked instruction itself).
|
|
1963
1980
|
* · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`, `"real_approval_forged"`,
|
|
1964
|
-
* `"read_face_damaged"`, `"read_face_forged"`, `"constraint_chain_missing"` (all terminal for the row's
|
|
1981
|
+
* `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
|
|
1965
1982
|
* current bytes — no worker version redeems a damaged row).
|
|
1966
1983
|
*/
|
|
1967
|
-
export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged"
|
|
1984
|
+
export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged"
|
|
1985
|
+
/** `state.writeFace` is present but not a well-formed section. No `_forged` twin: the section is
|
|
1986
|
+
* legal-and-absent at every version, so there is no version to judge a fabrication against — and a
|
|
1987
|
+
* fabricated "open" still needs the LIVE resolution to say open too. */
|
|
1988
|
+
| "write_face_damaged" | "constraint_chain_missing";
|
|
1968
1989
|
/** A typed checkpoint-store error so callers branch on `code` (mirrors `SessionError`). */
|
|
1969
1990
|
export declare class CheckpointError extends Error {
|
|
1970
1991
|
readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
|
|
@@ -73,7 +73,7 @@ export const MAX_PENDING_STEER_CHARS = 16_000;
|
|
|
73
73
|
export const PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48_000;
|
|
74
74
|
export const MAX_PENDING_STEER_ENTRIES = Math.floor(PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES / MAX_PENDING_STEER_CHARS);
|
|
75
75
|
export const PENDING_STEER_FROZEN_FIELDS = ["text", "trusted", "actor", "seq", "inputId", "priority"];
|
|
76
|
-
function isPlainRecord(x) {
|
|
76
|
+
export function isPlainRecord(x) {
|
|
77
77
|
try {
|
|
78
78
|
if (x === null || typeof x !== "object" || Array.isArray(x))
|
|
79
79
|
return false;
|
|
@@ -84,7 +84,7 @@ function isPlainRecord(x) {
|
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
function safeDataValue(obj, key) {
|
|
87
|
+
export function safeDataValue(obj, key) {
|
|
88
88
|
try {
|
|
89
89
|
const d = Object.getOwnPropertyDescriptor(obj, key);
|
|
90
90
|
return d && "value" in d ? d.value : undefined;
|
|
@@ -23,6 +23,22 @@ export interface EngineNotice {
|
|
|
23
23
|
* - `"config.tool_face_undeclared"` / `"config.tool_face_invalid"` (design/388 §2.7) — an MCP server's
|
|
24
24
|
* `toolFaces` overlay names a tool the listing does not carry / does not fit the tool's materialized
|
|
25
25
|
* schema (the tool mounts bare); once per leg per tool; `detail: { server, tool, problem?, sessionId, runId }`.
|
|
26
|
+
* - `"skills.listing_truncated"` — the `<skills>` disclosure block did not fit its byte budget, so
|
|
27
|
+
* at least one skill is listed without its description (`nameOnly`) or, in the pathological case,
|
|
28
|
+
* not listed at all (`dropped`); the `Skill` tool still serves every one of them. Audience
|
|
29
|
+
* `operator` — raising `TaskSpec.skillListingBudgetFraction`, lowering `skillListingMaxDescChars`
|
|
30
|
+
* or declaring fewer skills are all deployment acts. `detail: { budgetBytes,
|
|
31
|
+
* requestedBudgetBytes, descMaxChars, total, kept, nameOnly: [name], dropped: [name], sessionId }`
|
|
32
|
+
* — `requestedBudgetBytes` is what the declared fraction asked for BEFORE the delivery lane's
|
|
33
|
+
* structural clamp, and differs from `budgetBytes` only when that clamp bound, so a listing
|
|
34
|
+
* shortened by the lane is distinguishable from one shortened by the deployment's own fraction.
|
|
35
|
+
* Announced by the lane that DELIVERS a listing frame — the leg's first-frame producer, or the
|
|
36
|
+
* turn boundary that a drift (or a resume leg's initial listing) rides — once per delivered frame
|
|
37
|
+
* the budget actually degraded, and never for one that fit. Every count is read off that frame, so
|
|
38
|
+
* `total` is the whole roster on an initial listing and the newly announced set on a drift frame.
|
|
39
|
+
* A run whose deployment turned the listing off (`attachments.skillsListing: false`) emits nothing,
|
|
40
|
+
* since nothing is rendered. There is NO dedup key: each notice is one delivered frame`s cost, and
|
|
41
|
+
* a frame is delivered (and committed) once.
|
|
26
42
|
* - `"tool_result.offload_put_failed"` (#167) — a clear-with-offload persist's fire-and-forget put
|
|
27
43
|
* failed; THIS attempt stored nothing (the failure arm reports, it never re-inserts under the
|
|
28
44
|
* ref) — an earlier attempt of the same idempotent re-put may already have stored the row, so
|
|
@@ -85,9 +101,12 @@ export interface EngineNotice {
|
|
|
85
101
|
* The SEAT is announced, not a leg's resolved face: resolution (task seat, governance, the
|
|
86
102
|
* read-only mount) still runs per prepare. In-flight legs keep the face they prepared under;
|
|
87
103
|
* every later prepare resolves against the new declaration. A candidate outside the value gate
|
|
88
|
-
* (`
|
|
104
|
+
* (`assertContainmentFaceValue`) throws atomically and mints nothing. Own `occurrenceId` for the same
|
|
89
105
|
* reason as the catalog line (no run, no session, a two-word payload). Dedup key:
|
|
90
106
|
* (code, occurrenceId).
|
|
107
|
+
* - `"config.write_face_swapped"` — the same line for the WRITE seat (`RunnerDeps.writeFace`),
|
|
108
|
+
* same payload, same laws. A SEPARATE code rather than a face field on the line above: the audience
|
|
109
|
+
* table and every consumer table are keyed by code.
|
|
91
110
|
* - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
|
|
92
111
|
* DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
|
|
93
112
|
* the pairing pre-flight and the seat fell back to the primary model instead of sinking the
|
|
@@ -100,6 +119,35 @@ export interface EngineNotice {
|
|
|
100
119
|
* session falling back on the same seat used to mint byte-identical lines; `runId` is what
|
|
101
120
|
* tells one task falling back twice apart from two tasks falling back once each.
|
|
102
121
|
* Explicitly-named models never mint this: they refuse at the brain's request gate instead.
|
|
122
|
+
* - `"route.classifier_unroutable"` (same pre-flight, the other POSTURE) — the derived classifier seat
|
|
123
|
+
* failed the same pairing pre-flight while the deployment had declared NO fallback
|
|
124
|
+
* (`RunnerDeps.autoMode.classifierFallback: "none"`), so the seat was KEPT and its calls are refused at
|
|
125
|
+
* the request gate rather than served by the primary model. The line is why "no swap" is still not "no
|
|
126
|
+
* word". `detail: { seat, from, cause, fixHint, action: "refused", sessionId?, runId? }` — the twin
|
|
127
|
+
* above's keys MINUS `to` (nothing moved, and a `to` naming a model that was not used is the one lie a
|
|
128
|
+
* consumer cannot detect); `action` is the discriminator, so a reader never infers the posture from that
|
|
129
|
+
* absence. Audience `operator`, same entitlement reading as its twin. Dedup key: (code, seat) per run.
|
|
130
|
+
* - `"classifier.fallback"` (the classifier's candidate ladder, `src/core/runner/classifier-ladder.ts`)
|
|
131
|
+
* — a gated call's classification did NOT rule on the classifier seat and was re-asked on the model
|
|
132
|
+
* behind it. Sibling of `route.fallback_to_primary` one layer later: that one fires when the derived
|
|
133
|
+
* model is not ROUTABLE at all, this one when it is routable and still does not answer.
|
|
134
|
+
* `detail: { from, to, cause, sessionId?, runId? }` — `cause` is the `AutoModeFallbackCause` word, the
|
|
135
|
+
* two ids are the rungs. Dedup key: (code, from, cause) PER RUN; a second CAUSE on the same rung is
|
|
136
|
+
* its own line.
|
|
137
|
+
* - `"classifier.role_defaulted"` (`src/core/runner/classifier-ladder.ts`, the seat resolver) — no
|
|
138
|
+
* `classifier` role names a model, so another model STANDS IN for the seat. `detail: { model, reason,
|
|
139
|
+
* thinkingOffSpelled, sessionId?, runId? }` — `reason` ∈ `tier_below_main` (the nearest directly-bound
|
|
140
|
+
* tier below the run's main model) / `tier_table` (the static `flash` row, a main bound to no tier) /
|
|
141
|
+
* `role_fallback` (the `summarize → default` chain, no tiers) / `main_only` (the main model is the only
|
|
142
|
+
* model there is) / `role_unresolvable` (the role named a model the catalog cannot resolve; the main
|
|
143
|
+
* stands in); `thinkingOffSpelled` = whether that model's wire can carry a disable key. Minted
|
|
144
|
+
* AFTER the route pre-flight, so it names the seat actually used. Once per run.
|
|
145
|
+
* - `"classifier.contract_broken"` (same file, the round observer) — ONE model answered
|
|
146
|
+
* `AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER` CONSECUTIVE classifications without a verdict (each already
|
|
147
|
+
* re-asked once by the leg). `detail: { model, rung, consecutive, sessionId?, runId? }` — `rung` is the
|
|
148
|
+
* ladder position (0 the seat, 1 the model behind it), since two rungs may share an id. A ruling from
|
|
149
|
+
* that rung resets its count; an `unavailable` round neither counts nor resets. Once per run per rung,
|
|
150
|
+
* and by construction before the denial limit's third block hands the user its card.
|
|
103
151
|
* - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapDeps` moved a
|
|
104
152
|
* same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
|
|
105
153
|
* did not change: legal (a provider changing domains), but worth one loud line — if the
|
|
@@ -120,6 +168,9 @@ export interface EngineNotice {
|
|
|
120
168
|
* a library-direct `createHandsToolkit` mount announces at toolkit creation (one per mount,
|
|
121
169
|
* through the band-local `HandsToolkitOptions.onNotice` seat, absent ⇒ `console.warn`);
|
|
122
170
|
* `detail: { seat, declared, inForce, cause }`.
|
|
171
|
+
* - `"config.write_face_deployment_clamped"` — the write seat's twin of the line above, with the
|
|
172
|
+
* same stations, the same once-per-sink ledger (its OWN ledger row) and the same detail shape. The
|
|
173
|
+
* cause differs in prose only: that mount carries no write faces at all.
|
|
123
174
|
* - `"config.tool_model_gate_removed"` (design/277) — the model gate trimmed default-mounted
|
|
124
175
|
* scaffold entries from a task's roster. One notice per (model, class), de-duplicated once per
|
|
125
176
|
* `onNotice` SINK on the (modelId, class, canonical sorted removed-name set) line (same unit
|
|
@@ -629,6 +680,31 @@ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) =>
|
|
|
629
680
|
* one repeated disclosure about one run. The account is per-RUN by its own contract sentence ("at most
|
|
630
681
|
* once per family, per run"), so it must carry the run's own id. Same omit-never-fabricate rule.
|
|
631
682
|
*/
|
|
683
|
+
/**
|
|
684
|
+
* What this leg still OWES a person, one count per family — the input to {@link undrainedUserInputNotices}.
|
|
685
|
+
* TWO lanes accept caller input and either can be holding some when a run ends: the harness's live queue
|
|
686
|
+
* (`held`, the at-agent_end snapshot, minus what a durable park migrated onto the new checkpoint) and a resumed
|
|
687
|
+
* leg's undelivered continuation frames (`owedContinuation` — the checkpoint's parked steers and the caller's
|
|
688
|
+
* wake message, minus what the same migration carried). They are ONE book: a frame is owed for the same reason
|
|
689
|
+
* on either lane — accepted, and never in the transcript — and counting the live queue alone is what let a leg
|
|
690
|
+
* that committed no pause drop a person's instruction in silence (the original row is already spent by then).
|
|
691
|
+
*/
|
|
692
|
+
export declare function owedUserInputCounts(held: {
|
|
693
|
+
steer: number;
|
|
694
|
+
followUp: number;
|
|
695
|
+
} | undefined, migrated: {
|
|
696
|
+
steer: number;
|
|
697
|
+
followUp: number;
|
|
698
|
+
}, continuation: {
|
|
699
|
+
frames: ReadonlyArray<{
|
|
700
|
+
seq?: number;
|
|
701
|
+
}>;
|
|
702
|
+
carried: number;
|
|
703
|
+
rowMayReturn: boolean;
|
|
704
|
+
}): {
|
|
705
|
+
steer: number;
|
|
706
|
+
followUp: number;
|
|
707
|
+
};
|
|
632
708
|
export declare function undrainedUserInputNotices(counts: {
|
|
633
709
|
steer: number;
|
|
634
710
|
followUp: number;
|
|
@@ -31,6 +31,13 @@ export function deliverEngineNotice(onNotice, notice) {
|
|
|
31
31
|
}
|
|
32
32
|
console.warn(notice.message);
|
|
33
33
|
}
|
|
34
|
+
export function owedUserInputCounts(held, migrated, continuation) {
|
|
35
|
+
const owedContinuation = (continuation.rowMayReturn ? continuation.frames.filter((f) => f.seq === undefined).length : continuation.frames.length) - continuation.carried;
|
|
36
|
+
return {
|
|
37
|
+
steer: Math.max(0, (held?.steer ?? 0) - migrated.steer) + Math.max(0, owedContinuation),
|
|
38
|
+
followUp: Math.max(0, (held?.followUp ?? 0) - migrated.followUp),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
34
41
|
export function undrainedUserInputNotices(counts, taskId, sessionId, runId) {
|
|
35
42
|
const tid = taskId !== undefined ? { taskId } : {};
|
|
36
43
|
const sid = sessionId !== undefined ? { sessionId } : {};
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
2
2
|
import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
|
|
3
|
+
import { normalizeReadRootCandidate } from "./read-root-candidate.js";
|
|
3
4
|
import { traceHookCrash } from "./runner/gate-exit.js";
|
|
4
5
|
function withProbeTimeout(p, ms, signal) {
|
|
5
6
|
if (ms === undefined && signal === undefined)
|
|
@@ -80,6 +81,7 @@ export async function runGateFold(pass) {
|
|
|
80
81
|
let probeReason;
|
|
81
82
|
let probeCause;
|
|
82
83
|
let probeMandated;
|
|
84
|
+
let probeReadRootCandidate;
|
|
83
85
|
if (input.irreversibility === "maybe" && input.reversibilityProbe) {
|
|
84
86
|
let reversible = false;
|
|
85
87
|
const probeArgs = pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput;
|
|
@@ -105,6 +107,7 @@ export async function runGateFold(pass) {
|
|
|
105
107
|
probeCause = normalizeProbeCause(verdict?.cause);
|
|
106
108
|
if (verdict?.mandated === true)
|
|
107
109
|
probeMandated = true;
|
|
110
|
+
probeReadRootCandidate = normalizeReadRootCandidate(verdict?.readRootCandidate);
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
catch (err) {
|
|
@@ -123,6 +126,7 @@ export async function runGateFold(pass) {
|
|
|
123
126
|
...(probeReason !== undefined ? { probeReason } : {}),
|
|
124
127
|
...(probeCause !== undefined ? { probeCause } : {}),
|
|
125
128
|
...(probeMandated === true ? { probeMandated: true } : {}),
|
|
129
|
+
...(probeReadRootCandidate !== undefined ? { probeReadRootCandidate } : {}),
|
|
126
130
|
};
|
|
127
131
|
pass.tightenedBy = input.shellGated === true ? "shell_gate" : "safety";
|
|
128
132
|
}
|
|
@@ -139,7 +143,7 @@ export async function runGateFold(pass) {
|
|
|
139
143
|
pass.tightenedBy = "safety";
|
|
140
144
|
}
|
|
141
145
|
if (pass.decision.action === "allow" && input.writeProtectionCheck !== undefined) {
|
|
142
|
-
const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face);
|
|
146
|
+
const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face, input.trackedCwd?.());
|
|
143
147
|
if (writeProtectedHit !== null) {
|
|
144
148
|
pass.decision = {
|
|
145
149
|
action: "ask",
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown } from "./tool-policy.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { classifierMayAnswer, persistedRuleMandateOf, probeBoundaryMandate, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
|
|
3
|
+
import { classifierParseFailureDenyMessage, classifierUnavailableDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
4
4
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
5
5
|
import { isRuleBehavior } from "./permission-rule-model.js";
|
|
6
6
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
@@ -346,7 +346,7 @@ export async function runGateLanes(pass) {
|
|
|
346
346
|
if (pass.decision.action === "ask") {
|
|
347
347
|
const { ruleStoreUnreadable: _selfDeclared, ...unstamped } = pass.decision;
|
|
348
348
|
const stamped = { ...unstamped, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}), ...(originFacts.ruleStore !== undefined ? { ruleStoreUnreadable: originFacts.ruleStore } : {}) };
|
|
349
|
-
pass.decision =
|
|
349
|
+
pass.decision = stampAskOrigin(stamped, originFacts);
|
|
350
350
|
}
|
|
351
351
|
if (allowLayerMayClear() && pass.decision.action === "ask" && persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined && pass.req.toolName === COMMAND_RULE_TOOL) {
|
|
352
352
|
const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
|
|
@@ -369,6 +369,7 @@ export async function runGateLanes(pass) {
|
|
|
369
369
|
const verdict = await input.autoMode.decider
|
|
370
370
|
.decide({ req: pass.req, askMessage: decisionText(pass.decision) }, input.abortSignal)
|
|
371
371
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
372
|
+
pass.classifierRound = verdict.classifier;
|
|
372
373
|
if (verdict.kind === "allow") {
|
|
373
374
|
input.autoMode.denialTracking?.recordAllow();
|
|
374
375
|
pass.decision = {
|
|
@@ -381,12 +382,12 @@ export async function runGateLanes(pass) {
|
|
|
381
382
|
else if (verdict.kind === "unavailable") {
|
|
382
383
|
pass.decision = attestClassifierDenyCause({
|
|
383
384
|
action: "deny",
|
|
384
|
-
message: classifierUnavailableDenyMessage(toolName, verdict.cause),
|
|
385
|
+
message: classifierUnavailableDenyMessage(toolName, verdict.cause, verdict.classifier),
|
|
385
386
|
decisionReason: "classifier",
|
|
386
387
|
}, "unavailable", pass.req);
|
|
387
388
|
}
|
|
388
389
|
else {
|
|
389
|
-
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") :
|
|
390
|
+
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
|
|
390
391
|
const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
391
392
|
const tracked = input.autoMode.denialTracking?.recordBlock();
|
|
392
393
|
if (tracked?.limitReached === true) {
|
|
@@ -397,7 +398,7 @@ export async function runGateLanes(pass) {
|
|
|
397
398
|
requiresRealApproval: true,
|
|
398
399
|
denialLimitFallback: tracked.fallback,
|
|
399
400
|
};
|
|
400
|
-
pass.decision =
|
|
401
|
+
pass.decision = stampAskOrigin(fallbackAsk, originFacts);
|
|
401
402
|
}
|
|
402
403
|
else {
|
|
403
404
|
const denied = {
|
|
@@ -657,7 +658,7 @@ export async function runGateLanes(pass) {
|
|
|
657
658
|
const { ruleStoreUnreadable: _selfDeclaredOnEdit, ...recheckUnstamped } = recheck;
|
|
658
659
|
const editAskSnapshot = { ...recheckUnstamped, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
659
660
|
markedThisRound = undefined;
|
|
660
|
-
const editAskDecision = { ...editAskSnapshot, ...(editRuleStore !== undefined ? { ruleStoreUnreadable: editRuleStore } : {})
|
|
661
|
+
const editAskDecision = stampAskOrigin({ ...editAskSnapshot, ...(editRuleStore !== undefined ? { ruleStoreUnreadable: editRuleStore } : {}) }, askOriginFacts(editOrg.org, editRuleStore));
|
|
661
662
|
const editAskReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
662
663
|
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
663
664
|
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
@@ -676,7 +677,7 @@ export async function runGateLanes(pass) {
|
|
|
676
677
|
editRewrittenSinceHuman = false;
|
|
677
678
|
}
|
|
678
679
|
if (editDenied === undefined && editRewrittenSinceHuman && input.writeProtectionCheck !== undefined) {
|
|
679
|
-
const editHit = input.writeProtectionCheck(toolName, editArgs, pass.req.face);
|
|
680
|
+
const editHit = input.writeProtectionCheck(toolName, editArgs, pass.req.face, input.trackedCwd?.());
|
|
680
681
|
if (editHit !== null) {
|
|
681
682
|
editDenied = {
|
|
682
683
|
action: "deny",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* verdict ended.
|
|
9
9
|
*/
|
|
10
10
|
import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
|
|
11
|
+
import { type AutoModeClassifierRound } from "./auto-mode.js";
|
|
11
12
|
/**
|
|
12
13
|
* How the wait an ask was in ENDED — the closed set. One word per way a wait can end; the `who` on the
|
|
13
14
|
* record says which party ended it and is fixed BY the word (see {@link Settlement}).
|
|
@@ -92,6 +93,31 @@ export type Settlement = {
|
|
|
92
93
|
};
|
|
93
94
|
when: number;
|
|
94
95
|
};
|
|
96
|
+
/** The person arm of {@link Settlement} — the shape the two human words inhabit. Named so a site that
|
|
97
|
+
* both composes one and quotes its note can read the seat off the record instead of re-deriving it. */
|
|
98
|
+
export type PersonSettlement = Extract<Settlement, {
|
|
99
|
+
kind: "human_allowed" | "human_refused";
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* A decider's NOTE, normalised — the one rule for "did the decider say anything": empty text is a
|
|
103
|
+
* decider who attached nothing and reads as absent wherever the note travels. Whitespace-only text is
|
|
104
|
+
* not empty by this rule and stays a note: that half is PINNED CURRENT BEHAVIOUR, not a promise —
|
|
105
|
+
* nothing here requires a note to carry content, and narrowing it is a separate decision with its own
|
|
106
|
+
* consumers to warn.
|
|
107
|
+
*/
|
|
108
|
+
export declare function decidersNote(text: string | undefined): string | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* THE mint for a person's settlement: the synchronous resolver's human arms and the durable decide
|
|
111
|
+
* lane's person arm compose their record here, so one refusal plus one reason text cannot end as two
|
|
112
|
+
* different records depending on which path carried it.
|
|
113
|
+
*
|
|
114
|
+
* @contract settlement.note — the `note` seat is filled exactly when the decider attached a reason, and
|
|
115
|
+
* its absence is how the record says the refusal was bare (the runner reads that absence, and nothing
|
|
116
|
+
* else, as the issuing batch's control-flow boundary). Empty text folds to absence HERE, once, for
|
|
117
|
+
* every path that fills the seat ({@link decidersNote}); a site that hand-builds the record instead is
|
|
118
|
+
* a second answer to the same question, which is how one path came to halt a batch the other let run.
|
|
119
|
+
*/
|
|
120
|
+
export declare function settledByPerson(kind: PersonSettlement["kind"], approver: string | undefined, note?: string): PersonSettlement;
|
|
95
121
|
/** Which settlement kinds are REFUSALS (the ask ended as a deny). Total over the closed set — the fence
|
|
96
122
|
* below refuses a member without a row — so invariant I2 is a table lookup, not a list in a condition. */
|
|
97
123
|
export declare const SETTLEMENT_IS_REFUSAL: {
|
|
@@ -189,13 +215,23 @@ export type CauseTableCoversEveryDeniedBy = AssertAllKeysHandled<Exclude<DeniedB
|
|
|
189
215
|
/** The final disposition of a gate pass. `cause` is present exactly when the deny came out of an auto-mode
|
|
190
216
|
* classifier round that did not RULE ({@link ClassifierDenyCause}); a ruling block and every non-classifier
|
|
191
217
|
* deny carry none. Invariant I5 ({@link screenGateOutcome}): a cause stands only beside a layer word in
|
|
192
|
-
* {@link DENIED_BY_MAY_CARRY_CAUSE}.
|
|
218
|
+
* {@link DENIED_BY_MAY_CARRY_CAUSE}.
|
|
219
|
+
*
|
|
220
|
+
* `classifier` — the classification's model attribution, on BOTH arms (a classification ends either way,
|
|
221
|
+
* allow or deny): which model it was for, which one answered, and — only when the ladder moved — the rung it
|
|
222
|
+
* left and why. ABSENT reads UNKNOWN, never "the seat answered" (that is `fallback` absent WITH the member
|
|
223
|
+
* present), and a consumer cannot tell its three causes apart from the record alone: no classification
|
|
224
|
+
* happened; one ran on a decider given no candidate ladder, which can name no model; or one ran on an
|
|
225
|
+
* ANCESTOR's inherited chain, which carries the FORM (`cause`) but has no seat for the attribution yet.
|
|
226
|
+
* Additive. Invariant I6 ({@link screenGateOutcome}) screens its shape. */
|
|
193
227
|
export type GateDisposition = {
|
|
194
228
|
kind: "allowed";
|
|
229
|
+
classifier?: AutoModeClassifierRound;
|
|
195
230
|
} | {
|
|
196
231
|
kind: "denied";
|
|
197
232
|
deniedBy: DeniedBy;
|
|
198
233
|
cause?: ClassifierDenyCause;
|
|
234
|
+
classifier?: AutoModeClassifierRound;
|
|
199
235
|
};
|
|
200
236
|
/**
|
|
201
237
|
* The WHOLE record of one tool-gate pass: the final disposition, plus — when this pass settled an ask —
|
|
@@ -212,7 +248,11 @@ export type GateDisposition = {
|
|
|
212
248
|
* on the settlement (it is not the approver's refusal);
|
|
213
249
|
* I4 `disposition.kind === "allowed"` ⇒ `settlement` absent or `human_allowed`;
|
|
214
250
|
* I5 `disposition.cause` present ⇒ a member of {@link CLASSIFIER_DENY_CAUSES} beside a layer word
|
|
215
|
-
* {@link DENIED_BY_MAY_CARRY_CAUSE} admits (the classifier station, or the inherited chain's `policy`)
|
|
251
|
+
* {@link DENIED_BY_MAY_CARRY_CAUSE} admits (the classifier station, or the inherited chain's `policy`);
|
|
252
|
+
* I6 `disposition.classifier` present ⇒ both model ids are non-empty strings, and a `fallback` on it
|
|
253
|
+
* names a `to` that IS `modelUsed` (the ladder landed where the record says) with a cause in
|
|
254
|
+
* {@link import("./auto-mode.js").AUTO_MODE_FALLBACK_CAUSES} — a record whose `to` and `modelUsed`
|
|
255
|
+
* disagree would let a reader answer "which model decided this call?" two ways.
|
|
216
256
|
* Nothing here is read out of an ABSENCE: an ordinary allow with no ask is `{ disposition: { kind: "allowed" } }`,
|
|
217
257
|
* a direct policy deny is `{ disposition: { kind: "denied", deniedBy: "policy" } }`, and neither carries a
|
|
218
258
|
* settlement because neither settled one.
|