agentic-drive 0.6.0 → 0.7.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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ Read the breaking changes before you upgrade.
6
+
7
+ ### Breaking changes
8
+
9
+ - **The local drive has sharing on.** `openLocal` now passes `createSharing(store)`, as the Worker does. Items made before 0.7.0 have no `resources` docs. The CLI and the `mcp` command add them once, on the first drive command that has a valid key. The `principal` and `key` commands do not do it. A project that calls `openLocal` itself must run `backfillResources(store)`, exported from the package root, once on its existing data folder. If an item has no owner or parent, the backfill stops, and drive commands fail until you fix that item.
10
+
11
+ ### Added
12
+
13
+ - **Sharing in the CLI.** New commands: `shares`, `share`, `unshare`, `invite`, `accept-invite`, `link-create`, and `link-revoke`. They act like the HTTP routes and show in `--help`.
14
+ - **Sharing in the MCP server.** New tools: `drive_shares`, `drive_share`, `drive_unshare`, `drive_invite`, `drive_accept_invite`, `drive_create_link`, and `drive_revoke_link`.
15
+ - **`drive_delete_account` in the MCP server.** It erases the caller like `DELETE /account` and `delete-all`. It needs `confirm: "yes"`.
16
+
17
+ ## 0.6.1
18
+
19
+ ### Changed
20
+
21
+ - **Bearer tokens are checked by the agentic-identity HTTP helper.** The Worker and the local key check now call `requirePrincipal` from `agentic-identity/http` (agentic-identity 0.6). The package no longer has its own copy of the bearer-token check in `src/auth.ts` and `src/local.ts`. A request without an `authorization` header still gets no caller, so a session cookie does not sign in. A bad key still gives 401, and `resolveToken` still throws `AuthError` with the code `UNAUTHENTICATED`.
22
+ - **The dependency is agentic-identity 0.6.** The package now needs `agentic-identity` `^0.6.0`.
23
+
3
24
  ## 0.6.0
4
25
 
5
26
  ### Added
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Store, sort, and share files and folders.
9
9
 
10
- > Status: 0.6.0, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK and the HTTP API.
10
+ > Status: 0.7.0, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK, the HTTP API, the CLI, and the MCP server.
11
11
  > A small web UI works. See [UI](#ui).
12
12
 
13
13
  ## Live demo
@@ -305,7 +305,7 @@ export default { fetch: app.fetch };
305
305
  - The D1 store checks every document a transaction read. If another Worker changed one, the transaction runs again, and after 50 tries it fails with a `BUSY` error.
306
306
  - The D1 store keeps one generic table. A busy deployment needs an expression index on `owner` and `parentId`.
307
307
  - `region: "eu"` on the D1 and R2 adapters is a promise made at deploy time. The D1 adapter refuses a query that a non-EU region served. The code cannot check where the bucket lives.
308
- - Sharing is on in the Worker and in the SDK (`createDrive({ sharing })`). The CLI and the MCP server work on a local drive with one owner, so they do not share yet.
308
+ - Sharing is on in the Worker and in the SDK (`createDrive({ sharing })`). The CLI and the MCP server turn it on too. The CLI has `shares`, `share`, `unshare`, `invite`, `accept-invite`, `link-create`, and `link-revoke`. The MCP server has `drive_shares`, `drive_share`, `drive_unshare`, `drive_invite`, `drive_accept_invite`, `drive_create_link`, `drive_revoke_link`, and `drive_delete_account` (pass `confirm: "yes"`). On the first run after the upgrade, the CLI gives older local items their `resources` docs, once.
309
309
  - Drive writes the `resources` doc of each item itself, in the same transaction, because identity has no call that does it in a transaction. A drive with `sharing` off keeps no such docs. Turn sharing on at the start, or run `backfillResources` after you turn it on.
310
310
  - An invite sends no message. The caller passes the invite id on.
311
311
  - Sharing follows a path of 16 resources, the drive root included, so an item sits at most 15 folders deep. A deeper path, move, or restore fails with `TOO_DEEP`. A move or a restore of a folder spends up to 200 queries on its subfolders (never on files), once, outside the transaction, to count its height. A move up or sideways, a rename, and a file need no count. A bigger folder fails with `TOO_MANY_ITEMS`: move its parts one by one. An edit inside the folder while it moves can leave items past the cap. Nobody can then reach them, and moving the folder back up repairs it.
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.6.0" : readPackageVersion();
3
+ var VERSION = true ? "0.7.0" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
@@ -1346,19 +1346,94 @@ async function buildExport(drive, ctx, now = Date.now) {
1346
1346
 
1347
1347
  // src/auth.ts
1348
1348
  import { createStoreAuth } from "agentic-identity";
1349
+ import { requirePrincipal } from "agentic-identity/http";
1349
1350
  async function authenticate(request, store, now) {
1350
- if (!/^Bearer [A-Za-z0-9_-]{1,512}$/.test(request.headers.get("authorization") ?? "")) return null;
1351
- const principal = await createStoreAuth(store, now ? { now } : {}).authenticate(request);
1352
- return principal ? { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal } : null;
1351
+ if (!request.headers.has("authorization")) return null;
1352
+ const principal = await requirePrincipal(request, createStoreAuth(store, now ? { now } : {}));
1353
+ if (principal instanceof Response) return null;
1354
+ return { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal };
1355
+ }
1356
+ async function inviteIdentity(store, actorId) {
1357
+ const user = await store.get("users", actorId);
1358
+ const agent = await store.get("principals", actorId);
1359
+ return {
1360
+ ...user?.emailVerified === true && typeof user.email === "string" ? { email: user.email } : {},
1361
+ ...typeof agent?.handle === "string" ? { handle: agent.handle } : {}
1362
+ };
1363
+ }
1364
+
1365
+ // src/migrate.ts
1366
+ var PAGE = 10;
1367
+ var isText = (v) => typeof v === "string" && v !== "";
1368
+ async function walk(store, options, fix) {
1369
+ const { maxPages } = options;
1370
+ if (maxPages !== void 0 && !(Number.isSafeInteger(maxPages) && maxPages > 0)) {
1371
+ throw new Error("maxPages must be a positive whole number");
1372
+ }
1373
+ let fixed = 0;
1374
+ let cursor = options.cursor;
1375
+ for (let pages = 0; maxPages === void 0 || pages < maxPages; pages++) {
1376
+ const page = await store.query("drive_nodes", { limit: PAGE, cursor });
1377
+ fixed += await store.transaction(async (tx) => {
1378
+ const items = [];
1379
+ for (const item of page.items) {
1380
+ const data = await tx.get("drive_nodes", item.id);
1381
+ if (data) items.push({ id: item.id, data });
1382
+ }
1383
+ return fix(tx, items);
1384
+ });
1385
+ cursor = page.nextCursor;
1386
+ if (!cursor) return { fixed };
1387
+ }
1388
+ return { fixed, nextCursor: cursor };
1389
+ }
1390
+ function backfillAuthorIds(store, options = {}) {
1391
+ return walk(store, options, async (tx, items) => {
1392
+ let count = 0;
1393
+ for (const { id, data } of items) {
1394
+ const created = data.createdBy?.id;
1395
+ const updated = data.updatedBy?.id;
1396
+ if (!isText(created) || !isText(updated)) {
1397
+ throw new Error(`item ${id} has no author. Fix it before you run the backfill again.`);
1398
+ }
1399
+ if (data.createdById === created && data.updatedById === updated) continue;
1400
+ await tx.put("drive_nodes", id, { ...data, createdById: created, updatedById: updated });
1401
+ count++;
1402
+ }
1403
+ return count;
1404
+ });
1405
+ }
1406
+ function backfillResources(store, options = {}) {
1407
+ return walk(store, options, async (tx, items) => {
1408
+ const missing = [];
1409
+ for (const { id, data } of items) {
1410
+ if (!isText(data.owner) || !isText(data.parentId)) {
1411
+ throw new Error(`item ${id} has no owner or parent. Fix it before you run the backfill again.`);
1412
+ }
1413
+ if (await tx.get("resources", id)) continue;
1414
+ missing.push({ id, parentId: data.parentId === ROOT_ID ? await rootResource(data.owner) : data.parentId });
1415
+ }
1416
+ for (const { id, parentId } of missing) await tx.put("resources", id, { id, type: "drive", parentId });
1417
+ return missing.length;
1418
+ });
1353
1419
  }
1354
1420
 
1355
1421
  // src/local.ts
1356
1422
  import { createHash as createHash2 } from "crypto";
1357
1423
  import { join as join2 } from "path";
1358
- import { isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
1424
+ import { createSharing, isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
1359
1425
  function openLocal(dir) {
1360
1426
  const store = createFileStore(join2(dir, "db"));
1361
- return { store, drive: createDrive({ store, blobs: createFileBlobs(join2(dir, "blobs")) }) };
1427
+ return { store, drive: createDrive({ store, blobs: createFileBlobs(join2(dir, "blobs")), sharing: createSharing(store) }) };
1428
+ }
1429
+ async function ensureSharing(store) {
1430
+ if (await store.get("drive_meta", "sharing")) return;
1431
+ try {
1432
+ await backfillResources(store);
1433
+ } catch (error) {
1434
+ throw new Error(`the one-time sharing backfill of local items failed: ${error instanceof Error ? error.message : String(error)}`);
1435
+ }
1436
+ await store.put("drive_meta", "sharing", { backfilledAt: Date.now() });
1362
1437
  }
1363
1438
  var AuthError = class extends Error {
1364
1439
  code = "UNAUTHENTICATED";
@@ -1367,8 +1442,13 @@ var NO_TOKEN = "no principal: pass --token <key> or set AGENTIC_DRIVE_TOKEN to a
1367
1442
  var BAD_TOKEN = "the key is not valid: it is unknown, revoked, or expired, or its principal is revoked";
1368
1443
  async function resolveToken(store, token) {
1369
1444
  if (!token) throw new AuthError(NO_TOKEN);
1370
- if (!/^[A-Za-z0-9_-]{1,512}$/.test(token)) throw new AuthError(BAD_TOKEN);
1371
- const ctx = await authenticate(new Request("http://local", { headers: { authorization: `Bearer ${token}` } }), store);
1445
+ let request;
1446
+ try {
1447
+ request = new Request("http://local", { headers: { authorization: `Bearer ${token}` } });
1448
+ } catch {
1449
+ throw new AuthError(BAD_TOKEN);
1450
+ }
1451
+ const ctx = await authenticate(request, store);
1372
1452
  if (!ctx) throw new AuthError(BAD_TOKEN);
1373
1453
  return ctx;
1374
1454
  }
@@ -1411,7 +1491,6 @@ export {
1411
1491
  DEFAULT_URL_SECONDS,
1412
1492
  safeMime,
1413
1493
  parseName,
1414
- rootResource,
1415
1494
  subjectKindOf,
1416
1495
  parseSubject,
1417
1496
  createDrive,
@@ -1424,7 +1503,11 @@ export {
1424
1503
  readZip,
1425
1504
  buildExport,
1426
1505
  authenticate,
1506
+ inviteIdentity,
1507
+ backfillAuthorIds,
1508
+ backfillResources,
1427
1509
  openLocal,
1510
+ ensureSharing,
1428
1511
  AuthError,
1429
1512
  NO_TOKEN,
1430
1513
  resolveToken,
package/dist/cli.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  VERSION
4
- } from "./chunk-ES3NZ7SX.js";
4
+ } from "./chunk-EFO5PSJL.js";
5
5
  import {
6
6
  AuthError,
7
7
  DriveError,
8
8
  NO_TOKEN,
9
9
  addPrincipal,
10
10
  buildExport,
11
+ ensureSharing,
12
+ inviteIdentity,
11
13
  issueKey,
12
14
  openLocal,
13
15
  publicNode,
@@ -15,13 +17,14 @@ import {
15
17
  revokeKey,
16
18
  revokePrincipal,
17
19
  upload
18
- } from "./chunk-WJZNXLTR.js";
20
+ } from "./chunk-PJQ3CRVL.js";
19
21
 
20
22
  // src/cli.ts
21
23
  import { createReadStream, realpathSync, writeFileSync } from "fs";
22
24
  import { basename, join, resolve } from "path";
23
25
  import { pathToFileURL } from "url";
24
26
  import { parseArgs } from "util";
27
+ import { ROLES, SUBJECT_KINDS } from "agentic-identity";
25
28
  var HELP = `agentic-drive ${VERSION}
26
29
  Store, sort, and share files and folders.
27
30
 
@@ -43,6 +46,13 @@ Commands:
43
46
  home [--app <name>] List files and app documents, newest change first.
44
47
  register <ref> <name> Put an app document in the drive. Flag: --parent <folder>
45
48
  forget <ref> Move the references to an app document to the trash.
49
+ shares <path|id> Show who can reach an item, and by which links and invites. Owner only.
50
+ share <path|id> <subject> Give a role to a person or agent. Flags: --role <r> --subject-kind <human|agent>
51
+ unshare <path|id> <subject> Take a role away. Flag: --subject-kind <human|agent>
52
+ invite <path|id> Invite by --email (a person) or --handle (an agent). Flags: --role <r> --expires-in-ms <n>. Sends no message.
53
+ accept-invite <id> Accept an invite that names your verified email or agent handle.
54
+ link-create <path|id> Make a share link. The token shows once. Flags: --role <r> --expires-in-ms <n> --signed-in-only
55
+ link-revoke <id> Stop a share link.
46
56
  delete-all --yes Erase you for good: your drive, and what you shared or held.
47
57
  principal add <id> Make a person, or an agent with --of <person>. Flag: --scope <s> (repeat). No key needed.
48
58
  principal revoke <id> Stop a principal, and every agent below it. No key needed.
@@ -57,12 +67,13 @@ Flags:
57
67
  The key names a principal, found through agentic-identity. The drive belongs to
58
68
  the person at the top of that principal's chain. An agent key marks items as
59
69
  made by a machine. If the key is missing or not valid, the command exits with 3.
70
+ --role <r> share: owner, editor, commenter, or viewer. invite and link-create: editor, commenter, or viewer.
60
71
  --on-conflict <m> fail, rename, or replace
61
72
  -p, --parents Create missing parent folders
62
73
 
63
74
  Exit codes: 0 done, 1 error, 2 bad usage, 3 no principal or an invalid key.
64
75
  `;
65
- var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "forget", "delete-all", "mcp", "principal", "key"];
76
+ var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "forget", "shares", "share", "unshare", "invite", "accept-invite", "link-create", "link-revoke", "delete-all", "mcp", "principal", "key"];
66
77
  var UsageError = class extends Error {
67
78
  };
68
79
  var options = {
@@ -81,18 +92,35 @@ var options = {
81
92
  "min-size": { type: "string" },
82
93
  "max-size": { type: "string" },
83
94
  "created-by-kind": { type: "string" },
95
+ role: { type: "string" },
96
+ "subject-kind": { type: "string" },
97
+ email: { type: "string" },
98
+ handle: { type: "string" },
99
+ "expires-in-ms": { type: "string" },
100
+ "signed-in-only": { type: "boolean" },
84
101
  limit: { type: "string" },
85
102
  app: { type: "string" },
86
103
  yes: { type: "boolean" },
87
104
  help: { type: "boolean", short: "h" },
88
105
  version: { type: "boolean", short: "V" }
89
106
  };
107
+ function lifetime(value, flag) {
108
+ if (value === void 0) return void 0;
109
+ const n = Number(value);
110
+ if (value.trim() === "" || !Number.isSafeInteger(n) || n <= 0) throw new UsageError(`--${flag} needs a whole number above 0`);
111
+ return n;
112
+ }
90
113
  function num(value, flag) {
91
114
  if (value === void 0) return void 0;
92
115
  const n = Number(value);
93
116
  if (!Number.isFinite(n) || n < 0) throw new UsageError(`--${flag} needs a number of 0 or more`);
94
117
  return n;
95
118
  }
119
+ var LINK_ROLES = ROLES.filter((r) => r !== "owner");
120
+ function pick(value, allowed, flag) {
121
+ if (value === void 0 || !allowed.includes(value)) throw new UsageError(`--${flag} needs ${allowed.join(", ")}`);
122
+ return value;
123
+ }
96
124
  async function runCli(argv, io) {
97
125
  try {
98
126
  const { values, positionals } = parseArgs({ args: argv, options, allowPositionals: true, strict: true });
@@ -119,6 +147,7 @@ async function runCli(argv, io) {
119
147
  throw new UsageError("usage: agentic-drive principal add <id> [--of <person>] [--scope <s>] | principal revoke <id> | key issue <principal> | key revoke <key>");
120
148
  }
121
149
  const ctx = await resolveToken(store, token);
150
+ await ensureSharing(store);
122
151
  const onConflict = values["on-conflict"];
123
152
  if (onConflict && !["fail", "rename", "replace"].includes(onConflict)) throw new UsageError("--on-conflict needs fail, rename, or replace");
124
153
  const print = (value) => io.out(JSON.stringify(value));
@@ -207,13 +236,58 @@ async function runCli(argv, io) {
207
236
  need(1, "forget <ref>");
208
237
  print(await drive.forgetDocument(ctx, args[0]));
209
238
  break;
239
+ case "shares":
240
+ need(1, "shares <path|id>");
241
+ print(await drive.shares(ctx, args[0]));
242
+ break;
243
+ case "share":
244
+ need(2, "share <path|id> <subject> --role <r> --subject-kind <human|agent>");
245
+ await drive.shareWith(ctx, args[0], { kind: pick(values["subject-kind"], SUBJECT_KINDS, "subject-kind"), id: args[1] }, pick(values.role, ROLES, "role"));
246
+ print({ ok: true });
247
+ break;
248
+ case "unshare":
249
+ need(2, "unshare <path|id> <subject> --subject-kind <human|agent>");
250
+ await drive.unshare(ctx, args[0], { kind: pick(values["subject-kind"], SUBJECT_KINDS, "subject-kind"), id: args[1] });
251
+ print({ ok: true });
252
+ break;
253
+ case "invite": {
254
+ need(1, "invite <path|id> --role <r> (--email <e> | --handle <h>)");
255
+ const expiresInMs = lifetime(values["expires-in-ms"], "expires-in-ms");
256
+ print(await drive.invite(ctx, args[0], {
257
+ role: pick(values.role, LINK_ROLES, "role"),
258
+ ...values.email === void 0 ? {} : { email: values.email },
259
+ ...values.handle === void 0 ? {} : { handle: values.handle },
260
+ ...expiresInMs === void 0 ? {} : { expiresInMs }
261
+ }));
262
+ break;
263
+ }
264
+ case "accept-invite":
265
+ need(1, "accept-invite <id>");
266
+ await drive.acceptInvite(ctx, args[0], await inviteIdentity(store, ctx.actor.id));
267
+ print({ ok: true });
268
+ break;
269
+ case "link-create": {
270
+ need(1, "link-create <path|id> --role <r>");
271
+ const expiresInMs = lifetime(values["expires-in-ms"], "expires-in-ms");
272
+ print(await drive.createLink(ctx, args[0], {
273
+ role: pick(values.role, LINK_ROLES, "role"),
274
+ signedInOnly: values["signed-in-only"] === true,
275
+ ...expiresInMs === void 0 ? {} : { expiresAt: Date.now() + expiresInMs }
276
+ }));
277
+ break;
278
+ }
279
+ case "link-revoke":
280
+ need(1, "link-revoke <id>");
281
+ await drive.revokeLink(ctx, args[0]);
282
+ print({ ok: true });
283
+ break;
210
284
  case "delete-all":
211
285
  if (!values.yes) throw new UsageError("delete-all erases you for good. Add --yes to confirm.");
212
286
  print(await drive.erasePerson(ctx));
213
287
  break;
214
288
  case "mcp": {
215
289
  const { startMcp } = await import("./mcp.js");
216
- await startMcp({ drive, ctx, exportDir: join(dir, "exports") });
290
+ await startMcp({ drive, ctx, exportDir: join(dir, "exports"), store });
217
291
  return 0;
218
292
  }
219
293
  default:
package/dist/index.d.ts CHANGED
@@ -51,7 +51,7 @@ 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
- /** The drive and the store behind it, both kept in `dir`. The store holds the principals and keys too. */
54
+ /** The drive and the store behind it, both kept in `dir`. The store holds the principals and keys too. Sharing is on, as in the Worker. */
55
55
  declare function openLocal(dir: string): {
56
56
  drive: Drive;
57
57
  store: Store;
package/dist/index.js CHANGED
@@ -1,10 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-ES3NZ7SX.js";
4
- import {
5
- backfillAuthorIds,
6
- backfillResources
7
- } from "./chunk-BXFK3QXK.js";
3
+ } from "./chunk-EFO5PSJL.js";
8
4
  import {
9
5
  ACTOR_KINDS,
10
6
  AuthError,
@@ -12,6 +8,8 @@ import {
12
8
  DriveError,
13
9
  ROOT_ID,
14
10
  addPrincipal,
11
+ backfillAuthorIds,
12
+ backfillResources,
15
13
  buildExport,
16
14
  createDrive,
17
15
  createFileBlobs,
@@ -29,7 +27,7 @@ import {
29
27
  subjectKindOf,
30
28
  upload,
31
29
  writeZip
32
- } from "./chunk-WJZNXLTR.js";
30
+ } from "./chunk-PJQ3CRVL.js";
33
31
  export {
34
32
  ACTOR_KINDS,
35
33
  AuthError,
package/dist/mcp.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { D as Drive, C as Ctx } from './drive-BtRyexUP.js';
3
- import 'agentic-identity';
3
+ import { Store } from 'agentic-identity';
4
4
 
5
- declare function createMcpServer(drive: Drive, ctx: Ctx, exportDir: string): McpServer;
5
+ declare function createMcpServer(drive: Drive, ctx: Ctx, exportDir: string, store: Store): McpServer;
6
6
  declare function startMcp(deps: {
7
7
  drive: Drive;
8
8
  ctx: Ctx;
9
9
  exportDir: string;
10
+ store: Store;
10
11
  }): Promise<void>;
11
12
 
12
13
  export { createMcpServer, startMcp };
package/dist/mcp.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-ES3NZ7SX.js";
3
+ } from "./chunk-EFO5PSJL.js";
4
4
  import {
5
5
  ACTOR_KINDS,
6
6
  DriveError,
7
7
  buildExport,
8
+ inviteIdentity,
8
9
  publicNode,
9
10
  upload
10
- } from "./chunk-WJZNXLTR.js";
11
+ } from "./chunk-PJQ3CRVL.js";
11
12
 
12
13
  // src/mcp.ts
13
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -15,7 +16,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
15
16
  import { mkdirSync, writeFileSync } from "fs";
16
17
  import { join } from "path";
17
18
  import { z } from "zod";
19
+ import { ROLES, SUBJECT_KINDS } from "agentic-identity";
18
20
  var conflict = z.enum(["fail", "rename", "replace"]).optional();
21
+ var role = z.enum(ROLES);
22
+ var linkRole = z.enum(ROLES.filter((r) => r !== "owner"));
23
+ var subjectKind = z.enum(SUBJECT_KINDS);
24
+ var lifetime = z.number().int().positive().optional().describe("How long it lasts, in milliseconds.");
19
25
  var loc = z.string().describe('A path, such as "/x" or "<folder id>/x", or an item id.');
20
26
  var ok = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }] });
21
27
  function failure(error) {
@@ -23,7 +29,7 @@ function failure(error) {
23
29
  return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: { code: error.code, message: error.message } }) }] };
24
30
  }
25
31
  var EXPORT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}\.zip$/;
26
- function createMcpServer(drive, ctx, exportDir) {
32
+ function createMcpServer(drive, ctx, exportDir, store) {
27
33
  const server = new McpServer({ name: "agentic-drive", version: VERSION });
28
34
  const tool = (name, description, shape, run, annotations = {}) => server.registerTool(name, { description, inputSchema: shape, annotations }, (async (args) => {
29
35
  try {
@@ -86,6 +92,44 @@ function createMcpServer(drive, ctx, exportDir) {
86
92
  async ({ ref, name, folder }) => publicNode(await drive.registerDocument(ctx, { ref, name, folder }))
87
93
  );
88
94
  tool("drive_forget_document", "Move every reference to an app document to the trash.", { ref: z.string() }, async ({ ref }) => drive.forgetDocument(ctx, ref));
95
+ tool("drive_shares", "Show who can reach an item, and by which links and invites. Only an owner may look. Link tokens are never shown.", { path: loc }, async ({ path }) => drive.shares(ctx, path), { readOnlyHint: true });
96
+ tool(
97
+ "drive_share",
98
+ "Give a person or an agent a role on an item. A folder role passes down.",
99
+ { path: loc, subjectKind, subjectId: z.string(), role },
100
+ async ({ path, subjectKind: subjectKind2, subjectId, role: role2 }) => (await drive.shareWith(ctx, path, { kind: subjectKind2, id: subjectId }, role2), { ok: true })
101
+ );
102
+ tool(
103
+ "drive_unshare",
104
+ "Take a role away from a person or an agent on an item.",
105
+ { path: loc, subjectKind, subjectId: z.string() },
106
+ async ({ path, subjectKind: subjectKind2, subjectId }) => (await drive.unshare(ctx, path, { kind: subjectKind2, id: subjectId }), { ok: true }),
107
+ { destructiveHint: true }
108
+ );
109
+ tool(
110
+ "drive_invite",
111
+ "Invite by `email` (a person) or `handle` (an agent). It sends no message. The id comes back.",
112
+ { path: loc, role: linkRole, email: z.string().optional(), handle: z.string().optional(), expiresInMs: lifetime },
113
+ async ({ path, role: role2, email, handle, expiresInMs }) => drive.invite(ctx, path, { role: role2, ...email === void 0 ? {} : { email }, ...handle === void 0 ? {} : { handle }, ...expiresInMs === void 0 ? {} : { expiresInMs } })
114
+ );
115
+ tool("drive_accept_invite", "Accept an invite that names you by verified email or by agent handle.", { id: z.string() }, async ({ id }) => (await drive.acceptInvite(ctx, id, await inviteIdentity(store, ctx.actor.id)), { ok: true }));
116
+ tool(
117
+ "drive_create_link",
118
+ "Make a share link for an item. The token comes back once.",
119
+ { path: loc, role: linkRole, expiresInMs: lifetime, signedInOnly: z.boolean().optional() },
120
+ async ({ path, role: role2, expiresInMs, signedInOnly }) => drive.createLink(ctx, path, { role: role2, signedInOnly: signedInOnly === true, ...expiresInMs === void 0 ? {} : { expiresAt: Date.now() + expiresInMs } })
121
+ );
122
+ tool("drive_revoke_link", "Stop a share link.", { id: z.string() }, async ({ id }) => (await drive.revokeLink(ctx, id), { ok: true }), { destructiveHint: true });
123
+ tool(
124
+ "drive_delete_account",
125
+ 'Erase the caller for good: the drive, and what they shared or held. Only a person can. Pass confirm: "yes".',
126
+ { confirm: z.string() },
127
+ async ({ confirm }) => {
128
+ if (confirm !== "yes") throw new DriveError("INVALID_PATH", 'pass confirm: "yes" to erase you for good: your drive, and what you shared or held');
129
+ return drive.erasePerson(ctx);
130
+ },
131
+ { destructiveHint: true }
132
+ );
89
133
  tool(
90
134
  "drive_export",
91
135
  "Write everything the owner has to a new zip file in the export folder. Give a plain file name that ends in .zip. It never overwrites a file.",
@@ -108,7 +152,7 @@ function createMcpServer(drive, ctx, exportDir) {
108
152
  return server;
109
153
  }
110
154
  async function startMcp(deps) {
111
- await createMcpServer(deps.drive, deps.ctx, deps.exportDir).connect(new StdioServerTransport());
155
+ await createMcpServer(deps.drive, deps.ctx, deps.exportDir, deps.store).connect(new StdioServerTransport());
112
156
  }
113
157
  export {
114
158
  createMcpServer,
package/dist/worker.js CHANGED
@@ -1,18 +1,17 @@
1
- import {
2
- backfillAuthorIds,
3
- backfillResources
4
- } from "./chunk-BXFK3QXK.js";
5
1
  import {
6
2
  DEFAULT_MAX_BYTES,
7
3
  DEFAULT_URL_SECONDS,
8
4
  DriveError,
9
5
  authenticate,
6
+ backfillAuthorIds,
7
+ backfillResources,
10
8
  buildExport,
11
9
  createDrive,
10
+ inviteIdentity,
12
11
  publicNode,
13
12
  safeMime,
14
13
  upload
15
- } from "./chunk-WJZNXLTR.js";
14
+ } from "./chunk-PJQ3CRVL.js";
16
15
 
17
16
  // src/worker.ts
18
17
  import { Hono } from "hono";
@@ -255,12 +254,7 @@ function createApp(options = {}) {
255
254
  });
256
255
  api.post("/invites/accept", async (c) => {
257
256
  const id = str((await body(c)).id, "id");
258
- const user = await c.var.store.get("users", c.var.ctx.actor.id);
259
- const agent = await c.var.store.get("principals", c.var.ctx.actor.id);
260
- await c.var.drive.acceptInvite(c.var.ctx, id, {
261
- ...user?.emailVerified === true && typeof user.email === "string" ? { email: user.email } : {},
262
- ...typeof agent?.handle === "string" ? { handle: agent.handle } : {}
263
- });
257
+ await c.var.drive.acceptInvite(c.var.ctx, id, await inviteIdentity(c.var.store, c.var.ctx.actor.id));
264
258
  return c.json({ ok: true });
265
259
  });
266
260
  api.post("/links", async (c) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-drive",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Store, sort, and share files and folders.",
5
5
  "keywords": [
6
6
  "drive",
@@ -73,7 +73,7 @@
73
73
  "dependencies": {
74
74
  "@modelcontextprotocol/sdk": "^1.12.0",
75
75
  "agentic-design": "^0.1.0",
76
- "agentic-identity": "^0.4.0",
76
+ "agentic-identity": "^0.6.0",
77
77
  "hono": "^4.13.14",
78
78
  "zod": "^3.25.0"
79
79
  },
@@ -1,65 +0,0 @@
1
- import {
2
- ROOT_ID,
3
- rootResource
4
- } from "./chunk-WJZNXLTR.js";
5
-
6
- // src/migrate.ts
7
- var PAGE = 10;
8
- var isText = (v) => typeof v === "string" && v !== "";
9
- async function walk(store, options, fix) {
10
- const { maxPages } = options;
11
- if (maxPages !== void 0 && !(Number.isSafeInteger(maxPages) && maxPages > 0)) {
12
- throw new Error("maxPages must be a positive whole number");
13
- }
14
- let fixed = 0;
15
- let cursor = options.cursor;
16
- for (let pages = 0; maxPages === void 0 || pages < maxPages; pages++) {
17
- const page = await store.query("drive_nodes", { limit: PAGE, cursor });
18
- fixed += await store.transaction(async (tx) => {
19
- const items = [];
20
- for (const item of page.items) {
21
- const data = await tx.get("drive_nodes", item.id);
22
- if (data) items.push({ id: item.id, data });
23
- }
24
- return fix(tx, items);
25
- });
26
- cursor = page.nextCursor;
27
- if (!cursor) return { fixed };
28
- }
29
- return { fixed, nextCursor: cursor };
30
- }
31
- function backfillAuthorIds(store, options = {}) {
32
- return walk(store, options, async (tx, items) => {
33
- let count = 0;
34
- for (const { id, data } of items) {
35
- const created = data.createdBy?.id;
36
- const updated = data.updatedBy?.id;
37
- if (!isText(created) || !isText(updated)) {
38
- throw new Error(`item ${id} has no author. Fix it before you run the backfill again.`);
39
- }
40
- if (data.createdById === created && data.updatedById === updated) continue;
41
- await tx.put("drive_nodes", id, { ...data, createdById: created, updatedById: updated });
42
- count++;
43
- }
44
- return count;
45
- });
46
- }
47
- function backfillResources(store, options = {}) {
48
- return walk(store, options, async (tx, items) => {
49
- const missing = [];
50
- for (const { id, data } of items) {
51
- if (!isText(data.owner) || !isText(data.parentId)) {
52
- throw new Error(`item ${id} has no owner or parent. Fix it before you run the backfill again.`);
53
- }
54
- if (await tx.get("resources", id)) continue;
55
- missing.push({ id, parentId: data.parentId === ROOT_ID ? await rootResource(data.owner) : data.parentId });
56
- }
57
- for (const { id, parentId } of missing) await tx.put("resources", id, { id, type: "drive", parentId });
58
- return missing.length;
59
- });
60
- }
61
-
62
- export {
63
- backfillAuthorIds,
64
- backfillResources
65
- };