@agfpd/iapeer 0.4.88 → 0.4.89

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.4.88",
3
+ "version": "0.4.89",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -213,6 +213,41 @@ export function discoverPeerRuntimes(cwd: string, currentRuntime: Runtime): Runt
213
213
  return uniqueRuntimes(discovered)
214
214
  }
215
215
 
216
+ /** Agentic runtimes only an ARTIFICIAL peer can drive. A natural (human) or absent
217
+ * (no mind) peer has nobody to sit in a claude/codex session under its identity. */
218
+ const AGENTIC_GATED_RUNTIMES: ReadonlySet<string> = new Set(['claude', 'codex'])
219
+
220
+ /**
221
+ * DISCOVERY GATE (хвост-2, boris 15.07): drop agentic runtimes a NON-artificial peer
222
+ * would otherwise pick up from folder artifacts — a stray `.claude`/`.codex` dir or a
223
+ * leftover `.iapeer/<rt>` runtime scope in the cwd is NOT a declaration (live leak: a
224
+ * long-forgotten .claude/ in the human peer arthur's cwd made every create-прогон
225
+ * attach `claude` to his profile). The gate is by INTELLIGENCE, so it is robust to the
226
+ * artifacts' PRESENCE — the folder stays, the runtime is simply not attached. It also
227
+ * HEALS an already-leaked profile: applied to the merged set on re-provision, it
228
+ * scrubs previously-attached agentic runtimes off a natural/absent peer (with a warn).
229
+ * The EXPLICIT `options.runtime` always passes — an operator's deliberate command is
230
+ * not discovery (and explicit nature/runtime mismatches fail loud elsewhere:
231
+ * isIntelligenceAllowedForRuntime).
232
+ */
233
+ export function gateRuntimesForIntelligence(
234
+ runtimes: Runtime[],
235
+ intelligence: Intelligence,
236
+ explicitRuntime: Runtime,
237
+ warn?: (message: string) => void,
238
+ ): Runtime[] {
239
+ if (intelligence === 'artificial') return runtimes
240
+ const isGated = (rt: Runtime): boolean => AGENTIC_GATED_RUNTIMES.has(rt) && rt !== explicitRuntime
241
+ const dropped = runtimes.filter(isGated)
242
+ if (dropped.length === 0) return runtimes
243
+ warn?.(
244
+ `discovery gate: dropped agentic runtime(s) [${dropped.join(', ')}] for "${intelligence}" peer — ` +
245
+ `a .claude/.codex folder or leftover runtime scope in the cwd is not a declaration; ` +
246
+ `agentic runtimes attach only to artificial peers`,
247
+ )
248
+ return runtimes.filter(rt => !isGated(rt))
249
+ }
250
+
216
251
  export function readPeerProfile(cwd: string = process.cwd()): PeerProfile | null {
217
252
  const path = peerProfilePath(cwd)
218
253
  if (!existsSync(path)) return null
@@ -566,14 +601,17 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
566
601
  `PEER_PERSONALITY "${options.env.PEER_PERSONALITY}" does not match the peer being initialized ("${personality}")`,
567
602
  )
568
603
  }
604
+ // Intelligence FIRST — the discovery gate below keys on it (a natural/absent
605
+ // peer must not be born with agentic runtimes picked up from folder artifacts).
606
+ const intelligence = resolvedIntelligence(options.runtime)
569
607
  const profile: PeerProfile = {
570
608
  personality,
571
609
  runtime: options.runtime,
572
- runtimes: discoveredRuntimes,
610
+ runtimes: gateRuntimesForIntelligence(discoveredRuntimes, intelligence, options.runtime, options.warn),
573
611
  // В36 — the description lands in the LOCAL profile at birth (source of truth);
574
612
  // the registry row is a projection of this file, not the other way around.
575
613
  description: options.description?.trim() ?? '',
576
- intelligence: resolvedIntelligence(options.runtime),
614
+ intelligence,
577
615
  }
578
616
  ensureLocalRuntimeScopes(cwd, profile.runtimes)
579
617
  // INFRA runtime → provision the always-on launchd plist that holds it live.
@@ -609,8 +647,6 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
609
647
  `PEER_PERSONALITY must match ${IAPEER_DIR}/${PEER_PROFILE_FILE} personality "${existing.personality}", got "${options.env.PEER_PERSONALITY}"`,
610
648
  )
611
649
  }
612
- const mergedRuntimes = uniqueRuntimes([...existing.runtimes, ...discoveredRuntimes])
613
- ensureLocalRuntimeScopes(cwd, mergedRuntimes)
614
650
  // RE-PROVISION parity with the new-profile branch: an EXISTING infra peer (a
615
651
  // migration / re-provision — e.g. `iapeer create alice --runtime telegram` to move
616
652
  // a live telegram human onto the foundation) must ALSO get its always-on plist
@@ -641,11 +677,25 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
641
677
  // preserved). resolvedIntelligence validates the explicit against the runtime's allowed set.
642
678
  intelligence = resolvedIntelligence(options.runtime)
643
679
  }
680
+ // Merge AFTER the intelligence is final, and GATE the WHOLE merged set (not just the
681
+ // fresh discoveries): for a natural/absent peer this both blocks a new agentic pickup
682
+ // AND heals a previously-leaked one (the arthur claude-leak class) on the next
683
+ // re-provision — with the folder artifacts still in place.
684
+ const mergedRuntimes = gateRuntimesForIntelligence(
685
+ uniqueRuntimes([...existing.runtimes, ...discoveredRuntimes]),
686
+ intelligence,
687
+ options.runtime,
688
+ options.warn,
689
+ )
690
+ ensureLocalRuntimeScopes(cwd, mergedRuntimes)
644
691
  // В36 — an explicit description on RE-provision updates the local profile too (H1 in
645
692
  // writePeerProfileAtomic still guards: absent/empty never wipes an existing value).
646
693
  const explicitDescription = options.description?.trim()
647
694
  const descriptionChanged = Boolean(explicitDescription) && explicitDescription !== existing.description
648
- if (mergedRuntimes.length !== existing.runtimes.length || intelligence !== existing.intelligence || descriptionChanged) {
695
+ // CONTENT comparison, not length: the gate can DROP a leaked runtime while discovery
696
+ // ADDS a new one in the same pass (arthur: [telegram, claude] + web → [telegram, web])
697
+ // — equal lengths, different sets; a length check would silently skip the heal-write.
698
+ if (mergedRuntimes.join(',') !== existing.runtimes.join(',') || intelligence !== existing.intelligence || descriptionChanged) {
649
699
  const updated = {
650
700
  ...existing,
651
701
  runtimes: mergedRuntimes,