@abloatai/ablo 0.57.0 → 0.59.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/AGENTS.md +12 -5
- package/CHANGELOG.md +269 -13
- package/README.md +6 -5
- package/dist/ai-sdk.d.ts +1 -1
- package/dist/ai-sdk.d.ts.map +1 -1
- package/dist/context/evidence.d.ts +6 -8
- package/dist/context/evidence.d.ts.map +1 -1
- package/dist/context/evidence.js +6 -20
- package/dist/context/evidence.js.map +1 -1
- package/dist/context/index.d.ts +23 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +26 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/onChange.d.ts +9 -0
- package/dist/context/onChange.d.ts.map +1 -0
- package/dist/context/onChange.js +37 -0
- package/dist/context/onChange.js.map +1 -0
- package/dist/source-conformance.d.ts +1 -1
- package/dist/source-conformance.d.ts.map +1 -1
- package/dist/source-conformance.js +1 -1
- package/dist/source-conformance.js.map +1 -1
- package/dist/source-drizzle.d.ts +1 -1
- package/dist/source-drizzle.d.ts.map +1 -1
- package/dist/source-drizzle.js +1 -1
- package/dist/source-drizzle.js.map +1 -1
- package/dist/source-kysely.d.ts +1 -1
- package/dist/source-kysely.d.ts.map +1 -1
- package/dist/source-kysely.js +1 -1
- package/dist/source-kysely.js.map +1 -1
- package/dist/source-next.d.ts +1 -1
- package/dist/source-next.d.ts.map +1 -1
- package/dist/source-next.js +1 -1
- package/dist/source-next.js.map +1 -1
- package/docs/agents.md +38 -15
- package/docs/api-keys.md +6 -6
- package/docs/api.md +107 -34
- package/docs/basic-usage.md +84 -0
- package/docs/branch-development.md +23 -4
- package/docs/cli.md +16 -9
- package/docs/client-behavior.md +27 -29
- package/docs/comparison.md +63 -0
- package/docs/concurrency-convention.md +92 -75
- package/docs/context.md +76 -31
- package/docs/coordinate-existing-work.md +104 -0
- package/docs/coordination.md +93 -38
- package/docs/data-sources.md +12 -6
- package/docs/debugging.md +1 -1
- package/docs/deployment.md +19 -1
- package/docs/examples/agent-human.md +6 -18
- package/docs/examples/coordination-conformance.md +69 -0
- package/docs/examples/evidence-backed-document-pipeline.md +488 -0
- package/docs/examples/existing-python-backend.md +10 -13
- package/docs/examples/nextjs.md +2 -2
- package/docs/examples/scoped-agent.md +18 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/faq.md +75 -0
- package/docs/groups.md +19 -139
- package/docs/guarantees.md +8 -8
- package/docs/idempotency.md +3 -0
- package/docs/identity.md +2 -1
- package/docs/implement.md +61 -0
- package/docs/implementation-index.md +20 -0
- package/docs/index.md +59 -173
- package/docs/installation.md +77 -0
- package/docs/instrumentation.md +52 -0
- package/docs/integration-guide.md +20 -19
- package/docs/integrations/sandbox-runtime.md +157 -0
- package/docs/integrations.md +9 -0
- package/docs/migration.md +12 -7
- package/docs/operating-on-your-database.md +7 -0
- package/docs/options.md +172 -0
- package/docs/quickstart.md +25 -14
- package/docs/react.md +9 -9
- package/docs/schema-contract.md +14 -13
- package/docs/security.md +64 -0
- package/docs/sessions.md +1 -1
- package/examples/README.md +8 -2
- package/examples/agent-turn.ts +1 -1
- package/examples/expensive-agent-turn.ts +1 -1
- package/examples/stale-context-agent-turn.ts +106 -0
- package/llms.txt +23 -12
- package/package.json +7 -6
- package/dist/context/sources.d.ts +0 -21
- package/dist/context/sources.d.ts.map +0 -1
- package/dist/context/sources.js +0 -36
- package/dist/context/sources.js.map +0 -1
- package/dist/context.d.ts +0 -22
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -33
- package/dist/context.js.map +0 -1
package/docs/integrations.md
CHANGED
|
@@ -13,6 +13,7 @@ into authoritative application state.
|
|
|
13
13
|
|
|
14
14
|
| Category | Integration | Status | Use it for |
|
|
15
15
|
|---|---|---|---|
|
|
16
|
+
| Agent execution | [Anthropic Sandbox Runtime](./integrations/sandbox-runtime.md) | Available | Restricting the filesystem, network, sockets, and inherited authority of an agent process |
|
|
16
17
|
| Long-running records | [Temporal](./integrations/temporal.md) | Available | Durable Workflows, Activity retries, timers, cancellation, and durable AI SDK calls |
|
|
17
18
|
| Long-running records | [Inngest](./integrations/inngest.md) | Available | Event-driven durable functions, retriable steps, flow control, and checkpointed AI SDK calls |
|
|
18
19
|
| Data ingestion | Connector runtimes | Planned | Bringing external data into Ablo-backed models without creating a second write authority |
|
|
@@ -22,6 +23,14 @@ the boundary has been tested. A dedicated package comes later still: only
|
|
|
22
23
|
repeated production integrations that reveal substantial reusable behavior
|
|
23
24
|
justify adding another public runtime dependency.
|
|
24
25
|
|
|
26
|
+
## Agent execution
|
|
27
|
+
|
|
28
|
+
Use [Anthropic Sandbox Runtime](./integrations/sandbox-runtime.md) to enforce the
|
|
29
|
+
boundary around an agent process. The runtime owns filesystem and network
|
|
30
|
+
access. Ablo remains below it and owns typed shared-state operations, claims,
|
|
31
|
+
idempotency, and confirmation. The runnable example lives in
|
|
32
|
+
`examples/sandboxed-agent`.
|
|
33
|
+
|
|
25
34
|
## Long-running records
|
|
26
35
|
|
|
27
36
|
Use [Temporal](./integrations/temporal.md) when work must survive process
|
package/docs/migration.md
CHANGED
|
@@ -11,13 +11,14 @@ releases remain compatible within the same minor line.
|
|
|
11
11
|
2. Read the changelog entries between your installed and target versions.
|
|
12
12
|
3. Use the documentation bundled with the target package while changing code.
|
|
13
13
|
4. Run type-checks and tests before updating a production branch.
|
|
14
|
-
5.
|
|
14
|
+
5. Run the three-state deployment plan, then push the exact reviewed plan.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npm install @abloatai/ablo@0.48
|
|
18
18
|
npx ablo docs
|
|
19
19
|
npx ablo docs api
|
|
20
|
-
npx ablo
|
|
20
|
+
npx ablo plan
|
|
21
|
+
npx ablo plan --json
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
`npx ablo docs` is version-matched to the installed package. Prefer it during an
|
|
@@ -41,17 +42,21 @@ types and server-confirmed identity instead.
|
|
|
41
42
|
|
|
42
43
|
## Schema and database safety
|
|
43
44
|
|
|
44
|
-
An SDK upgrade and a database migration are separate operations
|
|
45
|
+
An SDK upgrade and a database migration are separate operations coordinated by
|
|
46
|
+
one deployment plan.
|
|
45
47
|
|
|
46
|
-
- `ablo
|
|
47
|
-
|
|
48
|
-
- `ablo
|
|
49
|
-
|
|
48
|
+
- `ablo plan` compares source, the active Ablo artifact, and PostgreSQL without
|
|
49
|
+
changing any of them. Its fingerprint pins all three observations.
|
|
50
|
+
- `ablo push` consumes that reviewed fingerprint and refuses if any state moved.
|
|
51
|
+
- `ablo check` is the database-compatibility view of the same plan.
|
|
50
52
|
- Your ORM or migration tool remains responsible for tables, columns,
|
|
51
53
|
constraints, and application data migrations.
|
|
52
54
|
|
|
53
55
|
When both need to change, deploy the database migration in a backwards-compatible
|
|
54
56
|
form first, push the compatible Ablo schema, then remove old application paths.
|
|
57
|
+
For a live rename or required-field change, keep expand, dual-write, resumable
|
|
58
|
+
backfill, verification, switch, and contract as explicit gates. Contract is a
|
|
59
|
+
later, separately approved deployment—not the tail of expand.
|
|
55
60
|
|
|
56
61
|
## If an upgrade fails
|
|
57
62
|
|
|
@@ -39,6 +39,13 @@ Ablo prevents a non-holder from writing through another participant's claim by
|
|
|
39
39
|
default. It does not infer that every plain update is a read-modify-write
|
|
40
40
|
operation; use the guarded forms when lost-update protection matters.
|
|
41
41
|
|
|
42
|
+
Direct SQL remains outside that enforcement path. If an existing application
|
|
43
|
+
service writes the same row directly, keep its database constraints and locks
|
|
44
|
+
as the final integrity boundary and route every participating caller through
|
|
45
|
+
one named operation. Ablo can select one worker before expensive work begins;
|
|
46
|
+
the existing service must still re-read, validate, and commit authoritatively.
|
|
47
|
+
See [Coordinate an existing database operation](./coordination.md#coordinate-an-existing-database-operation).
|
|
48
|
+
|
|
42
49
|
## Three kinds of action
|
|
43
50
|
|
|
44
51
|
Sort any action you're about to take into one of these, and the right move
|
package/docs/options.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Options
|
|
2
|
+
|
|
3
|
+
> Every option accepted by the default `Ablo({ ... })` client.
|
|
4
|
+
|
|
5
|
+
Import `Ablo` from `@abloatai/ablo`. Only `schema` is required. A server can
|
|
6
|
+
usually rely on `ABLO_API_KEY` and keep the constructor small:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import Ablo from '@abloatai/ablo';
|
|
10
|
+
import { schema } from './ablo/schema';
|
|
11
|
+
|
|
12
|
+
export const ablo = Ablo({ schema });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
These options configure the stateless HTTP client exported by the package root.
|
|
16
|
+
For a live human interface, use the [React guide](./react.md).
|
|
17
|
+
|
|
18
|
+
## schema
|
|
19
|
+
|
|
20
|
+
The schema created with `defineSchema()`. It gives each declared model a typed
|
|
21
|
+
`ablo.<model>` client. This is the only required option.
|
|
22
|
+
|
|
23
|
+
## apiKey
|
|
24
|
+
|
|
25
|
+
A server API key, or an async function that resolves a credential at request
|
|
26
|
+
time. When omitted, Ablo reads `ABLO_API_KEY`.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use a resolver for credentials that rotate. Return `null` when the login has
|
|
33
|
+
ended; throw when credential resolution failed temporarily. Do not pass both
|
|
34
|
+
`apiKey` and `authEndpoint`.
|
|
35
|
+
|
|
36
|
+
## authEndpoint
|
|
37
|
+
|
|
38
|
+
A same-origin URL that mints a short-lived credential, or an async credential
|
|
39
|
+
resolver. The client sends a `POST` with cookies included and renews the token
|
|
40
|
+
when needed.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use this instead of placing a private API key in browser code.
|
|
47
|
+
|
|
48
|
+
## authToken
|
|
49
|
+
|
|
50
|
+
A bearer token the caller already holds. This is mainly for self-hosted or
|
|
51
|
+
custom authentication layers. Hosted applications normally use `apiKey` or
|
|
52
|
+
`authEndpoint`.
|
|
53
|
+
|
|
54
|
+
## baseURL
|
|
55
|
+
|
|
56
|
+
Overrides the Ablo API URL. Leave it unset for hosted Ablo. Use it for a private
|
|
57
|
+
deployment, local development proxy, or test server.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const ablo = Ablo({ schema, baseURL: 'https://ablo.internal.example' });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Because credentials are sent to this URL, Ablo rejects URLs containing embedded
|
|
64
|
+
credentials, query parameters, or fragments. Plain HTTP is accepted only for
|
|
65
|
+
local hosts.
|
|
66
|
+
|
|
67
|
+
## dangerouslyAllowBrowser
|
|
68
|
+
|
|
69
|
+
Allows a credential-bearing client to run in a browser. Defaults to `false`.
|
|
70
|
+
|
|
71
|
+
Private API keys must not ship to browsers. Prefer `authEndpoint`; enable this
|
|
72
|
+
option only when the browser receives a narrowly scoped session credential or
|
|
73
|
+
all traffic passes through a controlled server proxy.
|
|
74
|
+
|
|
75
|
+
## fetch
|
|
76
|
+
|
|
77
|
+
A custom `fetch` implementation for tests, proxies, or runtimes without the
|
|
78
|
+
standard global implementation.
|
|
79
|
+
|
|
80
|
+
## authTimeoutMs
|
|
81
|
+
|
|
82
|
+
The deadline in milliseconds for a request to `authEndpoint`. Defaults to
|
|
83
|
+
`10000`. This is separate from `timeoutMs`, which covers ordinary Ablo API
|
|
84
|
+
requests.
|
|
85
|
+
|
|
86
|
+
## allowCrossOriginAuthEndpoint
|
|
87
|
+
|
|
88
|
+
Allows `authEndpoint` to use a different origin. Defaults to `false`.
|
|
89
|
+
|
|
90
|
+
Keep the default unless the credential-minting service intentionally lives on a
|
|
91
|
+
different trusted origin.
|
|
92
|
+
|
|
93
|
+
## bootstrapBaseUrl
|
|
94
|
+
|
|
95
|
+
Overrides the URL used for credential exchange and bootstrap. Most applications
|
|
96
|
+
should leave this unset and use `baseURL` for a private or test deployment.
|
|
97
|
+
|
|
98
|
+
## defaultHeaders
|
|
99
|
+
|
|
100
|
+
Headers included with every Ablo HTTP request. A `null` value removes a default
|
|
101
|
+
header.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const ablo = Ablo({
|
|
105
|
+
schema,
|
|
106
|
+
defaultHeaders: { 'x-deployment': 'worker-eu' },
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Do not use this option to duplicate the credential header; authentication is
|
|
111
|
+
owned by `apiKey`, `authEndpoint`, or `authToken`.
|
|
112
|
+
|
|
113
|
+
## defaultQuery
|
|
114
|
+
|
|
115
|
+
Query parameters included with every Ablo HTTP request. This is primarily for
|
|
116
|
+
proxies and controlled test deployments.
|
|
117
|
+
|
|
118
|
+
## observability
|
|
119
|
+
|
|
120
|
+
A sink for claim lifecycle and rejected-write events. It implements
|
|
121
|
+
`captureClaim(event)` and `captureConflict(event)`. Use it to connect Ablo's
|
|
122
|
+
coordination outcomes to the application's existing telemetry.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const ablo = Ablo({
|
|
126
|
+
schema,
|
|
127
|
+
observability: {
|
|
128
|
+
captureClaim: (event) => telemetry.capture('ablo.claim', event),
|
|
129
|
+
captureConflict: (event) => telemetry.capture('ablo.conflict', event),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## durableWrites
|
|
135
|
+
|
|
136
|
+
Persists an outbound write before dispatch so a worker can recover an
|
|
137
|
+
unacknowledged `create`, `update`, or `delete` after a crash.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const ablo = Ablo({
|
|
141
|
+
schema,
|
|
142
|
+
durableWrites: { store, namespace: 'invoice-worker' },
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The store must implement `seal()`, `list()`, and `remove()`. `namespace` separates
|
|
147
|
+
deployments or workflow lanes sharing the same authenticated actor. Most clients
|
|
148
|
+
do not need durable writes.
|
|
149
|
+
|
|
150
|
+
## commitOutbox
|
|
151
|
+
|
|
152
|
+
Deprecated compatibility name for the durable write store. Use
|
|
153
|
+
`durableWrites: { store }`. Passing both forms is an error.
|
|
154
|
+
|
|
155
|
+
## commitOutboxScope
|
|
156
|
+
|
|
157
|
+
Deprecated compatibility scope for `commitOutbox`. Authentication now resolves
|
|
158
|
+
actor identity. Use `durableWrites.namespace` when shared storage needs separate
|
|
159
|
+
workflow or deployment lanes.
|
|
160
|
+
|
|
161
|
+
## transport
|
|
162
|
+
|
|
163
|
+
The package-root client uses request/response HTTP. `transport: 'http'` is
|
|
164
|
+
accepted but optional.
|
|
165
|
+
|
|
166
|
+
Live state, presence, and local reads belong to the reactive client described in
|
|
167
|
+
the [React guide](./react.md), rather than another value on this option.
|
|
168
|
+
|
|
169
|
+
## timeoutMs
|
|
170
|
+
|
|
171
|
+
The deadline in milliseconds for an Ablo HTTP request. Defaults to `30000`. Pass
|
|
172
|
+
`0` only when the surrounding runtime already enforces a deadline.
|
package/docs/quickstart.md
CHANGED
|
@@ -10,10 +10,11 @@ confirms it by tailing your write-ahead log (WAL). Your rows live in your databa
|
|
|
10
10
|
which stays the system of record. Ablo writes rows but **runs no DDL and owns no
|
|
11
11
|
schema** — your migration tool stays in charge of the shape of your database.
|
|
12
12
|
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
> database when you're ready
|
|
13
|
+
> **Which database?** A branch needs one connected before a schema will push, so
|
|
14
|
+
> start on a throwaway rather than your production Postgres. `ablo dev` gives
|
|
15
|
+
> each Git branch its own isolated plane: point a development branch at a local
|
|
16
|
+
> or separate database, and connect your production one when you're ready for it
|
|
17
|
+
> to be the system of record.
|
|
17
18
|
|
|
18
19
|
## 1. Install and initialize
|
|
19
20
|
|
|
@@ -26,14 +27,16 @@ npx ablo init
|
|
|
26
27
|
by signing you in — one browser click, and a project-scoped `mk_` management
|
|
27
28
|
credential is saved locally. Later, `npx ablo dev` (step 4) prepares an isolated branch and writes
|
|
28
29
|
its temporary `ABLO_API_KEY` into `.env.local`, so the SDK follows your Git
|
|
29
|
-
branch with no manual copy-paste. `npx ablo login` also exists standalone.
|
|
30
|
-
CI
|
|
30
|
+
branch with no manual copy-paste. `npx ablo login` also exists standalone.
|
|
31
|
+
Headless CI uses the same credential input as every other Ablo process; during branch
|
|
32
|
+
preparation it contains an `mk_` credential:
|
|
31
33
|
|
|
32
34
|
```bash
|
|
33
|
-
export
|
|
35
|
+
export ABLO_API_KEY=mk_...
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
Do not pass that value to application code. Every runtime call needs a
|
|
39
|
+
branch-bound API key. `ablo dev` exchanges the
|
|
37
40
|
stored management credential for
|
|
38
41
|
an expiring `sk_*` key bound to the current development branch.
|
|
39
42
|
Production runtimes use `sk_*`. In production a key points at the database
|
|
@@ -67,9 +70,12 @@ export const schema = defineSchema({
|
|
|
67
70
|
});
|
|
68
71
|
```
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
`
|
|
73
|
+
**`id` is the only automatic field.** Every row carries one, so leave it out of
|
|
74
|
+
`model(...)`. Everything else is yours, audit fields included: declare
|
|
75
|
+
`createdAt`, `updatedAt` or `createdBy` and `ablo migrate` gives each a column,
|
|
76
|
+
which your write or a database default then fills. Ablo does not write them for
|
|
77
|
+
you. A model that omits them still reads and writes, it just orders and
|
|
78
|
+
attributes its history less precisely.
|
|
73
79
|
|
|
74
80
|
The schema is registered once (init scaffolds `ablo/register.ts` for you), and
|
|
75
81
|
every type is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
@@ -229,12 +235,12 @@ const updated = await ablo.weatherReports.update({
|
|
|
229
235
|
console.log({ id: updated.id, status: updated.status }); // { id: '...', status: 'ready' }
|
|
230
236
|
```
|
|
231
237
|
|
|
232
|
-
Read a single row back with `
|
|
238
|
+
Read a single row back with `read({ id })`. It resolves to the row, or to
|
|
233
239
|
`undefined` when no row has that id — so narrow it once, then the fields are
|
|
234
240
|
fully typed:
|
|
235
241
|
|
|
236
242
|
```ts
|
|
237
|
-
const report = await ablo.weatherReports.
|
|
243
|
+
const report = await ablo.weatherReports.read({ id: created.id });
|
|
238
244
|
if (!report) throw new Error(`weatherReports ${created.id} not found`);
|
|
239
245
|
|
|
240
246
|
console.log(report.status); // 'ready'
|
|
@@ -275,6 +281,7 @@ await ablo.weatherReports.update({
|
|
|
275
281
|
status: 'ready',
|
|
276
282
|
forecast: weather.summary,
|
|
277
283
|
},
|
|
284
|
+
claim: handle,
|
|
278
285
|
});
|
|
279
286
|
// scope exit releases the claim — no manual release, even if the work threw
|
|
280
287
|
```
|
|
@@ -301,7 +308,11 @@ if (active) {
|
|
|
301
308
|
}
|
|
302
309
|
|
|
303
310
|
await using handle = await ablo.weatherReports.claim({ id: 'weather_stockholm' });
|
|
304
|
-
await ablo.weatherReports.update({
|
|
311
|
+
await ablo.weatherReports.update({
|
|
312
|
+
id: handle.data.id,
|
|
313
|
+
data: { status: 'ready' },
|
|
314
|
+
claim: handle,
|
|
315
|
+
});
|
|
305
316
|
```
|
|
306
317
|
|
|
307
318
|
Use `contention: { mode: 'skip' }` when work should be skipped instead of
|
package/docs/react.md
CHANGED
|
@@ -25,7 +25,7 @@ as props. Construct the client once, then pass that instance to the provider.
|
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
// lib/ablo.ts
|
|
28
|
-
import Ablo from '@abloatai/ablo';
|
|
28
|
+
import { Ablo } from '@abloatai/ablo/react';
|
|
29
29
|
import { createAbloReact } from '@abloatai/ablo/react';
|
|
30
30
|
import { schema } from '@/ablo/schema';
|
|
31
31
|
|
|
@@ -140,12 +140,12 @@ const reports = useAblo((ablo) =>
|
|
|
140
140
|
## Server Load
|
|
141
141
|
|
|
142
142
|
```tsx
|
|
143
|
-
const report = await ablo.weatherReports.
|
|
143
|
+
const report = await ablo.weatherReports.read({ id });
|
|
144
144
|
```
|
|
145
145
|
|
|
146
146
|
Use `get` in Server Components when the row may not be in the local pool
|
|
147
147
|
yet — it hydrates from the local store and the server, and returns a Promise, so
|
|
148
|
-
`await` it. (Server reads come in two shapes: `
|
|
148
|
+
`await` it. (Server reads come in two shapes: `read({ id })` for one row and
|
|
149
149
|
`list({ where })` for many; both are async. The synchronous local reads are
|
|
150
150
|
the `local` reads, used in render below.)
|
|
151
151
|
|
|
@@ -156,12 +156,12 @@ For Server Actions and route handlers, call the SDK directly:
|
|
|
156
156
|
```ts
|
|
157
157
|
import { ablo } from '@/lib/ablo';
|
|
158
158
|
|
|
159
|
-
const
|
|
159
|
+
const report = await ablo.weatherReports.read({ id });
|
|
160
|
+
if (!report) throw new Error('report not found');
|
|
160
161
|
await ablo.weatherReports.update({
|
|
161
162
|
id,
|
|
162
163
|
data: patch,
|
|
163
|
-
|
|
164
|
-
onStale: 'reject',
|
|
164
|
+
reads: [report],
|
|
165
165
|
});
|
|
166
166
|
```
|
|
167
167
|
|
|
@@ -173,12 +173,12 @@ const ablo = useAblo();
|
|
|
173
173
|
|
|
174
174
|
async function markReady() {
|
|
175
175
|
if (!ablo) return;
|
|
176
|
-
const
|
|
176
|
+
const report = await ablo.weatherReports.read({ id });
|
|
177
|
+
if (!report) return;
|
|
177
178
|
await ablo.weatherReports.update({
|
|
178
179
|
id,
|
|
179
180
|
data: { status: 'ready' },
|
|
180
|
-
|
|
181
|
-
onStale: 'reject',
|
|
181
|
+
reads: [report],
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
184
|
```
|
package/docs/schema-contract.md
CHANGED
|
@@ -51,26 +51,27 @@ The model key (`weatherReports`) becomes the client namespace
|
|
|
51
51
|
contract. You should not create a parallel string-keyed write path for the same
|
|
52
52
|
data.
|
|
53
53
|
|
|
54
|
-
###
|
|
54
|
+
### The one field you don't declare
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
`id` is supplied on every row, so leave it out of your `model(...)` fields. That
|
|
57
|
+
is the whole list.
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
Two things look like framework territory and are not. **Audit fields are yours to
|
|
60
|
+
declare and yours to fill.** Add `createdAt`, `updatedAt` or `createdBy` and
|
|
61
|
+
`ablo migrate` gives each a column, which your own write or a database default
|
|
62
|
+
then populates; Ablo records who changed what in its transaction log and does not
|
|
63
|
+
write these columns for you. Omit them and no column is created at all; the model
|
|
64
|
+
still reads and writes, it just orders and attributes its history less precisely.
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
**The tenancy column** (`organizationId` by default) comes from the model's
|
|
67
|
+
`policy` rather than its field list, so you neither declare it nor lose it.
|
|
67
68
|
|
|
68
69
|
## Reads and writes
|
|
69
70
|
|
|
70
71
|
Use async reads when the row may not be local:
|
|
71
72
|
|
|
72
73
|
```ts
|
|
73
|
-
const report = await ablo.weatherReports.
|
|
74
|
+
const report = await ablo.weatherReports.read({ id: reportId });
|
|
74
75
|
const ready = await ablo.weatherReports.list({ where: { status: 'ready' } });
|
|
75
76
|
```
|
|
76
77
|
|
|
@@ -123,8 +124,8 @@ session route, never a raw API key.
|
|
|
123
124
|
- Keep direct database writes out of the coordinated path unless they are reported
|
|
124
125
|
back through Data Source events.
|
|
125
126
|
- Use `claim` for slow read -> think -> write spans.
|
|
126
|
-
- Use `
|
|
127
|
-
after it was read.
|
|
127
|
+
- Use `read` and pass its exact row in `reads` when a write must fail if the row
|
|
128
|
+
changed after it was read.
|
|
128
129
|
|
|
129
130
|
For the shortest runnable path, start with [Quickstart](./quickstart.md). For a
|
|
130
131
|
production app, continue with [Integration Guide](./integration-guide.md).
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
> The authority boundaries to preserve when agents, applications, and people coordinate through Ablo.
|
|
4
|
+
|
|
5
|
+
Ablo carries authenticated participant identity into coordination and writes.
|
|
6
|
+
It does not replace your application's authorization, PostgreSQL constraints, or
|
|
7
|
+
transactional invariants.
|
|
8
|
+
|
|
9
|
+
## Keep secret credentials on the server
|
|
10
|
+
|
|
11
|
+
Trusted agents, workers, route handlers, and services use a server-side `sk_`
|
|
12
|
+
credential, normally supplied through `ABLO_API_KEY`. Never include it in a
|
|
13
|
+
browser bundle or agent-generated output.
|
|
14
|
+
|
|
15
|
+
Browsers use either a publishable read-only `pk_` credential or a short-lived,
|
|
16
|
+
scoped session minted by your backend through `authEndpoint`. See [API
|
|
17
|
+
Keys](./api-keys.md) and [Sessions](./sessions.md) for the credential classes and
|
|
18
|
+
minting flow.
|
|
19
|
+
|
|
20
|
+
## Give every participant its own identity
|
|
21
|
+
|
|
22
|
+
Claims are re-entrant for the same participant. Two workers that share one
|
|
23
|
+
credential can therefore appear to Ablo as the same owner. Use separate scoped
|
|
24
|
+
participant credentials when independently operating agents must contend.
|
|
25
|
+
|
|
26
|
+
The credential also determines project, branch, organization, and allowed
|
|
27
|
+
operations. Callers cannot broaden that authority by adding ids to a request.
|
|
28
|
+
|
|
29
|
+
## Treat claims as coordination, not authorization
|
|
30
|
+
|
|
31
|
+
A claim says who currently owns a piece of work. It does not grant permission to
|
|
32
|
+
read or write that resource. Authorization is evaluated independently, and the
|
|
33
|
+
final write must still satisfy the database schema and application invariants.
|
|
34
|
+
|
|
35
|
+
Claims are leases rather than permanent locks. They expire when their owner
|
|
36
|
+
stops heartbeating. A guarded Ablo write checks ownership again at commit time so
|
|
37
|
+
an expired participant cannot use an old claim handle.
|
|
38
|
+
|
|
39
|
+
## Keep PostgreSQL authoritative
|
|
40
|
+
|
|
41
|
+
Ablo coordinates work before and during a write; PostgreSQL remains the durable
|
|
42
|
+
source of truth. Existing constraints, transactions, row-level security, and
|
|
43
|
+
short database locks can remain in place.
|
|
44
|
+
|
|
45
|
+
A direct database write bypasses Ablo's claims and request ordering. Logical
|
|
46
|
+
replication makes the result visible to Ablo readers, but cannot retroactively
|
|
47
|
+
coordinate the writer. Preserve database constraints for every invariant that
|
|
48
|
+
must also hold for bypass writers.
|
|
49
|
+
|
|
50
|
+
## Bound external side effects separately
|
|
51
|
+
|
|
52
|
+
Ablo idempotency covers an Ablo request. It cannot make an email, payment, model
|
|
53
|
+
call, or third-party API mutation exactly once. Give the external provider its
|
|
54
|
+
own idempotency key, or persist an application-owned effect record and reconcile
|
|
55
|
+
ambiguous outcomes.
|
|
56
|
+
|
|
57
|
+
## Report vulnerabilities privately
|
|
58
|
+
|
|
59
|
+
Do not put credentials, customer data, or an unpatched vulnerability in a public
|
|
60
|
+
issue. Report it through [GitHub Security
|
|
61
|
+
Advisories](https://github.com/Abloatai/ablo/security/advisories/new).
|
|
62
|
+
|
|
63
|
+
For operational checks and key rotation, continue to [API Keys](./api-keys.md),
|
|
64
|
+
[Audit Log](./audit.md), and [Operating on Your Database](./operating-on-your-database.md).
|
package/docs/sessions.md
CHANGED
package/examples/README.md
CHANGED
|
@@ -30,8 +30,8 @@ For read-reason-write work, pass the exact returned rows that informed the
|
|
|
30
30
|
decision. Their watermarks stay opaque:
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
const record = await ablo.records.
|
|
34
|
-
const policy = await ablo.policies.
|
|
33
|
+
const record = await ablo.records.read({ id: recordId });
|
|
34
|
+
const policy = await ablo.policies.read({ id: policyId });
|
|
35
35
|
const result = await model({ record, policy });
|
|
36
36
|
await ablo.records.update({
|
|
37
37
|
id: record.id,
|
|
@@ -48,6 +48,11 @@ rejected locally.
|
|
|
48
48
|
heartbeating claim, post-grant model input, durable commit inspection, automatic
|
|
49
49
|
release, and a released-claim fencing check.
|
|
50
50
|
|
|
51
|
+
`stale-context-agent-turn.ts` owns the standard long-running agent policy:
|
|
52
|
+
subscribe to exact-read changes, abort cancellable work, retain the guarded
|
|
53
|
+
write, rebuild context for bounded retries, and reconcile rather than replay
|
|
54
|
+
after an irreversible side effect.
|
|
55
|
+
|
|
51
56
|
Import the same schema in every runtime. Use `commits.create` only when several
|
|
52
57
|
typed row operations must land atomically; ordinary writes stay on
|
|
53
58
|
`ablo.<model>.create/update/delete`.
|
|
@@ -63,6 +68,7 @@ cd packages/ablo
|
|
|
63
68
|
ABLO_API_KEY=sk_... npx tsx examples/quickstart.ts
|
|
64
69
|
ABLO_API_KEY=sk_... RECORD_ID=record_... npx tsx examples/agent-turn.ts
|
|
65
70
|
ABLO_API_KEY=sk_... JOB_ID=job_... npx tsx examples/expensive-agent-turn.ts
|
|
71
|
+
ABLO_API_KEY=sk_... RECORD_ID=record_... npx tsx examples/stale-context-agent-turn.ts
|
|
66
72
|
```
|
|
67
73
|
|
|
68
74
|
## Data Source (customer-owned database)
|
package/examples/agent-turn.ts
CHANGED
|
@@ -21,7 +21,7 @@ if (!recordId) throw new Error('RECORD_ID is required');
|
|
|
21
21
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
22
22
|
try {
|
|
23
23
|
await ablo.ready();
|
|
24
|
-
const record = await ablo.records.
|
|
24
|
+
const record = await ablo.records.read({ id: recordId });
|
|
25
25
|
if (!record) throw new Error(`Record ${recordId} was not found`);
|
|
26
26
|
const commitId = `record:${recordId}:cheap`;
|
|
27
27
|
await ablo.records.update({
|
|
@@ -49,7 +49,7 @@ try {
|
|
|
49
49
|
if (!durable) throw new Error(`Commit ${commitId} was not retained`);
|
|
50
50
|
console.log({
|
|
51
51
|
identity: ablo.identity,
|
|
52
|
-
|
|
52
|
+
reads: durable.reads,
|
|
53
53
|
attempts: durable.attempts,
|
|
54
54
|
claims: durable.claims,
|
|
55
55
|
authority: durable.authority,
|