@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
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, hostSessionAlive } from '../launch/ptyHost.ts' // spawn-flip Ф0b-3: host-aware attach
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). A supervisor-HOSTED session has no tmux to attach,
2265
- * so the operator is handed to the supervisor client (raw passthrough + repaint-on-attach + Ctrl-]
2266
- * detach); otherwise `tmux attach`. The operator window is bracketed in lifecycle.log
2267
- * (ev=attach … ev=attach-end) so a death timestamp inside the window reads directly off the log.
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
- socketPath: string,
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
- if (hostSessionAlive(identity)) {
2278
- // runSupervisorClient calls process.exit on every terminal path → bracket attach-end via an
2279
- // exit hook (appendFileSync is signal/exit-safe).
2280
- process.on('exit', () =>
2281
- appendLifecycleEvent(eventLogDir, { ev: 'attach-end', identity, rc: process.exitCode ?? 0 }, { env }),
2282
- )
2283
- await runSupervisorClient(hostRunDir(), identity)
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) {
@@ -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, hostSessionAlive } from '../launch/ptyHost.ts' // spawn-flip Ф0b-3: host-aware attach
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
- // SPAWN-FLIP Ф0b-3: a supervisor-HOSTED session attaches via the supervisor client (no tmux).
144
- if (hostSessionAlive(r.identity)) {
145
- await runSupervisorClient(hostRunDir(), r.identity)
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 {
@@ -1,43 +1,12 @@
1
- // tmux command-log — the SOCKET-COMMAND observability layer of the killer hunt.
2
- //
3
- // Why (a session-death class): a tmux server killed by a
4
- // CLIENT COMMAND (`kill-server` / `kill-session` over the unix socket) dies with
5
- // zero signals — structurally invisible to ALL THREE existing layers: the eslogger
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 { chmodSync, mkdirSync, openSync, closeSync, readSync, readdirSync, rmSync, statSync, writeFileSync, fstatSync } from 'fs'
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 (tmux `cat >>`, the supervisor append-fd):
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),