@sema-agent/core 7.19.0 → 7.20.1
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 +199 -2
- package/dist/agents/subagent.js +12 -20
- package/dist/agents/teacher.js +2 -5
- package/dist/agents/verify.js +21 -10
- package/dist/brain/anthropic.js +3 -1
- package/dist/brain/open-responses.js +4 -2
- package/dist/brain/openai.js +3 -1
- package/dist/brain/stream-engine.d.ts +4 -0
- package/dist/brain/stream-engine.js +4 -0
- package/dist/core/auto-mode.d.ts +37 -18
- package/dist/core/auto-mode.js +1 -1
- package/dist/core/child-clamp-seats.d.ts +79 -0
- package/dist/core/child-clamp-seats.js +29 -0
- package/dist/core/engine-notice.d.ts +76 -49
- package/dist/core/fs-write-gate-policy.d.ts +17 -9
- package/dist/core/fs-write-gate-policy.js +51 -4
- package/dist/core/gate-bounded-await.d.ts +38 -0
- package/dist/core/gate-bounded-await.js +53 -0
- package/dist/core/gate-fold.js +16 -48
- package/dist/core/gate-lanes.js +11 -3
- package/dist/core/gate-outcome.d.ts +9 -6
- package/dist/core/governance-codes.d.ts +16 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +8 -8
- package/dist/core/memory-engine/engine.d.ts +14 -9
- package/dist/core/memory-engine/engine.js +21 -8
- package/dist/core/memory-engine/layout.d.ts +31 -0
- package/dist/core/memory-engine/layout.js +68 -3
- package/dist/core/memory-engine/tools.js +6 -0
- package/dist/core/memory-engine/types.d.ts +24 -0
- package/dist/core/memory.d.ts +19 -0
- package/dist/core/memory.js +12 -0
- package/dist/core/remote-env.d.ts +32 -0
- package/dist/core/remote-env.js +4 -1
- package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
- package/dist/core/runner/advertised-writable-dirs.js +51 -3
- package/dist/core/runner/announce-once-ledger.d.ts +15 -0
- package/dist/core/runner/announce-once-ledger.js +8 -0
- package/dist/core/runner/classifier-ladder.d.ts +5 -0
- package/dist/core/runner/classifier-ladder.js +13 -2
- package/dist/core/runner/contracts.d.ts +44 -1
- package/dist/core/runner/denial-limit-arms.d.ts +14 -1
- package/dist/core/runner/denial-limit-arms.js +5 -4
- package/dist/core/runner/env-capability-census.d.ts +30 -0
- package/dist/core/runner/env-capability-census.js +38 -0
- package/dist/core/runner/forward-sink.d.ts +32 -0
- package/dist/core/runner/forward-sink.js +41 -0
- package/dist/core/runner/gate-exit.d.ts +4 -0
- package/dist/core/runner/gate-exit.js +4 -1
- package/dist/core/runner/git-status-frame.d.ts +0 -9
- package/dist/core/runner/listing-fold.d.ts +57 -0
- package/dist/core/runner/listing-fold.js +75 -0
- package/dist/core/runner/prepare-execution-env.d.ts +1 -1
- package/dist/core/runner/prepare-execution-env.js +2 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
- package/dist/core/runner/prepare-gate-stations.js +1 -3
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +3 -5
- package/dist/core/runner/prepare-listings.d.ts +3 -0
- package/dist/core/runner/prepare-listings.js +4 -2
- package/dist/core/runner/prepare-memory.js +7 -1
- package/dist/core/runner/prepare-policy-chain.js +5 -5
- package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
- package/dist/core/runner/prepare-run-refs.d.ts +4 -2
- package/dist/core/runner/prepare-run-refs.js +5 -4
- package/dist/core/runner/prepare-task.js +8 -6
- package/dist/core/runner/prepare-wiring-manifest.d.ts +8 -1
- package/dist/core/runner/prepare-wiring-manifest.js +5 -2
- package/dist/core/runner/run-clock-and-content.js +3 -7
- package/dist/core/runner/run-harness-handlers.js +10 -10
- package/dist/core/runner/run-leg.js +28 -19
- package/dist/core/runner/run-settle-and-teardown.js +2 -5
- package/dist/core/runner/run-turn-boundary.js +44 -33
- package/dist/core/runner/runtask.js +2 -1
- package/dist/core/runner/synthetic-tools.d.ts +13 -39
- package/dist/core/runner/synthetic-tools.js +15 -59
- package/dist/core/runner/task-root-canonical.d.ts +45 -0
- package/dist/core/runner/task-root-canonical.js +18 -0
- package/dist/core/runner/turn-attachments.d.ts +125 -49
- package/dist/core/runner/turn-attachments.js +124 -66
- package/dist/core/runner/write-protection-station.d.ts +59 -0
- package/dist/core/runner/write-protection-station.js +32 -0
- package/dist/core/safe-notify.d.ts +5 -0
- package/dist/core/safe-notify.js +24 -2
- package/dist/core/sensitive-path-policy.d.ts +53 -27
- package/dist/core/sensitive-path-policy.js +16 -26
- package/dist/core/skill-listing-budget.d.ts +34 -40
- package/dist/core/skill-listing-budget.js +25 -26
- package/dist/core/skills-directory.js +17 -10
- package/dist/core/task-registry.js +2 -1
- package/dist/core/task-spec.d.ts +8 -7
- package/dist/core/tool-policy.d.ts +12 -1
- package/dist/core/tool-policy.js +39 -25
- package/dist/core/wiring-manifest.d.ts +31 -0
- package/dist/core/wiring-manifest.js +1 -0
- package/dist/core/write-protect.d.ts +124 -18
- package/dist/core/write-protect.js +91 -34
- package/dist/index.d.ts +6 -5
- package/dist/index.js +6 -5
- package/dist/orchestration/workflow-primitives.js +10 -20
- package/dist/stores/file/memory-store.d.ts +5 -2
- package/dist/stores/file/memory-store.js +8 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +13 -2
- package/dist/tools/fs/bash-readonly-classifier.js +318 -36
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +12 -25
- package/dist/tools/fs/fs-shared.d.ts +22 -0
- package/dist/tools/fs/fs-shared.js +30 -0
- package/dist/tools/fs/index.d.ts +1 -1
- package/dist/tools/fs/index.js +1 -1
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +25 -3
- package/wire-changes/core@7.20.0.json +568 -0
- package/wire-changes/core@7.20.1.json +183 -0
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* planted path is never written through); an advertiser whose directory is created elsewhere passes
|
|
21
21
|
* no marker and an absent directory narrows.
|
|
22
22
|
*
|
|
23
|
+
* A directory the fence already holds is never admitted as a HOLE in it: a candidate that resolves onto
|
|
24
|
+
* a root — or above one — would turn that whole root into the advertised, write-gate-exempt domain, so
|
|
25
|
+
* an ordinary source write inside the workspace would stop asking. One predicate
|
|
26
|
+
* ({@link swallowedFenceRoot}) states that, and the write gate's configured domains apply the same one.
|
|
27
|
+
*
|
|
23
28
|
* Reverse check (the seat's own invariant): nothing enters `roots` except through `admit`, and every
|
|
24
29
|
* caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so no
|
|
25
30
|
* directory reaches the ROOTS FENCE through this seat without the prompt advertising it, and none
|
|
@@ -55,12 +60,65 @@ export interface AdvertisedWritableDir {
|
|
|
55
60
|
}
|
|
56
61
|
/** The skip announcement — the one loud outlet for a directory that was advertised but could not be admitted. */
|
|
57
62
|
export type AdvertisedWritableDirSkipped = (dir: AdvertisedWritableDir, reason: string) => void;
|
|
63
|
+
/**
|
|
64
|
+
* ONE rule for both halves of the auto-approved write surface: a directory offered as a WRITABLE DOMAIN
|
|
65
|
+
* (an advertised writable directory here; a configured `exemptDirs`/`acceptDirs` entry at the write
|
|
66
|
+
* gate) must not SWALLOW a root the fence already holds. Returns the first root the candidate equals or
|
|
67
|
+
* is an ancestor of — the one to name in the refusal — else `undefined`.
|
|
68
|
+
*
|
|
69
|
+
* Why a domain that contains a root is not a domain: the point of the domain is to exempt a small area
|
|
70
|
+
* the prompt (or the deployment) named. A candidate resolving onto the task root — a symlinked
|
|
71
|
+
* scratchpad or memory home is all it takes, and the resolution is what makes it invisible to the
|
|
72
|
+
* spelling — exempts the ENTIRE workspace instead, turning every ordinary source write from `ask` into
|
|
73
|
+
* `allow`. Refusing the admission is the fix; a special case inside the gate ("ignore this domain when
|
|
74
|
+
* it contains the root") would be the same rule written twice, in the place where it is easiest to
|
|
75
|
+
* forget. Both arguments must already be CANONICAL — this compares spellings, it does not resolve them.
|
|
76
|
+
*/
|
|
77
|
+
export declare function swallowedFenceRoot(domainCanonical: string, fenceRootsCanonical: readonly string[]): string | undefined;
|
|
58
78
|
export declare class AdvertisedWritableDirs {
|
|
59
79
|
private readonly env;
|
|
60
80
|
private readonly onSkipped;
|
|
81
|
+
/** The extra fence roots the SPEC declares (`additionalDirectories` / `additionalReadDirectories`), as
|
|
82
|
+
* spelled. Resolved by this seat rather than awaited from the hands phase — the memory advertiser is
|
|
83
|
+
* admitted BEFORE that phase, so a list that only grew there made the rule depend on phase order. An
|
|
84
|
+
* entry that does not resolve is not a fence root at all (the hands phase reads the SAME memoized
|
|
85
|
+
* answer and skips it too, announcing it there), so there is nothing here for it to guard. */
|
|
86
|
+
private readonly declaredRootSpellings;
|
|
61
87
|
private readonly admitted;
|
|
62
88
|
private readonly verdicts;
|
|
63
|
-
|
|
89
|
+
/** The CANONICAL roots the fence holds: the task root at construction, plus every root the spec
|
|
90
|
+
* DECLARES, resolved by this seat before it judges anything (see {@link resolveDeclaredRoots}) —
|
|
91
|
+
* so which advertiser runs first cannot decide what the rule sees. */
|
|
92
|
+
private readonly fenceRoots;
|
|
93
|
+
/** Latched after the declared spellings have been resolved once (the resolution is per seat, not per call). */
|
|
94
|
+
private declaredRootsResolved;
|
|
95
|
+
/** ONE answer per declared spelling, for this seat AND for the hands phase that folds the same entries
|
|
96
|
+
* into the allowlist ({@link canonicalizeDeclaredRoot}): two independent lookups of one path can
|
|
97
|
+
* DISAGREE, and a root outside this rule's list while inside the fence is exactly the gap. */
|
|
98
|
+
private readonly declaredRootAnswers;
|
|
99
|
+
constructor(env: ExecutionEnv, onSkipped: AdvertisedWritableDirSkipped,
|
|
100
|
+
/** The run's task root, CANONICAL (the driver's one resolution — see `task-root-canonical.ts`). */
|
|
101
|
+
taskRootCanonical: string,
|
|
102
|
+
/** The extra fence roots the SPEC declares (`additionalDirectories` / `additionalReadDirectories`), as
|
|
103
|
+
* spelled. Resolved by this seat rather than awaited from the hands phase — the memory advertiser is
|
|
104
|
+
* admitted BEFORE that phase, so a list that only grew there made the rule depend on phase order. An
|
|
105
|
+
* entry that does not resolve is not a fence root at all (the hands phase reads the SAME memoized
|
|
106
|
+
* answer and skips it too, announcing it there), so there is nothing here for it to guard. */
|
|
107
|
+
declaredRootSpellings?: readonly string[]);
|
|
108
|
+
/** Declare one more CANONICAL root the fence holds (an `additionalDirectories` /
|
|
109
|
+
* `additionalReadDirectories` entry, once the hands phase has resolved it). Declaring is
|
|
110
|
+
* idempotent and order-free; a root declared after an advertiser was judged does not re-open it. */
|
|
111
|
+
declareFenceRoot(canonical: string): void;
|
|
112
|
+
/**
|
|
113
|
+
* The canonical form of a DECLARED fence-root spelling — resolved at most once per spelling and
|
|
114
|
+
* served from the memo thereafter. The hands phase calls this instead of canonicalizing the same
|
|
115
|
+
* entries again, so the list this rule judges against and the allowlist the fence ends up holding
|
|
116
|
+
* are built from ONE answer per path: a lookup that fails for this seat fails for that phase too,
|
|
117
|
+
* and a root cannot be outside the rule's view while being inside the fence.
|
|
118
|
+
*/
|
|
119
|
+
canonicalizeDeclaredRoot(spelled: string): Promise<Awaited<ReturnType<ExecutionEnv["canonicalPath"]>>>;
|
|
120
|
+
/** Resolve the spec-declared root spellings into {@link fenceRoots}, exactly once per seat. */
|
|
121
|
+
private resolveDeclaredRoots;
|
|
64
122
|
/** The admitted roots, CANONICAL, in admission order, deduplicated. The hands phase folds this list into the
|
|
65
123
|
* write-capable containment allowlist before the toolkit compiles; it is not a live view after that. */
|
|
66
124
|
get roots(): readonly string[];
|
|
@@ -1,11 +1,52 @@
|
|
|
1
|
+
import { isWithin } from "./session-rule-policy.js";
|
|
2
|
+
export function swallowedFenceRoot(domainCanonical, fenceRootsCanonical) {
|
|
3
|
+
for (const root of fenceRootsCanonical) {
|
|
4
|
+
if (root && isWithin(domainCanonical, root))
|
|
5
|
+
return root;
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
1
9
|
export class AdvertisedWritableDirs {
|
|
2
10
|
env;
|
|
3
11
|
onSkipped;
|
|
12
|
+
declaredRootSpellings;
|
|
4
13
|
admitted = [];
|
|
5
14
|
verdicts = new Map();
|
|
6
|
-
|
|
15
|
+
fenceRoots;
|
|
16
|
+
declaredRootsResolved;
|
|
17
|
+
declaredRootAnswers = new Map();
|
|
18
|
+
constructor(env, onSkipped, taskRootCanonical, declaredRootSpellings = []) {
|
|
7
19
|
this.env = env;
|
|
8
20
|
this.onSkipped = onSkipped;
|
|
21
|
+
this.declaredRootSpellings = declaredRootSpellings;
|
|
22
|
+
this.fenceRoots = [taskRootCanonical];
|
|
23
|
+
}
|
|
24
|
+
declareFenceRoot(canonical) {
|
|
25
|
+
if (canonical && !this.fenceRoots.includes(canonical))
|
|
26
|
+
this.fenceRoots.push(canonical);
|
|
27
|
+
}
|
|
28
|
+
async canonicalizeDeclaredRoot(spelled) {
|
|
29
|
+
const memo = this.declaredRootAnswers.get(spelled);
|
|
30
|
+
if (memo !== undefined)
|
|
31
|
+
return memo;
|
|
32
|
+
const c = await this.env.canonicalPath(spelled);
|
|
33
|
+
const first = this.declaredRootAnswers.get(spelled);
|
|
34
|
+
if (first !== undefined)
|
|
35
|
+
return first;
|
|
36
|
+
this.declaredRootAnswers.set(spelled, c);
|
|
37
|
+
if (c.ok)
|
|
38
|
+
this.declareFenceRoot(c.value);
|
|
39
|
+
return c;
|
|
40
|
+
}
|
|
41
|
+
resolveDeclaredRoots() {
|
|
42
|
+
this.declaredRootsResolved ??= (async () => {
|
|
43
|
+
for (const spelled of this.declaredRootSpellings) {
|
|
44
|
+
if (!spelled || !spelled.trim())
|
|
45
|
+
continue;
|
|
46
|
+
await this.canonicalizeDeclaredRoot(spelled);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
return this.declaredRootsResolved;
|
|
9
50
|
}
|
|
10
51
|
get roots() {
|
|
11
52
|
return this.admitted;
|
|
@@ -41,8 +82,15 @@ export class AdvertisedWritableDirs {
|
|
|
41
82
|
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
83
|
return false;
|
|
43
84
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
85
|
+
if (this.admitted.includes(c.value))
|
|
86
|
+
return true;
|
|
87
|
+
await this.resolveDeclaredRoots();
|
|
88
|
+
const swallowed = swallowedFenceRoot(c.value, [...this.fenceRoots, ...this.admitted]);
|
|
89
|
+
if (swallowed !== undefined) {
|
|
90
|
+
this.onSkipped(dir, `it resolves to ${c.value}, which covers ${swallowed === this.fenceRoots[0] ? "task root" : "root"} ${swallowed} — a directory that holds a root is not a hole in the fence but the whole of it, and admitting it would auto-approve every write under that root`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
this.admitted.push(c.value);
|
|
46
94
|
return true;
|
|
47
95
|
}
|
|
48
96
|
}
|
|
@@ -27,6 +27,21 @@ export interface AnnounceOnceSession {
|
|
|
27
27
|
getAnnouncedListing(): Promise<AnnouncedListingSnapshot | undefined>;
|
|
28
28
|
appendAnnouncedListing(snapshot: AnnouncedListingSnapshotInput): Promise<string>;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Flush the PARKED announced-listing snapshot write (`FrameReceiptLedger.listingSnapshotOwed`, set by a
|
|
32
|
+
* frame receipt's commit inside the `message_end` walk) at a serialization point — the boundary's end,
|
|
33
|
+
* the prompt's settle. Best-effort: a lost append is a duplicate announcement next run. Never while
|
|
34
|
+
* prepare's branch read failed: a whole-snapshot write would replace the branch's record with this
|
|
35
|
+
* leg's partial view (the flag is cleared either way — nothing is owed on such a leg).
|
|
36
|
+
*/
|
|
37
|
+
export declare function flushOwedAnnouncedSnapshot(prepared: {
|
|
38
|
+
frameReceipts: {
|
|
39
|
+
listingSnapshotOwed: boolean;
|
|
40
|
+
};
|
|
41
|
+
announcedSnapshotRecovered: boolean;
|
|
42
|
+
announcedListingsRef: AnnouncedListingSnapshotInput;
|
|
43
|
+
session: Pick<AnnounceOnceSession, "appendAnnouncedListing">;
|
|
44
|
+
}): Promise<void>;
|
|
30
45
|
/** The run-local ledger — see the module header for the two commit disciplines it serves. */
|
|
31
46
|
export interface AnnounceOnceLedger {
|
|
32
47
|
/** The nearest announced snapshot on the branch as read at prepare (undefined = pre-entry
|
|
@@ -29,6 +29,14 @@ export function withAnnounceDigests(existing, add) {
|
|
|
29
29
|
function sameFailure(a, b) {
|
|
30
30
|
return a.name === b.name && a.error === b.error;
|
|
31
31
|
}
|
|
32
|
+
export async function flushOwedAnnouncedSnapshot(prepared) {
|
|
33
|
+
if (!prepared.frameReceipts.listingSnapshotOwed)
|
|
34
|
+
return;
|
|
35
|
+
prepared.frameReceipts.listingSnapshotOwed = false;
|
|
36
|
+
if (!prepared.announcedSnapshotRecovered)
|
|
37
|
+
return;
|
|
38
|
+
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
39
|
+
}
|
|
32
40
|
export async function loadAnnounceOnceLedger(session, checkpointMirror, onError) {
|
|
33
41
|
let recovered = true;
|
|
34
42
|
const entry = await session.getAnnouncedListing().catch(() => {
|
|
@@ -87,6 +87,11 @@ export type ClassifierSeat = {
|
|
|
87
87
|
* all ⇒ the main model, `main_only`.
|
|
88
88
|
* The relative pick follows the run's MAIN model on purpose: a side channel that resolved without it is the
|
|
89
89
|
* misfit the ticket names (a `flash` classifier above a `lite` main; two tiers below a `max` main).
|
|
90
|
+
*
|
|
91
|
+
* @contract classifier.seat.one_resolver — this function is the ONE answer to "which model judges this run's
|
|
92
|
+
* gated calls". The engine's arming calls it and a host that DISPLAYS the seat (a capabilities face, an
|
|
93
|
+
* operator pane) calls the same exported function rather than re-deriving the pick from catalog, roles and
|
|
94
|
+
* tiers: a second derivation is a second answer, and the two drift the moment either side's rules move.
|
|
90
95
|
*/
|
|
91
96
|
export declare function classifierSeatModel(args: {
|
|
92
97
|
deps: Pick<RunnerDeps, "models" | "roles" | "tiers">;
|
|
@@ -2,7 +2,7 @@ import { sameRouteIdentity } from "../../brain/route-adjudicator.js";
|
|
|
2
2
|
import { thinkingOffSpelled } from "../../brain/reasoning.js";
|
|
3
3
|
import { AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER, AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS, AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS } from "../auto-mode-defaults.js";
|
|
4
4
|
import { autoModeClassifierFallbackOf, parseAutoModeResponse } from "../auto-mode.js";
|
|
5
|
-
import { DEFAULT_TIER_ORDER, resolveTaskModel, roleSpecNamesModel } from "../roles.js";
|
|
5
|
+
import { DEFAULT_TIER_ORDER, expandTiers, resolveTaskModel, roleSpecNamesModel } from "../roles.js";
|
|
6
6
|
import { emitTrace } from "../trace.js";
|
|
7
7
|
import { deliverEngineNotice } from "../types.js";
|
|
8
8
|
export function classifierLadderRungs(knob, classifier, main) {
|
|
@@ -20,7 +20,8 @@ function sameRouteQuietly(a, b) {
|
|
|
20
20
|
}
|
|
21
21
|
export const CLASSIFIER_ROLE_DEFAULT_REASONS = ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
|
|
22
22
|
export function classifierSeatModel(args) {
|
|
23
|
-
const {
|
|
23
|
+
const { main } = args;
|
|
24
|
+
const deps = normalizeSeatDeps(args.deps);
|
|
24
25
|
if (roleSpecNamesModel(deps.roles?.classifier)) {
|
|
25
26
|
try {
|
|
26
27
|
return { model: resolveTaskModel({ modelRole: "classifier" }, deps).model };
|
|
@@ -42,6 +43,16 @@ export function classifierSeatModel(args) {
|
|
|
42
43
|
return { model: main, defaulted: "main_only" };
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
function normalizeSeatDeps(deps) {
|
|
47
|
+
if (deps.tiers === undefined)
|
|
48
|
+
return deps;
|
|
49
|
+
try {
|
|
50
|
+
return { ...deps, models: expandTiers(deps.models, deps.tiers) };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return deps;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
function tierBelowMain(deps, main) {
|
|
46
57
|
if (deps.tiers === undefined || Object.keys(deps.tiers).length === 0 || deps.models === undefined)
|
|
47
58
|
return undefined;
|
|
@@ -173,6 +173,35 @@ export interface CommittedPause {
|
|
|
173
173
|
scope: string;
|
|
174
174
|
gatedCallId?: string;
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* One engine frame whose commit waits for the receipt of the message carrying it.
|
|
178
|
+
*
|
|
179
|
+
* @contract frames.commit_on_receipt — an engine-minted frame that rides a user message (the git-status
|
|
180
|
+
* frame, an `agent_listing` or `skills_listing` frame) is recorded as announced ONLY when the run loop's
|
|
181
|
+
* `message_end` walk sees a persisted user entry whose text equals the carrying text it was registered
|
|
182
|
+
* with; every receipt with that text settles on that one entry. Until then nothing is recorded — a
|
|
183
|
+
* message that never reaches the transcript (a pre-append failure, a suppressed or undrained steer, a
|
|
184
|
+
* leg that ends first) leaves the frame unannounced, and the next boundary or leg renders it again.
|
|
185
|
+
* The error asymmetry is one-directional by design: a lost receipt costs a duplicate frame, never a
|
|
186
|
+
* fact the model was recorded as told and was not.
|
|
187
|
+
*/
|
|
188
|
+
export interface FrameReceipt {
|
|
189
|
+
/** The carrying message's exact text (its first text block) — the receipt's match key. */
|
|
190
|
+
text: string;
|
|
191
|
+
/** Which frame this receipt settles. A boundary collector must not render a listing whose receipt is
|
|
192
|
+
* still pending (the frame is queued, not yet in the transcript). */
|
|
193
|
+
source: "git_status" | "agent_listing" | "skills_listing";
|
|
194
|
+
/** Runs inside the `message_end` walk (synchronous, never awaited): advances the run-local record
|
|
195
|
+
* and parks any session write for the next serialization point. */
|
|
196
|
+
commit: (entryId: string) => void;
|
|
197
|
+
}
|
|
198
|
+
/** The leg's frame-receipt ledger — see {@link FrameReceipt}. `listingSnapshotOwed` is the parked
|
|
199
|
+
* announced-listing snapshot write (a session append from inside the walk would race the loop's own
|
|
200
|
+
* next transcript append); the boundary's end and the prompt's settle flush it. */
|
|
201
|
+
export interface FrameReceiptLedger {
|
|
202
|
+
pending: FrameReceipt[];
|
|
203
|
+
listingSnapshotOwed: boolean;
|
|
204
|
+
}
|
|
176
205
|
export interface Prepared {
|
|
177
206
|
harness: AgentHarness;
|
|
178
207
|
/** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
|
|
@@ -1017,6 +1046,7 @@ export interface Prepared {
|
|
|
1017
1046
|
toolName: string;
|
|
1018
1047
|
seedAnnounced: boolean;
|
|
1019
1048
|
models?: readonly string[];
|
|
1049
|
+
budget: ResolvedSkillListingBudget;
|
|
1020
1050
|
};
|
|
1021
1051
|
/** [c209-C] skills_listing counterpart of {@link agentListing} — the normalized skills METADATA
|
|
1022
1052
|
* projection (names/descriptions/attachment paths, never bodies; the exact list `createSkillTool`
|
|
@@ -1038,6 +1068,9 @@ export interface Prepared {
|
|
|
1038
1068
|
* announce-once-ledger.ts): seeded at prepare from the branch's record, advanced by the run loop
|
|
1039
1069
|
* on intact delivery of the tools_delta `failedServers` arm / the mcp_dropped_tools frame. */
|
|
1040
1070
|
announcedListingsRef: AnnouncedListingSnapshotInput;
|
|
1071
|
+
/** The frame-receipt ledger — the ONE commit point of every engine frame that rides a user message
|
|
1072
|
+
* (the git-status frame, the agent and skills listing frames). Always present; per leg. */
|
|
1073
|
+
frameReceipts: FrameReceiptLedger;
|
|
1041
1074
|
/** env-tail migration — the git-status frame lane's run-local state: this leg's
|
|
1042
1075
|
* resolved frame (probe outcome rendered + hashed at prepare), the announced `(kind, hash)`
|
|
1043
1076
|
* mirror the checkpoint serializer reads, the trim-protection slot the request-build context
|
|
@@ -1796,7 +1829,11 @@ export interface RunInternals {
|
|
|
1796
1829
|
* `forwardSubagentEvents: true` it ALSO forwards the child's content events (`text_delta` / `text_end` /
|
|
1797
1830
|
* `reasoning_delta` / `tool_start` / `tool_end` — the subagent viewing pane, carrying the same UNTRUSTED-RAW/consumer-must-redact
|
|
1798
1831
|
* contract as the main stream's tool events). Either way the child stream is NEVER merged into the parent's
|
|
1799
|
-
* MODEL context (this is purely a render channel). Absent unless the deployment opted in.
|
|
1832
|
+
* MODEL context (this is purely a render channel). Absent unless the deployment opted in. CONTAINED at
|
|
1833
|
+
* one door: every engine site feeding this seat goes through `forwardSafely` (`forward-sink.ts`), so a
|
|
1834
|
+
* sink that throws — or, since an `async` function satisfies this `=> void` type, one that returns a
|
|
1835
|
+
* REJECTING promise — drops its frame and nothing else, never awaited
|
|
1836
|
+
* (`@contract runtask.forward_sink_never_faults_the_run`).
|
|
1800
1837
|
*/
|
|
1801
1838
|
onForwardEvent?: (event: TaskEvent) => void;
|
|
1802
1839
|
/**
|
|
@@ -1815,6 +1852,12 @@ export interface RunInternals {
|
|
|
1815
1852
|
/** The run-local per-tool-call detach hub. runtask creates it and exposes
|
|
1816
1853
|
* `TaskStream.detach(toolCallId)`; the hands Bash tool threads `signalFor(toolCallId)` into env.exec. */
|
|
1817
1854
|
detachHub?: import("../tool-detach.js").ToolDetachHub;
|
|
1855
|
+
/** The run's forwarding-failure bookkeeping for {@link onForwardEvent} — ENGINE-owned and opaque,
|
|
1856
|
+
* minted once per run by `Runner.runTaskStream` and carried by every later COPY of this object (the
|
|
1857
|
+
* prepare re-spread, a placement-root rebind, a child's snapshot), which is what makes "announced once
|
|
1858
|
+
* per run" true across the lanes that forward. A caller value is overridden like the refs beside it;
|
|
1859
|
+
* absent ⇒ the door mints a per-object one, so containment never depends on the threading. */
|
|
1860
|
+
forwardScope?: import("./forward-sink.js").ForwardSinkScope;
|
|
1818
1861
|
/**
|
|
1819
1862
|
* The Runner's per-session read-file-state seats (CC parity: readFileState is
|
|
1820
1863
|
* session-scoped). Always set by the Runner's own prepare call (overriding any caller value, like
|
|
@@ -105,9 +105,12 @@ export declare function inheritedAskCarry(judged: Pick<Extract<InheritedClassifi
|
|
|
105
105
|
}>, "origin" | "fallback" | "ask">, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
|
|
106
106
|
/** What {@link judgeInheritedClassifier} hands back to the arm that called it. */
|
|
107
107
|
export type InheritedClassifierJudgment<A extends InheritedAsk> =
|
|
108
|
-
/** The frozen classifier allowed
|
|
108
|
+
/** The frozen classifier allowed. `result` is the allow the arm RETURNS — minted at the judging station so
|
|
109
|
+
* the model attribution rides the same one object the deny arm's does (an arm re-minting its own
|
|
110
|
+
* `{action:"allow"}` drops it); it already carries the ask's rewrite when the arm declared one. */
|
|
109
111
|
{
|
|
110
112
|
kind: "allow";
|
|
113
|
+
result: PermissionResult;
|
|
111
114
|
}
|
|
112
115
|
/** A block below the bound — the exact deny the ancestor's own gate would have produced. */
|
|
113
116
|
| {
|
|
@@ -145,6 +148,13 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
|
|
|
145
148
|
* `req` is the request as the classifier must see it (the arm's presented / edited args);
|
|
146
149
|
* `subject` is the noun the deny names ("this call" on the wrapper arms, "the approved edit" on the
|
|
147
150
|
* re-check).
|
|
151
|
+
*
|
|
152
|
+
* ONE MINT for the classification's model attribution: every decision this station hands back — the allow it
|
|
153
|
+
* mints from `allowRewrite` as much as either deny — carries it on the identity-keyed seat beside the one
|
|
154
|
+
* carrying the FORM, and the arms return the station's object instead of re-minting their own. The
|
|
155
|
+
* descendant's fold reads both seats at its single attestation read. A decider with no candidate ladder
|
|
156
|
+
* vouches for no name and the seat stays empty (absence = unknown); a block AT the bound reaches no seat at
|
|
157
|
+
* all, since it becomes an ask and the disposition then names that resolution.
|
|
148
158
|
*/
|
|
149
159
|
export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
|
|
150
160
|
autoMode: InheritedAutoMode | undefined;
|
|
@@ -152,6 +162,9 @@ export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
|
|
|
152
162
|
req: ToolCallRequest;
|
|
153
163
|
signal: AbortSignal;
|
|
154
164
|
subject: "this call" | "the approved edit";
|
|
165
|
+
/** The rewrite the arm's allow would have carried (the ask's own `updatedInput`, the re-check's edited
|
|
166
|
+
* args) — handed IN so the allow is minted here, attested, once; absent ⇒ a bare allow. */
|
|
167
|
+
allowRewrite?: unknown;
|
|
155
168
|
}): Promise<InheritedClassifierJudgment<A>>;
|
|
156
169
|
/** The resolver's answer as the settlement reads it. */
|
|
157
170
|
export type FallbackResolution = Pick<ResolvedAsk, "action" | "settlement" | "approverUnavailable">;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { classifierParseFailureDenyMessage, 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
|
+
import { attestClassifierDenyCause, attestClassifierRound } from "../tool-policy.js";
|
|
4
4
|
import { deliverEngineNotice } from "../types.js";
|
|
5
5
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
6
6
|
import { askOriginOf, classifierMayAnswer, probeMandatedAsk } from "../ask-origin.js";
|
|
@@ -82,14 +82,15 @@ export async function judgeInheritedClassifier(opts) {
|
|
|
82
82
|
const verdict = await autoMode.decider
|
|
83
83
|
.decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal)
|
|
84
84
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
85
|
+
const attributed = (d) => (verdict.classifier !== undefined ? attestClassifierRound(d, verdict.classifier, req) : d);
|
|
85
86
|
if (verdict.kind === "allow") {
|
|
86
87
|
autoMode.denialTracking?.recordAllow();
|
|
87
|
-
return { kind: "allow" };
|
|
88
|
+
return { kind: "allow", result: attributed({ action: "allow", ...(opts.allowRewrite !== undefined ? { updatedInput: opts.allowRewrite } : {}) }) };
|
|
88
89
|
}
|
|
89
90
|
if (verdict.kind === "unavailable") {
|
|
90
91
|
return {
|
|
91
92
|
kind: "deny",
|
|
92
|
-
result: attestClassifierDenyCause({ action: "deny", message: classifierUnavailableDenyMessage(req.toolName, verdict.cause), decisionReason: "classifier" }, "unavailable", req),
|
|
93
|
+
result: attributed(attestClassifierDenyCause({ action: "deny", message: classifierUnavailableDenyMessage(req.toolName, verdict.cause), decisionReason: "classifier" }, "unavailable", req)),
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
|
|
@@ -101,7 +102,7 @@ export async function judgeInheritedClassifier(opts) {
|
|
|
101
102
|
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
103
|
decisionReason: "classifier",
|
|
103
104
|
};
|
|
104
|
-
return { kind: "deny", result: verdict.kind === "parse_error" ? attestClassifierDenyCause(denied, "parse_error", req) : denied };
|
|
105
|
+
return { kind: "deny", result: attributed(verdict.kind === "parse_error" ? attestClassifierDenyCause(denied, "parse_error", req) : denied) };
|
|
105
106
|
}
|
|
106
107
|
const fallback = tracked.fallback;
|
|
107
108
|
const minted = { ...ask, message: denialLimitFallbackMessage(fallback, reason || category || req.toolName), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ExecutionEnv } from "../../internal/harness.js";
|
|
2
|
+
import type { EngineNotice } from "../engine-notice.js";
|
|
3
|
+
/** One capability key whose declared value is not a boolean, with the type it actually carries. */
|
|
4
|
+
export interface MalformedCapabilityDeclaration {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly valueType: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Every boolean capability this env DECLARES with a value that is not a boolean.
|
|
10
|
+
*
|
|
11
|
+
* The probe is the VALUE, not membership, because the value is what the readers get: a `capabilities`
|
|
12
|
+
* that answers `has` and `get` differently (a Proxy) would otherwise hide from the census exactly the
|
|
13
|
+
* value a reader goes on to act on. `undefined` is where absence lands — an absent key and an explicit
|
|
14
|
+
* `undefined` are the same word to every reader, and both are the documented silent reading — so one
|
|
15
|
+
* test covers both and the census needs no membership rule at all.
|
|
16
|
+
*
|
|
17
|
+
* TOTAL by construction: a diagnostic may not be the thing that fails a run. Every read of the adapter
|
|
18
|
+
* can throw on a getter or a Proxy trap — the structural probe, the block acquisition and each key —
|
|
19
|
+
* and a read that throws is itself a declaration nobody can use, so all three are reported rather than
|
|
20
|
+
* raised (`unreadable`, under the key when there is one and under `capabilities` when the block itself
|
|
21
|
+
* could not be had).
|
|
22
|
+
*
|
|
23
|
+
* What a snapshot cannot promise: this is taken once, at admission. An adapter whose capability values
|
|
24
|
+
* CHANGE afterwards can hand a later reader something this census never saw; the declaration is
|
|
25
|
+
* documented as a constant property of the adapter, and a mutable one is outside what any one-time
|
|
26
|
+
* reading covers.
|
|
27
|
+
*/
|
|
28
|
+
export declare function malformedCapabilityDeclarations(env: ExecutionEnv): MalformedCapabilityDeclaration[];
|
|
29
|
+
/** Announce each malformed declaration once, to the operator who wired the adapter. */
|
|
30
|
+
export declare function announceMalformedCapabilities(env: ExecutionEnv, onNotice: ((notice: EngineNotice) => void) | undefined): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isRemoteExecutionEnv } from "../remote-env.js";
|
|
2
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
3
|
+
const BOOLEAN_CAPABILITY_KEYS = ["isolation", "suspendable", "canonicalPathAuthoritative"];
|
|
4
|
+
export function malformedCapabilityDeclarations(env) {
|
|
5
|
+
let caps;
|
|
6
|
+
try {
|
|
7
|
+
if (!isRemoteExecutionEnv(env))
|
|
8
|
+
return [];
|
|
9
|
+
caps = env.capabilities;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return [{ key: "capabilities", valueType: "unreadable" }];
|
|
13
|
+
}
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const key of BOOLEAN_CAPABILITY_KEYS) {
|
|
16
|
+
let value;
|
|
17
|
+
try {
|
|
18
|
+
value = caps[key];
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
out.push({ key, valueType: "unreadable" });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (value === undefined || typeof value === "boolean")
|
|
25
|
+
continue;
|
|
26
|
+
out.push({ key, valueType: value === null ? "null" : typeof value });
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
export function announceMalformedCapabilities(env, onNotice) {
|
|
31
|
+
for (const { key, valueType } of malformedCapabilityDeclarations(env)) {
|
|
32
|
+
deliverEngineNotice(onNotice, {
|
|
33
|
+
code: "config.execution_env_capability_invalid",
|
|
34
|
+
message: `The execution environment declares capabilities.${key} as a ${valueType}, not a boolean. What a reader makes of that is the reader's own business and differs between them — the containment readers take only the boolean, so this reads as undeclared, while the suspend reader takes any truthy value, so a non-empty string reads as TRUE. That is why a declaration of the wrong shape cannot pass quietly: the engine may act on something other than what the adapter meant. Fix the adapter — this is a wiring fault, not a supported way to leave the capability unset.`,
|
|
35
|
+
detail: { key, valueType },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TaskEvent } from "../task-event.js";
|
|
2
|
+
import type { RunnerDeps } from "../runner-deps.js";
|
|
3
|
+
import { type SafeNotifier } from "../safe-notify.js";
|
|
4
|
+
/** One run's forwarding-failure bookkeeping: opaque to everything but this module. */
|
|
5
|
+
export interface ForwardSinkScope {
|
|
6
|
+
readonly notifier: SafeNotifier;
|
|
7
|
+
}
|
|
8
|
+
/** The two seats this module reads off a run's trusted internals — spelled structurally so a SNAPSHOT
|
|
9
|
+
* of them (the delegation revive lane holds one) is as valid a holder as the live object. */
|
|
10
|
+
export interface ForwardSinkHolder {
|
|
11
|
+
onForwardEvent?: (event: TaskEvent) => void;
|
|
12
|
+
forwardScope?: ForwardSinkScope;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Mint the forwarding scope for ONE run, bound to the deployment's notice seat. Called where the run's
|
|
16
|
+
* internals object is composed (`Runner.runTaskStream`), so every copy of that object made later — the
|
|
17
|
+
* prepare re-spread, a placement-root rebind, a child's snapshot — carries the same counters.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createForwardSinkScope(onNotice: RunnerDeps["onNotice"]): ForwardSinkScope;
|
|
20
|
+
/**
|
|
21
|
+
* Forward ONE event to the deployment's display sink, containing both failure shapes.
|
|
22
|
+
*
|
|
23
|
+
* @contract runtask.forward_sink_never_faults_the_run — a `RunInternals.onForwardEvent` that throws,
|
|
24
|
+
* or that returns a promise which rejects, never reaches the run's control flow: the frame is dropped,
|
|
25
|
+
* the failure is counted on the run's own `forwardScope`, and the FIRST one is announced once per run
|
|
26
|
+
* as `config.forward_sink_failed` (later ones are counted only). Delivery is never awaited — describing
|
|
27
|
+
* a run is not part of it, so a sink that never settles cannot hold a turn open. An absent sink is a
|
|
28
|
+
* no-op, and a healthy sink sees the same call it always did, with the same receiver.
|
|
29
|
+
*/
|
|
30
|
+
export declare function forwardSafely(holder: ForwardSinkHolder | undefined, event: TaskEvent): void;
|
|
31
|
+
/** Contained forwarding failures on this scope (diagnostics; 0 on the healthy path). */
|
|
32
|
+
export declare function forwardSinkFailures(holder: ForwardSinkHolder | undefined): number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
2
|
+
import { createSafeNotifier, observeThenableRejection, safeErrorMessage } from "../safe-notify.js";
|
|
3
|
+
const FORWARD_SINK_SITE = "runtask.onForwardEvent";
|
|
4
|
+
const ORPHAN_SCOPES = new WeakMap();
|
|
5
|
+
function forwardSinkFailedNotice(message) {
|
|
6
|
+
return {
|
|
7
|
+
code: "config.forward_sink_failed",
|
|
8
|
+
message: `The subagent display sink (RunInternals.onForwardEvent) failed and the frame was dropped: ${message}. ` +
|
|
9
|
+
`The run is unaffected — forwarding is a render channel — and further failures of this run are counted, not repeated.`,
|
|
10
|
+
detail: { site: FORWARD_SINK_SITE, cause: message },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function createForwardSinkScope(onNotice) {
|
|
14
|
+
return {
|
|
15
|
+
notifier: createSafeNotifier({
|
|
16
|
+
onError: (f) => deliverEngineNotice(onNotice, forwardSinkFailedNotice(safeErrorMessage(f.error))),
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function scopeOf(holder) {
|
|
21
|
+
if (holder.forwardScope !== undefined)
|
|
22
|
+
return holder.forwardScope;
|
|
23
|
+
const existing = ORPHAN_SCOPES.get(holder);
|
|
24
|
+
if (existing !== undefined)
|
|
25
|
+
return existing;
|
|
26
|
+
const scope = createForwardSinkScope(undefined);
|
|
27
|
+
ORPHAN_SCOPES.set(holder, scope);
|
|
28
|
+
return scope;
|
|
29
|
+
}
|
|
30
|
+
export function forwardSafely(holder, event) {
|
|
31
|
+
if (holder?.onForwardEvent === undefined)
|
|
32
|
+
return;
|
|
33
|
+
const { notifier } = scopeOf(holder);
|
|
34
|
+
notifier.notify(() => observeThenableRejection(holder.onForwardEvent?.(event), notifier, FORWARD_SINK_SITE), FORWARD_SINK_SITE);
|
|
35
|
+
}
|
|
36
|
+
export function forwardSinkFailures(holder) {
|
|
37
|
+
if (holder === undefined)
|
|
38
|
+
return 0;
|
|
39
|
+
const scope = holder.forwardScope ?? ORPHAN_SCOPES.get(holder);
|
|
40
|
+
return scope?.notifier.failuresAt(FORWARD_SINK_SITE) ?? 0;
|
|
41
|
+
}
|
|
@@ -92,6 +92,10 @@ export interface SettlementLedger {
|
|
|
92
92
|
* `parse_error` round travelling through an ancestor wrapper into this fold); undefined for a ruling
|
|
93
93
|
* block, a non-classifier deny, an allow or an ask. */
|
|
94
94
|
causeOf(decision: PermissionResult): ClassifierDenyCause | undefined;
|
|
95
|
+
/** Read a decision's attested CLASSIFICATION for this call — which model an ancestor's frozen classifier
|
|
96
|
+
* asked and which answered, on the allow arm as much as the deny arm. Undefined when no inherited
|
|
97
|
+
* classifier judged the call, or when the ancestor's decider could vouch for no name. */
|
|
98
|
+
classifierRoundOf(decision: PermissionResult): AutoModeClassifierRound | undefined;
|
|
95
99
|
/** A surviving ask supersedes whatever an earlier decision settled. */
|
|
96
100
|
supersede(): void;
|
|
97
101
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
|
|
2
|
-
import { askUnresolvableCauseOf, classifierDenyCauseOf, decisionText, engineSettlementOf } from "../tool-policy.js";
|
|
2
|
+
import { askUnresolvableCauseOf, classifierDenyCauseOf, classifierRoundOf, 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) {
|
|
@@ -56,6 +56,9 @@ export function createSettlementLedger(call) {
|
|
|
56
56
|
causeOf(decision) {
|
|
57
57
|
return decision.action === "deny" ? classifierDenyCauseOf(decision, call) : undefined;
|
|
58
58
|
},
|
|
59
|
+
classifierRoundOf(decision) {
|
|
60
|
+
return classifierRoundOf(decision, call);
|
|
61
|
+
},
|
|
59
62
|
supersede() {
|
|
60
63
|
settled = undefined;
|
|
61
64
|
},
|
|
@@ -126,15 +126,6 @@ export interface GitStatusLaneRef {
|
|
|
126
126
|
hash: string;
|
|
127
127
|
entryId: string;
|
|
128
128
|
};
|
|
129
|
-
/** The duplicate-tolerant receipt slot: set when a frame delivery is in flight, settled by the
|
|
130
|
-
* run loop's `message_end` walk when the CARRYING message commits (matched on the exact
|
|
131
|
-
* engine-held message text). Commit-on-receipt, never commit-before-append — the git lane's
|
|
132
|
-
* error asymmetry is the REVERSE of the listing lane's (a false "announced" = stale git facts
|
|
133
|
-
* forever; a duplicate frame = a few idempotent kilobytes). */
|
|
134
|
-
pendingReceipt?: {
|
|
135
|
-
text: string;
|
|
136
|
-
commit: (entryId: string) => void;
|
|
137
|
-
};
|
|
138
129
|
}
|
|
139
130
|
/**
|
|
140
131
|
* prepare-side probe half of the lane (H4, extracted from prepareTask): run the CC-shape snapshot
|