@agfpd/iapeer 0.1.0
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/bin/iapeer +25 -0
- package/package.json +37 -0
- package/src/cli/cli.test.ts +130 -0
- package/src/cli/index.ts +608 -0
- package/src/cli/listTui.test.ts +70 -0
- package/src/cli/listTui.ts +165 -0
- package/src/codec/codec.test.ts +271 -0
- package/src/codec/index.ts +217 -0
- package/src/core/constants.test.ts +21 -0
- package/src/core/constants.ts +180 -0
- package/src/core/errors.ts +20 -0
- package/src/core/index.ts +3 -0
- package/src/core/normalize.test.ts +98 -0
- package/src/core/normalize.ts +89 -0
- package/src/core/socket.ts +63 -0
- package/src/create/create.test.ts +143 -0
- package/src/create/index.ts +178 -0
- package/src/daemon/daemon-http.test.ts +114 -0
- package/src/daemon/daemon.test.ts +103 -0
- package/src/daemon/index.ts +439 -0
- package/src/daemon/main.test.ts +194 -0
- package/src/daemon/main.ts +230 -0
- package/src/enable/enable.test.ts +92 -0
- package/src/enable/index.ts +381 -0
- package/src/identity/identity.test.ts +262 -0
- package/src/identity/index.ts +603 -0
- package/src/index.ts +27 -0
- package/src/init/index.ts +408 -0
- package/src/init/init.test.ts +171 -0
- package/src/init/runtime-resolve.test.ts +49 -0
- package/src/install/index.ts +84 -0
- package/src/install/install.test.ts +31 -0
- package/src/launch/adapters/claude.ts +250 -0
- package/src/launch/adapters/codex.ts +329 -0
- package/src/launch/adapters/notifier.ts +90 -0
- package/src/launch/adapters/telegram.ts +130 -0
- package/src/launch/bootstrap.test.ts +56 -0
- package/src/launch/composeSystemPrompt.layers.test.ts +319 -0
- package/src/launch/composeSystemPrompt.test.ts +98 -0
- package/src/launch/composeSystemPrompt.ts +261 -0
- package/src/launch/index.ts +253 -0
- package/src/launch/launch.test.ts +233 -0
- package/src/launch/launchd.test.ts +363 -0
- package/src/launch/launchd.ts +375 -0
- package/src/launch/launchdRun.ts +168 -0
- package/src/launch/sockdir.test.ts +70 -0
- package/src/launch/types.ts +300 -0
- package/src/lifecycle/index.ts +840 -0
- package/src/lifecycle/lifecycle.test.ts +496 -0
- package/src/onboard/index.ts +135 -0
- package/src/onboard/onboard.test.ts +39 -0
- package/src/provision/index.ts +170 -0
- package/src/provision/provision.test.ts +104 -0
- package/src/registry/index.ts +453 -0
- package/src/registry/registry.test.ts +400 -0
- package/src/runtime/deploy.ts +230 -0
- package/src/runtime/index.ts +191 -0
- package/src/runtime/runtime.test.ts +226 -0
- package/src/storage/index.ts +331 -0
- package/src/storage/peers-home.test.ts +34 -0
- package/src/storage/storage.test.ts +65 -0
- package/src/transport/index.ts +522 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Provision — the one-call peer-creation entrypoint. Composes the two foundation
|
|
2
|
+
// writes a new peer needs: identity.ensurePeerProfile (local <cwd>/.iapeer/
|
|
3
|
+
// peer-profile.json + scaffold + — for an INFRA runtime — the always-on launchd
|
|
4
|
+
// plist) and registry.upsertPeer (the global peers-profiles.json entry the daemon
|
|
5
|
+
// reads for its tool-list, caller resolution, and wake-on-miss findPeer).
|
|
6
|
+
//
|
|
7
|
+
// INFRA gotcha closed at this layer: a notifier/telegram always-on plist runs its
|
|
8
|
+
// launcher under launchd's MINIMAL PATH (no ~/.local/bin, ~/.bun/bin). So before
|
|
9
|
+
// writing the plist we RESOLVE the runtime launcher to an absolute path against the
|
|
10
|
+
// rich provisioning PATH and bake it in (NOTIFIER_RUNTIME_BIN / TELEGRAM_RUNTIME_BIN
|
|
11
|
+
// via installAlwaysOnPlist). If it does not resolve to an executable, we REFUSE to
|
|
12
|
+
// provision rather than create a peer whose always-on session crash-loops.
|
|
13
|
+
|
|
14
|
+
import { basename, resolve } from 'path'
|
|
15
|
+
import {
|
|
16
|
+
INFRA_RUNTIME_DEFAULT_BIN,
|
|
17
|
+
isInfraRuntime,
|
|
18
|
+
isRuntime,
|
|
19
|
+
normalizeNameCandidate,
|
|
20
|
+
type Intelligence,
|
|
21
|
+
type Runtime,
|
|
22
|
+
} from '../core/constants.ts'
|
|
23
|
+
import { IapError } from '../core/errors.ts'
|
|
24
|
+
import { peerProfilePath } from '../storage/index.ts'
|
|
25
|
+
import { ensurePeerProfile } from '../identity/index.ts'
|
|
26
|
+
import { readPeersIndex, upsertPeer } from '../registry/index.ts'
|
|
27
|
+
import { launchdPlistPath, resolveExecutable } from '../launch/launchd.ts'
|
|
28
|
+
|
|
29
|
+
export interface ProvisionPeerOptions {
|
|
30
|
+
/** The peer's working directory. personality defaults to normalized basename. */
|
|
31
|
+
cwd: string
|
|
32
|
+
runtime: Runtime
|
|
33
|
+
/** Explicit personality override (default: normalized basename(cwd)). */
|
|
34
|
+
personality?: string
|
|
35
|
+
description?: string
|
|
36
|
+
intelligence?: Intelligence
|
|
37
|
+
/** Infra runtime launcher (abs path or PATH name); resolved to an abs path. */
|
|
38
|
+
runtimeBin?: string
|
|
39
|
+
env?: NodeJS.ProcessEnv
|
|
40
|
+
warn?: (message: string) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ProvisionResult {
|
|
44
|
+
personality: string
|
|
45
|
+
runtime: Runtime
|
|
46
|
+
cwd: string
|
|
47
|
+
profilePath: string
|
|
48
|
+
intelligence: Intelligence
|
|
49
|
+
/** For an infra runtime: the installed always-on plist path. */
|
|
50
|
+
plistPath?: string
|
|
51
|
+
/** For an infra runtime: the absolute launcher path baked into the plist. */
|
|
52
|
+
runtimeBin?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Provision a new peer in one call: local profile + scaffold (+ infra always-on
|
|
57
|
+
* plist with a pinned launcher) then the registry entry. Idempotent-ish: if the
|
|
58
|
+
* cwd already has a peer-profile.json, ensurePeerProfile returns it unchanged (no
|
|
59
|
+
* second plist install) and the registry is upserted (merge-with-existing).
|
|
60
|
+
*/
|
|
61
|
+
export async function provisionPeer(opts: ProvisionPeerOptions): Promise<ProvisionResult> {
|
|
62
|
+
const env = opts.env ?? process.env
|
|
63
|
+
if (!isRuntime(opts.runtime)) {
|
|
64
|
+
throw new IapError(`invalid runtime "${opts.runtime}" — must match /^[a-z][a-z0-9]{0,31}$/`)
|
|
65
|
+
}
|
|
66
|
+
const cwd = resolve(opts.cwd)
|
|
67
|
+
|
|
68
|
+
// INFRA: resolve the launcher to an abs path NOW (rich provisioning PATH) and
|
|
69
|
+
// refuse to provision a crash-looper. Warm-on-demand runtimes need no plist.
|
|
70
|
+
let runtimeBin: string | undefined
|
|
71
|
+
if (isInfraRuntime(opts.runtime)) {
|
|
72
|
+
const want = opts.runtimeBin ?? INFRA_RUNTIME_DEFAULT_BIN[opts.runtime] ?? opts.runtime
|
|
73
|
+
runtimeBin = resolveExecutable(want, env)
|
|
74
|
+
if (!runtimeBin) {
|
|
75
|
+
throw new IapError(
|
|
76
|
+
`cannot provision infra peer "${opts.personality ?? normalizeNameCandidate(basename(cwd))}": ` +
|
|
77
|
+
`runtime launcher "${want}" not found on PATH or not executable. Install ${opts.runtime}-runtime ` +
|
|
78
|
+
`(or pass an absolute --bin) so the always-on plist resolves it under launchd's minimal PATH.`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const peers = readPeersIndex({ env }).peers
|
|
84
|
+
// 1) local profile + scaffold + (infra) always-on plist with the pinned launcher.
|
|
85
|
+
const profile = ensurePeerProfile({
|
|
86
|
+
cwd,
|
|
87
|
+
runtime: opts.runtime,
|
|
88
|
+
env,
|
|
89
|
+
peers,
|
|
90
|
+
personality: opts.personality,
|
|
91
|
+
runtimeBin,
|
|
92
|
+
warn: opts.warn,
|
|
93
|
+
})
|
|
94
|
+
// 2) registry entry — without it the daemon does not see the peer (tool-list,
|
|
95
|
+
// caller resolution, wake-on-miss findPeer all read peers-profiles.json).
|
|
96
|
+
await upsertPeer(
|
|
97
|
+
{
|
|
98
|
+
personality: profile.personality,
|
|
99
|
+
runtime: opts.runtime,
|
|
100
|
+
cwd,
|
|
101
|
+
intelligence: opts.intelligence ?? profile.intelligence,
|
|
102
|
+
description: opts.description,
|
|
103
|
+
},
|
|
104
|
+
{ env, warn: opts.warn },
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
personality: profile.personality,
|
|
109
|
+
runtime: opts.runtime,
|
|
110
|
+
cwd,
|
|
111
|
+
profilePath: peerProfilePath(cwd),
|
|
112
|
+
intelligence: opts.intelligence ?? profile.intelligence,
|
|
113
|
+
plistPath: isInfraRuntime(opts.runtime) ? launchdPlistPath(profile.personality, env) : undefined,
|
|
114
|
+
runtimeBin,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
// CLI — `bun src/provision/index.ts <cwd> <runtime> [--personality p] [--bin path]
|
|
120
|
+
// [--description d] [--intelligence i]`
|
|
121
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function parseFlags(argv: string[]): { positionals: string[]; flags: Record<string, string> } {
|
|
124
|
+
const positionals: string[] = []
|
|
125
|
+
const flags: Record<string, string> = {}
|
|
126
|
+
for (let i = 0; i < argv.length; i++) {
|
|
127
|
+
const a = argv[i]
|
|
128
|
+
if (a.startsWith('--')) {
|
|
129
|
+
flags[a.slice(2)] = argv[++i] ?? ''
|
|
130
|
+
} else {
|
|
131
|
+
positionals.push(a)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { positionals, flags }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (import.meta.main) {
|
|
138
|
+
const { positionals, flags } = parseFlags(process.argv.slice(2))
|
|
139
|
+
const [cwd, runtime] = positionals
|
|
140
|
+
if (!cwd || !runtime) {
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
'usage: provision-peer <cwd> <runtime> [--personality <p>] [--bin <abs-launcher>] ' +
|
|
143
|
+
'[--description <d>] [--intelligence artificial|natural|absent]\n',
|
|
144
|
+
)
|
|
145
|
+
process.exit(2)
|
|
146
|
+
}
|
|
147
|
+
provisionPeer({
|
|
148
|
+
cwd,
|
|
149
|
+
runtime,
|
|
150
|
+
personality: flags.personality,
|
|
151
|
+
description: flags.description,
|
|
152
|
+
intelligence: flags.intelligence as Intelligence | undefined,
|
|
153
|
+
runtimeBin: flags.bin,
|
|
154
|
+
warn: m => process.stderr.write(`warn: ${m}\n`),
|
|
155
|
+
})
|
|
156
|
+
.then(r => {
|
|
157
|
+
process.stdout.write(
|
|
158
|
+
`provisioned peer "${r.personality}" (${r.runtime}, ${r.intelligence})\n` +
|
|
159
|
+
` profile: ${r.profilePath}\n` +
|
|
160
|
+
` registry: peers-profiles.json updated\n` +
|
|
161
|
+
(r.plistPath ? ` plist: ${r.plistPath}\n launcher: ${r.runtimeBin}\n` : '') +
|
|
162
|
+
(r.plistPath ? ` (plist written, NOT loaded — run: launchctl bootstrap gui/$(id -u) ${r.plistPath})\n` : ''),
|
|
163
|
+
)
|
|
164
|
+
process.exit(0)
|
|
165
|
+
})
|
|
166
|
+
.catch(e => {
|
|
167
|
+
process.stderr.write(`provision failed: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
168
|
+
process.exit(1)
|
|
169
|
+
})
|
|
170
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// provisionPeer — one-call peer creation: profile + registry (+ infra plist with a
|
|
2
|
+
// PINNED launcher). All registry/plist writes go under IAPEER_ROOT /
|
|
3
|
+
// IAPEER_LAUNCHAGENTS_DIR temp dirs so the suite never touches the live fleet.
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
6
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
7
|
+
import { tmpdir } from 'os'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
import { provisionPeer } from './index.ts'
|
|
10
|
+
import { peerProfilePath } from '../storage/index.ts'
|
|
11
|
+
import { readPeerProfile } from '../identity/index.ts'
|
|
12
|
+
import { findPeer, readPeersIndex } from '../registry/index.ts'
|
|
13
|
+
import { isFoundationOwnedPlist, launchdPlistPath } from '../launch/index.ts'
|
|
14
|
+
|
|
15
|
+
const roots: string[] = []
|
|
16
|
+
function mkTmp(): string {
|
|
17
|
+
const d = mkdtempSync(join(tmpdir(), 'iapeer-prov-'))
|
|
18
|
+
roots.push(d)
|
|
19
|
+
return d
|
|
20
|
+
}
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function fakeBinDir(name = 'notifier-runtime'): { dir: string; bin: string } {
|
|
26
|
+
const dir = mkTmp()
|
|
27
|
+
const bin = join(dir, name)
|
|
28
|
+
writeFileSync(bin, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
|
|
29
|
+
return { dir, bin }
|
|
30
|
+
}
|
|
31
|
+
function envFor(root: string, path?: string): NodeJS.ProcessEnv {
|
|
32
|
+
return {
|
|
33
|
+
IAPEER_ROOT: join(root, 'iapeer'),
|
|
34
|
+
IAPEER_LAUNCHAGENTS_DIR: join(root, 'LA'),
|
|
35
|
+
HOME: root,
|
|
36
|
+
...(path ? { PATH: path } : {}),
|
|
37
|
+
} as NodeJS.ProcessEnv
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('provisionPeer', () => {
|
|
41
|
+
test('infra (notifier): writes profile + registry + plist with a pinned abs launcher', async () => {
|
|
42
|
+
const root = mkTmp()
|
|
43
|
+
const { dir: bindir, bin } = fakeBinDir()
|
|
44
|
+
const env = envFor(root, bindir)
|
|
45
|
+
const cwd = join(root, 'timer') // basename → personality 'timer'
|
|
46
|
+
|
|
47
|
+
const r = await provisionPeer({ cwd, runtime: 'notifier', env })
|
|
48
|
+
|
|
49
|
+
expect(r.personality).toBe('timer')
|
|
50
|
+
expect(r.intelligence).toBe('absent') // notifier zone default
|
|
51
|
+
expect(r.runtimeBin).toBe(bin)
|
|
52
|
+
// local profile
|
|
53
|
+
expect(existsSync(peerProfilePath(cwd))).toBe(true)
|
|
54
|
+
expect(readPeerProfile(cwd)!.runtime).toBe('notifier')
|
|
55
|
+
// registry entry (daemon reads this for tool-list / findPeer / wake)
|
|
56
|
+
expect(findPeer(readPeersIndex({ env }), 'timer')?.cwd).toBe(cwd)
|
|
57
|
+
// plist pins the launcher to an abs path (launchd minimal PATH safe)
|
|
58
|
+
const plist = launchdPlistPath('timer', env)
|
|
59
|
+
expect(existsSync(plist)).toBe(true)
|
|
60
|
+
expect(isFoundationOwnedPlist(plist)).toBe(true)
|
|
61
|
+
const xml = readFileSync(plist, 'utf8')
|
|
62
|
+
expect(xml).toContain('<key>NOTIFIER_RUNTIME_BIN</key>')
|
|
63
|
+
expect(xml).toContain(`<string>${bin}</string>`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('infra with unresolvable launcher → REFUSED (no profile, no registry, no plist)', async () => {
|
|
67
|
+
const root = mkTmp()
|
|
68
|
+
const env = envFor(root, join(root, 'empty')) // PATH dir without notifier-runtime
|
|
69
|
+
const cwd = join(root, 'timer')
|
|
70
|
+
|
|
71
|
+
await expect(provisionPeer({ cwd, runtime: 'notifier', env })).rejects.toThrow(/not found on PATH|launcher/i)
|
|
72
|
+
|
|
73
|
+
expect(existsSync(peerProfilePath(cwd))).toBe(false)
|
|
74
|
+
expect(findPeer(readPeersIndex({ env }), 'timer')).toBeNull()
|
|
75
|
+
expect(existsSync(launchdPlistPath('timer', env))).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('warm-on-demand (claude): profile + registry, NO plist (no launcher needed)', async () => {
|
|
79
|
+
const root = mkTmp()
|
|
80
|
+
const env = envFor(root)
|
|
81
|
+
const cwd = join(root, 'worker')
|
|
82
|
+
|
|
83
|
+
const r = await provisionPeer({ cwd, runtime: 'claude', env })
|
|
84
|
+
|
|
85
|
+
expect(r.plistPath).toBeUndefined()
|
|
86
|
+
expect(r.runtimeBin).toBeUndefined()
|
|
87
|
+
expect(existsSync(peerProfilePath(cwd))).toBe(true)
|
|
88
|
+
expect(findPeer(readPeersIndex({ env }), 'worker')?.runtime).toBe('claude')
|
|
89
|
+
expect(existsSync(launchdPlistPath('worker', env))).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('explicit personality override (not basename) provisions under that name', async () => {
|
|
93
|
+
const root = mkTmp()
|
|
94
|
+
const { dir: bindir } = fakeBinDir()
|
|
95
|
+
const env = envFor(root, bindir)
|
|
96
|
+
const cwd = join(root, 'some-dir')
|
|
97
|
+
|
|
98
|
+
const r = await provisionPeer({ cwd, runtime: 'notifier', personality: 'timer', env })
|
|
99
|
+
|
|
100
|
+
expect(r.personality).toBe('timer')
|
|
101
|
+
expect(existsSync(launchdPlistPath('timer', env))).toBe(true)
|
|
102
|
+
expect(findPeer(readPeersIndex({ env }), 'timer')?.cwd).toBe(cwd)
|
|
103
|
+
})
|
|
104
|
+
})
|