@agfpd/iapeer 0.2.97 → 0.2.98

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/README.md CHANGED
@@ -81,6 +81,7 @@ Planned, not yet built:
81
81
  - **More agent runtimes** — `opencode` and `antigravity`, two further CLI runtimes alongside Claude Code and Codex.
82
82
  - **Discord runtime** — a human-presence runtime for Discord, alongside Telegram.
83
83
  - **Permissions gate** — finer control over what an agent is allowed to do.
84
+ - **Approve permissions over Telegram** — approve an agent's permission requests from the chat; today permissions are auto-allowed by flags.
84
85
 
85
86
  ## License
86
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.2.97",
3
+ "version": "0.2.98",
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
@@ -63,8 +63,8 @@ import {
63
63
  } from '../identity/profileStandard.ts'
64
64
  import { runAlwaysOn } from '../launch/launchdRun.ts'
65
65
  import { ensureDaemonStarted, installDaemonPlist, startConfiguredDaemon } from '../daemon/main.ts'
66
- import { MARKETPLACE_NAME, onboardHost, tccFullDiskAccessNote } from '../onboard/index.ts'
67
- import { readMemoryProvider } from '../status/index.ts'
66
+ import { MARKETPLACE_NAME, onboardHost, runtimeAuthNote, tccFullDiskAccessNote } from '../onboard/index.ts'
67
+ import { probeFullDiskAccess, readMemoryProvider } from '../status/index.ts'
68
68
  import { appendLifecycleEvent } from '../lifecycle/eventlog.ts'
69
69
  import { pluginLogsDir } from '../storage/index.ts'
70
70
 
@@ -1096,9 +1096,22 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
1096
1096
  })
1097
1097
  const memLabel = mem.provider ? `${mem.provider.provider} ${mem.provider.version}` : 'none'
1098
1098
  out(`memory: ${mem.state}${mem.detail ? ` — ${mem.detail}` : ''} (slot: ${memLabel})\n`)
1099
- // macOS TCC advisory (manual grant — no flag/setting can): gated on a memory provider, since a
1100
- // vault (often an iCloud Obsidian dir) is the TCC-protected resource. Base ~/.iapeer needs none.
1101
- const tcc = tccFullDiskAccessNote({ memoryActive: mem.provider != null })
1099
+ // Runtime auth readiness (clean-host prerequisite): a peer runs the runtime's
1100
+ // interactive TUI; the launcher auto-clears first-run modals (theme/trust) but
1101
+ // CANNOT complete a login OAuth flow. Warn for every INSTALLED runtime (marketplace
1102
+ // state != runtime-missing) that has no positive auth evidence — before it ships a
1103
+ // peer that would fail to wake on the login screen.
1104
+ for (const m of r.marketplaces) {
1105
+ if (m.state === 'runtime-missing') continue
1106
+ const authNote = runtimeAuthNote(m.runtime, env)
1107
+ if (authNote) errOut(authNote + '\n')
1108
+ }
1109
+ // macOS TCC advisory (manual grant — no flag/setting/script can): PROBE-DRIVEN, not
1110
+ // memory-gated — a missing grant silently breaks file I/O (EPERM, no prompt/hang) on
1111
+ // any TCC-protected path (iCloud vault, Documents/Desktop/Downloads peer cwd). Silent
1112
+ // when granted / non-macOS / undeterminable.
1113
+ const { iapeerBinPath } = await import('../install/index.ts')
1114
+ const tcc = tccFullDiskAccessNote({ fda: probeFullDiskAccess(env), binPath: iapeerBinPath(env) })
1102
1115
  if (tcc) out(tcc)
1103
1116
  return daemonFailed || r.marketplaces.some(m => m.state === 'failed') || infraFailed ? 1 : 0
1104
1117
  }
@@ -49,6 +49,12 @@ const CLAUDE_BOOT_DIALOG_MARKERS = [
49
49
  'I am using this for local development',
50
50
  'Resume from summary',
51
51
  'Resuming the full session',
52
+ // First-run theme picker ("Let's get started. Choose the text style…") — appears
53
+ // on a CLEAN host (fresh config dir) BEFORE the input prompt, verified live on
54
+ // claude 2.1.181 in a pty. NOT removed by --dangerously-skip-permissions; if not
55
+ // cleared the peer never reaches ready. (There is NO separate "accept dangerous
56
+ // permissions" screen in 2.1.181 — verified absent.)
57
+ 'Choose the text style',
52
58
  ] as const
53
59
 
54
60
  function anyBootDialog(pane: string): boolean {
@@ -185,7 +191,16 @@ export const claudeAdapter: RuntimeAdapter = {
185
191
  if (
186
192
  pane.includes('trust this folder') ||
187
193
  pane.includes('Allow external CLAUDE.md file imports?') ||
188
- pane.includes('I am using this for local development')
194
+ pane.includes('I am using this for local development') ||
195
+ // First-run theme picker (clean host): the default-highlighted row is always a
196
+ // valid theme (any choice is harmless for a headless peer), so a bare Enter
197
+ // accepts it. Without this the picker blocks the boot forever on a fresh host.
198
+ // NOTE: the LOGIN method picker that can follow on an UNAUTHENTICATED host is
199
+ // deliberately NOT auto-answered — selecting subscription opens a browser OAuth
200
+ // flow no headless peer can complete. Login is an onboard PREREQUISITE
201
+ // (ANTHROPIC_API_KEY or a one-time `claude login`); a never-authed host fails
202
+ // the wake LOUD at the boot deadline rather than auto-entering a dead OAuth.
203
+ pane.includes('Choose the text style')
189
204
  ) {
190
205
  return ['Enter']
191
206
  }
@@ -12,7 +12,7 @@
12
12
  import { spawnSync } from 'child_process'
13
13
  import { homedir } from 'os'
14
14
  import { join } from 'path'
15
- import { accessSync, constants as FS } from 'fs'
15
+ import { accessSync, constants as FS, existsSync, readFileSync } from 'fs'
16
16
 
17
17
  /** OUR marketplace — GitHub owner/repo, the source both runtimes' `marketplace add`
18
18
  * takes; and the registered NAME both runtimes' `marketplace list` shows. */
@@ -216,23 +216,120 @@ export function onboardHost(opts: OnboardOptions = {}): OnboardResult {
216
216
  /**
217
217
  * macOS Full Disk Access (TCC) advisory printed at the END of onboard. The runtime permission layer
218
218
  * (`--add-dir` / `additionalDirectories` for claude, the sandbox flags for codex) governs what the
219
- * RUNTIME allows; macOS TCC is a SEPARATE OS layer that NO flag or settings key can grant — it is
220
- * user-granted in System Settings, by design. `~/.iapeer` lives under $HOME and is NOT TCC-protected,
221
- * so a BASE install needs nothing. The trigger that warrants the grant is a memory vault — commonly an
222
- * iCloud Obsidian dir, and iCloud Drive / Desktop / Documents / Downloads ARE TCC-protected — so the
223
- * note is GATED on a memory provider being present; a base ~/.iapeer-only host is never nagged.
224
- * Returns null when not applicable (caller prints nothing). Pure (no I/O) — unit-testable.
219
+ * RUNTIME allows; macOS TCC is a SEPARATE OS layer that NO flag, settings key, config, env var or
220
+ * script can grant — it is user-granted in System Settings, by design (the TCC db is SIP-protected).
221
+ *
222
+ * PROBE-DRIVEN, not memory-gated (was: gated on a memory provider). The advisory now fires off the
223
+ * REAL FDA state (`fda` from probeFullDiskAccess), because a missing grant is a latent fault no proxy
224
+ * captures: the DEFAULT memory vault is an iCloud Obsidian dir AND a peer work dir can itself sit in a
225
+ * protected location (Documents/Desktop/Downloads) with NO memory provider at all. So: grant present
226
+ * → silent; grant ABSENT on macOS → warn, every time.
227
+ *
228
+ * Failure mode is SILENT, not a hang: per Apple (DTS/Quinn, devforums 678819) a launchd-spawned CLI
229
+ * with no GUI front gets NO TCC dialog — the access is denied with EPERM. A peer therefore does NOT
230
+ * block; its file I/O on a protected path just silently fails. `~/.iapeer` (under $HOME) is NOT
231
+ * TCC-protected and needs nothing.
232
+ *
233
+ * Returns null when granted / undeterminable / non-macOS (`fda` true or null). Pure (no I/O) —
234
+ * the FDA probe and binPath are injected by the caller, so this stays unit-testable.
225
235
  */
226
- export function tccFullDiskAccessNote(opts: { memoryActive: boolean }): string | null {
227
- if (!opts.memoryActive) return null
236
+ export function tccFullDiskAccessNote(opts: { fda: boolean | null; binPath: string }): string | null {
237
+ if (opts.fda !== false) return null // granted, non-macOS, or undeterminable → nothing to nag
228
238
  return (
229
239
  [
230
- 'macOS Full Disk Access (manual — TCC cannot be set by any flag or settings key):',
231
- ' ~/.iapeer needs none (it is under $HOME). BUT a memory vault or a peer work dir in a',
232
- ' TCC-protected location (iCloud Drive, Desktop, Documents, Downloads — e.g. an iCloud',
233
- ' Obsidian vault) does, or file tools silently fail to read/write there.',
234
- ' Grant it to ~/.local/bin/iapeer (and to Terminal too, if you attach) in:',
240
+ 'macOS Full Disk Access NOT granted (manual — TCC cannot be set by any flag, settings key or script):',
241
+ ' Without it, file I/O on TCC-protected paths SILENTLY FAILS with EPERM (no prompt, no hang) —',
242
+ ' a peer reading/writing an iCloud Obsidian vault (iCloud Drive / Desktop / Documents / Downloads)',
243
+ ' just gets denied. `~/.iapeer` itself is under $HOME and needs nothing.',
244
+ ` Grant it to ${opts.binPath} (and to Terminal too, if you attach) in:`,
235
245
  ' System Settings → Privacy & Security → Full Disk Access',
236
246
  ].join('\n') + '\n'
237
247
  )
238
248
  }
249
+
250
+ // ─────────────────────────────────────────────────────────────────────────────
251
+ // Runtime auth readiness — the one CLEAN-HOST modal a launcher CANNOT auto-clear
252
+ // ─────────────────────────────────────────────────────────────────────────────
253
+ //
254
+ // A headless peer runs the runtime's INTERACTIVE TUI (a pty), not `-p`. On a clean
255
+ // host the first-run flow blocks on modals; the launcher auto-answers the ones it
256
+ // recognizes (theme picker, folder-trust, …) — but the LOGIN screen cannot be
257
+ // auto-answered (selecting subscription opens a browser OAuth flow no headless peer
258
+ // can complete). So login is a PREREQUISITE: the host must be authenticated BEFORE
259
+ // the first peer launches, or that peer's wake fails loud at the boot deadline.
260
+ //
261
+ // These probes are CONSERVATIVE: they return ready ONLY on positive evidence, so a
262
+ // missing signal nags (one harmless advisory line) rather than letting an
263
+ // unauthenticated host silently ship a peer that will fail to wake.
264
+
265
+ /** Resolve claude's config dir (CLAUDE_CONFIG_DIR override, else ~/.claude). */
266
+ function claudeConfigDir(env: NodeJS.ProcessEnv): string {
267
+ const override = env.CLAUDE_CONFIG_DIR?.trim()
268
+ if (override) return override
269
+ return join(env.HOME?.trim() || homedir(), '.claude')
270
+ }
271
+
272
+ /**
273
+ * True when claude is authenticated for a headless launch (won't block on the login
274
+ * picker). Positive evidence, in order: an auth env var; a completed onboarding +
275
+ * account in `~/.claude.json` (the subscription `claude login` path); or an
276
+ * `apiKeyHelper` in settings.json. Anything else → not ready (nag). Reads are
277
+ * env-rooted (HOME/CLAUDE_CONFIG_DIR) so a hermetic test isolates them.
278
+ */
279
+ export function claudeAuthReady(env: NodeJS.ProcessEnv = process.env): boolean {
280
+ if (env.ANTHROPIC_API_KEY?.trim() || env.ANTHROPIC_AUTH_TOKEN?.trim() || env.CLAUDE_CODE_OAUTH_TOKEN?.trim()) {
281
+ return true
282
+ }
283
+ // `~/.claude.json` lives at HOME root (not inside the config dir) in the default
284
+ // layout the fleet uses; hasCompletedOnboarding + an account/key marker proves the
285
+ // first-run flow (theme + login) is done.
286
+ const statePath = join(env.HOME?.trim() || homedir(), '.claude.json')
287
+ try {
288
+ const j = JSON.parse(readFileSync(statePath, 'utf8')) as Record<string, unknown>
289
+ if (j.hasCompletedOnboarding === true && (j.oauthAccount != null || j.customApiKeyResponses != null)) return true
290
+ } catch {
291
+ /* absent/unreadable → fall through to settings */
292
+ }
293
+ try {
294
+ const s = JSON.parse(readFileSync(join(claudeConfigDir(env), 'settings.json'), 'utf8')) as Record<string, unknown>
295
+ if (typeof s.apiKeyHelper === 'string' && s.apiKeyHelper.trim()) return true
296
+ } catch {
297
+ /* no settings / no helper */
298
+ }
299
+ return false
300
+ }
301
+
302
+ /**
303
+ * True when codex is authenticated (won't block / error at startup). Positive
304
+ * evidence: `OPENAI_API_KEY` env, or an `auth.json` under $CODEX_HOME/~/.codex.
305
+ * Env-rooted for hermetic tests.
306
+ */
307
+ export function codexAuthReady(env: NodeJS.ProcessEnv = process.env): boolean {
308
+ if (env.OPENAI_API_KEY?.trim()) return true
309
+ const codexHome = env.CODEX_HOME?.trim()
310
+ const authPath = codexHome
311
+ ? join(codexHome, 'auth.json')
312
+ : join(env.HOME?.trim() || homedir(), '.codex', 'auth.json')
313
+ return existsSync(authPath)
314
+ }
315
+
316
+ /**
317
+ * Advisory line for a runtime that is NOT authenticated (so the operator fixes it
318
+ * BEFORE launching peers). Returns null when the runtime is ready. Pure dispatch
319
+ * over the per-runtime probes above.
320
+ */
321
+ export function runtimeAuthNote(runtime: OnboardRuntime, env: NodeJS.ProcessEnv = process.env): string | null {
322
+ if (runtime === 'claude') {
323
+ if (claudeAuthReady(env)) return null
324
+ return (
325
+ 'claude: NOT authenticated — the first peer would block on the login screen (a launcher cannot\n' +
326
+ ' complete the browser OAuth flow). Before launching peers, either run `claude` once in your\n' +
327
+ ' terminal to sign in (subscription), or set ANTHROPIC_API_KEY in the daemon environment.'
328
+ )
329
+ }
330
+ if (codexAuthReady(env)) return null
331
+ return (
332
+ 'codex: NOT authenticated — the first codex peer cannot reach the model. Before launching codex\n' +
333
+ ' peers, run `codex login` once (or set OPENAI_API_KEY in the daemon environment).'
334
+ )
335
+ }