@siduri-x/core 2.0.0 → 2.0.2
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/dist/action-policy.d.ts +35 -1
- package/dist/action-policy.js +228 -8
- package/dist/action-policy.test.js +171 -1
- package/dist/action.d.ts +1 -1
- package/dist/adversarial.test.js +4 -2
- package/dist/capability.d.ts +15 -2
- package/dist/capability.js +12 -1
- package/dist/capability.test.js +44 -1
- package/dist/chat-contract.d.ts +3 -1
- package/dist/chat-contract.js +5 -2
- package/dist/container.d.ts +74 -0
- package/dist/container.js +81 -0
- package/dist/context.d.ts +1 -1
- package/dist/index.d.ts +11 -7
- package/dist/index.js +2 -0
- package/dist/input-normalizer.js +3 -1
- package/dist/perception-pipeline.d.ts +76 -0
- package/dist/perception-pipeline.js +255 -0
- package/dist/perception-pipeline.test.d.ts +1 -0
- package/dist/perception-pipeline.test.js +65 -0
- package/dist/runtime-facades.test.js +27 -27
- package/dist/runtime.d.ts +36 -112
- package/dist/runtime.js +58 -392
- package/dist/schema-validator.test.js +2 -2
- package/dist/siduri-db.d.ts +27 -8
- package/dist/siduri-db.js +260 -17
- package/dist/siduri-db.test.js +284 -0
- package/dist/sqlite-action-store.d.ts +3 -2
- package/dist/sqlite-action-store.js +60 -6
- package/dist/sqlite-action-store.test.js +10 -0
- package/package.json +6 -6
package/dist/siduri-db.js
CHANGED
|
@@ -114,7 +114,9 @@ class SiduriDatabase {
|
|
|
114
114
|
valid_from TEXT,
|
|
115
115
|
valid_until TEXT,
|
|
116
116
|
evidence TEXT,
|
|
117
|
-
asserted_at TEXT DEFAULT (datetime('now'))
|
|
117
|
+
asserted_at TEXT DEFAULT (datetime('now')),
|
|
118
|
+
supersedes TEXT,
|
|
119
|
+
source_event_id TEXT
|
|
118
120
|
);
|
|
119
121
|
|
|
120
122
|
-- FTS5 Virtual Table for Memory Claims
|
|
@@ -145,6 +147,18 @@ class SiduriDatabase {
|
|
|
145
147
|
END;
|
|
146
148
|
`;
|
|
147
149
|
this.db.exec(schema);
|
|
150
|
+
try {
|
|
151
|
+
this.db.exec("ALTER TABLE memory_claims ADD COLUMN supersedes TEXT");
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Column already exists
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
this.db.exec("ALTER TABLE memory_claims ADD COLUMN source_event_id TEXT");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Column already exists
|
|
161
|
+
}
|
|
148
162
|
}
|
|
149
163
|
close() {
|
|
150
164
|
this.db.close();
|
|
@@ -228,9 +242,106 @@ class SiduriDatabase {
|
|
|
228
242
|
`);
|
|
229
243
|
stmt.run(directive.id, directive.companionId, directive.priority, directive.directive, directive.status, directive.category, directive.supersedesId || null, directive.createdAt || new Date().toISOString());
|
|
230
244
|
}
|
|
231
|
-
|
|
232
|
-
const stmt =
|
|
233
|
-
|
|
245
|
+
getDirective(id, companionId) {
|
|
246
|
+
const stmt = companionId
|
|
247
|
+
? this.db.prepare('SELECT * FROM self_directives WHERE id = ? AND companion_id = ?')
|
|
248
|
+
: this.db.prepare('SELECT * FROM self_directives WHERE id = ?');
|
|
249
|
+
const row = (companionId ? stmt.get(id, companionId) : stmt.get(id));
|
|
250
|
+
if (!row)
|
|
251
|
+
return undefined;
|
|
252
|
+
return {
|
|
253
|
+
id: row.id,
|
|
254
|
+
companionId: row.companion_id,
|
|
255
|
+
priority: row.priority,
|
|
256
|
+
directive: row.directive,
|
|
257
|
+
status: row.status,
|
|
258
|
+
category: row.category,
|
|
259
|
+
supersedesId: row.supersedes_id || undefined,
|
|
260
|
+
createdAt: row.created_at,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
approveDirective(id, companionId) {
|
|
264
|
+
const findStmt = companionId
|
|
265
|
+
? this.db.prepare("SELECT id, companion_id, status, supersedes_id FROM self_directives WHERE id = ? AND companion_id = ?")
|
|
266
|
+
: this.db.prepare("SELECT id, companion_id, status, supersedes_id FROM self_directives WHERE id = ?");
|
|
267
|
+
const row = (companionId ? findStmt.get(id, companionId) : findStmt.get(id));
|
|
268
|
+
if (!row) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (row.status !== 'PENDING') {
|
|
272
|
+
throw new Error(`Cannot approve directive '${id}': invalid transition from status '${row.status}' to 'ACTIVE' (only PENDING directives can be approved)`);
|
|
273
|
+
}
|
|
274
|
+
// If this directive supersedes an earlier directive, transition that prior directive to SUPERSEDED
|
|
275
|
+
if (row.supersedes_id) {
|
|
276
|
+
const supersededId = row.supersedes_id;
|
|
277
|
+
const effectiveCompanionId = companionId || row.companion_id;
|
|
278
|
+
if (effectiveCompanionId) {
|
|
279
|
+
const supersedeStmt = this.db.prepare("UPDATE self_directives SET status = 'SUPERSEDED' WHERE id = ? AND companion_id = ?");
|
|
280
|
+
supersedeStmt.run(supersededId, effectiveCompanionId);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const supersedeStmt = this.db.prepare("UPDATE self_directives SET status = 'SUPERSEDED' WHERE id = ?");
|
|
284
|
+
supersedeStmt.run(supersededId);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (companionId) {
|
|
288
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'ACTIVE' WHERE id = ? AND companion_id = ? AND status = 'PENDING'");
|
|
289
|
+
stmt.run(id, companionId);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'ACTIVE' WHERE id = ? AND status = 'PENDING'");
|
|
293
|
+
stmt.run(id);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
rejectDirective(id, companionId) {
|
|
297
|
+
const findStmt = companionId
|
|
298
|
+
? this.db.prepare("SELECT id, companion_id, status FROM self_directives WHERE id = ? AND companion_id = ?")
|
|
299
|
+
: this.db.prepare("SELECT id, companion_id, status FROM self_directives WHERE id = ?");
|
|
300
|
+
const row = (companionId ? findStmt.get(id, companionId) : findStmt.get(id));
|
|
301
|
+
if (!row) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (row.status !== 'PENDING') {
|
|
305
|
+
throw new Error(`Cannot reject directive '${id}': invalid transition from status '${row.status}' to 'REJECTED' (only PENDING directives can be rejected)`);
|
|
306
|
+
}
|
|
307
|
+
if (companionId) {
|
|
308
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'REJECTED' WHERE id = ? AND companion_id = ? AND status = 'PENDING'");
|
|
309
|
+
stmt.run(id, companionId);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'REJECTED' WHERE id = ? AND status = 'PENDING'");
|
|
313
|
+
stmt.run(id);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
revokeDirective(id, companionId) {
|
|
317
|
+
if (companionId) {
|
|
318
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'REVOKED' WHERE id = ? AND companion_id = ?");
|
|
319
|
+
stmt.run(id, companionId);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'REVOKED' WHERE id = ?");
|
|
323
|
+
stmt.run(id);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
expireDirective(id, companionId) {
|
|
327
|
+
if (companionId) {
|
|
328
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'EXPIRED' WHERE id = ? AND companion_id = ?");
|
|
329
|
+
stmt.run(id, companionId);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'EXPIRED' WHERE id = ?");
|
|
333
|
+
stmt.run(id);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
disableDirective(id, companionId) {
|
|
337
|
+
if (companionId) {
|
|
338
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'DISABLED' WHERE id = ? AND companion_id = ?");
|
|
339
|
+
stmt.run(id, companionId);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'DISABLED' WHERE id = ?");
|
|
343
|
+
stmt.run(id);
|
|
344
|
+
}
|
|
234
345
|
}
|
|
235
346
|
getRelationship(companionId, entityId) {
|
|
236
347
|
const stmt = this.db.prepare('SELECT * FROM self_relationships WHERE companion_id = ? AND entity_id = ?');
|
|
@@ -380,33 +491,116 @@ class SiduriDatabase {
|
|
|
380
491
|
payload: JSON.parse(row.payload)
|
|
381
492
|
}));
|
|
382
493
|
}
|
|
494
|
+
getEvent(id) {
|
|
495
|
+
const stmt = this.db.prepare('SELECT * FROM memory_events WHERE id = ?');
|
|
496
|
+
const row = stmt.get(id);
|
|
497
|
+
if (!row)
|
|
498
|
+
return undefined;
|
|
499
|
+
return {
|
|
500
|
+
id: row.id,
|
|
501
|
+
companionId: row.companion_id,
|
|
502
|
+
sourceType: row.source_type,
|
|
503
|
+
occurredAt: row.occurred_at,
|
|
504
|
+
payload: JSON.parse(row.payload)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
383
507
|
proposeClaim(claim) {
|
|
384
508
|
const id = claim.id || crypto.randomUUID();
|
|
385
509
|
const status = 'PENDING';
|
|
510
|
+
const confidence = claim.confidence ?? 1.0;
|
|
511
|
+
const assertedAt = claim.assertedAt || new Date().toISOString();
|
|
386
512
|
const stmt = this.db.prepare(`
|
|
387
|
-
INSERT INTO memory_claims (id, companion_id, subject, predicate, value, status, confidence, valid_from, valid_until, evidence, asserted_at)
|
|
388
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, coalesce(?, datetime('now')))
|
|
513
|
+
INSERT INTO memory_claims (id, companion_id, subject, predicate, value, status, confidence, valid_from, valid_until, evidence, asserted_at, supersedes, source_event_id)
|
|
514
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, coalesce(?, datetime('now')), ?, ?)
|
|
389
515
|
`);
|
|
390
|
-
stmt.run(id, claim.companionId, claim.subject, claim.predicate, claim.value, status,
|
|
516
|
+
stmt.run(id, claim.companionId, claim.subject, claim.predicate, claim.value, status, confidence, claim.validFrom || null, claim.validUntil || null, claim.evidence ? JSON.stringify(claim.evidence) : null, assertedAt || null, claim.supersedes || null, claim.sourceEventId || null);
|
|
391
517
|
return {
|
|
392
518
|
...claim,
|
|
393
519
|
id,
|
|
394
|
-
status
|
|
520
|
+
status,
|
|
521
|
+
confidence,
|
|
522
|
+
assertedAt,
|
|
523
|
+
supersedes: claim.supersedes,
|
|
524
|
+
sourceEventId: claim.sourceEventId,
|
|
395
525
|
};
|
|
396
526
|
}
|
|
397
|
-
approveClaim(id) {
|
|
398
|
-
const
|
|
399
|
-
|
|
527
|
+
approveClaim(id, companionId) {
|
|
528
|
+
const findClaim = companionId
|
|
529
|
+
? this.db.prepare("SELECT id, status, supersedes FROM memory_claims WHERE id = ? AND companion_id = ?")
|
|
530
|
+
: this.db.prepare("SELECT id, status, supersedes FROM memory_claims WHERE id = ?");
|
|
531
|
+
const row = (companionId ? findClaim.get(id, companionId) : findClaim.get(id));
|
|
532
|
+
if (!row) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (row.status !== 'PENDING') {
|
|
536
|
+
throw new Error(`Cannot approve claim '${id}': invalid transition from status '${row.status}' to 'APPROVED' (only PENDING claims can be approved)`);
|
|
537
|
+
}
|
|
538
|
+
// If this claim supersedes an earlier claim, transition that prior claim to SUPERSEDED
|
|
539
|
+
if (row.supersedes) {
|
|
540
|
+
const supersededId = row.supersedes;
|
|
541
|
+
if (companionId) {
|
|
542
|
+
const supersedeStmt = this.db.prepare("UPDATE memory_claims SET status = 'SUPERSEDED' WHERE id = ? AND companion_id = ?");
|
|
543
|
+
supersedeStmt.run(supersededId, companionId);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
const supersedeStmt = this.db.prepare("UPDATE memory_claims SET status = 'SUPERSEDED' WHERE id = ?");
|
|
547
|
+
supersedeStmt.run(supersededId);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (companionId) {
|
|
551
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'APPROVED' WHERE id = ? AND companion_id = ? AND status = 'PENDING'");
|
|
552
|
+
stmt.run(id, companionId);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'APPROVED' WHERE id = ? AND status = 'PENDING'");
|
|
556
|
+
stmt.run(id);
|
|
557
|
+
}
|
|
400
558
|
}
|
|
401
|
-
rejectClaim(id) {
|
|
402
|
-
|
|
403
|
-
|
|
559
|
+
rejectClaim(id, companionId) {
|
|
560
|
+
if (companionId) {
|
|
561
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'REJECTED' WHERE id = ? AND companion_id = ?");
|
|
562
|
+
stmt.run(id, companionId);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'REJECTED' WHERE id = ?");
|
|
566
|
+
stmt.run(id);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
revokeClaim(id, companionId) {
|
|
570
|
+
if (companionId) {
|
|
571
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'REVOKED' WHERE id = ? AND companion_id = ?");
|
|
572
|
+
stmt.run(id, companionId);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'REVOKED' WHERE id = ?");
|
|
576
|
+
stmt.run(id);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
expireClaim(id, companionId) {
|
|
580
|
+
if (companionId) {
|
|
581
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'EXPIRED' WHERE id = ? AND companion_id = ?");
|
|
582
|
+
stmt.run(id, companionId);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'EXPIRED' WHERE id = ?");
|
|
586
|
+
stmt.run(id);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
markClaimSessionOnly(id, companionId) {
|
|
590
|
+
if (companionId) {
|
|
591
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'SESSION_ONLY' WHERE id = ? AND companion_id = ?");
|
|
592
|
+
stmt.run(id, companionId);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'SESSION_ONLY' WHERE id = ?");
|
|
596
|
+
stmt.run(id);
|
|
597
|
+
}
|
|
404
598
|
}
|
|
405
599
|
searchClaims(companionId, query, limit = 20) {
|
|
406
600
|
const stmt = this.db.prepare(`
|
|
407
601
|
SELECT c.* FROM memory_claims c
|
|
408
602
|
JOIN memory_search s ON c.rowid = s.rowid
|
|
409
|
-
WHERE c.companion_id = ? AND memory_search MATCH ?
|
|
603
|
+
WHERE c.companion_id = ? AND c.status = 'APPROVED' AND memory_search MATCH ?
|
|
410
604
|
ORDER BY rank
|
|
411
605
|
LIMIT ?
|
|
412
606
|
`);
|
|
@@ -421,7 +615,27 @@ class SiduriDatabase {
|
|
|
421
615
|
validFrom: row.valid_from || undefined,
|
|
422
616
|
validUntil: row.valid_until || undefined,
|
|
423
617
|
evidence: row.evidence ? JSON.parse(row.evidence) : undefined,
|
|
424
|
-
assertedAt: row.asserted_at
|
|
618
|
+
assertedAt: row.asserted_at,
|
|
619
|
+
supersedes: row.supersedes || undefined,
|
|
620
|
+
sourceEventId: row.source_event_id || undefined,
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
getPendingClaims(companionId, limit = 50) {
|
|
624
|
+
const stmt = this.db.prepare("SELECT * FROM memory_claims WHERE companion_id = ? AND status = 'PENDING' ORDER BY asserted_at DESC LIMIT ?");
|
|
625
|
+
return stmt.all(companionId, limit).map((row) => ({
|
|
626
|
+
id: row.id,
|
|
627
|
+
companionId: row.companion_id,
|
|
628
|
+
subject: row.subject,
|
|
629
|
+
predicate: row.predicate,
|
|
630
|
+
value: row.value,
|
|
631
|
+
status: row.status,
|
|
632
|
+
confidence: row.confidence,
|
|
633
|
+
validFrom: row.valid_from || undefined,
|
|
634
|
+
validUntil: row.valid_until || undefined,
|
|
635
|
+
evidence: row.evidence ? JSON.parse(row.evidence) : undefined,
|
|
636
|
+
assertedAt: row.asserted_at,
|
|
637
|
+
supersedes: row.supersedes || undefined,
|
|
638
|
+
sourceEventId: row.source_event_id || undefined,
|
|
425
639
|
}));
|
|
426
640
|
}
|
|
427
641
|
getApprovedClaims(companionId, limit = 50) {
|
|
@@ -437,8 +651,37 @@ class SiduriDatabase {
|
|
|
437
651
|
validFrom: row.valid_from || undefined,
|
|
438
652
|
validUntil: row.valid_until || undefined,
|
|
439
653
|
evidence: row.evidence ? JSON.parse(row.evidence) : undefined,
|
|
440
|
-
assertedAt: row.asserted_at
|
|
654
|
+
assertedAt: row.asserted_at,
|
|
655
|
+
supersedes: row.supersedes || undefined,
|
|
656
|
+
sourceEventId: row.source_event_id || undefined,
|
|
441
657
|
}));
|
|
442
658
|
}
|
|
659
|
+
getClaim(id) {
|
|
660
|
+
const stmt = this.db.prepare("SELECT * FROM memory_claims WHERE id = ?");
|
|
661
|
+
const row = stmt.get(id);
|
|
662
|
+
if (!row)
|
|
663
|
+
return undefined;
|
|
664
|
+
return {
|
|
665
|
+
id: row.id,
|
|
666
|
+
companionId: row.companion_id,
|
|
667
|
+
subject: row.subject,
|
|
668
|
+
predicate: row.predicate,
|
|
669
|
+
value: row.value,
|
|
670
|
+
status: row.status,
|
|
671
|
+
confidence: row.confidence,
|
|
672
|
+
validFrom: row.valid_from || undefined,
|
|
673
|
+
validUntil: row.valid_until || undefined,
|
|
674
|
+
evidence: row.evidence ? JSON.parse(row.evidence) : undefined,
|
|
675
|
+
assertedAt: row.asserted_at,
|
|
676
|
+
supersedes: row.supersedes || undefined,
|
|
677
|
+
sourceEventId: row.source_event_id || undefined,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
resetMemory(companionId) {
|
|
681
|
+
const deleteClaims = this.db.prepare("DELETE FROM memory_claims WHERE companion_id = ?");
|
|
682
|
+
deleteClaims.run(companionId);
|
|
683
|
+
const deleteEvents = this.db.prepare("DELETE FROM memory_events WHERE companion_id = ?");
|
|
684
|
+
deleteEvents.run(companionId);
|
|
685
|
+
}
|
|
443
686
|
}
|
|
444
687
|
exports.SiduriDatabase = SiduriDatabase;
|
package/dist/siduri-db.test.js
CHANGED
|
@@ -72,6 +72,16 @@ describe('SiduriDatabase', () => {
|
|
|
72
72
|
memDb.close();
|
|
73
73
|
}).not.toThrow();
|
|
74
74
|
});
|
|
75
|
+
it('initializes schema and WAL mode within the startup latency budget (<1000ms in CI, typical <20ms locally)', () => {
|
|
76
|
+
const start = performance.now();
|
|
77
|
+
const benchDb = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
78
|
+
const duration = performance.now() - start;
|
|
79
|
+
benchDb.close();
|
|
80
|
+
// In bare-metal local development, SQLite cold init is ~2-5ms.
|
|
81
|
+
// Under virtualized CI runners with concurrent Turbo tasks and shared I/O, allow up to 1000ms.
|
|
82
|
+
const budgetMs = process.env.CI ? 1000 : 250;
|
|
83
|
+
expect(duration).toBeLessThan(budgetMs);
|
|
84
|
+
});
|
|
75
85
|
it('stores and retrieves companion identity', () => {
|
|
76
86
|
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
77
87
|
const identity = {
|
|
@@ -465,6 +475,91 @@ describe('SiduriDatabase', () => {
|
|
|
465
475
|
// More occurrences of "dark" → better BM25 rank (lower rank value = better match)
|
|
466
476
|
expect(results[0].id).toBe(c1.id);
|
|
467
477
|
});
|
|
478
|
+
it('excludes pending and rejected claims from searchClaims in SiduriDatabase', () => {
|
|
479
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
480
|
+
const pending = db.proposeClaim({
|
|
481
|
+
id: crypto.randomUUID(),
|
|
482
|
+
companionId: 'siduri-test',
|
|
483
|
+
subject: 'SecretProject',
|
|
484
|
+
predicate: 'status',
|
|
485
|
+
value: 'unapproved draft specification',
|
|
486
|
+
confidence: 0.9,
|
|
487
|
+
assertedAt: new Date().toISOString(),
|
|
488
|
+
});
|
|
489
|
+
// Must not match while pending
|
|
490
|
+
let results = db.searchClaims('siduri-test', 'unapproved');
|
|
491
|
+
expect(results.some((r) => r.id === pending.id)).toBe(false);
|
|
492
|
+
// Approve: now it matches
|
|
493
|
+
db.approveClaim(pending.id, 'siduri-test');
|
|
494
|
+
results = db.searchClaims('siduri-test', 'unapproved');
|
|
495
|
+
expect(results.some((r) => r.id === pending.id)).toBe(true);
|
|
496
|
+
// Revoke: must no longer match
|
|
497
|
+
db.revokeClaim(pending.id, 'siduri-test');
|
|
498
|
+
results = db.searchClaims('siduri-test', 'unapproved');
|
|
499
|
+
expect(results.some((r) => r.id === pending.id)).toBe(false);
|
|
500
|
+
// Explicitly rejected claims must also not match
|
|
501
|
+
const rejected = db.proposeClaim({
|
|
502
|
+
id: crypto.randomUUID(),
|
|
503
|
+
companionId: 'siduri-test',
|
|
504
|
+
subject: 'RejectedFact',
|
|
505
|
+
predicate: 'status',
|
|
506
|
+
value: 'unapproved rejection draft',
|
|
507
|
+
confidence: 0.1,
|
|
508
|
+
});
|
|
509
|
+
db.rejectClaim(rejected.id, 'siduri-test');
|
|
510
|
+
results = db.searchClaims('siduri-test', 'rejection');
|
|
511
|
+
expect(results.some((r) => r.id === rejected.id)).toBe(false);
|
|
512
|
+
});
|
|
513
|
+
it('bounds approveClaim to specific companionId when provided', () => {
|
|
514
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
515
|
+
const claim = db.proposeClaim({
|
|
516
|
+
id: crypto.randomUUID(),
|
|
517
|
+
companionId: 'companion-target',
|
|
518
|
+
subject: 'ProtectedFact',
|
|
519
|
+
predicate: 'belongsTo',
|
|
520
|
+
value: 'Target',
|
|
521
|
+
confidence: 1.0,
|
|
522
|
+
assertedAt: new Date().toISOString(),
|
|
523
|
+
});
|
|
524
|
+
// Attempting to approve for a different companion must not affect it
|
|
525
|
+
db.approveClaim(claim.id, 'companion-intruder');
|
|
526
|
+
expect(db.getApprovedClaims('companion-target')).toHaveLength(0);
|
|
527
|
+
// Approving with the correct companionId succeeds
|
|
528
|
+
db.approveClaim(claim.id, 'companion-target');
|
|
529
|
+
expect(db.getApprovedClaims('companion-target')).toHaveLength(1);
|
|
530
|
+
});
|
|
531
|
+
it('resets memory for the specified companion only', () => {
|
|
532
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
533
|
+
const c1 = db.proposeClaim({
|
|
534
|
+
id: crypto.randomUUID(),
|
|
535
|
+
companionId: 'comp-1',
|
|
536
|
+
subject: 'Fact1',
|
|
537
|
+
predicate: 'is',
|
|
538
|
+
value: 'One',
|
|
539
|
+
confidence: 1.0,
|
|
540
|
+
});
|
|
541
|
+
const c2 = db.proposeClaim({
|
|
542
|
+
id: crypto.randomUUID(),
|
|
543
|
+
companionId: 'comp-2',
|
|
544
|
+
subject: 'Fact2',
|
|
545
|
+
predicate: 'is',
|
|
546
|
+
value: 'Two',
|
|
547
|
+
confidence: 1.0,
|
|
548
|
+
});
|
|
549
|
+
db.approveClaim(c1.id, 'comp-1');
|
|
550
|
+
db.approveClaim(c2.id, 'comp-2');
|
|
551
|
+
db.recordEvent({
|
|
552
|
+
id: crypto.randomUUID(),
|
|
553
|
+
companionId: 'comp-1',
|
|
554
|
+
sourceType: 'chat_turn',
|
|
555
|
+
occurredAt: new Date().toISOString(),
|
|
556
|
+
payload: { text: 'Hello' },
|
|
557
|
+
});
|
|
558
|
+
db.resetMemory('comp-1');
|
|
559
|
+
expect(db.getApprovedClaims('comp-1')).toHaveLength(0);
|
|
560
|
+
expect(db.getRecentEvents('comp-1')).toHaveLength(0);
|
|
561
|
+
expect(db.getApprovedClaims('comp-2')).toHaveLength(1);
|
|
562
|
+
});
|
|
468
563
|
});
|
|
469
564
|
// ==========================================
|
|
470
565
|
// Cross-Domain & Persistence Tests
|
|
@@ -576,5 +671,194 @@ describe('SiduriDatabase', () => {
|
|
|
576
671
|
expect(db.searchClaims(cId, 'anything')).toHaveLength(0);
|
|
577
672
|
expect(db.getApprovedClaims(cId)).toHaveLength(0);
|
|
578
673
|
});
|
|
674
|
+
it('enforces directive state transitions (pending -> active -> disabled/rejected/revoked)', () => {
|
|
675
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
676
|
+
const cId = 'directive-state-test';
|
|
677
|
+
const dId = 'dir-lifecycle-1';
|
|
678
|
+
// 1. Commit directive in PENDING state
|
|
679
|
+
db.commitDirective({
|
|
680
|
+
id: dId,
|
|
681
|
+
companionId: cId,
|
|
682
|
+
priority: 60,
|
|
683
|
+
directive: 'Always verify claims',
|
|
684
|
+
status: 'PENDING',
|
|
685
|
+
category: 'behavioral',
|
|
686
|
+
});
|
|
687
|
+
// Pending directives must not be returned by getActiveDirectives
|
|
688
|
+
expect(db.getActiveDirectives(cId)).toHaveLength(0);
|
|
689
|
+
// 2. Approve directive
|
|
690
|
+
db.approveDirective(dId);
|
|
691
|
+
const active = db.getActiveDirectives(cId);
|
|
692
|
+
expect(active).toHaveLength(1);
|
|
693
|
+
expect(active[0].id).toBe(dId);
|
|
694
|
+
expect(active[0].status).toBe('ACTIVE');
|
|
695
|
+
// 3. Revoke directive
|
|
696
|
+
db.revokeDirective(dId);
|
|
697
|
+
expect(db.getActiveDirectives(cId)).toHaveLength(0);
|
|
698
|
+
// 4. Reject directive
|
|
699
|
+
const d2Id = 'dir-lifecycle-2';
|
|
700
|
+
db.commitDirective({
|
|
701
|
+
id: d2Id,
|
|
702
|
+
companionId: cId,
|
|
703
|
+
priority: 50,
|
|
704
|
+
directive: 'Unsafe rule',
|
|
705
|
+
status: 'PENDING',
|
|
706
|
+
category: 'behavioral',
|
|
707
|
+
});
|
|
708
|
+
db.rejectDirective(d2Id);
|
|
709
|
+
expect(db.getActiveDirectives(cId)).toHaveLength(0);
|
|
710
|
+
});
|
|
711
|
+
it('rejects invalid directive state transitions (throws when approving non-PENDING directive)', () => {
|
|
712
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
713
|
+
const cId = 'directive-invalid-transition-test';
|
|
714
|
+
// 1. Commit directive in REJECTED state
|
|
715
|
+
db.commitDirective({
|
|
716
|
+
id: 'dir-rejected-1',
|
|
717
|
+
companionId: cId,
|
|
718
|
+
priority: 50,
|
|
719
|
+
directive: 'Rejected directive',
|
|
720
|
+
status: 'REJECTED',
|
|
721
|
+
category: 'behavioral',
|
|
722
|
+
});
|
|
723
|
+
expect(() => db.approveDirective('dir-rejected-1')).toThrow(/invalid transition from status 'REJECTED' to 'ACTIVE'/);
|
|
724
|
+
// 2. Commit directive in ACTIVE state
|
|
725
|
+
db.commitDirective({
|
|
726
|
+
id: 'dir-active-1',
|
|
727
|
+
companionId: cId,
|
|
728
|
+
priority: 50,
|
|
729
|
+
directive: 'Already active directive',
|
|
730
|
+
status: 'ACTIVE',
|
|
731
|
+
category: 'behavioral',
|
|
732
|
+
});
|
|
733
|
+
expect(() => db.approveDirective('dir-active-1')).toThrow(/invalid transition from status 'ACTIVE' to 'ACTIVE'/);
|
|
734
|
+
// 3. Rejecting an already ACTIVE directive throws
|
|
735
|
+
expect(() => db.rejectDirective('dir-active-1')).toThrow(/invalid transition from status 'ACTIVE' to 'REJECTED'/);
|
|
736
|
+
});
|
|
737
|
+
it('automatically marks prior directive as SUPERSEDED when approving superseding directive', () => {
|
|
738
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
739
|
+
const cId = 'directive-supersede-test';
|
|
740
|
+
// 1. Initial directive active
|
|
741
|
+
db.commitDirective({
|
|
742
|
+
id: 'dir-original-1',
|
|
743
|
+
companionId: cId,
|
|
744
|
+
priority: 50,
|
|
745
|
+
directive: 'Original rule',
|
|
746
|
+
status: 'ACTIVE',
|
|
747
|
+
category: 'behavioral',
|
|
748
|
+
});
|
|
749
|
+
expect(db.getActiveDirectives(cId)).toHaveLength(1);
|
|
750
|
+
// 2. Propose a superseding directive
|
|
751
|
+
db.commitDirective({
|
|
752
|
+
id: 'dir-replacement-1',
|
|
753
|
+
companionId: cId,
|
|
754
|
+
priority: 55,
|
|
755
|
+
directive: 'Updated replacement rule',
|
|
756
|
+
status: 'PENDING',
|
|
757
|
+
category: 'behavioral',
|
|
758
|
+
supersedesId: 'dir-original-1',
|
|
759
|
+
});
|
|
760
|
+
// Original is still active, replacement is pending
|
|
761
|
+
expect(db.getActiveDirectives(cId)).toHaveLength(1);
|
|
762
|
+
expect(db.getActiveDirectives(cId)[0].id).toBe('dir-original-1');
|
|
763
|
+
// 3. Approve replacement directive
|
|
764
|
+
db.approveDirective('dir-replacement-1', cId);
|
|
765
|
+
const active = db.getActiveDirectives(cId);
|
|
766
|
+
expect(active).toHaveLength(1);
|
|
767
|
+
expect(active[0].id).toBe('dir-replacement-1');
|
|
768
|
+
const original = db.getDirective('dir-original-1', cId);
|
|
769
|
+
expect(original?.status).toBe('SUPERSEDED');
|
|
770
|
+
});
|
|
771
|
+
it('enforces companion isolation on directive approval', () => {
|
|
772
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
773
|
+
const cIdA = 'companion-alpha';
|
|
774
|
+
const cIdB = 'companion-beta';
|
|
775
|
+
db.commitDirective({
|
|
776
|
+
id: 'dir-beta-1',
|
|
777
|
+
companionId: cIdB,
|
|
778
|
+
priority: 50,
|
|
779
|
+
directive: 'Beta private rule',
|
|
780
|
+
status: 'PENDING',
|
|
781
|
+
category: 'behavioral',
|
|
782
|
+
});
|
|
783
|
+
// Alpha attempts to approve Beta's directive scoped to Alpha
|
|
784
|
+
db.approveDirective('dir-beta-1', cIdA);
|
|
785
|
+
// Beta's directive must remain PENDING and unapproved
|
|
786
|
+
const betaDirective = db.getDirective('dir-beta-1', cIdB);
|
|
787
|
+
expect(betaDirective?.status).toBe('PENDING');
|
|
788
|
+
expect(db.getActiveDirectives(cIdB)).toHaveLength(0);
|
|
789
|
+
// Beta approves its own directive successfully
|
|
790
|
+
db.approveDirective('dir-beta-1', cIdB);
|
|
791
|
+
expect(db.getActiveDirectives(cIdB)).toHaveLength(1);
|
|
792
|
+
});
|
|
793
|
+
it('enforces claim state transitions (pending -> approved -> revoked/expired/session_only)', () => {
|
|
794
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
795
|
+
const cId = 'claim-state-test';
|
|
796
|
+
const claim = db.proposeClaim({
|
|
797
|
+
id: 'claim-1',
|
|
798
|
+
companionId: cId,
|
|
799
|
+
subject: 'user',
|
|
800
|
+
predicate: 'likes',
|
|
801
|
+
value: 'matcha',
|
|
802
|
+
});
|
|
803
|
+
expect(claim.status).toBe('PENDING');
|
|
804
|
+
expect(db.getApprovedClaims(cId)).toHaveLength(0);
|
|
805
|
+
// Approve
|
|
806
|
+
db.approveClaim('claim-1');
|
|
807
|
+
expect(db.getApprovedClaims(cId)).toHaveLength(1);
|
|
808
|
+
// Revoke
|
|
809
|
+
db.revokeClaim('claim-1');
|
|
810
|
+
expect(db.getApprovedClaims(cId)).toHaveLength(0);
|
|
811
|
+
// Session only
|
|
812
|
+
const claim2 = db.proposeClaim({
|
|
813
|
+
id: 'claim-2',
|
|
814
|
+
companionId: cId,
|
|
815
|
+
subject: 'session',
|
|
816
|
+
predicate: 'topic',
|
|
817
|
+
value: 'investigation',
|
|
818
|
+
});
|
|
819
|
+
db.markClaimSessionOnly('claim-2');
|
|
820
|
+
expect(db.getApprovedClaims(cId)).toHaveLength(0);
|
|
821
|
+
// Expire
|
|
822
|
+
db.expireClaim('claim-2');
|
|
823
|
+
expect(db.getApprovedClaims(cId)).toHaveLength(0);
|
|
824
|
+
});
|
|
825
|
+
it('strictly rejects illegal claim state transitions in approveClaim', () => {
|
|
826
|
+
db = new siduri_db_1.SiduriDatabase({ dbPath });
|
|
827
|
+
const cId = 'claim-transition-test';
|
|
828
|
+
// 1. Propose and reject claim
|
|
829
|
+
const rejectedClaim = db.proposeClaim({
|
|
830
|
+
id: 'claim-rejected',
|
|
831
|
+
companionId: cId,
|
|
832
|
+
subject: 'fact',
|
|
833
|
+
predicate: 'is',
|
|
834
|
+
value: 'false',
|
|
835
|
+
});
|
|
836
|
+
db.rejectClaim(rejectedClaim.id);
|
|
837
|
+
// Attempting to approve a REJECTED claim must throw
|
|
838
|
+
expect(() => db.approveClaim(rejectedClaim.id)).toThrow(/invalid transition from status 'REJECTED' to 'APPROVED'/);
|
|
839
|
+
// 2. Propose and approve claim, then revoke
|
|
840
|
+
const revokedClaim = db.proposeClaim({
|
|
841
|
+
id: 'claim-revoked',
|
|
842
|
+
companionId: cId,
|
|
843
|
+
subject: 'fact',
|
|
844
|
+
predicate: 'is',
|
|
845
|
+
value: 'outdated',
|
|
846
|
+
});
|
|
847
|
+
db.approveClaim(revokedClaim.id);
|
|
848
|
+
db.revokeClaim(revokedClaim.id);
|
|
849
|
+
// Attempting to approve a REVOKED claim must throw
|
|
850
|
+
expect(() => db.approveClaim(revokedClaim.id)).toThrow(/invalid transition from status 'REVOKED' to 'APPROVED'/);
|
|
851
|
+
// 3. Propose and expire claim
|
|
852
|
+
const expiredClaim = db.proposeClaim({
|
|
853
|
+
id: 'claim-expired',
|
|
854
|
+
companionId: cId,
|
|
855
|
+
subject: 'fact',
|
|
856
|
+
predicate: 'is',
|
|
857
|
+
value: 'temporary',
|
|
858
|
+
});
|
|
859
|
+
db.expireClaim(expiredClaim.id);
|
|
860
|
+
// Attempting to approve an EXPIRED claim must throw
|
|
861
|
+
expect(() => db.approveClaim(expiredClaim.id)).toThrow(/invalid transition from status 'EXPIRED' to 'APPROVED'/);
|
|
862
|
+
});
|
|
579
863
|
});
|
|
580
864
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionAuditEvent } from './action';
|
|
2
|
-
import { ActionStore, PersistentExecutionRecord } from './capability';
|
|
2
|
+
import { ActionStore, PersistentExecutionRecord, ActionApprovalRecord } from './capability';
|
|
3
3
|
export interface SqliteActionStoreOptions {
|
|
4
4
|
dbPath?: string;
|
|
5
5
|
}
|
|
@@ -12,8 +12,9 @@ export declare class SqliteActionStore implements ActionStore {
|
|
|
12
12
|
reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
|
|
13
13
|
updateExecution(record: PersistentExecutionRecord): Promise<void>;
|
|
14
14
|
getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
|
|
15
|
-
saveApproval(executionId: string, approverActorId: string, reason?: string): Promise<void>;
|
|
15
|
+
saveApproval(executionId: string, approverActorId: string, reason?: string, approverRole?: string, toolName?: string, parametersHash?: string, companionId?: string, actorId?: string): Promise<void>;
|
|
16
16
|
isActionApproved(executionId: string): Promise<boolean>;
|
|
17
|
+
getApproval(executionId: string): Promise<ActionApprovalRecord | undefined>;
|
|
17
18
|
appendAudit(event: ActionAuditEvent): Promise<void>;
|
|
18
19
|
getAuditLog(executionId?: string): Promise<ActionAuditEvent[]>;
|
|
19
20
|
close(): void;
|