@ship.zone/ci-spec 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/.smartconfig.json +46 -0
  2. package/changelog.md +190 -0
  3. package/conformance/archive-cases.json +446 -0
  4. package/conformance/ci-actions/compile-invalid/build-arg-secret-collision.yml +31 -0
  5. package/conformance/ci-actions/compile-invalid/candidate-without-needs.yml +36 -0
  6. package/conformance/ci-actions/compile-invalid/needs-wider-triggers.yml +42 -0
  7. package/conformance/ci-actions/compile-invalid/npm-read-registry-not-allowlisted.yml +34 -0
  8. package/conformance/ci-actions/compile-invalid/publish-job-excludes-tag.yml +57 -0
  9. package/conformance/ci-actions/compile-invalid/shared-memory-above-memory.yml +28 -0
  10. package/conformance/ci-actions/compile-invalid/trigger-kind-without-job.yml +30 -0
  11. package/conformance/ci-actions/invalid/alias-affix-invalid.yml +33 -0
  12. package/conformance/ci-actions/invalid/alias.yml +32 -0
  13. package/conformance/ci-actions/invalid/bridge-network.yml +27 -0
  14. package/conformance/ci-actions/invalid/build-darwin-platform.yml +24 -0
  15. package/conformance/ci-actions/invalid/build-with-steps.yml +27 -0
  16. package/conformance/ci-actions/invalid/concurrency-empty-group.yml +28 -0
  17. package/conformance/ci-actions/invalid/concurrency-without-cancel-in-progress.yml +27 -0
  18. package/conformance/ci-actions/invalid/container-job-build-profile.yml +25 -0
  19. package/conformance/ci-actions/invalid/custom-tag.txt +23 -0
  20. package/conformance/ci-actions/invalid/duplicate-key.txt +24 -0
  21. package/conformance/ci-actions/invalid/egress-bare-wildcard.yml +29 -0
  22. package/conformance/ci-actions/invalid/egress-empty-allowlist.yml +28 -0
  23. package/conformance/ci-actions/invalid/egress-ip-literal.yml +29 -0
  24. package/conformance/ci-actions/invalid/egress-single-label-wildcard.yml +29 -0
  25. package/conformance/ci-actions/invalid/job-publish-permission.yml +28 -0
  26. package/conformance/ci-actions/invalid/job-trigger-undeclared-kind.yml +27 -0
  27. package/conformance/ci-actions/invalid/literal-alias-affix.yml +33 -0
  28. package/conformance/ci-actions/invalid/missing-spec.yml +24 -0
  29. package/conformance/ci-actions/invalid/multiple-documents.yml +25 -0
  30. package/conformance/ci-actions/invalid/npm-read-invalid-scope.yml +33 -0
  31. package/conformance/ci-actions/invalid/oci-darwin-platform.yml +25 -0
  32. package/conformance/ci-actions/invalid/permission-without-publication.yml +31 -0
  33. package/conformance/ci-actions/invalid/publish-order-duplicate.yml +50 -0
  34. package/conformance/ci-actions/invalid/publish-order-missing-kind.yml +48 -0
  35. package/conformance/ci-actions/invalid/publish-order-undeclared-kind.yml +45 -0
  36. package/conformance/ci-actions/invalid/publish-without-permission.yml +31 -0
  37. package/conformance/ci-actions/invalid/publish-without-tag-trigger.yml +31 -0
  38. package/conformance/ci-actions/invalid/release-notes-without-assets.yml +46 -0
  39. package/conformance/ci-actions/invalid/reserved-label-prefix.yml +25 -0
  40. package/conformance/ci-actions/invalid/reserved-label.yml +25 -0
  41. package/conformance/ci-actions/invalid/retention-above-maximum.yml +83 -0
  42. package/conformance/ci-actions/invalid/retention-below-minimum.yml +83 -0
  43. package/conformance/ci-actions/invalid/schema-version-field.yml +26 -0
  44. package/conformance/ci-actions/invalid/shared-memory-below-minimum.yml +28 -0
  45. package/conformance/ci-actions/invalid/shell-string.yml +21 -0
  46. package/conformance/ci-actions/invalid/spec-number.yml +25 -0
  47. package/conformance/ci-actions/invalid/spec-prerelease.yml +25 -0
  48. package/conformance/ci-actions/invalid/spec-range.yml +25 -0
  49. package/conformance/ci-actions/invalid/timeout-above-maximum.yml +26 -0
  50. package/conformance/ci-actions/invalid/timeout-below-minimum.yml +26 -0
  51. package/conformance/ci-actions/invalid/unknown-alias-kind.yml +31 -0
  52. package/conformance/ci-actions/invalid/unknown-key.yml +24 -0
  53. package/conformance/ci-actions/invalid/vm-darwin-candidate.yml +38 -0
  54. package/conformance/ci-actions/invalid/vm-darwin-shared-memory.yml +28 -0
  55. package/conformance/ci-actions/invalid/vm-oci-execution.yml +25 -0
  56. package/conformance/ci-actions/invalid/vm-unsupported-platform.yml +25 -0
  57. package/conformance/ci-actions/invalid/vm-without-platform.yml +24 -0
  58. package/conformance/ci-actions/valid/concurrency.yml +83 -0
  59. package/conformance/ci-actions/valid/egress.yml +40 -0
  60. package/conformance/ci-actions/valid/image-build.yml +80 -0
  61. package/conformance/ci-actions/valid/matrix.yml +45 -0
  62. package/conformance/ci-actions/valid/minimal.yml +25 -0
  63. package/conformance/ci-actions/valid/npm-read.yml +60 -0
  64. package/conformance/ci-actions/valid/release-assets.yml +114 -0
  65. package/conformance/ci-actions/valid/release-signing.yml +71 -0
  66. package/conformance/ci-actions/valid/release.yml +109 -0
  67. package/conformance/ci-actions/valid/resources.yml +79 -0
  68. package/conformance/ci-actions/valid/vm.yml +77 -0
  69. package/conformance/compile-cases.json +5885 -0
  70. package/conformance/compile-cases.schema.json +766 -0
  71. package/conformance/digest-cases.json +144 -0
  72. package/conformance/runner-cases.json +1238 -0
  73. package/conformance/runner-jobs/invalid/bridge-network.json +70 -0
  74. package/conformance/runner-jobs/invalid/build-darwin-platform.json +100 -0
  75. package/conformance/runner-jobs/invalid/build-with-steps.json +111 -0
  76. package/conformance/runner-jobs/invalid/build-without-microvm.json +100 -0
  77. package/conformance/runner-jobs/invalid/egress-empty-allowlist.json +71 -0
  78. package/conformance/runner-jobs/invalid/mutable-image.json +70 -0
  79. package/conformance/runner-jobs/invalid/npm-read-reserved-environment.json +82 -0
  80. package/conformance/runner-jobs/invalid/oci-darwin-platform.json +71 -0
  81. package/conformance/runner-jobs/invalid/push-permission-container.json +70 -0
  82. package/conformance/runner-jobs/invalid/shared-memory-below-minimum.json +71 -0
  83. package/conformance/runner-jobs/invalid/timeout-missing.json +69 -0
  84. package/conformance/runner-jobs/invalid/vm-without-microvm.json +82 -0
  85. package/conformance/runner-jobs/valid/candidate-test.json +72 -0
  86. package/conformance/runner-jobs/valid/egress.json +80 -0
  87. package/conformance/runner-jobs/valid/image-build.json +100 -0
  88. package/conformance/runner-jobs/valid/minimal.json +70 -0
  89. package/conformance/runner-jobs/valid/npm-read.json +81 -0
  90. package/conformance/runner-jobs/valid/resources.json +71 -0
  91. package/conformance/runner-jobs/valid/timeout-retention-defaults.json +91 -0
  92. package/conformance/runner-jobs/valid/vm-darwin.json +70 -0
  93. package/conformance/runner-jobs/valid/vm.json +82 -0
  94. package/conformance/runner-messages.json +2035 -0
  95. package/conformance/version-cases.json +125 -0
  96. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  97. package/dist_ts/00_commitinfo_data.js +9 -0
  98. package/dist_ts/constants.d.ts +199 -0
  99. package/dist_ts/constants.js +106 -0
  100. package/dist_ts/index.d.ts +1 -0
  101. package/dist_ts/index.js +2 -0
  102. package/dist_ts/plugins.d.ts +1 -0
  103. package/dist_ts/plugins.js +3 -0
  104. package/examples/ci_actions.yml +64 -0
  105. package/license.md +21 -0
  106. package/package.json +72 -0
  107. package/readme.md +146 -0
  108. package/schemas/ci_actions.schema.json +1664 -0
  109. package/schemas/runner-job.schema.json +1172 -0
  110. package/spec/ci-actions.md +433 -0
  111. package/spec/runner-protocol.md +378 -0
  112. package/spec/runner.openapi.json +2182 -0
  113. package/ts/00_commitinfo_data.ts +8 -0
  114. package/ts/constants.ts +112 -0
  115. package/ts/index.ts +1 -0
  116. package/ts/plugins.ts +1 -0
@@ -0,0 +1,378 @@
1
+ # ship.zone CI Runner Protocol
2
+
3
+ Status: draft. Versioned as the `@ship.zone/ci-spec` package; see Specification Version.
4
+
5
+ Base path: `/api/runner`
6
+
7
+ This protocol is intentionally incompatible with the legacy internal runner protocol it replaces. It defines the runner-facing execution data plane only. Workflow parsing, repository authorization, trigger policy, scheduling, candidate assembly, publication, canonical state, and administrative APIs are coordinator concerns.
8
+
9
+ Statements that no conformance case can observe are marked *(non-testable)*.
10
+
11
+ ## Specification Version
12
+
13
+ The specification version is the version of the published `@ship.zone/ci-spec` package, in the canonical `MAJOR.MINOR.PATCH` form of the `specVersion` schema definition. It is the only version identifier of this protocol, the compiled job, and `ci_actions.yml`: profiles, features, schemas, endpoints, and fixtures carry no version of their own. Every incompatible change to a normative artifact is released as a new major version *(non-testable)*.
14
+
15
+ An implementation implements exactly one version, the package version it is built against. A declared version `D` is accepted by an implementation of version `I` when `D` is canonical, `D` has the same major version as `I`, and `D` is not greater than `I`, comparing major, minor, and patch numerically in that order. Otherwise the outcome is `malformed`, `major-mismatch`, or `newer`, checked in that order. `conformance/version-cases.json` fixes the outcome for representative pairs.
16
+
17
+ Versions appear on the wire only as `spec`:
18
+
19
+ - `DiscoveryResponse`, `RegisterRunnerResponse`, and `StartSessionResponse` carry the coordinator's version `C`.
20
+ - `RegisterRunnerRequest` and `StartSessionRequest` carry the runner's version `R`. The coordinator rejects a request whose `R` has a different major version than `C` with HTTP 409 and error code `spec_incompatible`, without consuming a bootstrap credential, creating a session, or fencing an older session. Within one major version either side may be newer.
21
+ - A compiled job carries the `spec` of the workflow it was compiled from. The coordinator leases a job only to a runner session whose `R` accepts the job's `spec`; a runner that receives a job its `R` does not accept abandons it as `runner_error` before acceptance.
22
+
23
+ A compiled job uses only constructs defined at its `spec`, and every other message uses only constructs defined at the lower of `C` and `R` *(non-testable)*.
24
+
25
+ ## Document Identifiers
26
+
27
+ Every normative JSON document is identified by `https://ship.zone/spec/ci/` followed by its path in the package: the workflow schema is `https://ship.zone/spec/ci/schemas/ci_actions.schema.json`, the compiled job schema is `https://ship.zone/spec/ci/schemas/runner-job.schema.json`, and this protocol's OpenAPI description is `https://ship.zone/spec/ci/spec/runner.openapi.json`. The schemas carry their identifier as `$id`. The OpenAPI description refers to the compiled job schema as `../schemas/runner-job.schema.json`, which resolves against the description's identifier to the schema's `$id`, and against its location in the package to the schema file. A validator therefore registers each schema under its `$id` and the OpenAPI components under the description's identifier, and needs no other key.
28
+
29
+ ## Transport and Authentication
30
+
31
+ Production transport requires HTTPS with certificate and hostname validation. Plain HTTP is permitted only for explicit loopback development.
32
+
33
+ An operator first creates a runner registration and receives a one-time bootstrap bearer credential. Registration binds a runner identity, allowed trust class, labels, and policy. Successful registration returns a short-lived runner bearer credential.
34
+
35
+ Every endpoint declares one authorization class:
36
+
37
+ - public discovery;
38
+ - bootstrap registration;
39
+ - authenticated runner lifecycle;
40
+ - authenticated and fenced job or transfer operation.
41
+
42
+ Runner credentials are bound to one runner ID and cannot authorize coordinator administration. Source, artifact, cache, and image-record operations additionally require the current runner, session, job, attempt, lease, and fence identity. The bearer-bound runner, request identity headers or body, and path identifiers must match exactly. An invalid bearer returns HTTP 401; any authenticated identity or fence mismatch returns HTTP 409 without state mutation or target-state disclosure. A retained byte-equivalent replay returns its previously recorded success after the attempt becomes terminal, but grants no renewed execution or mutation authority. The runner never receives a Git repository credential.
43
+
44
+ Discovery establishes the coordinator origin. Every transfer endpoint is relative to that same origin and the canonical base path. Transfer redirects are forbidden; runners do not follow them and never forward bearer or identity headers to another origin.
45
+
46
+ ## Registration Replay
47
+
48
+ Registration is idempotent by `(bootstrapCredentialId, registrationRequestId, canonicalRequestDigest)`. On first success the bootstrap credential is consumed and the coordinator stores the encrypted exact response for ten minutes.
49
+
50
+ An identical retry during that window receives the exact same response. A changed body or request ID is rejected. After the replay deadline the consumed bootstrap credential is unusable. The returned credential `expiresAt` is at least 60 seconds after `registrationReplayUntil`, so every replayed response contains a credential valid for at least one maximum heartbeat interval at the replay deadline.
51
+
52
+ ## Canonical Digests
53
+
54
+ `canonicalRequestDigest` is the lowercase SHA-256 of the UTF-8 bytes produced by applying RFC 8785 JSON Canonicalization Scheme to the complete JSON request body. HTTP headers, bearer credentials, and transport framing are outside the digest. Retries reuse the same canonical body and request identifier.
55
+
56
+ `compiledPlanDigest` is the lowercase SHA-256 of an RFC 8785 projection of the complete compiled runner job. The projection removes `compiledPlanDigest` and replaces every value in the `secrets` object with JSON `true`. It therefore covers immutable execution, build, step, network, source, permission, resource, npm read, transfer-limit, and secret-target data, and candidate references by name, while excluding secret values, credentials, session or attempt identities, and transfer authorization. Image grants, npm grants, and candidate bindings are lease fields outside `job` and are therefore outside the digest by construction. Conformance fixtures provide canonical bytes and expected digests.
57
+
58
+ ## Credential Rotation
59
+
60
+ Credentials have opaque IDs and explicit expiry. Rotation creates one replacement credential and permits old and replacement credentials to overlap for no more than ten minutes.
61
+
62
+ Whenever the old credential is used during overlap, the coordinator replays the same replacement credential. The first authenticated request using the replacement acknowledges it. The old credential is revoked after one runner heartbeat grace interval, or at the advertised overlap deadline if no acknowledgement is observed. A replacement credential expires at least 60 seconds after `previousCredentialValidUntil`.
63
+
64
+ The coordinator must not create multiple pending replacements for one rotation. Session and heartbeat responses carry the exact pending replacement until it is acknowledged or expires.
65
+
66
+ ## Capabilities and Matching
67
+
68
+ A runner advertises:
69
+
70
+ - execution profiles: `oci` (container steps), `oci-image` (image builds), and `vm` (steps in a microVM guest);
71
+ - isolation modes: `container` and `microvm` (see Isolation);
72
+ - platforms it executes natively, as `os/arch[/variant]`;
73
+ - labels;
74
+ - transfer features;
75
+ - network modes: `none` and `egress`;
76
+ - source formats;
77
+ - maximum source archive bytes, extracted bytes, and entries;
78
+ - maximum artifact archive bytes, extracted bytes, entries, and count;
79
+ - maximum cache archive bytes, extracted bytes, entries, and count;
80
+ - maximum log chunk bytes and total log bytes;
81
+ - maximum workspace bytes and entries;
82
+ - optionally, `resources`: the largest memory, CPU, PID, and shared-memory ceilings it can enforce per attempt;
83
+ - optionally, `leaseFeatures`: the lease contents it uses, which are `image-pull-grants` (pull grants), `candidate-bindings` (bindings), and `npm-read-grants` (`npmGrants`);
84
+ - optionally, `maximumTimeoutMs`: the largest job `timeoutMs` it executes.
85
+
86
+ The coordinator may issue a lease only when the session's `spec` accepts the job's `spec` and every compiled job requirement fits the runner capabilities and limits: the execution profile, isolation mode, platform when required, network mode, labels, features, and limits. When the capability snapshot contains `resources`, every value of the job's `resources` must also be at most its matching maximum: `memoryBytes` against `maximumMemoryBytes`, `cpus` against `maximumCpus`, `pids` against `maximumPids`, and `sharedMemoryBytes` against `maximumSharedMemoryBytes`. When the capability snapshot contains `leaseFeatures`, the lease uses only listed lease features: a lease with a pull grant requires `image-pull-grants`, a lease with a binding requires `candidate-bindings`, and a lease with `npmGrants` requires `npm-read-grants`; push grants belong to the `oci-image` profile and are not a lease feature. Every bound image has a pull grant, so a snapshot that lists `candidate-bindings` also lists `image-pull-grants`; the capability schema rejects any other combination. When the capability snapshot contains `maximumTimeoutMs`, the job's `timeoutMs` must be at most it. A snapshot without `leaseFeatures` or without `maximumTimeoutMs` imposes no condition of that kind. A mismatch results in no lease. A runner must still validate the leased job and fail closed if it violates the advertised capability snapshot.
87
+
88
+ A capability field that a later minor version adds follows the rule of Specification Version: a runner sends it only when the lower of `C` and `R` defines it, and a capability snapshot without it imposes no condition of its kind.
89
+
90
+ The coordinator also leases a job only to a runner whose registration allows the trust class of the job's run, as the workflow standard's Trust defines it. The allowed trust class that a registration binds is trusted, untrusted, or both, as the operator sets it when creating the registration: a runner registered for trusted work takes only jobs of trusted runs, which include protected tag runs; a runner registered for untrusted work takes only jobs of untrusted runs; and a runner whose registration allows both takes jobs of either. The trust class is not part of the compiled job, the lease, or the capability snapshot, so this matching is the coordinator's alone and adds nothing a runner validates.
91
+
92
+ A platform is advertised only when the runner executes it natively. Binary translation through `binfmt_misc`, user-mode emulation, or any other emulator never satisfies a platform requirement. The rule governs only where a job executes: compiling for other targets, and running an emulator as an ordinary process of the job, are job content that the runner neither restricts nor counts as a platform.
93
+
94
+ A `vm` job always carries `requirements.platform`, and it is leased only to a runner whose capability snapshot contains the `vm` profile, `microvm` isolation, and that platform. A runner that advertises `vm` executes a platform natively when it boots guests of that platform under hardware virtualization on the host's own architecture. It advertises `darwin/arm64` only when it runs on Apple silicon hardware and boots `darwin/arm64` guests.
95
+
96
+ `oci` and `oci-image` execute only `linux` platforms: the compiled job schema restricts their `requirements.platform` to `linux` platforms, so `darwin/arm64` jobs exist only for `vm`. An `oci` job without `requirements.platform` is leased only to a runner that advertises at least one `linux` platform, and runs on one of them.
97
+
98
+ Image builds run on dedicated builder runners. A runner that advertises `oci-image` advertises no other execution profile and includes `microvm` isolation; the capability schema rejects any other combination. A runner that advertises `vm` includes `microvm` isolation and may also advertise `oci`, but never `oci-image`; the capability schema rejects a `vm` runner without `microvm`.
99
+
100
+ Every compiled job is internally coherent before enqueueing and again before acceptance: the job has exactly the shape of its execution profile; `source.tar-gz` is required; non-empty artifact or cache declarations require their matching feature and permission; `none` permission requires an empty declaration list; a read-write cache requires `caches: read-write`; `images: push` is present exactly for `oci-image`; a `vm` job carries `microvm` isolation and `requirements.platform`; the `requirements.platform` of an `oci` or `oci-image` job is a `linux` platform; in a job with `npmRead`, no compiled step environment defines `NPM_CONFIG_USERCONFIG` and no build secret has id `npmrc`; every build secret names a key of `secrets` and every key of `secrets` is named by a build secret in an `oci-image` job; source metadata and every declared artifact/cache maximum fit `requirements.limits`; `resources.sharedMemoryBytes` is at most `resources.memoryBytes` when both are present; declaration counts fit their required maxima; and every job requirement fits the accepted runner capability snapshot.
101
+
102
+ A lease is coherent with its job when every candidate reference in the job has exactly one binding with that candidate name and no binding is unreferenced; at most one grant exists per registry, repository, and access; every bound image and every private base image has a pull grant for its repository; an `oci-image` lease has exactly one push grant and an `oci` or `vm` lease has none; `npmGrants` is present exactly when the job declares `npmRead`, with exactly one grant per `npmRead` entry that carries the entry's registry and scopes.
103
+
104
+ A violation is rejected before enqueueing or abandoned as `runner_error` before acceptance.
105
+
106
+ ## Sessions and Leases
107
+
108
+ Each runner process starts a new session. Session identity is included in every claim and job mutation. Session creation is idempotent by `(runnerId, sessionRequestId, canonicalRequestDigest)`. The coordinator durably stores the exact response before atomically making older sessions stale. An exact retry returns the same session and response; a changed body returns HTTP 409. The key remains tombstoned for the resulting session lifetime plus ten minutes. A different request ID intentionally starts a new session and immediately fences older sessions when the new response is committed.
109
+
110
+ Claims are idempotent by `(runnerId, sessionId, claimRequestId, canonicalRequestDigest)`. An exact retry returns the same lease while its attempt is active. After expiry or terminal state, the claim returns HTTP 409 and never leases a different job for the same key. Claim keys remain tombstoned for the runner session lifetime plus ten minutes.
111
+
112
+ The runner validates the full lease against its advertised capability snapshot before acceptance. A valid `accept` request atomically transitions `leased` to `running`; execution must not start before HTTP 204. Acceptance is idempotent by `acceptRequestId`, full job identity, and canonical body. Exact acceptance replays return HTTP 204 for as long as attempt metadata is retained and never revive an expired or terminal attempt. Changed, expired, or stale acceptance returns HTTP 409.
113
+
114
+ Every job mutation carries:
115
+
116
+ - `runnerId`;
117
+ - `sessionId`;
118
+ - `jobId`;
119
+ - `attemptId`;
120
+ - `leaseId`;
121
+ - opaque `fencingToken`.
122
+
123
+ The coordinator returns HTTP 409 for stale sessions, attempts, leases, or fencing tokens. The only exception is replay of a retained, previously accepted idempotent mutation, which returns its recorded response without new state mutation. The runner immediately stops work and suppresses new logs and completion after fencing loss.
124
+
125
+ Lease deadlines use coordinator `serverTime`, not the runner wall clock. Lease durations are between 30 and 600 seconds. Runner and job heartbeat intervals are between 5 and 60 seconds.
126
+
127
+ ## Job State
128
+
129
+ The authoritative state machine is:
130
+
131
+ ```text
132
+ queued -> leased/running -> succeeded | failed | canceled | timed_out | system_error
133
+ ```
134
+
135
+ Exactly one terminal state is accepted. Completion is idempotent by `completionRequestId` and the full canonical terminal payload. An exact replay returns the prior success. A conflicting terminal retry returns HTTP 409.
136
+
137
+ Abandonment is idempotent by `abandonRequestId` and releases an unstarted or active attempt without recording successful work. Accept, abandon, completion, and terminal replay records are retained with attempt metadata for at least 30 days after terminal state.
138
+
139
+ Recovery is authenticated runner-lifecycle reconciliation, not a fenced job mutation and not resume authorization. It is idempotent by `(runnerId, currentSessionId, recoveryRequestId, canonicalRequestDigest)` for the current session lifetime plus ten minutes. The authenticated runner, active current session, referenced attempt owner, and exact previous session must all match. A mismatch returns HTTP 409 without disclosing target state. Recovery may revoke and reconcile the interrupted attempt, but cannot renew its lease or fence, execute work, transfer data, emit logs, or complete it.
140
+
141
+ ## Cancellation
142
+
143
+ Cancellation is delivered in the fenced job-heartbeat response:
144
+
145
+ ```json
146
+ {
147
+ "requestId": "cancel-...",
148
+ "requestedAt": "...",
149
+ "reason": "user_requested",
150
+ "terminateBy": "..."
151
+ }
152
+ ```
153
+
154
+ The directive is redelivered on every accepted heartbeat until the attempt becomes terminal. The next heartbeat request includes `observedCancellationRequestId` as an idempotent acknowledgement.
155
+
156
+ The runner begins termination immediately and force-stops execution by `terminateBy`. The grace interval may not exceed 30 seconds. The lease remains renewable while bounded termination proceeds. Completion uses status `canceled` and includes the same cancellation request ID. If fencing is lost first, completion is suppressed.
157
+
158
+ The reason `superseded` means that the coordinator canceled the attempt's run because a newer run with the same concurrency key was created, as defined in the workflow standard's Concurrency.
159
+
160
+ ## Job Timeout
161
+
162
+ The *attempt timeout* is the coordinator `serverTime` at which the coordinator committed the attempt's acceptance, plus the job's `timeoutMs`. The runner measures its deadline with a monotonic clock from the moment it receives HTTP 204 for the acceptance. It therefore never ends execution before the attempt timeout.
163
+
164
+ The deadline covers everything the runner does for the attempt up to the end of execution:
165
+
166
+ - for `oci` and `vm`: source extraction, cache restoration, and every step;
167
+ - for `oci-image`: source extraction, cache restoration, the build, the push, and the image record.
168
+
169
+ Artifact collection and upload, cache publication, and completion follow execution. They are outside the deadline and bounded by the fence.
170
+
171
+ When the deadline is reached before execution ends, the runner terminates execution as for a cancellation: it begins termination immediately and force-stops execution within at most 30 seconds. The attempt is unsuccessful, so the runner then archives the artifacts declared `when: always` or `when: failure`, not those declared `when: success`, and publishes no read-write cache. It completes `timed_out` while its fence remains valid. Whichever of an accepted cancellation and the deadline occurs first decides the outcome: an attempt already terminating for a cancellation completes `canceled`, and an attempt already terminating at its deadline completes `timed_out`.
172
+
173
+ The attempt timeout also bounds the `expiresAt` of the attempt's image and npm grants: `expiresAt` is no later than the attempt timeout plus the maximum cancellation grace (see Image Grants and Bindings and npm Read Grants). A coordinator that issues grants in the lease, before acceptance, satisfies that bound by counting from the time it issues the lease.
174
+
175
+ ## Logs
176
+
177
+ Log chunks are UTF-8 and carry a zero-based sequence. Log data is redacted as defined in Secret Redaction before it is sent. A chunk is appended exactly once for `(attemptId, sequence)`.
178
+
179
+ - The next expected sequence is accepted and advances the acknowledgement.
180
+ - An exact duplicate returns the current acknowledgement without another append.
181
+ - A conflicting duplicate or future sequence returns HTTP 409.
182
+
183
+ The maximum UTF-8 encoded log data per request is 64 KiB and the maximum total accepted log data per attempt is 1 GiB. Coordinators and runners may advertise lower limits. A chunk over the negotiated chunk limit returns HTTP 413 without mutation. Reaching the total limit causes the runner to terminate execution and complete `system_error` while its fence remains valid; logs are never silently truncated.
184
+
185
+ ## Transfers and Integrity
186
+
187
+ Source archives, artifacts, and caches use bounded `tar.gz` streaming endpoints. Immutable source descriptors include exact compressed bytes, extracted bytes, entry count, and lowercase SHA-256. Artifact and cache job declarations contain maxima because produced metadata is not known at compilation. Transfer headers and records carry actual compressed bytes, extracted bytes, entry count, and digest. Senders and receivers independently recompute every value; declared metadata is never trusted.
188
+
189
+ Artifact and cache uploads are idempotent by `(runnerId, sessionId, jobId, attemptId, leaseId, transfer kind, declared name, transferRequestId, sha256, compressed bytes, extracted bytes, entry count)`. The first verified publication durably stores its `TransferRecord` before replying. An exact retry returns the same record without another publication or cache generation; a changed key or metadata returns HTTP 409. Upload replay records are retained with attempt metadata for at least 30 days after terminal state.
190
+
191
+ Source grants are attempt-bound and may fetch only the assigned immutable archive. Artifact and cache mutations require the full attempt identity and current fence. Cache namespaces include tenant, repository, and trust class so untrusted jobs cannot read trusted caches. Protected tag runs, as defined by the workflow standard, have a namespace of their own: their jobs restore only caches that jobs of protected tag runs of the same repository published, and the caches they publish are visible to no other run. The coordinator fixes a run's namespace at compilation.
192
+
193
+ Runners reject output above the declared per-item maximum. Coordinators must not issue jobs whose declared source, artifact, cache, log, or workspace limits exceed runner capabilities.
194
+
195
+ Before extraction or publication, implementations stage the complete archive, verify compressed length and digest, and validate every entry. They reject:
196
+
197
+ - absolute, empty, NUL-containing, or parent-traversing paths;
198
+ - duplicate normalized paths;
199
+ - symlink or hardlink targets that are absolute or resolve outside the extraction root, including through link chains;
200
+ - device nodes, FIFOs, sockets, sparse files, and unsupported entry types;
201
+ - archives exceeding declared or negotiated compressed bytes, extracted bytes, entry count, path length, workspace bytes, or workspace entries.
202
+
203
+ Every archive has a fixed 134,217,728-byte path-metadata ceiling. Implementations sum the UTF-8 byte length of every effective normalized filesystem entry path and, for each symbolic link, its exact effective target. Effective PAX `path` and `linkpath` values replace their USTAR fields and are counted once; PAX placeholder header names and framing are not counted. Checked accounting occurs before an effective path or target is retained, extracted, or published. The ceiling applies independently to every source, artifact, and cache archive.
204
+
205
+ Validation never normalizes unsafe input into an accepted path. Extraction occurs only into a new attempt-owned directory and publication is atomic after complete verification. A source or cache integrity/extraction failure before execution completes `system_error` while the fence remains valid. Artifact publication fails atomically; a required artifact failure completes `system_error`, while an optional artifact is omitted and reported without becoming visible.
206
+
207
+ Archive creation is rooted in the attempt workspace. Collection uses `lstat`-equivalent traversal, never follows symlinks while discovering content, never crosses a mount or filesystem boundary, and rejects any selected path or link target escaping the workspace. Safe relative symlink metadata may be archived only when its resolved target remains within the declared collection root. Host paths, container runtime mounts, and files outside declared artifact/cache paths are never included.
208
+
209
+ ## Retention
210
+
211
+ The coordinator keeps a published artifact or cache available until its *expiry*. The expiry is the `createdAt` of its `TransferRecord` plus `retentionDays` × 86,400 seconds. `retentionDays` is taken from the declaration that published the item; without it the value is 30 for an artifact and 7 for a cache. Retention is a coordinator obligation: runners do not use `retentionDays`.
212
+
213
+ From its expiry on, an item is never served for download, never restored, and never used for qualification or publication. The coordinator deletes its content within 7 days after the expiry.
214
+
215
+ Only the current cache of a key, the last successful publication in its namespace, is ever restored. Restoring it does not extend its expiry, and a newer publication replaces it with its own expiry. When the current cache has expired, the key has no cache: the attempt restores nothing into its paths, as when no cache of that key was ever published, and proceeds.
216
+
217
+ An exact upload replay after the expiry returns its recorded `TransferRecord`. It does not make the content available again and does not extend the expiry.
218
+
219
+ ## Secret Redaction
220
+
221
+ The *redaction set* of an attempt holds every value of the job's `secrets` and every `secret` of the lease's `npmGrants`, and, for a value that contains a line terminator, each of its lines; members shorter than 8 bytes are left out. The line terminators are CR LF, LF, and CR, where CR LF is one terminator, and the lines of a value are the parts before, between, and after its terminators, except that a value ending with a terminator has no line after it.
222
+
223
+ A coordinator stores and delivers only admissible secret values, which are at least 8 bytes (see the workflow standard's Secret Values), and every npm grant `secret` is one line of at least 32 characters (see npm Read Grants). Every value a coordinator delivers is therefore itself a member of the redaction set. A line shorter than 8 bytes of a value of several lines is left out by design: the runner does not replace it when it appears in log data apart from the rest of the value, for example when a job prints the value line by line, and an archive whose only occurrence of the value is such a line is published.
224
+
225
+ - Logs: the runner replaces every occurrence of a member with `***` before sending log data, so that no log chunk and no concatenation of consecutive log chunks of the attempt contains a member.
226
+ - Artifacts and caches: before publishing an archive, the runner checks the content, path, and link target of every entry. An archive in which any of them contains a member is not published: a required artifact or a cache makes the attempt complete `system_error`, and an optional artifact is omitted and reported.
227
+
228
+ *(non-testable)* Redaction matches exact bytes. A job that encodes, splits, or otherwise transforms a value it receives can still disclose it; confidentiality against the job's own code rests on which runs receive a value, which is why protected secrets reach only protected tag runs.
229
+
230
+ ## Isolation
231
+
232
+ `requirements.isolation` selects the sandbox boundary. `container` runs every step in a non-privileged container on the runner host. `microvm` is defined by its guarantees, not by a virtualization technology:
233
+
234
+ - a dedicated guest kernel for every attempt;
235
+ - no host filesystem path, socket, or device is visible inside the guest;
236
+ - the guest root filesystem, image store, builder state, and workspace are created for the attempt without any state of another attempt, and destroyed when it becomes terminal, is abandoned, or loses its fence;
237
+ - all guest network traffic passes through the enforced network mode.
238
+
239
+ `oci-image` and `vm` require `microvm`. Deployment policy may also require `microvm` for `oci` jobs; the coordinator then compiles `isolation: microvm` into those jobs.
240
+
241
+ Every attempt has network namespaces of its own, shared with no other attempt and never with the host; in `oci` every step container receives a new one, and in `vm` the guest's own network stack is the attempt's namespace. Each namespace has a loopback interface. Traffic that stays on the loopback interface of the attempt's own namespace does not leave the sandbox, is not governed by the network mode, and is always permitted, including under `none`. Ports an attempt binds are therefore never visible to, and never collide with, another attempt. The runner exposes no host or runner service on an attempt's loopback interface.
242
+
243
+ ## Network
244
+
245
+ `network` is part of the compiled job and the plan digest. Its modes are:
246
+
247
+ - `none`: no traffic leaves the sandbox. This is the default: a workflow job without `network` compiles to `{ "mode": "none" }`.
248
+ - `egress`: outbound access only to the listed `allow` entries. Each entry names one lowercase DNS host, either exact or `*.` followed by at least two labels, and its TCP ports. `*.example.com` matches names below `example.com` but not `example.com` itself. IP literals and single-label names are invalid.
249
+
250
+ An empty allowlist cannot be expressed: `egress` requires at least one entry, `none` is the only representation of no network, and no implicit allowlist exists. Repository templates that need network access declare `egress` with the hosts they need.
251
+
252
+ The network mode governs traffic leaving the sandbox; loopback traffic inside the attempt's own namespaces is defined in Isolation. A runner that advertises `egress` enforces, for every connection originating inside the sandbox and leaving it, including Dockerfile instructions and image pulls performed inside the sandbox:
253
+
254
+ - the sandbox's only DNS resolver is provided by the runner, answers only `A` and `AAAA` queries for allowlisted names, and refuses every other name and query type;
255
+ - TCP connections are permitted only to an address that resolver returned to this attempt for an allowlisted name, within the returned TTL, and only on a port listed for the matching entry;
256
+ - UDP is blocked except DNS to the runner resolver;
257
+ - on ports 443 and 80, the TLS server name indication or the HTTP `Host` header must name the allowlisted host under which the address was resolved;
258
+ - addresses that are not globally reachable are withheld from answers and blocked even when an allowlisted name resolves to them: loopback, private, link-local including `169.254.169.254`, shared address space, unique-local, multicast, unspecified, IPv4-mapped forms of these, and deployment-declared internal ranges including the coordinator's internal networks.
259
+
260
+ In a `vm` guest, the namespaces, bridges, and containers the job creates inside the guest are part of the sandbox: traffic between them does not leave the sandbox, and every connection that leaves the guest, including connections of nested containers through the guest's own address translation and their DNS queries, is enforced as above.
261
+
262
+ Runner-plane traffic, which is coordinator API calls, source, artifact, and cache transfers, and registry operations authorized by lease grants, is performed by the runner or by a runner-controlled builder component and is not governed by the job's network declaration. For `oci-image`, the builder inside the guest may reach exactly the registry origins named in the lease grants in addition to the allowlist; registries of base images without a grant must be allowlisted.
263
+
264
+ *(non-testable)* Residual risk: a content delivery network that serves several tenants under one allowlisted name can be abused for domain fronting inside TLS. Authors should allowlist the narrowest names available.
265
+
266
+ ## Image Grants and Bindings
267
+
268
+ A lease carries `grants` and `bindings`. Both are outside `job` and outside `compiledPlanDigest`.
269
+
270
+ An image grant authorizes registry access for one attempt, one registry host, one repository, and one access mode. The runner, or its builder inside the guest, presents `username` and `secret` as the registry credential for that repository, over HTTPS unless the registry is a loopback development registry. Grants are never placed in step environments, build arguments, build secrets, logs, error bodies, image layers, or image configuration, and never sent to any other origin. Registry redirects are not followed with or without credentials; the operation fails instead.
271
+
272
+ - A `pull` grant is read-only. The coordinator issues pull grants for bound candidates and for private base or job images.
273
+ - A `push` grant accepts only blob uploads, blob mounts from repositories the same attempt holds pull grants for, and manifest writes referenced by digest, all in the granted candidate repository. Tag writes, deletes, and every other repository are rejected.
274
+
275
+ The registry validates every grant use against live attempt state: fence loss, abandonment, or a terminal state revokes the grant immediately, independent of `expiresAt`. `expiresAt` is no later than the attempt timeout plus the maximum cancellation grace.
276
+
277
+ A grant *applies* to an image reference when the grant's `registry` and `repository` equal the reference's registry and repository exactly. The registry of a reference is its text before the first `/`, and its repository is the text between that `/` and the `@`; no default registry, implicit repository prefix, or case folding applies. A reference without a `/`, or whose registry or repository does not match `RegistryHost` or `OciRepository`, is *hostless*: no grant applies to it. An image has a pull grant for its repository, as lease coherence requires, when a pull grant applies to it. The runner, or its builder inside the guest, presents a pull grant only when it pulls an image the grant applies to, and pulls an image no grant applies to without credentials. When it presents a grant, it connects to the grant's `registry` exactly as written, which is the registry of the image reference, and applies no default registry, mirror, or other rewriting of the host, so a grant's `secret` is never sent to a host other than the one the grant was issued for.
278
+
279
+ A binding resolves one candidate reference to a digest-pinned image. The runner verifies that the pulled manifest or index has exactly the bound digest. When it is an index, the runner selects the manifest for its native platform, or `requirements.platform` when present, and completes `system_error` when that platform is missing. Content pulled with a pull grant is stored only in an attempt-private image store that is removed with the attempt.
280
+
281
+ ## npm Read Grants
282
+
283
+ A lease of a job with `npmRead` carries `npmGrants`, outside `job` and outside `compiledPlanDigest`: one grant per `npmRead` entry, with that entry's registry and scopes. A grant's `secret` is a bearer token of at least 32 characters, as the schema requires, that contains no line terminator (see Secret Redaction): it is one line of the npm configuration file and a single member of the redaction set. It authorizes reading packages and package metadata of the listed scopes from that registry for one attempt; the registry rejects every write and every other scope. The registry validates every use against live attempt state: fence loss, abandonment, or a terminal state revokes the grant immediately, independent of `expiresAt`. `expiresAt` is no later than the attempt timeout plus the maximum cancellation grace. The registry that serves the scopes must support such attempt-bound tokens; the coordinator issues grants only for registries that do.
284
+
285
+ The runner delivers the grants as one npm configuration file that maps every scope to its registry (`@scope:registry=<registry>`) and holds each token for its registry (`//<registry host and path>/:_authToken=<secret>`):
286
+
287
+ - for `oci` and `vm`, the runner writes the file outside the workspace, readable but not writable by step processes, and adds `NPM_CONFIG_USERCONFIG` with the file's absolute path to the environment of every step; archive collection never includes the file. `vm` steps run as guest root, so there the file is provided on a read-only device or file system that the runner creates for the attempt, holds only that file, and whose read-only mode the host enforces;
288
+ - for `oci-image`, the runner passes the file content as the BuildKit secret with id `npmrc`, which a Dockerfile mounts with `RUN --mount=type=secret,id=npmrc,...`.
289
+
290
+ The runner places a grant token nowhere else: not in another environment value, command, build argument, or build secret, not in the compiled job or an error body, and not in an image layer, label, or image configuration field that the runner or its builder adds. The token belongs to the attempt's redaction set, so it is redacted from log chunks, and an artifact or cache that contains it is not published (see Secret Redaction). *(non-testable)* Steps and Dockerfile instructions read the token and can copy it: a `RUN --mount=type=secret,id=npmrc` instruction that copies the file writes the token into an image layer, and a transformed copy escapes redaction. The runner cannot prevent this; the grant is read-only, limited to its scopes, and revoked when the attempt ends, which bounds what a copied token permits. Whether a package manager honours `NPM_CONFIG_USERCONFIG` is a property of that package manager.
291
+
292
+ ## Image Records
293
+
294
+ An `oci-image` attempt reports its pushed manifest through `PUT /jobs/{jobId}/attempts/{attemptId}/image` with `RecordImageRequest`. Recording is idempotent by `(identity, recordRequestId, canonicalRequestDigest)`: an exact retry returns the same `ImageRecord`; a changed body returns HTTP 409. An attempt has at most one image record, and replay records are retained with attempt metadata for at least 30 days after terminal state.
295
+
296
+ Before accepting a record, the coordinator verifies against the registry that the manifest exists in the push grant's repository, was written under this attempt's push grant, has exactly the reported digest, size, and media type, and that its image configuration names `requirements.platform` and contains every compiled label with its exact value. Any mismatch returns HTTP 409 without recording.
297
+
298
+ The coordinator rejects a `succeeded` completion of an `oci-image` attempt without an accepted image record with HTTP 409. The runner then completes `system_error` while its fence remains valid.
299
+
300
+ ## OCI Container Profile
301
+
302
+ `oci` requires an image that is digest-pinned or a candidate reference, and ordered compiled steps containing argument arrays. It does not support shell command strings. A candidate reference is resolved only through the lease binding.
303
+
304
+ The profile excludes:
305
+
306
+ - privileged containers;
307
+ - host PID, IPC, or network namespaces;
308
+ - host devices;
309
+ - host filesystem paths or sockets;
310
+ - mutable image tags;
311
+ - arbitrary runtime arguments.
312
+
313
+ Network access follows the job's network mode. Runtime implementations enforce non-root execution, a read-only root filesystem, dropped capabilities, `no-new-privileges`, bounded writable storage, and CPU, memory, PID, shared-memory, and time limits. Values from the job's `resources` are these ceilings for the whole attempt; `sharedMemoryBytes` is the size of `/dev/shm` in every step container, and omitted values take the runner's defaults.
314
+
315
+ Each attempt receives a new empty workspace. The runner extracts source first, then restores caches in declaration order only into their declared, non-overlapping relative paths. Each compiled step runs in order in a fresh non-privileged container using the same digest-pinned image and sharing only that bounded workspace. Every step receives its fully compiled non-secret environment plus all job-wide secret values, and `NPM_CONFIG_USERCONFIG` when the job declares `npmRead` (see npm Read Grants). This draft reserves `SHIPZONE_CI_MATRIX_JSON` and `SHIPZONE_CI_INPUTS_JSON`, which contain RFC 8785 JSON text and are included in every compiled step, using `{}` when no values apply, and, in a job with `npmRead`, `NPM_CONFIG_USERCONFIG`. Workflow authors cannot define any `SHIPZONE_CI_*` name. The combined environment, secret targets, and reserved names must be unique and total no more than 128. Step containers and descendant processes are stopped before the next step.
316
+
317
+ The runner stops on the first non-zero step exit. Exit status maps to `succeeded` when every step exits zero, `failed` for a non-zero command exit, `timed_out` for the job deadline, `canceled` for an accepted cancellation, and `system_error` for sandbox, integrity, protocol, or enforced resource-limit failures. After execution, artifacts are archived according to `when`; read-write caches are published only after all steps succeed. Completion is sent last. The workspace and all containers are removed on terminal state, abandonment, or fence loss.
318
+
319
+ ## VM Profile
320
+
321
+ `vm` runs the compiled steps in one microVM guest per attempt. The job has the container job shape with `execution.profile: vm`. It requires `microvm` isolation and `requirements.platform`, has `images: none`, and its lease has no push grant. Candidate references resolve through lease bindings as for `oci`, and image grants are used by the runner outside the guest and never enter it.
322
+
323
+ For each attempt the runner boots one new guest of `requirements.platform` from the job image:
324
+
325
+ - for a `linux` platform the image is an OCI image; a writable copy of its root filesystem for that platform, created for the attempt, becomes the guest root filesystem, and the runner provides the guest kernel. That kernel provides namespaces, cgroups, loop devices, nftables, and tun devices to the steps;
326
+ - for `darwin/arm64` the image is a digest-pinned, bootable `darwin/arm64` VM image, and the guest runs the kernel of that image. *(non-testable)* This version does not fix the media types of such an image; the runner deployment documents them.
327
+
328
+ The runner creates the workspace inside the guest, extracts the source into it, and restores caches as for `oci`. Every step runs in order in that one guest with root privileges, in `execution.workingDirectory` when present, and receives the environment defined for `oci` steps. When a step exits, the runner terminates every process the step started, including detached and daemonized descendants, before the next step starts: files persist across steps, processes do not. A step that needs a daemon, such as the container engine a job image brings, starts it itself.
329
+
330
+ Root in the guest is confined to the guest. No host device, filesystem path, or socket is visible, and no step can change network enforcement, reach runner-plane credentials, or observe another attempt. Nested virtualization is not guaranteed: a guest may have no `/dev/kvm`, and an emulator a job runs may run without hardware acceleration. `memoryBytes` is the guest memory, `cpus` bounds the guest's CPU time, `pids` bounds the processes of the steps, and `sharedMemoryBytes` is the size of `/dev/shm` in a `linux` guest. The remaining guarantees of Isolation apply.
331
+
332
+ The runner stops on the first non-zero step exit, and status maps as for `oci`. Artifacts are collected from the guest workspace according to `when`, read-write caches are published only after all steps succeed, and completion is sent last. The guest is destroyed on terminal state, abandonment, or fence loss.
333
+
334
+ ## OCI Image Build Profile
335
+
336
+ `oci-image` builds one single-platform OCI image from the source archive. The job has a `build` object and no `execution` or `steps`. It requires `microvm` isolation, `requirements.platform`, `images: push`, and exactly one push grant; it declares no artifacts and at most one cache.
337
+
338
+ For each attempt the runner:
339
+
340
+ 1. creates a new guest, extracts the source into its workspace, and restores the declared cache into a guest directory outside the build context;
341
+ 2. builds with a rootless BuildKit daemon inside the guest, for exactly `requirements.platform`, using `build.context` as the context directory and `build.dockerfile` as the Dockerfile, both relative to the source root, and `build.target` when present;
342
+ 3. passes `buildArgs` as build arguments, every `build.secrets` entry as a BuildKit secret whose value is the named `secrets` value, every `build.labels` entry as an image label, every `baseImages` entry as a named build context `name=docker-image://<image>` with the bound or pinned digest reference, and, when the job declares `npmRead`, the npm configuration as the BuildKit secret `npmrc` (see npm Read Grants);
343
+ 4. pushes the result by digest, without tags, to the push grant's repository with OCI media types and with provenance, SBOM, and every other attestation disabled, so the output is exactly one image manifest;
344
+ 5. records the image, publishes a read-write cache as `type=local` BuildKit cache export with `mode=max` only after the build succeeded, and completes.
345
+
346
+ Every image source the build resolves, including `FROM`, `COPY --from`, `RUN --mount=from`, and Dockerfile syntax frontends, is either a named base-image context or pinned by SHA-256 digest; the runner rejects any other image source before fetching it. With BuildKit this can be enforced by a source policy whose rules are a `DENY` of `docker-image://*` followed by an `ALLOW` of references matching `@sha256:[a-f0-9]{64}$` (last match wins). Build arguments are non-secret: they are part of the plan digest and may persist in image history. Build secrets are exposed only as BuildKit secret mounts: the runner and its builder never write a build secret into a layer, history entry, configuration, label, or exported cache record, and a secret mount leaves no trace in them. *(non-testable)* A Dockerfile instruction that copies a mounted secret into the image filesystem writes it into a layer and into the exported cache, where layer compression also hides it from the exact-byte check of Secret Redaction; the runner cannot prevent this, which is why protected secrets reach only protected tag runs.
347
+
348
+ Status maps to `succeeded` when the build, push, and image record succeed; `failed` when a Dockerfile instruction, the Dockerfile frontend, or image-source validation fails; `timed_out`, `canceled`, and `system_error` as for `oci`. `exitCode` is `null` for `oci-image` completions. The guest and all builder state are destroyed on terminal state, abandonment, or fence loss.
349
+
350
+ ## Errors and Retries
351
+
352
+ Errors use a bounded JSON envelope with a stable code and safe message. Authentication errors are terminal. HTTP 409 indicates a state, idempotency, sequence, identity, ownership, fencing, or specification-version conflict. HTTP 413 indicates a request or stream above a schema, declaration, negotiated, or protocol limit and performs no state mutation.
353
+
354
+ Only HTTP 429, 502, 503, and 504 are retryable. `Retry-After` is honored up to 30 seconds. Retries must reuse the original idempotency key and canonical body.
355
+
356
+ ## Protocol Limits
357
+
358
+ - JSON request or response body: 2 MiB.
359
+ - Log chunk data: 64 KiB.
360
+ - Total log data per attempt: 1 GiB.
361
+ - Identifier: 64 characters unless a schema is stricter.
362
+ - Command: 128 arguments, 16 KiB each, 128 KiB total after UTF-8 encoding.
363
+ - Combined non-secret environment, secret target, and reserved entries per step: 128.
364
+ - Source archive: 8 GiB compressed, 64 GiB extracted, 1,000,000 entries.
365
+ - Artifact archive: 16 GiB compressed, 128 GiB extracted, 1,000,000 entries per item; 64 items.
366
+ - Cache archive: 16 GiB compressed, 128 GiB extracted, 1,000,000 entries per item; 32 items.
367
+ - Archive path metadata: 134,217,728 UTF-8 bytes per source, artifact, or cache archive.
368
+ - Workspace: 1 TiB and 2,000,000 entries.
369
+ - Image grants and candidate bindings: 64 each per lease; npm grants: 8 per lease.
370
+ - Egress allowlist: 64 entries, 16 ports per entry.
371
+ - Image manifest: 4 MiB.
372
+ - Lease: 30 to 600 seconds.
373
+ - Cancellation grace: at most 30 seconds.
374
+ - Job timeout: 1 second to 24 hours; 1 hour when the workflow omits it.
375
+ - Retention: 1 to 3,650 days; 30 days for an artifact and 7 days for a cache when omitted.
376
+ - All byte counts: non-negative safe integers.
377
+
378
+ Implementations may set lower policy limits but may not accept values above protocol ceilings.