# DEVU agent API v1 Base URL: `https://devu.nu/api/agent/v1`. Create an agent token in **Workspace options > Account settings > Agent API**. Choose one vault, a name, expiry (1–365 days; default 30), and separate readable/writable folder lists. The secret is shown once. Keep it in your agent's credential store, never in notes, source control, URLs or tool logs. ## Permissions Each token belongs to its issuing account and one vault. Permissions are additive: - A readable subtree permits listing current note metadata, reading Markdown and reading its revision history. - A writable subtree permits reading, creating and editing Markdown descendants. Write therefore includes read access. - Subtrees include future Markdown descendants, using case-sensitive, Unicode NFC paths and exact folder boundaries. `Agents` includes `Agents/Plan.md`, not `Agents sibling/Plan.md`. - In the web form, `/` means the vault root and blank means no access. In JSON, root is `""` and no access is `[]`. Relative prefixes may have one trailing `/`; absolute paths and traversal are rejected. - Example: `readPrefixes: [""]`, `writePrefixes: ["Agent inbox"]` permits whole-vault reading and writes only below `Agent inbox/`. Tokens cannot access other vaults, create vaults, delete or rename notes, change scopes, issue more tokens, change passwords, pair Obsidian devices or manage accounts. No global version or event feed is exposed. Regular files up to 100 MiB use the file endpoints below. Executable content is never rendered or run by the service. An agent allowed to read a subtree may read all retained revisions of its notes, including older text. Each request requires `Authorization: Bearer vpat_...`. There is no cookie or query-string authentication on agent routes. Tokens are stored hashed on the server. HTTPS is required publicly. Server responses are `no-store`. Agent requests are limited to 1,000 requests per token per 15 minutes, including reads; excess requests receive 429. Revoking a token invalidates new requests and pending uploads before commit. Password changes/recovery, disabling an account and **Revoke all sessions** also invalidate its agent tokens. Revocation does not erase content an agent already retrieved. Scope changes require revoking the token and creating another. ## Endpoints ### GET `/status` Returns `{ "protocol": 1, "instanceId": "...", "grant": { "id": "...", "name": "...", "vaultId": "...", "readPrefixes": [""], "writePrefixes": ["Agent inbox"], "created": 0, "expires": 0 } }`. Times are Unix milliseconds. No account email or token secret is returned. ### GET `/notes` Returns an array of visible current note metadata: `{ "id", "path", "revisionId", "hash", "bytes" }`. `hash` is SHA-256 of UTF-8 Markdown. Notes outside the grants are omitted. ### GET `/notes?path=Agent%20inbox%2FPlan.md` Finds one current note by exact, NFC-normalized vault-relative path. Returns metadata plus `content`. URL-encode the path. Missing and unauthorized paths both return 404. ### GET `/notes/:noteId` Returns metadata plus Markdown `content`. Optional `?revision=:revisionId` reads a retained revision of this note. A foreign revision or unauthorized note returns 404. ### GET `/notes/:noteId/history` Returns up to 100 retained revisions, newest first: `{ "revisionId", "created", "bytes" }`. `created` is an ISO timestamp. The same read scope applies. ### POST `/notes` Send JSON with `Content-Type: application/json`: ```json { "path": "Agent inbox/Plan.md", "content": "# Plan\n\nA synthetic example.\n", "baseRevisionId": null, "operationId": "a-unique-request-id" } ``` For a new path, set `baseRevisionId` to `null`. To edit, read the current note and send its `revisionId` as `baseRevisionId`. The server commits only if that revision is still current; a stale revision returns 409 and retains the accepted server content. Retain your rejected draft and re-read/reconcile deliberately. No force-write operation exists. Successful writes return current metadata (not content), HTTP 200. Use a unique `operationId` of 1–128 ASCII letters/digits/hyphens per logical write. Retry an uncertain request with the same ID and identical body for exactly-once revision creation; do not use a new ID just because an acknowledgment was lost. Reusing an ID with different data returns 409. Receipts are isolated per token. Permission and expiry checks still apply to retries. Paths follow the service's portable Markdown rules: no traversal, hidden/configuration segments, credential-like paths, symlinks or non-Markdown uploads. Notes are limited to 1 MiB of UTF-8 Markdown; per-vault storage and retained-history quotas apply to agents too. Formatting and line endings are preserved. ## Regular files and large Markdown The separate file routes use the same bearer token, vault binding, read/write subtrees and revocation rules: - `GET /files`: all accessible file metadata, including small Markdown. Each item has `kind: "markdown"` (browser-editable) or `kind: "file"` (download-only), plus `id`, `path`, `revisionId`, `hash`, `bytes`. - `GET /files/:id/content?revision=:revisionId`: exact file bytes, always `application/octet-stream` with attachment disposition. Optional revision must belong to this file. Verify the SHA-256 against metadata before using a download. - `GET /files/:id/history`: retained Markdown and binary revision metadata for the file. - `POST /files`: raw bytes with `Content-Type: application/octet-stream`. Set `X-Vault-Path` to the percent-encoded vault-relative path, `X-Vault-Base-Revision` to the current revision (omit or empty for create), and `X-Vault-Operation` to a unique 1–128 character ASCII alphanumeric/hyphen operation ID. Retries use exactly the same headers/body. HTTP 200 returns current metadata. Each file can be at most 100 MiB. UTF-8 Markdown up to 1 MiB remains editable; larger Markdown, invalid UTF-8 Markdown and other regular files are byte-preserving downloadable files. Hidden/configuration/credential-like paths and symlinks remain excluded. Empty folders, rename and deletion are not propagated. The service stores immutable file objects outside SQLite, with compare-and-swap heads, idempotency receipts and hash-verified backups. Raw uploads stream to private staging files and recheck authorization before committing. At most two raw uploads and eight raw downloads run concurrently across the service. File uploads have a five-minute request deadline. Storage limits include 10,000 files and 1 GiB of current content per vault, 2 GiB of retained binary/large-file revisions per vault, a 2 GiB deduplicated file-object budget across the instance and a 512 MiB disk reserve. Editable Markdown retains its 64 MiB current/512 MiB history limits. Quota failure preserves acknowledged versions. The Pi `vault` tool remains a Markdown editing tool; use these file routes in a dedicated binary client for attachments. ## Example Python client Save the token in a local file accessible only to your user (mode 0600). Set `VAULT_AGENT_TOKEN_FILE` to that filename, not to the token value. This example reads a note and creates or updates a separate agent-owned note with compare-and-swap semantics. The token stays out of shell arguments and output. ```python import json import os from pathlib import Path from urllib.request import Request, urlopen from urllib.error import HTTPError from uuid import uuid4 base = 'https://devu.nu/api/agent/v1' token = Path(os.environ['VAULT_AGENT_TOKEN_FILE']).read_text().strip() def api(path, data=None): request = Request(base + path, data=None if data is None else json.dumps(data).encode(), headers={'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}) with urlopen(request, timeout=30) as response: return json.load(response) notes = api('/notes') path = 'Agent inbox/Example.md' current = next((note for note in notes if note['path'] == path), None) operation = { 'path': path, 'content': '# Example\n\nWritten by an agent.\n', 'baseRevisionId': current['revisionId'] if current else None, 'operationId': str(uuid4()), } # Preserve operation for any retry of an uncertain response. result = api('/notes', operation) print(result['path'], result['revisionId']) ``` Errors return `{ "error": "..." }`: 400 invalid input, 401 invalid/expired/revoked credentials, 403 denied Origin or browser-only administration, 404 missing/unauthorized resources, 409 revision/idempotency conflict, 413 note/storage quota exceeded, 429 rate/token limit. Do not log Authorization headers or secret-creation responses. ## Browser-only token management API These routes use the normal signed-in browser cookie, configured `Origin` and matching `X-Vault-User` header. Agent and device tokens cannot call them. - `GET /api/account/agent-tokens`: active tokens for this account; metadata only. - `POST /api/account/agent-tokens`: `{ "name": "Research helper", "vaultId": "...", "readPrefixes": [""], "writePrefixes": ["Agent inbox"], "expiresInDays": 30 }`. HTTP 201 returns metadata plus the one-time `token` secret. At most 100 active tokens per account. - `POST /api/account/agent-tokens/revoke`: `{ "id": "..." }`. Revokes only a token owned by this account. Management paths above are relative to `https://devu.nu/`. Do not send account passwords to agent routes.