@gregiteen/ssss-cli 0.8.0 → 0.9.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +55 -4
  2. package/README.md +42 -84
  3. package/VERSION +1 -1
  4. package/conformance/README.md +1 -1
  5. package/conformance/fixtures.json +4 -48
  6. package/conformance/reference-bundle.ucw.json +22 -21
  7. package/docs/help/bundle.md +2 -2
  8. package/docs/help/overview.md +9 -6
  9. package/docs/help/semantic.md +5 -4
  10. package/docs/ssss-spec.md +42 -86
  11. package/package.json +22 -3
  12. package/registry/core.json +22 -29
  13. package/scripts/build-reference-bundle.mjs +0 -20
  14. package/scripts/cmd-adapter.mjs +29 -0
  15. package/scripts/cmd-import.mjs +1 -1
  16. package/scripts/cmd-migrate.mjs +26 -0
  17. package/scripts/cmd-new.mjs +5 -5
  18. package/scripts/cmd-primitive.mjs +48 -0
  19. package/scripts/cmd-registry.mjs +25 -0
  20. package/scripts/cmd-semantic.mjs +6 -8
  21. package/scripts/conformance-09.mjs +331 -0
  22. package/scripts/conformance.mjs +153 -186
  23. package/scripts/generate-sbom.mjs +96 -0
  24. package/scripts/phase9-cross-host-corpus.mjs +223 -0
  25. package/scripts/ssss.mjs +8 -2
  26. package/skills/ssss/SKILL.md +32 -208
  27. package/skills/ssss/evals/evals.json +3 -3
  28. package/skills/ssss/references/contract-reference.md +7 -8
  29. package/skills/ssss/scripts/validate-schema.mjs +1 -44
  30. package/src/authorization.mjs +62 -0
  31. package/src/bundle.mjs +41 -5
  32. package/src/engine.mjs +101 -503
  33. package/src/events.mjs +155 -0
  34. package/src/guard.mjs +32 -0
  35. package/src/http.mjs +26 -0
  36. package/src/idempotency.mjs +55 -0
  37. package/src/kernel.mjs +260 -0
  38. package/src/leases.mjs +173 -0
  39. package/src/primitive.mjs +227 -0
  40. package/src/projections.mjs +133 -0
  41. package/src/registry.mjs +190 -39
  42. package/src/semantic.mjs +124 -316
  43. package/src/ui.mjs +100 -0
  44. package/src/validator.mjs +87 -0
  45. package/src/vfs.mjs +203 -0
  46. package/types/kernel.d.ts +48 -0
  47. package/docs/help/localization.md +0 -43
  48. package/scripts/cmd-localize.mjs +0 -44
@@ -0,0 +1,62 @@
1
+ /** Verified identity and fail-closed capability authorization contracts. */
2
+ const PRINCIPAL_KINDS = new Set(['human', 'agent', 'service', 'system']);
3
+ const ACTIONS = new Set(['create', 'replace', 'patch', 'append', 'event', 'delete', 'define', 'migrate']);
4
+ const ASSURANCE = { unverified: 0, verified: 1, elevated: 2, hardware: 3 };
5
+
6
+ export function validateVerifiedPrincipal(principal) {
7
+ const errors = [];
8
+ if (!principal || typeof principal !== 'object') errors.push('Verified principal is required.');
9
+ else {
10
+ if (typeof principal.id !== 'string' || !principal.id) errors.push('Principal id is required.');
11
+ if (!PRINCIPAL_KINDS.has(principal.kind)) errors.push(`Principal kind must be one of: ${[...PRINCIPAL_KINDS].join(', ')}.`);
12
+ if (!Array.isArray(principal.workspaceIds)) errors.push('Principal workspaceIds must be an array.');
13
+ if (!principal.authentication || typeof principal.authentication.provider !== 'string') {
14
+ errors.push('Authentication provenance is required.');
15
+ }
16
+ }
17
+ return { valid: errors.length === 0, errors };
18
+ }
19
+
20
+ export function requiredCapabilities(definition, action) {
21
+ if (!ACTIONS.has(action)) throw new Error(`Unknown authorization action '${action}'.`);
22
+ const declared = definition?.capabilities?.[action];
23
+ if (Array.isArray(declared) && declared.length) return declared;
24
+ const primitive = definition?.qualified_type || definition?.type || 'unknown';
25
+ return [`${primitive}:${action}`];
26
+ }
27
+
28
+ export function createCapabilityAuthorizer(options = {}) {
29
+ const policyFloors = options.policyFloors || {};
30
+ return async function authorize({ principal, workspaceId, definition, action, context = {} }) {
31
+ const principalValidation = validateVerifiedPrincipal(principal);
32
+ if (!principalValidation.valid) return { allowed: false, reason: principalValidation.errors.join(' ') };
33
+ if (principal.kind !== 'system' && !principal.workspaceIds.includes(workspaceId)) {
34
+ return { allowed: false, reason: `Principal '${principal.id}' is not scoped to workspace '${workspaceId}'.` };
35
+ }
36
+ const floorKey = definition?.qualified_type || definition?.primitive_id || definition?.type;
37
+ const required = new Set([
38
+ ...requiredCapabilities(definition, action),
39
+ ...(policyFloors[floorKey]?.[action] || []),
40
+ ...(context.policyFloor || []),
41
+ ]);
42
+ const granted = new Set(principal.capabilities || []);
43
+ if (principal.kind === 'system' && options.systemBypass !== false) return { allowed: true, required: [...required] };
44
+ const missing = [...required].filter((capability) =>
45
+ !granted.has(capability) && !granted.has('*:*') && !granted.has(`${capability.split(':')[0]}:*`));
46
+ if (missing.length) return { allowed: false, required: [...required], missing, reason: `Missing capabilities: ${missing.join(', ')}.` };
47
+ if (context.requiresHumanConfirmation && principal.kind !== 'human') {
48
+ return { allowed: false, required: [...required], reason: 'A verified human confirmation is required.' };
49
+ }
50
+ if (context.requiresHumanConfirmation && context.humanConfirmation !== true) {
51
+ return { allowed: false, required: [...required], reason: 'Explicit human confirmation is required.' };
52
+ }
53
+ if (context.requiredAssurance) {
54
+ const actual = ASSURANCE[principal.authentication.assurance] ?? -1;
55
+ const requiredAssurance = ASSURANCE[context.requiredAssurance] ?? Number.POSITIVE_INFINITY;
56
+ if (actual < requiredAssurance) {
57
+ return { allowed: false, required: [...required], reason: `Authentication step-up to '${context.requiredAssurance}' is required.` };
58
+ }
59
+ }
60
+ return { allowed: true, required: [...required] };
61
+ };
62
+ }
package/src/bundle.mjs CHANGED
@@ -197,6 +197,17 @@ export function exportBundle(vaultRoot, opts = {}) {
197
197
  }
198
198
 
199
199
  const files = canonicalFiles(kept);
200
+ const hash = contentHash(files);
201
+ const primitiveDeps = Object.keys(inventory).sort();
202
+ const migrationDeps = files
203
+ .map((file) => {
204
+ try {
205
+ const { data } = parseDocument(file.content);
206
+ return data.type === 'migration' ? (data.migration_id || data.slug || file.path) : null;
207
+ } catch { return null; }
208
+ })
209
+ .filter(Boolean)
210
+ .sort();
200
211
  const manifest = {
201
212
  name, description, version,
202
213
  exported_at: exportedAt,
@@ -204,10 +215,16 @@ export function exportBundle(vaultRoot, opts = {}) {
204
215
  required_extensions: requiredExtensions,
205
216
  export_profile: profile,
206
217
  primitive_inventory: inventory,
218
+ dependencies: {
219
+ primitives: primitiveDeps,
220
+ extensions: [...requiredExtensions].sort(),
221
+ migrations: migrationDeps,
222
+ integrity: { content_hash: hash, ssss_core_version: core.spec_version },
223
+ },
207
224
  provisioning: manifestProvisioning,
208
225
  parameters: manifestParameters,
209
226
  file_count: files.length,
210
- provenance: { content_hash: contentHash(files), exporter },
227
+ provenance: { content_hash: hash, exporter },
211
228
  };
212
229
  // source_workspace_id is operator-identifying — only carried in a backup (§16.2).
213
230
  if (profile === 'backup' && sourceWorkspaceId) manifest.source_workspace_id = sourceWorkspaceId;
@@ -240,6 +257,25 @@ export function validateBundle(bundle, opts = {}) {
240
257
  if (!m.primitive_inventory || typeof m.primitive_inventory !== 'object' || Array.isArray(m.primitive_inventory)) {
241
258
  errors.push('manifest.primitive_inventory must be an object');
242
259
  }
260
+ if (m.dependencies !== undefined) {
261
+ if (!m.dependencies || typeof m.dependencies !== 'object' || Array.isArray(m.dependencies)) {
262
+ errors.push('manifest.dependencies must be an object');
263
+ } else {
264
+ for (const key of ['primitives', 'extensions', 'migrations']) {
265
+ if (!Array.isArray(m.dependencies[key])) errors.push(`manifest.dependencies.${key} must be an array`);
266
+ }
267
+ if (!m.dependencies.integrity || typeof m.dependencies.integrity !== 'object' || Array.isArray(m.dependencies.integrity)) {
268
+ errors.push('manifest.dependencies.integrity must be an object');
269
+ } else if (typeof m.dependencies.integrity.content_hash !== 'string' || !m.dependencies.integrity.content_hash.startsWith('sha256:')) {
270
+ errors.push('manifest.dependencies.integrity.content_hash must be a sha256 digest');
271
+ } else if (
272
+ m.provenance?.content_hash
273
+ && m.dependencies.integrity.content_hash !== m.provenance.content_hash
274
+ ) {
275
+ errors.push('manifest.dependencies.integrity.content_hash must match provenance.content_hash');
276
+ }
277
+ }
278
+ }
243
279
  for (const ext of requiredExtensions) {
244
280
  if (typeof ext !== 'string' || !ext) {
245
281
  errors.push('required extension ids must be non-empty strings');
@@ -429,7 +465,7 @@ export function provisionBundle(bundle, opts = {}) {
429
465
  * import(plan, target) → replay (§17.1). Runs each envelope through the engine
430
466
  * (§6.3). Idempotent: each envelope's idempotency_key makes a re-run a no-op.
431
467
  */
432
- export function importBundle(plan, vaultRoot, engine) {
468
+ export async function importBundle(plan, vaultRoot, engine) {
433
469
  const dryRunFlags = new Set(plan.map((envelope) => !!envelope.dry_run));
434
470
  if (dryRunFlags.size > 1) {
435
471
  return {
@@ -457,7 +493,7 @@ export function importBundle(plan, vaultRoot, engine) {
457
493
  // Two-phase import: validate the complete plan with in-plan references first.
458
494
  // A single invalid document prevents every commit, eliminating partial bundle
459
495
  // installation and allowing dry-run to validate cross-file constraints.
460
- const preflight = plan.map((envelope) => {
496
+ const preflight = await Promise.all(plan.map((envelope) => {
461
497
  const preflightKey = 'preflight-' + crypto.createHash('sha256')
462
498
  .update(`${envelope.workspace_id}:${envelope.idempotency_key}:${envelope.path}`)
463
499
  .digest('hex')
@@ -467,7 +503,7 @@ export function importBundle(plan, vaultRoot, engine) {
467
503
  vaultRoot,
468
504
  operationOptions
469
505
  );
470
- });
506
+ }));
471
507
  if (!preflight.every((result) => result.success)) {
472
508
  return { ok: false, committed: 0, wouldCommit: 0, results: preflight };
473
509
  }
@@ -480,7 +516,7 @@ export function importBundle(plan, vaultRoot, engine) {
480
516
  };
481
517
  }
482
518
  const results = [];
483
- for (const env of plan) results.push(engine.processOperation(env, vaultRoot, operationOptions));
519
+ for (const env of plan) results.push(await engine.processOperation(env, vaultRoot, operationOptions));
484
520
  const committed = results.filter((r) => r.success && !r.replay && !r.dry_run).length;
485
521
  return { ok: results.every((r) => r.success), committed, wouldCommit: 0, results };
486
522
  }