About
Store, sort, and share files and folders.
README
Store, sort, and share files and folders.
Status: 0.10.3, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK, the HTTP API, the CLI, and the MCP server. A small web UI works. See UI.
Live demo
A demo Worker runs at https://agentic-drive-demo.hello-fd1.workers.dev. GET /health answers 200, and the web UI is served at /.
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.
To redeploy it, set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in your shell, then run:
npm run deploy:demo
This uses wrangler.demo.jsonc (D1 in weur, R2 in the EU jurisdiction, workers_dev on). The URL signing secret lives in the Worker:
openssl rand -hex 32 | npx wrangler secret put DRIVE_URL_SECRET -c wrangler.demo.jsonc. Deploys are manual. No CI secret is used.
Install
npm install agentic-drive
Quick start
import { createDrive } from "agentic-drive";
import { createFileBlobs, createFileStore } from "agentic-identity/file";
const drive = createDrive({ store: createFileStore({ path: "./data/db" }), blobs: createFileBlobs({ dir: "./data/blobs" }) });
const me = { owner: "u1", actor: { id: "u1", kind: "human" as const } };
await drive.mkdir(me, "/reports/2026", { parents: true });
await drive.putFile(me, "/reports/2026/q1.txt", new TextEncoder().encode("hello"), { mime: "text/plain" });
await drive.putRef(me, "/reports/plan", "docs:abc123");
await drive.move(me, "/reports/plan", "/reports/2026");
await drive.trash(me, "/reports/2026");
CLI and MCP
Every command except help, --version, principal, and key needs a principal. Pass an API key with
--token <key> or AGENTIC_DRIVE_TOKEN. Prefer the variable: a flag shows in ps.
agentic-identity resolves the key. The drive belongs to the person at the top of the
principal's chain. An agent key marks each item it makes as made by a machine. A revoked,
expired, or unknown key fails with exit code 3.
npx agentic-drive principal add alice
export AGENTIC_DRIVE_TOKEN=$(npx agentic-drive key issue alice | jq -r .token)
npx agentic-drive principal add helper --of alice # an agent of alice
npx agentic-drive key issue helper # the key shows once
npx agentic-drive key revoke <key>
npx agentic-drive principal revoke helper # stops its keys, and its agents
principal and key need no key: anyone who can write to the --dir folder can run them.
Set --dir (or AGENTIC_DRIVE_DIR) to choose the data folder.
The local drive checks the owner only. It does not apply the scopes of an agent key.
npx agentic-drive mkdir -p /reports
npx agentic-drive upload ./q1.pdf /reports/
npx agentic-drive search --name q1
npx agentic-drive export ./everything.zip
npx agentic-drive mcp
Every command prints JSON, except help, --version, mcp, and download to -. A failure prints {"error":{"code","message"}} on stderr and exits 1.
Exit code 2 means bad usage. Exit code 3 means no principal, or a key that is not valid.
HTTP API
A Cloudflare Worker (Hono) serves /api/v1/drive and mirrors the CLI. It uses D1 and R2
through the shared adapters of agentic-identity.
npx wrangler secret put DRIVE_URL_SECRET # 32 bytes or more. It signs download URLs.
npx wrangler secret put DRIVE_PUBLIC_URL # The origin of the Worker, such as https://drive.example.com
npx wrangler deploy
curl -H "authorization: Bearer <token>" "$URL/api/v1/drive/items?path=/"
Routes: GET /items, GET /stat, POST /folders, PUT /files, GET /files, GET /url,
POST /move, POST /trash, GET /trash, POST /restore, POST /purge, GET /search,
GET /items/:id/activity, GET /export, DELETE /account?confirm=yes, GET /home, POST /documents, DELETE /documents?ref=, GET /trash/paths, GET /refs?ref=, POST /refs.
Sign in with a bearer token from agentic-identity. The token is a credential in the credentials collection of
the same D1 database, and it names a principal in principals. The Worker resolves the whole delegation chain on
every request. A missing, expired, or revoked credential, or a revoked or expired principal (or one above it), gets 401.
A session cookie is refused: only a bearer token counts. An agent acts in its owner's drive with the narrower scopes of its grant.
Scopes are drive:read, drive:write, and drive:admin.
Sharing routes: GET /shares?path=, POST /shares, DELETE /shares?path=&subjectId=&subjectKind=, POST /invites,
POST /invites/accept, POST /links, DELETE /links?id=. A link visitor reads with the header
x-link-token on GET /shared/:id, GET /shared/:id/items, and GET /shared/:id/content.
Name a shared item by its id. A path can start at a folder id: <folder id>/notes/a.txt.
Without DRIVE_URL_SECRET or DRIVE_PUBLIC_URL, every API request gets 503. /health still answers.
Create the R2 bucket with --jurisdiction eu and the D1 database with --location weur.
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.
SDK
agentic-drive/client is a typed client for the HTTP API. It uses only fetch, so it runs in browsers, Workers, and Node 22.
import { createClient, DriveApiError } from "agentic-drive/client";
const drive = createClient({ baseUrl: "https://drive.example.com", token: process.env.DRIVE_TOKEN });
await drive.mkdir("/docs", { parents: true });
const file = await drive.upload("/docs/a.txt", "hello", { mime: "text/plain" });
const { data } = await drive.download(file.id);
try {
await drive.stat("/missing");
} catch (error) {
if (error instanceof DriveApiError) console.log(error.code, error.status); // NOT_FOUND 404
}
- One method for each bearer route and each shared route:
list,stat,mkdir,upload,download,url,move,trash,listTrash,listActivity,restore,purge,search,export,deleteAccount,home,registerDocument,forgetDocument,findByRef,listTrashWithPaths,claimRef,shares,share,unshare,invite,acceptInvite,createLink,revokeLink,openShared,listShared, andreadShared. - Each method has typed input and output. The client checks the shape of every answer, and
downloadchecks the bytes against their hash (it needs WebCrypto).readShareddoes not: the Worker sends no hash there. An answer that fails a check throwsBAD_RESPONSE.urlgives a signed link that you fetch yourself. The last argument takes{ signal }to stop the call with anAbortSignal. - A failed call throws
DriveApiError. It has thecodeof the API and the HTTPstatus. A call that gets no answer has the codeNETWORK_ERRORand status 0. An abort stays the abort error offetch. A code that the client does not know givesBAD_RESPONSE. - The client never sends a write again.
retries(default 0) sends a read (GET) again after a network error or a 502, 503, or 504, with a short wait. A redirect is an error (BAD_RESPONSE, with the status of the redirect, or 0 in a browser): setbaseUrlto the final URL. fetchis an option, for tests and for runtimes with their own.openShared,listShared, andreadSharedwork without a token, with alinkToken.- A stream body for
uploadis refused by some browsers. The error says so, and nothing is sent. Items from before 0.2.0 needbackfillAuthorIds, and grants from before 0.3.0 need identity'smigrateGrants. Without them, an answer that holds one fails withBAD_RESPONSE. - A browser page needs the Worker on its own origin for now: the Worker sends no CORS headers.
- Share with a subject:
share("/pub", { kind: "human", id: "u2" }, "viewer").
Atomic app resource ownership
Trusted hosts can call drive.claimResourceRef(ctx, path, ref, ownership, opts).
Use the same Store for Drive and identity sharing. Check app access first. This
host-only helper is not exposed by HTTP, CLI, or MCP.
const item = await drive.claimResourceRef(ctx, "/Plan", "docs:plan", {
resourceId: "plan", type: "doc", subject: { kind: "human", id: "ana" },
});
await drive.trash(ctx, item.id);
await drive.restore(ctx, item.id);
The same claim at the same path returns the existing reference, including when
calls run at the same time. Options accept parents to create missing folders.
Resource ids use 1 to 128 letters, digits, underscores, or hyphens. Resource types
start with a lowercase letter and use lowercase letters, digits, underscores, or
hyphens. The reference, resource, and owner grant commit in one transaction. A conflict
throws DriveError with code REF_CONFLICT (HTTP status 409). A failed path or
owner grant leaves no reference or claim. One subject can hold several references.
Direct trash releases one claim. Restore reacquires it and can conflict with a
new owner. Trashing a folder only hides its children; their claims stay until
those references are trashed or removed. Hard removal releases live claims.
If identity erase has removed an old claim, local trash and removal still succeed.
They keep any claim that another subject now holds. Call erasePerson as part of
account erase. It clears the subject from claimed references in other drives,
even when another person made the reference. Restore then restores the item
without recreating the erased subject's ownership.
Existing resources must already grant the subject ownership. The identity
lifecycle keeps existing shares and resources when it releases a claim.
This helper needs agentic-identity 0.12.0 or later.
Other exports
The root export, agentic-drive, also exports these names. The package typings give the type of each one.
VERSIONis the package version.ROOT_IDis the id of a drive's root folder,"root".ACTOR_KINDSlists the five principal kinds thatagentic-identitynames.DEFAULT_MAX_BYTESis the upload cap: 25 MiB (26,214,400 bytes).sniffMime(head)returns the type that the first bytes name: PNG, JPEG, GIF, PDF, ZIP, gzip, or WebP. It returnsnullfor any other bytes.parseName(raw)returns{ name, key }. It normalizes the name to NFC, andkeyis the lowercase name. It throws aDriveErrorwith the codeINVALID_NAMEfor a blank name,.,.., a lone surrogate, a/, a control character (U+0000 to U+001F, or U+007F), or a name over 255 bytes in UTF-8.parseSubject(input)returns{ kind, id }for a valid subject, and throwsINVALID_SHAREfor any other input.subjectKindOf(kind)returnshumanoragent, and throwsINVALID_SHAREfor any other kind.A
DriveErrorhas acodefromErrorCode, such asNOT_FOUNDorNAME_TAKEN.ERROR_STATUSmaps each code to the HTTP status that the Worker answers with (404 forNOT_FOUND, 409 forNAME_TAKEN, and so on). A host that serves the drive reuses it, and theagentic-drive/workerentry exports it too. The typesNodeandErrorCodecome from the root,agentic-drive/client, andagentic-drive/worker.readZip(bytes)reads a zip thatwriteZipmade, with stored (uncompressed) entries only. It returns aMapfrom each entry name to its bytes. It throws for a compressed entry.writeZip(entries, at)writes a zip with one{ name, data }entry for each file, andatsets the time in milliseconds. It throws for an unsafe name, such as one with..or a leading/.The local drive has the functions behind the CLI.
openLocal(dir)returns{ drive, store, blobs }for a folder. Callawait importLegacyLocal(dir, store, blobs)first on a folder that an older version made. It copies the old data once, and it refuses when the new store already holds data.addPrincipal(store, id, { parent?, scopes? })makes a principal.issueKey(store, principalId, label?)returns the key. Keep it: the store keeps no plain copy.resolveToken(store, token)returns the caller, or throwsAuthErrorwith the codeUNAUTHENTICATED.revokeKey(store, token)stops one key.revokePrincipal(store, id)stops a principal, its keys, and every agent below it.publicNode(node, path?)returns an item without its storage keys and itspurgingflag, and addsmachineMade. Whenpathis given, it also addspath.The local functions trust their caller. Local data is the trust boundary: anyone who can write the folder can act as any principal in it.
Public exports
Each import path below is part of the package. The examples use TypeScript.
The CLI and MCP are commands, not import paths. The package does not export
agentic-drive/cli, agentic-drive/mcp, or an adapters subpath.
Root: agentic-drive
import { ROOT_ID, parseName, type SearchQuery } from "agentic-drive";
const name = parseName("report.txt");
const query: SearchQuery = { name: name.name };
console.log(ROOT_ID, query);
The root values are VERSION (version), ROOT_ID (root id), ACTOR_KINDS
(principal kinds), createDrive (drive factory), DriveError and ERROR_STATUS
(errors and HTTP statuses), parseName, parseSubject, subjectKindOf (input
checks), upload, sniffMime, DEFAULT_MAX_BYTES (upload helpers), buildExport
(account ZIP), readZip and writeZip (stored ZIP entries), importLegacyLocal
(old local data), AuthError, openLocal, addPrincipal, issueKey,
resolveToken, revokeKey, revokePrincipal, publicNode (local access),
backfillAuthorIds and backfillResources (migrations), and driveAccount
(account plug-in). See Other exports and Features for behavior and limits.
Root types: ActivityAction, ActivityEntry, Action, Actor, ActorKind,
Can, Ctx, Drive, DriveDeps, ErrorCode, Node, OnConflict, ResourceOwnership,
UploadOptions, SearchQuery, BackfillOptions, BackfillResult,
DriveAccount, and DriveAccountDeps. They describe activity, callers, drive
operations, uploads, migrations, and account operations.
Client: agentic-drive/client
import { createClient, type ClientOptions } from "agentic-drive/client";
const options: ClientOptions = { baseUrl: "https://drive.example.com", token: "your-token" };
const client = createClient(options);
await client.list("/");
createClient makes the HTTP client; DriveApiError reports failures. Types:
ActivityAction, ActivityEntry, ErrorCode, Node, Role, OnConflict,
SubjectKind, Subject, Item, PathItem, ApiErrorCode, ClientErrorCode,
ClientOptions, CallOptions, UploadBody, Downloaded, and DriveClient.
These describe API data, options, errors, and results.
Account: agentic-drive/account
import { driveAccount, type DriveAccountDeps } from "agentic-drive/account";
// Use the subject's own drive and context.
async function exportMine(deps: DriveAccountDeps) {
return driveAccount.exportAccount({ id: "u1", kind: "human" }, deps);
}
driveAccount exports and erases an account. DriveAccount describes the plug-in;
DriveAccountDeps describes its drive and caller context.
Worker: agentic-drive/worker
import { createApp, type WorkerEnv } from "agentic-drive/worker";
const app = createApp<WorkerEnv>();
export default { fetch: app.fetch };
The default export is the ready Worker. createApp makes an app with optional
injected ports. authenticate resolves a bearer token. ERROR_STATUS maps drive
errors to HTTP statuses. backfillAuthorIds and backfillResources migrate old
stores. Types: ErrorCode, Node, BackfillOptions, BackfillResult, WorkerEnv,
and AppOptions describe errors, items, migrations, bindings, and injected ports.
UI: agentic-drive/ui and agentic-drive/ui.css
import "agentic-design/styles.css";
import "agentic-drive/ui.css";
import { mount, type DriveUiOptions } from "agentic-drive/ui";
const options: DriveUiOptions = { baseUrl: "https://drive.example.com", getToken: () => "your-token" };
const view = mount(document.getElementById("drive")!, options);
view.unmount();
mount returns a MountedDrive handle. DriveBrowser is the React component;
DriveUiOptions is its configuration. DriveLocation is the folder, trash, activity, or file location type. DriveNavigationOptions describes host history updates. OnOpenItem is the reference-open callback type, and Item is its argument type. The CSS path supplies its styles.
CLI, MCP, and adapters
npx agentic-drive ls /
npx agentic-drive mcp
The CLI uses AGENTIC_DRIVE_TOKEN. The MCP command serves tools over stdio;
call drive_ls with a path. Storage adapters are provided by agentic-identity,
not by a drive subpath. The Quick start shows createFileStore and
createFileBlobs; the Worker uses D1 and R2 adapters by default.
Account export and erase
A host that runs account export and erase across many packages calls driveAccount (from agentic-drive/account). It has exportAccount(subject, { drive, ctx }) and eraseAccount(subject, { drive, ctx }), so the host does not read the storage layout. subject is an id and a kind (human, or agent with its own identity), and ctx is the drive context of that subject. A context for someone else throws FORBIDDEN. A bad subject, or a missing drive or ctx, throws a TypeError.
The checks of an export write access-log rows to audit, as every read does. That log is the record of access to personal data. The export writes no other data of the person. The export is plain JSON with items (your drive, trashed items flagged, file bytes as base64 in data), authored (items you made in other drives), agents, grants (shares you hold on drive items), invites (invites you sent on drive items), audit, and activity (the entries of your changes to items, with your kind). Only an agent with its own identity can be a subject: it gets its own items and empty collections. An agent that acts for a person is refused with FORBIDDEN. The erase removes your drive with its bytes, the shares you hold on drive items, and the invites you sent on drive items. It returns counts of items, grants, invites, and anonymized. invites counts only the invites sent on other drives: the invites on your own items go with your drive and are not counted. What you made in other drives stays, with your id set to erased and the kind kept. Only the person, not an agent, may erase: an agent subject gets FORBIDDEN. A second erase counts zero. The shape is provisional until agentic-identity adds its AccountPackage contract.
UI
The Worker serves a small web UI. It uses the 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.
Run it locally:
npm ci
npm run build:ui
WRANGLER_SEND_METRICS=false npx wrangler dev --local \
--var DRIVE_PUBLIC_URL:http://127.0.0.1:8787 \
--var DRIVE_URL_SECRET:<32 or more characters> --var DRIVE_REGION:unknown
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.
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.
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.
Set DRIVE_PUBLIC_URL to the origin that serves the UI. A download link on another origin is refused.
Mount the UI in your app
agentic-drive/ui renders the same UI inside your own page. It needs no framework in your app. You give it an element, the address of a drive server, and a function that returns the bearer token. mount returns a handle.
import "agentic-design/styles.css"; // tokens, fonts, and base styles for body, headings, and links
import "agentic-drive/ui.css"; // the drive styles
import { mount } from "agentic-drive/ui";
const drive = mount(document.getElementById("drive")!, {
baseUrl: "https://drive.example.com",
getToken: () => session.token, // runs before every request, so it can refresh
});
drive.update({ baseUrl, getToken }); // use another server or callback
drive.unmount(); // remove the UI
Use onOpenItem to open a reference in your app. It receives the full Item, including id, kind ("ref"), and ref (for example, "docs:plan"). Return true to handle the action and skip the download. Return false or nothing to keep the built-in download action. The hook runs for reference links in the list and grid, by pointer or Enter. Files and folders keep their built-in action. Modified clicks keep their existing behavior.
import type { Item, OnOpenItem } from "agentic-drive/ui";
const onOpenItem: OnOpenItem = (item: Item) => {
if (!item.ref?.startsWith("docs:")) return false;
openEditor(item.ref.slice(5)); // Your app opens its editor.
return true;
};
drive.update({ baseUrl: location.origin, getToken: () => session.token, onOpenItem });
DriveUiOptions exports this optional hook for both mount and DriveBrowser. update() can replace or remove it. React hosts can change the onOpenItem prop without resetting the current folder. The callback is synchronous.
Embed navigation
Use the exported DriveLocation type to select a folder, trash, item activity,
or file download. A folder location has { kind: "folder", path: "/" }.
Its path accepts a folder id, an absolute path such as "/Work/Plans",
or a path from a folder id. Other locations use { kind: "trash" },
{ kind: "activity", itemId }, or { kind: "file", itemId }.
The server checks access for each request. An inaccessible folder shows an error.
mount starts at options.location, or root when it is absent.
setLocation changes the view without remounting the drive.
getLocation returns a copy of the active location. For a folder, pass its
path as folder to the client's registerDocument method.
Trash, activity, and downloads have no active folder.
import { mount, type DriveLocation } from "agentic-drive/ui";
const readLocation = (): DriveLocation =>
JSON.parse(new URL(location.href).searchParams.get("drive") ??
'{"kind":"folder","path":"/"}'); // Validate URL data in your app.
const drive = mount(document.getElementById("drive")!, {
baseUrl: location.origin,
getToken: () => session.token,
location: readLocation(),
onNavigate(next, { replace }) {
const url = new URL(location.href);
url.searchParams.set("drive", JSON.stringify(next));
if (replace) history.replaceState(null, "", url);
else history.pushState(null, "", url);
},
});
addEventListener("popstate", () => drive.setLocation(readLocation()));
const active = drive.getLocation();
if (active.kind === "folder") {
await client.registerDocument({ name: "Plan", ref: "docs:plan", folder: active.path });
}
onNavigate reports folder, breadcrumb, trash, and activity navigation with
replace: false. File links download in place without changing the location or
notifying the host. Errors leave the folder open. Navigation cancels pending
downloads, so a late response cannot move the view or start a download.
An explicit file location returns to its parent with replace: true. Use
replaceState for that return. Root returns use "/".
Assigning a location through initial render, setLocation, or update does not
call the callback itself. An explicit file location starts a download; its completed return
does call the callback. An explicit file location that fails shows an error and
stays selected; the host can set a folder location to leave it.
update keeps the current location unless it receives a new location.
In React, DriveBrowser location={current} onNavigate={setCurrent} is controlled:
the host must apply the next location. Without location, React keeps it locally.
Changes to server or token options keep the location and reload its data.
The embed never changes the host URL itself.
getToken returns 1 to 512 letters, digits, - or _, or undefined to send no authorization header. A bad baseUrl or a getToken that is not a function makes mount throw a TypeError. The server still checks every request against the token. The embed shows files and trash with upload, rename, move, share, and the "Created by agent" label. It has no sign-in form, because your app owns sign-in. It does not change the page address, and it has no theme switch. agentic-design/styles.css sets base styles for the whole page, and it follows the dark mode of the system unless your page sets data-theme. Add agentic-design@">=0.8.0 <1.0.0" to your own dependencies, because your page imports its files. The Worker sends no CORS headers, so the server must be on the origin of your page. Set baseUrl to the same address as DRIVE_PUBLIC_URL, or the UI refuses its download links. unmount and update abort the requests that run and stop an upload that has more files to send. The UI keeps the list or grid choice in localStorage under agentic-drive-view, and it has its own heading for each view. Dialogs, menus, and messages render in document.body, outside your element. A click with Ctrl, Cmd, Shift, or the middle button on a folder link does not open the folder. The class names start with drive-. The design-system classes start with agd-. DriveBrowser is the same UI as a React component. It closes its requests when it unmounts. Wrap it in TooltipProvider and ToastProvider from agentic-design/react. react and react-dom (version 19 or later) are optional peer dependencies. Install them in your project when you use agentic-drive/ui. The web UI that the Worker serves uses the same views.
Serve the UI from the package
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:
// src/worker.js
export { default } from "agentic-drive/worker";
// wrangler.jsonc: the Worker reads the bindings DB (D1) and FILES (R2). Name yours the same.
// Create the bucket with `wrangler r2 bucket create agentic-drive-files --jurisdiction eu`.
// Create the database with `wrangler d1 create agentic-drive --location weur`, then put its id below.
{
"name": "agentic-drive",
"main": "src/worker.js",
"compatibility_date": "2026-01-01",
"compatibility_flags": ["nodejs_compat"],
"d1_databases": [{ "binding": "DB", "database_name": "agentic-drive", "database_id": "<your id>" }],
"r2_buckets": [{ "binding": "FILES", "bucket_name": "agentic-drive-files", "jurisdiction": "eu" }],
// In a workspace, point this at the root node_modules instead.
"assets": { "directory": "./node_modules/agentic-drive/ui-dist", "not_found_handling": "single-page-application", "run_worker_first": ["/api/*", "/health"] }
}
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:
- 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". - Insert a row into
store_docswithcollectionprincipalsandidme. Its body is{ "id": "me", "kind": "human", "scopes": ["drive:admin"] }. - Make the token with
openssl rand -hex 32. A token may use only letters, digits,_, and-, up to 512 characters. Insert a row intostore_docswithcollectioncredentials. Itsidis the lowercase hex SHA-256 of the token, whichprintf %s "$TOKEN" | sha256sumprints. Its body is{ "principalId": "me", "kind": "api_key" }.
The seed script 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.
The UI does not make snapshot links yet. The address of a share link shows once, when you create it.
Inject a store, blobs, and sign-in
A host that has its own Worker can serve the drive routes from it. createApp from agentic-drive/worker takes the parts to use. Each part is optional. A part may be a function of the Worker env, which runs on each request.
import { createApp } from "agentic-drive/worker";
const app = createApp({
store: (env) => myStore(env), // a Store of agentic-identity
blobs: (env) => myBlobs(env), // a Blobs of agentic-identity
// Return a Ctx for the caller, or null for a 401.
auth: async (request, store, env) => {
const principal = await mySignIn(request, env);
return principal && { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal };
},
});
export default { fetch: app.fetch };
- With
store, the Worker reads noDBbinding. It does not create the table either, so the host owns the schema. - With
blobs, the Worker reads noFILESbinding and needs noDRIVE_PUBLIC_URL. The blobs must sign download URLs that point to/api/v1/drive/blobon this Worker. That route checks the signature withDRIVE_URL_SECRETbefore it reads the store or the blobs, so set that secret. - With
auth, the Worker does not read bearer tokens from the store. The callback decides who the caller is. Return a wholeCtx: aprincipal, and anownerandactorthat match it. Any other value gets 401. Make sureprincipal.chainandprincipal.scopesare real, because the drive checks them. A callback that reads a session cookie must check the request origin itself. Every write route is open to a cross-site form until it does. - With no options,
createApp()works as before: D1, R2, and bearer tokens from the store.export { default } from "agentic-drive/worker"is the same.
Features
- Folders, files, and typed references to documents in other apps (
docs:<id>). - Paths that start with
/. Anything else is an item id. - Move, rename, trash, restore, and purge. Trashing a folder hides its whole tree.
- Names compare without regard to case or Unicode form. A clash fails, or renames, or replaces.
- Each item has an activity log: create, rename, move, content update, share, unshare, delete, and restore, each with the actor (
{ id, kind: "human" | "agent" }) and the time. Read it withlistActivity,GET /items/:id/activity,agentic-drive activity <path|id>, or the MCP tooldrive_activity. The UI shows it with an "AI agent" label on each change that an agent made. The export holds the activity of the person and their agents. Purge deletes the log of the item. Items from before the log have no entries until they change. - Each item records its size, type, owner, and who made and last changed it, with the actor kind.
- Storage sits behind the
StoreandBlobsports ofagentic-identity. Tests use its memory adapters. The CLI uses a local file adapter that passes the same contract suites. The Worker uses its D1 and R2 adapters. - Roles: viewer, commenter, editor, owner. A role on a folder reaches everything under it, and follows an item that moves. Identity decides every request through
can(), which also writes the audit entry. A caller who cannot read an item getsNOT_FOUND. A caller who can read it but not change it getsFORBIDDEN. Editors write and trash. An item takes the roles of its new folder, so moving an item into a folder where the caller is owner needs owner on the item. Only owners share, purge, and read the share list. - Share by a subject (
{ kind: "human" | "agent", id }), by invite (an email for a person, a handle for an agent), or by link with a role, an expiry, and an optional sign-in. The SDK calls areshareWith,unshare,invite,acceptInvite,createLink,revokeLink,shares,openShared,listShared, andreadShared. - Two live items never share a name in one folder. A claim document holds each name. It is written in the same transaction as the item, and it is deleted when the item goes to the trash.
upload()reads a stream with a size cap, finds the type from the first bytes, and stores a SHA-256 hash.downloadUrl()returns a signed URL. The URL signs an attachment header with the file name and the stored type. The Worker verifies it and serves the bytes with those headers, so the link holder cannot change how the bytes are served. A link lasts 15 minutes by default and 1 hour at most. Trashing a file does not revoke a link already issued.search()finds live items by name, type, kind, size, and who made them.- App documents: an app calls
registerDocument({ ref: "docs:<id>", name, folder })to put a document in the drive, andforgetDocument(ref)when it deletes the document.home({ app })lists files and every app's documents together, newest change first, with a count per app.search({ app })filters the same way. The CLI hashome,register, andforget. The MCP server hasdrive_home,drive_register_document, anddrive_forget_document. - Host operations.
findByRef(ref)returns{ node, path }for each live reference to an app document in the caller's own drive that the caller can read, sorted by path (a reference that sits in a drive shared with the caller is not listed).listTrashWithPaths()lists what is in the trash (it reads the whole drive, so it costs more thanlistTrashon a big one), newest first, with the path each item had before.claimRef(path, ref, { subject, role }, opts)puts a reference and gives the subject a role on it as one step: if the grant fails (a bad subject or role, or a caller who is not an owner of the new item), the reference is removed again, the error is thrown, and the name is free. Folders thatparentsmade stay.onConflict: "replace"acts asfailhere, because a rollback could not bring back an item that was replaced. It needs sharing. The rollback is not a database transaction, because identity's sharing writes outside one: another caller can see the reference for a moment before it goes. The routes areGET /refs?ref=,POST /refs, andGET /trash/paths. The CLI hasfind-ref,claim-ref, andtrash --paths. The MCP server hasdrive_find_ref,drive_claim_ref, anddrive_trashwithpaths. buildExport()writes everything one person has to a zip with amanifest.json(format version 2). It names the person by id and kind. It lists the person's own items (trashed ones are flagged, and a file whose bytes are gone is markedmissing). For the person in person it also lists the ids of what they or their agents made or changed in other drives (no names, and nothing of the people who own them), the shares they hold, the invites they sent, and the audit entries of what they and their agents did. An agent getspartial: trueand none of that. Invitee emails and link tokens are never in it. Entries are named by item id.erasePerson()erases a person for good, and only the person in person can call it (the CLIdelete-allandDELETE /accountcall it). It deletes their drive with all its shares, the shares they hold, and the invites they or their agents sent, on drive items only: the grants of other apps stay for those apps and for identity. What they or their agents made in other drives stays, because it belongs to those drives: the id becomeserased, and the kind stays, so machine-made work keeps its label. Agents are found through theprincipalscollection, as identity finds them: an agent with its own identity names the person asownerId, and a delegated agent names its parent. An agent with its own identity belongs to the person, and so does its drive: erase deletes it, and the export lists its items (no bytes). A person with more than 1000 agents cannot be erased here: the call fails and deletes nothing more. The audit entries of the erased items go with them, through identitydeleteResource. The entries about what the person did in other drives stay: they hold ids and kinds only. A failed call can run again.deleteAll()is the drive part alone.- A
can()hook decides who may read, write, or delete. Withoutsharing, the default allows the owner only.
Limits
Erase does not delete what identity keeps about the person: their principal, credentials, and sign-in record. Identity erases those. An invite that names only an email, sent by someone else, stays until it expires.
Items made before drive kept
createdByIdandupdatedByIdhave no such fields, so an export or an erase cannot find them. RunbackfillAuthorIds(store, { maxPages: 1 })on a store of that age, once the old Worker has stopped writing. Each call runsmaxPagespages of 10 items, and returns anextCursorwhile items remain. Pass the cursor to the next call until none is returned. WithoutmaxPages, one call runs every page, which can exceed a Worker's query cap on a large store.There are no retention settings yet, and no per-workspace export. An export covers one person.
registerDocumentmakes one item per document in a drive. Two calls at the same moment can make two.Drive reads the
store_docstable in D1. The CLI keeps its data in the file store ofagentic-identity/file. On first run it copies data from an oldstore.jsonljournal and hashed blob files in the same folder, once, and leaves the old files in place. Drive reads no other older layout, and there is no claim backfill.Uploads buffer the stream in memory up to the cap. The default cap is 25 MiB (
DEFAULT_MAX_BYTES).The CLI folder has no signing secret, so its blobs make no download URL. The Worker signs its own.
Drive writes each new blob only when its key is free (
ifNoneMatch). A write that finds the key taken fails withBLOB_EXISTS.Only
upload()enforces the size cap. A caller ofputFile()must check size first.A claimed type that a browser would run (HTML, SVG, scripts) is stored as
application/octet-stream.The file adapters allow one process at a time.
The MCP
drive_exporttool writes a new.zipinto<dir>/exportsand never overwrites a file. It takes a plain file name, not a path.A purge or an erase works in chunks of 25 items (10 with sharing on), and each chunk commits. A Worker call has a cap on database queries, so a very large tree can need several calls. A failed or cut-off call leaves marked items hidden, and the same call finishes the job.
Hard delete removes blobs first and then rows. If a blob delete fails, the call throws and the same call can run again.
The export holds every file in memory. A very large drive needs a streaming export, which is not built yet.
The zip export stores entries without compression and has no Zip64 support.
No access log for personal data yet. That comes with agentic-identity.
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
BUSYerror.The D1 store keeps one generic table. A busy deployment needs an expression index on
ownerandparentId.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.Sharing is on in the Worker and in the SDK (
createDrive({ sharing })). The CLI and the MCP server turn it on too. The CLI hasshares,share,unshare,invite,accept-invite,link-create, andlink-revoke. The MCP server hasdrive_shares,drive_share,drive_unshare,drive_invite,drive_accept_invite,drive_create_link,drive_revoke_link, anddrive_delete_account(passconfirm: "yes"). On the first run after the upgrade, the CLI gives older local items theirresourcesdocs, once.Drive writes the
resourcesdoc of each item itself, in the same transaction, because identity has no call that does it in a transaction. A drive withsharingoff keeps no such docs. Turn sharing on at the start, or runbackfillResourcesafter you turn it on.An invite sends no message. The caller passes the invite id on.
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 withTOO_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.Every access check writes one audit row, and a transaction that runs again writes its rows again. The audit holds ids and kinds only: no emails and no content.
Data made before sharing has no
resourcesdocs, so its owner getsNOT_FOUND. RunbackfillResources(store, { maxPages: 1 })on that store, paged the same way, once the old Worker has stopped writing. See CHANGELOG.md for the order of the upgrade steps.A person who is not an owner cannot move an item into their own drive: the move fails with
CROSS_DRIVE.
Run locally
bun install # or: npm ci
bun run dev # or: npm run dev
curl http://localhost:8787/health
The script builds the UI, then runs wrangler dev --local with DRIVE_PUBLIC_URL=http://127.0.0.1:8787 and DRIVE_REGION=unknown. /health needs no token and no secret. Every /api/v1/drive call answers 503 until you set a secret: copy .dev.vars.example to .dev.vars and set DRIVE_URL_SECRET to a random value of 32 bytes or more (openssl rand -hex 32). .dev.vars stays out of git. If you change the port, also pass --var DRIVE_PUBLIC_URL:http://127.0.0.1:<port>, or the signed links point at port 8787. A fresh local database has no tokens: see e2e/serve.mjs to seed one.
The scripts call npm and node (Node 22 or newer), so install both, even if you use bun. They need a POSIX shell.
Run npm run dev:smoke to check this path. It starts the dev server on port 8799, waits for GET /health to answer 200, and stops the server. CI runs it as the "dev smoke" step. Set DEV_SMOKE_PORT to use another port.
Develop
npm ci
npm run ci:local
License
MIT.
Install Agentic Drive in Claude Desktop, Claude Code & Cursor
unyly install agentic-driveInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add agentic-drive --env AGENTIC_DRIVE_TOKEN="" --env DRIVE_TOKEN="" --env WRANGLER_SEND_METRICS="" -- npx -y agentic-driveStep-by-step: how to install Agentic Drive
FAQ
Is Agentic Drive MCP free?
Yes, Agentic Drive MCP is free — one-click install via Unyly at no cost.
Does Agentic Drive need an API key?
Yes, it requires environment variables: AGENTIC_DRIVE_TOKEN, DRIVE_TOKEN, WRANGLER_SEND_METRICS. Unyly injects them into the config during install.
Is Agentic Drive hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Agentic Drive in Claude Desktop, Claude Code or Cursor?
Open Agentic Drive on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Changes
Versions and requested access over time.
- New version published
Related MCPs
Notion
Read and write pages in your workspace
by NotionLinear
Issues, cycles, triage — from Claude
by LinearGoogle Drive
Search and read your Drive files
by Googlemindsdb/mindsdb
Connect and unify data across various platforms and databases with [MindsDB as a single MCP server](https://docs.mindsdb.com/mcp/overview).
by mindsdbfulcradynamics/fulcra-context-mcp
Access personal health and biometric data (sleep stages, heart rate, HRV, glucose, workouts), calendar and location via the Fulcra Life API.
by fulcradynamicsrinadelph/Agent-MCP
Framework for building multi-agent systems that coordinate over MCP, with task management, shared context and RAG.
by rinadelphWhenLabs-org/when
Developer toolkit: stack detection for AI context files, port conflicts, .env schema validation, docs drift, dependency license audits and task timing.
by WhenLabs-orgBeltran12138/wecom-docs-mcp-server
WeCom (Enterprise WeChat) document operations via MCP: create, read, and edit Docs and Smartsheets (9 tools). Fills the doc-CRUD gap — existing WeCom MCP server
by Beltran12138freema/mcp-gsheets
Google Sheets API integration for reading, writing, formatting and sheet management.
by freemamattijsdp/dbt-docs-mcp
dbt-core (OSS) project metadata, model and column-level lineage, and dbt documentation.
by mattijsdpCompare Agentic Drive with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All productivity MCPs
