@sema-agent/core 5.21.1 → 5.23.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/agents/send-message-tool.js +6 -3
  3. package/dist/agents/subagent.d.ts +6 -0
  4. package/dist/agents/subagent.js +48 -6
  5. package/dist/brain/errors.d.ts +20 -0
  6. package/dist/brain/errors.js +40 -0
  7. package/dist/brain/retry.d.ts +16 -2
  8. package/dist/brain/retry.js +3 -2
  9. package/dist/brain/status-sink.d.ts +9 -2
  10. package/dist/brain/stream-engine.d.ts +22 -0
  11. package/dist/brain/stream-engine.js +41 -10
  12. package/dist/core/ask-class.d.ts +48 -0
  13. package/dist/core/ask-class.js +33 -0
  14. package/dist/core/checkpoint-store.d.ts +103 -10
  15. package/dist/core/checkpoint-store.js +3 -1
  16. package/dist/core/governance-codes.d.ts +38 -0
  17. package/dist/core/governance-codes.js +14 -0
  18. package/dist/core/hooks.d.ts +101 -1
  19. package/dist/core/hooks.js +116 -14
  20. package/dist/core/locked-config.d.ts +7 -1
  21. package/dist/core/locked-config.js +2 -1
  22. package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
  23. package/dist/core/memory-engine/delegation-provenance.js +26 -0
  24. package/dist/core/memory-engine/engine.d.ts +94 -1
  25. package/dist/core/memory-engine/engine.js +329 -12
  26. package/dist/core/memory-engine/header-hints.d.ts +30 -0
  27. package/dist/core/memory-engine/header-hints.js +41 -0
  28. package/dist/core/memory-engine/index.d.ts +3 -2
  29. package/dist/core/memory-engine/index.js +3 -2
  30. package/dist/core/memory-engine/layout.d.ts +232 -0
  31. package/dist/core/memory-engine/layout.js +469 -1
  32. package/dist/core/memory-engine/tools.d.ts +30 -0
  33. package/dist/core/memory-engine/tools.js +108 -17
  34. package/dist/core/permission-rule-consent.d.ts +25 -9
  35. package/dist/core/permission-rule-consent.js +93 -21
  36. package/dist/core/permission-rule-model.d.ts +9 -1
  37. package/dist/core/permission-rule-model.js +2 -2
  38. package/dist/core/permission-rule-org.d.ts +195 -0
  39. package/dist/core/permission-rule-org.js +234 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +314 -3
  42. package/dist/core/permission-rule-sync.d.ts +139 -0
  43. package/dist/core/permission-rule-sync.js +343 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +64 -3
  46. package/dist/core/runner/prepare-task.js +357 -32
  47. package/dist/core/runner/runtask.js +166 -6
  48. package/dist/core/shared-memory/contract.js +19 -4
  49. package/dist/core/shared-memory/normalize.d.ts +3 -1
  50. package/dist/core/shared-memory/tools.js +73 -17
  51. package/dist/core/shared-memory/types.d.ts +27 -1
  52. package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
  53. package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
  54. package/dist/core/task-notification.d.ts +5 -2
  55. package/dist/core/task-registry-agent.d.ts +1 -1
  56. package/dist/core/task-registry-agent.js +6 -2
  57. package/dist/core/task-registry-shared.d.ts +9 -2
  58. package/dist/core/task-registry.d.ts +9 -3
  59. package/dist/core/task-registry.js +2 -0
  60. package/dist/core/tool-policy.d.ts +155 -4
  61. package/dist/core/tool-policy.js +148 -10
  62. package/dist/core/tool-result-store.d.ts +9 -1
  63. package/dist/core/tool-result-store.js +2 -1
  64. package/dist/core/trace.d.ts +79 -1
  65. package/dist/core/types.d.ts +94 -3
  66. package/dist/core/wiring-manifest.d.ts +16 -1
  67. package/dist/core/wiring-manifest.js +7 -1
  68. package/dist/index.d.ts +15 -8
  69. package/dist/index.js +12 -5
  70. package/dist/orchestration/goal.d.ts +10 -0
  71. package/dist/orchestration/goal.js +6 -5
  72. package/dist/stores/file/adoption/adopt.d.ts +146 -0
  73. package/dist/stores/file/adoption/adopt.js +616 -0
  74. package/dist/stores/file/adoption/marker.d.ts +194 -0
  75. package/dist/stores/file/adoption/marker.js +198 -0
  76. package/dist/stores/file/background-agent-store.js +2 -0
  77. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  78. package/dist/stores/file/checkpoint-store.js +3 -0
  79. package/dist/stores/file/file-snapshot-store.js +2 -0
  80. package/dist/stores/file/index.d.ts +2 -0
  81. package/dist/stores/file/index.js +4 -0
  82. package/dist/stores/file/mailbox-store.js +2 -0
  83. package/dist/stores/file/memory-store.js +2 -0
  84. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  85. package/dist/stores/file/permission-rule-adopt.js +95 -0
  86. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  87. package/dist/stores/file/permission-rule-store.js +189 -46
  88. package/dist/stores/file/session-policy-store.js +2 -0
  89. package/dist/stores/file/session-store.js +2 -0
  90. package/dist/stores/file/task-list-store.js +2 -0
  91. package/dist/stores/file/tool-result-store.js +2 -0
  92. package/dist/stores/file/usage-window-store.js +2 -0
  93. package/dist/stores/file/workflow-journal-store.js +2 -0
  94. package/dist/stores/file/workflow-run-store.js +2 -0
  95. package/dist/tools/fs/bash-readonly-classifier.js +59 -10
  96. package/package.json +3 -2
@@ -0,0 +1,195 @@
1
+ /**
2
+ * design/182 §7 — the ORGANIZATION rule layer: core's half of the contract.
3
+ *
4
+ * Org rules and personal rules are opposite in polarity (deny/ask vs allow), authority (a server-side
5
+ * administrator publish vs one human click per add) and evolution (whole-snapshot replacement vs
6
+ * convergent merge). So the org layer has NO CRDT, no tombstones and no join: the server is the single
7
+ * authority, a client holds a read-only versioned snapshot, an update is a whole replacement (revision
8
+ * strictly ordered), and a rollback is a NEW revision carrying old content. Cross-layer tombstones are
9
+ * structurally impossible — an org rule never enters `PersistedAllowRule`, and an org snapshot replace
10
+ * never touches a personal store's bytes. The two layers meet only at the CONSUMPTION site, where org
11
+ * deny/ask folds strict-side: **org deny > org ask (non-dismissable) > personal allow rule > bare ask**.
12
+ *
13
+ * ## Availability semantics (the fail-closed shape, ruled through three review rounds)
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:
18
+ * - the last successfully installed snapshot persists durably (`PersistedOrgRuleState`) and keeps
19
+ * adjudicating while the provider is unreachable, up to a deployment-configured staleness bound;
20
+ * - beyond the bound (or before any snapshot was ever installed) the resolution is `unavailable`, and
21
+ * the CONSUMING gate must fail the ENTIRE decision boundary closed: every terminal allow — policy
22
+ * allow and default allow included — tightens to an ask carrying `decisionReason: "org_unavailable"`
23
+ * AND `requiresRealApproval: true` (the same non-dismissable bit an org ask itself carries; without
24
+ * it a blanket `onAsk: "allow"` deployment would clear the synthetic ask — a third fail-open path),
25
+ * with the personal-rule lane and the auto-mode classifier both disarmed. That obligation lives at
26
+ * the gate integration; THIS module's contract is to report `unavailable` honestly and never lapse
27
+ * into an empty-rules answer that reads as "not governed".
28
+ *
29
+ * ## Anti-rollback
30
+ *
31
+ * `revision` is an orderable server-monotone number, and the installed high-water mark persists with
32
+ * the snapshot in ONE atomic write: a replayed older snapshot (fresh `fetchedAtMs`, old content) is
33
+ * refused and disclosed — identity comparison alone cannot catch it, and an in-memory-only mark would
34
+ * accept revision N−1 after a restart. Signature / predecessor chaining belongs to the privilege-
35
+ * separation ticket; the high-water mark is the floor that needs no key distribution.
36
+ */
37
+ import { type RuleScope } from "./permission-rule-model.js";
38
+ import { type PermissionRuleStore, type PermissionRuleStoreProvider, type RuleOwner } from "./permission-rule-store.js";
39
+ /** One org rule. There is structurally no allow bucket (design/179 §9: the org layer only tightens). */
40
+ export interface OrgPermissionRule {
41
+ rule: string;
42
+ behavior: "deny" | "ask";
43
+ }
44
+ /** The read-only versioned snapshot a client holds. */
45
+ export interface OrgRuleSnapshot {
46
+ /** Server-monotone orderable revision. A snapshot below the persisted high-water mark is refused. */
47
+ revision: number;
48
+ rules: OrgPermissionRule[];
49
+ /** Reserved for the privilege-separation ticket's signed-snapshot contract. Carried, not verified here. */
50
+ signature?: string;
51
+ /** Observation timestamp (epoch ms) — staleness input only, never adjudication input. */
52
+ fetchedAtMs: number;
53
+ }
54
+ /** Where the deployment fetches the current snapshot. `undefined` = the provider has none to give. */
55
+ export interface OrgRuleSnapshotProvider {
56
+ current(): Promise<OrgRuleSnapshot | undefined>;
57
+ }
58
+ /**
59
+ * The durable org state: last-known-good snapshot + the anti-rollback high-water mark, installed in one
60
+ * atomic write. SQL twin note (server/org ticket): `revision_high_water` and `installed_at_ms` are
61
+ * structured columns (`_ms` suffix for epoch milliseconds); the snapshot itself is a JSON blob — it is
62
+ * installed and read whole, never row-queried.
63
+ */
64
+ export interface PersistedOrgRuleState {
65
+ revisionHighWater: number;
66
+ snapshot: OrgRuleSnapshot;
67
+ installedAtMs: number;
68
+ }
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.
71
+ *
72
+ * CONTRACT: `installOrgState` must REFUSE (throw) an install that would LOWER the persisted
73
+ * `revisionHighWater` — the overlay serializes its own resolves, but two overlay instances over one
74
+ * durable home can interleave read-then-write, and the backend's atomic write is the one place the
75
+ * lowering can be caught. The file backend enforces this under its write serialization. */
76
+ export interface OrgRuleStatePersistence {
77
+ readOrgState(): Promise<PersistedOrgRuleState | undefined>;
78
+ installOrgState(state: PersistedOrgRuleState): Promise<void>;
79
+ }
80
+ /** Duck-typed discovery of the persistence face on a store backend, `writerOf` style. */
81
+ export declare function orgRuleStatePersistenceOf(store: PermissionRuleStore): OrgRuleStatePersistence | undefined;
82
+ /** The `decisionReason` the consuming gate stamps on the synthetic asks it mints while org adjudication
83
+ * is unavailable. Exported so the gate integration and its tests share one spelling — and TYPED as the
84
+ * member of {@link import("./tool-policy.js").DecisionReason} it must equal, so the constant and the
85
+ * vocabulary cannot drift into two spellings of one word. */
86
+ export declare const ORG_UNAVAILABLE_DECISION_REASON: Extract<import("./tool-policy.js").DecisionReason, "org_unavailable">;
87
+ /**
88
+ * How long a single org adjudication may take before the consuming gate stops waiting and reads the
89
+ * answer as UNAVAILABLE (its fail-closed word).
90
+ *
91
+ * A bound is MANDATORY rather than optional because of where this call sits: on the hot path of every
92
+ * governed tool call, and — on the durable resume leg — after the checkpoint has been consumed and the
93
+ * start frame emitted. A provider that never settles would otherwise wedge the call past any task
94
+ * deadline (tasks carry no walltime by default), leaving the frame unpaired and the approval spent with
95
+ * nothing retryable. A hang is the one outcome worse than either verdict.
96
+ *
97
+ * The value matches the auto-mode classifier's per-round-trip cap — the other model/network call the
98
+ * permission path makes — because the reasoning is the same: a governance lookup that takes longer than
99
+ * this is indistinguishable from an outage, and an outage is exactly what the unavailable arm is for.
100
+ * The task's own abort signal still applies in parallel; whichever fires first ends the wait.
101
+ */
102
+ export declare const ORG_ADJUDICATION_TIMEOUT_MS = 15000;
103
+ /**
104
+ * Await `p`, but settle with `fallback` if the deadline elapses or the signal fires first.
105
+ *
106
+ * Shared by both org consumption sites so one bound cannot drift into two. Deliberately NOT `.unref()`
107
+ * on the timer: this is a foreground rescue timer, and the caller is awaiting it inside the control
108
+ * flow — an unref'd timer stops firing in exactly the situation it exists for (an otherwise idle loop).
109
+ * A rejecting `p` also lands on the fallback: the callers' fail-closed word is the same either way, and
110
+ * an escaping rejection here would turn a dependency failure into a task crash.
111
+ */
112
+ export declare function settleOrgVerdictWithin<T>(p: Promise<T>, fallback: T, opts: {
113
+ signal?: AbortSignal;
114
+ timeoutMs: number;
115
+ }): Promise<T>;
116
+ /** The `decisionReason` of a decision an ORG RULE produced (a deny, or a non-dismissable ask). Same
117
+ * single-spelling contract as {@link ORG_UNAVAILABLE_DECISION_REASON}. */
118
+ export declare const ORG_RULE_DECISION_REASON: Extract<import("./tool-policy.js").DecisionReason, "org_rule">;
119
+ /** How far into the future a snapshot's `fetchedAtMs` may sit before it is refused — ordinary NTP-level
120
+ * clock skew passes; a far-future timestamp (which would satisfy the staleness bound INDEFINITELY,
121
+ * turning fail-closed into evergreen freshness) does not. */
122
+ export declare const ORG_FETCHED_AT_SKEW_ALLOWANCE_MS: number;
123
+ export type OrgOverlayStatus = "fresh" | "last-known-good" | "unavailable";
124
+ /** What one resolution reports. `rules` is empty ⟺ status `unavailable` or the snapshot is empty —
125
+ * the two are distinguished by `status`, never by the emptiness of the list. */
126
+ export interface OrgOverlayResolution {
127
+ status: OrgOverlayStatus;
128
+ rules: OrgPermissionRule[];
129
+ revision?: number;
130
+ /** Honest-disclosure lines: provider failures, refused snapshots, rollback refusals. */
131
+ disclosures: string[];
132
+ }
133
+ export interface OrgRuleOverlay {
134
+ resolve(): Promise<OrgOverlayResolution>;
135
+ }
136
+ /**
137
+ * Construct the org overlay for a GOVERNED deployment. This constructor IS the boot gate:
138
+ * - `governed` must be literally `true` — the overlay does not exist for ungoverned deployments
139
+ * (their decision path stays byte-identical to a build without this module);
140
+ * - a governed declaration WITHOUT a provider is a configuration contradiction and refuses to boot —
141
+ * a silently ungoverned "governed" deployment is precisely the failure this bit exists to prevent;
142
+ * - `stalenessBoundMs` follows the knob discipline: a non-number, NaN, non-finite or non-positive
143
+ * bound is refused loudly, never coerced.
144
+ */
145
+ export declare function createOrgRuleOverlay(cfg: {
146
+ governed: true;
147
+ provider?: OrgRuleSnapshotProvider;
148
+ /** Durable LKG + high-water home. Omitted ⇒ process-local only, DISCLOSED on every resolution: a
149
+ * restart then forgets both the last-known-good snapshot and the anti-rollback mark. */
150
+ persistence?: OrgRuleStatePersistence;
151
+ stalenessBoundMs: number;
152
+ now?: () => number;
153
+ }): OrgRuleOverlay;
154
+ /**
155
+ * design/182 §7.2 — which org rule speaks for this command, if any. Deny outranks ask; within a
156
+ * behavior the first textual match wins (reporting order only — all denies are the same one answer).
157
+ * Matching uses the SAME parser and matcher the personal lane uses (tighten direction: an
158
+ * interpreter-headed prefix deny like `Bash(node:*)` is a legitimately wide tightening and matches).
159
+ * A rule text the validator refuses never gets here — snapshot validation refuses the whole snapshot,
160
+ * so an unenforceable deny cannot install as policy; the skip below is a defensive floor only.
161
+ */
162
+ export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], call: {
163
+ tool: string;
164
+ command: string;
165
+ }): {
166
+ behavior: "deny" | "ask";
167
+ rule: string;
168
+ } | undefined;
169
+ /** The effective status of one personal rule under the current org overlay — computed, never stored. */
170
+ export interface EffectivePermissionRule {
171
+ rule: string;
172
+ scope: RuleScope;
173
+ status: "live" | "shadowed-by-org" | "removed";
174
+ }
175
+ /**
176
+ * design/182 §7.3 — the introspection face: every personal rule with its effective status under the org
177
+ * overlay. An org deny does not delete a personal rule — the personal rule is the user's approval
178
+ * record, and the org's power is to OVERRIDE it, not to forge its removal — so a shadowed rule reports
179
+ * `shadowed-by-org` and comes back by itself when the org deny is withdrawn. A rule whose every add is
180
+ * tombstoned reports `removed` (its tombstone identity is still visible in the store).
181
+ *
182
+ * The shadow predicate: an org DENY rule admits the personal rule's command pattern (for a prefix
183
+ * personal rule, the org deny admits its prefix body — a wider org deny shadows every narrower allow
184
+ * under it). org ASK rules do not shadow: the personal lane never consumes a `requiresRealApproval`
185
+ * ask, so the two never actually meet on one call.
186
+ */
187
+ export declare function effectivePermissionRules(opts: {
188
+ provider: PermissionRuleStoreProvider;
189
+ principal?: string;
190
+ owner?: RuleOwner;
191
+ orgSnapshot?: OrgRuleSnapshot;
192
+ }): Promise<EffectivePermissionRule[]>;
193
+ /** Does this store carry a write face? Re-exported convenience for org-ticket integrations that need to
194
+ * distinguish a read-only projection from a writable personal bucket without importing the writer seam. */
195
+ export declare function isWritablePermissionRuleStore(store: PermissionRuleStore): boolean;
@@ -0,0 +1,234 @@
1
+ import { parseAllowRuleText, ruleAdmitsCommand } 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
+ }
9
+ export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
10
+ export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
11
+ export function settleOrgVerdictWithin(p, fallback, opts) {
12
+ return new Promise((resolve) => {
13
+ let settled = false;
14
+ const finish = (v) => {
15
+ if (settled)
16
+ return;
17
+ settled = true;
18
+ clearTimeout(timer);
19
+ opts.signal?.removeEventListener("abort", onAbort);
20
+ resolve(v);
21
+ };
22
+ const onAbort = () => finish(fallback);
23
+ const timer = setTimeout(() => finish(fallback), opts.timeoutMs);
24
+ if (opts.signal?.aborted === true) {
25
+ finish(fallback);
26
+ return;
27
+ }
28
+ opts.signal?.addEventListener("abort", onAbort);
29
+ p.then(finish, () => finish(fallback));
30
+ });
31
+ }
32
+ export const ORG_RULE_DECISION_REASON = "org_rule";
33
+ export const ORG_FETCHED_AT_SKEW_ALLOWANCE_MS = 5 * 60_000;
34
+ export function createOrgRuleOverlay(cfg) {
35
+ if (cfg.governed !== true) {
36
+ throw new Error("createOrgRuleOverlay is only for org-governed deployments — pass `governed: true` or do not construct one");
37
+ }
38
+ if (cfg.provider === undefined) {
39
+ 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
+ }
41
+ if (typeof cfg.stalenessBoundMs !== "number" || !Number.isFinite(cfg.stalenessBoundMs) || cfg.stalenessBoundMs <= 0) {
42
+ throw new Error(`org stalenessBoundMs must be a finite positive number of milliseconds (got ${String(cfg.stalenessBoundMs)})`);
43
+ }
44
+ const provider = cfg.provider;
45
+ const now = cfg.now ?? Date.now;
46
+ let memoryState;
47
+ const readState = async () => {
48
+ const state = cfg.persistence !== undefined ? await cfg.persistence.readOrgState() : memoryState;
49
+ return state === undefined ? undefined : structuredClone(state);
50
+ };
51
+ const writeState = async (state) => {
52
+ if (cfg.persistence !== undefined)
53
+ await cfg.persistence.installOrgState(structuredClone(state));
54
+ else
55
+ memoryState = structuredClone(state);
56
+ };
57
+ let chain = Promise.resolve();
58
+ const serialize = (fn) => {
59
+ const run = chain.then(fn, fn);
60
+ chain = run.then(() => undefined, () => undefined);
61
+ return run;
62
+ };
63
+ const resolve = async () => {
64
+ const disclosures = [];
65
+ if (cfg.persistence === undefined) {
66
+ disclosures.push("org rule state has no durable persistence wired — the last-known-good snapshot and the anti-rollback mark do not survive a restart");
67
+ }
68
+ let fetched;
69
+ let fetchFailed = false;
70
+ try {
71
+ fetched = await provider.current();
72
+ }
73
+ catch (err) {
74
+ fetchFailed = true;
75
+ disclosures.push(`org snapshot provider failed: ${err instanceof Error ? err.message : String(err)}`);
76
+ }
77
+ return await serialize(async () => {
78
+ const nowMs = now();
79
+ const guardedRead = async (context) => {
80
+ try {
81
+ return await readState();
82
+ }
83
+ catch (err) {
84
+ disclosures.push(`org state ${context} read failed: ${err instanceof Error ? err.message : String(err)}`);
85
+ return undefined;
86
+ }
87
+ };
88
+ const installed = await (async () => {
89
+ const persisted = await guardedRead("persistence");
90
+ if (fetched === undefined)
91
+ return persisted;
92
+ const invalid = validateOrgSnapshot(fetched, nowMs);
93
+ if (invalid !== undefined) {
94
+ disclosures.push(`org snapshot refused: ${invalid}; the previously installed snapshot (if any) remains in force`);
95
+ return persisted;
96
+ }
97
+ if (persisted !== undefined && fetched.revision < persisted.revisionHighWater) {
98
+ disclosures.push(`org snapshot revision ${fetched.revision} is below the installed high-water mark ${persisted.revisionHighWater} — refusing the rollback; the current deny set is unchanged`);
99
+ return persisted;
100
+ }
101
+ if (persisted !== undefined && fetched.revision === persisted.snapshot.revision && !sameOrgPolicyContent(fetched, persisted.snapshot)) {
102
+ 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`);
103
+ return persisted;
104
+ }
105
+ const state = {
106
+ revisionHighWater: Math.max(persisted?.revisionHighWater ?? 0, fetched.revision),
107
+ snapshot: fetched,
108
+ installedAtMs: nowMs,
109
+ };
110
+ try {
111
+ await writeState(state);
112
+ }
113
+ catch (err) {
114
+ const reread = await guardedRead("reconciliation");
115
+ if (reread !== undefined && reread.revisionHighWater >= state.revisionHighWater) {
116
+ disclosures.push(`org snapshot install was superseded by a concurrent install at revision ${reread.snapshot.revision} — continuing on the newer installed state`);
117
+ return reread;
118
+ }
119
+ disclosures.push(`org snapshot install failed (${err instanceof Error ? err.message : String(err)}) and no superseding installed state exists — org adjudication is UNAVAILABLE`);
120
+ return undefined;
121
+ }
122
+ return state;
123
+ })();
124
+ const decisionMs = now();
125
+ const withinBound = (snap) => decisionMs - snap.fetchedAtMs <= cfg.stalenessBoundMs && snap.fetchedAtMs <= decisionMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS;
126
+ if (installed !== undefined) {
127
+ if (installed.snapshot.fetchedAtMs > decisionMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS) {
128
+ disclosures.push(`the installed org snapshot claims a FUTURE observation time (${installed.snapshot.fetchedAtMs} vs now ${decisionMs}) — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`);
129
+ return { status: "unavailable", rules: [], disclosures };
130
+ }
131
+ if (withinBound(installed.snapshot)) {
132
+ const freshlyInstalled = fetched !== undefined && installed.snapshot === fetched;
133
+ if (!freshlyInstalled && !fetchFailed && fetched === undefined) {
134
+ disclosures.push("org snapshot provider returned no snapshot — continuing on the last-known-good");
135
+ }
136
+ return {
137
+ status: freshlyInstalled ? "fresh" : "last-known-good",
138
+ rules: structuredClone(installed.snapshot.rules),
139
+ revision: installed.snapshot.revision,
140
+ disclosures,
141
+ };
142
+ }
143
+ 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`);
144
+ return { status: "unavailable", rules: [], disclosures };
145
+ }
146
+ 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");
147
+ return { status: "unavailable", rules: [], disclosures };
148
+ });
149
+ };
150
+ return { resolve };
151
+ }
152
+ function sameOrgPolicyContent(a, b) {
153
+ if (a.rules.length !== b.rules.length)
154
+ return false;
155
+ return a.rules.every((r, i) => b.rules[i]?.rule === r.rule && b.rules[i]?.behavior === r.behavior);
156
+ }
157
+ function validateOrgSnapshot(s, nowMs) {
158
+ if (typeof s.revision !== "number" || !Number.isFinite(s.revision))
159
+ return `revision is not a finite number (${String(s.revision)})`;
160
+ if (typeof s.fetchedAtMs !== "number" || !Number.isFinite(s.fetchedAtMs))
161
+ return `fetchedAtMs is not a finite number (${String(s.fetchedAtMs)})`;
162
+ if (s.fetchedAtMs > nowMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS) {
163
+ return `fetchedAtMs is ${s.fetchedAtMs - nowMs}ms in the future (allowance ${ORG_FETCHED_AT_SKEW_ALLOWANCE_MS}ms) — a future-dated observation would satisfy the staleness bound indefinitely`;
164
+ }
165
+ if (!Array.isArray(s.rules))
166
+ return "rules is not an array";
167
+ for (const r of s.rules) {
168
+ if (typeof r?.rule !== "string" || r.rule === "")
169
+ return "a rule entry carries no rule text";
170
+ if (r.behavior !== "deny" && r.behavior !== "ask")
171
+ return `rule "${r.rule}" carries behavior "${String(r.behavior)}" — the org layer has no allow bucket`;
172
+ const parsed = parseAllowRuleText(r.rule, { direction: "tighten" });
173
+ if ("reject" in parsed)
174
+ return `rule "${r.rule}" does not parse (${parsed.reject.code}) — an unenforceable ${r.behavior} must not install as policy`;
175
+ }
176
+ return undefined;
177
+ }
178
+ export function orgRuleVerdictFor(rules, call) {
179
+ let ask;
180
+ for (const r of rules) {
181
+ const parsed = parseAllowRuleText(r.rule, { direction: "tighten" });
182
+ if ("reject" in parsed)
183
+ continue;
184
+ if (parsed.rule.tool !== call.tool)
185
+ continue;
186
+ if (!ruleAdmitsCommand(parsed.rule, call.command))
187
+ continue;
188
+ if (r.behavior === "deny")
189
+ return { behavior: "deny", rule: r.rule };
190
+ ask ??= { behavior: "ask", rule: r.rule };
191
+ }
192
+ return ask;
193
+ }
194
+ export async function effectivePermissionRules(opts) {
195
+ const store = resolveIntrospectionStore(opts);
196
+ const listed = await store.list();
197
+ const orgDenies = (opts.orgSnapshot?.rules ?? []).filter((r) => r.behavior === "deny");
198
+ const out = [];
199
+ for (const r of listed.rules) {
200
+ const shadowed = orgDenies.some((d) => {
201
+ const parsed = parseAllowRuleText(d.rule, { direction: "tighten" });
202
+ return !("reject" in parsed) && parsed.rule.tool === r.tool && ruleAdmitsCommand(parsed.rule, r.command);
203
+ });
204
+ out.push({ rule: r.rule, scope: r.scope, status: shadowed ? "shadowed-by-org" : "live" });
205
+ }
206
+ for (const t of listed.tombstones) {
207
+ if (listed.rules.some((r) => r.rule === t.rule && sameScope(r.scope, t.scope)))
208
+ continue;
209
+ if (out.some((e) => e.rule === t.rule && sameScope(e.scope, t.scope)))
210
+ continue;
211
+ out.push({ rule: t.rule, scope: t.scope, status: "removed" });
212
+ }
213
+ return out;
214
+ }
215
+ function resolveIntrospectionStore(opts) {
216
+ if (opts.owner !== undefined && opts.owner.kind === "local-owner") {
217
+ if (opts.principal !== undefined) {
218
+ throw new Error("pass either a principal or a local-owner, not both — a bucket has one owner");
219
+ }
220
+ const store = opts.provider.forLocalOwner?.();
221
+ if (store === undefined) {
222
+ throw new Error("this provider has no local-owner bucket (forLocalOwner is not implemented)");
223
+ }
224
+ return store;
225
+ }
226
+ if (opts.owner !== undefined && opts.owner.kind === "principal" && opts.principal !== undefined && opts.owner.principal !== opts.principal) {
227
+ throw new Error(`contradictory identity: principal "${opts.principal}" and owner principal "${opts.owner.principal}" disagree`);
228
+ }
229
+ const principal = opts.owner?.kind === "principal" ? opts.owner.principal : opts.principal;
230
+ return opts.provider.forPrincipal(principal);
231
+ }
232
+ export function isWritablePermissionRuleStore(store) {
233
+ return writerOf(store) !== undefined;
234
+ }