> Call POST /v1/check before a tool that writes. DENY means do not invoke the tool. For clean Markdown of any page, append .md to the page URL. For the complete index, fetch https://docs.withwrit.com/llms.txt.

# Portal agent

> Chat with your tenant's Writ configuration. Phase 1 is internal-only and feature-flagged.

# Portal agent

The portal agent is a conversational interface for configuring your Writ tenant. It is docked in the right-side panel of the Overview page and acts as its own first-class principal (`portal-agent`).

> **Note**
>
> Phase 1 is internal-only and gated by the `WRIT_AGENT_TENANTS` allow-list. API key management is planned for Phase 2.

## What it can do (Phase 1)

* **Read** the tenant policy, environment overrides, per-principal overrides, principals, API key metadata, recent receipts, and the audit-log chain verification.
* **Update policy** — tenant default, per-environment (`dev`/`staging`/`prod`), and per-principal.
* **Create, update, and delete principals**.
* **Create, update, and delete custom verbs**.
* **Revert** a policy change within one hour ("revert that") — staged as a normal diff, approved the same way.

It cannot touch billing, other tenants, API keys (Phase 2), or its own governing verbs. It cannot disable its `writ_check` wrapping or touch the audit log.

## How it works

Every mutating tool call the agent makes goes through `POST /v1/check` against the tenant's own policy under `agent.*` verbs:

| Agent action                              | Verb                              | Default            |
| ----------------------------------------- | --------------------------------- | ------------------ |
| Read policy / list principals / list keys | `agent.read`                      | `allow`            |
| Update tenant policy                      | `agent.policy.update`             | `step_up`          |
| Update environment policy                 | `agent.policy.environment.update` | `step_up`          |
| Update principal policy                   | `agent.policy.principal.update`   | `step_up`          |
| Create principal                          | `agent.principal.create`          | `step_up`          |
| Update principal                          | `agent.principal.update`          | `step_up`          |
| Delete principal                          | `agent.principal.delete`          | `step_up` (always) |
| Revoke principal keys                     | `agent.principal.revoke_keys`     | `step_up` (always) |
| Create custom verb                        | `agent.verb.create`               | `step_up`          |
| Update custom verb                        | `agent.verb.update`               | `step_up`          |
| Delete custom verb                        | `agent.verb.delete`               | `step_up` (always) |

Destructive / irreversible actions (principal delete, key revoke, verb delete) always require step-up even if a tenant policy were somehow set to `allow`. Every `prod` environment policy change also forces step-up, widening or not.

## Diff previews and approval

Nothing mutates on a bare chat message — not even when the tenant policy would allow the action. When the agent wants to write, it stages a pending action and returns a diff-preview card showing:

* The exact verb / environment / principal
* The current value (`before`)
* The proposed value (`after`)

The user chooses **Approve**, **Edit**, or **Cancel**. Approving calls `POST /v1/agent/actions/{actionId}/approve`. If the verb requires step-up, the request must include a valid `totpCode`. An optional `params` object edits the staged diff — edited params are re-validated against the tool schema and the meta-policy, then re-checked before apply.

The approval endpoint re-runs the policy check (a flip to `deny` stops the write), enforces TOTP step-up with a one-time grant, verifies the fresh auth token immediately before the write, and only then executes the tool. A receipt with principal `portal-agent` and purpose = the user's chat request is written to the audit log for every agent action.

## Chat endpoint

Send a message:

```bash
curl -X POST https://api.withwrit.com/v1/agent/chat \
  -H "Content-Type: application/json" \
  -b "writ_session=..." \
  -H "X-Writ-CSRF: 1" \
  -d '{"message": "set refund to step_up in prod"}'
```

Response when a diff is pending:

```json
{
  "type": "reply",
  "message": "I drafted a change. Please review the diff and approve or cancel.",
  "pendingActions": [
    {
      "tool": "update_env_policy",
      "params": { "environment": "prod", "updates": { "refund": "step_up" } },
      "result": {
        "status": "pending",
        "actionId": "act_...",
        "stepUp": true,
        "diff": {
          "verb": "agent.policy.environment.update",
          "environment": "prod",
          "before": { "refund": "require_grant" },
          "after": { "refund": "step_up" }
        }
      }
    }
  ]
}
```

Approve with TOTP:

```bash
curl -X POST https://api.withwrit.com/v1/agent/actions/act_.../approve \
  -H "Content-Type: application/json" \
  -b "writ_session=..." \
  -H "X-Writ-CSRF: 1" \
  -d '{"totpCode": "123456"}'
```

Approve with edits to the staged diff:

```bash
curl -X POST https://api.withwrit.com/v1/agent/actions/act_.../approve \
  -H "Content-Type: application/json" \
  -b "writ_session=..." \
  -H "X-Writ-CSRF: 1" \
  -d '{"totpCode": "123456", "params": {"updates": {"refund": "deny"}}}'
```

Streaming: send `Accept: text/event-stream` with `POST /v1/agent/chat` to receive the reply as SSE frames (`event: message`, then `event: done`).

## Guardrails

* **Tenant scope only** — verified server-side on every tool call.
* **No sponsor token** — the agent rides the logged-in user's session. It never holds, displays, or rotates the sponsor token.
* **No full API keys in chat** — key values are shown once at creation; the agent displays prefixes only.
* **Prompt injection resistance** — chat input is data, not instruction. The immutable server-side system prompt, the tool allowlist, and the policy engine are the backstops: a model-proposed plan naming an unknown tool is rejected outright.
* **Rate caps** — per-user / per-tenant tool-call ledger, 60 calls/hour by default (`WRIT_AGENT_HOURLY_CAP`); global kill switch (`WRIT_AGENT_KILL_SWITCH`) plus a per-tenant kill switch (`WRIT_AGENT_DISABLED_TENANTS`).
* **Meta-policy** — the agent cannot edit `agent.*` verbs, create or rename custom verbs into the `agent.*` namespace, delete/disable/rename itself, revoke its own keys, set per-principal policy on itself, disable its wrapping, or touch the audit log. Those controls are enforced in the tool layer, not in the prompt.
* **Model** — Bedrock, server-side only (`WRIT_AGENT_BEDROCK_MODEL`); no model keys in the browser. When unconfigured, a deterministic parser handles Phase 1 intents so the wrap stays testable without credentials.

## Adversarial requests

Requests such as "disable the audit log", "reveal the full API key", or "allow \* on prod" are detected and blocked with a `DENY` receipt written to the audit log.

## Operations

Conversation threads (24h TTL), pending diffs (1h TTL), applied-action revert records (1h TTL), and the hourly tool-call ledger live in DynamoDB table `writ-agent` (PK `id`, TTL attribute `ttl`, selected by `WRIT_AGENT_TABLE`; without it the Lambda falls back to per-invocation memory, which loses pending diffs). Phase 1 rollout: set `WRIT_AGENT_TENANTS` to Chad's tenant id only.