@sema-agent/core 7.4.0 → 7.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/peer-notices.d.ts +11 -1
  4. package/dist/agents/peer-session-drain.js +2 -0
  5. package/dist/agents/verify.d.ts +2 -1
  6. package/dist/core/ask-origin.d.ts +130 -0
  7. package/dist/core/ask-origin.js +35 -0
  8. package/dist/core/auto-mode-arming.d.ts +40 -1
  9. package/dist/core/auto-mode-arming.js +51 -3
  10. package/dist/core/auto-mode.d.ts +87 -10
  11. package/dist/core/auto-mode.js +34 -6
  12. package/dist/core/checkpoint-store.d.ts +41 -6
  13. package/dist/core/checkpoint-store.js +8 -0
  14. package/dist/core/hooks.d.ts +63 -19
  15. package/dist/core/hooks.js +37 -16
  16. package/dist/core/mcp.d.ts +47 -55
  17. package/dist/core/mcp.js +89 -31
  18. package/dist/core/park-selfcheck.js +3 -0
  19. package/dist/core/permission-rule-consent.d.ts +2 -11
  20. package/dist/core/permission-rule-consent.js +13 -62
  21. package/dist/core/permission-rule-org.d.ts +70 -54
  22. package/dist/core/permission-rule-org.js +47 -61
  23. package/dist/core/permission-rule-provider.d.ts +162 -0
  24. package/dist/core/permission-rule-provider.js +148 -0
  25. package/dist/core/permission-rule-session.d.ts +23 -19
  26. package/dist/core/permission-rule-session.js +5 -5
  27. package/dist/core/permission-rule-store.d.ts +46 -21
  28. package/dist/core/permission-rule-store.js +13 -6
  29. package/dist/core/permission-rule-sync.d.ts +2 -1
  30. package/dist/core/permission-rule-sync.js +11 -0
  31. package/dist/core/runner/assemble-result.d.ts +3 -2
  32. package/dist/core/runner/checkpoint-scope.d.ts +32 -0
  33. package/dist/core/runner/checkpoint-scope.js +4 -0
  34. package/dist/core/runner/contracts.d.ts +1878 -0
  35. package/dist/core/runner/contracts.js +1 -0
  36. package/dist/core/runner/denial-limit-arms.d.ts +57 -31
  37. package/dist/core/runner/denial-limit-arms.js +42 -17
  38. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  39. package/dist/core/runner/derived-route-fallback.js +16 -0
  40. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  41. package/dist/core/runner/prepare-announce-once.d.ts +83 -0
  42. package/dist/core/runner/prepare-announce-once.js +105 -0
  43. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  44. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  45. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  46. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  47. package/dist/core/runner/prepare-defer-classify.js +107 -0
  48. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  49. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  50. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  51. package/dist/core/runner/prepare-execution-env.js +86 -0
  52. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  53. package/dist/core/runner/prepare-file-history.js +383 -0
  54. package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
  55. package/dist/core/runner/prepare-hands-readface.js +3 -3
  56. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  57. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  58. package/dist/core/runner/prepare-listings.d.ts +77 -0
  59. package/dist/core/runner/prepare-listings.js +76 -0
  60. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  61. package/dist/core/runner/prepare-lsp.js +27 -0
  62. package/dist/core/runner/prepare-memory.d.ts +1 -1
  63. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  64. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  65. package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
  66. package/dist/core/runner/prepare-permission-rules.js +140 -0
  67. package/dist/core/runner/prepare-project-context.d.ts +131 -0
  68. package/dist/core/runner/prepare-project-context.js +150 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  71. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  72. package/dist/core/runner/prepare-protocol-tools.js +182 -0
  73. package/dist/core/runner/prepare-question-face.d.ts +119 -0
  74. package/dist/core/runner/prepare-question-face.js +83 -0
  75. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  76. package/dist/core/runner/prepare-run-refs.js +39 -0
  77. package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
  78. package/dist/core/runner/prepare-task.d.ts +11 -1815
  79. package/dist/core/runner/prepare-task.js +138 -2542
  80. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  81. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  82. package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
  83. package/dist/core/runner/prepare-wiring-manifest.js +240 -0
  84. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
  85. package/dist/core/runner/prepare-workspace-restore.js +1 -22
  86. package/dist/core/runner/rollback-stack.d.ts +32 -0
  87. package/dist/core/runner/rollback-stack.js +30 -0
  88. package/dist/core/runner/runtask.d.ts +11 -2
  89. package/dist/core/runner/runtask.js +27 -9
  90. package/dist/core/runner/workspace-path.d.ts +33 -0
  91. package/dist/core/runner/workspace-path.js +22 -0
  92. package/dist/core/sensitive-path-policy.d.ts +16 -0
  93. package/dist/core/sensitive-path-policy.js +1 -1
  94. package/dist/core/tool-policy.d.ts +57 -9
  95. package/dist/core/tool-policy.js +11 -0
  96. package/dist/core/types.d.ts +63 -51
  97. package/dist/core/wiring-manifest.d.ts +40 -3
  98. package/dist/core/wiring-manifest.js +4 -3
  99. package/dist/core/write-protect.d.ts +13 -2
  100. package/dist/core/write-protect.js +58 -29
  101. package/dist/engine/harness/types.d.ts +38 -16
  102. package/dist/engine/harness/types.js +25 -1
  103. package/dist/engine/session/session.d.ts +3 -11
  104. package/dist/index.d.ts +10 -6
  105. package/dist/index.js +9 -5
  106. package/dist/internal/harness.d.ts +1 -0
  107. package/dist/stores/file/adoption/adopt.d.ts +1 -1
  108. package/dist/stores/file/adoption/marker.d.ts +1 -1
  109. package/dist/stores/file/permission-rule-adopt.js +4 -3
  110. package/dist/stores/file/permission-rule-store.d.ts +65 -25
  111. package/dist/stores/file/permission-rule-store.js +215 -37
  112. package/dist/stores/file/task-list-store.d.ts +1 -1
  113. package/dist/tools/fs/read-face.d.ts +1 -1
  114. package/package.json +8 -2
  115. package/test/export-surface.snapshot.json +76 -28
@@ -32,7 +32,7 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
32
32
  ".zsh_history",
33
33
  ];
34
34
  const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
35
- function dataRootSpellings(explicit) {
35
+ export function dataRootSpellings(explicit) {
36
36
  const raw = explicit ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
37
37
  const home = homedir();
38
38
  const tilde = expandHomeTilde(raw, home);
@@ -211,14 +211,18 @@ export type PermissionResult = {
211
211
  /** #548 (additive): this ask is the classifier DENIAL-LIMIT fallback — the block that reached a
212
212
  * bound (CC 2.1.250 `FO`, count-then-judge) handed to a person instead of being denied.
213
213
  * ENGINE-STAMPED beside `requiresRealApproval: true` and `decisionReason: "classifier"` at the
214
- * classifier block sites (the gate's own and the inherited-lane arms); carries the counts and
215
- * THIS ask's auto-deny window see {@link import("./auto-mode.js").DenialLimitFallback}.
216
- * Consumers: the gate's classifier step refuses to re-judge an ask carrying it (the classifier
217
- * already spoke; rules and classifier stand BEFORE this ask, never after), and the ask resolver
218
- * arms its deadline from `autoDenyAfterMs`. Carried onto the approval request. A policy that
219
- * self-declares it only ever opts its own ask OUT of classifier resolution and INTO a bounded
220
- * wait (tightening the same safe direction as `matchedAskRule`). */
221
- denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
214
+ * classifier block sites (the gate's own and the inherited-lane arms); carries the COUNTS ONLY —
215
+ * the auto-deny window is a fact of the ask's ROUTE, so this type has no member to hold one
216
+ * ({@link import("./auto-mode.js").UnarmedDenialLimitFallback}) and the window appears first on
217
+ * the {@link AskRequest} the mint station builds, iff that request goes to a live function
218
+ * approver. Consumers: the gate's classifier step refuses to re-judge an ask carrying it (the
219
+ * classifier already spoke; rules and classifier stand BEFORE this ask, never after), and the ask
220
+ * resolver arms its deadline from the REQUEST's `autoDenyAfterMs`. Carried onto the approval
221
+ * request through one of the two route stations. A policy that self-declares it only ever opts
222
+ * its own ask OUT of classifier resolution and INTO the fallback's bounded lane (tightening — the
223
+ * same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
224
+ * route it would be a window for has not been chosen yet. */
225
+ denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
222
226
  /** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
223
227
  * ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
224
228
  * egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
@@ -277,6 +281,17 @@ export type PermissionResult = {
277
281
  * value nobody stamps over can only shrink or annotate the card's ADVISORY offers — every
278
282
  * offered rule still passes the validator at redemption. */
279
283
  segmentCoverage?: readonly import("./permission-rule-model.js").SegmentCoverage[];
284
+ /** WHICH AUTHORITY raised this ask — one word from the closed {@link import("./ask-origin.js").AskOrigin}
285
+ * set (org rule / org unavailable / hook / explicit ask rule / the classifier's denial-limit
286
+ * fallback / the reserved question tool / a marked call / plain policy). ENGINE-STAMPED inside the
287
+ * gate at the same single stamp point as `ruleEvidence`, derived from what the gate's own layers
288
+ * did ({@link import("./ask-origin.js").askOriginOf}) — a policy that self-declares it is overwritten
289
+ * there, so the member is never a channel a policy can loosen through. Consumers: the gate's
290
+ * auto-mode classifier step admits an ask by ONE table lookup on this word
291
+ * ({@link import("./ask-origin.js").classifierMayAnswer}) instead of a chain of per-kind
292
+ * exclusions; an approval surface may render the card by it. Optional on the TYPE only because a
293
+ * decision is caller-composable; every ask the gate hands to its resolver carries it. */
294
+ origin?: import("./ask-origin.js").AskOrigin;
280
295
  } | {
281
296
  action: "deny";
282
297
  updatedInput?: unknown;
@@ -1190,8 +1205,25 @@ export interface AskRequest {
1190
1205
  * this ask's own auto-deny window. Two readers: a card renders it as a countdown; {@link resolveAsk}
1191
1206
  * arms its deadline from `autoDenyAfterMs` (> 0 ⇒ an unanswered function approver auto-denies at
1192
1207
  * that deadline — `settledBy:"timeout"`, `resolution:"window_expired"`, `autoDenied:true`). Filled
1193
- * by the gate from the decision, never a caller/worker-settable field. */
1208
+ * by the gate from the decision, never a caller/worker-settable field. This is the ARMED type: the
1209
+ * decision's member carries the counts alone, and the only two ways to reach this one are the route
1210
+ * stations — `AutoModeDenialTracker.armTimedWindow` (a hand-out to a live function approver, the one
1211
+ * writer of a non-zero window) and `unarmedWindow` (every route that cannot count down). An adapter
1212
+ * that forwards a decision's member here does not compile, which is the point: the window is the
1213
+ * route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
1194
1214
  readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1215
+ /** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
1216
+ * member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
1217
+ * The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
1218
+ * derive it from the ancestor's decision under the station facts (a self-declared value on that
1219
+ * decision is never read). An approver renders the card by it; a durable park persists the same word
1220
+ * on its row (`PendingAction.tool_approval.origin`). Filled by the mint station, never written by a
1221
+ * caller/worker directly — a policy can only STEER the derivation toward a stricter class by
1222
+ * self-declaring one of its tighten-only inputs (`decisionReason: "hook"`, `matchedAskRule`,
1223
+ * `denialLimitFallback`), never toward `policy`; a card that attributes an ask to a hook or a rule is
1224
+ * therefore reading the deployment's own claim about its own ask, which is the direction that cannot
1225
+ * loosen anything. */
1226
+ readonly origin?: import("./ask-origin.js").AskOrigin;
1195
1227
  /** Engine-judged risk axes of the action awaiting approval, for the human decision surface: a
1196
1228
  * coarse `requiresRealApproval` cannot tell "cannot be undone" from "data leaves the machine",
1197
1229
  * and those call for different scrutiny. Filled by the gate from the SAME resolved axes the
@@ -1383,6 +1415,22 @@ export declare function withDelegationProvenance(onAsk: OnAsk, delegation: AskDe
1383
1415
  * consultation". A non-wrapper value (including `"allow"`/`"deny"`/undefined) is its own identity.
1384
1416
  */
1385
1417
  export declare function askApproverIdentity(onAsk: unknown): unknown;
1418
+ /**
1419
+ * Is this `onAsk` seat a LIVE APPROVER — a person the run can actually reach?
1420
+ *
1421
+ * Only a function is. A blanket `"allow"`/`"deny"` string is a policy setting (the same reading the
1422
+ * interaction-posture door gives it), and an absent seat is nobody at all; neither can answer an ask, so
1423
+ * neither may outrank a durable park.
1424
+ *
1425
+ * ONE definition on purpose. Two sites must agree on this — the run's own sync-first decline, and the
1426
+ * mandate frozen into the chain its children inherit — and when they were two hand-written booleans they
1427
+ * drifted: the decline narrowed to the function test while the freeze kept testing mere presence, and
1428
+ * each still read correctly on its own, so nothing pointed at the pair. A blanket seat was therefore not
1429
+ * an approver for the parent's asks and an approver for its children's. Declared here, in the vocabulary
1430
+ * beside {@link askApproverIdentity}, because the two sites are now two prepare phases (the inherited-gate
1431
+ * fold and the wiring station) plus the orchestrator, and each reaches DOWN for the one definition.
1432
+ */
1433
+ export declare function isLiveApproverSeat(onAsk: OnAsk | undefined): boolean;
1386
1434
  /** EXACT structural clone for the approval boundary (alias isolation) — fail-closed, never lossy,
1387
1435
  * never identity: a JSON round-trip would silently DROP unclonable members (presented ≠ executing =
1388
1436
  * consent misrepresentation) and an identity fallback would hand the approver the live executing
@@ -133,6 +133,10 @@ export function constraintChainDigest(chain) {
133
133
  }
134
134
  const ALLOW = { action: "allow" };
135
135
  const RETIRED_TEXT_FIELD = "reason";
136
+ const RETIRED_WINDOW_MEMBER = "autoDenyAfterMs";
137
+ const RETIRED_WINDOW_MEMBER_DENY_MESSAGE = `a permission decision's "denialLimitFallback" carries "${RETIRED_WINDOW_MEMBER}" — the decision names the counts only; ` +
138
+ `the auto-deny window is decided by the ask's route (a live approver arms it, every other route runs none) and cannot be ` +
139
+ `declared by a policy; denied fail-closed rather than silently replacing the declared number`;
136
140
  const RETIRED_TEXT_FIELD_DENY_MESSAGE = `a permission decision carries the retired "${RETIRED_TEXT_FIELD}" field — rename it to "message" (the one text field ` +
137
141
  `a decision carries); denied fail-closed rather than executing a decision whose text this layer cannot read`;
138
142
  export function refuseOutOfContractDecision(d, opts) {
@@ -141,6 +145,10 @@ export function refuseOutOfContractDecision(d, opts) {
141
145
  if (Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD)) {
142
146
  return { action: "deny", message: RETIRED_TEXT_FIELD_DENY_MESSAGE, decisionReason: "rule" };
143
147
  }
148
+ const fb = d.denialLimitFallback;
149
+ if (typeof fb === "object" && fb !== null && fb.autoDenyAfterMs !== undefined) {
150
+ return { action: "deny", message: RETIRED_WINDOW_MEMBER_DENY_MESSAGE, decisionReason: "rule" };
151
+ }
144
152
  if (opts?.reasonIsNonInput === true)
145
153
  return d;
146
154
  const dr = d.decisionReason;
@@ -839,6 +847,9 @@ export function withDelegationProvenance(onAsk, delegation) {
839
847
  export function askApproverIdentity(onAsk) {
840
848
  return typeof onAsk === "function" ? (delegatedApproverRoot.get(onAsk) ?? onAsk) : onAsk;
841
849
  }
850
+ export function isLiveApproverSeat(onAsk) {
851
+ return typeof onAsk === "function";
852
+ }
842
853
  export function tryCloneArgs(v) {
843
854
  try {
844
855
  const value = structuredClone(v);
@@ -662,7 +662,7 @@ export interface ToolExecuteContext {
662
662
  * at all; a park under a screening entry announces the requirement on `onError`
663
663
  * (classification `screening-constraint-in-durable-chain`) at park time.
664
664
  */
665
- inheritedGate?: import("./runner/prepare-task.js").InheritedGate;
665
+ inheritedGate?: import("./runner/contracts.js").InheritedGate;
666
666
  };
667
667
  /**
668
668
  * design/176 — the peer-chain SEED for a message-driven (tier-3) revival: the hop chain of the
@@ -1046,7 +1046,7 @@ export interface ToolExecuteContext {
1046
1046
  * read-only, NEVER a model/tool argument (same posture as {@link activeSkillScope}). Undefined when the
1047
1047
  * tool runs outside a Runner task.
1048
1048
  */
1049
- inheritedGateForChildren?: () => import("./runner/prepare-task.js").InheritedGate;
1049
+ inheritedGateForChildren?: () => import("./runner/contracts.js").InheritedGate;
1050
1050
  /**
1051
1051
  * design/180 half A — the delegation runtime-provenance ARMING face. Runner-filled; a delegation
1052
1052
  * tool calls it at spawn time: a non-undefined return means this (parent) run is armed (it mounts
@@ -1492,6 +1492,14 @@ export interface AgentDefinition {
1492
1492
  export interface McpServerSpec {
1493
1493
  /** Stable name; tool names are namespaced as `<name>__<tool>` to avoid collisions. */
1494
1494
  name: string;
1495
+ /**
1496
+ * Where this declaration CAME FROM, in the declaring side's own words (a shell's `plugin` / `user` /
1497
+ * `project` / `local` config tiers, or anything else the deployment finds useful). Core reads
1498
+ * nothing off it: it is echoed verbatim (single-line, length-bounded) onto the wiring manifest's
1499
+ * per-server read face (`WiringManifest.mcp[].source`) so a consumer can group servers by their
1500
+ * declaring tier without keeping a side table keyed on names. Optional; absent stays absent.
1501
+ */
1502
+ source?: string;
1495
1503
  transport: {
1496
1504
  kind: "stdio";
1497
1505
  command: string;
@@ -4495,6 +4503,22 @@ export type TaskEvent = ({
4495
4503
  * `settledBy`/reason instead). A consumer must not read a semantic out of the absence.
4496
4504
  */
4497
4505
  resolution?: import("./tool-policy.js").AskDenyResolution;
4506
+ /**
4507
+ * #557 (additive) — `true` ⇔ this frame closes a call the classifier DENIAL-LIMIT fallback ask
4508
+ * auto-denied itself: the ask's own window (`AskRequest.denialLimitFallback.autoDenyAfterMs`,
4509
+ * 120 s by default) elapsed with nobody answering. Such a frame also reads `settledBy:"timeout"`
4510
+ * + `resolution:"window_expired"` — the SAME two words the approval factory's own window
4511
+ * (`createApprovalPolicy.approvalTimeoutMs`) settles with — and this bit is the discriminant: the
4512
+ * factory's deadline never carries it, so a shell that sees the pair without it is looking at the
4513
+ * host-configured approval window having elapsed, and with it at the classifier fallback's own
4514
+ * countdown having run out. A DURABLE park's expiry is neither: it settles on the decide lane with
4515
+ * its own `settledBy`/reason and carries no `resolution` word at all (see that field's doc above).
4516
+ * Minted at the resolver's deadline arm and carried on
4517
+ * the engine-owned settlement sideband (never read out of the tool result, never writable by a
4518
+ * tool or policy — a marker on an executing call is dropped with a defect reported). ABSENT on
4519
+ * everything else; a consumer must not read a semantic out of the absence.
4520
+ */
4521
+ autoDenied?: true;
4498
4522
  /**
4499
4523
  * design/252 G-7 — WHOSE settlement that was: the identifier the approval channel reported for
4500
4524
  * the party that ended this wait, beside the {@link settledBy} word that says what KIND of end
@@ -6872,64 +6896,50 @@ export interface RunnerDeps {
6872
6896
  */
6873
6897
  sessionPolicyStore?: import("./session-policy-store.js").SessionPolicyStore;
6874
6898
  /**
6875
- * design/179persisted ALLOW rules: the standing form of approvals a person already gave, so a command
6876
- * they confirmed once is not asked about again.
6877
- *
6878
- * This is the one seam in this file that LOOSENS, and it is shaped so it can only do so within limits
6879
- * the engine holds. The provider hands out a READ face anchored to one verified principal. The write
6880
- * face is an exported BACKEND CONTRACT (ruled 2026-08-10: an out-of-repo store twin builds against the
6881
- * same definitions instead of mirroring them), but the ENGINE reaches it only on the consent lanes —
6882
- * redemption of a confirmed human decision, the tighten-delete, and the sync join a wiring invariant
6883
- * pinned by a registered-caller scan (test/permission-rule-writer-callers); no exported convenience
6884
- * mints a rule around consent. Rules are consumed post-fold, in the gate's ask branch, and never
6885
- * resolve an ask carrying `requiresRealApproval` or one a PreToolUse hook raised.
6886
- *
6887
- * Omitted ⇒ the lane does not exist: no rules are read, no field is added to any ask, and the decision
6888
- * path is byte-identical to a build without it. An unauthenticated task (no `principal`) resolves to
6889
- * zero rules rather than to a shared bucket, and a store that cannot be read yields zero rules plus a
6890
- * disclosure a loosening face fails toward asking.
6891
- */
6892
- permissionRuleStore?: import("./permission-rule-store.js").PermissionRuleStoreProvider;
6899
+ * design/389the ONE permission-rule seam: the unified store, partitioned by source
6900
+ * (`org | user | project | session`), built by `createPermissionRuleStoreProvider({ durable?, org?, session? })`.
6901
+ *
6902
+ * It is the one seam in this file that LOOSENS, and it is shaped so it can only do so within limits the
6903
+ * engine holds. The provider hands out a store whose ONE read query answers every partition with each
6904
+ * row's source and effective status; the write face is an exported BACKEND CONTRACT for the durable
6905
+ * partition (an out-of-repo store twin builds against the same definitions instead of mirroring them),
6906
+ * but the ENGINE reaches it only on the consent lanes redemption of a confirmed human decision, the
6907
+ * tighten-delete, and the sync join — a wiring invariant pinned by a registered-caller scan
6908
+ * (test/permission-rule-writer-callers); no exported convenience mints a rule around consent. Personal
6909
+ * rules are consumed post-fold, in the gate's ask branch, and never resolve an ask carrying
6910
+ * `requiresRealApproval` or one a PreToolUse hook raised.
6911
+ *
6912
+ * The ORG partition (present iff the provider was composed with an `org` config — the governance
6913
+ * declaration, design/182 §7.4) is the outermost TIGHTENING authority: every tool call is adjudicated
6914
+ * against the org's published deny/ask snapshot BEFORE the ask-resolution chain (org deny > org ask >
6915
+ * personal allow rule > bare ask), and while the org partition cannot adjudicate the WHOLE decision
6916
+ * boundary fails closed — every terminal allow becomes a real-approval ask and both ask→allow seams are
6917
+ * disarmed. The SESSION partition (present iff composed with one) is where `{kind:"session"}` grants
6918
+ * land and are read from, spliced in front of the durable rows on every adjudication of that session.
6919
+ *
6920
+ * Omitted ⇒ no rule lane, no org layer, no session source: no rules are read, no field is added to any
6921
+ * ask, and the decision path is byte-identical to a build without it. An unauthenticated task (no
6922
+ * `principal`) resolves to zero personal rules rather than to a shared bucket (the org partition still
6923
+ * adjudicates), and a store whose durable partition cannot be read yields zero rules plus a disclosure
6924
+ * — a loosening face fails toward asking.
6925
+ */
6926
+ permissionRuleStore?: import("./permission-rule-provider.js").PermissionRuleStoreProvider;
6893
6927
  /**
6894
6928
  * design/182 §4.5 (F-011) — declare that this deployment keeps its permission rules in the
6895
6929
  * IDENTITY-LESS local bucket: a task with no `principal` resolves rules through the provider's
6896
6930
  * `forLocalOwner()` face instead of resolving to zero rules.
6897
6931
  *
6898
- * A DECLARATION, never an inference (same rule as {@link permissionRuleOrg}): the local bucket is
6899
- * "this machine's owner", a fact only the deployment knows. Omitted/`false` ⇒ v1 exactly an
6900
- * unauthenticated task reads no rules. Declared `true` while the wired provider has no local-owner
6901
- * face (or while no provider is wired at all) is a configuration contradiction and is refused loudly
6902
- * at prepare, rather than silently degrading to "the rules this person approved stopped applying".
6932
+ * A DECLARATION, never an inference: the local bucket is "this machine's owner", a fact only the
6933
+ * deployment knows. Omitted/`false` ⇒ an unauthenticated task reads no rules. Declared `true` while the
6934
+ * wired provider has no local-owner face (or while no provider is wired at all) is a configuration
6935
+ * contradiction and is refused loudly at prepare, rather than silently degrading to "the rules this
6936
+ * person approved stopped applying".
6903
6937
  *
6904
6938
  * A local-owner bucket cannot cloud-sync (syncing is an authenticated act); it is adopted into a
6905
6939
  * principal bucket by `adoptFilePermissionRuleStore`, after which the local-owner face resolves the
6906
6940
  * adopted principal's bucket forever.
6907
6941
  */
6908
6942
  localOwnerRules?: boolean;
6909
- /**
6910
- * design/382 §4.3 — the SESSION-RULE OVERLAY: where `{kind:"session"}` scoped allow rules live
6911
- * (the session's own state, never the persisted store). Wired, the gate's persisted-rule lane
6912
- * splices the CURRENT session's overlay rows in front of the store's `list()` on every
6913
- * adjudication — session > project > global, the narrowest-first reporting order — and threads the
6914
- * session's identity as the eligibility context's third axis; a host lands rows into it through
6915
- * the consent protocol (`RuleConsentDeps.sessionRules`, a session-scope candidate's redemption).
6916
- * Omitted ⇒ the session dimension does not exist at this gate and the lane is byte-identical.
6917
- * See `permission-rule-session.ts` for the contract and the reference implementation.
6918
- */
6919
- sessionPermissionRules?: import("./permission-rule-session.js").SessionRuleOverlay;
6920
- /**
6921
- * design/182 §7 — the ORG rule overlay for an org-GOVERNED deployment. Constructed with
6922
- * `createOrgRuleOverlay` (that constructor is the boot gate: a governed declaration with no snapshot
6923
- * provider refuses to boot), so a value here IS the declaration — nothing is inferred from wiring.
6924
- *
6925
- * Present ⇒ every tool call is adjudicated against the org's published deny/ask snapshot BEFORE the
6926
- * ask-resolution chain (org deny > org ask > personal allow rule > bare ask), and while the overlay
6927
- * cannot adjudicate (never installed / past the staleness bound / a refused rollback) the WHOLE
6928
- * decision boundary fails closed: every terminal allow becomes a real-approval ask and both
6929
- * ask→allow seams are disarmed. Omitted ⇒ the org layer does not exist and the decision path is
6930
- * byte-identical to a build without it.
6931
- */
6932
- permissionRuleOrg?: import("./permission-rule-org.js").OrgRuleOverlay;
6933
6943
  /**
6934
6944
  * design/182 §9 — DECLARE that this deployment drives cloud sync (`syncPermissionRules`) for the
6935
6945
  * wired rule store. Purely a disclosure input: it is reported as `permissionRules.syncWired` on the
@@ -7064,8 +7074,10 @@ export interface RunnerDeps {
7064
7074
  * — the block that reaches a bound is itself the one that becomes an `ask` (the 3rd consecutive
7065
7075
  * block asks). That ask carries `requiresRealApproval: true` (no automatic lane may clear it — not
7066
7076
  * a sandbox admission, not an inherited resolver, not a blanket `onAsk:"allow"`) plus the additive
7067
- * `denialLimitFallback` member with the counts and its own auto-deny window; an unanswered ask
7068
- * auto-denies after `autoDenyAfterMs` (default 120s; `0` = no window). A classifier allow, or a
7077
+ * `denialLimitFallback` member with the counts and its own auto-deny window; an ask handed to a LIVE
7078
+ * function approver and left unanswered auto-denies after `autoDenyAfterMs` (default 120s; `0` = no
7079
+ * window) — the window is armed at that hand-out only, so a durably PARKED fallback carries `0`
7080
+ * (nothing counts down on that lane). A classifier allow, or a
7069
7081
  * person's allow of the fallback ask, zeroes the consecutive count; reaching the total bound
7070
7082
  * zeroes everything. With no approver wired at all (headless), the fallback has nowhere to go and
7071
7083
  * the run STOPS with `TaskResult.errorCode = "classifier.denial_limit"` (a notice of the same
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import type { RunnerDeps, TaskSpec } from "./types.js";
24
24
  import { type StoreDurability } from "./checkpoint-store.js";
25
+ import type { McpFailureCode } from "./mcp.js";
25
26
  export type { StoreDurability };
26
27
  /** The manifest-face projection of {@link StoreDurability}: `"declared_durable"` (the name says it —
27
28
  * a DECLARATION relayed, not a verification) vs `"process_local"` (declared so, or fail-closed
@@ -78,6 +79,26 @@ export type ParkLaneReason = "no_checkpoint_store" | "no_durable_approval_opt_in
78
79
  */
79
80
  export declare const AUTO_MODE_ARM_REASONS: readonly ["armed", "no_intent", "no_face", "denied", "resolver_fault", "latch_open"];
80
81
  export type AutoModeArmReason = (typeof AUTO_MODE_ARM_REASONS)[number];
82
+ /**
83
+ * One declared MCP server's materialization result for THIS leg — the per-server connection-state
84
+ * read face on {@link WiringManifest.mcp}. `status` is the connect-time verdict of this leg's
85
+ * materialize (a snapshot, never a heartbeat — the same semantics as `MaterializedMcp.statuses`,
86
+ * which it is projected from): `connected` (tools listed; `toolCount` = the mounted roster size after
87
+ * intake), `failed` (the connect or the tool listing threw — `errorCode` names the class, `error` the
88
+ * neutralized, length-bounded remote text), `skipped` (declared on the spec but this leg produced no
89
+ * status for it — the materialization did not reach it). `source` is the DECLARING side's own label
90
+ * (`McpServerSpec.source`: plugin / user / project / local, or anything the deployment writes),
91
+ * echoed back so a shell can group servers by where the declaration came from; core reads nothing
92
+ * off it. Ordered as declared.
93
+ */
94
+ export interface WiringManifestMcpEntry {
95
+ name: string;
96
+ source?: string;
97
+ status: "connected" | "failed" | "skipped";
98
+ errorCode?: McpFailureCode;
99
+ error?: string;
100
+ toolCount: number;
101
+ }
81
102
  /**
82
103
  * design/173 §2.2/§8.4 — the wiring manifest. Consumers MUST ignore unknown fields
83
104
  * (`schemaVersion` bumps only on a semantic break of an EXISTING field). The TaskEvent face is the
@@ -178,8 +199,8 @@ export interface WiringManifest {
178
199
  * reads off the manifest rather than infers. `false` on a deployment that does not sync — same
179
200
  * present-and-false shape as `storeWired`, for the same reason. */
180
201
  syncWired: boolean;
181
- /** design/182 §7 — this deployment DECLARED org governance (`RunnerDeps.permissionRuleOrg`, whose
182
- * constructor refuses to boot a governed declaration with no snapshot provider). `true` also means
202
+ /** design/182 §7 / design/389 — this deployment DECLARED org governance (the store was composed with
203
+ * an `org` partition, whose constructor refuses a declaration with no snapshot source). `true` also means
183
204
  * the fail-closed availability contract is armed: while no org snapshot can be adjudicated
184
205
  * against, every terminal allow tightens to a real-approval ask. */
185
206
  orgGoverned: boolean;
@@ -225,6 +246,19 @@ export interface WiringManifest {
225
246
  armed: boolean;
226
247
  reason: AutoModeArmReason;
227
248
  };
249
+ /**
250
+ * EFFECTIVE half only, and ALWAYS present on an engine-minted effective manifest (the `autoMode`
251
+ * posture: every leg has an answer — an empty array is "this leg declared no servers", absence is
252
+ * an older mint or an external derivation). The per-server MCP connection-state read face: one
253
+ * {@link WiringManifestMcpEntry} per server the leg's frozen spec declared, in declaration order,
254
+ * projected from THIS leg's materialization (`MaterializedMcp.statuses`) — a connect-time snapshot
255
+ * of "did this leg connect", never a live health signal. A shell that shows per-server state
256
+ * (connected / failed with a cause a person can act on / tool counts, grouped by declaring
257
+ * `source`) reads it here instead of inferring from the roster or the failure notices. NOT part of
258
+ * {@link configFingerprint}: connection state is a fact about this leg's attempt, not about the
259
+ * assembly, and legs of one assembly must fingerprint alike whether or not a server was up.
260
+ */
261
+ mcp?: readonly WiringManifestMcpEntry[];
228
262
  /** EFFECTIVE half only — a short, non-sensitive fingerprint (sha256 prefix over the canonical
229
263
  * JSON of this manifest's own resolved facts; every field here is an enum/boolean/count, no
230
264
  * secrets) so an operator can correlate legs that ran under the same resolved assembly. */
@@ -293,10 +327,13 @@ export interface WiringFacts {
293
327
  armed: boolean;
294
328
  reason: AutoModeArmReason;
295
329
  };
330
+ /** Effective half only — see {@link WiringManifest.mcp}; the static half materializes nothing.
331
+ * Copied entry-wise onto the manifest (the caller's array is never aliased). */
332
+ mcp?: readonly WiringManifestMcpEntry[];
296
333
  }
297
334
  /** Named view of the deps seats the static half reads (a `Pick` of the real {@link RunnerDeps} —
298
335
  * single-source shapes, no parallel hand-copied interface). */
299
- export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy" | "permissionRuleStore" | "permissionRuleSyncWired" | "permissionRuleOrg">;
336
+ export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy" | "permissionRuleStore" | "permissionRuleSyncWired">;
300
337
  /** Named view of the spec seats the static half reads (a `Pick` of the real {@link TaskSpec}). */
301
338
  export type StaticWiringSpec = Pick<TaskSpec, "onAsk" | "onQuestion" | "checkpointStore" | "durableApproval" | "mcp" | "interactiveTools" | "interactionPosture">;
302
339
  /**
@@ -126,9 +126,10 @@ export function deriveWiringManifest(facts) {
126
126
  },
127
127
  ...(facts.half === "effective" && facts.modelGate !== undefined ? { modelGate: { class: facts.modelGate.class, removed: [...facts.modelGate.removed], restore: facts.modelGate.restore } } : {}),
128
128
  ...(facts.half === "effective" && facts.autoMode !== undefined ? { autoMode: readAutoModeFact(facts.autoMode) } : {}),
129
+ ...(facts.half === "effective" && facts.mcp !== undefined ? { mcp: facts.mcp.map((e) => ({ ...e })) } : {}),
129
130
  };
130
131
  if (facts.half === "effective") {
131
- const { leg: _leg, ...assembly } = manifest;
132
+ const { leg: _leg, mcp: _mcp, ...assembly } = manifest;
132
133
  const { provenance: _askSeat, ...askForHash } = assembly.ask;
133
134
  const { provenance: _questionSeat, ...questionForHash } = assembly.question;
134
135
  manifest.configFingerprint = createHash("sha256")
@@ -211,9 +212,9 @@ export function describeStaticWiring(deps, spec = {}) {
211
212
  sessionDurability: resolveDeclaredDurability(deps.sessionStore, "sessionStore"),
212
213
  backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
213
214
  subagentTranscriptTier: resolveSubagentTranscriptTier(deps.backgroundAgentStore !== undefined, deps.sessionStore),
214
- permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
215
+ permissionRuleStoreWired: deps.permissionRuleStore?.partitions.durable === true,
215
216
  permissionRuleSyncWired: deps.permissionRuleSyncWired === true,
216
- permissionRuleOrgGoverned: deps.permissionRuleOrg !== undefined,
217
+ permissionRuleOrgGoverned: deps.permissionRuleStore?.partitions.org === true,
217
218
  hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
218
219
  lockedConfigWired: deps.lockedConfig !== undefined,
219
220
  complianceWired: deps.compliancePostureResolver !== undefined,
@@ -57,8 +57,16 @@ export interface WriteProtectionMatcher {
57
57
  * Compile the write-protection judge for a deployment configuration. Returns `undefined` when the
58
58
  * resolved table is EMPTY (`[]` replacement) — the caller then mounts no tighten at all, keeping an
59
59
  * opted-out deployment's decision path byte-identical to a build without this layer.
60
+ *
61
+ * `opts.dataRoot` names the engine data root for the passthrough in the module header (same seat and
62
+ * same meaning as `createSensitivePathPolicy`'s): omitted ⇒ the knob its consumers read
63
+ * (`$AGENT_DATA_DIR ?? ~/.ai-agent`), read ONCE here at compile so the judge stays pure and
64
+ * synchronous. The passthrough is a property of the JUDGE, not of the default table — a deployment
65
+ * that replaces the table wholesale gets the same law over its own rows.
60
66
  */
61
- export declare function compileWriteProtection(entries?: readonly WriteProtectedEntry[]): WriteProtectionMatcher | undefined;
67
+ export declare function compileWriteProtection(entries?: readonly WriteProtectedEntry[], opts?: {
68
+ dataRoot?: string;
69
+ }): WriteProtectionMatcher | undefined;
62
70
  /**
63
71
  * Build the ENGINE-FILLED gate input (`ToolGateInput.writeProtectionCheck`): covered tools are the
64
72
  * path-confinable write set (Write/Edit/NotebookEdit — the shared spelling), the judged target is
@@ -69,5 +77,8 @@ export declare function compileWriteProtection(entries?: readonly WriteProtected
69
77
  * the tool's own schema validation refuses it before any write); the fail-closed treatment of the
70
78
  * unresolvable case belongs to the containment gates (fs-write-gate-policy documents that split).
71
79
  * Returns `undefined` when the resolved table is empty — nothing to judge, mount nothing.
80
+ * `opts.dataRoot` is the judge's own seat, passed straight through ({@link compileWriteProtection}).
72
81
  */
73
- export declare function createWriteProtectionCheck(entries?: readonly WriteProtectedEntry[]): ((toolName: string, args: unknown) => WriteProtectedHit | null) | undefined;
82
+ export declare function createWriteProtectionCheck(entries?: readonly WriteProtectedEntry[], opts?: {
83
+ dataRoot?: string;
84
+ }): ((toolName: string, args: unknown) => WriteProtectedHit | null) | undefined;
@@ -1,5 +1,6 @@
1
- import { writeTargetPath } from "../tools/fs/safety.js";
1
+ import { isWinFormPath, writeTargetPath, isUncPath } from "../tools/fs/safety.js";
2
2
  import { PATH_CONFINABLE_WRITE_TOOLS } from "./runner/session-rule-policy.js";
3
+ import { dataRootSpellings } from "./sensitive-path-policy.js";
3
4
  const freezeTable = (rows) => Object.freeze(rows.map((r) => Object.freeze(r)));
4
5
  export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
5
6
  { name: ".gitconfig", kind: "basename" },
@@ -51,6 +52,8 @@ export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
51
52
  { name: ".config/git", kind: "segment-run" },
52
53
  { name: ".ssh", kind: "segment" },
53
54
  { name: ".gnupg", kind: "segment" },
55
+ { name: ".sema.json", kind: "basename" },
56
+ { name: ".sema", kind: "segment" },
54
57
  ]);
55
58
  export function foldWriteProtectCase(s) {
56
59
  return s.toLowerCase().replace(/ı/g, "i").replace(/ſ/g, "s");
@@ -110,10 +113,56 @@ export function resolveWriteProtectedTable(entries) {
110
113
  }
111
114
  return freezeTable(out);
112
115
  }
113
- export function compileWriteProtection(entries) {
116
+ function pathViewSegments(path, win32, seps = BOTH_SEPARATOR_FAMILIES, anchorFloor = 0) {
117
+ const segs = [];
118
+ for (const raw of path.split(seps)) {
119
+ if (raw.length === 0)
120
+ continue;
121
+ const dot = !win32 || /^\.{1,2}$/.test(raw) ? raw : raw.replace(/ +$/, "");
122
+ if (dot === ".")
123
+ continue;
124
+ if (dot === "..") {
125
+ const last = segs[segs.length - 1];
126
+ if (segs.length > anchorFloor && last !== "..")
127
+ segs.pop();
128
+ else if (segs.length > anchorFloor || anchorFloor === 0)
129
+ segs.push("..");
130
+ continue;
131
+ }
132
+ const stripped = win32 ? raw.replace(/[. ]+$/, "") : raw;
133
+ segs.push(stripped.length > 0 ? stripped : raw);
134
+ }
135
+ return segs;
136
+ }
137
+ const BOTH_SEPARATOR_FAMILIES = /[\\/]/;
138
+ const POSIX_SEPARATOR_FAMILY = /\//;
139
+ function uncAnchorSegments(path) {
140
+ const third = path[2];
141
+ return isUncPath(path) && third !== undefined && third !== "\\" && third !== "/" ? 2 : 0;
142
+ }
143
+ function relativeUnderDataRoot(path, roots) {
144
+ for (const root of roots) {
145
+ if (root.segments.length === 0)
146
+ continue;
147
+ if (root.win ? !isWinFormPath(path) : isWinFormPath(path) || !path.startsWith("/"))
148
+ continue;
149
+ const seps = root.win ? BOTH_SEPARATOR_FAMILIES : POSIX_SEPARATOR_FAMILY;
150
+ const segs = pathViewSegments(path, false, seps, uncAnchorSegments(path));
151
+ if (segs.length < root.segments.length)
152
+ continue;
153
+ if (root.segments.every((s, i) => segs[i] === s))
154
+ return segs.slice(root.segments.length).join("/");
155
+ }
156
+ return null;
157
+ }
158
+ export function compileWriteProtection(entries, opts) {
114
159
  const rows = resolveWriteProtectedTable(entries);
115
160
  if (rows.length === 0)
116
161
  return undefined;
162
+ const rootViews = dataRootSpellings(opts?.dataRoot).map((r) => {
163
+ const win = isWinFormPath(r);
164
+ return { win, segments: pathViewSegments(r, false, win ? BOTH_SEPARATOR_FAMILIES : POSIX_SEPARATOR_FAMILY, uncAnchorSegments(r)) };
165
+ });
117
166
  const basenames = new Map();
118
167
  const segments = new Map();
119
168
  const runs = [];
@@ -134,30 +183,11 @@ export function compileWriteProtection(entries) {
134
183
  return {
135
184
  rows,
136
185
  matchPath(path) {
137
- const viewOf = (win32) => {
138
- const segs = [];
139
- for (const raw of path.split(/[\\/]/)) {
140
- if (raw.length === 0)
141
- continue;
142
- const dot = !win32 || /^\.{1,2}$/.test(raw) ? raw : raw.replace(/ +$/, "");
143
- if (dot === ".")
144
- continue;
145
- if (dot === "..") {
146
- const last = segs[segs.length - 1];
147
- if (segs.length > 0 && last !== "..")
148
- segs.pop();
149
- else
150
- segs.push("..");
151
- continue;
152
- }
153
- const stripped = win32 ? raw.replace(/[. ]+$/, "") : raw;
154
- segs.push(foldWriteProtectCase(stripped.length > 0 ? stripped : raw));
155
- }
156
- return segs;
157
- };
158
- const judge = (segs) => {
159
- if (segs.length === 0)
186
+ const judged = relativeUnderDataRoot(path, rootViews) ?? path;
187
+ const judge = (raw) => {
188
+ if (raw.length === 0)
160
189
  return null;
190
+ const segs = raw.map(foldWriteProtectCase);
161
191
  const base = basenames.get(segs[segs.length - 1] ?? "");
162
192
  if (base !== undefined)
163
193
  return { name: base, kind: "basename" };
@@ -175,13 +205,12 @@ export function compileWriteProtection(entries) {
175
205
  }
176
206
  return null;
177
207
  };
178
- const posix = viewOf(false);
179
- return judge(posix) ?? judge(viewOf(true));
208
+ return judge(pathViewSegments(judged, false)) ?? judge(pathViewSegments(judged, true));
180
209
  },
181
210
  };
182
211
  }
183
- export function createWriteProtectionCheck(entries) {
184
- const matcher = compileWriteProtection(entries);
212
+ export function createWriteProtectionCheck(entries, opts) {
213
+ const matcher = compileWriteProtection(entries, opts);
185
214
  if (matcher === undefined)
186
215
  return undefined;
187
216
  return (toolName, args) => {