@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
|
@@ -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,54 @@ 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
|
+
> Loop view only. Full claim reference — methods, the claim-state object, the
|
|
55
|
+
> `queue`, errors — is [Coordination](./coordination.md).
|
|
56
|
+
|
|
57
|
+
Claims broadcast across the org. Claim a row through the flat model verb, write
|
|
58
|
+
through the normal `update`, and the claim releases when the callback returns:
|
|
115
59
|
|
|
116
60
|
```ts
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await
|
|
61
|
+
await ablo.weatherReports.claim(
|
|
62
|
+
'report_stockholm',
|
|
63
|
+
async (report) => {
|
|
64
|
+
await ablo.weatherReports.update(report.id, { status: 'ready' }); // stale-guarded under the claim
|
|
65
|
+
},
|
|
66
|
+
{ action: 'editing' },
|
|
67
|
+
);
|
|
121
68
|
```
|
|
122
69
|
|
|
123
|
-
`
|
|
124
|
-
|
|
125
|
-
`
|
|
126
|
-
|
|
127
|
-
|
|
70
|
+
`ablo.weatherReports.claimState('report_stockholm')` reads the live claim (or `null`) without
|
|
71
|
+
blocking. The claim is **advisory**: if another participant holds the row,
|
|
72
|
+
`claim` waits for them to finish and re-reads before handing back the row. The
|
|
73
|
+
same signal is visible to every schema client through `claimState(id)` and the live
|
|
74
|
+
claim stream.
|
|
128
75
|
|
|
129
76
|
## Conflict resolution
|
|
130
77
|
|
|
@@ -137,16 +84,6 @@ Schema updates can carry `readAt` and `onStale`. If the state advanced past
|
|
|
137
84
|
|
|
138
85
|
The choice is per-commit. No CRDT default; the policy is explicit.
|
|
139
86
|
|
|
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
87
|
## The contract in one sentence
|
|
151
88
|
|
|
152
|
-
Declare schema, load state, coordinate
|
|
89
|
+
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
|
|
package/docs/mcp.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Model Context Protocol
|
|
2
2
|
|
|
3
|
-
Ablo
|
|
4
|
-
assistant — Claude Code, Cursor, Windsurf — and your sync
|
|
3
|
+
Ablo ships an MCP server at `/api/mcp`. Connect any MCP-compatible AI
|
|
4
|
+
assistant — Claude Code, Cursor, Windsurf — and your sync models become
|
|
5
5
|
typed, callable tools.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
@@ -14,38 +14,23 @@ Pick your client:
|
|
|
14
14
|
|
|
15
15
|
## How it works
|
|
16
16
|
|
|
17
|
-
Each
|
|
17
|
+
Each model you declare becomes one or more MCP tools:
|
|
18
18
|
|
|
19
|
-
|
|
|
19
|
+
| Model method | MCP tool name | What it does |
|
|
20
20
|
|---|---|---|
|
|
21
|
-
| `retrieve` | `<
|
|
22
|
-
| `list` | `<
|
|
23
|
-
| `update` | `<
|
|
24
|
-
| `<model>.
|
|
21
|
+
| `retrieve` | `<model>.retrieve` | Returns the row + a stamp. |
|
|
22
|
+
| `list` | `<model>.list` | Cursor-paginated discovery. |
|
|
23
|
+
| `update` | `<model>.update` | Write, requires the prior stamp. |
|
|
24
|
+
| `<model>.claim` | `claim.create` | Claim a row before writing, then release when held work finishes. |
|
|
25
25
|
|
|
26
26
|
The assistant gets typed JSON schemas, real argument types, and typed
|
|
27
27
|
rejections when it writes stale state. No invention, no hallucinated IDs.
|
|
28
28
|
|
|
29
29
|
## Auth
|
|
30
30
|
|
|
31
|
-
The MCP transport
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
const capability = await ablo.capabilities.create({
|
|
36
|
-
participantKind: 'agent',
|
|
37
|
-
participantId: 'agent:claude-code',
|
|
38
|
-
// Strings derive from the schema's `identityRoles` templates
|
|
39
|
-
// (see integration-guide.md §1).
|
|
40
|
-
syncGroups: ['org:acme'],
|
|
41
|
-
operations: ['tasks.retrieve', 'tasks.update'],
|
|
42
|
-
label: 'claude-code dev session',
|
|
43
|
-
lease: '8h',
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Pass `capability.token` into the MCP client's auth header configuration.
|
|
48
|
-
See your client's setup guide for the exact mechanism.
|
|
31
|
+
The MCP transport uses a scoped bearer token issued by your server. Pass that
|
|
32
|
+
token into the MCP client's auth header configuration. See your client's setup
|
|
33
|
+
guide for the exact mechanism.
|
|
49
34
|
|
|
50
35
|
## Limits
|
|
51
36
|
|
package/docs/quickstart.md
CHANGED
|
@@ -21,7 +21,7 @@ export ABLO_API_KEY=sk_test_...
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
`ABLO_API_KEY` is for trusted server runtimes. Browser apps should use the React
|
|
24
|
-
provider with a scoped
|
|
24
|
+
provider with a scoped session route, not a bundled API key.
|
|
25
25
|
|
|
26
26
|
## 3. Declare a Schema
|
|
27
27
|
|
|
@@ -44,7 +44,7 @@ export const ablo = Ablo({
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Customer apps should always pass `schema`. Treat it like Prisma's schema file:
|
|
47
|
-
it is the source of truth for typed model
|
|
47
|
+
it is the source of truth for typed model clients, realtime subscriptions,
|
|
48
48
|
agent writes, and Data Source requests.
|
|
49
49
|
|
|
50
50
|
## 4. Create and Update
|
|
@@ -81,71 +81,65 @@ ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
|
|
|
81
81
|
## 6. AI Activity on Existing State
|
|
82
82
|
|
|
83
83
|
When AI or background work will touch an existing row for more than a quick
|
|
84
|
-
write, coordinate through `ablo.<model>.
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
write, coordinate through the flat model verbs: `ablo.<model>.claim(id, ...)` to
|
|
85
|
+
claim the row (returns the row), `ablo.<model>.claimState(id)` to read who's working
|
|
86
|
+
on it (synchronous; never blocks), and the normal `ablo.<model>.update(id, ...)`
|
|
87
|
+
to write. Normal reads still work while the claim is held; server reads can opt
|
|
88
|
+
into `ifClaimed: 'wait'` or `ifClaimed: 'fail'` when they should not read through
|
|
89
|
+
active work. The callback form releases the claim when the callback returns or
|
|
90
|
+
throws.
|
|
87
91
|
|
|
88
92
|
```ts
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
await report.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
forecast: weather.summary,
|
|
105
|
-
});
|
|
93
|
+
// Claim the row so other participants serialize behind us while we work.
|
|
94
|
+
await ablo.weatherReports.claim(
|
|
95
|
+
'weather_stockholm',
|
|
96
|
+
async (report) => {
|
|
97
|
+
// Your existing weather tool or agent call. While this runs, other clients
|
|
98
|
+
// see that weather_stockholm is being checked.
|
|
99
|
+
const weather = await weatherAgent.getWeather(report.location);
|
|
100
|
+
|
|
101
|
+
await ablo.weatherReports.update(report.id, {
|
|
102
|
+
status: 'ready',
|
|
103
|
+
forecast: weather.summary,
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
{ action: 'checking_weather', ttl: '2m' },
|
|
107
|
+
);
|
|
106
108
|
```
|
|
107
109
|
|
|
108
|
-
Ablo does not fetch the weather.
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
Ablo does not fetch the weather. The claim is **advisory**: if another
|
|
111
|
+
participant already holds the row, `claim` waits for them to finish and re-reads
|
|
112
|
+
before handing back the row. While you hold the claim, `update(id, ...)` is
|
|
113
|
+
stale-guarded and rejects with `AbloStaleContextError` if the row changed under
|
|
114
|
+
you. The claim releases automatically once the callback returns or throws.
|
|
111
115
|
|
|
112
|
-
## 7. Multiplayer and
|
|
116
|
+
## 7. Multiplayer and Claimed Work
|
|
113
117
|
|
|
114
118
|
There is no separate multiplayer mode. Use the same schema client for human UI,
|
|
115
119
|
server actions, and agents; Ablo fans out confirmed writes and keeps active
|
|
116
|
-
|
|
120
|
+
claims visible on the same model row.
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
schema clients,
|
|
122
|
+
`claimState(id)` tells you when another human or agent is active on the same row.
|
|
123
|
+
For schema clients, `claim(id, work)` waits fairly, re-reads, and then lets you
|
|
124
|
+
write through the model.
|
|
120
125
|
|
|
121
126
|
```ts
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (busy.length > 0) {
|
|
128
|
-
await ablo.intents.waitFor(
|
|
129
|
-
{ resource: 'weatherReports', id: 'weather_stockholm' },
|
|
130
|
-
{ timeout: 30_000 },
|
|
131
|
-
);
|
|
127
|
+
const active = ablo.weatherReports.claimState('weather_stockholm');
|
|
128
|
+
if (active) {
|
|
129
|
+
console.log(`${active.heldBy} is ${active.action}`);
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
await ablo.weatherReports.
|
|
132
|
+
await ablo.weatherReports.claim('weather_stockholm', async (report) => {
|
|
133
|
+
await ablo.weatherReports.update(report.id, { status: 'ready' });
|
|
134
|
+
});
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
- `return` returns immediately with active intents.
|
|
141
|
-
- `wait` waits for the intent stream to clear.
|
|
142
|
-
- `fail` throws `AbloBusyError` with the active intents attached.
|
|
137
|
+
Use `{ wait: false }` on `claim` when work should be skipped instead of queued
|
|
138
|
+
behind an active holder.
|
|
143
139
|
|
|
144
140
|
## 8. Next Steps
|
|
145
141
|
|
|
146
|
-
Keep using the schema client for app and agent writes.
|
|
147
|
-
schema-less agent wrapper only when a worker intentionally cannot import the
|
|
148
|
-
app schema.
|
|
142
|
+
Keep using the schema client for app and agent writes.
|
|
149
143
|
|
|
150
144
|
- [Integration Guide](./integration-guide.md) explains the full app, React, Data Source, multiplayer, and agent path.
|
|
151
145
|
- [Guarantees](./guarantees.md) explains what confirmed writes and stale checks mean.
|
package/docs/react.md
CHANGED
|
@@ -14,21 +14,71 @@ The React bindings ship with the main package — no extra install.
|
|
|
14
14
|
import { useAblo } from '@abloatai/ablo/react';
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## AbloProvider
|
|
18
|
+
|
|
19
|
+
Mount it once near the root of your tree. It owns the connection, the local
|
|
20
|
+
pool, and the engine lifecycle; everything below it reads with `useAblo`.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
'use client';
|
|
24
|
+
|
|
25
|
+
import { AbloProvider } from '@abloatai/ablo/react';
|
|
26
|
+
import { schema } from '@/ablo/schema';
|
|
27
|
+
|
|
28
|
+
export function Providers({
|
|
29
|
+
children,
|
|
30
|
+
user, // resolved server-side from YOUR auth
|
|
31
|
+
}: {
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
user: { id: string; teamIds: string[] };
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<AbloProvider
|
|
37
|
+
schema={schema}
|
|
38
|
+
userId={user.id}
|
|
39
|
+
teamIds={user.teamIds}
|
|
40
|
+
fallback={<AppSkeleton />}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</AbloProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`schema` is the only required prop. The rest are situational:
|
|
49
|
+
|
|
50
|
+
| Prop | Default | Purpose |
|
|
51
|
+
| ------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `schema` | — | **Required.** From `defineSchema()`. Determines the typed hook surface. |
|
|
53
|
+
| `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles.extract`. Not the security boundary. |
|
|
54
|
+
| `teamIds` | resolved from auth | Team ids expanded into team sync groups via `identityRoles`. |
|
|
55
|
+
| `syncGroups` | full allowed set | **Narrows** the subscription to a subset of what auth allows (e.g. `['deck:abc123']`). Never widens it. |
|
|
56
|
+
| `url` | hosted endpoint | WebSocket URL of the sync server (`wss://…`). Hosted apps omit it. |
|
|
57
|
+
| `apiKey` | session/cookie | Bootstrap auth. Browser apps **omit this** — the key stays server-side. See Identity below. |
|
|
58
|
+
| `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
|
|
59
|
+
| `bootstrapMode` | `'full'` | `'full'` pulls the org's baseline before ready; `'none'` skips the baseline and processes live deltas only.|
|
|
60
|
+
| `persistence` | `'volatile'` | `'indexeddb'` opts into a durable browser cache that survives reloads. |
|
|
61
|
+
| `onSessionExpired` | — | Fired after the engine has already purged on a rejected session — use for redirect-to-sign-in. |
|
|
62
|
+
| `onError` | — | Engine / WebSocket / `postBootstrap` errors. Wire to Sentry / Datadog. |
|
|
63
|
+
|
|
64
|
+
Where `userId` / `teamIds` / `syncGroups` come from, and why the API key never
|
|
65
|
+
reaches the browser, is the whole of
|
|
66
|
+
[Identity & Sync Groups](./identity.md) — read that if it isn't obvious how org
|
|
67
|
+
/ team / user map to what a participant can see.
|
|
68
|
+
|
|
69
|
+
## useAblo — model client
|
|
18
70
|
|
|
19
71
|
```tsx
|
|
20
72
|
'use client';
|
|
21
73
|
|
|
22
74
|
import { useAblo } from '@abloatai/ablo/react';
|
|
23
75
|
|
|
24
|
-
export function
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
) ?? [];
|
|
29
|
-
const busy = intents.length > 0;
|
|
76
|
+
export function ReportView({ report: serverReport }: { report: { id: string; location: string } }) {
|
|
77
|
+
const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
|
|
78
|
+
const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
|
|
79
|
+
const claimed = Boolean(active);
|
|
30
80
|
|
|
31
|
-
return <article>{
|
|
81
|
+
return <article>{report.location}</article>;
|
|
32
82
|
}
|
|
33
83
|
```
|
|
34
84
|
|
|
@@ -37,9 +87,9 @@ The hook:
|
|
|
37
87
|
1. Reads through the same `ablo.<model>` methods as the rest of the SDK.
|
|
38
88
|
2. Tracks the model fields read by the selector and re-renders when confirmed
|
|
39
89
|
deltas arrive.
|
|
40
|
-
3. Lets Server Component data stay outside the hook: use `??
|
|
90
|
+
3. Lets Server Component data stay outside the hook: use `?? serverReport` when a
|
|
41
91
|
parent already loaded the row.
|
|
42
|
-
4. Works for coordination state too, such as `ablo.
|
|
92
|
+
4. Works for coordination state too, such as `ablo.weatherReports.claimState(id)`.
|
|
43
93
|
|
|
44
94
|
Use the zero-argument form only when you need the full client for callbacks,
|
|
45
95
|
effects, or writes:
|
|
@@ -50,16 +100,16 @@ const abloClient = useAblo();
|
|
|
50
100
|
|
|
51
101
|
Prefer selector reads like `useAblo((ablo) => ablo.<model>.retrieve(id))`.
|
|
52
102
|
String model names are kept on older hooks for compatibility, but first examples
|
|
53
|
-
should use the same model-
|
|
103
|
+
should use the same model-client shape as the rest of the SDK.
|
|
54
104
|
|
|
55
|
-
For collections, keep the selector on the model
|
|
105
|
+
For collections, keep the selector on the model client too:
|
|
56
106
|
|
|
57
107
|
```tsx
|
|
58
|
-
const
|
|
59
|
-
ablo.
|
|
108
|
+
const reports = useAblo((ablo) =>
|
|
109
|
+
ablo.weatherReports.list({
|
|
60
110
|
where: { projectId },
|
|
61
|
-
filter: (
|
|
62
|
-
|
|
111
|
+
filter: (report) => report.status !== 'ready',
|
|
112
|
+
state: 'live',
|
|
63
113
|
}),
|
|
64
114
|
);
|
|
65
115
|
```
|
|
@@ -67,7 +117,7 @@ const tasks = useAblo((ablo) =>
|
|
|
67
117
|
## Server Load
|
|
68
118
|
|
|
69
119
|
```tsx
|
|
70
|
-
const [
|
|
120
|
+
const [report] = await ablo.weatherReports.load({ where: { id } });
|
|
71
121
|
```
|
|
72
122
|
|
|
73
123
|
Use `load` in Server Components when the row may not be in the local pool yet.
|
|
@@ -79,8 +129,8 @@ For Server Actions and route handlers, call the SDK directly:
|
|
|
79
129
|
```ts
|
|
80
130
|
import { ablo } from '@/lib/ablo';
|
|
81
131
|
|
|
82
|
-
const snap = ablo.snapshot({
|
|
83
|
-
await ablo.
|
|
132
|
+
const snap = ablo.snapshot({ weatherReports: id });
|
|
133
|
+
await ablo.weatherReports.update(id, patch, {
|
|
84
134
|
readAt: snap.stamp,
|
|
85
135
|
onStale: 'reject',
|
|
86
136
|
wait: 'confirmed',
|
|
@@ -88,17 +138,17 @@ await ablo.tasks.update(id, patch, {
|
|
|
88
138
|
```
|
|
89
139
|
|
|
90
140
|
For client event handlers, get the provider-owned client and call the same
|
|
91
|
-
model
|
|
141
|
+
model client:
|
|
92
142
|
|
|
93
143
|
```tsx
|
|
94
144
|
const ablo = useAblo();
|
|
95
145
|
|
|
96
|
-
async function
|
|
146
|
+
async function markReady() {
|
|
97
147
|
if (!ablo) return;
|
|
98
|
-
const snap = ablo.snapshot({
|
|
99
|
-
await ablo.
|
|
148
|
+
const snap = ablo.snapshot({ weatherReports: id });
|
|
149
|
+
await ablo.weatherReports.update(
|
|
100
150
|
id,
|
|
101
|
-
{ status: '
|
|
151
|
+
{ status: 'ready' },
|
|
102
152
|
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
|
|
103
153
|
);
|
|
104
154
|
}
|
package/docs/roadmap.md
CHANGED
|
@@ -4,22 +4,32 @@ What is shipped, what is next, and what we will not build.
|
|
|
4
4
|
|
|
5
5
|
## Shipped
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **Capability tokens** — Biscuit-signed, scoped, attenuable, hot-revocable.
|
|
7
|
+
- **Models, claims, commits** — the core API.
|
|
9
8
|
- **Audit log** — hash-chained per principal, with `delegationChainRoot`.
|
|
10
9
|
- **MCP transport** — HTTP server at `/api/mcp`.
|
|
11
10
|
- **TypeScript SDK** — `@abloatai/ablo`, with React bindings.
|
|
12
11
|
- **Dashboard** — keys, audit, metrics, allowed origins.
|
|
12
|
+
- **Schema migrations** — declarative model schema changes. Pure
|
|
13
|
+
diff/classify/cast-safety/backfill planning engine (`diffSchema`,
|
|
14
|
+
`classifyMigration`, `classifyCast`, `isAutoApplicable`) ships in the SDK;
|
|
15
|
+
`ablo generate` emits typed clients from a pushed schema; the server
|
|
16
|
+
applies and activates migrations only on success.
|
|
17
|
+
- **Structured error contract** — versioned (`ERROR_CONTRACT_VERSION`),
|
|
18
|
+
drift-guarded code registry shared across the HTTP, WebSocket, and MCP
|
|
19
|
+
planes, with always-on request ids and an OpenAPI error envelope.
|
|
20
|
+
- **Relation-driven sync groups** — membership-routed fan-out via branded
|
|
21
|
+
`SyncGroup` + schema-declared identity roles (schema-JSON v2).
|
|
22
|
+
- **Intent coordination plane** — Stripe-shaped `Intent` with per-model
|
|
23
|
+
`intent(id)` handles for lease/await/commit coordination between agents.
|
|
13
24
|
|
|
14
25
|
## In flight
|
|
15
26
|
|
|
16
|
-
- **Real-time presence** — see who else is viewing/editing a
|
|
27
|
+
- **Real-time presence** — see who else is viewing/editing a model
|
|
28
|
+
(coordination primitives landed; presence surface in progress).
|
|
17
29
|
- **Cross-instance fan-out via Redis** — pub/sub deltas at scale.
|
|
18
|
-
- **Hot capability revocation UI** — in the dashboard, today via API only.
|
|
19
30
|
|
|
20
31
|
## On deck
|
|
21
32
|
|
|
22
|
-
- **Schema migrations** — declarative resource schema changes.
|
|
23
33
|
- **Field-level subscriptions** — subscribe to one path, not the whole row.
|
|
24
34
|
- **Bulk import/export** — CSV/JSON round-trip with chain verification.
|
|
25
35
|
|
|
@@ -31,11 +41,11 @@ What is shipped, what is next, and what we will not build.
|
|
|
31
41
|
|
|
32
42
|
## We will not build
|
|
33
43
|
|
|
34
|
-
- **A general-purpose Postgres wrapper** — Ablo
|
|
44
|
+
- **A general-purpose Postgres wrapper** — Ablo is for state with
|
|
35
45
|
concurrency semantics, not for storing every table.
|
|
36
46
|
- **Server-side compute** — no triggers, no stored procedures. Compute
|
|
37
47
|
belongs in your application code.
|
|
38
|
-
- **A document database UI** — your data lives in Ablo
|
|
48
|
+
- **A document database UI** — your data lives in Ablo; the UI is your
|
|
39
49
|
product, not ours.
|
|
40
50
|
|
|
41
51
|
## How priorities shift
|