Portal agent
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).
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:
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:
Response when a diff is pending:
Approve with TOTP:
Approve with edits to the staged diff:
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 theagent.*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.