@cynodia/axiom 0.8.1-alpha.1 → 0.9.0-alpha.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/docs/ACTIONS_TRANSACTIONS.md +35 -1
- package/docs/AGENT_API.md +28 -1
- package/docs/AGENT_REFERENCE.md +123 -5
- package/docs/ANTI_PATTERNS.md +95 -1
- package/docs/AUTHORITY.md +71 -12
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/EFFECTS.md +131 -1
- package/docs/EVENTS.md +18 -5
- package/docs/EXPRESSIONS.md +1 -1
- package/docs/GRAPH_MODEL.md +2 -2
- package/docs/INTEGRATIONS.md +23 -1
- package/docs/LOCATIONS.md +1 -1
- package/docs/PRESENTATION.md +1 -1
- package/docs/RUNTIME.md +3 -1
- package/docs/SEMANTIC_CONTRACT.md +1 -1
- package/docs/STATE.md +1 -1
- package/docs/STORAGE.md +253 -0
- package/docs/SUBSCRIPTIONS.md +315 -0
- package/docs/TRIGGERS.md +26 -10
- package/docs/UI.md +1 -1
- package/docs/VALIDATION.md +15 -2
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Actions and transactions
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. An action is behavior expressed as data, executed as a transaction.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
{
|
|
@@ -210,6 +210,40 @@ like a mutation is. The adapter runs only after the transaction commits, and the
|
|
|
210
210
|
this action's caller gets back never waits for it — "committed, effect pending." Never legal
|
|
211
211
|
inside `for-each`. Full model: [`EFFECTS.md`](EFFECTS.md).
|
|
212
212
|
|
|
213
|
+
### `blob-metadata`
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
{ kind: 'blob-metadata', storageId: NodeId, blobKey: Expression, bindAs: NodeId }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Reads a stored object's metadata from a `StorageDef` and binds the `BlobRef` into scope —
|
|
220
|
+
query-like in exactly the sense `integration-query` is, and resolved in the same
|
|
221
|
+
pre-transaction phase. It returns the reference, never the bytes. A key that names nothing,
|
|
222
|
+
or names a still-staged upload, **fails the invocation** rather than binding a plausible
|
|
223
|
+
empty record. Never legal inside `for-each`. Full model: [`STORAGE.md`](STORAGE.md).
|
|
224
|
+
|
|
225
|
+
### `blob-commit`
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
{ kind: 'blob-commit', storageId: NodeId, blobKey: Expression, succeededEventId?: NodeId, failedEventId?: NodeId }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Promotes a staged upload to a stored object. Effect-like, and for the same reason every
|
|
232
|
+
effect is: an object store cannot join an Axiom transaction. Reaching it records intent,
|
|
233
|
+
committed atomically with the state that references the object and dispatched only once that
|
|
234
|
+
state is durable. A rolled-back transaction dispatches nothing and leaves the upload staged.
|
|
235
|
+
Never legal inside `for-each`.
|
|
236
|
+
|
|
237
|
+
### `blob-delete`
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
{ kind: 'blob-delete', storageId: NodeId, blobKey: Expression, succeededEventId?: NodeId, failedEventId?: NodeId }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Removes a stored object, post-commit. The state that stopped referencing it commits first; if
|
|
244
|
+
the external deletion then fails, state is still correct and the orphan is visible in
|
|
245
|
+
`server.blobLog()`. Never legal inside `for-each`.
|
|
246
|
+
|
|
213
247
|
## Authorization
|
|
214
248
|
|
|
215
249
|
```ts
|
package/docs/AGENT_API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent API
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. The machine-facing interface. Agents query semantics and apply
|
|
4
4
|
structural transformations; they never edit generated code.
|
|
5
5
|
|
|
6
6
|
```ts
|
|
@@ -160,6 +160,33 @@ agent.getPersistenceForState(stateId)
|
|
|
160
160
|
Authority is derived from what an action writes, so these answers cannot disagree with what
|
|
161
161
|
the graph does. See [`AUTHORITY.md`](AUTHORITY.md).
|
|
162
162
|
|
|
163
|
+
## External-world queries
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
agent.listIntegrations() / agent.listIntegrationOperations(integrationId?)
|
|
167
|
+
agent.getActionsUsingIntegration(integrationId) / agent.getEffectsForAction(actionId)
|
|
168
|
+
agent.getTriggersForAction(actionId) / agent.getTimedTriggers()
|
|
169
|
+
agent.getActionsTriggeredByEvent(eventId) / agent.getTriggeredEvents()
|
|
170
|
+
agent.getExternalDependencies() // { integrations, operations }
|
|
171
|
+
|
|
172
|
+
agent.listSubscriptions()
|
|
173
|
+
agent.getSubscriptionsForIntegration(integrationId)
|
|
174
|
+
agent.getEventForSubscription(subscriptionId)
|
|
175
|
+
agent.getActionsReachableFromSubscription(subscriptionId)
|
|
176
|
+
agent.getExternalEventSources() // { subscriptions, events, integrations }
|
|
177
|
+
|
|
178
|
+
agent.listStorages()
|
|
179
|
+
agent.getActionsUsingStorage(storageId)
|
|
180
|
+
agent.getStoragesWithoutAccessRules() // stores that serve and accept nothing
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
All of these are **graph-static**: they answer what the application *can* reach, not what a
|
|
184
|
+
running authority has done. `getActionsReachableFromSubscription` is the one to reach for
|
|
185
|
+
before changing a live feed — it follows the subscription's event through every bound
|
|
186
|
+
trigger, so "what can this feed actually change" needs no traversal by the consumer. Runtime
|
|
187
|
+
answers come from `AxiomServer.subscriptionLog()` and `blobLog()` instead; see
|
|
188
|
+
[`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md) and [`STORAGE.md`](STORAGE.md).
|
|
189
|
+
|
|
163
190
|
## Transactions
|
|
164
191
|
|
|
165
192
|
Every change is staged on a private copy. The graph an agent or a runtime can observe is
|
package/docs/AGENT_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent reference
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
|
|
4
4
|
declarations before authoring or modifying an Axiom application.
|
|
5
5
|
|
|
6
6
|
Formal guarantees: [`SEMANTIC_CONTRACT.md`](SEMANTIC_CONTRACT.md). Mistakes that compile:
|
|
@@ -31,7 +31,7 @@ One canonical term per concept. These are not interchangeable.
|
|
|
31
31
|
## Graph construction
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
|
-
const graph = new ApplicationGraph(id, name); // version defaults to '0.8.
|
|
34
|
+
const graph = new ApplicationGraph(id, name); // version defaults to '0.8.2'
|
|
35
35
|
graph.addNode<StateDef>({ id, kind: 'state', ... }); // returns NodeId; throws if id exists
|
|
36
36
|
graph.getNode<StateDef>(id); // deep clone, or undefined
|
|
37
37
|
graph.updateNode(node); // write a modified node back
|
|
@@ -496,6 +496,22 @@ if (!result.ok) {
|
|
|
496
496
|
warnings never make a graph invalid. 72 codes in `VALIDATION_CODES`, grouped in
|
|
497
497
|
[`VALIDATION.md`](VALIDATION.md).
|
|
498
498
|
|
|
499
|
+
**`validateGraph(graph)` with no options is target-neutral by design** (spec 8.2 §2-4): a
|
|
500
|
+
graph is never rejected for a renderer or trigger runtime nobody named. This means a bare
|
|
501
|
+
`validateGraph(graph).valid === true` does **not** by itself guarantee the graph is
|
|
502
|
+
executable by the browser client — a UI node kind no renderer implements, or a
|
|
503
|
+
client-authority trigger kind the browser trigger runtime does not execute
|
|
504
|
+
(`CLIENT_TRIGGER_UNSUPPORTED`), both validate silently under the no-options call. Use
|
|
505
|
+
`validateForBrowser(graph)` (`@cynodia/axiom-compiler`) for a validate-only check against
|
|
506
|
+
real browser capabilities, or call `compileToIR(graph)` directly — it applies those same
|
|
507
|
+
capabilities and throws `GraphValidationError` on exactly what `validateForBrowser` would
|
|
508
|
+
report as an error. See [Renderability](#renderability) below for the parallel UI-kind gate.
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
validateGraph(graph).valid; // target-neutral: accepts every UI kind and trigger kind
|
|
512
|
+
validateForBrowser(graph).valid; // browser-real: same as compileToIR's validation step
|
|
513
|
+
```
|
|
514
|
+
|
|
499
515
|
## Agent API
|
|
500
516
|
|
|
501
517
|
```ts
|
|
@@ -638,7 +654,7 @@ event.
|
|
|
638
654
|
agent.listIntegrations() / agent.listIntegrationOperations(id?);
|
|
639
655
|
agent.getActionsUsingIntegration(id) / agent.getEffectsForAction(actionId);
|
|
640
656
|
agent.getTriggersForAction(actionId) / agent.getTimedTriggers();
|
|
641
|
-
agent.getActionsTriggeredByEvent(eventId) / agent.
|
|
657
|
+
agent.getActionsTriggeredByEvent(eventId) / agent.getTriggeredEvents(); // graph-static: has a bound trigger, not "was delivered"
|
|
642
658
|
agent.getExternalDependencies(); // { integrations, operations } — the deployment manifest
|
|
643
659
|
agent.getSystemOnlyActions() / agent.getTriggersTargetingClientOnlyActions();
|
|
644
660
|
agent.isClientInvocable(actionId) / agent.isSystemOnly(actionId);
|
|
@@ -647,17 +663,106 @@ agent.isClientInvocable(actionId) / agent.isSystemOnly(actionId);
|
|
|
647
663
|
Portable artifacts, for a runtime written in another language:
|
|
648
664
|
|
|
649
665
|
```
|
|
650
|
-
@cynodia/axiom-server/conformance the fixture manifest
|
|
666
|
+
@cynodia/axiom-server/conformance the fixture manifest (fixture.format, below)
|
|
651
667
|
@cynodia/axiom-server/conformance/<name>.json one fixture, pure data
|
|
652
|
-
@cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for
|
|
668
|
+
@cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for axiom.server.v1 (frozen)
|
|
669
|
+
@cynodia/axiom-server/schema/server-ir.v2.schema.json JSON Schema for axiom.server.v2
|
|
670
|
+
@cynodia/axiom-server/schema/server-ir.v3.schema.json JSON Schema for axiom.server.v3
|
|
671
|
+
@cynodia/axiom-server/schema/server-ir.v4.schema.json JSON Schema for axiom.server.v4
|
|
672
|
+
@cynodia/axiom-server/schema/server-ir.v5.schema.json JSON Schema for axiom.server.v5 (latest)
|
|
653
673
|
@cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
|
|
654
674
|
```
|
|
655
675
|
|
|
676
|
+
This list is generated/tested content, not hand-maintained prose: `packages/demo/test
|
|
677
|
+
/documentation.test.ts` fails if a shipped `schema/*.json` file is missing from it or a
|
|
678
|
+
listed file no longer exists (spec 8.2 §43-45). `runConformanceFixture`/
|
|
679
|
+
`runConformanceSuite` (`@cynodia/axiom-server`) are the public runner over that fixture
|
|
680
|
+
format — see [`AUTHORITY.md`](AUTHORITY.md#conformance) for `fixture.conformance` (the
|
|
681
|
+
fixture-format version) vs `fixture.serverIR.contract` (the per-fixture Server IR contract)
|
|
682
|
+
vs the manifest's own `conformance`/`protocol`/`release` fields — three separate concepts,
|
|
683
|
+
never conflated under one "contract" name (spec 8.2 §9-10).
|
|
684
|
+
|
|
656
685
|
Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZATION_DENIED`
|
|
657
686
|
`INVOCATION_SOURCE_NOT_ALLOWED` `CONCURRENCY_CONFLICT` `MALFORMED_REQUEST`
|
|
658
687
|
`AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE` and `REMOTE_ACTION_UNAVAILABLE` on the
|
|
659
688
|
client.
|
|
660
689
|
|
|
690
|
+
## SUBSCRIPTIONS AND STORAGE
|
|
691
|
+
|
|
692
|
+
Full model: [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md), [`STORAGE.md`](STORAGE.md). The external
|
|
693
|
+
world reaching *in*, and binary data, which 0.8 had no vocabulary for.
|
|
694
|
+
|
|
695
|
+
The external-interaction model is exactly three directions. Anything else is one of them
|
|
696
|
+
wearing a different name.
|
|
697
|
+
|
|
698
|
+
| Direction | Shape | Vocabulary |
|
|
699
|
+
| --- | --- | --- |
|
|
700
|
+
| Query | Ask; wait for a finite answer. | `integration-query`, `blob-metadata` |
|
|
701
|
+
| Effect | Tell; no answer joins the transaction. | `integration-effect`, `blob-commit`, `blob-delete` |
|
|
702
|
+
| Subscription | The world tells you, while you are listening. | `SubscriptionDef` → `EventDef` |
|
|
703
|
+
|
|
704
|
+
1. **SUBSCRIPTION INVARIANT** — a long-lived external source is a `SubscriptionDef`, never a client, a socket or a callback in the graph. A delivery becomes an `EventDef` payload and enters the existing `EventDef → TriggerDef → ActionDef` pipeline; there is no second event system.
|
|
705
|
+
2. **RAW-I/O INVARIANT** — OS I/O primitives are not graph vocabulary. No `readFile(path)`, `openSocket(host, port)`, `exec(command)`, `spawn(process)`, file descriptor, Node stream or POSIX path exists or will. They live inside an adapter, which is exactly what lets a Rust runtime implement the same graph with different primitives.
|
|
706
|
+
3. **DELIVERY INVARIANT** — at-least-once; effectively-once where `delivery.deduplicateBy` names an external identity, and that deduplication survives a restart when the persistence adapter is durable. Per-subscription ordering is guaranteed; cross-subscription ordering is guaranteed to be **nothing**.
|
|
707
|
+
4. **BACKPRESSURE INVARIANT** — the queue is always bounded (`maxQueued`, default 64) and the default policy (`block`) cannot lose an event. A policy that may discard one (`drop-oldest`/`drop-newest`) is declared in the graph and reports `SUBSCRIPTION_DELIVERY_DROPPED` every time. Loss is never silent and never a default.
|
|
708
|
+
5. **SHUTDOWN INVARIANT** — after `server.stop()`, no delivery reaches application state. A stopped subscription answers `stopped` to everything, including deliveries already in flight.
|
|
709
|
+
6. **BLOB INVARIANT** — bytes never enter the graph, the Server IR or canonical state. A `BlobRef` (`blobRefEntity()` — key, media type, size, filename?, checksum?) is what state holds, and it discloses nothing about the provider.
|
|
710
|
+
7. **BLOB AUTHORIZATION INVARIANT** — possession of a key is not permission. `StorageDef.readAuthorization` is evaluated with the caller bound to `PRINCIPAL` and the `BlobRef` bound to `ref(<storageId>)`; a store with no rule serves nothing. A key that names nothing is refused identically to one the caller may not read.
|
|
711
|
+
8. **STAGED-COMMIT INVARIANT** — an object store does not join an Axiom transaction, and nothing pretends it does. An upload lands `staged`; `blob-commit` promotes it post-commit. A refused transaction leaves a sweepable staged object, never a state referencing bytes that were never claimed. A failed `blob-delete` leaves state correct and the orphan visible in `blobLog()`.
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
{ kind: 'subscription', id: SUB_STATUS, integrationId: INTEGRATION, source: 'device-status',
|
|
715
|
+
eventId: EVENT_STATUS_CHANGED,
|
|
716
|
+
lifecycle: { autoStart: true, required: false, reconnect: { policy: 'exponential', maxAttempts: 5, delayMs: 1000 } },
|
|
717
|
+
delivery: { maxQueued: 32, backpressure: 'block', deduplicateBy: F_DELIVERY_ID, maxAttempts: 1, onFailure: 'report' } }
|
|
718
|
+
|
|
719
|
+
{ kind: 'storage', id: STORAGE_LOGS, blobEntityId: ENTITY_BLOB,
|
|
720
|
+
readAuthorization: <Expression>, uploadAuthorization: <Expression>,
|
|
721
|
+
acceptedMediaTypes: ['text/plain'], maxSizeBytes: 8388608 }
|
|
722
|
+
|
|
723
|
+
{ kind: 'blob-metadata', storageId: STORAGE_LOGS, blobKey: <Expression>, bindAs: SCOPE_BLOB }
|
|
724
|
+
{ kind: 'blob-commit', storageId: STORAGE_LOGS, blobKey: <Expression> }
|
|
725
|
+
{ kind: 'blob-delete', storageId: STORAGE_LOGS, blobKey: <Expression> }
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Lifecycle: `inactive → starting → active → reconnecting → failed`, plus `stopped`. Startup
|
|
729
|
+
decides what activates; application code never calls `start()`. A failed source leaves the
|
|
730
|
+
application running unless `lifecycle.required`.
|
|
731
|
+
|
|
732
|
+
Subscription vs. webhook vs. polling — three different things, do not conflate them:
|
|
733
|
+
|
|
734
|
+
| | What it is | Vocabulary |
|
|
735
|
+
| --- | --- | --- |
|
|
736
|
+
| Webhook | Externally initiated finite request; each delivery enters independently. | host `webhooks` → `EventRequest` |
|
|
737
|
+
| Subscription | Standing semantic interest in a long-lived source. | `SubscriptionDef` |
|
|
738
|
+
| Polling | You ask, repeatedly, on a schedule. | `interval` `TriggerDef` → `integration-query` |
|
|
739
|
+
|
|
740
|
+
Upload is `POST /axiom/blob/<storageId>` and download is `GET /axiom/blob/<storageId>/<key>`
|
|
741
|
+
— one host transport for every Axiom application. Application-authored upload/download
|
|
742
|
+
routes: zero.
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
agent.listSubscriptions() / agent.getSubscriptionsForIntegration(id);
|
|
746
|
+
agent.getEventForSubscription(id) / agent.getActionsReachableFromSubscription(id);
|
|
747
|
+
agent.getExternalEventSources(); // { subscriptions, events, integrations }
|
|
748
|
+
agent.listStorages() / agent.getActionsUsingStorage(id) / agent.getStoragesWithoutAccessRules();
|
|
749
|
+
|
|
750
|
+
server.subscriptionLog() / server.subscriptionStatus(id); // state, counters, last delivery, last failure
|
|
751
|
+
server.blobLog(); // storage effects and their outcomes
|
|
752
|
+
server.stageBlob(storageId, principal, upload);
|
|
753
|
+
server.authorizeBlobRead(storageId, key, principal);
|
|
754
|
+
server.authorizeBlobUpload(storageId, principal, { mediaType, size });
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Adapters: `SubscriptionAdapter` (`createScriptedSubscriptionAdapter` is the deterministic
|
|
758
|
+
fake) and `BlobStorageAdapter` (`createMemoryBlobStore`). A declared subscription or store
|
|
759
|
+
with no registered adapter fails `start()` rather than staying silently inert.
|
|
760
|
+
|
|
761
|
+
Diagnostics: `SUBSCRIPTION_ADAPTER_MISSING` `SUBSCRIPTION_START_FAILED`
|
|
762
|
+
`SUBSCRIPTION_DELIVERY_DROPPED` `SUBSCRIPTION_DELIVERY_FAILED` `BLOB_STORE_MISSING`
|
|
763
|
+
`BLOB_NOT_FOUND` `BLOB_ACCESS_DENIED` `BLOB_TOO_LARGE` `BLOB_MEDIA_TYPE_REJECTED`
|
|
764
|
+
`BLOB_OPERATION_FAILED` `BLOB_STORAGE_UNAVAILABLE` `BLOB_METADATA_FAILED`.
|
|
765
|
+
|
|
661
766
|
## Metadata classes
|
|
662
767
|
|
|
663
768
|
```ts
|
|
@@ -683,6 +788,19 @@ Capabilities describe **node-kind** support, not partial support: a renderer can
|
|
|
683
788
|
that it draws a kind but not one of its options. Nothing in the current vocabulary needs that,
|
|
684
789
|
and it is a deliberate future extension rather than an oversight.
|
|
685
790
|
|
|
791
|
+
The same gate exists for client-authority triggers, since the browser trigger runtime
|
|
792
|
+
implements no `TriggerSpec.kind` at all:
|
|
793
|
+
|
|
794
|
+
```ts
|
|
795
|
+
validateGraph(graph, { triggerRuntime: BROWSER_TRIGGER_CAPABILITIES }); // CLIENT_TRIGGER_UNSUPPORTED
|
|
796
|
+
compileToIR(graph); // applies it by default
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
`CLIENT_TRIGGER_UNSUPPORTED`'s message states the remediation, not only the refusal: move
|
|
800
|
+
the trigger's target action to server authority, or compile for a trigger runtime that
|
|
801
|
+
publishes the kind (`compileToIR(graph, { triggerRuntime })`). Full model:
|
|
802
|
+
[`TRIGGERS.md`](TRIGGERS.md#where-a-trigger-executes).
|
|
803
|
+
|
|
686
804
|
## Named expressions
|
|
687
805
|
|
|
688
806
|
```ts
|
package/docs/ANTI_PATTERNS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-patterns
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
|
|
4
4
|
alternative.
|
|
5
5
|
|
|
6
6
|
## 1. Field names as entity runtime keys
|
|
@@ -405,3 +405,97 @@ a side effect nothing can roll back. See [`INTEGRATIONS.md`](INTEGRATIONS.md) an
|
|
|
405
405
|
|
|
406
406
|
// RIGHT — declare `required: true` on the FieldDef and let the renderer mark it.
|
|
407
407
|
```
|
|
408
|
+
|
|
409
|
+
## 32. OS I/O primitives are not graph vocabulary
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
// WRONG — every one of these. None exists, and none will.
|
|
413
|
+
readFile(path); writeFile(path); openSocket(host, port); exec(command); spawn(process);
|
|
414
|
+
openSerialPort(device);
|
|
415
|
+
{ kind: 'native', implementationId: 'app.readAttachment', inputs: { path: literal('/var/uploads/x') } }
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
An `ApplicationGraph` exposes no filesystem path, no socket, no stream, no file descriptor
|
|
419
|
+
and no subprocess. That is not squeamishness about I/O — the Node host uses all of them
|
|
420
|
+
freely. It is that a graph naming one stops being:
|
|
421
|
+
|
|
422
|
+
| | Why |
|
|
423
|
+
| --- | --- |
|
|
424
|
+
| **portable** | A Rust runtime, or a browser, has different primitives — or none. |
|
|
425
|
+
| **analyzable for authority** | "What can this action reach?" has no answer once `exec` is in the vocabulary. |
|
|
426
|
+
| **secure** | A path is a capability the graph hands out; a key checked against a declared rule is not. |
|
|
427
|
+
| **deterministically testable** | A conformance fixture cannot script a real socket. |
|
|
428
|
+
| **introspectable** | `getExternalDependencies()` can enumerate typed operations. It cannot enumerate what a shell command does. |
|
|
429
|
+
|
|
430
|
+
Low-level I/O is permitted, and expected, **inside an adapter**:
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
PrinterIntegration.print() → adapter implementation → TCP
|
|
434
|
+
VideoIntegration.transcode() → adapter implementation → ffmpeg subprocess
|
|
435
|
+
DeviceStream subscription → adapter implementation → serial port, MQTT, WebSocket
|
|
436
|
+
DiagnosticLogs storage → adapter implementation → local directory, or S3
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
The graph says *what the interaction means*; the adapter decides *how*. Replace Node with
|
|
440
|
+
Rust, MQTT with a WebSocket, or a local directory with S3, and the graph does not change —
|
|
441
|
+
which is the test that the abstraction is at the right level.
|
|
442
|
+
|
|
443
|
+
Use [`INTEGRATIONS.md`](INTEGRATIONS.md), [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md) or
|
|
444
|
+
[`STORAGE.md`](STORAGE.md) instead.
|
|
445
|
+
|
|
446
|
+
## 33. `setInterval` + `fetch` for polling, or a client in application code
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
// WRONG — all four, in application code.
|
|
450
|
+
setInterval(() => fetch('/api/devices').then(apply), 5000);
|
|
451
|
+
const socket = new WebSocket('wss://…');
|
|
452
|
+
const client = mqtt.connect('mqtt://…');
|
|
453
|
+
socket.onmessage = (event) => applyStatus(JSON.parse(event.data));
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Each of these puts scheduling, transport and a callback-driven mutation path into the
|
|
457
|
+
application, where nothing can analyze, test or authorize them.
|
|
458
|
+
|
|
459
|
+
| Instead of | Declare |
|
|
460
|
+
| --- | --- |
|
|
461
|
+
| `setInterval` + `fetch` | `TriggerDef{when:{kind:'interval'}}` → `integration-query` |
|
|
462
|
+
| `new WebSocket` / an MQTT client | `SubscriptionDef` → `EventDef` → `TriggerDef` |
|
|
463
|
+
| `socket.onmessage = handler` | The trigger's target action. Deliveries never invoke a callback. |
|
|
464
|
+
| A hand-rolled webhook route | The host's `webhooks` option → `EventRequest` |
|
|
465
|
+
|
|
466
|
+
## 34. A client-authored subscription event
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
// WRONG — an action a subscription's trigger invokes, left open to clients.
|
|
470
|
+
{ kind: 'action', id: ACTION_APPLY_STATUS, /* no `invocation` */ operations: [ … ] }
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Any anonymous client that guesses the id can then assert whatever the live feed asserts.
|
|
474
|
+
Declare `invocation: { allowedSources: ['system'] }`: a client-sourced call is refused with
|
|
475
|
+
`INVOCATION_SOURCE_NOT_ALLOWED` before identity is even consulted.
|
|
476
|
+
|
|
477
|
+
## 35. base64 bytes in canonical state
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
// WRONG — the attachment's contents in the record, in the Server IR, in every snapshot.
|
|
481
|
+
{ id: F_DOCUMENT_ATTACHMENT, valueType: primitiveType('string') } // "data:application/pdf;base64,…"
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Every read, every snapshot, every persisted write and every `changes` map then carries the
|
|
485
|
+
whole object. Store a `BlobRef` (`blobRefEntity()`) and let the bytes move through the
|
|
486
|
+
host's own upload and download transport — see [`STORAGE.md`](STORAGE.md). A 5MB attachment
|
|
487
|
+
leaves the record exactly as large as a 5-byte one.
|
|
488
|
+
|
|
489
|
+
## 36. An application-authored upload or download route
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
// WRONG — a route the graph does not know about, guarding data the graph does own.
|
|
493
|
+
express.post('/upload', (request, response) => { /* … */ });
|
|
494
|
+
express.get('/files/:key', (request, response) => response.sendFile(`/var/uploads/${request.params.key}`));
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The second is also a path traversal waiting to happen, and neither can be reached by
|
|
498
|
+
`validateGraph`, by `AgentAPI`, or by a conformance fixture. `POST /axiom/blob/<storageId>`
|
|
499
|
+
and `GET /axiom/blob/<storageId>/<key>` already exist, for every Axiom application, with
|
|
500
|
+
`StorageDef.uploadAuthorization` and `StorageDef.readAuthorization` enforced by the
|
|
501
|
+
authority. Application-authored upload/download routes: zero.
|
package/docs/AUTHORITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Authority
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. How an application crosses the trust boundary.
|
|
4
4
|
|
|
5
5
|
Until 0.5.x an Axiom application executed locally. 0.6 adds an **authority**: a generic
|
|
6
6
|
runtime that owns state, decides mutations and persists them. The same semantic graph
|
|
@@ -477,6 +477,16 @@ does for a local failure.
|
|
|
477
477
|
| `INTEGRATION_ADAPTER_MISSING` | The Server IR requires an integration with no registered adapter — refused at `start()`, never deferred to first invocation. |
|
|
478
478
|
| `EVENT_DISPATCH_DEPTH_EXCEEDED` | An event → action → effect → event chain was stopped before it could recurse unboundedly. |
|
|
479
479
|
| `WEBHOOK_VERIFICATION_FAILED` | A webhook delivery failed provider signature verification and was refused before an event was ever constructed. |
|
|
480
|
+
| `SUBSCRIPTION_ADAPTER_MISSING` | The Server IR declares a subscription whose integration has no registered `SubscriptionAdapter`. Startup refuses, rather than leaving a declared live source permanently inactive. |
|
|
481
|
+
| `SUBSCRIPTION_START_FAILED` | A subscription's source could not be established, after every attempt its declared reconnect policy allows. |
|
|
482
|
+
| `SUBSCRIPTION_DELIVERY_DROPPED` | A delivery was discarded by a declared lossy backpressure policy. Loss is always declared and never silent. |
|
|
483
|
+
| `SUBSCRIPTION_DELIVERY_FAILED` | A delivery's triggered action failed after every permitted attempt. |
|
|
484
|
+
| `BLOB_STORE_MISSING` | The Server IR declares a `StorageDef` with no registered `BlobStorageAdapter`. |
|
|
485
|
+
| `BLOB_NOT_FOUND` | No object with that key, or the key names a still-staged upload. |
|
|
486
|
+
| `BLOB_ACCESS_DENIED` | The caller may not read, download or upload this object. Also the answer for a key that names nothing at all, so the endpoint is not an oracle for enumerating keys. |
|
|
487
|
+
| `BLOB_TOO_LARGE` | An upload exceeded the store's declared `maxSizeBytes`. |
|
|
488
|
+
| `BLOB_MEDIA_TYPE_REJECTED` | An upload's media type is not in the store's declared `acceptedMediaTypes`. |
|
|
489
|
+
| `BLOB_OPERATION_FAILED` | A `blob-commit` or `blob-delete` failed at the store, after its retry policy. |
|
|
480
490
|
| `INVOCATION_SOURCE_NOT_ALLOWED` | The action's `invocation.allowedSources` does not include this invocation's source (spec 8.1 §3-9) — refused before `authorization` is even evaluated, because no caller reaching the authority this way may invoke the action at all. |
|
|
481
491
|
|
|
482
492
|
Two client-side codes belong to the boundary as well:
|
|
@@ -549,6 +559,26 @@ Running them requires no part of this implementation. That is the point: the Ser
|
|
|
549
559
|
specification plus these fixtures are the whole contract, so an independent runtime in
|
|
550
560
|
another language can be held to exactly the same standard.
|
|
551
561
|
|
|
562
|
+
**A public reference runner is exported for the TypeScript reference runtime itself**
|
|
563
|
+
(spec 8.2 §14-16):
|
|
564
|
+
|
|
565
|
+
```ts
|
|
566
|
+
import { runConformanceFixture, runConformanceSuite } from '@cynodia/axiom-server';
|
|
567
|
+
|
|
568
|
+
const result = await runConformanceFixture(fixtureJson); // { name, ok, failures[] }
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
It imports only `@cynodia/axiom-server` and fixture data — no graph, no compiler, no
|
|
572
|
+
builder — and reports structured pass/fail rather than throwing on an ordinary fixture
|
|
573
|
+
mismatch. `packages/server/test/conformance.test.ts` runs every shipped fixture through
|
|
574
|
+
this exact function, and `npm run conformance:run` (`scripts/run-conformance.mjs`) runs the
|
|
575
|
+
whole suite from a standalone script using nothing but this public API and the manifest —
|
|
576
|
+
the "held to the same standard by an outside caller" claim, demonstrated rather than
|
|
577
|
+
asserted. The fixture **model** (`ConformanceFixture` and friends, `conformance-types.ts`)
|
|
578
|
+
is deliberately kept separate from this **adapter**
|
|
579
|
+
(`conformance-runner.ts`): a non-TypeScript implementation needs only the model's shape and
|
|
580
|
+
the semantics on this page, never this file.
|
|
581
|
+
|
|
552
582
|
Enumerate the suite from its manifest rather than by listing a directory:
|
|
553
583
|
|
|
554
584
|
```
|
|
@@ -556,12 +586,24 @@ Enumerate the suite from its manifest rather than by listing a directory:
|
|
|
556
586
|
@cynodia/axiom-server/conformance/<name>.json → one fixture
|
|
557
587
|
```
|
|
558
588
|
|
|
559
|
-
The manifest
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
the
|
|
589
|
+
The manifest carries three **separate, non-overlapping** version concepts (spec 8.2 §9-10;
|
|
590
|
+
do not conflate them under one "contract" name):
|
|
591
|
+
|
|
592
|
+
| Field | Means |
|
|
593
|
+
| --- | --- |
|
|
594
|
+
| `manifest.conformance` | The **fixture-format** version (`axiom.conformance.v1`/`v2`) — the shape of the JSON documents themselves: what top-level keys a fixture may have (`invocations` vs `steps`/`externalAdapters`, and so on). |
|
|
595
|
+
| `manifest.baseContract` | The **oldest** Server IR contract any fixture in this manifest may use — `axiom.server.v1`, always, since new fixtures are added without ever raising this floor. It does **not** describe what the newest fixture needs. |
|
|
596
|
+
| `manifest.fixtures[].contract` | The Server IR contract **that specific fixture** requires — this is what is authoritative for what running it needs. The suite ships fixtures spanning `v1` through `v4` simultaneously, each correctly labelled. |
|
|
597
|
+
| `manifest.release` | The `@cynodia/axiom` package version this snapshot of the suite shipped with — unrelated to either version above. |
|
|
598
|
+
|
|
599
|
+
Before 8.2 the manifest's top-level field was named `contract` and fixed at
|
|
600
|
+
`axiom.server.v1`, which read as a claim about the whole suite even after `v3`/`v4`
|
|
601
|
+
fixtures were added; it is now named `baseContract` with the meaning above, precisely
|
|
602
|
+
because per-fixture `contract` is what is actually authoritative. A runtime that does not
|
|
603
|
+
implement a contract a fixture names should refuse that fixture rather than discover the
|
|
604
|
+
mismatch mid-assertion. The files are plain JSON in a documented package directory, so a
|
|
605
|
+
non-JavaScript consumer can read them straight out of the tarball without Node's module
|
|
606
|
+
resolver.
|
|
565
607
|
|
|
566
608
|
Every fixture is executed against the reference runtime by this repository's own test suite,
|
|
567
609
|
and its expectations are exhaustive — a fixture that says which states changed must name all
|
|
@@ -573,6 +615,7 @@ of them and no others. No fixture is permitted to disagree with the shipped runt
|
|
|
573
615
|
@cynodia/axiom-server/schema/server-ir.v1.schema.json
|
|
574
616
|
@cynodia/axiom-server/schema/server-ir.v2.schema.json
|
|
575
617
|
@cynodia/axiom-server/schema/server-ir.v3.schema.json
|
|
618
|
+
@cynodia/axiom-server/schema/server-ir.v4.schema.json
|
|
576
619
|
@cynodia/axiom-server/schema/protocol.v1.schema.json
|
|
577
620
|
```
|
|
578
621
|
|
|
@@ -592,15 +635,31 @@ page plus the conformance fixtures.
|
|
|
592
635
|
| `axiom.server.v1` | the frozen 0.6.1 contract, below | 0.6.1 |
|
|
593
636
|
| `axiom.server.v2` | the expression kinds `group` and `expression-ref`, and the `expressionDefs` they resolve against | 0.7.0 |
|
|
594
637
|
| `axiom.server.v3` | integrations, integration operations, events, triggers, and the `integration-query`/`integration-effect` operation kinds | 0.8.0 |
|
|
638
|
+
| `axiom.server.v4` | `ActionDef.invocation.allowedSources` invocation-source restriction, and the structured effect-outcome envelope (`effectOutcomeEntity`, `EFFECT_ID_FIELD` and its sibling reserved fields) that every effect dispatch uses from 8.1 onward | 0.8.1 |
|
|
639
|
+
| `axiom.server.v5` | `SubscriptionDef` and `StorageDef`, and the `blob-metadata`/`blob-commit`/`blob-delete` operation kinds — the inbound external-I/O direction and binary object storage | 0.9.0 |
|
|
595
640
|
|
|
596
|
-
`SERVER_IR_CONTRACTS` enumerates all
|
|
641
|
+
`SERVER_IR_CONTRACTS` enumerates all five, and is the single source of truth this table is
|
|
642
|
+
tested against — `packages/demo/test/documentation.test.ts` fails if a contract in
|
|
643
|
+
`SERVER_IR_CONTRACTS` has no row here, or a row here names a contract the code does not
|
|
644
|
+
declare (spec 8.2 §7-8). The rules:
|
|
597
645
|
|
|
598
|
-
- **A document declares the oldest contract that can carry it.** `compileToServerIR` computes the label from the vocabulary the document actually uses, so an application that uses nothing from 0.7 or 0.8 produces a byte-identical `axiom.server.v1` document, and the committed v1 conformance fixtures are unchanged.
|
|
599
|
-
- **A runtime MUST refuse a contract it does not implement**, and MUST refuse a document whose vocabulary exceeds its declared contract. A v2 runtime executing a v1-labelled document that uses `group` would accept what a conforming v1 runtime elsewhere refuses, and the two would then disagree about the same file. `createAxiomServer` raises rather than executing one.
|
|
600
|
-
- **A frozen contract gains nothing.** `axiom.server.v1` does not contain `group`, `expression-ref`, `expressionDefs`, an integration, a trigger, an event,
|
|
646
|
+
- **A document declares the oldest contract that can carry it.** `compileToServerIR` computes the label from the vocabulary the document actually uses, so an application that uses nothing from 0.7 or 0.8 produces a byte-identical `axiom.server.v1` document, and the committed v1 conformance fixtures are unchanged. `usesV4Semantics` computes the v4 case specifically: an action's `invocation.allowedSources` genuinely restricting the default two-source set, or any `integration-operation` with `mode: 'effect'` (since every effect dispatch uses the structured v4 envelope) — a document that merely mentions `invocation` without restricting it only needs `axiom.server.v2`, the same tier `group`/`expression-ref` occupy.
|
|
647
|
+
- **A runtime MUST refuse a contract it does not implement**, and MUST refuse a document whose vocabulary exceeds its declared contract. A v2 runtime executing a v1-labelled document that uses `group` would accept what a conforming v1 runtime elsewhere refuses, and the two would then disagree about the same file. `createAxiomServer` raises rather than executing one — including refusing a document that **understates** its own contract (`understatedContract`).
|
|
648
|
+
- **A frozen contract gains nothing.** `axiom.server.v1` does not contain `group`, `expression-ref`, `expressionDefs`, an integration, a trigger, an event, the `integration-query`/`integration-effect` operation kinds, `invocation`, or the structured effect-outcome envelope, and `server-ir.v1.schema.json` is byte-frozen. Vocabulary arrives under a new identifier or not at all.
|
|
649
|
+
- **`axiom.server.v5` is the latest contract as of 0.9.0** (`SERVER_IR_LATEST_CONTRACT`). `usesExternalIOVocabulary` computes it from the document: any `subscriptions`, any `storages`, or any of the three blob operation kinds. The reason it is an incompatible change rather than an additive one is exact — a v4 runtime that ignored `subscriptions` would start an application whose declared live event source never activates, and one that ignored a `blob-commit` would leave state referencing an object that stays staged forever. Both are silent divergence, which is what a contract label exists to prevent.
|
|
601
650
|
|
|
602
651
|
There is one JSON Schema per contract, each generated from the runtime's own vocabulary and
|
|
603
|
-
each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`, `server-ir.v3.schema.json
|
|
652
|
+
each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`, `server-ir.v3.schema.json`,
|
|
653
|
+
`server-ir.v4.schema.json`, `server-ir.v5.schema.json`.
|
|
654
|
+
|
|
655
|
+
**`SubscriptionDef`, `StorageDef` and the blob operations.** Their normative semantics —
|
|
656
|
+
lifecycle states and transitions, at-least-once delivery, per-subscription ordering and the
|
|
657
|
+
explicit absence of cross-subscription ordering, bounded queues and every backpressure policy,
|
|
658
|
+
deduplication and its restart durability, poison-delivery bounds, staged-then-committed object
|
|
659
|
+
lifecycle, and the authorization rule a store evaluates before serving a byte — are documented
|
|
660
|
+
in [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md) and [`STORAGE.md`](STORAGE.md), and are executable in
|
|
661
|
+
the `subscription-*` and `blob-*` conformance fixtures. An independent implementer needs those
|
|
662
|
+
two documents, the v5 schema and those fixtures, and no TypeScript.
|
|
604
663
|
|
|
605
664
|
**`group`.** Partitions a collection: `Collection<A>` → `Collection<Group<K, A>>`. Groups appear
|
|
606
665
|
in the order their key was **first seen** in the source; members keep source order; two keys are
|