@sema-agent/core 7.53.0 → 7.54.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 +291 -1
- package/LICENSE +3 -1
- package/dist/agents/peer-session-drain.d.ts +0 -1
- package/dist/agents/peer-session-drain.js +0 -2
- package/dist/agents/send-message-tool.js +3 -4
- package/dist/agents/subagent.d.ts +3 -5
- package/dist/agents/subagent.js +18 -11
- package/dist/agents/tool-filter.d.ts +7 -4
- package/dist/agents/tool-filter.js +7 -3
- package/dist/core/a2a.js +4 -2
- package/dist/core/allow-entry.d.ts +1 -0
- package/dist/core/allow-entry.js +1 -0
- package/dist/core/approval-object.d.ts +87 -0
- package/dist/core/approval-object.js +85 -0
- package/dist/core/arg-summary.d.ts +3 -2
- package/dist/core/arg-summary.js +17 -11
- package/dist/core/ask-origin.d.ts +43 -27
- package/dist/core/ask-origin.js +20 -11
- package/dist/core/ask-question.d.ts +0 -1
- package/dist/core/ask-question.js +4 -67
- package/dist/core/ask-unresolvable-notice.d.ts +3 -2
- package/dist/core/auto-compaction.d.ts +49 -50
- package/dist/core/auto-compaction.js +41 -51
- package/dist/core/auto-mode-prompt.d.ts +4 -2
- package/dist/core/auto-mode-prompt.js +4 -3
- package/dist/core/canonical-json.d.ts +69 -0
- package/dist/core/canonical-json.js +130 -8
- package/dist/core/checkpoint-requirements.d.ts +13 -7
- package/dist/core/checkpoint-requirements.js +4 -1
- package/dist/core/checkpoint-store.d.ts +33 -18
- package/dist/core/checkpoint-store.js +5 -4
- package/dist/core/child-clamp-seats.d.ts +45 -6
- package/dist/core/child-clamp-seats.js +56 -4
- package/dist/core/consolidate-scope.d.ts +1 -1
- package/dist/core/cross-boundary-taint.d.ts +213 -0
- package/dist/core/cross-boundary-taint.js +196 -0
- package/dist/core/decision-receipt.d.ts +44 -1
- package/dist/core/decision-receipt.js +16 -0
- package/dist/core/effective-path-target.d.ts +3 -0
- package/dist/core/effective-path-target.js +4 -0
- package/dist/core/engine-notice.d.ts +34 -2
- package/dist/core/engine-notice.js +17 -1
- package/dist/core/engine-storage-backend.d.ts +73 -0
- package/dist/core/engine-storage-backend.js +1 -0
- package/dist/core/file-history-store.d.ts +6 -2
- package/dist/core/file-history-store.js +1 -0
- package/dist/core/gate-fold.js +10 -2
- package/dist/core/gate-lanes.js +30 -21
- package/dist/core/gate-outcome.d.ts +7 -3
- package/dist/core/governance-codes.d.ts +7 -2
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/hooks.d.ts +14 -14
- package/dist/core/hooks.js +1 -2
- package/dist/core/locked-config.d.ts +28 -1
- package/dist/core/locked-config.js +30 -3
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/mcp-server-spec.d.ts +66 -9
- package/dist/core/mcp-server-spec.js +52 -19
- package/dist/core/mcp.js +5 -4
- package/dist/core/memory-engine/engine.js +2 -1
- package/dist/core/memory-engine/export-bundle.d.ts +1 -1
- package/dist/core/memory-engine/export-bundle.js +1 -1
- package/dist/core/memory-engine/file-backend.d.ts +2 -6
- package/dist/core/memory-engine/file-backend.js +1 -12
- package/dist/core/memory.d.ts +3 -3
- package/dist/core/mode-policy.d.ts +13 -8
- package/dist/core/mode-policy.js +3 -2
- package/dist/core/mods/kernel/receive.d.ts +2 -1
- package/dist/core/mods/kernel/receive.js +7 -2
- package/dist/core/parent-spec-seat.d.ts +10 -13
- package/dist/core/parent-spec-seat.js +14 -6
- package/dist/core/pause-registry.d.ts +35 -1
- package/dist/core/pause-registry.js +10 -0
- package/dist/core/permission-rule-consent.d.ts +18 -5
- package/dist/core/permission-rule-consent.js +13 -8
- package/dist/core/permission-rule-model.d.ts +29 -6
- package/dist/core/permission-rule-model.js +6 -1
- package/dist/core/permission-rule-org.d.ts +37 -0
- package/dist/core/permission-rule-org.js +34 -42
- package/dist/core/permission-rule-session.js +2 -2
- package/dist/core/permission-rule-store.d.ts +8 -0
- package/dist/core/permission-rule-store.js +1 -0
- package/dist/core/permission-rule-sync.js +3 -2
- package/dist/core/read-only-shell.d.ts +1 -1
- package/dist/core/read-only-shell.js +30 -9
- package/dist/core/remote-env.d.ts +41 -1
- package/dist/core/remote-env.js +18 -0
- package/dist/core/resume-startup-cause.d.ts +18 -0
- package/dist/core/resume-startup-cause.js +12 -0
- package/dist/core/runner/approval-feedback.d.ts +46 -0
- package/dist/core/runner/approval-feedback.js +41 -0
- package/dist/core/runner/compaction-call-options.d.ts +3 -3
- package/dist/core/runner/compaction-call-options.js +2 -2
- package/dist/core/runner/contracts.d.ts +33 -41
- package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
- package/dist/core/runner/cross-boundary-lane.js +190 -0
- package/dist/core/runner/decide-continuation.d.ts +4 -4
- package/dist/core/runner/decide-continuation.js +3 -1
- package/dist/core/runner/gate-exit.d.ts +32 -12
- package/dist/core/runner/gate-exit.js +75 -33
- package/dist/core/runner/inherited-ask-grants.js +2 -2
- package/dist/core/runner/person-path-rows.d.ts +2 -2
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +7 -9
- package/dist/core/runner/prepare-acquire-reconcile.js +8 -6
- package/dist/core/runner/prepare-ask-lane.js +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
- package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
- package/dist/core/runner/prepare-config-doors.d.ts +12 -15
- package/dist/core/runner/prepare-config-doors.js +8 -3
- package/dist/core/runner/prepare-context-lane.js +6 -3
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
- package/dist/core/runner/prepare-delegation-surface.js +2 -3
- package/dist/core/runner/prepare-execution-env.d.ts +1 -2
- package/dist/core/runner/prepare-execution-env.js +2 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
- package/dist/core/runner/prepare-gate-stations.js +4 -3
- package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
- package/dist/core/runner/prepare-hands-readface.js +1 -1
- package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-park-ask.d.ts +1 -1
- package/dist/core/runner/prepare-park-ask.js +9 -3
- package/dist/core/runner/prepare-policy-chain.js +2 -7
- package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
- package/dist/core/runner/prepare-safety-scan.js +34 -27
- package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
- package/dist/core/runner/prepare-suspend-saga.js +6 -2
- package/dist/core/runner/prepare-task.js +13 -10
- package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
- package/dist/core/runner/prepare-turn-wiring.js +14 -49
- package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
- package/dist/core/runner/prepare-wiring-manifest.js +2 -2
- package/dist/core/runner/prepare-workspace-restore.js +2 -2
- package/dist/core/runner/resume-admission.js +9 -9
- package/dist/core/runner/resume-apply.js +4 -4
- package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
- package/dist/core/runner/resume-policy-outcome.js +18 -15
- package/dist/core/runner/resume-preflight.js +2 -2
- package/dist/core/runner/resume-review-outcome.js +3 -3
- package/dist/core/runner/run-leg.js +57 -33
- package/dist/core/runner/run-settle-and-teardown.js +2 -0
- package/dist/core/runner/run-terminal-adoption.js +15 -4
- package/dist/core/runner/session-file-state-replay.d.ts +5 -2
- package/dist/core/runner/session-file-state-replay.js +3 -2
- package/dist/core/runner/stream-steer-verb.js +2 -2
- package/dist/core/runner/tool-face-overlay.d.ts +24 -6
- package/dist/core/runner/tool-face-overlay.js +24 -15
- package/dist/core/runner-deps.d.ts +9 -9
- package/dist/core/session-store.d.ts +35 -16
- package/dist/core/session-store.js +20 -12
- package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
- package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
- package/dist/core/shell-wrapper-table.d.ts +5 -3
- package/dist/core/shell-wrapper-table.js +1 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
- package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
- package/dist/core/store-contracts/session-store-contract.js +683 -0
- package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
- package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
- package/dist/core/task-event.d.ts +6 -4
- package/dist/core/task-registry-agent.js +1 -0
- package/dist/core/task-registry-shared.d.ts +5 -0
- package/dist/core/task-registry.d.ts +7 -0
- package/dist/core/task-registry.js +17 -0
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +6 -6
- package/dist/core/tighten-task-spec.d.ts +6 -4
- package/dist/core/tighten-task-spec.js +2 -2
- package/dist/core/tool-catalog-entries.d.ts +1 -1
- package/dist/core/tool-catalog-entries.js +52 -52
- package/dist/core/tool-face-reader.d.ts +49 -0
- package/dist/core/tool-face-reader.js +94 -0
- package/dist/core/tool-face.d.ts +17 -8
- package/dist/core/tool-face.js +1 -0
- package/dist/core/tool-policy.d.ts +64 -23
- package/dist/core/tool-policy.js +62 -56
- package/dist/core/tool-roster.d.ts +46 -28
- package/dist/core/tool-roster.js +91 -14
- package/dist/core/tool-spec-members.js +2 -2
- package/dist/core/tool-spec.d.ts +20 -4
- package/dist/core/tools.js +2 -0
- package/dist/core/trace.d.ts +24 -2
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-text.d.ts +7 -0
- package/dist/core/untrusted-text.js +3 -8
- package/dist/core/user-attachment.d.ts +43 -0
- package/dist/core/user-attachment.js +92 -15
- package/dist/engine/compaction/compaction.d.ts +2 -2
- package/dist/engine/compaction/utils.d.ts +2 -2
- package/dist/engine/harness/types.d.ts +7 -4
- package/dist/engine/llm/types.d.ts +7 -2
- package/dist/engine/llm/types.js +2 -1
- package/dist/engine/session/parent-chain-guard.d.ts +10 -0
- package/dist/engine/session/parent-chain-guard.js +10 -0
- package/dist/engine/session/repo-utils.d.ts +5 -0
- package/dist/engine/session/repo-utils.js +9 -0
- package/dist/engine/session/storage-base.js +5 -2
- package/dist/index.d.ts +23 -9
- package/dist/index.js +19 -8
- package/dist/internal/harness.d.ts +1 -0
- package/dist/internal/harness.js +1 -0
- package/dist/internal/llm.d.ts +1 -1
- package/dist/internal/llm.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +3 -0
- package/dist/orchestration/run-workflow-tool.js +1 -0
- package/dist/orchestration/workflow-governance.d.ts +1 -1
- package/dist/orchestration/workflow.d.ts +3 -0
- package/dist/orchestration/workflow.js +2 -1
- package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
- package/dist/prompt-assembly/catalog/schema.js +3 -0
- package/dist/prompt-assembly/overlay.js +2 -2
- package/dist/prompt-assembly/select.js +2 -2
- package/dist/prompts/default.d.ts +1 -1
- package/dist/shell/lex.d.ts +87 -8
- package/dist/shell/lex.js +283 -49
- package/dist/stores/file/index.d.ts +8 -6
- package/dist/tools/artifact/artifact-tool.js +10 -0
- package/dist/tools/fs/bash-program-position.d.ts +3 -15
- package/dist/tools/fs/bash-program-position.js +3 -10
- package/dist/tools/fs/bash-readonly-classifier.js +2 -0
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-bash.js +72 -101
- package/dist/tools/fs/fs-read.d.ts +12 -0
- package/dist/tools/fs/fs-read.js +10 -9
- package/dist/tools/fs/fs-shared.d.ts +27 -31
- package/dist/tools/fs/fs-shared.js +12 -15
- package/dist/tools/fs/fs-write.js +1 -1
- package/dist/tools/fs/index.js +2 -2
- package/dist/tools/fs/read-deny-home.d.ts +1 -1
- package/dist/tools/fs/read-deny-home.js +5 -4
- package/dist/tools/fs/read-deny.d.ts +16 -3
- package/dist/tools/fs/read-deny.js +12 -9
- package/dist/tools/fs/safety.d.ts +7 -6
- package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
- package/dist/tools/fs/shell-read-bindings.js +15 -16
- package/dist/tools/fs/shell-read-device.js +1 -1
- package/dist/tools/fs/shell-read-plan.js +36 -47
- package/dist/tools/fs/shell-read-word.d.ts +1 -6
- package/dist/tools/fs/shell-read-word.js +2 -20
- package/dist/tools/fs/shell-reentry-text.js +1 -1
- package/dist/tools/web.d.ts +2 -2
- package/package.json +4 -1
- package/test/export-surface.snapshot.json +5654 -5520
- package/wire-changes/core@7.53.0.json +30 -8
- package/wire-changes/core@7.54.0.json +2870 -0
- package/wire-changes/core@7.54.1.json +37 -0
- package/dist/core/shell-scan.d.ts +0 -60
- package/dist/core/shell-scan.js +0 -183
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The device lane's cross-boundary WIRING for one leg: the tree's shared state ref, the session record that carries the state from
|
|
3
|
+
* one leg of a session to the next, and the gate seat whose ONE obligation reading the station and both exits share.
|
|
4
|
+
*
|
|
5
|
+
* WHERE THE STATE LIVES. The tree's state is one object, reached from the tree's opaque token (`RunInternals.crossBoundaryTree`,
|
|
6
|
+
* `ToolExecuteContext.crossBoundaryTreeForChildren`) through this module alone; each leg joins into it what it knows at prepare (its
|
|
7
|
+
* environment's declaration, its session's newest record, its posture) and every bit an exit sets lands there before the call
|
|
8
|
+
* runs. Each leg's session holds a record (`CROSS_BOUNDARY_RECORD_TYPE`) of the state its content was produced under — written only
|
|
9
|
+
* for an armed tree: at prepare and at an exit (before the call runs) when the state grew past what the session already says, and at
|
|
10
|
+
* a leg's end (before a suspend's row is put, when the leg settles) also when conversation content follows the newest record. A
|
|
11
|
+
* write that fails refuses its call (or its row) on a governed tree; a prepare's or a settle's, and any write on a tree that is not
|
|
12
|
+
* governed, mints `config.cross_boundary_record_unwritten`. A record covers the content before it: a leg that takes over a session
|
|
13
|
+
* whose content no record covers (none on the branch, or after the newest), on an armed tree, reads that content as off-target.
|
|
14
|
+
*
|
|
15
|
+
* THE OBLIGATION of a call is read off three things at the moment it is asked ({@link crossBoundaryObligation}): the legs the
|
|
16
|
+
* call travels (its roster row's declaration, or what the dispatching code gives per call), whether the environment the `env`
|
|
17
|
+
* leg reaches declares itself the target NOW, and the tree's state NOW.
|
|
18
|
+
*/
|
|
19
|
+
import type { AgentTool } from "../../internal/harness.js";
|
|
20
|
+
import type { SessionTreeEntry } from "../../internal/harness-types.js";
|
|
21
|
+
import { type CrossBoundaryCheck, type CrossBoundaryPosture, type CrossBoundaryTreeToken } from "../cross-boundary-taint.js";
|
|
22
|
+
import { type ToolRoster } from "../tool-roster.js";
|
|
23
|
+
import type { RunnerDeps } from "../types.js";
|
|
24
|
+
/** A new tree with no bit set, and the token that names it — the one mint of a token. */
|
|
25
|
+
export declare function mintCrossBoundaryTree(): CrossBoundaryTreeToken;
|
|
26
|
+
/** The tree a leg joins: the one its handed token names, or a new one (a root leg) — synchronous, so the phases that hand the token
|
|
27
|
+
* down never wait on the session. A token the engine did not mint names no tree it can read: reported, and the leg's tree reads as
|
|
28
|
+
* the strictest state. */
|
|
29
|
+
export declare function joinedCrossBoundaryTree(handed: CrossBoundaryTreeToken | undefined, report: (message: string) => void): CrossBoundaryTreeToken;
|
|
30
|
+
/** One leg's cross-boundary wiring (see the module header). */
|
|
31
|
+
export interface CrossBoundaryLane {
|
|
32
|
+
/** The tree's token — the one the leg's children are handed. */
|
|
33
|
+
readonly token: CrossBoundaryTreeToken;
|
|
34
|
+
/** The station and the exits are ENGAGED on this leg: a posture is declared, the leg runs on a target, or its tree is governed —
|
|
35
|
+
* each fixed at prepare, so no live consumer flips between two readings mid-run. */
|
|
36
|
+
readonly stationEngaged: boolean;
|
|
37
|
+
/** The gate seat, minted once the leg's roster and tool instances exist; undefined on a leg whose station is not engaged. */
|
|
38
|
+
seat(view: CrossBoundaryLegView): CrossBoundaryCheck | undefined;
|
|
39
|
+
/** The seat minted by {@link seat}, for the resumed call's exit. */
|
|
40
|
+
readonly check: CrossBoundaryCheck | undefined;
|
|
41
|
+
/** Write the record the tree's state owes this leg's session: the new entry's id, or undefined when nothing was owed (or, on a tree
|
|
42
|
+
* that is not governed, the write failed and was reported); throws when it cannot be written on a governed tree. With `atLeaf` (a
|
|
43
|
+
* suspend: a leg's end), the record also covers the content after the newest one and is written only as the child of that leaf
|
|
44
|
+
* (the row then points at it). */
|
|
45
|
+
owe(atLeaf?: string): Promise<string | undefined>;
|
|
46
|
+
/** The tree is armed, so the session OWES the tree's record and a parked row says a reader must read it — on a tree that is not
|
|
47
|
+
* governed a failed write leaves the record missing (the notice says so), and the reader then reads the content after the newest
|
|
48
|
+
* record as off-target. */
|
|
49
|
+
recorded(): boolean;
|
|
50
|
+
/** The leg settles: the record that covers what it added is written, a failure reported loudly (the next leg then reads the
|
|
51
|
+
* content after the newest record as off-target). */
|
|
52
|
+
settle(): Promise<void>;
|
|
53
|
+
/** How many times this leg's record writes have waited on the session store — a station that judged before an exit reads again
|
|
54
|
+
* when the count moved across it (the exit's own form: judged again after a wait). */
|
|
55
|
+
storeWaits(): number;
|
|
56
|
+
}
|
|
57
|
+
/** What the seat reads per call: the live roster (the legs a row declares) and the mounted instances (a `"per_call"` resolver). */
|
|
58
|
+
export interface CrossBoundaryLegView {
|
|
59
|
+
readonly tools: readonly AgentTool[];
|
|
60
|
+
readonly roster: {
|
|
61
|
+
readonly current: ToolRoster | undefined;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export interface CrossBoundaryLaneInput {
|
|
65
|
+
/** The tree this leg joins ({@link joinedCrossBoundaryTree}), or none — a new tree. */
|
|
66
|
+
token?: CrossBoundaryTreeToken;
|
|
67
|
+
/** The posture door's reading (`undefined` = undeclared). */
|
|
68
|
+
posture: CrossBoundaryPosture | undefined;
|
|
69
|
+
/** The run fold: this leg's environment declares the target, or the row it resumes was minted on one. */
|
|
70
|
+
externalContentTargetActive: boolean;
|
|
71
|
+
/** The leg's own environment — read for its CURRENT declaration at each judgment. */
|
|
72
|
+
env: {
|
|
73
|
+
readonly externalContentTarget?: boolean;
|
|
74
|
+
};
|
|
75
|
+
session: {
|
|
76
|
+
getBranch(): Promise<ReadonlyArray<SessionTreeEntry>>;
|
|
77
|
+
getLeafId(): Promise<string | null>;
|
|
78
|
+
appendCustomEntry(customType: string, data?: unknown): Promise<string>;
|
|
79
|
+
};
|
|
80
|
+
resumeLeg: boolean;
|
|
81
|
+
sessionId: string;
|
|
82
|
+
runId: string;
|
|
83
|
+
hostTaskId: string;
|
|
84
|
+
deps: Pick<RunnerDeps, "onError" | "onNotice" | "tracer">;
|
|
85
|
+
}
|
|
86
|
+
/** The leg's lane: the tree ref joined with what this leg knows, the session's record read and (when the state grew) written. */
|
|
87
|
+
export declare function prepareCrossBoundaryLane(input: CrossBoundaryLaneInput): Promise<CrossBoundaryLane>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { BOUNDARY_LEGS, CROSS_BOUNDARY_RECORD_TYPE, CROSS_BOUNDARY_TAINT_BITS, STRICTEST_CROSS_BOUNDARY_TREE, crossBoundaryObligation, crossBoundaryPostureUndeclaredNotice, crossBoundaryRecordUnwrittenNotice, crossBoundaryRecordData, crossBoundaryTreeCovers, crossBoundaryTreeGoverned, joinCrossBoundaryTree, readCrossBoundaryRecord, } from "../cross-boundary-taint.js";
|
|
2
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
3
|
+
import { policyCannotAnswer } from "../real-approval.js";
|
|
4
|
+
import { thrownText } from "../thrown-value.js";
|
|
5
|
+
import { rosterEntryNamed } from "../tool-roster.js";
|
|
6
|
+
import { emitTrace } from "../trace.js";
|
|
7
|
+
const TREE_STATE = new WeakMap();
|
|
8
|
+
export function mintCrossBoundaryTree() {
|
|
9
|
+
const token = Object.freeze(Object.create(null));
|
|
10
|
+
TREE_STATE.set(token, { current: Object.freeze({}) });
|
|
11
|
+
return token;
|
|
12
|
+
}
|
|
13
|
+
export function joinedCrossBoundaryTree(handed, report) {
|
|
14
|
+
if (handed !== undefined && TREE_STATE.has(handed))
|
|
15
|
+
return handed;
|
|
16
|
+
const token = mintCrossBoundaryTree();
|
|
17
|
+
if (handed !== undefined) {
|
|
18
|
+
report("a cross-boundary tree token the engine did not mint was handed to this leg — it reads its tree as armed, observed and governed");
|
|
19
|
+
TREE_STATE.get(token).current = STRICTEST_CROSS_BOUNDARY_TREE;
|
|
20
|
+
}
|
|
21
|
+
return token;
|
|
22
|
+
}
|
|
23
|
+
const EVERY_LEG = Object.freeze([...BOUNDARY_LEGS]);
|
|
24
|
+
const bitsOf = (t) => CROSS_BOUNDARY_TAINT_BITS.filter((b) => t[b] === true);
|
|
25
|
+
function legsOfCall(view, legEnv, token, call, report) {
|
|
26
|
+
const roster = view.roster.current;
|
|
27
|
+
const row = roster !== undefined ? rosterEntryNamed(roster, call.toolName) : undefined;
|
|
28
|
+
if (row === undefined)
|
|
29
|
+
return { legs: EVERY_LEG, env: undefined };
|
|
30
|
+
if (row.boundaryLegs !== "per_call")
|
|
31
|
+
return { legs: row.boundaryLegs, env: legEnv };
|
|
32
|
+
const strictest = (why) => (report(`tool "${row.name}" declares its boundary legs per call, and its boundaryLegsFor ${why} — the call reads as every leg, off the target (the strictest reading)`), { legs: EVERY_LEG, env: undefined });
|
|
33
|
+
const resolve = view.tools.find((t) => t.name === row.name)?.boundaryLegsFor;
|
|
34
|
+
if (typeof resolve !== "function")
|
|
35
|
+
return strictest("is missing");
|
|
36
|
+
let given;
|
|
37
|
+
try {
|
|
38
|
+
given = resolve(call.args, { tree: token });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return strictest(`threw (${thrownText(err)})`);
|
|
42
|
+
}
|
|
43
|
+
const { legs, env } = (typeof given === "object" && given !== null ? given : {});
|
|
44
|
+
if (!Array.isArray(legs) || !legs.every((l) => BOUNDARY_LEGS.some((b) => b === l)) || (env !== undefined && (typeof env !== "object" || env === null)))
|
|
45
|
+
return strictest("answered outside its type ({ legs: a list of boundary legs, env?: an environment })");
|
|
46
|
+
return { legs: legs, env: env ?? legEnv };
|
|
47
|
+
}
|
|
48
|
+
export async function prepareCrossBoundaryLane(input) {
|
|
49
|
+
const { posture, externalContentTargetActive, env, session, resumeLeg, sessionId, runId, hostTaskId, deps } = input;
|
|
50
|
+
const report = (message) => deps.onError?.(new Error(message), { phase: "config", sessionId });
|
|
51
|
+
const token = input.token ?? mintCrossBoundaryTree();
|
|
52
|
+
const ref = TREE_STATE.get(token);
|
|
53
|
+
if (ref === undefined)
|
|
54
|
+
throw new TypeError("prepareCrossBoundaryLane: the token names no tree (resolve it with joinedCrossBoundaryTree)");
|
|
55
|
+
let branch;
|
|
56
|
+
try {
|
|
57
|
+
branch = await session.getBranch();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
report(`the session's cross-boundary record could not be read (${thrownText(err)}) — this leg reads its tree as armed, observed and governed`);
|
|
61
|
+
}
|
|
62
|
+
const read = branch !== undefined ? readCrossBoundaryRecord(branch) : undefined;
|
|
63
|
+
if (read?.kind === "malformed")
|
|
64
|
+
report(`${read.why} — this leg reads its tree as armed, observed and governed`);
|
|
65
|
+
const fromRecord = read === undefined ? STRICTEST_CROSS_BOUNDARY_TREE : read.kind === "absent" ? {} : read.tree;
|
|
66
|
+
let recorded = read?.kind === "recorded" ? read.tree : {};
|
|
67
|
+
const own = { ...(externalContentTargetActive ? { targetExecutionArmed: true } : {}), ...(posture === "multi_tenant" ? { governed: true } : {}) };
|
|
68
|
+
let tree = joinCrossBoundaryTree(joinCrossBoundaryTree(ref.current, fromRecord), own);
|
|
69
|
+
if (tree.targetExecutionArmed === true && read !== undefined && read.kind !== "malformed" && read.uncoveredContent)
|
|
70
|
+
tree = joinCrossBoundaryTree(tree, { offTargetReadObserved: true });
|
|
71
|
+
ref.current = tree;
|
|
72
|
+
const stationEngaged = posture !== undefined || externalContentTargetActive || crossBoundaryTreeGoverned(tree);
|
|
73
|
+
const unwritten = (phase, err) => deliverEngineNotice(deps.onNotice, crossBoundaryRecordUnwrittenNotice({ sessionId, runId, phase, governed: crossBoundaryTreeGoverned(ref.current), cause: thrownText(err) }));
|
|
74
|
+
let writing = Promise.resolve();
|
|
75
|
+
let storeWaits = 0;
|
|
76
|
+
const write = (phase, atLeaf) => {
|
|
77
|
+
const next = writing.then(async () => {
|
|
78
|
+
let leafAt = atLeaf;
|
|
79
|
+
let cover = phase === "suspend" || phase === "settle";
|
|
80
|
+
let written;
|
|
81
|
+
for (;;) {
|
|
82
|
+
const state = ref.current;
|
|
83
|
+
if (state.targetExecutionArmed !== true)
|
|
84
|
+
return written;
|
|
85
|
+
const grown = !crossBoundaryTreeCovers(recorded, state);
|
|
86
|
+
if (!grown && !cover)
|
|
87
|
+
return written;
|
|
88
|
+
try {
|
|
89
|
+
if (!grown) {
|
|
90
|
+
storeWaits++;
|
|
91
|
+
const now = readCrossBoundaryRecord((await session.getBranch()));
|
|
92
|
+
if (now.kind !== "malformed" && !now.uncoveredContent) {
|
|
93
|
+
cover = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (leafAt !== undefined) {
|
|
98
|
+
storeWaits++;
|
|
99
|
+
if ((await session.getLeafId()) !== leafAt)
|
|
100
|
+
throw new Error("the session moved past the row's leaf before its record could be written");
|
|
101
|
+
}
|
|
102
|
+
storeWaits++;
|
|
103
|
+
const entryId = await session.appendCustomEntry(CROSS_BOUNDARY_RECORD_TYPE, crossBoundaryRecordData(state));
|
|
104
|
+
if (typeof entryId !== "string" || entryId.length === 0)
|
|
105
|
+
throw new Error(`the session store answered the record's append with no entry id (${typeof entryId})`);
|
|
106
|
+
if (leafAt !== undefined && (storeWaits++, await session.getLeafId()) !== entryId)
|
|
107
|
+
throw new Error("the record the session store appended is not the session's new leaf");
|
|
108
|
+
recorded = state;
|
|
109
|
+
written = entryId;
|
|
110
|
+
if (leafAt !== undefined)
|
|
111
|
+
leafAt = entryId;
|
|
112
|
+
cover = false;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (crossBoundaryTreeGoverned(ref.current))
|
|
116
|
+
throw err;
|
|
117
|
+
unwritten(phase, err);
|
|
118
|
+
recorded = state;
|
|
119
|
+
return written;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
writing = next.catch(() => undefined);
|
|
124
|
+
return next;
|
|
125
|
+
};
|
|
126
|
+
const owe = (atLeaf) => write(atLeaf !== undefined ? "suspend" : "exit", atLeaf);
|
|
127
|
+
const trace = (phase, call) => emitTrace(deps.tracer, () => ({ kind: "permission.cross_boundary_taint", version: 1, taskId: hostTaskId, runId, phase, taint: bitsOf(ref.current), governed: ref.current.governed === true, ...(call !== undefined ? { toolName: call.toolName, ...(call.toolCallId !== undefined ? { toolCallId: call.toolCallId } : {}) } : {}), ts: Date.now() }));
|
|
128
|
+
if (tree.targetExecutionArmed === true) {
|
|
129
|
+
trace(resumeLeg ? "resume" : "prepare");
|
|
130
|
+
if (posture === undefined && tree.governed !== true)
|
|
131
|
+
deliverEngineNotice(deps.onNotice, crossBoundaryPostureUndeclaredNotice({ sessionId, runId, tree }));
|
|
132
|
+
await write("prepare").catch((err) => unwritten("prepare", err));
|
|
133
|
+
}
|
|
134
|
+
let check;
|
|
135
|
+
const lane = {
|
|
136
|
+
token,
|
|
137
|
+
stationEngaged,
|
|
138
|
+
get check() {
|
|
139
|
+
return check;
|
|
140
|
+
},
|
|
141
|
+
seat(view) {
|
|
142
|
+
if (!stationEngaged)
|
|
143
|
+
return undefined;
|
|
144
|
+
const obligationOf = (call, say) => {
|
|
145
|
+
const { legs, env: reached } = legsOfCall(view, env, token, call, say);
|
|
146
|
+
try {
|
|
147
|
+
return crossBoundaryObligation(legs, reached?.externalContentTarget === true, ref.current);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
say(`the cross-boundary obligation of "${call.toolName}" could not be read (${thrownText(err)}) — the call reads as every leg, off the target`);
|
|
151
|
+
return crossBoundaryObligation(EVERY_LEG, false, ref.current);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const quiet = () => undefined;
|
|
155
|
+
check = {
|
|
156
|
+
obligation: (call) => obligationOf(call, report),
|
|
157
|
+
async exit(call, decidedBy) {
|
|
158
|
+
for (;;) {
|
|
159
|
+
const now = obligationOf(call, quiet);
|
|
160
|
+
if (now.mandate && policyCannotAnswer({ action: "ask", requiresRealApproval: true }, decidedBy)) {
|
|
161
|
+
return {
|
|
162
|
+
kind: "unconfirmed",
|
|
163
|
+
message: `tool "${call.toolName}" needs a person's confirmation: this run executes on a target device${ref.current.offTargetReadObserved === true ? " and has since let content from beyond it in, so a call that reaches beyond the device or back into it" : ", so a call that writes beyond the device"} is put to a person — this call was allowed by a configuration, not a person, and was not executed`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (now.record && ref.current.offTargetReadObserved !== true) {
|
|
167
|
+
ref.current = joinCrossBoundaryTree(ref.current, { offTargetReadObserved: true });
|
|
168
|
+
trace("observed", call);
|
|
169
|
+
}
|
|
170
|
+
const judged = ref.current;
|
|
171
|
+
try {
|
|
172
|
+
await owe();
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
return { kind: "unrecorded", message: `tool "${call.toolName}" was not executed: the session's cross-boundary record could not be written before it ran (${thrownText(err)}) — on a governed tree a call that owes its record does not run without it; refused fail-closed` };
|
|
176
|
+
}
|
|
177
|
+
if (ref.current === judged)
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
return check;
|
|
183
|
+
},
|
|
184
|
+
owe,
|
|
185
|
+
storeWaits: () => storeWaits,
|
|
186
|
+
recorded: () => ref.current.targetExecutionArmed === true,
|
|
187
|
+
settle: () => write("settle").then(() => undefined, (err) => unwritten("settle", err)),
|
|
188
|
+
};
|
|
189
|
+
return lane;
|
|
190
|
+
}
|
|
@@ -121,9 +121,9 @@ export declare function mintDecideRecord(cp: Checkpoint, facts: HostDecisionFact
|
|
|
121
121
|
}): GateOutcome;
|
|
122
122
|
/**
|
|
123
123
|
* Capture-time domain check for a decide outcome's TEXT payloads — the review lanes' `editedPlan` /
|
|
124
|
-
* `reason
|
|
125
|
-
* compared when an `env_failed` reopen replays it) and all
|
|
126
|
-
*
|
|
124
|
+
* `reason`, the approval lane's deny `reason` and its allow `feedback`. All are persisted (the recorded winner's payload,
|
|
125
|
+
* compared when an `env_failed` reopen replays it) and all reach the model-facing continuation (the plan and the
|
|
126
|
+
* notes fenced into its reminder, the feedback as the person's own words). The declared wire type is a string; a value outside it reads one way in this
|
|
127
127
|
* process and another after a JSON round-trip, and reaches the reminder as whatever its stringification
|
|
128
128
|
* happens to be. Worse, the pre-CAS tag-hygiene check and the fence BOTH reach the value through
|
|
129
129
|
* `String.prototype.replace`: an object carrying its own `replace` is therefore read twice and may
|
|
@@ -133,7 +133,7 @@ export declare function mintDecideRecord(cp: Checkpoint, facts: HostDecisionFact
|
|
|
133
133
|
* is named in the message only — `CheckpointError`'s structured `detail.field` is a closed set covering
|
|
134
134
|
* the approval lane's payload names, and widening it is a wire-surface change of its own.
|
|
135
135
|
*/
|
|
136
|
-
export declare function assertOutcomeText(value: string | undefined, field: "editedPlan" | "reason"): void;
|
|
136
|
+
export declare function assertOutcomeText(value: string | undefined, field: "editedPlan" | "reason" | "feedback"): void;
|
|
137
137
|
/**
|
|
138
138
|
* The continuation prompt for a resumed run. The vendored harness only exposes
|
|
139
139
|
* `prompt(text)` (it always appends a user message — there is no "continue from tool results" entry), so
|
|
@@ -8,6 +8,7 @@ import { facetOf, isGateKind, isQuestionGate } from "../pause-registry.js";
|
|
|
8
8
|
import { humanRefusalSentence, policyCannotAnswer, screenApproverAttribution } from "../tool-policy.js";
|
|
9
9
|
import { mintGateOutcome } from "./gate-exit.js";
|
|
10
10
|
import { delimitUntrusted, REVIEWER_NOTE_MAX_BODY } from "../untrusted-text.js";
|
|
11
|
+
import { writtenFeedback } from "./approval-feedback.js";
|
|
11
12
|
import { engineCardOf } from "../tool-roster.js";
|
|
12
13
|
export function resumeDecisionWasNegative(resume) {
|
|
13
14
|
const o = resume.outcome;
|
|
@@ -52,7 +53,8 @@ export function sameWinner(incoming, persisted) {
|
|
|
52
53
|
incoming.decision === persisted.decision &&
|
|
53
54
|
deepJsonEqual(incoming.updatedInput, persisted.updatedInput) &&
|
|
54
55
|
deepJsonEqual(incoming.answer, persisted.answer) &&
|
|
55
|
-
(persisted.reason === undefined || incoming.reason === persisted.reason)
|
|
56
|
+
(persisted.reason === undefined || incoming.reason === persisted.reason) &&
|
|
57
|
+
(persisted.feedback === undefined || writtenFeedback(incoming.feedback) === writtenFeedback(persisted.feedback)));
|
|
56
58
|
}
|
|
57
59
|
export function pendingContentAskCallId(cp) {
|
|
58
60
|
return cp.pendingAction.kind === "tool_approval" && engineCardOf(cp.pendingAction) === "question" ? cp.pendingAction.toolCallId : undefined;
|
|
@@ -118,12 +118,16 @@ export declare function createSettlementLedger(call: {
|
|
|
118
118
|
*/
|
|
119
119
|
export declare function cloneObserverInput(input: unknown): unknown;
|
|
120
120
|
/**
|
|
121
|
-
* The input a gate exit decided on, as the call's record keeps it ({@link ToolGateResult.finalInput}): a
|
|
122
|
-
* FROZEN
|
|
123
|
-
*
|
|
124
|
-
* {@link
|
|
125
|
-
*
|
|
126
|
-
*
|
|
121
|
+
* The input a gate exit decided on, as the call's record keeps it ({@link ToolGateResult.finalInput}): a PLAIN-DATA PROJECTION of
|
|
122
|
+
* it, deep and FROZEN — never the live input (a tool, a later layer or the caller writing into its own arguments cannot rewrite
|
|
123
|
+
* what the gate decided), and never writable by a reader; the deny observer gets a writable {@link cloneObserverInput} of it. Read
|
|
124
|
+
* by the engine's one receipt ({@link RECORD_RECEIPT}), so nothing of the caller's runs: every record and list copied off its own
|
|
125
|
+
* enumerable data members, and every member that is not data replaced by a marker naming what it was (`[not recorded: …]`) — a
|
|
126
|
+
* Map, a Date, a class instance, a function or a bigint by a marker, never by reference; a symbol-keyed member's marker joins its
|
|
127
|
+
* container's list of them (`[not recorded: symbol-keyed members]`, under a key no data member holds). On an ALLOW exit too: the
|
|
128
|
+
* record is the projection while the call runs with the value itself — the gate's own copy (`ownArgs`: records and lists, a getter
|
|
129
|
+
* already read into data) recorded as it is save a bigint, the member closing a cycle and containers past the bound, which are
|
|
130
|
+
* markers. A value the receipt refuses whole (a Proxy, a function, a Map in place of the arguments) is recorded as its marker.
|
|
127
131
|
*/
|
|
128
132
|
export declare function frozenInputSnapshot(input: unknown): unknown;
|
|
129
133
|
/** What one `classic.PreToolUse` screening came to: an answer the receipt took (`undefined` = no opinion), or the
|
|
@@ -211,9 +215,10 @@ export interface GatePass {
|
|
|
211
215
|
* behind the policy's answer could not land in between. Returns the gate result (a park's suspension or the
|
|
212
216
|
* exit's record). Writer: the dispatch (`runToolGate`, which wires `runGateLanes`). Reader: the fold's tail. */
|
|
213
217
|
readonly afterFold: (pass: GatePass) => Promise<ToolGateResult>;
|
|
214
|
-
/** borrowed-mutable — the FINAL args,
|
|
215
|
-
*
|
|
216
|
-
*
|
|
218
|
+
/** borrowed-mutable — the FINAL args, the pass's own immutable value (`ownArgs`): the model's input as taken at the entry,
|
|
219
|
+
* rewritten by the phase-1 hook, by the policy rewrite on the ask path and by the exits' rewrite fold — save on a refusal
|
|
220
|
+
* of a value nothing took ({@link untakenValueExit}), where it holds that value as given and only the deny exit's receipt
|
|
221
|
+
* reads it. Writers: the dispatch (phase 1), the lanes, the exits. */
|
|
217
222
|
currentInput: unknown;
|
|
218
223
|
/** borrowed-mutable — the adjudication request. Minted over the pass's args so the record is whole from
|
|
219
224
|
* the first station, and RE-MINTED by the adjudication station over the FINAL post-screen args (the
|
|
@@ -341,9 +346,24 @@ export declare function engineFailClosedExit(pass: GatePass, reason: string): Pr
|
|
|
341
346
|
/** The refusal sentence for args the gate could not take into its own copy (`ownArgs` refused them), naming what. */
|
|
342
347
|
export declare const unownedArgsMessage: (subject: string, reason: string) => string;
|
|
343
348
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
349
|
+
* A value nothing took — the model's args the pass could not take into its own copy (`ownArgs`: a function, a shared buffer, a
|
|
350
|
+
* typed array; {@link unownedArgsMessage}), or a resolution's edit no approval can be bound to (`bindableArgsOf`) — is refused
|
|
351
|
+
* here, through the single deny exit (the observer fires): nobody judged it, so nothing may run it, and it is no args of the
|
|
352
|
+
* call, so no reading of the tool's runs over it. The refusal carries no judged bytes (no `updatedInput`): the pass holds the
|
|
353
|
+
* value as given, and the deny exit's one receipt records it (`finalInput`, its plain-data projection — one marker when the
|
|
354
|
+
* receipt refuses it whole: a Map, a Date, a class instance, a Proxy or a function in place of the arguments). In the refusing
|
|
355
|
+
* layer's word: the resolution's for an edit (the lanes mint its `presentation_failed` record), the policy layer's for the
|
|
356
|
+
* model's args ({@link unownedArgsExit}).
|
|
357
|
+
*/
|
|
358
|
+
export declare function untakenValueExit(pass: GatePass, untaken: {
|
|
359
|
+
readonly value: unknown;
|
|
360
|
+
readonly message: string;
|
|
361
|
+
readonly deniedBy: Extract<DeniedBy, "policy" | "ask_resolution">;
|
|
362
|
+
}): Promise<ToolGateResult>;
|
|
363
|
+
/**
|
|
364
|
+
* The model's args could not be taken into the pass's own copy (a function, a shared buffer, a typed array): the dispatch seeded
|
|
365
|
+
* the pass with them as given, and they leave through the exit for a value nothing took, in the policy layer's word — the value is
|
|
366
|
+
* the deployment's own (a host driving the gate; the runner's calls are always clonable).
|
|
347
367
|
*/
|
|
348
368
|
export declare function unownedArgsExit(pass: GatePass, reason: string): Promise<ToolGateResult>;
|
|
349
369
|
/** The hook's OWN deny in phase 1. A PreToolUse hook deny does NOT fire `permissionDenied`
|
|
@@ -2,13 +2,15 @@ import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
|
|
|
2
2
|
import { askFaceOf, classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
|
|
3
3
|
import { wireOf } from "../tool-name-readings.js";
|
|
4
4
|
import { isProtectedWrite } from "../tool-registry.js";
|
|
5
|
-
import { attestationOf, decisionText } from "../tool-policy.js";
|
|
5
|
+
import { attestationOf, decisionText, licenseCovers } from "../tool-policy.js";
|
|
6
6
|
import { pathDenialCardOf } from "../../tools/fs/safety.js";
|
|
7
|
-
import {
|
|
8
|
-
import { argsAsTheToolReads, argsReadFrom } from "../effective-path-target.js";
|
|
7
|
+
import { argsAsTheToolReads } from "../effective-path-target.js";
|
|
9
8
|
import { askUnresolvableRemedySeat } from "../ask-unresolvable-notice.js";
|
|
10
9
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
11
10
|
import { formatHookFeedback } from "../reminder-mint.js";
|
|
11
|
+
import { types } from "node:util";
|
|
12
|
+
import { receive } from "../mods/kernel/receive.js";
|
|
13
|
+
import { describeBindingFault, MAX_DEPTH } from "../canonical-json.js";
|
|
12
14
|
export function mintGateOutcome(facts) {
|
|
13
15
|
const settlement = facts.settled !== undefined ? Object.freeze({ ...facts.settled.settlement, who: Object.freeze({ ...facts.settled.settlement.who }) }) : undefined;
|
|
14
16
|
const attribution = facts.classifier !== undefined ? Object.freeze({ ...facts.classifier, ...(facts.classifier.fallback !== undefined ? { fallback: Object.freeze({ ...facts.classifier.fallback }) } : {}) }) : undefined;
|
|
@@ -82,12 +84,7 @@ export function createSettlementLedger(call) {
|
|
|
82
84
|
`(\`attestation\`), never a resolver's member; denied fail-closed rather than recording a settlement nobody can attribute`, contractRefusal(origin));
|
|
83
85
|
}
|
|
84
86
|
const edited = resolved.action === "allow" && resolved.updatedInput !== undefined;
|
|
85
|
-
const licensed = () =>
|
|
86
|
-
if (edited)
|
|
87
|
-
return record.inputDigest === boundInputHashOf(resolved.updatedInput);
|
|
88
|
-
const askedDigest = boundInputHashOf(req.args);
|
|
89
|
-
return record.inputDigest === askedDigest && (resolved.presentedInput === undefined || boundInputHashOf(resolved.presentedInput) === askedDigest);
|
|
90
|
-
};
|
|
87
|
+
const licensed = () => edited ? licenseCovers(record.inputDigest, resolved.updatedInput) : licenseCovers(record.inputDigest, req.args) && (resolved.presentedInput === undefined || licenseCovers(record.inputDigest, resolved.presentedInput));
|
|
91
88
|
const bound = record?.settlement !== undefined && record.origin === origin && sameClasses(record.classes, asked.classes) ? record.settlement : undefined;
|
|
92
89
|
const refusal = bound !== undefined && SETTLEMENT_IS_REFUSAL[bound.kind] ? bound : undefined;
|
|
93
90
|
if (resolved.action === "allow") {
|
|
@@ -128,7 +125,7 @@ export function createSettlementLedger(call) {
|
|
|
128
125
|
return { decision, deniedBy: refusal ? "ask_resolution" : "policy" };
|
|
129
126
|
},
|
|
130
127
|
keepForBytes(runs) {
|
|
131
|
-
if (settled !== undefined &&
|
|
128
|
+
if (settled !== undefined && !licenseCovers(settled.licensed, runs))
|
|
132
129
|
settled = undefined;
|
|
133
130
|
},
|
|
134
131
|
causeOf(decision) {
|
|
@@ -154,32 +151,63 @@ export function cloneObserverInput(input) {
|
|
|
154
151
|
return input;
|
|
155
152
|
}
|
|
156
153
|
}
|
|
154
|
+
const RECORD_RECEIPT = Object.freeze({ accessors: "refuse", depth: MAX_DEPTH, others: "refuse", contain: true, isProxy: (v) => types.isProxy(v) });
|
|
157
155
|
export function frozenInputSnapshot(input) {
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
const received = receive(input, RECORD_RECEIPT);
|
|
157
|
+
if (!received.ok)
|
|
158
|
+
return notRecorded(input, received.fault);
|
|
159
|
+
const record = received.value;
|
|
160
|
+
const symbolMarkers = new Map();
|
|
161
|
+
for (const fault of received.faults ?? []) {
|
|
162
|
+
const above = fault.path.slice(0, -1);
|
|
163
|
+
const holder = memberAt(record, above);
|
|
164
|
+
const last = fault.path[fault.path.length - 1];
|
|
165
|
+
if (typeof holder !== "object" || holder === null || last === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
const rawHolder = memberAt(input, above);
|
|
168
|
+
const symbolKeyed = fault.kind === "hidden" && (Object.hasOwn(holder, last) || typeof rawHolder !== "object" || rawHolder === null || !Object.hasOwn(rawHolder, last));
|
|
169
|
+
if (symbolKeyed) {
|
|
170
|
+
const listed = symbolMarkers.get(holder);
|
|
171
|
+
if (listed === undefined)
|
|
172
|
+
symbolMarkers.set(holder, [notRecorded(input, fault)]);
|
|
173
|
+
else
|
|
174
|
+
listed.push(notRecorded(input, fault));
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
Object.defineProperty(holder, last, { value: notRecorded(input, fault), enumerable: true, writable: true, configurable: true });
|
|
160
178
|
}
|
|
161
|
-
|
|
162
|
-
|
|
179
|
+
for (const [holder, markers] of symbolMarkers) {
|
|
180
|
+
let key = "[not recorded: symbol-keyed members]";
|
|
181
|
+
while (Object.hasOwn(holder, key))
|
|
182
|
+
key += "'";
|
|
183
|
+
Object.defineProperty(holder, key, { value: markers, enumerable: true, writable: true, configurable: true });
|
|
163
184
|
}
|
|
185
|
+
return freezeRecord(record);
|
|
164
186
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
function freezeRecord(record) {
|
|
188
|
+
const pending = [record];
|
|
189
|
+
for (let at = pending.pop(); at !== undefined || pending.length > 0; at = pending.pop()) {
|
|
190
|
+
if (typeof at !== "object" || at === null || Object.isFrozen(at))
|
|
191
|
+
continue;
|
|
192
|
+
Object.freeze(at);
|
|
193
|
+
for (const member of Object.values(at))
|
|
194
|
+
pending.push(member);
|
|
173
195
|
}
|
|
174
|
-
return
|
|
196
|
+
return record;
|
|
197
|
+
}
|
|
198
|
+
const notRecorded = (input, fault) => `[not recorded: ${describeBindingFault(input, fault)}]`;
|
|
199
|
+
function memberAt(value, path) {
|
|
200
|
+
let at = value;
|
|
201
|
+
for (const key of path)
|
|
202
|
+
at = typeof at === "object" && at !== null ? at[key] : undefined;
|
|
203
|
+
return at;
|
|
175
204
|
}
|
|
176
205
|
const PARK_FAILURE_CAUSE_MAX = 600;
|
|
177
|
-
function withParkFailureCause(reason, parkFailed) {
|
|
206
|
+
function withParkFailureCause(reason, parkFailed, nobodyJudged) {
|
|
178
207
|
if (parkFailed === undefined)
|
|
179
208
|
return reason;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
`left to say, not the reason the call stopped.`);
|
|
209
|
+
const note = `${reason} — note: a durable approval park was attempted for this call and could not be minted (${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)})`;
|
|
210
|
+
return nobodyJudged ? `${note}; with nobody to judge the call, that is why it stopped — it can be issued again once its park can be minted.` : `${note}.`;
|
|
183
211
|
}
|
|
184
212
|
export function passAskFace(pass) {
|
|
185
213
|
return askFaceOf(wireOf("gate.askFace", pass.req));
|
|
@@ -192,11 +220,15 @@ export async function engineFailClosedExit(pass, reason) {
|
|
|
192
220
|
return { block: true, reason: formatHookFeedback(reason, input.reminderMark), gate, finalInput, preToolContext: pass.preToolContext };
|
|
193
221
|
}
|
|
194
222
|
export const unownedArgsMessage = (subject, reason) => `${subject} could not be taken into the gate's own copy (${reason}) — denied fail-closed`;
|
|
195
|
-
export async function
|
|
196
|
-
pass.
|
|
197
|
-
pass.
|
|
223
|
+
export async function untakenValueExit(pass, untaken) {
|
|
224
|
+
pass.currentInput = untaken.value;
|
|
225
|
+
pass.decision = { action: "deny", message: untaken.message };
|
|
226
|
+
pass.deniedBy = untaken.deniedBy;
|
|
198
227
|
return await exitGate(pass);
|
|
199
228
|
}
|
|
229
|
+
export async function unownedArgsExit(pass, reason) {
|
|
230
|
+
return await untakenValueExit(pass, { value: pass.currentInput, message: unownedArgsMessage(`the arguments of "${pass.toolName}"`, reason), deniedBy: "policy" });
|
|
231
|
+
}
|
|
200
232
|
export function hookDenyExit(pass, deniedReason) {
|
|
201
233
|
const { input } = pass;
|
|
202
234
|
return {
|
|
@@ -236,11 +268,13 @@ export async function exitGate(pass) {
|
|
|
236
268
|
const unresolvable = foldCause ?? unresolvableCauseOfSettlement(gate);
|
|
237
269
|
const tableSeat = unresolvable !== undefined ? askUnresolvableRemedySeat(unresolvable) : undefined;
|
|
238
270
|
const remedySeat = tableSeat === "absent" && input.askSeatForm === "declared_none" && pass.askClassification?.classes.has("content_question") !== true ? "declared_none" : tableSeat;
|
|
239
|
-
const
|
|
240
|
-
const
|
|
271
|
+
const bindingRefusal = attestationOf(pass.decision, { toolCallId, toolName })?.bindingRefusal;
|
|
272
|
+
const remedy = bindingRefusal !== undefined ? undefined : remedySeat !== undefined ? headlessApprovalRemedy({ mode: input.permissionMode?.(), classification: foldCause !== undefined ? ANCESTOR_UNRESOLVED_CLASSIFICATION : pass.askClassification, approverSeat: remedySeat, coveredWrite: isProtectedWrite(pass.req), ...(pass.shellUnplacedOptions !== undefined ? { unplacedOptions: pass.shellUnplacedOptions } : {}) }) : undefined;
|
|
273
|
+
const denyReason = withParkFailureCause(`${decisionText(pass.decision) ?? `tool "${toolName}" denied by policy`}${remedy !== undefined ? ` — ${remedy}` : ""}`, pass.parkFailed, unresolvable !== undefined);
|
|
241
274
|
if (unresolvable !== undefined) {
|
|
242
275
|
const cause = unresolvable;
|
|
243
|
-
|
|
276
|
+
const noticeRemedy = bindingRefusal ?? remedy;
|
|
277
|
+
pass.notifier.notify(() => input.onAskUnresolvable?.({ toolName, toolCallId, cause, parkLaneExisted: pass.suspendAsk !== undefined, ...(noticeRemedy !== undefined ? { remedy: noticeRemedy } : {}) }), "toolGate.onAskUnresolvable");
|
|
244
278
|
}
|
|
245
279
|
const finalInput = frozenInputSnapshot(pass.currentInput);
|
|
246
280
|
await notifyPermissionDenied({
|
|
@@ -269,6 +303,14 @@ export async function exitGate(pass) {
|
|
|
269
303
|
pass.currentInput = argsAsTheToolReads(pass.req, rw);
|
|
270
304
|
}
|
|
271
305
|
ledger.keepForBytes(pass.currentInput);
|
|
306
|
+
const crossBoundary = await input.crossBoundaryCheck?.exit({ toolName, args: pass.currentInput, toolCallId }, ledger.settled?.settlement.who.party === "person" ? "person" : "policy");
|
|
307
|
+
if (crossBoundary !== undefined) {
|
|
308
|
+
pass.decision = { action: "deny", message: crossBoundary.message, decisionReason: "safety" };
|
|
309
|
+
pass.deniedBy = crossBoundary.kind === "unconfirmed" ? "ask_resolution" : "policy";
|
|
310
|
+
if (crossBoundary.kind === "unconfirmed")
|
|
311
|
+
ledger.set({ settlement: { kind: "blanket_allow_refused", who: { party: "none" }, when: Date.now() }, origin: ledger.settled?.origin ?? "safety_tighten" });
|
|
312
|
+
return exitGate(pass);
|
|
313
|
+
}
|
|
272
314
|
const release = pass.arbitration?.kind === "released" ? pass.arbitration : (() => { if (ledger.settled !== undefined)
|
|
273
315
|
return undefined; const a = pass.arbitration === undefined ? input.ancestorArbitrationOf?.(toolCallId) : undefined; return a?.kind === "released" ? a : undefined; })();
|
|
274
316
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { primaryActivityArg } from "../arg-summary.js";
|
|
2
2
|
import { askApproverIdentity, attest, attestationOf } from "../tool-policy.js";
|
|
3
|
-
import {
|
|
3
|
+
import { exactBoundInputHashOf } from "../canonical-json.js";
|
|
4
4
|
export function askGrantCovers(granted, asked) {
|
|
5
5
|
return asked !== undefined && asked.length > 0 && granted.length > 0 && asked.every((c) => c === "ordinary" || granted.includes(c));
|
|
6
6
|
}
|
|
@@ -34,5 +34,5 @@ export function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision
|
|
|
34
34
|
const settlement = attestationOf(grant.settled, req)?.settlement;
|
|
35
35
|
if (settlement === undefined || decision.action !== "ask")
|
|
36
36
|
return reused;
|
|
37
|
-
return attest(reused, req, { settlement, ...(decision.origin !== undefined ? { origin: decision.origin } : {}), ...(decision.classes !== undefined ? { classes: decision.classes } : {}), inputDigest:
|
|
37
|
+
return attest(reused, req, { settlement, ...(decision.origin !== undefined ? { origin: decision.origin } : {}), ...(decision.classes !== undefined ? { classes: decision.classes } : {}), inputDigest: exactBoundInputHashOf(grant.presented) });
|
|
38
38
|
}
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Every read's outcome lands in the run's unit, per personal partition (the durable one — its user and project rows — and
|
|
16
16
|
* the session one), and only where it is newer, by the order the reads STARTED: a read that answers late never replaces
|
|
17
|
-
* a later one. The prepare read is the unit's first write. A reader that is not a call (the compaction
|
|
18
|
-
*
|
|
17
|
+
* a later one. The prepare read is the unit's first write. A reader that is not a call (the compaction's kept-tail
|
|
18
|
+
* normalizer, the user-attachment seat's reader (the compaction restore's too), the Artifact tool's local read, a read root's grant eligibility, the classifier's table, a language server's session read)
|
|
19
19
|
* reads the unit's newest tables: while the rows the unit last took are compiling it waits for them (within
|
|
20
20
|
* {@link PATH_TABLES_READY_BOUND_MS}), and a read of tables still compiling, or that could not be compiled, refuses —
|
|
21
21
|
* never the tables of rows the unit has since replaced.
|