@gregiteen/ssss-cli 0.7.1 → 0.9.0

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 +42 -4
  2. package/README.md +19 -4
  3. package/VERSION +1 -1
  4. package/conformance/README.md +11 -7
  5. package/conformance/fixtures.json +24 -2
  6. package/conformance/reference-bundle.ucw.json +2 -2
  7. package/docs/help/bundle.md +6 -2
  8. package/docs/help/import.md +2 -0
  9. package/docs/help/overview.md +8 -4
  10. package/docs/help/semantic.md +38 -0
  11. package/docs/ssss-spec.md +67 -7
  12. package/package.json +27 -3
  13. package/registry/core.json +36 -3
  14. package/scripts/audit-registry-field-usage.mjs +10 -1
  15. package/scripts/build-reference-bundle.mjs +4 -1
  16. package/scripts/cmd-adapter.mjs +29 -0
  17. package/scripts/cmd-export.mjs +3 -3
  18. package/scripts/cmd-import.mjs +1 -1
  19. package/scripts/cmd-migrate.mjs +26 -0
  20. package/scripts/cmd-new.mjs +5 -5
  21. package/scripts/cmd-primitive.mjs +48 -0
  22. package/scripts/cmd-registry.mjs +25 -0
  23. package/scripts/cmd-semantic.mjs +86 -0
  24. package/scripts/conformance-09.mjs +199 -0
  25. package/scripts/conformance.mjs +320 -28
  26. package/scripts/ssss.mjs +10 -0
  27. package/skills/ssss/SKILL.md +221 -0
  28. package/skills/ssss/evals/evals.json +37 -0
  29. package/skills/ssss/references/contract-reference.md +95 -0
  30. package/skills/ssss/scripts/validate-schema.mjs +215 -0
  31. package/skills/ssss/subagents/audit-ssss-contract.md +22 -0
  32. package/src/authorization.mjs +61 -0
  33. package/src/bundle.mjs +171 -35
  34. package/src/engine.mjs +101 -429
  35. package/src/events.mjs +90 -0
  36. package/src/guard.mjs +32 -0
  37. package/src/http.mjs +26 -0
  38. package/src/idempotency.mjs +55 -0
  39. package/src/kernel.mjs +260 -0
  40. package/src/leases.mjs +125 -0
  41. package/src/primitive.mjs +227 -0
  42. package/src/projections.mjs +62 -0
  43. package/src/registry.mjs +408 -17
  44. package/src/semantic.mjs +210 -0
  45. package/src/ui.mjs +100 -0
  46. package/src/validator.mjs +87 -0
  47. package/src/vfs.mjs +203 -0
  48. package/types/kernel.d.ts +48 -0
package/src/ui.mjs ADDED
@@ -0,0 +1,100 @@
1
+ /** Typed, capability-bound generative UI projection contract. */
2
+ import crypto from 'node:crypto';
3
+
4
+ const SAFE_ID = /^[a-z][a-z0-9_.-]{0,127}$/;
5
+ const LAYOUTS = new Set(['form', 'table', 'detail', 'detail-with-actions', 'dashboard', 'wizard']);
6
+
7
+ function isRecord(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
8
+
9
+ export class UiRegistry {
10
+ constructor() { this.components = new Map(); this.actions = new Map(); }
11
+ registerComponent(definition) {
12
+ if (!SAFE_ID.test(definition?.id || '') || typeof definition.validateProps !== 'function') throw new Error('Component id and validateProps are required.');
13
+ if (this.components.has(definition.id)) throw new Error(`Duplicate UI component '${definition.id}'.`);
14
+ this.components.set(definition.id, { ...definition });
15
+ }
16
+ registerAction(definition) {
17
+ if (!SAFE_ID.test(definition?.id || '') || !definition.capability || !definition.command) throw new Error('Action id, capability, and command are required.');
18
+ if (this.actions.has(definition.id)) throw new Error(`Duplicate UI action '${definition.id}'.`);
19
+ this.actions.set(definition.id, { ...definition });
20
+ }
21
+ }
22
+
23
+ export function validateUiManifest(manifest, { registry, visibleFields = [], grantedCapabilities = [], maxComponents = 100 } = {}) {
24
+ const errors = [];
25
+ if (!isRecord(manifest)) return { valid: false, errors: [{ field: '(root)', issue: 'UI manifest must be an object.' }] };
26
+ if (manifest.type !== 'ssss:ui_projection') errors.push({ field: 'type', issue: 'Expected ssss:ui_projection.' });
27
+ if (!LAYOUTS.has(manifest.layout)) errors.push({ field: 'layout', issue: `Use one of: ${[...LAYOUTS].join(', ')}.` });
28
+ if (!Array.isArray(manifest.components)) errors.push({ field: 'components', issue: 'Components must be an array.' });
29
+ else if (manifest.components.length > maxComponents) errors.push({ field: 'components', issue: `At most ${maxComponents} components are allowed.` });
30
+ const allowedFields = new Set(visibleFields);
31
+ const capabilities = new Set(grantedCapabilities);
32
+ for (const [index, component] of (manifest.components || []).entries()) {
33
+ const prefix = `components[${index}]`;
34
+ if (!isRecord(component)) { errors.push({ field: prefix, issue: 'Component must be an object.' }); continue; }
35
+ const definition = registry?.components.get(component.component);
36
+ if (!definition) { errors.push({ field: `${prefix}.component`, issue: `Unregistered component '${component.component}'.` }); continue; }
37
+ if (component.bind && !allowedFields.has(component.bind)) errors.push({ field: `${prefix}.bind`, issue: `Field '${component.bind}' is hidden or unknown.` });
38
+ if (!isRecord(component.props) || typeof component.props.label !== 'string' || !component.props.label.trim()) {
39
+ errors.push({ field: `${prefix}.props.label`, issue: 'An accessible label is required.' });
40
+ }
41
+ if (component.action) {
42
+ const action = registry.actions.get(component.action);
43
+ if (!action) errors.push({ field: `${prefix}.action`, issue: `Unregistered action '${component.action}'.` });
44
+ else if (!capabilities.has(action.capability) && !capabilities.has('*:*')) errors.push({ field: `${prefix}.action`, issue: `Missing capability '${action.capability}'.` });
45
+ }
46
+ const propIssues = definition.validateProps(component.props || {});
47
+ for (const issue of propIssues || []) errors.push({ field: `${prefix}.props.${issue.field || '(root)'}`, issue: issue.issue || String(issue) });
48
+ }
49
+ return { valid: errors.length === 0, errors };
50
+ }
51
+
52
+ export function createDeterministicUi(definition, options = {}) {
53
+ const fields = (options.visibleFields || definition.fields || []).map((field) => typeof field === 'string' ? { id: field, name: field, kind: 'string' } : field);
54
+ return {
55
+ type: 'ssss:ui_projection',
56
+ projection_id: `ui_${crypto.createHash('sha256').update(`${definition.primitive_id || definition.qualified_type}\0${options.mode || 'form'}`).digest('hex').slice(0, 16)}`,
57
+ primitive: definition.primitive_id || definition.qualified_type,
58
+ layout: options.mode || 'form',
59
+ title: definition.name || definition.qualified_type,
60
+ components: fields.map((field) => ({
61
+ component: field.kind === 'enum' ? 'select-field' : field.kind === 'boolean' ? 'boolean-field' : 'data-field',
62
+ bind: field.id,
63
+ props: { label: field.name || field.id, required: !!field.required, values: field.values || undefined },
64
+ })),
65
+ };
66
+ }
67
+
68
+ export async function planUi({ definition, data, principal, language, planner, registry, visibleFields, grantedCapabilities }) {
69
+ const fallback = createDeterministicUi(definition, { visibleFields });
70
+ if (typeof planner !== 'function') return { manifest: fallback, generated: false, fallback_reason: 'planner unavailable' };
71
+ const safeData = Object.fromEntries(Object.entries(data || {}).filter(([key]) => visibleFields.includes(key)));
72
+ let candidate;
73
+ try {
74
+ candidate = await planner({
75
+ instruction: 'Compose a UI manifest using only registered components, visible fields, and actions. Do not emit executable code.',
76
+ primitive: {
77
+ primitive_id: definition.primitive_id,
78
+ qualified_type: definition.qualified_type,
79
+ name: definition.name,
80
+ fields: (definition.fields || []).filter((field) => visibleFields.includes(field.id)),
81
+ },
82
+ data: safeData,
83
+ principal: { id: principal.id, kind: principal.kind },
84
+ language,
85
+ components: [...registry.components.keys()],
86
+ actions: [...registry.actions.values()].filter((action) => grantedCapabilities.includes(action.capability)).map((action) => action.id),
87
+ });
88
+ } catch (error) {
89
+ return { manifest: fallback, generated: false, fallback_reason: error.message };
90
+ }
91
+ const validation = validateUiManifest(candidate, { registry, visibleFields, grantedCapabilities });
92
+ if (!validation.valid) return { manifest: fallback, generated: false, fallback_reason: 'generated manifest rejected', errors: validation.errors };
93
+ return { manifest: candidate, generated: true };
94
+ }
95
+
96
+ export function actionToEnvelope(manifestComponent, registry, input = {}) {
97
+ const action = registry.actions.get(manifestComponent.action);
98
+ if (!action) throw new Error(`Unknown UI action '${manifestComponent.action}'.`);
99
+ return typeof action.command === 'function' ? action.command(input) : structuredClone(action.command);
100
+ }
@@ -0,0 +1,87 @@
1
+ /** Canonical registry-driven validation shared by every SSSS host. */
2
+ import { parseDocument } from './frontmatter.mjs';
3
+ import {
4
+ composeRegistries,
5
+ loadRegistries,
6
+ resolvePrimitiveDefinition,
7
+ validateDataConstraints,
8
+ validateReferenceConstraints,
9
+ } from './registry.mjs';
10
+ import { validatePrimitiveDefinition } from './primitive.mjs';
11
+
12
+ function registryFor(options = {}) {
13
+ if (options.registrySet) return options.registrySet;
14
+ const base = loadRegistries(options.registryDir);
15
+ if (!options.extensions?.length) return base;
16
+ return composeRegistries({ core: base.core, extensions: options.extensions });
17
+ }
18
+
19
+ function result(declaredType, definition, issues, warnings = []) {
20
+ return {
21
+ valid: issues.length === 0,
22
+ type: definition?.qualified_type || declaredType || 'unknown',
23
+ declared_type: declaredType || null,
24
+ registry: definition?.registry || null,
25
+ primitive_version: definition?.primitive_version || 1,
26
+ errors: issues.map((entry) => `${entry.field}: ${entry.issue}`),
27
+ warnings,
28
+ field_errors: issues,
29
+ };
30
+ }
31
+
32
+ export function validateData(data, options = {}) {
33
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
34
+ return result(null, null, [{ field: 'frontmatter', issue: 'Document frontmatter must be an object.' }]);
35
+ }
36
+ const declaredType = typeof data.type === 'string' ? data.type : null;
37
+ if (!declaredType) return result(null, null, [{ field: 'type', issue: "Missing required field 'type'." }]);
38
+ const registrySet = registryFor(options);
39
+ const definition = resolvePrimitiveDefinition(registrySet, declaredType);
40
+ if (!definition) return result(declaredType, null, [{ field: 'type', issue: `Unknown SSSS primitive '${declaredType}'.` }]);
41
+ const universal = options.enforceUniversalFields === false
42
+ ? ['type']
43
+ : (registrySet.core.universal_frontmatter?.required || ['type']);
44
+ const issues = validateDataConstraints(definition, data, universal);
45
+
46
+ if (definition.qualified_type === 'ssss:primitive' || definition.type === 'primitive') {
47
+ issues.push(...validatePrimitiveDefinition(data).errors);
48
+ }
49
+
50
+ if (definition.references && options.resolveReference) {
51
+ issues.push(...validateReferenceConstraints(
52
+ definition,
53
+ data,
54
+ options.resolveReference,
55
+ registrySet.types
56
+ ));
57
+ }
58
+ if (options.validateExtension) {
59
+ const extra = options.validateExtension({ data, definition, registrySet }) || [];
60
+ issues.push(...extra);
61
+ }
62
+ return result(declaredType, definition, issues);
63
+ }
64
+
65
+ export function validateDocument(content, options = {}) {
66
+ if (typeof content !== 'string') {
67
+ return result(null, null, [{ field: 'content', issue: 'Document content must be a string.' }]);
68
+ }
69
+ try {
70
+ const parsed = parseDocument(content);
71
+ const validation = validateData(parsed.data, options);
72
+ return { ...validation, data: parsed.data, body: parsed.body };
73
+ } catch (error) {
74
+ return result(null, null, [{ field: 'frontmatter', issue: `Invalid frontmatter: ${error.message}` }]);
75
+ }
76
+ }
77
+
78
+ export function createValidator(options = {}) {
79
+ const registrySet = registryFor(options);
80
+ const fixed = { ...options, registrySet, extensions: undefined, registryDir: undefined };
81
+ return {
82
+ registry: registrySet,
83
+ validateData: (data, callOptions = {}) => validateData(data, { ...fixed, ...callOptions, registrySet }),
84
+ validateDocument: (content, callOptions = {}) => validateDocument(content, { ...fixed, ...callOptions, registrySet }),
85
+ resolve: (type) => resolvePrimitiveDefinition(registrySet, type),
86
+ };
87
+ }
package/src/vfs.mjs ADDED
@@ -0,0 +1,203 @@
1
+ /** Standard SSSS VFS adapter contract and reference implementations. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import { isSafeDocumentPath } from './registry.mjs';
6
+
7
+ export class VfsConflictError extends Error {
8
+ constructor(message) { super(message); this.name = 'VfsConflictError'; this.code = 'SSSS_VFS_CONFLICT'; }
9
+ }
10
+
11
+ export class VfsPathError extends Error {
12
+ constructor(message) { super(message); this.name = 'VfsPathError'; this.code = 'SSSS_VFS_PATH'; }
13
+ }
14
+
15
+ export function bytesHash(bytes) {
16
+ return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
17
+ }
18
+
19
+ function asBuffer(value) {
20
+ return Buffer.isBuffer(value) ? value : Buffer.from(String(value), 'utf8');
21
+ }
22
+
23
+ function checkPrecondition(current, precondition = {}) {
24
+ if (precondition.ifAbsent && current) throw new VfsConflictError('Target already exists.');
25
+ if (precondition.ifPresent && !current) throw new VfsConflictError('Target does not exist.');
26
+ if (precondition.version !== undefined && current?.version !== precondition.version) {
27
+ throw new VfsConflictError(`Version mismatch; expected '${precondition.version}', found '${current?.version || '(missing)'}'.`);
28
+ }
29
+ }
30
+
31
+ export class MemoryVfs {
32
+ #entries = new Map();
33
+
34
+ async read(vfsPath) {
35
+ if (!isSafeDocumentPath(vfsPath)) throw new VfsPathError(`Unsafe VFS path '${vfsPath}'.`);
36
+ const entry = this.#entries.get(vfsPath);
37
+ return entry ? { bytes: Buffer.from(entry.bytes), version: entry.version, hash: entry.hash } : null;
38
+ }
39
+
40
+ async stat(vfsPath) {
41
+ const entry = await this.read(vfsPath);
42
+ return entry ? { path: vfsPath, kind: 'file', size: entry.bytes.length, version: entry.version, hash: entry.hash } : null;
43
+ }
44
+
45
+ async *list(prefix = '') {
46
+ if (prefix && !isSafeDocumentPath(prefix)) throw new VfsPathError(`Unsafe VFS prefix '${prefix}'.`);
47
+ for (const key of [...this.#entries.keys()].sort()) {
48
+ if (!prefix || key === prefix || key.startsWith(`${prefix}/`)) yield await this.stat(key);
49
+ }
50
+ }
51
+
52
+ async writeAtomic(vfsPath, bytes, precondition = {}) {
53
+ if (!isSafeDocumentPath(vfsPath)) throw new VfsPathError(`Unsafe VFS path '${vfsPath}'.`);
54
+ const current = await this.read(vfsPath);
55
+ checkPrecondition(current, precondition);
56
+ const nextBytes = asBuffer(bytes);
57
+ const hash = bytesHash(nextBytes);
58
+ const version = hash;
59
+ this.#entries.set(vfsPath, { bytes: Buffer.from(nextBytes), hash, version });
60
+ return { path: vfsPath, hash, version, size: nextBytes.length };
61
+ }
62
+
63
+ async append(vfsPath, bytes, precondition = {}) {
64
+ const current = await this.read(vfsPath);
65
+ checkPrecondition(current, precondition);
66
+ const combined = Buffer.concat([current?.bytes || Buffer.alloc(0), asBuffer(bytes)]);
67
+ return this.writeAtomic(vfsPath, combined, current ? { version: current.version } : { ifAbsent: true });
68
+ }
69
+
70
+ async remove(vfsPath, precondition = {}) {
71
+ const current = await this.read(vfsPath);
72
+ checkPrecondition(current, precondition);
73
+ if (!current) return { path: vfsPath, removed: false };
74
+ this.#entries.delete(vfsPath);
75
+ return { path: vfsPath, removed: true, previous_hash: current.hash };
76
+ }
77
+ }
78
+
79
+ export class FileSystemVfs {
80
+ constructor(root) {
81
+ if (!root) throw new Error('FileSystemVfs requires a root directory.');
82
+ fs.mkdirSync(root, { recursive: true });
83
+ this.root = fs.realpathSync(root);
84
+ }
85
+
86
+ #resolve(vfsPath, { allowMissing = true } = {}) {
87
+ if (!isSafeDocumentPath(vfsPath)) throw new VfsPathError(`Unsafe VFS path '${vfsPath}'.`);
88
+ const target = path.resolve(this.root, ...vfsPath.split('/'));
89
+ if (target === this.root || !target.startsWith(`${this.root}${path.sep}`)) {
90
+ throw new VfsPathError(`VFS path '${vfsPath}' escapes the root.`);
91
+ }
92
+ let cursor = target;
93
+ while (!fs.existsSync(cursor)) {
94
+ const parent = path.dirname(cursor);
95
+ if (parent === cursor) break;
96
+ cursor = parent;
97
+ }
98
+ if (fs.existsSync(cursor)) {
99
+ const stat = fs.lstatSync(cursor);
100
+ if (stat.isSymbolicLink()) throw new VfsPathError(`Symlinked VFS path is forbidden: '${vfsPath}'.`);
101
+ const real = fs.realpathSync(cursor);
102
+ if (real !== this.root && !real.startsWith(`${this.root}${path.sep}`)) {
103
+ throw new VfsPathError(`VFS path '${vfsPath}' escapes through a filesystem alias.`);
104
+ }
105
+ }
106
+ if (!allowMissing && !fs.existsSync(target)) return null;
107
+ return target;
108
+ }
109
+
110
+ async read(vfsPath) {
111
+ const target = this.#resolve(vfsPath, { allowMissing: false });
112
+ if (!target) return null;
113
+ const stat = fs.lstatSync(target);
114
+ if (stat.isSymbolicLink()) throw new VfsPathError(`Symlinked VFS path is forbidden: '${vfsPath}'.`);
115
+ if (!stat.isFile()) throw new VfsPathError(`VFS path '${vfsPath}' is not a file.`);
116
+ const bytes = fs.readFileSync(target);
117
+ const hash = bytesHash(bytes);
118
+ return { bytes, hash, version: hash };
119
+ }
120
+
121
+ async stat(vfsPath) {
122
+ const entry = await this.read(vfsPath);
123
+ return entry ? { path: vfsPath, kind: 'file', size: entry.bytes.length, hash: entry.hash, version: entry.version } : null;
124
+ }
125
+
126
+ async *list(prefix = '') {
127
+ if (prefix && !isSafeDocumentPath(prefix)) throw new VfsPathError(`Unsafe VFS prefix '${prefix}'.`);
128
+ const start = prefix ? this.#resolve(prefix) : this.root;
129
+ if (!fs.existsSync(start)) return;
130
+ const stack = [start];
131
+ while (stack.length) {
132
+ const dir = stack.pop();
133
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
134
+ const absolute = path.join(dir, entry.name);
135
+ const relative = path.relative(this.root, absolute).split(path.sep).join('/');
136
+ if (entry.isSymbolicLink()) throw new VfsPathError(`Symlinked VFS entry is forbidden: '${relative}'.`);
137
+ if (entry.isDirectory()) stack.push(absolute);
138
+ else if (entry.isFile() && (!prefix || relative === prefix || relative.startsWith(`${prefix}/`))) {
139
+ yield await this.stat(relative);
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ async writeAtomic(vfsPath, bytes, precondition = {}) {
146
+ const target = this.#resolve(vfsPath);
147
+ const current = await this.read(vfsPath);
148
+ checkPrecondition(current, precondition);
149
+ fs.mkdirSync(path.dirname(target), { recursive: true });
150
+ const parentReal = fs.realpathSync(path.dirname(target));
151
+ if (parentReal !== this.root && !parentReal.startsWith(`${this.root}${path.sep}`)) {
152
+ throw new VfsPathError(`VFS parent for '${vfsPath}' escapes the root.`);
153
+ }
154
+ const nextBytes = asBuffer(bytes);
155
+ const temp = path.join(path.dirname(target), `.${path.basename(target)}.${crypto.randomUUID()}.tmp`);
156
+ try {
157
+ fs.writeFileSync(temp, nextBytes, { flag: 'wx', mode: 0o600 });
158
+ fs.renameSync(temp, target);
159
+ } finally {
160
+ fs.rmSync(temp, { force: true });
161
+ }
162
+ const hash = bytesHash(nextBytes);
163
+ return { path: vfsPath, hash, version: hash, size: nextBytes.length };
164
+ }
165
+
166
+ async append(vfsPath, bytes, precondition = {}) {
167
+ const current = await this.read(vfsPath);
168
+ checkPrecondition(current, precondition);
169
+ const combined = Buffer.concat([current?.bytes || Buffer.alloc(0), asBuffer(bytes)]);
170
+ return this.writeAtomic(vfsPath, combined, current ? { version: current.version } : { ifAbsent: true });
171
+ }
172
+
173
+ async remove(vfsPath, precondition = {}) {
174
+ const target = this.#resolve(vfsPath, { allowMissing: false });
175
+ const current = await this.read(vfsPath);
176
+ checkPrecondition(current, precondition);
177
+ if (!target || !current) return { path: vfsPath, removed: false };
178
+ fs.rmSync(target);
179
+ return { path: vfsPath, removed: true, previous_hash: current.hash };
180
+ }
181
+ }
182
+
183
+ export async function runVfsContract(vfs) {
184
+ const pathName = `contract/${crypto.randomUUID()}.md`;
185
+ const checks = [];
186
+ const record = (name, condition) => checks.push({ name, passed: !!condition });
187
+ record('missing read returns null', await vfs.read(pathName) === null);
188
+ const first = await vfs.writeAtomic(pathName, 'one', { ifAbsent: true });
189
+ record('atomic create returns version', typeof first.version === 'string');
190
+ const read = await vfs.read(pathName);
191
+ record('read preserves bytes', read?.bytes.toString() === 'one');
192
+ let conflict = false;
193
+ try { await vfs.writeAtomic(pathName, 'two', { version: 'wrong' }); } catch (error) { conflict = error instanceof VfsConflictError; }
194
+ record('compare-and-swap rejects stale version', conflict);
195
+ const appended = await vfs.append(pathName, '\ntwo', { version: first.version });
196
+ record('append preserves existing bytes', (await vfs.read(pathName)).bytes.toString() === 'one\ntwo');
197
+ await vfs.remove(pathName, { version: appended.version });
198
+ record('remove deletes target', await vfs.read(pathName) === null);
199
+ let traversal = false;
200
+ try { await vfs.read('../escape.md'); } catch (error) { traversal = error instanceof VfsPathError; }
201
+ record('traversal is rejected', traversal);
202
+ return { passed: checks.every((check) => check.passed), checks };
203
+ }
@@ -0,0 +1,48 @@
1
+ export type SsssCommandType = 'operation' | 'patch' | 'event' | 'delete';
2
+
3
+ export interface VerifiedPrincipal {
4
+ id: string;
5
+ kind: 'human' | 'agent' | 'service' | 'system';
6
+ workspaceIds: string[];
7
+ capabilities?: string[];
8
+ authentication: { provider: string; assurance: string; [key: string]: unknown };
9
+ }
10
+
11
+ export interface SsssCommand {
12
+ type: SsssCommandType;
13
+ workspace_id: string;
14
+ idempotency_key: string;
15
+ path: string;
16
+ operation_id?: string;
17
+ primitive_type?: string;
18
+ content?: string;
19
+ patches?: Record<string, unknown> & { __body__?: string };
20
+ lease_id?: string;
21
+ dry_run?: boolean;
22
+ }
23
+
24
+ export interface SsssResponse {
25
+ success: boolean;
26
+ type: SsssCommandType | null;
27
+ operation_id: string;
28
+ path: string;
29
+ committed_at: string | null;
30
+ dry_run: boolean;
31
+ replay?: boolean;
32
+ event_id?: string;
33
+ validation: { valid: boolean; type: string | null; errors: string[]; warnings: string[] };
34
+ repair?: { field_errors: Array<{ field: string; issue: string }> };
35
+ }
36
+
37
+ export interface SsssKernel {
38
+ execute(command: SsssCommand, context: { principal: VerifiedPrincipal; [key: string]: unknown }): Promise<SsssResponse>;
39
+ registry: unknown;
40
+ validator: unknown;
41
+ }
42
+
43
+ export function canonicalRequestHash(command: SsssCommand, principal: VerifiedPrincipal): string;
44
+ export class MemoryIdempotencyStore {
45
+ get(workspaceId: string, idempotencyKey: string): Promise<unknown>;
46
+ put(workspaceId: string, idempotencyKey: string, value: unknown): Promise<void>;
47
+ }
48
+ export function createKernel(options: Record<string, unknown>): SsssKernel;