@sema-agent/core 7.2.0 → 7.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/agents/cross-session-envelope.d.ts +7 -0
- package/dist/agents/cross-session-envelope.js +4 -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 +31 -0
- package/dist/agents/send-message-tool.js +145 -4
- package/dist/agents/subagent-steps.d.ts +11 -0
- package/dist/agents/subagent-steps.js +27 -4
- package/dist/brain/status-sink.d.ts +10 -0
- package/dist/brain/status-sink.js +13 -4
- package/dist/brain/stream-engine.d.ts +11 -0
- package/dist/brain/stream-engine.js +39 -3
- package/dist/core/arg-summary.d.ts +13 -3
- package/dist/core/arg-summary.js +138 -7
- 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 +14 -0
- package/dist/core/checkpoint-store.js +4 -3
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/mailbox-store.d.ts +89 -2
- package/dist/core/mailbox-store.js +77 -2
- package/dist/core/permission-rule-model.d.ts +9 -0
- package/dist/core/permission-rule-model.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +20 -0
- package/dist/core/runner/prepare-task.js +152 -37
- package/dist/core/runner/runtask.js +5 -2
- 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 +38 -9
- package/dist/core/task-notification.js +8 -2
- package/dist/core/tools.js +1 -0
- package/dist/core/types.d.ts +176 -26
- package/dist/core/wiring-manifest.d.ts +62 -5
- package/dist/core/wiring-manifest.js +9 -0
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +3 -0
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/loop/agent-loop.d.ts +7 -0
- package/dist/engine/loop/agent-loop.js +79 -0
- package/dist/engine/loop/types.d.ts +42 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +11 -4
- package/dist/internal/harness-types.d.ts +1 -1
- 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/dist/tools/fs/fs-write.js +69 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +121 -1
package/dist/core/arg-summary.js
CHANGED
|
@@ -25,10 +25,10 @@ function mapBackOnePass(edits, pos) {
|
|
|
25
25
|
return pos - delta;
|
|
26
26
|
}
|
|
27
27
|
const PREEXISTING_MARKER_RE = /\[redacted(?:-[a-z]+)?\]/g;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const FORMAT_CHAR_RE = /\p{Cf}/u;
|
|
29
|
+
const MAX_FORMAT_SLOT_SCANS = 64;
|
|
30
|
+
const REDACTED_WHOLE = "[redacted]";
|
|
31
|
+
function applyPasses(input, passes, report, toOriginal) {
|
|
32
32
|
const batches = [];
|
|
33
33
|
let cur = input;
|
|
34
34
|
for (const pass of passes) {
|
|
@@ -51,18 +51,149 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
51
51
|
report.findings.push({
|
|
52
52
|
kind: pass.kind,
|
|
53
53
|
confidence: pass.confidence,
|
|
54
|
-
span: [s0, e0],
|
|
54
|
+
span: [toOriginal(s0), toOriginal(e0)],
|
|
55
55
|
marker: pass.marker,
|
|
56
56
|
source: pass.source,
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length });
|
|
59
|
+
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length, inserted });
|
|
60
60
|
}
|
|
61
61
|
return inserted;
|
|
62
62
|
});
|
|
63
63
|
batches.push(edits);
|
|
64
64
|
}
|
|
65
|
-
return cur;
|
|
65
|
+
return { out: cur, batches };
|
|
66
|
+
}
|
|
67
|
+
function replacedSpans(batches) {
|
|
68
|
+
const spans = [];
|
|
69
|
+
for (let j = 0; j < batches.length; j++) {
|
|
70
|
+
for (const e of batches[j]) {
|
|
71
|
+
let s0 = e.at;
|
|
72
|
+
let e0 = e.at + e.removedLen;
|
|
73
|
+
for (let i = j - 1; i >= 0; i--) {
|
|
74
|
+
s0 = mapBackOnePass(batches[i], s0);
|
|
75
|
+
e0 = mapBackOnePass(batches[i], e0);
|
|
76
|
+
}
|
|
77
|
+
if (e0 - s0 !== e.removedLen)
|
|
78
|
+
return undefined;
|
|
79
|
+
spans.push({ start: s0, end: e0, inserted: e.inserted, pass: j });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
spans.sort((a, b) => a.start - b.start);
|
|
83
|
+
for (let i = 1; i < spans.length; i++)
|
|
84
|
+
if (spans[i].start < spans[i - 1].end)
|
|
85
|
+
return undefined;
|
|
86
|
+
return spans;
|
|
87
|
+
}
|
|
88
|
+
export function runRedactionPasses(input, passes, report) {
|
|
89
|
+
if (report !== undefined && report.preexistingMarkers === undefined) {
|
|
90
|
+
report.preexistingMarkers = input.match(PREEXISTING_MARKER_RE)?.length ?? 0;
|
|
91
|
+
}
|
|
92
|
+
if (!FORMAT_CHAR_RE.test(input))
|
|
93
|
+
return applyPasses(input, passes, report, (pos) => pos).out;
|
|
94
|
+
const viewChars = [];
|
|
95
|
+
const viewToOriginal = [];
|
|
96
|
+
const formatAtSlot = [];
|
|
97
|
+
const charStartOffsets = [];
|
|
98
|
+
{
|
|
99
|
+
let orig = 0;
|
|
100
|
+
let viewOff = 0;
|
|
101
|
+
let slotBuf = "";
|
|
102
|
+
for (const ch of input) {
|
|
103
|
+
if (FORMAT_CHAR_RE.test(ch)) {
|
|
104
|
+
slotBuf += ch;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
formatAtSlot.push(slotBuf);
|
|
108
|
+
slotBuf = "";
|
|
109
|
+
viewToOriginal.push(orig);
|
|
110
|
+
charStartOffsets.push(viewOff);
|
|
111
|
+
viewChars.push(ch);
|
|
112
|
+
viewOff += ch.length;
|
|
113
|
+
}
|
|
114
|
+
orig += ch.length;
|
|
115
|
+
}
|
|
116
|
+
formatAtSlot.push(slotBuf);
|
|
117
|
+
viewToOriginal.push(input.length);
|
|
118
|
+
charStartOffsets.push(viewOff);
|
|
119
|
+
}
|
|
120
|
+
const view = viewChars.join("");
|
|
121
|
+
const offsetToCharIndex = new Map();
|
|
122
|
+
charStartOffsets.forEach((off, idx) => offsetToCharIndex.set(off, idx));
|
|
123
|
+
const toOriginal = (pos) => {
|
|
124
|
+
const idx = offsetToCharIndex.get(pos);
|
|
125
|
+
return idx === undefined ? input.length : viewToOriginal[idx];
|
|
126
|
+
};
|
|
127
|
+
const { out, batches } = applyPasses(view, passes, report, toOriginal);
|
|
128
|
+
const viewSpans = replacedSpans(batches);
|
|
129
|
+
if (viewSpans === undefined)
|
|
130
|
+
return out;
|
|
131
|
+
const spans = viewSpans.map((sp) => {
|
|
132
|
+
const startIdx = offsetToCharIndex.get(sp.start) ?? viewChars.length;
|
|
133
|
+
const endIdx = offsetToCharIndex.get(sp.end) ?? viewChars.length;
|
|
134
|
+
const lastIdx = endIdx - 1;
|
|
135
|
+
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
|
|
136
|
+
return { start: viewToOriginal[startIdx], end: endOrig, inserted: sp.inserted };
|
|
137
|
+
});
|
|
138
|
+
const viewOffsetToOriginal = (viewStart, viewEnd) => {
|
|
139
|
+
const startIdx = offsetToCharIndex.get(viewStart) ?? viewChars.length;
|
|
140
|
+
const endIdx = offsetToCharIndex.get(viewEnd) ?? viewChars.length;
|
|
141
|
+
const lastIdx = endIdx - 1;
|
|
142
|
+
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
|
|
143
|
+
return { start: viewToOriginal[startIdx], end: endOrig };
|
|
144
|
+
};
|
|
145
|
+
const cutOffsets = [];
|
|
146
|
+
for (let k = 1; k < viewChars.length; k++)
|
|
147
|
+
if (formatAtSlot[k] !== "")
|
|
148
|
+
cutOffsets.push(charStartOffsets[k]);
|
|
149
|
+
if (cutOffsets.length > MAX_FORMAT_SLOT_SCANS)
|
|
150
|
+
return REDACTED_WHOLE;
|
|
151
|
+
const extra = [];
|
|
152
|
+
const rawSpans = replacedSpans(applyPasses(input, passes, undefined, (pos) => pos).batches);
|
|
153
|
+
if (rawSpans !== undefined)
|
|
154
|
+
extra.push(...rawSpans);
|
|
155
|
+
for (const cut of cutOffsets) {
|
|
156
|
+
const cutSpans = replacedSpans(applyPasses(view.slice(cut), passes, undefined, (pos) => pos).batches);
|
|
157
|
+
if (cutSpans === undefined)
|
|
158
|
+
continue;
|
|
159
|
+
for (const c of cutSpans) {
|
|
160
|
+
const o = viewOffsetToOriginal(c.start + cut, c.end + cut);
|
|
161
|
+
extra.push({ start: o.start, end: o.end, inserted: c.inserted, pass: c.pass });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
extra.sort((a, b) => a.start - b.start);
|
|
165
|
+
for (const r of extra) {
|
|
166
|
+
const overlapping = spans.find((v) => r.start < v.end && v.start < r.end);
|
|
167
|
+
if (overlapping !== undefined) {
|
|
168
|
+
if (r.start < overlapping.start)
|
|
169
|
+
overlapping.start = r.start;
|
|
170
|
+
if (r.end > overlapping.end)
|
|
171
|
+
overlapping.end = r.end;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
spans.push({ start: r.start, end: r.end, inserted: r.inserted });
|
|
175
|
+
if (report !== undefined) {
|
|
176
|
+
const pass = passes[r.pass];
|
|
177
|
+
report.findings.push({ kind: pass.kind, confidence: pass.confidence, span: [r.start, r.end], marker: pass.marker, source: pass.source });
|
|
178
|
+
}
|
|
179
|
+
spans.sort((a, b) => a.start - b.start);
|
|
180
|
+
}
|
|
181
|
+
for (let i = 1; i < spans.length;) {
|
|
182
|
+
if (spans[i].start < spans[i - 1].end) {
|
|
183
|
+
spans[i - 1].end = Math.max(spans[i - 1].end, spans[i].end);
|
|
184
|
+
spans.splice(i, 1);
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
let result = "";
|
|
190
|
+
let cursor = 0;
|
|
191
|
+
for (const sp of spans) {
|
|
192
|
+
result += input.slice(cursor, sp.start) + sp.inserted;
|
|
193
|
+
cursor = sp.end;
|
|
194
|
+
}
|
|
195
|
+
result += input.slice(cursor);
|
|
196
|
+
return result;
|
|
66
197
|
}
|
|
67
198
|
export const SECRET_PASSES = [
|
|
68
199
|
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
|
|
@@ -29,6 +29,14 @@ export interface AutoModeArmingRecipe {
|
|
|
29
29
|
timeoutMs?: number;
|
|
30
30
|
/** Consecutive-failure threshold opening the one-way breaker (floored, as the decider itself floors it). */
|
|
31
31
|
failureThreshold?: number;
|
|
32
|
+
/**
|
|
33
|
+
* The cross-session lane's classifier rule was spliced into the assembled prompt (the lane was
|
|
34
|
+
* mounted on the arming leg). Part of the PROMPT BODY: the rebuild re-splices the same engine
|
|
35
|
+
* constant when the bit is set, so the recorded `promptDigest` is reproducible, and the fold treats
|
|
36
|
+
* the bit as body (a rebuild under a deployment whose own face does not declare it refuses as
|
|
37
|
+
* `settings_moved` — the two prompts differ by a rule block). Absent = the rule was not spliced.
|
|
38
|
+
*/
|
|
39
|
+
crossSessionMessagesRule?: true;
|
|
32
40
|
/**
|
|
33
41
|
* A digest of the EXACT classifier system prompt this arming assembled (see
|
|
34
42
|
* `rebuildAutoModeDecider`). The recipe records the deployment's OVERRIDES; the bulk of the criteria —
|
|
@@ -57,6 +65,9 @@ export interface AutoModeArmingFace {
|
|
|
57
65
|
timeoutMs?: number;
|
|
58
66
|
failureThreshold?: number;
|
|
59
67
|
settingsEpoch?: string;
|
|
68
|
+
/** `true` when the cross-session lane's classifier rule is spliced into this deployment's classifier
|
|
69
|
+
* prompt (the Runner sets it from its own lane mount; a redeeming host declares it from its). */
|
|
70
|
+
crossSessionMessagesRule?: boolean;
|
|
60
71
|
}
|
|
61
72
|
/**
|
|
62
73
|
* Canonicalize + VALIDATE an arming recipe: the plain-data form that persists, or `undefined` when the
|
|
@@ -81,9 +81,13 @@ export function sanitizeAutoModeArmingRecipe(value) {
|
|
|
81
81
|
const promptDigest = value.promptDigest;
|
|
82
82
|
if (promptDigest !== undefined && (typeof promptDigest !== "string" || promptDigest === ""))
|
|
83
83
|
return undefined;
|
|
84
|
+
const crossSessionMessagesRule = value.crossSessionMessagesRule;
|
|
85
|
+
if (crossSessionMessagesRule !== undefined && typeof crossSessionMessagesRule !== "boolean")
|
|
86
|
+
return undefined;
|
|
84
87
|
return {
|
|
85
88
|
v: AUTO_MODE_ARMING_RECIPE_VERSION,
|
|
86
89
|
...(promptDigest !== undefined ? { promptDigest } : {}),
|
|
90
|
+
...(crossSessionMessagesRule === true ? { crossSessionMessagesRule: true } : {}),
|
|
87
91
|
...(rules !== undefined ? { rules } : {}),
|
|
88
92
|
...(settingsDenyRules !== undefined ? { settingsDenyRules } : {}),
|
|
89
93
|
...(sessionContext !== undefined ? { sessionContext } : {}),
|
|
@@ -104,6 +108,7 @@ export function autoModeArmingRecipeOf(face, bind) {
|
|
|
104
108
|
...(face.timeoutMs !== undefined ? { timeoutMs: face.timeoutMs } : {}),
|
|
105
109
|
...(face.failureThreshold !== undefined ? { failureThreshold: face.failureThreshold } : {}),
|
|
106
110
|
...(face.settingsEpoch !== undefined ? { settingsEpoch: face.settingsEpoch } : {}),
|
|
111
|
+
...(face.crossSessionMessagesRule !== undefined ? { crossSessionMessagesRule: face.crossSessionMessagesRule } : {}),
|
|
107
112
|
});
|
|
108
113
|
}
|
|
109
114
|
function sameArmingBody(a, b) {
|
|
@@ -116,6 +121,7 @@ function sameArmingBody(a, b) {
|
|
|
116
121
|
r.sessionContext ?? null,
|
|
117
122
|
r.window?.maxEntries ?? null,
|
|
118
123
|
r.window?.maxCharsPerEntry ?? null,
|
|
124
|
+
r.crossSessionMessagesRule === true,
|
|
119
125
|
]);
|
|
120
126
|
return body(a) === body(b);
|
|
121
127
|
}
|
|
@@ -160,7 +166,7 @@ export function foldAutoModeArming(recorded, current) {
|
|
|
160
166
|
ok: false,
|
|
161
167
|
reason: "settings_moved",
|
|
162
168
|
message: "the auto-mode settings moved between the recorded arming and this deployment's current ones (rule sections / settings-deny rules / " +
|
|
163
|
-
"session context / window bounds differ), and free-text rule sets have no sound stricter-than ordering — refusing to rebuild " +
|
|
169
|
+
"session context / window bounds / the cross-session lane rule differ), and free-text rule sets have no sound stricter-than ordering — refusing to rebuild " +
|
|
164
170
|
"(the strictest decidable answer: the inherited ask flows the original chain to a human)",
|
|
165
171
|
};
|
|
166
172
|
}
|
|
@@ -22,6 +22,11 @@ export interface BuildAutoModePromptOptions {
|
|
|
22
22
|
/** Extra session-context facts (e.g. the CC user-identity line) — appended as a
|
|
23
23
|
* `## Session Context` bullet block after the assembled document. */
|
|
24
24
|
sessionContext?: readonly string[];
|
|
25
|
+
/** design/385 §4.5 — the text spliced into the `<cross_session_messages_rule>` slot. Absent (the
|
|
26
|
+
* default, and every deployment without the cross-session lane) ⇒ the slot is blanked exactly as
|
|
27
|
+
* before; the lane mount passes `CROSS_SESSION_CLASSIFIER_RULE`. Callback form at the splice (the
|
|
28
|
+
* text may carry `$`). */
|
|
29
|
+
crossSessionMessagesRule?: string;
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
32
|
* Assemble the classifier SYSTEM prompt (CC `KRg`, content-equivalent single string — CC splits the
|
|
@@ -34,7 +34,8 @@ const PAIRED_SECTIONS = [
|
|
|
34
34
|
["environment", /<user_environment_to_replace>([\s\S]*?)<\/user_environment_to_replace>/],
|
|
35
35
|
];
|
|
36
36
|
export function buildAutoModePrompt(options) {
|
|
37
|
-
|
|
37
|
+
const crossSessionRule = options?.crossSessionMessagesRule ?? "";
|
|
38
|
+
let out = AUTO_MODE_BASE_PROMPT.replace("<permissions_template>", () => AUTO_MODE_PERMISSIONS_EXTERNAL).replace("<cross_session_messages_rule>", () => crossSessionRule);
|
|
38
39
|
for (const [key, re] of PAIRED_SECTIONS) {
|
|
39
40
|
out = out.replace(re, (_m, inner) => mergeRuleSection(options?.rules?.[key], inner));
|
|
40
41
|
}
|
|
@@ -58,7 +58,8 @@ export type AutoModeRebuildResult = {
|
|
|
58
58
|
* Rebuild a decider with a PARKED ancestor's criteria from its recorded recipe plus a fresh model leg.
|
|
59
59
|
*
|
|
60
60
|
* What is reproduced: the assembled system prompt (`buildAutoModePrompt` over the recorded rule
|
|
61
|
-
* overrides / settings-deny rules / session context
|
|
61
|
+
* overrides / settings-deny rules / session context, plus the engine's cross-session lane rule when
|
|
62
|
+
* the recipe says the arming leg spliced it), the transcript-window bounds, the round-trip
|
|
62
63
|
* timeout and the breaker threshold — i.e. every input the ancestor's own `createAutoModeDecider` call
|
|
63
64
|
* had except the model leg and the alarm closure.
|
|
64
65
|
*
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { createAutoModeDecider } from "./auto-mode.js";
|
|
3
3
|
import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "./auto-mode-prompt.js";
|
|
4
4
|
import { foldAutoModeArming, } from "./auto-mode-arming.js";
|
|
5
|
+
import { CROSS_SESSION_CLASSIFIER_RULE } from "../agents/cross-session-envelope.js";
|
|
5
6
|
export function rebuildAutoModeDecider(opts) {
|
|
6
7
|
const folded = foldAutoModeArming(opts.recorded, opts.current);
|
|
7
8
|
if (!folded.ok)
|
|
@@ -11,6 +12,7 @@ export function rebuildAutoModeDecider(opts) {
|
|
|
11
12
|
...(effective.rules !== undefined ? { rules: effective.rules } : {}),
|
|
12
13
|
...(effective.settingsDenyRules !== undefined ? { settingsDenyRules: effective.settingsDenyRules } : {}),
|
|
13
14
|
...(effective.sessionContext !== undefined ? { sessionContext: effective.sessionContext } : {}),
|
|
15
|
+
...(effective.crossSessionMessagesRule === true ? { crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : {}),
|
|
14
16
|
};
|
|
15
17
|
const systemPrompt = buildAutoModePrompt(promptOptions);
|
|
16
18
|
const assembledDigest = `apv1:${createHash("sha256").update(systemPrompt).digest("hex")}`;
|
|
@@ -1065,6 +1065,20 @@ export interface CheckpointState {
|
|
|
1065
1065
|
rules: SessionPermissionRules;
|
|
1066
1066
|
}>;
|
|
1067
1067
|
shellGate?: "off" | "always" | "classify";
|
|
1068
|
+
/** The chain's AUTO-MODE INTENT at suspend (data half, same law as `shellGate`): `true` when the
|
|
1069
|
+
* suspended leg was an auto-mode task — its own seat, the bit its live chain carried, or the bit
|
|
1070
|
+
* an earlier suspend of the same chain recorded (carried forward across a re-suspend). A resume
|
|
1071
|
+
* leg reads it as one more INTENT source beside the re-supplied seat and the waking chain, so a
|
|
1072
|
+
* redemption in another process (no seat re-passed, no live chain) still arms exactly as the
|
|
1073
|
+
* suspend leg did. It is a MEMORY of intent, never an authorization: the resuming deployment's
|
|
1074
|
+
* face (`RunnerDeps.autoMode`) and the resuming principal's deny bit (`RuntimeCaps.autoMode`)
|
|
1075
|
+
* are judged afresh on every leg — a bit on the row cannot arm where the redeeming deployment
|
|
1076
|
+
* would not. Follows the classifier's latch: a leg armed here writes it only while its own breaker
|
|
1077
|
+
* is untripped and untouched (a session that fell back to non-auto hands nothing forward); a leg
|
|
1078
|
+
* never armed here carries the memory as is. Absent on older checkpoints and on non-auto tasks
|
|
1079
|
+
* (byte-identical to the pre-bit row); an older worker that ignores it resumes un-armed, the
|
|
1080
|
+
* narrower direction. */
|
|
1081
|
+
autoModeRequested?: true;
|
|
1068
1082
|
/** Org-memory admission freeze (ruled 2026-08-05): the chain's admitted org-scope set at
|
|
1069
1083
|
* suspend (data half, plain strings). The resume leg folds it seed ∩ live (tighten-only) and
|
|
1070
1084
|
* re-runs admission under it — a resume must never widen the delegation freeze. Absent on
|
|
@@ -2,7 +2,8 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { uuidv7 } from "../internal/harness.js";
|
|
3
3
|
import { PROBE_CAUSE_PATH_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
4
4
|
import { carriesBidiControls } from "./tool-policy.js";
|
|
5
|
-
import { renderUntrustedCommandText } from "./permission-rule-model.js";
|
|
5
|
+
import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
|
|
6
|
+
import { redactSecrets } from "./untrusted-egress.js";
|
|
6
7
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
7
8
|
export function mintCheckpointToken() {
|
|
8
9
|
return randomBytes(16).toString("hex");
|
|
@@ -124,7 +125,7 @@ export function buildRiskDescriptor(input) {
|
|
|
124
125
|
if (shell || toolName === "Bash") {
|
|
125
126
|
const cmd = isPlainRecord(args) ? safeDataValue(args, "command") : undefined;
|
|
126
127
|
if (typeof cmd === "string" && cmd.length > 0)
|
|
127
|
-
summary = renderUntrustedCommandText(cmd, SUMMARY_CMD_MAX);
|
|
128
|
+
summary = renderUntrustedCommandText(redactSecrets(stripFormatCharacters(cmd)), SUMMARY_CMD_MAX);
|
|
128
129
|
const bg = isPlainRecord(args) ? safeDataValue(args, "run_in_background") : undefined;
|
|
129
130
|
if (bg === true)
|
|
130
131
|
summary = `[background persistent process — no per-step recheck] ${summary ?? ""}`.trimEnd();
|
|
@@ -139,7 +140,7 @@ export function buildRiskDescriptor(input) {
|
|
|
139
140
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
140
141
|
const encDigest = (s) => s.replace(/%/g, "%25").replace(/=/g, "%3D").replace(/ /g, "%20");
|
|
141
142
|
const k = encDigest(renderUntrustedCommandText(key, 40));
|
|
142
|
-
const val = encDigest(renderUntrustedCommandText(String(v), SUMMARY_VALUE_MAX));
|
|
143
|
+
const val = encDigest(renderUntrustedCommandText(redactSecrets(stripFormatCharacters(String(v))), SUMMARY_VALUE_MAX));
|
|
143
144
|
parts.push(`${k}=${val}`);
|
|
144
145
|
}
|
|
145
146
|
}
|
|
@@ -103,7 +103,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
103
103
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
104
104
|
* no longer minted.
|
|
105
105
|
*/
|
|
106
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
106
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.durable_gate_unavailable", "config.peer_lane_unmounted", "peer.inbound_disposition", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
107
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
108
108
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
109
109
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -101,6 +101,9 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
101
101
|
"config.tool_model_gate_removed",
|
|
102
102
|
"config.tool_model_gate_unknown_class",
|
|
103
103
|
"config.tool_model_gate_env_invalid",
|
|
104
|
+
"config.durable_gate_unavailable",
|
|
105
|
+
"config.peer_lane_unmounted",
|
|
106
|
+
"peer.inbound_disposition",
|
|
104
107
|
"delegation.transcript_integrity",
|
|
105
108
|
"mcp.revocation_probe_failed",
|
|
106
109
|
"workflow.governance_key_stripped",
|
|
@@ -143,6 +146,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
143
146
|
"steering.parked_input_blocked": "user",
|
|
144
147
|
"task.halt_unconsumed": "user",
|
|
145
148
|
"task.late_approval": "user",
|
|
149
|
+
"config.durable_gate_unavailable": "user",
|
|
146
150
|
"memory.capture_opted_out": "user",
|
|
147
151
|
"memory.capture_optout_unpersisted": "user",
|
|
148
152
|
"memory.consolidation_withheld": "user",
|
|
@@ -154,6 +158,8 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
154
158
|
"config.tool_model_gate_removed": "operator",
|
|
155
159
|
"config.tool_model_gate_unknown_class": "operator",
|
|
156
160
|
"config.tool_model_gate_env_invalid": "operator",
|
|
161
|
+
"config.peer_lane_unmounted": "operator",
|
|
162
|
+
"peer.inbound_disposition": "user",
|
|
157
163
|
"delegation.transcript_integrity": "operator",
|
|
158
164
|
"mcp.revocation_probe_failed": "operator",
|
|
159
165
|
"workflow.governance_key_stripped": "operator",
|
|
@@ -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));
|
|
@@ -331,6 +331,15 @@ export declare const SUGGESTION_LEXICON: readonly string[];
|
|
|
331
331
|
* through, so an ordinary rule text reads normally. The result is length-bounded.
|
|
332
332
|
*/
|
|
333
333
|
export declare function escapeForDisclosure(value: unknown): string;
|
|
334
|
+
/**
|
|
335
|
+
* The zero-width half of {@link renderUntrustedCommandText} on its own: every `\p{Cf}` character
|
|
336
|
+
* REMOVED, nothing else touched. Exposed for a seat that must run a PATTERN pass (a secret scanner)
|
|
337
|
+
* before the display render — a credential the text splits with a format character is invisible to
|
|
338
|
+
* the scanner while it runs, and the render's own strip would then glue it back together in clear
|
|
339
|
+
* AFTER the scanner missed it. Strip first, scan, then render: the render's strip is idempotent over
|
|
340
|
+
* text this already stripped, so the final bytes are the same as a single render, minus the leak.
|
|
341
|
+
*/
|
|
342
|
+
export declare function stripFormatCharacters(text: string): string;
|
|
334
343
|
/**
|
|
335
344
|
* Render raw, untrusted COMMAND text for a display surface — the minimal safe baseline for the seats
|
|
336
345
|
* that carry post-rewrite command bytes verbatim ({@link SegmentRuleSuggestion.segment} is the
|