# Security & access

> Token types (personal, agent, service), scopes, repository and route limits, 2FA, the secret check and allowSecret, lawful purge and the audit trail.
>
> Source: https://www.weave.directory/docs/security

Who can do what in Weave, how tokens are limited, and the guards that keep secrets and history safe. For how we run the service, see the [Security](https://www.weave.directory/security) page.

## Token types

| Token | Prefix | Who it is | Calls | Lifetime |
| --- | --- | --- | --- | --- |
| Personal access token (PAT) | `weave_` | A person | Platform `/api/v1/*`, the Weave server `/api/weave/*`, the CLI | 1–365 days (default 30) |
| Agent token | `wva_` | A registered agent (`agent:<id>`), acting for the person who issued it | The Weave server and the CLI; **not** `/api/v1` (except worker agents removing / restoring workstreams) | ≤ 24 hours (default 60 minutes) |
| Service token | `wvs_` | A service account: an organization-owned, non-personal identity that can never sign in | Everything a PAT can, except issuing PATs (`POST /api/v1/tokens`) and managing service accounts | 1–365 days (default 90) |

Every token is stored only as a SHA-256 hash and shown **once**, when it is created. Validate any token with `GET https://weave.directory/api/whoami`:

```json
{ "id": "agent:cm9x2…", "name": "builder-1", "kind": "agent",
  "agent": { "id": "cm9x2…", "name": "builder-1", "kind": "worker", "actingFor": "<userId>" },
  "scopes": ["repo:read", "repo:write"], "repoAccess": { "cm3k…": "write" },
  "repos": ["cm3k…"], "repoRefs": [{ "id": "cm3k…", "name": "shop", "org": "acme", "slug": "acme/shop" }],
  "pathScope": ["app/**", ".weave/decisions/**"], "tokenId": "…" }
```

`kind` is `user`, `agent` or `service`. `pathScope` appears only for agents with a path limit; `allowedRoutes` only for confined tokens.

### Service accounts

A service account belongs to an organization, has no password, sign-in or email, and acts only through `wvs_` tokens. Use one for a builder or CI so nothing runs as one person. Managing them needs a **person's** PAT from an owner or admin of the organization (service and agent tokens are refused: no self-escalation).

```bash
curl -s https://weave.directory/api/v1/orgs/acme/service-accounts \
  -H "Authorization: Bearer $WEAVE_PAT" -H "Content-Type: application/json" \
  -d '{"name": "builder", "role": "admin"}'

curl -s https://weave.directory/api/v1/orgs/acme/service-accounts/$SA_ID/tokens \
  -H "Authorization: Bearer $WEAVE_PAT" -H "Content-Type: application/json" \
  -d '{"name": "builder-prod", "scopes": ["org:admin", "repo:admin", "tokens:write"], "expiresInDays": 90}'
```

A service token may register agents and issue agent tokens; those agents then act for the service account. Disabling a service account deletes its tokens and revokes agent tokens acting for it.

## Scopes

| Scope | Allows |
| --- | --- |
| `repo:read` | Read repositories |
| `repo:write` | Stitch and weave (includes `repo:read`) |
| `repo:admin` | Administer and delete repositories (includes `repo:write`) |
| `org:admin` | Administer organizations: create repositories, register agents |
| `tokens:write` | Issue and revoke tokens |

- A token **never exceeds its owner's real access**: scopes only narrow it. On the Weave server, no `repo:write` means read-only, no `repo:admin` means at most write, and no `repo:*` scope means no repository access.
- A token can't issue a PAT or agent token with scopes or repositories beyond its own.
- A token stored with no scopes (an old one) counts as having all five.

## Limiting a token

| Limit | On | Effect |
| --- | --- | --- |
| `repositoryIds` | PATs, agent tokens, service tokens | Only these repositories. A token limited to repositories can't create repositories (`403 insufficient_scope`). |
| `allowedRoutes` | Service tokens | Up to 50 patterns `METHOD /path/glob` (`*` one segment, `**` the rest). Applies to `/api/v1` and every Weave server route, including the WebSocket handshake. `GET /api/whoami` is always allowed. Refused: platform `403 route_not_allowed`; Weave server `403 forbidden`. |
| `pathScope` | Agents | Globs the agent may write to. Writes outside: `403 path_out_of_scope`. See [For AI agents](https://www.weave.directory/docs/agents#pathscope). |

Example `allowedRoutes`: `["GET /api/weave/*/workstreams", "POST /api/weave/*/stitch", "* /api/v1/repos/r1/**"]`.

## Repository access levels

The platform tells the Weave server each caller's level per repository: `read`, `write` or `admin`. Archived repositories are capped at `read`. Access comes from organization membership, teams and direct collaborators. Mutations need `write`; admin-only actions (protection, tags deletion, purges, mirrors, imports, audit export, deciding suggestions as admin) need `admin`.

## Two-factor authentication

- 2FA (TOTP) is available to everyone: avatar → **Settings** → **Account** → **Set up 2FA**.
- **Owners and admins of an organization need 2FA** to administer it (personal organizations are exempt). Without it they act as plain members. Over the API the call answers `403 two_factor_required`.
- An organization can **require 2FA of every member** (org **Settings** → **Security** → **Require two-factor authentication**). Members without it lose access to its private repositories, through PATs too.
- Issuing a PAT needs a verified email (`403 email_unverified`) and 2FA when an organization requires it. Service accounts count as compliant.

## Secret detection

The same secret detector runs in the CLI (`weave add`, `weave stitch`) and on the server for **every** stitch, web edit, change-request resolution, blob upload and seed, whatever sends it. It finds:

- secret-looking file names: `.env`, `.env.local`, `.env.*.local`, `.env.development*`, `.env.production*`, `.env.test*`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `id_rsa*`, `*.tfstate*`, `*.tfplan`, `.terraform/**`;
- content: PEM private keys, AWS access key ids, GitHub tokens (`ghp_…`, `github_pat_…`), OpenAI and Anthropic API keys, Stripe live keys (`sk_live_…`, `rk_live_…`), Slack tokens (`xoxb-…`, `xoxa-…`, `xoxp-…`) and Google API keys.

Only what a request **introduces** counts. The refusal names the file, line and kind, never the secret:

```json
{ "type": "error", "code": "secret_detected", "message": "…",
  "findings": [{ "path": "config/keys.ts", "line": 4, "kind": "AWS access key id" }] }
```

The CLI refuses before sending. `weave init` writes a starter `.weaveignore` with the secrets section above.

### Allowing one deliberately

`weave add --allow-secret <path>` and `weave stitch --allow-secret <path>` (repeatable; a file or folder) include such a file on purpose. Over the API, send `"allowSecret": ["<path>", …]` in the body (`""` = everything). The server honours it for **repository admins only** and records it in the operation log (`via: "allow_secret: <paths>"`).

## Lawful purge

A lawful purge removes content — a leaked secret, personal data — from a repository's **whole history** without breaking a single hash. The content is withheld; its hash stays known, so states, changesets and the log still verify.

| Kind | Removes |
| --- | --- |
| `blob` | One binary file's bytes |
| `text` | Every occurrence of a string (8–4,096 characters), anywhere in history |

It takes **two different repository admins**: one requests, another approves. Agents can do neither. A request expires after 7 days.

```bash
# admin 1: request (the text is never returned, logged or sent in events)
curl -s https://api.weave.directory/api/weave/$REPO_ID/purges \
  -H "Authorization: Bearer $ADMIN1_PAT" -H "Content-Type: application/json" \
  -d '{"text": "AKIA…the leaked key…", "reason": "leaked AWS key (rotated)"}'

# admin 2: approve
curl -s -X POST https://api.weave.directory/api/weave/$REPO_ID/purges/$PURGE_ID/approve \
  -H "Authorization: Bearer $ADMIN2_PAT" -H "Content-Type: application/json" -d '{"note": "checked"}'
```

- A text purge is refused (`409`) while any active workstream head still holds the text. **Stitch the fix and rotate the secret first**, then purge history.
- Approval withholds the content in every stored state, redacts changesets, suggestions, change requests, resolution points and event payloads, replaces rendered copies with a placeholder, deletes exports and drops search embeddings. A manifest records what was withheld; its hash is in the log.
- After a purge, the file reads as `[weave] The content of this file was removed by a lawful purge (<id>).`. `weave clone` writes the placeholder, `weave status` lists it under "Withheld by a lawful purge", and new content simply replaces it.
- Not rewritten: the operation log itself, git mirrors already pushed, repository copies and other people's local checkouts.
- Other routes: `GET …/purges[?state=]`, `GET …/purges/{id}`, `GET …/purges/{id}/manifest`, `POST …/purges/{id}/cancel`. Webhooks: `purge.requested`, `purge.approved`, `purge.cancelled`.

## Audit trail

- **Operation log.** Every action in a repository is in an append-only, hash-chained log (`weave log`, `weave log --verify`, `GET /api/weave/{repoId}/log/verify`).
- **Checkpoints.** Every hour the server verifies each changed repository's chain and writes checkpoints to write-once storage, each run chained to the previous one. `GET /api/weave/{repoId}/audit/verification` shows `status` (`verified`, `broken` with `brokenAt`, or `pending`) and the latest checkpoint; `?deep=true` (admin) re-reads the stored checkpoint.
- **Export.** `GET /api/weave/{repoId}/audit/export?from=&to=&after=&limit=&format=json|ndjson` (admin). Each entry's id is the hash of its canonical JSON, so the export verifies itself.
- **Token use.** Every authenticated request to the platform is recorded (time, token id, method, path, a hash of the IP, trace id) for 90 days. Read your own: `GET /api/v1/tokens/{id}/usage`; a service account's: `GET /api/v1/orgs/{slug}/service-accounts/{id}/usage`.

## Rate limits

| Where | Per | Limit |
| --- | --- | --- |
| Platform `/api/v1` | IP | 1200 / minute |
| Platform `/api/v1` | token | 600 / minute |
| Weave server | IP | 6000 / minute |
| Weave server | token | 3000 / minute |

Over a limit: `429 rate_limited` with `Retry-After: <seconds>`. Back off, or spread work across tokens.
