@agfpd/iapeer 0.2.77 → 0.2.79

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.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/package.json +21 -2
  4. package/src/cli/index.ts +95 -78
  5. package/src/connect/index.ts +4 -4
  6. package/src/core/constants.ts +3 -3
  7. package/src/core/normalize.ts +2 -2
  8. package/src/create/index.ts +1 -1
  9. package/src/daemon/index.ts +5 -5
  10. package/src/daemon/main.ts +9 -9
  11. package/src/enable/index.ts +19 -18
  12. package/src/enable/provisionCommand.ts +10 -10
  13. package/src/identity/index.ts +2 -2
  14. package/src/identity/profileStandard.ts +8 -8
  15. package/src/init/index.ts +10 -10
  16. package/src/install/index.ts +2 -2
  17. package/src/install/signing.ts +3 -4
  18. package/src/launch/adapters/claude.ts +7 -7
  19. package/src/launch/adapters/codex.ts +5 -6
  20. package/src/launch/canary.ts +8 -8
  21. package/src/launch/cmdlog.ts +3 -3
  22. package/src/launch/composeSystemPrompt.ts +2 -1
  23. package/src/launch/index.ts +5 -6
  24. package/src/launch/invocation.ts +1 -1
  25. package/src/launch/launchd.ts +14 -15
  26. package/src/launch/launchdRun.ts +6 -6
  27. package/src/launch/nativeMemory.ts +9 -9
  28. package/src/launch/ptyHost.ts +1 -1
  29. package/src/launch/readyGateModel.ts +3 -3
  30. package/src/launch/types.ts +7 -7
  31. package/src/lifecycle/eventlog.ts +2 -2
  32. package/src/lifecycle/index.ts +21 -21
  33. package/src/lifecycle/queue.ts +1 -1
  34. package/src/onboard/index.ts +19 -13
  35. package/src/onboard/memory.ts +5 -5
  36. package/src/onboard/steps.ts +8 -9
  37. package/src/provision/index.ts +2 -3
  38. package/src/runtime/deploy.ts +2 -2
  39. package/src/runtime/index.ts +4 -4
  40. package/src/shadow/gate.ts +6 -6
  41. package/src/shadow/index.ts +2 -2
  42. package/src/status/index.ts +11 -11
  43. package/src/storage/index.ts +1 -1
  44. package/src/supervisor/daemon.ts +20 -13
  45. package/src/transport/index.ts +11 -11
  46. package/src/update/index.ts +4 -4
  47. package/src/cli/cli.test.ts +0 -603
  48. package/src/cli/listTui.test.ts +0 -71
  49. package/src/codec/codec.test.ts +0 -271
  50. package/src/connect/connect.test.ts +0 -163
  51. package/src/core/constants.test.ts +0 -32
  52. package/src/core/normalize.test.ts +0 -98
  53. package/src/create/create.test.ts +0 -143
  54. package/src/daemon/daemon-http.test.ts +0 -114
  55. package/src/daemon/daemon.test.ts +0 -267
  56. package/src/daemon/main.test.ts +0 -413
  57. package/src/enable/enable.test.ts +0 -95
  58. package/src/enable/provisionCommand.test.ts +0 -107
  59. package/src/identity/identity.test.ts +0 -279
  60. package/src/identity/profileStandard.test.ts +0 -204
  61. package/src/init/init.test.ts +0 -215
  62. package/src/init/runtime-resolve.test.ts +0 -49
  63. package/src/install/install.test.ts +0 -66
  64. package/src/install/signing.test.ts +0 -84
  65. package/src/launch/bootdeliver.test.ts +0 -108
  66. package/src/launch/bootstrap.test.ts +0 -56
  67. package/src/launch/canary.test.ts +0 -346
  68. package/src/launch/cmdlog.test.ts +0 -94
  69. package/src/launch/codexHooksTrust.test.ts +0 -248
  70. package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
  71. package/src/launch/composeSystemPrompt.test.ts +0 -145
  72. package/src/launch/hostComposerOccupied.test.ts +0 -63
  73. package/src/launch/invocation.test.ts +0 -174
  74. package/src/launch/launch.test.ts +0 -359
  75. package/src/launch/launchd.test.ts +0 -499
  76. package/src/launch/nativeMemory.test.ts +0 -193
  77. package/src/launch/ptyHost.test.ts +0 -38
  78. package/src/launch/readyGateModel.test.ts +0 -72
  79. package/src/launch/sockdir.test.ts +0 -74
  80. package/src/lifecycle/eventlog.test.ts +0 -114
  81. package/src/lifecycle/lifecycle.test.ts +0 -1056
  82. package/src/lifecycle/queue.test.ts +0 -185
  83. package/src/onboard/memory.test.ts +0 -157
  84. package/src/onboard/onboard.test.ts +0 -39
  85. package/src/onboard/steps.test.ts +0 -201
  86. package/src/provision/provision.test.ts +0 -288
  87. package/src/registry/registry.test.ts +0 -432
  88. package/src/runtime/runtime.test.ts +0 -226
  89. package/src/runtime/update.test.ts +0 -192
  90. package/src/shadow/enumerate-skip.test.ts +0 -67
  91. package/src/shadow/gate.test.ts +0 -286
  92. package/src/shadow/install.test.ts +0 -153
  93. package/src/shadow/shadow.test.ts +0 -42
  94. package/src/status/status.test.ts +0 -208
  95. package/src/storage/peers-home.test.ts +0 -34
  96. package/src/storage/rotatelog.test.ts +0 -44
  97. package/src/storage/storage.test.ts +0 -65
  98. package/src/supervisor/attached-marker.test.ts +0 -84
  99. package/src/supervisor/boot.test.ts +0 -66
  100. package/src/supervisor/client.attach.test.ts +0 -107
  101. package/src/supervisor/daemon.test.ts +0 -213
  102. package/src/supervisor/deliver.test.ts +0 -58
  103. package/src/supervisor/panelog.test.ts +0 -95
  104. package/src/supervisor/protocol.test.ts +0 -222
  105. package/src/supervisor/selfargv.test.ts +0 -22
  106. package/src/supervisor/spinner-strip.test.ts +0 -52
  107. package/src/transport/compactDone.test.ts +0 -144
  108. package/src/transport/composerQueue.test.ts +0 -204
  109. package/src/transport/hostControl.test.ts +0 -87
  110. package/src/transport/hostDeliver.test.ts +0 -137
  111. package/src/transport/hostLive.test.ts +0 -50
  112. package/src/transport/routesend-wake.test.ts +0 -357
  113. package/src/update/update.test.ts +0 -208
@@ -1,215 +0,0 @@
1
- // init — per-peer onboarding: HTTP-MCP .mcp.json wiring (the install-gate-proven
2
- // transport), doctrine template, and the initPeer orchestration over provisionPeer.
3
-
4
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
5
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
6
- import { tmpdir } from 'os'
7
- import { join } from 'path'
8
- import {
9
- CODEX_BEARER_ENV_VAR,
10
- IAPEER_MCP_SERVER_NAME,
11
- codexConfigPath,
12
- ensureCodexUpdateCheckDisabled,
13
- ensureDoctrineTemplate,
14
- initPeer,
15
- resolveDaemonMcpUrl,
16
- writeClaudeMcpConfig,
17
- writeCodexMcpConfig,
18
- } from './index.ts'
19
- import { readPeersIndex } from '../registry/index.ts'
20
- import { readPeerProfile } from '../identity/index.ts'
21
-
22
- let root: string
23
- let cwd: string
24
- function cleanEnv(extra: Record<string, string> = {}): NodeJS.ProcessEnv {
25
- const env: NodeJS.ProcessEnv = { ...process.env, IAPEER_ROOT: root, ...extra }
26
- delete env.PEER_PERSONALITY
27
- delete env.PEER_IDENTITY
28
- delete env.PEER_RUNTIME
29
- return env
30
- }
31
-
32
- beforeEach(() => {
33
- root = mkdtempSync(join(tmpdir(), 'iapeer-init-root-'))
34
- cwd = mkdtempSync(join(tmpdir(), 'iapeer-init-cwd-'))
35
- })
36
- afterEach(() => {
37
- rmSync(root, { recursive: true, force: true })
38
- rmSync(cwd, { recursive: true, force: true })
39
- })
40
-
41
- describe('resolveDaemonMcpUrl', () => {
42
- test('no router.json → well-known default loopback URL (IAPEER_PORT respected)', () => {
43
- expect(resolveDaemonMcpUrl({ env: cleanEnv() })).toBe('http://127.0.0.1:8765/mcp')
44
- expect(resolveDaemonMcpUrl({ env: cleanEnv({ IAPEER_PORT: '9999' }) })).toBe('http://127.0.0.1:9999/mcp')
45
- })
46
- test('router.json present → its tcp field (daemon-published endpoint)', () => {
47
- const stateDir = join(root, 'state', 'iapeer')
48
- mkdirSync(stateDir, { recursive: true })
49
- writeFileSync(join(stateDir, 'router.json'), JSON.stringify({ sock: '/x.sock', tcp: 'http://127.0.0.1:8765/mcp' }))
50
- expect(resolveDaemonMcpUrl({ env: cleanEnv() })).toBe('http://127.0.0.1:8765/mcp')
51
- })
52
- })
53
-
54
- describe('writeClaudeMcpConfig', () => {
55
- test('writes the iapeer http server with the X-IAPeer-Identity header', () => {
56
- const path = writeClaudeMcpConfig(cwd, 'boris', 'http://127.0.0.1:8765/mcp')
57
- const doc = JSON.parse(readFileSync(path, 'utf8'))
58
- expect(doc.mcpServers[IAPEER_MCP_SERVER_NAME]).toEqual({
59
- type: 'http',
60
- url: 'http://127.0.0.1:8765/mcp',
61
- // env-expanded by claude from the session PEER_IDENTITY; literal fallback for
62
- // a manual (no-env) session — identity follows the SESSION, not the cwd file.
63
- headers: { 'X-IAPeer-Identity': '${PEER_IDENTITY:-claude-boris}' },
64
- })
65
- })
66
- test('MERGES — preserves other mcpServers, only (re)writes iapeer', () => {
67
- writeFileSync(join(cwd, '.mcp.json'), JSON.stringify({ mcpServers: { other: { type: 'http', url: 'https://x/mcp' } } }))
68
- writeClaudeMcpConfig(cwd, 'boris', 'http://127.0.0.1:8765/mcp')
69
- const doc = JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8'))
70
- expect(doc.mcpServers.other).toEqual({ type: 'http', url: 'https://x/mcp' })
71
- expect(doc.mcpServers.iapeer.headers['X-IAPeer-Identity']).toBe('${PEER_IDENTITY:-claude-boris}')
72
- })
73
- test('idempotent — re-running yields the same bytes', () => {
74
- const p = writeClaudeMcpConfig(cwd, 'boris', 'http://127.0.0.1:8765/mcp')
75
- const first = readFileSync(p, 'utf8')
76
- writeClaudeMcpConfig(cwd, 'boris', 'http://127.0.0.1:8765/mcp')
77
- expect(readFileSync(p, 'utf8')).toBe(first)
78
- })
79
- })
80
-
81
- describe('ensureDoctrineTemplate', () => {
82
- test('creates the template when absent', () => {
83
- const { path, created } = ensureDoctrineTemplate(cwd)
84
- expect(created).toBe(true)
85
- expect(existsSync(path)).toBe(true)
86
- expect(readFileSync(path, 'utf8')).toContain('Peer doctrine')
87
- })
88
- test('never overwrites an existing doctrine', () => {
89
- mkdirSync(join(cwd, '.iapeer'), { recursive: true })
90
- writeFileSync(join(cwd, '.iapeer', 'IAPEER.md'), 'I am boris.')
91
- const { created } = ensureDoctrineTemplate(cwd)
92
- expect(created).toBe(false)
93
- expect(readFileSync(join(cwd, '.iapeer', 'IAPEER.md'), 'utf8')).toBe('I am boris.')
94
- })
95
- })
96
-
97
- describe('writeCodexMcpConfig (token-free recipe — dummy bearer + env_http_headers identity)', () => {
98
- test('adds [mcp_servers.iapeer] with url + approve + bearer_token_env_var + env_http_headers', () => {
99
- const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome-'))
100
- try {
101
- const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
102
- const { path, added } = writeCodexMcpConfig('http://127.0.0.1:8765/mcp', { env })
103
- expect(added).toBe(true)
104
- expect(path).toBe(codexConfigPath({ env }))
105
- const toml = readFileSync(path, 'utf8')
106
- expect(toml).toContain('[mcp_servers.iapeer]')
107
- expect(toml).toContain('url = "http://127.0.0.1:8765/mcp"')
108
- expect(toml).toContain('default_tools_approval_mode = "approve"')
109
- expect(toml).toContain(`bearer_token_env_var = "${CODEX_BEARER_ENV_VAR}"`)
110
- expect(toml).toContain('[mcp_servers.iapeer.env_http_headers]')
111
- expect(toml).toContain('"X-IAPeer-Identity" = "PEER_IDENTITY"')
112
- } finally {
113
- rmSync(codexHome, { recursive: true, force: true })
114
- }
115
- })
116
- test('idempotent — re-run does NOT duplicate the block, and preserves other config', () => {
117
- const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome2-'))
118
- try {
119
- const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
120
- writeFileSync(codexConfigPath({ env }), '[other]\nkeep = true\n')
121
- expect(writeCodexMcpConfig('http://x/mcp', { env }).added).toBe(true)
122
- expect(writeCodexMcpConfig('http://x/mcp', { env }).added).toBe(false) // idempotent
123
- const toml = readFileSync(codexConfigPath({ env }), 'utf8')
124
- expect((toml.match(/\[mcp_servers\.iapeer\]/g) ?? []).length).toBe(1) // no duplicate
125
- expect(toml).toContain('[other]') // other config preserved
126
- expect(toml).toContain('keep = true')
127
- } finally {
128
- rmSync(codexHome, { recursive: true, force: true })
129
- }
130
- })
131
- })
132
-
133
- describe('initPeer orchestration', () => {
134
- test('codex peer: profile + registry + token-free codex config + doctrine', async () => {
135
- const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome3-'))
136
- try {
137
- const env = cleanEnv({ CODEX_HOME: codexHome })
138
- const r = await initPeer({ cwd, runtime: 'codex', personality: 'cpeer', env })
139
- expect(readPeerProfile(cwd)?.personality).toBe('cpeer')
140
- expect(r.mcpConfigPaths.length).toBe(0) // no claude .mcp.json for a codex peer
141
- expect(r.codexMcpConfigPath).toBe(codexConfigPath({ env }))
142
- const toml = readFileSync(r.codexMcpConfigPath!, 'utf8')
143
- expect(toml).toContain('[mcp_servers.iapeer]')
144
- expect(toml).toContain(`bearer_token_env_var = "${CODEX_BEARER_ENV_VAR}"`) // token-free recipe wired
145
- } finally {
146
- rmSync(codexHome, { recursive: true, force: true })
147
- }
148
- })
149
-
150
- test('claude peer: profile + registry + .mcp.json + doctrine, all wired', async () => {
151
- const env = cleanEnv()
152
- const r = await initPeer({ cwd, runtime: 'claude', personality: 'mypeer', env })
153
- // identity + registry (via provision)
154
- expect(readPeerProfile(cwd)?.personality).toBe('mypeer')
155
- expect(readPeersIndex({ env }).peers.some(p => p.personality === 'mypeer')).toBe(true)
156
- // .mcp.json wired to the daemon with the per-peer identity header
157
- expect(r.mcpConfigPaths.length).toBe(1)
158
- const mcp = JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8'))
159
- expect(mcp.mcpServers.iapeer.headers['X-IAPeer-Identity']).toBe('${PEER_IDENTITY:-claude-mypeer}')
160
- expect(mcp.mcpServers.iapeer.url).toBe('http://127.0.0.1:8765/mcp')
161
- // doctrine template created
162
- expect(r.doctrineCreated).toBe(true)
163
- expect(existsSync(r.doctrinePath)).toBe(true)
164
- })
165
-
166
- test('idempotent — re-init does not throw and keeps an existing doctrine', async () => {
167
- const env = cleanEnv()
168
- await initPeer({ cwd, runtime: 'claude', personality: 'mypeer', env })
169
- writeFileSync(join(cwd, '.iapeer', 'IAPEER.md'), 'custom doctrine')
170
- const r2 = await initPeer({ cwd, runtime: 'claude', personality: 'mypeer', env })
171
- expect(r2.doctrineCreated).toBe(false)
172
- expect(readFileSync(join(cwd, '.iapeer', 'IAPEER.md'), 'utf8')).toBe('custom doctrine')
173
- })
174
- })
175
-
176
- describe('ensureCodexUpdateCheckDisabled (boot-gate hygiene — incident 11.06 codex-linus)', () => {
177
- test('PREPENDS the top-level key BEFORE the first [section] (TOML top-level scope)', () => {
178
- const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome3-'))
179
- try {
180
- const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
181
- writeFileSync(codexConfigPath({ env }), 'model = "gpt-5.5"\n\n[projects."/x"]\ntrust_level = "trusted"\n')
182
- const { changed } = ensureCodexUpdateCheckDisabled({ env })
183
- expect(changed).toBe(true)
184
- const toml = readFileSync(codexConfigPath({ env }), 'utf8')
185
- expect(toml.startsWith('check_for_update_on_startup = false\n')).toBe(true)
186
- expect(toml).toContain('[projects."/x"]') // existing config preserved
187
- // idempotent: second run is a no-op
188
- expect(ensureCodexUpdateCheckDisabled({ env }).changed).toBe(false)
189
- expect((toml.match(/check_for_update_on_startup/g) ?? []).length).toBe(1)
190
- } finally {
191
- rmSync(codexHome, { recursive: true, force: true })
192
- }
193
- })
194
- test('respects an EXPLICIT existing value (never overrides the operator)', () => {
195
- const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome4-'))
196
- try {
197
- const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
198
- writeFileSync(codexConfigPath({ env }), 'check_for_update_on_startup = true\n')
199
- expect(ensureCodexUpdateCheckDisabled({ env }).changed).toBe(false)
200
- expect(readFileSync(codexConfigPath({ env }), 'utf8')).toContain('check_for_update_on_startup = true')
201
- } finally {
202
- rmSync(codexHome, { recursive: true, force: true })
203
- }
204
- })
205
- test('absent config → created with just the key', () => {
206
- const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome5-'))
207
- try {
208
- const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
209
- expect(ensureCodexUpdateCheckDisabled({ env }).changed).toBe(true)
210
- expect(readFileSync(codexConfigPath({ env }), 'utf8')).toBe('check_for_update_on_startup = false\n')
211
- } finally {
212
- rmSync(codexHome, { recursive: true, force: true })
213
- }
214
- })
215
- })
@@ -1,49 +0,0 @@
1
- // resolvePrimaryRuntime — the runtime-aware, CONSISTENT primary-runtime resolution
2
- // for init/create. Pure (filesystem markers only); no registry / launchd touched.
3
-
4
- import { afterEach, describe, expect, test } from 'bun:test'
5
- import { mkdirSync, mkdtempSync, rmSync } from 'fs'
6
- import { tmpdir } from 'os'
7
- import { join } from 'path'
8
- import { resolvePrimaryRuntime } from './index.ts'
9
-
10
- const dirs: string[] = []
11
- function mkTmp(): string {
12
- const d = mkdtempSync(join(tmpdir(), 'iapeer-rt-'))
13
- dirs.push(d)
14
- return d
15
- }
16
- afterEach(() => {
17
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
18
- })
19
-
20
- describe('resolvePrimaryRuntime', () => {
21
- test('explicit runtime wins regardless of markers', () => {
22
- const cwd = mkTmp()
23
- mkdirSync(join(cwd, '.codex'))
24
- expect(resolvePrimaryRuntime(cwd, 'telegram')).toBe('telegram')
25
- })
26
-
27
- test('no marker → claude (default)', () => {
28
- expect(resolvePrimaryRuntime(mkTmp())).toBe('claude')
29
- })
30
-
31
- test('.codex-only folder → codex primary (was the inconsistency: defaulted to claude)', () => {
32
- const cwd = mkTmp()
33
- mkdirSync(join(cwd, '.codex'))
34
- expect(resolvePrimaryRuntime(cwd)).toBe('codex')
35
- })
36
-
37
- test('.claude folder → claude', () => {
38
- const cwd = mkTmp()
39
- mkdirSync(join(cwd, '.claude'))
40
- expect(resolvePrimaryRuntime(cwd)).toBe('claude')
41
- })
42
-
43
- test('both markers → claude primary (agentic default order)', () => {
44
- const cwd = mkTmp()
45
- mkdirSync(join(cwd, '.claude'))
46
- mkdirSync(join(cwd, '.codex'))
47
- expect(resolvePrimaryRuntime(cwd)).toBe('claude')
48
- })
49
- })
@@ -1,66 +0,0 @@
1
- // install — the stable binary path + (lightly) the build. iapeerBinPath is the
2
- // decoupling anchor: the launchd plists reference it, not process.execPath / a src
3
- // file. The full `bun build --compile` is exercised LIVE (it writes a ~60M binary —
4
- // too heavy for a unit test); here we pin the path resolution.
5
-
6
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
7
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
8
- import { tmpdir } from 'os'
9
- import { join } from 'path'
10
- import { iapeerBinPath, iapeerPrevBinPath, installIapeer, rollbackIapeer } from './index.ts'
11
-
12
- describe('iapeerBinPath', () => {
13
- test('default = <home>/.local/bin/iapeer (stable host-wide path, on $PATH)', () => {
14
- expect(iapeerBinPath({ HOME: '/Users/x' } as NodeJS.ProcessEnv)).toBe('/Users/x/.local/bin/iapeer')
15
- })
16
- test('IAPEER_BIN_DIR override (tests/sandbox — never a real ~/.local/bin)', () => {
17
- expect(iapeerBinPath({ HOME: '/Users/x', IAPEER_BIN_DIR: '/tmp/sbx/bin' } as NodeJS.ProcessEnv)).toBe(
18
- join('/tmp/sbx/bin', 'iapeer'),
19
- )
20
- })
21
- })
22
-
23
- // Audit #25 — fail-closed sandbox guard (symmetric to the registry's). installIapeer
24
- // overwrites the live prod binary ~/.local/bin/iapeer; a sandbox/test that forgets
25
- // IAPEER_BIN_DIR must be REFUSED, never clobber prod. The guard fires BEFORE the build,
26
- // so no `bun build --compile` runs here.
27
- describe('installIapeer fail-closed sandbox guard', () => {
28
- test('THROWS under IAPEER_TEST_SANDBOX=1 when binPath falls through to the REAL ~/.local/bin/iapeer', () => {
29
- const env = { IAPEER_TEST_SANDBOX: '1', HOME: '/Users/fake-home' } as NodeJS.ProcessEnv
30
- expect(iapeerBinPath(env)).toBe('/Users/fake-home/.local/bin/iapeer')
31
- expect(() => installIapeer('/x/entry.ts', env)).toThrow(/refusing to overwrite the REAL prod binary/)
32
- })
33
- })
34
-
35
- describe('rollbackIapeer', () => {
36
- let binDir: string
37
- let env: NodeJS.ProcessEnv
38
- beforeEach(() => {
39
- binDir = mkdtempSync(join(tmpdir(), 'iapeer-rollback-'))
40
- env = { IAPEER_TEST_SANDBOX: '1', HOME: '/Users/x', IAPEER_BIN_DIR: binDir } as NodeJS.ProcessEnv
41
- })
42
- afterEach(() => {
43
- rmSync(binDir, { recursive: true, force: true })
44
- })
45
-
46
- test('no .prev → failed (nothing to roll back to), binary untouched', () => {
47
- writeFileSync(iapeerBinPath(env), 'CURRENT')
48
- const r = rollbackIapeer(env)
49
- expect(r.status).toBe('failed')
50
- expect(r.reason).toMatch(/nothing to roll back/i)
51
- expect(readFileSync(iapeerBinPath(env), 'utf8')).toBe('CURRENT') // unchanged
52
- })
53
-
54
- test('restores the .prev bytes over the binary', () => {
55
- writeFileSync(iapeerBinPath(env), 'NEW-BROKEN')
56
- writeFileSync(iapeerPrevBinPath(env), 'OLD-GOOD')
57
- const r = rollbackIapeer(env)
58
- expect(r.status).toBe('rolled-back')
59
- expect(readFileSync(iapeerBinPath(env), 'utf8')).toBe('OLD-GOOD')
60
- })
61
-
62
- test('fail-closed sandbox guard: refuses the REAL prod binary path', () => {
63
- const realEnv = { IAPEER_TEST_SANDBOX: '1', HOME: '/Users/fake-home' } as NodeJS.ProcessEnv
64
- expect(() => rollbackIapeer(realEnv)).toThrow(/refusing to overwrite the REAL prod binary/)
65
- })
66
- })
@@ -1,84 +0,0 @@
1
- // signInstalledBinary — stable-identity re-sign so TCC grants survive updates
2
- // (Артур's DX requirement 10.06). DI-runner units; the real keychain flow was
3
- // proven live (/tmp experiment: two binaries, different CDHash, IDENTICAL
4
- // designated requirement `identifier "com.agfpd.iapeer" and certificate leaf`).
5
- // The sandbox guard double-checks process.env, so these tests inject a runner
6
- // AND call with the flag stripped via a direct env — guard tested separately.
7
-
8
- import { describe, expect, test } from 'bun:test'
9
- import { SIGNING_IDENTIFIER, SIGNING_IDENTITY_CN, signInstalledBinary, type SigningRunner } from './signing.ts'
10
-
11
- function harness(opts: { identityExists: boolean; failAt?: 'req' | 'pkcs12' | 'import' | 'codesign' }) {
12
- const calls: { cmd: string; args: string[] }[] = []
13
- const run: SigningRunner = (cmd, args) => {
14
- calls.push({ cmd, args })
15
- if (cmd === 'security' && args[0] === 'find-identity') {
16
- return { status: 0, stdout: opts.identityExists ? `1) ABC "${SIGNING_IDENTITY_CN}" (CSSMERR_TP_NOT_TRUSTED)\n` : 'no identities\n', stderr: '' }
17
- }
18
- if (cmd.endsWith('openssl') && args[0] === 'req') return { status: opts.failAt === 'req' ? 1 : 0, stdout: '', stderr: 'req boom' }
19
- if (cmd.endsWith('openssl') && args[0] === 'pkcs12') return { status: opts.failAt === 'pkcs12' ? 1 : 0, stdout: '', stderr: 'p12 boom' }
20
- if (cmd === 'security' && args[0] === 'import') return { status: opts.failAt === 'import' ? 1 : 0, stdout: '', stderr: 'import boom' }
21
- if (cmd === 'codesign') return { status: opts.failAt === 'codesign' ? 1 : 0, stdout: '', stderr: 'sign boom' }
22
- return { status: 0, stdout: '', stderr: '' }
23
- }
24
- return { calls, run }
25
- }
26
-
27
- // NOTE: process.env.IAPEER_TEST_SANDBOX === '1' under `bun run test`, so the
28
- // guard SHORT-CIRCUITS every real call — these units therefore stub process.env
29
- // off for the duration of each call.
30
- function withSandboxOff<T>(fn: () => T): T {
31
- const prev = process.env.IAPEER_TEST_SANDBOX
32
- delete process.env.IAPEER_TEST_SANDBOX
33
- try {
34
- return fn()
35
- } finally {
36
- if (prev !== undefined) process.env.IAPEER_TEST_SANDBOX = prev
37
- }
38
- }
39
-
40
- describe('signInstalledBinary (stable identity → TCC grants survive updates)', () => {
41
- test('sandbox guard: never touches the keychain under IAPEER_TEST_SANDBOX', () => {
42
- const h = harness({ identityExists: true })
43
- const r = signInstalledBinary('/x/iapeer', { IAPEER_TEST_SANDBOX: '1' } as NodeJS.ProcessEnv, h.run)
44
- expect(r.state).toBe('skipped-sandbox')
45
- expect(h.calls.length).toBe(0)
46
- })
47
-
48
- test('existing identity → single codesign with the stable identifier', () => {
49
- const h = harness({ identityExists: true })
50
- const r = withSandboxOff(() => signInstalledBinary('/x/iapeer', {} as NodeJS.ProcessEnv, h.run))
51
- expect(r.state).toBe('signed')
52
- const sign = h.calls.find(c => c.cmd === 'codesign')!
53
- expect(sign.args).toEqual(['-f', '-s', SIGNING_IDENTITY_CN, '--identifier', SIGNING_IDENTIFIER, '/x/iapeer'])
54
- // identity lookup is NOT -v (an untrusted self-signed identity must be found)
55
- const find = h.calls.find(c => c.args[0] === 'find-identity')!
56
- expect(find.args).not.toContain('-v')
57
- })
58
-
59
- test('no identity → created once (openssl req → pkcs12 → import -T codesign), then signed', () => {
60
- const h = harness({ identityExists: false })
61
- const r = withSandboxOff(() => signInstalledBinary('/x/iapeer', {} as NodeJS.ProcessEnv, h.run))
62
- expect(r.state).toBe('signed-new-identity')
63
- const seq = h.calls.map(c => `${c.cmd.split('/').pop()}:${c.args[0]}`)
64
- expect(seq).toEqual(['security:find-identity', 'openssl:req', 'openssl:pkcs12', 'security:import', 'codesign:-f'])
65
- const imp = h.calls.find(c => c.args[0] === 'import')!
66
- expect(imp.args).toContain('-T') // codesign pre-authorized in the key ACL
67
- expect(imp.args).toContain('/usr/bin/codesign')
68
- })
69
-
70
- test('identity-creation failure → failed-soft with the loud TCC consequence, codesign never attempted', () => {
71
- const h = harness({ identityExists: false, failAt: 'import' })
72
- const r = withSandboxOff(() => signInstalledBinary('/x/iapeer', {} as NodeJS.ProcessEnv, h.run))
73
- expect(r.state).toBe('failed-soft')
74
- expect(r.detail).toContain('TCC prompts will re-appear')
75
- expect(h.calls.some(c => c.cmd === 'codesign')).toBe(false)
76
- })
77
-
78
- test('codesign failure → failed-soft (install never breaks on a signing hiccup)', () => {
79
- const h = harness({ identityExists: true, failAt: 'codesign' })
80
- const r = withSandboxOff(() => signInstalledBinary('/x/iapeer', {} as NodeJS.ProcessEnv, h.run))
81
- expect(r.state).toBe('failed-soft')
82
- expect(r.detail).toContain('sign boom')
83
- })
84
- })
@@ -1,108 +0,0 @@
1
- // Ф-#8b: cold-wake boot first-message delivery via load-buffer + bracketed
2
- // paste-buffer — the SAME byte-path as warm delivery (transport.deliverViaTmux),
3
- // replacing the old `send-keys -l` retype. Proven hermetically against a REAL
4
- // tmux (gated like sockdir.test.ts) with a fake tui adapter whose "runtime" is
5
- // `cat >> <file>`: whatever the boot path injects into the pane lands verbatim in
6
- // the file, and the ready-gate keys on that file's mtime (the activity proxy).
7
- //
8
- // pty note: `cat` reads the pane pty in CANONICAL mode (line-buffered, ~1 KiB
9
- // line cap on macOS) — real TUIs run raw and have no such cap. The fixture
10
- // message therefore uses many sub-1-KiB LINES to total multi-KiB; what this test
11
- // pins is the INJECTION path (one bracketed paste, no option-parsing traps, no
12
- // key-by-key retype), not the tty discipline.
13
-
14
- import { afterEach, describe, expect, test } from 'bun:test'
15
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs'
16
- import { tmpdir } from 'os'
17
- import { join } from 'path'
18
- import { spawnSync } from 'child_process'
19
- import { launch } from './index.ts'
20
- import type { LaunchConfig, LaunchSpec, RuntimeAdapter } from './types.ts'
21
-
22
- const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
23
-
24
- const dirs: string[] = []
25
- function mkTmp(): string {
26
- const d = mkdtempSync(join(tmpdir(), 'iapeer-bootdeliver-'))
27
- dirs.push(d)
28
- return d
29
- }
30
- afterEach(() => {
31
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
32
- })
33
-
34
- /** Fake tui adapter: the "runtime" appends its pty input to `recvPath`; the
35
- * activity proxy is that file's mtime (absent → null, exactly like a missing
36
- * transcript), so the boot baseline is 0 and the ready-gate flips on receipt. */
37
- function catAdapter(recvPath: string): RuntimeAdapter {
38
- return {
39
- runtime: 'claude', // any tui Runtime — the adapter object itself is what launch consumes
40
- kind: 'tui',
41
- usesDoctrine: false,
42
- deliveryMarkers: { promptGlyphs: [] },
43
- buildArgv: () => ['/bin/sh', '-c', `cat >> ${recvPath}`],
44
- bootDialogKeys: () => null,
45
- isInputReady: () => true,
46
- newestActivityMtime: () => {
47
- try {
48
- return statSync(recvPath).mtimeMs
49
- } catch {
50
- return null
51
- }
52
- },
53
- resolveResume: () => ({ ok: true }),
54
- executeControl: () => null,
55
- }
56
- }
57
-
58
- describe('boot first-message delivery (load-buffer + bracketed paste)', () => {
59
- test.if(tmuxAvailable)(
60
- 'a multi-line, dash-leading, multi-KiB first message lands INTACT and launch goes READY',
61
- async () => {
62
- const root = mkTmp()
63
- const recv = join(root, 'received.txt')
64
- const sock = join(root, 'tmux-iap-claude-bootd.sock')
65
- // The fixture stresses every historical boot-inject trap at once:
66
- // • leading '-' — the send-keys option-parsing trap (audit #6);
67
- // • quotes/$()/; — shell-metachar corruption if anything re-quoted the body;
68
- // • multi-KiB — a size send-keys -l replayed key-by-key (8 × ~700 B lines).
69
- const firstMessage = [
70
- '- dash-leading first line (the send-keys option-parsing trap)',
71
- `quotes "double" 'single' and $dollar \`backtick\` ; semicolon`,
72
- ...Array.from({ length: 8 }, (_, i) => `${i}:${'x'.repeat(700)}`),
73
- ].join('\n')
74
- const spec: LaunchSpec = {
75
- personality: 'bootd',
76
- runtime: 'claude',
77
- cwd: root,
78
- identity: 'claude-bootd',
79
- socketPath: sock,
80
- intelligence: 'artificial',
81
- }
82
- const cfg: LaunchConfig = {
83
- claudeBin: 'unused',
84
- codexBin: 'unused',
85
- sockDir: root,
86
- bootDeadlineSecs: 10,
87
- readyGateSecs: 8,
88
- logDir: join(root, 'logs'),
89
- }
90
- // pty-supervisor hosting is the DEFAULT now; this fixture exercises the legacy TMUX boot/delivery
91
- // path (load-buffer + bracketed paste) explicitly, so opt this peer OUT of pty with a .no-pty-host
92
- // marker (otherwise launch hosts it under the supervisor and the tmux-path assertions never apply).
93
- mkdirSync(cfg.logDir, { recursive: true })
94
- writeFileSync(join(cfg.logDir, `${spec.identity}.no-pty-host`), '')
95
- try {
96
- const r = await launch(spec, catAdapter(recv), firstMessage, cfg)
97
- expect(r.status).toBe('READY')
98
- const got = readFileSync(recv, 'utf8')
99
- expect(got).toContain('- dash-leading first line (the send-keys option-parsing trap)')
100
- expect(got).toContain(`quotes "double" 'single' and $dollar \`backtick\` ; semicolon`)
101
- for (let i = 0; i < 8; i++) expect(got).toContain(`${i}:${'x'.repeat(700)}`)
102
- } finally {
103
- spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
104
- }
105
- },
106
- 60000,
107
- )
108
- })
@@ -1,56 +0,0 @@
1
- // launchctlBootstrap — the AUTO-load primitive guards. The happy `loaded` path is
2
- // proven LIVE (it calls real launchctl); the unit tests cover the three guards that
3
- // must hold WITHOUT touching launchd: foreign-plist refusal, sandbox skip, and the
4
- // foundation-owned gate. (The test script sets IAPEER_TEST_SANDBOX=1, so the skip
5
- // branch is the default; the foreign-refusal branch is checked first regardless.)
6
-
7
- import { afterEach, describe, expect, test } from 'bun:test'
8
- import { mkdtempSync, rmSync, writeFileSync } from 'fs'
9
- import { tmpdir } from 'os'
10
- import { join } from 'path'
11
- import { launchctlBootstrap, installAlwaysOnPlist } from './index.ts'
12
-
13
- const dirs: string[] = []
14
- function mkTmp(): string {
15
- const d = mkdtempSync(join(tmpdir(), 'iapeer-boot-'))
16
- dirs.push(d)
17
- return d
18
- }
19
- afterEach(() => {
20
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
21
- })
22
-
23
- describe('launchctlBootstrap guards', () => {
24
- test('refuses a NON-foundation plist (no ownership sentinel) — fleet guard, checked first', () => {
25
- const dir = mkTmp()
26
- const foreign = join(dir, 'com.iapeer.boris.plist')
27
- writeFileSync(foreign, '<?xml version="1.0"?><plist><dict></dict></plist>')
28
- // refused-foreign wins even with IAPEER_TEST_SANDBOX=1 (the guard is checked before sandbox)
29
- const r = launchctlBootstrap('boris', foreign, { IAPEER_TEST_SANDBOX: '1' } as NodeJS.ProcessEnv)
30
- expect(r.state).toBe('refused-foreign')
31
- expect(r.label).toBe('com.iapeer.boris')
32
- })
33
-
34
- test('a foundation-owned plist under IAPEER_TEST_SANDBOX=1 → skipped-sandbox (no launchctl)', () => {
35
- const root = mkTmp()
36
- const bindir = mkTmp()
37
- const bin = join(bindir, 'notifier-runtime')
38
- writeFileSync(bin, '#!/bin/sh\nexec sleep 1\n', { mode: 0o755 })
39
- const env = {
40
- IAPEER_LAUNCHAGENTS_DIR: join(root, 'LA'),
41
- HOME: root,
42
- PATH: bindir,
43
- IAPEER_TEST_SANDBOX: '1',
44
- } as NodeJS.ProcessEnv
45
- const cwd = join(root, 'timer')
46
- const plist = installAlwaysOnPlist({ personality: 'timer', runtime: 'notifier', cwd, runtimeBin: bin, env })
47
- const r = launchctlBootstrap('timer', plist, env)
48
- expect(r.state).toBe('skipped-sandbox')
49
- expect(r.label).toBe('com.iapeer.timer')
50
- })
51
-
52
- test('absent/unreadable plist → refused-foreign (not provably ours)', () => {
53
- const r = launchctlBootstrap('ghost', join(mkTmp(), 'nope.plist'), { IAPEER_TEST_SANDBOX: '1' } as NodeJS.ProcessEnv)
54
- expect(r.state).toBe('refused-foreign')
55
- })
56
- })