@abloatai/ablo 0.11.0 → 0.11.2
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 +58 -0
- package/README.md +72 -25
- package/dist/Model.d.ts +39 -0
- package/dist/Model.js +68 -0
- package/dist/auth/credentialPolicy.d.ts +145 -0
- package/dist/auth/credentialPolicy.js +130 -0
- package/dist/cli.cjs +154 -25
- package/dist/client/Ablo.d.ts +39 -88
- package/dist/client/Ablo.js +54 -99
- package/dist/client/ApiClient.d.ts +10 -1
- package/dist/client/ApiClient.js +23 -12
- package/dist/client/auth.d.ts +21 -9
- package/dist/client/auth.js +42 -6
- package/dist/client/createModelProxy.d.ts +74 -10
- package/dist/client/createModelProxy.js +85 -4
- package/dist/client/httpClient.d.ts +17 -3
- package/dist/client/httpClient.js +1 -0
- package/dist/client/identity.js +134 -122
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sessionMint.d.ts +15 -0
- package/dist/client/sessionMint.js +86 -0
- package/dist/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +3 -1
- package/dist/errors.d.ts +3 -2
- package/dist/errors.js +3 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -7
- package/dist/mutators/RecordingTransaction.js +14 -42
- package/dist/react/AbloProvider.d.ts +1 -6
- package/dist/react/AbloProvider.js +1 -5
- package/dist/react/context.d.ts +1 -31
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +0 -6
- package/dist/react/index.js +0 -7
- package/dist/react/useSyncStatus.d.ts +1 -1
- package/dist/realtime/index.d.ts +1 -1
- package/dist/schema/generate.js +1 -2
- package/dist/schema/schema.d.ts +16 -5
- package/dist/schema/schema.js +26 -0
- package/dist/surface.d.ts +29 -0
- package/dist/surface.js +60 -0
- package/dist/sync/ConnectionManager.d.ts +16 -5
- package/dist/sync/ConnectionManager.js +42 -7
- package/dist/transactions/TransactionQueue.js +22 -10
- package/dist/types/global.d.ts +11 -3
- package/dist/types/global.js +8 -3
- package/dist/types/streams.d.ts +0 -22
- package/dist/utils/mobx-setup.js +1 -0
- package/docs/api-keys.md +49 -0
- package/docs/api.md +6 -5
- package/docs/client-behavior.md +7 -3
- package/docs/coordination.md +88 -24
- package/docs/data-sources.md +29 -9
- package/docs/examples/existing-python-backend.md +9 -5
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/guarantees.md +4 -3
- package/docs/identity.md +89 -82
- package/docs/integration-guide.md +19 -10
- package/docs/migration.md +49 -2
- package/docs/quickstart.md +65 -33
- package/docs/react.md +49 -3
- package/docs/schema-contract.md +23 -5
- package/llms-full.txt +43 -24
- package/llms.txt +17 -15
- package/package.json +1 -1
- package/dist/api/index.d.ts +0 -10
- package/dist/api/index.js +0 -9
- package/dist/principal.d.ts +0 -44
- package/dist/principal.js +0 -49
- package/dist/react/SyncGroupProvider.d.ts +0 -19
- package/dist/react/SyncGroupProvider.js +0 -44
- package/dist/react/useClaim.d.ts +0 -29
- package/dist/react/useClaim.js +0 -42
- package/dist/react/usePresence.d.ts +0 -32
- package/dist/react/usePresence.js +0 -41
|
@@ -81,7 +81,7 @@ import { schema } from './schema';
|
|
|
81
81
|
|
|
82
82
|
const ablo = Ablo({
|
|
83
83
|
schema,
|
|
84
|
-
|
|
84
|
+
apiKey: async () => mintDeckAgentSession(deckId, agentId),
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
// The agent run is mounted on behalf of its triggering user.
|
package/docs/guarantees.md
CHANGED
|
@@ -82,9 +82,10 @@ Claims are live coordination signals. They are not database locks.
|
|
|
82
82
|
already holds the row, the claim waits for them to finish, then re-reads the row
|
|
83
83
|
before handing it back, so you proceed from fresh state. Reads stay open while a
|
|
84
84
|
claim is held — `ablo.<model>.claim.state({ id })` returns the current claim state
|
|
85
|
-
(or `null`) without ever blocking. A server read can pass `ifClaimed: '
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
(or `null`) without ever blocking. A server read can pass `ifClaimed: 'fail'` to
|
|
86
|
+
error out, when it should not return a row while someone else is mid-edit. Reads
|
|
87
|
+
never block on a claim — to wait for a row to free up, `claim({ id })` it (the
|
|
88
|
+
claim queues fairly behind the holder).
|
|
88
89
|
|
|
89
90
|
A claim does not reject or block other writers; it announces work so peers
|
|
90
91
|
serialize behind it rather than racing. While you hold a claim, the matching
|
package/docs/identity.md
CHANGED
|
@@ -36,8 +36,8 @@ runnable place, so the concepts below have code to attach to.
|
|
|
36
36
|
## Declare it, end to end
|
|
37
37
|
|
|
38
38
|
The entire declaration surface is: `identityRoles` (who may see what), and on
|
|
39
|
-
each model `scope` / `parent` / `grants` (which group a row fans out on), plus
|
|
40
|
-
optional `
|
|
39
|
+
each model `scope` / `parent` / `grants` (which group a row fans out on), plus
|
|
40
|
+
optional `syncGroups` at session-mint time (narrowing). Read the three blocks first —
|
|
41
41
|
a human gets their `org` / `team` scope, an agent gets one `deck` — then the
|
|
42
42
|
sections after explain each.
|
|
43
43
|
|
|
@@ -84,18 +84,17 @@ export const schema = defineSchema(
|
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
86
|
// 3. an AGENT run inherits its user, narrowed to the entities in play.
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
// You narrow at SESSION-MINT time: your backend calls `sessions.create` with the
|
|
88
|
+
// agent's allowed `syncGroups`, built from each model's scope via the
|
|
89
|
+
// `syncGroup(kind, id)` helper — never a hand-built `deck:<id>` string. The agent's
|
|
90
|
+
// runtime then connects with the minted token.
|
|
91
|
+
const session = await server.sessions.create({
|
|
92
|
+
agent: { id: agentId },
|
|
93
|
+
can: { Deck: ['read', 'update'] },
|
|
94
|
+
syncGroups: [syncGroup('deck', deckId)], // floor: just the deck it's working on
|
|
94
95
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{children}
|
|
98
|
-
</AbloProvider>;
|
|
96
|
+
// the agent runtime authenticates with the minted token
|
|
97
|
+
const ablo = Ablo({ schema, apiKey: session.token });
|
|
99
98
|
```
|
|
100
99
|
|
|
101
100
|
That's the whole surface. The rest of this doc is the *why* behind each line.
|
|
@@ -127,11 +126,12 @@ so you pass them in code when you start the run.
|
|
|
127
126
|
> **One line:** humans subscribe by who they are; agents subscribe by what
|
|
128
127
|
> they've been given.
|
|
129
128
|
|
|
130
|
-
That's why you never write per-user scope code, but you always
|
|
131
|
-
|
|
129
|
+
That's why you never write per-user scope code, but you always choose an agent's
|
|
130
|
+
groups at the dispatch site. A user's org/team/user don't change per request, so
|
|
132
131
|
their scope is a **rule the schema derives automatically**. An agent's reach
|
|
133
132
|
depends on *what it's working on*, which is only knowable at dispatch — so you
|
|
134
|
-
|
|
133
|
+
pass its `syncGroups` **when your backend mints the agent session**
|
|
134
|
+
(`sessions.create({ agent, can, syncGroups })`). The schema's
|
|
135
135
|
only job for entities is to declare *that* a model is
|
|
136
136
|
entity-scopable and *what its group is named* (`scope: 'deck'` → `deck:{id}`);
|
|
137
137
|
it never declares *which* entities a given agent gets. (A human can opt into the
|
|
@@ -305,8 +305,9 @@ server, never by the browser.**
|
|
|
305
305
|
|
|
306
306
|
The identity your server resolved is carried by the client you build and the
|
|
307
307
|
`userId` prop. In a Next.js app, resolve the user in a Server Component and pass
|
|
308
|
-
it down. Build the client once (the schema, `teamIds`, and
|
|
309
|
-
live here), then hand
|
|
308
|
+
it down. Build the client once (the schema, `teamIds`, and the `apiKey` resolver
|
|
309
|
+
live here; entity narrowing rides the minted session's `syncGroups`), then hand
|
|
310
|
+
it to the provider:
|
|
310
311
|
|
|
311
312
|
```ts
|
|
312
313
|
// lib/ablo.ts
|
|
@@ -318,7 +319,10 @@ import { schema } from '@/ablo/schema';
|
|
|
318
319
|
export function makeAblo(user: { teamIds: string[] }) {
|
|
319
320
|
return Ablo({
|
|
320
321
|
schema,
|
|
321
|
-
|
|
322
|
+
// The browser holds no secret — the `apiKey` resolver fetches the
|
|
323
|
+
// short-lived session token your `/api/ablo-session` route minted, and the
|
|
324
|
+
// client keeps it fresh before expiry.
|
|
325
|
+
apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
|
|
322
326
|
teamIds: user.teamIds,
|
|
323
327
|
});
|
|
324
328
|
}
|
|
@@ -354,7 +358,7 @@ What carries identity — and just as importantly, what does *not* set the bound
|
|
|
354
358
|
| ------------ | ------------------------------------------------------------------------------------------------ |
|
|
355
359
|
| `userId` prop | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary — the server enforces scope from the authenticated request. |
|
|
356
360
|
| `teamIds` (on the client) | Team ids expanded into team sync groups via your `identityRoles`. |
|
|
357
|
-
| `
|
|
361
|
+
| `syncGroups` (at session mint) | Optional. **Narrows** a minted session's subscription to a subset of what auth already allows — it can never widen it. Passed to `sessions.create({ user \| agent, syncGroups })`; build entries with `syncGroup(kind, id)`. Use it to scope an agent (or a focused page's session) to one entity, e.g. `[syncGroup('deck', 'abc123')]`. |
|
|
358
362
|
|
|
359
363
|
Because the server is the boundary, a client that changes `userId` to another
|
|
360
364
|
user's id does not gain their data — the server resolves and enforces the real
|
|
@@ -398,20 +402,20 @@ subset of what its user could see:
|
|
|
398
402
|
|
|
399
403
|
```ts
|
|
400
404
|
// agent run triggered by `user`, working on one document + one deck.
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
scope: { documents: documentId, decks: deckId },
|
|
405
|
+
// Your backend mints the agent session narrowed to just the entities in play
|
|
406
|
+
// (the floor). Build each group from the model's scope with `syncGroup(kind, id)`.
|
|
407
|
+
const session = await server.sessions.create({
|
|
408
|
+
agent: { id: agentId },
|
|
409
|
+
can: { Document: ['read', 'update'], Deck: ['read', 'update'] },
|
|
410
|
+
syncGroups: [syncGroup('document', documentId), syncGroup('deck', deckId)],
|
|
408
411
|
});
|
|
409
|
-
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
+
// identity (the ceiling) is inherited from the triggering user via your
|
|
413
|
+
// session-mint logic; the agent runtime connects with the minted token.
|
|
414
|
+
const ablo = Ablo({ schema, apiKey: session.token });
|
|
412
415
|
```
|
|
413
416
|
|
|
414
|
-
As the run touches more entities
|
|
417
|
+
As the run touches more entities, claim or read them and the client auto-enrolls
|
|
418
|
+
in their entity groups — its set **accretes** to cover them; it never
|
|
415
419
|
widens past the user's ceiling, and it carries no standing access to entities it
|
|
416
420
|
isn't working on. The `identityRoles` need no agent-specific entry: the agent
|
|
417
421
|
carries the triggering user's `userId`, so the same `user:{id}` role that scopes
|
|
@@ -444,52 +448,55 @@ runs the [Coordinating long agent work](../README.md#coordinating-long-agent-wor
|
|
|
444
448
|
`claim` loop is, to the scoping layer, that same participant — scoped to the row
|
|
445
449
|
it claimed.
|
|
446
450
|
|
|
447
|
-
## Narrowing to specific entities
|
|
448
|
-
|
|
449
|
-
A human gets their full membership automatically (`identityRoles`).
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
`
|
|
454
|
-
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const ablo = Ablo({
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
>
|
|
484
|
-
> (
|
|
485
|
-
>
|
|
486
|
-
>
|
|
487
|
-
|
|
488
|
-
>
|
|
489
|
-
>
|
|
490
|
-
|
|
491
|
-
>
|
|
492
|
-
>
|
|
451
|
+
## Narrowing to specific entities
|
|
452
|
+
|
|
453
|
+
A human gets their full membership automatically (`identityRoles`). There are
|
|
454
|
+
three ways to narrow a participant to specific entities — a page on one deck, or
|
|
455
|
+
an agent pointed at the entities it's working on. You **never hand-write**
|
|
456
|
+
`deck:<id>`; build groups from the model's `scope` (Half 2) with the typed
|
|
457
|
+
`syncGroup(kind, id)` helper from `@abloatai/ablo/schema`.
|
|
458
|
+
|
|
459
|
+
1. **At session mint — `syncGroups`.** When your backend mints a session, pass the
|
|
460
|
+
exact groups it may subscribe to. This is the floor for a delegated agent (and
|
|
461
|
+
the way to scope a focused page's session):
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
// an agent working across two decks and a document
|
|
465
|
+
const session = await server.sessions.create({
|
|
466
|
+
agent: { id: agentId },
|
|
467
|
+
can: { Deck: ['read', 'update'], Document: ['read'] },
|
|
468
|
+
syncGroups: [
|
|
469
|
+
syncGroup('deck', deckA),
|
|
470
|
+
syncGroup('deck', deckB),
|
|
471
|
+
syncGroup('document', docId),
|
|
472
|
+
],
|
|
473
|
+
});
|
|
474
|
+
const ablo = Ablo({ schema, apiKey: session.token });
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
2. **Automatically, on read or claim.** Reading a row (`retrieve`/`get`/
|
|
478
|
+
`claim.state`) auto-enrolls the client in that row's entity group
|
|
479
|
+
(**read-interest**), and `claim`-ing it pins a **write-intent** subscription.
|
|
480
|
+
So an agent's reachable set **accretes** as it works — no extra subscribe call.
|
|
481
|
+
|
|
482
|
+
3. **Explicitly, for presence — `watch`.** To hold presence on a known set of rows
|
|
483
|
+
and react to peers, use the WebSocket-only `ablo.<model>.watch(ids, { ttl })`
|
|
484
|
+
(it returns a participant handle with `.peers`). See
|
|
485
|
+
[Coordination](./coordination.md).
|
|
486
|
+
|
|
487
|
+
> **`scope` is the schema model option, not a client setting.** `scope: 'deck'`
|
|
488
|
+
> in `model(...)` declares a scope root ([Half 2](#half-2--per-model-scope-row--group)) —
|
|
489
|
+
> it names the group (`deck:<id>`) that the mechanisms above then subscribe to.
|
|
490
|
+
> There is no `Ablo({ scope })` constructor option. The lifecycle filter on
|
|
491
|
+
> [`list()`](./api.md#model-methods) is a separate axis named **`state`**
|
|
492
|
+
> (`'live' | 'archived' | 'all'`, GitHub's open/closed/all), precisely so it
|
|
493
|
+
> doesn't share the word.
|
|
494
|
+
|
|
495
|
+
> **Requested groups never grant.** At connect, the server intersects the session's
|
|
496
|
+
> `syncGroups` with what the identity is actually allowed (`requested ∩ allowed`).
|
|
497
|
+
> So `syncGroups` only ever *narrows* within a participant's ceiling — an agent
|
|
498
|
+
> can't reach a deck its capability doesn't already permit, no matter what it
|
|
499
|
+
> passes. Smaller bootstrap, less fan-out, same server-enforced boundary.
|
|
493
500
|
|
|
494
501
|
## How this compares — and the best practices it follows
|
|
495
502
|
|
|
@@ -512,7 +519,7 @@ how to reason about it.
|
|
|
512
519
|
the room/shape and the server signs off, as in
|
|
513
520
|
[Pusher's channel authorization endpoint](https://pusher.com/docs/channels/server_api/authorizing-users/),
|
|
514
521
|
[ElectricSQL **gatekeeper auth**](https://github.com/electric-sql/electric/blob/main/examples/gatekeeper-auth/README.md),
|
|
515
|
-
and Liveblocks **access tokens**. Ablo's
|
|
522
|
+
and Liveblocks **access tokens**. Ablo's session-mint `syncGroups` is the
|
|
516
523
|
*narrowing* half of this — but it can only ever shrink the server-derived set,
|
|
517
524
|
never grow it.
|
|
518
525
|
|
|
@@ -529,10 +536,10 @@ The best practices Ablo inherits from that lineage:
|
|
|
529
536
|
2. **Trusted vs untrusted claims is the whole security argument.** PowerSync draws
|
|
530
537
|
the line precisely: [token parameters are trusted and usable for access
|
|
531
538
|
control; client parameters are not](https://docs.powersync.com/usage/sync-rules/advanced-topics/client-parameters).
|
|
532
|
-
In Ablo terms, the identity your server vouches for
|
|
533
|
-
|
|
534
|
-
client input* — convenient for app-owned fields
|
|
535
|
-
boundary. This is why changing `userId` in the browser grants nothing.
|
|
539
|
+
In Ablo terms, the identity your server vouches for — and the session's
|
|
540
|
+
`syncGroups`, minted server-side — are the *trusted* claims that set scope; the
|
|
541
|
+
`userId` prop is *untrusted client input* — convenient for app-owned fields, but
|
|
542
|
+
never the boundary. This is why changing `userId` in the browser grants nothing.
|
|
536
543
|
|
|
537
544
|
3. **Scope by a hierarchical naming convention, declared once.** Ablo's `kind:id`
|
|
538
545
|
group naming (`org:…` / `team:…` from `identityRoles`, `deck:…` from a model's
|
|
@@ -44,13 +44,18 @@ objects by hand.
|
|
|
44
44
|
|
|
45
45
|
## Your Database
|
|
46
46
|
|
|
47
|
-
Every schema model is backed by **your own database**.
|
|
48
|
-
|
|
49
|
-
Postgres. The SDK call shape is the same everywhere.
|
|
47
|
+
Every schema model is backed by **your own database**. The SDK call shape is the
|
|
48
|
+
same everywhere.
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
|
|
50
|
+
In this guide — an app that already owns its backend and database — keep
|
|
51
|
+
`DATABASE_URL` inside your app and expose a signed Data Source endpoint: Ablo
|
|
52
|
+
coordinates each write and your app commits it to your Postgres. Do not pass
|
|
53
|
+
`databaseUrl` to `Ablo(...)` here; application and agent code use `ABLO_API_KEY`.
|
|
54
|
+
|
|
55
|
+
If instead you want Ablo to connect to your Postgres directly, pass `databaseUrl`
|
|
56
|
+
(a live, server-only option) to `Ablo(...)`. That and the sandbox-only `apiKey`
|
|
57
|
+
shape are the other two start states — [Connect Your Database](./data-sources.md)
|
|
58
|
+
is the single source of truth for all three.
|
|
54
59
|
|
|
55
60
|
## Test With Sandboxes
|
|
56
61
|
|
|
@@ -94,12 +99,13 @@ import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
|
94
99
|
export const schema = defineSchema(
|
|
95
100
|
{
|
|
96
101
|
weatherReports: model({
|
|
97
|
-
id
|
|
102
|
+
// Reserved fields (id, createdAt, updatedAt, organizationId, createdBy)
|
|
103
|
+
// are SDK-provided automatically — never declare them. Declare only your
|
|
104
|
+
// own fields.
|
|
98
105
|
projectId: z.string(),
|
|
99
106
|
location: z.string(),
|
|
100
107
|
status: z.enum(['pending', 'ready']),
|
|
101
108
|
assigneeId: z.string().nullable(),
|
|
102
|
-
updatedAt: z.string(),
|
|
103
109
|
}),
|
|
104
110
|
},
|
|
105
111
|
{
|
|
@@ -169,7 +175,7 @@ export const ablo = Ablo({
|
|
|
169
175
|
Browser apps should use the React provider or a scoped session token, not a
|
|
170
176
|
server API key in the bundle. Build the client first, then hand it to the
|
|
171
177
|
provider — `AbloProvider` takes `{ client, userId?, onError?, fallback? }`, and
|
|
172
|
-
nothing else (`schema`, `teamIds`,
|
|
178
|
+
nothing else (`schema`, `teamIds`, and `apiKey` all live on the
|
|
173
179
|
client now).
|
|
174
180
|
|
|
175
181
|
```tsx
|
|
@@ -179,7 +185,10 @@ import { schema } from '@/ablo/schema';
|
|
|
179
185
|
|
|
180
186
|
// The browser never holds the API key. The client mints a short-lived token
|
|
181
187
|
// from your session route (see below) and refreshes it before expiry.
|
|
182
|
-
export const ablo = Ablo({
|
|
188
|
+
export const ablo = Ablo({
|
|
189
|
+
schema,
|
|
190
|
+
apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
|
|
191
|
+
});
|
|
183
192
|
```
|
|
184
193
|
|
|
185
194
|
```tsx
|
package/docs/migration.md
CHANGED
|
@@ -11,6 +11,7 @@ change when you upgrade.
|
|
|
11
11
|
|
|
12
12
|
| Version | What changed | What to do |
|
|
13
13
|
|---|---|---|
|
|
14
|
+
| **0.11.0** | `intent` → `claim` rename completed across the React hook, type namespace, and wire frames | `useIntent` → `useClaim`; `Register.Intents` → `Register.Claims`; `Ablo.Intent.*` → `Ablo.Claim.*`. Upgrade client **and** server together (wire frames moved `intent_*` → `claim_*`) |
|
|
14
15
|
| **0.10.0** | Environment enum renamed `test`/`live` → `sandbox`/`production` | Update code that branches on the environment (e.g. source `mode`): `'test'`→`'sandbox'`, `'live'`→`'production'`. Key prefixes `sk_test_`/`sk_live_` are unchanged |
|
|
15
16
|
| **0.9.2** | `turn` primitive + agent-work `tasks` resource removed | Coordinate with `claim`; mint a scoped session instead of `agent().run()` |
|
|
16
17
|
| **0.9.2** | `intents` deprecated in favor of `claim` | Use `ablo.<model>.claim`; `ablo.intents` is now `@internal` |
|
|
@@ -24,6 +25,45 @@ change when you upgrade.
|
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
28
|
+
## 0.11.0 — `intent` → `claim` rename completed
|
|
29
|
+
|
|
30
|
+
The coordination primitive has been `claim` since 0.9.2, but a few `intent`-named
|
|
31
|
+
surfaces lingered. 0.11.0 finishes the rename. There are three edits, all
|
|
32
|
+
mechanical:
|
|
33
|
+
|
|
34
|
+
**1. React hook.** `useIntent` is now `useClaim` (same signature):
|
|
35
|
+
|
|
36
|
+
```diff
|
|
37
|
+
- import { useIntent } from '@abloatai/ablo/react';
|
|
38
|
+
- const claimEditLayer = useIntent('editLayer');
|
|
39
|
+
+ import { useClaim } from '@abloatai/ablo/react';
|
|
40
|
+
+ const claimEditLayer = useClaim('editLayer');
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Type registration.** The `Register` interface key is `Claims`, not `Intents`:
|
|
44
|
+
|
|
45
|
+
```diff
|
|
46
|
+
declare module '@abloatai/ablo' {
|
|
47
|
+
interface Register {
|
|
48
|
+
- Intents: { editLayer: { slideId: string; layerId: string } };
|
|
49
|
+
+ Claims: { editLayer: { slideId: string; layerId: string } };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**3. Type namespace.** The `Ablo.Intent.*` helper types moved to `Ablo.Claim.*`.
|
|
55
|
+
If you referenced them directly, rename the namespace; the shapes are unchanged.
|
|
56
|
+
|
|
57
|
+
> **Coordinated deploy required.** The on-the-wire frames moved from `intent_*`
|
|
58
|
+
> to `claim_*`. A `claim_*`-aware client cannot coordinate with an `intent_*`
|
|
59
|
+
> server (and vice-versa), so ship the client and server together. If you run a
|
|
60
|
+
> self-managed sync server, deploy it first.
|
|
61
|
+
|
|
62
|
+
Two non-breaking improvements ride along: claim-rejection errors now surface the
|
|
63
|
+
contending holders (`AbloClaimedError.claims` and a policy reason folded into the
|
|
64
|
+
message), and `participantKind` is the canonical `'user' | 'agent' | 'system'`
|
|
65
|
+
on presence and claim state.
|
|
66
|
+
|
|
27
67
|
## 0.10.0 — environment enum `sandbox` / `production`; stateless HTTP transport
|
|
28
68
|
|
|
29
69
|
### Environment enum rename (the only breaking change)
|
|
@@ -73,6 +113,11 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: 'http'
|
|
|
73
113
|
await ablo.tasks.update({ id, data: { status: 'done' } });
|
|
74
114
|
```
|
|
75
115
|
|
|
116
|
+
> **Minting still needs the stateful client.** `sessions.create(...)` is not on
|
|
117
|
+
> the `transport: 'http'` surface. Keep a default-transport `server` client for
|
|
118
|
+
> minting short-lived credentials (see the 0.9.2 example below), and use the
|
|
119
|
+
> http client for the per-request reads and writes.
|
|
120
|
+
|
|
76
121
|
---
|
|
77
122
|
|
|
78
123
|
## 0.9.2 — `turn` / agent-`tasks` removed; `intents` deprecated
|
|
@@ -95,8 +140,10 @@ helper, and the agent/task type family (`Agent`, `AgentOptions`,
|
|
|
95
140
|
```diff
|
|
96
141
|
- const turn = await engine.beginTurn();
|
|
97
142
|
- await Ablo({ apiKey }).agent(agentId, opts).run(prompt, handler);
|
|
98
|
-
+ // Mint a scoped credential
|
|
99
|
-
+
|
|
143
|
+
+ // Mint a scoped credential from a stateful (default-transport) server client —
|
|
144
|
+
+ // sessions.create lives on the stateful client, not on transport: 'http'.
|
|
145
|
+
+ const server = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
146
|
+
+ const { token } = await server.sessions.create({ agent: { id: agentId } });
|
|
100
147
|
+ const agent = Ablo({ schema, apiKey: token });
|
|
101
148
|
+ await using claim = await agent.tasks.claim({ id });
|
|
102
149
|
+ await agent.tasks.update({ id, data: { status: 'done' }, wait: 'confirmed' });
|
package/docs/quickstart.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# Quickstart
|
|
2
2
|
|
|
3
|
-
Build with Ablo on **
|
|
4
|
-
models humans and agents edit together, hand the client your
|
|
5
|
-
`DATABASE_URL
|
|
6
|
-
is the system of record
|
|
7
|
-
layer on top: it registers your connection, commits every write there
|
|
8
|
-
row-level security, and fans the confirmed rows out to every connected
|
|
3
|
+
Build with Ablo on **the Postgres you already have**. You declare a small Ablo
|
|
4
|
+
schema for the models humans and agents edit together, hand the client your
|
|
5
|
+
Postgres `DATABASE_URL` (passed explicitly), and coordinate every write through
|
|
6
|
+
`ablo.<model>`. In production, your database is the system of record. Ablo is the
|
|
7
|
+
transaction layer on top: it registers your connection, commits every write there
|
|
8
|
+
behind row-level security, and fans the confirmed rows out to every connected
|
|
9
|
+
client.
|
|
10
|
+
|
|
11
|
+
> No database yet? The hosted **sandbox** can host rows in Ablo's test plane —
|
|
12
|
+
> pass an `apiKey` only and omit `databaseUrl`, like Stripe test mode — so you can
|
|
13
|
+
> try Ablo before pointing it at your Postgres.
|
|
9
14
|
|
|
10
15
|
## 1. Install and initialize
|
|
11
16
|
|
|
@@ -25,10 +30,12 @@ instead:
|
|
|
25
30
|
export ABLO_API_KEY=sk_test_...
|
|
26
31
|
```
|
|
27
32
|
|
|
28
|
-
Every SDK and CLI call needs a key. Test and live keys work like Stripe's
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
Every SDK and CLI call needs a key. Test and live keys work like Stripe's:
|
|
34
|
+
`sk_test_*` for the sandbox, `sk_live_*` for production. In production a key
|
|
35
|
+
points at the database *you* own; in the sandbox you can skip the database
|
|
36
|
+
entirely and let Ablo's test plane host the rows (apiKey only). There is no
|
|
37
|
+
keyless mode — a key is always required. (The public `/sandbox` page is a
|
|
38
|
+
separate hosted demo, not your app.)
|
|
32
39
|
|
|
33
40
|
## 2. Your Ablo schema (init scaffolded it)
|
|
34
41
|
|
|
@@ -50,19 +57,28 @@ export const schema = defineSchema({
|
|
|
50
57
|
});
|
|
51
58
|
```
|
|
52
59
|
|
|
60
|
+
**Reserved fields** — `id`, `createdAt`, `updatedAt`, `organizationId`, and
|
|
61
|
+
`createdBy` are provided by the SDK automatically. Don't declare them in your
|
|
62
|
+
`model(...)` fields; declare only your own.
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
64
|
+
The schema is registered once (init scaffolds `ablo/register.ts` for you), and
|
|
65
|
+
every type is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
56
66
|
|
|
57
67
|
```ts
|
|
58
|
-
// ablo.
|
|
59
|
-
import type { schema } from './
|
|
68
|
+
// ablo/register.ts — scaffolded by `npx ablo init`, sits beside ablo/schema.ts
|
|
69
|
+
import type { schema } from './schema';
|
|
60
70
|
declare module '@abloatai/ablo' {
|
|
61
71
|
interface Register { Schema: typeof schema }
|
|
62
72
|
}
|
|
63
73
|
export {};
|
|
64
74
|
```
|
|
65
75
|
|
|
76
|
+
It's a regular `.ts` module, not a hand-authored `.d.ts`. The top-level
|
|
77
|
+
`import type { schema }` makes the `declare module` block *merge* into (augment)
|
|
78
|
+
the SDK's `Register` interface instead of colliding with it — the same shape
|
|
79
|
+
[TanStack Router uses in `src/router.tsx`](https://tanstack.com/router/latest/docs/framework/react/guide/type-safety). Any `.ts` file in your
|
|
80
|
+
`tsconfig` `include` works; it never needs to be imported.
|
|
81
|
+
|
|
66
82
|
```ts
|
|
67
83
|
import type { Model } from '@abloatai/ablo/schema';
|
|
68
84
|
|
|
@@ -73,6 +89,12 @@ type WeatherReport = Model<'weatherReports'>; // fully typed from YOUR schema
|
|
|
73
89
|
TanStack-Router pattern: declare the source of truth once, everything
|
|
74
90
|
infers from it.)
|
|
75
91
|
|
|
92
|
+
When you need to name the client type — to pass it to a function or store it in
|
|
93
|
+
a context — **infer it from the value**: `type Sync = typeof sync`. That's the
|
|
94
|
+
same idiom as tRPC's `typeof appRouter` and Drizzle's `typeof db`; it resolves
|
|
95
|
+
the typed overload at the call site. Avoid `ReturnType<typeof Ablo>`, which
|
|
96
|
+
collapses to the untyped client.
|
|
97
|
+
|
|
76
98
|
## 3. Point Ablo at your database
|
|
77
99
|
|
|
78
100
|
The client takes your schema, your key, and your `DATABASE_URL`. On first
|
|
@@ -93,10 +115,14 @@ import { schema } from './schema';
|
|
|
93
115
|
export const ablo = Ablo({
|
|
94
116
|
schema,
|
|
95
117
|
apiKey: process.env.ABLO_API_KEY,
|
|
96
|
-
databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here
|
|
118
|
+
databaseUrl: process.env.DATABASE_URL, // your Postgres, passed explicitly — rows live here
|
|
97
119
|
});
|
|
98
120
|
```
|
|
99
121
|
|
|
122
|
+
`databaseUrl` is not auto-read from the environment — you pass it explicitly
|
|
123
|
+
(as above). If a `DATABASE_URL` is set for another tool, `Ablo()` ignores it
|
|
124
|
+
unless you wire it in like this.
|
|
125
|
+
|
|
100
126
|
Use a dedicated **non-superuser role** for the connection — Ablo enforces
|
|
101
127
|
tenant isolation with row-level security, so the server rejects superuser or
|
|
102
128
|
`BYPASSRLS` roles outright (`database_role_cannot_enforce_rls`).
|
|
@@ -127,29 +153,34 @@ built from an ORM adapter instead — same product, same writes, see
|
|
|
127
153
|
[Connect Your Database](./data-sources.md). In that setup, omit `databaseUrl`
|
|
128
154
|
from `Ablo(...)`.
|
|
129
155
|
|
|
130
|
-
## 4. Push
|
|
156
|
+
## 4. Push the schema, then map it to tables
|
|
131
157
|
|
|
132
158
|
```bash
|
|
133
159
|
npx ablo push # checks your DATABASE_URL role, pushes the schema (sandbox),
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
# .env.local. Add --watch to re-push on every save.
|
|
160
|
+
# and writes ABLO_API_KEY to .env.local. Add --watch to
|
|
161
|
+
# re-push on every save.
|
|
137
162
|
```
|
|
138
163
|
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
`ablo push` uploads the schema *definition* — model names, fields, types. That
|
|
165
|
+
metadata is what tells Ablo which models to coordinate. Skipping it makes every
|
|
166
|
+
write to a new or changed model fail with `server_execute_unknown_model` — that
|
|
167
|
+
error literally means "run `npx ablo push`."
|
|
168
|
+
|
|
169
|
+
Now Ablo needs real Postgres tables behind those models. Two ways, depending on
|
|
170
|
+
who owns the tables:
|
|
141
171
|
|
|
142
|
-
|
|
143
|
-
tables
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
your
|
|
172
|
+
- **Adopt existing tables (the common case).** Most teams already have the
|
|
173
|
+
tables — created by Prisma, Drizzle, or hand-written migrations. Run
|
|
174
|
+
`npx ablo pull` to import their shape into your schema, or `npx ablo check`
|
|
175
|
+
to verify your schema and the live tables agree. Keep managing the tables
|
|
176
|
+
with your own migration tool; Ablo just syncs the subset of models you
|
|
177
|
+
declared.
|
|
178
|
+
- **Let Ablo provision them.** If Ablo should own the tables, `npx ablo migrate`
|
|
179
|
+
creates your synced-model tables (with row-level security) in the registered
|
|
180
|
+
database. Your other tables are left untouched.
|
|
147
181
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
rows stay in your database. Skipping the push makes every write to a new or
|
|
151
|
-
changed model fail with `server_execute_unknown_model` — that error literally
|
|
152
|
-
means "run `npx ablo push`."
|
|
182
|
+
Nothing runs locally — there is no dev server to start. Your app talks to Ablo's
|
|
183
|
+
hosted API with the sandbox key; the rows land in your database.
|
|
153
184
|
|
|
154
185
|
## 5. Write through the model
|
|
155
186
|
|
|
@@ -182,8 +213,9 @@ and you write the usual way with `ablo.<model>.update({ id, data })`.
|
|
|
182
213
|
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
183
214
|
re-reads the fresh row, then hands it to you — so two writers serialize instead
|
|
184
215
|
of clobbering. Normal reads still work while the claim is held. If a server read
|
|
185
|
-
should not return a row while someone else is mid-edit, pass `ifClaimed: '
|
|
186
|
-
to
|
|
216
|
+
should not return a row while someone else is mid-edit, pass `ifClaimed: 'fail'`
|
|
217
|
+
to error out instead. Reads never block on a claim — to wait for a row to free
|
|
218
|
+
up, `claim({ id })` it (the claim queues fairly behind the holder).
|
|
187
219
|
Call `handle.release()` when your work is done.
|
|
188
220
|
|
|
189
221
|
```ts
|