@abloatai/ablo 0.5.1 → 0.7.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 +61 -0
- package/README.md +248 -124
- package/dist/BaseSyncedStore.d.ts +3 -3
- package/dist/BaseSyncedStore.js +3 -3
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +91 -93
- package/dist/client/Ablo.js +122 -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 +116 -90
- package/dist/client/createModelProxy.js +128 -128
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +5 -5
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +264 -0
- package/dist/errorCodes.js +251 -0
- package/dist/errors.d.ts +59 -14
- package/dist/errors.js +73 -12
- package/dist/index.d.ts +11 -9
- package/dist/index.js +8 -12
- 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/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +14 -6
- 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/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +167 -0
- package/dist/schema/diff.js +280 -0
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +9 -3
- package/dist/schema/index.js +14 -2
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +10 -69
- package/dist/schema/schema.js +58 -24
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +96 -0
- package/dist/schema/serialize.js +231 -0
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +23 -17
- 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.d.ts +2 -1
- package/dist/sync/createIntentStream.js +89 -5
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +9 -18
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +90 -42
- package/docs/api-keys.md +44 -0
- package/docs/api.md +72 -173
- package/docs/audit.md +5 -5
- package/docs/cli.md +212 -0
- package/docs/client-behavior.md +42 -43
- package/docs/coordination.md +343 -0
- package/docs/data-sources.md +16 -16
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +38 -36
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/scoped-agent.md +78 -0
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +34 -56
- package/docs/identity.md +529 -0
- package/docs/index.md +18 -24
- package/docs/integration-guide.md +130 -144
- package/docs/interaction-model.md +32 -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 +74 -24
- package/docs/roadmap.md +17 -7
- package/llms.txt +34 -39
- package/package.json +8 -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
package/docs/api.md
CHANGED
|
@@ -11,67 +11,59 @@ import Ablo from '@abloatai/ablo';
|
|
|
11
11
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
12
12
|
|
|
13
13
|
const schema = defineSchema({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
status: z.enum(['
|
|
14
|
+
weatherReports: model({
|
|
15
|
+
location: z.string(),
|
|
16
|
+
status: z.enum(['pending', 'ready']),
|
|
17
17
|
}),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
21
21
|
|
|
22
22
|
await ablo.ready();
|
|
23
|
-
const [
|
|
24
|
-
if (!
|
|
23
|
+
const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
24
|
+
if (!report) throw new Error('Row not found');
|
|
25
25
|
|
|
26
|
-
await ablo.
|
|
26
|
+
await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## Model Methods
|
|
30
30
|
|
|
31
|
-
Each schema model becomes a typed
|
|
31
|
+
Each schema model becomes a typed model on the client:
|
|
32
32
|
|
|
33
|
-
- `ablo.
|
|
34
|
-
- `ablo.
|
|
35
|
-
- `ablo.
|
|
36
|
-
- `ablo.
|
|
37
|
-
- `ablo.
|
|
33
|
+
- `ablo.weatherReports.load({ where })` hydrates rows asynchronously.
|
|
34
|
+
- `ablo.weatherReports.retrieve(id)` reads one already-loaded row synchronously.
|
|
35
|
+
- `ablo.weatherReports.create(data)` creates a row.
|
|
36
|
+
- `ablo.weatherReports.update(id, data, options?)` updates a row.
|
|
37
|
+
- `ablo.weatherReports.delete(id, options?)` deletes a row.
|
|
38
38
|
|
|
39
|
-
`load` and `retrieve` are not aliases. Use `load` when the row may not be
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
`load` and `retrieve` are not aliases. Use `load` when the row may not be loaded
|
|
40
|
+
yet. Use `retrieve` after `ready()` or `load()` when you want a cheap
|
|
41
|
+
synchronous read.
|
|
42
42
|
|
|
43
43
|
| Method | Returns | Use when |
|
|
44
44
|
|---|---|---|
|
|
45
45
|
| `load({ where })` | `Promise<T[]>` | You need to hydrate rows from local store and server. |
|
|
46
|
-
| `retrieve(id)` | `T \| undefined` | You already loaded the row and want a synchronous
|
|
47
|
-
| `list(options?)` | `T[]` | You want a synchronous
|
|
48
|
-
| `count(options?)` | `number` | You want a synchronous
|
|
46
|
+
| `retrieve(id)` | `T \| undefined` | You already loaded the row and want a synchronous read. |
|
|
47
|
+
| `list(options?)` | `T[]` | You want a synchronous list of loaded rows. |
|
|
48
|
+
| `count(options?)` | `number` | You want a synchronous count of loaded rows. |
|
|
49
49
|
| `create(data, options?)` | `Promise<T>` | You want to create through the schema model. |
|
|
50
50
|
| `update(id, data, options?)` | `Promise<T>` | You want to update through the schema model. |
|
|
51
51
|
| `delete(id, options?)` | `Promise<void>` | You want to delete through the schema model. |
|
|
52
52
|
|
|
53
|
-
`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const activeDoneTasks = ablo.tasks.list({
|
|
57
|
-
where: { status: 'done' },
|
|
58
|
-
filter: (task) => !task.title.startsWith('[archived]'),
|
|
59
|
-
orderBy: { updatedAt: 'desc' },
|
|
60
|
-
limit: 20,
|
|
61
|
-
scope: 'live', // 'live' | 'archived' | 'all'
|
|
62
|
-
});
|
|
63
|
-
```
|
|
53
|
+
`load`, `create`, `update`, and `delete` are the main path — they go through the
|
|
54
|
+
server. `retrieve` / `list` / `count` are **synchronous reads** off the rows a
|
|
55
|
+
session has already loaded, so a cheap re-read needs no round-trip.
|
|
64
56
|
|
|
65
57
|
## Protected Writes
|
|
66
58
|
|
|
67
59
|
Use `snapshot` when a write should reject if the row changed mid-flight:
|
|
68
60
|
|
|
69
61
|
```ts
|
|
70
|
-
const snap = ablo.snapshot({
|
|
62
|
+
const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
|
|
71
63
|
|
|
72
|
-
await ablo.
|
|
73
|
-
'
|
|
74
|
-
{ status: '
|
|
64
|
+
await ablo.weatherReports.update(
|
|
65
|
+
'report_stockholm',
|
|
66
|
+
{ status: 'ready' },
|
|
75
67
|
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
|
|
76
68
|
);
|
|
77
69
|
```
|
|
@@ -82,62 +74,43 @@ Protected write options:
|
|
|
82
74
|
|---|---|
|
|
83
75
|
| `readAt` | The state cursor the write was based on. |
|
|
84
76
|
| `onStale` | Stale-state policy. Prefer `reject` for agent writes. |
|
|
85
|
-
| `intent` | Active work claim associated with the write. |
|
|
86
77
|
| `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
|
|
87
78
|
| `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
|
|
88
79
|
| `timeout` | Maximum time to wait for the write call. |
|
|
89
80
|
|
|
90
|
-
##
|
|
91
|
-
|
|
92
|
-
Use `resource(name)` only when you intentionally need the raw protocol shape:
|
|
93
|
-
generic server runtimes, MCP routes, batch tools, or code that has no schema.
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
const tasks = ablo.resource<{ status: string }>('tasks');
|
|
97
|
-
|
|
98
|
-
const { data, stamp, intents } = await tasks.retrieve('task_123', {
|
|
99
|
-
ifBusy: 'return',
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`stamp` is the state watermark. Pass it as `readAt` to reject stale writes.
|
|
81
|
+
## Claims
|
|
104
82
|
|
|
105
|
-
|
|
106
|
-
|
|
83
|
+
A claim tells humans and agents who is working on a target before the write
|
|
84
|
+
lands. One self-describing object carries the lifecycle in a single `status`
|
|
85
|
+
field. It lives on the coordination plane: ephemeral, TTL'd, broadcast to peers
|
|
86
|
+
in real time, and never persisted as a row.
|
|
107
87
|
|
|
108
|
-
|
|
88
|
+
Coordinate one through flat verbs on the model, beside `create`/`update`/`retrieve`:
|
|
89
|
+
`ablo.<model>.claim(id, ...)` to claim a row, `ablo.<model>.claimState(id)` to read
|
|
90
|
+
who holds it (synchronous; never blocks), and `ablo.<model>.release(id)` to release
|
|
91
|
+
early. Claims are **advisory** — they serialize on contention rather than locking.
|
|
109
92
|
|
|
110
|
-
|
|
111
|
-
a target before the write lands. Like Stripe's `PaymentIntent`, one
|
|
112
|
-
self-describing object carries the whole lifecycle in a single `status` field.
|
|
113
|
-
It lives on the **coordination plane**: ephemeral, TTL'd, broadcast to peers in
|
|
114
|
-
real time, and never persisted as a row.
|
|
115
|
-
|
|
116
|
-
Read or open one through the model accessor — `ablo.<model>.intent(id)` — which
|
|
117
|
-
sits beside `create`/`update`/`retrieve` and returns a handle **synchronously**,
|
|
118
|
-
so you can inspect who holds a target without awaiting.
|
|
119
|
-
|
|
120
|
-
### The Intent object
|
|
93
|
+
### The Claim State Object
|
|
121
94
|
|
|
122
95
|
| Field | Type | Description |
|
|
123
96
|
|---|---|---|
|
|
124
|
-
| `object` | `'
|
|
125
|
-
| `id` | string | Unique identifier for the
|
|
97
|
+
| `object` | `'claim'` | String representing the object's type. |
|
|
98
|
+
| `id` | string | Unique identifier for the claim. |
|
|
126
99
|
| `status` | `'active' \| 'committed' \| 'expired' \| 'canceled'` | The whole lifecycle, in one field. |
|
|
127
100
|
| `target` | `{ type, id, field? }` | What is being coordinated. |
|
|
128
101
|
| `action` | string | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
|
|
129
|
-
| `heldBy` | string | Participant id holding the
|
|
102
|
+
| `heldBy` | string | Participant id holding the claim. |
|
|
130
103
|
| `participantKind` | `'human' \| 'agent'` | Whether a human session or an agent holds it. |
|
|
131
104
|
| `expiresAt` | string | Ms-epoch at which the server auto-expires it if the holder doesn't finish. |
|
|
132
105
|
|
|
133
106
|
```json
|
|
134
107
|
{
|
|
135
|
-
"object": "
|
|
136
|
-
"id": "
|
|
108
|
+
"object": "claim",
|
|
109
|
+
"id": "claim_3MtwBwLkdIwHu7ix",
|
|
137
110
|
"status": "active",
|
|
138
|
-
"target": { "type": "
|
|
111
|
+
"target": { "type": "weatherReports", "id": "report_stockholm", "field": "status" },
|
|
139
112
|
"action": "editing",
|
|
140
|
-
"heldBy": "agent:
|
|
113
|
+
"heldBy": "agent:report-writer",
|
|
141
114
|
"participantKind": "agent",
|
|
142
115
|
"expiresAt": "1716580000000"
|
|
143
116
|
}
|
|
@@ -146,128 +119,54 @@ so you can inspect who holds a target without awaiting.
|
|
|
146
119
|
### Lifecycle
|
|
147
120
|
|
|
148
121
|
```
|
|
149
|
-
claim()
|
|
122
|
+
claim(id) update(id) lands
|
|
150
123
|
(free) ───────────▶ active ───────────────────────▶ committed
|
|
151
124
|
│
|
|
152
125
|
┌───────────┴───────────┐
|
|
153
126
|
▼ ▼
|
|
154
127
|
canceled expired
|
|
155
|
-
|
|
128
|
+
(release w/o write) (TTL; holder died)
|
|
156
129
|
```
|
|
157
130
|
|
|
158
|
-
A target is free when `ablo.<model>.
|
|
159
|
-
states drop out of the live stream
|
|
160
|
-
`active`.
|
|
131
|
+
A target is free when `ablo.<model>.claimState(id)` is `null`. Terminal
|
|
132
|
+
states drop out of the live stream, so a present claim is active.
|
|
161
133
|
|
|
162
134
|
### Reading and claiming
|
|
163
135
|
|
|
164
|
-
|
|
165
|
-
|
|
136
|
+
`claimState(id)` is the read side for observers: synchronous, never blocks, and
|
|
137
|
+
returns the live claim state object (or `null`). `claim(id, ...)` is the write side:
|
|
138
|
+
it claims the row and returns the row. Because the claim is **advisory**, if
|
|
139
|
+
someone else already holds the row, `claim` waits for them to finish, then
|
|
140
|
+
re-reads the row before handing it back — so you always proceed from fresh state.
|
|
141
|
+
Default reads stay open; server/model reads can opt into `ifClaimed: 'wait'` or
|
|
142
|
+
`ifClaimed: 'fail'` when they should not read through active work.
|
|
166
143
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
144
|
+
```ts
|
|
145
|
+
const claim = ablo.weatherReports.claimState('report_stockholm');
|
|
146
|
+
if (claim) {
|
|
147
|
+
claim.heldBy;
|
|
148
|
+
claim.action;
|
|
172
149
|
}
|
|
173
150
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
`task.update(...)` carries the same stale-check as a plain update: it rejects
|
|
181
|
-
with `AbloStaleContextError` if the row advanced past your claim point, so you
|
|
182
|
-
re-read before retrying. The intent releases automatically when `update`
|
|
183
|
-
resolves; call `task.finish()` if the work ends without a write.
|
|
184
|
-
|
|
185
|
-
`task.whenFree({ timeout })` waits until the target is free. Pass `timeout` only
|
|
186
|
-
when your product needs an upper bound.
|
|
187
|
-
|
|
188
|
-
### Cross-resource coordination
|
|
189
|
-
|
|
190
|
-
For lower-level coordination that isn't scoped to a single model row, the
|
|
191
|
-
top-level `ablo.intents` resource (`create`, `list`, `waitFor`) remains
|
|
192
|
-
available. Most callers should prefer `ablo.<model>.intent(id)`.
|
|
193
|
-
|
|
194
|
-
## Advanced Commit API
|
|
195
|
-
|
|
196
|
-
Most callers use `ablo.<model>.update(...)` or `resource.update(...)`. For atomic
|
|
197
|
-
batches or custom runtimes, use `commits.create`.
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
await ablo.commits.create({
|
|
201
|
-
wait: 'confirmed',
|
|
202
|
-
operations: [
|
|
203
|
-
{
|
|
204
|
-
action: 'update',
|
|
205
|
-
resource: 'tasks',
|
|
206
|
-
id: 'task_123',
|
|
207
|
-
data: { status: 'done' },
|
|
208
|
-
readAt: stamp,
|
|
209
|
-
onStale: 'reject',
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
});
|
|
151
|
+
const updated = await ablo.weatherReports.claim(
|
|
152
|
+
'report_stockholm',
|
|
153
|
+
async (report) => ablo.weatherReports.update(report.id, { status: 'ready' }),
|
|
154
|
+
{ action: 'editing', ttl: '2m' },
|
|
155
|
+
);
|
|
213
156
|
```
|
|
214
157
|
|
|
215
|
-
|
|
216
|
-
|
|
158
|
+
Writes go through the normal flat `ablo.<model>.update(id, data)`. While you hold
|
|
159
|
+
a claim on `id`, that `update` is automatically stale-guarded: it rejects with
|
|
160
|
+
`AbloStaleContextError` if the row advanced past your claim point, so you re-read
|
|
161
|
+
before retrying. The callback form releases automatically when the callback
|
|
162
|
+
returns or throws, or call `ablo.weatherReports.release(id)` if you claimed manually and
|
|
163
|
+
need to release early.
|
|
217
164
|
|
|
218
165
|
## Agent
|
|
219
166
|
|
|
220
167
|
Most agents should import the same schema as the app and call
|
|
221
|
-
`ablo.<model>.load(...)
|
|
222
|
-
`
|
|
223
|
-
the app schema; it creates the capability and task internally and returns
|
|
224
|
-
`done`, `failed`, or `cancelled`.
|
|
225
|
-
|
|
226
|
-
## Data Source
|
|
227
|
-
|
|
228
|
-
Use `dataSource(...)` only when the customer's app database remains canonical
|
|
229
|
-
and Ablo should call a signed endpoint instead of storing customer rows itself.
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
import { dataSource } from '@abloatai/ablo';
|
|
233
|
-
import { schema } from './ablo.schema';
|
|
234
|
-
|
|
235
|
-
export const POST = dataSource({
|
|
236
|
-
schema,
|
|
237
|
-
apiKey: process.env.ABLO_API_KEY,
|
|
238
|
-
async commit({ operations, clientTxId, context }) {
|
|
239
|
-
// Write operations to the customer's database transaction.
|
|
240
|
-
return { rows: [] };
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
The SDK still uses `ablo.<model>.update(...)`. The Data Source endpoint is a
|
|
246
|
-
server-to-server storage adapter. See [Connect Your Database](./data-sources.md).
|
|
247
|
-
|
|
248
|
-
## Capability
|
|
249
|
-
|
|
250
|
-
Capabilities are the lower-level permission boundary. Most apps should let
|
|
251
|
-
`agent.run(...)` create and revoke them.
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
const capability = await api.capabilities.create({
|
|
255
|
-
participantKind: 'agent',
|
|
256
|
-
participantId: 'agent:task-writer',
|
|
257
|
-
// Strings derive from the schema's `identityRoles` templates
|
|
258
|
-
// (see integration-guide.md §1) or a model's `syncGroupFormat`.
|
|
259
|
-
syncGroups: ['org:acme', 'user:agent:task-writer'],
|
|
260
|
-
operations: ['tasks.retrieve', 'tasks.update'],
|
|
261
|
-
lease: '10m',
|
|
262
|
-
});
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Use `lease` as a crash cleanup window. Normal agent runs still close when the
|
|
266
|
-
handler returns, fails, or is cancelled.
|
|
267
|
-
|
|
268
|
-
For the design rationale — why capabilities instead of static API keys,
|
|
269
|
-
why the lease + signature + revocation triple, and how this maps to AWS
|
|
270
|
-
STS / Vault / Auth0 Token Vault — see [Capabilities](./capabilities.md#why-capabilities-not-api-keys).
|
|
168
|
+
`ablo.<model>.load(...)`, `ablo.<model>.claim(...)`, and
|
|
169
|
+
`ablo.<model>.update(...)`.
|
|
271
170
|
|
|
272
171
|
## Errors
|
|
273
172
|
|
|
@@ -283,6 +182,6 @@ All SDK errors extend `AbloError` and expose a stable `type` string.
|
|
|
283
182
|
| `AbloValidationError` | Invalid input. |
|
|
284
183
|
| `AbloServerError` | Server-side 5xx. |
|
|
285
184
|
| `AbloStaleContextError` | `readAt` no longer matches current state. |
|
|
286
|
-
| `
|
|
185
|
+
| `AbloClaimedError` | Active claim conflict or claim wait timeout. |
|
|
287
186
|
|
|
288
187
|
See [Client Behavior](./client-behavior.md) for retry and timeout guidance.
|
package/docs/audit.md
CHANGED
|
@@ -12,11 +12,11 @@ tamper-evident, queryable, exportable.
|
|
|
12
12
|
actorId: string,
|
|
13
13
|
onBehalfOfKind: 'user' | 'agent' | 'system' | null,
|
|
14
14
|
onBehalfOfId: string | null,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
credentialId: string | null,
|
|
16
|
+
credentialLabel: string | null,
|
|
17
17
|
delegationChainRoot: string | null, // always points at a human
|
|
18
|
-
|
|
19
|
-
actionType: string, // e.g. '
|
|
18
|
+
causedByRunId: string | null,
|
|
19
|
+
actionType: string, // e.g. 'weatherReport.update'
|
|
20
20
|
modelName: string | null, // e.g. 'claude-opus-4-7'
|
|
21
21
|
diffSummary: unknown,
|
|
22
22
|
// tamper-evident
|
|
@@ -37,7 +37,7 @@ a thing in this system; every chain starts with a person.
|
|
|
37
37
|
```bash
|
|
38
38
|
curl https://<your-app>/api/orgs/<slug>/audit/verify-chain?\
|
|
39
39
|
principalKind=agent\
|
|
40
|
-
&principalId=
|
|
40
|
+
&principalId=weather-agent-v3
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Returns either:
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
The `ablo` CLI gets you from an empty project to live-syncing data: scaffold a
|
|
4
|
+
schema, authenticate, push the schema, and watch it sync. Your
|
|
5
|
+
`defineSchema(...)` is the single source of truth — the CLI and the hosted
|
|
6
|
+
server lower it to **the same SQL** through one engine
|
|
7
|
+
(`generateProvisionPlan` / `generateMigrationPlan` in `@abloatai/ablo/schema`).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ablo init # scaffold ablo/schema.ts + client
|
|
11
|
+
npx ablo login # authorize in the browser
|
|
12
|
+
npx ablo dev # push schema to the test sandbox + watch
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Authenticate
|
|
16
|
+
|
|
17
|
+
`ablo login` runs the OAuth 2.0 device flow: it opens your browser, you choose
|
|
18
|
+
**log in** or **create an account** and approve, and the CLI provisions a
|
|
19
|
+
**test + live key pair** (90-day, restricted) and stores them locally. This
|
|
20
|
+
mirrors `stripe login`.
|
|
21
|
+
|
|
22
|
+
| Command | What it does |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `ablo login` | Authorize in the browser; provisions + stores a test and a live key. |
|
|
25
|
+
| `ablo logout` | Remove the stored keys. |
|
|
26
|
+
| `ablo status` | Show the active org, mode, both keys (prefix + expiry), and server health. |
|
|
27
|
+
| `ablo mode [test\|live]` | Switch the active mode. With no argument, prompts. |
|
|
28
|
+
|
|
29
|
+
Keys are stored in `~/.config/ablo/config.json` (mode `0600`). In **CI**, don't
|
|
30
|
+
log in — set `ABLO_API_KEY`, which always overrides the stored key.
|
|
31
|
+
|
|
32
|
+
## Test vs live
|
|
33
|
+
|
|
34
|
+
Like Stripe, every account has a **test** mode and a **live** mode, and a key
|
|
35
|
+
belongs to one of them. Test keys are bound to an isolated sandbox: their reads
|
|
36
|
+
and writes never touch live data. Switch with `ablo mode`; `ablo dev` is always
|
|
37
|
+
test mode by design.
|
|
38
|
+
|
|
39
|
+
The schema, however, is **shared** across the org — pushing a schema (from
|
|
40
|
+
either mode) defines the same models test and live see; only the rows differ.
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
| Command | What it does | Flags |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `ablo init` | Scaffold `ablo/` (`schema.ts`, client, optional Data Source / agent / component), write `.env`, install the SDK. Offers to log in at the end. | — |
|
|
47
|
+
| `ablo login` / `logout` / `status` | Authentication & status (above). | — |
|
|
48
|
+
| `ablo mode [test\|live]` | Switch active mode. | — |
|
|
49
|
+
| `ablo dev` | **Hosted** — push the schema to your test sandbox, then watch `ablo/schema.ts` and re-push on save. | `--no-watch`, `--schema <path>`, `--export <name>`, `--url <url>` |
|
|
50
|
+
| `ablo logs` | Tail your scope's commit activity (`stripe logs tail`). Follows by default. | `-n, --tail <N>`, `--since <dur\|ts>`, `--model`, `--op`, `--json`, `--no-follow`, `--mode test\|live` |
|
|
51
|
+
| `ablo schema push` | **Hosted** — upload the schema to Ablo; the server diffs, migrates, and activates it. | `--force`, `--rename old:new`, `--backfill model.field=value`, `--schema`, `--export`, `--url` |
|
|
52
|
+
| `ablo pull` | **BYO** — generate `defineSchema(...)` from your existing tables (read-only, like `prisma db pull`). | `--out <path>`, `--app-schema <name>`, `--import <pkg>`, `--force` |
|
|
53
|
+
| `ablo check` | **BYO** — verify your *existing* tables fit the schema (read-only, no DDL). | `--schema <path>`, `--export <name>`, `--app-schema <name>` |
|
|
54
|
+
| `ablo generate` | Emit TypeScript types from the schema. | `--out <path>`, `--schema`, `--export` |
|
|
55
|
+
|
|
56
|
+
## `ablo dev`
|
|
57
|
+
|
|
58
|
+
The development loop. It pushes `ablo/schema.ts` to your **test sandbox**,
|
|
59
|
+
prints the env line your app needs, then watches the file and re-pushes on every
|
|
60
|
+
save (300 ms debounce). It refuses live keys so a tight save loop can never
|
|
61
|
+
churn production data.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx ablo dev # push + watch
|
|
65
|
+
npx ablo dev --no-watch # push once and exit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## `ablo logs`
|
|
69
|
+
|
|
70
|
+
Tail commit activity, like `stripe logs tail`. Scope comes from the key — a test
|
|
71
|
+
key streams only its sandbox's writes, a live key the org's — so you never pass
|
|
72
|
+
an org. Follows by default; `--no-follow` prints recent and exits.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx ablo logs # last 50, then stream
|
|
76
|
+
npx ablo logs -n 100 --model task # backfill 100, one model
|
|
77
|
+
npx ablo logs --since 15m --json # last 15m as NDJSON, then stream
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each line is `time · op · model · id · actor`. `--json` emits one event per line
|
|
81
|
+
(NDJSON) for piping to `jq` or an agent.
|
|
82
|
+
|
|
83
|
+
## `ablo pull`
|
|
84
|
+
|
|
85
|
+
Generate `defineSchema(...)` from the tables you already have — the inverse of
|
|
86
|
+
provisioning, and read-only (like `prisma db pull`). It introspects
|
|
87
|
+
`DATABASE_URL`, emits a model per adoptable table (one that has `id` +
|
|
88
|
+
`organization_id`), maps Postgres types back to Zod, and writes `ablo/schema.ts`.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
DATABASE_URL=postgres://… npx ablo pull
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
It never touches the database, and won't overwrite an existing schema without
|
|
95
|
+
`--force`. Introspection is lossy — enum members, JSON shape, relations, and
|
|
96
|
+
defaults can't be recovered from columns — so treat the output as a starting
|
|
97
|
+
point: review it, then run `ablo check`.
|
|
98
|
+
|
|
99
|
+
## `ablo check`
|
|
100
|
+
|
|
101
|
+
The BYO front door. Instead of migrating (DDL on your database), Ablo *adopts*
|
|
102
|
+
the tables you already have: `ablo check` introspects `DATABASE_URL`, compares it
|
|
103
|
+
to your `defineSchema(...)`, and reports — per model — whether the table is
|
|
104
|
+
adoptable. It never writes or alters anything.
|
|
105
|
+
|
|
106
|
+
A table is adoptable when it has a primary key `id` and (for org-scoped models)
|
|
107
|
+
an `organization_id` column — the tenancy marker the engine isolates on. Every
|
|
108
|
+
other table in your database is ignored.
|
|
109
|
+
|
|
110
|
+
**Why `organization_id`?** It's the one column that makes a table safe to
|
|
111
|
+
multiplayer-sync. Row-level security scopes every read and write by it (org A
|
|
112
|
+
can't see org B's rows), and the engine routes realtime deltas by `org:<id>`. A
|
|
113
|
+
table without a tenancy key has no isolation boundary, so Ablo excludes it
|
|
114
|
+
**by default** rather than risk exposing it across tenants. If your tenancy
|
|
115
|
+
column has a different name, keep that table behind a
|
|
116
|
+
[Data Source endpoint](/data-sources) for now.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
DATABASE_URL=postgres://… npx ablo check
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
✓ tasks → tasks (id, organization_id ok)
|
|
124
|
+
✗ projects → projects
|
|
125
|
+
• missing "organization_id" — add it, or move this model behind a Data Source
|
|
126
|
+
2 models · 1 ok · 1 error
|
|
127
|
+
12 other tables in your database — ignored by Ablo
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If a table can't carry `organization_id` (or has business logic Ablo shouldn't
|
|
131
|
+
bypass), keep it behind a [Data Source endpoint](/data-sources) rather than
|
|
132
|
+
reshaping it. `ablo check` is read-only; it never proposes a migration.
|
|
133
|
+
|
|
134
|
+
## `migrate` vs `schema push`
|
|
135
|
+
|
|
136
|
+
Two front doors to the same engine. Use `migrate` when your app owns the
|
|
137
|
+
database (it applies to `DATABASE_URL`); use `schema push` (and `dev`) on the
|
|
138
|
+
hosted path (the server applies to Ablo-managed Postgres and version-gates
|
|
139
|
+
connecting clients).
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
ablo migrate --dry-run # preview the exact SQL
|
|
143
|
+
ablo migrate # apply to DATABASE_URL
|
|
144
|
+
ablo migrate --output schema.sql # write SQL to a file
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Zod → Postgres type mapping
|
|
148
|
+
|
|
149
|
+
The one type map, shared by both paths (there is no second mapping):
|
|
150
|
+
|
|
151
|
+
| Zod | Postgres |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `z.string()` | `TEXT` |
|
|
154
|
+
| `z.number()` | `DOUBLE PRECISION` — never `INTEGER`; a Zod number may be fractional, and truncating is silent data loss |
|
|
155
|
+
| `z.boolean()` | `BOOLEAN` |
|
|
156
|
+
| `z.date()` | `TIMESTAMPTZ` |
|
|
157
|
+
| `z.enum([...])` | `TEXT` + a `CHECK (col IN (...))` constraint |
|
|
158
|
+
| `z.object` / `z.array` / `z.record` / `z.union` / `z.custom` | `JSONB` |
|
|
159
|
+
| `.optional()` / `.nullable()` | nullable column |
|
|
160
|
+
|
|
161
|
+
Each table also gets the platform columns (`id`, `organization_id`,
|
|
162
|
+
`created_by`, `created_at`, `updated_at`), an `organization_id` index, and
|
|
163
|
+
row-level security keyed on `current_setting('app.current_org_id')` for tenant
|
|
164
|
+
isolation.
|
|
165
|
+
|
|
166
|
+
`.default(...)` is **not** emitted as a SQL column default — Zod applies the
|
|
167
|
+
default at write time (`create`), in one place, so a DB default and a schema
|
|
168
|
+
default can't drift.
|
|
169
|
+
|
|
170
|
+
## Structured errors
|
|
171
|
+
|
|
172
|
+
A failed migration aborts the whole transaction (nothing partial lands) and
|
|
173
|
+
reports the same `migration_failed` shape on both paths — naming the statement
|
|
174
|
+
that broke and the Postgres SQLSTATE, not just "migration failed".
|
|
175
|
+
|
|
176
|
+
`ablo migrate` (local) logs it:
|
|
177
|
+
|
|
178
|
+
```txt
|
|
179
|
+
[migrate] migration plan failed {
|
|
180
|
+
code: 'migration_failed',
|
|
181
|
+
failedStatement: 'ALTER TABLE "public"."tasks" RENAME COLUMN a TO b;',
|
|
182
|
+
failedStatementIndex: 4,
|
|
183
|
+
pgCode: '42P01',
|
|
184
|
+
durationMs: 133
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`ablo schema push` (hosted) returns the canonical error envelope (HTTP 500),
|
|
189
|
+
which the SDK reconstructs as a typed `AbloServerError`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"type": "AbloServerError",
|
|
194
|
+
"code": "migration_failed",
|
|
195
|
+
"message": "schema migration failed: relation \"...\" does not exist",
|
|
196
|
+
"doc_url": "https://docs.abloatai.com/errors#migration_failed",
|
|
197
|
+
"failedStatement": "ALTER TABLE ... RENAME COLUMN a TO b;",
|
|
198
|
+
"pgCode": "42P01"
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The pushed artifact is recorded `failed` and is never activated, so a broken
|
|
203
|
+
migration can't leave clients gated against tables that don't match.
|
|
204
|
+
|
|
205
|
+
## Environment
|
|
206
|
+
|
|
207
|
+
| Variable | Purpose | Default |
|
|
208
|
+
| --- | --- | --- |
|
|
209
|
+
| `ABLO_API_KEY` | Authenticate without `ablo login` (CI). Always overrides the stored key. | — |
|
|
210
|
+
| `ABLO_API_URL` | Control-plane / API host (`schema push`, `dev`, `status`). | `https://api.abloatai.com` |
|
|
211
|
+
| `ABLO_AUTH_URL` | Dashboard origin for `ablo login`'s device flow. | `https://abloatai.com` |
|
|
212
|
+
| `ABLO_CONFIG_DIR` / `XDG_CONFIG_HOME` | Where the credential file lives. | `~/.config/ablo` |
|