@abloatai/ablo 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +713 -629
- package/README.md +56 -519
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +24 -4
- package/dist/BaseSyncedStore.js +53 -37
- package/dist/Database.d.ts +8 -20
- package/dist/Database.js +61 -59
- package/dist/InstanceCache.d.ts +6 -2
- package/dist/InstanceCache.js +18 -16
- package/dist/Model.d.ts +17 -7
- package/dist/Model.js +17 -7
- package/dist/ModelRegistry.d.ts +4 -0
- package/dist/ModelRegistry.js +17 -15
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/SyncClient.d.ts +5 -15
- package/dist/SyncClient.js +60 -57
- package/dist/client/Ablo.js +30 -19
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +8 -2
- package/dist/client/createModelProxy.d.ts +21 -1
- package/dist/client/createModelProxy.js +123 -57
- package/dist/client/humans.d.ts +30 -9
- package/dist/client/humans.js +45 -19
- package/dist/client/reactiveEngine.d.ts +16 -11
- package/dist/client/reactiveEngine.js +113 -335
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +23 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/docs/catalog.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/query/client.d.ts +3 -0
- package/dist/query/client.js +6 -5
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +5 -2
- package/dist/react/context.d.ts +2 -2
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAblo.d.ts +9 -2
- package/dist/react/useAblo.js +25 -7
- package/dist/schema/coordination.js +5 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/select.js +3 -0
- package/dist/schema/serialize.js +3 -0
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +13 -12
- package/dist/{core → stores}/StoreManager.d.ts +5 -3
- package/dist/{core → stores}/StoreManager.js +24 -22
- package/dist/stores/SyncActionStore.d.ts +3 -1
- package/dist/stores/SyncActionStore.js +9 -7
- package/dist/sync/BootstrapFetcher.d.ts +4 -0
- package/dist/sync/BootstrapFetcher.js +28 -26
- package/dist/sync/OnDemandLoader.d.ts +3 -0
- package/dist/sync/OnDemandLoader.js +1 -0
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +19 -2
- package/dist/sync/participants.js +3 -1
- package/dist/sync/schemas.d.ts +2 -1
- package/dist/sync/schemas.js +3 -3
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/auth/capability.d.ts +35 -0
- package/dist/transaction/auth/capability.js +25 -0
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/index.d.ts +4 -4
- package/dist/transaction/coordination/index.js +4 -3
- package/dist/transaction/coordination/locator.d.ts +23 -2
- package/dist/transaction/coordination/locator.js +22 -2
- package/dist/transaction/coordination/schema.d.ts +125 -62
- package/dist/transaction/coordination/schema.js +228 -64
- package/dist/transaction/coordination/targetConflict.js +32 -28
- package/dist/transaction/errorCodes.d.ts +2 -2
- package/dist/transaction/errorCodes.js +13 -9
- package/dist/transaction/plugin.d.ts +95 -2
- package/dist/transaction/plugin.js +21 -2
- package/dist/transaction/resources/httpResources.d.ts +57 -2
- package/dist/transaction/resources/modelOperations.d.ts +148 -40
- package/dist/transaction/resources/where.d.ts +16 -0
- package/dist/transaction/resources/where.js +45 -0
- package/dist/transaction/schema/field.d.ts +12 -18
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/openapi.d.ts +16 -15
- package/dist/transaction/schema/openapi.js +186 -25
- package/dist/transaction/schema/relation.d.ts +7 -2
- package/dist/transaction/schema/schema.d.ts +27 -0
- package/dist/transaction/schema/schema.js +20 -0
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
- package/dist/transaction/transport/httpClient.d.ts +9 -1
- package/dist/transaction/transport/httpClient.js +1 -0
- package/dist/transaction/transport/httpTransport.js +156 -44
- package/dist/transaction/transport/wsTransport.d.ts +2 -4
- package/dist/transaction/transport/wsTransport.js +16 -10
- package/dist/transaction/types/streams.d.ts +10 -0
- package/dist/transaction/utils/duration.d.ts +25 -0
- package/dist/transaction/utils/duration.js +32 -0
- package/dist/transaction/wire/accountResponses.d.ts +69 -0
- package/dist/transaction/wire/accountResponses.js +36 -1
- package/dist/transaction/wire/auth.d.ts +9 -2
- package/dist/transaction/wire/auth.js +7 -1
- package/dist/transaction/wire/claims.d.ts +164 -97
- package/dist/transaction/wire/claims.js +126 -28
- package/dist/transaction/wire/commit.d.ts +1 -1
- package/dist/transaction/wire/feedEvent.d.ts +27 -0
- package/dist/transaction/wire/feedEvent.js +27 -1
- package/dist/transaction/wire/frames.d.ts +2 -2
- package/dist/transaction/wire/inboundFrames.d.ts +12 -2
- package/dist/transaction/wire/index.d.ts +10 -6
- package/dist/transaction/wire/index.js +12 -3
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
- package/dist/transactions/mutations/MutationQueue.js +55 -45
- package/dist/transactions/mutations/commitPayload.d.ts +3 -2
- package/dist/transactions/mutations/commitPayload.js +5 -5
- package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
- package/dist/transactions/mutations/deltaConfirmation.js +10 -8
- package/dist/transactions/mutations/replayValidation.d.ts +2 -1
- package/dist/transactions/mutations/replayValidation.js +3 -2
- package/dist/{core → views}/QueryView.d.ts +1 -1
- package/dist/{core → views}/QueryView.js +1 -1
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/docs/agents.md +1 -1
- package/docs/api-keys.md +6 -6
- package/docs/api.md +5 -43
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +4 -4
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +235 -83
- package/docs/data-sources.md +4 -4
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +29 -5
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +22 -23
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +14 -3
- package/docs/mcp.md +7 -7
- package/docs/migration.md +34 -15
- package/docs/projects.md +1 -1
- package/docs/react.md +19 -8
- package/docs/sessions.md +1 -1
- package/docs/webhooks.md +9 -9
- package/llms.txt +4 -4
- package/package.json +13 -20
- package/dist/cli.cjs +0 -288600
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/docs/interaction-model.md +0 -99
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{source → transaction}/footprint.d.ts +0 -0
- /package/dist/{source → transaction}/footprint.js +0 -0
- /package/dist/{core → views}/ViewRegistry.js +0 -0
package/docs/deployment.md
CHANGED
|
@@ -57,10 +57,10 @@ A **plane** is the isolation unit a credential acts on. `production` is the root
|
|
|
57
57
|
plane; every sandbox sits beside it. Three things are per-plane, and knowing
|
|
58
58
|
which three is most of what production readiness means:
|
|
59
59
|
|
|
60
|
-
- **Rows
|
|
61
|
-
- **The registered database
|
|
60
|
+
- **Rows:** a sandbox write is invisible to production and to every other sandbox.
|
|
61
|
+
- **The registered database:** one per plane, so your production database and
|
|
62
62
|
your dev database are separate registrations.
|
|
63
|
-
- **The active schema artifact
|
|
63
|
+
- **The active schema artifact:** the model shapes the engine actually routes on.
|
|
64
64
|
|
|
65
65
|
A key's plane is fixed at mint and spelled in its prefix: `sk_live_` acts on
|
|
66
66
|
production, `sk_test_` on a sandbox. There is no runtime override — the
|
|
@@ -219,14 +219,14 @@ handler, the Standard Webhooks signature, and rolling a secret.
|
|
|
219
219
|
|
|
220
220
|
## What to watch once it is live
|
|
221
221
|
|
|
222
|
-
- **`ablo logs
|
|
222
|
+
- **`ablo logs`:** commit activity as it happens, scoped by the key, so a live
|
|
223
223
|
key streams the org and a test key streams only its sandbox. `--json` emits
|
|
224
224
|
NDJSON for piping.
|
|
225
|
-
- **`ablo status
|
|
225
|
+
- **`ablo status`:** the readiness verdict. Cheap enough to run from a health
|
|
226
226
|
check on your own side.
|
|
227
|
-
- **The [audit log](./audit.md)
|
|
227
|
+
- **The [audit log](./audit.md):** every confirmed write traced back to the key
|
|
228
228
|
that made it and the person who authorized that key.
|
|
229
|
-
- **Your own logger
|
|
229
|
+
- **Your own logger:** pass `logger` to the client and SDK lifecycle, sync,
|
|
230
230
|
retry, and rollback events join your existing pipeline.
|
|
231
231
|
|
|
232
232
|
Writes carry receipts rather than being fire-and-forget: a commit is accepted the
|
|
@@ -242,7 +242,7 @@ and what each promises.
|
|
|
242
242
|
| `password authentication failed` during connect | Often a pooled host refusing a session it cannot serve, in the words of a wrong password. | Register the direct database host. |
|
|
243
243
|
| `server_execute_unknown_model` | The plane's active schema does not carry that model. | `ablo push` with a key for that plane. |
|
|
244
244
|
| Clients rejected at connect | The deployed schema and the client's schema disagree. | Push this tree, or deploy the revision the server is running. |
|
|
245
|
-
| `project_scope_denied` (403) | The model belongs to another project in your org. | Use a key minted for that project
|
|
245
|
+
| `project_scope_denied` (403) | The model belongs to another project in your org. | Use a key minted for that project: a push cannot cross projects. |
|
|
246
246
|
| 403 on `ablo push` | The key authenticated but cannot author schema. | A secret `sk_live_`; the `ablo login` live key is observe-only. |
|
|
247
247
|
|
|
248
248
|
## The checklist
|
|
@@ -15,7 +15,7 @@ The three steps below show how to declare it, scope the agent, and write.
|
|
|
15
15
|
|
|
16
16
|
See [Identity & Sync Groups](../identity.md) for the full reference.
|
|
17
17
|
|
|
18
|
-
## 1. Schema
|
|
18
|
+
## 1. Schema: declare the relationship, once
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
import { defineSchema, identityRole, model, relation, z } from '@abloatai/ablo/schema';
|
|
@@ -44,7 +44,7 @@ export const schema = defineSchema(
|
|
|
44
44
|
);
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
## 2. Dispatch
|
|
47
|
+
## 2. Dispatch: narrow the agent to the workspace it's working on
|
|
48
48
|
|
|
49
49
|
An agent can never reach more than the user who triggered it — that's the upper
|
|
50
50
|
limit. From there you narrow it to a single workspace by minting the agent's session
|
|
@@ -95,7 +95,7 @@ const ablo = Ablo({
|
|
|
95
95
|
groups the session asks for with the groups the identity is actually allowed, so
|
|
96
96
|
the agent can never reach a workspace its triggering user couldn't.
|
|
97
97
|
|
|
98
|
-
## 3. Write
|
|
98
|
+
## 3. Write: it fans out to everyone on that workspace
|
|
99
99
|
|
|
100
100
|
Inside any component under the provider, grab the scoped client with `useAblo()`
|
|
101
101
|
and write. The connection is already narrowed to `workspace:<workspaceId>` from Step 2.
|
package/docs/groups.md
CHANGED
|
@@ -38,6 +38,56 @@ never persists work built on a premise it can no longer see.
|
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
+
## How you hear about it
|
|
42
|
+
|
|
43
|
+
Four channels carry "something changed", and they answer four different
|
|
44
|
+
questions. Pick by the question you have.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// A screen that stays current.
|
|
48
|
+
ablo.documents.onChange((docs) => render(docs));
|
|
49
|
+
|
|
50
|
+
// Who else is in here, and what are they holding.
|
|
51
|
+
await using room = await ablo.documents.join(documentIds, { ttl: '5m' });
|
|
52
|
+
room.peers;
|
|
53
|
+
|
|
54
|
+
// Stop this write if the thing I read moved while I composed it.
|
|
55
|
+
await ablo.blocks.update({ id, data, reads: [{ group: 'workspace:abc', readAt, onStale: 'notify' }] });
|
|
56
|
+
|
|
57
|
+
// Tell me later if this moves, even though I am not writing now.
|
|
58
|
+
await ablo.documents.track({ id: 's-1' });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Question | Channel | Arrives |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| What do the rows say right now? | `onChange` | As deltas land, on the socket |
|
|
64
|
+
| Who else is working here? | `join`, then `room.peers` and `room.claims` | As participants come and go, on the socket |
|
|
65
|
+
| Did the premise for **this** write move? | `reads` on the write | On that write's receipt, before it applies |
|
|
66
|
+
| Has anything I read moved since? | `track` | On your next commit's receipt |
|
|
67
|
+
|
|
68
|
+
Two distinctions do most of the work here.
|
|
69
|
+
|
|
70
|
+
**`join` is about people; `track` is about data.** Both open a subscription and
|
|
71
|
+
both are scoped by sync group, which is why they look alike. `join` reports
|
|
72
|
+
participants: who is present, what they are doing, which rows they hold. `track`
|
|
73
|
+
reports the rows themselves: something you said you cared about moved, here is
|
|
74
|
+
the watermark to re-read it at. A tool that wants to avoid duplicating a peer's
|
|
75
|
+
work needs `join`. A tool whose output goes stale when its inputs change needs
|
|
76
|
+
`track`.
|
|
77
|
+
|
|
78
|
+
**`reads` guards one write; `track` outlives it.** They speak the same
|
|
79
|
+
vocabulary and produce the same `StaleNotification`. A `reads` entry is checked
|
|
80
|
+
once, at the commit that carried it, and discarded. A `track` is persisted and
|
|
81
|
+
re-checked against every delta after it, so a long-running actor hears about a
|
|
82
|
+
change that landed while it was thinking, on the next commit it makes.
|
|
83
|
+
|
|
84
|
+
`onChange` and `join` need a live socket, so they are available on the default
|
|
85
|
+
WebSocket client. `reads` and `track` ride the commit, so they reach a socketless
|
|
86
|
+
actor over HTTP too, which is what makes them the notification path for agents
|
|
87
|
+
and workers.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
41
91
|
## Three ways a change reaches other rows
|
|
42
92
|
|
|
43
93
|
"A affects B and C" means three different things. The engine does the first two
|
|
@@ -206,8 +256,12 @@ them coarse everywhere else.
|
|
|
206
256
|
|
|
207
257
|
## Where this is defined
|
|
208
258
|
|
|
209
|
-
- **Access
|
|
210
|
-
|
|
259
|
+
- **Access**, meaning who may read or write a group, is
|
|
260
|
+
[`identity.md`](./identity.md).
|
|
261
|
+
- **The convention** behind non-coercion, the premise, and the notification is
|
|
211
262
|
[`concurrency-convention.md`](./concurrency-convention.md) (§4 and §5).
|
|
212
|
-
- **The mechanics
|
|
263
|
+
- **The mechanics**, the three coordination blocks underneath, are
|
|
213
264
|
[`coordination.md`](./coordination.md).
|
|
265
|
+
- **`join` and presence**, the participant half of the table above, are
|
|
266
|
+
[`coordination.md`](./coordination.md) for the claim stream and
|
|
267
|
+
[`react.md`](./react.md) for `useJoin`.
|
package/docs/guarantees.md
CHANGED
|
@@ -65,11 +65,13 @@ await ablo.weatherReports.update({
|
|
|
65
65
|
`onStale: 'reject'` prevents lost updates. If the target changed after the
|
|
66
66
|
snapshot, the server rejects the write instead of applying stale reasoning.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
Two other dispositions exist. `overwrite` applies the write with no stale check
|
|
69
|
+
at all. `notify` **holds** the write, so the row is left as it stands, and hands
|
|
70
|
+
back a `StaleNotification` carrying the current value for the actor to reconcile
|
|
71
|
+
and re-issue; the rest of the batch still commits.
|
|
69
72
|
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
- `notify` accepts the write and marks it for product review.
|
|
73
|
+
See [Concurrency Convention](./concurrency-convention.md) for the full taxonomy,
|
|
74
|
+
what each disposition is checked against, and where the convention stops.
|
|
73
75
|
|
|
74
76
|
## Claim Coordination
|
|
75
77
|
|
|
@@ -99,14 +101,39 @@ Agents should import the same schema as the app and write through
|
|
|
99
101
|
|
|
100
102
|
## Audit Trail
|
|
101
103
|
|
|
102
|
-
|
|
104
|
+
Attribution is not a separate log you opt into. It rides on the change itself.
|
|
105
|
+
Every broadcast delta names the actor, the authority it acted under, the
|
|
106
|
+
credential that authorized it, and the approval stage it was in:
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
```ts
|
|
109
|
+
{
|
|
110
|
+
modelName: 'weatherReports',
|
|
111
|
+
modelId: 'report_stockholm',
|
|
112
|
+
actionType: 'U',
|
|
113
|
+
actor: { kind: 'agent', id: 'weather-agent-v3' },
|
|
114
|
+
onBehalfOf: { kind: 'user', id: 'user_8f2a' },
|
|
115
|
+
capabilityId: '…', // the key the write was authorized by
|
|
116
|
+
confirmationState: 'auto', // previewed | approved | required_human_approval
|
|
117
|
+
createdAt: '2026-05-14T14:22:01.034Z',
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`actor` and `onBehalfOf` are derived from the credential, not from the call site,
|
|
122
|
+
so an agent cannot name a different actor in its own write. `capabilityId` is
|
|
123
|
+
non-null for every agent and system commit, so a write can always be traced to
|
|
124
|
+
the key that made it, and from that key to the person it was issued to.
|
|
125
|
+
|
|
126
|
+
The stored history goes one step further than recording. Audit rows are chained
|
|
127
|
+
with a keyed hash, so the log is tamper-*evident*: `verify-chain` walks the chain
|
|
128
|
+
and, if it breaks, names the sequence number and the hashes that disagree. No
|
|
129
|
+
chain roots at an agent. The delegation root is always the person who set the
|
|
130
|
+
work in motion.
|
|
131
|
+
|
|
132
|
+
For agent work this is what answers, after the fact: what changed, who authorized
|
|
133
|
+
it, which run did it, and whether a human was in the loop.
|
|
107
134
|
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
See [Audit Log](./audit.md) for the stored row shape, the filters, verification,
|
|
136
|
+
and export.
|
|
110
137
|
|
|
111
138
|
## Persistence
|
|
112
139
|
|
package/docs/how-it-works.md
CHANGED
|
@@ -13,13 +13,13 @@ await ablo.tasks.update({ id: 'task_42', data: { status: 'done' }, wait: 'confir
|
|
|
13
13
|
const task = ablo.tasks.local.retrieve('task_42');
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
## The mental model
|
|
16
|
+
## The mental model: read this once
|
|
17
17
|
|
|
18
18
|
Ablo is a **coordination layer in front of your Postgres**. Agents, background
|
|
19
19
|
jobs, and the people alongside them all change the same application data through
|
|
20
20
|
one API, and Ablo makes sure their writes don't clobber each other.
|
|
21
21
|
|
|
22
|
-
- **Writes go through Ablo
|
|
22
|
+
- **Writes go through Ablo:** `ablo.<model>.create / update / delete` enter Ablo's
|
|
23
23
|
commit chokepoint — where claims, ordering, and idempotency are enforced — and
|
|
24
24
|
Ablo applies the change to your Postgres through a scoped writer role. The commit
|
|
25
25
|
is accepted (`queued`) the moment Ablo takes it.
|
|
@@ -37,17 +37,41 @@ one API, and Ablo makes sure their writes don't clobber each other.
|
|
|
37
37
|
That's the shape: **you write through Ablo → it lands in your Postgres → the WAL
|
|
38
38
|
echo confirms it → everyone connected sees it live.**
|
|
39
39
|
|
|
40
|
+
## The primitives
|
|
41
|
+
|
|
42
|
+
| Primitive | Plane | Purpose |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `Schema` | State | Declares typed models the app and agents can read and write. |
|
|
45
|
+
| `Model` | State | The generated `ablo.<model>` model. Use `retrieve`/`list` (async reads), `local.retrieve`/`local.list`/`local.count` (the same verbs, synchronous and local-only), `create`, `update`, and `delete`. |
|
|
46
|
+
| `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim({ id })` and read via `ablo.<model>.claim.state({ id })`. Ephemeral, never persisted. |
|
|
47
|
+
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
48
|
+
| `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
|
|
49
|
+
|
|
50
|
+
### Why each primitive is separate
|
|
51
|
+
|
|
52
|
+
Why are `Claim`, `Commit`, and `Receipt` separate things instead of one? Each
|
|
53
|
+
does a job the others cannot. If you are coming from Replicache or Yjs you would
|
|
54
|
+
expect just `Commit`. Here is what the other two buy you over that minimum:
|
|
55
|
+
|
|
56
|
+
- **`Claim` is not a read lock.** Reads stay open. Claims serialize
|
|
57
|
+
acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
|
|
58
|
+
from fresh state.
|
|
59
|
+
- **`Receipt` is not a `200 OK`.** It is the durable artifact a commit produced:
|
|
60
|
+
accepted commit id, server-assigned timestamps, stale-check outcome. It is
|
|
61
|
+
addressable after the fact and replayable into a different client. A status
|
|
62
|
+
code cannot be re-read by a sub-agent that was not on the original call.
|
|
63
|
+
|
|
40
64
|
## Where your data lives
|
|
41
65
|
|
|
42
66
|
You point Ablo at a Postgres database, and that's where its rows live. Only *which*
|
|
43
67
|
database differs by environment — the code is identical.
|
|
44
68
|
|
|
45
|
-
- **Production
|
|
69
|
+
- **Production:** your Postgres. `ablo connect` sets up a scoped writer role and
|
|
46
70
|
logical replication; your rows live in your database, and Ablo writes to them
|
|
47
71
|
through that role.
|
|
48
|
-
- **Sandbox and local dev
|
|
72
|
+
- **Sandbox and local dev:** a separate or local Postgres you can throw away. Same
|
|
49
73
|
models, same code, a different database behind them.
|
|
50
|
-
- **Before you connect one
|
|
74
|
+
- **Before you connect one.** Ablo keeps state in its own log, so you can build the
|
|
51
75
|
whole app today and point it at a real database when you're ready.
|
|
52
76
|
|
|
53
77
|
Registering the database is the whole switch. There is no tier or flag to choose.
|
package/docs/idempotency.md
CHANGED
|
@@ -35,10 +35,10 @@ The key is not a lookup that happens before the write — it is the **execution
|
|
|
35
35
|
itself**. Ablo inserts a pending row keyed by the caller and the key inside the same transaction as
|
|
36
36
|
the mutation, and a unique index makes that insert the lock:
|
|
37
37
|
|
|
38
|
-
- **Insert wins
|
|
39
|
-
- **Insert conflicts
|
|
38
|
+
- **Insert wins:** this transaction owns the execution and runs the write.
|
|
39
|
+
- **Insert conflicts:** someone else owns it. The second caller waits for the owner to finish and
|
|
40
40
|
then replays its recorded result.
|
|
41
|
-
- **Insert conflicts, different request
|
|
41
|
+
- **Insert conflicts, different request:** the key was reused to mean something else. Rejected.
|
|
42
42
|
|
|
43
43
|
Because the lock and the write share a transaction, there is no window in which a write has happened
|
|
44
44
|
but its key has not been recorded.
|
|
@@ -52,7 +52,7 @@ same key string without colliding, and one agent can never replay another's resu
|
|
|
52
52
|
|---|---|
|
|
53
53
|
| A new key | Runs the write. |
|
|
54
54
|
| The same key, the same request, already finished | Replays the recorded result. The write does not run again. |
|
|
55
|
-
| The same key, the same request, still running | Waits for the in-flight attempt, then replays its result. If the original is still running after a short wait, rejects with `idempotency_conflict` (409)
|
|
55
|
+
| The same key, the same request, still running | Waits for the in-flight attempt, then replays its result. If the original is still running after a short wait, rejects with `idempotency_conflict` (409): retry the same key. |
|
|
56
56
|
| The same key, a **different** request | Rejects with `idempotency_conflict` (409). A key is bound to the request it first arrived with. |
|
|
57
57
|
|
|
58
58
|
Both conflict cases return the same code, so tell them apart by what your own
|
|
@@ -60,7 +60,7 @@ client did. If you retried an identical request, the original is still in flight
|
|
|
60
60
|
— wait and retry the same key. If you changed the request, that is a client bug:
|
|
61
61
|
use a new key.
|
|
62
62
|
|
|
63
|
-
## Failures are not replayed
|
|
63
|
+
## Failures are not replayed: they re-run
|
|
64
64
|
|
|
65
65
|
This is where Ablo deliberately differs from Stripe and from most payment APIs, and it is the
|
|
66
66
|
behaviour most likely to surprise you.
|
|
@@ -106,7 +106,7 @@ may already have committed. Restore the original route and retry the same key.
|
|
|
106
106
|
| Timeout or dropped connection, no response | Retry with the **same** key, with backoff. You get the recorded success, or the write runs now. |
|
|
107
107
|
| `source_unreachable` (503) | Retry with the **same** key once connectivity recovers. The write stays pinned to its route. |
|
|
108
108
|
| `replication_lag_timeout` (504) | The write may have materialized. Retry with the **same** key, or wait for source ingestion to catch up. |
|
|
109
|
-
| `AbloStaleContextError` | Re-read the row, regenerate, then write under a **new** key
|
|
109
|
+
| `AbloStaleContextError` | Re-read the row, regenerate, then write under a **new** key: the new write is a new intention. |
|
|
110
110
|
| `AbloClaimedError` | Someone else holds the row. Wait or yield; the key is unused, so reuse it when you retry. |
|
|
111
111
|
| `idempotency_conflict` (409) after an identical retry | The original is still in flight. Wait, then retry the **same** key. |
|
|
112
112
|
| `idempotency_conflict` (409) after changing the request | A client bug: a key is bound to the first request sent under it. Use a **new** key. |
|
package/docs/identity.md
CHANGED
|
@@ -24,9 +24,9 @@ that.
|
|
|
24
24
|
A **sync group** is a named channel of shared state — a string like
|
|
25
25
|
`org:acme` or `workspace:abc123`. It is simultaneously:
|
|
26
26
|
|
|
27
|
-
- **the unit of fan-out
|
|
27
|
+
- **the unit of fan-out:** a confirmed write to a row publishes a delta to
|
|
28
28
|
every participant subscribed to that row's sync group(s), and
|
|
29
|
-
- **the unit of access
|
|
29
|
+
- **the unit of access:** a participant receives a row's deltas *only if* the
|
|
30
30
|
row's sync group is in their allowed set.
|
|
31
31
|
|
|
32
32
|
There is no built-in `org` / `team` / `user` concept in the engine. Those are
|
|
@@ -101,16 +101,16 @@ const ablo = Ablo({ schema, apiKey: session.token });
|
|
|
101
101
|
|
|
102
102
|
That's the whole surface. The rest of this doc is the *why* behind each line.
|
|
103
103
|
|
|
104
|
-
## Two kinds of group
|
|
104
|
+
## Two kinds of group: the whole mental model
|
|
105
105
|
|
|
106
106
|
You just saw a human get `org` / `team` groups and an agent get one `workspace`
|
|
107
107
|
group. That split is the model. Every sync group is named after one of two
|
|
108
108
|
things:
|
|
109
109
|
|
|
110
|
-
- **Membership groups
|
|
110
|
+
- **Membership groups:** named after *who you are*: `org:{id}`, `team:{id}`,
|
|
111
111
|
`user:{id}`. Produced from **identity** (`identityRoles`, Half 1). They're
|
|
112
112
|
standing and durable — they don't change as you work.
|
|
113
|
-
- **Entity groups
|
|
113
|
+
- **Entity groups:** named after *a thing*: `dataroom:{id}`, `workspace:{id}`,
|
|
114
114
|
`document:{id}`. Produced from a **row's id** (a model's entity scope, Half 2).
|
|
115
115
|
They're granular — one per record — and any participant can be pointed at a
|
|
116
116
|
specific set of them.
|
|
@@ -122,8 +122,8 @@ they are, so you declare them once in the schema.
|
|
|
122
122
|
|
|
123
123
|
| | Subscribed by | Declared where | Gets |
|
|
124
124
|
| --- | --- | --- | --- |
|
|
125
|
-
| **Human** | *who they are
|
|
126
|
-
| **Agent** | *what it's been given
|
|
125
|
+
| **Human** | *who they are*: membership | **the schema** (`identityRoles`): a rule, written once | every `org` / `team` / `user` group their identity implies: their whole standing world |
|
|
126
|
+
| **Agent** | *what it's been given*: entities | **code, at the spawn site**: chosen per run | a handful of entity groups: the dataroom it's in, the documents it has read: never beyond what its user's membership could reach |
|
|
127
127
|
|
|
128
128
|
> **One line:** humans subscribe by who they are; agents subscribe by what
|
|
129
129
|
> they've been given.
|
|
@@ -197,7 +197,7 @@ Scoping is two declarations that meet in the middle. One describes the
|
|
|
197
197
|
(which group does this row belong to?). A participant sees a row **iff** the
|
|
198
198
|
row's sync group is in the participant's allowed set.
|
|
199
199
|
|
|
200
|
-
### Half 1
|
|
200
|
+
### Half 1 (`identityRoles`): identity → allowed groups
|
|
201
201
|
|
|
202
202
|
Declared once, on the schema, via the `identityRole({ kind, source })` factory.
|
|
203
203
|
Each role is **pure data**: a `kind` (the group's prefix — `org`, `user`, `team`)
|
|
@@ -238,7 +238,7 @@ in-process and on a hosted server that only ever sees the compiled JSON.
|
|
|
238
238
|
> `user:{id}` role above already covers it — see
|
|
239
239
|
> [Agents are participants too](#agents-are-participants-too).
|
|
240
240
|
|
|
241
|
-
### Half 2
|
|
241
|
+
### Half 2 (per-model scope): row → group
|
|
242
242
|
|
|
243
243
|
You never write a sync-group string for a row. You declare a model's *place* in
|
|
244
244
|
the entity graph and the engine derives the groups its rows fan out on. Three
|
|
@@ -310,7 +310,7 @@ options as `tenancy-option-removed` errors and steers you to `policy: { by:
|
|
|
310
310
|
explicit `policy: { by: 'none' }`. See
|
|
311
311
|
`packages/sync-engine/src/schema/model.ts` for the full option set.
|
|
312
312
|
|
|
313
|
-
## How identity reaches Ablo
|
|
313
|
+
## How identity reaches Ablo: the proxy model
|
|
314
314
|
|
|
315
315
|
This is the part the README's "authenticates with the signed-in user's
|
|
316
316
|
session" glossed over. Concretely:
|
|
@@ -400,9 +400,9 @@ What carries identity — and just as importantly, what does *not* set the bound
|
|
|
400
400
|
|
|
401
401
|
| Where | Purpose |
|
|
402
402
|
| ------------ | ------------------------------------------------------------------------------------------------ |
|
|
403
|
-
| `userId` prop | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary
|
|
403
|
+
| `userId` prop | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary: the server enforces scope from the authenticated request. |
|
|
404
404
|
| `teamIds` (on the client) | Team ids expanded into team sync groups via your `identityRoles`. |
|
|
405
|
-
| `syncGroups` (at session mint) | Optional. **Narrows** a minted session's subscription to a subset of what auth already allows
|
|
405
|
+
| `syncGroups` (at session mint) | Optional. **Narrows** a minted session's subscription to a subset of what auth already allows: it can never widen it. Passed to `sessions.create({ user \| agent, syncGroups })`; build entries with `syncGroup(kind, id)`. Use it to scope an agent (or a focused page's session) to one entity, e.g. `[syncGroup('workspace', 'abc123')]`. |
|
|
406
406
|
|
|
407
407
|
Because the server is the boundary, a client that changes `userId` to another
|
|
408
408
|
user's id does not gain their data — the server resolves and enforces the real
|
|
@@ -431,7 +431,7 @@ agent authority = (triggering user's allowed set) ← ceiling, inherited (on-
|
|
|
431
431
|
```
|
|
432
432
|
|
|
433
433
|
Concretely: each model an agent edits declares a `scope`
|
|
434
|
-
([Half 2](#half-2
|
|
434
|
+
([Half 2](#half-2-per-model-scope-row--group)), so each row forms its own
|
|
435
435
|
group. The agent subscribes only to the groups for the rows it touches. Declare
|
|
436
436
|
an entity anchor on the models an agent operates on:
|
|
437
437
|
|
|
@@ -475,22 +475,21 @@ not *what's reachable*.
|
|
|
475
475
|
Three rules make agent access safe, and they fall out of the model above rather
|
|
476
476
|
than needing a separate agent permission system:
|
|
477
477
|
|
|
478
|
-
- **Inherit the user, and no more
|
|
478
|
+
- **Inherit the user, and no more:** the OAuth
|
|
479
479
|
[on-behalf-of](https://workos.com/blog/oauth-on-behalf-of-ai-agents) model: the
|
|
480
480
|
agent's reach is tied to the consenting user, never the org.
|
|
481
|
-
- **Least privilege, just-in-time
|
|
481
|
+
- **Least privilege, just-in-time:** scoped to the task's entities, not standing
|
|
482
482
|
org-wide access (the over-privilege pattern
|
|
483
483
|
[OWASP's NHI Top 10](https://www.token.security/assets/the-ultimate-non-human-identity-security-guide)
|
|
484
484
|
flags as the dominant agent risk).
|
|
485
|
-
- **Dual-principal attribution
|
|
485
|
+
- **Dual-principal attribution:** record both the executing agent and the
|
|
486
486
|
triggering human.
|
|
487
487
|
|
|
488
488
|
Identity is 1:1 with a human participant; authority is narrowed to the work. That
|
|
489
489
|
split is what lets Ablo keep *one model API for every actor* without ever
|
|
490
490
|
granting an agent standing access to everything its user can see. The agent that
|
|
491
|
-
runs the [
|
|
492
|
-
|
|
493
|
-
it claimed.
|
|
491
|
+
runs the [Coordination](./coordination.md) `claim` loop is, to the scoping layer,
|
|
492
|
+
that same participant — scoped to the row it claimed.
|
|
494
493
|
|
|
495
494
|
## Narrowing to specific entities
|
|
496
495
|
|
|
@@ -530,7 +529,7 @@ an agent pointed at the entities it's working on. You **never hand-write**
|
|
|
530
529
|
|
|
531
530
|
> **`groups.root` is the schema model option, not a client setting.**
|
|
532
531
|
> `groups: { root: 'workspace' }` in `model(...)` declares a scope root
|
|
533
|
-
> ([Half 2](#half-2
|
|
532
|
+
> ([Half 2](#half-2-per-model-scope-row--group)) — it names the group
|
|
534
533
|
> (`workspace:<id>`) that the mechanisms above then subscribe to.
|
|
535
534
|
> There is no `Ablo({ scope })` constructor option. The lifecycle filter on
|
|
536
535
|
> [`list()`](./api.md#model-methods) is a separate axis named **`state`**
|
|
@@ -543,7 +542,7 @@ an agent pointed at the entities it's working on. You **never hand-write**
|
|
|
543
542
|
> can't reach a workspace its capability doesn't already permit, no matter what it
|
|
544
543
|
> passes. Smaller bootstrap, less fan-out, same server-enforced boundary.
|
|
545
544
|
|
|
546
|
-
## How this compares
|
|
545
|
+
## How this compares, and the best practices it follows
|
|
547
546
|
|
|
548
547
|
Ablo's identity model is not novel; it's the convergent answer every serious
|
|
549
548
|
realtime / sync SDK arrived at. Knowing which industry pattern it *is* tells you
|
|
@@ -551,7 +550,7 @@ how to reason about it.
|
|
|
551
550
|
|
|
552
551
|
**Realtime authorization splits into two shapes.** Ablo is firmly in the first:
|
|
553
552
|
|
|
554
|
-
- **Server derives scope from authenticated identity
|
|
553
|
+
- **Server derives scope from authenticated identity:** the server decides what
|
|
555
554
|
a participant may read/write and the client cannot override it. This is Ablo's
|
|
556
555
|
proxy model. It's the same shape as
|
|
557
556
|
[Supabase Realtime's RLS-on-connect](https://supabase.com/docs/guides/realtime/authorization)
|
|
@@ -560,7 +559,7 @@ how to reason about it.
|
|
|
560
559
|
checks the permissions for you" — recommended for production), and
|
|
561
560
|
[ElectricSQL **proxy auth**](https://electric-sql.com/docs/guides/auth) (a
|
|
562
561
|
reverse-proxy sets shape params server-side before forwarding).
|
|
563
|
-
- **Client proposes, server authorizes the exact request
|
|
562
|
+
- **Client proposes, server authorizes the exact request:** the client names
|
|
564
563
|
the room/shape and the server signs off, as in
|
|
565
564
|
[Pusher's channel authorization endpoint](https://pusher.com/docs/channels/server_api/authorizing-users/),
|
|
566
565
|
[ElectricSQL **gatekeeper auth**](https://github.com/electric-sql/electric/blob/main/examples/gatekeeper-auth/README.md),
|
package/docs/index.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ablo Docs
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Collaboration infrastructure for AI agents: one API for agents, apps, and services to claim, change, and confirm the same rows.
|
|
4
4
|
|
|
5
5
|
Two agents reach for the same row. One claims it, does slow work — an LLM call,
|
|
6
6
|
a fetch, a chain of tools — and commits. The second is neither rejected nor
|
|
@@ -110,12 +110,12 @@ Every surface reaches the same coordinated state. Pick by who is calling.
|
|
|
110
110
|
|
|
111
111
|
| Surface | Use it for |
|
|
112
112
|
|---|---|
|
|
113
|
-
| **SDK
|
|
114
|
-
| **Coordination MCP
|
|
115
|
-
| **`humans()
|
|
116
|
-
| **CLI
|
|
117
|
-
| **REST
|
|
118
|
-
| **Integration-helper MCP
|
|
113
|
+
| **SDK**: `@abloatai/ablo`, `transport: 'http'` | The agents themselves. Stateless, request/response, nothing held open. The main path. |
|
|
114
|
+
| **Coordination MCP**: `@abloatai/mcp` | An agent living inside an MCP host that needs claim and commit as tools. A data plane. |
|
|
115
|
+
| **`humans()`**: with `@abloatai/ablo/react` | The interfaces a person watches agent work arrive in: presence, live queries, a local copy. |
|
|
116
|
+
| **CLI**: `ablo` | Scaffolding, schema push, connecting a database. Terminals and CI. |
|
|
117
|
+
| **REST**: `/api/v1` | Runtimes with no SDK. |
|
|
118
|
+
| **Integration-helper MCP**: hosted `/api/mcp` | Teaching a coding assistant the SDK while you build. Docs, lint, and scaffolds only. |
|
|
119
119
|
|
|
120
120
|
The two MCP servers are not interchangeable. The coordination server changes
|
|
121
121
|
your data; the integration-helper server serves documentation and has no
|
|
@@ -136,6 +136,7 @@ default caller, not a special one.
|
|
|
136
136
|
|
|
137
137
|
## Concepts
|
|
138
138
|
|
|
139
|
+
- [How Ablo Works](./how-it-works.md) — the mental model in one page: you write through Ablo, it lands in your Postgres, the write-ahead log confirms it. **Read this first.**
|
|
139
140
|
- [Coordination](./coordination.md) — `claim`, `claim.state`, and `claim.queue`: who holds a row, and who is waiting.
|
|
140
141
|
- [Concurrency Convention](./concurrency-convention.md) — the governing rule for how concurrent writes resolve.
|
|
141
142
|
- [Guarantees](./guarantees.md) — what a confirmed write, a stale-write rejection, and a claim each promise.
|
|
@@ -144,7 +145,6 @@ default caller, not a special one.
|
|
|
144
145
|
- [Agents](./agents.md) — the stateless participant: wake, read, claim, commit, idle.
|
|
145
146
|
- [Agent Messaging](./agent-messaging.md) — durable handoffs between agents, linked to the claim they discuss.
|
|
146
147
|
- [Identity & Sync Groups](./identity.md) — who is connecting, and which slice of state they see.
|
|
147
|
-
- [Interaction Model](./interaction-model.md) — the schema, claim, update, confirmation loop.
|
|
148
148
|
- [Change Propagation](./groups.md) — how one row's change reaches the actors that depend on it.
|
|
149
149
|
- [Client Behavior](./client-behavior.md) — options, errors, retries, timeouts, and imports.
|
|
150
150
|
|
|
@@ -11,7 +11,7 @@ backend and a database, one model at a time.
|
|
|
11
11
|
|
|
12
12
|
Three things hold no matter which actor is writing:
|
|
13
13
|
|
|
14
|
-
- **One model API for every actor
|
|
14
|
+
- **One model API for every actor:** `ablo.<model>.update(...)` is what
|
|
15
15
|
React components, server actions, background workers, and AI agents
|
|
16
16
|
all call. No separate "agent SDK," no parallel mutation path. The
|
|
17
17
|
attribution comes from the credential, not the call site.
|
|
@@ -351,8 +351,19 @@ server -> ablo.weatherReports.update(...)
|
|
|
351
351
|
Ablo coordinates those writes, fans out confirmed deltas, exposes active claims,
|
|
352
352
|
and lets callers reject stale writes with `readAt`.
|
|
353
353
|
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
A write that reaches your database some other way still reaches connected
|
|
355
|
+
clients. A `psql` session, a cron job, an admin tool, a legacy endpoint: Ablo
|
|
356
|
+
tails your write-ahead log, so a change it did not make is picked up and fanned
|
|
357
|
+
out like any other, attributed to the data source rather than to an agent.
|
|
358
|
+
|
|
359
|
+
What such a write does not get is the coordination. It never entered the commit
|
|
360
|
+
chokepoint, so no claim was checked, no `readAt` was compared, and no idempotency
|
|
361
|
+
key was honoured. It can land on top of a row an agent is holding. Route anything
|
|
362
|
+
that must respect a claim through `ablo.<model>`.
|
|
363
|
+
|
|
364
|
+
On the [Data Source endpoint fallback](./data-sources.md) there is no replication
|
|
365
|
+
stream to tail, and there the original caveat holds: a direct write stays
|
|
366
|
+
invisible until your app reports it through Data Source events.
|
|
356
367
|
|
|
357
368
|
## 6. Existing API Backend
|
|
358
369
|
|
package/docs/mcp.md
CHANGED
|
@@ -6,7 +6,7 @@ Ablo publishes **two** MCP servers for two different jobs. Don't confuse them:
|
|
|
6
6
|
|
|
7
7
|
| Server | Purpose | Auth | Tools |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
-
| **Coordination** (`@abloatai/mcp`) | Manage your Ablo the way the CLI does, and let an agent safely read & mutate application data | API key (`sk_…` / `rk_…`) | projects, schema, logs, usage
|
|
9
|
+
| **Coordination** (`@abloatai/mcp`) | Manage your Ablo the way the CLI does, and let an agent safely read & mutate application data | API key (`sk_…` / `rk_…`) | projects, schema, logs, usage: plus `get` / `list` / `create` / `update` / `delete` / `claim` / `release` over your rows |
|
|
10
10
|
| **Integration-helper** (hosted `/api/mcp`) | Help an AI coding assistant write SDK integration code that compiles | none (public docs) | doc search, export surface, schema lint, scaffold |
|
|
11
11
|
|
|
12
12
|
The coordination server manages your account **and is the data plane** — it is
|
|
@@ -37,7 +37,7 @@ claude mcp add ablo -- npx -y @abloatai/mcp
|
|
|
37
37
|
| `list_projects` | `ablo projects list` | the org's projects (needs `sk_`) |
|
|
38
38
|
| `create_project` | `ablo projects create` | create one (needs `sk_`) |
|
|
39
39
|
| `tail_logs` | `ablo logs` | recent commits and the actor behind each |
|
|
40
|
-
| `get_usage`
|
|
40
|
+
| `get_usage` |: | usage in daily buckets |
|
|
41
41
|
|
|
42
42
|
There are no key-management tools. A mint returns the plaintext once — only a
|
|
43
43
|
hash is kept — so no tool can hand it back later, and returning it at mint time
|
|
@@ -63,7 +63,7 @@ Each tool mirrors an SDK verb, scoped to a model + id. Model names come from
|
|
|
63
63
|
| `update_model` | `ablo.<model>.update({ id, … })` | guarded update |
|
|
64
64
|
| `delete_model` | `ablo.<model>.delete({ id })` | guarded delete |
|
|
65
65
|
| `claim_model` | `ablo.<model>.claim({ id })` | acquire / queue a coordination lease |
|
|
66
|
-
| `release_claim`
|
|
66
|
+
| `release_claim` |: | release the lease so others proceed |
|
|
67
67
|
|
|
68
68
|
The agent-facing contract — the safe loop, the "derive idempotency keys from
|
|
69
69
|
the business event" rule, and the error-code playbook — ships as a loadable
|
|
@@ -94,9 +94,9 @@ claude mcp add --transport http ablo https://<your-app>/api/mcp
|
|
|
94
94
|
|
|
95
95
|
The endpoint is identical for every client — only the config surface differs:
|
|
96
96
|
|
|
97
|
-
- **Claude Code
|
|
98
|
-
- **Cursor
|
|
99
|
-
- **Windsurf
|
|
97
|
+
- **Claude Code:** run the `claude mcp add` command above; verify with `/mcp list`, remove with `claude mcp remove ablo`.
|
|
98
|
+
- **Cursor:** add the server to `~/.cursor/mcp.json` (macOS / Linux), then restart.
|
|
99
|
+
- **Windsurf:** add the same JSON via Settings → Cascade → MCP, then restart.
|
|
100
100
|
|
|
101
101
|
Cursor and Windsurf use the same config shape:
|
|
102
102
|
|
|
@@ -120,7 +120,7 @@ Each client then lists the Ablo tools (`search_ablo_docs`, `get_recipe`, `get_ap
|
|
|
120
120
|
| `get_recipe` | Returns the full markdown of one doc by name (e.g. `readme`, `quickstart`, `schema-contract`, `integration-guide`, `api`, `guarantees`). |
|
|
121
121
|
| `get_api_surface` | Returns the structured export list for an SDK subpath (`@abloatai/ablo`, `./react`, `./schema`, `./testing`, …). Call with no argument to list every subpath. |
|
|
122
122
|
| `validate_schema` | Lints `defineSchema` source against the DSL rules (camelCase fields, lowercase model keys, `scope`/`grants` sync groups, valid `load` strategies, no legacy builders) and returns a structured issue list. Runs no code. |
|
|
123
|
-
| `scaffold_app` | Emits a starter file tree for a schema-first integration
|
|
123
|
+
| `scaffold_app` | Emits a starter file tree for a schema-first integration: `next`, `node-agent`, or `plain`, with a `data-source` (your own database) endpoint. |
|
|
124
124
|
|
|
125
125
|
#### Resources
|
|
126
126
|
|