@agfpd/iapeer 0.4.43 → 0.4.45
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/package.json +1 -1
- package/src/daemon/index.ts +6 -1
- package/src/identity/index.ts +55 -1
- package/src/registry/index.ts +7 -1
- package/src/transport/index.ts +29 -16
package/package.json
CHANGED
package/src/daemon/index.ts
CHANGED
|
@@ -200,6 +200,9 @@ function errResult(text: string): ToolResult {
|
|
|
200
200
|
/** Injected seams for callTool — all OPTIONAL, all wired by the composition point
|
|
201
201
|
* (daemon/main.ts); library/test callers omit them and stay hermetic. */
|
|
202
202
|
export interface CallToolDeps {
|
|
203
|
+
/** Б7 — injected env for the request-path registry read + routeSend (daemon:286 isolation). Default
|
|
204
|
+
* process.env; the production daemon threads its startDaemon env here. */
|
|
205
|
+
env?: NodeJS.ProcessEnv
|
|
203
206
|
/** Wake-on-miss primitive (Ф2) — see StartDaemonOptions.wake. */
|
|
204
207
|
wake?: WakeFn
|
|
205
208
|
/** Per-delivery outcome log dir (Ф-#8a) — see StartDaemonOptions.deliveryLogDir. */
|
|
@@ -243,6 +246,7 @@ export async function callTool(
|
|
|
243
246
|
}
|
|
244
247
|
const t0 = Date.now()
|
|
245
248
|
const sent = await routeSend(caller, input, {
|
|
249
|
+
env: deps.env,
|
|
246
250
|
wake: deps.wake,
|
|
247
251
|
ephemeral: deps.ephemeral,
|
|
248
252
|
noteLiveTopic: deps.noteLiveTopic,
|
|
@@ -311,7 +315,7 @@ export function createMcpServer(deps: CallToolDeps = {}): Server {
|
|
|
311
315
|
const headerIdentity = headerFromRequestInfo(extra as never)
|
|
312
316
|
let caller: ResolvedCaller
|
|
313
317
|
try {
|
|
314
|
-
caller = resolveCallerFromHeader(headerIdentity, readPeersIndex())
|
|
318
|
+
caller = resolveCallerFromHeader(headerIdentity, readPeersIndex({ env: deps.env }))
|
|
315
319
|
} catch (e) {
|
|
316
320
|
// No silent default — reject the CallTool when identity is missing/invalid.
|
|
317
321
|
if (process.env.IAPEER_DAEMON_LOG) {
|
|
@@ -459,6 +463,7 @@ export async function startDaemon(opts: StartDaemonOptions = {}): Promise<Daemon
|
|
|
459
463
|
return
|
|
460
464
|
}
|
|
461
465
|
const server = createMcpServer({
|
|
466
|
+
env: opts.env, // Б7 — thread the daemon's storage env into the request path (registry read + routeSend)
|
|
462
467
|
wake: opts.wake,
|
|
463
468
|
deliveryLogDir: opts.deliveryLogDir,
|
|
464
469
|
onDelivered: opts.onDelivered,
|
package/src/identity/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// wipe description without an explicit new value). Path helpers moved to storage.
|
|
7
7
|
|
|
8
8
|
import { basename, join, resolve } from 'path'
|
|
9
|
-
import { existsSync, readFileSync, realpathSync } from 'fs'
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmdirSync, statSync } from 'fs'
|
|
10
10
|
import {
|
|
11
11
|
IAPEER_DIR,
|
|
12
12
|
PEER_PROFILE_FILE,
|
|
@@ -251,10 +251,64 @@ export function readPeerProfile(cwd: string = process.cwd()): PeerProfile | null
|
|
|
251
251
|
* The profile file (basename peer-profile.json) is allowed through
|
|
252
252
|
* storage.writeFileAtomic; only peers-profiles.json is guarded there.
|
|
253
253
|
*/
|
|
254
|
+
const PROFILE_LOCK_WAIT_MS = 2000
|
|
255
|
+
const PROFILE_LOCK_STALE_MS = 10_000
|
|
256
|
+
|
|
257
|
+
/** В31 — serialize the read-merge-write of a peer's local profile so concurrent FOUNDATION writers
|
|
258
|
+
* (verify --fix, the `default-runtime` verb, ensurePeerProfile at re-provision) don't lose each other's
|
|
259
|
+
* owner sections via a read-modify-write race. A sync mkdir-lock: writePeerProfileAtomic is sync and off
|
|
260
|
+
* the delivery hot path (rare operator calls). Best-effort — a stale lock is reclaimed, and on timeout it
|
|
261
|
+
* proceeds WITHOUT the lock so it can never deadlock. CROSS-PACKAGE CAVEAT: telegram-runtime writes this
|
|
262
|
+
* file with its OWN code and does not take this lock; full safety needs it to adopt the same lock (a
|
|
263
|
+
* separate cross-package contract — this closes the foundation-vs-foundation window). */
|
|
264
|
+
function withProfileLockSync<T>(cwd: string, fn: () => T): T {
|
|
265
|
+
const lockDir = join(cwd, '.iapeer', '.peer-profile.lock')
|
|
266
|
+
const deadline = Date.now() + PROFILE_LOCK_WAIT_MS
|
|
267
|
+
let held = false
|
|
268
|
+
for (;;) {
|
|
269
|
+
try {
|
|
270
|
+
mkdirSync(lockDir)
|
|
271
|
+
held = true
|
|
272
|
+
break
|
|
273
|
+
} catch (e) {
|
|
274
|
+
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') break // fs issue → proceed unlocked (best-effort)
|
|
275
|
+
try {
|
|
276
|
+
if (Date.now() - statSync(lockDir).mtimeMs > PROFILE_LOCK_STALE_MS) {
|
|
277
|
+
rmdirSync(lockDir)
|
|
278
|
+
continue // reclaimed a crashed writer's stale lock → retry immediately
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
/* the lock vanished between checks — retry */
|
|
282
|
+
}
|
|
283
|
+
if (Date.now() > deadline) break // give up waiting → proceed unlocked (never deadlock)
|
|
284
|
+
try {
|
|
285
|
+
;(globalThis as { Bun?: { sleepSync(ms: number): void } }).Bun?.sleepSync(15)
|
|
286
|
+
} catch {
|
|
287
|
+
/* */
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
return fn()
|
|
293
|
+
} finally {
|
|
294
|
+
if (held)
|
|
295
|
+
try {
|
|
296
|
+
rmdirSync(lockDir)
|
|
297
|
+
} catch {
|
|
298
|
+
/* */
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
254
303
|
export function writePeerProfileAtomic(cwd: string, profile: PeerProfileWrite): void {
|
|
255
304
|
ensureLocalIapScaffold(cwd)
|
|
256
305
|
const path = peerProfilePath(cwd)
|
|
306
|
+
// В31 — the ENTIRE read-merge-write runs under the per-cwd lock so a concurrent foundation writer's
|
|
307
|
+
// owner sections are never clobbered by a stale-read merge.
|
|
308
|
+
withProfileLockSync(cwd, () => writePeerProfileMerged(path, profile))
|
|
309
|
+
}
|
|
257
310
|
|
|
311
|
+
function writePeerProfileMerged(path: string, profile: PeerProfileWrite): void {
|
|
258
312
|
let existing: Record<string, unknown> = {}
|
|
259
313
|
try {
|
|
260
314
|
const parsed = JSON.parse(readFileSync(path, 'utf8'))
|
package/src/registry/index.ts
CHANGED
|
@@ -490,7 +490,13 @@ export async function upsertPeer(
|
|
|
490
490
|
|
|
491
491
|
const record: PeerRecord = {
|
|
492
492
|
personality: args.personality,
|
|
493
|
-
|
|
493
|
+
// В34 — default_runtime is an H1 field: it INHERITS from existing, it does NOT flip to args.runtime.
|
|
494
|
+
// A re-provision / `init --runtime codex` on an existing claude-default peer ADDS codex to runtimes
|
|
495
|
+
// (below) but must NOT silently make codex the default — that mis-wakes the peer on the wrong
|
|
496
|
+
// runtime until the next reindex heals it (which never runs when addRuntime returns 'already'). The
|
|
497
|
+
// default changes ONLY via the explicit `default-runtime` verb (writes the local profile +
|
|
498
|
+
// reindexFromLocals). A brand-new peer (no existing) naturally takes args.runtime as its default.
|
|
499
|
+
runtime: existing ? existing.runtime : runtime,
|
|
494
500
|
runtimes,
|
|
495
501
|
description,
|
|
496
502
|
intelligence,
|
package/src/transport/index.ts
CHANGED
|
@@ -107,8 +107,11 @@ export function resolveDeliveryTarget(args: {
|
|
|
107
107
|
personality: string
|
|
108
108
|
runtime?: string
|
|
109
109
|
sockDir?: string
|
|
110
|
+
/** Б7 — injected env sandboxes the liveness reads (supervisor run-dir); default process.env (prod). */
|
|
111
|
+
env?: NodeJS.ProcessEnv
|
|
110
112
|
}): Result<DeliveryTarget> {
|
|
111
|
-
const
|
|
113
|
+
const env = args.env ?? process.env
|
|
114
|
+
const sockDir = args.sockDir ?? resolveSockDir(env)
|
|
112
115
|
if (!isValidName(args.personality)) {
|
|
113
116
|
return err(`invalid personality "${args.personality}" — must match /^[a-z][a-z0-9-]{0,31}$/`)
|
|
114
117
|
}
|
|
@@ -117,12 +120,12 @@ export function resolveDeliveryTarget(args: {
|
|
|
117
120
|
const runtime = args.runtime
|
|
118
121
|
const socketPath = buildSocketPath(runtime, args.personality, sockDir)
|
|
119
122
|
const address = buildProcessAddress(runtime, args.personality)
|
|
120
|
-
if (!sessionAlive(socketPath, address)) {
|
|
123
|
+
if (!sessionAlive(socketPath, address, env)) {
|
|
121
124
|
return err(`peer offline: ${args.personality} (${args.runtime})`)
|
|
122
125
|
}
|
|
123
126
|
return ok({ runtime, personality: args.personality, address, socketPath })
|
|
124
127
|
}
|
|
125
|
-
const matches = listOnlinePeers(sockDir).filter(peer => peer.personality === args.personality)
|
|
128
|
+
const matches = listOnlinePeers(sockDir, env).filter(peer => peer.personality === args.personality)
|
|
126
129
|
if (matches.length === 0) return err(`peer offline: ${args.personality}`)
|
|
127
130
|
if (matches.length > 1) {
|
|
128
131
|
return err(
|
|
@@ -145,13 +148,14 @@ export function resolvePeerDeliveryTarget(
|
|
|
145
148
|
personality: string,
|
|
146
149
|
runtime: string | undefined,
|
|
147
150
|
peer: PeerRecord,
|
|
151
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
148
152
|
): Result<DeliveryTarget> {
|
|
149
|
-
if (runtime) return resolveDeliveryTarget({ personality, runtime })
|
|
153
|
+
if (runtime) return resolveDeliveryTarget({ personality, runtime, env })
|
|
150
154
|
if (peer.runtime) {
|
|
151
|
-
const exactDefault = resolveDeliveryTarget({ personality, runtime: peer.runtime })
|
|
155
|
+
const exactDefault = resolveDeliveryTarget({ personality, runtime: peer.runtime, env })
|
|
152
156
|
if (exactDefault.ok) return exactDefault
|
|
153
157
|
}
|
|
154
|
-
return resolveDeliveryTarget({ personality })
|
|
158
|
+
return resolveDeliveryTarget({ personality, env })
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
// Confirm poll interval — how often deliverViaHost re-reads the transcript for a NEW record carrying
|
|
@@ -232,13 +236,14 @@ export async function deliverWarm(
|
|
|
232
236
|
envelope: string,
|
|
233
237
|
cwd?: string,
|
|
234
238
|
seam: WarmDeliverSeam = {},
|
|
239
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
235
240
|
): Promise<Result<void>> {
|
|
236
241
|
const address = target.address
|
|
237
242
|
const prev = deliverTails.get(address) ?? Promise.resolve()
|
|
238
243
|
// run AFTER prev settles (success or failure) — a prior delivery's error must not stall the chain
|
|
239
244
|
const run = prev.then(
|
|
240
|
-
() => deliverViaHost(target, envelope, cwd, seam),
|
|
241
|
-
() => deliverViaHost(target, envelope, cwd, seam),
|
|
245
|
+
() => deliverViaHost(target, envelope, cwd, seam, env),
|
|
246
|
+
() => deliverViaHost(target, envelope, cwd, seam, env),
|
|
242
247
|
)
|
|
243
248
|
deliverTails.set(
|
|
244
249
|
address,
|
|
@@ -255,6 +260,7 @@ async function deliverViaHost(
|
|
|
255
260
|
envelope: string,
|
|
256
261
|
cwd: string | undefined,
|
|
257
262
|
seam: WarmDeliverSeam,
|
|
263
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
258
264
|
): Promise<Result<void>> {
|
|
259
265
|
const adapter = getAdapter(target.runtime)
|
|
260
266
|
// Baseline the transcript/session-jsonl file SIZES BEFORE delivery (per-runtime candidate files).
|
|
@@ -264,8 +270,9 @@ async function deliverViaHost(
|
|
|
264
270
|
// socket-ack runtimes (codex) return before it — computing it for them recursively scanned all of
|
|
265
271
|
// ~/.codex/sessions (full-file read per session) on every delivery for nothing.
|
|
266
272
|
const usesBaseline = adapter.kind !== 'router' && adapter.deliveryConfirm !== 'socket-ack'
|
|
267
|
-
const baseline = cwd && usesBaseline ? compactDoneBaseline(target.runtime, cwd) : null
|
|
268
|
-
|
|
273
|
+
const baseline = cwd && usesBaseline ? compactDoneBaseline(target.runtime, cwd, { env }) : null
|
|
274
|
+
// Б7 — the default deliverHosted resolves the supervisor run-dir from env (sandboxed under a test root).
|
|
275
|
+
const send = seam.deliverHosted ?? ((identity: string, msg: string) => deliverHosted(identity, msg, env))
|
|
269
276
|
const sent = await send(target.address, envelope)
|
|
270
277
|
if (!sent.ok) {
|
|
271
278
|
return err(
|
|
@@ -892,6 +899,11 @@ export interface ComposerQueueRouteDeps {
|
|
|
892
899
|
}
|
|
893
900
|
|
|
894
901
|
export interface RouteDeps {
|
|
902
|
+
/** Б7 — injected env sandboxes ALL of routeSend's env-resolved reads (the registry, the supervisor
|
|
903
|
+
* run-dir liveness/delivery). Default process.env (prod: env === process.env). Closes the daemon:286
|
|
904
|
+
* isolation invariant — a library/test daemon with an injected root no longer reads the real registry
|
|
905
|
+
* or the real fleet's live sessions. */
|
|
906
|
+
env?: NodeJS.ProcessEnv
|
|
895
907
|
/** On a miss, wake the dead peer instead of returning offline (Ф2). */
|
|
896
908
|
wake?: WakeFn
|
|
897
909
|
/** Ephemeral-target serial-queue delivery (M3); absent → normal routing. */
|
|
@@ -1275,7 +1287,8 @@ export async function routeSend(
|
|
|
1275
1287
|
return err(`message exceeds ${MAX_MESSAGE_LEN} char limit (got ${message.length})`)
|
|
1276
1288
|
}
|
|
1277
1289
|
|
|
1278
|
-
const
|
|
1290
|
+
const env = deps.env ?? process.env
|
|
1291
|
+
const index = readPeersIndex({ env })
|
|
1279
1292
|
const peer = findPeer(index, personality)
|
|
1280
1293
|
if (!peer) {
|
|
1281
1294
|
return err(`peer "${personality}" is not in the iapeer peers index; message NOT delivered`)
|
|
@@ -1309,7 +1322,7 @@ export async function routeSend(
|
|
|
1309
1322
|
return deps.ephemeral.deliver({ peer, envelope, topic, runtime })
|
|
1310
1323
|
}
|
|
1311
1324
|
|
|
1312
|
-
const target = resolvePeerDeliveryTarget(personality, runtime, peer)
|
|
1325
|
+
const target = resolvePeerDeliveryTarget(personality, runtime, peer, env)
|
|
1313
1326
|
if (target.ok) {
|
|
1314
1327
|
if (target.value.address === caller.address) return err('cannot send to self')
|
|
1315
1328
|
// Telegram sender policy — RESOLVED channel (the only-live-session fallback can
|
|
@@ -1321,7 +1334,7 @@ export async function routeSend(
|
|
|
1321
1334
|
// peer.cwd enables the Ф-B transcript-mtime liveness probe (busy-session case).
|
|
1322
1335
|
// deliverWarm is HOST-AWARE (spawn-flip Ф0b-2): a supervisor-hosted target delivers over its
|
|
1323
1336
|
// socket, any other target keeps the tmux path. Flag-off → byte-identical to deliverViaTmux.
|
|
1324
|
-
const delivered = await deliverWarm(target.value, envelope, peer.cwd)
|
|
1337
|
+
const delivered = await deliverWarm(target.value, envelope, peer.cwd, {}, env)
|
|
1325
1338
|
if (!delivered.ok) return delivered
|
|
1326
1339
|
noteTopic(deps, target.value.address, topic)
|
|
1327
1340
|
noteDelivered(deps, target.value.address) // В7 — floor the idle proxy so this delivery isn't reaped away
|
|
@@ -1349,12 +1362,12 @@ export async function routeSend(
|
|
|
1349
1362
|
const reviveDeadline = monotonicMs() + launchdReviveGraceMs()
|
|
1350
1363
|
while (monotonicMs() < reviveDeadline) {
|
|
1351
1364
|
await sleep(LAUNCHD_REVIVE_POLL_MS)
|
|
1352
|
-
const revived = resolvePeerDeliveryTarget(personality, runtime, peer)
|
|
1365
|
+
const revived = resolvePeerDeliveryTarget(personality, runtime, peer, env)
|
|
1353
1366
|
if (!revived.ok) continue // not back yet — keep polling until the deadline
|
|
1354
1367
|
if (revived.value.address === caller.address) return err('cannot send to self')
|
|
1355
1368
|
const reviveGuard = telegramSenderGuard(caller, revived.value.runtime, personality)
|
|
1356
1369
|
if (!reviveGuard.ok) return reviveGuard
|
|
1357
|
-
const delivered = await deliverWarm(revived.value, envelope, peer.cwd)
|
|
1370
|
+
const delivered = await deliverWarm(revived.value, envelope, peer.cwd, {}, env)
|
|
1358
1371
|
if (delivered.ok) {
|
|
1359
1372
|
noteTopic(deps, revived.value.address, topic)
|
|
1360
1373
|
return ok({
|
|
@@ -1399,7 +1412,7 @@ export async function routeSend(
|
|
|
1399
1412
|
if (woke.taskDelivered === false) {
|
|
1400
1413
|
const queued = await deps.composerQueue?.tryEnqueue({ caller, peer, target: live.value, envelope, topic })
|
|
1401
1414
|
if (queued) return queued
|
|
1402
|
-
const delivered = await deliverWarm(live.value, envelope, peer.cwd)
|
|
1415
|
+
const delivered = await deliverWarm(live.value, envelope, peer.cwd, {}, env)
|
|
1403
1416
|
if (!delivered.ok) return delivered
|
|
1404
1417
|
noteTopic(deps, live.value.address, topic)
|
|
1405
1418
|
return ok({
|