agent-work-layer 0.2.1 → 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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-10-11
4
+
5
+ Both packages: `agent-work-layer` 0.4.0 and `agent-work-layer-mcp` 0.4.0.
6
+
7
+ ### Added
8
+
9
+ - MCP gateway (#6): `awl-desk gateway --order <id> --config <file>` sits between a worker and its real MCP servers and enforces the order at every tool call. It lists only the tools the order allows, refuses the rest, refuses calls once `tool_calls` or `seconds` is spent or while the order is not `working`, and logs each call to `<store>/calls/<id>.jsonl` with an evidence ref `<server>/<tool>#<n>`. Its `awl_usage` tool returns `tools_used` and `cost` for the report. In code: `createGateway` from `agent-work-layer-mcp`. Enforcement is optional: `--observe` (`enforce: false`) refuses nothing and logs what the order would have refused, for setups whose own gateway already controls access.
10
+ - SPEC.md §9.1: what an MCP gateway must enforce.
11
+
12
+ ### Changed
13
+
14
+ - `agent-work-layer-mcp` now depends on `agent-work-layer` ^0.4.0.
15
+
16
+ ## 0.3.0 — 2026-10-10
17
+
18
+ Both packages: `agent-work-layer` 0.3.0 and `agent-work-layer-mcp` 0.3.0.
19
+
20
+ ### Added
21
+
22
+ - `SqliteStore`: a `DeskStore` backed by SQLite through Node's built-in `node:sqlite` (Node 22.13+), with an atomic CONFLICT check (#1).
23
+
24
+ ### Changed
25
+
26
+ - `agent-work-layer-mcp` now depends on `agent-work-layer` ^0.3.0.
27
+
3
28
  ## 0.2.1 — 2026-10-09
4
29
 
5
30
  Documentation only; `agent-work-layer` alone (`agent-work-layer-mcp` stays at 0.2.0).
package/README.md CHANGED
@@ -40,7 +40,7 @@ Node.js 20.11 or later. ESM only. Types included.
40
40
  ```ts
41
41
  import { WorkDesk } from "agent-work-layer";
42
42
 
43
- const desk = new WorkDesk(); // in-memory; use FileStore or your own DeskStore to persist
43
+ const desk = new WorkDesk(); // in-memory; use FileStore, SqliteStore or your own DeskStore to persist
44
44
 
45
45
  // Manager issues a job
46
46
  const { order } = await desk.create(
@@ -194,7 +194,7 @@ Any active state ──cancel──▶ cancelled · deadline or budget excee
194
194
  | Export | Purpose |
195
195
  |---|---|
196
196
  | `WorkDesk` | Runs the lifecycle: `create`, `accept`, `decline`, `start`, `submitReport`, `answer`, `review`, `cancel`, `get`, `list`, `waitForChange` |
197
- | `MemoryStore`, `FileStore`, `DeskStore` | Storage. Implement `DeskStore` (4 methods) to use a database |
197
+ | `MemoryStore`, `FileStore`, `SqliteStore`, `DeskStore` | Storage. Implement `DeskStore` (4 methods) to use a database |
198
198
  | `validateOrder`, `validateReport`, `validateChildOrder`, `hasInvalid` | Spec checks, returning `Finding[]` |
199
199
  | `isWorkOrder`, `isWorkReport` | Type guards |
200
200
  | `toolAllowed`, `patternCovered` | Tool pattern matching |
@@ -211,6 +211,16 @@ Any active state ──cancel──▶ cancelled · deadline or budget excee
211
211
 
212
212
  ### Custom storage
213
213
 
214
+ `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):
215
+
216
+ ```ts
217
+ import { WorkDesk, SqliteStore } from "agent-work-layer";
218
+
219
+ const desk = new WorkDesk({ store: new SqliteStore("./awl.db") }); // or ":memory:"
220
+ ```
221
+
222
+ To use another database, implement `DeskStore`:
223
+
214
224
  ```ts
215
225
  import type { DeskStore, OrderRecord } from "agent-work-layer";
216
226
 
@@ -224,7 +234,7 @@ class PostgresStore implements DeskStore {
224
234
 
225
235
  ## Production notes
226
236
 
227
- The desk trusts the `actor` it is given. In production, derive the actor from authentication, not from tool arguments. For real-time enforcement, check `tools` and `budget` at each tool call in an MCP gateway, not only when the report arrives. See [SPEC.md §10](SPEC.md#10-security).
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).
228
238
 
229
239
  ## Development
230
240
 
package/SPEC.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent Work Layer (AWL) — Specification v0.2 (draft)
2
2
 
3
- Status: draft · Date: 2026-10-09 · Changes from v0.1: the optional `handoff` field (§4.4); readers accept both versions (§11); related work (§13).
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
 
5
5
  ## 1. Purpose
6
6
 
@@ -213,10 +213,26 @@ How AWL documents travel. A desk MAY support several.
213
213
  - **MCP**: a desk exposed as an MCP server with tools such as `create_work_order`, `submit_work_report` and `review_work_report`. The reference implementation is the `agent-work-layer-mcp` package in this repo.
214
214
  - **A2A**: the document travels as a data part of an A2A message with the AWL media type. The A2A task id SHOULD be stored in `metadata.a2a_task_id`.
215
215
 
216
+ ### 9.1 MCP gateway
217
+
218
+ A gateway is an MCP server that sits between one worker and its real MCP servers, for one order. It enforces the order while the worker works, instead of only checking the report afterwards (§10).
219
+
220
+ 1. It MUST refuse a call to a tool that no pattern in `tools` allows (§4.3), and SHOULD NOT list such tools.
221
+ 2. It MUST refuse calls once `budget.tool_calls` calls have reached a tool, and once `budget.seconds` have passed since the order was first started. It SHOULD stop a running call when the seconds run out.
222
+ 3. It MUST refuse calls while the order is not `working`. It MAY start an `accepted` order on its first call.
223
+ 4. Refused calls do not reach the tool and do not count against the budget. A call that reached the tool counts even if the tool returned an error.
224
+ 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
+ 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
+ 7. The report's `tools_used` and its `cost.tool_calls` and `cost.seconds` SHOULD be taken from the gateway's log.
227
+
228
+ 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
+
230
+ The gateway cannot measure `usd` or `tokens`; those are still checked when the report arrives (R7). The reference implementation is `awl-desk gateway` in `agent-work-layer-mcp`.
231
+
216
232
  ## 10. Security
217
233
 
218
234
  - A desk MUST bind `actor` to an authenticated identity in production. The reference desk trusts the `actor` argument and is for development only.
219
- - A worker's MCP gateway SHOULD enforce `tools` and `budget` at call time, not only after the report arrives.
235
+ - A worker's MCP gateway SHOULD enforce `tools` and `budget` at call time, not only after the report arrives (§9.1).
220
236
  - Evidence `ref` values can point at sensitive data. Desks SHOULD apply the same access rules to evidence as to the data it points at.
221
237
 
222
238
  ## 11. Versioning
package/dist/generated.js CHANGED
@@ -1,6 +1,6 @@
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.2.1";
3
+ export const VERSION = "0.4.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",
package/dist/index.d.ts CHANGED
@@ -11,6 +11,6 @@ export { STATES, TERMINAL_STATES, ROLE_FOR, isTerminal, nextState, allowedAction
11
11
  export type { Role } from "./lifecycle.ts";
12
12
  export { WorkDesk, summarize, lastSeq } from "./desk.ts";
13
13
  export type { DeskOptions, ListFilter, WaitOptions, SubmitResult } from "./desk.ts";
14
- export { MemoryStore, FileStore } from "./store.ts";
14
+ export { MemoryStore, FileStore, SqliteStore } from "./store.ts";
15
15
  export type { DeskStore } from "./store.ts";
16
16
  export { VERSION } from "./version.ts";
package/dist/index.js CHANGED
@@ -8,5 +8,5 @@ export { AwlError, isAwlError } from "./errors.js";
8
8
  export { validateOrder, validateReport, validateChildOrder, hasInvalid, isWorkOrder, isWorkReport, toolAllowed, patternCovered, workOrderSchema, workReportSchema, } from "./validate.js";
9
9
  export { STATES, TERMINAL_STATES, ROLE_FOR, isTerminal, nextState, allowedActions } from "./lifecycle.js";
10
10
  export { WorkDesk, summarize, lastSeq } from "./desk.js";
11
- export { MemoryStore, FileStore } from "./store.js";
11
+ export { MemoryStore, FileStore, SqliteStore } from "./store.js";
12
12
  export { VERSION } from "./version.js";
package/dist/store.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { OrderRecord } from "./types.ts";
2
+ import type { DatabaseSync } from "node:sqlite";
2
3
  export interface DeskStore {
3
4
  /** Returns the record, or null when there is none. */
4
5
  read(id: string): Promise<OrderRecord | null>;
@@ -33,3 +34,18 @@ export declare class FileStore implements DeskStore {
33
34
  update(record: OrderRecord, expectedSeq: number): Promise<void>;
34
35
  list(): Promise<OrderRecord[]>;
35
36
  }
37
+ /**
38
+ * One SQLite database, using Node's built-in `node:sqlite` (Node 22.13+). Pass a file path,
39
+ * or ":memory:" for a throwaway database. Several processes may share a file; the CONFLICT
40
+ * check is atomic (`UPDATE ... WHERE last_seq = ?`).
41
+ */
42
+ export declare class SqliteStore implements DeskStore {
43
+ readonly db: DatabaseSync;
44
+ constructor(path?: string);
45
+ read(id: string): Promise<OrderRecord | null>;
46
+ insert(record: OrderRecord): Promise<void>;
47
+ update(record: OrderRecord, expectedSeq: number): Promise<void>;
48
+ list(): Promise<OrderRecord[]>;
49
+ /** Closes the database. */
50
+ close(): void;
51
+ }
package/dist/store.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Storage for desks. Implement DeskStore to back a desk with a database.
2
2
  import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
3
4
  import { join } from "node:path";
4
5
  import { AwlError } from "./errors.js";
5
6
  const lastSeq = (r) => r.events.at(-1)?.seq ?? 0;
@@ -82,3 +83,53 @@ export class FileStore {
82
83
  return records.filter((r) => r !== null);
83
84
  }
84
85
  }
86
+ /**
87
+ * One SQLite database, using Node's built-in `node:sqlite` (Node 22.13+). Pass a file path,
88
+ * or ":memory:" for a throwaway database. Several processes may share a file; the CONFLICT
89
+ * check is atomic (`UPDATE ... WHERE last_seq = ?`).
90
+ */
91
+ export class SqliteStore {
92
+ db;
93
+ constructor(path = ":memory:") {
94
+ // Loaded here so importing the package does not need node:sqlite.
95
+ const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
96
+ this.db = new DatabaseSync(path);
97
+ this.db.exec(`
98
+ PRAGMA busy_timeout = 5000;
99
+ CREATE TABLE IF NOT EXISTS orders (
100
+ id TEXT PRIMARY KEY,
101
+ last_seq INTEGER NOT NULL,
102
+ record TEXT NOT NULL
103
+ ) STRICT;
104
+ `);
105
+ }
106
+ async read(id) {
107
+ const row = this.db.prepare("SELECT record FROM orders WHERE id = ?").get(id);
108
+ return row ? JSON.parse(row.record) : null;
109
+ }
110
+ async insert(record) {
111
+ const { changes } = this.db
112
+ .prepare("INSERT INTO orders (id, last_seq, record) VALUES (?, ?, ?) ON CONFLICT (id) DO NOTHING")
113
+ .run(record.order.id, lastSeq(record), JSON.stringify(record));
114
+ if (!changes)
115
+ throw new AwlError("ALREADY_EXISTS", `order "${record.order.id}" already exists`);
116
+ }
117
+ async update(record, expectedSeq) {
118
+ const { changes } = this.db
119
+ .prepare("UPDATE orders SET last_seq = ?, record = ? WHERE id = ? AND last_seq = ?")
120
+ .run(lastSeq(record), JSON.stringify(record), record.order.id, expectedSeq);
121
+ if (changes)
122
+ return;
123
+ if (!(await this.read(record.order.id)))
124
+ throw new AwlError("NOT_FOUND", `no order "${record.order.id}"`);
125
+ throw new AwlError("CONFLICT", `order "${record.order.id}" changed since it was read`);
126
+ }
127
+ async list() {
128
+ const rows = this.db.prepare("SELECT record FROM orders ORDER BY id").all();
129
+ return rows.map((r) => JSON.parse(r.record));
130
+ }
131
+ /** Closes the database. */
132
+ close() {
133
+ this.db.close();
134
+ }
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-work-layer",
3
- "version": "0.2.1",
3
+ "version": "0.4.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",