Approval gates
Overview
An approval gate holds a certificate job at pending_approval until an authorized workspace member explicitly approves it. A job in this state is invisible to agents: the claim path only ever selects jobs already at pending, so nothing runs until a human signs off. See Scheduled renewals and approvals for where this fits into the broader automation picture.
Approval gates are opt-in per job, not a workspace-wide setting:
- Manual jobs: set
requiresApproval: truewhen creating a job (dashboard Create manual job dialog, orPOST /api/v1/workspaces/{id}/certops/jobs). - Bulk renew: set
requiresApproval: trueon aPOST /api/v1/workspaces/{id}/certops/jobs/bulk-renewrequest; every certificate in the batch gets the same gate. - Scheduled automation: renewal jobs created by the scheduler sweep do not currently support
requiresApproval. Gate a certificate's renewals today by using bulk renew (or a manual job) instead of waiting for the automatic sweep.
Without requiresApproval, a job starts at pending and is claimable immediately, exactly as before approval gates existed.
The non-requester rule
The person who requested a job cannot approve their own job. This is enforced server-side, not just in the UI:
- If the job was created by a signed-in user, that user's approval attempt is rejected with
403 CERTOPS_APPROVAL_SELF_APPROVAL_FORBIDDEN. Any other authorized member (workspace manager or admin) can approve it. - If the job was created through the API by a machine token rather than a session, there is no requester user to compare against, so any authorized member can approve it, including whoever operates the automation that created it. Treat API-token-originated jobs as needing the same human judgment at approval time that a manual job would.
- Rejection has no non-requester restriction. Any authorized member, including the original requester, can reject/withdraw a pending job. This is deliberate: you should always be able to cancel your own request.
What invalidates an approval
An approval is bound to the exact bytes of the job payload at the moment it was granted, not just to the job id:
- When you approve a job, TokenTimer computes a SHA-256 hash of the canonical job payload and stores it alongside the approval.
- Before an agent can claim the job, the same hash is recomputed from the payload as it stands right then.
- If the hashes differ, the job is flipped back to
pending_approvalautomatically, anapproval.invalidatedentry is written to the job log, and the job stays unclaimable until it is approved again.
In practice this rarely triggers today: no human-facing route mutates a job's payload after creation. It exists as defense in depth, so an approval can never be silently reinterpreted against different work than what was reviewed.
Approving or rejecting a job
Dashboard: open Certificate operations, find the job at status Pending approval, and use Approve or Reject. Rejecting prompts for an optional reason.
API: both actions require a workspace-manager (or admin) session; machine tokens cannot approve or reject.
POST /api/v1/workspaces/{id}/certops/jobs/{jobId}/approve
POST /api/v1/workspaces/{id}/certops/jobs/{jobId}/reject
Both accept an optional JSON body:
{ "reason": "Confirmed with the domain owner before renewing" }
reason is free text, capped at 1024 characters; omit it if you have nothing to add. Approving moves the job to pending (claimable); rejecting moves it directly to the terminal rejected status with error_code: "CERTOPS_APPROVAL_REJECTED".
Common error codes: 404 CERTOPS_JOB_NOT_FOUND, 409 CERTOPS_APPROVAL_JOB_NOT_PENDING_APPROVAL (the job already left pending_approval, for example a concurrent decision won the race), 403 CERTOPS_APPROVAL_SELF_APPROVAL_FORBIDDEN (approve only), 400 CERTOPS_APPROVAL_REASON_INVALID (reason not a string, or over the length cap).
Note that deciding an approval works even while the workspace kill switch is paused: the agent claim path is already blocked by the kill switch, so approving or rejecting during an incident cannot cause unwanted execution, and rejecting is exactly the kind of action an operator may need to take while paused.
Job log and audit trail
Every decision is recorded twice, atomically, in the same transaction as the status change:
- Job timeline (
GET .../jobs/{jobId}/log): anapproval.granted,approval.rejected, orapproval.invalidatedentry, with the reason (if any) and the deciding user. - Workspace audit log: the decision appears alongside other CertOps admin actions, searchable by job id.
API reference
POST /api/v1/workspaces/{id}/certops/jobs/{jobId}/approve— approve a pending-approval job (workspace manager+, session auth).POST /api/v1/workspaces/{id}/certops/jobs/{jobId}/reject— reject a pending-approval job (workspace manager+, session auth).GET /api/v1/workspaces/{id}/certops/jobs/{jobId}/log— job timeline, including approval decisions.
See Certificate Automation for the surrounding job/evidence API, and Executor jobs for job statuses and how jobs are created in the first place.