src/ops-types/portal.ts
191 lines and 0 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
None: it only declares types or values.
Imports
It imports src/ops-types/feed.ts. Imported by src/audit-detail.ts, src/controls-base.ts, src/controls-perform.ts, src/controls-plan.ts, src/controls-token.ts, src/controls.ts, src/portal-actions.ts, src/portal-activity.ts, src/tools/controls.ts.
Source
import type { OpsFeed } from "./feed";
// ---------------------------------------------------------------------------
// The Portal's controls (capsid/research/design-portal-unify.md, section 2).
//
// Two requests, as ruled 2026-09-11: a preview that writes nothing and returns what
// will change plus a signed token, then a perform that carries only the token. Both
// are JSON POSTs with the headers X-Capsid-CSRF (the feed's csrf value) and
// Content-Type: application/json.
// POST /portal/api/actions/preview body PortalActionRequest -> PortalPreview
// POST /portal/api/actions/perform body { token } -> PortalPerformed
// GET /portal/api/namespaces -> PortalNamespaces
// GET /portal/api/activity?namespace=&actor= | ?id= -> PortalActivity
// GET /portal/api/claims?job= | ?namespace=&agent=&since=&until= -> PortalClaimsJob | PortalClaimsAggregate
// GET /portal/api/packages/history?name= -> PortalPackageHistory
// GET /portal/api/stale -> PortalStale
// GET /portal/api/plan -> PortalPlan
// GET /portal/api/digest?since= -> PortalDigest
// GET /portal/api/maintenance -> PortalMaintenance
// GET /portal/api/convergence -> PortalConvergence
// GET /portal/api/push -> PortalPush
// POST /portal/api/push body { op: "on", subscription, label } | { op: "off", id } | { op: "test" } -> PortalPush
// POST /portal/api/sign-out body {} -> 204, the Portal's cookies expired
// A refusal is text/plain: 400 refused or invalid, 403 CSRF or cross-site, 410 the
// token expired (preview again), 413 body too large. Signed out is the gate's 302.
export type PortalAction =
| "overnight"
| "resume_job"
| "release_job"
| "fail_job"
| "close_shipped"
| "supersede_job"
| "park_job"
| "unpark_job"
| "pr_close"
| "approve_pr"
| "revoke_agent"
| "site_add"
| "site_edit"
| "site_remove"
| "reset_breaker"
| "package_add"
| "package_edit"
| "package_remove"
| "canon_approve"
| "canon_reject"
// Every pending canon proposal in one decision (src/canon.ts, readSweep).
| "canon_approve_all"
| "site_repair"
// The merge units (docs/design/design-merge-pipeline.md section 7): a signed policy
// change that promotes or removes a unit, and the Undo of a unit's suspension.
| "sign_policy_change"
// Write the auto-merge policy from docs/policy/auto-merge.md on the default branch and
// sign it (src/policy-sign.ts, planPolicyFromRepo).
| "sign_policy_from_repo"
| "unit_resume"
// Lift a namespace's auto-merge pause (src/merge-safety.ts), the shutoff after bad
// unattended merges.
| "automerge_resume";
// params by action:
// pause { namespace, reason, undo? } reason required
// unpause { namespace, reason, undo? } reason required
// mode { value: "api" | "subscription" | "off", reason, undo? } reason required
// overnight { value: "off" | "api" | "subscription", reason, undo? } reason required
// choosing subscription records Dustin's decision, its date and its
// reasoning with the switch (src/overnight.ts)
// undo: "true" marks the reverse of a change just made, from the
// Portal's Undo; the click row is then portal-undo-<action>
// instead of portal-<action>.
// resume_job { id, reason, note? } reason required; note is the full approval,
// which the driver reads as resume_note.note
// release_job { id, reason } reason required
// fail_job { id, reason } reason required
// close_shipped { id, reason } reason required; the seat's complete of a
// blocked job another credential holds, credited to that holder
// supersede_job { id, reason, replaced_by? } reason required; a queued job, or a
// claimed one with no work recorded, ends as superseded (no outcome)
// park_job { id, reason, undo? } reason required, one line: when it comes back
// unpark_job { id, reason?, undo? } a parked job goes back to the queue; the reason
// is recorded in the click row only (unpark itself takes none)
// pr_close { namespace, number, reason, repo? } reason required; closes an open
// pull request as capsid_close_pr action close does, head branch cleanup included
// approve_pr { namespace, prs, shas, repo?, confirm?, reason?, undo? } prs and shas
// comma-separated, in the same order: each pull request at the head sha
// that was reviewed. Records approvals (src/pr-approvals.ts) and never
// merges; a typed (money) one only alone, with its number as confirm.
// undo: "true" voids the open approvals of prs, "undone"
// revoke_agent { name }
// site_add { namespace, name?, origin?, health_path?, platform?, script? }
// no origin: the namespace serves no site, and takes no other field
// site_edit { namespace, revision, name?, origin?, health_path?, platform?, script? }
// every field is the row as it will be; revision is the one shown
// site_remove { namespace, revision }
// reset_breaker { namespace } the queue's circuit breaker (src/job-breaker.ts)
// package_add { name, repo?, formerly? }
// package_edit { name, revision, repo?, formerly? } the row as it will be
// package_remove { name, revision }
// canon_approve { id } a pending canon proposal (src/canon.ts); refused
// when the document moved past the body it was written against
// canon_reject { id, reason } reason required; the proposer reads it
// canon_approve_all { reason? } every pending proposal: the newest on each
// document approved unless stale, older ones rejected as superseded,
// stale ones as outdated. The token also binds the plan the preview read
// (params.plan, "<id>:<outcome>,..."), and a perform whose plan reads
// differently now is refused. reason is recorded in the click row.
// site_repair { namespace, tool } one allowlisted tool on a site's operator API
// (src/site-repair.ts); refused for a tool outside the site's list or the
// Worker's ceiling, and while the rate limit holds
export interface PortalActionRequest {
action: PortalAction;
params: Record<string, string>;
}
export interface PortalPreview {
action: PortalAction;
// One sentence: what this does, naming the target.
summary: string;
// Exactly what changes, one line each, from the state read now.
changes: string[];
// The audit rows the perform will write, as "<action> by <actor>".
audit: string[];
// Signed; carries action and params. Valid until expires_at.
token: string;
expires_at: string;
}
export interface PortalPerformed {
action: PortalAction;
summary: string;
// Set when the action happened but the click's own audit row was not written.
warning: string | null;
// The feed as it stands after the action.
feed: OpsFeed;
}
export interface PortalNamespace {
namespace: string;
// The queue's circuit breaker: open after the threshold of holder fails in 24 hours,
// until reset. since is the window's start in audit_log's UTC format.
breaker: { open: boolean; failed: number; threshold: number; since: string; reset_at: string | null };
// Null means no truth report was ever run, which is not an integrity of zero.
latest_report: { integrity: number | null; generated: string } | null;
jobs: { queued: number; claimed: number; blocked: number; done_today: number };
}
export interface PortalNamespaces {
generated: string;
namespaces: PortalNamespace[];
}
export interface PortalActivityRow {
// The audit_log row id: unique, so the view keys on it.
id: number;
at: string;
actor: string | null;
action: string | null;
namespace: string | null;
path: string | null;
// What the row records. A job transition writes two rows with one action, actor and
// path: "job" for the job and "document" for its mirror document. null when the
// row's params name neither.
target: "job" | "document" | null;
// What the row recorded, by name (src/audit-detail.ts). Never the raw params.
detail: AuditDetail;
}
// One field of an audit row's params, labelled in plain English.
export interface AuditField {
name: string;
value: string;
}
// One field that differs between a row's `before` and `after`. before is null for a
// field the change added, after is null for one it removed.
export interface AuditChange {
field: string;
before: string | null;
after: string | null;
}
export interface AuditDetail {
// The reason typed with the change, when the row carries one.
reason: string | null;
// Field by field, when the row carries a before or an after; null when it carries neither.
changes: AuditChange[] | null;
fields: AuditField[];
// Recorded fields not shown: hashes, signatures and nested values.
withheld: number;
// The params are not JSON (rows older than that rule), so nothing can be shown.
unreadable: boolean;
}
export interface PortalActivity {
generated: string;
// id: one row by its id, for the Activity drawer; the namespace and actor filters
// are then not applied.
filter: { namespace: string | null; actor: string | null; id: number | null };
// Newest first, at most `limit`.
rows: PortalActivityRow[];
limit: number;
}