@sema-agent/core 7.13.0 → 7.14.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 +80 -0
- package/dist/agents/subagent.d.ts +4 -9
- package/dist/agents/subagent.js +16 -12
- package/dist/agents/teacher.js +1 -1
- package/dist/brain/reasoning.d.ts +23 -0
- package/dist/brain/reasoning.js +35 -4
- package/dist/brain/stream-engine.js +35 -17
- package/dist/brain/timeout.d.ts +25 -4
- package/dist/brain/timeout.js +1 -1
- package/dist/core/ask-origin.d.ts +28 -9
- package/dist/core/ask-origin.js +10 -5
- package/dist/core/auto-mode.d.ts +3 -3
- package/dist/core/checkpoint-seat.d.ts +18 -0
- package/dist/core/checkpoint-seat.js +6 -0
- package/dist/core/checkpoint-store.d.ts +9 -16
- package/dist/core/checkpoint-store.js +7 -4
- package/dist/core/engine-notice.d.ts +3 -1
- package/dist/core/env-knob-announce.d.ts +16 -0
- package/dist/core/env-knob-announce.js +14 -0
- package/dist/core/gate-fold.js +2 -0
- package/dist/core/gate-lanes.js +21 -12
- package/dist/core/gate-outcome.d.ts +42 -3
- package/dist/core/gate-outcome.js +22 -0
- package/dist/core/governance-codes.js +1 -1
- package/dist/core/hooks.d.ts +5 -16
- package/dist/core/hooks.js +6 -4
- package/dist/core/mcp.d.ts +3 -2
- package/dist/core/mcp.js +5 -11
- package/dist/core/memory-engine/engine.d.ts +2 -2
- package/dist/core/runner/advertised-writable-dirs.d.ts +74 -0
- package/dist/core/runner/advertised-writable-dirs.js +48 -0
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/denial-limit-arms.d.ts +7 -9
- package/dist/core/runner/denial-limit-arms.js +8 -10
- package/dist/core/runner/gate-exit.d.ts +15 -3
- package/dist/core/runner/gate-exit.js +6 -4
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
- package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
- package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +10 -4
- package/dist/core/runner/prepare-hands-readface.js +5 -14
- package/dist/core/runner/prepare-inherited-gate.d.ts +5 -3
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-memory.d.ts +31 -34
- package/dist/core/runner/prepare-memory.js +73 -51
- package/dist/core/runner/prepare-policy-chain.js +5 -2
- package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
- package/dist/core/runner/prepare-prompt-inputs.js +2 -2
- package/dist/core/runner/prepare-question-face.d.ts +10 -8
- package/dist/core/runner/prepare-question-face.js +1 -3
- package/dist/core/runner/prepare-safety-scan.js +1 -1
- package/dist/core/runner/prepare-task.js +57 -48
- package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
- package/dist/core/runner/prepare-wiring-manifest.js +1 -1
- package/dist/core/runner/run-compaction-machinery.js +2 -0
- package/dist/core/runner/run-harness-handlers.js +3 -1
- package/dist/core/runner/stream-settle-backstop.js +1 -1
- package/dist/core/runtime-caps.d.ts +21 -0
- package/dist/core/runtime-caps.js +5 -1
- package/dist/core/task-event.d.ts +11 -2
- package/dist/core/task-registry-shared.js +8 -6
- package/dist/core/task-result.d.ts +15 -0
- package/dist/core/terminal-cause.d.ts +6 -2
- package/dist/core/tool-policy.d.ts +34 -28
- package/dist/core/tool-policy.js +29 -5
- package/dist/core/tool-roster.js +2 -0
- package/dist/core/tool-spec.d.ts +13 -12
- package/dist/core/types.d.ts +2 -1
- package/dist/core/types.js +1 -0
- package/dist/core/wiring-manifest.d.ts +16 -9
- package/dist/core/wiring-manifest.js +8 -3
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/orchestration/run-workflow-tool.d.ts +8 -8
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +4 -3
- package/dist/orchestration/workflow-primitives.js +3 -3
- package/dist/orchestration/workflow-types.d.ts +14 -0
- package/dist/orchestration/workflow.d.ts +41 -4
- package/dist/orchestration/workflow.js +23 -5
- package/dist/tools/fs/fs-shared.js +9 -3
- package/dist/tools/fs/read-deny.d.ts +24 -8
- package/dist/tools/fs/read-deny.js +20 -1
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +25 -1
package/dist/core/hooks.js
CHANGED
|
@@ -5,8 +5,7 @@ import { runGateLanes } from "./gate-lanes.js";
|
|
|
5
5
|
export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
7
7
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
8
|
-
import { isAskOrigin } from "./ask-origin.js";
|
|
9
|
-
import { isAutoModeUnavailableCause } from "./auto-mode.js";
|
|
8
|
+
import { isAskOrigin, isRuleStoreUnreadable } from "./ask-origin.js";
|
|
10
9
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
11
10
|
export { formatHookFeedback } from "./reminder-mint.js";
|
|
12
11
|
import { createSafeNotifier } from "./safe-notify.js";
|
|
@@ -235,10 +234,12 @@ export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash, timeo
|
|
|
235
234
|
export function askCarryRowMembers(carry) {
|
|
236
235
|
if (carry === undefined)
|
|
237
236
|
return {};
|
|
237
|
+
const origin = carry.origin;
|
|
238
|
+
const ruleStoreUnreadable = carry.ruleStoreUnreadable;
|
|
238
239
|
return {
|
|
239
240
|
...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
|
|
240
|
-
...(isAskOrigin(
|
|
241
|
-
...(
|
|
241
|
+
...(isAskOrigin(origin) ? { origin } : {}),
|
|
242
|
+
...(isRuleStoreUnreadable(ruleStoreUnreadable) ? { ruleStoreUnreadable } : {}),
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
export async function runToolGate(input) {
|
|
@@ -308,6 +309,7 @@ export async function runToolGate(input) {
|
|
|
308
309
|
hookAsk: undefined,
|
|
309
310
|
parkFailed: undefined,
|
|
310
311
|
deniedBy: undefined,
|
|
312
|
+
denyCause: undefined,
|
|
311
313
|
decision: { action: "allow" },
|
|
312
314
|
tightenedBy: undefined,
|
|
313
315
|
policyRewrite: undefined,
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -330,8 +330,9 @@ export declare const MCP_TOOL_TIMEOUT_DEFAULT_MS = 100000000;
|
|
|
330
330
|
*/
|
|
331
331
|
export declare const MCP_ENV_MS_MIN = 1000;
|
|
332
332
|
export declare const MCP_ENV_MS_MAX = 2147483647;
|
|
333
|
-
/** Test seam
|
|
334
|
-
* the
|
|
333
|
+
/** Test seam kept under the name its callers already use; the ledger it clears is the shared one
|
|
334
|
+
* ({@link __resetEnvKnobAnnouncements} — the MCP knobs are not a family of their own, they are
|
|
335
|
+
* process-level env knobs like the output-length pair). Never called by production code. */
|
|
335
336
|
export declare function __resetMcpEnvAnnouncements(): void;
|
|
336
337
|
/** Resolve the per-call tool timeout in ms (env `MCP_TOOL_TIMEOUT`, else the large default). Exported for tests. */
|
|
337
338
|
export declare function mcpToolTimeoutMs(): number;
|
package/dist/core/mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { redactSecrets } from "./untrusted-egress.js";
|
|
2
|
+
import { announceEnvKnob, __resetEnvKnobAnnouncements } from "./env-knob-announce.js";
|
|
2
3
|
import { MCP_NAMESPACE } from "./protocol-table.js";
|
|
3
4
|
import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
|
|
4
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -38,7 +39,7 @@ function mcpMaxOutputTokens() {
|
|
|
38
39
|
return MCP_OUTPUT_TOKENS_DEFAULT;
|
|
39
40
|
const n = parseWholeNumber(raw);
|
|
40
41
|
if (n === undefined || n <= 0) {
|
|
41
|
-
|
|
42
|
+
announceEnvKnob(`MAX_MCP_OUTPUT_TOKENS=${rawEnv} was ignored — it is not a positive whole number of tokens. Using ${MCP_OUTPUT_TOKENS_DEFAULT} instead.`);
|
|
42
43
|
return MCP_OUTPUT_TOKENS_DEFAULT;
|
|
43
44
|
}
|
|
44
45
|
return n;
|
|
@@ -132,15 +133,8 @@ function parseWholeNumber(raw) {
|
|
|
132
133
|
return parseInt(raw.replace(ENV_GROUP_SEPARATORS_RE, ""), 10);
|
|
133
134
|
return undefined;
|
|
134
135
|
}
|
|
135
|
-
const announcedMcpEnvKnobs = new Set();
|
|
136
|
-
function announceMcpEnvKnob(line) {
|
|
137
|
-
if (announcedMcpEnvKnobs.has(line))
|
|
138
|
-
return;
|
|
139
|
-
announcedMcpEnvKnobs.add(line);
|
|
140
|
-
console.warn(line);
|
|
141
|
-
}
|
|
142
136
|
export function __resetMcpEnvAnnouncements() {
|
|
143
|
-
|
|
137
|
+
__resetEnvKnobAnnouncements();
|
|
144
138
|
}
|
|
145
139
|
function parseEnvMs(name) {
|
|
146
140
|
const rawEnv = process.env[name];
|
|
@@ -151,12 +145,12 @@ function parseEnvMs(name) {
|
|
|
151
145
|
return undefined;
|
|
152
146
|
const n = parseWholeNumber(raw);
|
|
153
147
|
if (n === undefined || n <= 0) {
|
|
154
|
-
|
|
148
|
+
announceEnvKnob(`${name}=${rawEnv} was ignored — it is not a positive whole number of milliseconds. Using the built-in default instead.`);
|
|
155
149
|
return undefined;
|
|
156
150
|
}
|
|
157
151
|
const clamped = Math.min(Math.max(n, MCP_ENV_MS_MIN), MCP_ENV_MS_MAX);
|
|
158
152
|
if (clamped !== n) {
|
|
159
|
-
|
|
153
|
+
announceEnvKnob(`${name}=${rawEnv} is outside the range this runtime can honor (${MCP_ENV_MS_MIN}..${MCP_ENV_MS_MAX} ms). Using ${clamped}ms instead.`);
|
|
160
154
|
}
|
|
161
155
|
return clamped;
|
|
162
156
|
}
|
|
@@ -1663,8 +1663,8 @@ export declare class MemoryEngine {
|
|
|
1663
1663
|
* @returns the rejection to report, or `undefined` when the index is clean/absent.
|
|
1664
1664
|
*/
|
|
1665
1665
|
private gateDerivedIndex;
|
|
1666
|
-
/** Names of read-only sibling scope dirs directly under the writable root (excluded from scans). */
|
|
1667
|
-
|
|
1666
|
+
/** Names of read-only sibling scope dirs directly under the writable root (excluded from scans; public since #691 — the runner refuses to admit a writable root that contains another scope's directory to the file tools' allowlist). */
|
|
1667
|
+
readonlyDirNamesUnderRoot(handle: MemorySessionHandle): Set<string>;
|
|
1668
1668
|
/** Projection write: reads the target to skip a no-op, then writes — the same check-then-use shape
|
|
1669
1669
|
* as the containment restores, so it takes the same no-follow write. A link at a projection path
|
|
1670
1670
|
* is never a supported layout (the harvest scan refuses symlinked entry files outright), and this
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The advertised-writable-directory seat — ONE rule for every directory the prompt tells the model
|
|
3
|
+
* it may write with the file tools:
|
|
4
|
+
*
|
|
5
|
+
* a directory the prompt advertises as writable is a directory the root fence ADMITS; the same
|
|
6
|
+
* admission decides the hole and the teaching, so a directory the fence cannot admit is neither
|
|
7
|
+
* opened nor taught, and the skip is announced.
|
|
8
|
+
*
|
|
9
|
+
* Two advertisers share it: the deployment's scratchpad (`TaskSpec.envFacts.scratchpadDir`, spoken
|
|
10
|
+
* by the `# Environment` block) and the memory engine's write root (spoken by the `# Memory`
|
|
11
|
+
* instruction, "write to it directly with the Write tool"). Before this seat each advertiser judged
|
|
12
|
+
* on its own: the scratchpad opened its hole in the hands phase, the memory instruction checked only
|
|
13
|
+
* that a `Write` tool was mounted — and the fence refused the very path the instruction named
|
|
14
|
+
* (`path_not_in_root`), a contradictory gate that taught the model to route around the tools.
|
|
15
|
+
*
|
|
16
|
+
* Admission = fail-closed canonicalization through the SAME ExecutionEnv the fence judges with (a
|
|
17
|
+
* raw or dangling entry is never admitted — the ancestor-canonicalize fallback would let a symlinked
|
|
18
|
+
* prefix carry writes outside every root). An advertiser that OWNS its directory's creation may ask
|
|
19
|
+
* for materialization on a DEFINITIVE `not_found` (the exclusive-create marker, so a concurrently
|
|
20
|
+
* planted path is never written through); an advertiser whose directory is created elsewhere passes
|
|
21
|
+
* no marker and an absent directory narrows.
|
|
22
|
+
*
|
|
23
|
+
* Reverse check (the seat's own invariant): nothing enters `roots` except through `admit`, and every
|
|
24
|
+
* caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so there is
|
|
25
|
+
* no directory the fence opens that the prompt does not advertise, and none the prompt advertises
|
|
26
|
+
* that the fence refuses.
|
|
27
|
+
*/
|
|
28
|
+
import type { ExecutionEnv } from "../../internal/harness.js";
|
|
29
|
+
export interface AdvertisedWritableDir {
|
|
30
|
+
/** The directory as the advertisement spells it. */
|
|
31
|
+
path: string;
|
|
32
|
+
/** Who advertises it — names the skip line and the trace. */
|
|
33
|
+
advertiser: "scratchpad" | "memory";
|
|
34
|
+
/**
|
|
35
|
+
* How the directory comes to exist when the env reports it absent. The advertiser that owns the
|
|
36
|
+
* directory's creation supplies the marker file name for the atomic exclusive-create
|
|
37
|
+
* materialization (parent dirs created like writeFile; a marker that already exists — planted
|
|
38
|
+
* concurrently, symlink or not — is never written through; an env without an exclusive primitive
|
|
39
|
+
* narrows instead of emulating one with an overwriting write). An advertiser whose directory is
|
|
40
|
+
* created elsewhere (the memory engine materializes its own root) passes nothing — absent narrows.
|
|
41
|
+
*/
|
|
42
|
+
materializeMarker?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The canonical spelling the advertiser's OWN gates key on, when it has one (the memory engine keys its
|
|
45
|
+
* write gate and its harvest boundary on `MemorySessionHandle.writableRoot`). A fence that resolves the
|
|
46
|
+
* directory to a different canonical path would admit writes those gates never see — such a mismatch is
|
|
47
|
+
* a skip, announced with both spellings, never an admission.
|
|
48
|
+
*/
|
|
49
|
+
expectCanonical?: string;
|
|
50
|
+
}
|
|
51
|
+
/** The skip announcement — the one loud outlet for a directory that was advertised but could not be admitted. */
|
|
52
|
+
export type AdvertisedWritableDirSkipped = (dir: AdvertisedWritableDir, reason: string) => void;
|
|
53
|
+
export declare class AdvertisedWritableDirs {
|
|
54
|
+
private readonly env;
|
|
55
|
+
private readonly onSkipped;
|
|
56
|
+
private readonly admitted;
|
|
57
|
+
private readonly verdicts;
|
|
58
|
+
constructor(env: ExecutionEnv, onSkipped: AdvertisedWritableDirSkipped);
|
|
59
|
+
/** The admitted roots, CANONICAL, in admission order, deduplicated. The hands phase folds this list into the
|
|
60
|
+
* write-capable containment allowlist before the toolkit compiles; it is not a live view after that. */
|
|
61
|
+
get roots(): readonly string[];
|
|
62
|
+
/** What became of an advertiser's directory: admitted, skipped (announced), or never asked (the advertiser
|
|
63
|
+
* had nothing to advertise, or decided not to). The prompt side reads this to withhold an advertisement. */
|
|
64
|
+
verdictOf(advertiser: AdvertisedWritableDir["advertiser"]): "admitted" | "skipped" | "not-asked";
|
|
65
|
+
/**
|
|
66
|
+
* Admit one advertised directory. TRUE ⇔ the fence will admit writes under it (its canonical form is on
|
|
67
|
+
* {@link roots}); FALSE ⇔ it was skipped (announced through the seat's outlet) and the advertiser must not
|
|
68
|
+
* advertise it. The env's answers are Result-shaped, so the judgment itself never throws; the skip outlet is
|
|
69
|
+
* the caller's own sink (`deps.onError` in the driver) and is deliberately not guarded here — every phase
|
|
70
|
+
* lets that sink's own exceptions propagate, and a seat that swallowed them would hide the deployment's bug.
|
|
71
|
+
*/
|
|
72
|
+
admit(dir: AdvertisedWritableDir): Promise<boolean>;
|
|
73
|
+
private judge;
|
|
74
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class AdvertisedWritableDirs {
|
|
2
|
+
env;
|
|
3
|
+
onSkipped;
|
|
4
|
+
admitted = [];
|
|
5
|
+
verdicts = new Map();
|
|
6
|
+
constructor(env, onSkipped) {
|
|
7
|
+
this.env = env;
|
|
8
|
+
this.onSkipped = onSkipped;
|
|
9
|
+
}
|
|
10
|
+
get roots() {
|
|
11
|
+
return this.admitted;
|
|
12
|
+
}
|
|
13
|
+
verdictOf(advertiser) {
|
|
14
|
+
return this.verdicts.get(advertiser) ?? "not-asked";
|
|
15
|
+
}
|
|
16
|
+
async admit(dir) {
|
|
17
|
+
const ok = await this.judge(dir);
|
|
18
|
+
this.verdicts.set(dir.advertiser, ok ? "admitted" : "skipped");
|
|
19
|
+
return ok;
|
|
20
|
+
}
|
|
21
|
+
async judge(dir) {
|
|
22
|
+
if (!dir.path || !dir.path.trim()) {
|
|
23
|
+
this.onSkipped(dir, "empty path");
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
let c = await this.env.canonicalPath(dir.path);
|
|
27
|
+
if (!c.ok && c.error.code === "not_found" && dir.materializeMarker !== undefined) {
|
|
28
|
+
const marker = await this.env.joinPath([dir.path, dir.materializeMarker]);
|
|
29
|
+
const exclusiveCreate = this.env.writeFileExclusive?.bind(this.env);
|
|
30
|
+
if (marker.ok && exclusiveCreate) {
|
|
31
|
+
const w = await exclusiveCreate(marker.value, "");
|
|
32
|
+
if (w.ok || w.error.code === "already_exists")
|
|
33
|
+
c = await this.env.canonicalPath(dir.path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!c.ok) {
|
|
37
|
+
this.onSkipped(dir, `${c.error.code}: ${c.error.message}`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (dir.expectCanonical !== undefined && c.value !== dir.expectCanonical) {
|
|
41
|
+
this.onSkipped(dir, `the execution environment resolves it to ${c.value} while the advertiser's gates key on ${dir.expectCanonical} — a fence hole those gates cannot see is not opened`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!this.admitted.includes(c.value))
|
|
45
|
+
this.admitted.push(c.value);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -18,6 +18,11 @@ export interface Stats {
|
|
|
18
18
|
* it fails the type check instead of silently reporting a total short by its own leg. */
|
|
19
19
|
totalInputTokens: number;
|
|
20
20
|
costMicroUsd: number;
|
|
21
|
+
/** #692 C-b — present (`true`) when at least one model round of this leg reported NO usage frame (the brain's
|
|
22
|
+
* own `usageMissing` mark on the assistant message: an aborted / errored / usage-less provider reply). The
|
|
23
|
+
* numeric fields stay required and stay what was COUNTED — with this key present they are a LOWER BOUND,
|
|
24
|
+
* not a measurement ("0 is unknown, not free"). Absent ⇔ every round reported usage. */
|
|
25
|
+
usageMissing?: true;
|
|
21
26
|
nested?: NestedUsage;
|
|
22
27
|
/** Post-task memory-consolidation usage (design/41); set after the result is assembled. Kept out of
|
|
23
28
|
* the budget gate (`overBudget` reads `costMicroUsd`/`tokens`, never this). */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type
|
|
1
|
+
import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type DenialLimitFallback, type DenialLimitFallbackFace, type UnarmedDenialLimitFallback } from "../auto-mode.js";
|
|
2
2
|
import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
|
|
4
4
|
import { type EngineNotice } from "../types.js";
|
|
5
|
-
import { type AskOrigin } from "../ask-origin.js";
|
|
5
|
+
import { type AskOrigin, type RuleStoreUnreadable } from "../ask-origin.js";
|
|
6
6
|
import type { Prepared } from "./contracts.js";
|
|
7
7
|
/**
|
|
8
8
|
* #548 — the classifier DENIAL-LIMIT arms of the tool gate's inherited (delegation) lane, extracted
|
|
@@ -83,14 +83,12 @@ export type InheritedAsk = Extract<PermissionResult, {
|
|
|
83
83
|
}>;
|
|
84
84
|
/** The members of a surviving ask that ride onto the approval request with a COMPUTED value — what the
|
|
85
85
|
* four request mint stations (the gate's own and the three inherited-lane ones) spread after the seats
|
|
86
|
-
* they spell themselves: the ask's origin word
|
|
87
|
-
* classifier-unavailable fact (#616; the engine's own fact rides the DENY since #661). */
|
|
86
|
+
* they spell themselves: the ask's origin word and its denial-limit fallback. */
|
|
88
87
|
export interface AskRequestCarry {
|
|
89
88
|
origin?: AskOrigin;
|
|
89
|
+
/** #688 C3: rides only from the gate's OWN station (the inherited stations' facts never read a rule store). */
|
|
90
|
+
ruleStoreUnreadable?: RuleStoreUnreadable;
|
|
90
91
|
denialLimitFallback?: DenialLimitFallback;
|
|
91
|
-
classifierUnavailable?: {
|
|
92
|
-
readonly cause: AutoModeUnavailableCause;
|
|
93
|
-
};
|
|
94
92
|
}
|
|
95
93
|
/** The GATE's own mint station: both members are read off the surviving decision, which is the gate's
|
|
96
94
|
* OWN snapshot (its stamp point spreads the policy's object and stamps the word onto the copy), so the
|
|
@@ -124,7 +122,7 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
|
|
|
124
122
|
* stateful accessor cannot answer the exclusion with one word and the card with another); on the
|
|
125
123
|
* bound branch it is derived from the re-spoken ask this station minted (its own object). */
|
|
126
124
|
/** `ask` is the incoming ask unchanged (the caller's own object); an UNAVAILABLE round never reaches this arm —
|
|
127
|
-
* it is a `deny`
|
|
125
|
+
* it is a `deny` whose attested cause reads `unavailable` (#661). */
|
|
128
126
|
| {
|
|
129
127
|
kind: "resolve";
|
|
130
128
|
ask: A;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE, classifierUnavailableDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
|
|
2
2
|
import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
|
|
3
|
+
import { attestClassifierDenyCause } from "../tool-policy.js";
|
|
3
4
|
import { deliverEngineNotice } from "../types.js";
|
|
4
5
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
5
6
|
import { askOriginOf, classifierMayAnswer, probeMandatedAsk } from "../ask-origin.js";
|
|
@@ -62,15 +63,14 @@ function fallbackCarry(fallback, liveApprover, tracker) {
|
|
|
62
63
|
export function gateAskCarry(decision, liveApprover, tracker) {
|
|
63
64
|
return {
|
|
64
65
|
...(decision.origin !== undefined ? { origin: decision.origin } : {}),
|
|
66
|
+
...(decision.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: decision.ruleStoreUnreadable } : {}),
|
|
65
67
|
...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker),
|
|
66
|
-
...(decision.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: decision.classifierUnavailable.cause } } : {}),
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
export function inheritedAskCarry(judged, liveApprover, tracker) {
|
|
70
71
|
return {
|
|
71
72
|
origin: judged.origin,
|
|
72
73
|
...fallbackCarry(judged.fallback, liveApprover, tracker),
|
|
73
|
-
...(judged.ask.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: judged.ask.classifierUnavailable.cause } } : {}),
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
export async function judgeInheritedClassifier(opts) {
|
|
@@ -89,21 +89,19 @@ export async function judgeInheritedClassifier(opts) {
|
|
|
89
89
|
if (verdict.kind === "unavailable") {
|
|
90
90
|
return {
|
|
91
91
|
kind: "deny",
|
|
92
|
-
result: { action: "deny", message: classifierUnavailableDenyMessage(req.toolName, verdict.cause), decisionReason: "classifier"
|
|
92
|
+
result: attestClassifierDenyCause({ action: "deny", message: classifierUnavailableDenyMessage(req.toolName, verdict.cause), decisionReason: "classifier" }, "unavailable", req),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE;
|
|
96
96
|
const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
97
97
|
const tracked = autoMode.denialTracking?.recordBlock();
|
|
98
98
|
if (tracked?.limitReached !== true) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
message: verdict.kind === "block" ? `auto-mode classifier blocked ${opts.subject} at an inherited ancestor layer${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : `${reason} (${opts.subject}, at an inherited ancestor layer)`,
|
|
104
|
-
decisionReason: "classifier",
|
|
105
|
-
},
|
|
99
|
+
const denied = {
|
|
100
|
+
action: "deny",
|
|
101
|
+
message: verdict.kind === "block" ? `auto-mode classifier blocked ${opts.subject} at an inherited ancestor layer${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : `${reason} (${opts.subject}, at an inherited ancestor layer)`,
|
|
102
|
+
decisionReason: "classifier",
|
|
106
103
|
};
|
|
104
|
+
return { kind: "deny", result: verdict.kind === "parse_error" ? attestClassifierDenyCause(denied, "parse_error", req) : denied };
|
|
107
105
|
}
|
|
108
106
|
const fallback = tracked.fallback;
|
|
109
107
|
const minted = { ...ask, message: denialLimitFallbackMessage(fallback, reason || category || req.toolName), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback };
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* the reminder envelope and the text helpers. Its `import type` edges up to `hooks.ts` name the gate's public
|
|
22
22
|
* contract (`ToolGateInput`, `ToolGateResult`, the observer payload), which stays declared there.
|
|
23
23
|
*/
|
|
24
|
-
import { type DeniedBy, type GateOutcome, type Settlement } from "../gate-outcome.js";
|
|
24
|
+
import { type ClassifierDenyCause, type DeniedBy, type GateOutcome, type Settlement } from "../gate-outcome.js";
|
|
25
25
|
import type { AskOrigin, AskOriginFacts } from "../ask-origin.js";
|
|
26
26
|
import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
|
|
27
27
|
import type { SafeNotifier } from "../safe-notify.js";
|
|
@@ -32,10 +32,13 @@ export interface SettledAsk {
|
|
|
32
32
|
settlement: Settlement;
|
|
33
33
|
origin: AskOrigin;
|
|
34
34
|
}
|
|
35
|
-
/** The facts a gate pass hands the mint. `deniedBy` is present exactly when the pass refused; `
|
|
36
|
-
*
|
|
35
|
+
/** The facts a gate pass hands the mint. `deniedBy` is present exactly when the pass refused; `cause` only
|
|
36
|
+
* beside it, when the refusing round was a classifier round that did not rule (the station's attestation,
|
|
37
|
+
* read once by the fold or the classifier step); `settled` exactly when the pass consumed an ask's
|
|
38
|
+
* settlement (a person's yes that was later vetoed included). */
|
|
37
39
|
export interface GateExitFacts {
|
|
38
40
|
deniedBy?: DeniedBy;
|
|
41
|
+
cause?: ClassifierDenyCause;
|
|
39
42
|
settled?: SettledAsk;
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
@@ -80,6 +83,10 @@ export interface SettlementLedger {
|
|
|
80
83
|
* for an attested refusal, `policy` otherwise — a veto when a yes rides the settlement); undefined
|
|
81
84
|
* for an allow or an ask. */
|
|
82
85
|
fromDecision(decision: PermissionResult): DeniedBy | undefined;
|
|
86
|
+
/** Read a deny's attested classifier CAUSE for this call (an inherited station's `unavailable` /
|
|
87
|
+
* `parse_error` round travelling through an ancestor wrapper into this fold); undefined for a ruling
|
|
88
|
+
* block, a non-classifier deny, an allow or an ask. */
|
|
89
|
+
causeOf(decision: PermissionResult): ClassifierDenyCause | undefined;
|
|
83
90
|
/** A surviving ask supersedes whatever an earlier decision settled. */
|
|
84
91
|
supersede(): void;
|
|
85
92
|
}
|
|
@@ -208,6 +215,11 @@ export interface GatePass {
|
|
|
208
215
|
* that ended as a refusal). The exit mints the outcome from it beside the ledger's `settled`; a refusal
|
|
209
216
|
* that reaches the exit without it is an engine defect and the mint throws. */
|
|
210
217
|
deniedBy: DeniedBy | undefined;
|
|
218
|
+
/** borrowed-mutable — the FORM of the classifier round behind a deny (`unavailable` / `parse_error`), written
|
|
219
|
+
* beside `deniedBy` by the gate's own classifier step (its attestation, read once) or by the fold / the
|
|
220
|
+
* re-check when an inherited station's attested deny arrives through the chain; undefined for a ruling
|
|
221
|
+
* block and every non-classifier deny. The exit mints `GateDisposition.cause` from it. */
|
|
222
|
+
denyCause: ClassifierDenyCause | undefined;
|
|
211
223
|
/** borrowed-mutable — the standing verdict. The no-policy default until the adjudication station writes the
|
|
212
224
|
* policy's answer; then rewritten by every station that changes the verdict (fold, tightens, org, rule
|
|
213
225
|
* lane, stamp, classifier, sandbox, content lane, resolution, edit chain). */
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
|
|
2
|
-
import { decisionText, engineSettlementOf } from "../tool-policy.js";
|
|
2
|
+
import { classifierDenyCauseOf, decisionText, engineSettlementOf } from "../tool-policy.js";
|
|
3
3
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
4
4
|
import { formatHookFeedback } from "../reminder-mint.js";
|
|
5
5
|
export function mintGateOutcome(facts) {
|
|
6
6
|
const settlement = facts.settled !== undefined ? Object.freeze({ ...facts.settled.settlement, who: Object.freeze({ ...facts.settled.settlement.who }) }) : undefined;
|
|
7
7
|
const outcome = Object.freeze({
|
|
8
|
-
disposition: Object.freeze(facts.deniedBy !== undefined ? { kind: "denied", deniedBy: facts.deniedBy } : { kind: "allowed" }),
|
|
8
|
+
disposition: Object.freeze(facts.deniedBy !== undefined ? { kind: "denied", deniedBy: facts.deniedBy, ...(facts.cause !== undefined ? { cause: facts.cause } : {}) } : { kind: "allowed" }),
|
|
9
9
|
...(facts.settled !== undefined ? { settlement: settlement, origin: facts.settled.origin } : {}),
|
|
10
10
|
});
|
|
11
11
|
const defects = screenGateOutcome(outcome);
|
|
@@ -50,6 +50,9 @@ export function createSettlementLedger(call) {
|
|
|
50
50
|
return undefined;
|
|
51
51
|
return attested !== undefined && SETTLEMENT_IS_REFUSAL[attested.settlement.kind] ? "ask_resolution" : "policy";
|
|
52
52
|
},
|
|
53
|
+
causeOf(decision) {
|
|
54
|
+
return decision.action === "deny" ? classifierDenyCauseOf(decision, call) : undefined;
|
|
55
|
+
},
|
|
53
56
|
supersede() {
|
|
54
57
|
settled = undefined;
|
|
55
58
|
},
|
|
@@ -102,7 +105,7 @@ export async function exitGate(pass) {
|
|
|
102
105
|
}
|
|
103
106
|
if (pass.deniedBy === undefined)
|
|
104
107
|
throw new Error(`the tool gate refused "${toolName}" without a refusing layer — every deny site attributes itself`);
|
|
105
|
-
const gate = mintGateOutcome({ deniedBy: pass.deniedBy, ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}) });
|
|
108
|
+
const gate = mintGateOutcome({ deniedBy: pass.deniedBy, ...(pass.denyCause !== undefined ? { cause: pass.denyCause } : {}), ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}) });
|
|
106
109
|
await notifyPermissionDenied({
|
|
107
110
|
toolName,
|
|
108
111
|
input: cloneObserverInput(pass.currentInput),
|
|
@@ -110,7 +113,6 @@ export async function exitGate(pass) {
|
|
|
110
113
|
reason: denyReason,
|
|
111
114
|
gate,
|
|
112
115
|
...(input.identity !== undefined ? { identity: input.identity } : {}),
|
|
113
|
-
...(pass.decision.action === "deny" && pass.decision.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: pass.decision.classifierUnavailable.cause } } : {}),
|
|
114
116
|
});
|
|
115
117
|
return {
|
|
116
118
|
block: true,
|
|
@@ -25,28 +25,33 @@ import type { PromptEpochArtifact } from "../../prompt-assembly/artifact.js";
|
|
|
25
25
|
import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
|
|
26
26
|
import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
|
|
27
27
|
import { type AutoModeDecider, type AutoModeDenialTracker } from "../auto-mode.js";
|
|
28
|
+
import { type CheckpointSeat } from "../checkpoint-seat.js";
|
|
28
29
|
import { type ComplianceCapability } from "../compliance.js";
|
|
29
30
|
import type { LockedPreflight } from "../locked-config.js";
|
|
30
31
|
import type { StoredSession } from "../session.js";
|
|
31
32
|
import type { OnAsk } from "../tool-policy.js";
|
|
32
33
|
import type { RunnerDeps, RuntimeCaps, TaskEvent, TaskSpec, ToolExecuteContext } from "../types.js";
|
|
33
|
-
import type {
|
|
34
|
+
import type { AutoModeArmFact } from "../wiring-manifest.js";
|
|
35
|
+
import type { AssertAllKeysHandled } from "../ask-origin.js";
|
|
34
36
|
import type { ReadFace } from "../../tools/fs/index.js";
|
|
35
37
|
import type { InheritedGate, Prepared, RunInternals, RunnerSelfSeat, ToolFaceSnapshot } from "./contracts.js";
|
|
36
38
|
import { type BlockedRef } from "./synthetic-tools.js";
|
|
37
39
|
import { RosterBuilder } from "../tool-roster.js";
|
|
38
40
|
export interface PrepareCapsAndWorkflowInput {
|
|
39
41
|
/** borrowed-readonly — the REBOUND spec, whole: the roster map reads `tools`; the mounts read `enableBlockedReport`,
|
|
40
|
-
* `enablePlanMode`, `interactiveTools
|
|
42
|
+
* `enablePlanMode`, `interactiveTools`; the caps/compliance stations read `principal`, `selfOrchestration`,
|
|
41
43
|
* `enableFork`, `getApiKeyAndHeaders`; the Workflow mount forwards `oneShot`, `handsReadOnly`, `interactiveTools`. The
|
|
42
44
|
* self-orchestration predicate takes the whole spec. Never mutated. */
|
|
43
45
|
spec: TaskSpec;
|
|
44
46
|
/** borrowed-readonly — the deployment seats, whole: `runtimeCapsResolver`, `compliancePostureResolver`, `onError`, `onNotice`,
|
|
45
|
-
* `autoMode`, `brain`, `models`,
|
|
47
|
+
* `autoMode`, `brain`, `models`, and the Workflow mount's dozen deps (`workflowScriptRunner`,
|
|
46
48
|
* `workflowGovernanceBaseline`, `workflowRunStore`, `workflowJournalStore`, `workflowScriptStore`, `builtinWorkflows`,
|
|
47
49
|
* `onWorkflowAgentSpawn`, `workflowCompletionNotifier`, `workflowLimits`, `agents`, `builtinAgents`, `onBackgroundChildEvent`).
|
|
48
50
|
* The self-orchestration predicate takes the whole record. */
|
|
49
51
|
deps: RunnerDeps;
|
|
52
|
+
/** borrowed-readonly — the seat this run hands its children (`checkpointSeatOf`, minted once by the driver right after the
|
|
53
|
+
* config doors): the plan-pair face reads the store it names, the Workflow mount hands the seat itself to its children. */
|
|
54
|
+
checkpointSeat: CheckpointSeat | undefined;
|
|
50
55
|
/** borrowed-readonly — the trusted spawn-side channel; the Workflow mount forwards `rootSessionId`, `placementRoot`,
|
|
51
56
|
* `onTaskNotification`, `workflowDepth`, and the capture-floor getter reads `memoryCaptureAncestors`. */
|
|
52
57
|
internals: Pick<RunInternals, "rootSessionId" | "placementRoot" | "onTaskNotification" | "workflowDepth" | "memoryCaptureAncestors" | "workflowParkedResume"> | undefined;
|
|
@@ -145,7 +150,8 @@ export interface PrepareCapsAndWorkflowResult {
|
|
|
145
150
|
/** owned — the observers grant (`allowObservers === true`, explicit opt-in). */
|
|
146
151
|
observersActive: boolean;
|
|
147
152
|
/** owned — the arming outcome as a closed reason (the manifest's auto-mode face). */
|
|
148
|
-
|
|
153
|
+
/** #529 / #692 C-d — the leg's auto-mode arming fact (`armed` ⇔ `reason === "armed"`; `deniedSource` only beside a STATED `denied`). */
|
|
154
|
+
autoModeArmReason: AutoModeArmFact;
|
|
149
155
|
/** owned — the armed classifier, or undefined when any arming arm failed. */
|
|
150
156
|
autoModeDecider: AutoModeDecider | undefined;
|
|
151
157
|
/** owned — the denial tracker built beside the decider (same face, same arming). */
|
|
@@ -162,5 +168,29 @@ export interface PrepareCapsAndWorkflowResult {
|
|
|
162
168
|
current: () => WorkflowSizeGuideline;
|
|
163
169
|
} | undefined;
|
|
164
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* The resolved-caps value screen (the caps-side twin of `refuseInvalidAutoModeSeat`): the arming arm reads
|
|
173
|
+
* `autoMode !== false`, so a resolver that returns a MARSHALLED value — a non-boolean member (`"false"`,
|
|
174
|
+
* `0`, `null`) or a non-record container (`false`, `"false"`, `[]`; the resolver is an HTTP-backed seam the
|
|
175
|
+
* type system does not validate at runtime) — would pass a value meant as a denial straight into arming.
|
|
176
|
+
* Either shape is treated as a disclosed resolver fault: announced through onError (phase config), and
|
|
177
|
+
* coined fail-closed — a bad MEMBER to `autoMode:false` (the rest of the record kept), a bad CONTAINER to
|
|
178
|
+
* the same degrade object the resolver-throw arm uses (every `!== false`-gated cap denied), with
|
|
179
|
+
* `faulted` raised so the posture-keyed consumers read it as an outage, not as a verdict. `undefined`, or
|
|
180
|
+
* a record whose `autoMode` is `undefined`/`true`/`false`, passes untouched (byte-identical).
|
|
181
|
+
*/
|
|
182
|
+
/** The supported capability keys, fenced total over {@link RuntimeCaps}: a key added to the type without a row here
|
|
183
|
+
* is a red build, so the snapshot below can never silently drop a new restriction. */
|
|
184
|
+
declare const RUNTIME_CAP_KEYS: {
|
|
185
|
+
readonly allowWorkflows: true;
|
|
186
|
+
readonly allowFork: true;
|
|
187
|
+
readonly allowObservers: true;
|
|
188
|
+
readonly forceDurableGate: true;
|
|
189
|
+
readonly autoMode: true;
|
|
190
|
+
readonly autoModeDenySource: true;
|
|
191
|
+
readonly allowMemoryOptOut: true;
|
|
192
|
+
};
|
|
193
|
+
export type RuntimeCapKeysCoverEveryCap = AssertAllKeysHandled<Exclude<keyof RuntimeCaps, keyof typeof RUNTIME_CAP_KEYS>>;
|
|
165
194
|
/** The M4 phase body — prepareTask's caps-and-workflow stretch, verbatim (see the module header). */
|
|
166
195
|
export declare function prepareCapsAndWorkflow(input: PrepareCapsAndWorkflowInput): Promise<PrepareCapsAndWorkflowResult>;
|
|
196
|
+
export {};
|
|
@@ -10,7 +10,7 @@ import { autoModeArmingRecipeOf } from "../auto-mode-arming.js";
|
|
|
10
10
|
import { AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "../auto-mode-defaults.js";
|
|
11
11
|
import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
|
|
12
12
|
import { createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
|
|
13
|
-
import {
|
|
13
|
+
import { checkpointStoreOfSeat } from "../checkpoint-seat.js";
|
|
14
14
|
import { COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME, resolveComplianceDenies } from "../compliance.js";
|
|
15
15
|
import { createPresentPlanTool, createEnterPlanModeTool } from "../present-plan-tool.js";
|
|
16
16
|
import { resolveTaskModel } from "../roles.js";
|
|
@@ -19,6 +19,7 @@ import { defaultTaskRegistry } from "../task-registry.js";
|
|
|
19
19
|
import { emitTrace } from "../trace.js";
|
|
20
20
|
import { defineTool, isDefineToolProduct, rebindDefineToolCtx } from "../tools.js";
|
|
21
21
|
const MOUNT_REMEDY = "author it as a plain-object ToolSpec (own `name` and `execute`), hand the defineTool product itself, or wrap a product through the supported wrapper form (a fresh object re-stamped with `stampDefineToolBrand` whose rebind re-wraps the rebound product)";
|
|
22
|
+
import { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "../runtime-caps.js";
|
|
22
23
|
import { derivedRouteFallsBack } from "./derived-route-fallback.js";
|
|
23
24
|
import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
|
|
24
25
|
function classifierReplyText(reply) {
|
|
@@ -42,6 +43,24 @@ function assembleParentCaptureState(o, i, ctl, ancestors) {
|
|
|
42
43
|
});
|
|
43
44
|
return o instanceof Promise || i instanceof Promise ? Promise.all([o, i]).then(([ov, iv]) => build(ov, iv)) : build(o, i);
|
|
44
45
|
}
|
|
46
|
+
const RUNTIME_CAP_KEYS = {
|
|
47
|
+
allowWorkflows: true,
|
|
48
|
+
allowFork: true,
|
|
49
|
+
allowObservers: true,
|
|
50
|
+
forceDurableGate: true,
|
|
51
|
+
autoMode: true,
|
|
52
|
+
autoModeDenySource: true,
|
|
53
|
+
allowMemoryOptOut: true,
|
|
54
|
+
};
|
|
55
|
+
function snapshotRuntimeCaps(record) {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const key of Object.keys(RUNTIME_CAP_KEYS)) {
|
|
58
|
+
const v = record[key];
|
|
59
|
+
if (v !== undefined)
|
|
60
|
+
out[key] = v;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
45
64
|
function screenAutoModeCap(caps, onError, sessionId) {
|
|
46
65
|
if (caps === undefined)
|
|
47
66
|
return { caps, faulted: false };
|
|
@@ -49,7 +68,20 @@ function screenAutoModeCap(caps, onError, sessionId) {
|
|
|
49
68
|
onError?.(new Error(`runtimeCapsResolver returned a non-record value (${JSON.stringify(caps)}) — read as a resolver fault: every per-principal capability is DENIED for this run (auto mode is not armed; asks flow the original chain). Return a RuntimeCaps object or undefined.`), { phase: "config", sessionId });
|
|
50
69
|
return { caps: { allowWorkflows: false, allowFork: false, autoMode: false }, faulted: true };
|
|
51
70
|
}
|
|
52
|
-
|
|
71
|
+
let record;
|
|
72
|
+
try {
|
|
73
|
+
record = snapshotRuntimeCaps(caps);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
onError?.(new Error(`runtimeCapsResolver's record threw while being read (${err instanceof Error ? err.message : String(err)}) — read as a resolver fault: every per-principal capability is DENIED for this run (auto mode is not armed; asks flow the original chain).`), { phase: "config", sessionId });
|
|
77
|
+
return { caps: { allowWorkflows: false, allowFork: false, autoMode: false }, faulted: true };
|
|
78
|
+
}
|
|
79
|
+
const { autoMode, autoModeDenySource } = record;
|
|
80
|
+
if (autoModeDenySource !== undefined && (!isAutoModeDenySource(autoModeDenySource) || autoMode !== false)) {
|
|
81
|
+
onError?.(new Error(`runtimeCapsResolver returned autoModeDenySource=${JSON.stringify(autoModeDenySource)} beside autoMode=${JSON.stringify(autoMode)} — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow the original chain). Name a source only beside autoMode:false, and only one of ${AUTO_MODE_DENY_SOURCES.join("|")}.`), { phase: "config", sessionId });
|
|
82
|
+
const { autoModeDenySource: _refused, ...rest } = record;
|
|
83
|
+
return { caps: { ...rest, autoMode: false }, faulted: true };
|
|
84
|
+
}
|
|
53
85
|
if (record.autoMode === undefined || typeof record.autoMode === "boolean")
|
|
54
86
|
return { caps: record, faulted: false };
|
|
55
87
|
onError?.(new Error(`runtimeCapsResolver returned a non-boolean autoMode (${JSON.stringify(record.autoMode)}) — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow the original chain). Return true, false, or omit the key.`), { phase: "config", sessionId });
|
|
@@ -67,17 +99,20 @@ function mintPersistedArming(am, laneRule, classifierSystemPrompt, onError, sess
|
|
|
67
99
|
}
|
|
68
100
|
return arming;
|
|
69
101
|
}
|
|
70
|
-
function autoModeArmReasonOf(intent, facePresent,
|
|
102
|
+
function autoModeArmReasonOf(intent, facePresent, caps, capsFaulted) {
|
|
71
103
|
if (!intent)
|
|
72
|
-
return "no_intent";
|
|
104
|
+
return { armed: false, reason: "no_intent" };
|
|
73
105
|
if (!facePresent)
|
|
74
|
-
return "no_face";
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
return { armed: false, reason: "no_face" };
|
|
107
|
+
if (caps?.autoMode === false) {
|
|
108
|
+
if (capsFaulted)
|
|
109
|
+
return { armed: false, reason: "resolver_fault" };
|
|
110
|
+
return { armed: false, reason: "denied", ...(caps.autoModeDenySource !== undefined ? { deniedSource: caps.autoModeDenySource } : {}) };
|
|
111
|
+
}
|
|
112
|
+
return { armed: true, reason: "armed" };
|
|
78
113
|
}
|
|
79
114
|
export async function prepareCapsAndWorkflow(input) {
|
|
80
|
-
const { spec, deps, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
|
|
115
|
+
const { spec, deps, checkpointSeat, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
|
|
81
116
|
const roster = new RosterBuilder(toolEffects);
|
|
82
117
|
for (const t of spec.tools ?? []) {
|
|
83
118
|
const mounted = mountCallerTool(t);
|
|
@@ -112,7 +147,7 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
112
147
|
roster.mount(createReportFindingsTool(), { source: "builtin", mountedBy: "always" });
|
|
113
148
|
}
|
|
114
149
|
if (spec.enablePlanMode === true && spec.interactiveTools !== false) {
|
|
115
|
-
const planReviewFace =
|
|
150
|
+
const planReviewFace = checkpointStoreOfSeat(checkpointSeat) !== undefined;
|
|
116
151
|
if (spec.interactiveTools === true || planReviewFace) {
|
|
117
152
|
roster.mount(defineTool(createPresentPlanTool(requestReview)), { source: "builtin", mountedBy: "optional" });
|
|
118
153
|
if (planReviewFace) {
|
|
@@ -173,7 +208,7 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
173
208
|
let autoModeDecider;
|
|
174
209
|
let autoModeDenialTracking;
|
|
175
210
|
let autoModeArming;
|
|
176
|
-
const autoModeArmReason = autoModeArmReasonOf(autoModeIntent, deps.autoMode !== undefined, runtimeCaps
|
|
211
|
+
const autoModeArmReason = autoModeArmReasonOf(autoModeIntent, deps.autoMode !== undefined, runtimeCaps, runtimeCapsFaulted);
|
|
177
212
|
if (autoModeIntent && deps.autoMode !== undefined && runtimeCaps?.autoMode !== false) {
|
|
178
213
|
const am = deps.autoMode;
|
|
179
214
|
let classifierModel;
|
|
@@ -297,7 +332,7 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
297
332
|
},
|
|
298
333
|
...(spec.handsReadOnly === true ? { parentHandsReadOnly: true } : {}), ...(spec.interactiveTools === false ? { parentInteractiveTools: false } : {}),
|
|
299
334
|
onNotice: deps.onNotice,
|
|
300
|
-
|
|
335
|
+
...(checkpointSeat !== undefined ? { parentCheckpointStore: checkpointSeat } : {}),
|
|
301
336
|
parentCenterArtifactDigest: () => input.centerAdoption()?.artifact.artifactDigest,
|
|
302
337
|
parentCenterSourceRevision: () => input.centerAdoption()?.sourceRevision,
|
|
303
338
|
...(forwardEvent ? { forwardEvent } : {}),
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* gate-machinery block, kept as the block's one guard here); the `tool_result` station keys off the hook record alone, as
|
|
20
20
|
* it always did.
|
|
21
21
|
*
|
|
22
|
-
* Every station this file holds is a `
|
|
22
|
+
* Every station this file holds is a `deniedBy` / settlement WRITE site the decomposition's next slice maps onto one
|
|
23
23
|
* gate outcome; nothing here changes a word, a source or an order — the stations are moved, not re-adjudicated.
|
|
24
24
|
*/
|
|
25
25
|
import type { AgentHarness, AgentTool } from "../../internal/harness.js";
|