@abloatai/ablo 0.58.0 → 0.59.1

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.
@@ -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).
@@ -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)
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Canonical stale-context turn: abort early, retain the authoritative write
3
+ * guard, rebuild context on a bounded retry, and never blindly replay an
4
+ * irreversible tool side effect.
5
+ *
6
+ * The application-owned model and notification functions below are small,
7
+ * deterministic stand-ins. Keep their cancellation/idempotency contracts when
8
+ * replacing them with real providers.
9
+ *
10
+ * Run: ABLO_API_KEY=sk_... RECORD_ID=record_... npx tsx examples/stale-context-agent-turn.ts
11
+ */
12
+ import { Ablo, AbloStaleContextError } from '@abloatai/ablo';
13
+ import { context } from '@abloatai/ablo/context';
14
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
15
+
16
+ const schema = defineSchema({
17
+ records: model({
18
+ title: z.string(),
19
+ status: z.enum(['pending', 'done']),
20
+ result: z.string().optional(),
21
+ }),
22
+ });
23
+
24
+ const delivered = new Set<string>();
25
+
26
+ async function callModel(title: string, signal: AbortSignal): Promise<string> {
27
+ await new Promise<void>((resolve, reject) => {
28
+ const timer = setTimeout(resolve, 25);
29
+ signal.addEventListener('abort', () => {
30
+ clearTimeout(timer);
31
+ reject(signal.reason);
32
+ }, { once: true });
33
+ });
34
+ return `Completed: ${title}`;
35
+ }
36
+
37
+ async function sendResult(operationKey: string, signal: AbortSignal): Promise<void> {
38
+ signal.throwIfAborted();
39
+ // Replace with a provider call that accepts operationKey as its idempotency key.
40
+ delivered.add(operationKey);
41
+ }
42
+
43
+ async function wasResultSent(operationKey: string): Promise<boolean> {
44
+ // Replace with the provider's outcome lookup using the same key.
45
+ return delivered.has(operationKey);
46
+ }
47
+
48
+ const recordId = process.env.RECORD_ID;
49
+ if (!recordId) throw new Error('RECORD_ID is required');
50
+
51
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
52
+
53
+ async function completeRecord(id: string): Promise<void> {
54
+ const operationKey = `complete-record:${id}`;
55
+ const maxAttempts = 3;
56
+
57
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
58
+ const ctx = await context({
59
+ ablo,
60
+ data: { record: ablo.records.read({ id }) },
61
+ });
62
+ if (!ctx.data.record) throw new Error(`Record ${id} was not found`);
63
+
64
+ const controller = new AbortController();
65
+ const stop = ctx.onChange((error) => controller.abort(error));
66
+ let resultMayHaveBeenSent = false;
67
+
68
+ try {
69
+ const result = await callModel(ctx.data.record.title, controller.signal);
70
+ resultMayHaveBeenSent = true;
71
+ await sendResult(operationKey, controller.signal);
72
+
73
+ await ablo.records.update({
74
+ id: ctx.data.record.id,
75
+ data: { status: 'done', result },
76
+ reads: ctx.reads,
77
+ idempotencyKey: operationKey,
78
+ });
79
+ console.log({ attempt, operationKey, status: 'done' });
80
+ return;
81
+ } catch (error) {
82
+ const stale = error instanceof AbloStaleContextError ||
83
+ controller.signal.reason instanceof AbloStaleContextError;
84
+ if (!stale) throw error;
85
+
86
+ if (resultMayHaveBeenSent) {
87
+ const sent = await wasResultSent(operationKey);
88
+ throw new Error(
89
+ `Context changed after the external action (sent=${sent}). ` +
90
+ `Reconcile operation ${operationKey}; do not replay it automatically.`,
91
+ );
92
+ }
93
+ if (attempt === maxAttempts) throw error;
94
+ // The next iteration assembles new data and new read evidence.
95
+ } finally {
96
+ stop();
97
+ }
98
+ }
99
+ }
100
+
101
+ try {
102
+ await ablo.ready();
103
+ await completeRecord(recordId);
104
+ } finally {
105
+ await ablo.dispose();
106
+ }
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ablo
2
2
 
3
- Ablo is collaboration infrastructure for AI agents: one API for agents, apps, and services to claim, change, and confirm the same rows.
3
+ Ablo is coordination infrastructure for agents, applications, services, and people working on shared state.
4
4
 
5
5
  Here is the problem it solves. Two agents reach for `report_stockholm` at once. One claims the row, does slow work (an LLM call, a fetch, a chain of tools), and commits. The second is neither rejected nor allowed to clobber: it waits in line, is handed the row as it now stands, and proceeds. Claims don't lock. If another writer holds the row, `claim` waits for them, re-reads the fresh row, then hands it to you — so writers serialize instead of colliding. A person editing that row is simply another holder; the agent waits behind them the same way. And a claim is as narrow as its target: name a `path`, a `range`, or a `field` and two claims on non-overlapping parts of the same row are both granted — region locking within one row, with no queueing between regions.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.58.0",
3
+ "version": "0.59.1",
4
4
  "description": "The public Ablo SDK for coordinated reads, commits, claims, observation, and reactive applications.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -112,6 +112,7 @@
112
112
  "prepack": "npm run build && node scripts/strip-source-condition.mjs",
113
113
  "postpack": "node scripts/restore-source-condition.mjs",
114
114
  "pack:check": "node scripts/pack-check.mjs",
115
+ "verify:context-package": "node scripts/verify-context-package.mjs",
115
116
  "typecheck": "tsc --noEmit && tsc -p typetests/tsconfig.json && tsc -p examples/tsconfig.json",
116
117
  "test": "vitest run",
117
118
  "generate:errors": "tsx scripts/generate-error-docs.mts",
@@ -139,8 +140,8 @@
139
140
  "directory": "packages/ablo"
140
141
  },
141
142
  "dependencies": {
142
- "@abloatai/humans": "^0.58.0",
143
- "@abloatai/transaction": "^0.58.0",
143
+ "@abloatai/humans": "^0.59.1",
144
+ "@abloatai/transaction": "^0.59.1",
144
145
  "zod": "^4.4.3"
145
146
  },
146
147
  "peerDependencies": {
@@ -1,123 +0,0 @@
1
- # Agent Integration Decision Guide
2
-
3
- > Choose one integration route before reading an example. Most existing products should coordinate one existing operation first; they should not copy the document pipeline.
4
-
5
- ## Start with the operation you already own
6
-
7
- Name one existing application operation, such as `completeTask`, `approveInvoice`,
8
- or `publishReport`. Keep its authorization, database transaction, constraints,
9
- and public API in place. Add Ablo at that operation boundary.
10
-
11
- Use this routing table for the decisions that are easy to conflate:
12
-
13
- | Question | Choose | When |
14
- |---|---|---|
15
- | What is being coordinated? | Identifier-only claim | Work has a stable business identity, but the authoritative row and final write remain in the existing service or Postgres. |
16
- | | Row-backed claim | The coordinated row is an Ablo schema model and the final write goes through that model resource. |
17
- | Did the decision depend on previously read rows? | Captured reads | Read the premises with `read(...)`, then pass those returned rows through `reads`. Use this even when the written row is different from a premise row. |
18
- | Must several Ablo mutations either all land or none land? | Atomic commit | Put the mutations, captured reads, and any typed claim handles in one `commits.create(...)`. Separate mutation calls are independently successful or failed. |
19
- | Where should the final write happen? | Existing database write | Keep it when the current service owns the transaction, constraints, or rollout switch. Ablo's lease does not join a transaction running in another process. Re-read and validate inside the database transaction. |
20
- | | Ablo-routed write | Use it for declared schema models after the database connection and server schema are configured. Guard decision-dependent writes with a held claim or captured reads. |
21
- | Who is participating? | Stateless HTTP client | Agents, jobs, and request/response server handlers. Give each concurrent participant its own scoped credential. |
22
- | | Reactive WebSocket client | Human-facing applications that need live state, presence, or local reactive reads. This transport is not required for worker coordination. |
23
-
24
- These choices compose. For example, a stateless worker can take an
25
- identifier-only claim, perform slow work, and then call an existing Postgres
26
- operation. Another worker can take a row-backed claim and submit an atomic Ablo
27
- commit guarded by captured premise rows.
28
-
29
- ## Choose the smallest example
30
-
31
- ### Coordinate existing work
32
-
33
- Start with
34
- [`examples/graphql-existing-backend`](../../../examples/graphql-existing-backend/README.md)
35
- when an application already owns its API, operation, and Postgres write.
36
-
37
- It demonstrates:
38
-
39
- - GraphQL delegating to a named application operation;
40
- - an identifier lease around expensive work;
41
- - the existing service retaining its authoritative transaction and re-read;
42
- - an operation-level switch between existing and coordinated paths; and
43
- - recovery and contract parity without replacing the application's API.
44
-
45
- Use
46
- [`examples/coordination-conformance`](../../../examples/coordination-conformance/README.md)
47
- alongside it to verify real hosted lease behavior independently of the domain.
48
-
49
- ### Build evidence-backed document state
50
-
51
- Read
52
- [`examples/existing-document-pipeline`](../../../examples/existing-document-pipeline/README.md)
53
- only when the feature genuinely needs versioned source evidence, citations,
54
- guarded review decisions, atomic multi-row review writes, and retained search
55
- projections.
56
-
57
- That example is an advanced reference application. Its document ingestion,
58
- search, review, projection-retention, and append-only event policies are not
59
- prerequisites for adopting Ablo.
60
-
61
- ## Know which owner makes each promise
62
-
63
- | Ablo responsibility | Application responsibility |
64
- |---|---|
65
- | Participant-scoped claims, lease expiry, wait/skip behavior, heartbeat, release, and commit-time fencing | Choosing the business claim identity and issuing distinct participant credentials |
66
- | Capturing model-row versions returned by `read(...)` and rejecting a guarded write when those premises are stale | Choosing every row that is a premise of the decision |
67
- | Atomicity among mutations submitted in one Ablo commit | Database transactions and constraints outside that commit; never presenting separate writes as an atomic batch |
68
- | Request idempotency within the documented identity, retention, and identical-request rules | Durable workflow idempotency and deduplication of external effects |
69
- | Synchronizing declared model rows and serving reactive state | Uploads, search semantics, projections, workflow execution, review policy, and external APIs |
70
- | Credentials, participant attribution, and schema-declared scope enforcement | Existing application authentication and authorization at the operation boundary |
71
-
72
- Claims coordinate cooperative participants; they are leases, not absolute locks.
73
- A writer outside the coordinated path can still change Postgres. Database
74
- constraints and a commit-time guarded re-read remain the final backstop.
75
-
76
- ## Minimum integration contract
77
-
78
- Write down these answers next to the operation before implementing it:
79
-
80
- 1. **Existing operation:** Which named operation and public API remain stable?
81
- 2. **Claim identity:** Which stable model row or business identifier represents the contested work?
82
- 3. **Participant identity:** Which distinct scoped credential does each concurrent human, agent, or worker use?
83
- 4. **Premises:** Which exact rows does the decision depend on, and which of them must use `read(...)`?
84
- 5. **Atomic boundary:** Which writes must all succeed together? Are they one Ablo commit, one existing database transaction, or deliberately independent?
85
- 6. **Persistence owner:** What remains in Postgres and which writes, if any, are routed through Ablo?
86
- 7. **Failure behavior:** What happens on contention, lease expiry, stale evidence, request retry, partial completion, and an external side-effect failure?
87
- 8. **Proof:** Which local contract test and which hosted or staging test proves each claimed guarantee?
88
-
89
- If an answer is unknown, keep the existing write path available. Do not broaden
90
- the schema or copy an advanced example to hide the missing decision.
91
-
92
- ## Guarantee-to-test matrix
93
-
94
- The examples prove different layers. A local fixture proves application behavior;
95
- it does not prove hosted infrastructure. Conversely, hosted claim conformance
96
- does not prove a domain transition or a real Postgres transaction.
97
-
98
- | Statement | Proof level | Executable evidence |
99
- |---|---|---|
100
- | GraphQL keeps the same result while the named operation changes implementation | Local application contract | `examples/graphql-existing-backend/tests/graphql.test.ts` and `tests/pilot.test.ts` — `the GraphQL resolver delegates one typed input to the named operation`; `the operation switch preserves the uncontended GraphQL contract` |
101
- | Coordination moves expensive work outside the retained database critical section | Local application contract; real DB timing requires staging | `examples/graphql-existing-backend/tests/pilot.test.ts` — `coordination moves expensive work outside the retained critical section`; optional `npm run test:live:postgres` |
102
- | Contenders do not duplicate expensive work, and failure releases the path | Local application contract | `examples/graphql-existing-backend/tests/pilot.test.ts` — `two coordinated workers pay for expensive work once`; `a failed owner releases coordination so the existing path remains available` |
103
- | Distinct hosted participants exclude one another, heartbeat, release, and recover after expiry | Hosted infrastructure | `examples/coordination-conformance`: `npm run test:live`; its live runner also executes the exit-without-release expiry probe |
104
- | A decision based on changed source evidence is rejected | Local application contract using Ablo-shaped guards | `examples/existing-document-pipeline/tests/processDocument.test.ts` — `a source change rejects stale extracted output`; `tests/review.test.ts` — `guarded mutations reject stale evidence and release claims for retry` |
105
- | Several review records submitted as one commit are atomic; separate calls can partially complete | Local application contract | `examples/existing-document-pipeline/tests/review.test.ts` — `requesting review atomically creates durable issue state and an event`; `independent review writes expose partial completion and retry only the stale target` |
106
- | Rebuilding search does not remove a complete snapshot referenced by durable review evidence | Local application policy | `examples/existing-document-pipeline/tests/search.test.ts` — `publishing a rebuild retains the complete projection snapshot referenced by review`; `an unreferenced superseded projection becomes removable as one snapshot` |
107
- | The documented ownership tree, public exports, dependency direction, and lack of cycles match disk | Local structure contract | Each focused example's `tests/structure.test.ts`; the document fixture additionally validates exact inventory and dependency direction from `structure.json` |
108
- | Authorization, latency, database locking, and external-effect behavior match the production application | Partner staging | Run the operation against the real auth, Postgres schema, workload, and provider sandbox. No repository fixture can establish this claim. |
109
-
110
- ## Safe first adoption
111
-
112
- For an existing product, the default sequence is:
113
-
114
- 1. Wrap one named operation without changing its public API.
115
- 2. Use an identifier-only claim if the existing database remains authoritative.
116
- 3. Keep the Postgres transaction, lock, validation, and constraints in place.
117
- 4. Verify participant-scoped lease behavior with coordination conformance.
118
- 5. Add captured reads or an atomic Ablo commit only when the operation actually needs them.
119
- 6. Move more persistence through Ablo only after the guarded hosted write path and staging behavior are proven for that operation.
120
-
121
- Continue with the [Integration Guide](./integration-guide.md) for setup and API
122
- details, or [Concurrency Convention](./concurrency-convention.md) for the exact
123
- guarding rules.