@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * design/182 §7 — the ORGANIZATION rule layer: core's half of the contract.
2
+ * design/182 §7 / design/389 — the ORG partition of the permission-rule store: core's half of the contract.
3
3
  *
4
4
  * Org rules and personal rules are opposite in polarity (deny/ask vs allow), authority (a server-side
5
5
  * administrator publish vs one human click per add) and evolution (whole-snapshot replacement vs
@@ -12,9 +12,10 @@
12
12
  *
13
13
  * ## Availability semantics (the fail-closed shape, ruled through three review rounds)
14
14
  *
15
- * A deployment DECLARES governance (`governed: true`) governance is never inferred from wiring, so a
16
- * forgotten provider is a loud boot contradiction instead of a silently ungoverned deployment. Under
17
- * that declaration the org adjudication surface must ALWAYS be effective:
15
+ * A deployment DECLARES governance by handing `createPermissionRuleStoreProvider` an `org` partition
16
+ * config governance is never inferred from a provider happening to be wired somewhere, and a config
17
+ * naming no snapshot source is a loud boot contradiction instead of a silently ungoverned deployment.
18
+ * Under that declaration the org adjudication surface must ALWAYS be effective:
18
19
  * - the last successfully installed snapshot persists durably (`PersistedOrgRuleState`) and keeps
19
20
  * adjudicating while the provider is unreachable, up to a deployment-configured staleness bound;
20
21
  * - beyond the bound (or before any snapshot was ever installed) the resolution is `unavailable`, and
@@ -34,8 +35,8 @@
34
35
  * accept revision N−1 after a restart. Signature / predecessor chaining belongs to the privilege-
35
36
  * separation ticket; the high-water mark is the floor that needs no key distribution.
36
37
  */
37
- import { type RuleRejectCode, type RuleScope } from "./permission-rule-model.js";
38
- import { type PermissionRuleStore, type PermissionRuleStoreProvider, type RuleOwner } from "./permission-rule-store.js";
38
+ import { type PersistedAllowRule, type RuleRejectCode } from "./permission-rule-model.js";
39
+ import type { PermissionRuleStore } from "./permission-rule-provider.js";
39
40
  /** One org rule. There is structurally no allow bucket (design/179 §9: the org layer only tightens). */
40
41
  export interface OrgPermissionRule {
41
42
  rule: string;
@@ -66,8 +67,9 @@ export interface PersistedOrgRuleState {
66
67
  snapshot: OrgRuleSnapshot;
67
68
  installedAtMs: number;
68
69
  }
69
- /** The durable home of {@link PersistedOrgRuleState}. The file rule-store backend implements it over an
70
- * org block published with the same O_EXCL/fsync/rename discipline as the rule file itself.
70
+ /** The durable home of {@link PersistedOrgRuleState}. The file backend implements it over a
71
+ * deployment-level `org.json` published with the same O_EXCL/fsync/rename discipline as the rule files
72
+ * (`FileDurableRulePartitionProvider.orgStatePersistence()`); a SQL twin keeps one blob row.
71
73
  *
72
74
  * CONTRACT: `installOrgState` must REFUSE (throw) an install that would LOWER the persisted
73
75
  * `revisionHighWater` — the overlay serializes its own resolves, but two overlay instances over one
@@ -77,8 +79,6 @@ export interface OrgRuleStatePersistence {
77
79
  readOrgState(): Promise<PersistedOrgRuleState | undefined>;
78
80
  installOrgState(state: PersistedOrgRuleState): Promise<void>;
79
81
  }
80
- /** Duck-typed discovery of the persistence face on a store backend, `writerOf` style. */
81
- export declare function orgRuleStatePersistenceOf(store: PermissionRuleStore): OrgRuleStatePersistence | undefined;
82
82
  /** The `decisionReason` the consuming gate stamps on the synthetic asks it mints while org adjudication
83
83
  * is unavailable. Exported so the gate integration and its tests share one spelling — and TYPED as the
84
84
  * member of {@link import("./tool-policy.js").DecisionReason} it must equal, so the constant and the
@@ -129,37 +129,48 @@ export declare const ORG_RULE_DECISION_REASON: Extract<import("./tool-policy.js"
129
129
  * clock skew passes; a far-future timestamp (which would satisfy the staleness bound INDEFINITELY,
130
130
  * turning fail-closed into evergreen freshness) does not. */
131
131
  export declare const ORG_FETCHED_AT_SKEW_ALLOWANCE_MS: number;
132
- export type OrgOverlayStatus = "fresh" | "last-known-good" | "unavailable";
133
- /** What one resolution reports. `rules` is empty status `unavailable` or the snapshot is empty
134
- * the two are distinguished by `status`, never by the emptiness of the list. */
135
- export interface OrgOverlayResolution {
136
- status: OrgOverlayStatus;
132
+ /** The org partition's resolution word. `ungoverned` is the store's answer on a deployment that
133
+ * configured no org partition a fourth member so "no org layer here" is never spelled as an
134
+ * empty `fresh` snapshot. */
135
+ export type OrgRuleStatus = "fresh" | "last-known-good" | "unavailable" | "ungoverned";
136
+ /** What one resolution reports. `rules` is empty ⟺ status `unavailable`/`ungoverned` or the snapshot
137
+ * is empty — the cases are distinguished by `status`, never by the emptiness of the list. */
138
+ export interface OrgRuleResolution {
139
+ source: "org";
140
+ status: OrgRuleStatus;
137
141
  rules: OrgPermissionRule[];
138
142
  revision?: number;
139
- /** Honest-disclosure lines: provider failures, refused snapshots, rollback refusals. */
143
+ /** Honest-disclosure lines: source failures, refused snapshots, rollback refusals. */
140
144
  disclosures: string[];
141
145
  }
142
- export interface OrgRuleOverlay {
143
- resolve(): Promise<OrgOverlayResolution>;
146
+ /** The org partition engine the unified store composes (design/389): fetch → validate → install-if-newer
147
+ * → judge freshness, one serialized chain per deployment. */
148
+ export interface OrgRulePartition {
149
+ resolve(): Promise<OrgRuleResolution>;
144
150
  }
145
- /**
146
- * Construct the org overlay for a GOVERNED deployment. This constructor IS the boot gate:
147
- * - `governed` must be literally `true` — the overlay does not exist for ungoverned deployments
148
- * (their decision path stays byte-identical to a build without this module);
149
- * - a governed declaration WITHOUT a provider is a configuration contradiction and refuses to boot
150
- * a silently ungoverned "governed" deployment is precisely the failure this bit exists to prevent;
151
- * - `stalenessBoundMs` follows the knob discipline: a non-number, NaN, non-finite or non-positive
152
- * bound is refused loudly, never coerced.
153
- */
154
- export declare function createOrgRuleOverlay(cfg: {
155
- governed: true;
156
- provider?: OrgRuleSnapshotProvider;
151
+ /** The partition config a deployment hands to `createPermissionRuleStoreProvider({ org })`. Its
152
+ * PRESENCE is the governance declaration. */
153
+ export interface OrgRulePartitionConfig {
154
+ /** Where the deployment fetches the current snapshot. Required — a governed declaration without a
155
+ * source is a configuration contradiction and refuses to boot. */
156
+ source: OrgRuleSnapshotProvider;
157
157
  /** Durable LKG + high-water home. Omitted ⇒ process-local only, DISCLOSED on every resolution: a
158
158
  * restart then forgets both the last-known-good snapshot and the anti-rollback mark. */
159
159
  persistence?: OrgRuleStatePersistence;
160
160
  stalenessBoundMs: number;
161
161
  now?: () => number;
162
- }): OrgRuleOverlay;
162
+ }
163
+ /** The ungoverned resolution — what a store with no org partition answers. */
164
+ export declare const UNGOVERNED_ORG_RESOLUTION: Readonly<OrgRuleResolution>;
165
+ /**
166
+ * Construct the org partition engine for a GOVERNED deployment. This constructor IS the boot gate:
167
+ * - a declaration WITHOUT a snapshot source is a configuration contradiction and refuses to boot —
168
+ * a silently ungoverned "governed" deployment is precisely the failure this bit exists to prevent
169
+ * (the type requires `source`; the check is executed for a JavaScript caller);
170
+ * - `stalenessBoundMs` follows the knob discipline: a non-number, NaN, non-finite or non-positive
171
+ * bound is refused loudly, never coerced.
172
+ */
173
+ export declare function createOrgRulePartition(cfg: OrgRulePartitionConfig): OrgRulePartition;
163
174
  /**
164
175
  * Which entries of an org snapshot the CURRENT rule validator refuses — the published deny/ask rules
165
176
  * that can match nothing on this build. Empty ⟺ every rule is enforceable.
@@ -180,7 +191,7 @@ export declare function unenforceableOrgRules(rules: readonly OrgPermissionRule[
180
191
  * interpreter-headed prefix deny like `Bash(node:*)` is a legitimately wide tightening and matches).
181
192
  *
182
193
  * The skip below is a defensive floor, and what stands behind it is worth stating precisely: rules that
183
- * arrive from {@link OrgRuleOverlay.resolve} have passed {@link validateOrgSnapshot} on BOTH the install
194
+ * arrive from the org partition's `resolve` ({@link createOrgRulePartition}) have passed {@link validateOrgSnapshot} on BOTH the install
184
195
  * and the serving path (backlog #177), so an unenforceable entry cannot reach here through the overlay
185
196
  * — the whole snapshot is refused, or the resolution reports `unavailable`, and either way it is
186
197
  * disclosed. A caller that hands this function a snapshot it obtained ELSEWHERE gets no such guarantee:
@@ -194,31 +205,36 @@ export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], c
194
205
  behavior: "deny" | "ask";
195
206
  rule: string;
196
207
  } | undefined;
197
- /** The effective status of one personal rule under the current org overlay — computed, never stored. */
198
- export interface EffectivePermissionRule {
199
- rule: string;
200
- scope: RuleScope;
201
- status: "live" | "shadowed-by-org" | "removed";
202
- }
203
208
  /**
204
- * design/182 §7.3 — the introspection face: every personal rule with its effective status under the org
205
- * overlay. An org deny does not delete a personal rule the personal rule is the user's approval
206
- * record, and the org's power is to OVERRIDE it, not to self-declare its removal so a shadowed rule reports
207
- * `shadowed-by-org` and comes back by itself when the org deny is withdrawn. A rule whose every add is
208
- * tombstoned reports `removed` (its tombstone identity is still visible in the store).
209
+ * design/182 §7.3 / design/389 — the SHADOW predicate the unified store's read face applies to every
210
+ * personal allow row: does an org DENY reach this rule's own command pattern? An org deny does not
211
+ * delete a personal rule the personal rule is the user's approval record, and the org's power is to
212
+ * OVERRIDE it, not to self-declare its removal so a shadowed rule reports `shadowed-by-org` and comes
213
+ * back by itself when the org deny is withdrawn.
214
+ *
215
+ * The SAME reach the gate uses (`orgRuleReaches`), so the view and the decision cannot disagree about
216
+ * whether a deny touches this rule at all. A personal rule may name a connector chain, and a deny on one
217
+ * of its segments shadows it exactly as a deny on the whole does.
218
+ *
219
+ * WHAT `shadowed-by-org` DOES AND DOES NOT CLAIM: the predicate asks whether the deny reaches the
220
+ * personal rule's own command PATTERN — one exemplar — not whether it covers every call that pattern
221
+ * admits. The two differ for a PREFIX personal rule under a narrower deny: `Bash(git status:*)` with an
222
+ * org deny of exactly `Bash(git status)` reports shadowed, yet `git status --short` is still admitted at
223
+ * the gate. The inverse partial overlap (personal `Bash(git:*)`, org deny `Bash(git status:*)`) reports
224
+ * live while part of its reach is denied. A truthful answer needs pattern CONTAINMENT and a third status
225
+ * for partial overlap, which is an API change with its own design; what the two-value view is honest
226
+ * about today is "this rule is touched by a deny" / "no deny mentions it". The gate is the authority
227
+ * either way. org ASK rules do not shadow: the personal lane never consumes a `requiresRealApproval`
228
+ * ask, so the two never actually meet on one call.
209
229
  *
210
- * The shadow predicate: an org DENY rule REACHES the personal rule's command pattern (for a prefix
211
- * personal rule, the org deny admits its prefix body a wider org deny shadows every narrower allow
212
- * under it; for a personal rule naming a connector chain, a deny on any one segment shadows it, which
213
- * is the same reach the gate decides with — see `orgRuleReaches`). org ASK rules do not shadow: the
214
- * personal lane never consumes a `requiresRealApproval` ask, so the two never actually meet on one call.
230
+ * design/382 §2.6 a Read directory rule is never org-shadowed in this version: the org layer has no
231
+ * Read grammar (a snapshot carrying one refuses whole at the validator), so there is no deny that could
232
+ * reach it, and its `command` is a PATH that must not be fed to the shell lane's segment reader as
233
+ * though it were a command line.
215
234
  */
216
- export declare function effectivePermissionRules(opts: {
217
- provider: PermissionRuleStoreProvider;
218
- principal?: string;
219
- owner?: RuleOwner;
220
- orgSnapshot?: OrgRuleSnapshot;
221
- }): Promise<EffectivePermissionRule[]>;
235
+ export declare function orgRuleShadows(orgRules: readonly OrgPermissionRule[], rule: Pick<PersistedAllowRule, "tool" | "command">): boolean;
236
+ /** The shadow predicate with the org denies parsed ONCE — what a read over many rows uses. */
237
+ export declare function compileOrgShadowPredicate(orgRules: readonly OrgPermissionRule[]): (rule: Pick<PersistedAllowRule, "tool" | "command">) => boolean;
222
238
  /** Does this store carry a write face? Re-exported convenience for org-ticket integrations that need to
223
239
  * distinguish a read-only projection from a writable personal bucket without importing the writer seam. */
224
240
  export declare function isWritablePermissionRuleStore(store: PermissionRuleStore): boolean;
@@ -1,11 +1,5 @@
1
1
  import { escapeForDisclosure, parseAllowRuleText, ruleAdmitsProgramRun, ruleLaneSegmentsOf } from "./permission-rule-model.js";
2
- import { sameScope, writerOf } from "./permission-rule-store.js";
3
- export function orgRuleStatePersistenceOf(store) {
4
- const s = store;
5
- return typeof s.readOrgState === "function" && typeof s.installOrgState === "function"
6
- ? { readOrgState: s.readOrgState.bind(store), installOrgState: s.installOrgState.bind(store) }
7
- : undefined;
8
- }
2
+ import { writerOf } from "./permission-rule-store.js";
9
3
  export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
10
4
  export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
11
5
  export function settleOrgVerdictWithin(p, fallback, opts) {
@@ -38,17 +32,18 @@ export function settleOrgVerdictWithin(p, fallback, opts) {
38
32
  }
39
33
  export const ORG_RULE_DECISION_REASON = "org_rule";
40
34
  export const ORG_FETCHED_AT_SKEW_ALLOWANCE_MS = 5 * 60_000;
41
- export function createOrgRuleOverlay(cfg) {
42
- if (cfg.governed !== true) {
43
- throw new Error("createOrgRuleOverlay is only for org-governed deployments pass `governed: true` or do not construct one");
35
+ export const UNGOVERNED_ORG_RESOLUTION = Object.freeze({ source: "org", status: "ungoverned", rules: [], disclosures: [] });
36
+ export function createOrgRulePartition(cfg) {
37
+ if (cfg === null || typeof cfg !== "object") {
38
+ throw new Error("the org partition config must be an object naming a snapshot source and a staleness bound");
44
39
  }
45
- if (cfg.provider === undefined) {
46
- throw new Error("org-governed is declared but no OrgRuleSnapshotProvider is wired — a governed deployment without an org source is a configuration contradiction; refusing to boot rather than running silently ungoverned");
40
+ if (cfg.source === undefined || typeof cfg.source.current !== "function") {
41
+ throw new Error("org-governed is declared but no OrgRuleSnapshotProvider is wired as `org.source` — a governed deployment without an org source is a configuration contradiction; refusing to boot rather than running silently ungoverned");
47
42
  }
48
43
  if (typeof cfg.stalenessBoundMs !== "number" || !Number.isFinite(cfg.stalenessBoundMs) || cfg.stalenessBoundMs <= 0) {
49
44
  throw new Error(`org stalenessBoundMs must be a finite positive number of milliseconds (got ${escapeForDisclosure(cfg.stalenessBoundMs)})`);
50
45
  }
51
- const provider = cfg.provider;
46
+ const provider = cfg.source;
52
47
  const now = cfg.now ?? Date.now;
53
48
  let memoryState;
54
49
  const readState = async () => {
@@ -74,14 +69,19 @@ export function createOrgRuleOverlay(cfg) {
74
69
  }
75
70
  let fetched;
76
71
  let fetchFailed = false;
72
+ const settled = await settleOrgVerdictWithin(provider.current().then((v) => ({ v }), (e) => ({ e })), { timedOut: true }, { timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
77
73
  try {
78
- fetched = structuredClone(await provider.current());
74
+ if ("timedOut" in settled)
75
+ throw new Error(`the org snapshot source did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms`);
76
+ if ("e" in settled)
77
+ throw settled.e;
78
+ fetched = structuredClone(settled.v);
79
79
  }
80
80
  catch (err) {
81
81
  fetchFailed = true;
82
- disclosures.push(`org snapshot provider failed: ${escapeForDisclosure(err instanceof Error ? err.message : err)}`);
82
+ disclosures.push(`org snapshot source failed: ${escapeForDisclosure(err instanceof Error ? err.message : err)}`);
83
83
  }
84
- return await serialize(async () => {
84
+ return await settleOrgVerdictWithin(serialize(async () => {
85
85
  const nowMs = now();
86
86
  const guardedRead = async (context) => {
87
87
  try {
@@ -109,6 +109,9 @@ export function createOrgRuleOverlay(cfg) {
109
109
  disclosures.push(`org snapshot revision ${fetched.revision} is below the installed high-water mark ${escapeForDisclosure(persistedFloor)} — refusing the rollback; the current deny set is unchanged`);
110
110
  return persisted;
111
111
  }
112
+ if (persisted !== undefined && fetched.revision === persisted.snapshot.revision && sameOrgPolicyContent(fetched, persisted.snapshot) && fetched.fetchedAtMs <= persisted.snapshot.fetchedAtMs) {
113
+ return persisted;
114
+ }
112
115
  if (persisted !== undefined && fetched.revision === persisted.snapshot.revision && !sameOrgPolicyContent(fetched, persisted.snapshot)) {
113
116
  disclosures.push(`org snapshot revision ${fetched.revision} equals the installed revision but carries DIFFERENT policy content — refusing the swap; the current deny set is unchanged`);
114
117
  return persisted;
@@ -137,19 +140,20 @@ export function createOrgRuleOverlay(cfg) {
137
140
  if (installed !== undefined) {
138
141
  if (installed.snapshot.fetchedAtMs > decisionMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS) {
139
142
  disclosures.push(`the installed org snapshot claims a FUTURE observation time (${escapeForDisclosure(installed.snapshot.fetchedAtMs)} vs now ${decisionMs}) — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`);
140
- return { status: "unavailable", rules: [], disclosures };
143
+ return { source: "org", status: "unavailable", rules: [], disclosures };
141
144
  }
142
145
  const servingRefusal = validateOrgSnapshot(installed.snapshot, decisionMs);
143
146
  if (servingRefusal !== undefined) {
144
147
  disclosures.push(`the installed org snapshot (revision ${escapeForDisclosure(installed.snapshot.revision)}) does not meet this version's validator: ${servingRefusal} — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`);
145
- return { status: "unavailable", rules: [], disclosures };
148
+ return { source: "org", status: "unavailable", rules: [], disclosures };
146
149
  }
147
150
  if (withinBound(installed.snapshot)) {
148
151
  const freshlyInstalled = fetched !== undefined && installed.snapshot === fetched;
149
152
  if (!freshlyInstalled && !fetchFailed && fetched === undefined) {
150
- disclosures.push("org snapshot provider returned no snapshot — continuing on the last-known-good");
153
+ disclosures.push("org snapshot source returned no snapshot — continuing on the last-known-good");
151
154
  }
152
155
  return {
156
+ source: "org",
153
157
  status: freshlyInstalled ? "fresh" : "last-known-good",
154
158
  rules: structuredClone(installed.snapshot.rules),
155
159
  revision: installed.snapshot.revision,
@@ -157,11 +161,16 @@ export function createOrgRuleOverlay(cfg) {
157
161
  };
158
162
  }
159
163
  disclosures.push(`the newest org snapshot was observed ${decisionMs - installed.snapshot.fetchedAtMs}ms ago (bound ${cfg.stalenessBoundMs}ms) — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`);
160
- return { status: "unavailable", rules: [], disclosures };
164
+ return { source: "org", status: "unavailable", rules: [], disclosures };
161
165
  }
162
166
  disclosures.push("no org snapshot has ever been installed — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask");
163
- return { status: "unavailable", rules: [], disclosures };
164
- });
167
+ return { source: "org", status: "unavailable", rules: [], disclosures };
168
+ }), {
169
+ source: "org",
170
+ status: "unavailable",
171
+ rules: [],
172
+ disclosures: [...disclosures, `org state persistence did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`],
173
+ }, { timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
165
174
  };
166
175
  return { resolve };
167
176
  }
@@ -257,47 +266,24 @@ function orgRuleReaches(rule, command, segments) {
257
266
  return false;
258
267
  return segments.some((segment) => ruleAdmitsProgramRun(rule, segment));
259
268
  }
260
- export async function effectivePermissionRules(opts) {
261
- const store = resolveIntrospectionStore(opts);
262
- const listed = await store.list();
263
- const orgDenies = (opts.orgSnapshot?.rules ?? []).filter((r) => r.behavior === "deny");
264
- const out = [];
265
- for (const r of listed.rules) {
266
- const shadowed = r.tool === "Bash" &&
267
- (() => {
268
- const segments = ruleLaneSegmentsOf(r.command);
269
- return orgDenies.some((d) => {
270
- const parsed = parseAllowRuleText(d.rule, { direction: "tighten" });
271
- return !("reject" in parsed) && parsed.rule.tool === r.tool && orgRuleReaches(parsed.rule, r.command, segments);
272
- });
273
- })();
274
- out.push({ rule: r.rule, scope: r.scope, status: shadowed ? "shadowed-by-org" : "live" });
275
- }
276
- for (const t of listed.tombstones) {
277
- if (listed.rules.some((r) => r.rule === t.rule && sameScope(r.scope, t.scope)))
278
- continue;
279
- if (out.some((e) => e.rule === t.rule && sameScope(e.scope, t.scope)))
280
- continue;
281
- out.push({ rule: t.rule, scope: t.scope, status: "removed" });
282
- }
283
- return out;
269
+ export function orgRuleShadows(orgRules, rule) {
270
+ return compileOrgShadowPredicate(orgRules)(rule);
284
271
  }
285
- function resolveIntrospectionStore(opts) {
286
- if (opts.owner !== undefined && opts.owner.kind === "local-owner") {
287
- if (opts.principal !== undefined) {
288
- throw new Error("pass either a principal or a local-owner, not both — a bucket has one owner");
289
- }
290
- const store = opts.provider.forLocalOwner?.();
291
- if (store === undefined) {
292
- throw new Error("this provider has no local-owner bucket (forLocalOwner is not implemented)");
293
- }
294
- return store;
295
- }
296
- if (opts.owner !== undefined && opts.owner.kind === "principal" && opts.principal !== undefined && opts.owner.principal !== opts.principal) {
297
- throw new Error(`contradictory identity: principal "${escapeForDisclosure(opts.principal)}" and owner principal "${escapeForDisclosure(opts.owner.principal)}" disagree`);
272
+ export function compileOrgShadowPredicate(orgRules) {
273
+ const denies = [];
274
+ for (const d of orgRules) {
275
+ if (d.behavior !== "deny")
276
+ continue;
277
+ const parsed = parseAllowRuleText(d.rule, { direction: "tighten" });
278
+ if (!("reject" in parsed))
279
+ denies.push(parsed.rule);
298
280
  }
299
- const principal = opts.owner?.kind === "principal" ? opts.owner.principal : opts.principal;
300
- return opts.provider.forPrincipal(principal);
281
+ return (rule) => {
282
+ if (rule.tool !== "Bash" || denies.length === 0)
283
+ return false;
284
+ const segments = ruleLaneSegmentsOf(rule.command);
285
+ return denies.some((d) => d.tool === rule.tool && orgRuleReaches(d, rule.command, segments));
286
+ };
301
287
  }
302
288
  export function isWritablePermissionRuleStore(store) {
303
289
  return writerOf(store) !== undefined;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * design/389 — ONE permission-rule store, partitioned by SOURCE.
3
+ *
4
+ * A rule comes from one of four sources — `org | user | project | session` — and the four differ in
5
+ * polarity, authority and evolution (an org snapshot is replaced whole by a server administrator; a
6
+ * user/project rule is one human click folded into an OR-Set CRDT; a session grant lives exactly as long
7
+ * as its session). Those differences are WRITE LAWS, and each partition keeps its own. What is one is the
8
+ * READ: "which rules stand for this owner, in this session, and what does each one's standing look like"
9
+ * is a single question, so it has a single answer — {@link PermissionRuleStore.effective}, a query the
10
+ * store answers across every partition with each row's `source` and `status`, the org shadow predicate
11
+ * applied inside the store rather than at each caller.
12
+ *
13
+ * ## Composition, not inheritance
14
+ *
15
+ * {@link createPermissionRuleStoreProvider} is the ONE way to build the store. A deployment hands it
16
+ * partition backends — the durable CRDT partition (`DurableRulePartitionProvider`: file, in-memory, an
17
+ * out-of-repo SQL twin), an org partition config (its snapshot source and staleness bound), a session
18
+ * partition — and the engine composes them. The org and session partition LAWS are engine code the
19
+ * backend cannot weaken; the durable partition's law is the exported pure fold every backend runs.
20
+ * A partition a deployment does not configure does not exist: its source answers nothing, and the
21
+ * provider's `partitions` face says so, so a manifest reads a declaration rather than inferring one.
22
+ *
23
+ * ## Source is a projection of the row's address
24
+ *
25
+ * `user` ⟺ scope `global`, `project` ⟺ scope `project`, `session` ⟺ scope `session`. A row never carries
26
+ * a second source byte that could drift from its scope — {@link ruleSourceOf} is the whole mapping.
27
+ *
28
+ * ## One write path, routed by scope
29
+ *
30
+ * `writerOf(store)` hands back a writer whose `apply` routes a `redemption-add` by the delta's scope: a
31
+ * session scope lands in the session partition (idempotent by dot, refused `session_ended` past the
32
+ * session's epoch seal), the two durable scopes take the CRDT partition's compare-and-set. The consent
33
+ * protocol therefore has ONE write path with ONE result vocabulary ({@link RuleWriteOutcome}); the
34
+ * durable backend keeps refusing a session row loudly should one ever reach it — the route is the
35
+ * store's, the fence stays the partition's.
36
+ */
37
+ import type { PersistedAllowRule, RuleScope } from "./permission-rule-model.js";
38
+ import { type DurableRulePartitionProvider, type QuarantinedRuleAdd, type RuleOwner } from "./permission-rule-store.js";
39
+ import { type OrgRulePartitionConfig, type OrgRuleResolution } from "./permission-rule-org.js";
40
+ import type { SessionRulePartition } from "./permission-rule-session.js";
41
+ import type { StoreDurability, StoreFidelity } from "./checkpoint-store.js";
42
+ /** The four sources a rule can come from — CC's settings-layer vocabulary folded onto sema's four. */
43
+ export type RuleSource = "org" | "user" | "project" | "session";
44
+ /** The source of a row is a projection of its scope — `global` is the `user` source. */
45
+ export declare function ruleSourceOf(scope: RuleScope): Exclude<RuleSource, "org">;
46
+ /** One live personal rule in the effective view: the row itself, its source, and its standing under the
47
+ * org partition (`shadowed-by-org` = an org deny reaches its command pattern; see `orgRuleShadows`). */
48
+ export interface EffectivePermissionRule extends PersistedAllowRule {
49
+ source: Exclude<RuleSource, "org">;
50
+ status: "live" | "shadowed-by-org";
51
+ }
52
+ /** A durable rule identity every add of which is tombstoned — the rule no longer stands, and its
53
+ * tombstone identity is still visible in the partition. Session rows never appear here (they die
54
+ * with their session and leave no tombstone). */
55
+ export interface RemovedPermissionRule {
56
+ rule: string;
57
+ scope: RuleScope;
58
+ source: "user" | "project";
59
+ status: "removed";
60
+ }
61
+ /** THE answer of the store's one read query. */
62
+ export interface EffectivePermissionRules {
63
+ /** Live personal rows, in reporting order: this session's rows first (the narrowest standing yes), then
64
+ * the durable partition in its own table order — byte-equivalent to the pre-389 splice. */
65
+ rules: EffectivePermissionRule[];
66
+ /** Tombstoned durable identities (introspection). */
67
+ removed: RemovedPermissionRule[];
68
+ /** The org partition's resolution. `ungoverned` on a deployment with no org partition. */
69
+ org: OrgRuleResolution;
70
+ /** The durable partition's revision — the optimistic-concurrency anchor for its writes. */
71
+ rev: number;
72
+ /** Present iff the DURABLE partition could not be read: `rules`/`removed` are then empty and `rev` is 0,
73
+ * while the org partition's resolution beside it still stands (a flaky personal store must not become
74
+ * an org governance outage). A consumer that needs the durable rows treats this as a read failure —
75
+ * {@link effectiveOrThrow} is that reading; the gate's personal lane reads it as "unreadable"
76
+ * (zero rules + a disclosure, fail toward asking). */
77
+ unreadable?: string;
78
+ /** Disclosure lines from the personal partitions (a session partition that could not be read, rows it
79
+ * served that are not canonical session rows of this session). The org partition's lines ride
80
+ * `org.disclosures`. */
81
+ disclosures: string[];
82
+ }
83
+ /**
84
+ * The host-visible store, already anchored to one owner. One read verb, which never throws: each
85
+ * partition reports its own failure in the view — the DURABLE partition's read failure as `unreadable`
86
+ * (zero rows; the caller maps it to "unreadable": a loud disclosure, fail toward asking, exactly as the
87
+ * old `list()` throw did), the SESSION partition's as zero session rows plus a `disclosures` line, the
88
+ * ORG partition's as its `status`. The three are independent on purpose: the org verdict must stand
89
+ * while a personal store is down, and the personal rows must stand while an org source is down.
90
+ */
91
+ export interface PermissionRuleStore {
92
+ effective(query?: {
93
+ sessionId?: string;
94
+ }): Promise<EffectivePermissionRules>;
95
+ /** design/182 §5.2 — the durable partition's quarantine area, for introspection. */
96
+ quarantined?(): Promise<QuarantinedRuleAdd[]>;
97
+ /** The durable partition's declared durability; an undeclared backend reads fail-closed as `"process-local"`. */
98
+ readonly durability?: StoreDurability;
99
+ readonly fidelity?: StoreFidelity;
100
+ }
101
+ /**
102
+ * The unified provider — the ONE `RunnerDeps.permissionRuleStore` seam. `partitions` is the declaration
103
+ * face: the wiring manifest's cells and the ask evidence's `not_wired` words read it verbatim instead of
104
+ * inferring from three separate seams.
105
+ */
106
+ export interface PermissionRuleStoreProvider {
107
+ /** The store for one verified principal. `undefined` ⇒ zero personal rules — but the org partition
108
+ * still adjudicates: governance applies to everyone, an anonymous task included. */
109
+ forPrincipal(principal: string | undefined): PermissionRuleStore;
110
+ /** design/182 §4.5 — the identity-less local bucket. Present iff the durable backend has one. */
111
+ forLocalOwner?(): PermissionRuleStore;
112
+ readonly partitions: Readonly<{
113
+ durable: boolean;
114
+ org: boolean;
115
+ session: boolean;
116
+ }>;
117
+ }
118
+ /** What {@link createPermissionRuleStoreProvider} composes. */
119
+ export interface PermissionRuleStoreConfig {
120
+ /** The durable CRDT partition (user + project sources). Absent ⇒ no personal rules exist on this
121
+ * deployment (the rule lane does not arm), and no session grant can land either (the redemption
122
+ * dot is minted by the durable replica). */
123
+ durable?: DurableRulePartitionProvider;
124
+ /** The org partition. Its PRESENCE is the governance declaration (design/182 §7.4 D3): under it the
125
+ * org adjudication surface must always be effective, and an unreadable snapshot fails the whole
126
+ * decision boundary closed. */
127
+ org?: OrgRulePartitionConfig;
128
+ /** The session partition. Absent ⇒ the session source does not exist: a session-scope redemption is
129
+ * refused loudly (nowhere to land), and `effective()` carries no session rows. */
130
+ session?: SessionRulePartition;
131
+ }
132
+ /**
133
+ * Compose the unified store over its partition backends. This is the boot gate of the org declaration
134
+ * (`createOrgRulePartition` refuses a config naming no source) and the ONLY constructor of the store.
135
+ */
136
+ export declare function createPermissionRuleStoreProvider(cfg: PermissionRuleStoreConfig): PermissionRuleStoreProvider;
137
+ /**
138
+ * The one read query for a consumer that NEEDS the durable rows (a write's optimistic-concurrency anchor,
139
+ * a coverage judgment, a removal): an `unreadable` durable partition is a thrown read failure here, so
140
+ * the caller's existing "could not read the permission-rule store" arm fires — the gate lane is the one
141
+ * consumer that reads the word itself.
142
+ */
143
+ export declare function effectiveOrThrow(store: PermissionRuleStore, query?: {
144
+ sessionId?: string;
145
+ }): Promise<EffectivePermissionRules>;
146
+ /**
147
+ * The one read query, addressed by owner (design/182 §7.3 introspection, design/389 form): resolve the
148
+ * owner's store and ask it. Nothing is assembled here — a consumer that needs the effective view with
149
+ * `source`/`status` per row and the org resolution beside it gets the store's own answer.
150
+ */
151
+ export declare function effectivePermissionRules(opts: {
152
+ provider: PermissionRuleStoreProvider;
153
+ principal?: string;
154
+ owner?: RuleOwner;
155
+ sessionId?: string;
156
+ }): Promise<EffectivePermissionRules>;
157
+ /** Owner routing shared by the introspection entry: either a principal or the local owner, never both. */
158
+ export declare function resolveOwnerStore(opts: {
159
+ provider: PermissionRuleStoreProvider;
160
+ principal?: string;
161
+ owner?: RuleOwner;
162
+ }): PermissionRuleStore;