Skip to navigation

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).

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 actionVerbDefault
Read policy / list principals / list keysagent.readallow
Update tenant policyagent.policy.updatestep_up
Update environment policyagent.policy.environment.updatestep_up
Update principal policyagent.policy.principal.updatestep_up
Create principalagent.principal.createstep_up
Update principalagent.principal.updatestep_up
Delete principalagent.principal.deletestep_up (always)
Revoke principal keysagent.principal.revoke_keysstep_up (always)
Create custom verbagent.verb.createstep_up
Update custom verbagent.verb.updatestep_up
Delete custom verbagent.verb.deletestep_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:

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:

{
"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:

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:

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.