@agfpd/iapeer 0.4.41 → 0.4.42
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/identity/profileStandard.ts +11 -1
- package/src/registry/index.ts +43 -1
package/package.json
CHANGED
|
@@ -201,7 +201,17 @@ export function migrateProfileRuntimeField(cwd: string): boolean {
|
|
|
201
201
|
* profile. Used by verify (compare against the live index) and reindex (rewrite it).
|
|
202
202
|
*/
|
|
203
203
|
export function projectProfileToRecord(cwd: string): PeerRecord | null {
|
|
204
|
-
|
|
204
|
+
// В32 — a CORRUPT local profile (invalid JSON / invalid stored field) makes readPeerProfile THROW.
|
|
205
|
+
// Unguarded, one bad profile aborts the whole reconcile/reindex map → verify crashes for all ~30 peers
|
|
206
|
+
// (with a message that does not even name the peer), and verify --fix never reaches reindex. Treat a
|
|
207
|
+
// corrupt profile like a missing one: reconcileIndex reports null drift, and reindexFromLocals PRESERVES
|
|
208
|
+
// the existing registry record (its `if (!projected) return rec`), so nothing is crashed or removed.
|
|
209
|
+
let profile
|
|
210
|
+
try {
|
|
211
|
+
profile = readPeerProfile(cwd)
|
|
212
|
+
} catch {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
205
215
|
if (!profile) return null
|
|
206
216
|
return {
|
|
207
217
|
personality: profile.personality,
|
package/src/registry/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// is no unlocked path to the registry file anywhere in the package.
|
|
10
10
|
|
|
11
11
|
import * as lockfile from 'proper-lockfile'
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'fs'
|
|
13
13
|
import { homedir } from 'os'
|
|
14
14
|
import { join } from 'path'
|
|
15
15
|
import { writeFileAtomicRaw } from '../storage/atomicWrite.ts'
|
|
@@ -345,6 +345,24 @@ function writePeersIndexAtomic(paths: PeersPaths, index: PeersIndex): void {
|
|
|
345
345
|
writeFileAtomicRaw(paths.peersFile, `${JSON.stringify(normalized, null, 2)}\n`, 0o600, paths.tmpDir)
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
/** В33 — assert the READ invariants BEFORE publishing. writePeersIndexAtomic does no validation, so a
|
|
349
|
+
* duplicate personality (e.g. a partial renamePeer + create, or reindex projecting two same-named local
|
|
350
|
+
* profiles) or an empty cwd would write SUCCESSFULLY and then make EVERY readPeersIndex throw — the whole
|
|
351
|
+
* fleet stops resolving until the file is hand-repaired. Fail LOUD here (the single-writer chokepoint) so
|
|
352
|
+
* the on-disk file is never left unreadable. */
|
|
353
|
+
function assertWritableIndex(index: PeersIndex): void {
|
|
354
|
+
const seen = new Set<string>()
|
|
355
|
+
for (const p of index.peers) {
|
|
356
|
+
if (!p.cwd || !p.cwd.trim()) {
|
|
357
|
+
throw new IapError(`refusing to write peers-profiles.json: peer "${p.personality}" has an empty cwd (would be unreadable)`)
|
|
358
|
+
}
|
|
359
|
+
if (seen.has(p.personality)) {
|
|
360
|
+
throw new IapError(`refusing to write peers-profiles.json: duplicate peer "${p.personality}" (would make the registry unreadable for the whole fleet)`)
|
|
361
|
+
}
|
|
362
|
+
seen.add(p.personality)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
348
366
|
export async function updatePeersIndex(
|
|
349
367
|
updater: (index: PeersIndex) => PeersIndex,
|
|
350
368
|
options: PeersUpdateOptions = {},
|
|
@@ -352,6 +370,7 @@ export async function updatePeersIndex(
|
|
|
352
370
|
return withPeersLock(options, paths => {
|
|
353
371
|
const current = readPeersIndex({ ...options, rootDir: paths.rootDir })
|
|
354
372
|
const next = updater(current)
|
|
373
|
+
assertWritableIndex(next) // В33 — fail before publish; the file stays readable
|
|
355
374
|
writePeersIndexAtomic(paths, next)
|
|
356
375
|
return next
|
|
357
376
|
})
|
|
@@ -371,6 +390,17 @@ export interface UpsertPeerArgs {
|
|
|
371
390
|
cwd: string
|
|
372
391
|
}
|
|
373
392
|
|
|
393
|
+
/** Two cwds refer to the SAME folder (realpath-tolerant so a symlinked path compares equal to its
|
|
394
|
+
* target). Falls back to a literal compare when a path can't be resolved (e.g. not yet created). */
|
|
395
|
+
function sameCwd(a: string, b: string): boolean {
|
|
396
|
+
if (a === b) return true
|
|
397
|
+
try {
|
|
398
|
+
return realpathSync(a) === realpathSync(b)
|
|
399
|
+
} catch {
|
|
400
|
+
return a === b
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
374
404
|
export async function upsertPeer(
|
|
375
405
|
args: UpsertPeerArgs,
|
|
376
406
|
options: PeersUpdateOptions = {},
|
|
@@ -411,6 +441,18 @@ export async function upsertPeer(
|
|
|
411
441
|
return updatePeersIndex(index => {
|
|
412
442
|
const existing = index.peers.find(peer => peer.personality === args.personality)
|
|
413
443
|
|
|
444
|
+
// В35 — re-check the personality↔cwd collision UNDER THE LOCK (closing the TOCTOU: the pre-lock
|
|
445
|
+
// snapshot check in provisionPeer can be stale by the seconds-long birth-hook window). An existing
|
|
446
|
+
// peer at a DIFFERENT folder must NOT be silently re-pointed — that splits one identity across two
|
|
447
|
+
// folders (routing goes to whichever the entry point resolves). Legitimate folder moves are
|
|
448
|
+
// fail-closed in ensurePeerProfile; renames go through renamePeer — neither re-points via upsert.
|
|
449
|
+
if (existing && !sameCwd(existing.cwd, args.cwd)) {
|
|
450
|
+
throw new IapError(
|
|
451
|
+
`peer "${args.personality}" already exists at ${existing.cwd}; refusing to re-point it to ${args.cwd} ` +
|
|
452
|
+
`(one personality = one folder — use 'iapeer rename' to move it)`,
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
414
456
|
// H1 merge-with-existing: a field ABSENT from args inherits from `existing`,
|
|
415
457
|
// NOT from a runtime default. The runtime default applies ONLY to a brand-new
|
|
416
458
|
// peer (no existing). This is what stops a claude-boot upsert (no intelligence)
|