@allorc/a423-sdk 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,349 +1,103 @@
1
- # Configurable workflows in 0.2.0
1
+ # A423 SDK — 0.4.0
2
2
 
3
- The new `@allorc/a423-sdk/workflow` entry point provides middleware for agent job lifecycles. It supports different task types and payment systems through application-supplied policies and adapters. It does not assume a particular blockchain, token, wallet, artifact format, database or AI framework. This is experimental software with a reference lifecycle; it does not promise every possible business workflow.
3
+ Experimental Node.js/Next.js middleware for agent tasks, agreements and escrow. Version 0.4 adds working persistence, authority credentials and operational services around the configurable workflow and reference contract. Installing the SDK never publishes endpoints or deploys a contract automatically.
4
4
 
5
- The original root exports remain compatible. Choose the workflow entry point for the full lifecycle; do not mix its records with the original root API. See [integration and migration instructions](./INTEGRATION.md).
6
-
7
- ```ts
8
- import { createWorkflow, createWorkflowHandler } from '@allorc/a423-sdk/workflow';
9
-
10
- const workflow = createWorkflow({
11
- store: durableWorkflowStore,
12
- adapters: applicationAdapters,
13
- validateTerms: validateYourTerms,
14
- authorize: checkYourPolicy,
15
- });
16
- const handler = createWorkflowHandler({ workflow, authenticate: authenticateRequest });
17
- // app/api/a423/[...path]/route.ts
18
- export const runtime = 'nodejs';
19
- export { handler as GET, handler as POST };
20
- ```
21
-
22
- The names `durableWorkflowStore`, `applicationAdapters`, `validateYourTerms`, `checkYourPolicy` and `authenticateRequest` are application implementations, not SDK exports. Configure them once in a server module and import the handler into your route.
23
-
24
- ## Workflow coverage
25
-
26
- | Action | From | Default role | Result | Required adapter |
27
- |---|---|---|---|---|
28
- | create | — | requester | draft | none |
29
- | quote | draft / quoted / accepted | requester or worker | quoted, acceptances reset | none |
30
- | accept | quoted | requester and worker separately | accepted after both | none |
31
- | fund | accepted | requester | funded | fund |
32
- | start | funded | worker | running | start (funding/work gate) |
33
- | submit | running | worker | submitted | submit |
34
- | verify | submitted | verifier | verified | verify |
35
- | release | verified | verifier | paid | release |
36
- | refund | funded / running | requester | refunded | refund |
37
- | cancel | draft / quoted / accepted | requester | cancelled | none |
38
- | expire | draft / quoted / accepted | requester or worker | expired | expire |
39
- | dispute | submitted / verified | any of the three parties | disputed | none |
40
- | resolve | disputed | verifier | resolved | resolve |
41
-
42
- `resolved` records a completed resolution process; it does not mean paid or refunded. The resolution adapter must record its actual outcome in evidence. Refund eligibility, quote expiry, deadlines, spending limits, asset rules and verification semantics belong in application policies/adapters. There is no automatic timer. A scheduler calls the API using an authenticated participant or an explicitly configured scheduler role.
43
-
44
- ### Terms and acceptance
45
-
46
- Use JSON terms for your domain: task specification, price, asset, raw amount, deadline, wallet addresses, verifier policy and artifact expectations. Put every term you want committed into this object, including parties and settlement configuration where relevant. The SDK does not invent or validate those domain fields; `validateTerms` should throw on invalid terms. Use decimal strings for raw token amounts, not floating point or JSON bigint.
47
-
48
- The SDK hashes sorted-key canonical JSON with SHA-256 (array order preserved, no whitespace). Hash inputs must be plain JSON; avoid cyclic structures and sparse arrays. This is the SDK's own format, not a claim of RFC 8785 compatibility. Names and IDs are case-sensitive. A quote stores revision, terms, hash and acceptances. Accept requires both `agreement_hash` and `revision`; editing a quote clears acceptance. Terms cannot be revised after funding begins under the default rules. Hashes establish integrity, not caller identity or legal consent.
49
-
50
- ### Adapter contract and recovery
51
-
52
- Each effect adapter is either an async function or `{ execute, reconcile }`. It receives `{ job, input, operation }` and returns:
53
-
54
- ```js
55
- { status: 'pending', reference: 'provider-operation-reference' }
56
- { status: 'confirmed', evidence: { /* domain-specific verified observations */ } }
57
- { status: 'rejected', reason: 'definitive rejection' }
5
+ ```sh
6
+ npm install @allorc/a423-sdk@0.4.0 ethers@^6.15.0 pdf-lib@^1.17.1
58
7
  ```
59
8
 
60
- Confirmed results require an evidence object. The SDK trusts the adapter's verdict; it cannot independently establish payment, artifact quality or chain finality. An arbitrary reference is not payment proof. Chain adapters should compare configured chain/contract, job ID, amount, token, participants, deadline and commitments against the accepted terms, plus relevant successful transaction events and the chosen finality policy. `start` must recheck eligibility before work is dispatched.
9
+ Node.js 22.13+ is required for the SQLite module. Node 24 is the tested runtime. `ethers` is optional unless you import `/escrow`; `pdf-lib` is optional unless you render PDFs. Core/workflow remain framework-independent.
61
10
 
62
- The SDK atomically stores a pending operation before invoking an effect. Its `operation.id` is stable across retries: use it as the provider idempotency key. **Adapters must durably deduplicate effects, including concurrent calls.** Compare-and-set does not make an external payment exactly-once. If execution throws or returns an invalid result, pending remains; inspect/reconcile it. A pending result retains its observation. A rejected result clears pending without advancing state; repeat the same action key returns the stored job, so use a new key only after a definitive rejection and a deliberate new attempt.
11
+ ## Modules
63
12
 
64
- `reconcile(id, actor)` calls only the pending adapter's `reconcile`, never its execute method. That implementation should inspect known provider operations, not blindly repeat a transaction. Supply reconciliation for every external effect. Reuse the same adapter configuration and transitions across restarts. Reconciliation can record additional pending observations. No other action runs while an operation remains pending. If a final database save fails, recovery relies on the same persisted operation and provider key.
13
+ | Import | Implemented behaviour |
14
+ |---|---|
15
+ | `/workflow` | Versioned terms, bilateral acceptance, role guards, CAS state/events, pending operations, explicit observe/resume and safety holds |
16
+ | `/escrow` | Reference ERC-20 contract, full custody lifecycle, EOA/ERC-1271 worker consent, strict chain evidence, managed signing/delegation, allowlisted deployment registry, bounded fee replacements and incremental log indexing |
17
+ | `/storage` | Single-host SQLite workflow store, transaction/replacement journal, atomic nonce reservations, leased queue/checkpoints, spending reservations and authority/event storage |
18
+ | `/authority` | Operator-approved organization registry, organization-signed agent enrollment, single-use challenges, signed audience-bound bearer credentials, revocation, public-key verification and renewal |
19
+ | `/operations` | Typed task/evidence validation, spending authorizer, maintenance/keeper ticks, durable execution dispatcher, fresh receipts/PDF, reorg quarantine, SSE, webhook outbox, safe migration planning and app/chat integration handler |
20
+ | Root | Preserved original simple offer/accept/submit/approve API; [legacy documentation](./LEGACY.md) |
65
21
 
66
- The SDK does not run agents or background queues. Persist intent and schedule work in adapters; use a durable worker to submit actual output. Do not mark work submitted merely because a worker was queued. Custom transitions can add milestones, rejection/revision loops, manual review, nonpayment completion, or alternative arbitration. Override whole rules with `{ from, roles, to, effect }`; applications assume responsibility for the resulting invariants. Effect adapters must be trusted server code.
22
+ ## Start with the actual lifecycle
67
23
 
68
- ### Storage, permissions and receipts
24
+ Create a draft → propose immutable terms → both parties accept the exact revision/hash → worker signs consent → payer authorizes funding → contract holds tokens → worker passes fresh eligibility gate → deliver/commit artifact → verify → release. Expired unsubmitted jobs can refund; submitted disagreements can dispute/resolve; the agreed timeout allocation remains available if trusted roles disappear.
69
25
 
70
- A workflow store implements `get`, atomic insert-if-absent `create`, and atomic version-checked `compareAndSet`. Persist the entire record (state, pending operation, evidence, events, idempotency history) atomically. Use a separate table/namespace from legacy records. The root `createMemoryStore()` also works structurally at runtime for development, but is not durable or suitable for distributed deployment; a typed workflow store should implement the interface from the workflow entry point.
26
+ **Acceptance, token approval and confirmed custody are different states.** Only matching confirmed chain evidence establishes that money is locked. Neither a public identity hash nor a caller-supplied agent ID authenticates the caller.
71
27
 
72
- The handler requires authentication. It obtains actor identity from your callback, never from action bodies. Default role checks always run; `authorize` adds application policy and must return exactly true. Creation requires requester authentication, and requester must differ from worker. The verifier may be the requester or a separate participant. Authentication, wallet/key custody, identity-to-wallet binding and proof of ownership remain application responsibilities. `@allorc/identity` only derives public identifiers.
28
+ Read [ESCROW.md](./ESCROW.md) for exact fields, inputs, roles and contract limits. Read [OPERATIONS.md](./OPERATIONS.md) for configuration, authority enrollment, storage, worker scheduling, spending limits, fresh receipts, monitoring, UI/chat wiring and migration.
73
29
 
74
- Receipts are JSON snapshots of agreement, submission, stored adapter evidence, events and pending state. They are not freshly queried chain proofs or signed attestations. Render PDFs separately, and revalidate provider evidence if fresh verification is required. Protect credentials and never put secrets in terms, evidence, metadata or events: all participants can read these fields. Records/events grow over time; production storage should enforce application quotas. The HTTP client limits responses to 1 MiB.
75
-
76
- ### HTTP and client
77
-
78
- | Method | Path beneath `/api/a423` | Body |
79
- |---|---|---|
80
- | POST | `/jobs` | `{ id, participants, metadata? }` |
81
- | GET | `/jobs/:id` | — |
82
- | POST | `/jobs/:id/actions` | `{ action, input }` plus `Idempotency-Key` |
83
- | POST | `/jobs/:id/reconcile` | `{}` |
84
- | GET | `/jobs/:id/receipts` | — |
85
-
86
- The handler uses standard Web Request/Response, bounded streaming JSON (64 KiB default), explicit errors, and no background HTTP work. IDs and idempotency keys use 1–128 ASCII letters/digits/underscores/hyphens. Create with a stable application-assigned ID; duplicate creation returns conflict, then retrieve and compare the existing record. Every action needs an actor-scoped key; reusing it with different input fails. Transient conflicts should reload and retry using the same key. Authentication callbacks must inspect headers/cookies or a cloned request if they need the body.
30
+ ## Minimal server setup
87
31
 
88
32
  ```ts
89
- import { createWorkflowClient } from '@allorc/a423-sdk/workflow';
90
- const client = createWorkflowClient({
91
- baseUrl: 'https://your-app.example/api/a423',
92
- headers: () => ({ Authorization: `Bearer ${serverCredential}` }),
33
+ import { createSqliteStorage } from '@allorc/a423-sdk/storage';
34
+ import { createWorkflow } from '@allorc/a423-sdk/workflow';
35
+ import { createEscrowAdapters, escrowTransitions } from '@allorc/a423-sdk/escrow';
36
+ import { createSpendingAuthorizer, createApplicationHandler } from '@allorc/a423-sdk/operations';
37
+
38
+ const storage = createSqliteStorage({ filename: '/your-protected-local-disk/a423.sqlite' });
39
+ const authorizeTransaction = createSpendingAuthorizer({
40
+ spending: storage.spending,
41
+ limitsFor: loadApprovedRawTokenLimits,
42
+ authorize: checkSignerDelegationAndJobSafety,
93
43
  });
94
- const quote = await client.act(jobId, 'quote', { terms }, { idempotencyKey: 'quote_1' });
95
- await client.act(jobId, 'accept', {
96
- agreement_hash: quote.agreement!.hash,
97
- revision: quote.agreement!.revision,
98
- }, { idempotencyKey: 'accept_1' });
99
- ```
100
-
101
- Each participant needs its own credentials. The client does not automatically retry. Default timeout is 30 seconds; adapters should return pending quickly for long operations. Use TLS and appropriate CSRF protection for cookie-authenticated mutations.
102
-
103
- ## Original 0.1 API documentation
104
-
105
- # @allorc/a423-sdk
106
-
107
- One experimental package for exposing an agent job API from a Next.js App Router application. Configure a handler, export it from a catch-all route, and use the typed client to offer, accept, submit and approve work.
108
-
109
- **Status:** local experimental v0.1.0. The npm package has not been published by this checkout. Source repository: https://github.com/Allorc-official/A423-SDK. This implements an initial workflow, not a finalized A423 standard or a production escrow system. Node.js 22+. ESM, CommonJS and TypeScript declarations; zero runtime dependencies.
110
-
111
- ## Quick setup in Next.js
112
-
113
- Until publication, install the local package into your existing Next.js application:
114
-
115
- ```sh
116
- npm install /absolute/path/to/servers/server/a423_sdk
117
- ```
118
-
119
- After publication: `npm install @allorc/a423-sdk`.
120
-
121
- Create `lib/a423.ts`:
122
-
123
- ```ts
124
- import 'server-only';
125
- import { createA423Handler, createMemoryStore } from '@allorc/a423-sdk';
126
-
127
- // Development only. Replace with a durable StateStore before deployment.
128
- const store = createMemoryStore();
129
- const token = process.env.A423_API_TOKEN;
130
- const actorId = process.env.A423_AGENT_ID;
131
- if (!token || !actorId) throw new Error('Configure A423_API_TOKEN and A423_AGENT_ID');
132
-
133
- export const a423 = createA423Handler({
134
- store,
135
- authenticate(request) {
136
- return request.headers.get('authorization') === `Bearer ${token}`
137
- ? actorId
138
- : null;
139
- }
44
+ const adapters = createEscrowAdapters({
45
+ provider, chainId, contractAddress, tokenAddress, confirmations,
46
+ transactions: storage.transactions, signerFor, addressFor,
47
+ authorizeTransaction, validateArtifact,
140
48
  });
141
- ```
142
-
143
- This minimal authentication example maps one secret credential to one public actor ID. A complete workflow requires separate requester and worker credentials. The two-actor example is in `examples/nextjs/lib/a423.ts`; replace it with your own authenticated identity integration when connecting real agents.
144
-
145
- Create `app/api/a423/[...path]/route.ts`:
146
-
147
- ```ts
148
- import { a423 } from '@/lib/a423';
149
-
150
- export const runtime = 'nodejs';
151
- export const GET = a423;
152
- export const POST = a423;
153
- ```
154
-
155
- The `@/` import assumes your application's usual root alias; adjust it if your app uses `src/` or relative imports. This is one catch-all route, not one file per action. Do not run it in the Edge runtime. The SDK does not start a listener or publish routes on its own: Next.js exposes the route you configure.
156
-
157
- Keep credentials in server environment variables without `NEXT_PUBLIC_`. The package's client is designed for Node/server use, not for embedding worker credentials in client-side React. Your existing Next.js application supplies Next.js, React, TypeScript and `server-only`; the SDK itself does not import framework internals.
158
-
159
- ## Use the typed client
160
-
161
- ```ts
162
- import { createA423Client } from '@allorc/a423-sdk';
163
-
164
- const requester = createA423Client({
165
- baseUrl: 'https://your-app.example/api/a423',
166
- headers: { authorization: `Bearer ${requesterToken}` }
49
+ const workflow = createWorkflow({
50
+ store: storage.jobs, adapters, transitions: escrowTransitions,
51
+ validateTerms, authorize: authorizeWorkflowAction,
167
52
  });
168
- const worker = createA423Client({
169
- baseUrl: 'https://your-app.example/api/a423',
170
- headers: { authorization: `Bearer ${workerToken}` }
53
+ const handler = createApplicationHandler({
54
+ workflow, store: storage.jobs, queue: storage.queue,
55
+ authenticate, revalidate, consentFor,
171
56
  });
172
-
173
- const job = await requester.offer({
174
- worker_id: workerAgentId,
175
- title: 'Prepare a short brief',
176
- description: 'Research the topic and return a deliverable link.',
177
- price: { amount_minor: 500, currency: 'USD' }
178
- }, { idempotencyKey: 'brief-request-001' });
179
-
180
- await worker.accept(job.id);
181
- await worker.submit(job.id, { deliverable: 'https://example.com/brief.md' });
182
- await requester.approve(job.id);
183
- const receipts = await requester.receipts(job.id);
57
+ // Import this configured handler in the appropriate Next.js catch-all route:
58
+ export const runtime = 'nodejs';
59
+ export { handler as GET, handler as POST };
184
60
  ```
185
61
 
186
- `requesterToken`, `workerToken` and `workerAgentId` come from your application, not the SDK. The requester and worker are distinct authenticated actors. The example orchestrates both sides for demonstration; in a real integration they normally run independently.
187
-
188
- The price is an agreed term: 500 USD minor units means $5.00 when your adapter uses USD cents. Currency codes are uppercase strings of 3–12 letters/digits starting with a letter. The SDK does not calculate exchange rates, currency precision, fees or blockchain token decimals; define those semantics in your settlement integration. `amount_minor` must be a nonnegative safe integer. Zero-price jobs are allowed.
189
-
190
- A deliverable is an opaque string, such as a URL, repository reference or storage path. The SDK stores it and never downloads or executes it. It does not perform the work, evaluate quality or run an agent automatically.
62
+ Names such as `loadApprovedRawTokenLimits`, `provider`, `signerFor`, `validateArtifact`, `authenticate`, `revalidate` and `consentFor` are application services, not invented SDK exports. Complete recipes and actual exports are in OPERATIONS.md and TypeScript declarations. SQLite files must live on persistent local disk; use a different store for serverless/multi-host deployment.
191
63
 
192
- Client methods return `Job`, except `receipts`, which returns `Receipts`. `headers` may be an async function for fresh credentials. `fetch` can be injected for tests or custom networking. Requests default to a 30-second timeout, refuse redirects and do not retry automatically.
64
+ ## Integration surfaces
193
65
 
194
- ## HTTP API
66
+ `createApplicationHandler` serves authenticated workflow routes under `/api/a423`, projections under `/api/state`, resumable events at `/api/a423-events`, wallet typed-data at `/api/a423-worker-consent/:id`, and freshly revalidated JSON/PDF at `/api/a423-receipts/:id[.pdf]`. An optional `legacyState(actor)` callback preserves legacy dashboard data alongside `a423.jobs` and `a423.wallet_transactions`.
195
67
 
196
- Every request requires successful `authenticate(request)`. POSTs use `Content-Type: application/json`.
68
+ `createChatTools` maps offer, quote, accept, fund, start, submit, verify, release, refund, dispute, resolve and timeout tools to the same workflow records. Work is dispatched through the durable queue; it is never marked submitted because an AI run was merely scheduled. The HTML application shell includes wallet consent, action controls, states, parties, evidence and receipts.
197
69
 
198
- | Method | Route | Body | Who can call |
199
- | --- | --- | --- | --- |
200
- | POST | `/api/a423/offers` | `{worker_id,title,description,price}` and `Idempotency-Key` header | Authenticated requester |
201
- | GET | `/api/a423/jobs/:id` | None | Requester or assigned worker |
202
- | POST | `/api/a423/jobs/:id/accept` | `{}` | Assigned worker |
203
- | POST | `/api/a423/jobs/:id/submit` | `{deliverable}` | Assigned worker |
204
- | POST | `/api/a423/jobs/:id/approve` | `{}` | Requester |
205
- | GET | `/api/a423/jobs/:id/receipts` | None | Requester or assigned worker |
206
-
207
- Offer creation returns HTTP 201 with `{job}`; an identical retry returns 200 with the existing job. Other actions return `{job}`. Receipts return `{job_id,state,settlement,events}`. Responses use `Cache-Control: no-store`. Unknown jobs and jobs belonging to other actors both return 404.
208
-
209
- External agents can use plain HTTP without installing the SDK. For example:
70
+ Run the supplied shell and operational worker with an explicitly configured application module:
210
71
 
211
72
  ```sh
212
- curl https://your-app.example/api/a423/offers \
213
- -H "Authorization: Bearer $A423_REQUESTER_TOKEN" \
214
- -H 'Content-Type: application/json' \
215
- -H 'Idempotency-Key: brief-request-001' \
216
- -d '{"worker_id":"worker-agent-id","title":"Brief","description":"Prepare a short brief","price":{"amount_minor":500,"currency":"USD"}}'
217
- ```
218
-
219
- Adjust `basePath` if you mount at a different URL. This initial implementation has no list/search, cancellation, disputes, amendments, webhooks, discovery endpoint, background scheduler or UI. Poll known job IDs or coordinate workers through your own queue. Authentication is mandatory; signatures and message-level cryptographic verification are not built in.
220
-
221
- ## Workflow and retry behaviour
222
-
223
- A job follows `offered → accepted → submitted → completed`. With a settlement adapter it follows `submitted → settlement_pending → completed` during approval.
224
-
225
- The server checks the actor and current state before each change. Every successful change increments the job version and appends an event. Submission is immutable: retrying with the same deliverable succeeds, but replacing it is rejected. Repeating acceptance after acceptance or approval after completion returns the current job without adding a duplicate event.
226
-
227
- Offer IDs are deterministic SHA-256 values scoped by protocol version, authenticated requester ID and `Idempotency-Key`. Reuse the same key and exact logical offer when retrying after a network failure. Reusing it with different offer details returns 409. Store the key on your side; a freshly generated key means a new offer. Keys are visible ASCII without spaces, at most 200 characters, and should not contain secrets.
228
-
229
- Storage changes use compare-and-set. Concurrent writes can return 409 `CONFLICT`; reload the job and retry the intended action. The SDK does not keep an HTTP request open for the duration of a task. Worker execution and scheduling belong in your application's background jobs.
230
-
231
- ## Storage adapter
232
-
233
- `store` is required. `createMemoryStore()` is only for local development/tests: it loses data on restart and is not shared across serverless instances or processes. There is no silent in-memory default.
234
-
235
- Implement the `StateStore` interface against your database:
236
-
237
- ```ts
238
- interface StateStore {
239
- get(id: string): Promise<Job | null>;
240
- create(job: Job): Promise<boolean>;
241
- compareAndSet(id: string, expectedVersion: number, job: Job): Promise<boolean>;
242
- }
243
- ```
244
-
245
- `create` must atomically insert only if the ID is absent; enforce a unique ID constraint. `compareAndSet` must atomically update only when both ID and version match, then report whether one row changed. Store job fields and events in the same transaction. A read followed by an unconditional update is not sufficient. Return independent snapshots so caller mutations do not change stored state.
246
-
247
- For example, a relational adapter can use an ID primary key, a version column and a JSON document. Create with insert-on-conflict-do-nothing; update using `WHERE id = ? AND version = ?`. This is a contract, not a shipped SQL adapter. Durable storage must also support pending settlement recovery.
248
-
249
- ## Identity and authentication
250
-
251
- `authenticate(request)` is the integration point for your identity system. It returns an authenticated actor's public agent ID or `null` to reject. It may verify a bearer credential, a session, or an externally signed request. The returned ID determines permissions; an untrusted ID in a request body/header does not.
252
-
253
- `@allorc/identity` 0.3.0 can form a public agent ID:
254
-
255
- ```ts
256
- import { VERSION, createIdentity } from '@allorc/identity';
257
-
258
- const identity = createIdentity({
259
- version: VERSION,
260
- organization_public_key: 'externally-supplied-public-value',
261
- agent_reference: 'worker-42',
262
- agent_name: 'Alice',
263
- agent_email: 'alice@example.com'
264
- });
265
- // Register identity.agent_id with your credential/authority system separately.
73
+ A423_CONFIG_MODULE=/absolute/path/config.mjs node examples/operations-server.mjs
74
+ A423_CONFIG_MODULE=/absolute/path/config.mjs node scripts/operations-worker.mjs
266
75
  ```
267
76
 
268
- Install that optional package separately when needed. A hash match proves record consistency, not ownership or authentication. This SDK does not claim to verify the organisation behind a public key, and does not depend on the identity package to authenticate actors. Changes to that package's public ingredients change its derived agent ID; your identity integration must manage those updates.
77
+ The shell binds to localhost. There is no bootstrap endpoint, default credential, built-in private key or automatically provisioned arbiter. Supply distinct signer references for treasury, worker, verifier and arbitrator according to your policy.
269
78
 
270
- For cookie-based authentication, your integration must also enforce its CSRF/origin policy. The SDK does not configure sessions, CORS, network access, rate limits or tenant onboarding for you. The supplied example uses server-issued bearer tokens with fixed actor bindings.
79
+ ## Upgrade from 0.3
271
80
 
272
- ## Optional settlement adapter
81
+ - `authorizeTransaction` is now mandatory for escrow adapters. Explicitly configure spending/delegation checks; a blanket `true` is appropriate only for controlled tests.
82
+ - The transaction-store interface adds `list` and atomic `appendReplacement`. The supplied SQLite and memory implementations include them.
83
+ - Use a new deployment for ERC-1271 support. Existing 0.3 contracts/locked jobs stay on their original ABI/contract; an npm upgrade does not upgrade a deployed contract.
84
+ - `workflow.resume(id, actor)` explicitly authorizes the next pending step. `reconcile` remains observe-only. `runMaintenanceOnce` only resumes when `allowExecute: true`.
85
+ - Reference evidence now includes settlement reason/dispute commitment. Existing incomplete evidence needs explicit refresh/migration; do not fabricate fields or relabel old financial states.
86
+ - `getSafety`/`setSafety` enable quarantine. Monitor evidence continuously and require fresh eligibility immediately before executing real work.
273
87
 
274
- Without an adapter, approval marks the job completed with:
88
+ ## Boundaries
275
89
 
276
- ```json
277
- {"status":"not_configured","reference":null}
278
- ```
90
+ This remains experimental and independently unaudited. Verifiers/arbitrators remain trusted; there is no objective quality oracle or guaranteed challenge window. The reference contract retains one token, one immutable submission and one settlement per job. It does not add partial milestone releases, resubmission, admin withdrawals, pause/upgrades or fee-on-transfer/rebasing support. Separate deployments are selected through the registry. A timeout needs a transaction and a functioning chain/token.
279
91
 
280
- **This records task approval and moves no money.** A configured adapter is called only after requester approval and after the job is persisted as `settlement_pending`:
92
+ KMS/HSM providers are integrated through a checked digest signer; the package does not provision vendor accounts or certify hardware. Authority approval establishes trust within your configured registry, not government recognition or automatic legal-company verification. AI execution uses your explicitly supplied handler, never an implicit model subscription. Reorg handling quarantines invalid observations; it does not silently rewrite accepted agreements or erase paid history. Fee replacement uses explicit caps and the same intent/nonce; it cannot force a censored transaction to execute.
281
93
 
282
- ```ts
283
- const handler = createA423Handler({
284
- store,
285
- authenticate,
286
- settlement: {
287
- async settle({ job, idempotencyKey }) {
288
- // Invoke your own escrow/payment service with this stable key.
289
- // Persistently deduplicate that key, including concurrent invocations.
290
- const result = await yourSettlementService.release({
291
- jobId: job.id,
292
- requesterId: job.requester_id,
293
- workerId: job.worker_id,
294
- price: job.price,
295
- idempotencyKey
296
- });
297
- return { reference: result.receiptId };
298
- }
299
- }
300
- });
301
- ```
302
-
303
- `yourSettlementService` is your external integration, not an included export. The SDK passes `a423:settle:<job-id>` as a stable provider idempotency key. Your adapter MUST durably deduplicate it and return the same result on retries, including simultaneous calls from multiple server instances. The SDK cannot guarantee exactly-once movement of funds across an external provider and a database transaction.
304
-
305
- On a provider error the server returns 502 and leaves the job pending; retry `approve` as the requester. If the provider succeeds but the database write fails, retrying approval uses the same key. If you run a recovery worker, route it through equivalent actor and idempotency rules. No automatic retry queue is included.
306
-
307
- This is not funded escrow: the SDK does not reserve funds at offer/acceptance, validate wallets, sign financial authorisations, collect mandates or guarantee payment. Your adapter must enforce those rules before moving funds. `settled` records the adapter's reported success, not an independently verified blockchain confirmation.
308
-
309
- ## Errors and limits
310
-
311
- Errors are `{error:{code,message}}`. The client throws `A423Error` with `code` and `status` for HTTP failures. Network/timeout errors propagate from fetch. Internal exception details stay server-side; optional `onError(error)` lets your application record them.
312
-
313
- | HTTP status | Typical code |
314
- | --- | --- |
315
- | 400 | `INVALID_INPUT`, `INVALID_JSON` |
316
- | 401 | `UNAUTHENTICATED` |
317
- | 403 | `FORBIDDEN` |
318
- | 404 | `NOT_FOUND` |
319
- | 405 | `METHOD_NOT_ALLOWED` |
320
- | 409 | `INVALID_STATE`, `CONFLICT`, `IDEMPOTENCY_CONFLICT` |
321
- | 413 | `BODY_TOO_LARGE` |
322
- | 415 | `UNSUPPORTED_MEDIA_TYPE` |
323
- | 500 | `INTERNAL_ERROR` |
324
- | 502 | `SETTLEMENT_UNAVAILABLE` |
325
-
326
- Request bodies default to 64 KiB and are limited while streaming, even without `Content-Length`; `maxBodyBytes` can set 1 byte through 1 MiB. Description limit: 16,000 characters; deliverable: 8,000; actor IDs and titles: 256. Client response bodies are limited to 1 MiB. Bound requests at your ingress too. There is no multipart/file upload or arbitrary server-side URL fetching.
327
-
328
- ## Package exports
329
-
330
- | Export | Purpose |
331
- | --- | --- |
332
- | `createA423Handler(options)` | Standard Web Request/Response handler usable in Next.js routes |
333
- | `createA423Client(options)` | Typed HTTP client |
334
- | `createMemoryStore()` | Explicit local development store |
335
- | `A423Error` | Structured client/protocol error |
336
- | `PROTOCOL_VERSION` | `a423.experimental.v1` |
337
-
338
- ESM and CommonJS expose the same names. Interfaces for `Job`, `StateStore`, `SettlementAdapter` and configuration are in `src/index.d.ts`. The handler is framework-independent at runtime, but the included integration targets Next.js App Router. Installing this SDK does not publish an API or establish protocol compatibility with other A423 implementations.
339
-
340
- ## Development
94
+ ## Validation and release
341
95
 
342
96
  ```sh
97
+ npm ci --ignore-scripts
98
+ npm run typecheck
343
99
  npm test
344
- npm run pack:check
100
+ npm pack --dry-run --ignore-scripts
345
101
  ```
346
102
 
347
- Tests exercise the complete workflow through actual Web Request/Response objects, permissions, malformed input, streaming limits, concurrent updates, retry semantics and settlement recovery. An additional socket-based test runs where local listeners are permitted, and explicitly skips when the execution environment blocks them. Next.js example files are templates for an existing application; they are not a standalone generated Next.js app.
348
-
349
- No runtime dependencies or build step. Before a public release, review the package allowlist, configure the repository/private security reporting channel, and verify a real Next.js deployment and durable adapter. Publication is a separate maintainer action; this package has not been published by creating these files.
103
+ Tests compile and deploy the contract on a local EVM, exercise payment/refund/dispute/timeout, check consent and permissions, and cover credential-protected HTTP → durable SQLite → escrow → fresh PDF evidence. Tests also cover storage leases, spend caps, renewal/revocation, queue dispatch, SSE/webhooks, fee replacement and migration restrictions. External testnet deployment, source-verification completion and an independent audit are separate operations; no result is inferred from a hook or submitted verification ticket.
package/SECURITY.md CHANGED
@@ -1,9 +1,15 @@
1
- # Security policy
1
+ # Security
2
2
 
3
- Experimental SDK; no independent audit or stable support commitment. The built-in memory store and fixed bearer-token example are for development, not a production deployment.
3
+ Experimental SDK and independently unaudited reference escrow. Use local/testnet evaluation until the deployment and operational model receive independent review.
4
4
 
5
- Authentication must be supplied. Never treat a public ID, public ingredients or a matching hash as evidence that a request belongs to an agent. Verify credentials/signatures using your own identity adapter; enforce session/CSRF policy when using cookies. Configure TLS, ingress limits, rate limits, CORS and durable atomic storage in your host application.
5
+ Reference-contract custody is distinct from the legacy root API, which has no funded escrow. Authority credentials identify keys approved by your operator registry; they are not proof of legal-company status. Administrative enrollment/revocation capabilities must not be exposed to untrusted callers. Use explicit trust roots, TLS, short credential lifetimes, revocation checks and rate-limited proof endpoints.
6
6
 
7
- Settlement adapters must durably deduplicate the SDK's stable key across retries and concurrent calls. This SDK does not fund escrow, establish mandates, custody keys or guarantee exactly-once payments. Validate recipient, amount, permission and provider status independently. Completion without an adapter explicitly means no settlement is configured.
7
+ Keep private keys, raw signed transaction journals, SQLite files and webhook signing secrets private. The supplied SQLite driver is single-host and requires persistent local disk, restricted filesystem access and backups. Do not use memory stores for production custody, share nonces across uncoordinated writers, or reuse an arbitrator key for treasury/bootstrap. Use the checked managed signer and bounded delegation or your reviewed signing service.
8
8
 
9
- Do not include tokens, private keys, customer data or live exploit details in public reports. Use a private repository reporting channel when configured; maintainers should establish one before requesting sensitive reports. Never log credentials. No private security contact is invented by this initial package.
9
+ Authorize before signing and rebroadcasting, enforce raw-token spend limits atomically, and preserve unknown outcomes for reconciliation. Queue handlers and webhook receivers must be idempotent; leases do not imply exactly-once external effects. Only configure webhook destinations you control/approve; redirects are disabled, but DNS and egress controls remain deployment responsibilities.
10
+
11
+ The reference token must support exact transfers. Unsupported fee/rebase/blacklist behaviours can prevent settlement. EOA/ERC-1271 consent is supported by the new contract; verifiers/arbitrators still decide quality and may have conflicts of interest. Timeout allocation must be explicitly agreed. Contract upgrades, admin recovery, challenge windows and partial releases are not introduced by this version.
12
+
13
+ Reorg watchers quarantine stale/unavailable evidence; they cannot guarantee finality or undo already executed external work. Continuous operation, monitoring, deliberate recovery and chain availability are still necessary. Do not clear holds without current matching evidence.
14
+
15
+ Report vulnerabilities privately through the repository's configured security reporting channel. Do not post credentials, customer records or live exploit details publicly. No completed independent audit is claimed.
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.24;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
7
+ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
8
+ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
9
+ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
10
+
11
+ /// @notice Experimental reference escrow for standard, non-rebasing ERC20 tokens.
12
+ /// No admin withdrawals/upgrades. Arbitration and timeout allocation are agreed before funding.
13
+ contract A423Escrow is ReentrancyGuard, EIP712 {
14
+ using SafeERC20 for IERC20;
15
+ enum State { None, Funded, Submitted, Verified, Disputed, Paid, Refunded, Split }
16
+ struct Terms {
17
+ bytes32 jobId;
18
+ address payer;
19
+ address worker;
20
+ address verifier;
21
+ address arbitrator;
22
+ uint256 amount;
23
+ uint64 workDeadline;
24
+ uint64 reviewDeadline;
25
+ uint64 resolutionDeadline;
26
+ uint16 timeoutWorkerBps;
27
+ bytes32 agreementHash;
28
+ }
29
+ struct Job { Terms terms; State state; bytes32 resultHash; uint256 workerPaid; }
30
+ IERC20 public immutable token;
31
+ mapping(bytes32 => Job) private jobs;
32
+ bytes32 public constant TERMS_TYPEHASH = keccak256("Terms(bytes32 jobId,address payer,address worker,address verifier,address arbitrator,uint256 amount,uint64 workDeadline,uint64 reviewDeadline,uint64 resolutionDeadline,uint16 timeoutWorkerBps,bytes32 agreementHash)");
33
+ event Funded(bytes32 indexed jobId, bytes32 indexed agreementHash, address indexed payer, address worker, uint256 amount);
34
+ event Submitted(bytes32 indexed jobId, bytes32 resultHash);
35
+ event Verified(bytes32 indexed jobId, bytes32 resultHash);
36
+ event Disputed(bytes32 indexed jobId, bytes32 reasonHash);
37
+ event Settled(bytes32 indexed jobId, uint256 workerAmount, uint256 payerAmount, uint8 reason);
38
+ error InvalidTerms();
39
+ error WrongState();
40
+ error Unauthorized();
41
+ error Deadline();
42
+ error TokenAmountMismatch();
43
+
44
+ constructor(address token_) EIP712("A423Escrow", "1") {
45
+ if (token_.code.length == 0) revert InvalidTerms();
46
+ token = IERC20(token_);
47
+ }
48
+ function termsDigest(Terms calldata t) public view returns (bytes32) {
49
+ return _hashTypedDataV4(keccak256(abi.encode(TERMS_TYPEHASH, t)));
50
+ }
51
+ function getJob(bytes32 id) external view returns (Job memory) { return jobs[id]; }
52
+ /// Payer transaction plus worker EIP712 signature establish bilateral on-chain consent.
53
+ function fund(Terms calldata t, bytes calldata workerSignature) external nonReentrant {
54
+ if (msg.sender != t.payer) revert Unauthorized();
55
+ if (jobs[t.jobId].state != State.None) revert WrongState();
56
+ if (t.jobId == bytes32(0) || t.agreementHash == bytes32(0) || t.amount == 0 || t.worker == address(0) || t.worker == t.payer || t.verifier == address(0) || t.arbitrator == address(0) || t.arbitrator == t.payer || t.arbitrator == t.worker || t.timeoutWorkerBps > 10000) revert InvalidTerms();
57
+ if (t.workDeadline <= block.timestamp || t.reviewDeadline <= t.workDeadline || t.resolutionDeadline <= t.reviewDeadline) revert InvalidTerms();
58
+ bool valid;
59
+ if (t.worker.code.length == 0) { valid = ECDSA.recover(termsDigest(t), workerSignature) == t.worker; }
60
+ else { try IERC1271(t.worker).isValidSignature(termsDigest(t), workerSignature) returns (bytes4 magic) { valid = magic == IERC1271.isValidSignature.selector; } catch { valid = false; } }
61
+ if (!valid) revert Unauthorized();
62
+ jobs[t.jobId] = Job(t, State.Funded, bytes32(0), 0);
63
+ uint256 beforeBalance = token.balanceOf(address(this));
64
+ token.safeTransferFrom(t.payer, address(this), t.amount);
65
+ if (token.balanceOf(address(this)) != beforeBalance + t.amount) revert TokenAmountMismatch();
66
+ emit Funded(t.jobId, t.agreementHash, t.payer, t.worker, t.amount);
67
+ }
68
+ function submit(bytes32 id, bytes32 resultHash) external {
69
+ Job storage j = jobs[id];
70
+ if (msg.sender != j.terms.worker) revert Unauthorized();
71
+ if (j.state != State.Funded) revert WrongState();
72
+ if (block.timestamp > j.terms.workDeadline) revert Deadline();
73
+ if (resultHash == bytes32(0)) revert InvalidTerms();
74
+ j.resultHash = resultHash; j.state = State.Submitted;
75
+ emit Submitted(id, resultHash);
76
+ }
77
+ function verify(bytes32 id, bytes32 resultHash) external {
78
+ Job storage j = jobs[id];
79
+ if (msg.sender != j.terms.verifier) revert Unauthorized();
80
+ if (j.state != State.Submitted) revert WrongState();
81
+ if (block.timestamp > j.terms.reviewDeadline) revert Deadline();
82
+ if (resultHash != j.resultHash) revert InvalidTerms();
83
+ j.state = State.Verified; emit Verified(id, resultHash);
84
+ }
85
+ function release(bytes32 id) external nonReentrant {
86
+ Job storage j = jobs[id];
87
+ if (msg.sender != j.terms.verifier) revert Unauthorized();
88
+ if (j.state != State.Verified) revert WrongState();
89
+ if (block.timestamp > j.terms.resolutionDeadline) revert Deadline();
90
+ _settle(id, j, j.terms.amount, 0);
91
+ }
92
+ function refundExpired(bytes32 id) external nonReentrant {
93
+ Job storage j = jobs[id];
94
+ if (j.state != State.Funded) revert WrongState();
95
+ if (block.timestamp <= j.terms.workDeadline) revert Deadline();
96
+ _settle(id, j, 0, 1);
97
+ }
98
+ function dispute(bytes32 id, bytes32 reasonHash) external {
99
+ Job storage j = jobs[id];
100
+ if (msg.sender != j.terms.payer && msg.sender != j.terms.worker && msg.sender != j.terms.verifier) revert Unauthorized();
101
+ if (j.state != State.Submitted && j.state != State.Verified) revert WrongState();
102
+ if (block.timestamp > j.terms.resolutionDeadline) revert Deadline();
103
+ j.state = State.Disputed; emit Disputed(id, reasonHash);
104
+ }
105
+ function resolve(bytes32 id, uint256 workerAmount) external nonReentrant {
106
+ Job storage j = jobs[id];
107
+ if (msg.sender != j.terms.arbitrator) revert Unauthorized();
108
+ if (j.state != State.Disputed) revert WrongState();
109
+ if (block.timestamp > j.terms.resolutionDeadline || workerAmount > j.terms.amount) revert InvalidTerms();
110
+ _settle(id, j, workerAmount, 2);
111
+ }
112
+ /// Permissionless liveness fallback; requires an actual transaction after the deadline.
113
+ function timeout(bytes32 id) external nonReentrant {
114
+ Job storage j = jobs[id];
115
+ if (j.state != State.Submitted && j.state != State.Verified && j.state != State.Disputed) revert WrongState();
116
+ if (block.timestamp <= j.terms.resolutionDeadline) revert Deadline();
117
+ // Quotient/remainder avoids overflowing amount * bps.
118
+ uint256 amount = (j.terms.amount / 10000) * j.terms.timeoutWorkerBps + ((j.terms.amount % 10000) * j.terms.timeoutWorkerBps) / 10000;
119
+ _settle(id, j, amount, 3);
120
+ }
121
+ function _settle(bytes32 id, Job storage j, uint256 workerAmount, uint8 reason) private {
122
+ uint256 payerAmount = j.terms.amount - workerAmount;
123
+ j.state = workerAmount == 0 ? State.Refunded : payerAmount == 0 ? State.Paid : State.Split;
124
+ j.workerPaid = workerAmount;
125
+ if (workerAmount > 0) _transferExact(j.terms.worker, workerAmount);
126
+ if (payerAmount > 0) _transferExact(j.terms.payer, payerAmount);
127
+ emit Settled(id, workerAmount, payerAmount, reason);
128
+ }
129
+ function _transferExact(address to, uint256 amount) private {
130
+ uint256 beforeBalance = token.balanceOf(address(this));
131
+ uint256 beforeRecipient = token.balanceOf(to);
132
+ token.safeTransfer(to, amount);
133
+ if (token.balanceOf(address(this)) != beforeBalance - amount || token.balanceOf(to) != beforeRecipient + amount) revert TokenAmountMismatch();
134
+ }
135
+ }