@sema-agent/core 7.9.1 → 7.10.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 +54 -0
- package/dist/agents/child-model-seat.d.ts +81 -0
- package/dist/agents/child-model-seat.js +46 -0
- package/dist/agents/subagent.d.ts +5 -3
- package/dist/agents/subagent.js +20 -14
- package/dist/core/ask-unresolvable-notice.d.ts +52 -0
- package/dist/core/ask-unresolvable-notice.js +25 -0
- package/dist/core/auto-mode.d.ts +62 -3
- package/dist/core/auto-mode.js +31 -0
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/engine-notice.d.ts +28 -7
- package/dist/core/gate-lanes.js +15 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +24 -1
- package/dist/core/hooks.js +2 -0
- package/dist/core/permission-rule-model.d.ts +51 -16
- package/dist/core/permission-rule-model.js +55 -21
- package/dist/core/permission-rules.d.ts +6 -4
- package/dist/core/permission-rules.js +14 -14
- package/dist/core/roles.d.ts +8 -0
- package/dist/core/runner/contracts.d.ts +29 -4
- package/dist/core/runner/denial-limit-arms.d.ts +14 -3
- package/dist/core/runner/denial-limit-arms.js +15 -5
- package/dist/core/runner/permission-rule-lanes.d.ts +7 -1
- package/dist/core/runner/permission-rule-lanes.js +9 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +14 -4
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -2
- package/dist/core/runner/prepare-gate-stations.js +3 -0
- package/dist/core/runner/prepare-policy-chain.js +7 -6
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +8 -1
- package/dist/core/runner/runtask.d.ts +34 -32
- package/dist/core/runner/runtask.js +64 -34
- package/dist/core/runner-deps.d.ts +9 -2
- package/dist/core/swappable-deps.d.ts +90 -0
- package/dist/core/swappable-deps.js +55 -0
- package/dist/core/tool-policy.d.ts +26 -0
- package/dist/core/tool-policy.js +5 -1
- package/dist/core/wiring-manifest.d.ts +15 -1
- package/dist/core/wiring-manifest.js +10 -2
- package/dist/core/workflow-journal-store.d.ts +21 -2
- package/dist/core/workflow-journal-store.js +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.js +2 -1
- package/dist/engine/harness/types.d.ts +11 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +17 -0
- package/dist/orchestration/run-workflow-tool.js +12 -0
- package/dist/orchestration/workflow-observe.d.ts +1 -1
- package/dist/orchestration/workflow-observe.js +2 -0
- package/dist/orchestration/workflow-types.d.ts +37 -2
- package/dist/orchestration/workflow-types.js +16 -0
- package/dist/orchestration/workflow.d.ts +41 -2
- package/dist/orchestration/workflow.js +316 -48
- package/dist/stores/file/workflow-journal-store.js +10 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +51 -5
|
@@ -2,7 +2,7 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { uuidv7 } from "../internal/harness.js";
|
|
3
3
|
import { PROBE_CAUSE_PATH_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
4
4
|
import { carriesBidiControls } from "./tool-policy.js";
|
|
5
|
-
import { readDenialLimitFallback } from "./auto-mode.js";
|
|
5
|
+
import { isAutoModeUnavailableCause, readDenialLimitFallback } from "./auto-mode.js";
|
|
6
6
|
import { isAskOrigin } from "./ask-origin.js";
|
|
7
7
|
import { checkpointExecutionRecorded, executionRecordDisposition, ExecutionOutcomeConflictError, } from "./checkpoint-execution-record.js";
|
|
8
8
|
import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
|
|
@@ -278,6 +278,7 @@ export function summarizeCheckpoint(cp) {
|
|
|
278
278
|
return fb !== undefined ? { denialLimitFallback: fb } : {};
|
|
279
279
|
})(),
|
|
280
280
|
...(tool !== undefined && isAskOrigin(tool.origin) ? { origin: tool.origin } : {}),
|
|
281
|
+
...(tool !== undefined && isAutoModeUnavailableCause(tool.classifierUnavailable?.cause) ? { classifierUnavailable: { cause: tool.classifierUnavailable.cause } } : {}),
|
|
281
282
|
...(hasBidi ? { hasBidiControls: true } : {}),
|
|
282
283
|
};
|
|
283
284
|
}
|
|
@@ -69,15 +69,25 @@ export interface EngineNotice {
|
|
|
69
69
|
* (`MCP_INJECTION_DROP_REASONS`, CLOSED-SETS), `field` names the bad key for `malformed_entry`
|
|
70
70
|
* when known. `"user"` audience; `sessionId` REQUIRED at the mint (a user row with no session
|
|
71
71
|
* routes nowhere — refused, never fabricated). Dedup unit: per leg per server name, the host's.
|
|
72
|
-
* - `"config.models_swapped"` — `Runner.
|
|
73
|
-
* (zero-restart model switching
|
|
74
|
-
* never the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
72
|
+
* - `"config.models_swapped"` — `Runner.swapDeps` replaced the model catalog generation
|
|
73
|
+
* (zero-restart model switching; the `models` / `tiers` seats of the swappable set). `detail:
|
|
74
|
+
* { models, tiers, occurrenceId }` — key COUNTS only, never the catalog itself. In-flight tasks
|
|
75
|
+
* finish on the models they resolved at prepare (natural snapshot); every later prepare resolves
|
|
76
|
+
* against the new generation. A failed swap (illegal tier binding, a refused candidate) throws
|
|
77
|
+
* atomically and mints nothing.
|
|
78
78
|
* `occurrenceId` (#495, uuidv7 minted at the announcement): the verb carries no run and no
|
|
79
79
|
* session, and the payload is a pair of counts, so two swaps of the same shape were byte-
|
|
80
80
|
* identical lines. Dedup key: (code, occurrenceId).
|
|
81
|
+
* - `"config.read_face_swapped"` — `Runner.swapDeps` replaced the deployment's read-face
|
|
82
|
+
* declaration (`RunnerDeps.readFace`, the `readFace` seat of the swappable set) without a
|
|
83
|
+
* restart. `detail: { from, to, occurrenceId }` — the seat's previous and new value, each
|
|
84
|
+
* `"open"` | `"roots"` | `null` (`null` = no declaration; the engine default `roots` applies).
|
|
85
|
+
* The SEAT is announced, not a leg's resolved face: resolution (task seat, governance, the
|
|
86
|
+
* read-only mount) still runs per prepare. In-flight legs keep the face they prepared under;
|
|
87
|
+
* every later prepare resolves against the new declaration. A candidate outside the value gate
|
|
88
|
+
* (`assertReadFaceValue`) throws atomically and mints nothing. Own `occurrenceId` for the same
|
|
89
|
+
* reason as the catalog line (no run, no session, a two-word payload). Dedup key:
|
|
90
|
+
* (code, occurrenceId).
|
|
81
91
|
* - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
|
|
82
92
|
* DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
|
|
83
93
|
* the pairing pre-flight and the seat fell back to the primary model instead of sinking the
|
|
@@ -90,7 +100,7 @@ export interface EngineNotice {
|
|
|
90
100
|
* session falling back on the same seat used to mint byte-identical lines; `runId` is what
|
|
91
101
|
* tells one task falling back twice apart from two tasks falling back once each.
|
|
92
102
|
* Explicitly-named models never mint this: they refuse at the brain's request gate instead.
|
|
93
|
-
* - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.
|
|
103
|
+
* - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapDeps` moved a
|
|
94
104
|
* same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
|
|
95
105
|
* did not change: legal (a provider changing domains), but worth one loud line — if the
|
|
96
106
|
* PROVIDER changed, the credential reference needs the same update. Advisory only, never a
|
|
@@ -484,6 +494,17 @@ export interface EngineNotice {
|
|
|
484
494
|
* access scope of the read that found the gap — from the continuation read faces (SendMessage preflight /
|
|
485
495
|
* AgentTranscript's durable leg); the per-call honest refusals are unchanged, and the declared
|
|
486
496
|
* tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates).
|
|
497
|
+
* - `"delegation.ask_unresolvable"` (#648) — an `ask` reached its FINAL deny with nobody having judged
|
|
498
|
+
* it: the approver consulted for the call (the run's own seat, or an ancestor's frozen seat on a
|
|
499
|
+
* delegated child — the sync / workflow lanes, or a background child outside the park family)
|
|
500
|
+
* answered `unavailable` (no operator reachable for this ask) and no durable approval park took
|
|
501
|
+
* the call afterwards. The deny itself is unchanged (`tool_end.gate.settlement.kind:
|
|
502
|
+
* "approver_unavailable"`, the fail-closed sentence as the tool result); this is its person-facing
|
|
503
|
+
* half. Minted at the one gate exit both unavailable deny arms reach, once per tool call, by
|
|
504
|
+
* `ask-unresolvable-notice.ts`. `detail: { sessionId, toolName, toolCallId, settlementKind,
|
|
505
|
+
* parkLaneExisted }` — `parkLaneExisted` says whether a park lane was armed and declined/failed
|
|
506
|
+
* (`true`) or none existed (`false`, the usual shape). Audience `"user"`: the refused work is the
|
|
507
|
+
* session user's own; the operator hears it through the sink like every code.
|
|
487
508
|
*
|
|
488
509
|
* - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
|
|
489
510
|
* session count crossed the consolidation thresholds (time gate open ∧ enough distinct
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -385,6 +385,9 @@ export async function runGateLanes(pass) {
|
|
|
385
385
|
pass.deniedBy = "classifier";
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
|
+
else if (verdict.kind === "unavailable") {
|
|
389
|
+
pass.decision = { ...pass.decision, classifierUnavailable: { cause: verdict.cause } };
|
|
390
|
+
}
|
|
388
391
|
}
|
|
389
392
|
if (input.sandboxAdmission !== undefined &&
|
|
390
393
|
pass.decision.action === "ask" &&
|
|
@@ -425,6 +428,7 @@ export async function runGateLanes(pass) {
|
|
|
425
428
|
...(callSignal !== undefined ? { signal: callSignal } : {}),
|
|
426
429
|
...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
|
|
427
430
|
...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
|
|
431
|
+
...(d.action === "ask" && d.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: d.classifierUnavailable.cause } } : {}),
|
|
428
432
|
});
|
|
429
433
|
if (suspendAsk && pass.decision.action === "ask") {
|
|
430
434
|
const parkArgs = [pass.req, pass.currentInput, safety, undefined, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.persistedRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
|
|
@@ -492,6 +496,16 @@ export async function runGateLanes(pass) {
|
|
|
492
496
|
return { suspend: suspended, preToolContext: pass.preToolContext };
|
|
493
497
|
}
|
|
494
498
|
}
|
|
499
|
+
const noteUnresolvable = (r) => {
|
|
500
|
+
if (r.action !== "deny" || r.approverUnavailable !== true)
|
|
501
|
+
return;
|
|
502
|
+
try {
|
|
503
|
+
input.onAskUnresolvable?.({ toolName, toolCallId, settlementKind: "approver_unavailable", parkLaneExisted: suspendAsk !== undefined });
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
noteUnresolvable(resolved);
|
|
495
509
|
if (askBeforeResolve.action === "ask" && askBeforeResolve.denialLimitFallback !== undefined) {
|
|
496
510
|
if (pass.decision.action === "allow") {
|
|
497
511
|
input.autoMode?.denialTracking?.recordAllow();
|
|
@@ -624,6 +638,7 @@ export async function runGateLanes(pass) {
|
|
|
624
638
|
const editAskReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
625
639
|
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
626
640
|
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
641
|
+
noteUnresolvable(rr);
|
|
627
642
|
if (rr.action !== "allow") {
|
|
628
643
|
editDenied = rr;
|
|
629
644
|
pass.deniedBy = "ask_resolution";
|
|
@@ -103,7 +103,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
103
103
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
104
104
|
* no longer minted.
|
|
105
105
|
*/
|
|
106
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
106
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
107
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
108
108
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
109
109
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -97,6 +97,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
97
97
|
"config.env_timeout_discarded",
|
|
98
98
|
"config.materialize_env_discarded",
|
|
99
99
|
"config.models_swapped",
|
|
100
|
+
"config.read_face_swapped",
|
|
100
101
|
"config.read_face_deployment_clamped",
|
|
101
102
|
"config.tool_model_gate_removed",
|
|
102
103
|
"config.tool_model_gate_unknown_class",
|
|
@@ -113,6 +114,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
113
114
|
"classifier.denial_limit",
|
|
114
115
|
"checkpoint.execution_outcome_unrecorded",
|
|
115
116
|
"delegation.transcript_integrity",
|
|
117
|
+
"delegation.ask_unresolvable",
|
|
116
118
|
"mcp.injection_dropped",
|
|
117
119
|
"mcp.revocation_probe_failed",
|
|
118
120
|
"workflow.governance_key_stripped",
|
|
@@ -160,10 +162,12 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
160
162
|
"memory.capture_opted_out": "user",
|
|
161
163
|
"memory.capture_optout_unpersisted": "user",
|
|
162
164
|
"memory.consolidation_withheld": "user",
|
|
165
|
+
"delegation.ask_unresolvable": "user",
|
|
163
166
|
"config.autocompact_window_clamped": "operator",
|
|
164
167
|
"config.env_timeout_discarded": "operator",
|
|
165
168
|
"config.materialize_env_discarded": "operator",
|
|
166
169
|
"config.models_swapped": "operator",
|
|
170
|
+
"config.read_face_swapped": "operator",
|
|
167
171
|
"config.read_face_deployment_clamped": "operator",
|
|
168
172
|
"config.tool_model_gate_removed": "operator",
|
|
169
173
|
"config.tool_model_gate_unknown_class": "operator",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { GateOutcome } from "./gate-outcome.js";
|
|
|
5
5
|
export { normalizeOrgGateVerdict, normalizePersistedRuleHit, persistedRuleMandateOf } from "./gate-lanes.js";
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
7
7
|
import { type AskClass } from "./ask-class.js";
|
|
8
|
-
import type
|
|
8
|
+
import { type AutoModeDenialTracker } from "./auto-mode.js";
|
|
9
9
|
export { formatHookFeedback } from "./reminder-mint.js";
|
|
10
10
|
import type { WiringLegKind } from "./wiring-manifest.js";
|
|
11
11
|
/**
|
|
@@ -1405,6 +1405,20 @@ export interface ToolGateInput {
|
|
|
1405
1405
|
toolCallId: string;
|
|
1406
1406
|
fallback: import("./auto-mode.js").DenialLimitFallback;
|
|
1407
1407
|
}) => void;
|
|
1408
|
+
/**
|
|
1409
|
+
* #648 — the deny exit nobody judged: the ask's approver (the run's own, or an ancestor's frozen seat on
|
|
1410
|
+
* a delegated child) answered `unavailable` and the durable park re-route did not take the call, so the
|
|
1411
|
+
* fail-closed deny STANDS. Fired once per such call, after the park has had its one attempt, with the
|
|
1412
|
+
* facts the `delegation.ask_unresolvable` notice is composed from (`ask-unresolvable-notice.ts`);
|
|
1413
|
+
* `parkLaneExisted` = a park lane was armed for this run. Observe-only: a throwing seat never alters
|
|
1414
|
+
* the deny. Absent ⇒ the deny stands silently (a host driving the gate directly, no notice sink).
|
|
1415
|
+
*/
|
|
1416
|
+
onAskUnresolvable?: (info: {
|
|
1417
|
+
toolName: string;
|
|
1418
|
+
toolCallId: string;
|
|
1419
|
+
settlementKind: "approver_unavailable";
|
|
1420
|
+
parkLaneExisted: boolean;
|
|
1421
|
+
}) => void;
|
|
1408
1422
|
/**
|
|
1409
1423
|
* design/153 §2/§7.4 (件4 复审, MED): true means this call's ask is MARKED — an inherited ancestor
|
|
1410
1424
|
* constraint already determined "no synchronous layer may resolve this ask" (the ancestor's frozen
|
|
@@ -1643,6 +1657,12 @@ export interface AskCarry {
|
|
|
1643
1657
|
/** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
|
|
1644
1658
|
* on the row so a durable card is rendered by the same word the synchronous card is. */
|
|
1645
1659
|
readonly origin?: import("./ask-origin.js").AskOrigin;
|
|
1660
|
+
/** #616: the surviving ask's classifier-unavailable fact (the classifier was consulted on this ask and could
|
|
1661
|
+
* not run — `cause` is the verdict's word), persisted on the row so a durable card says why it is asking
|
|
1662
|
+
* exactly as the synchronous card does. Absent ⇒ the classifier answered, was not eligible, or was not wired. */
|
|
1663
|
+
readonly classifierUnavailable?: {
|
|
1664
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
1665
|
+
};
|
|
1646
1666
|
}
|
|
1647
1667
|
/** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
|
|
1648
1668
|
* the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
|
|
@@ -1651,6 +1671,9 @@ export interface AskCarry {
|
|
|
1651
1671
|
export declare function askCarryRowMembers(carry: AskCarry | undefined): {
|
|
1652
1672
|
denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
1653
1673
|
origin?: import("./ask-origin.js").AskOrigin;
|
|
1674
|
+
classifierUnavailable?: {
|
|
1675
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
1676
|
+
};
|
|
1654
1677
|
};
|
|
1655
1678
|
/**
|
|
1656
1679
|
* The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
|
package/dist/core/hooks.js
CHANGED
|
@@ -6,6 +6,7 @@ export { normalizeOrgGateVerdict, normalizePersistedRuleHit, persistedRuleMandat
|
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
7
7
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
8
8
|
import { isAskOrigin } from "./ask-origin.js";
|
|
9
|
+
import { isAutoModeUnavailableCause } from "./auto-mode.js";
|
|
9
10
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
10
11
|
export { formatHookFeedback } from "./reminder-mint.js";
|
|
11
12
|
import { createSafeNotifier } from "./safe-notify.js";
|
|
@@ -237,6 +238,7 @@ export function askCarryRowMembers(carry) {
|
|
|
237
238
|
return {
|
|
238
239
|
...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
|
|
239
240
|
...(isAskOrigin(carry.origin) ? { origin: carry.origin } : {}),
|
|
241
|
+
...(isAutoModeUnavailableCause(carry.classifierUnavailable?.cause) ? { classifierUnavailable: { cause: carry.classifierUnavailable.cause } } : {}),
|
|
240
242
|
};
|
|
241
243
|
}
|
|
242
244
|
export async function runToolGate(input) {
|
|
@@ -497,28 +497,63 @@ export declare function parseRuleText(text: string, behavior: RuleBehavior): {
|
|
|
497
497
|
* double-slash directory form; for `path` it is the pattern as spelled. */
|
|
498
498
|
export declare function formatRuleText(command: string, match: PersistedRuleMatch, tool: PersistedRuleTool): string;
|
|
499
499
|
/**
|
|
500
|
-
* The
|
|
501
|
-
*
|
|
502
|
-
* relative pattern (the live working directory;
|
|
503
|
-
*
|
|
500
|
+
* The closed set of BASES a path-form pattern resolves against — facts of the CALL (its execution
|
|
501
|
+
* environment and its task), never of the rule:
|
|
502
|
+
* - `cwd` — a relative pattern (`dist/**`, `./x`): the live tracked working directory; when no tracker
|
|
503
|
+
* moved (or the caller keeps none) the task root stands in for it — that stand-in is the WORD's
|
|
504
|
+
* own meaning (the same reading `TOOL_PATH_BASES.cwd` gives a tool's relative slot), not a
|
|
505
|
+
* fallback to a different base;
|
|
506
|
+
* - `root` — a root-relative pattern (`/dist/**`): the task's project root;
|
|
507
|
+
* - `home` — a home-relative pattern (`~/.ssh/**`): the home directory OF THE EXECUTION ENVIRONMENT the
|
|
508
|
+
* call runs in (`ExecutionEnv.homeDir`, declared by the adapter). NEVER the engine process's
|
|
509
|
+
* own home: on a remote or sandboxed leg the two differ, and a `~/` deny resolved against the
|
|
510
|
+
* engine host's home silently guarded the wrong directory (#644).
|
|
511
|
+
* A pattern whose base is ABSENT (or not an absolute path) cannot be judged: the reach is `unreadable`
|
|
512
|
+
* (fail-closed — a standing deny/ask the lane cannot read becomes an ask a person clears), never
|
|
513
|
+
* silence and never a guess. `//abs` patterns need no base. {@link ruleBasesNeeded} tells a host BEFORE
|
|
514
|
+
* prepare which base a rule will need, so it can declare it (or expect the ask).
|
|
504
515
|
*/
|
|
505
|
-
export
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
516
|
+
export declare const PATH_RULE_BASES: readonly ["cwd", "root", "home"];
|
|
517
|
+
export type PathRuleBase = (typeof PATH_RULE_BASES)[number];
|
|
518
|
+
/** The bases a call supplies, keyed by {@link PathRuleBase}; every member optional (absent = not supplied). */
|
|
519
|
+
export type PathRuleBases = {
|
|
520
|
+
readonly [K in PathRuleBase]?: string;
|
|
521
|
+
};
|
|
522
|
+
/** The noun a refusal or an unreadable reason names each base by — the disposition table over the set
|
|
523
|
+
* (a base without a row does not compile: {@link PathRuleBaseLabelCoversEveryBase}). */
|
|
524
|
+
export declare const PATH_RULE_BASE_LABEL: {
|
|
525
|
+
readonly cwd: "the working directory";
|
|
526
|
+
readonly root: "the project root";
|
|
527
|
+
readonly home: "the home directory of the execution environment";
|
|
528
|
+
};
|
|
529
|
+
/** The fence over the label table: `never` while every base has a row. */
|
|
530
|
+
export type PathRuleBaseLabelCoversEveryBase = AssertAllKeysHandled<Exclude<PathRuleBase, keyof typeof PATH_RULE_BASE_LABEL>>;
|
|
531
|
+
/**
|
|
532
|
+
* WHICH base a rule's pattern resolves against — the one classification `resolvePathPattern`, the rule
|
|
533
|
+
* compiler's compile-time check and a host's pre-prepare "can this environment judge this rule?" all read.
|
|
534
|
+
* Empty for a rule that needs none: a `//abs` pattern, a `subpath` (directory) rule, a command rule. The
|
|
535
|
+
* relative form names `cwd`, whose own meaning includes "the root stands in when no tracker moved" — a
|
|
536
|
+
* host that supplies `root` has therefore supplied `cwd`'s stand-in too.
|
|
537
|
+
*/
|
|
538
|
+
export declare function ruleBasesNeeded(rule: Pick<PersistedRule, "match" | "command">): readonly PathRuleBase[];
|
|
510
539
|
/** Is this base usable by {@link resolvePathPattern} — an absolute path with a lexical normal form? A rule
|
|
511
540
|
* compiler asks this at compile time so a pattern is refused rather than compiled inert. */
|
|
512
541
|
export declare function isUsablePathBase(base: string | undefined): boolean;
|
|
513
542
|
/**
|
|
514
|
-
* The PATH family's tightening
|
|
515
|
-
*
|
|
516
|
-
* fences judge with). A `subpath` rule
|
|
517
|
-
* ({@link directoryRuleAdmits}); a `path` rule resolves its
|
|
518
|
-
* matches segment-wise (`*` within a segment, `**` across
|
|
519
|
-
* everything under a directory it names. A command-family rule
|
|
543
|
+
* The PATH family's tightening reach — three-valued like the command family's ({@link programRunReachOf}):
|
|
544
|
+
* does this deny/ask rule reach the call's target path? The target is the caller's ALREADY-RESOLVED
|
|
545
|
+
* lexical-normal absolute path (the same identity the read/write fences judge with). A `subpath` rule
|
|
546
|
+
* reaches the directory and everything under it ({@link directoryRuleAdmits}); a `path` rule resolves its
|
|
547
|
+
* spelling against the call's bases and matches segment-wise (`*` within a segment, `**` across
|
|
548
|
+
* segments), reaching the path it names and everything under a directory it names. A command-family rule
|
|
549
|
+
* reaches no path. `unreadable` (#644): the pattern needs a base the call did not supply (or supplied as
|
|
550
|
+
* a non-absolute spelling) — the rule CANNOT be judged, and the answer says which base; a lane that must
|
|
551
|
+
* fail closed on it turns it into the ask a person clears (the persisted lane's unreadable answer, the
|
|
552
|
+
* content lane's unreadable ask), exactly as an unreadable command is treated. Silence here was the #644
|
|
553
|
+
* defect (a `~/` deny falling back to the engine host's home guarded the wrong directory; a `/…` deny with
|
|
554
|
+
* no root reached nothing at all).
|
|
520
555
|
*/
|
|
521
|
-
export declare function
|
|
556
|
+
export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command">, target: string, bases: PathRuleBases): ProgramRunReachOutcome;
|
|
522
557
|
/**
|
|
523
558
|
* Does this rule's command pattern admit `command`?
|
|
524
559
|
*
|
|
@@ -331,20 +331,47 @@ function parsePathRuleContent(text, tool, content, behavior) {
|
|
|
331
331
|
}
|
|
332
332
|
return { rule: { behavior, rule: formatRuleText(content, "path", tool), tool, match: "path", command: content } };
|
|
333
333
|
}
|
|
334
|
-
|
|
334
|
+
export const PATH_RULE_BASES = ["cwd", "root", "home"];
|
|
335
|
+
export const PATH_RULE_BASE_LABEL = {
|
|
336
|
+
cwd: "the working directory",
|
|
337
|
+
root: "the project root",
|
|
338
|
+
home: "the home directory of the execution environment",
|
|
339
|
+
};
|
|
340
|
+
export function ruleBasesNeeded(rule) {
|
|
341
|
+
if (rule.match !== "path")
|
|
342
|
+
return [];
|
|
343
|
+
const pattern = rule.command;
|
|
335
344
|
if (pattern.startsWith("//"))
|
|
336
|
-
return
|
|
337
|
-
const under = (base, rest) => {
|
|
338
|
-
const normal = base === undefined ? undefined : lexicalNormalAbsolutePathOf(base);
|
|
339
|
-
if (normal === undefined)
|
|
340
|
-
return undefined;
|
|
341
|
-
return (normal === "/" ? "" : normal) + "/" + rest;
|
|
342
|
-
};
|
|
345
|
+
return [];
|
|
343
346
|
if (pattern.startsWith("~/"))
|
|
344
|
-
return
|
|
347
|
+
return ["home"];
|
|
345
348
|
if (pattern.startsWith("/"))
|
|
346
|
-
return
|
|
347
|
-
return
|
|
349
|
+
return ["root"];
|
|
350
|
+
return ["cwd"];
|
|
351
|
+
}
|
|
352
|
+
function baseValueOf(base, bases) {
|
|
353
|
+
switch (base) {
|
|
354
|
+
case "home":
|
|
355
|
+
return bases.home;
|
|
356
|
+
case "root":
|
|
357
|
+
return bases.root;
|
|
358
|
+
case "cwd":
|
|
359
|
+
return bases.cwd ?? bases.root;
|
|
360
|
+
default: {
|
|
361
|
+
const _exhaustive = base;
|
|
362
|
+
return _exhaustive;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function resolvePathPattern(pattern, bases) {
|
|
367
|
+
if (pattern.startsWith("//"))
|
|
368
|
+
return { resolved: "/" + pattern.slice(2) };
|
|
369
|
+
const needed = ruleBasesNeeded({ match: "path", command: pattern })[0];
|
|
370
|
+
const normal = lexicalNormalAbsolutePathOf(baseValueOf(needed, bases) ?? "");
|
|
371
|
+
if (normal === undefined)
|
|
372
|
+
return { missingBase: needed };
|
|
373
|
+
const rest = pattern.startsWith("~/") ? pattern.slice(2) : pattern.startsWith("/") ? pattern.slice(1) : pattern.startsWith("./") ? pattern.slice(2) : pattern;
|
|
374
|
+
return { resolved: (normal === "/" ? "" : normal) + "/" + rest };
|
|
348
375
|
}
|
|
349
376
|
export function isUsablePathBase(base) {
|
|
350
377
|
return base !== undefined && lexicalNormalAbsolutePathOf(base) !== undefined;
|
|
@@ -389,19 +416,20 @@ function globSegmentsReach(globs, target) {
|
|
|
389
416
|
};
|
|
390
417
|
return walk(0, 0);
|
|
391
418
|
}
|
|
392
|
-
export function
|
|
419
|
+
export function pathRuleReachOf(rule, target, bases) {
|
|
393
420
|
if (rule.match === "subpath")
|
|
394
|
-
return directoryRuleAdmits(rule, target);
|
|
421
|
+
return directoryRuleAdmits(rule, target) ? REACHED : NOT_REACHED;
|
|
395
422
|
if (rule.match !== "path")
|
|
396
|
-
return
|
|
423
|
+
return NOT_REACHED;
|
|
397
424
|
if (!isLexicalNormalAbsoluteDir(target) && target !== "/")
|
|
398
|
-
return
|
|
399
|
-
const
|
|
400
|
-
if (
|
|
401
|
-
return
|
|
402
|
-
|
|
425
|
+
return NOT_REACHED;
|
|
426
|
+
const r = resolvePathPattern(rule.command, bases);
|
|
427
|
+
if ("missingBase" in r) {
|
|
428
|
+
return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base to resolve it against` };
|
|
429
|
+
}
|
|
430
|
+
const globs = r.resolved.split("/").filter((sg) => sg !== "");
|
|
403
431
|
const segments = target.split("/").filter((sg) => sg !== "");
|
|
404
|
-
return globSegmentsReach(globs, segments);
|
|
432
|
+
return globSegmentsReach(globs, segments) ? REACHED : NOT_REACHED;
|
|
405
433
|
}
|
|
406
434
|
function hasUnescapedStar(s) {
|
|
407
435
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -778,12 +806,18 @@ export function adjudicatePersistedPathRules(rules, call, targets, bases) {
|
|
|
778
806
|
}
|
|
779
807
|
if (targets.tighten === undefined)
|
|
780
808
|
continue;
|
|
809
|
+
let unreadable;
|
|
781
810
|
for (const rule of rules) {
|
|
782
811
|
if (!eligiblePersisted(rule, call, behavior))
|
|
783
812
|
continue;
|
|
784
|
-
|
|
813
|
+
const outcome = pathRuleReachOf(rule, targets.tighten, bases);
|
|
814
|
+
if (outcome.reach === "reached")
|
|
785
815
|
return { behavior, rules: [rule] };
|
|
816
|
+
if (outcome.reach === "unreadable")
|
|
817
|
+
unreadable ??= { behavior: "ask", rules: [rule], unreadable: outcome.reason };
|
|
786
818
|
}
|
|
819
|
+
if (unreadable !== undefined)
|
|
820
|
+
return unreadable;
|
|
787
821
|
}
|
|
788
822
|
return undefined;
|
|
789
823
|
}
|
|
@@ -132,10 +132,12 @@ export interface PermissionRulePolicyOptions {
|
|
|
132
132
|
*/
|
|
133
133
|
ruleFaces?: (toolName: string) => RuleFaceView | undefined;
|
|
134
134
|
/**
|
|
135
|
-
* The bases a path-form deny/ask pattern resolves against at match time: `root`
|
|
136
|
-
* project root), `cwd` for a relative pattern (
|
|
137
|
-
*
|
|
138
|
-
*
|
|
135
|
+
* The bases a path-form deny/ask pattern resolves against at match time ({@link PathRuleBases}): `root`
|
|
136
|
+
* for `/…` (the task's project root), `cwd` for a relative pattern (the root stands in when absent — the
|
|
137
|
+
* word's own meaning), `home` for `~/…` (the EXECUTION ENVIRONMENT's home — never the engine process's
|
|
138
|
+
* own; #644). Facts of the DEPLOYMENT and the task, never of the rule — a pattern is stored as spelled.
|
|
139
|
+
* A rule whose base is absent (or not an absolute path) here is refused at compile time
|
|
140
|
+
* (`unsupported.path_base`, naming the base — {@link ruleBasesNeeded} is the same classification).
|
|
139
141
|
*/
|
|
140
142
|
pathBases?: PathRuleBases;
|
|
141
143
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
|
|
2
2
|
import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
|
|
3
3
|
import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
|
|
4
|
-
import { isUsablePathBase, parseRuleText,
|
|
4
|
+
import { PATH_RULE_BASE_LABEL, isUsablePathBase, parseRuleText, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
|
|
5
5
|
import { effectivePathTargetOf } from "./effective-path-target.js";
|
|
6
6
|
import { protectivePathTargetOf } from "./tool-registry.js";
|
|
7
7
|
export { parsePermissionRule };
|
|
@@ -186,19 +186,14 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
186
186
|
bad(text, "invalid.command_rule", `${parsedPath.reject.code}: ${parsedPath.reject.message}`);
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!isUsablePathBase(base)) {
|
|
194
|
-
bad(text, "unsupported.path_base", pattern.startsWith("~/")
|
|
195
|
-
? `"${text}" is relative to the home directory, and this policy's \`pathBases.home\` is not an absolute path — the rule would reach nothing`
|
|
196
|
-
: needsRoot
|
|
197
|
-
? `"${text}" is relative to the project root, and this policy was given no absolute \`pathBases.root\` to resolve it against — the rule would reach nothing`
|
|
198
|
-
: `"${text}" is relative to the working directory, and this policy was given no absolute \`pathBases.cwd\` (or \`root\`) to resolve it against — the rule would reach nothing`);
|
|
199
|
-
continue;
|
|
189
|
+
for (const base of ruleBasesNeeded(parsedPath.rule)) {
|
|
190
|
+
const value = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
|
|
191
|
+
if (!isUsablePathBase(value)) {
|
|
192
|
+
bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and this policy was given no absolute \`pathBases.${base}\`${base === "cwd" ? " (or `root`)" : ""} to resolve it against — the rule could not be judged on any call`);
|
|
200
193
|
}
|
|
201
194
|
}
|
|
195
|
+
if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
|
|
196
|
+
continue;
|
|
202
197
|
entry.content[r.behavior].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
|
|
203
198
|
byTool.set(toolName, entry);
|
|
204
199
|
continue;
|
|
@@ -326,8 +321,13 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
326
321
|
let unreadable;
|
|
327
322
|
for (const r of lane) {
|
|
328
323
|
if (r.parsed.match === "subpath" || r.parsed.match === "path") {
|
|
329
|
-
if (target
|
|
324
|
+
if (target === undefined)
|
|
325
|
+
continue;
|
|
326
|
+
const outcome = pathRuleReachOf(r.parsed, target, pathBases);
|
|
327
|
+
if (outcome.reach === "reached")
|
|
330
328
|
return { rule: r };
|
|
329
|
+
if (outcome.reach === "unreadable")
|
|
330
|
+
unreadable ??= { rule: r, unreadable: outcome.reason };
|
|
331
331
|
continue;
|
|
332
332
|
}
|
|
333
333
|
if (typeof command !== "string")
|
|
@@ -342,7 +342,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
342
342
|
};
|
|
343
343
|
const unreadableAsk = (hit, req) => ({
|
|
344
344
|
action: "ask",
|
|
345
|
-
message: `tool "${req.toolName}" needs approval: the
|
|
345
|
+
message: `tool "${req.toolName}" needs approval: the call could not be read against the permission rule ${hit.rule.ruleText} (${hit.unreadable}) — a person must decide`,
|
|
346
346
|
matchedAskRule: hit.rule.ruleText,
|
|
347
347
|
requiresRealApproval: true,
|
|
348
348
|
});
|
package/dist/core/roles.d.ts
CHANGED
|
@@ -98,6 +98,14 @@ export interface ResolvedRole {
|
|
|
98
98
|
* Resolve the model (and default thinking) for a task. Explicit `spec.model` wins; otherwise walk the
|
|
99
99
|
* `modelRole`'s fallback chain over the merged roles (task overrides runner) and use the first role
|
|
100
100
|
* that supplies a model. Throws a helpful error if nothing resolves.
|
|
101
|
+
*
|
|
102
|
+
* @contract roles.side_channels_deployment_only — an engine SIDE CHANNEL (the auto-mode classifier,
|
|
103
|
+
* the compaction/summarize lane, memory consolidation, next-prompt suggestion) resolves here with a
|
|
104
|
+
* `modelRole` and NO run model: it runs on the deployment's roles alone — the boot default at the end of
|
|
105
|
+
* its chain — and never follows the model a run was switched to. Only a run's CHILDREN follow the parent:
|
|
106
|
+
* the delegation seat (`childModelSeat`) passes the parent's `Model` as `spec.model` with the role kept
|
|
107
|
+
* on `modelRole`, so the role's presets still apply. A side channel that must follow a run passes that
|
|
108
|
+
* run's model explicitly; nothing here infers it.
|
|
101
109
|
*/
|
|
102
110
|
export declare function resolveTaskModel(spec: {
|
|
103
111
|
model?: ModelRef;
|
|
@@ -43,7 +43,7 @@ import type { SystemInjectionPriority, TaskNotificationPayload } from "../task-n
|
|
|
43
43
|
import type { OnAsk, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
|
|
44
44
|
import type { RemoteExecutionEnv, SnapshotId, WorkspaceHandle } from "../remote-env.js";
|
|
45
45
|
import type { ToolDisclosureManifest } from "../trace.js";
|
|
46
|
-
import type { AgentDefinition, Brain, CompactOutcome, NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
|
|
46
|
+
import type { AgentDefinition, Brain, CompactOutcome, ModelRoles, NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
|
|
47
47
|
import type { WiringManifest } from "../wiring-manifest.js";
|
|
48
48
|
import type { ActiveSkillFrame } from "./active-skill-scope.js";
|
|
49
49
|
import type { BudgetAxis } from "./assemble-result.js";
|
|
@@ -1486,6 +1486,22 @@ export interface RunInternals {
|
|
|
1486
1486
|
* see from `spec`. Consumed by the `run_workflow` tool wiring (S8c), not by `prepareTask` itself.
|
|
1487
1487
|
*/
|
|
1488
1488
|
workflowDepth?: number;
|
|
1489
|
+
/**
|
|
1490
|
+
* #642 — the host's DECISIONS for parked workflow-agent rows (`wa*` rows with `status:"parked"`), a TRUSTED
|
|
1491
|
+
* run channel like `workflowDepth` (never a `TaskSpec` field, never a tool argument — a model cannot decide
|
|
1492
|
+
* an approval). A host that decided a workflow child's parked checkpoint launches the run that re-invokes
|
|
1493
|
+
* `Workflow({resumeFromRunId})` with the decision here; the Workflow tool applies the entries naming the
|
|
1494
|
+
* resumed run (`RunWorkflowOptions.parkedResume`) and the resume drives the parked child on instead of
|
|
1495
|
+
* re-parking. `inheritedGate` is the cross-process re-supply of the checkpoint's opaque parent constraints
|
|
1496
|
+
* (the same seat the background revival's `reviveClaim.parkedResume` takes). Consumed by the `run_workflow`
|
|
1497
|
+
* tool wiring, not by `prepareTask` itself.
|
|
1498
|
+
*/
|
|
1499
|
+
workflowParkedResume?: ReadonlyArray<{
|
|
1500
|
+
runId: string;
|
|
1501
|
+
token: CheckpointToken;
|
|
1502
|
+
outcome: ResumeOutcome;
|
|
1503
|
+
inheritedGate?: InheritedGate;
|
|
1504
|
+
}>;
|
|
1489
1505
|
/**
|
|
1490
1506
|
* design/110 — set ONLY by the Agent tool's fork route (`Agent(subagent_type:"fork")`, a core caller) on the
|
|
1491
1507
|
* child it spawns: this run IS a forked child. `prepareTask` threads it to tool ctx as `insideFork` so the
|
|
@@ -1762,6 +1778,14 @@ export interface RunInternals {
|
|
|
1762
1778
|
* channel, same posture as every other field here.
|
|
1763
1779
|
*/
|
|
1764
1780
|
sessionReadStates?: SessionReadFileStates;
|
|
1781
|
+
/**
|
|
1782
|
+
* #616 — the Runner's per-session breaker ledger ({@link import("../auto-mode.js").AutoModeBreakerLedger}):
|
|
1783
|
+
* the arming site's `onBreakerOpen` wrap records a trip here, the wiring-manifest phase of a LATER leg of
|
|
1784
|
+
* the same session reads the most recent one onto `autoMode.breaker`. Always set by the Runner's own
|
|
1785
|
+
* prepare call (overriding any caller value, like `sessionReadStates` beside it); absent on a standalone
|
|
1786
|
+
* prepareTask, where no trip is recorded and none is reported. Trusted internals channel.
|
|
1787
|
+
*/
|
|
1788
|
+
autoModeBreakerLedger?: import("../auto-mode.js").AutoModeBreakerLedger;
|
|
1765
1789
|
onTaskNotification?: (notification: TaskNotificationPayload,
|
|
1766
1790
|
/** Injection tier (design/373 — the ladder is LIVE): "next" = the running turn's next boundary
|
|
1767
1791
|
* (arrival order, consecutive frames batch); "later" = the run's would-otherwise-stop seat
|
|
@@ -2357,7 +2381,7 @@ export interface HarnessHandlersDeps {
|
|
|
2357
2381
|
* THE MEMBER SET IS MEASURED, NOT DESIGNED: it is exactly the union of what those consumers read off a
|
|
2358
2382
|
* Runner today (a TS-Program census over `src/`, pinned in test/runner-self-seat.test.ts so a member
|
|
2359
2383
|
* nobody reads reds as dead weight and a consumer reaching for a member outside it reds as a widening).
|
|
2360
|
-
* `
|
|
2384
|
+
* `swapDeps`, `sideQuery` and the constructor are NOT here — no consumer reaches them; a host that
|
|
2361
2385
|
* needs them holds the class. The class implements this interface, so the two cannot drift apart.
|
|
2362
2386
|
*
|
|
2363
2387
|
* Signatures are spelled out (not `Pick<Runner, …>`), because naming the class from here would be the
|
|
@@ -2365,7 +2389,7 @@ export interface HarnessHandlersDeps {
|
|
|
2365
2389
|
*/
|
|
2366
2390
|
/**
|
|
2367
2391
|
* The Runner's deployment deps as a LIVE read for the lanes that read them after an await (the resume ladder's
|
|
2368
|
-
* rungs). `Runner.deps` is not a constant: `
|
|
2392
|
+
* rungs). `Runner.deps` is not a constant: `swapDeps` replaces the object, so a lane handed the object at its
|
|
2369
2393
|
* call would read a stale catalog — and a stale anything — where the one-function ladder read `this.deps.x` at the
|
|
2370
2394
|
* instant of the read. The seat is the Runner's own view (a getter over the private field), so `runner.deps.x`
|
|
2371
2395
|
* in a lane is exactly `this.deps.x` in the method it came from.
|
|
@@ -2377,11 +2401,12 @@ export interface RunnerDepsSeat {
|
|
|
2377
2401
|
export interface RunnerSelfSeat {
|
|
2378
2402
|
/** The session store this Runner was constructed over — the delegation tools acquire / release / fork through it. */
|
|
2379
2403
|
readonly sessions: SessionStore;
|
|
2380
|
-
/** The deployment agent + model catalog, READ-ONLY (a fresh copy per read; the definitions are the shared objects). */
|
|
2404
|
+
/** The deployment agent + model catalog, READ-ONLY (a fresh copy per read; the definitions are the shared objects); `roles` = `RunnerDeps.roles` (the delegation seat's `childModelSeat` reads `roles.subagent`). */
|
|
2381
2405
|
readonly agentCatalog: {
|
|
2382
2406
|
agents?: AgentDefinition[];
|
|
2383
2407
|
builtinAgents?: boolean;
|
|
2384
2408
|
models?: Record<string, Model>;
|
|
2409
|
+
roles?: ModelRoles;
|
|
2385
2410
|
};
|
|
2386
2411
|
/** The deployment-level gating baseline (deps toolPolicy / hooks) as delegate wrappers — for assemblers that compose with it. */
|
|
2387
2412
|
readonly gateBaseline: {
|