@agfpd/iapeer 0.4.7 → 0.4.9
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/cli/index.ts +13 -80
- package/src/cli/listTui.ts +4 -14
- package/src/launch/cmdlog.ts +7 -110
- package/src/launch/index.ts +14 -314
- package/src/launch/launchdRun.ts +9 -52
- package/src/launch/ptyHost.ts +0 -12
- package/src/launch/readyGateModel.ts +9 -80
- package/src/lifecycle/index.ts +25 -101
- package/src/supervisor/daemon.ts +35 -4
- package/src/{shadow → supervisor}/render.ts +10 -10
- package/src/transport/index.ts +41 -400
- package/src/update/index.ts +0 -9
- package/src/launch/canary.ts +0 -239
- package/src/shadow/gate.ts +0 -396
- package/src/shadow/index.ts +0 -174
- package/src/shadow/install.ts +0 -256
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
type WakeResult,
|
|
51
51
|
} from '../lifecycle/index.ts'
|
|
52
52
|
import { getAdapter } from '../launch/index.ts'
|
|
53
|
-
import { hostRunDir
|
|
53
|
+
import { hostRunDir } from '../launch/ptyHost.ts' // pty-only: attach via the supervisor client
|
|
54
54
|
import { runSupervisorClient } from '../supervisor/client.ts' // @xterm-free attach client (both reattach port-deps baked in)
|
|
55
55
|
import { cycleDaemon, isFoundationOwnedPlist, launchctlBootstrap, launchdLabel, launchdPlistPath } from '../launch/launchd.ts'
|
|
56
56
|
import { readPeerProfile, resolveCallerIdentity, resolveIdentity, writePeerProfileAtomic } from '../identity/index.ts'
|
|
@@ -1025,12 +1025,6 @@ const VERBS: ReadonlyArray<{ sig: string; desc: string }> = [
|
|
|
1025
1025
|
{ sig: 'self-done', desc: '(agent self-call, ephemeral) silent finish: arm own quiet-reap, wake no one' },
|
|
1026
1026
|
{ sig: 'native-memory <off|on> (--peer <p> | --all)', desc: "gate/restore runtimes' native memory (canonized lever; контракт «Слот памяти»)" },
|
|
1027
1027
|
{ sig: 'trust-hooks <hooks.json> [--check]', desc: 'pre-seed codex hooks trust for a file-form hooks.json (no modal); --check = drift report' },
|
|
1028
|
-
{
|
|
1029
|
-
sig: 'shadow [--once | --minutes <m>] [--interval <ms>]',
|
|
1030
|
-
desc: 'READ-ONLY tmux→pty fidelity observer (migration burn-in): compares pty-model verdicts vs live tmux on the warm fleet → ~/.iapeer/logs/iapeer/shadow-fidelity.json. Never delivers/mutates.',
|
|
1031
|
-
},
|
|
1032
|
-
{ sig: 'shadow-install', desc: 'install + load the always-on shadow burn-in launchd job (com.iapeer.shadow-fidelity); idempotent, re-cycled by update/rollback' },
|
|
1033
|
-
{ sig: 'shadow-uninstall', desc: 'bootout + remove the shadow burn-in launchd job (the correct way to stop it; STOP file is respawned past by KeepAlive)' },
|
|
1034
1028
|
{
|
|
1035
1029
|
sig: 'supervisor up|start|attach|list|kill <sess> [runtime]',
|
|
1036
1030
|
desc: 'DARK (cutover Block 2): detach-persistent pty-supervisor PoC port; serves nothing on the live fleet (throwaway validation only)',
|
|
@@ -2122,54 +2116,6 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2122
2116
|
const attachCfg = loadLifecycleConfig(env)
|
|
2123
2117
|
return await attachIntoSession(r.identity, r.socketPath, env, attachCfg.eventLogDir, r.woke)
|
|
2124
2118
|
}
|
|
2125
|
-
case 'shadow': {
|
|
2126
|
-
// Read-only fidelity OBSERVER for the tmux→pty migration burn-in (track «b»). Compares
|
|
2127
|
-
// the pty-model verdicts (occupancy/ready-gate/liveness) vs live tmux verdicts on the
|
|
2128
|
-
// warm fleet, logs to <eventLogDir>/shadow-fidelity.{jsonl,json}. STRICTLY read-only —
|
|
2129
|
-
// nothing in delivery/lifecycle imports the shadow module; the dynamic import keeps
|
|
2130
|
-
// @xterm OUT of the hot path (the daemon never loads it). --once | --minutes <m> |
|
|
2131
|
-
// --interval <ms>; stop a long run with `touch <eventLogDir>/shadow-fidelity.STOP`.
|
|
2132
|
-
const cfg = loadLifecycleConfig(env)
|
|
2133
|
-
const { runShadowFidelity } = await import('../shadow/index.ts')
|
|
2134
|
-
await runShadowFidelity({
|
|
2135
|
-
logDir: cfg.logDir,
|
|
2136
|
-
eventLogDir: cfg.eventLogDir,
|
|
2137
|
-
sockDir: cfg.sockDir,
|
|
2138
|
-
intervalMs: flags.interval ? Number(flags.interval) : 5000,
|
|
2139
|
-
maxMinutes: flags.minutes ? Number(flags.minutes) : 0,
|
|
2140
|
-
once: flags.once === true,
|
|
2141
|
-
log: errOut,
|
|
2142
|
-
})
|
|
2143
|
-
out(`shadow-fidelity → ${cfg.eventLogDir}/shadow-fidelity.json\n`)
|
|
2144
|
-
return 0
|
|
2145
|
-
}
|
|
2146
|
-
case 'shadow-install': {
|
|
2147
|
-
// Code-managed install of the always-on shadow burn-in launchd job
|
|
2148
|
-
// (com.iapeer.shadow-fidelity) — reproducible, ownership-sentinel-guarded, and
|
|
2149
|
-
// re-cycled automatically by `iapeer update` / `rollback`. Replaces a hand-written
|
|
2150
|
-
// plist. Idempotent (no-op on an unchanged, loaded job). install.ts carries NO @xterm
|
|
2151
|
-
// — dynamic-imported anyway to keep the CLI's common path light.
|
|
2152
|
-
const { installShadowJob } = await import('../shadow/install.ts')
|
|
2153
|
-
const r = installShadowJob(env)
|
|
2154
|
-
if (r.action === 'failed' || r.action === 'refused-foreign') {
|
|
2155
|
-
errOut(`shadow-install: ${r.action}${r.detail ? ` — ${r.detail}` : ''}\n plist: ${r.path}\n`)
|
|
2156
|
-
return 1
|
|
2157
|
-
}
|
|
2158
|
-
out(`shadow burn-in job ${r.action} (plist ${r.changed ? 'written' : 'unchanged'}): ${r.path}\n`)
|
|
2159
|
-
return 0
|
|
2160
|
-
}
|
|
2161
|
-
case 'shadow-uninstall': {
|
|
2162
|
-
// Stop + remove the burn-in job (bootout + rm) — the CORRECT halt (KeepAlive would
|
|
2163
|
-
// respawn past the STOP sentinel). Refuses a non-foundation plist (ownership guard).
|
|
2164
|
-
const { uninstallShadowJob } = await import('../shadow/install.ts')
|
|
2165
|
-
const r = uninstallShadowJob(env)
|
|
2166
|
-
if (r.action === 'failed' || r.action === 'refused-foreign') {
|
|
2167
|
-
errOut(`shadow-uninstall: ${r.action}${r.detail ? ` — ${r.detail}` : ''}\n plist: ${r.path}\n`)
|
|
2168
|
-
return 1
|
|
2169
|
-
}
|
|
2170
|
-
out(`shadow burn-in job ${r.action}: ${r.path}\n`)
|
|
2171
|
-
return 0
|
|
2172
|
-
}
|
|
2173
2119
|
case 'supervisor': {
|
|
2174
2120
|
// DARK (cutover Block 2): the detach-persistent pty-supervisor (PoC pts.mjs port). NOT
|
|
2175
2121
|
// wired into delivery/launch — it serves NOTHING on the live fleet; it is validated on
|
|
@@ -2261,38 +2207,25 @@ function defaultFromIdentity(env: NodeJS.ProcessEnv): string {
|
|
|
2261
2207
|
}
|
|
2262
2208
|
|
|
2263
2209
|
/** Drop the operator into a live peer session — the interactive open shared by `attach` (resume)
|
|
2264
|
-
* and a TTY `iapeer <runtime>` (fresh launch).
|
|
2265
|
-
*
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
2268
|
-
* Returns the client exit code. */
|
|
2210
|
+
* and a TTY `iapeer <runtime>` (fresh launch). pty-only: the operator is handed to the supervisor
|
|
2211
|
+
* client (raw passthrough + repaint-on-attach + Ctrl-] detach). The operator window is bracketed in
|
|
2212
|
+
* lifecycle.log (ev=attach … ev=attach-end) so a death timestamp inside the window reads directly off
|
|
2213
|
+
* the log. Returns the client exit code. (`_socketPath` retained for call-site compatibility.) */
|
|
2269
2214
|
async function attachIntoSession(
|
|
2270
2215
|
identity: string,
|
|
2271
|
-
|
|
2216
|
+
_socketPath: string,
|
|
2272
2217
|
env: NodeJS.ProcessEnv,
|
|
2273
2218
|
eventLogDir: string,
|
|
2274
2219
|
woke: boolean,
|
|
2275
2220
|
): Promise<number> {
|
|
2276
2221
|
appendLifecycleEvent(eventLogDir, { ev: 'attach', identity, woke: String(woke) }, { env })
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
return 0 // unreachable — runSupervisorClient exits on detach / session end
|
|
2285
|
-
}
|
|
2286
|
-
// `env -u TMUX` so a nested attach from inside tmux does not error ("sessions should be nested
|
|
2287
|
-
// with care").
|
|
2288
|
-
const attachEnv = { ...env }
|
|
2289
|
-
delete attachEnv.TMUX
|
|
2290
|
-
const a = spawnSync('tmux', ['-S', socketPath, 'attach', '-t', identity], {
|
|
2291
|
-
stdio: 'inherit',
|
|
2292
|
-
env: attachEnv as Record<string, string>,
|
|
2293
|
-
})
|
|
2294
|
-
appendLifecycleEvent(eventLogDir, { ev: 'attach-end', identity, rc: a.status ?? -1 }, { env })
|
|
2295
|
-
return a.status ?? 0
|
|
2222
|
+
// runSupervisorClient calls process.exit on every terminal path → bracket attach-end via an exit
|
|
2223
|
+
// hook (appendFileSync is signal/exit-safe).
|
|
2224
|
+
process.on('exit', () =>
|
|
2225
|
+
appendLifecycleEvent(eventLogDir, { ev: 'attach-end', identity, rc: process.exitCode ?? 0 }, { env }),
|
|
2226
|
+
)
|
|
2227
|
+
await runSupervisorClient(hostRunDir(), identity)
|
|
2228
|
+
return 0 // unreachable — runSupervisorClient exits on detach / session end
|
|
2296
2229
|
}
|
|
2297
2230
|
|
|
2298
2231
|
if (import.meta.main) {
|
package/src/cli/listTui.ts
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
// raw-mode terminal I/O, and on ENTER it hands off to attachPeer + tmux attach — the
|
|
8
8
|
// same ensure-live+resume path the `attach` verb uses (so a live session is never torn).
|
|
9
9
|
|
|
10
|
-
import { spawnSync } from 'child_process'
|
|
11
10
|
import { attachPeer } from '../lifecycle/index.ts'
|
|
12
|
-
import { hostRunDir
|
|
11
|
+
import { hostRunDir } from '../launch/ptyHost.ts' // pty-only: attach via the supervisor client
|
|
13
12
|
import { runSupervisorClient } from '../supervisor/client.ts'
|
|
14
13
|
import { listPeers, type PeerListing } from './index.ts'
|
|
15
14
|
|
|
@@ -140,18 +139,9 @@ export async function runListTui(env: NodeJS.ProcessEnv = process.env): Promise<
|
|
|
140
139
|
return 1
|
|
141
140
|
}
|
|
142
141
|
stdout.write(`${r.woke ? 'woke + ' : ''}attaching ${r.identity}…\n`)
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return 0 // unreachable — runSupervisorClient exits on detach / session end
|
|
147
|
-
}
|
|
148
|
-
const attachEnv = { ...env }
|
|
149
|
-
delete attachEnv.TMUX
|
|
150
|
-
const a = spawnSync('tmux', ['-S', r.socketPath, 'attach', '-t', r.identity], {
|
|
151
|
-
stdio: 'inherit',
|
|
152
|
-
env: attachEnv as Record<string, string>,
|
|
153
|
-
})
|
|
154
|
-
return a.status ?? 0
|
|
142
|
+
// pty-only: attach via the supervisor client (no tmux).
|
|
143
|
+
await runSupervisorClient(hostRunDir(), r.identity)
|
|
144
|
+
return 0 // unreachable — runSupervisorClient exits on detach / session end
|
|
155
145
|
}
|
|
156
146
|
}
|
|
157
147
|
} finally {
|
package/src/launch/cmdlog.ts
CHANGED
|
@@ -1,43 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// trap (no kill(2) anywhere; kernel-originated SIGHUP to the panes is also proven
|
|
7
|
-
// invisible), the pane-died hook (kill-session destroys the pane without retention)
|
|
8
|
-
// and the server-death canary (sees only "server gone"). The tmux server's OWN
|
|
9
|
-
// verbose log is the only witness of that layer: started with `-v`, the server
|
|
10
|
-
// appends `message: client-<pid> command: <verb> <args>` for every client command —
|
|
11
|
-
// the attribution (client pid + exact command + timestamp) that decides the class
|
|
12
|
-
// binarily: "killed by client X" vs "no command ⇒ internal exit".
|
|
13
|
-
//
|
|
14
|
-
// Sensitivity: the -v log carries FULL send-keys payloads
|
|
15
|
-
// (every delivered envelope, plaintext) — tmux has no commands-only level and the
|
|
16
|
-
// server writes the file itself (name tmux-server-<pid>.log in the STARTING
|
|
17
|
-
// client's cwd — no FIFO/pipe interposition possible). Containment instead:
|
|
18
|
-
// • the per-identity dir is 0700 (others cannot traverse — same protection class
|
|
19
|
-
// as the transcripts that already hold the same texts);
|
|
20
|
-
// • files are chmod'ed 0600 right after the server starts (hardenCmdLogDir);
|
|
21
|
-
// • volume is capped: ~7 KB/s under a 10 fps TUI redraw (measured) — the
|
|
22
|
-
// supervise tick tail-keeps each log over CAP_BYTES down to KEEP_BYTES.
|
|
23
|
-
// tmux writes with O_APPEND (proven live: external truncation is safe, the
|
|
24
|
-
// server keeps appending at the new EOF), and a killer's command is by nature
|
|
25
|
-
// the LAST thing in the log — the death tail always survives capping.
|
|
26
|
-
// • scope: agent-runtime (warm) sessions only — always-on infra sessions relaunch
|
|
27
|
-
// rarely (no rotation point) and are not the dying class.
|
|
28
|
-
// Rotation: prepareCmdLogDir WIPES the identity dir on every launch — one server
|
|
29
|
-
// generation per dir, no unbounded accumulation across relaunches.
|
|
30
|
-
//
|
|
31
|
-
// Best-effort BY CONSTRUCTION: observability must never fail a launch or a
|
|
32
|
-
// supervise tick — every export swallows FS errors.
|
|
1
|
+
// Pane-log volume cap — tail-keep the per-identity pane-logs (<logDir>/<identity>.log), the RAW child
|
|
2
|
+
// pty byte stream the supervisor appends for a session's whole life. Unlike the lifecycle/delivery logs
|
|
3
|
+
// (their own size-rotation under logs/iapeer), the pane-log had NO bound and grew to hundreds of MB per
|
|
4
|
+
// warm peer on an always-on host; the supervise tick caps it here. Best-effort BY CONSTRUCTION: a cap
|
|
5
|
+
// must never fail a supervise tick — every export swallows FS errors.
|
|
33
6
|
|
|
34
|
-
import {
|
|
7
|
+
import { openSync, closeSync, readSync, readdirSync, statSync, writeFileSync, fstatSync } from 'fs'
|
|
35
8
|
import { join } from 'path'
|
|
36
9
|
|
|
37
|
-
/** Tail-keep cap: a log over CAP_BYTES is cut down to its last KEEP_BYTES. */
|
|
38
|
-
export const CMDLOG_CAP_BYTES = 8 * 1024 * 1024
|
|
39
|
-
export const CMDLOG_KEEP_BYTES = 1024 * 1024
|
|
40
|
-
|
|
41
10
|
/** Pane-log tail-keep cap (capPaneLogs): a per-identity pane-log over CAP is cut to
|
|
42
11
|
* its last KEEP. KEEP_BYTES INVARIANT — must stay ≥ the pane-log reader's seed
|
|
43
12
|
* window (readyGateModel SEED_BYTES = 4 MiB, the tail fed into the headless xterm
|
|
@@ -46,48 +15,8 @@ export const CMDLOG_KEEP_BYTES = 1024 * 1024
|
|
|
46
15
|
export const PANELOG_CAP_BYTES = 32 * 1024 * 1024
|
|
47
16
|
export const PANELOG_KEEP_BYTES = 8 * 1024 * 1024
|
|
48
17
|
|
|
49
|
-
const CMDLOG_SUBDIR = 'tmux-cmdlog'
|
|
50
|
-
|
|
51
|
-
/** The per-identity command-log dir: <eventLogDir>/tmux-cmdlog/<identity>. The
|
|
52
|
-
* session-creating tmux client runs FROM this dir with `-v`, so the server drops
|
|
53
|
-
* tmux-server-<pid>.log (and the client its tmux-client-*.log) here. */
|
|
54
|
-
export function cmdLogDirFor(eventLogDir: string, identity: string): string {
|
|
55
|
-
return join(eventLogDir, CMDLOG_SUBDIR, identity)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Wipe + recreate the identity's command-log dir (0700) for a fresh server
|
|
59
|
-
* generation. Returns the dir, or undefined when the FS refuses (the caller then
|
|
60
|
-
* launches WITHOUT -v — observability is never load-bearing). */
|
|
61
|
-
export function prepareCmdLogDir(eventLogDir: string, identity: string): string | undefined {
|
|
62
|
-
try {
|
|
63
|
-
const dir = cmdLogDirFor(eventLogDir, identity)
|
|
64
|
-
rmSync(dir, { recursive: true, force: true })
|
|
65
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
66
|
-
return dir
|
|
67
|
-
} catch {
|
|
68
|
-
return undefined
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Clamp every file the freshly-started server/client dropped into the dir to
|
|
73
|
-
* 0600 (the dir's 0700 already blocks traversal; this is the belt to that
|
|
74
|
-
* suspender — the acceptance criterion: no file wider than 0600). */
|
|
75
|
-
export function hardenCmdLogDir(dir: string): void {
|
|
76
|
-
try {
|
|
77
|
-
for (const name of readdirSync(dir)) {
|
|
78
|
-
try {
|
|
79
|
-
chmodSync(join(dir, name), 0o600)
|
|
80
|
-
} catch {
|
|
81
|
-
/* per-file best-effort */
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
/* dir gone — nothing to harden */
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
18
|
/** Tail-keep ONE file: if it exceeds capBytes, rewrite it as its last keepBytes.
|
|
90
|
-
* Safe against a live O_APPEND writer (
|
|
19
|
+
* Safe against a live O_APPEND writer (the supervisor append-fd):
|
|
91
20
|
* the next write lands at the new EOF (proven by live truncation test). Lines
|
|
92
21
|
* appended between the tail-read and the rewrite are lost — acceptable mid-life
|
|
93
22
|
* noise. Returns true iff the file was capped. Best-effort: swallows FS errors. */
|
|
@@ -111,38 +40,6 @@ function tailKeepFile(path: string, capBytes: number, keepBytes: number): boolea
|
|
|
111
40
|
}
|
|
112
41
|
}
|
|
113
42
|
|
|
114
|
-
/** Tail-keep cap over every command log under <eventLogDir>/tmux-cmdlog/<identity>.
|
|
115
|
-
* A file over capBytes is rewritten as its last keepBytes. The death tail is never
|
|
116
|
-
* here because the peer is alive while being capped. */
|
|
117
|
-
export function capCmdLogs(
|
|
118
|
-
eventLogDir: string,
|
|
119
|
-
capBytes: number = CMDLOG_CAP_BYTES,
|
|
120
|
-
keepBytes: number = CMDLOG_KEEP_BYTES,
|
|
121
|
-
): string[] {
|
|
122
|
-
const capped: string[] = []
|
|
123
|
-
let identities: string[]
|
|
124
|
-
try {
|
|
125
|
-
identities = readdirSync(join(eventLogDir, CMDLOG_SUBDIR))
|
|
126
|
-
} catch {
|
|
127
|
-
return capped
|
|
128
|
-
}
|
|
129
|
-
for (const identity of identities) {
|
|
130
|
-
const dir = join(eventLogDir, CMDLOG_SUBDIR, identity)
|
|
131
|
-
let files: string[]
|
|
132
|
-
try {
|
|
133
|
-
files = readdirSync(dir)
|
|
134
|
-
} catch {
|
|
135
|
-
continue
|
|
136
|
-
}
|
|
137
|
-
for (const name of files) {
|
|
138
|
-
if (!name.endsWith('.log')) continue
|
|
139
|
-
const path = join(dir, name)
|
|
140
|
-
if (tailKeepFile(path, capBytes, keepBytes)) capped.push(path)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return capped
|
|
144
|
-
}
|
|
145
|
-
|
|
146
43
|
/** Tail-keep cap over the per-identity PANE-LOGS (<logDir>/<identity>.log) — the RAW
|
|
147
44
|
* TUI byte stream that pipe-pane (tmux) / the supervisor append for a session's whole
|
|
148
45
|
* life. Unlike the lifecycle/delivery logs (their own size-rotation under logs/iapeer),
|