@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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/** The row schema version this engine writes and fully understands. A row carrying a HIGHER version
|
|
2
|
+
* is a newer engine's row: it is skipped as `schema_newer` (capability negotiation — the row is not
|
|
3
|
+
* garbage, this engine simply cannot judge it), never treated as a malformed value. */
|
|
4
|
+
export declare const PEER_SESSION_RECORD_SCHEMA_VERSION = 1;
|
|
5
|
+
/** CC `Fa(s, 262144)` — the read cap of one registration file. */
|
|
6
|
+
export declare const PEER_SESSION_RECORD_MAX_BYTES = 262144;
|
|
7
|
+
export type PeerSessionLiveness = "live" | "dead" | "deleted";
|
|
8
|
+
export type PeerSessionTempo = "active" | "idle" | "blocked";
|
|
9
|
+
/** The registration row. Field set = CC's load-bearing subset + sema's `scope` axis + the sema-only
|
|
10
|
+
* liveness/tombstone members. A host writes it; every terminal reads it through {@link readPeerSessionRecord}. */
|
|
11
|
+
export interface PeerSessionRecord {
|
|
12
|
+
schemaVersion: number;
|
|
13
|
+
/** The session's durable identity: its box address is `session.<sessionId>` ({@link peerSessionBoxHandle}). */
|
|
14
|
+
sessionId: string;
|
|
15
|
+
/** sema axis — the principal scope the row belongs to. A reader filters by the ACCESSOR's scope
|
|
16
|
+
* before anything else (the mechanical seat of the cross-principal wall). */
|
|
17
|
+
scope: string;
|
|
18
|
+
/** Display name (raw). Matching is on {@link normalizePeerName} of it, never on the raw string. */
|
|
19
|
+
name: string;
|
|
20
|
+
nameSource?: "auto" | "flag" | "rename";
|
|
21
|
+
formerNames?: readonly string[];
|
|
22
|
+
pid?: number;
|
|
23
|
+
/** Process start time (ms) — paired with `pid` against PID reuse. */
|
|
24
|
+
procStartMs?: number;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
startedAt: number;
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
liveness: PeerSessionLiveness;
|
|
29
|
+
/** Present iff `liveness !== "live"`: when the row was marked dead (or the tombstone was written). */
|
|
30
|
+
diedAt?: number;
|
|
31
|
+
tempo?: PeerSessionTempo;
|
|
32
|
+
entrypoint?: string;
|
|
33
|
+
peerProtocol?: number;
|
|
34
|
+
/** CC capability bits (`/^[a-z0-9_]{1,32}$/`, ≤16); `sema_mailbox_v1` marks a store-lane peer. */
|
|
35
|
+
peerFeatures?: readonly string[];
|
|
36
|
+
sockPath?: string;
|
|
37
|
+
/** CC `cross_session_inbound: available|unavailable` — the session's SELF-REPORTED inbound posture.
|
|
38
|
+
* `"unavailable"` lets a sender refuse synchronously (degraded, best-effort); absent/stale means the
|
|
39
|
+
* drain point decides. */
|
|
40
|
+
inboundPosture?: "available" | "unavailable";
|
|
41
|
+
}
|
|
42
|
+
/** The box-address prefix of a session box (design/385 §2.2 — the dot form: `:` is outside the file
|
|
43
|
+
* backend's path charset, `.` is inside it and outside the a* handle grammar). */
|
|
44
|
+
export declare const SESSION_BOX_PREFIX = "session.";
|
|
45
|
+
/** The grammar, spelled for a refusal message. */
|
|
46
|
+
export declare const PEER_SESSION_ID_GRAMMAR = "^[A-Za-z0-9_-]{1,80}$";
|
|
47
|
+
/** Is this session id addressable on the peer lane (spellable as a `session.<id>` box handle)? The
|
|
48
|
+
* lane mount asks this BEFORE binding a drain: a host-minted id outside the grammar refuses the lane
|
|
49
|
+
* by name instead of throwing out of {@link peerSessionBoxHandle} mid-prepare. */
|
|
50
|
+
export declare function isPeerSessionId(sessionId: string): boolean;
|
|
51
|
+
/** The mailbox handle of a session's own box. Folded to lower case: both bundled backends key boxes
|
|
52
|
+
* case-insensitively, and the address form is spelled lower-case. */
|
|
53
|
+
export declare function peerSessionBoxHandle(sessionId: string): string;
|
|
54
|
+
/** Is `text` spelled as an explicit session address? Returns the (lower-cased) session id, or
|
|
55
|
+
* `undefined` when the text is not an address at all. An address is NEVER a name: the resolution
|
|
56
|
+
* ladder ranks this arm beside the precise a* id, ahead of every name rung. */
|
|
57
|
+
export declare function parsePeerSessionAddress(text: string): string | undefined;
|
|
58
|
+
export type PeerSessionRecordRefusal = "not_an_object" | "schema_missing" | "schema_newer" | "session_id_invalid" | "scope_invalid" | "name_invalid" | "liveness_invalid" | "field_invalid";
|
|
59
|
+
export type PeerSessionRecordRead = {
|
|
60
|
+
ok: true;
|
|
61
|
+
record: PeerSessionRecord;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
reason: PeerSessionRecordRefusal;
|
|
65
|
+
field?: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Validate one raw row (parsed JSON, a store row, a wire object) into a typed record — a DETACHED copy
|
|
69
|
+
* (only the declared members are carried; unknown keys are dropped, a newer schema is refused as
|
|
70
|
+
* `schema_newer`, a malformed member names its field). A reader SKIPS a refused row loudly (the host
|
|
71
|
+
* logs the reason) and never adopts a partial one — a half-typed row would let the resolver judge on
|
|
72
|
+
* fabricated liveness.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readPeerSessionRecord(raw: unknown): PeerSessionRecordRead;
|
|
75
|
+
/** What a liveness judgment needs from the OS — injectable so a test never has to spawn a process. */
|
|
76
|
+
export interface PeerLivenessProbe {
|
|
77
|
+
/** Does a process with this pid exist (signal 0 semantics)? */
|
|
78
|
+
processExists(pid: number): boolean;
|
|
79
|
+
/** The process's start time in ms, or `undefined` when the platform cannot say. */
|
|
80
|
+
processStartMs?(pid: number): number | undefined;
|
|
81
|
+
}
|
|
82
|
+
/** The bundled probe: `process.kill(pid, 0)` for existence (EPERM counts as existing — a process this
|
|
83
|
+
* uid may not signal is still a process), no start-time source (the host adapter supplies a platform
|
|
84
|
+
* one; without it the start-time arm is skipped, which is the honest weaker judgment). */
|
|
85
|
+
export declare const defaultPeerLivenessProbe: PeerLivenessProbe;
|
|
86
|
+
/**
|
|
87
|
+
* Is the row's process alive? `false` when the pid is gone, OR when the probe knows a start time and it
|
|
88
|
+
* disagrees with the row's (PID reuse — a different process wears the number). A row with no pid has
|
|
89
|
+
* no process to probe and is judged by its declared liveness alone.
|
|
90
|
+
*/
|
|
91
|
+
export declare function isPeerSessionProcessAlive(record: Pick<PeerSessionRecord, "pid" | "procStartMs" | "liveness">, probe?: PeerLivenessProbe): boolean;
|
|
92
|
+
export interface PeerDirectoryAccess {
|
|
93
|
+
/** The accessor's scope — rows of any other scope are never returned (§10 wall). */
|
|
94
|
+
scope: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The discovery contract (RunnerDeps `peerDirectory`; no seat = the cross-session lane is absent and
|
|
98
|
+
* every face of it stays byte-identical to a pre-385 build). ONE read face: every row the accessor may
|
|
99
|
+
* see — live, dead AND deleted — because the resolution set is the FULL set (a ref minted over the
|
|
100
|
+
* visible-only list would not be unique against a same-named dead row). The engine resolves over the
|
|
101
|
+
* returned rows with {@link resolvePeerSessions}; a host that can resolve server-side may still hand
|
|
102
|
+
* back the full list (the engine's ref minting needs it). Rows a host cannot vouch for (foreign owner,
|
|
103
|
+
* group/world-writable file, newer schema) are SKIPPED by the host's reader, never returned partially.
|
|
104
|
+
*/
|
|
105
|
+
export interface PeerDirectory {
|
|
106
|
+
listPeerSessions(access: PeerDirectoryAccess): readonly PeerSessionRecord[] | Promise<readonly PeerSessionRecord[]>;
|
|
107
|
+
/**
|
|
108
|
+
* design/385 §2.1 (the sweep race, send-side backstop) — restore a row ONLY IF ABSENT (CAS-on-absent:
|
|
109
|
+
* a live row a concurrent registration wrote is never overwritten). The sweep predicate (box empty ⇒
|
|
110
|
+
* remove the dead row) is not transactional with `append`: a sender can resolve a dead row, the sweep
|
|
111
|
+
* can remove it, the append then lands in a box no row addresses. The message is never lost (the
|
|
112
|
+
* recipient drains its box by its OWN session id, and re-registers its row on its next start); what
|
|
113
|
+
* breaks is NAME addressing until then — and this call closes that window: SendMessage re-reads the
|
|
114
|
+
* directory after every append and, when the target row is gone, restores the row it resolved.
|
|
115
|
+
* Optional: a directory that cannot write (a server session table the engine only reads) omits it,
|
|
116
|
+
* and the residual window is disclosed in the receipt instead.
|
|
117
|
+
*/
|
|
118
|
+
restorePeerSession?(record: PeerSessionRecord): boolean | Promise<boolean>;
|
|
119
|
+
}
|
|
120
|
+
export interface PeerSessionCandidate {
|
|
121
|
+
record: PeerSessionRecord;
|
|
122
|
+
/** The listing-minted ref (6..12 hex) of this row — stable across the live/dead filter because it is
|
|
123
|
+
* minted over the FULL set. */
|
|
124
|
+
ref: string;
|
|
125
|
+
}
|
|
126
|
+
export type PeerSessionResolution = {
|
|
127
|
+
status: "found";
|
|
128
|
+
candidate: PeerSessionCandidate;
|
|
129
|
+
rung: "address" | "name" | "ref";
|
|
130
|
+
} | {
|
|
131
|
+
status: "deleted";
|
|
132
|
+
candidate: PeerSessionCandidate;
|
|
133
|
+
} | {
|
|
134
|
+
status: "self";
|
|
135
|
+
} | {
|
|
136
|
+
status: "ambiguous";
|
|
137
|
+
candidates: readonly PeerSessionCandidate[];
|
|
138
|
+
message: string;
|
|
139
|
+
}
|
|
140
|
+
/** A ref was supplied but matches no row of the current set (peer set moved on) — re-list. */
|
|
141
|
+
| {
|
|
142
|
+
status: "stale_ref";
|
|
143
|
+
ref: string;
|
|
144
|
+
} | {
|
|
145
|
+
status: "not_found";
|
|
146
|
+
};
|
|
147
|
+
/** Mint refs for every row of the directory's full set (self participates in disambiguation). */
|
|
148
|
+
export declare function mintPeerSessionCandidates(records: readonly PeerSessionRecord[], self?: {
|
|
149
|
+
sessionId: string;
|
|
150
|
+
}): PeerSessionCandidate[];
|
|
151
|
+
/**
|
|
152
|
+
* Resolve `to` against the directory's full set. Order (design/385 §2.2): an explicit `session.<id>`
|
|
153
|
+
* ADDRESS resolves directly (only to a KNOWN row — the phantom-box defense: an address nobody
|
|
154
|
+
* registered is `not_found`, never an append target); otherwise `name [ref]` — a ref must match its
|
|
155
|
+
* name's row exactly (a ref that resolves nowhere is `stale_ref`: "re-run ListAgents"); a bare name
|
|
156
|
+
* that matches ONE row (live or dead) resolves, several ⇒ `ambiguous` with every candidate's ref (the
|
|
157
|
+
* dead ones marked offline — CC's second ref source, the disambiguation error). A `deleted` row is
|
|
158
|
+
* returned as `deleted` on every rung so the caller refuses with `invalid_target`; the caller's own
|
|
159
|
+
* session is `self`.
|
|
160
|
+
*/
|
|
161
|
+
export declare function resolvePeerSessions(records: readonly PeerSessionRecord[], to: string, self?: {
|
|
162
|
+
sessionId: string;
|
|
163
|
+
}): PeerSessionResolution;
|
|
164
|
+
export interface InMemoryPeerDirectory extends PeerDirectory {
|
|
165
|
+
/** Write or replace a row (keyed by scope + sessionId). A name that spells an address is refused by
|
|
166
|
+
* the caller's own `reservedPeerNameReason` check — this store trusts its writer. */
|
|
167
|
+
upsert(record: PeerSessionRecord): void;
|
|
168
|
+
/** §2.1 dead-row posture: mark, keep (the name stays resolvable for offline delivery). */
|
|
169
|
+
markDead(scope: string, sessionId: string, diedAt?: number): boolean;
|
|
170
|
+
/** §1.2⑤ tombstone: the deletion cascade flips the row BEFORE dropping the box. */
|
|
171
|
+
markDeleted(scope: string, sessionId: string, at?: number): boolean;
|
|
172
|
+
/** §2.1 sweep predicate: a non-live row is removable ONLY while its box is EMPTY (the box-empty
|
|
173
|
+
* predicate is the caller's — it reads the mailbox). Returns the swept session ids. */
|
|
174
|
+
sweep(scope: string, boxEmpty: (sessionId: string) => boolean | Promise<boolean>): Promise<string[]>;
|
|
175
|
+
restorePeerSession(record: PeerSessionRecord): boolean;
|
|
176
|
+
}
|
|
177
|
+
export declare function createInMemoryPeerDirectory(): InMemoryPeerDirectory;
|
|
178
|
+
/** The closed refusal set of the registry-directory ancestor-chain vetting. Each ancestor from the
|
|
179
|
+
* directory up to the filesystem root must be a real directory (no symlink on the chain — a shared
|
|
180
|
+
* /tmp lets another uid pre-plant one), owned by this uid, and not group/world-writable. */
|
|
181
|
+
export type PeerRegistryDirectoryRefusal = "not_absolute" | "missing" | "symlink" | "not_directory" | "foreign_owner" | "group_or_world_writable";
|
|
182
|
+
export type PeerRegistryDirectoryVerdict = {
|
|
183
|
+
ok: true;
|
|
184
|
+
} | {
|
|
185
|
+
ok: false;
|
|
186
|
+
code: PeerRegistryDirectoryRefusal;
|
|
187
|
+
path: string;
|
|
188
|
+
};
|
|
189
|
+
/** Vet a registry directory and every ancestor. `uid` defaults to the process uid; on a platform with
|
|
190
|
+
* no uid (win32) the owner arm is skipped. Refuses on the FIRST failing ancestor (closest first). */
|
|
191
|
+
export declare function vetPeerRegistryDirectory(dir: string, opts?: {
|
|
192
|
+
uid?: number;
|
|
193
|
+
stopAt?: string;
|
|
194
|
+
}): Promise<PeerRegistryDirectoryVerdict>;
|
|
195
|
+
/** §2.1 ③ — the read-side judgment on ONE registration file's stat facts (pure; the reader supplies
|
|
196
|
+
* the stat): owner must be this uid, mode must carry no group/world write bit, and it must be a
|
|
197
|
+
* regular file under the read cap. A refused file is skipped LOUDLY by the reader. */
|
|
198
|
+
export declare function judgePeerRecordFile(stat: {
|
|
199
|
+
uid?: number;
|
|
200
|
+
mode: number;
|
|
201
|
+
isFile: boolean;
|
|
202
|
+
size: number;
|
|
203
|
+
}, selfUid: number | undefined): {
|
|
204
|
+
ok: true;
|
|
205
|
+
} | {
|
|
206
|
+
ok: false;
|
|
207
|
+
code: "not_regular" | "foreign_owner" | "group_or_world_writable" | "too_large";
|
|
208
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, dirname, resolve as resolvePath } from "node:path";
|
|
3
|
+
import { formatPeerNameRef, mintPeerRef, normalizePeerName, parsePeerNameRef, PEER_REF_RE } from "./cross-session-ref.js";
|
|
4
|
+
import { inlineUntrusted } from "../core/untrusted-text.js";
|
|
5
|
+
export const PEER_SESSION_RECORD_SCHEMA_VERSION = 1;
|
|
6
|
+
export const PEER_SESSION_RECORD_MAX_BYTES = 262_144;
|
|
7
|
+
export const SESSION_BOX_PREFIX = "session.";
|
|
8
|
+
const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,80}$/;
|
|
9
|
+
export const PEER_SESSION_ID_GRAMMAR = "^[A-Za-z0-9_-]{1,80}$";
|
|
10
|
+
export function isPeerSessionId(sessionId) {
|
|
11
|
+
return SESSION_ID_RE.test(sessionId);
|
|
12
|
+
}
|
|
13
|
+
const SESSION_ADDRESS_RE = /^session\.([A-Za-z0-9_-]{1,80})$/i;
|
|
14
|
+
export function peerSessionBoxHandle(sessionId) {
|
|
15
|
+
if (!SESSION_ID_RE.test(sessionId))
|
|
16
|
+
throw new Error(`peerSessionBoxHandle: session id ${JSON.stringify(sessionId)} is outside the address grammar ^[A-Za-z0-9_-]{1,80}$`);
|
|
17
|
+
return `${SESSION_BOX_PREFIX}${sessionId.toLowerCase()}`;
|
|
18
|
+
}
|
|
19
|
+
export function parsePeerSessionAddress(text) {
|
|
20
|
+
const m = SESSION_ADDRESS_RE.exec(text.trim());
|
|
21
|
+
return m === null ? undefined : m[1].toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function optNumber(v) {
|
|
24
|
+
return v === undefined || (typeof v === "number" && Number.isFinite(v));
|
|
25
|
+
}
|
|
26
|
+
function optString(v) {
|
|
27
|
+
return v === undefined || typeof v === "string";
|
|
28
|
+
}
|
|
29
|
+
function optStringArray(v) {
|
|
30
|
+
return v === undefined || (Array.isArray(v) && v.every((x) => typeof x === "string"));
|
|
31
|
+
}
|
|
32
|
+
export function readPeerSessionRecord(raw) {
|
|
33
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
34
|
+
return { ok: false, reason: "not_an_object" };
|
|
35
|
+
const r = raw;
|
|
36
|
+
if (typeof r["schemaVersion"] !== "number" || !Number.isInteger(r["schemaVersion"]) || r["schemaVersion"] < 1)
|
|
37
|
+
return { ok: false, reason: "schema_missing" };
|
|
38
|
+
if (r["schemaVersion"] > PEER_SESSION_RECORD_SCHEMA_VERSION)
|
|
39
|
+
return { ok: false, reason: "schema_newer" };
|
|
40
|
+
if (typeof r["sessionId"] !== "string" || !SESSION_ID_RE.test(r["sessionId"]))
|
|
41
|
+
return { ok: false, reason: "session_id_invalid" };
|
|
42
|
+
if (typeof r["scope"] !== "string" || r["scope"] === "")
|
|
43
|
+
return { ok: false, reason: "scope_invalid" };
|
|
44
|
+
if (typeof r["name"] !== "string" || normalizePeerName(r["name"]) === "")
|
|
45
|
+
return { ok: false, reason: "name_invalid" };
|
|
46
|
+
const liveness = r["liveness"];
|
|
47
|
+
if (liveness !== "live" && liveness !== "dead" && liveness !== "deleted")
|
|
48
|
+
return { ok: false, reason: "liveness_invalid" };
|
|
49
|
+
const bad = (field) => ({ ok: false, reason: "field_invalid", field });
|
|
50
|
+
if (!optNumber(r["startedAt"]) || r["startedAt"] === undefined)
|
|
51
|
+
return bad("startedAt");
|
|
52
|
+
if (!optNumber(r["updatedAt"]) || r["updatedAt"] === undefined)
|
|
53
|
+
return bad("updatedAt");
|
|
54
|
+
if (!optNumber(r["pid"]))
|
|
55
|
+
return bad("pid");
|
|
56
|
+
if (!optNumber(r["procStartMs"]))
|
|
57
|
+
return bad("procStartMs");
|
|
58
|
+
if (!optNumber(r["diedAt"]))
|
|
59
|
+
return bad("diedAt");
|
|
60
|
+
if (!optNumber(r["peerProtocol"]))
|
|
61
|
+
return bad("peerProtocol");
|
|
62
|
+
if (!optString(r["cwd"]))
|
|
63
|
+
return bad("cwd");
|
|
64
|
+
if (!optString(r["entrypoint"]))
|
|
65
|
+
return bad("entrypoint");
|
|
66
|
+
if (!optString(r["sockPath"]))
|
|
67
|
+
return bad("sockPath");
|
|
68
|
+
if (!optStringArray(r["formerNames"]))
|
|
69
|
+
return bad("formerNames");
|
|
70
|
+
if (!optStringArray(r["peerFeatures"]))
|
|
71
|
+
return bad("peerFeatures");
|
|
72
|
+
const nameSource = r["nameSource"];
|
|
73
|
+
if (nameSource !== undefined && nameSource !== "auto" && nameSource !== "flag" && nameSource !== "rename")
|
|
74
|
+
return bad("nameSource");
|
|
75
|
+
const tempo = r["tempo"];
|
|
76
|
+
if (tempo !== undefined && tempo !== "active" && tempo !== "idle" && tempo !== "blocked")
|
|
77
|
+
return bad("tempo");
|
|
78
|
+
const inboundPosture = r["inboundPosture"];
|
|
79
|
+
if (inboundPosture !== undefined && inboundPosture !== "available" && inboundPosture !== "unavailable")
|
|
80
|
+
return bad("inboundPosture");
|
|
81
|
+
const record = {
|
|
82
|
+
schemaVersion: r["schemaVersion"],
|
|
83
|
+
sessionId: r["sessionId"],
|
|
84
|
+
scope: r["scope"],
|
|
85
|
+
name: r["name"],
|
|
86
|
+
...(nameSource !== undefined ? { nameSource } : {}),
|
|
87
|
+
...(r["formerNames"] !== undefined ? { formerNames: [...r["formerNames"]] } : {}),
|
|
88
|
+
...(r["pid"] !== undefined ? { pid: r["pid"] } : {}),
|
|
89
|
+
...(r["procStartMs"] !== undefined ? { procStartMs: r["procStartMs"] } : {}),
|
|
90
|
+
...(r["cwd"] !== undefined ? { cwd: r["cwd"] } : {}),
|
|
91
|
+
startedAt: r["startedAt"],
|
|
92
|
+
updatedAt: r["updatedAt"],
|
|
93
|
+
liveness,
|
|
94
|
+
...(r["diedAt"] !== undefined ? { diedAt: r["diedAt"] } : {}),
|
|
95
|
+
...(tempo !== undefined ? { tempo } : {}),
|
|
96
|
+
...(r["entrypoint"] !== undefined ? { entrypoint: r["entrypoint"] } : {}),
|
|
97
|
+
...(r["peerProtocol"] !== undefined ? { peerProtocol: r["peerProtocol"] } : {}),
|
|
98
|
+
...(r["peerFeatures"] !== undefined ? { peerFeatures: [...r["peerFeatures"]] } : {}),
|
|
99
|
+
...(r["sockPath"] !== undefined ? { sockPath: r["sockPath"] } : {}),
|
|
100
|
+
...(inboundPosture !== undefined ? { inboundPosture } : {}),
|
|
101
|
+
};
|
|
102
|
+
return { ok: true, record };
|
|
103
|
+
}
|
|
104
|
+
export const defaultPeerLivenessProbe = {
|
|
105
|
+
processExists(pid) {
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, 0);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
return e.code === "EPERM";
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
export function isPeerSessionProcessAlive(record, probe = defaultPeerLivenessProbe) {
|
|
116
|
+
if (record.liveness !== "live")
|
|
117
|
+
return false;
|
|
118
|
+
if (record.pid === undefined)
|
|
119
|
+
return true;
|
|
120
|
+
if (!probe.processExists(record.pid))
|
|
121
|
+
return false;
|
|
122
|
+
if (record.procStartMs !== undefined && probe.processStartMs !== undefined) {
|
|
123
|
+
const started = probe.processStartMs(record.pid);
|
|
124
|
+
if (started !== undefined && Math.abs(started - record.procStartMs) > 2_000)
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
export function mintPeerSessionCandidates(records, self) {
|
|
130
|
+
const entries = records.map((record) => ({ kind: "session", id: record.sessionId.toLowerCase(), record }));
|
|
131
|
+
const selfEntry = self !== undefined && !records.some((r) => r.sessionId.toLowerCase() === self.sessionId.toLowerCase()) ? { kind: "session", id: self.sessionId.toLowerCase() } : undefined;
|
|
132
|
+
return mintPeerRef(entries, selfEntry).map((e) => ({ record: e.record, ref: e.ref }));
|
|
133
|
+
}
|
|
134
|
+
function describeCandidate(c) {
|
|
135
|
+
const state = c.record.liveness === "live" ? "" : c.record.liveness === "dead" ? " (offline)" : " (deleted)";
|
|
136
|
+
return `${formatPeerNameRef(inlineUntrusted(c.record.name, 64), c.ref)}${state} → ${peerSessionBoxHandle(c.record.sessionId)}`;
|
|
137
|
+
}
|
|
138
|
+
export function resolvePeerSessions(records, to, self) {
|
|
139
|
+
const candidates = mintPeerSessionCandidates(records, self);
|
|
140
|
+
const selfId = self?.sessionId.toLowerCase();
|
|
141
|
+
const settle = (c, rung) => {
|
|
142
|
+
if (selfId !== undefined && c.record.sessionId.toLowerCase() === selfId)
|
|
143
|
+
return { status: "self" };
|
|
144
|
+
if (c.record.liveness === "deleted")
|
|
145
|
+
return { status: "deleted", candidate: c };
|
|
146
|
+
return { status: "found", candidate: c, rung };
|
|
147
|
+
};
|
|
148
|
+
const address = parsePeerSessionAddress(to);
|
|
149
|
+
if (address !== undefined) {
|
|
150
|
+
if (selfId !== undefined && address === selfId)
|
|
151
|
+
return { status: "self" };
|
|
152
|
+
const hit = candidates.find((c) => c.record.sessionId.toLowerCase() === address);
|
|
153
|
+
return hit === undefined ? { status: "not_found" } : settle(hit, "address");
|
|
154
|
+
}
|
|
155
|
+
const nameRef = parsePeerNameRef(to);
|
|
156
|
+
const rawName = nameRef?.name ?? to;
|
|
157
|
+
const key = normalizePeerName(rawName);
|
|
158
|
+
if (key === "")
|
|
159
|
+
return { status: "not_found" };
|
|
160
|
+
const byName = candidates.filter((c) => normalizePeerName(c.record.name) === key);
|
|
161
|
+
if (nameRef !== undefined) {
|
|
162
|
+
const hit = byName.find((c) => c.ref === nameRef.ref) ?? (PEER_REF_RE.test(nameRef.ref) ? candidates.find((c) => c.ref === nameRef.ref && normalizePeerName(c.record.name) === key) : undefined);
|
|
163
|
+
if (hit === undefined)
|
|
164
|
+
return { status: "stale_ref", ref: nameRef.ref };
|
|
165
|
+
return settle(hit, "ref");
|
|
166
|
+
}
|
|
167
|
+
if (byName.length === 0)
|
|
168
|
+
return { status: "not_found" };
|
|
169
|
+
if (byName.length === 1)
|
|
170
|
+
return settle(byName[0], "name");
|
|
171
|
+
const sorted = [...byName].sort((a, b) => (a.record.liveness === b.record.liveness ? b.record.updatedAt - a.record.updatedAt : a.record.liveness === "live" ? -1 : 1));
|
|
172
|
+
return {
|
|
173
|
+
status: "ambiguous",
|
|
174
|
+
candidates: sorted,
|
|
175
|
+
message: `"${inlineUntrusted(rawName, 64)}" matches ${sorted.length} sessions — re-send with the name and ref of the one you mean: ${sorted.map(describeCandidate).join("; ")}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function createInMemoryPeerDirectory() {
|
|
179
|
+
const rows = new Map();
|
|
180
|
+
const key = (scope, sessionId) => JSON.stringify([scope, sessionId.toLowerCase()]);
|
|
181
|
+
return {
|
|
182
|
+
listPeerSessions(access) {
|
|
183
|
+
return [...rows.values()].filter((r) => r.scope === access.scope).map((r) => ({ ...r }));
|
|
184
|
+
},
|
|
185
|
+
upsert(record) {
|
|
186
|
+
const read = readPeerSessionRecord(record);
|
|
187
|
+
if (!read.ok)
|
|
188
|
+
throw new Error(`InMemoryPeerDirectory.upsert: refused row (${read.reason}${read.field !== undefined ? `: ${read.field}` : ""})`);
|
|
189
|
+
rows.set(key(record.scope, record.sessionId), read.record);
|
|
190
|
+
},
|
|
191
|
+
markDead(scope, sessionId, diedAt = Date.now()) {
|
|
192
|
+
const r = rows.get(key(scope, sessionId));
|
|
193
|
+
if (r === undefined || r.liveness !== "live")
|
|
194
|
+
return false;
|
|
195
|
+
rows.set(key(scope, sessionId), { ...r, liveness: "dead", diedAt, updatedAt: diedAt });
|
|
196
|
+
return true;
|
|
197
|
+
},
|
|
198
|
+
markDeleted(scope, sessionId, at = Date.now()) {
|
|
199
|
+
const r = rows.get(key(scope, sessionId));
|
|
200
|
+
if (r === undefined)
|
|
201
|
+
return false;
|
|
202
|
+
rows.set(key(scope, sessionId), { ...r, liveness: "deleted", diedAt: r.diedAt ?? at, updatedAt: at });
|
|
203
|
+
return true;
|
|
204
|
+
},
|
|
205
|
+
restorePeerSession(record) {
|
|
206
|
+
const k = key(record.scope, record.sessionId);
|
|
207
|
+
if (rows.has(k))
|
|
208
|
+
return false;
|
|
209
|
+
const read = readPeerSessionRecord(record);
|
|
210
|
+
if (!read.ok)
|
|
211
|
+
return false;
|
|
212
|
+
rows.set(k, read.record);
|
|
213
|
+
return true;
|
|
214
|
+
},
|
|
215
|
+
async sweep(scope, boxEmpty) {
|
|
216
|
+
const swept = [];
|
|
217
|
+
for (const [k, r] of [...rows.entries()]) {
|
|
218
|
+
if (r.scope !== scope || r.liveness === "live")
|
|
219
|
+
continue;
|
|
220
|
+
if (await boxEmpty(r.sessionId)) {
|
|
221
|
+
if (rows.get(k) !== r)
|
|
222
|
+
continue;
|
|
223
|
+
rows.delete(k);
|
|
224
|
+
swept.push(r.sessionId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return swept;
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
export async function vetPeerRegistryDirectory(dir, opts) {
|
|
232
|
+
if (!isAbsolute(dir))
|
|
233
|
+
return { ok: false, code: "not_absolute", path: dir };
|
|
234
|
+
const uid = opts?.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
|
|
235
|
+
const stopAt = opts?.stopAt !== undefined ? resolvePath(opts.stopAt) : undefined;
|
|
236
|
+
let cursor = resolvePath(dir);
|
|
237
|
+
for (;;) {
|
|
238
|
+
let st;
|
|
239
|
+
try {
|
|
240
|
+
st = await lstat(cursor);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return { ok: false, code: "missing", path: cursor };
|
|
244
|
+
}
|
|
245
|
+
if (st.isSymbolicLink())
|
|
246
|
+
return { ok: false, code: "symlink", path: cursor };
|
|
247
|
+
if (!st.isDirectory())
|
|
248
|
+
return { ok: false, code: "not_directory", path: cursor };
|
|
249
|
+
const parent = dirname(cursor);
|
|
250
|
+
const isRoot = parent === cursor || (stopAt !== undefined && cursor === stopAt);
|
|
251
|
+
if (!isRoot) {
|
|
252
|
+
if (uid !== undefined && st.uid !== uid)
|
|
253
|
+
return { ok: false, code: "foreign_owner", path: cursor };
|
|
254
|
+
if ((st.mode & 0o022) !== 0)
|
|
255
|
+
return { ok: false, code: "group_or_world_writable", path: cursor };
|
|
256
|
+
}
|
|
257
|
+
if (isRoot)
|
|
258
|
+
return { ok: true };
|
|
259
|
+
cursor = parent;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function judgePeerRecordFile(stat, selfUid) {
|
|
263
|
+
if (!stat.isFile)
|
|
264
|
+
return { ok: false, code: "not_regular" };
|
|
265
|
+
if (selfUid !== undefined && stat.uid !== undefined && stat.uid !== selfUid)
|
|
266
|
+
return { ok: false, code: "foreign_owner" };
|
|
267
|
+
if ((stat.mode & 0o022) !== 0)
|
|
268
|
+
return { ok: false, code: "group_or_world_writable" };
|
|
269
|
+
if (stat.size > PEER_SESSION_RECORD_MAX_BYTES)
|
|
270
|
+
return { ok: false, code: "too_large" };
|
|
271
|
+
return { ok: true };
|
|
272
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { type MailboxCrossProcessVerdict, type MailboxStore } from "../core/mailbox-store.js";
|
|
2
|
+
import type { SystemInjectionPriority, TaskNotificationPayload } from "../core/task-notification.js";
|
|
3
|
+
import { type EngineNotice, type RunnerDeps } from "../core/types.js";
|
|
4
|
+
import { type PermissionModeClass } from "./cross-session-envelope.js";
|
|
5
|
+
import { type CrossSessionInboundSettingLayers } from "./cross-session-judge.js";
|
|
6
|
+
import { type PeerAdmissionConfig } from "./peer-admission.js";
|
|
7
|
+
import { type PeerDirectory } from "./peer-directory.js";
|
|
8
|
+
import { createListAgentsTool } from "./list-agents-tool.js";
|
|
9
|
+
import type { AskEffective } from "../core/wiring-manifest.js";
|
|
10
|
+
import type { SendMessageToolOptions } from "./send-message-tool.js";
|
|
11
|
+
/** design/385 §1.2④ — the drain lease TTL. Same sizing law as the tier-3 revive lease: well under the
|
|
12
|
+
* store's stale-row floor, long enough to cover one turn (a longer turn renews on the same owner at
|
|
13
|
+
* its next boundary; an expired lease re-serves the batch — at-least-once). */
|
|
14
|
+
export declare const PEER_DRAIN_LEASE_TTL_MS: number;
|
|
15
|
+
/** The lane-mount refusal for a run whose own session id cannot be spelled as a peer address. */
|
|
16
|
+
export declare const PEER_SESSION_ID_UNGRAMMATICAL_CODE = "peer.session_id_ungrammatical";
|
|
17
|
+
/** The lane's mount verdict: the mailbox declaration's verdict, or this leg's own address refusal. */
|
|
18
|
+
export type PeerLaneMountVerdict = MailboxCrossProcessVerdict | {
|
|
19
|
+
ok: false;
|
|
20
|
+
code: typeof PEER_SESSION_ID_UNGRAMMATICAL_CODE;
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* design/385 §1.2⑥ / §5.1 — may the cross-session lane mount on this deployment? `undefined` = no
|
|
25
|
+
* directory seat (no lane, nothing to say). A directory with no mailbox, or over a mailbox that does not
|
|
26
|
+
* declare cross-process safety, is REFUSED with a named reason (bad-value loudness: several terminals
|
|
27
|
+
* sharing one box on luck is exactly what the declaration exists to forbid). With a `sessionId`, the
|
|
28
|
+
* leg's OWN address is judged too: a host-minted session id outside the peer address grammar has no
|
|
29
|
+
* session box to drain and no address a peer could send to, so the lane refuses by name — the
|
|
30
|
+
* alternative was `peerSessionBoxHandle` throwing out of prepare and failing the whole run over an
|
|
31
|
+
* optional feature.
|
|
32
|
+
*/
|
|
33
|
+
export declare function judgePeerLaneMount(deps: {
|
|
34
|
+
peerDirectory?: PeerDirectory;
|
|
35
|
+
mailboxStore?: MailboxStore;
|
|
36
|
+
}, sessionId?: string): PeerLaneMountVerdict | undefined;
|
|
37
|
+
export interface PeerSessionDrainOptions {
|
|
38
|
+
mailbox: MailboxStore;
|
|
39
|
+
/** This session (the box owner) and this leg's cycle id (the lease owner). */
|
|
40
|
+
sessionId: string;
|
|
41
|
+
runId: string;
|
|
42
|
+
/** The registry/mailbox scope this run mounts in. */
|
|
43
|
+
scope: string;
|
|
44
|
+
/** The run's own notification injector (the task-notification lane; `next` tier). */
|
|
45
|
+
inject: (notification: TaskNotificationPayload, opts?: {
|
|
46
|
+
priority?: SystemInjectionPriority;
|
|
47
|
+
}) => void;
|
|
48
|
+
/** Deployment tuning of the admission gate (the same seat SendMessage reads). */
|
|
49
|
+
admission?: Partial<PeerAdmissionConfig>;
|
|
50
|
+
/** The recipient's self-token set for the hop check, read live (the run's peer identity ref). */
|
|
51
|
+
ownTokens: () => readonly string[];
|
|
52
|
+
/** The recipient's `crossSessionInbound` layers, read at every drain. */
|
|
53
|
+
settingLayers: () => CrossSessionInboundSettingLayers | undefined;
|
|
54
|
+
/** The recipient's own permission-mode class, read at every drain (the manifest's ask fold). */
|
|
55
|
+
selfModeClass: () => PermissionModeClass | "unknown";
|
|
56
|
+
onNotice?: (notice: EngineNotice) => void;
|
|
57
|
+
onError?: (error: unknown, ctx: {
|
|
58
|
+
phase: "degraded";
|
|
59
|
+
sessionId: string;
|
|
60
|
+
classification: string;
|
|
61
|
+
}) => void;
|
|
62
|
+
leaseTtlMs?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface PeerSessionDrain {
|
|
65
|
+
/** One drain round (serialized: a boundary never overlaps a still-running round). */
|
|
66
|
+
drain(): Promise<void>;
|
|
67
|
+
/** Run end: ack the last batch and release the lease so the session's next cycle claims at once. */
|
|
68
|
+
finish(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
export declare function createPeerSessionDrain(opts: PeerSessionDrainOptions): PeerSessionDrain;
|
|
71
|
+
/** The lane's late-bound facts: the ask fold resolves after the manifest derivation, the mount runs
|
|
72
|
+
* before it — both read this one cell. */
|
|
73
|
+
export interface PeerLaneRefs {
|
|
74
|
+
askEffective?: AskEffective;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The mount verdict + its loud half: `true` = the lane mounts on this leg; `false` = no seat, or the
|
|
78
|
+
* seat was refused — the refusal is announced ONCE per leg as `config.peer_lane_unmounted` naming the
|
|
79
|
+
* reason (never mounted on luck, never silently inert).
|
|
80
|
+
*/
|
|
81
|
+
export declare function announcePeerLaneMount(deps: {
|
|
82
|
+
peerDirectory?: PeerDirectory;
|
|
83
|
+
mailboxStore?: MailboxStore;
|
|
84
|
+
onNotice?: (n: EngineNotice) => void;
|
|
85
|
+
}, leg: {
|
|
86
|
+
sessionId: string;
|
|
87
|
+
runId: string;
|
|
88
|
+
}): boolean;
|
|
89
|
+
/** The SendMessage seats of the lane (design/385 §2): the directory (address arm + last name rung) and
|
|
90
|
+
* this session's identity/attestation, whose mode class folds LIVE off the manifest's ask derivation. */
|
|
91
|
+
export declare function peerLaneSendMessageSeats(args: {
|
|
92
|
+
peerDirectory: PeerDirectory;
|
|
93
|
+
sessionId: string;
|
|
94
|
+
scope: string;
|
|
95
|
+
name?: string;
|
|
96
|
+
refs: PeerLaneRefs;
|
|
97
|
+
listingMounted?: boolean;
|
|
98
|
+
}): Pick<SendMessageToolOptions, "peerDirectory" | "peerSelfSession" | "peerListingMounted">;
|
|
99
|
+
/**
|
|
100
|
+
* design/385 §5.1 — THE ListAgents mount predicate, single source: the built-in mounts only when the
|
|
101
|
+
* face does not exclude it and no caller tool owns either spelling (`ListAgents` / `ListPeers`) as its
|
|
102
|
+
* name OR as an alias. Every reader of "is ListAgents on this roster?" — the mount itself and the
|
|
103
|
+
* SendMessage face's listing phrase — calls this one predicate, so a face can never name a tool the
|
|
104
|
+
* roster does not carry (a second, inline copy of the rule drifted on exactly the alias arms).
|
|
105
|
+
*/
|
|
106
|
+
export declare function listAgentsMountable(face: {
|
|
107
|
+
exclude: readonly string[] | undefined;
|
|
108
|
+
specTools: ReadonlyArray<{
|
|
109
|
+
name: string;
|
|
110
|
+
aliases?: readonly string[];
|
|
111
|
+
}>;
|
|
112
|
+
}): boolean;
|
|
113
|
+
/** design/385 §5.1 — the ListAgents mount (alias ListPeers): read-only, concurrency-safe, on the peer
|
|
114
|
+
* seat's own arm beside SendMessage; a caller-supplied tool of the same name wins (skip, not throw). */
|
|
115
|
+
export declare function mountListAgents(args: {
|
|
116
|
+
peerDirectory: PeerDirectory;
|
|
117
|
+
sessionId: string;
|
|
118
|
+
scope: string;
|
|
119
|
+
hostTaskId: string;
|
|
120
|
+
/** The delegated child's trusted parent pair (RunInternals), when this run is a child. */
|
|
121
|
+
parentTaskId?: string;
|
|
122
|
+
parentSessionId?: string;
|
|
123
|
+
registry: ListAgentsToolRegistry;
|
|
124
|
+
roster?: NonNullable<Parameters<typeof createListAgentsTool>[0]["roster"]>;
|
|
125
|
+
specTools: ReadonlyArray<{
|
|
126
|
+
name: string;
|
|
127
|
+
aliases?: readonly string[];
|
|
128
|
+
}>;
|
|
129
|
+
toolEffects: Map<string, "read" | "write" | "idempotent">;
|
|
130
|
+
}): ReturnType<typeof createListAgentsTool> | undefined;
|
|
131
|
+
type ListAgentsToolRegistry = NonNullable<Parameters<typeof createListAgentsTool>[0]["registry"]>;
|
|
132
|
+
/** The harness surface the binding needs (structural — the machine itself never sees the harness). */
|
|
133
|
+
export interface PeerDrainHarness {
|
|
134
|
+
on(type: "turn_boundary" | "before_agent_start", handler: () => Promise<undefined>): unknown;
|
|
135
|
+
subscribe(listener: (event: {
|
|
136
|
+
type: string;
|
|
137
|
+
}) => void | Promise<void>): unknown;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Bind the drain to a leg: the run's own injector at the `next` tier, run open (the loop's first
|
|
141
|
+
* steering drain follows the `before_agent_start` hook) + every turn boundary, and the run-end
|
|
142
|
+
* ack/release on `agent_end`.
|
|
143
|
+
*/
|
|
144
|
+
export declare function bindPeerSessionDrain(harness: PeerDrainHarness, opts: PeerSessionDrainOptions): PeerSessionDrain;
|
|
145
|
+
/**
|
|
146
|
+
* The leg-side spelling of {@link bindPeerSessionDrain}: the deployment seats the drain reads
|
|
147
|
+
* (`mailboxStore` / `peerAdmission` / `crossSessionInbound` / the two sinks) come straight off deps, the
|
|
148
|
+
* recipient's mode class folds off the lane refs — one binding call per leg, no per-seat plumbing.
|
|
149
|
+
*/
|
|
150
|
+
export declare function bindPeerLaneDrain(harness: PeerDrainHarness, args: {
|
|
151
|
+
deps: Pick<RunnerDeps, "mailboxStore" | "peerAdmission" | "crossSessionInbound" | "onNotice" | "onError">;
|
|
152
|
+
sessionId: string;
|
|
153
|
+
runId: string;
|
|
154
|
+
scope: string;
|
|
155
|
+
inject: PeerSessionDrainOptions["inject"];
|
|
156
|
+
ownTokens: () => readonly string[];
|
|
157
|
+
refs: PeerLaneRefs;
|
|
158
|
+
}): PeerSessionDrain;
|
|
159
|
+
export {};
|