@gregiteen/ssss-cli 0.14.0 → 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.
- package/CHANGELOG.md +21 -0
- package/VERSION +1 -1
- package/conformance/reference-bundle.ucw.json +1 -1
- package/docs/ssss-spec.md +9 -9
- package/package.json +1 -1
- package/registry/core.json +14 -0
- package/scripts/audit-registry-field-usage.mjs +3 -0
- package/scripts/conformance-09.mjs +119 -0
- package/skills/ssss/SKILL.md +2 -2
- package/skills/ssss/core/SKILL.md +3 -3
- package/skills/ssss/core/references/core-registry.json +14 -0
- package/skills/ssss/core/references/ssss-spec.md +121 -16
- package/src/authorization.mjs +31 -3
- package/src/engine.mjs +11 -1
- package/src/kernel.mjs +3 -3
- package/src/registry.mjs +82 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.14.1] - 2026-11-02
|
|
11
|
+
### Added
|
|
12
|
+
- **Core primitive aliasing (`role` ↔ `security_role`, `thread` ↔ `conversation`)**:
|
|
13
|
+
- `registry/core.json`: Reciprocal `aliases` declared on `role` (`security_role`) and `thread` (`conversation`).
|
|
14
|
+
- `src/registry.mjs`: `composeRegistries()` executes two passes to register and validate core primitive aliases, enforcing mutual symmetry when aliasing existing primitives, rejecting self-aliases, and disallowing collisions with unrelated primitives.
|
|
15
|
+
- `src/registry.mjs`: `resolveEquivalentDefinitions()` and `isSamePrimitiveType()` resolve equivalence across canonical and alias types.
|
|
16
|
+
- `src/registry.mjs`: `validateReferenceConstraints()` resolves `allowed_types` and `disallowed_types` across primitive aliases.
|
|
17
|
+
- `src/kernel.mjs`: Operation replace and patch type-mutation guards accept equivalent primitive aliases transparently without requiring an explicit migration.
|
|
18
|
+
- `src/engine.mjs`: `legacyRoleCapabilities()` expands permissions across declared aliases for `role` and `security_role`.
|
|
19
|
+
- `src/authorization.mjs`: `createCapabilityAuthorizer()` enforces `policyFloors` across primitive aliases and matches capability grants across aliases for primitive actions.
|
|
20
|
+
- **Core primitive deprecation metadata**:
|
|
21
|
+
- `registry/core.json`: Declared `"deprecated": true` and `"deprecation_notice"` on inert primitives `release`, `model`, `page`, and `conflict`.
|
|
22
|
+
- `src/registry.mjs`: `validatePrimitiveDefinition()` validates optional boolean `deprecated` and string `deprecation_notice`.
|
|
23
|
+
- `docs/ssss-spec.md`: Documented core primitive aliases (§5.1, §5.3, §5.4) and deprecation metadata.
|
|
24
|
+
- Synchronized managed portable core SSSS skill package (`skills/ssss/`, `.agent/skills/ssss/`) with `0.14.1` reference materials.
|
|
25
|
+
|
|
26
|
+
### Behavior changes hosts should check
|
|
27
|
+
- Replacing or patching a document from `role` to `security_role` (or `security_role` to `role`), or `thread` to `conversation`, is now accepted by kernel type-mutation guards.
|
|
28
|
+
- Reference constraints with `allowed_types: ["role"]` accept targets of type `security_role`, and `disallowed_types: ["role"]` reject targets of type `security_role`.
|
|
29
|
+
- Policy floors configured on `ssss:role` or `role` are applied when mutating documents of type `security_role`.
|
|
30
|
+
|
|
10
31
|
## [0.14.0] - 2026-11-02
|
|
11
32
|
### Added
|
|
12
33
|
- **Parameter bindings & Resource bindings (D2, D4)**: `provisionBundle` supports `x_bind` in template frontmatter and resource bindings (`bindings` array with `relation`, `field`, `mode`), with type validation and parameter substitution.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.14.
|
|
1
|
+
0.14.1
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"file_count": 14,
|
|
107
107
|
"provenance": {
|
|
108
108
|
"content_hash": "sha256:7febd673938a3dc04c068e0bf08097a9783c09e612606d2df9a4db8dcb8907f5",
|
|
109
|
-
"exporter": "@gregiteen/ssss-cli@0.14.
|
|
109
|
+
"exporter": "@gregiteen/ssss-cli@0.14.1"
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
"files": [
|
package/docs/ssss-spec.md
CHANGED
|
@@ -250,14 +250,14 @@ primitive — it implements the subset its product requires — but any primitiv
|
|
|
250
250
|
| `assistant` | actor | structural | The definition of an AI assistant/persona. |
|
|
251
251
|
| `workflow` | work | structural | A reusable **task template**: a defined procedure. Firing a workflow submits a `task`. |
|
|
252
252
|
| `trigger` | work | structural | A standalone schedule or event-driven trigger that fires a workflow. |
|
|
253
|
-
| `model` | catalog | structural | The definition of an inference model. |
|
|
253
|
+
| `model` | catalog | structural | The definition of an inference model (deprecated). |
|
|
254
254
|
| `thread` | transcript | tenant_private | An append-only chat transcript. |
|
|
255
255
|
| `conversation` | transcript | tenant_private | An append-only chat transcript. Legacy alias of `thread`. |
|
|
256
256
|
| `run` | transcript | tenant_private | An append-only workflow execution record. |
|
|
257
|
-
| `conflict` | meta | tenant_private | A record of two contradicting primitives, blocking promotion. |
|
|
258
|
-
| `page` | capability | structural | A VFS-native sandboxed custom workspace page. |
|
|
257
|
+
| `conflict` | meta | tenant_private | A record of two contradicting primitives, blocking promotion (deprecated). |
|
|
258
|
+
| `page` | capability | structural | A VFS-native sandboxed custom workspace page (deprecated). |
|
|
259
259
|
| `migration` | meta | structural | An SSSS schema migration state record. |
|
|
260
|
-
| `release` | meta | structural | An SSSS system schema version release record. |
|
|
260
|
+
| `release` | meta | structural | An SSSS system schema version release record (deprecated). |
|
|
261
261
|
| `primitive` | meta | structural | A governed, versioned definition for a namespaced SSSS primitive. |
|
|
262
262
|
|
|
263
263
|
The **Portability** column is the primitive's default portability class (§5.5); a host
|
|
@@ -282,7 +282,7 @@ Document primitives are either:
|
|
|
282
282
|
entirely (`memory`, `skill`, `rule`, `security_role`, `task`, `assistant`,
|
|
283
283
|
`workflow`, `model`, `conflict`, `page`, `migration`, `release`, `primitive`).
|
|
284
284
|
- **Append-type** — the file is an ordered, append-only log; writes add records to
|
|
285
|
-
the body and MUST NOT rewrite prior records (`conversation`, `run`).
|
|
285
|
+
the body and MUST NOT rewrite prior records (`thread`, `conversation`, `run`).
|
|
286
286
|
|
|
287
287
|
Append-type documents MUST be mutated only by appending. A host MUST reject an
|
|
288
288
|
operation that would rewrite or remove existing records of an append-type document.
|
|
@@ -629,7 +629,7 @@ Maintains support assistants and records support events.
|
|
|
629
629
|
|
|
630
630
|
#### `model`
|
|
631
631
|
|
|
632
|
-
The definition of an inference model.
|
|
632
|
+
The definition of an inference model. Deprecated: inert catalog primitive; modern hosts configure models through provider runtime settings.
|
|
633
633
|
|
|
634
634
|
REQUIRED: `type`, `model_id`, `provider`.
|
|
635
635
|
|
|
@@ -731,7 +731,7 @@ Collected 12 messages.
|
|
|
731
731
|
|
|
732
732
|
#### `conflict`
|
|
733
733
|
|
|
734
|
-
A record of two contradicting primitives. Blocks promotion until resolved.
|
|
734
|
+
A record of two contradicting primitives. Blocks promotion until resolved. Deprecated: inert meta primitive; memory collision handling is managed by host memory kernels.
|
|
735
735
|
|
|
736
736
|
REQUIRED: `type`, `conflict_id`, `status` (`pending|resolved`), `new_slug`,
|
|
737
737
|
`existing_slug`, `detected_at`.
|
|
@@ -752,7 +752,7 @@ detected_at: 2026-05-16T18:30:00Z
|
|
|
752
752
|
|
|
753
753
|
#### `page`
|
|
754
754
|
|
|
755
|
-
A VFS-native sandboxed custom workspace page.
|
|
755
|
+
A VFS-native sandboxed custom workspace page. Deprecated: inert sandbox primitive with iframe assumptions; superseded by asset and modern UI primitives.
|
|
756
756
|
|
|
757
757
|
REQUIRED: `type`, `slug`, `name`, `sandbox_entry`.
|
|
758
758
|
|
|
@@ -791,7 +791,7 @@ description: "Add vector_embedding field"
|
|
|
791
791
|
|
|
792
792
|
#### `release`
|
|
793
793
|
|
|
794
|
-
An SSSS system schema version release.
|
|
794
|
+
An SSSS system schema version release. Deprecated: inert meta primitive; release tracking is managed by bundle and package manifests.
|
|
795
795
|
|
|
796
796
|
REQUIRED: `type`, `release_id`, `version`, `schema_version`, `summary`, `released_at`.
|
|
797
797
|
|
package/package.json
CHANGED
package/registry/core.json
CHANGED
|
@@ -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": {
|
|
@@ -45,6 +45,7 @@ const ENGINE_ENFORCED_KEYS = [
|
|
|
45
45
|
'immutable_fields',
|
|
46
46
|
'references',
|
|
47
47
|
'append_only',
|
|
48
|
+
'aliases',
|
|
48
49
|
];
|
|
49
50
|
|
|
50
51
|
// Keys that are real and load-bearing but are intentionally consumed
|
|
@@ -58,6 +59,8 @@ const KNOWN_NON_ENGINE_KEYS = new Set([
|
|
|
58
59
|
'portability', // consumed by src/registry.mjs resolvePortability() + src/bundle.mjs export filtering, not write-time validation
|
|
59
60
|
'resource', // extension-defined resource_bound metadata, consumed by src/bundle.mjs provisioning
|
|
60
61
|
'description', // human documentation on a primitive definition, same role as `notes`; never enforced at write time
|
|
62
|
+
'deprecated', // documentation / catalog deprecation status; consumed by tool displays and spec generation, not write-time validation
|
|
63
|
+
'deprecation_notice', // human guidance on migration path for deprecated primitives; consumed by documentation
|
|
61
64
|
// Product-side projection metadata. The SSSS engine never reads these: they are
|
|
62
65
|
// consumed by the host application's own projector to decide which target tables
|
|
63
66
|
// a document feeds and how (see festech.live SsssGenericProjector.ts). Kept in the
|
|
@@ -319,11 +319,109 @@ async function checkCapabilities(root, check) {
|
|
|
319
319
|
await write('lr-1', 'roles/editor/ROLE.md', role('editor', ['"write:rule"']), 'system');
|
|
320
320
|
await write('lr-2', 'roles/owner/ROLE.md', role('owner', ['"write:*"']), 'system');
|
|
321
321
|
await write('lr-3', 'roles/typed/ROLE.md', role('typed', ['"*:assistant"']), 'system');
|
|
322
|
+
await write('lr-rw', 'roles/rolewriter/ROLE.md', role('rolewriter', ['"write:role"']), 'system');
|
|
322
323
|
check('legacy roles: write:<type> grants writes to that type', (await write('lr-4', 'rules/a.md', doc('rule', 'A'), 'editor')).success);
|
|
323
324
|
check('legacy roles: write:<type> denies other types', (await write('lr-5', 'assistants/b/ASSISTANT.md', doc('assistant', 'B'), 'editor')).error?.code === 'forbidden');
|
|
324
325
|
check('legacy roles: *:<type> grants writes to that type', (await write('lr-6', 'assistants/c/ASSISTANT.md', doc('assistant', 'C'), 'typed')).success);
|
|
325
326
|
check('legacy roles: write:* grants writes to every type', (await write('lr-7', 'workflows/d/WORKFLOW.md', doc('workflow', 'D'), 'owner')).success);
|
|
326
327
|
check('legacy roles: an unknown role is forbidden, not promoted', (await write('lr-8', 'rules/e.md', doc('rule', 'E'), 'ghost')).error?.code === 'forbidden');
|
|
328
|
+
check('legacy roles: write:role grants writes to security_role (alias unification)',
|
|
329
|
+
(await write('lr-aliased-1', 'roles/sec/ROLE.md', role('sec', ['"write:rule"']), 'rolewriter')).success);
|
|
330
|
+
|
|
331
|
+
const roleDoc = (name, permissions) => `---\ntype: role\ntitle: "${name}"\ndescription: "Canonical role."\ntimestamp: "2026-09-24T00:00:00Z"\nname: "${name}"\npermissions: [${permissions.join(', ')}]\n---\n`;
|
|
332
|
+
const replaceRes = await write('lr-aliased-replace', 'roles/sec/ROLE.md', roleDoc('sec', ['"write:rule"', '"write:task"']), 'rolewriter');
|
|
333
|
+
check('kernel mutation: replace security_role with role succeeds (alias unification)', replaceRes.success);
|
|
334
|
+
|
|
335
|
+
const illegalTypeChange = await write('lr-illegal-type', 'roles/sec/ROLE.md', doc('assistant', 'SecAsst'), 'owner');
|
|
336
|
+
check('kernel mutation: replace role with assistant fails closed',
|
|
337
|
+
illegalTypeChange.error?.code === 'validation_failed' &&
|
|
338
|
+
illegalTypeChange.error?.message?.includes('Operation may not change an existing document type.'));
|
|
339
|
+
|
|
340
|
+
const convDoc = `---\ntype: conversation\ntitle: "Thread"\ndescription: "Chat transcript."\ntimestamp: "2026-09-24T00:00:00Z"\nthread_id: "t-1"\n---\n\nInitial message.\n`;
|
|
341
|
+
await engine.processOperation({ type: 'operation', workspace_id: 'w', idempotency_key: 'conv-init', path: 'threads/t1/CONVERSATION.md', content: convDoc, actor: { role: 'system' } }, vault);
|
|
342
|
+
const patchRes = await engine.processOperation({
|
|
343
|
+
type: 'patch', workspace_id: 'w', idempotency_key: 'conv-patch-alias', path: 'threads/t1/CONVERSATION.md',
|
|
344
|
+
patches: { type: 'thread', __body__: 'New reply.' },
|
|
345
|
+
actor: { role: 'system' },
|
|
346
|
+
}, vault);
|
|
347
|
+
check('kernel mutation: patch conversation with type thread succeeds (alias unification)', patchRes.success);
|
|
348
|
+
|
|
349
|
+
const regTypes = engine._registry.types;
|
|
350
|
+
check('core registry: release, model, page, conflict declare deprecated: true',
|
|
351
|
+
regTypes.get('release')?.deprecated === true &&
|
|
352
|
+
regTypes.get('model')?.deprecated === true &&
|
|
353
|
+
regTypes.get('page')?.deprecated === true &&
|
|
354
|
+
regTypes.get('conflict')?.deprecated === true);
|
|
355
|
+
|
|
356
|
+
let selfAliasRejected = false;
|
|
357
|
+
try {
|
|
358
|
+
composeRegistries({
|
|
359
|
+
core: {
|
|
360
|
+
registry: 'ssss',
|
|
361
|
+
version: '0.14.0',
|
|
362
|
+
universal_fields: ['type'],
|
|
363
|
+
portability: { classes: { structural: {} } },
|
|
364
|
+
document_primitives: {
|
|
365
|
+
rule: { family: 'core', append_only: false, portability: 'structural', required_fields: ['type'], aliases: ['rule'] },
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
extensions: [],
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
selfAliasRejected = err.message.includes('cannot alias itself');
|
|
372
|
+
}
|
|
373
|
+
check('core registry: self-alias on a primitive is rejected', selfAliasRejected);
|
|
374
|
+
|
|
375
|
+
let asymmetricAliasRejected = false;
|
|
376
|
+
try {
|
|
377
|
+
composeRegistries({
|
|
378
|
+
core: {
|
|
379
|
+
registry: 'ssss',
|
|
380
|
+
version: '0.14.0',
|
|
381
|
+
universal_fields: ['type'],
|
|
382
|
+
portability: { classes: { structural: {} } },
|
|
383
|
+
document_primitives: {
|
|
384
|
+
rule: { family: 'core', append_only: false, portability: 'structural', required_fields: ['type'], aliases: ['assistant'] },
|
|
385
|
+
assistant: { family: 'core', append_only: false, portability: 'structural', required_fields: ['type'] },
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
extensions: [],
|
|
389
|
+
});
|
|
390
|
+
} catch (err) {
|
|
391
|
+
asymmetricAliasRejected = err.message.includes('collides with existing primitive');
|
|
392
|
+
}
|
|
393
|
+
check('core registry: non-mutual core alias collision is rejected', asymmetricAliasRejected);
|
|
394
|
+
|
|
395
|
+
const floorAuthorizer = createCapabilityAuthorizer({
|
|
396
|
+
policyFloors: { 'ssss:role': { create: ['governance:elevated'] } },
|
|
397
|
+
});
|
|
398
|
+
const secRoleDef = regTypes.get('security_role');
|
|
399
|
+
const floorBlocked = await floorAuthorizer({
|
|
400
|
+
principal: {
|
|
401
|
+
id: 'u1',
|
|
402
|
+
kind: 'agent',
|
|
403
|
+
workspaceIds: ['w'],
|
|
404
|
+
capabilities: ['ssss:security_role:create'],
|
|
405
|
+
authentication: { provider: 'token', assurance: 'verified' },
|
|
406
|
+
},
|
|
407
|
+
workspaceId: 'w',
|
|
408
|
+
definition: secRoleDef,
|
|
409
|
+
action: 'create',
|
|
410
|
+
});
|
|
411
|
+
const floorAllowed = await floorAuthorizer({
|
|
412
|
+
principal: {
|
|
413
|
+
id: 'u1',
|
|
414
|
+
kind: 'agent',
|
|
415
|
+
workspaceIds: ['w'],
|
|
416
|
+
capabilities: ['ssss:security_role:create', 'governance:elevated'],
|
|
417
|
+
authentication: { provider: 'token', assurance: 'verified' },
|
|
418
|
+
},
|
|
419
|
+
workspaceId: 'w',
|
|
420
|
+
definition: secRoleDef,
|
|
421
|
+
action: 'create',
|
|
422
|
+
});
|
|
423
|
+
check('authorization: policyFloors on ssss:role apply when mutating security_role alias',
|
|
424
|
+
floorBlocked.allowed === false && floorBlocked.missing?.includes('governance:elevated') && floorAllowed.allowed === true);
|
|
327
425
|
|
|
328
426
|
const pathPrincipal = {
|
|
329
427
|
id: 'scoped-user',
|
|
@@ -432,6 +530,27 @@ async function checkResourceLifecycle(check) {
|
|
|
432
530
|
check('references: a reference to a disallowed type is rejected', wrongType.error?.code === 'validation_failed');
|
|
433
531
|
check('references: a patch cannot repoint a reference at a missing document', repointed.error?.code === 'validation_failed');
|
|
434
532
|
|
|
533
|
+
const withRoleRefs = createValidator({ extensions: [{
|
|
534
|
+
registry: 'acme', extends: 'core',
|
|
535
|
+
document_primitives: {
|
|
536
|
+
grant_doc: {
|
|
537
|
+
family: 'extension', append_only: false, portability: 'structural', required_fields: ['type', 'name', 'role_ref'],
|
|
538
|
+
references: { role_ref: { allowed_types: ['role'] } },
|
|
539
|
+
},
|
|
540
|
+
deny_role_doc: {
|
|
541
|
+
family: 'extension', append_only: false, portability: 'structural', required_fields: ['type', 'name', 'target_ref'],
|
|
542
|
+
references: { target_ref: { disallowed_types: ['role'] } },
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
}] });
|
|
546
|
+
const roleRefKernel = createKernel({ vfs: new MemoryVfs(), eventStore: new MemoryEventStore(), validator: withRoleRefs });
|
|
547
|
+
const writeRef = (key, pathName, content) => roleRefKernel.execute({ type: 'operation', workspace_id: 'w', idempotency_key: key, path: pathName, content }, { principal });
|
|
548
|
+
await writeRef('rr-1', 'roles/admin.md', `---\ntype: security_role\ntitle: "Admin"\ndescription: "D"\ntimestamp: "2026-09-24T00:00:00Z"\nrole_id: "admin"\nname: "Admin"\npermissions: ["*:*"]\n---\n`);
|
|
549
|
+
const allowedViaAlias = await writeRef('rr-2', 'grants/g1.md', `---\ntype: grant_doc\ntitle: "G"\ndescription: "D"\ntimestamp: "2026-09-24T00:00:00Z"\nname: "G"\nrole_ref: "roles/admin.md"\n---\n`);
|
|
550
|
+
const disallowedViaAlias = await writeRef('rr-3', 'denies/d1.md', `---\ntype: deny_role_doc\ntitle: "D"\ndescription: "D"\ntimestamp: "2026-09-24T00:00:00Z"\nname: "D"\ntarget_ref: "roles/admin.md"\n---\n`);
|
|
551
|
+
check('references: allowed_types role accepts aliased security_role target', allowedViaAlias.success);
|
|
552
|
+
check('references: disallowed_types role rejects aliased security_role target', disallowedViaAlias.error?.code === 'validation_failed');
|
|
553
|
+
|
|
435
554
|
// Every failure carries a symbolic code that maps to the §6.5 status table.
|
|
436
555
|
const plain = createKernel({ vfs: new MemoryVfs(), eventStore: new MemoryEventStore(), leaseStore: new MemoryLeaseStore() });
|
|
437
556
|
const missing = await plain.execute({ type: 'patch', workspace_id: 'w', idempotency_key: 'ec-404', path: 'rules/none.md', patches: { name: 'x' } }, { principal });
|
package/skills/ssss/SKILL.md
CHANGED
|
@@ -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-
|
|
7
|
-
version: 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.
|
|
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
|
|
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.
|
|
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": {
|
|
@@ -250,14 +250,14 @@ primitive — it implements the subset its product requires — but any primitiv
|
|
|
250
250
|
| `assistant` | actor | structural | The definition of an AI assistant/persona. |
|
|
251
251
|
| `workflow` | work | structural | A reusable **task template**: a defined procedure. Firing a workflow submits a `task`. |
|
|
252
252
|
| `trigger` | work | structural | A standalone schedule or event-driven trigger that fires a workflow. |
|
|
253
|
-
| `model` | catalog | structural | The definition of an inference model. |
|
|
253
|
+
| `model` | catalog | structural | The definition of an inference model (deprecated). |
|
|
254
254
|
| `thread` | transcript | tenant_private | An append-only chat transcript. |
|
|
255
255
|
| `conversation` | transcript | tenant_private | An append-only chat transcript. Legacy alias of `thread`. |
|
|
256
256
|
| `run` | transcript | tenant_private | An append-only workflow execution record. |
|
|
257
|
-
| `conflict` | meta | tenant_private | A record of two contradicting primitives, blocking promotion. |
|
|
258
|
-
| `page` | capability | structural | A VFS-native sandboxed custom workspace page. |
|
|
257
|
+
| `conflict` | meta | tenant_private | A record of two contradicting primitives, blocking promotion (deprecated). |
|
|
258
|
+
| `page` | capability | structural | A VFS-native sandboxed custom workspace page (deprecated). |
|
|
259
259
|
| `migration` | meta | structural | An SSSS schema migration state record. |
|
|
260
|
-
| `release` | meta | structural | An SSSS system schema version release record. |
|
|
260
|
+
| `release` | meta | structural | An SSSS system schema version release record (deprecated). |
|
|
261
261
|
| `primitive` | meta | structural | A governed, versioned definition for a namespaced SSSS primitive. |
|
|
262
262
|
|
|
263
263
|
The **Portability** column is the primitive's default portability class (§5.5); a host
|
|
@@ -282,7 +282,7 @@ Document primitives are either:
|
|
|
282
282
|
entirely (`memory`, `skill`, `rule`, `security_role`, `task`, `assistant`,
|
|
283
283
|
`workflow`, `model`, `conflict`, `page`, `migration`, `release`, `primitive`).
|
|
284
284
|
- **Append-type** — the file is an ordered, append-only log; writes add records to
|
|
285
|
-
the body and MUST NOT rewrite prior records (`conversation`, `run`).
|
|
285
|
+
the body and MUST NOT rewrite prior records (`thread`, `conversation`, `run`).
|
|
286
286
|
|
|
287
287
|
Append-type documents MUST be mutated only by appending. A host MUST reject an
|
|
288
288
|
operation that would rewrite or remove existing records of an append-type document.
|
|
@@ -629,7 +629,7 @@ Maintains support assistants and records support events.
|
|
|
629
629
|
|
|
630
630
|
#### `model`
|
|
631
631
|
|
|
632
|
-
The definition of an inference model.
|
|
632
|
+
The definition of an inference model. Deprecated: inert catalog primitive; modern hosts configure models through provider runtime settings.
|
|
633
633
|
|
|
634
634
|
REQUIRED: `type`, `model_id`, `provider`.
|
|
635
635
|
|
|
@@ -731,7 +731,7 @@ Collected 12 messages.
|
|
|
731
731
|
|
|
732
732
|
#### `conflict`
|
|
733
733
|
|
|
734
|
-
A record of two contradicting primitives. Blocks promotion until resolved.
|
|
734
|
+
A record of two contradicting primitives. Blocks promotion until resolved. Deprecated: inert meta primitive; memory collision handling is managed by host memory kernels.
|
|
735
735
|
|
|
736
736
|
REQUIRED: `type`, `conflict_id`, `status` (`pending|resolved`), `new_slug`,
|
|
737
737
|
`existing_slug`, `detected_at`.
|
|
@@ -752,7 +752,7 @@ detected_at: 2026-05-16T18:30:00Z
|
|
|
752
752
|
|
|
753
753
|
#### `page`
|
|
754
754
|
|
|
755
|
-
A VFS-native sandboxed custom workspace page.
|
|
755
|
+
A VFS-native sandboxed custom workspace page. Deprecated: inert sandbox primitive with iframe assumptions; superseded by asset and modern UI primitives.
|
|
756
756
|
|
|
757
757
|
REQUIRED: `type`, `slug`, `name`, `sandbox_entry`.
|
|
758
758
|
|
|
@@ -791,7 +791,7 @@ description: "Add vector_embedding field"
|
|
|
791
791
|
|
|
792
792
|
#### `release`
|
|
793
793
|
|
|
794
|
-
An SSSS system schema version release.
|
|
794
|
+
An SSSS system schema version release. Deprecated: inert meta primitive; release tracking is managed by bundle and package manifests.
|
|
795
795
|
|
|
796
796
|
REQUIRED: `type`, `release_id`, `version`, `schema_version`, `summary`, `released_at`.
|
|
797
797
|
|
|
@@ -957,6 +957,7 @@ An envelope is a JSON object:
|
|
|
957
957
|
"path": "assistants/bot/ASSISTANT.md", // vault-relative, no leading "/", no "." or ".." segments
|
|
958
958
|
"content": "---\ntype: …", // operation: full document; event: JSON payload string
|
|
959
959
|
"patches": { }, // patch only
|
|
960
|
+
"expected_before_hash": "sha256:…", // OPTIONAL — asserts target content hash before mutation (§6.1)
|
|
960
961
|
"primitive_type": "assistant", // OPTIONAL — asserts the target's type
|
|
961
962
|
"operation_id": "uuid", // OPTIONAL — client-chosen; generated if absent
|
|
962
963
|
"lease_id": "uuid", // OPTIONAL — §7
|
|
@@ -970,6 +971,14 @@ verified principal, next to the envelope and never inside it (§6.6). A kernel
|
|
|
970
971
|
MUST ignore any identity-bearing field a client places in the envelope, including
|
|
971
972
|
the pre-0.9 `actor` object.
|
|
972
973
|
|
|
974
|
+
A client or bundle provisioner MAY include `expected_before_hash` on an
|
|
975
|
+
`operation` or `patch` envelope to assert that the target document has not been
|
|
976
|
+
modified locally since the intended baseline was read. For a new document creation,
|
|
977
|
+
`expected_before_hash` is `null`. For an update, it is the `sha256:...` digest of
|
|
978
|
+
the baseline content. If the target document's current hash diverges from
|
|
979
|
+
`expected_before_hash` (and its content is not already equal to the intended write),
|
|
980
|
+
the kernel rejects the mutation with `version_conflict`.
|
|
981
|
+
|
|
973
982
|
A `path` MUST be rejected with `invalid_request` if it is empty, absolute, contains
|
|
974
983
|
a backslash or NUL, or has an empty, `.`, or `..` segment. A host MUST also refuse
|
|
975
984
|
to follow a symbolic link out of the vault.
|
|
@@ -1016,7 +1025,7 @@ Kernels MUST NOT reorder or skip stages. Adapters (§6.7) implement mechanics on
|
|
|
1016
1025
|
|---|-------|------------|
|
|
1017
1026
|
| 1 | **Envelope & identity** — known `type`, required fields present and well-typed (§6.2); a principal with an `id` and `kind` is attached. Nothing is read before this passes, so an anonymous caller learns nothing about the vault. | `invalid_request`, `unauthorized` |
|
|
1018
1027
|
| 2 | **Idempotency** — look up (`workspace_id`, `idempotency_key`). Same request hash: return the stored response with `replay: true` and stop. Different hash: stop (§6.4). | `idempotency_conflict` |
|
|
1019
|
-
| 3 | **Read current state** — load the target, if any, and its version. An unsafe path fails here
|
|
1028
|
+
| 3 | **Read current state & base-version check** — load the target, if any, and its version. An unsafe path fails here with `invalid_request`. If `expected_before_hash` is specified: if the target is absent and `expected_before_hash !== null`, or if the target exists and its hash does not match `expected_before_hash` (and its content differs from the intended write), reject with `version_conflict`. | `invalid_request`, `version_conflict`, or `internal_error` for unreadable stored state |
|
|
1020
1029
|
| 4 | **Content validation** — resolve the primitive and validate the resulting document (§9). Enforce type immutability, `immutable_fields`, patch/delete target existence, and append-only rules. | `not_found`, `validation_failed` |
|
|
1021
1030
|
| 5 | **Authorization** — the verified principal must be complete (§6.6), scoped to `workspace_id`, and hold every capability the primitive and action require. | `unauthorized`, `forbidden` |
|
|
1022
1031
|
| 6 | **Lease** — if the primitive is `lease_required` or the host requires a lease for this path, verify it (§7). | `lease_conflict` |
|
|
@@ -1181,6 +1190,11 @@ envelope, after authenticating the caller:
|
|
|
1181
1190
|
it ends in `:*` and the required capability starts with everything before that
|
|
1182
1191
|
`*`. So `ssss:*` covers every core primitive and `ssss:assistant:*` covers every
|
|
1183
1192
|
action on assistants.
|
|
1193
|
+
- A granted capability string MAY include an optional path selector separated by `@`:
|
|
1194
|
+
`<capability>@<glob>` (for example, `ssss:rule:*@rules/marketing/**` or
|
|
1195
|
+
`*:*@capabilities/messaging/**`). When a selector is present, the capability grants
|
|
1196
|
+
permission only when the envelope's target path matches the glob pattern. When omitted,
|
|
1197
|
+
the capability applies workspace-wide.
|
|
1184
1198
|
- A `system` principal bypasses capability checks. It exists for trusted internal
|
|
1185
1199
|
callers such as import (§17). A host MUST NOT grant `system` to anything a remote
|
|
1186
1200
|
caller controls.
|
|
@@ -1215,6 +1229,73 @@ each with a shared contract test in the conformance suite (§12):
|
|
|
1215
1229
|
"Across every writer" means across threads, processes, and hosts sharing the same
|
|
1216
1230
|
storage, not just within one instance.
|
|
1217
1231
|
|
|
1232
|
+
### 6.8 HTTP/JSON Wire Protocol & Subprocess Bridge
|
|
1233
|
+
|
|
1234
|
+
To enable portable, cross-language interoperation across diverse runtimes (Node.js, Python,
|
|
1235
|
+
Ruby, Go, Rust), SSSS defines a normative HTTP/JSON wire transport contract and a standard
|
|
1236
|
+
subprocess bridge. A host or engine that exposes SSSS over a network MUST conform to this
|
|
1237
|
+
wire contract.
|
|
1238
|
+
|
|
1239
|
+
#### Endpoint & HTTP Headers
|
|
1240
|
+
|
|
1241
|
+
- **Endpoint**: `POST /v1/operations` (hosts MAY also route `POST /` to this handler for convenience).
|
|
1242
|
+
- **Request Content-Type**: `application/json`.
|
|
1243
|
+
- **Response Content-Type**: `application/json`.
|
|
1244
|
+
- **Authentication**: When remote authentication is required, the client MUST present
|
|
1245
|
+
a bearer token via the `Authorization: Bearer <token>` HTTP header. The host verifies
|
|
1246
|
+
the token and resolves it out-of-band to a verified principal (§6.6). The envelope body
|
|
1247
|
+
itself carries no secret credentials.
|
|
1248
|
+
- **Health check**: A host exposing the HTTP interface SHOULD expose `GET /health`, returning
|
|
1249
|
+
HTTP 200 with `{ "ok": true, "version": "<version>" }`.
|
|
1250
|
+
|
|
1251
|
+
#### Request and Response Wire Contract
|
|
1252
|
+
|
|
1253
|
+
- **Request Body**: An Operation Contract envelope (§6.1, §6.2) as a JSON object:
|
|
1254
|
+
```json
|
|
1255
|
+
{
|
|
1256
|
+
"type": "operation",
|
|
1257
|
+
"idempotency_key": "00000000-0000-0000-0000-000000000001",
|
|
1258
|
+
"path": "assistants/support-bot.md",
|
|
1259
|
+
"workspace_id": "ws-main",
|
|
1260
|
+
"content": "---\ntype: assistant\nname: Support Bot\ntitle: Support Bot\ndescription: Customer support assistant\ntimestamp: 2026-07-02T00:00:00Z\n---\nHelp customers.\n"
|
|
1261
|
+
}
|
|
1262
|
+
```
|
|
1263
|
+
Callers attaching an explicit principal context MAY submit:
|
|
1264
|
+
```json
|
|
1265
|
+
{
|
|
1266
|
+
"envelope": {
|
|
1267
|
+
"type": "operation",
|
|
1268
|
+
"idempotency_key": "00000000-0000-0000-0000-000000000001",
|
|
1269
|
+
"path": "assistants/support-bot.md",
|
|
1270
|
+
"workspace_id": "ws-main",
|
|
1271
|
+
"content": "---\ntype: assistant\nname: Support Bot\ntitle: Support Bot\ndescription: Customer support assistant\ntimestamp: 2026-07-02T00:00:00Z\n---\nHelp customers.\n"
|
|
1272
|
+
},
|
|
1273
|
+
"principal": {
|
|
1274
|
+
"id": "agent-1",
|
|
1275
|
+
"kind": "agent",
|
|
1276
|
+
"workspaceIds": ["ws-main"],
|
|
1277
|
+
"capabilities": ["*:*"],
|
|
1278
|
+
"authentication": { "provider": "token", "assurance": "verified" }
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
```
|
|
1282
|
+
- **Response Body**: The kernel response object (§6.5) serialized as JSON:
|
|
1283
|
+
- On commit: `201 Created` with `{ "success": true, "type": "operation", ... }`.
|
|
1284
|
+
- On idempotent replay or dry run: `200 OK` with `{ "success": true, "replay": true, ... }`.
|
|
1285
|
+
- On error: status mapped from `ERROR_STATUS` (§6.5) with `{ "success": false, "error": { "code": "...", "message": "..." } }`.
|
|
1286
|
+
|
|
1287
|
+
#### Subprocess Bridge Contract (`ssss bridge`)
|
|
1288
|
+
|
|
1289
|
+
For local process orchestration without opening network ports (for example, Python hosts
|
|
1290
|
+
integrating SSSS as a subprocess), the reference CLI provides `ssss bridge`:
|
|
1291
|
+
|
|
1292
|
+
- Reads JSON from standard input (`stdin`). Supports:
|
|
1293
|
+
1. A single JSON request object.
|
|
1294
|
+
2. A JSON array of requests (`[...]`).
|
|
1295
|
+
3. Newline-delimited JSON (`NDJSON`), with one request per line when running with `--stream`.
|
|
1296
|
+
- Writes JSON response objects to standard output (`stdout`) matching the wire contract shape.
|
|
1297
|
+
- Does not alter envelope semantics, schema validation, or error status mappings.
|
|
1298
|
+
|
|
1218
1299
|
---
|
|
1219
1300
|
|
|
1220
1301
|
## 7. Leases — Concurrency Control
|
|
@@ -1868,6 +1949,23 @@ of `source: user` or `source: system` — e.g. the phone number, domain, and mai
|
|
|
1868
1949
|
must bind. This is how the standard guarantees a sold business says exactly what real-world
|
|
1869
1950
|
resources it needs without shipping the seller's own.
|
|
1870
1951
|
|
|
1952
|
+
#### 16.5.1 Frontmatter Parameter Binding (`x_bind`)
|
|
1953
|
+
|
|
1954
|
+
Values bind exclusively through **frontmatter**, never through free-text string substitution in Markdown body text. A template document declares frontmatter parameter bindings using the reserved `x_bind` mapping:
|
|
1955
|
+
|
|
1956
|
+
```yaml
|
|
1957
|
+
x_bind:
|
|
1958
|
+
channel_id: support_channel_id
|
|
1959
|
+
notify_email: primary_admin_email
|
|
1960
|
+
```
|
|
1961
|
+
|
|
1962
|
+
During `provisionBundle` (§17.1):
|
|
1963
|
+
1. For each `field: parameter_key` entry in `x_bind`, the provisioner resolves the value from the supplied `parameters` dictionary, or the parameter's `defaultValue` if omitted.
|
|
1964
|
+
2. If a parameter is `required: true` and has no supplied value or default, provisioning fails with `unresolved`.
|
|
1965
|
+
3. The resolved value MUST match the parameter's declared `type` (`string`, `boolean`, `number`, etc.).
|
|
1966
|
+
4. The target frontmatter field is assigned the resolved value, and the `x_bind` property is stripped from the provisioned output document.
|
|
1967
|
+
5. In `template` and `sale` export profiles (§5.5), when `reduceResourceBoundFile()` strips sensitive infrastructure values from `resource.binds` (such as domain names or phone numbers), it automatically emits corresponding `x_bind` bindings targeting the synthesized resource requirement parameters.
|
|
1968
|
+
|
|
1871
1969
|
---
|
|
1872
1970
|
|
|
1873
1971
|
## 17. The Provisioning Contract
|
|
@@ -1916,12 +2014,18 @@ binding each requires.
|
|
|
1916
2014
|
### 17.3 Determinism, Id-Remap & Link Integrity
|
|
1917
2015
|
|
|
1918
2016
|
When a bundle is provisioned into a target vault, identifiers (slugs, workspace ids,
|
|
1919
|
-
cross-file links) are **remapped** so the imported business is a fresh, self-consistent
|
|
1920
|
-
instance rather than a clone aliasing the seller's ids.
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2017
|
+
cross-file links, and typed references) are **remapped** so the imported business is a fresh, self-consistent
|
|
2018
|
+
instance rather than a clone aliasing the seller's ids.
|
|
2019
|
+
|
|
2020
|
+
Given a target `workspaceId` and optional `pathPrefix`:
|
|
2021
|
+
1. **Deterministic ID Map:** Every source identifier or slug derives a deterministic target identifier:
|
|
2022
|
+
`newId = "<pathPrefix>-" + sha256(workspaceId + ":" + sourceId).slice(0, 12)`
|
|
2023
|
+
2. **Path Prefixing:** Target file paths are placed under the path prefix:
|
|
2024
|
+
`targetPath = pathPrefix ? `${pathPrefix}/${sourcePath}` : sourcePath`
|
|
2025
|
+
3. **Identifier Fields:** Frontmatter identifier fields (`slug`, `id`, `workflow_id`, `run_id`, `task_id`, `migration_id`) are mapped to their derived `newId`.
|
|
2026
|
+
4. **Link Rewriting:** Body wiki-links `[[source-slug]]` and relative Markdown links `[title](source-path.md)` are rewritten to the target slug and target relative path.
|
|
2027
|
+
5. **Registry-Declared Typed References (§9):** Frontmatter fields constrained by `references` (e.g. `asset.resource_ref`, `trigger.target_workflow`) that point to relative paths within the bundle MUST be rewritten to point to their remapped target paths under `pathPrefix`.
|
|
2028
|
+
6. **Link & Reference Integrity:** Remapping MUST preserve link integrity: every internal `[[link]]`, relative markdown link, and typed frontmatter reference that resolved in the source bundle MUST resolve to the remapped target after import. An importer that cannot resolve a link or reference MUST fail the provision (`danglingLinks`) rather than emit a dangling reference.
|
|
1925
2029
|
|
|
1926
2030
|
### 17.4 Composition & Upgrade
|
|
1927
2031
|
|
|
@@ -1946,6 +2050,7 @@ MUST NOT be repurposed by hosts:
|
|
|
1946
2050
|
`aliases`, `feedback`, `confidence`.
|
|
1947
2051
|
- Lifecycle: `slug`, `schema_version`, `status`.
|
|
1948
2052
|
- Portability (§5.5): `x_portability`.
|
|
2053
|
+
- Parameter Binding (§16.5): `x_bind`.
|
|
1949
2054
|
|
|
1950
2055
|
The patch key `__body__` (§6.2) is reserved and can never be a frontmatter key.
|
|
1951
2056
|
|
package/src/authorization.mjs
CHANGED
|
@@ -65,17 +65,45 @@ export function createCapabilityAuthorizer(options = {}) {
|
|
|
65
65
|
if (principal.kind !== 'system' && !principal.workspaceIds.includes(workspaceId)) {
|
|
66
66
|
return { allowed: false, reason: `Principal '${principal.id}' is not scoped to workspace '${workspaceId}'.` };
|
|
67
67
|
}
|
|
68
|
-
const
|
|
68
|
+
const namespace = definition?.namespace || 'ssss';
|
|
69
|
+
const aliases = Array.isArray(definition?.aliases) ? definition.aliases : [];
|
|
70
|
+
const floorKeys = new Set(
|
|
71
|
+
[
|
|
72
|
+
definition?.qualified_type,
|
|
73
|
+
definition?.primitive_id,
|
|
74
|
+
definition?.type,
|
|
75
|
+
...aliases.flatMap((alias) => [
|
|
76
|
+
alias,
|
|
77
|
+
alias.includes(':') ? alias : `${namespace}:${alias}`,
|
|
78
|
+
]),
|
|
79
|
+
].filter(Boolean),
|
|
80
|
+
);
|
|
81
|
+
const floorCapabilities = [...floorKeys].flatMap((key) => policyFloors[key]?.[action] || []);
|
|
69
82
|
const required = new Set([
|
|
70
83
|
...requiredCapabilities(definition, action),
|
|
71
|
-
...
|
|
84
|
+
...floorCapabilities,
|
|
72
85
|
...(context.policyFloor || []),
|
|
73
86
|
]);
|
|
74
87
|
const granted = new Set(principal.capabilities || []);
|
|
75
88
|
if (principal.kind === 'system' && options.systemBypass !== false) return { allowed: true, required: [...required] };
|
|
76
89
|
const targetPath = context.path || null;
|
|
90
|
+
const defPrefixes = [...new Set([definition?.qualified_type, definition?.primitive_id, definition?.type].filter(Boolean))];
|
|
77
91
|
const missing = [...required].filter((capability) =>
|
|
78
|
-
![...granted].some((grant) =>
|
|
92
|
+
![...granted].some((grant) => {
|
|
93
|
+
if (capabilityCovers(grant, capability, targetPath)) return true;
|
|
94
|
+
if (!aliases.length) return false;
|
|
95
|
+
const matchedPrefix = defPrefixes.find((prefix) => capability.startsWith(`${prefix}:`));
|
|
96
|
+
if (!matchedPrefix) return false;
|
|
97
|
+
const actionTail = capability.slice(matchedPrefix.length + 1);
|
|
98
|
+
return aliases.some((alias) => {
|
|
99
|
+
const qualifiedAlias = alias.includes(':') ? alias : `${namespace}:${alias}`;
|
|
100
|
+
const bareAlias = alias.includes(':') ? alias.slice(alias.indexOf(':') + 1) : alias;
|
|
101
|
+
return (
|
|
102
|
+
capabilityCovers(grant, `${qualifiedAlias}:${actionTail}`, targetPath) ||
|
|
103
|
+
capabilityCovers(grant, `${bareAlias}:${actionTail}`, targetPath)
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}));
|
|
79
107
|
if (missing.length) return { allowed: false, required: [...required], missing, reason: `Missing capabilities: ${missing.join(', ')}.` };
|
|
80
108
|
if (context.requiresHumanConfirmation && principal.kind !== 'human') {
|
|
81
109
|
return { allowed: false, required: [...required], reason: 'A verified human confirmation is required.' };
|
package/src/engine.mjs
CHANGED
|
@@ -39,7 +39,17 @@ export function legacyRoleCapabilities(permissions, registrySet) {
|
|
|
39
39
|
if (permission === 'write:*' || permission === '*:*') { capabilities.add('*:*'); continue; }
|
|
40
40
|
const match = permission.match(/^(write|\*):(.+)$/);
|
|
41
41
|
const definition = match && resolvePrimitiveDefinition(registrySet, match[2]);
|
|
42
|
-
if (definition?.qualified_type)
|
|
42
|
+
if (definition?.qualified_type) {
|
|
43
|
+
capabilities.add(`${definition.qualified_type}:*`);
|
|
44
|
+
const prefix = definition.qualified_type.includes(':')
|
|
45
|
+
? definition.qualified_type.split(':')[0]
|
|
46
|
+
: 'ssss';
|
|
47
|
+
for (const alias of definition.aliases || []) {
|
|
48
|
+
capabilities.add(`${prefix}:${alias}:*`);
|
|
49
|
+
capabilities.add(`write:${alias}`);
|
|
50
|
+
capabilities.add(`*:${alias}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
54
|
return [...capabilities];
|
|
45
55
|
}
|
package/src/kernel.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { parseDocument, serializeDocument } from './frontmatter.mjs';
|
|
7
7
|
import { createValidator } from './validator.mjs';
|
|
8
|
-
import { isAppendType, isSafeDocumentPath, resolvePortability, resolvePrimitiveDefinition } from './registry.mjs';
|
|
8
|
+
import { isAppendType, isSafeDocumentPath, resolvePortability, resolvePrimitiveDefinition, isSamePrimitiveType } from './registry.mjs';
|
|
9
9
|
import { createCapabilityAuthorizer, validateVerifiedPrincipal } from './authorization.mjs';
|
|
10
10
|
import { createCanonicalEvent, idempotentEventId } from './events.mjs';
|
|
11
11
|
import { MemoryIdempotencyStore } from './idempotency.mjs';
|
|
@@ -284,7 +284,7 @@ export function createKernel(options = {}) {
|
|
|
284
284
|
nextContent = envelope.content;
|
|
285
285
|
nextData = validation.data;
|
|
286
286
|
}
|
|
287
|
-
if (currentData?.type && currentData.type
|
|
287
|
+
if (currentData?.type && !isSamePrimitiveType(registrySet, currentData.type, nextData.type)) {
|
|
288
288
|
return failure('validation_failed', envelope, operationId, ['Operation may not change an existing document type.'], [{ field: 'type', issue: 'Use an explicit migration.' }]);
|
|
289
289
|
}
|
|
290
290
|
// Replaying an identical full operation is a no-op, not a replace. This must
|
|
@@ -317,7 +317,7 @@ export function createKernel(options = {}) {
|
|
|
317
317
|
if (!current) return failure('not_found', envelope, operationId, ['Patch target does not exist.'], [{ field: 'path', issue: 'Target not found.' }]);
|
|
318
318
|
definition = resolvePrimitiveDefinition(registrySet, declaredType);
|
|
319
319
|
if (!definition) return failure('validation_failed', envelope, operationId, [`Unknown SSSS primitive '${declaredType}'.`], [{ field: 'type', issue: 'Unknown primitive.' }]);
|
|
320
|
-
if (Object.hasOwn(envelope.patches, 'type') && envelope.patches.type
|
|
320
|
+
if (Object.hasOwn(envelope.patches, 'type') && !isSamePrimitiveType(registrySet, envelope.patches.type, currentData.type)) {
|
|
321
321
|
return failure('validation_failed', envelope, operationId, ['Patch may not change document type.'], [{ field: 'type', issue: 'Use an explicit migration.' }]);
|
|
322
322
|
}
|
|
323
323
|
nextData = { ...currentData, ...Object.fromEntries(Object.entries(envelope.patches).filter(([key]) => key !== '__body__')) };
|
package/src/registry.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const DEFAULT_REGISTRY_DIR = path.resolve(__dirname, '..', 'registry');
|
|
18
18
|
|
|
19
|
-
const APPEND_TYPES = new Set(['conversation', 'run']);
|
|
19
|
+
const APPEND_TYPES = new Set(['conversation', 'run', 'thread']);
|
|
20
20
|
const SAFE_REGISTRY_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
21
21
|
const SAFE_TYPE_NAME = /^(?:[a-z][a-z0-9_-]{0,63}:)?[a-z][a-z0-9_-]{0,127}$/;
|
|
22
22
|
const SAFE_FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
@@ -52,6 +52,13 @@ function validatePrimitiveDefinition(name, def, source, portabilityClasses) {
|
|
|
52
52
|
assertStringArray(def.required_fields, `${source}: primitive '${name}' required_fields`);
|
|
53
53
|
assertStringArray(def.optional_fields, `${source}: primitive '${name}' optional_fields`);
|
|
54
54
|
assertStringArray(def.immutable_fields, `${source}: primitive '${name}' immutable_fields`);
|
|
55
|
+
assertStringArray(def.aliases, `${source}: primitive '${name}' aliases`);
|
|
56
|
+
if (def.deprecated !== undefined && typeof def.deprecated !== 'boolean') {
|
|
57
|
+
throw new Error(`${source}: primitive '${name}' deprecated must be a boolean.`);
|
|
58
|
+
}
|
|
59
|
+
if (def.deprecation_notice !== undefined && typeof def.deprecation_notice !== 'string') {
|
|
60
|
+
throw new Error(`${source}: primitive '${name}' deprecation_notice must be a string.`);
|
|
61
|
+
}
|
|
55
62
|
|
|
56
63
|
assertRecord(def.required_when, `${source}: primitive '${name}' required_when`);
|
|
57
64
|
for (const [condition, fields] of Object.entries(def.required_when || {})) {
|
|
@@ -157,6 +164,33 @@ export function composeRegistries({ core, extensions: extensionInputs = [] } = {
|
|
|
157
164
|
aliases.set(`ssss:${name}`, name);
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
for (const [name, def] of Object.entries(core.document_primitives || {})) {
|
|
168
|
+
for (const alias of def.aliases || []) {
|
|
169
|
+
if (!isPrimitiveTypeName(alias)) {
|
|
170
|
+
throw new Error(`core registry: primitive '${name}' has unsafe alias '${alias}'.`);
|
|
171
|
+
}
|
|
172
|
+
const bareAlias = alias.startsWith('ssss:') ? alias.slice('ssss:'.length) : alias;
|
|
173
|
+
const qualifiedAlias = alias.includes(':') ? alias : `ssss:${alias}`;
|
|
174
|
+
if (bareAlias === name) {
|
|
175
|
+
throw new Error(`core registry: primitive '${name}' cannot alias itself.`);
|
|
176
|
+
}
|
|
177
|
+
if (types.has(bareAlias)) {
|
|
178
|
+
const peer = types.get(bareAlias);
|
|
179
|
+
const peerAliases = (peer.aliases || []).map((a) => (a.startsWith('ssss:') ? a.slice('ssss:'.length) : a));
|
|
180
|
+
if (!peerAliases.includes(name)) {
|
|
181
|
+
throw new Error(`core registry: primitive alias '${alias}' on '${name}' collides with existing primitive '${bareAlias}'.`);
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if ((aliases.has(bareAlias) && aliases.get(bareAlias) !== name) ||
|
|
186
|
+
(aliases.has(qualifiedAlias) && aliases.get(qualifiedAlias) !== name)) {
|
|
187
|
+
throw new Error(`core registry: primitive alias '${alias}' collides with an existing primitive.`);
|
|
188
|
+
}
|
|
189
|
+
aliases.set(bareAlias, name);
|
|
190
|
+
aliases.set(qualifiedAlias, name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
160
194
|
for (const [index, item] of extensionInputs.entries()) {
|
|
161
195
|
const { source, value: ext } = normalizeExtensionInput(item, index);
|
|
162
196
|
if (!SAFE_REGISTRY_NAME.test(ext.registry || '')) {
|
|
@@ -232,6 +266,14 @@ export function composeRegistries({ core, extensions: extensionInputs = [] } = {
|
|
|
232
266
|
);
|
|
233
267
|
}
|
|
234
268
|
}
|
|
269
|
+
const aliasOverlap = (constraint.allowed_types || []).filter((allowed) =>
|
|
270
|
+
(constraint.disallowed_types || []).some((disallowed) => isSamePrimitiveType(registrySet, allowed, disallowed))
|
|
271
|
+
);
|
|
272
|
+
if (aliasOverlap.length) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`registry '${def.registry}' primitive '${def.type}' reference '${field}' both allows and forbids (via alias): ${aliasOverlap.join(', ')}.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
235
277
|
}
|
|
236
278
|
}
|
|
237
279
|
|
|
@@ -297,6 +339,42 @@ export function resolvePrimitiveDefinition(registrySet, type) {
|
|
|
297
339
|
return local ? registrySet.types.get(local) || null : null;
|
|
298
340
|
}
|
|
299
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Return all equivalent primitive definitions for a type or definition
|
|
344
|
+
* (the definition itself plus any definitions linked via `aliases`).
|
|
345
|
+
*/
|
|
346
|
+
export function resolveEquivalentDefinitions(registrySet, typeOrDef) {
|
|
347
|
+
const base = typeof typeOrDef === 'string'
|
|
348
|
+
? resolvePrimitiveDefinition(registrySet, typeOrDef)
|
|
349
|
+
: typeOrDef;
|
|
350
|
+
if (!base) return [];
|
|
351
|
+
const seen = new Map([[base.qualified_type || base.type, base]]);
|
|
352
|
+
for (const alias of base.aliases || []) {
|
|
353
|
+
const target = resolvePrimitiveDefinition(registrySet, alias);
|
|
354
|
+
if (target && !seen.has(target.qualified_type || target.type)) {
|
|
355
|
+
seen.set(target.qualified_type || target.type, target);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return [...seen.values()];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Return true if two primitive type identifiers resolve to the same canonical
|
|
363
|
+
* primitive definition (either exact match, qualified match, or mutual alias).
|
|
364
|
+
*/
|
|
365
|
+
export function isSamePrimitiveType(registrySet, typeA, typeB) {
|
|
366
|
+
if (typeA === typeB) return true;
|
|
367
|
+
if (!typeA || !typeB) return false;
|
|
368
|
+
const defA = resolvePrimitiveDefinition(registrySet, typeA);
|
|
369
|
+
const defB = resolvePrimitiveDefinition(registrySet, typeB);
|
|
370
|
+
if (!defA || !defB) return false;
|
|
371
|
+
if (defA.qualified_type === defB.qualified_type) return true;
|
|
372
|
+
const equivalentsA = resolveEquivalentDefinitions(registrySet, defA);
|
|
373
|
+
if (equivalentsA.some((d) => d.qualified_type === defB.qualified_type)) return true;
|
|
374
|
+
const equivalentsB = resolveEquivalentDefinitions(registrySet, defB);
|
|
375
|
+
return equivalentsB.some((d) => d.qualified_type === defA.qualified_type);
|
|
376
|
+
}
|
|
377
|
+
|
|
300
378
|
/**
|
|
301
379
|
* Names of the extensions loaded when a caller does not scope the load.
|
|
302
380
|
*
|
|
@@ -472,14 +550,15 @@ export function validateReferenceConstraints(typeDef, data, resolveReference, ty
|
|
|
472
550
|
continue;
|
|
473
551
|
}
|
|
474
552
|
const referencedType = referenced.data?.type;
|
|
553
|
+
const registrySurface = { types, aliases: types.aliases };
|
|
475
554
|
if (!lookupType(referencedType)) {
|
|
476
555
|
issues.push({ field, issue: `Referenced document declares unknown type '${referencedType || '(missing)'}'.` });
|
|
477
556
|
continue;
|
|
478
557
|
}
|
|
479
|
-
if (constraint.allowed_types && !constraint.allowed_types.
|
|
558
|
+
if (constraint.allowed_types && !constraint.allowed_types.some((allowed) => isSamePrimitiveType(registrySurface, allowed, referencedType))) {
|
|
480
559
|
issues.push({ field, issue: `Referenced type '${referencedType}' is not allowed.` });
|
|
481
560
|
}
|
|
482
|
-
if (constraint.disallowed_types?.
|
|
561
|
+
if (constraint.disallowed_types?.some((disallowed) => isSamePrimitiveType(registrySurface, disallowed, referencedType))) {
|
|
483
562
|
issues.push({ field, issue: `Referenced type '${referencedType}' is forbidden.` });
|
|
484
563
|
}
|
|
485
564
|
const referencedPortability = resolvePortability(lookupType(referencedType), referenced.data);
|