@lythos/cold-pool 0.14.2 → 0.14.3

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": "@lythos/cold-pool",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "Cold pool service layer — dedicated resource holder for skill repositories with intent/plan/execute primitives. Single owner of git side-effects; consumed by deck/curator/arena.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "homepage": "https://github.com/lythos-labs/lythoskill/tree/main/packages/lythoskill-cold-pool#readme",
44
44
  "dependencies": {
45
- "@lythos/infra": "^0.14.2",
45
+ "@lythos/infra": "^0.14.3",
46
46
  "simple-git": "^3.36.0"
47
47
  },
48
48
  "engines": {
package/src/cli.ts CHANGED
@@ -185,9 +185,19 @@ async function main(): Promise<void> {
185
185
  }
186
186
  }
187
187
 
188
+ // Metadata integrity check
189
+ const integrity = pool.metadata.validateIntegrity()
190
+
188
191
  pool.metadata.close()
189
192
 
190
193
  // Report
194
+ if (!integrity.ok) {
195
+ if (integrity.stored === null) {
196
+ console.log(`ℹ️ Metadata fingerprint not yet stored — first reconcile will create it.`)
197
+ } else {
198
+ console.log(`⚠️ Metadata integrity: ${integrity.message}`)
199
+ }
200
+ }
191
201
  console.log(`\n📊 Validate Report`)
192
202
  console.log(` Cold pool: ${coldPoolPath}`)
193
203
  console.log(` Skills declared: ${lock.skills.length}`)
@@ -155,4 +155,60 @@ describe('MetadataDB', () => {
155
155
  db2.close()
156
156
  })
157
157
  })
158
+
159
+ describe('integrity fingerprint', () => {
160
+ it('creates fingerprint after reconcileDeckReferences', () => {
161
+ db.reconcileDeckReferences('/tmp/test-deck', [
162
+ { locator: 'github.com/a/b/skill-x', alias: 'x' },
163
+ ])
164
+ const result = db.validateIntegrity()
165
+ expect(result.ok).toBe(true)
166
+ expect(result.stored).not.toBeNull()
167
+ expect(result.computed).toBe(result.stored!)
168
+ })
169
+
170
+ it('detects data tampering (corrupted active locators)', () => {
171
+ db.reconcileDeckReferences('/tmp/test-deck', [
172
+ { locator: 'github.com/a/b/skill-x', alias: 'x' },
173
+ ])
174
+ expect(db.validateIntegrity().ok).toBe(true)
175
+
176
+ // Tamper: remove a locator directly via SQL
177
+ db['db'].exec(`DELETE FROM deck_refs`)
178
+ const result = db.validateIntegrity()
179
+ expect(result.ok).toBe(false)
180
+ expect(result.message).toContain('mismatch')
181
+ })
182
+
183
+ it('detects swapped DB (different schema version)', () => {
184
+ db.reconcileDeckReferences('/tmp/test-deck', [
185
+ { locator: 'github.com/a/b/skill-x', alias: 'x' },
186
+ ])
187
+ expect(db.validateIntegrity().ok).toBe(true)
188
+
189
+ // Tamper: bump schema version in fingerprint record without migration
190
+ db['db'].exec(`UPDATE _meta_fingerprint SET schema_version = 99`)
191
+ const result = db.validateIntegrity()
192
+ expect(result.ok).toBe(false)
193
+ expect(result.message).toContain('Schema version')
194
+ })
195
+
196
+ it('returns ok when fingerprint is consistent', () => {
197
+ db.reconcileDeckReferences('/tmp/deck-1', [
198
+ { locator: 'github.com/x/y/skill-a', alias: 'a' },
199
+ { locator: 'github.com/x/y/skill-b', alias: 'b' },
200
+ ])
201
+ const r1 = db.validateIntegrity()
202
+ expect(r1.ok).toBe(true)
203
+
204
+ // Second reconcile with same data — fingerprint recomputed, still ok
205
+ db.reconcileDeckReferences('/tmp/deck-1', [
206
+ { locator: 'github.com/x/y/skill-a', alias: 'a' },
207
+ { locator: 'github.com/x/y/skill-b', alias: 'b' },
208
+ ])
209
+ const r2 = db.validateIntegrity()
210
+ expect(r2.ok).toBe(true)
211
+ expect(r2.stored).toBe(r1.computed) // stored should now be latest
212
+ })
213
+ })
158
214
  })
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { SqliteDb } from '@lythos/infra'
24
+ import { createHash } from 'node:crypto'
24
25
 
25
26
  export type DeckRefState = 'added' | 'linked' | 'removed'
26
27
 
@@ -52,7 +53,7 @@ export interface DeckReference {
52
53
  removedAt: string | null
53
54
  }
54
55
 
55
- const CURRENT_SCHEMA = 6
56
+ const CURRENT_SCHEMA = 7
56
57
 
57
58
  export class MetadataDB extends SqliteDb {
58
59
  constructor(dbPath: string) {
@@ -103,6 +104,16 @@ export class MetadataDB extends SqliteDb {
103
104
  this.exec(`CREATE INDEX IF NOT EXISTS idx_deck_refs_state ON deck_refs(state)`)
104
105
  this.exec(`CREATE INDEX IF NOT EXISTS idx_skills_repo ON skills(host, owner, repo)`)
105
106
 
107
+ this.exec(`
108
+ CREATE TABLE IF NOT EXISTS _meta_fingerprint (
109
+ id INTEGER PRIMARY KEY CHECK (id = 1),
110
+ fingerprint TEXT NOT NULL,
111
+ computed_at TEXT NOT NULL,
112
+ schema_version INTEGER NOT NULL,
113
+ active_locator_count INTEGER NOT NULL
114
+ )
115
+ `)
116
+
106
117
  // Schema migrations
107
118
  this.migrateSchema(CURRENT_SCHEMA, [
108
119
  { version: 1, sql: `CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER NOT NULL)` },
@@ -126,6 +137,18 @@ export class MetadataDB extends SqliteDb {
126
137
  version: 6,
127
138
  sql: `ALTER TABLE deck_refs ADD COLUMN mode TEXT DEFAULT 'symlink'`,
128
139
  },
140
+ {
141
+ version: 7,
142
+ sql: `
143
+ CREATE TABLE IF NOT EXISTS _meta_fingerprint (
144
+ id INTEGER PRIMARY KEY CHECK (id = 1),
145
+ fingerprint TEXT NOT NULL,
146
+ computed_at TEXT NOT NULL,
147
+ schema_version INTEGER NOT NULL,
148
+ active_locator_count INTEGER NOT NULL
149
+ )
150
+ `,
151
+ },
129
152
  ])
130
153
  }
131
154
 
@@ -360,5 +383,71 @@ export class MetadataDB extends SqliteDb {
360
383
  }
361
384
  insert.finalize()
362
385
  })()
386
+
387
+ this.computeAndStoreFingerprint()
388
+ }
389
+
390
+ // ── Integrity fingerprint ─────────────────────────────────────
391
+
392
+ private computeAndStoreFingerprint(): void {
393
+ const activeLocators = this.getAllActiveLocators().sort().join('\n')
394
+ const repoCount = (this.db.query('SELECT COUNT(*) as cnt FROM repos').get() as { cnt: number } | null)?.cnt ?? 0
395
+ const skillCount = (this.db.query('SELECT COUNT(*) as cnt FROM skills').get() as { cnt: number } | null)?.cnt ?? 0
396
+ const refCount = (this.db.query('SELECT COUNT(*) as cnt FROM deck_refs').get() as { cnt: number } | null)?.cnt ?? 0
397
+ const alc = activeLocators ? activeLocators.split('\n').length : 0
398
+ const payload = `${activeLocators}\n${CURRENT_SCHEMA}\n${repoCount}\n${skillCount}\n${refCount}`
399
+ const fingerprint = createHash('sha256').update(payload).digest('hex')
400
+
401
+ this.exec(
402
+ `INSERT OR REPLACE INTO _meta_fingerprint (id, fingerprint, computed_at, schema_version, active_locator_count)
403
+ VALUES (1, $fp, $now, $sv, $alc)`,
404
+ { $fp: fingerprint, $now: this.now(), $sv: CURRENT_SCHEMA, $alc: alc },
405
+ )
406
+ }
407
+
408
+ /**
409
+ * Recompute the fingerprint and compare with stored value.
410
+ * Returns { ok: true } if match, { ok: false, message } with repair hint if mismatch.
411
+ */
412
+ validateIntegrity(): { ok: boolean; stored: string | null; computed: string; message: string } {
413
+ const row = this.db
414
+ .query(`SELECT fingerprint, computed_at, schema_version FROM _meta_fingerprint WHERE id = 1`)
415
+ .get() as { fingerprint: string; computed_at: string; schema_version: number } | null
416
+
417
+ const activeLocators = this.getAllActiveLocators().sort().join('\n')
418
+ const repoCount = (this.db.query('SELECT COUNT(*) as cnt FROM repos').get() as { cnt: number } | null)?.cnt ?? 0
419
+ const skillCount = (this.db.query('SELECT COUNT(*) as cnt FROM skills').get() as { cnt: number } | null)?.cnt ?? 0
420
+ const refCount = (this.db.query('SELECT COUNT(*) as cnt FROM deck_refs').get() as { cnt: number } | null)?.cnt ?? 0
421
+ const payload = `${activeLocators}\n${CURRENT_SCHEMA}\n${repoCount}\n${skillCount}\n${refCount}`
422
+ const computed = createHash('sha256').update(payload).digest('hex')
423
+
424
+ if (!row) {
425
+ return {
426
+ ok: false,
427
+ stored: null,
428
+ computed,
429
+ message: 'No fingerprint stored. DB may be uninitialized — run deck link to rebuild.',
430
+ }
431
+ }
432
+
433
+ if (row.schema_version !== CURRENT_SCHEMA) {
434
+ return {
435
+ ok: false,
436
+ stored: row.fingerprint,
437
+ computed,
438
+ message: `Schema version mismatch: stored ${row.schema_version}, current ${CURRENT_SCHEMA}. Run deck link to rebuild metadata.`,
439
+ }
440
+ }
441
+
442
+ if (row.fingerprint !== computed) {
443
+ return {
444
+ ok: false,
445
+ stored: row.fingerprint,
446
+ computed,
447
+ message: 'Fingerprint mismatch — metadata DB may have been swapped, corrupted, or desynchronized from cold pool. Run deck link to rebuild.',
448
+ }
449
+ }
450
+
451
+ return { ok: true, stored: row.fingerprint, computed, message: 'Integrity OK' }
363
452
  }
364
453
  }