@gregiteen/ssss-cli 0.13.1 → 0.14.1

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.
@@ -26,6 +26,7 @@ import {
26
26
  validateBundle,
27
27
  provisionBundle,
28
28
  importBundle,
29
+ upgradeBundle,
29
30
  } from '../src/bundle.mjs';
30
31
  import { parseDocument } from '../src/frontmatter.mjs';
31
32
  import { loadRegistries } from '../src/registry.mjs';
@@ -41,6 +42,7 @@ import { auditRegistryFieldUsage } from './audit-registry-field-usage.mjs';
41
42
  import { validateSkills } from './validate-skills.mjs';
42
43
  import { auditSpecExamples } from './audit-spec-examples.mjs';
43
44
  import { runKernel09Conformance } from './conformance-09.mjs';
45
+ import { createServer } from './cmd-serve.mjs';
44
46
 
45
47
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
46
48
  const FIXTURES = path.resolve(__dirname, '..', 'conformance', 'fixtures.json');
@@ -375,6 +377,335 @@ async function runBundleConformance() {
375
377
  fs.rmSync(missingVault, { recursive: true, force: true });
376
378
  }
377
379
 
380
+ // Phase 3 Deliverables: S2, S3, S4, S11, D4 bindings, legacy bundle
381
+ const multiVault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-bundle-multi-'));
382
+ try {
383
+ const engine = createEngine({ registryDir: REGISTRY_DIR });
384
+ // S2: Two-workspace install into different workspaces with prefixes
385
+ const provAlpha = provisionBundle(bundle, {
386
+ workspaceId: 'ws-alpha',
387
+ pathPrefix: 'apps/alpha',
388
+ parameters: { business_name: 'Alpha Fest', domain: 'alpha.example' },
389
+ });
390
+ const provBeta = provisionBundle(bundle, {
391
+ workspaceId: 'ws-beta',
392
+ pathPrefix: 'apps/beta',
393
+ parameters: { business_name: 'Beta Fest', domain: 'beta.example' },
394
+ });
395
+ check('two-workspace provisioning succeeds for both instances', provAlpha.ok && provBeta.ok,
396
+ `alpha=${provAlpha.ok} beta=${provBeta.ok}`);
397
+
398
+ // Check slug remapping and prefixing
399
+ const alphaSlugs = provAlpha.plan.map((e) => parseDocument(e.content).data.slug).filter(Boolean);
400
+ const betaSlugs = provBeta.plan.map((e) => parseDocument(e.content).data.slug).filter(Boolean);
401
+ const distinctSlugs = alphaSlugs.every((s) => !betaSlugs.includes(s) && s.startsWith('apps-alpha-'));
402
+ check('two-workspace install isolates slugs and prefixes paths without alias collision (S2)',
403
+ distinctSlugs && provAlpha.plan.every((e) => e.path.startsWith('apps/alpha/')) &&
404
+ provBeta.plan.every((e) => e.path.startsWith('apps/beta/')));
405
+
406
+ // Check link integrity in both targets (no dangling links)
407
+ check('two-workspace install rewrites internal links and resolves all targets (S2)',
408
+ provAlpha.danglingLinks.length === 0 && provBeta.danglingLinks.length === 0);
409
+
410
+ // Import both into the same vault
411
+ const importAlpha = await importBundle(provAlpha.plan, multiVault, engine);
412
+ const importBeta = await importBundle(provBeta.plan, multiVault, engine);
413
+ check('two-workspace import commits both instances into the same vault without conflict (S2)',
414
+ importAlpha.ok && importBeta.ok &&
415
+ importAlpha.committed === bundle.files.length && importBeta.committed === bundle.files.length);
416
+
417
+ // S3: Determinism
418
+ const provBeta2 = provisionBundle(bundle, {
419
+ workspaceId: 'ws-beta',
420
+ pathPrefix: 'apps/beta',
421
+ parameters: { business_name: 'Beta Fest', domain: 'beta.example' },
422
+ });
423
+ check('provisioning is deterministic: equal inputs yield byte-identical plan (S3)',
424
+ JSON.stringify(provBeta.plan) === JSON.stringify(provBeta2.plan));
425
+
426
+ const reimportBeta = await importBundle(provBeta2.plan, multiVault, engine);
427
+ check('re-importing provisioned plan commits 0 new documents (S3)',
428
+ reimportBeta.ok && reimportBeta.committed === 0);
429
+
430
+ // D4: Resource bindings emitted in result
431
+ check('provisionBundle emits resource bindings with relations and modes (D4)',
432
+ provAlpha.bindings.length > 0 &&
433
+ provAlpha.bindings.some((b) => b.type === 'domain' && b.value === 'alpha.example' && b.relation === 'owns_domain'));
434
+
435
+ // D4: Unbound required resource fails when not dryRun
436
+ const unboundBundle = JSON.parse(JSON.stringify(bundle));
437
+ const provUnbound = provisionBundle(unboundBundle, {
438
+ workspaceId: 'ws-unbound',
439
+ parameters: { business_name: 'Unbound Fest' }, // domain omitted!
440
+ dryRun: false,
441
+ });
442
+ check('unbound required resource fails provisioning when dryRun is false (D4)',
443
+ !provUnbound.ok && provUnbound.errors.some((e) => e.includes('unbound required resource')));
444
+
445
+ const provUnboundDry = provisionBundle(unboundBundle, {
446
+ workspaceId: 'ws-unbound',
447
+ parameters: { business_name: 'Unbound Fest' },
448
+ dryRun: true,
449
+ });
450
+ check('unbound required resource error is suppressed when dryRun is true (D4)',
451
+ !provUnboundDry.errors.some((e) => e.includes('unbound required resource')));
452
+
453
+ // S4: Dependencies
454
+ const depFiles = [
455
+ { path: 'rules/dep.md', content: '---\ntype: rule\ntitle: Dep\ndescription: Dep.\ntimestamp: 2026-07-02T00:00:00Z\nname: Dep\n---\nDep body.\n' },
456
+ ];
457
+ const depBundle = {
458
+ manifest: {
459
+ name: 'core-dep',
460
+ description: 'Core dependency bundle',
461
+ version: '1.0.0',
462
+ exported_at: '2026-07-02T00:00:00Z',
463
+ ssss_core_version: '0.9',
464
+ required_extensions: [],
465
+ export_profile: 'template',
466
+ primitive_inventory: { rule: 1 },
467
+ provisioning: [],
468
+ parameters: [],
469
+ file_count: 1,
470
+ provenance: {
471
+ content_hash: contentHash(depFiles),
472
+ exporter: DEFAULT_EXPORTER,
473
+ },
474
+ },
475
+ files: depFiles,
476
+ };
477
+
478
+ const bundleWithDep = JSON.parse(JSON.stringify(bundle));
479
+ bundleWithDep.manifest.dependencies = {
480
+ primitives: bundle.manifest.dependencies?.primitives || [],
481
+ extensions: bundle.manifest.dependencies?.extensions || [],
482
+ migrations: [],
483
+ integrity: bundle.manifest.dependencies?.integrity,
484
+ bundles: [
485
+ { id: 'core-dep', installMode: 'required' },
486
+ { id: 'opt-pack', installMode: 'optional', description: 'Optional telemetry pack' },
487
+ ],
488
+ };
489
+
490
+ // Missing required dependency fails
491
+ const provMissingDep = provisionBundle(bundleWithDep, {
492
+ workspaceId: 'ws-dep-test',
493
+ parameters: { business_name: 'Test', domain: 'test.example' },
494
+ });
495
+ check('bundle with missing required dependency fails clearly (S4)',
496
+ !provMissingDep.ok && provMissingDep.errors.some((e) => e.includes("missing required bundle dependency 'core-dep'")));
497
+
498
+ // Supplied dependency installs first and suggested dependencies are returned
499
+ const provWithDep = provisionBundle(bundleWithDep, {
500
+ workspaceId: 'ws-dep-test',
501
+ parameters: { business_name: 'Test', domain: 'test.example' },
502
+ dependencies: { 'core-dep': depBundle },
503
+ });
504
+ check('bundle with supplied dependency provisions dependency first (S4)',
505
+ provWithDep.ok &&
506
+ provWithDep.dependencies.installed.includes('core-dep') &&
507
+ provWithDep.plan[0].path === 'rules/dep.md' &&
508
+ provWithDep.dependencies.suggested.some((s) => s.id === 'opt-pack' && s.installMode === 'optional'));
509
+
510
+ // Dependency cycle detection
511
+ const cyclicBundleA = JSON.parse(JSON.stringify(depBundle));
512
+ cyclicBundleA.manifest.name = 'cycle-a';
513
+ cyclicBundleA.manifest.dependencies = {
514
+ primitives: ['rule'], extensions: [], migrations: [],
515
+ integrity: { content_hash: cyclicBundleA.manifest.provenance.content_hash, ssss_core_version: '0.9' },
516
+ bundles: [{ id: 'cycle-b', installMode: 'required' }],
517
+ };
518
+ const cyclicBundleB = JSON.parse(JSON.stringify(depBundle));
519
+ cyclicBundleB.manifest.name = 'cycle-b';
520
+ cyclicBundleB.manifest.dependencies = {
521
+ primitives: ['rule'], extensions: [], migrations: [],
522
+ integrity: { content_hash: cyclicBundleB.manifest.provenance.content_hash, ssss_core_version: '0.9' },
523
+ bundles: [{ id: 'cycle-a', installMode: 'required' }],
524
+ };
525
+ const provCycle = provisionBundle(cyclicBundleA, {
526
+ dependencies: { 'cycle-b': cyclicBundleB },
527
+ });
528
+ check('dependency cycle is rejected with clear cycle error (D5)',
529
+ !provCycle.ok && provCycle.errors.some((e) => e.includes('dependency cycle detected')));
530
+
531
+ // S11 / D8: Base-version conflict protection
532
+ const durVault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-bundle-dur-'));
533
+ try {
534
+ const v1Envelope = {
535
+ type: 'operation', workspace_id: 'ws-dur', idempotency_key: 'v1-init',
536
+ path: 'rules/managed.md', actor: { role: 'system' },
537
+ content: '---\ntype: rule\ntitle: Base Rule\ndescription: Base version.\ntimestamp: 2026-07-02T00:00:00Z\nname: Base Rule\n---\nBase content.\n',
538
+ };
539
+ const v1Res = await engine.processOperation(v1Envelope, durVault);
540
+ check('v1 initial document commits cleanly', v1Res.success);
541
+ const v1Hash = 'sha256:' + crypto.createHash('sha256').update(Buffer.from(v1Envelope.content, 'utf8')).digest('hex');
542
+
543
+ // Local edit: someone modifies the document locally
544
+ const v2LocalEnvelope = {
545
+ type: 'operation', workspace_id: 'ws-dur', idempotency_key: 'v2-local',
546
+ path: 'rules/managed.md', actor: { role: 'admin' },
547
+ content: '---\ntype: rule\ntitle: Base Rule\ndescription: Modified locally.\ntimestamp: 2026-07-03T00:00:00Z\nname: Base Rule\n---\nModified locally.\n',
548
+ };
549
+ const v2Res = await engine.processOperation(v2LocalEnvelope, durVault);
550
+ check('local edit v2 commits cleanly', v2Res.success);
551
+
552
+ // Attempt to deliver an older/divergent content with expected_before_hash === v1Hash
553
+ const staleDeliveryEnvelope = {
554
+ type: 'operation', workspace_id: 'ws-dur', idempotency_key: 'stale-delivery',
555
+ path: 'rules/managed.md', actor: { role: 'system' },
556
+ expected_before_hash: v1Hash,
557
+ content: '---\ntype: rule\ntitle: Base Rule\ndescription: Stale update.\ntimestamp: 2026-07-04T00:00:00Z\nname: Base Rule\n---\nStale delivery.\n',
558
+ };
559
+ const staleRes = await engine.processOperation(staleDeliveryEnvelope, durVault);
560
+ check('stale delivery with outdated expected_before_hash fails with version_conflict (S11/D8)',
561
+ staleRes.success === false && staleRes.error?.code === 'version_conflict');
562
+
563
+ // Verify that local edit survived
564
+ const diskContent = fs.readFileSync(path.join(durVault, 'rules/managed.md'), 'utf8');
565
+ check('local edit survives intact after rejected stale delivery (S11/D8)',
566
+ diskContent.includes('Modified locally.'));
567
+ } finally {
568
+ fs.rmSync(durVault, { recursive: true, force: true });
569
+ }
570
+
571
+ // Phase 4 Deliverables: S5 upgradeBundle, migration record, tenant_private refusal
572
+ const upgradeVault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-bundle-upg-'));
573
+ try {
574
+ // Baseline bundle v1.0.0
575
+ const v1Files = [
576
+ {
577
+ path: 'rules/terms.md',
578
+ content: '---\ntype: rule\ntitle: Terms v1\ndescription: Baseline terms.\ntimestamp: 2026-07-02T00:00:00Z\nname: Terms v1\n---\nTerms v1 content.\n',
579
+ },
580
+ {
581
+ path: 'workflows/support.md',
582
+ content: '---\ntype: workflow\ntitle: Support Flow\ndescription: Support.\ntimestamp: 2026-07-02T00:00:00Z\nname: Support Flow\n---\nSupport body.\n',
583
+ },
584
+ ];
585
+ const v1Bundle = {
586
+ manifest: {
587
+ name: 'test-app',
588
+ description: 'Test application bundle',
589
+ version: '1.0.0',
590
+ exported_at: '2026-07-02T00:00:00Z',
591
+ ssss_core_version: '0.9',
592
+ required_extensions: [],
593
+ export_profile: 'template',
594
+ primitive_inventory: { rule: 1, workflow: 1 },
595
+ provisioning: [],
596
+ parameters: [],
597
+ file_count: 2,
598
+ provenance: { content_hash: contentHash(v1Files), exporter: DEFAULT_EXPORTER },
599
+ },
600
+ files: v1Files,
601
+ };
602
+
603
+ // Provision and import baseline v1
604
+ const provV1 = provisionBundle(v1Bundle, { workspaceId: 'ws-upg-test' });
605
+ const importV1 = await importBundle(provV1.plan, upgradeVault, engine);
606
+ check('upgrade fixture baseline v1 imports cleanly', importV1.ok && importV1.committed === 2);
607
+
608
+ // Operator creates a tenant_private task document in the vault
609
+ const tenantTaskEnvelope = {
610
+ type: 'operation',
611
+ workspace_id: 'ws-upg-test',
612
+ idempotency_key: 'tenant-ticket-1',
613
+ path: 'tasks/ticket-101.md',
614
+ actor: { role: 'admin' },
615
+ content: '---\ntype: task\ntitle: Customer Ticket 101\ndescription: Urgent assistance.\ntimestamp: 2026-07-03T00:00:00Z\nname: Ticket 101\nstatus: pending\npriority: urgent\ncategory: support\n---\nTenant private customer message.\n',
616
+ };
617
+ const taskRes = await engine.processOperation(tenantTaskEnvelope, upgradeVault);
618
+ check('tenant_private task commits into upgrade vault', taskRes.success);
619
+
620
+ // Build next bundle v1.1.0 with structural updates
621
+ const v2Files = [
622
+ {
623
+ path: 'rules/terms.md',
624
+ content: '---\ntype: rule\ntitle: Terms v2\ndescription: Updated terms for v2.\ntimestamp: 2026-07-04T00:00:00Z\nname: Terms v2\n---\nUpdated Terms v2 content.\n',
625
+ },
626
+ {
627
+ path: 'workflows/support.md',
628
+ content: '---\ntype: workflow\ntitle: Support Flow\ndescription: Support.\ntimestamp: 2026-07-02T00:00:00Z\nname: Support Flow\n---\nSupport body.\n', // unchanged
629
+ },
630
+ {
631
+ path: 'rules/refunds.md',
632
+ content: '---\ntype: rule\ntitle: Refund Policy\ndescription: New refund rules.\ntimestamp: 2026-07-04T00:00:00Z\nname: Refund Policy\n---\nNew refund policy.\n', // newly added structural file
633
+ },
634
+ ];
635
+ const v2Bundle = {
636
+ manifest: {
637
+ name: 'test-app',
638
+ description: 'Test application bundle',
639
+ version: '1.1.0',
640
+ exported_at: '2026-07-04T00:00:00Z',
641
+ ssss_core_version: '0.9',
642
+ required_extensions: [],
643
+ export_profile: 'template',
644
+ primitive_inventory: { rule: 2, workflow: 1 },
645
+ provisioning: [],
646
+ parameters: [],
647
+ file_count: 3,
648
+ provenance: { content_hash: contentHash(v2Files), exporter: DEFAULT_EXPORTER },
649
+ },
650
+ files: v2Files,
651
+ };
652
+
653
+ // Plan upgrade
654
+ const upgPlan = upgradeBundle(v1Bundle, v2Bundle, {
655
+ workspaceId: 'ws-upg-test',
656
+ registryDir: REGISTRY_DIR,
657
+ });
658
+ check('upgradeBundle generates plan with patch and migration (D6)',
659
+ upgPlan.ok && upgPlan.patchesCount === 1 && upgPlan.operationsCount === 2);
660
+
661
+ // Apply upgrade to vault
662
+ const upgImport = await importBundle(upgPlan.plan, upgradeVault, engine);
663
+ check('upgrade import applies structural changes and records migration (S5)',
664
+ upgImport.ok && upgImport.committed === 3);
665
+
666
+ // Verify structural document is updated
667
+ const updatedTerms = fs.readFileSync(path.join(upgradeVault, 'rules/terms.md'), 'utf8');
668
+ check('structural document was updated by upgrade patch (S5)',
669
+ updatedTerms.includes('Terms v2') && updatedTerms.includes('Updated Terms v2 content.'));
670
+
671
+ // Verify newly added structural file was created
672
+ check('new structural document was created during upgrade (S5)',
673
+ fs.existsSync(path.join(upgradeVault, 'rules/refunds.md')));
674
+
675
+ // Verify migration primitive record exists on disk
676
+ check('migration primitive record was committed during upgrade (D6)',
677
+ fs.existsSync(path.join(upgradeVault, upgPlan.migration.path)));
678
+
679
+ // Verify tenant_private document remains completely untouched
680
+ const tenantTaskDisk = fs.readFileSync(path.join(upgradeVault, 'tasks/ticket-101.md'), 'utf8');
681
+ check('tenant_private task document is completely untouched after upgrade (S5)',
682
+ tenantTaskDisk.includes('Tenant private customer message.'));
683
+
684
+ // Violating patch rejected: attempt to patch tenant_private file with intent: 'upgrade'
685
+ const badUpgradeEnvelope = {
686
+ type: 'patch',
687
+ workspace_id: 'ws-upg-test',
688
+ idempotency_key: 'bad-upg-patch',
689
+ path: 'tasks/ticket-101.md',
690
+ intent: 'upgrade',
691
+ actor: { role: 'system' },
692
+ patches: { title: 'Overwritten Ticket' },
693
+ };
694
+ const badRes = await engine.processOperation(badUpgradeEnvelope, upgradeVault);
695
+ check('engine refuses upgrade envelope targeting tenant_private document (S5/D6)',
696
+ badRes.success === false && badRes.error?.code === 'forbidden');
697
+
698
+ // Verify tenant task is still untouched after rejected attempt
699
+ const tenantTaskAfterBad = fs.readFileSync(path.join(upgradeVault, 'tasks/ticket-101.md'), 'utf8');
700
+ check('tenant_private document remains intact after engine rejected violating upgrade patch (S5/D6)',
701
+ tenantTaskAfterBad.includes('Customer Ticket 101') && !tenantTaskAfterBad.includes('Overwritten Ticket'));
702
+ } finally {
703
+ fs.rmSync(upgradeVault, { recursive: true, force: true });
704
+ }
705
+ } finally {
706
+ fs.rmSync(multiVault, { recursive: true, force: true });
707
+ }
708
+
378
709
  let pass = 0;
379
710
  for (const c of checks) {
380
711
  if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
@@ -924,6 +1255,78 @@ async function runCliSmokeConformance() {
924
1255
  check('ssss migrate 0.8-to-0.9 dry-run emits backup manifest without writes',
925
1256
  migrateOut.dry_run === true && migrateOut.writes_performed === 0 && Array.isArray(migrateOut.backup_manifest),
926
1257
  JSON.stringify(migrateOut));
1258
+
1259
+ const v1Path = path.join(tmp, 'v1.ucw.json');
1260
+ execFileSync('node', [
1261
+ path.join(__dirname, 'ssss.mjs'),
1262
+ 'export', vault,
1263
+ '--out', v1Path,
1264
+ '--profile', 'template',
1265
+ ]);
1266
+ const upgDryOut = JSON.parse(execFileSync('node', [
1267
+ path.join(__dirname, 'ssss.mjs'),
1268
+ 'upgrade', v1Path,
1269
+ '--vault', vault,
1270
+ '--from', v1Path,
1271
+ '--dry-run',
1272
+ '--json',
1273
+ ], { encoding: 'utf8' }));
1274
+ check('ssss upgrade --dry-run reports 0 needed patches for identical version',
1275
+ upgDryOut.ok === true && upgDryOut.patches_count === 0,
1276
+ JSON.stringify(upgDryOut));
1277
+
1278
+ // ssss serve smoke
1279
+ const serveVault = path.join(tmp, 'serve-vault');
1280
+ fs.mkdirSync(serveVault, { recursive: true });
1281
+ const serveInstance = createServer({ vault: serveVault, token: 'smoke-token', registryDir: REGISTRY_DIR });
1282
+ await new Promise((resolve) => serveInstance.listen(0, '127.0.0.1', resolve));
1283
+ const servePort = serveInstance.address().port;
1284
+ try {
1285
+ const healthRes = await fetch(`http://127.0.0.1:${servePort}/health`);
1286
+ const healthData = await healthRes.json();
1287
+ check('ssss serve health endpoint returns 200 with ok and version',
1288
+ healthRes.status === 200 && healthData.ok === true && typeof healthData.version === 'string',
1289
+ JSON.stringify(healthData));
1290
+
1291
+ const serveOpRes = await fetch(`http://127.0.0.1:${servePort}/v1/operations`, {
1292
+ method: 'POST',
1293
+ headers: { 'content-type': 'application/json', authorization: 'Bearer smoke-token' },
1294
+ body: JSON.stringify({
1295
+ type: 'operation',
1296
+ workspace_id: 'ws-smoke',
1297
+ path: 'rules/smoke.md',
1298
+ idempotency_key: 'smoke-serve-1',
1299
+ actor: { role: 'admin', agent_id: 'smoke-agent' },
1300
+ content: '---\ntype: rule\nname: "Smoke Rule"\ntitle: "Smoke Rule"\ndescription: "Smoke test."\ntimestamp: 2026-07-02T00:00:00Z\n---\nRule.\n',
1301
+ }),
1302
+ });
1303
+ const serveOpData = await serveOpRes.json();
1304
+ check('ssss serve commits valid operation envelope with 201 Created',
1305
+ serveOpRes.status === 201 && serveOpData.success === true,
1306
+ JSON.stringify(serveOpData));
1307
+ } finally {
1308
+ await new Promise((resolve) => serveInstance.close(resolve));
1309
+ }
1310
+
1311
+ // ssss bridge smoke
1312
+ const bridgeVault = path.join(tmp, 'bridge-vault');
1313
+ fs.mkdirSync(bridgeVault, { recursive: true });
1314
+ const bridgeInput = JSON.stringify({
1315
+ type: 'operation',
1316
+ workspace_id: 'ws-smoke',
1317
+ path: 'rules/bridge-rule.md',
1318
+ idempotency_key: 'smoke-bridge-1',
1319
+ actor: { role: 'admin', agent_id: 'smoke-agent' },
1320
+ content: '---\ntype: rule\nname: "Bridge Rule"\ntitle: "Bridge Rule"\ndescription: "Smoke test."\ntimestamp: 2026-07-02T00:00:00Z\n---\nRule.\n',
1321
+ });
1322
+ const bridgeOut = JSON.parse(execFileSync('node', [
1323
+ path.join(__dirname, 'ssss.mjs'),
1324
+ 'bridge',
1325
+ '--vault', bridgeVault,
1326
+ ], { input: bridgeInput, encoding: 'utf8' }));
1327
+ check('ssss bridge executes operation over stdin/stdout JSON',
1328
+ bridgeOut.success === true && bridgeOut.path === 'rules/bridge-rule.md',
1329
+ JSON.stringify(bridgeOut));
927
1330
  } finally {
928
1331
  fs.rmSync(tmp, { recursive: true, force: true });
929
1332
  }
@@ -937,6 +1340,73 @@ async function runCliSmokeConformance() {
937
1340
  return pass === checks.length;
938
1341
  }
939
1342
 
1343
+ /**
1344
+ * Cross-language wire protocol and bridge conformance (spec §6.8).
1345
+ * Proves that:
1346
+ * 1. The reference HTTP server (ssss serve) passes 100% of canonical fixtures via POST /v1/operations.
1347
+ * 2. The subprocess bridge (ssss bridge) passes 100% of canonical fixtures via stdin/stdout JSON.
1348
+ */
1349
+ async function runWireAndBridgeConformance(doc) {
1350
+ const checks = [];
1351
+ const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
1352
+
1353
+ // 1. HTTP Wire Contract Conformance (ssss serve, spec §6.8)
1354
+ const serveVault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-wire-conformance-'));
1355
+ try {
1356
+ const server = createServer({ vault: serveVault, token: 'wire-test-token', registryDir: REGISTRY_DIR });
1357
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
1358
+ const port = server.address().port;
1359
+ try {
1360
+ const endpoint = `http://127.0.0.1:${port}/v1/operations`;
1361
+ let pass = 0;
1362
+ for (const f of doc.fixtures) {
1363
+ const headers = { 'content-type': 'application/json', authorization: 'Bearer wire-test-token' };
1364
+ const res = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(f.request) });
1365
+ const body = await res.json().catch(() => ({}));
1366
+ const want = f.expected_response || {};
1367
+ const okSuccess = want.success == null || body.success === want.success;
1368
+ const okStatus = f.expected_http_status == null || res.status === f.expected_http_status;
1369
+ if (okSuccess && okStatus) pass++;
1370
+ }
1371
+ check(`reference HTTP server passes canonical fixtures (${pass}/${doc.fixtures.length}, spec §6.8)`,
1372
+ pass === doc.fixtures.length,
1373
+ `${pass}/${doc.fixtures.length} passed`);
1374
+ } finally {
1375
+ await new Promise((resolve) => server.close(resolve));
1376
+ }
1377
+ } finally {
1378
+ fs.rmSync(serveVault, { recursive: true, force: true });
1379
+ }
1380
+
1381
+ // 2. Subprocess Stdin-JSON Bridge Conformance (ssss bridge, spec §6.8)
1382
+ const bridgeVault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-bridge-conformance-'));
1383
+ try {
1384
+ let pass = 0;
1385
+ for (const f of doc.fixtures) {
1386
+ const stdout = execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'bridge', '--vault', bridgeVault], {
1387
+ input: JSON.stringify(f.request),
1388
+ encoding: 'utf8',
1389
+ });
1390
+ const body = JSON.parse(stdout.trim());
1391
+ const want = f.expected_response || {};
1392
+ const okSuccess = want.success == null || body.success === want.success;
1393
+ if (okSuccess) pass++;
1394
+ }
1395
+ check(`subprocess bridge passes canonical fixtures (${pass}/${doc.fixtures.length}, spec §6.8)`,
1396
+ pass === doc.fixtures.length,
1397
+ `${pass}/${doc.fixtures.length} passed`);
1398
+ } finally {
1399
+ fs.rmSync(bridgeVault, { recursive: true, force: true });
1400
+ }
1401
+
1402
+ let passCount = 0;
1403
+ for (const c of checks) {
1404
+ if (c.cond) { passCount++; console.log(` ✅ ${c.name}`); }
1405
+ else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
1406
+ }
1407
+ return passCount === checks.length;
1408
+ }
1409
+
940
1410
  /**
941
1411
  * Runtime conformance (spec §11.8). Proves that a conformant daemon can treat
942
1412
  * workflow frontmatter as the canonical schedule source, then derive trigger,
@@ -1155,9 +1625,11 @@ async function main() {
1155
1625
  const bundleOk = await runBundleConformance();
1156
1626
  console.log('\nRunning CLI smoke conformance (scripts/ssss.mjs) ...');
1157
1627
  const cliSmokeOk = await runCliSmokeConformance();
1628
+ console.log('\nRunning cross-language wire & bridge conformance (spec §6.8) ...');
1629
+ const wireBridgeOk = await runWireAndBridgeConformance(doc);
1158
1630
  process.exit(
1159
1631
  engineOk && runtimeOk && operationRegressionOk && assetPointerOk && extensionRegistryOk &&
1160
- semanticOk && kernel09Ok && bundleOk && cliSmokeOk ? 0 : 1
1632
+ semanticOk && kernel09Ok && bundleOk && cliSmokeOk && wireBridgeOk ? 0 : 1
1161
1633
  );
1162
1634
  } else {
1163
1635
  console.log('\nℹ No --endpoint/--engine given; ran structural + registry validation only.');
package/scripts/ssss.mjs CHANGED
@@ -34,8 +34,11 @@ Vault → bundle → tenant lifecycle:
34
34
  inspect <bundle> Human-readable summary of a bundle.
35
35
  provision <bundle> Plan an install: params + link integrity → envelopes (§17).
36
36
  import <bundle> Replay a bundle/plan into a target vault (idempotent).
37
+ upgrade <bundle> Move an installed bundle to a new version via structural patches (§17.4).
37
38
 
38
- Authoring & conformance:
39
+ Authoring, wire protocol & conformance:
40
+ serve [options] Start a reference HTTP server for operations (spec §6.8).
41
+ bridge [options] Execute operations over stdin/stdout JSON (spec §6.8).
39
42
  primitive <verb> Create, validate, inspect, migrate, or emit types.
40
43
  registry <verb> Compose, lock, or verify an effective registry.
41
44
  adapter conformance Run machine-readable adapter contract suites.
@@ -65,11 +68,14 @@ const MODULES = {
65
68
  inspect: './cmd-inspect.mjs',
66
69
  provision: './cmd-provision.mjs',
67
70
  import: './cmd-import.mjs',
71
+ upgrade: './cmd-upgrade.mjs',
68
72
  semantic: './cmd-semantic.mjs',
69
73
  primitive: './cmd-primitive.mjs',
70
74
  registry: './cmd-registry.mjs',
71
75
  adapter: './cmd-adapter.mjs',
72
76
  migrate: './cmd-migrate.mjs',
77
+ serve: './cmd-serve.mjs',
78
+ bridge: './cmd-bridge.mjs',
73
79
  help: './cmd-help.mjs',
74
80
  };
75
81
 
@@ -3,8 +3,8 @@ type: skill
3
3
  title: SSSS Protocol Reference
4
4
  name: ssss
5
5
  description: Validate or change SSSS documents, operations, registries, workflows and portable host adapters.
6
- timestamp: 2026-11-02T00:00:00Z
7
- version: 0.13.0
6
+ timestamp: 2026-11-02T19:00:00Z
7
+ version: 0.14.1
8
8
  repo_scoped: false
9
9
  ---
10
10
 
@@ -1,4 +1,4 @@
1
- # SSSS portable core — 0.13.0
1
+ # SSSS portable core — 0.14.1
2
2
 
3
3
  Verified against the SSSS owner package, registry and spec on 2026-11-02. Establish ground truth from the installed validator, composed registry plus lock, and actual host mutation path. Schema support does not prove a configured runner or provider integration.
4
4
 
@@ -10,9 +10,9 @@ Read only the relevant sections of the [normative spec](references/ssss-spec.md)
10
10
  Workflow triggers and standalone trigger documents are distinct canonical primitives. A standalone trigger targets a workflow template with parameters; workflows may own inline trigger definitions. Inspect the current registry/spec for required fields and enums. Use the installed runtime adapter to derive deterministic trigger/task/run operations; duplicate ticks replay through idempotency. A cron/interval/webhook/watcher is a wake-up mechanism, not a second scheduling database. Record observed task/run outcomes separately from declaration, enabled state and scheduler handled slots. Explicitly report unsupported host trigger types and missing execution capability.
11
11
 
12
12
  ## Bundles and adapters
13
- Preserve portability classes: structural definitions, tenant-private data and resource-bound requirements. Validate the complete import plan and references before commit, including forward references; missing references reject the import rather than weakening constraints. Check actual host authorization and adapter conformance; local fixtures do not prove provider execution. Use source-owned CLI help for flags and inspect structured validity, not process exit alone.
13
+ Preserve portability classes: structural definitions, tenant-private data and resource-bound requirements. Validate the complete import plan and references before commit, including forward references and `x_bind` provisioning bindings (`--bind` / `--remap-ids`); missing references or unbound requirements reject the import rather than weakening constraints. Check actual host authorization (`@<glob>` path grants, core aliases `role` ↔ `security_role` and `thread` ↔ `conversation`), optimistic CAS (`expected_before_hash`), schema upgrades (`ssss upgrade`), and wire/MCP transport (`ssss serve`, `ssss bridge`); local fixtures do not prove provider execution. Use source-owned CLI help for flags and inspect structured validity, not process exit alone.
14
14
 
15
15
  ## Verification
16
16
  Run relevant schema/registry/lock and operation conformance, with valid and invalid examples. Cover duplicate/conflicting idempotency (including retries answered from the event log), unsafe paths, unauthorized/expired actors, leases, append semantics, rollback and commit-intent crash recovery where changed. Use `ssss --help`, then operation-specific help; commands differ by installed version. Report exact package version, registry/lock identity and actual results. Keep secrets and private runtime data out of skill content, bundles and logs.
17
17
 
18
- Normative sources are bundled below from the owner release tag v0.13.0 (`docs/ssss-spec.md` and `registry/core.json` at that tag). Retain authored local wrappers/references; this managed core is the portable update boundary.
18
+ Normative sources are bundled below from the owner release tag v0.14.1 (`docs/ssss-spec.md` and `registry/core.json` at that tag). Retain authored local wrappers/references; this managed core is the portable update boundary.
@@ -87,6 +87,7 @@
87
87
  "portability": "structural",
88
88
  "required_fields": ["type", "name", "permissions"],
89
89
  "optional_fields": ["description"],
90
+ "aliases": ["security_role"],
90
91
  "notes": "A named set of capabilities used for role-based access control (RBAC). Canonical form of security_role.",
91
92
  "spec_ref": "§5.4 role"
92
93
  },
@@ -97,6 +98,8 @@
97
98
  "portability": "structural",
98
99
  "required_fields": ["type", "name", "permissions"],
99
100
  "optional_fields": ["description"],
101
+ "aliases": ["role"],
102
+ "notes": "A named set of capabilities used for role-based access control (RBAC). Legacy alias of role.",
100
103
  "spec_ref": "§5.4 security_role"
101
104
  },
102
105
  "task": {
@@ -149,6 +152,8 @@
149
152
  "append_only": false,
150
153
  "portability": "structural",
151
154
  "required_fields": ["type", "model_id", "provider"],
155
+ "deprecated": true,
156
+ "deprecation_notice": "Inert catalog primitive. Modern hosts configure models through provider runtime settings.",
152
157
  "spec_ref": "§5.4 model"
153
158
  },
154
159
  "thread": {
@@ -158,6 +163,7 @@
158
163
  "portability": "tenant_private",
159
164
  "required_fields": ["type", "thread_id"],
160
165
  "optional_fields": ["workspace_id", "user_id", "status", "turn_count", "created_at"],
166
+ "aliases": ["conversation"],
161
167
  "notes": "An append-only multi-turn chat transcript. Canonical form of conversation.",
162
168
  "spec_ref": "§5.4 thread"
163
169
  },
@@ -167,6 +173,8 @@
167
173
  "append_only": true,
168
174
  "portability": "tenant_private",
169
175
  "required_fields": ["type", "thread_id"],
176
+ "aliases": ["thread"],
177
+ "notes": "An append-only multi-turn chat transcript. Legacy alias of thread.",
170
178
  "spec_ref": "§5.4 conversation"
171
179
  },
172
180
  "run": {
@@ -186,6 +194,8 @@
186
194
  "portability": "tenant_private",
187
195
  "required_fields": ["type", "conflict_id", "status", "new_slug", "existing_slug", "detected_at"],
188
196
  "enums": { "status": ["pending", "resolved"] },
197
+ "deprecated": true,
198
+ "deprecation_notice": "Inert meta primitive. Memory collision handling is managed by host memory kernels.",
189
199
  "spec_ref": "§5.4 conflict"
190
200
  },
191
201
  "page": {
@@ -194,6 +204,8 @@
194
204
  "append_only": false,
195
205
  "portability": "structural",
196
206
  "required_fields": ["type", "slug", "name", "sandbox_entry"],
207
+ "deprecated": true,
208
+ "deprecation_notice": "Inert sandbox primitive with iframe assumptions. Superseded by asset and modern UI primitives.",
197
209
  "spec_ref": "§5.4 page"
198
210
  },
199
211
  "migration": {
@@ -210,6 +222,8 @@
210
222
  "append_only": false,
211
223
  "portability": "structural",
212
224
  "required_fields": ["type", "release_id", "version", "schema_version", "summary", "released_at"],
225
+ "deprecated": true,
226
+ "deprecation_notice": "Inert meta primitive. Release tracking is managed by bundle and package manifests.",
213
227
  "spec_ref": "§5.4 release"
214
228
  },
215
229
  "primitive": {