@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +242 -135
  3. package/dist/BaseSyncedStore.d.ts +2 -2
  4. package/dist/BaseSyncedStore.js +2 -2
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +90 -93
  8. package/dist/client/Ablo.js +121 -60
  9. package/dist/client/ApiClient.d.ts +14 -14
  10. package/dist/client/ApiClient.js +81 -55
  11. package/dist/client/createInternalComponents.d.ts +2 -3
  12. package/dist/client/createInternalComponents.js +2 -3
  13. package/dist/client/createModelProxy.d.ts +90 -87
  14. package/dist/client/createModelProxy.js +124 -127
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +3 -3
  18. package/dist/core/index.d.ts +2 -0
  19. package/dist/core/index.js +7 -0
  20. package/dist/errors.d.ts +8 -8
  21. package/dist/errors.js +18 -10
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +7 -11
  24. package/dist/interfaces/index.d.ts +2 -10
  25. package/dist/mutators/Transaction.d.ts +2 -2
  26. package/dist/mutators/Transaction.js +2 -2
  27. package/dist/mutators/mutateActions.d.ts +44 -0
  28. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  29. package/dist/mutators/readerActions.d.ts +32 -0
  30. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  31. package/dist/query/types.d.ts +1 -1
  32. package/dist/react/AbloProvider.d.ts +1 -1
  33. package/dist/react/AbloProvider.js +3 -3
  34. package/dist/react/context.d.ts +4 -4
  35. package/dist/react/index.d.ts +4 -5
  36. package/dist/react/index.js +3 -7
  37. package/dist/react/useAblo.d.ts +14 -14
  38. package/dist/react/useAblo.js +26 -26
  39. package/dist/react/useIntent.d.ts +2 -2
  40. package/dist/react/useIntent.js +2 -2
  41. package/dist/react/useMutators.d.ts +1 -1
  42. package/dist/react/usePresence.d.ts +3 -3
  43. package/dist/react/usePresence.js +4 -4
  44. package/dist/react/useUndoScope.d.ts +1 -1
  45. package/dist/schema/diff.d.ts +161 -0
  46. package/dist/schema/diff.js +262 -0
  47. package/dist/schema/generate.d.ts +19 -0
  48. package/dist/schema/generate.js +87 -0
  49. package/dist/schema/index.d.ts +4 -1
  50. package/dist/schema/index.js +7 -1
  51. package/dist/schema/schema.d.ts +83 -32
  52. package/dist/schema/schema.js +58 -12
  53. package/dist/schema/serialize.d.ts +92 -0
  54. package/dist/schema/serialize.js +227 -0
  55. package/dist/sync/SyncWebSocket.d.ts +17 -0
  56. package/dist/sync/SyncWebSocket.js +46 -1
  57. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  58. package/dist/sync/awaitIntentGrant.js +60 -0
  59. package/dist/sync/createIntentStream.js +43 -4
  60. package/dist/sync/createPresenceStream.js +1 -1
  61. package/dist/sync/participants.d.ts +2 -2
  62. package/dist/sync/participants.js +4 -4
  63. package/dist/types/global.d.ts +43 -52
  64. package/dist/types/global.js +16 -18
  65. package/dist/types/streams.d.ts +37 -9
  66. package/docs/api.md +68 -158
  67. package/docs/audit.md +5 -5
  68. package/docs/client-behavior.md +41 -42
  69. package/docs/coordination.md +294 -0
  70. package/docs/data-sources.md +14 -14
  71. package/docs/examples/agent-human.md +30 -32
  72. package/docs/examples/ai-sdk-tool.md +32 -33
  73. package/docs/examples/existing-python-backend.md +35 -33
  74. package/docs/examples/nextjs.md +24 -25
  75. package/docs/examples/server-agent.md +20 -61
  76. package/docs/guarantees.md +30 -55
  77. package/docs/identity.md +458 -0
  78. package/docs/index.md +12 -24
  79. package/docs/integration-guide.md +106 -116
  80. package/docs/interaction-model.md +29 -95
  81. package/docs/mcp/claude-code.md +3 -3
  82. package/docs/mcp/cursor.md +1 -1
  83. package/docs/mcp/windsurf.md +1 -1
  84. package/docs/mcp.md +11 -26
  85. package/docs/quickstart.md +43 -49
  86. package/docs/react.md +73 -23
  87. package/docs/roadmap.md +5 -7
  88. package/llms.txt +34 -39
  89. package/package.json +1 -1
  90. package/dist/react/useMutate.d.ts +0 -83
  91. package/dist/react/useQuery.d.ts +0 -123
  92. package/dist/react/useQuery.js +0 -145
  93. package/dist/react/useReader.d.ts +0 -69
  94. 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
- Capabilities, tasks, commits, and receipts exist under the hood. Most apps do
56
- not create them by hand.
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 | 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. |
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 resource through the schema model API.
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 Sync to this app for one resource that humans and agents both edit.
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
- tasks: model({
112
+ weatherReports: model({
114
113
  id: z.string(),
115
114
  projectId: z.string(),
116
- title: z.string(),
117
- status: z.enum(['todo', 'doing', 'done']),
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 *allowed* set; the
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
- { /* fields */ },
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 capability narrow into
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/capability, not a
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 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:
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─► Capability token ────► narrow scope, leased
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 *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.
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 [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
241
- if (!task) throw new Error('task not found');
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 task = ablo.tasks.retrieve('task_123');
249
- const activeTasks = ablo.tasks.list({
246
+ const report = ablo.weatherReports.retrieve('report_stockholm');
247
+ const activeReports = ablo.weatherReports.list({
250
248
  where: { projectId: 'proj_123' },
251
- filter: (task) => task.status !== 'done',
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 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
- );
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.tasks.update(
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({ tasks: 'task_123' });
292
+ const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
301
293
 
302
- await ablo.tasks.update(
303
- 'task_123',
304
- { status: 'done' },
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 model resource, they
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.tasks.update(...)
325
- agent -> ablo.tasks.update(...)
326
- server -> ablo.tasks.update(...)
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 intents,
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.tasks.update(...)
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 resource, such as `tasks`.
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.tasks.retrieve(id)) ?? serverTask` for live rows.
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/tasks/...')` to `ablo.tasks.update(...)`.
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.tasks.load(...)` and `ablo.tasks.update(...)`.
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
- tasks: {
385
+ reports: {
394
386
  async load({ id, context }) {
395
- return context.auth.db.task.findUnique({ where: { id } });
387
+ return context.auth.db.report.findUnique({ where: { id } });
396
388
  },
397
389
 
398
390
  async list({ query, context }) {
399
- return context.auth.db.task.findMany({
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 [task] = await ablo.tasks.load({ where: { id: taskId } });
424
- if (!task) return;
425
-
426
- const intents = ablo.intents.list({ resource: 'tasks', id: taskId });
427
- if (intents.length > 0) return { skipped: true, reason: 'busy' };
428
-
429
- const snap = ablo.snapshot({ tasks: taskId });
430
-
431
- await ablo.tasks.update(
432
- taskId,
433
- { status: 'done', summary: await summarize(task) },
434
- { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
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 completeTask = tool({
443
- description: 'Mark a task done with a summary',
435
+ const completeReport = tool({
436
+ description: 'Mark a weather report ready with a forecast',
444
437
  inputSchema: z.object({
445
- taskId: z.string(),
446
- summary: z.string(),
438
+ reportId: z.string(),
439
+ forecast: z.string(),
447
440
  }),
448
- execute: async ({ taskId, summary }) => {
449
- const snap = ablo.snapshot({ tasks: taskId });
450
- return ablo.tasks.update(
451
- taskId,
452
- { status: 'done', summary },
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
- Use schema-less `agent.run(...)`, `resource(...)`, and `commits.create(...)` only
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 | Why it exists |
465
- |---|---|
466
- | `/react` | Live React selectors, provider lifecycle, presence, sync status. |
467
- | `/testing` | Test harnesses and deterministic mocks. |
468
- | `Data Source` | Keep your app database canonical. |
469
- | `persistence: 'indexeddb'` | Durable browser cache and offline queueing for apps that need it. |
470
- | `intents` | Show active work and coordinate before a write. |
471
- | `snapshot` + `readAt` | Reject writes based on stale state. |
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 | Use it for |
482
- |---|---|
483
- | `load({ where })` | Async hydration from backing store/server. |
484
- | `retrieve(id)` | Synchronous local read of one loaded row. |
485
- | `list(options?)` | Synchronous local collection read. |
486
- | `count(options?)` | Synchronous local count. |
487
- | `create(data, options?)` | Create through the model resource. |
488
- | `update(id, data, options?)` | Update through the model resource. |
489
- | `delete(id, options?)` | Delete through the model resource. |
490
- | `intents.list(target)` | See active work on a resource. |
491
- | `intents.waitFor(target)` | Wait on the live intent stream. |
492
-
493
- Do not use `ablo.commit` as the first write API. Most callers should never need
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 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:
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
- Capability -> Task -> Usage
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>` resource. Use `load`, `retrieve`, `create`, `update`, and `delete`. |
23
- | `Intent` | Coordination | Who is working on a target, as one Stripe-shaped object with a single `status`. Opened and read through `ablo.<model>.intent(id)`. Ephemeral — never persisted. |
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
- - **`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.
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
- intents from operational-transform CRDTs and Linear's
67
- optimistic-multiplayer model, capabilities + tasks from AWS IAM
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 [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',
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
- Intents broadcast across the org. Open and read one on any row through the
114
- model accessor:
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
- const task = ablo.tasks.intent('task_123');
118
- if (task.current) await task.whenFree(); // someone's working — wait
119
- await task.claim({ action: 'editing' }); // claim so others yield
120
- await task.update({ status: 'done' }); // commits + releases
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
- `task.current` is the live `Intent` (or `null`); `task.whenFree()` resolves when
124
- the holder finishes. The same signal is visible to every schema client through
125
- `ablo.intents.list(...)` and the live intent stream, so callers decide whether
126
- to yield, wait, or fail fast. See [API Intent](./api.md#intent) for the object
127
- reference and lifecycle.
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 intent, update the model, and wait for confirmation.
86
+ Declare schema, load state, coordinate a claim, update the model, and wait for confirmation.
@@ -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 capability token (production setups should):
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 capability from the dashboard or via the API — see
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 resource tools enumerated.
31
+ You should see `ablo-sync` with the model tools enumerated.
32
32
 
33
33
  ## Removing
34
34
 
@@ -44,7 +44,7 @@ in your shell config.
44
44
  ## Verify
45
45
 
46
46
  In Cursor's agent panel, open the MCP tools list. You should see the
47
- Ablo Sync resource tools and their JSON schemas.
47
+ Ablo Sync model tools and their JSON schemas.
48
48
 
49
49
  ## More
50
50
 
@@ -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 resource tools enumerated.
40
+ should see `ablo-sync` with model tools enumerated.
41
41
 
42
42
  ## More
43
43