@abloatai/ablo 0.5.0 → 0.6.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 +22 -0
- package/README.md +242 -135
- package/dist/BaseSyncedStore.d.ts +2 -2
- package/dist/BaseSyncedStore.js +2 -2
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +90 -93
- package/dist/client/Ablo.js +121 -60
- package/dist/client/ApiClient.d.ts +14 -14
- package/dist/client/ApiClient.js +81 -55
- package/dist/client/createInternalComponents.d.ts +2 -3
- package/dist/client/createInternalComponents.js +2 -3
- package/dist/client/createModelProxy.d.ts +90 -87
- package/dist/client/createModelProxy.js +124 -127
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +3 -3
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/errors.d.ts +8 -8
- package/dist/errors.js +18 -10
- package/dist/index.d.ts +9 -8
- package/dist/index.js +7 -11
- package/dist/interfaces/index.d.ts +2 -10
- package/dist/mutators/Transaction.d.ts +2 -2
- package/dist/mutators/Transaction.js +2 -2
- package/dist/mutators/mutateActions.d.ts +44 -0
- package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
- package/dist/mutators/readerActions.d.ts +32 -0
- package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +1 -1
- package/dist/react/AbloProvider.js +3 -3
- package/dist/react/context.d.ts +4 -4
- package/dist/react/index.d.ts +4 -5
- package/dist/react/index.js +3 -7
- package/dist/react/useAblo.d.ts +14 -14
- package/dist/react/useAblo.js +26 -26
- package/dist/react/useIntent.d.ts +2 -2
- package/dist/react/useIntent.js +2 -2
- package/dist/react/useMutators.d.ts +1 -1
- package/dist/react/usePresence.d.ts +3 -3
- package/dist/react/usePresence.js +4 -4
- package/dist/react/useUndoScope.d.ts +1 -1
- package/dist/schema/diff.d.ts +161 -0
- package/dist/schema/diff.js +262 -0
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +4 -1
- package/dist/schema/index.js +7 -1
- package/dist/schema/schema.d.ts +83 -32
- package/dist/schema/schema.js +58 -12
- package/dist/schema/serialize.d.ts +92 -0
- package/dist/schema/serialize.js +227 -0
- package/dist/sync/SyncWebSocket.d.ts +17 -0
- package/dist/sync/SyncWebSocket.js +46 -1
- package/dist/sync/awaitIntentGrant.d.ts +26 -0
- package/dist/sync/awaitIntentGrant.js +60 -0
- package/dist/sync/createIntentStream.js +43 -4
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +4 -4
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +37 -9
- package/docs/api.md +68 -158
- package/docs/audit.md +5 -5
- package/docs/client-behavior.md +41 -42
- package/docs/coordination.md +294 -0
- package/docs/data-sources.md +14 -14
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +35 -33
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +30 -55
- package/docs/identity.md +458 -0
- package/docs/index.md +12 -24
- package/docs/integration-guide.md +106 -116
- package/docs/interaction-model.md +29 -95
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +1 -1
- package/docs/mcp.md +11 -26
- package/docs/quickstart.md +43 -49
- package/docs/react.md +73 -23
- package/docs/roadmap.md +5 -7
- package/llms.txt +34 -39
- package/package.json +1 -1
- package/dist/react/useMutate.d.ts +0 -83
- package/dist/react/useQuery.d.ts +0 -123
- package/dist/react/useQuery.js +0 -145
- package/dist/react/useReader.d.ts +0 -69
- package/docs/capabilities.md +0 -163
|
@@ -52,18 +52,17 @@ that same model path.
|
|
|
52
52
|
schema -> ablo.<model>.load(...) -> ablo.<model>.update(...)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
Commits and receipts exist under the hood. Most apps do not create protocol
|
|
56
|
+
objects by hand.
|
|
57
57
|
|
|
58
58
|
## Pick The Backing Mode
|
|
59
59
|
|
|
60
60
|
Every schema model has a backing store. The SDK call shape stays the same.
|
|
61
61
|
|
|
62
|
-
| Mode
|
|
63
|
-
|
|
64
|
-
| Ablo-managed | Ablo
|
|
65
|
-
| Data Source
|
|
66
|
-
| Schema-less resource API | Custom runtime | A server worker, MCP route, or migration script intentionally cannot import the app schema. |
|
|
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. |
|
|
67
66
|
|
|
68
67
|
Do not pass a database URL to `Ablo(...)`. Application and agent code use
|
|
69
68
|
`ABLO_API_KEY`. If your database stays canonical, expose a signed Data Source
|
|
@@ -74,7 +73,7 @@ endpoint from your app and keep the database credentials inside your app.
|
|
|
74
73
|
Use the public `/sandbox` page to understand the state flow. It is a visual,
|
|
75
74
|
deterministic demo; it does not call your API key or mutate hosted Ablo data.
|
|
76
75
|
It is also built for coding agents: copy the sandbox prompt into Claude Code or
|
|
77
|
-
Codex and ask it to wire one real
|
|
76
|
+
Codex and ask it to wire one real model through the schema model API.
|
|
78
77
|
|
|
79
78
|
Use the authenticated org dashboard sandbox for real integration work. The
|
|
80
79
|
default sandbox is the equivalent of Stripe test mode:
|
|
@@ -92,7 +91,7 @@ and write path are ready for production.
|
|
|
92
91
|
When handing this to a coding agent, give it a concrete target:
|
|
93
92
|
|
|
94
93
|
```txt
|
|
95
|
-
Add Ablo
|
|
94
|
+
Add Ablo to this app for one model that humans and agents both edit.
|
|
96
95
|
Use the org sandbox sk_test_* key. Declare schema, add the Ablo client, replace
|
|
97
96
|
one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
|
|
98
97
|
wait: 'confirmed' }), and add a smoke test for two concurrent writers.
|
|
@@ -110,11 +109,11 @@ import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
|
110
109
|
|
|
111
110
|
export const schema = defineSchema(
|
|
112
111
|
{
|
|
113
|
-
|
|
112
|
+
weatherReports: model({
|
|
114
113
|
id: z.string(),
|
|
115
114
|
projectId: z.string(),
|
|
116
|
-
|
|
117
|
-
status: z.enum(['
|
|
115
|
+
location: z.string(),
|
|
116
|
+
status: z.enum(['pending', 'ready']),
|
|
118
117
|
assigneeId: z.string().nullable(),
|
|
119
118
|
updatedAt: z.string(),
|
|
120
119
|
}),
|
|
@@ -138,7 +137,7 @@ export const schema = defineSchema(
|
|
|
138
137
|
extract: (i) => (i.userId ? [String(i.userId)] : []),
|
|
139
138
|
},
|
|
140
139
|
],
|
|
141
|
-
}
|
|
140
|
+
}
|
|
142
141
|
);
|
|
143
142
|
```
|
|
144
143
|
|
|
@@ -146,23 +145,25 @@ export const schema = defineSchema(
|
|
|
146
145
|
|
|
147
146
|
Per-row tenancy and per-entity sync-group anchors live on the
|
|
148
147
|
`defineModel` (or `model(...)`) options. The two halves compose: the
|
|
149
|
-
identity roles above produce a participant's
|
|
148
|
+
identity roles above produce a participant's _allowed_ set; the
|
|
150
149
|
per-model options below define how rows are filtered server-side and
|
|
151
150
|
which sync-group label each row fans out on.
|
|
152
151
|
|
|
153
152
|
```ts
|
|
154
153
|
model(
|
|
155
|
-
{
|
|
154
|
+
{
|
|
155
|
+
/* fields */
|
|
156
|
+
},
|
|
156
157
|
/* relations */ {},
|
|
157
158
|
{
|
|
158
159
|
// Rows carry organization_id and bootstrap filters on it.
|
|
159
160
|
orgScoped: true,
|
|
160
161
|
|
|
161
|
-
// Per-entity sync-group anchor. Lets a
|
|
162
|
+
// Per-entity sync-group anchor. Lets a scoped session narrow into
|
|
162
163
|
// one row's scope via `syncGroupFormat.replace('{id}', rowId)`.
|
|
163
164
|
syncGroupFormat: 'matter:{id}',
|
|
164
|
-
}
|
|
165
|
-
)
|
|
165
|
+
}
|
|
166
|
+
);
|
|
166
167
|
```
|
|
167
168
|
|
|
168
169
|
For rows that don't carry `organization_id` themselves but inherit
|
|
@@ -185,7 +186,7 @@ export const ablo = Ablo({
|
|
|
185
186
|
});
|
|
186
187
|
```
|
|
187
188
|
|
|
188
|
-
Browser apps should use the React provider or a scoped session
|
|
189
|
+
Browser apps should use the React provider or a scoped session token, not a
|
|
189
190
|
server API key in the bundle.
|
|
190
191
|
|
|
191
192
|
```tsx
|
|
@@ -208,15 +209,14 @@ bundle, and signs server-to-server requests. It is the right credential
|
|
|
208
209
|
for trusted runtimes (Next.js server actions, background workers,
|
|
209
210
|
migration scripts) where the code reading it is yours.
|
|
210
211
|
|
|
211
|
-
A browser
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
exchange is the bridge between two credential shapes:
|
|
212
|
+
A browser is not that environment. The React provider exchanges your API key for
|
|
213
|
+
a short-lived, narrowly scoped bearer token. The browser holds that scoped token;
|
|
214
|
+
the API key never leaves the server. The exchange is the bridge between two
|
|
215
|
+
credential shapes:
|
|
216
216
|
|
|
217
217
|
```
|
|
218
218
|
trusted runtime browser / agent
|
|
219
|
-
ABLO_API_KEY ─exchange─►
|
|
219
|
+
ABLO_API_KEY ─exchange─► scoped token ────────► narrow scope, leased
|
|
220
220
|
(long-lived, (short-lived,
|
|
221
221
|
broad scope, per-actor scope,
|
|
222
222
|
server only) revocable)
|
|
@@ -225,10 +225,8 @@ ABLO_API_KEY ─exchange─► Capability token ────► narrow scope, l
|
|
|
225
225
|
This is the same shape as Stripe's
|
|
226
226
|
ephemeral keys (Issuing Elements expires in 15 minutes) and AWS STS
|
|
227
227
|
AssumeRole (returns time-bounded creds with the minimal needed scope).
|
|
228
|
-
You never
|
|
229
|
-
|
|
230
|
-
[Capabilities](./capabilities.md) for the design rationale and the
|
|
231
|
-
manual create/revoke surface that custom runtimes use.
|
|
228
|
+
You never type that token into your app; the SDK mints one when it needs one and
|
|
229
|
+
refreshes before expiry.
|
|
232
230
|
|
|
233
231
|
## 3. Read State
|
|
234
232
|
|
|
@@ -237,18 +235,18 @@ Use `load` when the row may not already be local.
|
|
|
237
235
|
```ts
|
|
238
236
|
await ablo.ready();
|
|
239
237
|
|
|
240
|
-
const [
|
|
241
|
-
if (!
|
|
238
|
+
const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
239
|
+
if (!report) throw new Error('report not found');
|
|
242
240
|
```
|
|
243
241
|
|
|
244
242
|
Use `retrieve`, `list`, and `count` for synchronous local reads after data has
|
|
245
243
|
loaded.
|
|
246
244
|
|
|
247
245
|
```ts
|
|
248
|
-
const
|
|
249
|
-
const
|
|
246
|
+
const report = ablo.weatherReports.retrieve('report_stockholm');
|
|
247
|
+
const activeReports = ablo.weatherReports.list({
|
|
250
248
|
where: { projectId: 'proj_123' },
|
|
251
|
-
filter: (
|
|
249
|
+
filter: (report) => report.status !== 'ready',
|
|
252
250
|
orderBy: { updatedAt: 'desc' },
|
|
253
251
|
limit: 50,
|
|
254
252
|
});
|
|
@@ -261,17 +259,15 @@ In React, selector `useAblo` is the public read API:
|
|
|
261
259
|
|
|
262
260
|
import { useAblo } from '@abloatai/ablo/react';
|
|
263
261
|
|
|
264
|
-
export function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
</button>
|
|
274
|
-
);
|
|
262
|
+
export function ReportRow({
|
|
263
|
+
report: serverReport,
|
|
264
|
+
}: {
|
|
265
|
+
report: { id: string; location: string; status: string };
|
|
266
|
+
}) {
|
|
267
|
+
const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
|
|
268
|
+
const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
|
|
269
|
+
|
|
270
|
+
return <button disabled={Boolean(active) || report.status === 'ready'}>{report.location}</button>;
|
|
275
271
|
}
|
|
276
272
|
```
|
|
277
273
|
|
|
@@ -286,27 +282,23 @@ const ablo = useAblo();
|
|
|
286
282
|
For simple writes:
|
|
287
283
|
|
|
288
284
|
```ts
|
|
289
|
-
await ablo.
|
|
290
|
-
'task_123',
|
|
291
|
-
{ status: 'done' },
|
|
292
|
-
{ wait: 'confirmed' },
|
|
293
|
-
);
|
|
285
|
+
await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
|
|
294
286
|
```
|
|
295
287
|
|
|
296
288
|
For writes based on state the user or agent already read, snapshot first and
|
|
297
289
|
reject stale updates:
|
|
298
290
|
|
|
299
291
|
```ts
|
|
300
|
-
const snap = ablo.snapshot({
|
|
292
|
+
const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
|
|
301
293
|
|
|
302
|
-
await ablo.
|
|
303
|
-
'
|
|
304
|
-
{ status: '
|
|
294
|
+
await ablo.weatherReports.update(
|
|
295
|
+
'report_stockholm',
|
|
296
|
+
{ status: 'ready' },
|
|
305
297
|
{
|
|
306
298
|
readAt: snap.stamp,
|
|
307
299
|
onStale: 'reject',
|
|
308
300
|
wait: 'confirmed',
|
|
309
|
-
}
|
|
301
|
+
}
|
|
310
302
|
);
|
|
311
303
|
```
|
|
312
304
|
|
|
@@ -317,16 +309,16 @@ back optimistic local state and throw a typed `AbloError`.
|
|
|
317
309
|
|
|
318
310
|
There is no separate multiplayer setup.
|
|
319
311
|
|
|
320
|
-
If humans, server actions, and agents use the same schema
|
|
312
|
+
If humans, server actions, and agents use the same schema client, they
|
|
321
313
|
share the same stream:
|
|
322
314
|
|
|
323
315
|
```txt
|
|
324
|
-
human UI -> ablo.
|
|
325
|
-
agent -> ablo.
|
|
326
|
-
server -> ablo.
|
|
316
|
+
human UI -> ablo.weatherReports.update(...)
|
|
317
|
+
agent -> ablo.weatherReports.update(...)
|
|
318
|
+
server -> ablo.weatherReports.update(...)
|
|
327
319
|
```
|
|
328
320
|
|
|
329
|
-
Ablo coordinates those writes, fans out confirmed deltas, exposes active
|
|
321
|
+
Ablo coordinates those writes, fans out confirmed deltas, exposes active claims,
|
|
330
322
|
and lets callers reject stale writes with `readAt`.
|
|
331
323
|
|
|
332
324
|
Direct writes to your own database bypass that stream until your app reports the
|
|
@@ -342,7 +334,7 @@ the records that need multiplayer now and agent-safe writes later.
|
|
|
342
334
|
|
|
343
335
|
```txt
|
|
344
336
|
Button
|
|
345
|
-
-> ablo.
|
|
337
|
+
-> ablo.weatherReports.update(...)
|
|
346
338
|
-> Ablo
|
|
347
339
|
-> signed Data Source request
|
|
348
340
|
-> existing backend service
|
|
@@ -352,13 +344,13 @@ Button
|
|
|
352
344
|
|
|
353
345
|
The migration can be gradual:
|
|
354
346
|
|
|
355
|
-
1. Declare schema for one
|
|
347
|
+
1. Declare schema for one model, such as `reports`.
|
|
356
348
|
2. Keep existing server loads for first paint.
|
|
357
|
-
3. Add `useAblo((ablo) => ablo.
|
|
349
|
+
3. Add `useAblo((ablo) => ablo.weatherReports.retrieve(id)) ?? serverReport` for live rows.
|
|
358
350
|
4. Add one Data Source endpoint that calls the existing service layer.
|
|
359
|
-
5. Move one mutation button from `fetch('/api/
|
|
351
|
+
5. Move one mutation button from `fetch('/api/reports/...')` to `ablo.weatherReports.update(...)`.
|
|
360
352
|
6. Add an outbox/events path for writes that still happen outside Ablo.
|
|
361
|
-
7. Let agents use the same `ablo.
|
|
353
|
+
7. Let agents use the same `ablo.weatherReports.load(...)` and `ablo.weatherReports.update(...)`.
|
|
362
354
|
|
|
363
355
|
For the full Python shape, see
|
|
364
356
|
[Existing Python Backend](./examples/existing-python-backend.md).
|
|
@@ -390,13 +382,13 @@ export const POST = dataSource({
|
|
|
390
382
|
return { rows };
|
|
391
383
|
},
|
|
392
384
|
|
|
393
|
-
|
|
385
|
+
reports: {
|
|
394
386
|
async load({ id, context }) {
|
|
395
|
-
return context.auth.db.
|
|
387
|
+
return context.auth.db.report.findUnique({ where: { id } });
|
|
396
388
|
},
|
|
397
389
|
|
|
398
390
|
async list({ query, context }) {
|
|
399
|
-
return context.auth.db.
|
|
391
|
+
return context.auth.db.report.findMany({
|
|
400
392
|
take: query.limit ?? 100,
|
|
401
393
|
});
|
|
402
394
|
},
|
|
@@ -420,18 +412,19 @@ Agents should use the same model methods as the app when they can import the
|
|
|
420
412
|
schema.
|
|
421
413
|
|
|
422
414
|
```ts
|
|
423
|
-
const [
|
|
424
|
-
if (!
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
await
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
415
|
+
const [report] = await ablo.weatherReports.load({ where: { id: reportId } });
|
|
416
|
+
if (!report) return;
|
|
417
|
+
|
|
418
|
+
await ablo.weatherReports.claim(
|
|
419
|
+
reportId,
|
|
420
|
+
async (claimed) => {
|
|
421
|
+
await ablo.weatherReports.update(
|
|
422
|
+
claimed.id,
|
|
423
|
+
{ status: 'ready', forecast: await getForecast(claimed) },
|
|
424
|
+
{ wait: 'confirmed' }
|
|
425
|
+
);
|
|
426
|
+
},
|
|
427
|
+
{ wait: false, action: 'forecasting' }
|
|
435
428
|
);
|
|
436
429
|
```
|
|
437
430
|
|
|
@@ -439,38 +432,36 @@ Use AI SDK for the model loop. Put Ablo inside the tool that persists the final
|
|
|
439
432
|
change.
|
|
440
433
|
|
|
441
434
|
```ts
|
|
442
|
-
const
|
|
443
|
-
description: 'Mark a
|
|
435
|
+
const completeReport = tool({
|
|
436
|
+
description: 'Mark a weather report ready with a forecast',
|
|
444
437
|
inputSchema: z.object({
|
|
445
|
-
|
|
446
|
-
|
|
438
|
+
reportId: z.string(),
|
|
439
|
+
forecast: z.string(),
|
|
447
440
|
}),
|
|
448
|
-
execute: async ({
|
|
449
|
-
const snap = ablo.snapshot({
|
|
450
|
-
return ablo.
|
|
451
|
-
|
|
452
|
-
{ status: '
|
|
453
|
-
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' }
|
|
441
|
+
execute: async ({ reportId, forecast }) => {
|
|
442
|
+
const snap = ablo.snapshot({ weatherReports: reportId });
|
|
443
|
+
return ablo.weatherReports.update(
|
|
444
|
+
reportId,
|
|
445
|
+
{ status: 'ready', forecast },
|
|
446
|
+
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' }
|
|
454
447
|
);
|
|
455
448
|
},
|
|
456
449
|
});
|
|
457
450
|
```
|
|
458
451
|
|
|
459
|
-
|
|
460
|
-
for custom server runtimes that intentionally cannot import the schema.
|
|
452
|
+
Keep agent writes on the same schema client surface as the app.
|
|
461
453
|
|
|
462
454
|
## Optional Surface
|
|
463
455
|
|
|
464
|
-
| Optional piece
|
|
465
|
-
|
|
466
|
-
| `/react`
|
|
467
|
-
| `/testing`
|
|
468
|
-
| `Data Source`
|
|
469
|
-
| `persistence: 'indexeddb'`
|
|
470
|
-
| `
|
|
471
|
-
| `snapshot` + `readAt`
|
|
472
|
-
| `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and local-cache tuning.
|
|
473
|
-
| `resource(...)` and `commits.create(...)` | Low-level protocol access for custom runtimes. |
|
|
456
|
+
| Optional piece | Why it exists |
|
|
457
|
+
| ----------------------------------------- | ----------------------------------------------------------------- |
|
|
458
|
+
| `/react` | Live React selectors, provider lifecycle, presence, sync status. |
|
|
459
|
+
| `/testing` | Test harnesses and deterministic mocks. |
|
|
460
|
+
| `Data Source` | Keep your app database canonical. |
|
|
461
|
+
| `persistence: 'indexeddb'` | Durable browser cache and offline queueing for apps that need it. |
|
|
462
|
+
| `claim` / `claimState` / `queue` | Show active work and coordinate before a write. |
|
|
463
|
+
| `snapshot` + `readAt` | Reject writes based on stale state. |
|
|
464
|
+
| `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and local-cache tuning. |
|
|
474
465
|
|
|
475
466
|
The first integration should not need most of these. Start with schema and
|
|
476
467
|
model methods, then add the optional pieces where the product actually needs
|
|
@@ -478,17 +469,16 @@ them.
|
|
|
478
469
|
|
|
479
470
|
## Method Cheatsheet
|
|
480
471
|
|
|
481
|
-
| Method
|
|
482
|
-
|
|
483
|
-
| `load({ where })`
|
|
484
|
-
| `retrieve(id)`
|
|
485
|
-
| `list(options?)`
|
|
486
|
-
| `count(options?)`
|
|
487
|
-
| `create(data, options?)`
|
|
488
|
-
| `update(id, data, options?)` | Update through the model
|
|
489
|
-
| `delete(id, options?)`
|
|
490
|
-
| `
|
|
491
|
-
| `
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
the low-level commit plane directly.
|
|
472
|
+
| Method | Use it for |
|
|
473
|
+
| ---------------------------- | ---------------------------------------------------------------- |
|
|
474
|
+
| `load({ where })` | Async hydration from backing store/server. |
|
|
475
|
+
| `retrieve(id)` | Synchronous local read of one loaded row. |
|
|
476
|
+
| `list(options?)` | Synchronous local collection read. |
|
|
477
|
+
| `count(options?)` | Synchronous local count. |
|
|
478
|
+
| `create(data, options?)` | Create through the model client. |
|
|
479
|
+
| `update(id, data, options?)` | Update through the model client. |
|
|
480
|
+
| `delete(id, options?)` | Delete through the model client. |
|
|
481
|
+
| `claimState(id)` | See active work on a model row. |
|
|
482
|
+
| `claim(id, work, options?)` | Wait for your turn, re-read, and hold the row while `work` runs. |
|
|
483
|
+
|
|
484
|
+
Keep first integrations on the model methods above.
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
# Interaction Model
|
|
2
2
|
|
|
3
|
-
Ablo
|
|
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:
|
|
3
|
+
Ablo's public model is the path every human UI, server action, and agent uses on
|
|
4
|
+
every write:
|
|
12
5
|
|
|
13
6
|
```
|
|
14
|
-
|
|
7
|
+
Schema -> Model load -> Claim -> Model update -> Confirmation
|
|
15
8
|
```
|
|
16
9
|
|
|
17
10
|
## Primitives
|
|
@@ -19,16 +12,10 @@ Capability -> Task -> Usage
|
|
|
19
12
|
| Primitive | Plane | Purpose |
|
|
20
13
|
|---|---|---|
|
|
21
14
|
| `Schema` | State | Declares typed models the app and agents can read and write. |
|
|
22
|
-
| `Model` | State | The generated `ablo.<model>`
|
|
23
|
-
| `
|
|
15
|
+
| `Model` | State | The generated `ablo.<model>` model. Use `load`, `retrieve`, `create`, `update`, and `delete`. |
|
|
16
|
+
| `Claim` | Coordination | Who is working on a target. Claimed via `ablo.<model>.claim(id, ...)` and read via `ablo.<model>.claimState(id)`. Ephemeral — never persisted. |
|
|
24
17
|
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
25
18
|
| `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
19
|
|
|
33
20
|
### Why each primitive is separate
|
|
34
21
|
|
|
@@ -37,94 +24,51 @@ lose a property that's hard to recover later. A reader coming from
|
|
|
37
24
|
Replicache or Yjs would expect just `Commit`; here's what the others buy
|
|
38
25
|
you over that minimum:
|
|
39
26
|
|
|
40
|
-
- **`
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
27
|
+
- **`Claim` is not a read lock.** Reads stay open. Claims serialize
|
|
28
|
+
acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
|
|
29
|
+
from fresh state.
|
|
47
30
|
- **`Receipt` is not a `200 OK`.** It's the durable artifact a commit
|
|
48
31
|
produced — accepted commit id, server-assigned timestamps, stale-check
|
|
49
32
|
outcome — addressable after the fact and replayable into a different
|
|
50
33
|
client. A status code can't be re-read by a sub-agent that wasn't on
|
|
51
34
|
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
35
|
|
|
65
36
|
The shape is borrowed from systems that learned the cost of collapse:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(`Role` ≠ `RoleSession`) and Vault (`policy` ≠ `lease`).
|
|
37
|
+
coordination from operational-transform CRDTs and Linear's optimistic
|
|
38
|
+
multiplayer model, and receipts from durable write protocols.
|
|
69
39
|
|
|
70
40
|
## Run Loop
|
|
71
41
|
|
|
72
42
|
A normal schema-backed run is:
|
|
73
43
|
|
|
74
44
|
```
|
|
75
|
-
const [
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
await ablo.
|
|
79
|
-
readAt: snap.stamp,
|
|
80
|
-
onStale: 'reject',
|
|
81
|
-
wait: 'confirmed',
|
|
45
|
+
const [report] = await ablo.weatherReports.load({ where: { id } });
|
|
46
|
+
const active = ablo.weatherReports.claimState(id);
|
|
47
|
+
await ablo.weatherReports.claim(id, async (report) => {
|
|
48
|
+
await ablo.weatherReports.update(report.id, patch, { wait: 'confirmed' });
|
|
82
49
|
});
|
|
83
50
|
```
|
|
84
51
|
|
|
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
52
|
## Coordination
|
|
112
53
|
|
|
113
|
-
|
|
114
|
-
|
|
54
|
+
Claims broadcast across the org. Claim a row through the flat model verb, write
|
|
55
|
+
through the normal `update`, and the claim releases when the callback returns:
|
|
115
56
|
|
|
116
57
|
```ts
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await
|
|
58
|
+
await ablo.weatherReports.claim(
|
|
59
|
+
'report_stockholm',
|
|
60
|
+
async (report) => {
|
|
61
|
+
await ablo.weatherReports.update(report.id, { status: 'ready' }); // stale-guarded under the claim
|
|
62
|
+
},
|
|
63
|
+
{ action: 'editing' },
|
|
64
|
+
);
|
|
121
65
|
```
|
|
122
66
|
|
|
123
|
-
`
|
|
124
|
-
|
|
125
|
-
`
|
|
126
|
-
|
|
127
|
-
|
|
67
|
+
`ablo.weatherReports.claimState('report_stockholm')` reads the live claim (or `null`) without
|
|
68
|
+
blocking. The claim is **advisory**: if another participant holds the row,
|
|
69
|
+
`claim` waits for them to finish and re-reads before handing back the row. The
|
|
70
|
+
same signal is visible to every schema client through `claimState(id)` and the live
|
|
71
|
+
claim stream.
|
|
128
72
|
|
|
129
73
|
## Conflict resolution
|
|
130
74
|
|
|
@@ -137,16 +81,6 @@ Schema updates can carry `readAt` and `onStale`. If the state advanced past
|
|
|
137
81
|
|
|
138
82
|
The choice is per-commit. No CRDT default; the policy is explicit.
|
|
139
83
|
|
|
140
|
-
## Audit
|
|
141
|
-
|
|
142
|
-
Three tables observe the run:
|
|
143
|
-
|
|
144
|
-
- `agent_tasks` — one row per open/close cycle. Cost stats, prompt hash, capability id.
|
|
145
|
-
- `agent_actions_log` — one row per write, attributed to the task and the capability.
|
|
146
|
-
- `usage_event` — one row per accounted API call, attributed to the api key, the participant, and the task.
|
|
147
|
-
|
|
148
|
-
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.
|
|
149
|
-
|
|
150
84
|
## The contract in one sentence
|
|
151
85
|
|
|
152
|
-
Declare schema, load state, coordinate
|
|
86
|
+
Declare schema, load state, coordinate a claim, update the model, and wait for confirmation.
|
package/docs/mcp/claude-code.md
CHANGED
|
@@ -10,14 +10,14 @@ That's it. The next `/help` in Claude Code will list the Ablo Sync tools.
|
|
|
10
10
|
|
|
11
11
|
## With auth
|
|
12
12
|
|
|
13
|
-
If your deployment requires a
|
|
13
|
+
If your deployment requires a scoped bearer token (production setups should):
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
claude mcp add --transport http ablo-sync https://<your-app>/api/mcp \
|
|
17
17
|
--header "Authorization=Bearer $ABLO_MCP_TOKEN"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Create a session-scoped
|
|
20
|
+
Create a session-scoped bearer token from your server or dashboard — see
|
|
21
21
|
[MCP overview](/docs/mcp#auth).
|
|
22
22
|
|
|
23
23
|
## Verify
|
|
@@ -28,7 +28,7 @@ In Claude Code, run:
|
|
|
28
28
|
/mcp list
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
You should see `ablo-sync` with the
|
|
31
|
+
You should see `ablo-sync` with the model tools enumerated.
|
|
32
32
|
|
|
33
33
|
## Removing
|
|
34
34
|
|
package/docs/mcp/cursor.md
CHANGED
package/docs/mcp/windsurf.md
CHANGED
|
@@ -37,7 +37,7 @@ Cascade → MCP. Restart Windsurf after saving.
|
|
|
37
37
|
## Verify
|
|
38
38
|
|
|
39
39
|
Cascade's MCP panel lists every configured server with its tools. You
|
|
40
|
-
should see `ablo-sync` with
|
|
40
|
+
should see `ablo-sync` with model tools enumerated.
|
|
41
41
|
|
|
42
42
|
## More
|
|
43
43
|
|