@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.
- package/CHANGELOG.md +69 -0
- package/dist/agents/cascade.d.ts +2 -1
- package/dist/agents/peer-notices.d.ts +11 -1
- package/dist/agents/peer-session-drain.js +2 -0
- package/dist/agents/verify.d.ts +2 -1
- package/dist/core/ask-origin.d.ts +130 -0
- package/dist/core/ask-origin.js +35 -0
- package/dist/core/auto-mode-arming.d.ts +40 -1
- package/dist/core/auto-mode-arming.js +51 -3
- package/dist/core/auto-mode.d.ts +87 -10
- package/dist/core/auto-mode.js +34 -6
- package/dist/core/checkpoint-store.d.ts +41 -6
- package/dist/core/checkpoint-store.js +8 -0
- package/dist/core/hooks.d.ts +63 -19
- package/dist/core/hooks.js +37 -16
- package/dist/core/mcp.d.ts +47 -55
- package/dist/core/mcp.js +89 -31
- package/dist/core/park-selfcheck.js +3 -0
- package/dist/core/permission-rule-consent.d.ts +2 -11
- package/dist/core/permission-rule-consent.js +13 -62
- package/dist/core/permission-rule-org.d.ts +70 -54
- package/dist/core/permission-rule-org.js +47 -61
- package/dist/core/permission-rule-provider.d.ts +162 -0
- package/dist/core/permission-rule-provider.js +148 -0
- package/dist/core/permission-rule-session.d.ts +23 -19
- package/dist/core/permission-rule-session.js +5 -5
- package/dist/core/permission-rule-store.d.ts +46 -21
- package/dist/core/permission-rule-store.js +13 -6
- package/dist/core/permission-rule-sync.d.ts +2 -1
- package/dist/core/permission-rule-sync.js +11 -0
- package/dist/core/runner/assemble-result.d.ts +3 -2
- package/dist/core/runner/checkpoint-scope.d.ts +32 -0
- package/dist/core/runner/checkpoint-scope.js +4 -0
- package/dist/core/runner/contracts.d.ts +1878 -0
- package/dist/core/runner/contracts.js +1 -0
- package/dist/core/runner/denial-limit-arms.d.ts +57 -31
- package/dist/core/runner/denial-limit-arms.js +42 -17
- package/dist/core/runner/derived-route-fallback.d.ts +34 -0
- package/dist/core/runner/derived-route-fallback.js +16 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
- package/dist/core/runner/prepare-announce-once.d.ts +83 -0
- package/dist/core/runner/prepare-announce-once.js +105 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
- package/dist/core/runner/prepare-config-doors.d.ts +2 -10
- package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
- package/dist/core/runner/prepare-defer-classify.js +107 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
- package/dist/core/runner/prepare-delegation-surface.js +144 -0
- package/dist/core/runner/prepare-execution-env.d.ts +54 -0
- package/dist/core/runner/prepare-execution-env.js +86 -0
- package/dist/core/runner/prepare-file-history.d.ts +95 -0
- package/dist/core/runner/prepare-file-history.js +383 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
- package/dist/core/runner/prepare-hands-readface.js +3 -3
- package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
- package/dist/core/runner/prepare-inherited-gate.js +266 -0
- package/dist/core/runner/prepare-listings.d.ts +77 -0
- package/dist/core/runner/prepare-listings.js +76 -0
- package/dist/core/runner/prepare-lsp.d.ts +55 -0
- package/dist/core/runner/prepare-lsp.js +27 -0
- package/dist/core/runner/prepare-memory.d.ts +1 -1
- package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
- package/dist/core/runner/prepare-offload-wrappers.js +45 -0
- package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
- package/dist/core/runner/prepare-permission-rules.js +140 -0
- package/dist/core/runner/prepare-project-context.d.ts +131 -0
- package/dist/core/runner/prepare-project-context.js +150 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
- package/dist/core/runner/prepare-prompt-inputs.js +141 -0
- package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
- package/dist/core/runner/prepare-protocol-tools.js +182 -0
- package/dist/core/runner/prepare-question-face.d.ts +119 -0
- package/dist/core/runner/prepare-question-face.js +83 -0
- package/dist/core/runner/prepare-run-refs.d.ts +89 -0
- package/dist/core/runner/prepare-run-refs.js +39 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
- package/dist/core/runner/prepare-task.d.ts +11 -1815
- package/dist/core/runner/prepare-task.js +138 -2542
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
- package/dist/core/runner/prepare-wiring-manifest.js +240 -0
- package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
- package/dist/core/runner/prepare-workspace-restore.js +1 -22
- package/dist/core/runner/rollback-stack.d.ts +32 -0
- package/dist/core/runner/rollback-stack.js +30 -0
- package/dist/core/runner/runtask.d.ts +11 -2
- package/dist/core/runner/runtask.js +27 -9
- package/dist/core/runner/workspace-path.d.ts +33 -0
- package/dist/core/runner/workspace-path.js +22 -0
- package/dist/core/sensitive-path-policy.d.ts +16 -0
- package/dist/core/sensitive-path-policy.js +1 -1
- package/dist/core/tool-policy.d.ts +57 -9
- package/dist/core/tool-policy.js +11 -0
- package/dist/core/types.d.ts +63 -51
- package/dist/core/wiring-manifest.d.ts +40 -3
- package/dist/core/wiring-manifest.js +4 -3
- package/dist/core/write-protect.d.ts +13 -2
- package/dist/core/write-protect.js +58 -29
- package/dist/engine/harness/types.d.ts +38 -16
- package/dist/engine/harness/types.js +25 -1
- package/dist/engine/session/session.d.ts +3 -11
- package/dist/index.d.ts +10 -6
- package/dist/index.js +9 -5
- package/dist/internal/harness.d.ts +1 -0
- package/dist/stores/file/adoption/adopt.d.ts +1 -1
- package/dist/stores/file/adoption/marker.d.ts +1 -1
- package/dist/stores/file/permission-rule-adopt.js +4 -3
- package/dist/stores/file/permission-rule-store.d.ts +65 -25
- package/dist/stores/file/permission-rule-store.js +215 -37
- package/dist/stores/file/task-list-store.d.ts +1 -1
- package/dist/tools/fs/read-face.d.ts +1 -1
- package/package.json +8 -2
- package/test/export-surface.snapshot.json +76 -28
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* design/182 §7 — the
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
38
|
-
import {
|
|
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
|
|
70
|
-
* org
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
*
|
|
135
|
-
export
|
|
136
|
-
|
|
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:
|
|
143
|
+
/** Honest-disclosure lines: source failures, refused snapshots, rollback refusals. */
|
|
140
144
|
disclosures: string[];
|
|
141
145
|
}
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
*
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
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
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
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 {
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
261
|
-
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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;
|