@abloatai/ablo 0.3.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 +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# Integration Guide
|
|
2
|
+
|
|
3
|
+
Use this guide when you are adding Ablo to a real product, not a demo.
|
|
4
|
+
|
|
5
|
+
## Why Ablo, before the API
|
|
6
|
+
|
|
7
|
+
Ablo is a sync engine designed from the ground up for **humans and AI
|
|
8
|
+
agents editing the same state at the same time**. That premise drives
|
|
9
|
+
every design choice in this guide; if you only need server-to-server
|
|
10
|
+
data syncing without agents in the loop, the trade-offs land elsewhere
|
|
11
|
+
(Replicache, ElectricSQL, PowerSync are good answers for human-only
|
|
12
|
+
real-time apps; Zero is a good answer for query-shaped sync).
|
|
13
|
+
|
|
14
|
+
The shape of the SDK reflects three commitments:
|
|
15
|
+
|
|
16
|
+
- **One model API for every actor.** `ablo.<model>.update(...)` is what
|
|
17
|
+
React components, server actions, background workers, and AI agents
|
|
18
|
+
all call. No separate "agent SDK," no parallel mutation path. The
|
|
19
|
+
attribution comes from the credential, not the call site.
|
|
20
|
+
- **Server owns the scope convention; client picks a subset by id.** The
|
|
21
|
+
`org:` / `user:` / `team:` (or your own `region:` / `customer:`)
|
|
22
|
+
prefixes live in the schema's `identityRoles` once, never typed by
|
|
23
|
+
consumer code. Same boundary Liveblocks (`prepareSession`), PowerSync
|
|
24
|
+
(named streams), and Zero (synced queries) settled on after the same
|
|
25
|
+
realization: clients that compose scope strings drift; servers that
|
|
26
|
+
derive scope from authed identity don't.
|
|
27
|
+
- **Capabilities, not API keys, are how agents authenticate.** Static
|
|
28
|
+
API keys protect server-to-server humans. Agents get per-run,
|
|
29
|
+
per-scope, leased credentials with per-request signature verification
|
|
30
|
+
and instant revocation. The 2025-2026 AI-agent auth consensus
|
|
31
|
+
(OAuth 2.1 / MCP, AWS STS, Vault leases, Auth0 Token Vault) converged
|
|
32
|
+
on this shape. Capabilities are Ablo's instance.
|
|
33
|
+
|
|
34
|
+
If you have already built a sync layer or an agent runtime, you know
|
|
35
|
+
what each of those costs. This guide assumes you want them solved once,
|
|
36
|
+
together, behind one client.
|
|
37
|
+
|
|
38
|
+
## The integration in one diagram
|
|
39
|
+
|
|
40
|
+
The normal integration is one client:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import Ablo from '@ablo/sync-engine';
|
|
44
|
+
import { defineSchema, model, z } from '@ablo/sync-engine/schema';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Declare the models Ablo coordinates, then read and write through
|
|
48
|
+
`ablo.<model>`. React, server actions, backend workers, and agents should all use
|
|
49
|
+
that same model path.
|
|
50
|
+
|
|
51
|
+
```txt
|
|
52
|
+
schema -> ablo.<model>.load(...) -> ablo.<model>.update(...)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Capabilities, tasks, commits, and receipts exist under the hood. Most apps do
|
|
56
|
+
not create them by hand.
|
|
57
|
+
|
|
58
|
+
## Pick The Backing Mode
|
|
59
|
+
|
|
60
|
+
Every schema model has a backing store. The SDK call shape stays the same.
|
|
61
|
+
|
|
62
|
+
| Mode | Rows live in | Use when |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Ablo-managed | Ablo | New collaborative or agent-written state can live in Ablo. |
|
|
65
|
+
| Data Source | Your app database | You already have tables, service logic, and API endpoints that remain canonical. |
|
|
66
|
+
| Schema-less resource API | Custom runtime | A server worker, MCP route, or migration script intentionally cannot import the app schema. |
|
|
67
|
+
|
|
68
|
+
Do not pass a database URL to `Ablo(...)`. Application and agent code use
|
|
69
|
+
`ABLO_API_KEY`. If your database stays canonical, add a Data Source URL in Ablo
|
|
70
|
+
and keep the database credentials inside your app.
|
|
71
|
+
|
|
72
|
+
## Test With Sandboxes
|
|
73
|
+
|
|
74
|
+
Use the public `/sandbox` page to understand the state flow. It is a visual,
|
|
75
|
+
deterministic demo; it does not call your API key or mutate hosted Ablo data.
|
|
76
|
+
It is also built for coding agents: copy the sandbox prompt into Claude Code or
|
|
77
|
+
Codex and ask it to wire one real resource through the schema model API.
|
|
78
|
+
|
|
79
|
+
Use the authenticated org dashboard sandbox for real integration work. The
|
|
80
|
+
default sandbox is the equivalent of Stripe test mode:
|
|
81
|
+
|
|
82
|
+
- it is scoped to the organization,
|
|
83
|
+
- it has an isolated sync group prefix,
|
|
84
|
+
- it mints `sk_test_*` keys,
|
|
85
|
+
- it can be reset without touching live state,
|
|
86
|
+
- additional sandboxes can start blank or from copied live configuration.
|
|
87
|
+
|
|
88
|
+
Live keys and sandbox keys are separate. Use `sk_test_*` while wiring your app,
|
|
89
|
+
agents, and Data Source endpoint; move to `sk_live_*` only when the same schema
|
|
90
|
+
and write path are ready for production.
|
|
91
|
+
|
|
92
|
+
When handing this to a coding agent, give it a concrete target:
|
|
93
|
+
|
|
94
|
+
```txt
|
|
95
|
+
Add Ablo Sync to this app for one resource that humans and agents both edit.
|
|
96
|
+
Use the org sandbox sk_test_* key. Declare schema, add the Ablo client, replace
|
|
97
|
+
one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
|
|
98
|
+
wait: 'confirmed' }), and add a smoke test for two concurrent writers.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 1. Declare A Schema
|
|
102
|
+
|
|
103
|
+
Start with fields and relations. Keep load strategies, indexing hints, and
|
|
104
|
+
read-only/mutable shortcuts out of the first version unless you already need
|
|
105
|
+
offline-heavy local cache behavior.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// src/ablo.schema.ts
|
|
109
|
+
import { defineSchema, model, z } from '@ablo/sync-engine/schema';
|
|
110
|
+
|
|
111
|
+
export const schema = defineSchema(
|
|
112
|
+
{
|
|
113
|
+
tasks: model({
|
|
114
|
+
id: z.string(),
|
|
115
|
+
projectId: z.string(),
|
|
116
|
+
title: z.string(),
|
|
117
|
+
status: z.enum(['todo', 'doing', 'done']),
|
|
118
|
+
assigneeId: z.string().nullable(),
|
|
119
|
+
updatedAt: z.string(),
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
// Identity-anchored sync-group roles. The server walks these to
|
|
124
|
+
// build each participant's allowed subscription set from the
|
|
125
|
+
// resolved identity context. Templates and extractors are fully
|
|
126
|
+
// consumer-controlled — no hardcoded `org:` / `user:` convention
|
|
127
|
+
// anywhere in the engine. Omit `identityRoles` entirely if your
|
|
128
|
+
// schema doesn't need identity-derived scoping.
|
|
129
|
+
identityRoles: [
|
|
130
|
+
{
|
|
131
|
+
kind: 'tenant',
|
|
132
|
+
template: 'org:{id}',
|
|
133
|
+
extract: (i) => (i.organizationId ? [String(i.organizationId)] : []),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
kind: 'participant',
|
|
137
|
+
template: 'user:{id}',
|
|
138
|
+
extract: (i) => (i.userId ? [String(i.userId)] : []),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Declaring scope on a model
|
|
146
|
+
|
|
147
|
+
Per-row tenancy and per-entity sync-group anchors live on the
|
|
148
|
+
`defineModel` (or `model(...)`) options. The two halves compose: the
|
|
149
|
+
identity roles above produce a participant's *allowed* set; the
|
|
150
|
+
per-model options below define how rows are filtered server-side and
|
|
151
|
+
which sync-group label each row fans out on.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
model(
|
|
155
|
+
{ /* fields */ },
|
|
156
|
+
/* relations */ {},
|
|
157
|
+
{
|
|
158
|
+
// Rows carry organization_id and bootstrap filters on it.
|
|
159
|
+
orgScoped: true,
|
|
160
|
+
|
|
161
|
+
// Per-entity sync-group anchor. Lets a capability narrow into
|
|
162
|
+
// one row's scope via `syncGroupFormat.replace('{id}', rowId)`.
|
|
163
|
+
syncGroupFormat: 'matter:{id}',
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For rows that don't carry `organization_id` themselves but inherit
|
|
169
|
+
tenancy via a foreign key, use `scopedVia` instead of `orgScoped:
|
|
170
|
+
false` — the latter exposes the entire table cross-tenant. See
|
|
171
|
+
`packages/sync-engine/src/schema/model.ts` for the full option set.
|
|
172
|
+
|
|
173
|
+
## 2. Create The Client
|
|
174
|
+
|
|
175
|
+
Trusted runtimes can use `ABLO_API_KEY`.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// src/ablo.ts
|
|
179
|
+
import Ablo from '@ablo/sync-engine';
|
|
180
|
+
import { schema } from './ablo.schema';
|
|
181
|
+
|
|
182
|
+
export const ablo = Ablo({
|
|
183
|
+
schema,
|
|
184
|
+
apiKey: process.env.ABLO_API_KEY,
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Browser apps should use the React provider or a scoped session/capability, not a
|
|
189
|
+
server API key in the bundle.
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
// app/providers.tsx
|
|
193
|
+
'use client';
|
|
194
|
+
|
|
195
|
+
import { AbloProvider } from '@ablo/sync-engine/react';
|
|
196
|
+
import { schema } from '@/ablo.schema';
|
|
197
|
+
|
|
198
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
199
|
+
return <AbloProvider schema={schema}>{children}</AbloProvider>;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Why two credential shapes
|
|
204
|
+
|
|
205
|
+
`ABLO_API_KEY` is your long-lived account credential. Treat it like a
|
|
206
|
+
Stripe secret key: it stays on trusted servers, never reaches a browser
|
|
207
|
+
bundle, and signs server-to-server requests. It is the right credential
|
|
208
|
+
for trusted runtimes (Next.js server actions, background workers,
|
|
209
|
+
migration scripts) where the code reading it is yours.
|
|
210
|
+
|
|
211
|
+
A browser or an agent runtime is not that environment. The React
|
|
212
|
+
provider and the `agent.run(...)` wrapper both exchange your api key for
|
|
213
|
+
a **capability** — a short-lived, narrowly-scoped bearer token. The
|
|
214
|
+
browser holds the cap; the api key never leaves the server. The
|
|
215
|
+
exchange is the bridge between two credential shapes:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
trusted runtime browser / agent
|
|
219
|
+
ABLO_API_KEY ─exchange─► Capability token ────► narrow scope, leased
|
|
220
|
+
(long-lived, (short-lived,
|
|
221
|
+
broad scope, per-actor scope,
|
|
222
|
+
server only) revocable)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This is the same shape as Stripe's
|
|
226
|
+
ephemeral keys (Issuing Elements expires in 15 minutes) and AWS STS
|
|
227
|
+
AssumeRole (returns time-bounded creds with the minimal needed scope).
|
|
228
|
+
You never *type* a capability into your app; the SDK mints one when it
|
|
229
|
+
needs one and refreshes before expiry. See
|
|
230
|
+
[Capabilities](./capabilities.md) for the design rationale and the
|
|
231
|
+
manual create/revoke surface that custom runtimes use.
|
|
232
|
+
|
|
233
|
+
## 3. Read State
|
|
234
|
+
|
|
235
|
+
Use `load` when the row may not already be local.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
await ablo.ready();
|
|
239
|
+
|
|
240
|
+
const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
|
|
241
|
+
if (!task) throw new Error('task not found');
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Use `retrieve`, `list`, and `count` for synchronous local reads after data has
|
|
245
|
+
loaded.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const task = ablo.tasks.retrieve('task_123');
|
|
249
|
+
const activeTasks = ablo.tasks.list({
|
|
250
|
+
where: { projectId: 'proj_123' },
|
|
251
|
+
filter: (task) => task.status !== 'done',
|
|
252
|
+
orderBy: { updatedAt: 'desc' },
|
|
253
|
+
limit: 50,
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
In React, selector `useAblo` is the public read API:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
'use client';
|
|
261
|
+
|
|
262
|
+
import { useAblo } from '@ablo/sync-engine/react';
|
|
263
|
+
|
|
264
|
+
export function TaskRow({ task: serverTask }: { task: Task }) {
|
|
265
|
+
const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
|
|
266
|
+
const intents = useAblo((ablo) =>
|
|
267
|
+
ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
|
|
268
|
+
) ?? [];
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<button disabled={intents.length > 0 || task.status === 'done'}>
|
|
272
|
+
{task.title}
|
|
273
|
+
</button>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Use zero-argument `useAblo()` only in callbacks and effects:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
const ablo = useAblo();
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## 4. Write State
|
|
285
|
+
|
|
286
|
+
For simple writes:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
await ablo.tasks.update(
|
|
290
|
+
'task_123',
|
|
291
|
+
{ status: 'done' },
|
|
292
|
+
{ wait: 'confirmed' },
|
|
293
|
+
);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
For writes based on state the user or agent already read, snapshot first and
|
|
297
|
+
reject stale updates:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const snap = ablo.snapshot({ tasks: 'task_123' });
|
|
301
|
+
|
|
302
|
+
await ablo.tasks.update(
|
|
303
|
+
'task_123',
|
|
304
|
+
{ status: 'done' },
|
|
305
|
+
{
|
|
306
|
+
readAt: snap.stamp,
|
|
307
|
+
onStale: 'reject',
|
|
308
|
+
wait: 'confirmed',
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`wait: 'confirmed'` resolves after the server accepts the write. Rejections roll
|
|
314
|
+
back optimistic local state and throw a typed `AbloError`.
|
|
315
|
+
|
|
316
|
+
## 5. Multiplayer Is Automatic
|
|
317
|
+
|
|
318
|
+
There is no separate multiplayer setup.
|
|
319
|
+
|
|
320
|
+
If humans, server actions, and agents use the same schema model resource, they
|
|
321
|
+
share the same stream:
|
|
322
|
+
|
|
323
|
+
```txt
|
|
324
|
+
human UI -> ablo.tasks.update(...)
|
|
325
|
+
agent -> ablo.tasks.update(...)
|
|
326
|
+
server -> ablo.tasks.update(...)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Ablo coordinates those writes, fans out confirmed deltas, exposes active intents,
|
|
330
|
+
and lets callers reject stale writes with `readAt`.
|
|
331
|
+
|
|
332
|
+
Direct writes to your own database bypass that stream until your app reports the
|
|
333
|
+
change through Data Source events.
|
|
334
|
+
|
|
335
|
+
## 6. Existing API Backend
|
|
336
|
+
|
|
337
|
+
This is the path for a product where buttons already call Python, Rails, Go, or
|
|
338
|
+
Node endpoints.
|
|
339
|
+
|
|
340
|
+
Keep your backend and database canonical. Add Ablo as the shared write path for
|
|
341
|
+
the records that need multiplayer now and agent-safe writes later.
|
|
342
|
+
|
|
343
|
+
```txt
|
|
344
|
+
Button
|
|
345
|
+
-> ablo.tasks.update(...)
|
|
346
|
+
-> Ablo
|
|
347
|
+
-> signed Data Source request
|
|
348
|
+
-> existing backend service
|
|
349
|
+
-> app database
|
|
350
|
+
-> Ablo realtime fanout
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The migration can be gradual:
|
|
354
|
+
|
|
355
|
+
1. Declare schema for one resource, such as `tasks`.
|
|
356
|
+
2. Keep existing server loads for first paint.
|
|
357
|
+
3. Add `useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask` for live rows.
|
|
358
|
+
4. Add one Data Source endpoint that calls the existing service layer.
|
|
359
|
+
5. Move one mutation button from `fetch('/api/tasks/...')` to `ablo.tasks.update(...)`.
|
|
360
|
+
6. Add an outbox/events path for writes that still happen outside Ablo.
|
|
361
|
+
7. Let agents use the same `ablo.tasks.load(...)` and `ablo.tasks.update(...)`.
|
|
362
|
+
|
|
363
|
+
For the full Python shape, see
|
|
364
|
+
[Existing Python Backend](./examples/existing-python-backend.md).
|
|
365
|
+
|
|
366
|
+
## 7. Data Source Endpoint
|
|
367
|
+
|
|
368
|
+
Use a Data Source when your app database remains the source of truth.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
// app/api/ablo/source/route.ts
|
|
372
|
+
import { dataSource } from '@ablo/sync-engine';
|
|
373
|
+
import { schema } from '@/ablo.schema';
|
|
374
|
+
import { db } from '@/db';
|
|
375
|
+
|
|
376
|
+
export const POST = dataSource({
|
|
377
|
+
schema,
|
|
378
|
+
signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
|
|
379
|
+
|
|
380
|
+
authorize() {
|
|
381
|
+
return { db };
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
async commit({ operations, clientTxId, context }) {
|
|
385
|
+
const rows = await context.auth.db.transaction(async (tx) => {
|
|
386
|
+
await tx.idempotency.upsert({ key: clientTxId });
|
|
387
|
+
return applyOperations(tx, operations);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return { rows };
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
tasks: {
|
|
394
|
+
async load({ id, context }) {
|
|
395
|
+
return context.auth.db.task.findUnique({ where: { id } });
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
async list({ query, context }) {
|
|
399
|
+
return context.auth.db.task.findMany({
|
|
400
|
+
take: query.limit ?? 100,
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Ablo gives you a Data Source URL, a signing secret, push/events URL, and status.
|
|
408
|
+
Your app stores:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
The signing secret verifies Ablo's request. It is not a database credential.
|
|
415
|
+
|
|
416
|
+
## 8. Agents
|
|
417
|
+
|
|
418
|
+
Agents should use the same model methods as the app when they can import the
|
|
419
|
+
schema.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
const [task] = await ablo.tasks.load({ where: { id: taskId } });
|
|
423
|
+
if (!task) return;
|
|
424
|
+
|
|
425
|
+
const intents = ablo.intents.list({ resource: 'tasks', id: taskId });
|
|
426
|
+
if (intents.length > 0) return { skipped: true, reason: 'busy' };
|
|
427
|
+
|
|
428
|
+
const snap = ablo.snapshot({ tasks: taskId });
|
|
429
|
+
|
|
430
|
+
await ablo.tasks.update(
|
|
431
|
+
taskId,
|
|
432
|
+
{ status: 'done', summary: await summarize(task) },
|
|
433
|
+
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
|
|
434
|
+
);
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Use AI SDK for the model loop. Put Ablo inside the tool that persists the final
|
|
438
|
+
change.
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const completeTask = tool({
|
|
442
|
+
description: 'Mark a task done with a summary',
|
|
443
|
+
inputSchema: z.object({
|
|
444
|
+
taskId: z.string(),
|
|
445
|
+
summary: z.string(),
|
|
446
|
+
}),
|
|
447
|
+
execute: async ({ taskId, summary }) => {
|
|
448
|
+
const snap = ablo.snapshot({ tasks: taskId });
|
|
449
|
+
return ablo.tasks.update(
|
|
450
|
+
taskId,
|
|
451
|
+
{ status: 'done', summary },
|
|
452
|
+
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
|
|
453
|
+
);
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Use schema-less `agent.run(...)`, `resource(...)`, and `commits.create(...)` only
|
|
459
|
+
for custom server runtimes that intentionally cannot import the schema.
|
|
460
|
+
|
|
461
|
+
## Optional Surface
|
|
462
|
+
|
|
463
|
+
| Optional piece | Why it exists |
|
|
464
|
+
|---|---|
|
|
465
|
+
| `/react` | Live React selectors, provider lifecycle, presence, sync status. |
|
|
466
|
+
| `/testing` | Test harnesses and deterministic mocks. |
|
|
467
|
+
| `Data Source` | Keep your app database canonical. |
|
|
468
|
+
| `persistence: 'indexeddb'` | Durable browser cache and offline queueing for apps that need it. |
|
|
469
|
+
| `intents` | Show active work and coordinate before a write. |
|
|
470
|
+
| `snapshot` + `readAt` | Reject writes based on stale state. |
|
|
471
|
+
| `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and local-cache tuning. |
|
|
472
|
+
| `resource(...)` and `commits.create(...)` | Low-level protocol access for custom runtimes. |
|
|
473
|
+
|
|
474
|
+
The first integration should not need most of these. Start with schema and
|
|
475
|
+
model methods, then add the optional pieces where the product actually needs
|
|
476
|
+
them.
|
|
477
|
+
|
|
478
|
+
## Method Cheatsheet
|
|
479
|
+
|
|
480
|
+
| Method | Use it for |
|
|
481
|
+
|---|---|
|
|
482
|
+
| `load({ where })` | Async hydration from backing store/server. |
|
|
483
|
+
| `retrieve(id)` | Synchronous local read of one loaded row. |
|
|
484
|
+
| `list(options?)` | Synchronous local collection read. |
|
|
485
|
+
| `count(options?)` | Synchronous local count. |
|
|
486
|
+
| `create(data, options?)` | Create through the model resource. |
|
|
487
|
+
| `update(id, data, options?)` | Update through the model resource. |
|
|
488
|
+
| `delete(id, options?)` | Delete through the model resource. |
|
|
489
|
+
| `intents.list(target)` | See active work on a resource. |
|
|
490
|
+
| `intents.waitFor(target)` | Wait on the live intent stream. |
|
|
491
|
+
|
|
492
|
+
Do not use `ablo.commit` as the first write API. Most callers should never need
|
|
493
|
+
the low-level commit plane directly.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Interaction Model
|
|
2
|
+
|
|
3
|
+
Ablo separates the data path from the authority path.
|
|
4
|
+
|
|
5
|
+
The data path is what your application does on every write:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Schema -> Model load -> Intent -> Model update -> Confirmation
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The authority path is what makes that write defensible:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Capability -> Task -> Usage
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Primitives
|
|
18
|
+
|
|
19
|
+
| Primitive | Plane | Purpose |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `Schema` | State | Declares typed models the app and agents can read and write. |
|
|
22
|
+
| `Model` | State | The generated `ablo.<model>` resource. Use `load`, `retrieve`, `create`, `update`, and `delete`. |
|
|
23
|
+
| `Intent` | State | Pre-write coordination. It says what this actor is preparing to change. |
|
|
24
|
+
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
25
|
+
| `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
|
|
26
|
+
| `Capability` | Control | Signed credentials. It says who can do what, where, for how long, and on whose behalf. |
|
|
27
|
+
| `Task` | Control | One agent run. It groups prompts, commits, child tasks, and cost. |
|
|
28
|
+
| `Usage` | Control | Metering and audit rows derived from accepted work. |
|
|
29
|
+
|
|
30
|
+
Capabilities, tasks, and usage do not mutate product data. They define and
|
|
31
|
+
record the authority around mutation.
|
|
32
|
+
|
|
33
|
+
### Why each primitive is separate
|
|
34
|
+
|
|
35
|
+
The plane separation isn't ceremony — collapsing any two of these would
|
|
36
|
+
lose a property that's hard to recover later. A reader coming from
|
|
37
|
+
Replicache or Yjs would expect just `Commit`; here's what the others buy
|
|
38
|
+
you over that minimum:
|
|
39
|
+
|
|
40
|
+
- **`Intent` is not a lock.** A pessimistic lock blocks a writer; an
|
|
41
|
+
intent *announces* one. Other writers can yield, wait, or proceed —
|
|
42
|
+
the choice is theirs, not the system's. This is the only primitive
|
|
43
|
+
that lets two agents discover each other's planned work *before* the
|
|
44
|
+
conflict and self-arbitrate. Without intents, agents only learn about
|
|
45
|
+
contention at commit time, when one of them has already wasted a
|
|
46
|
+
token budget.
|
|
47
|
+
- **`Receipt` is not a `200 OK`.** It's the durable artifact a commit
|
|
48
|
+
produced — accepted commit id, server-assigned timestamps, stale-check
|
|
49
|
+
outcome — addressable after the fact and replayable into a different
|
|
50
|
+
client. A status code can't be re-read by a sub-agent that wasn't on
|
|
51
|
+
the original call.
|
|
52
|
+
- **`Capability` is not the actor.** The actor (`Task`) is what *ran*;
|
|
53
|
+
the capability is what it was *allowed* to do. Same human can spawn
|
|
54
|
+
many tasks under one cap (cheap re-run); same task can attenuate to
|
|
55
|
+
many sub-caps (sub-agent delegation). Folding them collapses both
|
|
56
|
+
directions of that fan.
|
|
57
|
+
- **`Task` is not the credential.** It's the audit envelope: prompt,
|
|
58
|
+
commits, child tasks, tokens, duration. Long after the cap has
|
|
59
|
+
expired, the task row is what answers "what did this run do." Folding
|
|
60
|
+
task into capability loses the post-expiry audit.
|
|
61
|
+
- **`Usage` is not derived from logs.** It's denormalized at commit
|
|
62
|
+
accept time so quota enforcement and billing reads stay O(1). Log
|
|
63
|
+
scans would work for audit but not for hot-path gating.
|
|
64
|
+
|
|
65
|
+
The shape is borrowed from systems that learned the cost of collapse:
|
|
66
|
+
intents from operational-transform CRDTs and Linear's
|
|
67
|
+
optimistic-multiplayer model, capabilities + tasks from AWS IAM
|
|
68
|
+
(`Role` ≠ `RoleSession`) and Vault (`policy` ≠ `lease`).
|
|
69
|
+
|
|
70
|
+
## Run Loop
|
|
71
|
+
|
|
72
|
+
A normal schema-backed run is:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
const [task] = await ablo.tasks.load({ where: { id } });
|
|
76
|
+
const busy = ablo.intents.list({ resource: 'tasks', id });
|
|
77
|
+
const snap = ablo.snapshot({ tasks: id });
|
|
78
|
+
await ablo.tasks.update(id, patch, {
|
|
79
|
+
readAt: snap.stamp,
|
|
80
|
+
onStale: 'reject',
|
|
81
|
+
wait: 'confirmed',
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Participants
|
|
86
|
+
|
|
87
|
+
Every action is performed by one of three kinds:
|
|
88
|
+
|
|
89
|
+
- `user` — a human, authenticated via session.
|
|
90
|
+
- `agent` — an AI process acting on behalf of a human, authenticated via a capability minted from that human's session.
|
|
91
|
+
- `system` — a customer-backend process acting on behalf of an organization, authenticated via an API key.
|
|
92
|
+
|
|
93
|
+
The participant kind is enforced at the boundary. An agent capability cannot impersonate a user. A user session cannot open a task.
|
|
94
|
+
|
|
95
|
+
## Delegation chain
|
|
96
|
+
|
|
97
|
+
Every capability resolves to a `delegationChainRootUserId` — the human at the head of the chain. The chain is denormalized onto every commit's `on_behalf_of_*` columns so audit queries answer "what did this human authorize" with one lookup, not a recursive join.
|
|
98
|
+
|
|
99
|
+
## Enforcement
|
|
100
|
+
|
|
101
|
+
Capabilities are enforced per operation, not per request. When a commit arrives, Ablo decodes the bearer token, checks each operation against `operations` and `syncGroups`, and rejects with `capability_scope_denied` if the scope is missing. Revocation takes effect within seconds of `DELETE /v1/capabilities/:id`.
|
|
102
|
+
|
|
103
|
+
Three independent checks gate every commit. The redundancy is intentional — each check covers a failure mode the others don't:
|
|
104
|
+
|
|
105
|
+
- **Lease (TTL on the token).** Decoded from the bearer; no DB lookup. Caps the lifetime of a leaked token. Without this, a stolen token works until manually revoked.
|
|
106
|
+
- **Signature + scope verification.** Stateless. Detects forged or tampered tokens and rejects operations outside the cap's `operations` / `syncGroups`. Without this, a malformed token with the right shape could pass.
|
|
107
|
+
- **Revocation.** `DELETE /v1/capabilities/:id` flips status server-side; live WS sessions close, future commits reject. Closes the gap between lease refresh cycles when you need *immediate* cutoff. Without this, a compromised cap with a long lease leaks until expiry.
|
|
108
|
+
|
|
109
|
+
Removing any one of the three leaves a class of attack uncovered. The pattern matches AWS STS, Vault leases, and the OAuth 2.1 / MCP agent-auth recommendation; see [Capabilities](./capabilities.md#the-three-layer-security-model) for the full design discussion.
|
|
110
|
+
|
|
111
|
+
## Coordination
|
|
112
|
+
|
|
113
|
+
Intents broadcast across the org. When `agent:task-writer` declares an intent to
|
|
114
|
+
update a task, schema clients can see it through `ablo.intents.list(...)` or the
|
|
115
|
+
live intent stream. Callers decide whether to yield, wait, or fail fast.
|
|
116
|
+
|
|
117
|
+
## Conflict resolution
|
|
118
|
+
|
|
119
|
+
Schema updates can carry `readAt` and `onStale`. If the state advanced past
|
|
120
|
+
`readAt`, Ablo applies the `onStale` policy:
|
|
121
|
+
|
|
122
|
+
- `reject` — fail the commit (first writer wins).
|
|
123
|
+
- `merge` — apply the write if it does not overlap with concurrent changes.
|
|
124
|
+
- `force` — apply the write unconditionally.
|
|
125
|
+
|
|
126
|
+
The choice is per-commit. No CRDT default; the policy is explicit.
|
|
127
|
+
|
|
128
|
+
## Audit
|
|
129
|
+
|
|
130
|
+
Three tables observe the run:
|
|
131
|
+
|
|
132
|
+
- `agent_tasks` — one row per open/close cycle. Cost stats, prompt hash, capability id.
|
|
133
|
+
- `agent_actions_log` — one row per write, attributed to the task and the capability.
|
|
134
|
+
- `usage_event` — one row per accounted API call, attributed to the api key, the participant, and the task.
|
|
135
|
+
|
|
136
|
+
Joins between them answer "what did this agent do, on whose authority, at what cost." That answer is what makes giving an agent write access defensible.
|
|
137
|
+
|
|
138
|
+
## The contract in one sentence
|
|
139
|
+
|
|
140
|
+
Declare schema, load state, coordinate intent, update the model, and wait for confirmation.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Claude Code
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
claude mcp add --transport http ablo-sync https://<your-app>/api/mcp
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That's it. The next `/help` in Claude Code will list the Ablo Sync tools.
|
|
10
|
+
|
|
11
|
+
## With auth
|
|
12
|
+
|
|
13
|
+
If your deployment requires a capability token (production setups should):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
claude mcp add --transport http ablo-sync https://<your-app>/api/mcp \
|
|
17
|
+
--header "Authorization=Bearer $ABLO_MCP_TOKEN"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Create a session-scoped capability from the dashboard or via the API — see
|
|
21
|
+
[MCP overview](/docs/mcp#auth).
|
|
22
|
+
|
|
23
|
+
## Verify
|
|
24
|
+
|
|
25
|
+
In Claude Code, run:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
/mcp list
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You should see `ablo-sync` with the resource tools enumerated.
|
|
32
|
+
|
|
33
|
+
## Removing
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
claude mcp remove ablo-sync
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## More
|
|
40
|
+
|
|
41
|
+
- [MCP overview](/docs/mcp) — how the transport works.
|
|
42
|
+
- [Cursor setup](/docs/mcp/cursor) — same JSON, different UI.
|
|
43
|
+
- [Windsurf setup](/docs/mcp/windsurf) — same JSON, different UI.
|