@sema-agent/core 7.1.0 → 7.3.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 +65 -0
- package/dist/agents/cross-session-envelope.d.ts +145 -0
- package/dist/agents/cross-session-envelope.js +195 -0
- package/dist/agents/cross-session-judge.d.ts +119 -0
- package/dist/agents/cross-session-judge.js +184 -0
- package/dist/agents/cross-session-ref.d.ts +52 -0
- package/dist/agents/cross-session-ref.js +64 -0
- package/dist/agents/list-agents-tool.d.ts +55 -0
- package/dist/agents/list-agents-tool.js +94 -0
- package/dist/agents/peer-admission.d.ts +17 -1
- package/dist/agents/peer-admission.js +19 -2
- package/dist/agents/peer-directory.d.ts +208 -0
- package/dist/agents/peer-directory.js +272 -0
- package/dist/agents/peer-session-drain.d.ts +159 -0
- package/dist/agents/peer-session-drain.js +245 -0
- package/dist/agents/send-message-tool.d.ts +44 -0
- package/dist/agents/send-message-tool.js +181 -16
- package/dist/agents/subagent-steps.d.ts +11 -0
- package/dist/agents/subagent-steps.js +27 -4
- package/dist/core/auto-mode-arming.d.ts +11 -0
- package/dist/core/auto-mode-arming.js +7 -1
- package/dist/core/auto-mode-prompt.d.ts +5 -0
- package/dist/core/auto-mode-prompt.js +2 -1
- package/dist/core/auto-mode-rebuild.d.ts +2 -1
- package/dist/core/auto-mode-rebuild.js +2 -0
- package/dist/core/checkpoint-store.d.ts +203 -3
- package/dist/core/checkpoint-store.js +60 -19
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/hooks.d.ts +15 -8
- package/dist/core/hooks.js +6 -3
- package/dist/core/mailbox-store.d.ts +89 -2
- package/dist/core/mailbox-store.js +77 -2
- package/dist/core/permission-rule-consent.d.ts +72 -23
- package/dist/core/permission-rule-consent.js +115 -26
- package/dist/core/permission-rule-model.d.ts +254 -51
- package/dist/core/permission-rule-model.js +316 -55
- package/dist/core/permission-rule-org.js +13 -6
- package/dist/core/remote-env.d.ts +8 -1
- package/dist/core/runner/assemble-result.js +2 -1
- package/dist/core/runner/prepare-task.d.ts +59 -1
- package/dist/core/runner/prepare-task.js +414 -149
- package/dist/core/runner/prepare-workspace-restore.d.ts +6 -1
- package/dist/core/runner/prepare-workspace-restore.js +2 -1
- package/dist/core/runner/runtask.js +16 -5
- package/dist/core/runner/tool-output-projection.js +1 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +23 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +157 -1
- package/dist/core/task-notification.d.ts +93 -5
- package/dist/core/task-notification.js +31 -4
- package/dist/core/tool-policy.d.ts +11 -0
- package/dist/core/types.d.ts +155 -21
- package/dist/core/untrusted-text.js +17 -1
- package/dist/core/wiring-manifest.d.ts +21 -0
- package/dist/core/wiring-manifest.js +1 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +13 -4
- package/dist/stores/cc/mailbox-store.d.ts +1 -1
- package/dist/stores/cc/mailbox-store.js +13 -0
- package/dist/stores/file/adoption/marker.d.ts +1 -1
- package/dist/stores/file/mailbox-store.d.ts +57 -0
- package/dist/stores/file/mailbox-store.js +369 -18
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +233 -1
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1220,13 +1220,14 @@ export interface ToolGateInput {
|
|
|
1220
1220
|
/**
|
|
1221
1221
|
* design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
|
|
1222
1222
|
* own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
|
|
1223
|
-
* gate itself never awaits on it — it hands the signal verbatim to the
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1223
|
+
* gate itself never awaits on it — it hands the signal verbatim to the four consuming seats
|
|
1224
|
+
* below (`adjudicate`, `resolveAsk`, `resolveContentAsk`, and — slice 2 — `suspendAsk`, whose
|
|
1225
|
+
* park closure composes it into the halt×park fence) as their optional trailing argument, and
|
|
1226
|
+
* the Runner's closures compose it with the run signal so a turn interrupt (bare halt /
|
|
1227
|
+
* steer-now) releases the wait — or, on the durable leg, makes the park concede. Absent (a host
|
|
1228
|
+
* driving the gate directly, or an unthreaded emit) ⇒ every seat is invoked exactly as before —
|
|
1229
|
+
* same arity, no trailing `undefined` (the threading is a conditional invocation) — and waits
|
|
1230
|
+
* stay bound to the run signal alone: the historical shape, byte-identical.
|
|
1230
1231
|
*/
|
|
1231
1232
|
callSignal?: AbortSignal;
|
|
1232
1233
|
/** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow).
|
|
@@ -1314,7 +1315,13 @@ export interface ToolGateInput {
|
|
|
1314
1315
|
* rule the lane would then refuse to honor. APPENDED at the tail for the reason its neighbour
|
|
1315
1316
|
* states — every parameter here is positional — and typed `boolean` so a mis-bind against the two
|
|
1316
1317
|
* trailing `string` seats is a compile error rather than a silent re-binding. */
|
|
1317
|
-
probeMandated?: boolean
|
|
1318
|
+
probeMandated?: boolean,
|
|
1319
|
+
/** design/384 slice 2 — {@link ToolGateInput.callSignal}, threaded VERBATIM as a CONDITIONAL
|
|
1320
|
+
* trailing argument (the slice-1 arity discipline: absent means the historical arity, never a
|
|
1321
|
+
* trailing `undefined`). The park closure composes it with the run signal into the halt×park
|
|
1322
|
+
* fence's cut signal; an implementation that ignores it keeps the run-level binding. APPENDED at
|
|
1323
|
+
* the tail like its neighbours, and typed `AbortSignal` so a mis-bind is a compile error. */
|
|
1324
|
+
callSignal?: AbortSignal) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
|
|
1318
1325
|
/**
|
|
1319
1326
|
* design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
|
|
1320
1327
|
* before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
|
package/dist/core/hooks.js
CHANGED
|
@@ -859,7 +859,8 @@ export async function runToolGate(input) {
|
|
|
859
859
|
? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
|
|
860
860
|
: undefined;
|
|
861
861
|
if (suspendAsk && decision.action === "ask") {
|
|
862
|
-
const
|
|
862
|
+
const parkArgs = [req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined];
|
|
863
|
+
const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
|
|
863
864
|
if (suspended) {
|
|
864
865
|
if ("parkFailed" in suspended)
|
|
865
866
|
parkFailed = suspended.parkFailed;
|
|
@@ -878,7 +879,8 @@ export async function runToolGate(input) {
|
|
|
878
879
|
req.args = outcome.presentedInput;
|
|
879
880
|
}
|
|
880
881
|
if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
|
|
881
|
-
const
|
|
882
|
+
const parkArgs = [req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined];
|
|
883
|
+
const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
|
|
882
884
|
if (suspended) {
|
|
883
885
|
if ("parkFailed" in suspended)
|
|
884
886
|
parkFailed = suspended.parkFailed;
|
|
@@ -910,7 +912,8 @@ export async function runToolGate(input) {
|
|
|
910
912
|
askDenyResolution = resolved.resolution;
|
|
911
913
|
decision = resolved;
|
|
912
914
|
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
913
|
-
const
|
|
915
|
+
const parkArgs = [req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined];
|
|
916
|
+
const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
|
|
914
917
|
if (suspended) {
|
|
915
918
|
if ("parkFailed" in suspended)
|
|
916
919
|
parkFailed = suspended.parkFailed;
|
|
@@ -12,7 +12,54 @@ export interface MailboxMessage {
|
|
|
12
12
|
* has other writers): such records deliver with an empty chain and are never retro-admitted —
|
|
13
13
|
* cross-engine records sit outside the guard's promise domain by ruling. */
|
|
14
14
|
hopChain?: string[];
|
|
15
|
+
/** design/385 — typed peer metadata the drain point's authoritative judgments read (from-mode parity,
|
|
16
|
+
* reply routing, admission sender key, record kind, the cross-principal trust anchors). Typed side
|
|
17
|
+
* channel, never model-facing text — encoding it into `content` would let text reach authority.
|
|
18
|
+
* ABSENT = a pre-385 or foreign record (same semantics as an absent `hopChain`): the drain treats it
|
|
19
|
+
* as foreign, never retro-admits it. Validated at `append` ({@link readMailboxPeerMeta}). */
|
|
20
|
+
peerMeta?: MailboxPeerMeta;
|
|
15
21
|
}
|
|
22
|
+
/** design/385 — the typed peer-metadata record on a parked message. ONE optional object, one-time
|
|
23
|
+
* contract extension; every field optional. Bundled backends persist it verbatim and hand back a
|
|
24
|
+
* detached copy. */
|
|
25
|
+
export interface MailboxPeerMeta {
|
|
26
|
+
/** The sending session's id (reply routing). */
|
|
27
|
+
fromSession?: string;
|
|
28
|
+
/** The sender's permission-mode class at send time — the drain judges parity against the RECEIVER. */
|
|
29
|
+
fromMode?: MailboxPeerFromMode;
|
|
30
|
+
/** The admission gate's sender key (rate/dedup axis). */
|
|
31
|
+
senderKey?: string;
|
|
32
|
+
/** Record kind: a peer message runs the parity judgment; notices bypass it and land on the notice face. */
|
|
33
|
+
kind?: MailboxPeerRecordKind;
|
|
34
|
+
/** Cross-principal delivery only — the SENDER's scope, the typed trust anchor for attribution. */
|
|
35
|
+
fromScope?: string;
|
|
36
|
+
/** Cross-principal delivery only — the delivery gate's receipt id (audit back-reference). */
|
|
37
|
+
gateReceiptId?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare const MAILBOX_PEER_FROM_MODES: readonly ["bypass", "prompting"];
|
|
40
|
+
export type MailboxPeerFromMode = (typeof MAILBOX_PEER_FROM_MODES)[number];
|
|
41
|
+
export declare const MAILBOX_PEER_RECORD_KINDS: readonly ["peer_message", "idle_notice", "delivery_notice"];
|
|
42
|
+
export type MailboxPeerRecordKind = (typeof MAILBOX_PEER_RECORD_KINDS)[number];
|
|
43
|
+
/** The `append` refusal code for a malformed `peerMeta` (design/385): a garbage record is refused up
|
|
44
|
+
* front with this code, never silently stripped or stored — a drain that reads a half-typed record
|
|
45
|
+
* would judge on fabricated inputs. */
|
|
46
|
+
export declare const MAILBOX_INVALID_PEER_META_CODE = "mailbox.invalid_peer_meta";
|
|
47
|
+
/** The lane-mount refusal code: the cross-session lane asked for a backend that does not declare
|
|
48
|
+
* cross-process safety ({@link mailboxCrossProcessMountVerdict}). */
|
|
49
|
+
export declare const MAILBOX_CROSS_PROCESS_UNSAFE_CODE = "mailbox.cross_process_unsafe";
|
|
50
|
+
/**
|
|
51
|
+
* Validate + detach an appended `peerMeta`. `undefined` ⇒ `undefined` (absent record). Anything else
|
|
52
|
+
* must be a plain object whose keys are all known, whose present values are non-empty strings, with
|
|
53
|
+
* `fromMode`/`kind` drawn from their closed sets; an `undefined`-valued key counts as absent. Any other
|
|
54
|
+
* shape throws {@link MailboxStoreError} with {@link MAILBOX_INVALID_PEER_META_CODE} — every bundled
|
|
55
|
+
* backend calls this BEFORE touching storage, so a refused append has zero side effects. Unknown keys
|
|
56
|
+
* are refused on purpose: the object is the contract's one typed slot, and a key nobody declared is
|
|
57
|
+
* either a typo or a newer schema this engine cannot judge on — both are loud, not silent.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readMailboxPeerMeta(raw: unknown): MailboxPeerMeta | undefined;
|
|
60
|
+
/** A detached copy of a persisted record's `peerMeta` (absent stays absent) — the read-side twin of
|
|
61
|
+
* {@link readMailboxPeerMeta}, so a lease consumer's mutation never reaches the stored record. */
|
|
62
|
+
export declare function cloneMailboxPeerMeta(meta: MailboxPeerMeta | undefined): MailboxPeerMeta | undefined;
|
|
16
63
|
/** The enqueue refusal code of the pre-delete clause (see {@link MailboxStore} and
|
|
17
64
|
* {@link MailboxStoreError}) — the ONE place it is spelled, so an out-of-repo store twin imports it
|
|
18
65
|
* instead of value-copying the string (same posture as `STALE_RUNNING_REAP_ATTRIBUTION`: a shared
|
|
@@ -31,9 +78,10 @@ export declare const MAILBOX_TOMBSTONED_RECIPIENT_CODE = "mailbox.recipient_tomb
|
|
|
31
78
|
* A backend may raise the same code with a plain `Error` carrying `.code`; consumers branch on the
|
|
32
79
|
* string, not on this class (a cross-process/out-of-repo store cannot hand back an instance).
|
|
33
80
|
*/
|
|
81
|
+
export type MailboxStoreErrorCode = typeof MAILBOX_TOMBSTONED_RECIPIENT_CODE | typeof MAILBOX_INVALID_PEER_META_CODE | typeof MAILBOX_CROSS_PROCESS_UNSAFE_CODE;
|
|
34
82
|
export declare class MailboxStoreError extends Error {
|
|
35
|
-
readonly code:
|
|
36
|
-
constructor(code:
|
|
83
|
+
readonly code: MailboxStoreErrorCode;
|
|
84
|
+
constructor(code: MailboxStoreErrorCode, message: string);
|
|
37
85
|
}
|
|
38
86
|
/** A leased batch: the messages a claim winner owns for delivery, plus the ack cursor. */
|
|
39
87
|
export interface MailboxLease {
|
|
@@ -49,6 +97,8 @@ export interface MailboxAppendMessage {
|
|
|
49
97
|
sentAt: number;
|
|
50
98
|
/** design/176 — see {@link MailboxMessage.hopChain}. */
|
|
51
99
|
hopChain?: string[];
|
|
100
|
+
/** design/385 — see {@link MailboxMessage.peerMeta}; validated by {@link readMailboxPeerMeta}. */
|
|
101
|
+
peerMeta?: MailboxPeerMeta;
|
|
52
102
|
}
|
|
53
103
|
/**
|
|
54
104
|
* The pluggable mailbox seam (design/151 §7.1). Contract notes for implementations (file/pg):
|
|
@@ -92,8 +142,25 @@ export interface MailboxAppendMessage {
|
|
|
92
142
|
* lifecycle (the two bundled ones, the CC inbox adapter) has nothing to refuse and keeps accepting
|
|
93
143
|
* — the clause fixes the SPELLING of the refusal, so a deployment reads one code instead of a
|
|
94
144
|
* per-backend dialect. Acceptance kit: `mailboxTombstonedRecipientContract`.
|
|
145
|
+
* - PEER METADATA (design/385, additive): `append` takes an optional typed `peerMeta`; a backend
|
|
146
|
+
* persists it and hands it back on `claimLease` byte-for-byte (a detached copy), absent stays absent,
|
|
147
|
+
* and a malformed value is REFUSED with `"mailbox.invalid_peer_meta"` before any side effect
|
|
148
|
+
* (`readMailboxPeerMeta` is the one validator; bundled backends call it first). T1 cases in
|
|
149
|
+
* `mailboxStoreContract` cover the round trip, the refusal and the aliasing.
|
|
150
|
+
* - CROSS-PROCESS SAFETY (design/385, optional capability declared on the store): the session-box
|
|
151
|
+
* drain shares ONE box between several OS processes. A backend that is correct under that sharing —
|
|
152
|
+
* seq minted once across processes, lease/ack never crossing, one process's housekeeping never
|
|
153
|
+
* discarding another's durable append — declares `crossProcessSafe: true` AFTER passing
|
|
154
|
+
* `mailboxCrossProcessContract` (the T2 kit that pins the whole set, not "has a lock"). A backend
|
|
155
|
+
* that does not declare it is refused by the lane at mount time with a named reason
|
|
156
|
+
* ({@link mailboxCrossProcessMountVerdict}) — never silently mounted on luck.
|
|
95
157
|
*/
|
|
96
158
|
export interface MailboxStore {
|
|
159
|
+
/** design/385 — cross-process safety declaration (see the interface notes). `true` = this backend
|
|
160
|
+
* passed `mailboxCrossProcessContract` over real OS processes; `false` = deliberately process-local
|
|
161
|
+
* (the in-memory reference); absent = undeclared (the CC inbox adapter). Read by
|
|
162
|
+
* {@link mailboxCrossProcessMountVerdict}; any other value is a malformed declaration and refuses. */
|
|
163
|
+
readonly crossProcessSafe?: boolean;
|
|
97
164
|
/** Durably park one message. Refuses `"mailbox.recipient_tombstoned"` when the backend can see
|
|
98
165
|
* that its recipient is in the deployment's pre-delete state (see the interface notes above). */
|
|
99
166
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
@@ -110,6 +177,21 @@ export interface MailboxStore {
|
|
|
110
177
|
maxAgeMs?: number;
|
|
111
178
|
}): Promise<number>;
|
|
112
179
|
}
|
|
180
|
+
/** The lane-mount verdict on a backend's cross-process declaration. */
|
|
181
|
+
export type MailboxCrossProcessVerdict = {
|
|
182
|
+
ok: true;
|
|
183
|
+
} | {
|
|
184
|
+
ok: false;
|
|
185
|
+
code: typeof MAILBOX_CROSS_PROCESS_UNSAFE_CODE;
|
|
186
|
+
reason: string;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* design/385 — may the cross-session lane (several OS processes draining one session box) mount over
|
|
190
|
+
* this backend? `ok` only for an explicit `crossProcessSafe: true`. Every other shape refuses with a
|
|
191
|
+
* named reason so the host can print it: absent (undeclared), `false` (process-local by design), or a
|
|
192
|
+
* value of the wrong type (a malformed declaration is refused, not read as "probably fine").
|
|
193
|
+
*/
|
|
194
|
+
export declare function mailboxCrossProcessMountVerdict(store: MailboxStore): MailboxCrossProcessVerdict;
|
|
113
195
|
/** RB-250② (2026-07-28) class fix — the box age for `reap` is the MAX `sentAt`, not the
|
|
114
196
|
* last array element: `sentAt` is caller-supplied, so append order need not be time order, and taking
|
|
115
197
|
* the tail let a box whose freshest message was mid-array be swept — both bundled backends discarded
|
|
@@ -118,12 +200,17 @@ export interface MailboxStore {
|
|
|
118
200
|
* adapter's documented posture — treating it as very old would let a malformed row delete a live
|
|
119
201
|
* box). Module-level export only; NOT re-exported from src/index.ts. */
|
|
120
202
|
export declare function newestSentAt(messages: readonly MailboxMessage[]): number | undefined;
|
|
203
|
+
/** A lease-facing copy of a stored record: every mutable member (hopChain, peerMeta) detached, so a
|
|
204
|
+
* consumer's mutation can never reach the record a redelivery serves. Shared by the bundled backends. */
|
|
205
|
+
export declare function detachMailboxMessage(m: MailboxMessage): MailboxMessage;
|
|
121
206
|
/**
|
|
122
207
|
* In-process reference implementation (single-instance / tests). Same posture as
|
|
123
208
|
* {@link import("./background-agent-store.js").InMemoryBackgroundAgentStore}: not default-mounted,
|
|
124
209
|
* detached copies at both boundaries, single-event-loop atomicity.
|
|
125
210
|
*/
|
|
126
211
|
export declare class InMemoryMailboxStore implements MailboxStore {
|
|
212
|
+
/** Process-local by construction (a Map): never a cross-process box, said so explicitly. */
|
|
213
|
+
readonly crossProcessSafe = false;
|
|
127
214
|
private boxes;
|
|
128
215
|
private key;
|
|
129
216
|
private box;
|
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
import { assertRetentionPolicy } from "./retention-policy.js";
|
|
2
|
+
export const MAILBOX_PEER_FROM_MODES = ["bypass", "prompting"];
|
|
3
|
+
export const MAILBOX_PEER_RECORD_KINDS = ["peer_message", "idle_notice", "delivery_notice"];
|
|
4
|
+
export const MAILBOX_INVALID_PEER_META_CODE = "mailbox.invalid_peer_meta";
|
|
5
|
+
export const MAILBOX_CROSS_PROCESS_UNSAFE_CODE = "mailbox.cross_process_unsafe";
|
|
6
|
+
const PEER_META_STRING_KEYS = ["fromSession", "senderKey", "fromScope", "gateReceiptId"];
|
|
7
|
+
export function readMailboxPeerMeta(raw) {
|
|
8
|
+
if (raw === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
const refuse = (why) => {
|
|
11
|
+
throw new MailboxStoreError(MAILBOX_INVALID_PEER_META_CODE, `MailboxStore.append: peerMeta ${why}`);
|
|
12
|
+
};
|
|
13
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
14
|
+
return refuse("must be a plain object");
|
|
15
|
+
const proto = Object.getPrototypeOf(raw);
|
|
16
|
+
if (proto !== Object.prototype && proto !== null)
|
|
17
|
+
return refuse("must be a plain object (a class instance, Date, Map or similar is not)");
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
20
|
+
if (typeof key !== "string")
|
|
21
|
+
return refuse("carries a symbol-keyed member");
|
|
22
|
+
const k = key;
|
|
23
|
+
const v = raw[k];
|
|
24
|
+
if (v === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
if (k === "fromMode") {
|
|
27
|
+
if (!MAILBOX_PEER_FROM_MODES.includes(v))
|
|
28
|
+
return refuse(`fromMode must be one of ${MAILBOX_PEER_FROM_MODES.join("|")}`);
|
|
29
|
+
out.fromMode = v;
|
|
30
|
+
}
|
|
31
|
+
else if (k === "kind") {
|
|
32
|
+
if (!MAILBOX_PEER_RECORD_KINDS.includes(v))
|
|
33
|
+
return refuse(`kind must be one of ${MAILBOX_PEER_RECORD_KINDS.join("|")}`);
|
|
34
|
+
out.kind = v;
|
|
35
|
+
}
|
|
36
|
+
else if (PEER_META_STRING_KEYS.includes(k)) {
|
|
37
|
+
if (typeof v !== "string" || v === "")
|
|
38
|
+
return refuse(`${k} must be a non-empty string`);
|
|
39
|
+
out[k] = v;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return refuse(`carries an unknown key ${JSON.stringify(k)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function cloneMailboxPeerMeta(meta) {
|
|
48
|
+
return meta === undefined ? undefined : { ...meta };
|
|
49
|
+
}
|
|
2
50
|
export const MAILBOX_TOMBSTONED_RECIPIENT_CODE = "mailbox.recipient_tombstoned";
|
|
3
51
|
export class MailboxStoreError extends Error {
|
|
4
52
|
code;
|
|
@@ -8,6 +56,17 @@ export class MailboxStoreError extends Error {
|
|
|
8
56
|
this.name = "MailboxStoreError";
|
|
9
57
|
}
|
|
10
58
|
}
|
|
59
|
+
export function mailboxCrossProcessMountVerdict(store) {
|
|
60
|
+
const declared = store.crossProcessSafe;
|
|
61
|
+
if (declared === true)
|
|
62
|
+
return { ok: true };
|
|
63
|
+
const reason = declared === undefined
|
|
64
|
+
? "the mailbox backend does not declare cross-process safety (crossProcessSafe is absent): several terminal sessions would share one session box on luck"
|
|
65
|
+
: declared === false
|
|
66
|
+
? "the mailbox backend declares crossProcessSafe: false (process-local by design): it cannot serve a session box shared across OS processes"
|
|
67
|
+
: `the mailbox backend's crossProcessSafe declaration is malformed (${typeof declared}, expected a boolean)`;
|
|
68
|
+
return { ok: false, code: MAILBOX_CROSS_PROCESS_UNSAFE_CODE, reason };
|
|
69
|
+
}
|
|
11
70
|
export function newestSentAt(messages) {
|
|
12
71
|
let newest;
|
|
13
72
|
for (const m of messages) {
|
|
@@ -18,7 +77,15 @@ export function newestSentAt(messages) {
|
|
|
18
77
|
}
|
|
19
78
|
return newest;
|
|
20
79
|
}
|
|
80
|
+
export function detachMailboxMessage(m) {
|
|
81
|
+
return {
|
|
82
|
+
...m,
|
|
83
|
+
...(m.hopChain !== undefined ? { hopChain: [...m.hopChain] } : {}),
|
|
84
|
+
...(m.peerMeta !== undefined ? { peerMeta: cloneMailboxPeerMeta(m.peerMeta) } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
21
87
|
export class InMemoryMailboxStore {
|
|
88
|
+
crossProcessSafe = false;
|
|
22
89
|
boxes = new Map();
|
|
23
90
|
key(scope, handle) {
|
|
24
91
|
if (handle.includes("\u0000")) {
|
|
@@ -38,9 +105,17 @@ export class InMemoryMailboxStore {
|
|
|
38
105
|
async append(scope, handle, msg) {
|
|
39
106
|
if (scope === undefined || scope === "")
|
|
40
107
|
throw new Error("MailboxStore.append: refusing a message without a scope");
|
|
108
|
+
const peerMeta = readMailboxPeerMeta(msg.peerMeta);
|
|
41
109
|
const b = this.box(scope, handle);
|
|
42
110
|
const seq = b.nextSeq++;
|
|
43
|
-
b.messages.push({
|
|
111
|
+
b.messages.push({
|
|
112
|
+
seq,
|
|
113
|
+
...(msg.from !== undefined ? { from: msg.from } : {}),
|
|
114
|
+
content: msg.content,
|
|
115
|
+
sentAt: msg.sentAt,
|
|
116
|
+
...(msg.hopChain !== undefined ? { hopChain: [...msg.hopChain] } : {}),
|
|
117
|
+
...(peerMeta !== undefined ? { peerMeta } : {}),
|
|
118
|
+
});
|
|
44
119
|
return seq;
|
|
45
120
|
}
|
|
46
121
|
async claimLease(scope, handle, owner, ttlMs, now = Date.now()) {
|
|
@@ -51,7 +126,7 @@ export class InMemoryMailboxStore {
|
|
|
51
126
|
return null;
|
|
52
127
|
const maxSeq = b.messages[b.messages.length - 1].seq;
|
|
53
128
|
b.lease = { owner, expiresAt: now + ttlMs, maxSeq };
|
|
54
|
-
return { messages: b.messages.map((m) => (
|
|
129
|
+
return { messages: b.messages.map((m) => detachMailboxMessage(m)), maxSeq };
|
|
55
130
|
}
|
|
56
131
|
async ack(scope, handle, owner, upToSeq) {
|
|
57
132
|
const b = this.boxes.get(this.key(scope, handle));
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* this by editing a file backend's file. That is the settings-file trust model, stated rather than
|
|
28
28
|
* defended against: for a file backend, host = user, no more and no less.
|
|
29
29
|
*/
|
|
30
|
-
import { type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
30
|
+
import { type EditedRuleBreadthWarning, type UncoveredSegmentDetail, type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
31
31
|
import type { PermissionRuleStoreProvider, RuleOwner } from "./permission-rule-store.js";
|
|
32
32
|
import type { SessionRuleOverlay } from "./permission-rule-session.js";
|
|
33
33
|
/** One candidate rule inside an approval record: the exact text and where it would apply. */
|
|
@@ -48,10 +48,16 @@ export type RuleApprovalKind = "card" | "import" | "starter";
|
|
|
48
48
|
* · `kind: "batch"` — a CONJUNCTION batch: confirming it is one yes to ALL of `candidates` at
|
|
49
49
|
* once, with no per-member sub-selection. On a CARD record the batch carries two MINT-TIME
|
|
50
50
|
* facts that cannot be re-derived from candidate texts and therefore must be stored:
|
|
51
|
-
* `segments` (each member's source segment, index-aligned with `candidates`)
|
|
51
|
+
* `segments` (each member's source segment, index-aligned with `candidates`),
|
|
52
52
|
* `uncoveredSegments` (the honest surplus count the wire offer disclosed, judged on the
|
|
53
|
-
* prepare-time coverage snapshot)
|
|
54
|
-
*
|
|
53
|
+
* prepare-time coverage snapshot) and — since `schema: 3` (design/382 §3.5) — `uncoveredDetail`,
|
|
54
|
+
* one reason row per counted segment in segment order: WHY each stayed uncovered is a mint-time
|
|
55
|
+
* fact that cannot be re-derived from candidate texts, so it must be stored or a crash/reload
|
|
56
|
+
* rebuild would render a card that says less than the first render did (record-is-what-you-see).
|
|
57
|
+
* REQUIRED on a card batch, all three: "optional, validated when present" is the
|
|
58
|
+
* silent-absence-passes shape this record form already refused once, and it is not re-imported
|
|
59
|
+
* for the new seat. On an import/starter record there is no compound command
|
|
60
|
+
* and no coverage snapshot, so all three fields must be ABSENT — honest absence, never an
|
|
55
61
|
* empty-string stand-in. The read-side integrity checks refuse either direction's violation.
|
|
56
62
|
*
|
|
57
63
|
* Cardinality: a batch references 1..N candidates where N is the record's own candidate count.
|
|
@@ -67,6 +73,7 @@ export type RuleOffer2 = {
|
|
|
67
73
|
readonly candidates: readonly number[];
|
|
68
74
|
readonly segments?: readonly string[];
|
|
69
75
|
readonly uncoveredSegments?: number;
|
|
76
|
+
readonly uncoveredDetail?: readonly UncoveredSegmentDetail[];
|
|
70
77
|
};
|
|
71
78
|
/** A durable record of one pending/settled human decision about minting rules.
|
|
72
79
|
*
|
|
@@ -81,12 +88,16 @@ export interface RuleApprovalRecord {
|
|
|
81
88
|
/** Structural owner (additive). Absent on every record a v1 tree minted. */
|
|
82
89
|
owner?: RuleOwner;
|
|
83
90
|
/**
|
|
84
|
-
* design/375 §4.5 — the record-form version stamp. A stored row WITHOUT it
|
|
85
|
-
* such a row confirms and redeems nothing (refused loudly,
|
|
86
|
-
* read through a compatibility shim: pending approvals are
|
|
87
|
-
* command draws a fresh card. Stores hand pre-form rows back
|
|
91
|
+
* design/375 §4.5 — the record-form version stamp. A stored row WITHOUT it — or carrying an older
|
|
92
|
+
* number — predates this form; such a row confirms and redeems nothing (refused loudly,
|
|
93
|
+
* `record_schema_stale`) and is never read through a compatibility shim: pending approvals are
|
|
94
|
+
* short-lived, and re-triggering the command draws a fresh card. Stores hand pre-form rows back
|
|
95
|
+
* as {@link StaleRuleApprovalRecord}. `3` (design/382 §3.5, B8/B7): the card batch's
|
|
96
|
+
* `uncoveredDetail` seat became REQUIRED and `edited.warnings` became storable — a `schema: 2`
|
|
97
|
+
* row is read back stale exactly like the pre-stamp rows before it (the short-lived-pending
|
|
98
|
+
* bargain, unchanged).
|
|
88
99
|
*/
|
|
89
|
-
schema:
|
|
100
|
+
schema: 3;
|
|
90
101
|
kind: RuleApprovalKind;
|
|
91
102
|
state: "pending" | "approved" | "redeemed";
|
|
92
103
|
candidates: RuleCandidate[];
|
|
@@ -137,18 +148,26 @@ export interface RuleApprovalRecord {
|
|
|
137
148
|
* same bytes); `at` is when the edit landed. Present ⇒ the record's chosen offer is the single
|
|
138
149
|
* offer minted over this candidate (the append, the offer and the choice are one CAS write).
|
|
139
150
|
* Absent on every card settled through an engine candidate.
|
|
151
|
+
*
|
|
152
|
+
* `warnings` (design/382 §3.3-5) — the BREADTH warning codes the text gates raised on this edit,
|
|
153
|
+
* written in the same CAS that settles it: "the width was named before the yes" becomes a fact the
|
|
154
|
+
* record itself carries, auditable after any crash or upgrade. Optional; present only when a
|
|
155
|
+
* warning was raised, and when present it is an array from the closed code set (the read-side
|
|
156
|
+
* integrity gate refuses anything else). Codes only — the sentences are derivable from the
|
|
157
|
+
* canonical rule text and are deliberately not stored (no second copy to drift).
|
|
140
158
|
*/
|
|
141
159
|
edited?: {
|
|
142
160
|
index: number;
|
|
143
161
|
text: string;
|
|
144
162
|
at: string;
|
|
163
|
+
warnings?: readonly EditedRuleBreadthWarning["code"][];
|
|
145
164
|
};
|
|
146
165
|
/** Dots already minted for this record, keyed by candidate index — the replay anchor. */
|
|
147
166
|
redeemedDots?: Record<number, RuleDot>;
|
|
148
167
|
}
|
|
149
168
|
/**
|
|
150
169
|
* design/375 §4.5 — the minimal ENVELOPE a store hands back for a durable row that predates the
|
|
151
|
-
* `schema:
|
|
170
|
+
* CURRENT (`schema: 3`) record form. A typed read cannot honestly return such a row as a
|
|
152
171
|
* {@link RuleApprovalRecord}, and dropping it at the store layer would misreport "stale" as
|
|
153
172
|
* corruption — so the store contract is this variant instead: `staleSchema: true` plus the
|
|
154
173
|
* IDENTITY fields, which are spelled the same way in every version and therefore safe to read.
|
|
@@ -209,6 +228,14 @@ export interface RuleConsentDeps {
|
|
|
209
228
|
* coverage reads see no session rows — both fail toward asking.
|
|
210
229
|
*/
|
|
211
230
|
sessionRules?: SessionRuleOverlay;
|
|
231
|
+
/**
|
|
232
|
+
* design/382 §2.4-6 — the deployment's sensitive-read negative control for the directoryRead MINT
|
|
233
|
+
* floor, same-source with its compiled read-deny judge (`(d) => readDeny.matchPath(d) !== null` is
|
|
234
|
+
* the canonical wiring). Consulted only when a card would PROPOSE a directory authorization from a
|
|
235
|
+
* cd segment — never by the explicit-consent doors (hand-written, import), which carry their own
|
|
236
|
+
* preview and validation. Absent ⇒ the engine's built-in default deny table stands in (refusal-only).
|
|
237
|
+
*/
|
|
238
|
+
deniesDirectoryRead?: (directory: string) => boolean;
|
|
212
239
|
}
|
|
213
240
|
/** In-memory approval records — the test backend and the reference CAS semantics, the stale-row
|
|
214
241
|
* envelope read included. */
|
|
@@ -297,6 +324,17 @@ export declare function prepareCardApproval(opts: {
|
|
|
297
324
|
* leans wide — the over-offer direction, which redemption's per-rule validation keeps safe.
|
|
298
325
|
*/
|
|
299
326
|
cwd?: string;
|
|
327
|
+
/**
|
|
328
|
+
* design/382 §2.4 (adversarial-review r2) — the EXECUTION working directory of the adjudicated
|
|
329
|
+
* call: the live tracked cwd the shell will resolve a relative `cd ./x` against, threaded by the
|
|
330
|
+
* caller exactly like `cwd` (the runner's ask-projection seat threads its `handsCwdRef.current`;
|
|
331
|
+
* a host preparing cards for its own calls threads whatever base those calls execute under).
|
|
332
|
+
* ONLY the relative-cd resolution reads it; scope eligibility and the derived project scope stay
|
|
333
|
+
* on `cwd`. Absent ⇒ `cwd` is the base (the single-value deployment, where the two coincide).
|
|
334
|
+
* Present and DIFFERENT, it is what keeps this durable consent path and the advisory ask-time
|
|
335
|
+
* projection minting the SAME directory member — two faces reading one world.
|
|
336
|
+
*/
|
|
337
|
+
execCwd?: string;
|
|
300
338
|
deps: RuleConsentDeps;
|
|
301
339
|
}): Promise<{
|
|
302
340
|
approvalId: string;
|
|
@@ -346,7 +384,7 @@ export declare function confirmRuleApproval(opts: {
|
|
|
346
384
|
/** Why a confirmation did not land. A closed set so a host can branch (re-present, re-fetch, give up).
|
|
347
385
|
* The two `record_*` members past `record_not_found` are the durable-row gates (design/375 §4.5/§4.7),
|
|
348
386
|
* disclosed to the record's own owner only — a non-owner gets `record_not_found` for every row:
|
|
349
|
-
* - `"record_schema_stale"` — the record predates the `schema:
|
|
387
|
+
* - `"record_schema_stale"` — the record predates the current (`schema: 3`) form; the decision it carried
|
|
350
388
|
* is void, and the recovery path is re-triggering the command for a fresh card (no
|
|
351
389
|
* compatibility read, no migration — `detail.message` says so);
|
|
352
390
|
* - `"record_malformed"` — the record's offer structure failed the read-side integrity checks
|
|
@@ -394,6 +432,16 @@ export type ConfirmResult = {
|
|
|
394
432
|
export type EditedRuleTextPrecheck = {
|
|
395
433
|
ok: true;
|
|
396
434
|
canonicalRule: string;
|
|
435
|
+
/**
|
|
436
|
+
* design/382 §3.3-5 — BREADTH notes about the accepted rule (additive; absent = nothing to
|
|
437
|
+
* say). A warning is never a refusal: the muscle-memory wide forms are legal (§3.4), and this
|
|
438
|
+
* seat is what makes accepting one an informed act. Rendering them beside the input line is
|
|
439
|
+
* the surface's duty (the engine cannot force a display); what the ENGINE guarantees is the
|
|
440
|
+
* durable half — a confirmation through the edit arm writes the hit codes into the record
|
|
441
|
+
* (`edited.warnings`), so "the width was named before the yes" is an auditable fact. The
|
|
442
|
+
* confirmation result deliberately does NOT repeat them (one fact, one loud place).
|
|
443
|
+
*/
|
|
444
|
+
warnings?: readonly EditedRuleBreadthWarning[];
|
|
397
445
|
} | {
|
|
398
446
|
ok: false;
|
|
399
447
|
code?: RuleRejectCode;
|
|
@@ -483,19 +531,20 @@ export interface ImportPreview {
|
|
|
483
531
|
reason: string;
|
|
484
532
|
}>;
|
|
485
533
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* Exactly ONE row per candidate the translation produced, in candidate order, carrying that
|
|
490
|
-
* candidate's `scope`. The scope is load-bearing rather than decoration: the same file line can
|
|
491
|
-
* appear in two layers and become two candidates in two scopes, and a report keyed on the text alone
|
|
492
|
-
* would show one row for two consents (independent-review finding — the row count and the candidate
|
|
493
|
-
* count must be readable against each other).
|
|
534
|
+
* ALWAYS EMPTY in this version — a compatibility seat, kept on the wire shape so an existing reader
|
|
535
|
+
* keeps finding the key (removing a wire field would break readers for no gain).
|
|
494
536
|
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
537
|
+
* The seat once carried the entries whose MATCH FORM was rewritten on the way in (the space-star
|
|
538
|
+
* suggestion form of the other product translated to this lane's colon-star form, one row per
|
|
539
|
+
* translated candidate with its `scope`). That translation is retired: the space-star wildcard is
|
|
540
|
+
* now a first-class match form of this lane's own grammar, so an entry's match form imports as the
|
|
541
|
+
* file states it — `Bash(npm run *)` lands as `Bash(npm run *)`, never as a colon-star — and nothing
|
|
542
|
+
* is ever pushed here. What `candidates` carries is the grammar's CANONICAL re-projection of each
|
|
543
|
+
* entry (the parser folds spacing: `Bash(npm<TAB>run *)` lands as `Bash(npm run *)`); that is the
|
|
544
|
+
* ordinary normalization every rule text in this lane undergoes, the same one the store applies,
|
|
545
|
+
* and not a translation this seat reports. An entry whose star placement has no rule form is
|
|
546
|
+
* refused into `skipped` under `unsupported.wildcard`, named as the file writes it. A reader
|
|
547
|
+
* rendering a "rewritten spellings" column from this seat renders an empty column, correctly.
|
|
499
548
|
*/
|
|
500
549
|
translated: Array<{
|
|
501
550
|
from: string;
|