@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 +70 -0
- package/dist/core/checkpoint-store.d.ts +47 -8
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/hooks.d.ts +12 -5
- package/dist/core/hooks.js +21 -4
- package/dist/core/memory-engine/engine.js +18 -3
- package/dist/core/permission-rule-org.d.ts +9 -0
- package/dist/core/permission-rule-org.js +12 -5
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +32 -4
- package/dist/core/runner/runtask.js +25 -8
- package/dist/core/tool-policy.d.ts +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/stores/file/adoption/adopt.d.ts +23 -3
- package/dist/stores/file/adoption/adopt.js +1 -0
- package/dist/stores/file/adoption/marker.d.ts +26 -11
- package/dist/stores/file/permission-rule-store.d.ts +15 -1
- package/dist/stores/file/permission-rule-store.js +4 -1
- package/dist/stores/file/task-list-store.d.ts +15 -1
- package/dist/stores/file/task-list-store.js +2 -2
- package/dist/tools/fs/fs-shared.js +23 -7
- package/package.json +1 -1
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
|
|
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
|
|
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.
|
|
1467
|
-
*
|
|
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.
|
|
1576
|
-
*
|
|
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
|
};
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
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
|
package/dist/core/hooks.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
2583
|
-
|
|
2584
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
|
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
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
*
|
|
@@ -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
|
|
9
|
-
* ({@link assertAdoptionBootGate} — invariant I6)
|
|
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
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
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
|
|
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 (
|
|
93
|
-
return;
|
|
94
|
-
const defect = bashTimeoutDefect(Math.floor(raw));
|
|
95
|
-
if (defect === undefined)
|
|
92
|
+
if (typeof raw === "string" && raw === "")
|
|
96
93
|
return;
|
|
97
|
-
const
|
|
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",
|
|
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",
|
|
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() {
|