API Error Codes
Destination currency (source-verified 2026-10-07 at
f9dbf896333339ed6ed3e655d4e8e3a793cc38fe): Evidence hosts attestation, canonical-record, capsule, and signing-key reads under/api/evidence/v1; OfflineKit hosts the standard export API under/api/offlinekit/v1/exports. The owner handler suffix determines the complete path. See Evidence architecture. This documentation update does not verify a live signing, promotion, or export operation.
Overview
All API errors follow a consistent format with error codes for programmatic handling.
Error Response Format
interface ApiErrorResponse {
success: false;
error: {
code: string; // Machine-readable error code
message: string; // Human-readable message
details?: object; // Additional context
validationErrors?: ValidationError[];
};
meta: {
requestId: string;
timestamp: string;
};
}
interface ValidationError {
field: string;
message: string;
code: string;
}
Error Code Categories
| Prefix | Category | HTTP Status Range |
|---|---|---|
AUTH_ | Authentication | 401 |
PERM_ | Authorization/Permission | 403 |
VAL_ | Validation | 400 |
RES_ | Resource | 404, 409 |
ENV_ | Environment | 422 |
REL_ | Release | 422 |
PROM_ | Promotion | 422 |
DEPLOY_ | Deployment | 422 |
GATE_ | Gate | 422 |
AGT_ | Agent | 422 |
INT_ | Integration | 422 |
WF_ | Workflow | 422 |
SYS_ | System | 500 |
Authentication Errors (401)
| Code | Message | Description |
|---|---|---|
AUTH_TOKEN_MISSING | Authentication token required | No token provided |
AUTH_TOKEN_INVALID | Invalid authentication token | Token cannot be parsed |
AUTH_TOKEN_EXPIRED | Authentication token expired | Token has expired |
AUTH_TOKEN_REVOKED | Authentication token revoked | Token has been revoked |
AUTH_AGENT_CERT_INVALID | Invalid agent certificate | Agent mTLS cert invalid |
AUTH_AGENT_CERT_EXPIRED | Agent certificate expired | Agent cert has expired |
AUTH_API_KEY_INVALID | Invalid API key | API key not recognized |
Permission Errors (403)
| Code | Message | Description |
|---|---|---|
PERM_DENIED | Permission denied | Generic permission denial |
PERM_RESOURCE_DENIED | Access to resource denied | Cannot access specific resource |
PERM_ACTION_DENIED | Action not permitted | Cannot perform specific action |
PERM_SCOPE_DENIED | Outside permitted scope | Action outside user’s scope |
PERM_SOD_VIOLATION | Separation of duties violation | SoD prevents action |
PERM_SELF_APPROVAL | Cannot approve own request | Self-approval not allowed |
PERM_TENANT_MISMATCH | Tenant mismatch | Resource belongs to different tenant |
Validation Errors (400)
| Code | Message | Description |
|---|---|---|
VAL_REQUIRED_FIELD | Required field missing | Field is required |
VAL_INVALID_FORMAT | Invalid field format | Field format incorrect |
VAL_INVALID_VALUE | Invalid field value | Value not in allowed set |
VAL_TOO_LONG | Field value too long | Exceeds max length |
VAL_TOO_SHORT | Field value too short | Below min length |
VAL_INVALID_UUID | Invalid UUID format | Not a valid UUID |
VAL_INVALID_DIGEST | Invalid digest format | Not a valid OCI digest |
VAL_INVALID_SEMVER | Invalid semver format | Not valid semantic version |
VAL_INVALID_JSON | Invalid JSON | Request body not valid JSON |
VAL_SCHEMA_MISMATCH | Schema validation failed | Doesn’t match schema |
Resource Errors (404, 409)
| Code | Message | HTTP | Description |
|---|---|---|---|
RES_NOT_FOUND | Resource not found | 404 | Generic not found |
RES_ENVIRONMENT_NOT_FOUND | Environment not found | 404 | Environment doesn’t exist |
RES_RELEASE_NOT_FOUND | Release not found | 404 | Release doesn’t exist |
RES_PROMOTION_NOT_FOUND | Promotion not found | 404 | Promotion doesn’t exist |
RES_TARGET_NOT_FOUND | Target not found | 404 | Target doesn’t exist |
RES_AGENT_NOT_FOUND | Agent not found | 404 | Agent doesn’t exist |
RES_CONFLICT | Resource conflict | 409 | Resource state conflict |
RES_ALREADY_EXISTS | Resource already exists | 409 | Duplicate resource |
RES_VERSION_CONFLICT | Version conflict | 409 | Optimistic lock failure |
Environment Errors (422)
| Code | Message | Description |
|---|---|---|
ENV_FROZEN | Environment is frozen | Deployment blocked by freeze window |
ENV_FREEZE_ACTIVE | Active freeze window | Cannot modify during freeze |
ENV_INVALID_ORDER | Invalid environment order | Order index conflict |
ENV_CIRCULAR_PROMOTION | Circular promotion path | Auto-promote creates cycle |
ENV_QUOTA_EXCEEDED | Environment quota exceeded | Max environments reached |
Release Errors (422)
| Code | Message | Description |
|---|---|---|
REL_ALREADY_FINALIZED | Release already finalized | Cannot modify finalized release |
REL_NOT_READY | Release not ready | Release not in ready state |
REL_DIGEST_MISMATCH | Digest mismatch | Resolved digest differs |
REL_TAG_NOT_FOUND | Tag not found in registry | Cannot resolve tag |
REL_COMPONENT_MISSING | Component not found | Referenced component missing |
REL_INVALID_STATUS_TRANSITION | Invalid status transition | Status change not allowed |
REL_DEPRECATED | Release deprecated | Cannot promote deprecated release |
Promotion Errors (422)
| Code | Message | Description |
|---|---|---|
PROM_ALREADY_EXISTS | Promotion already pending | Duplicate promotion request |
PROM_NOT_PENDING | Promotion not pending | Cannot approve/reject |
PROM_ALREADY_APPROVED | Promotion already approved | Already approved |
PROM_ALREADY_REJECTED | Promotion already rejected | Already rejected |
PROM_ALREADY_CANCELLED | Promotion already cancelled | Already cancelled |
PROM_DEPLOYING | Promotion is deploying | Cannot cancel during deploy |
PROM_INVALID_STATE | Invalid promotion state | State doesn’t allow action |
PROM_APPROVER_REQUIRED | Additional approvers required | Insufficient approvals |
PROM_SKIP_ENVIRONMENT | Cannot skip environments | Must promote sequentially |
Deployment Errors (422)
| Code | Message | Description |
|---|---|---|
DEPLOY_IN_PROGRESS | Deployment in progress | Another deployment running |
DEPLOY_NO_TARGETS | No targets available | No targets in environment |
DEPLOY_TARGET_UNHEALTHY | Target unhealthy | Target failed health check |
DEPLOY_AGENT_UNAVAILABLE | Agent unavailable | Required agent offline |
DEPLOY_ARTIFACT_MISSING | Deployment artifact missing | Required artifact not found |
DEPLOY_TIMEOUT | Deployment timeout | Exceeded timeout |
DEPLOY_PULL_FAILED | Image pull failed | Cannot pull container image |
DEPLOY_DIGEST_VERIFICATION_FAILED | Digest verification failed | Image tampered |
DEPLOY_HEALTH_CHECK_FAILED | Health check failed | Post-deploy health failed |
DEPLOY_ROLLBACK_IN_PROGRESS | Rollback in progress | Already rolling back |
DEPLOY_NOTHING_TO_ROLLBACK | Nothing to rollback | No previous deployment |
Deployment Failure Anatomy (execution failures)
The error codes above are pre-deploy 4xx API responses. Once a deploy passes gating and a deployment job runs, an execution failure is described by a structured failure anatomy attached to the job and to each failed target — not a raw exception string. It is surfaced on the deployment DTO (GET /api/v1/release-orchestrator/deployments/{id}) both job-level (failureAnatomy) and per-target (targets[].failureAnatomy), and rendered in the console deployment monitor.
interface DeploymentFailureAnatomy {
stage: string; // where it failed: artifact-generation | bundle-assembly | strategy-planning
// | traffic-gate | preflight | execution | health-probe | retry-prep | rollback
category: string; // typed cause (see below)
reason: string; // stable machine-readable slug
plainReason: string; // operator-facing one-line cause (the lead)
detail: string; // the raw technical string (exception "TypeName: message"), preserved verbatim
action: string; // plain-language remediation guidance
actionPath: string | null; // a real endpoint to act on, or null (never a dead link)
retryable: boolean; // true ONLY for genuinely transient causes
}
Categories: SecretResolution, RegistryUnreachable, AgentUnassigned, AgentOffline, Timeout, TargetNotFound, UnsupportedTarget, NoHealthyTargets, DependencyGraph, PreflightFailed, HealthCheckFailed, NoTargetsEnrolled, StrategyPlanIncomplete, StrategyGateHalted, RollbackNotEligible, Unclassified.
Honesty invariants (binding):
- No fabricated cause — an unrecognized exception/reason maps to
Unclassified(generic action), never a mislabeled specific category. - Detail is never lost — the raw exception type + message is preserved verbatim in
detail; the plain reason leads, the detail is demoted-but-reachable. The legacyfailureReason(job) /error(task) strings stay populated with the plain reason for back-compat. retryableis earned —trueonly forTimeout,RegistryUnreachable,AgentOffline; every content failure (preflight, health-check, secret, unsupported target, …) andUnclassifiedisfalse.- Absent anatomy (jobs created before this contract) → the DTO fields are
nulland the console falls back to the raw string (no fabricated stage/category).
Deployment Preflight (pre-launch readiness)
Before a deploy launches, a preflight runs a tri-state readiness checklist (GET /api/v1/release-orchestrator/releases/{releaseId}/deploy-preflight?environmentId=), and the launch chokepoint enforces it after the un-waivable security floor.
interface PreflightReport {
releaseId: string; environment: string | null;
armed: boolean; // true iff zero hard fails (unknowns do NOT dis-arm)
hasHardFail: boolean;
failedCheckKeys: string[];
checks: PreflightCheck[];
}
interface PreflightCheck {
key: string; // digest-pinned | image-resolvable | target-ready | secret-resolvable
title: string;
status: 'pass' | 'fail' | 'unknown';
detail: string;
anatomy: DeploymentFailureAnatomy | null; // present on a fail (reuses the D1 anatomy shape)
}
Honesty invariants (binding):
- Tri-state, never a false pass — a check that cannot determine its answer (registry unreachable, probe threw, GUID-keyed context unavailable) returns
unknown, neverpass.unknownis surfaced but never a lone hard block; only a definitivefailblocks the launch. - Hard failures are not bypassable — a definitive
failblocks the launch (preflight_failed) until the readiness problem is resolved. The former unsignedpreflightOverrideJustificationpath was retired; it is not equivalent to the signed gate-exception flow. - The scanner-pass floor is independently un-waivable — it runs before preflight enforcement, and a signed gate exception cannot turn an absent or failing scanner verdict into a deploy.
target-ready — current target readiness. The check reads the environment’s current topology (never a recorded deployment). The deploy engine runs only on targets whose stored health is Healthy (ITargetRegistry.ListHealthyAsync in DeployOrchestrator.StartAsync) and needs an active agent on them, so the check separates these outcomes, each with its own reason and next step. Every detail names each relevant target’s health and its last health-check time, or states that no health check is recorded.
| Class | When | Status | Anatomy category (next step) |
|---|---|---|---|
| Empty | no target is enrolled in the environment | fail | NoTargetsEnrolled (enrol a target) |
| Inaccessible | targets are enrolled, but none is currently Healthy (unreachable, unhealthy, degraded, or never checked) | fail | NoHealthyTargets (restore target health) |
| Healthy but not ready | a Healthy target has no assigned agent, or its assigned agent is not active | fail | AgentUnassigned (assign an agent) or AgentOffline (bring the agent back online) |
| Ready | a Healthy target with an active agent whose last health check is at most 15 minutes old | pass | none; the detail also names every other Healthy target that is not ready and why (no agent assigned, assigned agent not active, agent activity not confirmed, or readiness not confirmed), because the deploy engine creates a task on every Healthy target and a task on a target without an active agent fails |
| Not confirmed | a Healthy target with an active agent whose last health check is older than 15 minutes, absent, or later than the preflight clock; or an environment, target or agent lookup that could not be resolved | unknown | none (never a pass, never a lone block) |
The 15-minute freshness window matches the topology projection’s health-check TTL over the default 5-minute readiness-scheduler cadence. Before CCP-007 the check passed on any target with an active agent, regardless of stored health, while the deploy engine then refused the launch with NoHealthyTargets.
Landing Verification & Deployment Attestation (post-deploy)
After a deploy completes, the pipeline verifies the landing and — only on a fully-verified landing — emits a deployment attestation. Both surface on the deployment DTO (landingVerification job-level + targets[].landing per-target; deploymentAttestationRef when attested) and light the console custody Deploy step.
Landing verdict (per target, honest tri-state): compares the digest the agent reported deploying (manifestDigest) against the release’s gated digest. DeploymentLandingVerifier also accepts an independently observed running digest per target as corroboration (observedDigest), but the deployment pipeline (DeploymentEnginePipeline.ApplyLandingVerificationAsync) does not supply one today, so observedDigest is null and a verified result rests on the agent report alone. That is the accepted trust model: a matching agent report is verified without an independent observation.
verified— every digest held for the target (the agent report and, when supplied, the observation) equals the gated digest (deploy-by-digest proven for that target under the agent-report trust model).mismatch— a held digest differs from the gated digest, including an observation that contradicts a matching agent report (a loud drift/tamper finding, carrying aDigestMismatchfailure anatomy; non-retryable). Produces no attestation.unverified— never a fakeverified, for one of three reasons: the agent reported no digest and none was observed; no gated digest is recorded to compare with; or the result covers no targets (overall status only). Produces no attestation.
The verdict carries no observation timestamp.
Console presentation (deployment detail). The Web client normalizes overallStatus and each verdict status at its boundary: only verified, mismatch and unverified pass through; any other value (another spelling, a stored value such as pending, a number, or no status) becomes unknown, and the owner value is quoted rather than interpreted. Only allVerified: true counts as the all-verified claim. The badge, explanation, evidence source and attestation link all come from one state:
| State | When | Rendering |
|---|---|---|
| Verified | verified, a recorded gated digest, allVerified true, at least one target, every target verified with a reported or observed digest, and a recorded result for every target the deployment record lists | The only state that says the recorded target results match the gated digest. Names the source: agent report (not independently observed) or independent observation, per target. States that no observation time is recorded. Offers the Chain-of-Custody link for the deployment attestation. |
| Mismatch | mismatch | Alert; per-target DigestMismatch anatomy. |
| Unverified | unverified | Caution naming the reason: no reported or observed digest, no gated digest (the target line names whether its digest was agent-reported or independently observed), or no target results. |
| Unknown | a value outside the vocabulary, or a verified result that fails the Verified conditions (for example, a deployment target without a recorded result) | No landing conclusion; the owner value or the inconsistency is named. |
| Not recorded | the readable deployment record carries no landingVerification | Neutral; the result is recorded when the deployment job reaches a terminal state. |
| Unavailable | the stored deployment record could not be read (readError) | Caution; no result is shown. |
In any state other than Verified, a recorded deploymentAttestationRef stays visible as a reference, but the Chain-of-Custody link is not offered.
Deployment attestation (binding honesty): a deployment-kind in-toto attestation (predicate https://stellaops.dev/predicates/deployment@v1) is signed and anchored in the transparency log only when the job is Completed AND every target landed verified. The honesty gate returns before touching the signer/submitter otherwise. It lights custody Deploy = SIGNED (recorded) → PROVEN (verified via POST /api/evidence/v1/rekor/verify, the same path the Scan step uses). Absent a real attestation, Deploy stays RECORDED/MISSING — never a fabricated PROVEN. Signing/submission failure is fail-open (the deploy already succeeded): the landing verdict is still recorded, just without an attestation ref.
Deployment Strategy Plan (honesty preview)
A deployment strategy (rolling / all_at_once / blue_green / canary) is a target-batching planner. GET /api/v1/release-orchestrator/releases/{releaseId}/deploy-plan?strategy=&environmentId= previews what the requested strategy will actually do for a release’s real target count — it runs the strategy’s real plan and returns the phases verbatim plus honest caveats, so a requested strategy never implies behaviour it will not deliver.
{ "strategy": "canary", "requestedStrategy": "canary", "targetCount": 1,
"phases": [ { "index": 0, "targetCount": 1, "kind": "all-at-once" } ],
"effectiveBehaviour": "With 1 target, 'canary' runs as a single all-at-once deploy …",
"caveats": [ "single-target-collapse", "no-traffic-management" ] }
Honesty invariants (binding):
- Phases equal the real plan — copied verbatim from the strategy’s
PlanAsync; no idealized/fabricated phases. single-target-collapse— with a single target, canary/blue-green/rolling all reduce to one batch of one; the effective behaviour is reported as a single all-at-once deploy and the phase is labeledall-at-once, never a phased-canary illustration.no-traffic-management— canary/blue-green do phased target rollout, not traffic-splitting; traffic cutover is external/not automated by the adapter. The preview says so; the UI never implies an automated traffic canary/cutover.- cannot-preview — an unknown/unresolvable target count returns
{ "cannotPreview": true, "reason": … }, never a fabricated plan.
The preview is a current calculation, not a recorded plan. It runs over the environment’s healthy target count at request time with the default strategy options. A deployment record’s targets were recorded when that deployment started (LibraryDeploymentRequestStarter persists the environment’s targets at creation; the terminal and in-flight mirrors update those recorded targets and never re-resolve topology). The two have different identities and times.
Console presentation (deployment detail, Flight plan tab). The tab shows two separately labeled sections:
- Recorded rollout — the strategy, start time and every target the deployment recorded, with each target’s recorded status, failure reason, landing result and latest recorded time. A target later removed from the environment stays listed; a topology change does not alter the section or the landing result.
- Current environment (calculated now) — the
target-readyresult of a fresh preflight (read when the tab opens) with its readiness class, the owner’s reason, the next step and a handoff (Topology & hosts at/ops/platform-setupto enrol a target or assign an agent; the environment’s target inventory for target health), followed by the current plan preview. No recorded target is placed into a calculated phase. When the calculated target count differs from the recorded one, the section says so. A cannot-preview reason is quoted with a handoff to Topology & hosts.
The deployment header shows the latest progress time the record holds (its start and each recorded target start or end) and how current it is: recent, stale (a non-terminal record without recorded progress for more than 12 hours, the deployment-scope bound in the runtime contract), unknown (no usable time, or a latest progress time later than the console clock; an unset owner timestamp such as 0001-01-01 is not a recorded time) or final (terminal status). A stale or unknown record keeps its owner status; no completion or failure is inferred from elapsed time. The header’s time span is measured only from recorded times: Duration for a terminal record (recorded start to recorded end), Elapsed since start otherwise (recorded start to now). When the start or a terminal record’s end is not recorded, the end is earlier than the start, or the start is later than the console clock, the header states that instead of showing a value; the Flight plan tab likewise states that no start time is recorded. A record without any target states that it delivered nothing, whatever its status, with a handoff to target enrolment.
Watch / Drift (post-landing)
After landing, the deploy is watched: deploy agents report the container image digests they OBSERVE running, and the orchestrator compares them against the release’s approved/deployed digests. Tenant-scoped status, drift, and digest-location reads (GET /api/v1/release-orchestrator/estate/status|drift and GET /api/v1/release-orchestrator/estate/digest/{digest}) accept either release:read or orch:read; deviations and the other Estate reads remain orch:read-only.
// GET /estate/status
{ "deployedComponents": 8,
"drift": { "total": 1, "drift": 1, "inSync": 0, "unobserved": 0, "observingAgents": 1 },
"deviations": { "count": 0, "subjects": 3, "unevaluated": 1, "unknown": 0, "unbaselined": 0 } }
deviations.count is the number of outstanding estate-finding deviations: deployed (environment, release) subjects whose latest re-evaluation is more severe than the decision that authorized them. A standing regression counts once, and a release that recovered or was replaced does not count. deviations.subjects is the number of deployed (environment, release) subjects; unevaluated (not re-evaluated yet), unbaselined (no authorizing decision with a pass or failure outcome to compare against) and unknown (the re-evaluation reached neither a pass nor a failure) count the subjects whose current risk is not established.
Outstanding deviations surface in the console through the chain-of-custody read: the Watch stage of a running digest whose release has an outstanding deviation is unverifiable (estate-deviation), and the Compliance view’s assurance queue lists those digests first; a subject whose current risk is not established reads unavailable, never clean. The Doctor check check.release.environment.matches-approved fails on count or unbaselined and warns on unevaluated, unknown or an absent classification.
The drift signal surfaces in the console as a needs-you “components in drift” item, an estate-matrix drift chip, and the custody Run-stage rail.
Honesty invariants (binding):
unobservedis neverinSync— a component no agent is reporting is uncovered/unknown, never rendered green. The Run rail shows honest observing-agent coverage; it never claims “healthy” for an unobserved estate.- Drift is a real deviation, shown loudly — a running digest that is not an approved/deployed digest (
drift) is an alarm (unapproved or altered image), never suppressed. - No fabricated all-clear —
observingAgents == 0→ honest no-signal (not a fake green); a read failure → “couldn’t check”, never a fabricated in-sync or fabricated drift.
Gate Errors (422)
| Code | Message | Description |
|---|---|---|
GATE_EVALUATION_FAILED | Gate evaluation failed | Gate cannot be evaluated |
GATE_SECURITY_BLOCKED | Blocked by security gate | Security policy violation |
GATE_POLICY_BLOCKED | Blocked by policy gate | Custom policy violation |
GATE_APPROVAL_BLOCKED | Blocked pending approval | Awaiting approval |
GATE_TIMEOUT | Gate evaluation timeout | Evaluation exceeded timeout |
Agent Errors (422)
| Code | Message | Description |
|---|---|---|
AGT_REGISTRATION_FAILED | Agent registration failed | Cannot register agent |
AGT_TOKEN_INVALID | Invalid registration token | Bad or expired token |
AGT_TOKEN_USED | Registration token already used | One-time token reused |
AGT_CERTIFICATE_FAILED | Certificate issuance failed | Cannot issue certificate |
AGT_OFFLINE | Agent offline | Agent not responding |
AGT_CAPABILITY_MISSING | Missing capability | Agent lacks required capability |
AGT_TASK_FAILED | Task execution failed | Agent task failed |
AGT_HEARTBEAT_TIMEOUT | Heartbeat timeout | Agent heartbeat overdue |
Integration Errors (422)
| Code | Message | Description |
|---|---|---|
INT_CONNECTION_FAILED | Connection failed | Cannot connect to integration |
INT_AUTH_FAILED | Authentication failed | Integration auth failed |
INT_RATE_LIMITED | Rate limited | Integration rate limit hit |
INT_TIMEOUT | Integration timeout | Request timeout |
INT_INVALID_RESPONSE | Invalid response | Unexpected response format |
INT_RESOURCE_NOT_FOUND | External resource not found | Registry/SCM resource missing |
Workflow Errors (422)
| Code | Message | Description |
|---|---|---|
WF_TEMPLATE_NOT_FOUND | Workflow template not found | Template doesn’t exist |
WF_TEMPLATE_INVALID | Invalid workflow template | Template validation failed |
WF_CYCLE_DETECTED | Cycle detected in workflow | DAG contains cycle |
WF_STEP_FAILED | Workflow step failed | Step execution failed |
WF_ALREADY_RUNNING | Workflow already running | Duplicate workflow run |
WF_INVALID_STATE | Invalid workflow state | Cannot perform action |
WF_EXPRESSION_ERROR | Expression evaluation error | Bad expression |
System Errors (500)
| Code | Message | Description |
|---|---|---|
SYS_INTERNAL_ERROR | Internal server error | Unexpected error |
SYS_DATABASE_ERROR | Database error | Database operation failed |
SYS_STORAGE_ERROR | Storage error | Storage operation failed |
SYS_VAULT_ERROR | Vault error | Secret retrieval failed |
SYS_QUEUE_ERROR | Queue error | Message queue failed |
SYS_SERVICE_UNAVAILABLE | Service unavailable | Dependency unavailable |
SYS_OVERLOADED | System overloaded | Capacity exceeded |
Example Error Responses
Validation Error
{
"success": false,
"error": {
"code": "VAL_REQUIRED_FIELD",
"message": "Validation failed",
"validationErrors": [
{
"field": "releaseId",
"message": "Release ID is required",
"code": "VAL_REQUIRED_FIELD"
},
{
"field": "targetEnvironmentId",
"message": "Invalid UUID format",
"code": "VAL_INVALID_UUID"
}
]
},
"meta": {
"requestId": "req-12345",
"timestamp": "2026-01-10T14:30:00Z"
}
}
Permission Error
{
"success": false,
"error": {
"code": "PERM_SOD_VIOLATION",
"message": "Separation of duties violation: requester cannot approve their own promotion",
"details": {
"promotionId": "promo-uuid",
"requesterId": "user-uuid",
"approverId": "user-uuid",
"environmentId": "env-uuid",
"requiresSoD": true
}
},
"meta": {
"requestId": "req-12345",
"timestamp": "2026-01-10T14:30:00Z"
}
}
Gate Block Error
{
"success": false,
"error": {
"code": "GATE_SECURITY_BLOCKED",
"message": "Promotion blocked by security gate",
"details": {
"gateName": "security-gate",
"releaseId": "rel-uuid",
"targetEnvironment": "production",
"violations": [
{
"type": "critical_vulnerability",
"count": 3,
"threshold": 0
}
]
}
},
"meta": {
"requestId": "req-12345",
"timestamp": "2026-01-10T14:30:00Z"
}
}
