agent-work-layer 0.4.0 → 0.5.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 CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-10-17
4
+
5
+ Both packages: `agent-work-layer` 0.5.0 and `agent-work-layer-mcp` 0.5.0. Spec v0.3: Work Orders are now written as `awl/work-order@0.3`; Work Reports stay `@0.2`.
6
+
7
+ ### Added
8
+
9
+ - `continues` on the Work Order (#19), SPEC.md §4.5: the id of the finished order a new one picks up from, so a chain of reissued orders is the full history of one job.
10
+ - It is not `parent_id`: no narrowing applies, and the new order may have a different worker, budget and tools.
11
+ - The desk rejects a `continues` that names an unknown order (`NOT_FOUND`), one that is not `approved`, `declined`, `expired` or `cancelled` (`INVALID_TRANSITION`; cancel a failed or blocked order first), another manager's order (`FORBIDDEN`), or one that is already continued (`ALREADY_EXISTS`).
12
+ - `desk.list({ continues })` and `list_work_orders` follow the chain; summaries include `continues`. `create_work_order` takes it.
13
+ - Delegation Pass (#17), SPEC.md §10.2: an order's authority as a signed JWT (`typ` `awl-pass+jwt`, ES256 or EdDSA) with `iss`, `sub`, `awl_order`, `awl_parent`, `awl_tools`, `awl_budget`, `awl_deadline`, `awl_attempt`, `iat`, `exp` and `jti`.
14
+ - `agent-work-layer`: `signPass(order, key, options)` and `verifyPass(token, keys, options)`, with WebCrypto only, so they are also in `agent-work-layer/validate`. `verifyPass` checks signature, type, lifetime (allowing 60 seconds of clock skew on `iat`) and, when asked, issuer, worker and order; with `parent` it rejects a pass that is wider than its parent's (C1–C3) or names another parent. Keys are JWKs or WebCrypto keys from the caller. `decodePass` reads claims without checking them.
15
+ - `WorkDesk({ passes: { key, issuer } })` signs a pass (15 minutes, never past the deadline) each time an order enters `working`, keeps it on `record.pass`, and drops it when the order leaves `working`. `desk.issuePass(id, worker)` renews it.
16
+ - `agent-work-layer-mcp`: `awl-desk serve --pass-key <file> --issuer <iss>`. `start_work_order` and `get_work_order` return the pass to the order's worker only, and `renew_work_pass` renews it.
17
+ - Gateway: `--pass-keys <file>` (`passKeys` in code) enforces a pass instead of reading the desk's store. Each call needs a valid pass for the order and worker, from `--pass-file`, `AWL_PASS` or the call's `_meta["awl/pass"]`, and is refused with `PASS_INVALID` otherwise. Tools and `tool_calls` come from the pass.
18
+ - Gateway call records and result `_meta` carry the authority each call ran under: `order_id`, `attempt` and the pass's `jti`.
19
+ - Authenticated actor (#16): `awl-desk serve --keys <file>` runs one shared desk over MCP Streamable HTTP. Every request needs `Authorization: Bearer <key>`; the key decides the actor, and an `actor` argument naming anyone else gets `FORBIDDEN`. Requests without a valid key get 401. In code: `createDeskHttpHandler({ desk, authenticate })`, where `authenticate(request)` returns the agent ref or throws an `AuthError` (401, or 403). Two example authenticators: `apiKeyAuth` (a static key → agent map) and `bearerAuth` (OAuth 2.1 access tokens: checks `exp` and `aud`, maps `sub` to an agent ref, advertises `resource_metadata` in 401 challenges). The handler can serve Protected Resource Metadata (RFC 9728). Stdio mode is unchanged.
20
+ - SPEC.md §10.1: what binding `actor` to an authenticated identity means, and how an MCP desk does it.
21
+
22
+ ### Changed
23
+
24
+ - `agent-work-layer` writes Work Orders as `awl/work-order@0.3` and reads `@0.1`–`@0.3`. Readers on 0.4 or earlier reject `@0.3` orders; upgrade readers first. Reports are unchanged.
25
+ - `agent-work-layer-mcp` now depends on `agent-work-layer` ^0.5.0.
26
+
27
+ ### Fixed
28
+
29
+ - `agent-work-layer`: `FileStore` runs writes to one order one at a time within a process, so concurrent transitions get `CONFLICT` instead of overwriting each other's state and events, and temporary files get unique names. Across processes the check stays best-effort; use `SqliteStore` for an atomic one.
30
+
31
+ ## python 0.2.0 — 2026-10-17
32
+
33
+ ### Added
34
+
35
+ - Spec v0.3: `WorkOrder.continues`, and the validator reads Work Orders marked `@0.1`–`@0.3`. New orders are written as `@0.3`.
36
+
37
+ ## 0.4.1 — 2026-10-14
38
+
39
+ `agent-work-layer-mcp` alone (`agent-work-layer` stays at 0.4.0).
40
+
41
+ ### Added
42
+
43
+ - Every desk tool and the gateway's `awl_usage` tool declare `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint`, so hosts can warn before a tool runs and directories that require the hints accept the server.
44
+
45
+ ## python 0.1.0 — 2026-10-14
46
+
47
+ First release of the Python package `agent-work-layer` on PyPI (`pip install agent-work-layer`).
48
+
49
+ ### Added
50
+
51
+ - Pydantic models for Work Orders and Work Reports, and `validate_order`, `validate_report` and `validate_child_order` with the same rule ids, kinds and paths as the TypeScript SDK (#5).
52
+
3
53
  ## 0.4.0 — 2026-10-11
4
54
 
5
55
  Both packages: `agent-work-layer` 0.4.0 and `agent-work-layer-mcp` 0.4.0.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/agent-work-layer.svg)](https://www.npmjs.com/package/agent-work-layer)
6
6
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
7
+ [![M8ven Score](https://m8ven.ai/badge/mcp/probita-ai-agent-work-layer-0vyqyi?v=1b5d39882e3a71ebeecc28d9d2f02ec3)](https://m8ven.ai/mcp/probita-ai-agent-work-layer-0vyqyi?s=readme)
7
8
 
8
9
  - **Work Order** (manager → worker): the goal, how "done" is judged, budget, deadline, allowed tools.
9
10
  - **Work Report** (worker → manager): the result, evidence for each criterion, actual cost, tools used.
@@ -21,7 +22,7 @@ Agent Cards say who an agent is. A2A carries messages between agents. MCP connec
21
22
 
22
23
  <p align="center"><img src="docs/fields.svg" alt="Each Work Report field answers a Work Order field: evidence proves criteria, cost stays within budget, tools used stay within allowed tools" width="760"></p>
23
24
 
24
- More diagrams: [message sequence](docs/sequence.svg) · [lifecycle](docs/lifecycle.svg) · [delegation](docs/delegation.svg) · [handoff](docs/handoff.svg)
25
+ More diagrams: [message sequence](docs/sequence.svg) · [lifecycle](docs/lifecycle.svg) · [delegation](docs/delegation.svg) · [handoff](docs/handoff.svg) · [authenticated actor](docs/authenticated-actor.svg) · [Delegation Pass](docs/delegation-pass.svg)
25
26
 
26
27
  ## Install
27
28
 
@@ -31,9 +32,11 @@ Install only the part you use:
31
32
  |---|---|---|
32
33
  | Validate orders and reports, or run a desk in your code | `npm install agent-work-layer` | 7 |
33
34
  | Validate in a browser, Deno or Bun | `npm install agent-work-layer`, then import `agent-work-layer/validate` | 7 |
35
+ | Validate orders and reports in Python | `pip install agent-work-layer` ([python/](python/)) | 11 |
34
36
  | Connect agent sessions (e.g. Claude Code) through MCP | `npx -y agent-work-layer-mcp` ([setup](#run-as-an-mcp-server)) | ~100 |
37
+ | Sign or verify Delegation Passes | `npm install agent-work-layer` ([how](#delegation-pass)) | 7 |
35
38
 
36
- Node.js 20.11 or later. ESM only. Types included.
39
+ Node.js 20.11 or later. ESM only. Types included. The Python package needs Python 3.9 or later.
37
40
 
38
41
  ## Quick start
39
42
 
@@ -79,6 +82,8 @@ const { record } = await desk.submitReport(
79
82
  await desk.review(order.id, "agent:manager", "approve");
80
83
  ```
81
84
 
85
+ For a runnable version, clone the repo, run `npm install`, then run `node examples/two-agents.ts` (Node 22.18+). [examples/two-agents.ts](examples/two-agents.ts) walks one job through a rejected report, a rework request and approval, printing each event as it happens.
86
+
82
87
  The desk enforces the spec as you go:
83
88
 
84
89
  - Who may do what: only the worker reports, and only the manager approves.
@@ -87,12 +92,15 @@ The desk enforces the spec as you go:
87
92
  - Only allowed tools.
88
93
  - Cost within budget and finishing before the deadline. Going over either expires the order.
89
94
  - Sub-orders can only narrow their parent's authority.
95
+ - A reissued order can only continue a finished one, so one job never has two live orders.
96
+
97
+ Optionally, it also signs the worker's authority into a [Delegation Pass](#delegation-pass) that gateways and other systems can check without the desk.
90
98
 
91
99
  ### Hand off unfinished work
92
100
 
93
101
  When a worker fails, gets blocked or is cancelled, it can say what the next worker should know. The manager passes that along in the next order:
94
102
 
95
- <p align="center"><img src="docs/handoff.svg" alt="Worker A reports failed with a handoff; the manager reissues the job to Worker B with the same handoff: summary, decisions, what was tried, open questions and next step" width="760"></p>
103
+ <p align="center"><img src="docs/handoff.svg" alt="Worker A reports failed with a handoff; the manager cancels that order and reissues the job to Worker B with continues pointing at it and the same handoff: summary, decisions, what was tried, open questions and next step" width="760"></p>
96
104
 
97
105
  ```ts
98
106
  await desk.submitReport(order.id, {
@@ -107,10 +115,13 @@ await desk.submitReport(order.id, {
107
115
  }, "agent:researcher");
108
116
 
109
117
  const { handoff } = (await desk.get(order.id)).reports.at(-1)!;
110
- await desk.create({ ...nextJob, to: "agent:other-worker", handoff }, "agent:manager");
118
+ await desk.cancel(order.id, "agent:manager", "reissuing"); // a continued order must be finished
119
+ await desk.create({ ...nextJob, to: "agent:other-worker", handoff, continues: order.id }, "agent:manager");
120
+
121
+ await desk.list({ continues: order.id }); // the orders that pick up from this one
111
122
  ```
112
123
 
113
- `context` lists what to read; `handoff` says what was decided and already tried. See [SPEC.md §4.4](SPEC.md#44-handoff).
124
+ `context` lists what to read; `handoff` says what was decided and already tried; `continues` links the new order to the one it replaces, so the chain of orders is the job's full history. Unlike `parent_id`, it doesn't narrow anything: the new order can have a different worker, budget and tools. See [SPEC.md §4.4](SPEC.md#44-handoff) and [§4.5](SPEC.md#45-continuation).
114
125
 
115
126
  ## Validate documents
116
127
 
@@ -132,7 +143,7 @@ The JSON Schemas are exported too: `workOrderSchema`, `workReportSchema`, or `ag
132
143
 
133
144
  The server lives in its own package, [`agent-work-layer-mcp`](packages/mcp/), so people who only validate documents don't download the MCP SDK.
134
145
 
135
- Each agent session runs its own desk process. Sessions share one folder, and each is locked to one identity, so a worker cannot approve its own work.
146
+ **For development, over stdio.** Each agent session runs its own desk process. Sessions share one folder, and each is locked to the identity it is started with.
136
147
 
137
148
  ```sh
138
149
  # Claude Code: a manager session and a worker session, each added in its own project folder
@@ -140,7 +151,14 @@ claude mcp add awl -e AWL_STORE="$HOME/.awl" -e AWL_ACTOR=agent:manager -- npx -
140
151
  claude mcp add awl -e AWL_STORE="$HOME/.awl" -e AWL_ACTOR=agent:worker -- npx -y agent-work-layer-mcp
141
152
  ```
142
153
 
143
- Tools: `create_work_order`, `list_work_orders`, `get_work_order`, `accept_work_order`, `decline_work_order`, `start_work_order`, `submit_work_report`, `answer_question`, `review_work_report`, `cancel_work_order`, `wait_for_update`.
154
+ **For shared use, over HTTP.** One desk serves every agent, and each request's credential decides who it acts as, so a worker cannot approve its own work ([details](#authenticated-actor)):
155
+
156
+ ```sh
157
+ npx -y agent-work-layer-mcp serve --keys keys.json # { "keys": { "<key>": "agent:manager", ... } }
158
+ claude mcp add --transport http awl http://127.0.0.1:8787/mcp --header "Authorization: Bearer $AWL_KEY"
159
+ ```
160
+
161
+ Tools: `create_work_order`, `list_work_orders`, `get_work_order`, `accept_work_order`, `decline_work_order`, `start_work_order`, `submit_work_report`, `answer_question`, `review_work_report`, `cancel_work_order`, `wait_for_update`, and `renew_work_pass` when the desk issues [Delegation Passes](#delegation-pass).
144
162
 
145
163
  To embed the server in your own process or transport (`npm install agent-work-layer agent-work-layer-mcp`):
146
164
 
@@ -161,7 +179,9 @@ npx agent-work-layer validate order.json report.json # exit 1 on invalid, p
161
179
  npx agent-work-layer validate child.json --parent order.json
162
180
  npx agent-work-layer validate order.json report.json --json
163
181
  npx agent-work-layer schema order
164
- npx -y agent-work-layer-mcp --store ./.awl --actor agent:worker # the MCP desk (was "awl desk" before 0.2)
182
+ npx -y agent-work-layer-mcp --store ./.awl --actor agent:worker # the MCP desk on stdio (was "awl desk" before 0.2)
183
+ npx -y agent-work-layer-mcp serve --keys keys.json # one shared desk over HTTP; add --pass-key/--issuer to sign passes
184
+ npx -y agent-work-layer-mcp gateway --order <id> --config <file> # enforce an order at every tool call; add --pass-keys to use passes
165
185
  ```
166
186
 
167
187
  `new order` starts with one criterion, a USD 1 budget, no allowed tools and a
@@ -193,22 +213,27 @@ Any active state ──cancel──▶ cancelled · deadline or budget excee
193
213
 
194
214
  | Export | Purpose |
195
215
  |---|---|
196
- | `WorkDesk` | Runs the lifecycle: `create`, `accept`, `decline`, `start`, `submitReport`, `answer`, `review`, `cancel`, `get`, `list`, `waitForChange` |
216
+ | `WorkDesk` | Runs the lifecycle: `create`, `accept`, `decline`, `start`, `submitReport`, `answer`, `review`, `cancel`, `get`, `list`, `waitForChange`, and `issuePass` when it issues passes |
197
217
  | `MemoryStore`, `FileStore`, `SqliteStore`, `DeskStore` | Storage. Implement `DeskStore` (4 methods) to use a database |
198
218
  | `validateOrder`, `validateReport`, `validateChildOrder`, `hasInvalid` | Spec checks, returning `Finding[]` |
199
219
  | `isWorkOrder`, `isWorkReport` | Type guards |
200
220
  | `toolAllowed`, `patternCovered` | Tool pattern matching |
201
221
  | `nextState`, `allowedActions`, `STATES`, `ROLE_FOR`, `isTerminal` | The state machine |
202
222
  | `AwlError`, `isAwlError` | Typed errors with `code` and `findings` |
223
+ | `signPass`, `verifyPass`, `decodePass`, `PassError` | [Delegation Passes](#delegation-pass): sign, verify (including narrowing against a parent), read |
203
224
  | `ORDER_SPEC`, `REPORT_SPEC`, `ORDER_SPECS`, `REPORT_SPECS` | The spec version written, and every version read |
204
225
  | `createDeskServer` (from `agent-work-layer-mcp`) | MCP server for a desk |
226
+ | `createDeskHttpHandler`, `apiKeyAuth`, `bearerAuth`, `AuthError` (from `agent-work-layer-mcp`) | The desk over HTTP, with the actor from each request's credential |
227
+ | `createGateway` (from `agent-work-layer-mcp`) | MCP gateway enforcing an order, or a Delegation Pass, at every tool call |
205
228
 
206
- `WorkDesk` options: `store`, `clock` (for tests), `newId`, `onEvent(event, record)` (webhooks, logging, metrics).
229
+ `WorkDesk` options: `store`, `clock` (for tests), `newId`, `onEvent(event, record)` (webhooks, logging, metrics), `passes: { key, issuer, ttlSeconds? }` (sign Delegation Passes).
207
230
 
208
231
  ### Error codes
209
232
 
210
233
  `NOT_FOUND`, `ALREADY_EXISTS`, `FORBIDDEN`, `INVALID_TRANSITION`, `VALIDATION_FAILED` (with `findings`), `CONFLICT` (a concurrent writer got there first; re-read and retry), `BAD_REQUEST`.
211
234
 
235
+ `PassError` codes: `MALFORMED`, `UNSUPPORTED`, `BAD_SIGNATURE`, `EXPIRED`, `NOT_YET_VALID`, `WRONG_ISSUER`, `WRONG_WORKER`, `WRONG_ORDER`, `WRONG_PARENT`, `WIDER_THAN_PARENT` (with `findings` from C1–C3).
236
+
212
237
  ### Custom storage
213
238
 
214
239
  `MemoryStore` keeps records in memory, `FileStore` writes one JSON file per order, and `SqliteStore` keeps them in a SQLite database using Node's built-in `node:sqlite` (Node 22.13+, no extra dependency):
@@ -234,7 +259,45 @@ class PostgresStore implements DeskStore {
234
259
 
235
260
  ## Production notes
236
261
 
237
- The desk trusts the `actor` it is given. In production, derive the actor from authentication, not from tool arguments. For real-time enforcement, run the MCP gateway (`npx -y agent-work-layer-mcp gateway`), which checks `tools` and `budget` at each tool call, not only when the report arrives. See [SPEC.md §9.1](SPEC.md#91-mcp-gateway) and [§10](SPEC.md#10-security).
262
+ A stdio desk trusts the `actor` it is started with. In production, run one desk over HTTP that takes the actor from each request's credential: `npx -y agent-work-layer-mcp serve --keys keys.json`, or `createDeskHttpHandler` with your own `authenticate` hook (OAuth 2.1 bearer tokens, API keys, mTLS). See the [MCP package README](packages/mcp/README.md#shared-desk-over-http). For real-time enforcement, run the MCP gateway (`npx -y agent-work-layer-mcp gateway`), which checks `tools` and `budget` at each tool call, not only when the report arrives. When the gateway or another team's system can't read the desk, give it [Delegation Passes](#delegation-pass) instead. See [SPEC.md §9.1](SPEC.md#91-mcp-gateway) and [§10](SPEC.md#10-security).
263
+
264
+ ## Authenticated actor
265
+
266
+ With stdio, each agent runs its own desk and declares its own `actor`, so a worker could start one as its manager. Over HTTP, every agent connects to one desk, which takes the actor from the request's credential through a pluggable `authenticate(request)` hook. Teams plug in what they already use: OAuth 2.1 bearer tokens, API keys or mTLS. An `actor` argument naming anyone else is refused ([#16](https://github.com/probita-ai/agent-work-layer/issues/16), [SPEC.md §10.1](SPEC.md#101-authenticated-actor)).
267
+
268
+ <p align="center"><img src="docs/authenticated-actor.svg" alt="With stdio, a worker can start its own desk with --actor agent:manager and approve its own report. Over HTTP, agents send a credential to one desk, authenticate(request) maps it to an agent ref before the role check, and a spoofed actor argument is refused" width="760"></p>
269
+
270
+ ## Delegation Pass
271
+
272
+ When a worker starts, the desk signs a short-lived pass (a JWS) carrying the order's tools, budget and deadline. The worker presents it with each tool call, and a gateway or another team's system verifies it on its own, without calling the desk. A sub-order gets a narrower pass that names its parent, so the delegation rules (C1–C3) can be checked offline ([#17](https://github.com/probita-ai/agent-work-layer/issues/17), [SPEC.md §10.2](SPEC.md#102-delegation-pass)).
273
+
274
+ <p align="center"><img src="docs/delegation-pass.svg" alt="The desk signs a pass when the worker starts; the gateway verifies it on each tool call, allowing filings/search and refusing web/browse; a sub-order gets a narrower pass naming its parent. Below, the claims in a pass: iss, sub, awl_order, awl_tools, awl_budget, awl_parent, awl_deadline, awl_attempt, iat, exp, jti, and the typ awl-pass+jwt" width="760"></p>
275
+
276
+ - A pass lasts 15 minutes by default, never past the deadline. The desk issues a new one each time the order enters `working`, and the worker renews it while it works.
277
+ - It carries the order's whole budget; the gateway tracks what has been spent.
278
+ - Cancelling or expiring the order stops new passes. There is no other revocation, which is why passes are short.
279
+
280
+ ```ts
281
+ import { WorkDesk, verifyPass } from "agent-work-layer";
282
+
283
+ const desk = new WorkDesk({ passes: { key: privateJwk, issuer: "https://desk.example.com" } });
284
+ // ...create, accept...
285
+ const { pass } = await desk.start(id, "agent:worker"); // renew with desk.issuePass(id, "agent:worker")
286
+
287
+ // Anywhere else, with only the desk's public key:
288
+ const verified = await verifyPass(pass, publicJwk, { issuer: "https://desk.example.com", worker: "agent:worker", order: id });
289
+ verified.allows("filings/search"); // true
290
+ const child = await verifyPass(childPass, publicJwk, { parent: verified }); // throws WIDER_THAN_PARENT if it breaks C1–C3
291
+ ```
292
+
293
+ Keys are P-256 or Ed25519 JWKs (or WebCrypto keys) that you supply; the SDK does no key management. `signPass` and `verifyPass` use WebCrypto only, so they are also in `agent-work-layer/validate` for browsers, Deno and Bun. To make a key pair in Node:
294
+
295
+ ```js
296
+ const { privateKey, publicKey } = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
297
+ console.log(JSON.stringify(await crypto.subtle.exportKey("jwk", privateKey)), JSON.stringify(await crypto.subtle.exportKey("jwk", publicKey)));
298
+ ```
299
+
300
+ Over MCP, `awl-desk serve --pass-key priv.json --issuer <url>` issues passes, and `awl-desk gateway --pass-keys pub.json` enforces them; see the [MCP package README](packages/mcp/README.md#delegation-passes).
238
301
 
239
302
  ## Development
240
303
 
@@ -250,6 +313,14 @@ npm run check # all of the above; runs automatically before npm publish
250
313
  npm run generate # after editing schemas/*.json: refreshes packages/core/src/generated.ts
251
314
  ```
252
315
 
316
+ The Python package in [`python/`](python/) is separate from the npm workspace:
317
+
318
+ ```sh
319
+ cd python
320
+ python -m venv .venv && .venv/bin/pip install -e ".[test]"
321
+ .venv/bin/pytest
322
+ ```
323
+
253
324
  ## Contributing
254
325
 
255
326
  Issues, ideas and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and the [roadmap](ROADMAP.md). Spec changes start as a **Spec proposal** issue.
package/SPEC.md CHANGED
@@ -1,4 +1,4 @@
1
- # Agent Work Layer (AWL) — Specification v0.2 (draft)
1
+ # Agent Work Layer (AWL) — Specification v0.3 (draft)
2
2
 
3
3
  Status: draft · Date: 2026-10-09 · Changes from v0.1: the optional `handoff` field (§4.4); MCP gateway (§9.1); readers accept both versions (§11); related work (§13).
4
4
 
@@ -39,7 +39,7 @@ The key words MUST, SHOULD and MAY are used as in RFC 2119.
39
39
 
40
40
  | Field | Req | Type | Meaning |
41
41
  |----------------|-----|---------------------|---------|
42
- | `spec` | yes | `"awl/work-order@0.2"` | Format and version. Readers also accept `@0.1` (§11). |
42
+ | `spec` | yes | `"awl/work-order@0.3"` | Format and version. Readers also accept `@0.1` and `@0.2` (§11). |
43
43
  | `id` | yes | string | Unique job id, set by the desk or manager. |
44
44
  | `created_at` | yes | timestamp | When the order was issued. |
45
45
  | `from` | yes | agent ref | The manager. |
@@ -55,6 +55,7 @@ The key words MUST, SHOULD and MAY are used as in RFC 2119.
55
55
  | `handoff` | no | Handoff (§4.4) | What earlier work on this job established. |
56
56
  | `output_schema`| no | JSON Schema | Shape that `result.output` MUST match when done. |
57
57
  | `parent_id` | no | string | Set when this order was split from a larger one (§8). |
58
+ | `continues` | no | string | The id of the finished order this one picks up from (§4.5). |
58
59
  | `metadata` | no | object | Free-form; ignored by checks. |
59
60
 
60
61
  ### 4.1 Criterion
@@ -100,7 +101,21 @@ When a job moves from one worker to another (after a failure, a cancellation or
100
101
  | `open_questions` | array of string | Questions still unanswered. |
101
102
  | `next_step` | string | The most useful thing to do next. |
102
103
 
103
- Every field is optional, but a handoff MUST NOT be empty. A Work Report MAY carry a handoff (§5); a worker SHOULD include one when it reports `failed` or `blocked`. A manager reissuing the job MAY copy it, edited or not, into the new order. Handoff content is the previous worker's account, not verified fact; it is not checked against evidence.
104
+ Every field is optional, but a handoff MUST NOT be empty. A Work Report MAY carry a handoff (§5); a worker SHOULD include one when it reports `failed` or `blocked`. A manager reissuing the job MAY copy it, edited or not, into the new order, and SHOULD link the two with `continues` (§4.5). Handoff content is the previous worker's account, not verified fact; it is not checked against evidence.
105
+
106
+ ### 4.5 Continuation
107
+
108
+ When a job moves to another worker after it stopped, the new order names the order it picks up from in `continues`. A chain of orders linked this way is the full history of one job: who worked on it, and under which order, and so which authority, each earlier action ran.
109
+
110
+ `continues` is not `parent_id`. A sub-order (§8) is part of a live order and may only narrow it; a continuation replaces a finished one. No narrowing rules apply: the new order may have a different worker, budget, tools and deadline. An order MAY have both, e.g. a sub-order reissued under the same parent.
111
+
112
+ A desk MUST reject an order whose `continues`:
113
+
114
+ - names no order it knows;
115
+ - names an order that is not in a terminal state (`approved`, `declined`, `expired` or `cancelled`), so two orders are never live for the same job. A `failed` or `blocked` report leaves the order `submitted` or `blocked`; the manager cancels it first;
116
+ - names an order that another order already continues, so the chain stays a single line.
117
+
118
+ A desk SHOULD also accept continuations only from the earlier order's manager (`from`). Each order in a chain keeps its own budget and cost; nothing is carried over unless the manager puts it in the new order, usually as `handoff`.
104
119
 
105
120
  ## 5. Work Report
106
121
 
@@ -224,6 +239,9 @@ A gateway is an MCP server that sits between one worker and its real MCP servers
224
239
  5. It numbers the calls that reach a tool 1, 2, 3… per order, across reworks, and names each `<server>/<tool>#<n>`. That name is the `ref` of `tool_call` evidence (§5.1), and the gateway SHOULD return it with the call's result.
225
240
  6. It SHOULD keep a log of every call, refused ones included, that survives a restart, so restarting the gateway does not reset the budget.
226
241
  7. The report's `tools_used` and its `cost.tool_calls` and `cost.seconds` SHOULD be taken from the gateway's log.
242
+ 8. Each logged call SHOULD record the authority it ran under: the order id, the report attempt in progress (`reports` + 1) and, when there is one, the `jti` of the Delegation Pass (§10.2). A resumed or reissued job can then tell which grant applied to every earlier call.
243
+
244
+ A gateway MAY work from **Delegation Passes** instead of the desk's order state (§10.2). It then requires a valid pass for its order and worker on every call, and enforces the pass's `awl_tools` and `awl_budget.tool_calls` in place of rules 1–3. A pass does not say when the order was started, so `budget.seconds` is left to the report checks.
227
245
 
228
246
  A gateway MAY instead run in **observe** mode, for deployments whose own gateway or policy engine already controls access. It then applies rules 4–7 but not 1–3: it refuses nothing, and it SHOULD mark each logged call that rules 1–3 would have refused. Calls over budget or to tools the order does not allow still show up in the report, where checks R5 and R7 catch them.
229
247
 
@@ -231,19 +249,64 @@ The gateway cannot measure `usd` or `tokens`; those are still checked when the r
231
249
 
232
250
  ## 10. Security
233
251
 
234
- - A desk MUST bind `actor` to an authenticated identity in production. The reference desk trusts the `actor` argument and is for development only.
252
+ - A desk MUST bind `actor` to an authenticated identity in production (§10.1). The reference stdio desk trusts the identity it is started with and is for development only.
235
253
  - A worker's MCP gateway SHOULD enforce `tools` and `budget` at call time, not only after the report arrives (§9.1).
236
254
  - Evidence `ref` values can point at sensitive data. Desks SHOULD apply the same access rules to evidence as to the data it points at.
237
255
 
256
+ ### 10.1 Authenticated actor
257
+
258
+ `actor` is bound to an authenticated identity when the desk, not the caller, decides it:
259
+
260
+ - The desk derives the actor from a credential it has verified on the same request (an OAuth 2.1 access token, an API key, a client certificate), through a hook `authenticate(request) → AgentRef`. The desk does not define how credentials are issued or stored.
261
+ - A request whose credential is missing or invalid MUST be refused before any operation runs (HTTP 401). A valid credential that maps to no agent MUST be refused too (HTTP 403).
262
+ - The authenticated actor is the only actor for that request. An `actor` argument naming anyone else MUST be refused with `FORBIDDEN`; naming the same agent is allowed. The role checks of §6 then run against that actor.
263
+ - A desk SHOULD authenticate every request, not only the first one of a session, so a session never outlives its credential.
264
+
265
+ For an MCP desk over Streamable HTTP, the desk is an OAuth resource server as the MCP authorization spec describes. When it accepts bearer tokens it MUST check that a token is unexpired and was issued for the desk (`aud`), SHOULD answer 401 with a `WWW-Authenticate` header naming its Protected Resource Metadata (RFC 9728), and maps the token's subject to an agent reference, either as is or through a configured map. A desk over stdio is not bound: whoever starts the process chooses its identity.
266
+
267
+ ### 10.2 Delegation Pass
268
+
269
+ A Delegation Pass is an order's authority as a signed token: which tools, how much budget, until when, for which job and which worker. A gateway, a tool server or a sub-worker in another system can check a worker's authority with it, without calling the desk.
270
+
271
+ A pass is a JWS in compact form (a JWT) with header `typ` `awl-pass+jwt` and `alg` `ES256` (P-256) or `EdDSA` (Ed25519). Its claims:
272
+
273
+ | Claim | Value |
274
+ |---|---|
275
+ | `iss` | The desk that issued it |
276
+ | `sub` | The worker: the order's `to` |
277
+ | `awl_order` | The order's `id` |
278
+ | `awl_parent` | The parent order's `id`, for sub-orders |
279
+ | `awl_tools` | The order's `tools` |
280
+ | `awl_budget` | The order's whole `budget`, not what is left of it |
281
+ | `awl_deadline` | The order's `deadline` |
282
+ | `awl_attempt` | The report attempt the pass was issued for: `reports` + 1 |
283
+ | `iat`, `exp` | Issue and expiry time, in seconds since the epoch |
284
+ | `jti` | A unique id for this pass |
285
+
286
+ **Issuing.** A desk that issues passes signs one for the worker each time the order enters `working`, and on request while it stays `working`. A pass's `exp` MUST NOT be later than `awl_deadline`, and SHOULD be short (the reference desk uses 15 minutes); the worker renews it while it works. Short lifetimes are the only revocation: when an order is cancelled, expires or leaves `working`, the desk stops issuing passes for it. The desk SHOULD give a pass only to the authenticated worker (§10.1); a pass is only as trustworthy as the identity it was issued to.
287
+
288
+ **Verifying.** A verifier MUST reject a pass whose signature does not verify with one of the issuer's keys, whose `typ` or `alg` is not one of the above, that has expired (at `exp` or `awl_deadline`, whichever is sooner), or whose `iat` is in the future by more than an allowance for clock skew (the reference verifier allows 60 seconds). It SHOULD check `iss`, and that `sub` and `awl_order` are the worker and order it expects. A tool call is allowed when a pattern in `awl_tools` allows it (§4.3).
289
+
290
+ **Narrowing.** A sub-order's pass names its parent in `awl_parent`. A verifier holding a pass and its parent's pass checks C1–C3 (§8) between their `awl_budget`, `awl_tools` and `awl_deadline`, and MUST reject a pass wider than its parent's or naming a different parent. A chain is verified from the top down.
291
+
292
+ Key distribution, rotation and JWKS hosting are out of scope. The reference implementation is `signPass` and `verifyPass` in `agent-work-layer`, which use WebCrypto only.
293
+
238
294
  ## 11. Versioning
239
295
 
240
296
  `spec` carries the version. Minor versions (0.x) may add optional fields only. Readers MUST reject a major version they do not know.
241
297
 
242
- A v0.2 reader MUST accept documents marked `@0.1` and `@0.2`, and MUST reject any other version. A v0.2 writer marks new documents `@0.2`. Every v0.1 document is a valid v0.2 document once its version is accepted.
298
+ Each document type has its own version. v0.3 adds `continues` to the Work Order and leaves the Work Report unchanged:
299
+
300
+ | Spec | Work Order | Work Report |
301
+ |---|---|---|
302
+ | v0.1 | `@0.1` | `@0.1` |
303
+ | v0.2 | `@0.2` (adds `handoff`) | `@0.2` (adds `handoff`) |
304
+ | v0.3 | `@0.3` (adds `continues`) | `@0.2` |
305
+
306
+ A v0.3 reader MUST accept Work Orders marked `@0.1`, `@0.2` and `@0.3`, and Work Reports marked `@0.1` and `@0.2`, and MUST reject any other version. A v0.3 writer marks new orders `@0.3` and new reports `@0.2`. Every earlier document is a valid v0.3 document once its version is accepted.
243
307
 
244
308
  ## 12. Planned for later versions
245
309
 
246
- - Delegation Pass: a signed, standalone grant of budget, tools and time.
247
310
  - Track record: an aggregate of approved reports per worker.
248
311
  - Streaming progress events while `working`.
249
312
 
package/dist/desk.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { PassKey } from "./pass.ts";
1
2
  import type { DeskStore } from "./store.ts";
2
3
  import type { AgentRef, Finding, OrderRecord, OrderSummary, State, WorkEvent, WorkOrderInput, WorkReportInput } from "./types.ts";
3
4
  export interface DeskOptions {
@@ -9,12 +10,28 @@ export interface DeskOptions {
9
10
  newId?: () => string;
10
11
  /** Called after every recorded event, once it is stored. */
11
12
  onEvent?: (event: WorkEvent, record: OrderRecord) => void;
13
+ /**
14
+ * Issue Delegation Passes (SPEC.md §10.2): the desk signs a pass for the worker each time an
15
+ * order enters `working`, and `issuePass` renews it. Without this, the desk issues none.
16
+ */
17
+ passes?: PassOptions;
18
+ }
19
+ export interface PassOptions {
20
+ /** Private signing key (P-256 or Ed25519). */
21
+ key: PassKey;
22
+ /** Goes in each pass's `iss`. */
23
+ issuer: string;
24
+ /** Lifetime in seconds, capped at the order's deadline. Default 900. */
25
+ ttlSeconds?: number;
26
+ kid?: string;
12
27
  }
13
28
  export interface ListFilter {
14
29
  to?: AgentRef;
15
30
  from?: AgentRef;
16
31
  state?: State;
17
32
  parent_id?: string;
33
+ /** Orders that continue this order id. */
34
+ continues?: string;
18
35
  }
19
36
  export interface WaitOptions {
20
37
  timeoutMs?: number;
@@ -32,13 +49,18 @@ export declare class WorkDesk {
32
49
  #private;
33
50
  readonly store: DeskStore;
34
51
  constructor(options?: DeskOptions);
52
+ /** True when this desk signs Delegation Passes. */
53
+ get issuesPasses(): boolean;
35
54
  /** The full record for an order. Throws NOT_FOUND. */
36
55
  get(id: string): Promise<OrderRecord>;
37
56
  /** Summaries of orders matching every given filter field. */
38
57
  list(filter?: ListFilter): Promise<OrderSummary[]>;
39
58
  /** Resolves once the order has an event newer than `sinceSeq`, or when the timeout passes. */
40
59
  waitForChange(id: string, sinceSeq: number, options?: WaitOptions): Promise<OrderRecord>;
41
- /** Issues a new order. With `parent_id`, only the parent's worker may create it, and only narrower (§8). */
60
+ /**
61
+ * Issues a new order. With `parent_id`, only the parent's worker may create it, and only narrower (§8).
62
+ * With `continues`, the earlier order must be finished, be the same manager's, and not be continued yet (§4.5).
63
+ */
42
64
  create(input: WorkOrderInput, actor: AgentRef): Promise<OrderRecord>;
43
65
  answer(id: string, actor: AgentRef, answer: string): Promise<OrderRecord>;
44
66
  review(id: string, actor: AgentRef, decision: "approve" | "rework", reason?: string): Promise<OrderRecord>;
@@ -46,6 +68,11 @@ export declare class WorkDesk {
46
68
  accept(id: string, actor: AgentRef): Promise<OrderRecord>;
47
69
  decline(id: string, actor: AgentRef, reason?: string): Promise<OrderRecord>;
48
70
  start(id: string, actor: AgentRef): Promise<OrderRecord>;
71
+ /**
72
+ * Signs a fresh Delegation Pass for the order's worker while the order is `working`, replacing
73
+ * the current one. Workers call it before the current pass expires. Records no event.
74
+ */
75
+ issuePass(id: string, actor: AgentRef): Promise<string>;
49
76
  /**
50
77
  * Records a report. Invalid reports throw VALIDATION_FAILED and change nothing.
51
78
  * Reports over budget or past the deadline are kept and move the order to `expired`.
package/dist/desk.js CHANGED
@@ -2,24 +2,34 @@
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { AwlError } from "./errors.js";
4
4
  import { isTerminal, nextState, ROLE_FOR } from "./lifecycle.js";
5
+ import { signPass } from "./pass.js";
5
6
  import { MemoryStore } from "./store.js";
6
7
  import { hasInvalid, validateChildOrder, validateOrder, validateReport } from "./validate.js";
7
8
  import { ORDER_SPEC, REPORT_SPEC } from "./types.js";
8
9
  export const lastSeq = (record) => record.events.at(-1)?.seq ?? 0;
9
10
  export function summarize(record) {
10
- const { id, from, to, goal, deadline, parent_id } = record.order;
11
- return { id, state: record.state, from, to, goal, deadline, ...(parent_id ? { parent_id } : {}), last_seq: lastSeq(record) };
11
+ const { id, from, to, goal, deadline, parent_id, continues } = record.order;
12
+ return {
13
+ id, state: record.state, from, to, goal, deadline,
14
+ ...(parent_id ? { parent_id } : {}), ...(continues ? { continues } : {}), last_seq: lastSeq(record),
15
+ };
12
16
  }
13
17
  export class WorkDesk {
14
18
  store;
15
19
  #clock;
16
20
  #newId;
17
21
  #onEvent;
22
+ #passes;
18
23
  constructor(options = {}) {
19
24
  this.store = options.store ?? new MemoryStore();
20
25
  this.#clock = options.clock ?? (() => new Date());
21
26
  this.#newId = options.newId ?? (() => `wo_${randomUUID().replace(/-/g, "").slice(0, 16)}`);
22
27
  this.#onEvent = options.onEvent;
28
+ this.#passes = options.passes;
29
+ }
30
+ /** True when this desk signs Delegation Passes. */
31
+ get issuesPasses() {
32
+ return this.#passes !== undefined;
23
33
  }
24
34
  // ---- reads ----------------------------------------------------------------
25
35
  /** The full record for an order. Throws NOT_FOUND. */
@@ -36,7 +46,8 @@ export class WorkDesk {
36
46
  .filter((r) => (!filter.to || r.order.to === filter.to) &&
37
47
  (!filter.from || r.order.from === filter.from) &&
38
48
  (!filter.state || r.state === filter.state) &&
39
- (!filter.parent_id || r.order.parent_id === filter.parent_id))
49
+ (!filter.parent_id || r.order.parent_id === filter.parent_id) &&
50
+ (!filter.continues || r.order.continues === filter.continues))
40
51
  .sort((a, b) => a.order.created_at.localeCompare(b.order.created_at))
41
52
  .map(summarize);
42
53
  }
@@ -52,7 +63,10 @@ export class WorkDesk {
52
63
  }
53
64
  }
54
65
  // ---- manager actions ------------------------------------------------------
55
- /** Issues a new order. With `parent_id`, only the parent's worker may create it, and only narrower (§8). */
66
+ /**
67
+ * Issues a new order. With `parent_id`, only the parent's worker may create it, and only narrower (§8).
68
+ * With `continues`, the earlier order must be finished, be the same manager's, and not be continued yet (§4.5).
69
+ */
56
70
  async create(input, actor) {
57
71
  requireActor(actor);
58
72
  const order = {
@@ -75,6 +89,17 @@ export class WorkDesk {
75
89
  if (childFindings.length)
76
90
  throw new AwlError("VALIDATION_FAILED", "sub-order exceeds its parent's authority", childFindings);
77
91
  }
92
+ if (order.continues) {
93
+ const earlier = await this.get(order.continues);
94
+ if (!isTerminal(earlier.state)) {
95
+ throw new AwlError("INVALID_TRANSITION", `order "${order.continues}" is ${earlier.state}; only a finished order can be continued`);
96
+ }
97
+ if (actor !== earlier.order.from)
98
+ throw new AwlError("FORBIDDEN", `only the manager of "${order.continues}" can continue it`);
99
+ const next = (await this.store.list()).find((r) => r.order.continues === order.continues);
100
+ if (next)
101
+ throw new AwlError("ALREADY_EXISTS", `order "${order.continues}" is already continued by "${next.order.id}"`);
102
+ }
78
103
  const record = { order, state: "offered", reports: [], events: [], findings: [] };
79
104
  const event = this.#record(record, actor, "create", "offered");
80
105
  await this.store.insert(record);
@@ -106,6 +131,23 @@ export class WorkDesk {
106
131
  async start(id, actor) {
107
132
  return this.#act(id, "start", actor);
108
133
  }
134
+ /**
135
+ * Signs a fresh Delegation Pass for the order's worker while the order is `working`, replacing
136
+ * the current one. Workers call it before the current pass expires. Records no event.
137
+ */
138
+ async issuePass(id, actor) {
139
+ requireActor(actor);
140
+ if (!this.#passes)
141
+ throw new AwlError("BAD_REQUEST", "this desk does not issue passes");
142
+ const record = await this.#mutate(id, async (rec) => {
143
+ if (actor !== rec.order.to)
144
+ throw new AwlError("FORBIDDEN", `only the order's worker can get its pass`);
145
+ if (rec.state !== "working")
146
+ throw new AwlError("INVALID_TRANSITION", `no pass for an order that is ${rec.state}`);
147
+ rec.pass = await this.#sign(rec);
148
+ });
149
+ return record.pass;
150
+ }
109
151
  /**
110
152
  * Records a report. Invalid reports throw VALIDATION_FAILED and change nothing.
111
153
  * Reports over budget or past the deadline are kept and move the order to `expired`.
@@ -147,6 +189,17 @@ export class WorkDesk {
147
189
  #now() {
148
190
  return this.#clock().toISOString();
149
191
  }
192
+ #sign(record) {
193
+ const { key, issuer, ttlSeconds, kid } = this.#passes;
194
+ return signPass(record.order, key, { issuer, ttlSeconds, kid, attempt: record.reports.length + 1, now: this.#clock() });
195
+ }
196
+ /** A pass is issued on entering `working` and dropped on leaving it; cancel or expiry just stops new ones. */
197
+ async #syncPass(record, before) {
198
+ if (record.state !== "working")
199
+ delete record.pass;
200
+ else if (this.#passes && before !== "working")
201
+ record.pass = await this.#sign(record);
202
+ }
150
203
  #emit(event, record) {
151
204
  this.#onEvent?.(event, structuredClone(record));
152
205
  }
@@ -190,7 +243,9 @@ export class WorkDesk {
190
243
  async #mutate(id, change) {
191
244
  const record = await this.get(id);
192
245
  const expected = lastSeq(record);
193
- change(record);
246
+ const before = record.state;
247
+ await change(record);
248
+ await this.#syncPass(record, before);
194
249
  await this.store.update(record, expected);
195
250
  return record;
196
251
  }
@@ -200,6 +255,7 @@ export class WorkDesk {
200
255
  return record;
201
256
  const expected = lastSeq(record);
202
257
  const event = this.#record(record, "desk", "expire", "expired", "deadline passed");
258
+ delete record.pass;
203
259
  try {
204
260
  await this.store.update(record, expected);
205
261
  }
package/dist/generated.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // Generated by scripts/generate.ts from package.json and schemas/*.json. Do not edit.
2
2
  /** This package's version. */
3
- export const VERSION = "0.4.0";
3
+ export const VERSION = "0.5.0";
4
4
  /** The Work Order JSON Schema (schemas/work-order.schema.json). */
5
5
  export const workOrderSchema = {
6
6
  "$schema": "https://json-schema.org/draft/2020-12/schema",
7
- "$id": "https://awl.dev/schemas/work-order-0.2.json",
8
- "title": "AWL Work Order v0.2",
7
+ "$id": "https://awl.dev/schemas/work-order-0.3.json",
8
+ "title": "AWL Work Order v0.3",
9
9
  "type": "object",
10
10
  "required": [
11
11
  "spec",
@@ -23,7 +23,8 @@ export const workOrderSchema = {
23
23
  "spec": {
24
24
  "enum": [
25
25
  "awl/work-order@0.1",
26
- "awl/work-order@0.2"
26
+ "awl/work-order@0.2",
27
+ "awl/work-order@0.3"
27
28
  ]
28
29
  },
29
30
  "id": {
@@ -117,6 +118,10 @@ export const workOrderSchema = {
117
118
  "type": "string",
118
119
  "minLength": 1
119
120
  },
121
+ "continues": {
122
+ "type": "string",
123
+ "minLength": 1
124
+ },
120
125
  "metadata": {
121
126
  "type": "object"
122
127
  }
package/dist/index.d.ts CHANGED
@@ -7,10 +7,12 @@ export * from "./types.ts";
7
7
  export { AwlError, isAwlError } from "./errors.ts";
8
8
  export type { AwlErrorCode } from "./errors.ts";
9
9
  export { validateOrder, validateReport, validateChildOrder, hasInvalid, isWorkOrder, isWorkReport, toolAllowed, patternCovered, workOrderSchema, workReportSchema, } from "./validate.ts";
10
+ export { signPass, verifyPass, decodePass, PassError, PASS_TYP, PASS_TTL_SECONDS } from "./pass.ts";
11
+ export type { PassClaims, PassKey, PassKeys, PassJwk, PassCryptoKey, PassErrorCode, SignPassOptions, VerifyPassOptions, VerifiedPass } from "./pass.ts";
10
12
  export { STATES, TERMINAL_STATES, ROLE_FOR, isTerminal, nextState, allowedActions } from "./lifecycle.ts";
11
13
  export type { Role } from "./lifecycle.ts";
12
14
  export { WorkDesk, summarize, lastSeq } from "./desk.ts";
13
- export type { DeskOptions, ListFilter, WaitOptions, SubmitResult } from "./desk.ts";
15
+ export type { DeskOptions, PassOptions, ListFilter, WaitOptions, SubmitResult } from "./desk.ts";
14
16
  export { MemoryStore, FileStore, SqliteStore } from "./store.ts";
15
17
  export type { DeskStore } from "./store.ts";
16
18
  export { VERSION } from "./version.ts";
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@
6
6
  export * from "./types.js";
7
7
  export { AwlError, isAwlError } from "./errors.js";
8
8
  export { validateOrder, validateReport, validateChildOrder, hasInvalid, isWorkOrder, isWorkReport, toolAllowed, patternCovered, workOrderSchema, workReportSchema, } from "./validate.js";
9
+ export { signPass, verifyPass, decodePass, PassError, PASS_TYP, PASS_TTL_SECONDS } from "./pass.js";
9
10
  export { STATES, TERMINAL_STATES, ROLE_FOR, isTerminal, nextState, allowedActions } from "./lifecycle.js";
10
11
  export { WorkDesk, summarize, lastSeq } from "./desk.js";
11
12
  export { MemoryStore, FileStore, SqliteStore } from "./store.js";
package/dist/pass.d.ts ADDED
@@ -0,0 +1,114 @@
1
+ import type { AgentRef, Amounts, Finding, WorkOrder } from "./types.ts";
2
+ /** The JWS `typ` of every pass, so a pass is never mistaken for another kind of token. */
3
+ export declare const PASS_TYP = "awl-pass+jwt";
4
+ /** Default lifetime: 15 minutes, and never past the order's deadline. */
5
+ export declare const PASS_TTL_SECONDS = 900;
6
+ export interface PassClaims {
7
+ /** The desk that issued the pass. */
8
+ iss: string;
9
+ /** The worker the pass was issued to: the order's `to`. */
10
+ sub: AgentRef;
11
+ awl_order: string;
12
+ /** The parent order's id, for sub-orders. */
13
+ awl_parent?: string;
14
+ /** The order's tool patterns. */
15
+ awl_tools: string[];
16
+ /** The order's whole budget, not what is left of it. */
17
+ awl_budget: Amounts;
18
+ /** The order's deadline (RFC 3339). `exp` is never later. */
19
+ awl_deadline: string;
20
+ /** The report attempt the pass was issued for: 1, then +1 per report. */
21
+ awl_attempt?: number;
22
+ /** Issue and expiry times, in seconds since the epoch. */
23
+ iat: number;
24
+ exp: number;
25
+ /** Unique id of this pass. */
26
+ jti: string;
27
+ }
28
+ /** A JWK: a P-256 EC key (ES256) or an Ed25519 OKP key (EdDSA). */
29
+ export interface PassJwk {
30
+ kty: string;
31
+ crv: string;
32
+ x: string;
33
+ y?: string;
34
+ /** The private part; needed to sign. */
35
+ d?: string;
36
+ kid?: string;
37
+ [member: string]: unknown;
38
+ }
39
+ /** A WebCrypto `CryptoKey`, described by shape so this module needs no DOM or Node types. */
40
+ export interface PassCryptoKey {
41
+ readonly type: string;
42
+ readonly algorithm: {
43
+ name: string;
44
+ namedCurve?: string;
45
+ };
46
+ readonly usages: readonly string[];
47
+ }
48
+ /** A JWK or a WebCrypto key. Private to sign, public to verify. */
49
+ export type PassKey = PassJwk | PassCryptoKey;
50
+ /** One key, several, or a JWK Set. */
51
+ export type PassKeys = PassKey | readonly PassKey[] | {
52
+ keys: readonly PassJwk[];
53
+ };
54
+ export type PassErrorCode = "MALFORMED" | "UNSUPPORTED" | "BAD_SIGNATURE" | "EXPIRED" | "NOT_YET_VALID" | "WRONG_ISSUER" | "WRONG_WORKER" | "WRONG_ORDER" | "WRONG_PARENT" | "WIDER_THAN_PARENT";
55
+ /** Why a pass could not be signed or was rejected. `findings` is set for WIDER_THAN_PARENT (rules C1–C3). */
56
+ export declare class PassError extends Error {
57
+ readonly code: PassErrorCode;
58
+ readonly findings: Finding[];
59
+ constructor(code: PassErrorCode, message: string, findings?: Finding[]);
60
+ toJSON(): {
61
+ code: PassErrorCode;
62
+ message: string;
63
+ findings: Finding[];
64
+ };
65
+ }
66
+ export interface SignPassOptions {
67
+ /** Goes in `iss`; verifiers check it. */
68
+ issuer: string;
69
+ /** The report attempt the pass is for. */
70
+ attempt?: number;
71
+ /** Lifetime in seconds, capped at the order's deadline. Default 900. */
72
+ ttlSeconds?: number;
73
+ /** Key id for the JWS header. Default: the JWK's `kid`. */
74
+ kid?: string;
75
+ now?: Date;
76
+ jti?: string;
77
+ }
78
+ export interface VerifyPassOptions {
79
+ /** Required `iss`. */
80
+ issuer?: string;
81
+ /** Required `sub`: the worker presenting the pass. */
82
+ worker?: AgentRef;
83
+ /** Required `awl_order`. */
84
+ order?: string;
85
+ /**
86
+ * The parent order's verified pass. The pass must then name it in `awl_parent` and be no
87
+ * wider than it (C1–C3). Verify a chain from the top down, passing each result to the next.
88
+ */
89
+ parent?: VerifiedPass;
90
+ /** Seconds of clock skew to allow on `exp` (default 0) and on `iat` (at least 60, since issuer and verifier clocks differ). */
91
+ clockToleranceSeconds?: number;
92
+ now?: Date;
93
+ }
94
+ export interface VerifiedPass {
95
+ claims: PassClaims;
96
+ header: {
97
+ alg: string;
98
+ typ: string;
99
+ kid?: string;
100
+ };
101
+ /** When the pass stops being valid: `exp`, or the deadline if sooner. */
102
+ expiresAt: Date;
103
+ /** Does the pass allow this concrete tool (`server/tool`)? */
104
+ allows(tool: string): boolean;
105
+ }
106
+ /** Signs a pass for an order's worker. */
107
+ export declare function signPass(order: WorkOrder, key: PassKey, options: SignPassOptions): Promise<string>;
108
+ /** Reads a pass's claims without checking anything. For display and routing only; use verifyPass to trust it. */
109
+ export declare function decodePass(token: string): PassClaims;
110
+ /**
111
+ * Verifies a pass: its signature against `keys`, its type and lifetime, and, when asked,
112
+ * its issuer, worker, order and narrowing against the parent's pass. Throws a PassError.
113
+ */
114
+ export declare function verifyPass(token: string, keys: PassKeys, options?: VerifyPassOptions): Promise<VerifiedPass>;
package/dist/pass.js ADDED
@@ -0,0 +1,200 @@
1
+ // Delegation Pass (SPEC.md §10.2): an order's authority as a signed JWT anyone can verify.
2
+ // WebCrypto only, no Node built-ins: this also runs in browsers, Deno and Bun (see src/validator.ts).
3
+ import { toolAllowed, validateChildOrder } from "./validate.js";
4
+ import { UNITS } from "./types.js";
5
+ /** The JWS `typ` of every pass, so a pass is never mistaken for another kind of token. */
6
+ export const PASS_TYP = "awl-pass+jwt";
7
+ /** Default lifetime: 15 minutes, and never past the order's deadline. */
8
+ export const PASS_TTL_SECONDS = 900;
9
+ /** How far in the future `iat` may be: the issuer's clock may run ahead of the verifier's. */
10
+ const IAT_SKEW_SECONDS = 60;
11
+ /** Why a pass could not be signed or was rejected. `findings` is set for WIDER_THAN_PARENT (rules C1–C3). */
12
+ export class PassError extends Error {
13
+ code;
14
+ findings;
15
+ constructor(code, message, findings = []) {
16
+ super(message);
17
+ this.name = "PassError";
18
+ this.code = code;
19
+ this.findings = findings;
20
+ }
21
+ toJSON() {
22
+ return { code: this.code, message: this.message, findings: this.findings };
23
+ }
24
+ }
25
+ const webcrypto = () => globalThis.crypto;
26
+ const ALGS = {
27
+ ES256: { key: { name: "ECDSA", namedCurve: "P-256" }, sign: { name: "ECDSA", hash: "SHA-256" } },
28
+ EdDSA: { key: { name: "Ed25519" }, sign: { name: "Ed25519" } },
29
+ };
30
+ const utf8 = new TextEncoder();
31
+ function b64url(bytes) {
32
+ let s = "";
33
+ for (const b of bytes)
34
+ s += String.fromCharCode(b);
35
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
36
+ }
37
+ function unb64url(s) {
38
+ if (!/^[A-Za-z0-9_-]*$/.test(s) || s.length % 4 === 1)
39
+ throw new PassError("MALFORMED", "not base64url");
40
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4));
41
+ return Uint8Array.from(bin, (c) => c.charCodeAt(0));
42
+ }
43
+ const isCryptoKey = (key) => "algorithm" in key && "usages" in key;
44
+ function algOf(key) {
45
+ if (isCryptoKey(key)) {
46
+ const a = key.algorithm;
47
+ if (a.name === "ECDSA" && a.namedCurve === "P-256")
48
+ return "ES256";
49
+ if (a.name === "Ed25519")
50
+ return "EdDSA";
51
+ }
52
+ else {
53
+ if (key.kty === "EC" && key.crv === "P-256")
54
+ return "ES256";
55
+ if (key.kty === "OKP" && key.crv === "Ed25519")
56
+ return "EdDSA";
57
+ }
58
+ throw new PassError("UNSUPPORTED", "pass keys must be P-256 (ES256) or Ed25519 (EdDSA)");
59
+ }
60
+ async function cryptoKey(key, alg, use) {
61
+ if (isCryptoKey(key))
62
+ return key;
63
+ // Verifying only needs the public part; dropping the rest also drops key_ops that would forbid it.
64
+ const { kty, crv, x, y, d } = key;
65
+ const jwk = { kty, crv, x, ...(y ? { y } : {}), ...(use === "sign" ? { d } : {}) };
66
+ if (use === "sign" && !d)
67
+ throw new PassError("UNSUPPORTED", "signing needs a private key");
68
+ return webcrypto().subtle.importKey("jwk", jwk, ALGS[alg].key, false, [use]);
69
+ }
70
+ function keyList(keys) {
71
+ if (Array.isArray(keys))
72
+ return keys;
73
+ if ("keys" in keys && Array.isArray(keys.keys))
74
+ return keys.keys;
75
+ return [keys];
76
+ }
77
+ /** Signs a pass for an order's worker. */
78
+ export async function signPass(order, key, options) {
79
+ const { issuer, attempt, ttlSeconds = PASS_TTL_SECONDS, now = new Date(), jti = webcrypto().randomUUID() } = options;
80
+ if (!issuer)
81
+ throw new PassError("MALFORMED", "a pass needs an issuer");
82
+ const alg = algOf(key);
83
+ const kid = options.kid ?? (isCryptoKey(key) ? undefined : key.kid);
84
+ const iat = Math.floor(now.getTime() / 1000);
85
+ const exp = Math.floor(Math.min(now.getTime() + ttlSeconds * 1000, Date.parse(order.deadline)) / 1000);
86
+ if (exp <= iat)
87
+ throw new PassError("EXPIRED", `order "${order.id}" is past its deadline`);
88
+ const claims = {
89
+ iss: issuer,
90
+ sub: order.to,
91
+ awl_order: order.id,
92
+ ...(order.parent_id ? { awl_parent: order.parent_id } : {}),
93
+ awl_tools: [...order.tools],
94
+ awl_budget: { ...order.budget },
95
+ awl_deadline: order.deadline,
96
+ ...(attempt !== undefined ? { awl_attempt: attempt } : {}),
97
+ iat,
98
+ exp,
99
+ jti,
100
+ };
101
+ const header = { alg, typ: PASS_TYP, ...(kid ? { kid } : {}) };
102
+ const input = `${b64url(utf8.encode(JSON.stringify(header)))}.${b64url(utf8.encode(JSON.stringify(claims)))}`;
103
+ const signature = await webcrypto().subtle.sign(ALGS[alg].sign, await cryptoKey(key, alg, "sign"), utf8.encode(input));
104
+ return `${input}.${b64url(new Uint8Array(signature))}`;
105
+ }
106
+ /** Reads a pass's claims without checking anything. For display and routing only; use verifyPass to trust it. */
107
+ export function decodePass(token) {
108
+ const parts = typeof token === "string" ? token.split(".") : [];
109
+ if (parts.length !== 3)
110
+ throw new PassError("MALFORMED", "a pass is three base64url parts joined by dots");
111
+ try {
112
+ return JSON.parse(new TextDecoder().decode(unb64url(parts[1])));
113
+ }
114
+ catch (e) {
115
+ throw e instanceof PassError ? e : new PassError("MALFORMED", "the claims are not JSON");
116
+ }
117
+ }
118
+ const isString = (v) => typeof v === "string" && v.length > 0;
119
+ const isTime = (v) => typeof v === "number" && Number.isFinite(v);
120
+ function checkShape(c) {
121
+ const budget = c.awl_budget;
122
+ const ok = isString(c.iss) && isString(c.sub) && isString(c.awl_order) && isString(c.jti) &&
123
+ (c.awl_parent === undefined || isString(c.awl_parent)) &&
124
+ Array.isArray(c.awl_tools) && c.awl_tools.every(isString) &&
125
+ !!budget && typeof budget === "object" && !Array.isArray(budget) &&
126
+ Object.entries(budget).every(([unit, v]) => UNITS.includes(unit) && typeof v === "number" && v >= 0) &&
127
+ isString(c.awl_deadline) && !Number.isNaN(Date.parse(c.awl_deadline)) &&
128
+ (c.awl_attempt === undefined || (Number.isInteger(c.awl_attempt) && c.awl_attempt >= 1)) &&
129
+ isTime(c.iat) && isTime(c.exp);
130
+ if (!ok)
131
+ throw new PassError("MALFORMED", "the claims do not match the pass format");
132
+ }
133
+ /**
134
+ * Verifies a pass: its signature against `keys`, its type and lifetime, and, when asked,
135
+ * its issuer, worker, order and narrowing against the parent's pass. Throws a PassError.
136
+ */
137
+ export async function verifyPass(token, keys, options = {}) {
138
+ const { issuer, worker, order, parent, clockToleranceSeconds = 0, now = new Date() } = options;
139
+ const parts = typeof token === "string" ? token.split(".") : [];
140
+ if (parts.length !== 3)
141
+ throw new PassError("MALFORMED", "a pass is three base64url parts joined by dots");
142
+ let header;
143
+ let claims;
144
+ try {
145
+ header = JSON.parse(new TextDecoder().decode(unb64url(parts[0])));
146
+ claims = JSON.parse(new TextDecoder().decode(unb64url(parts[1])));
147
+ }
148
+ catch {
149
+ throw new PassError("MALFORMED", "the header or claims are not base64url JSON");
150
+ }
151
+ if (!header || typeof header !== "object" || header.typ !== PASS_TYP)
152
+ throw new PassError("MALFORMED", `typ must be "${PASS_TYP}"`);
153
+ if (!Object.hasOwn(ALGS, header.alg))
154
+ throw new PassError("UNSUPPORTED", `unsupported alg "${String(header.alg)}"`);
155
+ const alg = header.alg;
156
+ const candidates = keyList(keys).filter((k) => {
157
+ try {
158
+ return algOf(k) === alg && (!header.kid || isCryptoKey(k) || !k.kid || k.kid === header.kid);
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ });
164
+ const signature = unb64url(parts[2]);
165
+ const data = utf8.encode(`${parts[0]}.${parts[1]}`);
166
+ let valid = false;
167
+ for (const k of candidates) {
168
+ if (await webcrypto().subtle.verify(ALGS[alg].sign, await cryptoKey(k, alg, "verify"), signature, data)) {
169
+ valid = true;
170
+ break;
171
+ }
172
+ }
173
+ if (!valid)
174
+ throw new PassError("BAD_SIGNATURE", "no key verifies this pass");
175
+ if (!claims || typeof claims !== "object" || Array.isArray(claims))
176
+ throw new PassError("MALFORMED", "the claims are not an object");
177
+ checkShape(claims);
178
+ const c = claims;
179
+ if (issuer !== undefined && c.iss !== issuer)
180
+ throw new PassError("WRONG_ISSUER", `issued by "${c.iss}", not "${issuer}"`);
181
+ const t = now.getTime();
182
+ const expiresAt = new Date(Math.min(c.exp * 1000, Date.parse(c.awl_deadline)));
183
+ if (expiresAt.getTime() + clockToleranceSeconds * 1000 <= t)
184
+ throw new PassError("EXPIRED", `pass "${c.jti}" expired at ${expiresAt.toISOString()}`);
185
+ if ((c.iat - Math.max(clockToleranceSeconds, IAT_SKEW_SECONDS)) * 1000 > t)
186
+ throw new PassError("NOT_YET_VALID", `pass "${c.jti}" is issued in the future`);
187
+ if (worker !== undefined && c.sub !== worker)
188
+ throw new PassError("WRONG_WORKER", `issued to "${c.sub}", not "${worker}"`);
189
+ if (order !== undefined && c.awl_order !== order)
190
+ throw new PassError("WRONG_ORDER", `for order "${c.awl_order}", not "${order}"`);
191
+ if (parent) {
192
+ if (c.awl_parent !== parent.claims.awl_order) {
193
+ throw new PassError("WRONG_PARENT", `pass names parent "${c.awl_parent ?? "(none)"}", not "${parent.claims.awl_order}"`);
194
+ }
195
+ const findings = validateChildOrder({ budget: c.awl_budget, tools: c.awl_tools, deadline: c.awl_deadline }, { budget: parent.claims.awl_budget, tools: parent.claims.awl_tools, deadline: parent.claims.awl_deadline });
196
+ if (findings.length)
197
+ throw new PassError("WIDER_THAN_PARENT", `pass for "${c.awl_order}" is wider than its parent's`, findings);
198
+ }
199
+ return { claims: c, header, expiresAt, allows: (tool) => toolAllowed(tool, c.awl_tools) };
200
+ }
package/dist/store.d.ts CHANGED
@@ -23,7 +23,8 @@ export declare class MemoryStore implements DeskStore {
23
23
  }
24
24
  /**
25
25
  * One JSON file per order in `<dir>/orders`. Several processes may share a folder.
26
- * Writes are atomic (write then rename); the CONFLICT check is best-effort across processes.
26
+ * Writes are atomic (write then rename). Within one process, writes to an order run one at a
27
+ * time, so the CONFLICT check is exact; across processes it is best-effort.
27
28
  */
28
29
  export declare class FileStore implements DeskStore {
29
30
  #private;
package/dist/store.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // Storage for desks. Implement DeskStore to back a desk with a database.
2
+ import { randomUUID } from "node:crypto";
2
3
  import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
3
4
  import { createRequire } from "node:module";
4
5
  import { join } from "node:path";
@@ -32,11 +33,14 @@ export class MemoryStore {
32
33
  const SAFE_ID = /^[A-Za-z0-9_.-]{1,128}$/;
33
34
  /**
34
35
  * One JSON file per order in `<dir>/orders`. Several processes may share a folder.
35
- * Writes are atomic (write then rename); the CONFLICT check is best-effort across processes.
36
+ * Writes are atomic (write then rename). Within one process, writes to an order run one at a
37
+ * time, so the CONFLICT check is exact; across processes it is best-effort.
36
38
  */
37
39
  export class FileStore {
38
40
  dir;
39
41
  #ready;
42
+ /** Tail of each order's queue of pending writes. */
43
+ #queues = new Map();
40
44
  constructor(dir) {
41
45
  this.dir = join(dir, "orders");
42
46
  this.#ready = mkdir(this.dir, { recursive: true });
@@ -48,7 +52,7 @@ export class FileStore {
48
52
  }
49
53
  async #write(record) {
50
54
  const file = this.#path(record.order.id);
51
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
55
+ const tmp = `${file}.${randomUUID()}.tmp`;
52
56
  await writeFile(tmp, JSON.stringify(record, null, 2));
53
57
  await rename(tmp, file);
54
58
  }
@@ -63,18 +67,35 @@ export class FileStore {
63
67
  throw e;
64
68
  }
65
69
  }
70
+ /** Runs `fn` after every earlier write to order `id` in this process has finished. */
71
+ async #serial(id, fn) {
72
+ const run = (this.#queues.get(id) ?? Promise.resolve()).then(fn);
73
+ const tail = run.catch(() => { });
74
+ this.#queues.set(id, tail);
75
+ try {
76
+ return await run;
77
+ }
78
+ finally {
79
+ if (this.#queues.get(id) === tail)
80
+ this.#queues.delete(id);
81
+ }
82
+ }
66
83
  async insert(record) {
67
- if (await this.read(record.order.id))
68
- throw new AwlError("ALREADY_EXISTS", `order "${record.order.id}" already exists`);
69
- await this.#write(record);
84
+ await this.#serial(record.order.id, async () => {
85
+ if (await this.read(record.order.id))
86
+ throw new AwlError("ALREADY_EXISTS", `order "${record.order.id}" already exists`);
87
+ await this.#write(record);
88
+ });
70
89
  }
71
90
  async update(record, expectedSeq) {
72
- const current = await this.read(record.order.id);
73
- if (!current)
74
- throw new AwlError("NOT_FOUND", `no order "${record.order.id}"`);
75
- if (lastSeq(current) !== expectedSeq)
76
- throw new AwlError("CONFLICT", `order "${record.order.id}" changed since it was read`);
77
- await this.#write(record);
91
+ await this.#serial(record.order.id, async () => {
92
+ const current = await this.read(record.order.id);
93
+ if (!current)
94
+ throw new AwlError("NOT_FOUND", `no order "${record.order.id}"`);
95
+ if (lastSeq(current) !== expectedSeq)
96
+ throw new AwlError("CONFLICT", `order "${record.order.id}" changed since it was read`);
97
+ await this.#write(record);
98
+ });
78
99
  }
79
100
  async list() {
80
101
  await this.#ready;
package/dist/types.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- /** Document types for AWL v0.2. Field meanings are defined in SPEC.md §4 and §5. */
1
+ /** Document types for AWL v0.3. Field meanings are defined in SPEC.md §4 and §5. */
2
2
  /** The spec version this package writes. */
3
- export declare const ORDER_SPEC = "awl/work-order@0.2";
3
+ export declare const ORDER_SPEC = "awl/work-order@0.3";
4
4
  export declare const REPORT_SPEC = "awl/work-report@0.2";
5
5
  /** Every spec version this package reads (SPEC.md §11). */
6
- export declare const ORDER_SPECS: readonly ["awl/work-order@0.1", "awl/work-order@0.2"];
6
+ export declare const ORDER_SPECS: readonly ["awl/work-order@0.1", "awl/work-order@0.2", "awl/work-order@0.3"];
7
7
  export declare const REPORT_SPECS: readonly ["awl/work-report@0.1", "awl/work-report@0.2"];
8
8
  /** An Agent Card URL, an agent name, or `human:<id>`. */
9
9
  export type AgentRef = string;
@@ -51,6 +51,8 @@ export interface WorkOrder {
51
51
  handoff?: Handoff;
52
52
  output_schema?: Record<string, unknown>;
53
53
  parent_id?: string;
54
+ /** The order this one picks up from, after it stopped (§4.5). Not a sub-order: no narrowing applies. */
55
+ continues?: string;
54
56
  metadata?: Record<string, unknown>;
55
57
  [extension: `x-${string}`]: unknown;
56
58
  }
@@ -124,6 +126,8 @@ export interface OrderRecord {
124
126
  reports: WorkReport[];
125
127
  events: WorkEvent[];
126
128
  findings: Finding[];
129
+ /** The worker's current Delegation Pass (SPEC.md §10.2), while the order is `working` on a desk that issues passes. */
130
+ pass?: string;
127
131
  }
128
132
  export interface OrderSummary {
129
133
  id: string;
@@ -133,5 +137,6 @@ export interface OrderSummary {
133
137
  goal: string;
134
138
  deadline: string;
135
139
  parent_id?: string;
140
+ continues?: string;
136
141
  last_seq: number;
137
142
  }
package/dist/types.js CHANGED
@@ -1,9 +1,9 @@
1
- /** Document types for AWL v0.2. Field meanings are defined in SPEC.md §4 and §5. */
1
+ /** Document types for AWL v0.3. Field meanings are defined in SPEC.md §4 and §5. */
2
2
  /** The spec version this package writes. */
3
- export const ORDER_SPEC = "awl/work-order@0.2";
3
+ export const ORDER_SPEC = "awl/work-order@0.3";
4
4
  export const REPORT_SPEC = "awl/work-report@0.2";
5
5
  /** Every spec version this package reads (SPEC.md §11). */
6
- export const ORDER_SPECS = ["awl/work-order@0.1", ORDER_SPEC];
6
+ export const ORDER_SPECS = ["awl/work-order@0.1", "awl/work-order@0.2", ORDER_SPEC];
7
7
  export const REPORT_SPECS = ["awl/work-report@0.1", REPORT_SPEC];
8
8
  /** Budget and cost units (SPEC.md §4.2). */
9
9
  export const UNITS = ["usd", "tokens", "tool_calls", "seconds"];
@@ -14,7 +14,9 @@ export declare const isWorkReport: (value: unknown) => value is WorkReport;
14
14
  export declare function validateOrder(order: unknown): Finding[];
15
15
  /** Runs report checks R1–R12 against the order it answers. The order is assumed valid. */
16
16
  export declare function validateReport(report: unknown, order: WorkOrder): Finding[];
17
+ /** The parts of an order (or a Delegation Pass) that carry authority. */
18
+ type Authority = Pick<WorkOrder, "budget" | "tools" | "deadline">;
17
19
  /** Runs sub-order checks C1–C3: a child order may only narrow its parent's authority. */
18
- export declare function validateChildOrder(child: WorkOrder, parent: WorkOrder): Finding[];
20
+ export declare function validateChildOrder(child: Authority, parent: Authority): Finding[];
19
21
  /** True when any finding rejects the document. */
20
22
  export declare const hasInvalid: (findings: readonly Finding[]) => boolean;
@@ -1,7 +1,10 @@
1
1
  /**
2
- * Validation only: document types, JSON Schemas and every check in SPEC.md §7–§8.
2
+ * Validation only: document types, JSON Schemas, every check in SPEC.md §7–§8, and Delegation Pass
3
+ * signing and verification (§10.2, WebCrypto).
3
4
  * Imports no Node built-ins, so it runs in browsers, Deno and Bun.
4
5
  * @packageDocumentation
5
6
  */
6
7
  export * from "./types.ts";
7
8
  export { validateOrder, validateReport, validateChildOrder, hasInvalid, isWorkOrder, isWorkReport, toolAllowed, patternCovered, workOrderSchema, workReportSchema, } from "./validate.ts";
9
+ export { signPass, verifyPass, decodePass, PassError, PASS_TYP, PASS_TTL_SECONDS } from "./pass.ts";
10
+ export type { PassClaims, PassKey, PassKeys, PassJwk, PassCryptoKey, PassErrorCode, SignPassOptions, VerifyPassOptions, VerifiedPass } from "./pass.ts";
package/dist/validator.js CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Validation only: document types, JSON Schemas and every check in SPEC.md §7–§8.
2
+ * Validation only: document types, JSON Schemas, every check in SPEC.md §7–§8, and Delegation Pass
3
+ * signing and verification (§10.2, WebCrypto).
3
4
  * Imports no Node built-ins, so it runs in browsers, Deno and Bun.
4
5
  * @packageDocumentation
5
6
  */
6
7
  export * from "./types.js";
7
8
  export { validateOrder, validateReport, validateChildOrder, hasInvalid, isWorkOrder, isWorkReport, toolAllowed, patternCovered, workOrderSchema, workReportSchema, } from "./validate.js";
9
+ export { signPass, verifyPass, decodePass, PassError, PASS_TYP, PASS_TTL_SECONDS } from "./pass.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-work-layer",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Work Orders and Work Reports for AI agents: a typed SDK, validator and lifecycle desk. The MCP server is in agent-work-layer-mcp.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://awl.dev/schemas/work-order-0.2.json",
4
- "title": "AWL Work Order v0.2",
3
+ "$id": "https://awl.dev/schemas/work-order-0.3.json",
4
+ "title": "AWL Work Order v0.3",
5
5
  "type": "object",
6
6
  "required": ["spec", "id", "created_at", "from", "to", "goal", "criteria", "budget", "deadline", "tools"],
7
7
  "properties": {
8
- "spec": { "enum": ["awl/work-order@0.1", "awl/work-order@0.2"] },
8
+ "spec": { "enum": ["awl/work-order@0.1", "awl/work-order@0.2", "awl/work-order@0.3"] },
9
9
  "id": { "type": "string", "minLength": 1, "maxLength": 128 },
10
10
  "created_at": { "type": "string", "format": "date-time" },
11
11
  "from": { "$ref": "#/$defs/agentRef" },
@@ -41,6 +41,7 @@
41
41
  "handoff": { "$ref": "#/$defs/handoff" },
42
42
  "output_schema": { "type": "object" },
43
43
  "parent_id": { "type": "string", "minLength": 1 },
44
+ "continues": { "type": "string", "minLength": 1 },
44
45
  "metadata": { "type": "object" }
45
46
  },
46
47
  "patternProperties": { "^x-": {} },