@sema-agent/core 5.24.0 → 5.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.25.0 — 2026-08-10
4
+
5
+ No API-BREAKING changes (exports grow only; every new member is optional/additive). One
6
+ behavior-surface narrowing, called out below.
7
+
8
+ ### Narrowed (behavior, ruled)
9
+
10
+ - **The persisted-rule lane gains its mandate boundary (#144, dual-source measured).** *Allow rules
11
+ silence the classifier's questions, never a mandated one.* A persisted allow rule used to
12
+ short-circuit every surviving non-governance ask — including an operator's `shellGate:"always"`
13
+ (per-call confirmation mandated by deployment config) and a tool's own egress/irreversibility
14
+ marks (the non-budgetable family). The boundary is a single-source provenance predicate: the
15
+ classify-doctrine bash ask (coarse tier `"maybe"`) stays the rule lane's home turf (the
16
+ don't-ask-again main case is deliberately preserved); doctrine-installed `"always"` and
17
+ tool-declared marks are not clearable by rule. When a rule MATCHES but cannot clear, the surviving
18
+ ask discloses it on both channels: a message note naming the rule and the mandate, and the new
19
+ additive `PermissionResult.ask.persistedRuleShadowed` member (the matched rule text) — a consumer
20
+ renders "your rule is alive, just outranked". **Consumer note**: deployments under
21
+ `shellGate:"always"` or with marked tools will see asks their users' rules used to clear.
22
+
23
+ ### Added
24
+
25
+ - **`CheckpointError.detail.reason`** (closed set, additive; downstream-requested): discriminates
26
+ the pre-CAS refusal arms one code used to cover — `version_newer` / `env_factory_missing` /
27
+ `governed_unwired` on `unsupported_version` (each retryable on a differently-capable worker), and
28
+ `real_approval_damaged` / `real_approval_forged` / `constraint_chain_missing` on the
29
+ `invalid_outcome` row-integrity arms (terminal for the row's bytes). A deployment retry policy can
30
+ now tell "a capable worker can redeem this" from "no worker ever will".
31
+ - **`REAL_APPROVAL_CHECKPOINT_VERSION` exported** from the package root (joins the other five ladder
32
+ constants).
33
+ - **The I6 adoption boot gate reaches two more store faces** via an optional `dataRoot` anchor on
34
+ `FilePermissionRuleStoreProvider` and `createFileTaskListStore` (additive; the adoption arc's own
35
+ nested rule leg keeps its exemption, pinned). The freeze header states its real coverage — two
36
+ doors and the seams between them — instead of over-claiming.
37
+
38
+ ### Fixed
39
+
40
+ - **The disclosure channel is real end-to-end (pre-release rescan on this very batch).**
41
+ `persistedRuleShadowed` was write-only as first landed: `AskRequest` gains the member (all four
42
+ mint sites thread it), and the durable park mint carries it as `RiskDescriptor.shadowedRule`
43
+ (`inlineUntrusted`-capped) — the mandated population's normal route now discloses like the
44
+ synchronous one. The mandate predicate judges egress FIRST (the tool's own declaration is not
45
+ shadowed by the coarse doctrine sharing the seat). The secret-scrub quarantine capture stops
46
+ treating a name collision as a receipt (family suffix form; the polluted-index warning claims a
47
+ capture only when its write landed). The last silent timeout discard (a legal cap below the
48
+ resolved default) is announced; an empty env string is a written silence exception (the unset
49
+ idiom). The materialize-strategy announcements gain a per-value process ledger and a reset seam.
50
+ - **The loud-bad-value law lands on its two founding cases (#123, ruled).** `BASH_*_TIMEOUT_MS`
51
+ discards stop exempting the garbage/0/negative classes (every discarded value names the knob, what
52
+ arrived — env legs show the original string — and the value in force);
53
+ `SEMA_TOOL_MATERIALIZE_STRATEGY`'s refusal now matches the seat the bad value occupies: shadowed
54
+ by an explicit legal spec ⇒ loud discard (the documented "spec wins" precedence finally holds),
55
+ would-be-in-force ⇒ the closed-set refusal stands, dormant (no deferred tools) ⇒ announced once
56
+ per process instead of lying in wait.
57
+ - **A v7+ checkpoint declaring parent constraints must carry both the frozen chain and its digest**
58
+ (they are one write with the version stamp) — a row carrying neither is refused pre-CAS as damaged
59
+ instead of silently falling back to the count-only contract. Rows v6 and below keep the historic
60
+ contract.
61
+ - **The org resume belt's refusal carries the overlay's disclosure lines** (the resume path has no
62
+ `onUnavailable` seat) and tells a cancelled wait apart from unreadable governance. The verdict is
63
+ unchanged; the account is not.
64
+ - The sandbox-admission registry/decision domain split is documented and pinned (the two
65
+ un-instrumented fold families are safe for structural reasons — a family that gains an ask arm
66
+ reds instead of arriving unrecorded); frozen projections' fold position is disclosed; the adoption
67
+ report declares the memory-engine tree `action:"none"` explicitly; two engine quarantine captures
68
+ go `wx`-exclusive (no symlink at the final name, no overwrite of earlier evidence under a coarse
69
+ clock); the abort-races-timeout test pin gets real load headroom; the v8 stamp note carries its
70
+ erratum (v8 rows appear on ANY durable deployment via the always-mounted integrity policies — what
71
+ stays true: rows without the bit keep their historic stamps).
72
+
3
73
  ## 5.24.0 — 2026-08-10
4
74
 
5
75
  No API-BREAKING changes (exports grow only; `suspendAsk` gains an optional fifth parameter;
@@ -135,6 +135,11 @@ export interface RiskDescriptor {
135
135
  * attempted — over-reaching a shell parse risks a wrong/forgeable path). Each path `inlineUntrusted`-capped.
136
136
  * Omitted when none derivable. */
137
137
  touchedPaths?: string[];
138
+ /** #144 (additive): a persisted allow rule MATCHED this call but could not clear the mandated ask —
139
+ * the matched rule text (`inlineUntrusted`-capped), threaded to the mint so the durable-park route
140
+ * carries the same disclosure the synchronous ask does. The inbox renders "their rule is alive,
141
+ * just outranked" instead of the person concluding their rule silently broke. */
142
+ shadowedRule?: string;
138
143
  }
139
144
  /**
140
145
  * design/80 §D-E: the DETERMINISTIC severity tier (1..5) for an escalation checkpoint, a PURE function of the
@@ -266,6 +271,8 @@ export declare function buildRiskDescriptor(input: {
266
271
  shellGated?: boolean;
267
272
  /** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
268
273
  shellGateDoctrine?: "classify" | "always";
274
+ /** #144: the matched-but-outranked persisted rule to persist (see {@link RiskDescriptor.shadowedRule}). */
275
+ shadowedRule?: string;
269
276
  }): RiskDescriptor;
270
277
  /**
271
278
  * #130/#131/#120 (2026-08-10) — the durable record of an ask's `requiresRealApproval` bit, which used
@@ -805,8 +812,11 @@ export interface CheckpointState {
805
812
  * (extra deny-narrowing layers + the pre-CAS edit re-adjudication) rather than trusting a
806
813
  * re-supplied policy's execution half for the frozen deny data — a re-supplied closure that
807
814
  * matches the digest but behaves differently cannot un-deny what the suspend froze. Present iff
808
- * `requiresParentConstraint` on new mints; absent on older checkpoints (⇒ no frozen deny data —
809
- * the count-only contract governs, and an EDIT resume is refused for want of a validatable chain). */
815
+ * `requiresParentConstraint` on new mints; absent on PRE-F-012 checkpoints (⇒ no frozen deny data —
816
+ * the count-only contract governs, and an EDIT resume is refused for want of a validatable chain).
817
+ * "Older" is version-decidable, not guessed: chain, digest and the {@link F012_CHECKPOINT_VERSION}
818
+ * stamp are one write, so on a v7+ row the pair is REQUIRED — its absence there is a damaged row and
819
+ * the resume ladder refuses it pre-CAS rather than falling back to the count-only contract. */
810
820
  constraintChain?: ReadonlyArray<import("./tool-policy.js").ConstraintChainEntry>;
811
821
  /**
812
822
  * F-012 L1 — the content digest over `constraintChain` ({@link import("./tool-policy.js").constraintChainDigest}):
@@ -814,7 +824,8 @@ export interface CheckpointState {
814
824
  * content. The resume pre-CAS ladder recomputes the digest over the RE-SUPPLIED chain's
815
825
  * projections and rejects a mismatch (`resume.parent_constraint_mismatch`) — replacing the
816
826
  * count-only shape check with a content contract (a same-length chain of different frozen deny
817
- * data no longer passes). Present iff `constraintChain` is. */
827
+ * data no longer passes). Present iff `constraintChain` is — and on a v7+ row both are required
828
+ * (see {@link constraintChain}: the pair and the version stamp are minted together). */
818
829
  constraintDigest?: string;
819
830
  };
820
831
  /**
@@ -940,7 +951,11 @@ export declare const F012_CHECKPOINT_VERSION = 7;
940
951
  * either redeem a governed row with no org wiring at all (#120's exact hole, replayed through version
941
952
  * skew) or burn the approval on the unavailable belt the bit exists to soften. Stamping v8 forces it
942
953
  * to reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A gate
943
- * with NO `realApproval` keeps the historic stamps ungoverned deployments see zero version movement.
954
+ * with NO `realApproval` keeps the historic stamps. NOTE (erratum 2026-08-10, downstream-measured):
955
+ * "zero version movement for ungoverned deployments" is TOO WIDE — the two always-mounted integrity
956
+ * policies (transcript-integrity, unverifiable-delete) mint `requiresRealApproval` asks, so a park on
957
+ * one of THOSE stamps v8 with `origin:"policy"` on ANY durable deployment, org-governed or not. What
958
+ * stays true: rows whose gate carries no realApproval bit keep their historic stamps everywhere.
944
959
  */
945
960
  export declare const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
946
961
  /** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
@@ -1463,8 +1478,8 @@ export declare class CheckpointError extends Error {
1463
1478
  * computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
1464
1479
  * checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
1465
1480
  | "resume.constraint_unprojectable";
1466
- /** Structured discrimination a caller can branch on where the code alone is ambiguous. Today one
1467
- * member: `field` names WHICH part of the decision payload was rejected on a
1481
+ /** Structured discrimination a caller can branch on where the code alone is ambiguous. Two
1482
+ * members. `field` names WHICH part of the decision payload was rejected on a
1468
1483
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
1469
1484
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
1470
1485
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
@@ -1476,6 +1491,18 @@ export declare class CheckpointError extends Error {
1476
1491
  * discriminant. */
1477
1492
  readonly detail?: {
1478
1493
  field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
1494
+ /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1495
+ * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
1496
+ * damaged/forged and no worker ever will" — blanket-retriable and gate-shape heuristics were
1497
+ * both refuted downstream, so the throw site carries the fact it always knew). Closed set;
1498
+ * additive and optional like `field` — `code` remains the only REQUIRED discriminant.
1499
+ * · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
1500
+ * `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
1501
+ * (retryable on an org-wired worker).
1502
+ * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
1503
+ * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
1504
+ * current bytes — no worker version redeems a damaged row). */
1505
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
1479
1506
  } | undefined;
1480
1507
  constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
1481
1508
  /** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
@@ -1572,8 +1599,8 @@ export declare class CheckpointError extends Error {
1572
1599
  * computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
1573
1600
  * checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
1574
1601
  | "resume.constraint_unprojectable", message: string,
1575
- /** Structured discrimination a caller can branch on where the code alone is ambiguous. Today one
1576
- * member: `field` names WHICH part of the decision payload was rejected on a
1602
+ /** Structured discrimination a caller can branch on where the code alone is ambiguous. Two
1603
+ * members. `field` names WHICH part of the decision payload was rejected on a
1577
1604
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
1578
1605
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
1579
1606
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
@@ -1585,6 +1612,18 @@ export declare class CheckpointError extends Error {
1585
1612
  * discriminant. */
1586
1613
  detail?: {
1587
1614
  field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
1615
+ /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1616
+ * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
1617
+ * damaged/forged and no worker ever will" — blanket-retriable and gate-shape heuristics were
1618
+ * both refuted downstream, so the throw site carries the fact it always knew). Closed set;
1619
+ * additive and optional like `field` — `code` remains the only REQUIRED discriminant.
1620
+ * · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
1621
+ * `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
1622
+ * (retryable on an org-wired worker).
1623
+ * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
1624
+ * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
1625
+ * current bytes — no worker version redeems a damaged row). */
1626
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
1588
1627
  } | undefined);
1589
1628
  }
1590
1629
  /**
@@ -120,6 +120,7 @@ export function buildRiskDescriptor(input) {
120
120
  axes,
121
121
  toolName,
122
122
  ...(input.shellGated && input.shellGateDoctrine !== undefined ? { shellGateDoctrine: input.shellGateDoctrine } : {}),
123
+ ...(input.shadowedRule !== undefined ? { shadowedRule: inlineUntrusted(input.shadowedRule, 200) } : {}),
123
124
  ...(summary !== undefined ? { summary } : {}),
124
125
  ...(touchedPaths !== undefined ? { touchedPaths } : {}),
125
126
  };
@@ -608,7 +608,10 @@ export interface ToolGateInput {
608
608
  * auto-approve what only judgment may clear). `origin` records whether the bit came from an org
609
609
  * ASK rule, from the org-unavailable tighten (whose resume semantics differ — see
610
610
  * {@link import("./checkpoint-store.js").RealApprovalGateBit}), or from a policy/hook. */
611
- realApproval?: import("./checkpoint-store.js").RealApprovalGateBit) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
611
+ realApproval?: import("./checkpoint-store.js").RealApprovalGateBit,
612
+ /** #144: the matched-but-outranked persisted rule (the surviving ask's `persistedRuleShadowed`)
613
+ * — threaded so the park mint's risk descriptor carries the disclosure on the durable route. */
614
+ shadowedRule?: string) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
612
615
  /**
613
616
  * design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
614
617
  * before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
@@ -728,10 +731,14 @@ export interface ToolGateInput {
728
731
  * · the reserved question tool and a call MARKED unresolvable are excluded for the same reason the
729
732
  * classifier excludes them: both contracts require that no synchronous decision-maker stands between
730
733
  * the ask and the park / content route, and this lane is one.
731
- * What it DOES consume is the egress and irreversibility/shellGate tightens and unmarked bare asks —
732
- * deliberately, because a deployment forcing shell classification is exactly where the feature is for.
733
- * Consuming such an ask also skips the park it would have minted; that IS what a standing approval
734
- * means, and the first bullet is what keeps an integrity ask out of that set.
734
+ * · #144 (ruled): a MANDATED ask is never consumed an operator's shellGate:"always" tier and a
735
+ * tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
736
+ * ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
737
+ * but cannot clear, the surviving ask discloses it (message + `persistedRuleShadowed`).
738
+ * What it DOES consume is the classify-DOCTRINE shell ask (the coarse tier "maybe" — the
739
+ * don't-ask-again main case this feature exists for) and unmarked bare asks. Consuming such an ask
740
+ * also skips the park it would have minted; that IS what a standing approval means, and the
741
+ * bullets above are what keep integrity/hook/mandated asks out of that set.
735
742
  */
736
743
  persistedRules?: {
737
744
  /** The canonical text of the rule that admits this call, or `undefined`. Must not throw: a store that
@@ -315,6 +315,15 @@ export async function runToolGate(input) {
315
315
  currentInput = policyRewrite;
316
316
  req.args = policyRewrite;
317
317
  }
318
+ const persistedRuleMandate = input.egress === true
319
+ ? "tool_marks"
320
+ : input.shellGated === true
321
+ ? input.irreversibility === "always"
322
+ ? "operator_always"
323
+ : undefined
324
+ : input.irreversibility === "always" || input.irreversibility === "maybe"
325
+ ? "tool_marks"
326
+ : undefined;
318
327
  if (input.persistedRules &&
319
328
  !orgRealApprovalRequired &&
320
329
  decision.action === "ask" &&
@@ -323,7 +332,7 @@ export async function runToolGate(input) {
323
332
  req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
324
333
  input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
325
334
  const hit = await input.persistedRules.admits(req).catch(() => undefined);
326
- if (hit !== undefined) {
335
+ if (hit !== undefined && persistedRuleMandate === undefined) {
327
336
  decision = {
328
337
  action: "allow",
329
338
  message: `a persisted allow rule (${hit}) covers this call`,
@@ -332,6 +341,14 @@ export async function runToolGate(input) {
332
341
  };
333
342
  await notifier.notifyAsync(() => input.persistedRules?.onResolved?.({ toolName: req.toolName, toolCallId, rule: hit }), "toolGate.persistedRuleResolved");
334
343
  }
344
+ else if (hit !== undefined) {
345
+ const mandateNoun = persistedRuleMandate === "operator_always" ? "this deployment mandates per-call confirmation for shell commands (shellGate: always)" : "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)";
346
+ decision = {
347
+ ...decision,
348
+ persistedRuleShadowed: hit,
349
+ message: `${decision.message !== undefined ? `${decision.message} ` : ""}(a persisted allow rule (${hit}) matches this call but does not clear the ask — ${mandateNoun})`,
350
+ };
351
+ }
335
352
  }
336
353
  if (input.autoMode &&
337
354
  !orgRealApprovalRequired &&
@@ -390,7 +407,7 @@ export async function runToolGate(input) {
390
407
  ? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
391
408
  : undefined;
392
409
  if (suspendAsk && decision.action === "ask") {
393
- const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision));
410
+ const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined);
394
411
  if (suspended) {
395
412
  if ("parkFailed" in suspended)
396
413
  parkFailed = suspended.parkFailed;
@@ -409,7 +426,7 @@ export async function runToolGate(input) {
409
426
  req.args = outcome.presentedInput;
410
427
  }
411
428
  if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
412
- const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision));
429
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined);
413
430
  if (suspended) {
414
431
  if ("parkFailed" in suspended)
415
432
  parkFailed = suspended.parkFailed;
@@ -437,7 +454,7 @@ export async function runToolGate(input) {
437
454
  const resolved = await resolveAsk(decision, req);
438
455
  decision = resolved;
439
456
  if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
440
- const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve));
457
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined);
441
458
  if (suspended) {
442
459
  if ("parkFailed" in suspended)
443
460
  parkFailed = suspended.parkFailed;
@@ -656,16 +656,18 @@ export class MemoryEngine {
656
656
  const indexNow = readSafe(pollutedIndexPath);
657
657
  if (indexNow === undefined || indexNow === handle.indexText)
658
658
  return;
659
+ let indexCaptureLanded = false;
659
660
  try {
660
661
  const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-polluted-${MEMORY_INDEX_FILENAME}`);
661
662
  ensureDirExists(dirname(dest));
662
- writeFileSync(dest, indexNow, "utf8");
663
+ writeFileSync(dest, indexNow, { encoding: "utf8", flag: "wx" });
664
+ indexCaptureLanded = true;
663
665
  }
664
666
  catch {
665
667
  }
666
668
  try {
667
669
  writeFileNoFollow(pollutedIndexPath, handle.indexText);
668
- report.warnings.push("memory index restored to its pre-session state — this session's index additions were not retained (session polluted; the removed text was captured to quarantine)");
670
+ report.warnings.push(`memory index restored to its pre-session state — this session's index additions were not retained (session polluted; ${indexCaptureLanded ? "the removed text was captured to quarantine" : "the quarantine capture did NOT land — the removed text is gone"})`);
669
671
  }
670
672
  catch (err) {
671
673
  report.warnings.push(`memory index could NOT be restored to its pre-session state: ${err instanceof Error ? err.message : String(err)}`);
@@ -1225,7 +1227,20 @@ export class MemoryEngine {
1225
1227
  try {
1226
1228
  const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-${MEMORY_INDEX_FILENAME}`);
1227
1229
  ensureDirExists(dirname(dest));
1228
- writeFileSync(dest, text, "utf8");
1230
+ let landed = false;
1231
+ for (let attempt = 0; !landed && attempt < 10; attempt++) {
1232
+ const candidate = attempt === 0 ? dest : `${dest}.${attempt}`;
1233
+ try {
1234
+ writeFileSync(candidate, text, { encoding: "utf8", flag: "wx" });
1235
+ landed = true;
1236
+ }
1237
+ catch (err) {
1238
+ if (!(err instanceof Error && "code" in err && err.code === "EEXIST"))
1239
+ throw err;
1240
+ }
1241
+ }
1242
+ if (!landed)
1243
+ writeFileSync(`${dest}.${process.pid}`, text, { encoding: "utf8", flag: "wx" });
1229
1244
  captured = true;
1230
1245
  }
1231
1246
  catch (err) {
@@ -112,6 +112,15 @@ export declare const ORG_ADJUDICATION_TIMEOUT_MS = 15000;
112
112
  export declare function settleOrgVerdictWithin<T>(p: Promise<T>, fallback: T, opts: {
113
113
  signal?: AbortSignal;
114
114
  timeoutMs: number;
115
+ /**
116
+ * backlog #136④ — which arm WON, reported at the moment it won. A consumer that wants to tell a
117
+ * cancelled wait from an unreadable provider cannot get that by sampling `signal.aborted` after
118
+ * the await: a cancellation queued between the settlement and the continuation reads identically
119
+ * to one that actually ended the wait, and the operator-facing account would name the wrong
120
+ * cause. Called at most once, before `p` resolves, and only for the two non-provider arms —
121
+ * absent call ⇒ the awaited promise itself settled.
122
+ */
123
+ onFallback?: (cause: "aborted" | "timeout") => void;
115
124
  }): Promise<T>;
116
125
  /** The `decisionReason` of a decision an ORG RULE produced (a deny, or a non-dismissable ask). Same
117
126
  * single-spelling contract as {@link ORG_UNAVAILABLE_DECISION_REASON}. */
@@ -11,22 +11,29 @@ export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
11
11
  export function settleOrgVerdictWithin(p, fallback, opts) {
12
12
  return new Promise((resolve) => {
13
13
  let settled = false;
14
- const finish = (v) => {
14
+ const finish = (v, cause) => {
15
15
  if (settled)
16
16
  return;
17
17
  settled = true;
18
18
  clearTimeout(timer);
19
19
  opts.signal?.removeEventListener("abort", onAbort);
20
+ if (cause !== undefined) {
21
+ try {
22
+ opts.onFallback?.(cause);
23
+ }
24
+ catch {
25
+ }
26
+ }
20
27
  resolve(v);
21
28
  };
22
- const onAbort = () => finish(fallback);
23
- const timer = setTimeout(() => finish(fallback), opts.timeoutMs);
29
+ const onAbort = () => finish(fallback, "aborted");
30
+ const timer = setTimeout(() => finish(fallback, "timeout"), opts.timeoutMs);
24
31
  if (opts.signal?.aborted === true) {
25
- finish(fallback);
32
+ finish(fallback, "aborted");
26
33
  return;
27
34
  }
28
35
  opts.signal?.addEventListener("abort", onAbort);
29
- p.then(finish, () => finish(fallback));
36
+ p.then((v) => finish(v), () => finish(fallback));
30
37
  });
31
38
  }
32
39
  export const ORG_RULE_DECISION_REASON = "org_rule";
@@ -28,6 +28,8 @@ import { type WiringManifest } from "../wiring-manifest.js";
28
28
  import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
29
29
  import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskLimits, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
30
30
  import type { RepairBundle } from "../../agents/repair-loop.js";
31
+ /** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
32
+ export declare function __resetMaterializeEnvAnnouncements(): void;
31
33
  /**
32
34
  * design/164 — validate `TaskSpec.limits` at the door and return it unchanged.
33
35
  *
@@ -89,6 +89,10 @@ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOI
89
89
  import { boundInputHashOf } from "../canonical-json.js";
90
90
  import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
91
91
  import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
92
+ const announcedMaterializeEnv = new Set();
93
+ export function __resetMaterializeEnvAnnouncements() {
94
+ announcedMaterializeEnv.clear();
95
+ }
92
96
  const RECONCILE_MAX_RETRIES = 3;
93
97
  const DEFAULT_MAX_SUSPENDS = 5;
94
98
  const TASK_LIMIT_KEY_DICT = {
@@ -2572,6 +2576,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2572
2576
  toolsDeltaRef = { pending: [], pendingRemoved: [], pendingReadded: [], pendingFailed: failedMcpServers };
2573
2577
  }
2574
2578
  const listingRideRef = {};
2579
+ {
2580
+ const raw = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2581
+ if (raw !== undefined && raw !== "swap" && raw !== "static" && deferred.size === 0) {
2582
+ const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(raw)} is not "swap" or "static" — inert on this task (no deferred tools), but a deferring task WITHOUT an explicit spec strategy will refuse to prepare under it (an explicit legal spec outranks and discards it, loudly). Fix or unset the flag.`;
2583
+ if (!announcedMaterializeEnv.has(line)) {
2584
+ announcedMaterializeEnv.add(line);
2585
+ console.warn(line);
2586
+ }
2587
+ }
2588
+ }
2575
2589
  if (deferred.size > 0) {
2576
2590
  if (deferred.has(TOOL_SEARCH_NAME) || tools.some((t) => t.name === TOOL_SEARCH_NAME)) {
2577
2591
  const e = new Error(`Tool name "${TOOL_SEARCH_NAME}" is reserved when deferred tools are present.`);
@@ -2579,12 +2593,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2579
2593
  throw e;
2580
2594
  }
2581
2595
  const registry = buildDeferredRegistry(deferred, tools);
2582
- const envStrategy = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2583
- if (envStrategy !== undefined && envStrategy !== "swap" && envStrategy !== "static") {
2584
- const e = new Error(`SEMA_TOOL_MATERIALIZE_STRATEGY must be "swap" or "static" (got ${JSON.stringify(envStrategy)}).`);
2596
+ const rawEnvStrategy = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2597
+ const envStrategyInvalid = rawEnvStrategy !== undefined && rawEnvStrategy !== "swap" && rawEnvStrategy !== "static";
2598
+ if (envStrategyInvalid && spec.toolMaterializeStrategy === undefined) {
2599
+ const e = new Error(`SEMA_TOOL_MATERIALIZE_STRATEGY must be "swap" or "static" (got ${JSON.stringify(rawEnvStrategy)}).`);
2585
2600
  e.code = "config.tool_materialize_invalid";
2586
2601
  throw e;
2587
2602
  }
2603
+ if (envStrategyInvalid) {
2604
+ const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(rawEnvStrategy)} was ignored — not "swap" or "static", and the task spec pins toolMaterializeStrategy=${JSON.stringify(spec.toolMaterializeStrategy)} which outranks it. Fix or unset the env flag.`;
2605
+ if (!announcedMaterializeEnv.has(line)) {
2606
+ announcedMaterializeEnv.add(line);
2607
+ console.warn(line);
2608
+ }
2609
+ }
2610
+ const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
2588
2611
  const requestedStrategy = spec.toolMaterializeStrategy ?? envStrategy ?? "swap";
2589
2612
  const laneDegrade = requestedStrategy === "static" && spec.deferSelfResolve === false;
2590
2613
  const materializeStatic = requestedStrategy === "static" && !laneDegrade;
@@ -3273,6 +3296,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3273
3296
  ...askSourceIdentity(),
3274
3297
  ...riskAxesOf(creq.toolName),
3275
3298
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3299
+ ...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
3276
3300
  }, onAskOf, csignal ?? abortController.signal);
3277
3301
  if (rr.action !== "allow")
3278
3302
  return rr;
@@ -3357,6 +3381,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3357
3381
  ...askSourceIdentity(),
3358
3382
  ...riskAxesOf(creq.toolName),
3359
3383
  ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3384
+ ...(first.action === "ask" && first.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: first.persistedRuleShadowed } : {}),
3360
3385
  }, pc.onAsk, csignal ?? abortController.signal);
3361
3386
  const askWaitMs = Math.max(0, now() - askT0);
3362
3387
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -3443,6 +3468,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3443
3468
  ...askSourceIdentity(),
3444
3469
  ...riskAxesOf(creq.toolName),
3445
3470
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3471
+ ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3446
3472
  }, pc.onAsk, csignal ?? abortController.signal);
3447
3473
  const askWaitMs = Math.max(0, now() - askT0);
3448
3474
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -3755,6 +3781,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3755
3781
  ...askSourceIdentity(),
3756
3782
  ...riskAxesOf(req.toolName),
3757
3783
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3784
+ ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3758
3785
  }, onAsk, abortController.signal);
3759
3786
  const waitMs = Math.max(0, now() - t0);
3760
3787
  if (resolved.approverUnavailable !== true) {
@@ -4236,7 +4263,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4236
4263
  }
4237
4264
  };
4238
4265
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
4239
- ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval) => {
4266
+ ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule) => {
4240
4267
  const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
4241
4268
  if (syncFirstEligible &&
4242
4269
  runtimeCaps?.forceDurableGate !== true &&
@@ -4341,6 +4368,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4341
4368
  toolName: req.toolName,
4342
4369
  args: parkedArgs,
4343
4370
  safety,
4371
+ ...(shadowedRule !== undefined ? { shadowedRule } : {}),
4344
4372
  shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
4345
4373
  ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
4346
4374
  });
@@ -1,6 +1,6 @@
1
1
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
2
2
  import { snapshotActorAssertion } from "../../internal/llm.js";
3
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
3
+ import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
4
4
  import { engineVersion } from "../version.js";
5
5
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
6
6
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
@@ -71,6 +71,7 @@ const STOP_HOOK_BLOCK_CAP = 8;
71
71
  const COMPACTION_REGROWTH_FACTOR = 1.5;
72
72
  const COMPACTION_FREED_EPSILON = 256;
73
73
  const BATCH_RESPONSE_MAX_CHARS = 500;
74
+ const ORG_DISCLOSURE_MAX_CHARS = 600;
74
75
  function batchResponseDigest(result) {
75
76
  const content = result !== null && typeof result === "object" ? result.content : result;
76
77
  if (content === undefined || content === null)
@@ -3702,7 +3703,7 @@ export class Runner {
3702
3703
  }
3703
3704
  }
3704
3705
  if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
3705
- throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`);
3706
+ throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`, { reason: "version_newer" });
3706
3707
  }
3707
3708
  const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
3708
3709
  const preCasBitWellFormed = preCasGateBit !== undefined &&
@@ -3713,11 +3714,11 @@ export class Runner {
3713
3714
  if (checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? !preCasBitWellFormed : preCasGateBit !== undefined) {
3714
3715
  throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
3715
3716
  ? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed non-budgetable realApproval gate bit (origin org_rule/org_unavailable/policy) on an irreversible_ask gate — this row does not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
3716
- : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`);
3717
+ : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? "real_approval_damaged" : "real_approval_forged" });
3717
3718
  }
3718
3719
  if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
3719
3720
  this.deps.permissionRuleOrg === undefined) {
3720
- throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`);
3721
+ throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
3721
3722
  }
3722
3723
  const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
3723
3724
  if (retiredWalltimeTotal !== undefined) {
@@ -3729,7 +3730,7 @@ export class Runner {
3729
3730
  throw new CheckpointError("checkpoint.resume_aborted", "the resume was handed an ALREADY-ABORTED signal — refusing to consume the approval on a leg that cannot run it (the checkpoint stays pending and is resumable with a live signal)");
3730
3731
  }
3731
3732
  if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
3732
- throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env");
3733
+ throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env", { reason: "env_factory_missing" });
3733
3734
  }
3734
3735
  if (cp.state.inheritedGate?.requiresParentConstraint === true) {
3735
3736
  const supplied = internals?.inheritedGate?.parentConstraints?.length ?? 0;
@@ -3745,6 +3746,10 @@ export class Runner {
3745
3746
  "re-supply would run the resumed leg under a different ancestor chain than it suspended with; " +
3746
3747
  "rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
3747
3748
  }
3749
+ if (checkpointVersionOf(cp) >= F012_CHECKPOINT_VERSION &&
3750
+ (cp.state.inheritedGate.constraintChain === undefined || cp.state.inheritedGate.constraintDigest === undefined)) {
3751
+ throw new CheckpointError("checkpoint.invalid_outcome", `a v${checkpointVersionOf(cp)} checkpoint that requires parent constraints must carry BOTH the frozen constraintChain and its constraintDigest (they are minted in one write with the version stamp) — this row carries neither or only one; refusing to fall back to the count-only contract on a damaged row (corruption / downgrade guard), the checkpoint stays pending`, { reason: "constraint_chain_missing" });
3752
+ }
3748
3753
  const expectedDigest = cp.state.inheritedGate.constraintDigest;
3749
3754
  if (expectedDigest !== undefined) {
3750
3755
  const persistedChain = cp.state.inheritedGate.constraintChain;
@@ -4037,16 +4042,28 @@ export class Runner {
4037
4042
  const orgVerdict = prepared.permissionRuleOrg
4038
4043
  .adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
4039
4044
  .catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
4040
- const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
4045
+ let orgWaitCancelled = false;
4046
+ const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, {
4047
+ signal: prepared.abortController.signal,
4048
+ timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS,
4049
+ onFallback: (cause) => {
4050
+ orgWaitCancelled = cause === "aborted";
4051
+ },
4052
+ });
4041
4053
  const blocked = org.status === "unavailable"
4042
4054
  ? gateRealApproval?.origin === "org_unavailable"
4043
4055
  ? undefined
4044
- : "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
4056
+ : orgWaitCancelled
4057
+ ? "this deployment is org-governed and the task was cancelled before the organization policy snapshot could be adjudicated"
4058
+ : "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
4045
4059
  : org.verdict?.behavior === "deny"
4046
4060
  ? `an organization policy rule (${org.verdict.rule}) denies it`
4047
4061
  : undefined;
4048
4062
  if (blocked !== undefined) {
4049
- const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}. This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
4063
+ const orgDisclosures = org.status === "unavailable" && org.disclosures.length > 0
4064
+ ? ` Governance disclosures: ${inlineUntrusted(org.disclosures.join("; "), ORG_DISCLOSURE_MAX_CHARS)}.`
4065
+ : "";
4066
+ const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
4050
4067
  emitEnd(true, { content: orgDenial });
4051
4068
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
4052
4069
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -167,6 +167,13 @@ export type PermissionResult = {
167
167
  message?: string;
168
168
  decisionReason?: DecisionReason;
169
169
  requiresRealApproval?: boolean;
170
+ /** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
171
+ * ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
172
+ * egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
173
+ * the classifier's questions, never a mandated one". Carries the matched rule text so a
174
+ * consumer (approval card, wire frame) can tell the person their rule is alive, just outranked.
175
+ * Absent ⇒ no rule matched, or the ask was cleared normally. */
176
+ persistedRuleShadowed?: string;
170
177
  } | {
171
178
  action: "deny";
172
179
  updatedInput?: unknown;
@@ -601,6 +608,10 @@ export interface AskDelegationProvenance {
601
608
  /** The structured context an `onAsk` approver receives for an `ask` decision (design/37). */
602
609
  export interface AskRequest {
603
610
  toolName: string;
611
+ /** #144: a persisted allow rule MATCHED this call but could not clear the ask (mandated — see
612
+ * {@link PermissionResult}'s ask arm). The matched rule text, so the approval card renders "your
613
+ * rule is alive, just outranked" instead of leaving the person to regex the message prose. */
614
+ persistedRuleShadowed?: string;
604
615
  toolCallId: string;
605
616
  /** The (post-rewrite) args the tool would run with. */
606
617
  args: unknown;
package/dist/index.d.ts CHANGED
@@ -88,7 +88,7 @@ export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW,
88
88
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
89
89
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
90
90
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
91
- export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type RealApprovalGateBit, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
91
+ export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type RealApprovalGateBit, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
92
92
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, type UsageWindow, type UsageWindowStore, type UsageWindowReading, type UsageWindowRecord, type UsageSlot, type UsageBucketRow, } from "./core/usage-window-store.js";
93
93
  export { FileUsageWindowStore } from "./stores/file/usage-window-store.js";
94
94
  export { ENV_LIFETIME_SUSPEND_MARGIN_MS, USAGE_WINDOW_REAP_MARGIN_MS } from "./core/runner/prepare-task.js";
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW,
69
69
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
70
70
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
71
71
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, } from "./core/tool-result-store.js";
72
- export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
72
+ export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
73
73
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, } from "./core/usage-window-store.js";
74
74
  export { FileUsageWindowStore } from "./stores/file/usage-window-store.js";
75
75
  export { ENV_LIFETIME_SUSPEND_MARGIN_MS, USAGE_WINDOW_REAP_MARGIN_MS } from "./core/runner/prepare-task.js";
@@ -24,9 +24,29 @@
24
24
  * ⑥ the marker is atomically rewritten into its PERMANENT terminal record carrying the immutable
25
25
  * report. Never deleted; a re-run short-circuits to it.
26
26
  *
27
- * FREEZE (design/183 §8 load-bearing premise): from the moment ② lands, the whole root is read-only
28
- * for everyone but this machine live writers are excluded by the locks, and across crashes every
29
- * file store constructor refuses the in-flight marker (invariant I6, `assertAdoptionBootGate`).
27
+ * FREEZE (design/183 §8 load-bearing premise): from the moment ② lands, the root is closed to new
28
+ * writers and it is worth being exact about which mechanism covers what, because the two halves
29
+ * meet at a seam rather than overlapping (backlog #136⑧):
30
+ * · the LOCKS cover the boot-locked shape: this machine holds `LOCK` (the engine's own boot lock,
31
+ * taken by `FileStorageBackend`) and `ADOPTION-LOCK` for the whole arc, so a live backend refuses
32
+ * the adoption instead of interleaving with it, and a second adoption refuses too;
33
+ * · invariant I6 (`assertAdoptionBootGate`) covers CONSTRUCTION over a DATA-ROOT ANCHOR: while the
34
+ * marker is in flight, a store constructor handed the data root refuses it — including across a
35
+ * crash, when no lock survives.
36
+ * What NEITHER covers, enumerated rather than implied:
37
+ * · an ALREADY-constructed instance over this root through a path that does not hold `LOCK` (the
38
+ * independent store faces — `new FileMemoryStore(root)`, the mailbox family). It keeps its open
39
+ * handles, and I6 is a construction-time gate, not a write-time one;
40
+ * · a NEW construction whose anchor is not the data root. Two faces take a directory that need not
41
+ * BE the data root — `FilePermissionRuleStoreProvider` (the rule bucket) and
42
+ * `createFileTaskListStore` (the document's own directory) — so each takes its root anchor as an
43
+ * explicit option, and a caller that omits it constructs unGATED during a freeze. That default is
44
+ * what this arc's own nested rule leg rides (it must reach the bucket while the marker is in
45
+ * flight); a deployment wanting the gate passes the anchor. Both arms are pinned.
46
+ * That is the same boundary the file family draws everywhere else (one live writer per data root is
47
+ * the deployment's contract, mechanically enforced only for the backend-factory shape), and it is why
48
+ * "stop the engine before adopting" remains an operational instruction and not merely a machine-
49
+ * checked one.
30
50
  * Idempotent resume ("re-run completes the unfinished legs") is only sound because no per-store row
31
51
  * moves underneath it; the freeze is the premise of idempotence, not an implementation detail.
32
52
  *
@@ -249,6 +249,7 @@ const ZERO_ACTION_STORES = [
249
249
  "tool-result",
250
250
  "file-snapshot",
251
251
  "memory",
252
+ "memory-engine",
252
253
  "task-list",
253
254
  "mailbox",
254
255
  "workflow-run",
@@ -5,8 +5,11 @@
5
5
  * Two shapes live in `<dataRoot>/adoption.json`:
6
6
  * - the IN-FLIGHT marker (`marker`): a monotonically-advanced `phase` plus per-leg completion bits
7
7
  * for the one leg class without a self-attesting completion predicate (row rewrites). While this
8
- * shape is on disk the root is FROZEN: every file store constructor refuses to build over it
9
- * ({@link assertAdoptionBootGate} — invariant I6), so no writer can serve a half-migrated root.
8
+ * shape is on disk the root is FROZEN: every file store constructor ANCHORED TO THE DATA ROOT
9
+ * refuses to build over it ({@link assertAdoptionBootGate} — invariant I6). Coverage is stated
10
+ * precisely at the gate's own docstring: two faces whose constructor argument is not the root
11
+ * take an optional `dataRoot` anchor, and a caller omitting it is outside the gate (the seam is
12
+ * documented and pinned where those faces live).
10
13
  * - the PERMANENT terminal record (`adopted`): never deleted. It is the idempotence anchor (a re-run
11
14
  * with the same (from → toPrincipal) short-circuits to the recorded receipt) and it carries the
12
15
  * IMMUTABLE report — the byte-stable half of every receipt.
@@ -186,17 +189,29 @@ export declare function readRootAdoptionFile(root: string): RootAdoptionFile | u
186
189
  /** Atomically publish the root marker (write-temp → fsync → rename, same discipline as every store). */
187
190
  export declare function writeRootAdoptionFile(root: string, content: RootAdoptionFile): void;
188
191
  /**
189
- * Invariant I6 (design/183 §3.1) — the adoption BOOT GATE, called by every file store constructor:
190
- * an in-flight root marker means the root is mid-adoption (possibly after a crash), and serving a
191
- * half-migrated root is refused LOUDLY. The refusal names the in-flight arc and the way forward
192
- * (resume `adoptLocalDataRoot` to completion). A terminal record passes: a completed adoption is a
193
- * normal, readable root. A corrupt marker throws (fail-closed, see {@link readRootAdoptionFile}).
192
+ * Invariant I6 (design/183 §3.1) — the adoption BOOT GATE: an in-flight root marker means the root is
193
+ * mid-adoption (possibly after a crash), and serving a half-migrated root is refused LOUDLY. The
194
+ * refusal names the in-flight arc and the way forward (resume `adoptLocalDataRoot` to completion). A
195
+ * terminal record passes: a completed adoption is a normal, readable root. A corrupt marker throws
196
+ * (fail-closed, see {@link readRootAdoptionFile}).
197
+ *
198
+ * WHO calls it, precisely: every store constructor whose argument IS the data root (that is most of
199
+ * the family — they derive their own subdirectory from it). The two faces whose argument is a
200
+ * caller-chosen directory instead — `FilePermissionRuleStoreProvider` (a rule bucket) and
201
+ * `createFileTaskListStore` (the document's directory) — cannot anchor on their argument, so each
202
+ * takes the data root as an explicit option and is gated only when given one. `root` here is always
203
+ * the ANCHOR, never "wherever this store keeps its bytes": passing a subdirectory reads a marker that
204
+ * is not there and passes unconditionally.
194
205
  *
195
206
  * This gate is what turns "the engine must be stopped during adoption" from an operational assumption
196
207
  * into a machine-checked invariant across crashes: the adoption's own locks die with its process, but
197
- * the marker (and this gate) survive. Boundary, stated honestly: the gate fires at CONSTRUCTION time.
198
- * A store instance constructed BEFORE the marker landed in another OS process is outside it cross-
199
- * process sharing of one data dir is the file family's documented UNSUPPORTED shape (task-list F-14,
200
- * mailbox RB-249); adoption adds no new promise there.
208
+ * the marker (and this gate) survive. Boundary, stated honestly: the gate fires at CONSTRUCTION time,
209
+ * and it is the ONLY thing this function does it never reaches an object that already exists. So a
210
+ * store instance constructed BEFORE the marker landed keeps writing for as long as it is held, in
211
+ * ANOTHER OS process (cross-process sharing of one data dir is the file family's documented
212
+ * UNSUPPORTED shape — task-list F-14, mailbox RB-249) and, symmetrically, in THIS one (an independent
213
+ * store face built before the adoption started; the arc's `LOCK` acquisition excludes a live
214
+ * `FileStorageBackend`, which is the only shape that takes that lock). Adoption adds no new promise on
215
+ * either side of that seam — see the freeze note on `adoptLocalDataRoot` for the full division.
201
216
  */
202
217
  export declare function assertAdoptionBootGate(root: string, storeName: string): void;
@@ -157,12 +157,26 @@ declare class FilePermissionRuleStore implements WritablePermissionRuleStore {
157
157
  *
158
158
  * An absent principal resolves to a store that reads zero rules and refuses to write — an unauthenticated
159
159
  * task does not share anyone's bucket.
160
+ *
161
+ * ROOT FREEZE (design/183 invariant I6, backlog #136⑤): every sibling file store gates construction on
162
+ * the DATA ROOT's adoption marker, but this provider is built over the RULE BUCKET directory, which is
163
+ * a caller-chosen path that need not sit under (or equal) the data root — so `dir` is not a usable
164
+ * anchor for that gate and the bucket was writable straight through a root freeze. The anchor is
165
+ * therefore taken EXPLICITLY, via `opts.dataRoot`: supply it whenever the rule bucket belongs to a data
166
+ * root that `adoptLocalDataRoot` may adopt, and construction is refused while that root is mid-adoption
167
+ * exactly like `FileStorageBackend`'s. Omitting it keeps the previous behavior (no root gate) — which is
168
+ * also the EXEMPTION the adoption machine itself rides: its nested design/182 rule leg constructs this
169
+ * provider without a `dataRoot` precisely because it runs while the marker is in flight, and the
170
+ * bucket-level marker (read in `forPrincipal`/`forLocalOwner`) is what governs writes inside that arc.
160
171
  */
161
172
  export declare class FilePermissionRuleStoreProvider implements PermissionRuleStoreProvider {
162
173
  private readonly dir;
163
174
  private readonly onError?;
164
175
  private lock;
165
- constructor(dir: string, onError?: ((message: string) => void) | undefined);
176
+ constructor(dir: string, onError?: ((message: string) => void) | undefined, opts?: {
177
+ /** The data root whose adoption marker gates construction (design/183 I6). Absent ⇒ no root gate. */
178
+ dataRoot?: string;
179
+ });
166
180
  /** Take the writer lock, once, on first use of a write face. Reading never calls this. */
167
181
  private acquireWriteLock;
168
182
  forPrincipal(principal: string | undefined): PermissionRuleStore;
@@ -4,6 +4,7 @@ import { createHash, randomBytes } from "node:crypto";
4
4
  import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, foldDelta, } from "../../core/permission-rule-store.js";
5
5
  import { canonicalize } from "../../core/canonical-json.js";
6
6
  import { BootLock } from "./fs-atomic.js";
7
+ import { assertAdoptionBootGate } from "./adoption/marker.js";
7
8
  const LOCAL_OWNER_FILE = "local-owner.json";
8
9
  const ADOPTION_FILE = "PERMISSION-RULES-ADOPTION.json";
9
10
  export function readRuleAdoptionFile(dir) {
@@ -298,9 +299,11 @@ export class FilePermissionRuleStoreProvider {
298
299
  dir;
299
300
  onError;
300
301
  lock;
301
- constructor(dir, onError) {
302
+ constructor(dir, onError, opts) {
302
303
  this.dir = dir;
303
304
  this.onError = onError;
305
+ if (opts?.dataRoot !== undefined)
306
+ assertAdoptionBootGate(opts.dataRoot, "FilePermissionRuleStoreProvider");
304
307
  }
305
308
  acquireWriteLock = () => {
306
309
  if (this.lock !== undefined)
@@ -1,2 +1,16 @@
1
1
  import type { TaskListStore } from "../../tools/task-list.js";
2
- export declare function createFileTaskListStore(root: string): TaskListStore;
2
+ export declare function createFileTaskListStore(root: string, opts?: {
3
+ /**
4
+ * The DATA ROOT whose adoption marker gates construction (design/183 I6). Most of the file family
5
+ * takes the data root and derives `join(root, "<family>")` itself; this factory is one of the TWO
6
+ * faces that instead take a caller-chosen directory (the other being
7
+ * `FilePermissionRuleStoreProvider`, which takes a rule bucket) — here, the document's own
8
+ * directory. So when a deployment hands in a SUBDIRECTORY the I6 check would look for the marker
9
+ * inside that subdirectory, find nothing, and pass unconditionally: a gate that is always open
10
+ * (backlog #136⑦). Supply the data root here and the gate reads the anchor the root-taking faces
11
+ * read. Absent ⇒ the directory argument doubles as the anchor, which is exact when it IS the data
12
+ * root (the conventional wiring) and is the previous behavior otherwise; the adoption freeze note
13
+ * on `adoptLocalDataRoot` lists that default among the seams it does not cover.
14
+ */
15
+ dataRoot?: string;
16
+ }): TaskListStore;
@@ -4,9 +4,9 @@ import { assertJsonMetadata, normalizeTaskShape } from "../../tools/task-list.js
4
4
  import { canonicalStoreKey, atomicWriteFile, ensureDir } from "./fs-atomic.js";
5
5
  import { assertAdoptionBootGate } from "./adoption/marker.js";
6
6
  const dirLocks = new Map();
7
- export function createFileTaskListStore(root) {
7
+ export function createFileTaskListStore(root, opts) {
8
8
  const dir = resolve(root);
9
- assertAdoptionBootGate(dir, "createFileTaskListStore");
9
+ assertAdoptionBootGate(opts?.dataRoot !== undefined ? resolve(opts.dataRoot) : dir, "createFileTaskListStore");
10
10
  const tmpDir = join(dir, "tmp");
11
11
  const path = join(dir, "task-list.json");
12
12
  ensureDir(dir);
@@ -89,12 +89,19 @@ export function envErrorDetail(message) {
89
89
  }
90
90
  const announcedTimeoutConfig = new Set();
91
91
  function announceDiscardedTimeout(knob, raw, usedMs) {
92
- if (!Number.isFinite(raw) || raw <= 0)
93
- return;
94
- const defect = bashTimeoutDefect(Math.floor(raw));
95
- if (defect === undefined)
92
+ if (typeof raw === "string" && raw === "")
96
93
  return;
97
- const line = `${knob}=${raw} was ignored it ${defect}. Using ${usedMs}ms instead.`;
94
+ const n = typeof raw === "number" ? raw : Number(raw);
95
+ let line;
96
+ if (!Number.isFinite(n) || n <= 0) {
97
+ line = `${knob}=${String(raw)} was ignored — not a positive finite millisecond count. Using ${usedMs}ms instead.`;
98
+ }
99
+ else {
100
+ const defect = bashTimeoutDefect(Math.floor(n));
101
+ if (defect === undefined)
102
+ return;
103
+ line = `${knob}=${String(raw)} was ignored — it ${defect}. Using ${usedMs}ms instead.`;
104
+ }
98
105
  if (announcedTimeoutConfig.has(line))
99
106
  return;
100
107
  announcedTimeoutConfig.add(line);
@@ -126,11 +133,20 @@ export function resolveBashTimeoutCaps(opts) {
126
133
  if (opts?.bashDefaultTimeoutMs !== undefined)
127
134
  announceDiscardedTimeout("bashDefaultTimeoutMs", opts.bashDefaultTimeoutMs, defaultMs);
128
135
  else if (process.env.BASH_DEFAULT_TIMEOUT_MS !== undefined)
129
- announceDiscardedTimeout("BASH_DEFAULT_TIMEOUT_MS", Number(process.env.BASH_DEFAULT_TIMEOUT_MS), defaultMs);
136
+ announceDiscardedTimeout("BASH_DEFAULT_TIMEOUT_MS", process.env.BASH_DEFAULT_TIMEOUT_MS, defaultMs);
130
137
  if (opts?.bashMaxTimeoutMs !== undefined)
131
138
  announceDiscardedTimeout("bashMaxTimeoutMs", opts.bashMaxTimeoutMs, maxMs);
132
139
  else if (process.env.BASH_MAX_TIMEOUT_MS !== undefined)
133
- announceDiscardedTimeout("BASH_MAX_TIMEOUT_MS", Number(process.env.BASH_MAX_TIMEOUT_MS), maxMs);
140
+ announceDiscardedTimeout("BASH_MAX_TIMEOUT_MS", process.env.BASH_MAX_TIMEOUT_MS, maxMs);
141
+ const requestedCap = validTimeoutMs(opts?.bashMaxTimeoutMs) ?? validTimeoutMs(Number(process.env.BASH_MAX_TIMEOUT_MS));
142
+ if (requestedCap !== undefined && requestedCap < defaultMs) {
143
+ const knob = opts?.bashMaxTimeoutMs !== undefined ? "bashMaxTimeoutMs" : "BASH_MAX_TIMEOUT_MS";
144
+ const line = `${knob}=${requestedCap} is below the resolved default budget (${defaultMs}ms) — the ceiling was raised to ${maxMs}ms (the default always fits under the cap).`;
145
+ if (!announcedTimeoutConfig.has(line)) {
146
+ announcedTimeoutConfig.add(line);
147
+ console.warn(line);
148
+ }
149
+ }
134
150
  return { defaultMs, maxMs };
135
151
  }
136
152
  export function __resetBashTimeoutAnnouncements() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.24.0",
3
+ "version": "5.25.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",