agentic-drive 0.3.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,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ Read the breaking changes before you upgrade or deploy.
6
+
7
+ ### Breaking changes
8
+
9
+ - **Deploy the Worker and the client together.** Each item in the HTTP, CLI, and MCP output has a `machineMade` field next to `createdBy.kind`. The client now rejects an item with no `machineMade`. A client of 0.5.0 that talks to a Worker of 0.4.0 fails every call that returns items, such as list, stat, and search. Also, `Item` and `PathItem` in the client types now require `machineMade: boolean`. A project that builds those objects itself, such as test fixtures or mocks of the client, must add the field.
10
+
11
+ ### Added
12
+
13
+ - **Machine-made label from creation on.** The web UI shows "Created by agent" on every item that an agent made, not only after a person edits it.
14
+
15
+ ## 0.4.0
16
+
17
+ Read the breaking changes before you upgrade or deploy.
18
+
19
+ ### Breaking changes
20
+
21
+ - **The CLI acts as a real principal.** The `--owner`, `--actor`, and `--actor-kind` flags and the `AGENTIC_DRIVE_OWNER`, `_ACTOR`, and `_KIND` variables are gone. Pass an API key with `--token` or `AGENTIC_DRIVE_TOKEN`. agentic-identity resolves the key. With no key, or a revoked or unknown key, the CLI and `mcp` exit with code 3. The default owner `me` is gone.
22
+ - **`openLocal(dir)` takes a folder and returns `{ drive, store }`.** Use `resolveToken(store, token)` to get the `ctx`.
23
+ - **The Worker serves the web UI from `ui-dist`.** `wrangler.jsonc` names `ui-dist` as the assets folder. Run `npm run build:ui` before each `wrangler deploy`. Without the build, the deploy fails because `ui-dist` does not exist. A `ui-dist` left from an older build ships an old UI.
24
+ - **Paths other than `/api/*` and `/health` get the UI.** The assets answer every other path. An unknown GET path returns the UI page with status 200, not a 404 from the Worker. A client that expects a 404 for such a path must change.
25
+
26
+ ### Added
27
+
28
+ - **`principal add`, `principal revoke`, `key issue`, and `key revoke`** make local principals and keys. `principal add` takes `--of <person>` for an agent and `--scope <s>`, which repeats. `key issue` takes `--label`. The package root also exports `addPrincipal`, `revokePrincipal`, `issueKey`, `revokeKey`, and `AuthError`.
29
+ - **A web UI.** The Worker serves it from `ui-dist` (`npm run build:ui`). It uses agentic-design and the typed client. It has a folder tree, breadcrumbs, a list and a grid, upload by button and by drop, new folder, rename, move, trash and restore, and a share dialog. An item that an agent made shows a "Created by agent" label while a person is its last editor. "Edited by" names the last editor, a person or an agent. A Playwright test with an axe WCAG 2.2 AA check covers each view in light and dark.
30
+ - **A content security policy.** The UI loads scripts and fonts only from its own origin. It allows inline styles. The UI refuses to be framed. It also sends `X-Content-Type-Options: nosniff` and `Referrer-Policy: no-referrer`.
31
+ - **Same-origin downloads.** The UI refuses a download link on another origin. Set `DRIVE_PUBLIC_URL` to the origin that serves the UI.
32
+ - **The UI ships in the package.** `npm pack` runs `build:ui` in `prepack`, and the tarball has `ui-dist`.
33
+
34
+ ### Changed
35
+
36
+ - `npm run ci:local` and CI also build the UI and run `npm run test:e2e`. The E2E test needs Chromium.
37
+ - The package adds no runtime dependency. The UI build and test tools are dev dependencies.
38
+ - The MCP registry entry names `AGENTIC_DRIVE_TOKEN` as a required secret, and `AGENTIC_DRIVE_DIR` as optional.
39
+
40
+ ### Migration
41
+
42
+ Local data from 0.3.0 belongs to the owner `me`, or to the id you passed to `--owner`. The data folder does not change. Without the key of that owner, the CLI shows an empty drive. Make the principal and a key, then use the key:
43
+
44
+ 1. Run `npx agentic-drive principal add me`, with your old owner id if it was not `me`. Pass the same `--dir` or `AGENTIC_DRIVE_DIR` as before, if you set one.
45
+ 2. Run `export AGENTIC_DRIVE_TOKEN=$(npx agentic-drive key issue me | jq -r .token)`.
46
+ 3. Put the same key in each MCP client config as `AGENTIC_DRIVE_TOKEN`, and the same data folder as `AGENTIC_DRIVE_DIR`, if you set one.
47
+
3
48
  ## 0.3.0
4
49
 
5
50
  Read the breaking changes before you upgrade. The migration steps follow them.
package/README.md CHANGED
@@ -7,8 +7,22 @@
7
7
 
8
8
  Store, sort, and share files and folders.
9
9
 
10
- > Status: early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK and the HTTP API.
11
- > The web UI is not built yet.
10
+ > Status: 0.5.0, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK and the HTTP API.
11
+ > A small web UI works. See [UI](#ui).
12
+
13
+ ## Live demo
14
+
15
+ A demo Worker runs at https://agentic-drive-demo.hello-fd1.workers.dev. `GET /health` answers 200, and the web UI is served at `/`.
16
+ The API needs a bearer token, so a call without one gets 401. The demo holds throwaway data only, and it can be reset at any time.
17
+
18
+ To redeploy it, set `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` in your shell, then run:
19
+
20
+ ```sh
21
+ npm run deploy:demo
22
+ ```
23
+
24
+ This uses `wrangler.demo.jsonc` (D1 in `weur`, R2 in the EU jurisdiction, `workers_dev` on). The URL signing secret lives in the Worker:
25
+ `openssl rand -hex 32 | npx wrangler secret put DRIVE_URL_SECRET -c wrangler.demo.jsonc`. Deploys are manual. No CI secret is used.
12
26
 
13
27
  ## Install
14
28
 
@@ -42,8 +56,26 @@ npx agentic-drive mcp
42
56
  ```
43
57
 
44
58
  Every command prints JSON. A failure prints `{"error":{"code","message"}}` on stderr and exits 1.
45
- Set `--dir`, `--owner`, `--actor`, and `--actor-kind` (or the `AGENTIC_DRIVE_*` variables).
46
- An agent sets `--actor-kind agent_of_human`. Each item then records that a machine made it.
59
+ Exit code 2 means bad usage. Exit code 3 means no principal, or a key that is not valid.
60
+
61
+ Every command except `help`, `version`, `principal`, and `key` needs a principal. Pass an API key with
62
+ `--token <key>` or `AGENTIC_DRIVE_TOKEN`. Prefer the variable: a flag shows in `ps`.
63
+ `agentic-identity` resolves the key. The drive belongs to the person at the top of the
64
+ principal's chain. An agent key marks each item it makes as made by a machine. A revoked,
65
+ expired, or unknown key fails with exit code 3.
66
+
67
+ ```sh
68
+ npx agentic-drive principal add alice
69
+ export AGENTIC_DRIVE_TOKEN=$(npx agentic-drive key issue alice | jq -r .token)
70
+ npx agentic-drive principal add helper --of alice # an agent of alice
71
+ npx agentic-drive key issue helper # the key shows once
72
+ npx agentic-drive key revoke <key>
73
+ npx agentic-drive principal revoke helper # stops its keys, and its agents
74
+ ```
75
+
76
+ `principal` and `key` need no key: anyone who can read the `--dir` folder can run them.
77
+ Set `--dir` (or `AGENTIC_DRIVE_DIR`) to choose the data folder.
78
+ The local drive checks the owner only. It does not apply the scopes of an agent key.
47
79
 
48
80
  ## HTTP API
49
81
 
@@ -70,7 +102,7 @@ Sharing routes: `GET /shares?path=`, `POST /shares`, `DELETE /shares?path=&subje
70
102
  `POST /invites/accept`, `POST /links`, `DELETE /links?id=`. A link visitor reads with the header
71
103
  `x-link-token` on `GET /shared/:id`, `GET /shared/:id/items`, and `GET /shared/:id/content`.
72
104
  Name a shared item by its id. A path can start at a folder id: `<folder id>/notes/a.txt`.
73
- Without `DRIVE_URL_SECRET` or `DRIVE_PUBLIC_URL`, every request gets 503.
105
+ Without `DRIVE_URL_SECRET` or `DRIVE_PUBLIC_URL`, every API request gets 503. `/health` still answers.
74
106
  Create the R2 bucket with `--jurisdiction eu` and the D1 database with `--location weur`.
75
107
  The Worker declares region `eu` and refuses a D1 query that a non-EU region served. `DRIVE_REGION=unknown` turns this off for local runs.
76
108
 
@@ -101,6 +133,63 @@ try {
101
133
  - A browser page needs the Worker on its own origin for now: the Worker sends no CORS headers.
102
134
  - Share with a subject: `share("/pub", { kind: "human", id: "u2" }, "viewer")`.
103
135
 
136
+ ## UI
137
+
138
+ The Worker serves a small web UI. It uses the [agentic-design](https://www.npmjs.com/package/agentic-design) package and the typed client of this package (`agentic-drive/client`). It has a folder tree, breadcrumbs, a file list and grid, upload by button or by drag and drop, new folder, rename, move, trash and restore, and a share dialog with roles and a link. An item that an agent made shows a "Created by agent" label while a person is its last editor. "Edited by" names the last editor, a person or an agent. It works with the keyboard. An automated axe check for WCAG 2.2 AA passes on each view in the light and dark themes.
139
+
140
+ Run it locally:
141
+
142
+ ```sh
143
+ npm ci
144
+ npm run build:ui
145
+ WRANGLER_SEND_METRICS=false npx wrangler dev --local \
146
+ --var DRIVE_PUBLIC_URL:http://127.0.0.1:8787 \
147
+ --var DRIVE_URL_SECRET:<32 or more characters> --var DRIVE_REGION:unknown
148
+ ```
149
+
150
+ Open `http://127.0.0.1:8787` and sign in with a bearer token. The tab keeps the token until you close it. A fresh local database has no tokens. `e2e/serve.mjs` shows how to seed a person and an agent.
151
+
152
+ Test it with `npm run test:e2e`. The test starts `wrangler dev` on a seeded database, drives the UI in Chromium, and runs an axe WCAG 2.2 AA check on each view in both themes. Run `npx playwright install chromium` once first. The run also writes the screenshots in [docs/ui](docs/ui).
153
+
154
+ Run `npm run build:ui` before `npm run test:e2e` or `wrangler deploy`, because the Worker config names the `ui-dist` folder. The UI answers with a content security policy that loads scripts and fonts only from its own origin, and it refuses to be framed.
155
+
156
+ Set `DRIVE_PUBLIC_URL` to the origin that serves the UI. A download link on another origin is refused.
157
+
158
+ ### Serve the UI from the package
159
+
160
+ The npm package ships the built UI in `ui-dist`. The package does not ship a `wrangler.jsonc`, so a project that deploys its own Worker adds the assets block to its config. Its Worker re-exports the package Worker. In a workspace, the package can sit in the root `node_modules`. Then point `assets.directory` at that folder, such as `../../node_modules/agentic-drive/ui-dist`:
161
+
162
+ ```js
163
+ // src/worker.js
164
+ export { default } from "agentic-drive/worker";
165
+ ```
166
+
167
+ ```jsonc
168
+ // wrangler.jsonc: the Worker reads the bindings DB (D1) and FILES (R2). Name yours the same.
169
+ // Create the bucket with `wrangler r2 bucket create agentic-drive-files --jurisdiction eu`.
170
+ // Create the database with `wrangler d1 create agentic-drive --location weur`, then put its id below.
171
+ {
172
+ "name": "agentic-drive",
173
+ "main": "src/worker.js",
174
+ "compatibility_date": "2026-01-01",
175
+ "compatibility_flags": ["nodejs_compat"],
176
+ "d1_databases": [{ "binding": "DB", "database_name": "agentic-drive", "database_id": "<your id>" }],
177
+ "r2_buckets": [{ "binding": "FILES", "bucket_name": "agentic-drive-files", "jurisdiction": "eu" }],
178
+ // In a workspace, point this at the root node_modules instead.
179
+ "assets": { "directory": "./node_modules/agentic-drive/ui-dist", "not_found_handling": "single-page-application", "run_worker_first": ["/api/*", "/health"] }
180
+ }
181
+ ```
182
+
183
+ Set `DRIVE_URL_SECRET` and `DRIVE_PUBLIC_URL` with `npx wrangler secret put`. `DRIVE_URL_SECRET` needs 32 bytes or more. `DRIVE_PUBLIC_URL` is the origin of this Worker. The Worker declares region `eu` unless `DRIVE_REGION` says otherwise, so keep the EU jurisdiction and location above. Sign-in uses agentic-identity credentials in the same D1 database. Create a token there first, or every bearer API request gets 401. In the D1 database:
184
+
185
+ 1. Create the table, unless a request has already run: `wrangler d1 execute agentic-drive --remote --command "CREATE TABLE IF NOT EXISTS store_docs (collection TEXT NOT NULL, id TEXT NOT NULL, body TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (collection, id)) WITHOUT ROWID"`.
186
+ 2. Insert a row into `store_docs` with `collection` `principals` and `id` `me`. Its body is `{ "id": "me", "kind": "human", "scopes": ["drive:admin"] }`.
187
+ 3. Make the token with `openssl rand -hex 32`. A token may use only letters, digits, `_`, and `-`, up to 512 characters. Insert a row into `store_docs` with `collection` `credentials`. Its `id` is the lowercase hex SHA-256 of the token, which `printf %s "$TOKEN" | sha256sum` prints. Its body is `{ "principalId": "me", "kind": "api_key" }`.
188
+
189
+ The [seed script](https://github.com/pooriaarab/agentic-drive/blob/main/e2e/serve.mjs) shows these rows. Its tokens are for tests only. Never use them in a deployment. Without both secrets, the API answers 503. With this config, `/` serves the UI, and `/health` and `/api/v1/drive/*` answer from the Worker.
190
+
191
+ The UI does not make snapshot links yet. The address of a share link shows once, when you create it.
192
+
104
193
  ## Features
105
194
 
106
195
  - Folders, files, and typed references to documents in other apps (`docs:<id>`).
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ROOT_ID,
3
3
  rootResource
4
- } from "./chunk-VSVIJBMJ.js";
4
+ } from "./chunk-T6C5Y2WA.js";
5
5
 
6
6
  // src/migrate.ts
7
7
  var PAGE = 10;
@@ -1,9 +1,9 @@
1
1
  // src/drive.ts
2
- import { EraseRefusedError, MAX_FOLDER_DEPTH, SUBJECT_KINDS } from "agentic-identity";
2
+ import { EraseRefusedError, MAX_FOLDER_DEPTH, PRINCIPAL_KINDS, SUBJECT_KINDS } from "agentic-identity";
3
3
  var ROOT_ID = "root";
4
4
  var NODES = "drive_nodes";
5
5
  var NAMES = "drive_names";
6
- var ACTOR_KINDS = ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
6
+ var ACTOR_KINDS = PRINCIPAL_KINDS;
7
7
  var DriveError = class extends Error {
8
8
  constructor(code, message) {
9
9
  super(message);
@@ -1283,19 +1283,64 @@ function createFileBlobs(dir, options = {}) {
1283
1283
  };
1284
1284
  }
1285
1285
 
1286
+ // src/auth.ts
1287
+ import { createStoreAuth } from "agentic-identity";
1288
+ async function authenticate(request, store, now) {
1289
+ if (!/^Bearer [A-Za-z0-9_-]{1,512}$/.test(request.headers.get("authorization") ?? "")) return null;
1290
+ const principal = await createStoreAuth(store, now ? { now } : {}).authenticate(request);
1291
+ return principal ? { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal } : null;
1292
+ }
1293
+
1286
1294
  // src/local.ts
1295
+ import { createHash as createHash2 } from "crypto";
1287
1296
  import { join as join2 } from "path";
1288
- function openLocal(options) {
1289
- const kind = options.kind ?? "human";
1290
- if (!ACTOR_KINDS.includes(kind)) throw new Error(`unknown actor kind "${kind}"`);
1291
- return {
1292
- drive: createDrive({ store: createFileStore(join2(options.dir, "db")), blobs: createFileBlobs(join2(options.dir, "blobs")) }),
1293
- ctx: { owner: options.owner, actor: { id: options.actor ?? options.owner, kind } }
1294
- };
1297
+ import { isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
1298
+ function openLocal(dir) {
1299
+ const store = createFileStore(join2(dir, "db"));
1300
+ return { store, drive: createDrive({ store, blobs: createFileBlobs(join2(dir, "blobs")) }) };
1301
+ }
1302
+ var AuthError = class extends Error {
1303
+ code = "UNAUTHENTICATED";
1304
+ };
1305
+ var NO_TOKEN = "no principal: pass --token <key> or set AGENTIC_DRIVE_TOKEN to an API key";
1306
+ var BAD_TOKEN = "the key is not valid: it is unknown, revoked, or expired, or its principal is revoked";
1307
+ async function resolveToken(store, token) {
1308
+ if (!token) throw new AuthError(NO_TOKEN);
1309
+ if (!/^[A-Za-z0-9_-]{1,512}$/.test(token)) throw new AuthError(BAD_TOKEN);
1310
+ const ctx = await authenticate(new Request("http://local", { headers: { authorization: `Bearer ${token}` } }), store);
1311
+ if (!ctx) throw new AuthError(BAD_TOKEN);
1312
+ return ctx;
1313
+ }
1314
+ async function addPrincipal(store, id, opts) {
1315
+ if (!/^[A-Za-z0-9_.@-]{1,128}$/.test(id)) throw new Error(`invalid principal id "${id}"`);
1316
+ const scopes = opts.scopes?.length ? opts.scopes : [opts.parent ? "drive:write" : "drive:admin"];
1317
+ const bad = scopes.find((s) => !isScope(s));
1318
+ if (bad !== void 0) throw new Error(`invalid scope "${bad}"`);
1319
+ if (await store.get("principals", id)) throw new Error(`principal "${id}" exists`);
1320
+ if (opts.parent && !await store.get("principals", opts.parent)) throw new Error(`parent "${opts.parent}" does not exist`);
1321
+ const parent = opts.parent ? await store.get("principals", opts.parent) : null;
1322
+ if (parent && parent.kind !== "human") throw new Error(`parent "${opts.parent}" is not a person`);
1323
+ const doc = opts.parent ? { id, kind: "agent_of_human", parentId: opts.parent, scopes } : { id, kind: "human", scopes };
1324
+ await store.put("principals", id, doc);
1325
+ return doc;
1326
+ }
1327
+ async function revokePrincipal(store, id) {
1328
+ const doc = await store.get("principals", id);
1329
+ if (!doc) throw new DriveError("NOT_FOUND", `principal "${id}" does not exist`);
1330
+ await store.put("principals", id, { ...doc, revokedAt: Date.now() });
1331
+ }
1332
+ async function issueKey(store, principalId, label) {
1333
+ if (!await store.get("principals", principalId)) throw new Error(`principal "${principalId}" does not exist`);
1334
+ return issueCredential(store, { principalId, kind: "api_key", ...label ? { label } : {} });
1335
+ }
1336
+ async function revokeKey(store, token) {
1337
+ if (!await store.get("credentials", createHash2("sha256").update(token).digest("hex"))) throw new DriveError("NOT_FOUND", "no such key");
1338
+ await revokeCredential(store, token, Date.now());
1295
1339
  }
1296
1340
  function publicNode(node, path) {
1297
1341
  const { nameKey: _nameKey, blobKey: _blobKey, purging: _purging, ...rest } = node;
1298
- return path === void 0 ? rest : { path, ...rest };
1342
+ const shown = { ...rest, machineMade: isMachineMade(node.createdBy.kind) };
1343
+ return path === void 0 ? shown : { path, ...shown };
1299
1344
  }
1300
1345
 
1301
1346
  // src/upload.ts
@@ -1374,7 +1419,15 @@ export {
1374
1419
  buildExport,
1375
1420
  createFileStore,
1376
1421
  createFileBlobs,
1422
+ authenticate,
1377
1423
  openLocal,
1424
+ AuthError,
1425
+ NO_TOKEN,
1426
+ resolveToken,
1427
+ addPrincipal,
1428
+ revokePrincipal,
1429
+ issueKey,
1430
+ revokeKey,
1378
1431
  publicNode,
1379
1432
  DEFAULT_MAX_BYTES,
1380
1433
  sniffMime,
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.3.0" : readPackageVersion();
3
+ var VERSION = true ? "0.5.0" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
package/dist/cli.js CHANGED
@@ -1,14 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  VERSION
4
- } from "./chunk-GMA5I5L6.js";
4
+ } from "./chunk-WQHJNHAR.js";
5
5
  import {
6
+ AuthError,
6
7
  DriveError,
8
+ NO_TOKEN,
9
+ addPrincipal,
7
10
  buildExport,
11
+ issueKey,
8
12
  openLocal,
9
13
  publicNode,
14
+ resolveToken,
15
+ revokeKey,
16
+ revokePrincipal,
10
17
  upload
11
- } from "./chunk-VSVIJBMJ.js";
18
+ } from "./chunk-T6C5Y2WA.js";
12
19
 
13
20
  // src/cli.ts
14
21
  import { createReadStream, realpathSync, writeFileSync } from "fs";
@@ -37,23 +44,33 @@ Commands:
37
44
  register <ref> <name> Put an app document in the drive. Flag: --parent <folder>
38
45
  forget <ref> Move the references to an app document to the trash.
39
46
  delete-all --yes Erase you for good: your drive, and what you shared or held.
47
+ principal add <id> Make a person, or an agent with --of <person>. Flag: --scope <s> (repeat). No key needed.
48
+ principal revoke <id> Stop a principal, and every agent below it. No key needed.
49
+ key issue <principal> Print a new API key for a principal. It shows once. Flag: --label. No key needed.
50
+ key revoke <key> Stop a key from working. No key needed.
40
51
  mcp Start the MCP server on stdio.
41
52
 
42
53
  Flags:
43
54
  --dir <path> Data folder. Env AGENTIC_DRIVE_DIR. Default ./.agentic-drive
44
- --owner <id> Drive owner. Env AGENTIC_DRIVE_OWNER. Default "me"
45
- --actor <id> Who acts. Env AGENTIC_DRIVE_ACTOR. Default the owner
46
- --actor-kind <k> Actor kind. Env AGENTIC_DRIVE_KIND. Default human
55
+ --token <key> Who you are: an API key. Env AGENTIC_DRIVE_TOKEN. Needed by every command
56
+ except help, version, principal, and key. Prefer the env var: a flag shows in ps.
57
+ The key names a principal, found through agentic-identity. The drive belongs to
58
+ the person at the top of that principal's chain. An agent key marks items as
59
+ made by a machine. If the key is missing or not valid, the command exits with 3.
47
60
  --on-conflict <m> fail, rename, or replace
48
61
  -p, --parents Create missing parent folders
62
+
63
+ Exit codes: 0 done, 1 error, 2 bad usage, 3 no principal or an invalid key.
49
64
  `;
65
+ var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "forget", "delete-all", "mcp", "principal", "key"];
50
66
  var UsageError = class extends Error {
51
67
  };
52
68
  var options = {
53
69
  dir: { type: "string" },
54
- owner: { type: "string" },
55
- actor: { type: "string" },
56
- "actor-kind": { type: "string" },
70
+ token: { type: "string" },
71
+ of: { type: "string" },
72
+ scope: { type: "string", multiple: true },
73
+ label: { type: "string" },
57
74
  "on-conflict": { type: "string" },
58
75
  parents: { type: "boolean", short: "p" },
59
76
  mime: { type: "string" },
@@ -87,12 +104,21 @@ async function runCli(argv, io) {
87
104
  };
88
105
  const env = io.env;
89
106
  const dir = resolve(values.dir ?? env.AGENTIC_DRIVE_DIR ?? ".agentic-drive");
90
- const { drive, ctx } = openLocal({
91
- dir,
92
- owner: values.owner ?? env.AGENTIC_DRIVE_OWNER ?? "me",
93
- actor: values.actor ?? env.AGENTIC_DRIVE_ACTOR,
94
- kind: values["actor-kind"] ?? env.AGENTIC_DRIVE_KIND
95
- });
107
+ if (!COMMANDS.includes(command)) throw new UsageError(`unknown command "${command}"`);
108
+ const admin = command === "principal" || command === "key";
109
+ const token = values.token ?? env.AGENTIC_DRIVE_TOKEN;
110
+ if (!admin && !token) throw new AuthError(NO_TOKEN);
111
+ const { drive, store } = openLocal(dir);
112
+ if (admin || command === "key") {
113
+ const [sub, target] = args;
114
+ const out = (value) => io.out(JSON.stringify(value));
115
+ if (command === "principal" && sub === "add" && target) return out(await addPrincipal(store, target, { parent: values.of, scopes: values.scope })), 0;
116
+ if (command === "principal" && sub === "revoke" && target) return await revokePrincipal(store, target), out({ id: target, revoked: true }), 0;
117
+ if (command === "key" && sub === "issue" && target) return out({ principal: target, token: await issueKey(store, target, values.label) }), 0;
118
+ if (command === "key" && sub === "revoke" && target) return await revokeKey(store, target), out({ revoked: true }), 0;
119
+ throw new UsageError("usage: agentic-drive principal add <id> [--of <person>] [--scope <s>] | principal revoke <id> | key issue <principal> | key revoke <key>");
120
+ }
121
+ const ctx = await resolveToken(store, token);
96
122
  const onConflict = values["on-conflict"];
97
123
  if (onConflict && !["fail", "rename", "replace"].includes(onConflict)) throw new UsageError("--on-conflict needs fail, rename, or replace");
98
124
  const print = (value) => io.out(JSON.stringify(value));
@@ -199,6 +225,10 @@ async function runCli(argv, io) {
199
225
  io.err(JSON.stringify({ error: { code: "USAGE", message: error.message } }));
200
226
  return 2;
201
227
  }
228
+ if (error instanceof AuthError) {
229
+ io.err(JSON.stringify({ error: { code: error.code, message: error.message } }));
230
+ return 3;
231
+ }
202
232
  if (error instanceof DriveError) {
203
233
  io.err(JSON.stringify({ error: { code: error.code, message: error.message } }));
204
234
  return 1;
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { E as ErrorCode, N as Node, S as SearchQuery } from './drive-Dlzwukjq.js';
1
+ import { E as ErrorCode, N as Node, S as SearchQuery } from './drive-Cn3mpuDW.js';
2
2
  import 'agentic-identity';
3
3
 
4
4
  type Role = "viewer" | "commenter" | "editor" | "owner";
@@ -9,7 +9,10 @@ type Subject = {
9
9
  id: string;
10
10
  };
11
11
  /** An item as the API prints it: no storage keys. */
12
- type Item = Omit<Node, "nameKey" | "blobKey" | "purging">;
12
+ /** `machineMade` is true when the creator is not a human principal. */
13
+ type Item = Omit<Node, "nameKey" | "blobKey" | "purging"> & {
14
+ machineMade: boolean;
15
+ };
13
16
  /** A search hit, or a home item: an item with its path. */
14
17
  type PathItem = Item & {
15
18
  path: string;
package/dist/client.js CHANGED
@@ -43,11 +43,11 @@ var record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
43
43
  var text = (v) => typeof v === "string";
44
44
  var orNull = (test) => (v) => v === null || test(v);
45
45
  var num = (v) => typeof v === "number";
46
- var ACTOR_KINDS = ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
46
+ var ACTOR_KINDS = { human: true, agent_of_human: true, agent_of_agent: true, agent: true, user_token: true };
47
47
  var ROLES = ["viewer", "commenter", "editor", "owner"];
48
48
  var oneOf = (list) => (v) => typeof v === "string" && list.includes(v);
49
- var actor = (v) => record(v) && text(v.id) && oneOf(ACTOR_KINDS)(v.kind);
50
- var isItem = (v) => record(v) && text(v.id) && text(v.owner) && text(v.parentId) && text(v.name) && (v.kind === "folder" || v.kind === "file" || v.kind === "ref") && num(v.size) && orNull(text)(v.mime) && orNull(text)(v.hash) && orNull(text)(v.ref) && num(v.createdAt) && num(v.updatedAt) && actor(v.createdBy) && actor(v.updatedBy) && text(v.createdById) && text(v.updatedById) && typeof v.trashed === "boolean" && orNull(num)(v.trashedAt);
49
+ var actor = (v) => record(v) && text(v.id) && oneOf(Object.keys(ACTOR_KINDS))(v.kind);
50
+ var isItem = (v) => record(v) && text(v.id) && text(v.owner) && text(v.parentId) && text(v.name) && (v.kind === "folder" || v.kind === "file" || v.kind === "ref") && num(v.size) && orNull(text)(v.mime) && orNull(text)(v.hash) && orNull(text)(v.ref) && num(v.createdAt) && num(v.updatedAt) && actor(v.createdBy) && actor(v.updatedBy) && text(v.createdById) && text(v.updatedById) && typeof v.trashed === "boolean" && orNull(num)(v.trashedAt) && typeof v.machineMade === "boolean";
51
51
  var isPathItem = (v) => isItem(v) && typeof v.path === "string";
52
52
  var listOf = (check) => (v) => Array.isArray(v) && v.every(check);
53
53
  var shape = (test) => (v) => record(v) && test(v);
@@ -1,9 +1,9 @@
1
1
  import * as agentic_identity from 'agentic-identity';
2
- import { PrincipalContext, Store, Blobs, createSharing, Subject, Role, SubjectKind } from 'agentic-identity';
2
+ import { PrincipalKind, PrincipalContext, Store, Blobs, createSharing, Subject, Role, SubjectKind } from 'agentic-identity';
3
3
 
4
4
  declare const ROOT_ID = "root";
5
5
  declare const ACTOR_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
6
- type ActorKind = (typeof ACTOR_KINDS)[number];
6
+ type ActorKind = PrincipalKind;
7
7
  type Actor = {
8
8
  id: string;
9
9
  kind: ActorKind;
@@ -331,4 +331,4 @@ declare function createDrive(deps: DriveDeps): {
331
331
  }>;
332
332
  };
333
333
 
334
- export { type ActorKind as A, type Ctx as C, type Drive as D, type ErrorCode as E, type Node as N, type OnConflict as O, ROOT_ID as R, type SearchQuery as S, type Actor as a, ACTOR_KINDS as b, type Action as c, type Can as d, type DriveDeps as e, DriveError as f, createDrive as g, parseSubject as h, parseName as p, subjectKindOf as s };
334
+ export { type Actor as A, type Ctx as C, type Drive as D, type ErrorCode as E, type Node as N, type OnConflict as O, ROOT_ID as R, type SearchQuery as S, ACTOR_KINDS as a, type Action as b, type ActorKind as c, type Can as d, type DriveDeps as e, DriveError as f, createDrive as g, parseSubject as h, parseName as p, subjectKindOf as s };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { O as OnConflict, D as Drive, C as Ctx, N as Node, A as ActorKind, a as Actor } from './drive-Dlzwukjq.js';
2
- export { b as ACTOR_KINDS, c as Action, d as Can, e as DriveDeps, f as DriveError, E as ErrorCode, R as ROOT_ID, S as SearchQuery, g as createDrive, p as parseName, h as parseSubject, s as subjectKindOf } from './drive-Dlzwukjq.js';
3
- import { AdapterOptions, Blobs, Store } from 'agentic-identity';
1
+ import { O as OnConflict, D as Drive, C as Ctx, N as Node, A as Actor } from './drive-Cn3mpuDW.js';
2
+ export { a as ACTOR_KINDS, b as Action, c as ActorKind, d as Can, e as DriveDeps, f as DriveError, E as ErrorCode, R as ROOT_ID, S as SearchQuery, g as createDrive, p as parseName, h as parseSubject, s as subjectKindOf } from './drive-Cn3mpuDW.js';
3
+ import { AdapterOptions, Blobs, Store, Doc } from 'agentic-identity';
4
4
  export { B as BackfillOptions, a as BackfillResult, b as backfillAuthorIds, c as backfillResources } from './migrate-B79gRPMu.js';
5
5
 
6
6
  declare const VERSION: string;
@@ -51,22 +51,33 @@ declare function writeZip(entries: ZipEntry[], at: number): Uint8Array;
51
51
  /** Reads a zip made by `writeZip`. It checks every CRC. */
52
52
  declare function readZip(bytes: Uint8Array): Map<string, Uint8Array>;
53
53
 
54
- interface LocalOptions {
55
- /** Where the drive keeps its data. */
56
- dir: string;
57
- /** Whose drive this is. */
58
- owner: string;
59
- /** Who acts. Defaults to the owner. */
60
- actor?: string;
61
- /** What kind of actor. Defaults to `human`. Set an agent kind when an agent runs the command. */
62
- kind?: ActorKind;
63
- }
64
- declare function openLocal(options: LocalOptions): {
54
+ /** The drive and the store behind it, both kept in `dir`. The store holds the principals and keys too. */
55
+ declare function openLocal(dir: string): {
65
56
  drive: Drive;
66
- ctx: Ctx;
57
+ store: Store;
67
58
  };
68
- /** A node as the CLI and MCP print it: no storage keys. */
59
+ /** Why no caller could be named. The CLI turns it into exit code 3. */
60
+ declare class AuthError extends Error {
61
+ readonly code = "UNAUTHENTICATED";
62
+ }
63
+ /**
64
+ * The caller behind `token`, resolved through agentic-identity like the Worker does. The owner is the root
65
+ * of the delegation chain, and the actor is the principal itself, so an agent key shows as a machine.
66
+ */
67
+ declare function resolveToken(store: Store, token: string | undefined): Promise<Ctx>;
68
+ /** Make a principal. One with a parent is an agent of that human. One without is a human. Local data is the trust boundary. */
69
+ declare function addPrincipal(store: Store, id: string, opts: {
70
+ parent?: string;
71
+ scopes?: string[];
72
+ }): Promise<Doc>;
73
+ /** Stop a principal. Every agent below it stops too, because its chain is no longer live. */
74
+ declare function revokePrincipal(store: Store, id: string): Promise<void>;
75
+ /** Issue an API key for a principal. The token is shown once. */
76
+ declare function issueKey(store: Store, principalId: string, label?: string): Promise<string>;
77
+ declare function revokeKey(store: Store, token: string): Promise<void>;
78
+ /** A node as the CLI and MCP print it: no storage keys. `machineMade` is true when the creator is not a human principal. */
69
79
  declare function publicNode(node: Node, path?: string): {
80
+ machineMade: boolean;
70
81
  id: string;
71
82
  owner: string;
72
83
  parentId: string;
@@ -85,6 +96,7 @@ declare function publicNode(node: Node, path?: string): {
85
96
  trashed: boolean;
86
97
  trashedAt: number | null;
87
98
  } | {
99
+ machineMade: boolean;
88
100
  id: string;
89
101
  owner: string;
90
102
  parentId: string;
@@ -105,4 +117,4 @@ declare function publicNode(node: Node, path?: string): {
105
117
  path: string;
106
118
  };
107
119
 
108
- export { Actor, ActorKind, Ctx, DEFAULT_MAX_BYTES, Drive, Node, OnConflict, type UploadOptions, VERSION, buildExport, createFileBlobs, createFileStore, openLocal, publicNode, readZip, sniffMime, upload, writeZip };
120
+ export { Actor, AuthError, Ctx, DEFAULT_MAX_BYTES, Drive, Node, OnConflict, type UploadOptions, VERSION, addPrincipal, buildExport, createFileBlobs, createFileStore, issueKey, openLocal, publicNode, readZip, resolveToken, revokeKey, revokePrincipal, sniffMime, upload, writeZip };
package/dist/index.js CHANGED
@@ -1,46 +1,58 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-GMA5I5L6.js";
3
+ } from "./chunk-WQHJNHAR.js";
4
4
  import {
5
5
  backfillAuthorIds,
6
6
  backfillResources
7
- } from "./chunk-5KEISBS4.js";
7
+ } from "./chunk-B7E2Q3RB.js";
8
8
  import {
9
9
  ACTOR_KINDS,
10
+ AuthError,
10
11
  DEFAULT_MAX_BYTES,
11
12
  DriveError,
12
13
  ROOT_ID,
14
+ addPrincipal,
13
15
  buildExport,
14
16
  createDrive,
15
17
  createFileBlobs,
16
18
  createFileStore,
19
+ issueKey,
17
20
  openLocal,
18
21
  parseName,
19
22
  parseSubject,
20
23
  publicNode,
21
24
  readZip,
25
+ resolveToken,
26
+ revokeKey,
27
+ revokePrincipal,
22
28
  sniffMime,
23
29
  subjectKindOf,
24
30
  upload,
25
31
  writeZip
26
- } from "./chunk-VSVIJBMJ.js";
32
+ } from "./chunk-T6C5Y2WA.js";
27
33
  export {
28
34
  ACTOR_KINDS,
35
+ AuthError,
29
36
  DEFAULT_MAX_BYTES,
30
37
  DriveError,
31
38
  ROOT_ID,
32
39
  VERSION,
40
+ addPrincipal,
33
41
  backfillAuthorIds,
34
42
  backfillResources,
35
43
  buildExport,
36
44
  createDrive,
37
45
  createFileBlobs,
38
46
  createFileStore,
47
+ issueKey,
39
48
  openLocal,
40
49
  parseName,
41
50
  parseSubject,
42
51
  publicNode,
43
52
  readZip,
53
+ resolveToken,
54
+ revokeKey,
55
+ revokePrincipal,
44
56
  sniffMime,
45
57
  subjectKindOf,
46
58
  upload,
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { D as Drive, C as Ctx } from './drive-Dlzwukjq.js';
2
+ import { D as Drive, C as Ctx } from './drive-Cn3mpuDW.js';
3
3
  import 'agentic-identity';
4
4
 
5
5
  declare function createMcpServer(drive: Drive, ctx: Ctx, exportDir: string): McpServer;
package/dist/mcp.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-GMA5I5L6.js";
3
+ } from "./chunk-WQHJNHAR.js";
4
4
  import {
5
5
  ACTOR_KINDS,
6
6
  DriveError,
7
7
  buildExport,
8
8
  publicNode,
9
9
  upload
10
- } from "./chunk-VSVIJBMJ.js";
10
+ } from "./chunk-T6C5Y2WA.js";
11
11
 
12
12
  // src/mcp.ts
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";