@agfpd/iapeer 0.2.79 → 0.2.80

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.2.79",
3
+ "version": "0.2.80",
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
@@ -61,7 +61,7 @@ import {
61
61
  validateProfileStandard,
62
62
  } from '../identity/profileStandard.ts'
63
63
  import { runAlwaysOn } from '../launch/launchdRun.ts'
64
- import { installDaemonPlist, startConfiguredDaemon } from '../daemon/main.ts'
64
+ import { ensureDaemonStarted, installDaemonPlist, startConfiguredDaemon } from '../daemon/main.ts'
65
65
  import { MARKETPLACE_NAME, onboardHost } from '../onboard/index.ts'
66
66
  import { readMemoryProvider } from '../status/index.ts'
67
67
  import { appendLifecycleEvent } from '../lifecycle/eventlog.ts'
@@ -969,6 +969,14 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
969
969
  // reports the would-be actions without touching anything. --infra <csv> ALSO
970
970
  // onboards infra runtimes (§6): npx-install each package (auto-resolved) + deploy
971
971
  // its declared set. notifier → timer+watcher auto; telegram → operator-add after.
972
+ // STEP 0 (non-disableable): ensure the router daemon is up. Onboard is the
973
+ // explicit "set up the host" action, so starting the always-on daemon belongs
974
+ // here — the operator never types raw `launchctl bootstrap`. Idempotent (no-op
975
+ // if already loaded). A failure here is a real, host-breaking problem → exit 1.
976
+ const dstart = await ensureDaemonStarted({ dryRun: flags['dry-run'] === true, env })
977
+ const dLabel = dstart.state === 'would-start' ? 'would-start (dry-run)' : dstart.state
978
+ out(`daemon: ${dLabel}${dstart.healthy === false ? ' — UNHEALTHY' : ''}${dstart.detail ? ` — ${dstart.detail}` : ''}\n`)
979
+ const daemonFailed = dstart.state === 'failed' || dstart.healthy === false
972
980
  const r = onboardHost({ dryRun: flags['dry-run'] === true, env })
973
981
  for (const m of r.marketplaces) {
974
982
  out(`marketplace ${MARKETPLACE_NAME} @ ${m.runtime}: ${m.state}${m.detail ? ` — ${m.detail}` : ''}\n`)
@@ -1038,7 +1046,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
1038
1046
  })
1039
1047
  const memLabel = mem.provider ? `${mem.provider.provider} ${mem.provider.version}` : 'none'
1040
1048
  out(`memory: ${mem.state}${mem.detail ? ` — ${mem.detail}` : ''} (slot: ${memLabel})\n`)
1041
- return r.marketplaces.some(m => m.state === 'failed') || infraFailed ? 1 : 0
1049
+ return daemonFailed || r.marketplaces.some(m => m.state === 'failed') || infraFailed ? 1 : 0
1042
1050
  }
1043
1051
  case 'native-memory': {
1044
1052
  // The canonized runtime-memory lever (контракт «Слот памяти» §Native-память):
@@ -23,11 +23,13 @@ import { DAEMON_PLIST_LABEL } from '../core/constants.ts'
23
23
  import { IapError } from '../core/errors.ts'
24
24
  import { pluginLogsDir } from '../storage/index.ts'
25
25
  import {
26
+ bootstrapDaemon,
26
27
  isFoundationOwnedPlist,
27
28
  launchAgentsDir,
28
29
  renderLaunchdPlist,
29
30
  } from '../launch/launchd.ts'
30
- import type { LaunchdPlistSpec } from '../launch/launchd.ts'
31
+ import type { BootstrapState, LaunchdPlistSpec } from '../launch/launchd.ts'
32
+ import { waitForDaemonHealthy } from '../update/index.ts'
31
33
  import {
32
34
  drainAllEphemeralQueues,
33
35
  drainEphemeralQueue,
@@ -450,6 +452,40 @@ export function installDaemonPlist(opts: InstallDaemonPlistOptions = {}): Instal
450
452
  return { path, changed: true }
451
453
  }
452
454
 
455
+ export interface DaemonStartResult {
456
+ /** 'would-start' = dryRun report; otherwise the bootstrap outcome. */
457
+ state: BootstrapState | 'would-start'
458
+ /** Socket health after a (non-sandbox) bootstrap; undefined when not probed. */
459
+ healthy?: boolean
460
+ detail?: string
461
+ }
462
+
463
+ /**
464
+ * `iapeer onboard`'s first, non-disableable step: ensure the foundation daemon is up.
465
+ * Writes the plist if missing (idempotent write-if-changed — defensive when onboard
466
+ * runs on a host where the plist is absent), idempotently bootstraps it (no-op when
467
+ * already loaded — NOT a restart), then health-checks the socket. dryRun reports the
468
+ * intent without touching launchctl. Sandbox-safe: bootstrapDaemon + waitForDaemonHealthy
469
+ * both short-circuit under IAPEER_TEST_SANDBOX, so this never loads a real job in tests.
470
+ */
471
+ export async function ensureDaemonStarted(
472
+ opts: { env?: NodeJS.ProcessEnv; dryRun?: boolean } = {},
473
+ ): Promise<DaemonStartResult> {
474
+ const env = opts.env ?? process.env
475
+ if (opts.dryRun) {
476
+ return {
477
+ state: 'would-start',
478
+ detail: 'install plist if missing + launchctl bootstrap com.agfpd.iapeer (idempotent) + health-check',
479
+ }
480
+ }
481
+ installDaemonPlist({ env })
482
+ const b = bootstrapDaemon(env)
483
+ if (b.state === 'failed') return { state: 'failed', detail: b.detail }
484
+ if (b.state === 'skipped-sandbox') return { state: 'skipped-sandbox', detail: b.detail }
485
+ const h = await waitForDaemonHealthy({ env })
486
+ return { state: b.state, healthy: h.healthy, detail: h.healthy ? undefined : h.detail }
487
+ }
488
+
453
489
  // ─────────────────────────────────────────────────────────────────────────────
454
490
  // CLI — launchd-held process / foreground acceptance entrypoint
455
491
  // ─────────────────────────────────────────────────────────────────────────────
@@ -463,7 +499,7 @@ if (import.meta.main) {
463
499
  const { path: p, changed } = installDaemonPlist()
464
500
  process.stdout.write(
465
501
  `daemon plist ${changed ? 'written' : 'unchanged'}: ${p}\n` +
466
- `NOT loaded. To start it live: launchctl bootstrap gui/$(id -u) ${p}\n`,
502
+ `NOT loaded. \`iapeer onboard\` starts it (idempotent); or manually: launchctl bootstrap gui/$(id -u) ${p}\n`,
467
503
  )
468
504
  process.exit(0)
469
505
  } else {
@@ -382,6 +382,33 @@ export function cycleDaemon(env: NodeJS.ProcessEnv = process.env): DaemonRestart
382
382
  return cycleLaunchdJob(DAEMON_PLIST_LABEL, join(launchAgentsDir(env), `${DAEMON_PLIST_LABEL}.plist`), env)
383
383
  }
384
384
 
385
+ /**
386
+ * Ensure the foundation daemon (`com.agfpd.iapeer`) is LOADED in the gui domain — an
387
+ * IDEMPOTENT `launchctl bootstrap` (no-op when already loaded), NOT a restart. This is
388
+ * the "start it now" step `iapeer onboard` runs, so an operator never types raw
389
+ * launchctl. Composed on bootstrapJobCore (undead-job-safe wait-for-gone + backoff).
390
+ * SANDBOX FAIL-SAFE: under IAPEER_TEST_SANDBOX it never calls launchctl (`bootstrap
391
+ * gui/<uid>` is host-global regardless of IAPEER_ROOT), consulting BOTH the passed env
392
+ * and the process env — mirror of launchctlBootstrap's guard.
393
+ */
394
+ export function bootstrapDaemon(env: NodeJS.ProcessEnv = process.env): BootstrapResult {
395
+ if (env.IAPEER_TEST_SANDBOX === '1' || process.env.IAPEER_TEST_SANDBOX === '1') {
396
+ return { state: 'skipped-sandbox', label: DAEMON_PLIST_LABEL, detail: 'IAPEER_TEST_SANDBOX=1 — not loading a real launchd job' }
397
+ }
398
+ const uid = currentUid()
399
+ const plistPath = join(launchAgentsDir(env), `${DAEMON_PLIST_LABEL}.plist`)
400
+ const core = bootstrapJobCore(uid, DAEMON_PLIST_LABEL, plistPath, {
401
+ run: args => {
402
+ const r = spawnSync('launchctl', args, { encoding: 'utf8' })
403
+ return { status: r.status, stderr: r.stderr ?? '' }
404
+ },
405
+ sleepMs: ms => spawnSync('sleep', [String(ms / 1000)]),
406
+ })
407
+ return core.state === 'failed'
408
+ ? { state: 'failed', label: DAEMON_PLIST_LABEL, detail: core.detail }
409
+ : { state: core.state, label: DAEMON_PLIST_LABEL }
410
+ }
411
+
385
412
  /**
386
413
  * Restart any launchd job onto the executable(s) currently referenced by its plist:
387
414
  * bootout → bootstrap, never kickstart. Used by the daemon update path AND by