@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.
Files changed (90) hide show
  1. package/AGENTS.md +12 -5
  2. package/CHANGELOG.md +269 -13
  3. package/README.md +6 -5
  4. package/dist/ai-sdk.d.ts +1 -1
  5. package/dist/ai-sdk.d.ts.map +1 -1
  6. package/dist/context/evidence.d.ts +6 -8
  7. package/dist/context/evidence.d.ts.map +1 -1
  8. package/dist/context/evidence.js +6 -20
  9. package/dist/context/evidence.js.map +1 -1
  10. package/dist/context/index.d.ts +23 -0
  11. package/dist/context/index.d.ts.map +1 -0
  12. package/dist/context/index.js +26 -0
  13. package/dist/context/index.js.map +1 -0
  14. package/dist/context/onChange.d.ts +9 -0
  15. package/dist/context/onChange.d.ts.map +1 -0
  16. package/dist/context/onChange.js +37 -0
  17. package/dist/context/onChange.js.map +1 -0
  18. package/dist/source-conformance.d.ts +1 -1
  19. package/dist/source-conformance.d.ts.map +1 -1
  20. package/dist/source-conformance.js +1 -1
  21. package/dist/source-conformance.js.map +1 -1
  22. package/dist/source-drizzle.d.ts +1 -1
  23. package/dist/source-drizzle.d.ts.map +1 -1
  24. package/dist/source-drizzle.js +1 -1
  25. package/dist/source-drizzle.js.map +1 -1
  26. package/dist/source-kysely.d.ts +1 -1
  27. package/dist/source-kysely.d.ts.map +1 -1
  28. package/dist/source-kysely.js +1 -1
  29. package/dist/source-kysely.js.map +1 -1
  30. package/dist/source-next.d.ts +1 -1
  31. package/dist/source-next.d.ts.map +1 -1
  32. package/dist/source-next.js +1 -1
  33. package/dist/source-next.js.map +1 -1
  34. package/docs/agents.md +38 -15
  35. package/docs/api-keys.md +6 -6
  36. package/docs/api.md +107 -34
  37. package/docs/basic-usage.md +84 -0
  38. package/docs/branch-development.md +23 -4
  39. package/docs/cli.md +16 -9
  40. package/docs/client-behavior.md +27 -29
  41. package/docs/comparison.md +63 -0
  42. package/docs/concurrency-convention.md +92 -75
  43. package/docs/context.md +76 -31
  44. package/docs/coordinate-existing-work.md +104 -0
  45. package/docs/coordination.md +93 -38
  46. package/docs/data-sources.md +12 -6
  47. package/docs/debugging.md +1 -1
  48. package/docs/deployment.md +19 -1
  49. package/docs/examples/agent-human.md +6 -18
  50. package/docs/examples/coordination-conformance.md +69 -0
  51. package/docs/examples/evidence-backed-document-pipeline.md +488 -0
  52. package/docs/examples/existing-python-backend.md +10 -13
  53. package/docs/examples/nextjs.md +2 -2
  54. package/docs/examples/scoped-agent.md +18 -1
  55. package/docs/examples/server-agent.md +2 -2
  56. package/docs/faq.md +75 -0
  57. package/docs/groups.md +19 -139
  58. package/docs/guarantees.md +8 -8
  59. package/docs/idempotency.md +3 -0
  60. package/docs/identity.md +2 -1
  61. package/docs/implement.md +61 -0
  62. package/docs/implementation-index.md +20 -0
  63. package/docs/index.md +59 -173
  64. package/docs/installation.md +77 -0
  65. package/docs/instrumentation.md +52 -0
  66. package/docs/integration-guide.md +20 -19
  67. package/docs/integrations/sandbox-runtime.md +157 -0
  68. package/docs/integrations.md +9 -0
  69. package/docs/migration.md +12 -7
  70. package/docs/operating-on-your-database.md +7 -0
  71. package/docs/options.md +172 -0
  72. package/docs/quickstart.md +25 -14
  73. package/docs/react.md +9 -9
  74. package/docs/schema-contract.md +14 -13
  75. package/docs/security.md +64 -0
  76. package/docs/sessions.md +1 -1
  77. package/examples/README.md +8 -2
  78. package/examples/agent-turn.ts +1 -1
  79. package/examples/expensive-agent-turn.ts +1 -1
  80. package/examples/stale-context-agent-turn.ts +106 -0
  81. package/llms.txt +23 -12
  82. package/package.json +7 -6
  83. package/dist/context/sources.d.ts +0 -21
  84. package/dist/context/sources.d.ts.map +0 -1
  85. package/dist/context/sources.js +0 -36
  86. package/dist/context/sources.js.map +0 -1
  87. package/dist/context.d.ts +0 -22
  88. package/dist/context.d.ts.map +0 -1
  89. package/dist/context.js +0 -33
  90. package/dist/context.js.map +0 -1
@@ -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. Push schema changes deliberately after reviewing the generated diff.
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 check
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 push` updates Ablo's typed schema contract. It does not run application
47
- DDL or drop your tables.
48
- - `ablo check` compares the contract with the connected database without
49
- changing it.
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
@@ -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.
@@ -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
- > No database yet? Pass an `apiKey` only and Ablo keeps your rows in its own log,
14
- > so you can build the whole app today. Point it at a
15
- > separate or local Postgres for a development branch, or at your production
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. In
30
- CI, set project management access explicitly:
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 ABLO_MANAGEMENT_KEY=mk_...
35
+ export ABLO_API_KEY=mk_...
34
36
  ```
35
37
 
36
- Every runtime call needs a branch-bound API key. `ablo dev` exchanges the
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
- **Reserved fields** `id`, `createdAt`, `updatedAt`, `organizationId`, and
71
- `createdBy` are provided by the SDK automatically. Don't declare them in your
72
- `model(...)` fields; declare only your own.
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 `get({ id })`. It resolves to the row, or to
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.get({ id: created.id });
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({ id: handle.data.id, data: { status: 'ready' } });
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.get({ id });
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: `get({ id })` for one row and
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 snap = ablo.snapshot({ weatherReports: id });
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
- readAt: snap.stamp,
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 snap = ablo.snapshot({ weatherReports: id });
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
- readAt: snap.stamp,
181
- onStale: 'reject',
181
+ reads: [report],
182
182
  });
183
183
  }
184
184
  ```
@@ -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
- ### Reserved fields
54
+ ### The one field you don't declare
55
55
 
56
- The SDK provides these on every row automatically do **not** declare them in
57
- your `model(...)` fields:
56
+ `id` is supplied on every row, so leave it out of your `model(...)` fields. That
57
+ is the whole list.
58
58
 
59
- - `id`
60
- - `createdAt`
61
- - `updatedAt`
62
- - `organizationId`
63
- - `createdBy`
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
- Declare only your own fields; the reserved ones are still present on the row and
66
- readable, you just don't author them.
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.get({ id: reportId });
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 `readAt` + `onStale: 'reject'` when a write must fail if the row changed
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).
@@ -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
@@ -74,7 +74,7 @@ plumbing:
74
74
  ```tsx
75
75
  'use client';
76
76
 
77
- import Ablo from '@abloatai/ablo';
77
+ import { Ablo } from '@abloatai/ablo/react';
78
78
  import { AbloProvider } from '@abloatai/ablo/react';
79
79
  import { schema } from '@/ablo.schema';
80
80
 
@@ -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.get({ id: recordId });
34
- const policy = await ablo.policies.get({ id: policyId });
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)
@@ -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.get({ id: recordId });
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
- readSet: durable.readSet,
52
+ reads: durable.reads,
53
53
  attempts: durable.attempts,
54
54
  claims: durable.claims,
55
55
  authority: durable.authority,