@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/CHANGELOG.md +18 -0
- package/ESCROW.md +162 -0
- package/INTEGRATION.md +14 -2
- package/LEGACY.md +251 -0
- package/NOTICE +24 -0
- package/OPERATIONS.md +182 -0
- package/README.md +65 -311
- package/SECURITY.md +11 -5
- package/contracts/A423Escrow.sol +135 -0
- package/contracts/standard-input.json +80 -0
- package/examples/operations-dashboard.html +16 -0
- package/examples/operations-server.mjs +19 -0
- package/package.json +67 -5
- package/scripts/deploy-escrow.mjs +24 -0
- package/scripts/operations-worker.mjs +22 -0
- package/src/authority/index.cjs +81 -0
- package/src/authority/index.d.ts +24 -0
- package/src/authority/index.mjs +2 -0
- package/src/escrow/artifact.json +661 -0
- package/src/escrow/index.cjs +150 -0
- package/src/escrow/index.d.mts +48 -0
- package/src/escrow/index.d.ts +48 -0
- package/src/escrow/index.mjs +2 -0
- package/src/escrow/services.cjs +119 -0
- package/src/operations/application.cjs +38 -0
- package/src/operations/factory.cjs +33 -0
- package/src/operations/index.cjs +105 -0
- package/src/operations/index.d.ts +29 -0
- package/src/operations/index.mjs +2 -0
- package/src/storage/index.cjs +83 -0
- package/src/storage/index.d.ts +37 -0
- package/src/storage/index.mjs +2 -0
- package/src/workflow.cjs +14 -2
- package/src/workflow.d.ts +3 -0
package/README.md
CHANGED
|
@@ -1,349 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# A423 SDK — 0.4.0
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
## Modules
|
|
63
12
|
|
|
64
|
-
|
|
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
|
-
|
|
22
|
+
## Start with the actual lifecycle
|
|
67
23
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
53
|
+
const handler = createApplicationHandler({
|
|
54
|
+
workflow, store: storage.jobs, queue: storage.queue,
|
|
55
|
+
authenticate, revalidate, consentFor,
|
|
171
56
|
});
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
64
|
+
## Integration surfaces
|
|
193
65
|
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
## Upgrade from 0.3
|
|
271
80
|
|
|
272
|
-
|
|
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
|
-
|
|
88
|
+
## Boundaries
|
|
275
89
|
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
100
|
+
npm pack --dry-run --ignore-scripts
|
|
345
101
|
```
|
|
346
102
|
|
|
347
|
-
Tests
|
|
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
|
|
1
|
+
# Security
|
|
2
2
|
|
|
3
|
-
Experimental SDK
|
|
3
|
+
Experimental SDK and independently unaudited reference escrow. Use local/testnet evaluation until the deployment and operational model receive independent review.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|