@agfpd/iapeer 0.2.29 → 0.2.31
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 +1 -1
- package/src/cli/cli.test.ts +23 -0
- package/src/cli/index.ts +34 -1
- package/src/launch/nativeMemory.test.ts +43 -0
- package/src/launch/nativeMemory.ts +53 -1
package/package.json
CHANGED
package/src/cli/cli.test.ts
CHANGED
|
@@ -195,6 +195,29 @@ describe('remove (registry record via the locked writer)', () => {
|
|
|
195
195
|
expect(hasEphemeralArmed(loadLifecycleConfig(e2), 'claude-silentworker')).toBe(true)
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
+
test('self-done ephemeral check keys on the CANONICAL registry cwd, not process.cwd() (live false-warning, scriber 11.06)', async () => {
|
|
199
|
+
// an ephemeral peer registered with a cwd carrying wake_policy:ephemeral
|
|
200
|
+
const cwd = join(root, 'worker')
|
|
201
|
+
mkdirSync(join(cwd, '.iapeer'), { recursive: true })
|
|
202
|
+
writeFileSync(join(cwd, '.iapeer', 'peer-profile.json'), JSON.stringify({ personality: 'worker', runtime: 'claude', wake_policy: 'ephemeral' }))
|
|
203
|
+
await upsertPeer({ personality: 'worker', runtime: 'claude', cwd, intelligence: 'artificial' }, { rootDir: root })
|
|
204
|
+
const e = { ...env(), PEER_IDENTITY: 'claude-worker' }
|
|
205
|
+
let captured = ''
|
|
206
|
+
const origWrite = process.stdout.write
|
|
207
|
+
process.stdout.write = ((s: string | Uint8Array) => {
|
|
208
|
+
captured += typeof s === 'string' ? s : Buffer.from(s).toString('utf8')
|
|
209
|
+
return true
|
|
210
|
+
}) as typeof process.stdout.write
|
|
211
|
+
try {
|
|
212
|
+
// invoked from THIS test process's cwd (≠ peer cwd) — the warning must NOT fire
|
|
213
|
+
expect(await runCli(['self-done'], e)).toBe(0)
|
|
214
|
+
expect(captured).toContain('armed claude-worker')
|
|
215
|
+
expect(captured).not.toContain('marker is inert')
|
|
216
|
+
} finally {
|
|
217
|
+
process.stdout.write = origWrite
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
198
221
|
test('purges identity-keyed lifecycle state with the record — a namesake newborn must not inherit a dead peer\'s parking (boris 10.06)', async () => {
|
|
199
222
|
await register('reborn')
|
|
200
223
|
const e = env()
|
package/src/cli/index.ts
CHANGED
|
@@ -280,6 +280,10 @@ export interface RemoveOutcome {
|
|
|
280
280
|
/** v1.2: per-runtime unprovision outcomes (`<rt>:<state>`), present when the
|
|
281
281
|
* slot declares an unprovision command (occasion=remove ran before the purge). */
|
|
282
282
|
unprovision?: string[]
|
|
283
|
+
/** Codex pre-trust cleanup outcome: 'removed' when the peer's cwd trust entry
|
|
284
|
+
* was dropped from the host codex config; a failure detail otherwise. Absent
|
|
285
|
+
* for non-codex peers and when no entry existed. */
|
|
286
|
+
codexTrust?: string
|
|
283
287
|
/** Identity-keyed lifecycle artifacts purged with the record (state/lifecycle/
|
|
284
288
|
* `<identity>.*` per runtime). Without this purge a NEWBORN peer reusing the
|
|
285
289
|
* personality inherits the dead namesake's parking (live defect, boris 10.06:
|
|
@@ -355,6 +359,21 @@ export async function removePeerCli(
|
|
|
355
359
|
} catch (e) {
|
|
356
360
|
unprovisionOutcomes.push(`failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
357
361
|
}
|
|
362
|
+
// Codex pre-trust cleanup (reap-side counterpart of the birth-time
|
|
363
|
+
// preTrustCodexCwd; backlog made live by D4 11.06): a removed codex peer must
|
|
364
|
+
// not leave its cwd trusted in the host ~/.codex/config.toml forever. After
|
|
365
|
+
// unprovision (the provider sees the peer's last consistent state first),
|
|
366
|
+
// best-effort like everything else on this path.
|
|
367
|
+
let trustCleaned: string | undefined
|
|
368
|
+
if (peer.runtimes.includes('codex')) {
|
|
369
|
+
try {
|
|
370
|
+
const { removeCodexCwdTrust } = await import('../launch/nativeMemory.ts')
|
|
371
|
+
const t = removeCodexCwdTrust(peer.cwd, env)
|
|
372
|
+
trustCleaned = t.state === 'written' ? 'removed' : t.state === 'already' ? undefined : `${t.state}${t.detail ? ` (${t.detail})` : ''}`
|
|
373
|
+
} catch (e) {
|
|
374
|
+
trustCleaned = `failed (${e instanceof Error ? e.message : String(e)})`
|
|
375
|
+
}
|
|
376
|
+
}
|
|
358
377
|
// Purge identity-keyed lifecycle state WITH the record (per runtime): stale
|
|
359
378
|
// .stopped/.idle-reaped/... must never outlive the peer and ambush a future
|
|
360
379
|
// namesake (purgeIdentityState doc). After the registry write, so a failed
|
|
@@ -367,6 +386,7 @@ export async function removePeerCli(
|
|
|
367
386
|
cwd: peer.cwd,
|
|
368
387
|
purgedState,
|
|
369
388
|
...(unprovisionOutcomes.length ? { unprovision: unprovisionOutcomes } : {}),
|
|
389
|
+
...(trustCleaned ? { codexTrust: trustCleaned } : {}),
|
|
370
390
|
}
|
|
371
391
|
}
|
|
372
392
|
|
|
@@ -826,6 +846,10 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
826
846
|
if (o.unprovision?.length) {
|
|
827
847
|
out(`memory unprovision: ${o.unprovision.join(', ')}\n`)
|
|
828
848
|
}
|
|
849
|
+
// Codex pre-trust cleanup — the cwd's trust entry must die with the peer.
|
|
850
|
+
if (o.codexTrust) {
|
|
851
|
+
out(`codex trust entry: ${o.codexTrust}\n`)
|
|
852
|
+
}
|
|
829
853
|
// Stale identity-keyed markers must die with the record (boris 10.06: a
|
|
830
854
|
// namesake newborn inherited a dead peer's .stopped → refused to wake).
|
|
831
855
|
if (o.purgedState?.length) {
|
|
@@ -1034,7 +1058,16 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1034
1058
|
}
|
|
1035
1059
|
const cfg = loadLifecycleConfig(env)
|
|
1036
1060
|
setEphemeralArmed(cfg, identity)
|
|
1037
|
-
|
|
1061
|
+
// The ephemeral check keys on the peer's CANONICAL cwd (registry), NOT on
|
|
1062
|
+
// process.cwd(): the verb is invoked from wherever the agent's shell
|
|
1063
|
+
// happens to be (scriber lives in the vault half the time), and a foreign
|
|
1064
|
+
// cwd made this warning LIE — «marker is inert» on a genuinely ephemeral
|
|
1065
|
+
// peer (live case scriber 11.06: the false warning sent doc/scriber down a
|
|
1066
|
+
// wrong root-cause chase). The marker itself was never affected — it is
|
|
1067
|
+
// identity-keyed and supervise checks the SESSION's canonical cwd.
|
|
1068
|
+
const addr = parseSessionName(identity)!
|
|
1069
|
+
const canonicalCwd = findPeer(readPeersIndex({ env }), addr.personality)?.cwd ?? process.cwd()
|
|
1070
|
+
const ephemeral = isEphemeralPeer(canonicalCwd)
|
|
1038
1071
|
out(
|
|
1039
1072
|
`self-done: armed ${identity} for the quiet-window reap (no one woken)` +
|
|
1040
1073
|
(ephemeral ? '' : ' — NOTE: this peer is not wake_policy:ephemeral, the marker is inert') +
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
codexProjectConfigPath,
|
|
14
14
|
codexGlobalConfigPath,
|
|
15
15
|
preTrustCodexCwd,
|
|
16
|
+
removeCodexCwdTrust,
|
|
16
17
|
} from './nativeMemory.ts'
|
|
17
18
|
|
|
18
19
|
const dirs: string[] = []
|
|
@@ -127,6 +128,13 @@ describe('applyNativeMemory runtime dispatch', () => {
|
|
|
127
128
|
})
|
|
128
129
|
|
|
129
130
|
describe('preTrustCodexCwd (birth-time trust, codex global config)', () => {
|
|
131
|
+
test('codexGlobalConfigPath honors $CODEX_HOME first (same override set as init codexConfigPath — sandbox seam, live-caught by D4 11.06)', () => {
|
|
132
|
+
expect(codexGlobalConfigPath({ CODEX_HOME: '/sandbox/codex', HOME: '/real/home' } as NodeJS.ProcessEnv)).toBe(
|
|
133
|
+
'/sandbox/codex/config.toml',
|
|
134
|
+
)
|
|
135
|
+
expect(codexGlobalConfigPath({ HOME: '/real/home' } as NodeJS.ProcessEnv)).toBe('/real/home/.codex/config.toml')
|
|
136
|
+
})
|
|
137
|
+
|
|
130
138
|
test('appends a [projects] block once; NO-CLOBBER of existing content; idempotent', () => {
|
|
131
139
|
const home = mkTmp()
|
|
132
140
|
const env = { HOME: home } as NodeJS.ProcessEnv
|
|
@@ -141,6 +149,41 @@ describe('preTrustCodexCwd (birth-time trust, codex global config)', () => {
|
|
|
141
149
|
expect(preTrustCodexCwd('/peers/newborn', env).state).toBe('already')
|
|
142
150
|
})
|
|
143
151
|
|
|
152
|
+
test('removeCodexCwdTrust: drops EXACTLY the peer cwd section (header+body), keeps neighbors; idempotent; handles deleted cwd via literal match', () => {
|
|
153
|
+
const home = mkTmp()
|
|
154
|
+
const env = { HOME: home } as NodeJS.ProcessEnv
|
|
155
|
+
mkdirSync(join(home, '.codex'), { recursive: true })
|
|
156
|
+
const cfg =
|
|
157
|
+
'[mcp_servers.iapeer]\nurl = "http://x"\n\n' +
|
|
158
|
+
'[projects."/keep/me"]\ntrust_level = "trusted"\n\n' +
|
|
159
|
+
'[projects."/peers/doomed"]\ntrust_level = "trusted"\n\n' +
|
|
160
|
+
'[features]\nmemories = false\n'
|
|
161
|
+
writeFileSync(codexGlobalConfigPath(env), cfg)
|
|
162
|
+
// cwd "/peers/doomed" does not exist on disk → realpath fails → literal match
|
|
163
|
+
const first = removeCodexCwdTrust('/peers/doomed', env)
|
|
164
|
+
expect(first.state).toBe('written')
|
|
165
|
+
const text = readFileSync(codexGlobalConfigPath(env), 'utf8')
|
|
166
|
+
expect(text).not.toContain('/peers/doomed')
|
|
167
|
+
expect(text).toContain('[projects."/keep/me"]\ntrust_level = "trusted"') // neighbor intact
|
|
168
|
+
expect(text).toContain('[mcp_servers.iapeer]') // foreign sections intact
|
|
169
|
+
expect(text).toContain('[features]\nmemories = false')
|
|
170
|
+
expect(removeCodexCwdTrust('/peers/doomed', env).state).toBe('already') // idempotent
|
|
171
|
+
expect(removeCodexCwdTrust('/never/was', env).state).toBe('already') // absent entry → no-op
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('removeCodexCwdTrust matches the RESOLVED path of a live symlinked cwd (writer stores realpath)', () => {
|
|
175
|
+
const home = mkTmp()
|
|
176
|
+
const env = { HOME: home } as NodeJS.ProcessEnv
|
|
177
|
+
mkdirSync(join(home, '.codex'), { recursive: true })
|
|
178
|
+
const cwd = mkTmp() // /var/folders/... on macOS — realpath differs from literal /tmp form
|
|
179
|
+
// write the entry the way preTrustCodexCwd does (by realpath)
|
|
180
|
+
preTrustCodexCwd(cwd, env)
|
|
181
|
+
expect(readFileSync(codexGlobalConfigPath(env), 'utf8')).toContain('[projects."')
|
|
182
|
+
// remove by the LITERAL cwd — must hit the resolved-form entry
|
|
183
|
+
expect(removeCodexCwdTrust(cwd, env).state).toBe('written')
|
|
184
|
+
expect(readFileSync(codexGlobalConfigPath(env), 'utf8')).not.toContain('trust_level')
|
|
185
|
+
})
|
|
186
|
+
|
|
144
187
|
test('absent global config → created with just the block', () => {
|
|
145
188
|
const home = mkTmp()
|
|
146
189
|
const env = { HOME: home } as NodeJS.ProcessEnv
|
|
@@ -127,8 +127,14 @@ function applyCodexLever(cwd: string, state: NativeMemoryState): LeverOutcome {
|
|
|
127
127
|
// ─── codex trust (birth-time only) ───────────────────────────────────────────
|
|
128
128
|
|
|
129
129
|
/** The codex GLOBAL config (`~/.codex/config.toml`) — where trust lives as
|
|
130
|
-
* `[projects."<path>"] trust_level = "trusted"` blocks.
|
|
130
|
+
* `[projects."<path>"] trust_level = "trusted"` blocks. Honors $CODEX_HOME
|
|
131
|
+
* first, SAME as init's codexConfigPath — the two writers of this shared prod
|
|
132
|
+
* file must respect one override set, or a sandbox isolating one path still
|
|
133
|
+
* leaks through the other (live-caught by iapeer-memory's D4 11.06: an
|
|
134
|
+
* IAPEER_ROOT sandbox pre-trusted a throwaway cwd into the PROD config). */
|
|
131
135
|
export function codexGlobalConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
136
|
+
const codexHome = env.CODEX_HOME?.trim()
|
|
137
|
+
if (codexHome) return join(codexHome, 'config.toml')
|
|
132
138
|
const home = env.HOME?.trim() || homedir()
|
|
133
139
|
return join(home, '.codex', 'config.toml')
|
|
134
140
|
}
|
|
@@ -166,6 +172,52 @@ export function preTrustCodexCwd(cwd: string, env: NodeJS.ProcessEnv = process.e
|
|
|
166
172
|
}
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Remove a peer's pre-trust entry from the codex GLOBAL config — the reap-side
|
|
177
|
+
* counterpart of preTrustCodexCwd (backlog «remove не чистит pre-trust», made
|
|
178
|
+
* live by iapeer-memory's D4 11.06: every sandboxed/throwaway codex birth left
|
|
179
|
+
* a stale `[projects."<cwd>"]` block in the PROD config forever). Removes the
|
|
180
|
+
* section for BOTH the resolved and the literal path form (the writer stores
|
|
181
|
+
* realpath, but a since-deleted cwd cannot be resolved anymore — match either),
|
|
182
|
+
* strictly section-scoped: from the `[projects."<p>"]` header up to the next
|
|
183
|
+
* `[` header, other sections untouched. Idempotent: no entry → 'already'.
|
|
184
|
+
*/
|
|
185
|
+
export function removeCodexCwdTrust(cwd: string, env: NodeJS.ProcessEnv = process.env): LeverOutcome {
|
|
186
|
+
const path = codexGlobalConfigPath(env)
|
|
187
|
+
try {
|
|
188
|
+
if (!existsSync(path)) return { runtime: 'codex', path, state: 'already' }
|
|
189
|
+
let real = cwd
|
|
190
|
+
try {
|
|
191
|
+
real = realpathSync(cwd)
|
|
192
|
+
} catch {
|
|
193
|
+
/* cwd already deleted → only the literal form can match */
|
|
194
|
+
}
|
|
195
|
+
const candidates = new Set([real, cwd])
|
|
196
|
+
const lines = readFileSync(path, 'utf8').split('\n')
|
|
197
|
+
const kept: string[] = []
|
|
198
|
+
let inDoomed = false
|
|
199
|
+
let removed = false
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
const header = line.match(/^\s*\[projects\."(.+)"\]\s*$/)
|
|
202
|
+
if (header) {
|
|
203
|
+
inDoomed = candidates.has(header[1]!)
|
|
204
|
+
if (inDoomed) {
|
|
205
|
+
removed = true
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
} else if (inDoomed && /^\s*\[/.test(line)) {
|
|
209
|
+
inDoomed = false // next section starts — stop dropping
|
|
210
|
+
}
|
|
211
|
+
if (!inDoomed) kept.push(line)
|
|
212
|
+
}
|
|
213
|
+
if (!removed) return { runtime: 'codex', path, state: 'already' }
|
|
214
|
+
writeFileAtomic(path, kept.join('\n').replace(/\n{3,}/g, '\n\n'))
|
|
215
|
+
return { runtime: 'codex', path, state: 'written' }
|
|
216
|
+
} catch (e) {
|
|
217
|
+
return { runtime: 'codex', path, state: 'failed', detail: e instanceof Error ? e.message : String(e) }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
169
221
|
// ─── public entry ────────────────────────────────────────────────────────────
|
|
170
222
|
|
|
171
223
|
/**
|