@agfpd/iapeer 0.2.29 → 0.2.30

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.29",
3
+ "version": "0.2.30",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
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) {
@@ -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
  /**