@sema-agent/core 5.37.0 → 5.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +151 -0
- package/dist/agents/send-message-tool.d.ts +8 -0
- package/dist/agents/send-message-tool.js +8 -0
- package/dist/agents/subagent.js +6 -0
- package/dist/agents/teacher.js +12 -3
- package/dist/agents/team.d.ts +7 -1
- package/dist/agents/team.js +11 -9
- package/dist/agents/verify.js +12 -3
- package/dist/core/auto-mode-prompt-assets.d.ts +5 -3
- package/dist/core/auto-mode-prompt-assets.js +1 -1
- package/dist/core/checkpoint-store.d.ts +26 -1
- package/dist/core/hooks.d.ts +152 -2
- package/dist/core/hooks.js +65 -7
- package/dist/core/mailbox-store.d.ts +39 -0
- package/dist/core/mailbox-store.js +9 -0
- package/dist/core/permission-rule-consent.d.ts +27 -4
- package/dist/core/permission-rule-consent.js +29 -4
- package/dist/core/permission-rule-model.d.ts +7 -1
- package/dist/core/runner/prepare-config-doors.d.ts +17 -0
- package/dist/core/runner/prepare-config-doors.js +33 -2
- package/dist/core/runner/prepare-task.d.ts +17 -2
- package/dist/core/runner/prepare-task.js +135 -44
- package/dist/core/runner/runtask.js +46 -11
- package/dist/core/sensitive-path-policy.js +3 -3
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +29 -1
- package/dist/core/store-contracts/mailbox-store-contract.js +78 -0
- package/dist/core/tool-model-gate.d.ts +125 -0
- package/dist/core/tool-model-gate.js +303 -0
- package/dist/core/tool-policy.d.ts +1 -1
- package/dist/core/types.d.ts +210 -1
- package/dist/core/types.js +21 -0
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/write-protect.d.ts +93 -0
- package/dist/core/write-protect.js +194 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +5 -3
- package/dist/orchestration/builtin-workflows.d.ts +68 -6
- package/dist/orchestration/builtin-workflows.js +26 -9
- package/dist/orchestration/governance-baseline-validity.d.ts +44 -0
- package/dist/orchestration/governance-baseline-validity.js +55 -0
- package/dist/orchestration/run-workflow-tool.d.ts +10 -1
- package/dist/orchestration/run-workflow-tool.js +99 -31
- package/dist/orchestration/workflow-script-runner.js +9 -4
- package/dist/orchestration/workflow-script-store.d.ts +8 -3
- package/dist/prompts/coordinator.d.ts +4 -1
- package/dist/prompts/coordinator.js +8 -0
- package/dist/prompts/default.d.ts +14 -4
- package/dist/prompts/default.js +2 -1
- package/dist/scenarios/full-body.d.ts +5 -0
- package/dist/scenarios/full-body.js +8 -4
- package/dist/tools/fs/fs-shared.d.ts +3 -2
- package/dist/tools/fs/fs-shared.js +19 -9
- package/dist/tools/fs/read-deny.d.ts +15 -5
- package/dist/tools/fs/read-deny.js +33 -12
- package/dist/tools/fs/safety.d.ts +4 -1
- package/dist/tools/fs/safety.js +4 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +24 -1
package/dist/core/hooks.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DocumentContent, ImageContent, TextContent } from "../internal/llm
|
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
3
|
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
4
|
import { type AskClass } from "./ask-class.js";
|
|
5
|
+
import type { WiringLegKind } from "./wiring-manifest.js";
|
|
5
6
|
/**
|
|
6
7
|
* In-process hook seam (design/37) — a provider-agnostic interception layer modeled on CC's hooks,
|
|
7
8
|
* kept process-internal (no shell/HTTP executors, no settings files). The original tool-call trio:
|
|
@@ -67,7 +68,10 @@ export interface Hooks {
|
|
|
67
68
|
*/
|
|
68
69
|
preToolUseObservational?: true;
|
|
69
70
|
postToolUse?(toolName: string, input: unknown, output: HookToolOutput, ctx: HookToolContext): PostToolUseResult | undefined | Promise<PostToolUseResult | undefined>;
|
|
70
|
-
userPromptSubmit?(prompt: string
|
|
71
|
+
userPromptSubmit?(prompt: string,
|
|
72
|
+
/** #281 件A (additive): the run/leg identity envelope — an implementation declaring only
|
|
73
|
+
* `(prompt)` keeps working. Always supplied on the engine's emission. */
|
|
74
|
+
ctx?: UserPromptSubmitContext): UserPromptSubmitResult | undefined | Promise<UserPromptSubmitResult | undefined>;
|
|
71
75
|
/**
|
|
72
76
|
* roadmap #5 (CC 198 Stop hook, :473831-): runs when the agent WOULD OTHERWISE END its run (no
|
|
73
77
|
* more tool calls, steering and follow-up queues dry). Return `{ block: reason }` to PUSH BACK —
|
|
@@ -106,7 +110,11 @@ export interface Hooks {
|
|
|
106
110
|
*/
|
|
107
111
|
postToolBatch?(batch: PostToolBatchCall[], meta?: {
|
|
108
112
|
injectedThisTurn: "final_verification" | "finalize";
|
|
109
|
-
}
|
|
113
|
+
},
|
|
114
|
+
/** #281 件A (additive): the run/leg identity envelope, on its OWN parameter — `meta`'s presence
|
|
115
|
+
* is an existing signal ("the engine injected at this boundary") that must not become
|
|
116
|
+
* always-true just because identity rides along. Always supplied on the engine's emission. */
|
|
117
|
+
ctx?: PostToolBatchContext): PostToolBatchResult | undefined | Promise<PostToolBatchResult | undefined>;
|
|
110
118
|
/**
|
|
111
119
|
* design/134 (CC PreCompact parity): runs before each compaction, AFTER the trigger gate and a
|
|
112
120
|
* valid cut point are confirmed (so every preCompact corresponds to a compaction that would
|
|
@@ -177,6 +185,10 @@ export interface Hooks {
|
|
|
177
185
|
export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier" | "org";
|
|
178
186
|
/** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + `source`). */
|
|
179
187
|
export interface PermissionDeniedPayload {
|
|
188
|
+
/** #281 件A — the run/leg identity envelope. Present on every ENGINE emission (the gate's single
|
|
189
|
+
* deny exit, the hook-crash intercept, the plan-mode and compliance denies); absent only when a
|
|
190
|
+
* host drives {@link runToolGate} without `ToolGateInput.identity`. */
|
|
191
|
+
identity?: HookInvocationIdentity;
|
|
180
192
|
toolName: string;
|
|
181
193
|
/** The FINAL (post-hook-rewrite / post-policy-rewrite) args the chain adjudicated — what would have
|
|
182
194
|
* executed; not necessarily the model's original args. */
|
|
@@ -198,6 +210,9 @@ export interface PermissionDeniedPayload {
|
|
|
198
210
|
export declare function cloneObserverInput(input: unknown): unknown;
|
|
199
211
|
/** Context for {@link Hooks.stopFailure} — aligned with the TaskResult error face (observe-only). */
|
|
200
212
|
export interface StopFailureContext {
|
|
213
|
+
/** #281 件A — the run/leg identity envelope (always present on the engine's emission; the seat is
|
|
214
|
+
* optional only because the envelope's whole contract is). See {@link HookInvocationIdentity}. */
|
|
215
|
+
identity?: HookInvocationIdentity;
|
|
201
216
|
/** Human-readable error message (the assembled `errorMessage`; any `[code]` prefix already stripped). */
|
|
202
217
|
error: string;
|
|
203
218
|
/** Machine-readable kind — the assembled `errorCode` lifted from the brain's `[code]` prefix
|
|
@@ -242,6 +257,10 @@ export interface PostToolBatchResult {
|
|
|
242
257
|
}
|
|
243
258
|
/** Context for {@link Hooks.preCompact} (design/134). */
|
|
244
259
|
export interface PreCompactContext {
|
|
260
|
+
/** #281 件A — the run/leg identity envelope. Present when the Runner drives the compaction (its
|
|
261
|
+
* wrapper injects it); honestly absent when a deployment drives the compaction module directly
|
|
262
|
+
* (presence law arm 3 on {@link HookInvocationIdentity}). */
|
|
263
|
+
identity?: HookInvocationIdentity;
|
|
245
264
|
/** "auto" = threshold-triggered; "manual" = /compact; "forced" = promptTooLong recovery or
|
|
246
265
|
* trim-pressure propagation (block is ignored on forced — the compaction is not optional). */
|
|
247
266
|
trigger: "auto" | "manual" | "forced";
|
|
@@ -260,6 +279,8 @@ export interface PreCompactResult {
|
|
|
260
279
|
}
|
|
261
280
|
/** Context for {@link Hooks.postCompact} (design/134, observe-only). */
|
|
262
281
|
export interface PostCompactContext {
|
|
282
|
+
/** #281 件A — same presence law as {@link PreCompactContext.identity}. */
|
|
283
|
+
identity?: HookInvocationIdentity;
|
|
263
284
|
trigger: "auto" | "manual" | "forced";
|
|
264
285
|
/** The conversation summary the compaction produced (CC `compact_summary` parity). */
|
|
265
286
|
summary: string;
|
|
@@ -268,6 +289,11 @@ export interface PostCompactContext {
|
|
|
268
289
|
}
|
|
269
290
|
/** Context for the {@link Hooks.stop} hook (CC `stop_hook_active` parity). */
|
|
270
291
|
export interface StopHookContext {
|
|
292
|
+
/** #281 件A — the run/leg identity envelope. The load-bearing member for THIS seat: a process-level
|
|
293
|
+
* `deps.hooks.stop` fires in root, delegated-child and resume legs alike, and previously could not
|
|
294
|
+
* tell which run it was being asked to end (matrix row 10/13 residual). Always present on the
|
|
295
|
+
* engine's stop-gate emission. */
|
|
296
|
+
identity?: HookInvocationIdentity;
|
|
271
297
|
/** True when this run is already continuing because a previous stop() blocked — check it and
|
|
272
298
|
* return success (undefined) once your condition can't be improved, or you will loop to the cap. */
|
|
273
299
|
stopHookActive: boolean;
|
|
@@ -394,10 +420,94 @@ export interface HookEnvCapabilities {
|
|
|
394
420
|
* {@link createCwdReader} carries the one live read under the same rules.
|
|
395
421
|
*/
|
|
396
422
|
export declare function createHookEnvCapabilities(env: ExecutionEnv): HookEnvCapabilities;
|
|
423
|
+
/**
|
|
424
|
+
* #281 件A — the UNIFIED IDENTITY ENVELOPE every hook invocation can carry (`ctx.identity`): which
|
|
425
|
+
* run, which leg, and where in the delegation tree the hook is firing. The CC analog is
|
|
426
|
+
* `createBaseHookInput`'s per-event base fields; the members here are sema's OWN identity facts
|
|
427
|
+
* (session/leg/delegation axes), not a transcription of CC's field names.
|
|
428
|
+
*
|
|
429
|
+
* WHY: `deps.hooks` is PROCESS-level — one `stop`/`preToolUse`/`permissionDenied` seat serves the
|
|
430
|
+
* root run, every delegated child and every resume leg at once, and before this envelope none of
|
|
431
|
+
* those invocations could say which leg they belonged to. Additive: every seat is
|
|
432
|
+
* `identity?: HookInvocationIdentity`, so a consumer that never reads it sees nothing new.
|
|
433
|
+
*
|
|
434
|
+
* PRESENCE LAW (which invocations carry it — stated here once, referenced per seat):
|
|
435
|
+
* · PRESENT on every ENGINE-driven invocation of every hook seat: the tool gate's PreToolUse
|
|
436
|
+
* screenings (phase 1 + the approval-edit re-screen), postToolUse / postToolUseFailure,
|
|
437
|
+
* postToolBatch, userPromptSubmit, stop, stopFailure, preCompact / postCompact (the Runner's
|
|
438
|
+
* wrapper injects it), and every engine `permissionDenied` emission (gate deny exit, hook-crash
|
|
439
|
+
* intercept, plan-mode deny, compliance deny). The engine mints ONE frozen envelope per prepared
|
|
440
|
+
* leg ({@link mintHookInvocationIdentity} — single mint home) and every station reuses it.
|
|
441
|
+
* · HONESTLY ABSENT (no fabrication) at exactly four places, each structurally unable to know:
|
|
442
|
+
* 1. a HOST driving the exported {@link runToolGate} directly without supplying
|
|
443
|
+
* `ToolGateInput.identity` — the engine cannot know a leg it is not running;
|
|
444
|
+
* 2. an inherited PreToolUse screening face riding a DESCENDANT's constraint fold
|
|
445
|
+
* ({@link createPreToolUseConstraintPolicy}) — the fold consumes it as a `ToolPolicy`, whose
|
|
446
|
+
* request shape carries no leg identity, and stamping the INSTALL-site leg would name the
|
|
447
|
+
* wrong run;
|
|
448
|
+
* 3. a deployment driving the compaction module directly with its own `preCompact`/`postCompact`
|
|
449
|
+
* options — those contexts are built below the runner, which is where the envelope lives;
|
|
450
|
+
* 4. the PRE-CAS durable-resume observation of a declared-observational PreToolUse face
|
|
451
|
+
* (resumeStream's edited-args leg) — it runs BEFORE the resume leg prepares, so the leg's
|
|
452
|
+
* envelope does not exist yet, and minting a partial one off the checkpoint would guess at
|
|
453
|
+
* the delegation axes (the row persists `isDelegatedChild` alone; `insideFork`/`agentName`/
|
|
454
|
+
* `parentToolCallId` are internals-only, and a wrong guess is worse than an honest
|
|
455
|
+
* absence). The SAME face is re-consulted with full identity by the resumed leg's own gate.
|
|
456
|
+
*
|
|
457
|
+
* CONSUMER NOTE — the minted object is FROZEN and carries a NULL PROTOTYPE (same rule and same
|
|
458
|
+
* trade as {@link HookEnvCapabilities}: absence must be a fact about this object, not about what
|
|
459
|
+
* `Object.prototype` happens to hold). Read members directly (`identity.agentName`), test presence
|
|
460
|
+
* with `in` or `!== undefined`; do NOT call inherited methods on it — `identity.hasOwnProperty(...)`
|
|
461
|
+
* or implicit string coercion would throw, exactly as on the env capability face.
|
|
462
|
+
*/
|
|
463
|
+
export interface HookInvocationIdentity {
|
|
464
|
+
/** The run's session id (uuid domain) — the same key `TaskResult.sessionId` reports. */
|
|
465
|
+
readonly sessionId: string;
|
|
466
|
+
/** The run's unified task id (`spec.taskId ?? sessionId` — the value TaskEvent's `sourceTaskId`
|
|
467
|
+
* and `task_progress` frames key on). Equal to {@link sessionId} when no task id was declared. */
|
|
468
|
+
readonly taskId: string;
|
|
469
|
+
/** Which prepared leg this invocation belongs to — the wiring manifest's own `leg.kind`
|
|
470
|
+
* derivation, verbatim ({@link WiringLegKind}: `"root" | "child" | "resume"`). NOTE the fork
|
|
471
|
+
* lane's legs read `"child"` here like any other delegation; {@link insideFork} is the
|
|
472
|
+
* fork-lane discriminator. */
|
|
473
|
+
readonly legKind: WiringLegKind;
|
|
474
|
+
/** RB-204 delegation fact, verbatim: `true` iff this leg runs as a delegated child (every core
|
|
475
|
+
* spawn lane — sync/steer/background/fork/workflow — sets it; a top-level run reads `false`). */
|
|
476
|
+
readonly isDelegatedChild: boolean;
|
|
477
|
+
/** Present (`true`) iff this leg IS a forked child (`Agent(subagent_type:"fork")` — the
|
|
478
|
+
* design/110 trusted internals fact). Absent everywhere else. */
|
|
479
|
+
readonly insideFork?: true;
|
|
480
|
+
/** The delegated child's display name, when one was threaded at spawn (taskName / agent-type).
|
|
481
|
+
* UNTRUSTED display text (model-chosen) — already single-line-bounded at mint (same
|
|
482
|
+
* `inlineUntrusted` treatment as the `task_progress` name lane); never an identity key. */
|
|
483
|
+
readonly agentName?: string;
|
|
484
|
+
/** The spawning Agent tool call's own id, when this leg runs under one — the delegation-chain
|
|
485
|
+
* position anchor ({@link import("./types.js").TaskEvent}'s `parentToolCallId` twin). Absent on
|
|
486
|
+
* a top-level run and on a directly-started workflow child (no launching tool call — no id is
|
|
487
|
+
* fabricated). */
|
|
488
|
+
readonly parentToolCallId?: string;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* #281 件A — the ONE construction home of {@link HookInvocationIdentity} (domain-lexicon: one mint
|
|
492
|
+
* site; the runner calls it once per prepared leg and every hook station reuses the same object).
|
|
493
|
+
* Frozen AND null-prototype: the envelope is handed to arbitrarily many deployment callbacks, and a
|
|
494
|
+
* hook mutating `ctx.identity` must not rewrite what a later hook (or the delegation-lifecycle
|
|
495
|
+
* observer) reads. The null prototype is the {@link createHookEnvCapabilities} rule applied to the
|
|
496
|
+
* other absence-signaling face this module mints — "absent member" must be a fact about THIS object,
|
|
497
|
+
* and a frozen object with an ordinary prototype still answers `insideFork`/`agentName` reads (and
|
|
498
|
+
* `in` probes) from `Object.prototype`, so a prototype write elsewhere in the process could hand
|
|
499
|
+
* every leg a delegation axis the mint never stamped (codex r1). `agentName` is sanitized here — it
|
|
500
|
+
* is the only member whose value a model influences.
|
|
501
|
+
*/
|
|
502
|
+
export declare function mintHookInvocationIdentity(facts: HookInvocationIdentity): HookInvocationIdentity;
|
|
397
503
|
/** Identifying context passed to tool hooks. */
|
|
398
504
|
export interface HookToolContext {
|
|
399
505
|
toolCallId: string;
|
|
400
506
|
toolName: string;
|
|
507
|
+
/** #281 件A — the run/leg identity envelope; see {@link HookInvocationIdentity} for the presence
|
|
508
|
+
* law (present on engine-driven gates; absent on a host-driven gate without
|
|
509
|
+
* `ToolGateInput.identity` and on the delegation-fold twin). */
|
|
510
|
+
identity?: HookInvocationIdentity;
|
|
401
511
|
/**
|
|
402
512
|
* The read-only path-resolution face over the env the hands run against (see
|
|
403
513
|
* {@link HookEnvCapabilities} for the why, and for why it is capabilities rather than the env object).
|
|
@@ -450,6 +560,18 @@ export interface PostToolUseResult {
|
|
|
450
560
|
/** Appended to the result as a `<system-reminder>` the model can read. */
|
|
451
561
|
additionalContext?: string;
|
|
452
562
|
}
|
|
563
|
+
/** #281 件A — the context of a {@link Hooks.userPromptSubmit} invocation (second parameter,
|
|
564
|
+
* additive). Carries only the identity envelope today; an object so later additions stay additive. */
|
|
565
|
+
export interface UserPromptSubmitContext {
|
|
566
|
+
/** The run/leg identity envelope — see {@link HookInvocationIdentity} for the presence law. */
|
|
567
|
+
identity?: HookInvocationIdentity;
|
|
568
|
+
}
|
|
569
|
+
/** #281 件A — the context of a {@link Hooks.postToolBatch} invocation (third parameter, additive —
|
|
570
|
+
* deliberately NOT folded into `meta`, whose presence already means "the engine injected here"). */
|
|
571
|
+
export interface PostToolBatchContext {
|
|
572
|
+
/** The run/leg identity envelope — see {@link HookInvocationIdentity} for the presence law. */
|
|
573
|
+
identity?: HookInvocationIdentity;
|
|
574
|
+
}
|
|
453
575
|
/** A UserPromptSubmit hook result: block the submission, or inject context ahead of the prompt. */
|
|
454
576
|
export interface UserPromptSubmitResult {
|
|
455
577
|
/** Block submission entirely; the task fails with this model-readable reason. */
|
|
@@ -672,6 +794,11 @@ export interface ToolGateInput {
|
|
|
672
794
|
toolName: string;
|
|
673
795
|
input: Record<string, unknown>;
|
|
674
796
|
};
|
|
797
|
+
/** #281 件A — the run/leg identity envelope, put verbatim on every {@link HookToolContext} this
|
|
798
|
+
* gate call builds and on every {@link PermissionDeniedPayload} it emits. The Runner supplies its
|
|
799
|
+
* per-leg mint; a host driving the gate directly may omit it, and the contexts then carry no
|
|
800
|
+
* identity (honest absence — this layer never fabricates a leg). */
|
|
801
|
+
identity?: HookInvocationIdentity;
|
|
675
802
|
preToolUse?: Hooks["preToolUse"];
|
|
676
803
|
/** The read-only env capability face put on every {@link HookToolContext} this gate call
|
|
677
804
|
* builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
|
|
@@ -811,6 +938,29 @@ export interface ToolGateInput {
|
|
|
811
938
|
* per-tool mark — attributes a tighten-deny to `source:"shellGate"` instead of `"safety"`.
|
|
812
939
|
*/
|
|
813
940
|
shellGated?: boolean;
|
|
941
|
+
/**
|
|
942
|
+
* design/276 (CC 2.1.233 P11 arm 4): true when the called tool is the peer-message verb — a message
|
|
943
|
+
* to another agent (`SendMessage`). ENGINE-FILLED from the wire name at the caller, never a
|
|
944
|
+
* configuration knob; a caller's same-named shadow tool is keyed too (for a tighten, over-asking is
|
|
945
|
+
* the fail-safe direction). Under an armed {@link autoMode} the gate tightens a surviving `allow` to
|
|
946
|
+
* `ask` (the peer-referral tighten below) so the message passes the classifier's eye — its prompt
|
|
947
|
+
* already carries the Multi-Agent Coordination exemption, so the ordinary teammate message resolves
|
|
948
|
+
* allow and the injection-shaped one is what this member exists to catch. Absent/false, or with no
|
|
949
|
+
* armed auto mode, the decision path is byte-identical. Per-run semantic: armed by THIS run's
|
|
950
|
+
* resolved caps, never inherited down the delegation chain (CC parity — the mode predicate reads the
|
|
951
|
+
* SENDING session's own mode).
|
|
952
|
+
*/
|
|
953
|
+
peerMessage?: boolean;
|
|
954
|
+
/**
|
|
955
|
+
* backlog #286 (CC 2.1.233 `DANGEROUS_*` parity): the write-protection judge — ENGINE-BUILT from
|
|
956
|
+
* the deployment's table seat by `createWriteProtectionCheck` (write-protect.ts), never a
|
|
957
|
+
* deployment callback (the deployment authors table ROWS, which are validated loudly at compile;
|
|
958
|
+
* the judge itself is pure and synchronous, so it is trusted here like the `egress` mark). Judges
|
|
959
|
+
* the FINAL args of a path-confinable write tool; a hit demotes a surviving `allow` to `ask`
|
|
960
|
+
* (the write-protection tighten below). Absent ⇒ the deployment replaced the table with `[]` (or
|
|
961
|
+
* the caller runs the gate without one) and the decision path is byte-identical.
|
|
962
|
+
*/
|
|
963
|
+
writeProtectionCheck?: (toolName: string, args: unknown) => import("./write-protect.js").WriteProtectedHit | null;
|
|
814
964
|
/**
|
|
815
965
|
* design/143 批2 ([672]-A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
|
|
816
966
|
* small-model policy CLASSIFIER before any human/durable resolution:
|
package/dist/core/hooks.js
CHANGED
|
@@ -78,6 +78,18 @@ export function createHookEnvCapabilities(env) {
|
|
|
78
78
|
}
|
|
79
79
|
return Object.freeze(face);
|
|
80
80
|
}
|
|
81
|
+
const IDENTITY_AGENT_NAME_MAX = 80;
|
|
82
|
+
export function mintHookInvocationIdentity(facts) {
|
|
83
|
+
return Object.freeze(Object.assign(Object.create(null), {
|
|
84
|
+
sessionId: facts.sessionId,
|
|
85
|
+
taskId: facts.taskId,
|
|
86
|
+
legKind: facts.legKind,
|
|
87
|
+
isDelegatedChild: facts.isDelegatedChild,
|
|
88
|
+
...(facts.insideFork === true ? { insideFork: true } : {}),
|
|
89
|
+
...(facts.agentName !== undefined ? { agentName: inlineUntrusted(facts.agentName.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}),
|
|
90
|
+
...(facts.parentToolCallId !== undefined ? { parentToolCallId: facts.parentToolCallId } : {}),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
81
93
|
export function formatHookFeedback(text) {
|
|
82
94
|
return `<system-reminder>\n${text}\n</system-reminder>`;
|
|
83
95
|
}
|
|
@@ -270,7 +282,12 @@ export function persistedRuleMandateOf(marks) {
|
|
|
270
282
|
export async function runToolGate(input) {
|
|
271
283
|
const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
|
|
272
284
|
const { toolCallId, toolName } = event;
|
|
273
|
-
const hookCtx = () => ({
|
|
285
|
+
const hookCtx = () => ({
|
|
286
|
+
toolCallId,
|
|
287
|
+
toolName,
|
|
288
|
+
...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}),
|
|
289
|
+
...(input.identity !== undefined ? { identity: input.identity } : {}),
|
|
290
|
+
});
|
|
274
291
|
let currentInput = event.input;
|
|
275
292
|
const preToolContext = [];
|
|
276
293
|
let hookAsk;
|
|
@@ -285,7 +302,7 @@ export async function runToolGate(input) {
|
|
|
285
302
|
const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
|
|
286
303
|
traceHookCrash(input, err, notifier);
|
|
287
304
|
if (input.permissionDenied) {
|
|
288
|
-
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook" }), "toolGate.permissionDenied");
|
|
305
|
+
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook", ...(input.identity !== undefined ? { identity: input.identity } : {}) }), "toolGate.permissionDenied");
|
|
289
306
|
}
|
|
290
307
|
return { block: true, reason: formatHookFeedback(reason), preToolContext };
|
|
291
308
|
}
|
|
@@ -381,6 +398,25 @@ export async function runToolGate(input) {
|
|
|
381
398
|
denySource = input.shellGated === true ? "shellGate" : "safety";
|
|
382
399
|
}
|
|
383
400
|
}
|
|
401
|
+
if (input.peerMessage === true && decision.action === "allow" && input.autoMode !== undefined && !input.autoMode.decider.breakerOpen()) {
|
|
402
|
+
decision = {
|
|
403
|
+
action: "ask",
|
|
404
|
+
message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`,
|
|
405
|
+
decisionReason: "safety",
|
|
406
|
+
};
|
|
407
|
+
denySource = "safety";
|
|
408
|
+
}
|
|
409
|
+
if (decision.action === "allow" && input.writeProtectionCheck !== undefined) {
|
|
410
|
+
const writeProtectedHit = input.writeProtectionCheck(toolName, policyRewrite !== undefined ? policyRewrite : currentInput);
|
|
411
|
+
if (writeProtectedHit !== null) {
|
|
412
|
+
decision = {
|
|
413
|
+
action: "ask",
|
|
414
|
+
message: `tool "${toolName}" writes to a write-protected path (table entry "${writeProtectedHit.name}") — explicit approval required`,
|
|
415
|
+
decisionReason: "safety",
|
|
416
|
+
};
|
|
417
|
+
denySource = "safety";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
384
420
|
let orgRealApprovalRequired = false;
|
|
385
421
|
let orgAskOrigin;
|
|
386
422
|
let orgTightenCount = 0;
|
|
@@ -556,6 +592,7 @@ export async function runToolGate(input) {
|
|
|
556
592
|
decision.decisionReason !== "hook" &&
|
|
557
593
|
hookAsk === undefined &&
|
|
558
594
|
input.egress !== true &&
|
|
595
|
+
input.peerMessage !== true &&
|
|
559
596
|
input.sandboxAdmission.boundaryCapable !== true) {
|
|
560
597
|
const recorded = input.sandboxAdmission.askClassesOf(toolCallId);
|
|
561
598
|
const admissible = recorded !== undefined && recorded.length > 0 && recorded.every((r) => r.cls === "sandbox_local");
|
|
@@ -638,6 +675,7 @@ export async function runToolGate(input) {
|
|
|
638
675
|
if (decision.action === "allow" && decision.updatedInput !== undefined) {
|
|
639
676
|
let editArgs = decision.updatedInput;
|
|
640
677
|
let editDenied;
|
|
678
|
+
let editRewrittenSinceHuman = false;
|
|
641
679
|
for (let round = 0;; round++) {
|
|
642
680
|
if (round >= 3) {
|
|
643
681
|
editDenied = {
|
|
@@ -674,8 +712,10 @@ export async function runToolGate(input) {
|
|
|
674
712
|
denySource = "hook";
|
|
675
713
|
break;
|
|
676
714
|
}
|
|
677
|
-
if (hr.updatedInput !== undefined)
|
|
715
|
+
if (hr.updatedInput !== undefined) {
|
|
678
716
|
editArgs = hr.updatedInput;
|
|
717
|
+
editRewrittenSinceHuman = true;
|
|
718
|
+
}
|
|
679
719
|
}
|
|
680
720
|
}
|
|
681
721
|
if (!adjudicate && input.orgRules === undefined)
|
|
@@ -697,8 +737,10 @@ export async function runToolGate(input) {
|
|
|
697
737
|
denySource = "policy";
|
|
698
738
|
break;
|
|
699
739
|
}
|
|
700
|
-
if (recheck.updatedInput !== undefined)
|
|
740
|
+
if (recheck.updatedInput !== undefined) {
|
|
701
741
|
editArgs = recheck.updatedInput;
|
|
742
|
+
editRewrittenSinceHuman = true;
|
|
743
|
+
}
|
|
702
744
|
const orgTightensBefore = orgTightenCount;
|
|
703
745
|
recheck = await applyOrgLayer(recheck, editArgs);
|
|
704
746
|
const orgRaisedThisRound = orgTightenCount > orgTightensBefore;
|
|
@@ -707,12 +749,16 @@ export async function runToolGate(input) {
|
|
|
707
749
|
break;
|
|
708
750
|
}
|
|
709
751
|
if (recheck.action === "allow") {
|
|
710
|
-
if (recheck.updatedInput !== undefined)
|
|
752
|
+
if (recheck.updatedInput !== undefined) {
|
|
711
753
|
editArgs = recheck.updatedInput;
|
|
754
|
+
editRewrittenSinceHuman = true;
|
|
755
|
+
}
|
|
712
756
|
break;
|
|
713
757
|
}
|
|
714
|
-
if (recheck.updatedInput !== undefined)
|
|
758
|
+
if (recheck.updatedInput !== undefined) {
|
|
715
759
|
editArgs = recheck.updatedInput;
|
|
760
|
+
editRewrittenSinceHuman = true;
|
|
761
|
+
}
|
|
716
762
|
const rr = await resolveAsk({ ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) }, { toolName, args: editArgs, toolCallId });
|
|
717
763
|
resolvedApprover = rr.action !== "ask" ? rr.approver : undefined;
|
|
718
764
|
if (rr.action !== "allow") {
|
|
@@ -724,9 +770,21 @@ export async function runToolGate(input) {
|
|
|
724
770
|
if (rr.updatedInput === undefined) {
|
|
725
771
|
if (rr.presentedInput !== undefined)
|
|
726
772
|
editArgs = rr.presentedInput;
|
|
773
|
+
editRewrittenSinceHuman = false;
|
|
727
774
|
break;
|
|
728
775
|
}
|
|
729
776
|
editArgs = rr.updatedInput;
|
|
777
|
+
editRewrittenSinceHuman = false;
|
|
778
|
+
}
|
|
779
|
+
if (editDenied === undefined && editRewrittenSinceHuman && input.writeProtectionCheck !== undefined) {
|
|
780
|
+
const editHit = input.writeProtectionCheck(toolName, editArgs);
|
|
781
|
+
if (editHit !== null) {
|
|
782
|
+
editDenied = {
|
|
783
|
+
action: "deny",
|
|
784
|
+
message: `the approved edit for "${toolName}" was rewritten by the restriction chain onto a write-protected path (table entry "${editHit.name}") that no approval covers — denied fail-closed; re-submit the edited action directly`,
|
|
785
|
+
};
|
|
786
|
+
denySource = "safety";
|
|
787
|
+
}
|
|
730
788
|
}
|
|
731
789
|
decision = editDenied ?? { ...decision, updatedInput: editArgs };
|
|
732
790
|
}
|
|
@@ -741,7 +799,7 @@ export async function runToolGate(input) {
|
|
|
741
799
|
currentInput = decision.updatedInput;
|
|
742
800
|
}
|
|
743
801
|
if (input.permissionDenied) {
|
|
744
|
-
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource }), "toolGate.permissionDenied");
|
|
802
|
+
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource, ...(input.identity !== undefined ? { identity: input.identity } : {}) }), "toolGate.permissionDenied");
|
|
745
803
|
}
|
|
746
804
|
const denySettledBy = decision.settledBy;
|
|
747
805
|
const denyApprover = denySettledBy !== undefined ? resolvedApprover : undefined;
|
|
@@ -13,6 +13,28 @@ export interface MailboxMessage {
|
|
|
13
13
|
* cross-engine records sit outside the guard's promise domain by ruling. */
|
|
14
14
|
hopChain?: string[];
|
|
15
15
|
}
|
|
16
|
+
/** The enqueue refusal code of the pre-delete clause (see {@link MailboxStore} and
|
|
17
|
+
* {@link MailboxStoreError}) — the ONE place it is spelled, so an out-of-repo store twin imports it
|
|
18
|
+
* instead of value-copying the string (same posture as `STALE_RUNNING_REAP_ATTRIBUTION`: a shared
|
|
19
|
+
* symbol makes drift impossible rather than merely caught). */
|
|
20
|
+
export declare const MAILBOX_TOMBSTONED_RECIPIENT_CODE = "mailbox.recipient_tombstoned";
|
|
21
|
+
/**
|
|
22
|
+
* Typed store error so callers branch on `code` (mirrors `BackgroundAgentStoreError`).
|
|
23
|
+
*
|
|
24
|
+
* `recipient_tombstoned` is the ENQUEUE refusal a backend raises when the recipient it addresses is
|
|
25
|
+
* in a deployment's PRE-DELETE state — the window a retention/deletion cascade opens when it has
|
|
26
|
+
* decided a session (and every row hanging off it) is going away but has not finished removing the
|
|
27
|
+
* rows. An `append` accepted in that window returns a seq to the sender — a durable receipt — for a
|
|
28
|
+
* message the cascade then deletes before any consumer could ever lease it. The refusal is what
|
|
29
|
+
* turns that into a fact the sender can act on.
|
|
30
|
+
*
|
|
31
|
+
* A backend may raise the same code with a plain `Error` carrying `.code`; consumers branch on the
|
|
32
|
+
* string, not on this class (a cross-process/out-of-repo store cannot hand back an instance).
|
|
33
|
+
*/
|
|
34
|
+
export declare class MailboxStoreError extends Error {
|
|
35
|
+
readonly code: typeof MAILBOX_TOMBSTONED_RECIPIENT_CODE;
|
|
36
|
+
constructor(code: typeof MAILBOX_TOMBSTONED_RECIPIENT_CODE, message: string);
|
|
37
|
+
}
|
|
16
38
|
/** A leased batch: the messages a claim winner owns for delivery, plus the ack cursor. */
|
|
17
39
|
export interface MailboxLease {
|
|
18
40
|
messages: MailboxMessage[];
|
|
@@ -55,8 +77,25 @@ export interface MailboxAppendMessage {
|
|
|
55
77
|
* is ADVISORY: a drop fault never blocks the reap, and a box orphaned that way still ages out here.
|
|
56
78
|
* `drop` therefore has a real caller — an implementation that stubs it strands mailboxes until the
|
|
57
79
|
* age policy catches them.
|
|
80
|
+
* - PRE-DELETE STATE (optional capability, additive contract extension): a backend that can SEE its
|
|
81
|
+
* recipient's lifecycle — one whose rows live alongside the session rows a retention/deletion
|
|
82
|
+
* cascade removes — must refuse `append` for a recipient the cascade has already tombstoned,
|
|
83
|
+
* CODED: an `Error` whose `code` is `"mailbox.recipient_tombstoned"` ({@link MailboxStoreError} is
|
|
84
|
+
* the bundled shape; consumers branch on the string). Accepting is the failure this clause names:
|
|
85
|
+
* `append` is durable-first, so the returned seq is a receipt for a message the cascade deletes
|
|
86
|
+
* before any consumer can lease it — a delivery promised to the sender and kept to nobody, which
|
|
87
|
+
* no other face reports. The refusal is per RECIPIENT and enqueue-only: a tombstone belongs to one
|
|
88
|
+
* `(scope, handle)` — the same two-part identity every other method here is keyed by, so it says
|
|
89
|
+
* nothing about a sibling handle NOR about the same handle under another scope — and it is not a
|
|
90
|
+
* wipe (whatever is already parked stays under the same lease/ack rules until the cascade removes
|
|
91
|
+
* the box). A backend with NO view of that
|
|
92
|
+
* lifecycle (the two bundled ones, the CC inbox adapter) has nothing to refuse and keeps accepting
|
|
93
|
+
* — the clause fixes the SPELLING of the refusal, so a deployment reads one code instead of a
|
|
94
|
+
* per-backend dialect. Acceptance kit: `mailboxTombstonedRecipientContract`.
|
|
58
95
|
*/
|
|
59
96
|
export interface MailboxStore {
|
|
97
|
+
/** Durably park one message. Refuses `"mailbox.recipient_tombstoned"` when the backend can see
|
|
98
|
+
* that its recipient is in the deployment's pre-delete state (see the interface notes above). */
|
|
60
99
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
61
100
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
62
101
|
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { assertRetentionPolicy } from "./retention-policy.js";
|
|
2
|
+
export const MAILBOX_TOMBSTONED_RECIPIENT_CODE = "mailbox.recipient_tombstoned";
|
|
3
|
+
export class MailboxStoreError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = "MailboxStoreError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
2
11
|
export function newestSentAt(messages) {
|
|
3
12
|
let newest;
|
|
4
13
|
for (const m of messages) {
|
|
@@ -285,9 +285,9 @@ export interface CcImportLayer {
|
|
|
285
285
|
/**
|
|
286
286
|
* What the import WOULD do, shown before anyone confirms.
|
|
287
287
|
*
|
|
288
|
-
* `uncovered` is a
|
|
288
|
+
* `uncovered` is a fixed-key record, not a list: everything this version does not import is named in the
|
|
289
289
|
* TYPE, so an empty list, a missing member or a duplicate is not expressible. A preview that reported only
|
|
290
|
-
*
|
|
290
|
+
* what it read would be claiming completeness it does not have.
|
|
291
291
|
*/
|
|
292
292
|
export interface ImportPreview {
|
|
293
293
|
candidates: RuleCandidate[];
|
|
@@ -303,6 +303,26 @@ export interface ImportPreview {
|
|
|
303
303
|
uncovered: {
|
|
304
304
|
flagSettings: "not-imported-v1";
|
|
305
305
|
policySettings: "not-imported-v1";
|
|
306
|
+
/**
|
|
307
|
+
* The deny/ask buckets of the layers actually read: how many entries are SITTING THERE, across all
|
|
308
|
+
* layers, that this import deliberately leaves in place (they are the tightening direction and have
|
|
309
|
+
* their own channel — importing them through a loosening lane would be the wrong door).
|
|
310
|
+
*
|
|
311
|
+
* Always present, zero when the buckets are absent or empty: "this settings file has 14 deny entries
|
|
312
|
+
* we did not touch" and "there was nothing there" are different facts, and a key that appears only in
|
|
313
|
+
* the first case makes them indistinguishable for a reader who sees one preview. The count is of
|
|
314
|
+
* ENTRIES IN PLACE, not of a decision — nothing here is skipped-and-lost; `skipped` stays the list of
|
|
315
|
+
* allow-bucket entries that did not become candidates.
|
|
316
|
+
*
|
|
317
|
+
* A bucket that is PRESENT but not an array cannot be counted; it contributes 0 and is disclosed in
|
|
318
|
+
* `skipped` under the layer's path, so a zero is never the report for something unreadable. Same for
|
|
319
|
+
* a layer whose whole `permissions` member is not an object — every bucket in it reads as absent,
|
|
320
|
+
* which is exactly the shape that would otherwise report two clean zeros.
|
|
321
|
+
*/
|
|
322
|
+
denyAskBuckets: {
|
|
323
|
+
deny: number;
|
|
324
|
+
ask: number;
|
|
325
|
+
};
|
|
306
326
|
};
|
|
307
327
|
}
|
|
308
328
|
/** What the import ACTUALLY did — a different moment and a different contract from the preview, because
|
|
@@ -320,8 +340,11 @@ export interface ImportResult {
|
|
|
320
340
|
* Read the allow buckets of the user-editable settings layers and produce a preview plus a PENDING
|
|
321
341
|
* approval record. Nothing is stored until someone confirms that record and the batch is redeemed.
|
|
322
342
|
*
|
|
323
|
-
* Only the allow bucket is
|
|
324
|
-
* channel; importing them through a loosening lane would be the wrong door.
|
|
343
|
+
* Only the allow bucket is IMPORTED. The deny/ask buckets are the tightening direction and have their own
|
|
344
|
+
* channel; importing them through a loosening lane would be the wrong door. They are still COUNTED and
|
|
345
|
+
* reported (`preview.uncovered.denyAskBuckets`): by-design-not-imported and unnoticed look identical to
|
|
346
|
+
* the person confirming the batch, and only one of them is true here — the same reason the non-Bash allow
|
|
347
|
+
* entries are disclosed rather than dropped silently.
|
|
325
348
|
*/
|
|
326
349
|
export declare function prepareCcImport(opts: {
|
|
327
350
|
layers: CcImportLayer[];
|
|
@@ -337,12 +337,13 @@ async function applyRedemption(args) {
|
|
|
337
337
|
function originOfRecordKind(kind) {
|
|
338
338
|
return kind === "import" ? "imported-cc" : kind === "starter" ? "starter" : "user";
|
|
339
339
|
}
|
|
340
|
-
const
|
|
340
|
+
const IMPORT_UNCOVERED_LAYERS = { flagSettings: "not-imported-v1", policySettings: "not-imported-v1" };
|
|
341
341
|
export async function prepareCcImport(opts) {
|
|
342
342
|
const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareCcImport");
|
|
343
343
|
const candidates = [];
|
|
344
344
|
const skipped = [];
|
|
345
345
|
const layers = [];
|
|
346
|
+
const denyAskBuckets = { deny: 0, ask: 0 };
|
|
346
347
|
for (const layer of opts.layers) {
|
|
347
348
|
let raw;
|
|
348
349
|
try {
|
|
@@ -356,14 +357,38 @@ export async function prepareCcImport(opts) {
|
|
|
356
357
|
continue;
|
|
357
358
|
}
|
|
358
359
|
layers.push({ path: layer.path, layer: layer.layer, found: true });
|
|
359
|
-
let
|
|
360
|
+
let container;
|
|
360
361
|
try {
|
|
361
|
-
|
|
362
|
+
container = JSON.parse(raw)?.permissions;
|
|
362
363
|
}
|
|
363
364
|
catch (err) {
|
|
364
365
|
skipped.push({ rule: layer.path, reason: `settings file is not valid JSON (${errText(err)})` });
|
|
365
366
|
continue;
|
|
366
367
|
}
|
|
368
|
+
let permissions;
|
|
369
|
+
if (container === undefined || container === null)
|
|
370
|
+
permissions = undefined;
|
|
371
|
+
else if (typeof container === "object" && !Array.isArray(container))
|
|
372
|
+
permissions = container;
|
|
373
|
+
else {
|
|
374
|
+
skipped.push({
|
|
375
|
+
rule: layer.path,
|
|
376
|
+
reason: `settings "permissions" is not an object — no bucket in this layer could be read, so it is reported as unread rather than as empty (it stays in the settings file either way)`,
|
|
377
|
+
});
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
for (const bucket of ["deny", "ask"]) {
|
|
381
|
+
const entries = permissions?.[bucket];
|
|
382
|
+
if (Array.isArray(entries))
|
|
383
|
+
denyAskBuckets[bucket] += entries.length;
|
|
384
|
+
else if (entries !== undefined && entries !== null) {
|
|
385
|
+
skipped.push({
|
|
386
|
+
rule: layer.path,
|
|
387
|
+
reason: `settings ${bucket} bucket is not an array — its entries could not be counted for the not-imported report (it stays in the settings file either way)`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const allow = permissions?.allow;
|
|
367
392
|
if (!Array.isArray(allow))
|
|
368
393
|
continue;
|
|
369
394
|
const scope = layer.layer === "userSettings" ? { kind: "global" } : { kind: "project", root: layer.root };
|
|
@@ -401,7 +426,7 @@ export async function prepareCcImport(opts) {
|
|
|
401
426
|
createdAt: nowIso(opts.deps),
|
|
402
427
|
};
|
|
403
428
|
await opts.deps.approvals.create(record);
|
|
404
|
-
return { preview: { candidates, skipped, layers, uncovered:
|
|
429
|
+
return { preview: { candidates, skipped, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } }, approvalId: record.id };
|
|
405
430
|
}
|
|
406
431
|
export const STARTER_RULES = [
|
|
407
432
|
"Bash(ls)",
|
|
@@ -29,7 +29,13 @@
|
|
|
29
29
|
* here) and no leading-env-assignment stripping (`FOO=1 git status` simply does not match
|
|
30
30
|
* `Bash(git status:*)`) — the last two are strict-side divergences from upstream, registered as such.
|
|
31
31
|
*/
|
|
32
|
-
/** The one tool the v1 rule lane speaks for. The field exists on the rule so v2 can widen without a shape change.
|
|
32
|
+
/** The one tool the v1 rule lane speaks for. The field exists on the rule so v2 can widen without a shape change.
|
|
33
|
+
*
|
|
34
|
+
* design/276 §3.3 (doctrine, for whoever widens this set): a future `SendMessage(…)`-form rule MAY
|
|
35
|
+
* clear the peer-referral ask — that ask is classifier hesitation by construction (#144: allow rules
|
|
36
|
+
* silence the classifier's questions, never a mandated one), so a recorded human yes is exactly what
|
|
37
|
+
* clears it. The org/hook/matchedAskRule immunities stay: those conjuncts live in the gate's
|
|
38
|
+
* persisted-rule lane and do not loosen with this set. */
|
|
33
39
|
export type PersistedRuleTool = "Bash";
|
|
34
40
|
/** v1 match forms. `"wildcard"` is reserved for v2 and is not a value this version ever produces. */
|
|
35
41
|
export type PersistedRuleMatch = "exact" | "prefix";
|
|
@@ -107,6 +107,10 @@ export interface PrepareConfigDoorsResult {
|
|
|
107
107
|
exclude: readonly string[] | undefined;
|
|
108
108
|
defer: readonly string[] | undefined;
|
|
109
109
|
alwaysLoad: readonly string[] | undefined;
|
|
110
|
+
/** design/277 — the model-gate restore selector ({@link TaskSpec.restoreGatedTools}), fourth
|
|
111
|
+
* seat of the same frozen task-start snapshot: the gate decision and the delegation carrier
|
|
112
|
+
* read THIS, never the live spec. */
|
|
113
|
+
restoreGated: readonly string[] | true | undefined;
|
|
110
114
|
};
|
|
111
115
|
/** owned — the profile half of the RB-50 single decision point (model-independent by contract). */
|
|
112
116
|
promptProfile: "simple" | "classic";
|
|
@@ -127,6 +131,19 @@ export interface PrepareConfigDoorsResult {
|
|
|
127
131
|
compModel: Model | undefined;
|
|
128
132
|
/** owned — the mitigations half of the RB-50 decision point (model-family fact). */
|
|
129
133
|
fableMitigations: boolean;
|
|
134
|
+
/** owned, frozen — design/277 model-gate DISCLOSURE data (the application itself already
|
|
135
|
+
* happened on {@link spec}: its `tools` is the survivor rebind when anything was removed).
|
|
136
|
+
* prepareTask emits the notices from this after the doors return (the onNotice station);
|
|
137
|
+
* the doors stay side-effect-free on the announcement axis. */
|
|
138
|
+
modelGate: {
|
|
139
|
+
/** gate-class → removed wire names (sorted, unique); empty map = nothing removed. */
|
|
140
|
+
removedByClass: ReadonlyMap<string, readonly string[]>;
|
|
141
|
+
/** stamped classes with no merged-table row (inert tags, fail-open — announce material). */
|
|
142
|
+
unknownClasses: readonly string[];
|
|
143
|
+
/** a not-in-force `SEMA_TOOL_MODEL_GATE` value outside the closed set (discard-announce
|
|
144
|
+
* material; the in-force arm never lands here — it throws at the door). */
|
|
145
|
+
discardedEnvRaw: string | undefined;
|
|
146
|
+
};
|
|
130
147
|
/** owned — validated deployment governance windows (undefined = ungoverned). */
|
|
131
148
|
usageWindows: readonly UsageWindow[] | undefined;
|
|
132
149
|
/** owned, out-param cell — created EMPTY here; the brain-call wiring later installs into
|