# /agents.md
Complete agent contract for this Nabu instance.
This document is public and returns raw Markdown. Read this route before touching the browser UI. Protected API operations still require owner authentication or a scoped shared-space token.
## Operating rules
- Use the HTTP API for normal note operations. Use MCP when the deployment or agent integration requires it.
- Do not use browser automation or browser-use for normal note operations. Use the browser only for human navigation or explicit UI testing.
- Use HTTPS for non-loopback deployments.
- Check the HTTP status and JSON body on every request; do not treat an HTTP error as a successful operation.
- Never log, persist, or commit passwords, session cookies, invite URLs, access tokens, or bearer credentials.
- Use vault-relative paths only. Never send absolute paths, traversal paths, or paths outside the vault.
## Base URL and contract discovery
- Set `NABU_URL` to this deployment base URL, without a trailing slash.
- This deployment base URL is https://context.heyhiltd.tech.
- Fetch `${NABU_URL}/agents.md` again whenever the deployment or API version is uncertain.
- The contract is available without authentication so a new agent can discover the correct workflow before logging in or redeeming an invite.
## Authentication
- Owner login and legacy remote MCP use the same Nabu password; remote MCP derives its owner bearer credential internally when no issued agent token is configured.
- Owner login uses `POST /api/auth/login` with `Content-Type: application/x-www-form-urlencoded` and fields `password` and `redirect`.
- A successful login sets the nabu_session session cookie. Persist it in a cookie jar and reuse it for owner requests.
- Shared collaborators use `Authorization: Bearer <scoped-access-token>`; they do not use the owner password.
- A shared token is scoped to exactly one shared space, its permissions, and its lease expiry. It cannot be broadened or extended by the collaborator.
- A read-only URL token is a separate browser/API capability. It has read permission only and never grants owner authentication or write access.
- Native MCP uses `POST /mcp` with `Authorization: Bearer`; it does not use the browser cookie or URL query tokens.
- Without that header, MCP exposes only `redeem_shared_space_invite`. Persist the returned scoped token and reuse it across agents, sessions, and chats.
### Owner login example
```bash
curl -i -c /tmp/nabu-cookies.txt \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'password=YOUR_PASSWORD&redirect=%2Fagents.md' \
  https://context.heyhiltd.tech/api/auth/login
```
## Owner agent connection link
A human owner can create a non-technical agent connection from `Settings → Agents → Connect an agent`.
- Issue a link with `POST https://context.heyhiltd.tech/api/agent/connections` using the owner session cookie and JSON body `{ "permissions": ["read"] }` or `{ "permissions": ["read", "write"] }`.
- The response contains an opaque `connectionUrl`, its permission set, expiry, and a redemption descriptor. The URL expires after 10 minutes and works once.
- Send the full URL to the agent. The agent redeems it with `POST https://context.heyhiltd.tech/api/agent/connections/redeem` and JSON body `{ "connectionUrl": "<full-url>" }`. Do not GET or alter the URL.
- A successful redemption returns the durable `credential`, `permissions`, `apiBaseUrl`, `createdAt`, `expiresAt` (90 days after issuance), and `nextAction: "configure_agent"`. Store the credential in the agent secret store immediately; never print or put it in a URL. After expiry, ask the owner to issue a new link.
- A second, malformed, or expired redemption returns `410 AGENT_CONNECTION_INVALID`. If the first response is lost, ask the owner to generate a new link; the spent link is not replayable.
- Remote MCP may use `NABU_AGENT_TOKEN=<issued-credential>` with `NABU_URL=<https-url>`. Existing agents may continue using `NABU_PASSWORD=<same-password-as-Nabu>`.
Example redemption request:
```json
{ "connectionUrl": "https://nabu.example.test/connect/agent/ONE_TIME_SECRET" }
```
## Note identity and read surfaces
- Canonical note identity is relPath (the vault-relative relPath).
- Prefer deterministic reads through /api/vault/notes/by-path?path=<path>.
- Slug lookup through /api/vault/notes/$slug is convenience-only and may collide.
- URL-encode query parameters such as `path`, `q`, and `tag`.
- `GET /api/vault/`
- `GET /api/vault/index/stats`
- `GET /api/vault/tree`
- `GET /api/vault/folders?path=`
- `GET /api/vault/notes/$slug`
- `GET /api/vault/notes/by-path?path=`
- `GET /api/vault/notes/neighborhood?path=`
- `GET /api/vault/search?q=&path=&tag=&limit=&offset=`
- `GET /api/vault/assets?path=` for in-scope local images and files
## Note writes
- `POST /api/vault/folders`
- `DELETE /api/vault/folders?path=`
- `POST /api/vault/notes`
- `PUT /api/vault/notes/by-path`
- `PATCH /api/vault/notes/by-path`
- `DELETE /api/vault/notes/by-path?path=` (single note only)
- Versioned PDFs use `POST/GET/PATCH /api/vault/documents/:documentId`; agents may upload and read, while archive/restore requires a human session.
- Use `rawMarkdown`, not top-level `body` or `content`. Send note text as `rawMarkdown` or a structured `document`, never both.
- Before an agent creates or updates a note, read `governance/02-nabu-context-highway.md` and send `x-nabu-publishing-guide-read: governance/02-nabu-context-highway.md@2026-07-31`.
- Agent note writes must use a complete structured `document` with non-empty `title`, `summary`, `tags`, `authors`, `source`, and `body`; rawMarkdown-only agent writes are rejected.
- Agent-authored notes receive authenticated publisher provenance in frontmatter. Do not overwrite or impersonate another agent in `company/agent-continuity/`.
- Before an agent reads a note under `company/engineering/` (other than the four entrypoints), first read `company/engineering/README.md`, `backend.md`, `codebase-map.md`, and `frontend.md`, then send `x-nabu-engineering-guide-read: engineering-entrypoints-v1@2026-08-01`.
- Folder delete is empty-only and non-recursive.
- When writing notes, prefer canonical frontmatter metadata through structured documents. Supported metadata includes `title`, `summary`, `tags`, `authors`, `source`, and `references`.
- Use deterministic by-path reads after every mutation. Read the note again by canonical path and verify the content and metadata.
- Move notes with `{ "path": "from.md", "toPath": "to.md" }`. Moves do not rewrite wiki-links or Markdown links.
- Successful mutations rebuild the in-memory index immediately.
### Revision-aware writes
- Every note read returns `note.revision` and an `ETag` such as `"revision-42"`.
- Shared-token updates and moves must send `If-Match: "<revision>"` or `expectedRevision` in the request body.
- Existing owner agents may continue legacy writes without a revision during migration. Those responses include `migration.code = WRITE_REVISION_MIGRATION_REQUIRED`; migrate that agent to revision-aware writes.
- On `428 WRITE_REVISION_REQUIRED`, GET the supplied `readUrl`, merge the intended change, and retry with the returned revision.
- On `409 STALE_NOTE_REVISION`, GET `readUrl`, merge against `currentRevision`, and retry. Never silently overwrite a newer note.
- Human source saves may send `expectedRawContentHash`; a `409` then means the raw file changed since it was read. `expectedContentHash` remains the parsed-note agent contract.
### Create and update examples
```bash
curl -sS -b /tmp/nabu-cookies.txt -X POST https://context.heyhiltd.tech/api/vault/notes \
  -H 'Content-Type: application/json' \
  --data '{"path":"projects/example/notes/example.md","rawMarkdown":"# Example"}'
curl -sS -b /tmp/nabu-cookies.txt https://context.heyhiltd.tech/api/vault/notes/by-path?path=projects%2Fexample%2Fnotes%2Fexample.md
curl -sS -b /tmp/nabu-cookies.txt -X PUT https://context.heyhiltd.tech/api/vault/notes/by-path \
  -H 'Content-Type: application/json' \
  -H 'If-Match: "REVISION_FROM_READ"' \
  --data '{"path":"projects/example/notes/example.md","rawMarkdown":"# Example\n\nUpdated"}'
```
## Temporary shared spaces
Shared spaces are temporary, live recursive knowledge boundaries. Existing descendants are accessible immediately, and files or folders created later under the shared root become accessible automatically until the lease expires or is revoked.
- Do not share the vault root.
- Shared roots are segment-aware: `little-helpers` does not include `little-helpers-private`.
- Shared spaces have no exclusion rules in v1; choose a narrower root instead.
- A shared token cannot access parent paths, siblings, symlink targets, or metadata that reveals private paths.
- Private linked notes are filtered from search, backlinks, neighborhoods, and graph results.
- Lease duration defaults to 7 days, has a minimum of 1 day, and has a maximum of 183 days.
- Access expires synchronously using server time. Cleanup may happen asynchronously.
- Invite URLs are opaque one-time capabilities valid for one hour. They are not long-lived access tokens.
### Shared-space contract versions
- New clients should feature-detect `contractVersion: 2` in proposal and confirmation responses. For redemption, send an idempotency key on the first request to every server. If that request has an unknown network outcome, make at most one recovery request with the exact same invite and key: version 2 returns the same token, while version 1 either completes a still-pending redemption or returns `410` when the first request consumed the invite.
- Requests without `contractVersion: 2`, and proposals persisted without a version marker, retain version-1 behavior. Version-1 confirmation is still explicit, and a version-1 invite remains one-shot when redeemed without `Idempotency-Key`.
- A version-2 proposal binds the normalized root path, requested duration, and permission set. Confirmation may repeat those fields for compatibility, but a mismatch is rejected; it never silently changes consent. Create a new proposal after a mismatch or expiry.
### 1. Propose a scope (no sharing side effect)
POST https://context.heyhiltd.tech/api/shared-spaces/proposals
```json
{
  "contractVersion": 2,
  "path": "projects/example/shared",
  "durationDays": 14,
  "permissions": ["read", "write"]
}
```
- Normalize and validate the vault-relative path first.
- Reject empty/root paths, absolute paths, traversal, symlinks, and paths outside the vault.
- Show the human the complete recursive `files` and `folders` lists, `fileCount`, `totalBytes`, `warnings`, proposal ID, and expiry before asking for confirmation.
- The preview must clearly state: `Any files or folders added under this path later will also be part of the shared space.`
- Version 2 requires all four fields above. The server stores the normalized path, duration, and canonical permission order with the proposal.
- A proposal alone never shares anything.
### 2. Confirm explicitly and create an invite
POST https://context.heyhiltd.tech/api/shared-spaces/
```json
{
  "proposalId": "proposal_123",
  "confirmed": true,
  "contractVersion": 2,
  "path": "projects/example/shared",
  "durationDays": 14,
  "permissions": ["read", "write"]
}
```
- `confirmed: true` is mandatory. Never infer confirmation from a proposal request or from the original user wording.
- `durationDays` must be between 1 and 183. Omitted duration uses the 7-day default where supported.
- The proposal must be unexpired and owned by the authenticated owner.
- A version-2 confirmation response preserves the one-time `inviteUrl`, `inviteExpiresAt`, and `sharedSpaceExpiresAt`, and includes a `redemption` descriptor with `contractVersion: 2`, relative `endpoint: "/api/shared-spaces/invites/redeem"`, `method: "POST"`, exact `bodyField: "inviteUrl"`, `idempotencyHeader: "Idempotency-Key"`, `idempotencyRequired: true`, invite expiry, and `nextAction: "redeem_and_save_profile"`.
- The `/api/...` endpoint is a deployment-base-relative suffix: append it to this deployment’s canonical HTTPS base so a base-path prefix is preserved. Do not resolve it as an origin-root URL, and never derive the base from an untrusted Host, forwarded-host header, or request origin.
- Do not log or persist the invite URL. Send it only to the intended collaborator.
### 3. Redeem an invite and recover safely
POST https://context.heyhiltd.tech/api/shared-spaces/invites/redeem
```bash
INVITE_URL='https://nabu.example.test/invites/EXAMPLE_ONLY'
IDEMPOTENCY_KEY='example-key-generated-with-at-least-128-bits-of-entropy'
curl -sS -X POST https://context.heyhiltd.tech/api/shared-spaces/invites/redeem \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
  --data "{\"inviteUrl\":\"${INVITE_URL}\"}"
```
- The JSON field is exactly `inviteUrl`; do not substitute `invite`, `token`, or `inviteToken`.
- Do not GET the invite URL as the redemption operation. A direct GET may return 404; redemption is the JSON POST above.
- Every initial redemption sends a client-generated `Idempotency-Key` with at least 128 bits of entropy. Validate its length and allowed characters before sending it; never print or persist the key as ordinary text. After an unknown first outcome, make at most one recovery request with the same invite and key even though the version is not yet known.
- A successful `200` response preserves every version-1 field and adds `contractVersion: 2`, deterministic non-secret `profileId`, `nextAction: "save_credential_profile"`, and exactly four relative, token-free `links`: `tree`, `rootFolder`, `noteByPath`, and `search`.
- The fixed link templates are `/api/vault/tree`, `/api/vault/folders?path={rootPath}`, `/api/vault/notes/by-path?path={path}`, and `/api/vault/search?path={rootPath}&q={query}`. Append each deployment-base-relative suffix to the canonical base; do not resolve it as an origin-root URL that drops a base-path prefix. Encode only the allowlisted variables; links contain no token or `Authorization` material.
- Keep the returned `accessToken` in memory only long enough to write the profile below, or put it in that approved profile. Never print it, commit it, place it in Markdown, or put it in a URL.
- Use the token on normal vault APIs:
  `Authorization: Bearer <accessToken>`
- If a network failure leaves the first redemption status unknown, make at most one recovery request with the exact same invite URL and `Idempotency-Key`. Version 2 returns the identical token and metadata. On version 1, success means the recovery request performed the redemption; `410` means the first response and token were lost, so ask the owner for a new invite. A different key remains rejected.
- Headerless redemption keeps version-1 one-shot behavior. A version-1 response without `contractVersion` is the v1 fallback signal for an older server: redeem once, persist securely, and do not assume retry safety.
- `410 SHARED_SPACE_INVITE_INVALID` covers malformed, expired, already redeemed, revoked, or unavailable invites. Treat a locally expired profile or a failed verification as unusable; never silently reuse it or request a broader token.
- Each additional collaborator needs a newly generated one-time invite.
### 4. Persist one scoped credential profile
The profile is runtime-local persistence for one deployment and one shared space. It contains only the scoped collaborator token; owner passwords remain in an OS secret store.
```text
NABU_PROFILE_VERSION=2
NABU_API_BASE_URL=https://nabu.example.test/base
NABU_SHARED_SPACE_ID=space_EXAMPLE
NABU_ROOT_PATH=projects/example/shared
NABU_PERMISSIONS=read,write
NABU_ACCESS_TOKEN_EXPIRES_AT=2030-01-01T00:00:00.000Z
NABU_ACCESS_TOKEN=<scoped-token-from-redemption>
```
- Parse exactly this newline-delimited allowlist. Reject duplicate or unknown keys, malformed encoding, multiline values, and missing required fields; never shell-source the file.
- Choose the profile directory in this order: explicit `NABU_CREDENTIALS_DIR` (using the same `<deployment-id>/<shared-space-id>.env` structure beneath it); Codex `~/.codex/secrets/nabu/<deployment-id>/<shared-space-id>.env`; then Hermes `~/.hermes/secrets/nabu/<deployment-id>/<shared-space-id>.env`.
- `<deployment-id>` is exactly `sha256-<64 lowercase hex SHA-256 of canonical API base UTF-8 bytes>`, computed from the exact canonical HTTPS API base including a non-default port and base-path prefix. The profile filename is exactly the validated server-issued `sharedSpaceId` plus `.env`; `profileId` is only a response hint and is not persisted as an eighth key.
- On POSIX, directories are `0700` and profiles `0600`. On Windows, the profile has a current-user-only DACL. Reject symlinks/reparse points, wrong owners, and over-permissive destinations before reading the token.
- Write atomically: create an exclusive temporary file in the same directory, flush it, atomically rename it into place, then verify ownership and permissions. Do not leave temporary copies or print diagnostics containing the token.
### 5. Discover and verify at the beginning of a chat
- Normalize the requested deployment to the exact canonical HTTPS API base URL before scanning profiles. Scan only that deployment directory. If no deployment is named and more than one canonical base exists, ask the user instead of guessing.
- Reject malformed, symlinked, wrong-owner, over-permissive, or locally expired profiles. Discard profiles without the permission required by the operation (`read` for reads; `read,write` for writes), filter candidate roots using normalized segment-aware containment (`projects/example` does not contain `projects/example-private`), then select the greatest segment depth. An equal-depth set of eligible candidates is ambiguous and must stop discovery rather than silently choosing.
- Import the legacy Hermes single-file profile at `~/.hermes/secrets/nabu.env` only when no structured profile exists for that deployment; validate it with the same allowlist and migrate it to the structured path atomically before use.
- verify the selected bearer token against the scoped tree or another in-scope read before any requested note operation. A 401/expired/revoked response means the profile is unusable; request a new invite only after reporting that verification failed.
### 6. Issue a read-only URL token
POST https://context.heyhiltd.tech/api/shared-spaces/:sharedSpaceId/read-link
```json
{ "durationDays": 14 }
```
- This is owner-only. `durationDays` must be between 1 and 183 days; the default is 7 days and the effective expiry cannot outlive the shared space.
- The response returns `shareUrl`, `sharedSpaceId`, `rootPath`, `permission: "read"`, `durationDays`, and `expiresAt`. The opaque token is only in `shareUrl`.
- There is exactly one active read-only URL token per shared space. Issuing another token immediately invalidates the previous one.
- `DELETE /api/shared-spaces/:sharedSpaceId/read-link` revokes the current URL token without revoking the shared space. Repeating the DELETE is safe.
- Treat `shareUrl` as a secret capability. Never print it, commit it, or save it in an ordinary workspace file.
- A human can open the returned URL directly, for example `https://nabu.example.com/?path=projects%2Fcanner&token=OPAQUE_TOKEN`. The browser shows the shared subtree in read-only mode and keeps the token while navigating within that subtree.
- Agents can use the same token on read APIs as a query parameter, for example `GET /api/vault/tree?token=OPAQUE_TOKEN` or `GET /api/vault/notes/by-path?path=projects%2Fcanner%2Freadme.md&token=OPAQUE_TOKEN`.
- Public reads cannot access parent, sibling, or outside paths. Such links return a generic `Shared space unavailable` response; local in-scope assets use `/api/vault/assets?path=<vault-relative-file>&token=OPAQUE_TOKEN`.
- Do not use a read-only URL token on write endpoints. It has no write permission and must never be upgraded by the agent.
### Shared-space management
- `GET /api/shared-spaces/` lists spaces owned by the authenticated owner.
- `GET /api/shared-spaces/:sharedSpaceId` inspects an owned space.
- `POST /api/shared-spaces/:sharedSpaceId/revoke` revokes the space immediately; existing tokens fail synchronously.
- `POST /api/shared-spaces/:sharedSpaceId/invites` creates another one-time invite for an active owned space.
- `POST /api/shared-spaces/:sharedSpaceId/extend` requires explicit confirmation and may not exceed the 183-day maximum.
Extension body:
```json
{ "confirmed": true, "durationDays": 7 }
```
Owner-only management operations must use the owner password session or the password-derived owner bearer credential. Shared collaborators cannot create broader credentials, extend leases, or revoke spaces.
## Error contract
- `400`: invalid path, invalid request body, invalid duration, or invalid permissions. Fix the request; do not retry unchanged.
- `401`: missing, expired, or revoked authentication. Re-authenticate as owner or use a valid shared token.
- `403`: authenticated principal lacks permission for the operation or path.
- `404`: note/folder not found or intentionally hidden because it is outside the shared scope.
- `409`: create conflict, move destination exists, folder is not empty, raw source conflict, or stale note revision.
- `410 SHARED_SPACE_INVITE_INVALID`: invite is invalid, expired, redeemed, revoked, or its space is unavailable.
- `428 WRITE_REVISION_REQUIRED`: re-read the note and retry with `If-Match` or `expectedRevision`.
- When present, use structured `code`, `nextAction`, `readUrl`, and `currentRevision` fields to decide the next request.
- Do not expose or repeat private paths from errors, search results, backlinks, neighborhoods, or graph responses.
## MCP
- Local command: npm run mcp.
- Direct-vault environment: NABU_MCP_MODE=direct, KNOWLEDGE_PATH=<absolute-vault-path>.
- Deployed-service environment: NABU_MCP_MODE=remote, NABU_URL=<https-url>, NABU_AGENT_TOKEN=<issued-credential> or NABU_PASSWORD=<same-password-as-Nabu>.
- Transport: stdio.
- Native remote MCP endpoint: `POST https://context.heyhiltd.tech/mcp` (streamable-http-stateless).
- Native MCP authentication: `Authorization: Bearer`; bootstrap behavior: No Authorization header exposes only redeem_shared_space_invite; persist the returned scoped token.
- Owner agents should use the issued `NABU_AGENT_TOKEN`; existing agents may use `NABU_PASSWORD` as the backwards-compatible remote stdio fallback. Keep both secrets outside the repository and use HTTPS for non-loopback services.
- Use MCP redemption for one-time invites, then save the scoped token in a secure profile. Owner-only MCP tools handle shared-space management.
- For native MCP `create_note` and `update_note`, send the publishing acknowledgement header above on the JSON-RPC request and use the complete structured document form.
- The MCP and HTTP layers use the same domain service and authorization rules.
## Deployment requirements
- A deployment may be operated by anyone, so agents must use the deployment-specific `/agents.md` rather than assuming a hosted Nabu version.
- Shared-space leases, invite hashes, and access-token hashes must live in durable server-side metadata, not Markdown files or ephemeral per-request memory.
- If the deployment runs multiple instances, all instances must share the same durable metadata store and vault; otherwise one-time redemption and revocation consistency cannot be guaranteed.
- Never store raw invite or access-token secrets. Use cryptographically secure randomness, hashed persistence, atomic redemption, and server-time expiry checks.
## Safety checklist
- For sharing: preview -> show complete scope -> obtain explicit confirmation -> confirm -> distribute one-time invite.
- For redemption: generate one in-memory idempotency key -> POST the exact `inviteUrl` payload -> after an unknown first outcome make at most one recovery request with the same invite and key -> capture the token securely -> persist the scoped profile atomically -> verify with bearer auth -> never log either secret. A version-1 `410` after recovery requires a new invite.
- For writes: read -> edit/merge -> send revision precondition -> on 409/428 re-read and retry safely -> verify by canonical path.
- For private data: authorize before reading or mutating every endpoint and filter all derived results to the caller scope.