Orchestrator ↔ Agent messages
This layer carries job dispatch commands and execution status reports between customer-deployed orchestrators and agents.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Error fields never carry raw exception text
Section titled “Error fields never carry raw exception text”Ten orchestrator-to-agent fields describe a failure in free text: error on event.emit.response, agent.api.response, artifacts.upload.response, artifacts.download.response and scaler.claim-credentials.response, and reason on step.approval-resolved, artifacts.upload.complete.ack, auth.failure, job.cancel and job.concurrency.ack.
None of them ever carries the text of an orchestrator exception. The rule is the same in every family:
- A failure the workflow author can act on — an unknown job context, a rejected upload name, an approver’s decline note, an expired approval window — rides the orchestrator’s own return value with fixed, author-facing wording. It stays specific enough to fix the workflow without operator log access.
- An internal failure is recorded in the orchestrator’s own logs with the full exception, and the agent receives a safe fixed string that classifies the failure without naming any infrastructure.
The reason is the trust boundary: the agent executes customer workflow code, and whatever these fields carry is readable by that code and persisted into the author’s step logs. Raw exception text there would disclose database and storage endpoints, constraint names and internal identifiers to anyone who can write a workflow.
A new orchestrator-to-agent failure field inherits this rule; it does not need its own entry here.
Job dispatch and lifecycle messages
Section titled “Job dispatch and lifecycle messages”These messages carry job dispatch and cancellation, agent registration, and the status, log and heartbeat reports an agent sends back while it executes.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”job.dispatch
Section titled “job.dispatch”Dispatches a job to an agent for execution. Contains everything the agent needs to clone, configure, and run the job.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "job.dispatch" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| repoUrl | string | Yes | Git repository URL |
| ref | string | Yes | Git ref to checkout (branch name or SHA) |
| sha | string | Yes | Commit SHA |
| lockFileUrl | string | Yes | URL to fetch the lock file |
| jobConfig | Record<string, unknown> | Yes | Job configuration from the lock file (LockJob or LockDynamicJobFn) |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
| token | string | No | Short-lived GitHub installation token for private repo clone |
| secrets | Record<string, string> | No | Orchestrator-provided secrets to merge into step environment |
| namespacedSecrets | Record<string, Record<string, string>> | No | Namespaced secrets by context name: { 'context-name': { KEY: 'value' } } |
| maxLogSizeBytes | number | No | Max log size per step in bytes (agent defaults to 10MB) |
| concurrencyWaitTimeoutMs | number | No | Orchestrator-resolved concurrency-slot wait timeout in milliseconds, from the fleet-wide cluster_settings.concurrency_wait_timeout_ms. Absent on orchestrators that predate the setting — the agent then falls back to its own env/config default (1h). |
| sourceTarUrl | string | No | Pre-signed URL to the .kici/ source tarball (skips agent-side clone). Agent extracts into the work directory and imports the workflow .ts via the shared TypeScript loader hook. |
| sourceTarDigest | string | No | SHA-256 of the source tarball’s own bytes. The agent verifies the download against it before extraction; after extraction it re-computes the workflow contentHash against the extracted source to detect drift against the lock file. |
| depsUrl | string | No | Pre-signed URL to the node_modules tarball (skips npm install) |
| depsHash | string | No | SHA-256 hash of the dependency tarball bytes, used for streaming integrity verification on download |
| requestId | string | No | Trace ID (UUIDv4) from the originating webhook event |
| runPublicKey | string | No | Base64-encoded X25519 public key for encrypting secret outputs |
| upstreamJobOutputs | Record<string, Record<string, unknown>> | No | Outputs from upstream dependency jobs, keyed by job name |
| upstreamJobStatuses | Record<string, ExecutionJobStatus> | No | Terminal status of each upstream job, keyed by job name (per-child for fan-out). Powers ctx.needs.<job>.status. |
| upstreamInvokeResults | Record<string, InvokeResult[]> | No | Per-invoke-gate results for any upstream gate this job needs, keyed by gate job name. One entry per run the gate triggered, carrying that run’s non-secret declared outputs. Powers ctx.needs['<gate>'].result on a standard downstream job. Absent on orchestrators that predate it — the agent then resolves the gate need through the fan-out group shape instead. |
| sourceAuth | GitAuth | No | Structured clone auth for the source repo. Preferred over token; when both are set they must agree (sourceAuth.kind === 'basic' and sourceAuth.secret === token) or the dispatch is rejected. |
| workflowAuth | GitAuth | No | Structured clone auth for the workflow repo in a global-workflow dispatch where the workflow is authored on a different source than the source repo. Absent for same-provider global workflows (agent reuses sourceAuth for both clones). |
| npmRegistries | NpmRegistry[] | No | Private npm registries the agent should authenticate against before npm install. Each entry’s token is the resolved value (the orchestrator already looked it up via the per-environment secret resolver and protection-rule gates have already passed). Untrusted contributors get an empty list. |
| containerRegistryAuth | ContainerRegistryAuth | No | Registry credentials for pulling this job’s container image, already resolved by the orchestrator (the lock file carries secret names; the agent never resolves them itself). Absent on orchestrators that predate it — the agent then pulls anonymously. |
| installEnvSecrets | Record<string, string> | No | Extra resolved secrets to project as env vars on the install subprocess. Keyed by the bare secret name (the qualified env: prefix is stripped at resolution time). For use with a customer-committed .kici/.npmrc containing ${VAR} placeholders. |
| orgId | string | No | Org id that owns this run. Namespaces the user-facing cache so one tenant can never read another’s entries. |
| repoId | string | No | Repo identifier (e.g. owner/repo). Second namespacing level for the user-facing cache. |
| cacheRefScope | enum | No | Cache write scope for this job: shared lets it write the org-shared default-branch cache, isolated confines writes to a per-run scope while still allowing shared-scope reads. Absent is treated as isolated by the agent (fail-closed). |
NpmRegistry:
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Registry URL |
| scope | string | No | npm scope this registry serves (e.g., @my-org) |
| alwaysAuth | boolean | Yes | Send credentials on every request (not just authenticated routes) |
| token | string | Yes | Resolved auth token for the registry |
ContainerRegistryAuth:
| Field | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Registry username |
| password | string | Yes | Resolved registry password or token |
| serveraddress | string | Yes | Registry host the credentials authenticate to |
InvokeResult: { repo, workflow, runId, status, outputs } — one summoned run’s identity, terminal status, and declared outputs.
GitAuth:
| Field | Type | Required | Description |
|---|---|---|---|
| kind | enum | Yes | One of: basic (HTTPS Basic auth, PAT/password) or ssh (SSH private key) |
| user | string | No | Basic-auth username (omit for SSH; defaults filled in by the provider, e.g. x-access-token for GitHub-style PATs) |
| secret | string | Yes | Basic-auth password/PAT, or PEM-encoded SSH private key |
| sshHostKeyPolicy | enum | No | SSH-only. accept-new trusts first-seen host keys; pinned requires sshKnownHostsPem |
| sshKnownHostsPem | string | Conditional | SSH-only, required when sshHostKeyPolicy === 'pinned'. OpenSSH known_hosts content |
requestId persistence through the dispatch queue: When a job is dispatched immediately (agent available), requestId is read from the current AsyncLocalStorage context. When a job is queued (no agent available) and later drained, the requestId is persisted in the dispatch_queue database table and restored when the job is dequeued. This ensures the job.dispatch message always carries the original webhook’s trace ID, enabling end-to-end traceability even for delayed dispatches.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—jobDispatchSchema
job.cancel
Section titled “job.cancel”Cancels a running or queued job on the agent.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "job.cancel" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID to cancel |
| reason | string | Yes | Human-readable cancellation reason |
| force | boolean | No | When true, force-cancel immediately without waiting for hooks |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—jobCancelSchema
register.ack
Section titled “register.ack”Acknowledges agent registration and sends confirmed config back to the agent. The agent transitions to registered state only after receiving this message (with a 10s fallback for backward compatibility with older orchestrators).
| Field | Type | Required | Description |
|---|---|---|---|
| type | "register.ack" | Yes | Message discriminator |
| agentId | string | Yes | Confirmed agent identifier |
| labels | string[] | Yes | Confirmed capability labels |
| scalerManaged | boolean | No | Whether agent is managed by auto-scaler (default: false) |
| pendingDispatch | boolean | No | When set, the orchestrator’s scaler bound a specific queued job to this agent at spawn time and the job.dispatch message is in flight. Scaler-managed agents that see this flag must not arm the short KICI_SCALER_IDLE_TIMEOUT timer on register — the KICI_SCALER_PENDING_DISPATCH_TIMEOUT safety net still applies if the dispatch never arrives. |
| warmPool | boolean | No | When set, the orchestrator pre-spawned this agent for a warm pool rather than for a specific queued job. An agent that sees this flag must not arm any idle-shutdown timer on register: the orchestrator’s warm-pool reaper owns its lifetime and either dispatches a job to it or destroys it. Absent for a job-bound spawn, for a static agent, and on orchestrators that predate warm pools. An agent that ignores the flag arms the short KICI_SCALER_IDLE_TIMEOUT timer as usual. |
| capabilities | object | No | Optional agent-facing capabilities this orchestrator supports (absent on orchestrators that predate capability advertisement). Every flag is optional and absent means unsupported, so a newer agent falls back to the unnegotiated behavior. Today: artifactCompleteAck — the orchestrator acks artifacts.upload.complete. |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—registerAckSchema
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”agent.register
Section titled “agent.register”Sent immediately after connection as the first message. The orchestrator responds with register.ack to confirm registration and provide config. The agent transitions to registered state upon receiving the ack. A 10s fallback timer allows backward compatibility with older orchestrators that do not send register.ack.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "agent.register" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| agentId | string | Yes | Unique agent identifier |
| labels | string[] | Yes | Capability labels for job routing |
| maxConcurrency | number | No | Maximum concurrent jobs this agent handles (defaults to 1) |
| platform | string | No | Agent platform (os.platform(), e.g., linux, darwin) |
| arch | string | No | Agent architecture (os.arch(), e.g., x64, arm64) |
| version | string | No | Agent version (e.g., "0.0.1") |
| inFlightJobs | InFlightJob[] | No | Jobs still running on reconnection (enables job recovery) |
| hostname | string | No | Machine hostname (os.hostname()) |
| osRelease | string | No | OS kernel release (os.release()) |
| osVersion | string | No | OS version string (os.version()) |
| totalMemoryMb | number | No | Total system memory in MiB |
| cpuCount | number | No | Number of logical CPUs |
| nodeVersion | string | No | Node.js version (process.versions.node) |
| runningAsUser | string | No | Username of the OS user running the agent process |
| runningAsUid | number | No | UID of the OS user running the agent process |
| properties | Record<string, string | number | boolean> | No | Agent-reported typed host-vars (the KICI_PROPERTIES bag), shallow-merged into the host roster’s host properties with agent-reported keys winning. Absent means none |
InFlightJob: { jobId: string, runId: string }
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—agentRegisterSchema
agent.status
Section titled “agent.status”Periodic capacity update. Tells the orchestrator how many job slots are available.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "agent.status" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| agentId | string | Yes | Unique agent identifier |
| activeJobs | number | Yes | Number of currently running jobs |
| memoryUsedMb | number | No | Used memory in MiB (os.totalmem() - os.freemem()) |
| memoryAvailableMb | number | No | Available memory in MiB (os.freemem()) |
| uptimeSeconds | number | No | System uptime in seconds (os.uptime()) |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—agentStatusSchema
job.status
Section titled “job.status”Reports a job execution state transition. Sent at each lifecycle boundary (queued, running, success, failed, etc.).
| Field | Type | Required | Description |
|---|---|---|---|
| type | "job.status" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| state | enum | Yes | One of: pending, queued, running, recovering, cancelling, success, failed, cancelled, skipped, timed_out_stale, drift_dropped, unroutable |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
| data | Record<string, unknown> | No | Optional state-specific data (error messages, timing, etc.) |
| droppedJobs | string[] | No | Job names dropped by determinism drift (agent re-eval produced fewer jobs than expected) |
| secretOutputs | Record<string, { agentPublicKey: string, encrypted: string }> | No | Encrypted secret outputs from agent (present on job success when secret outputs exist) |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—jobStatusSchema
job.status (and step.status below) ride the agent’s durable send path: when the
WebSocket is down or the agent has not yet re-registered, the frame is enqueued in
the agent’s reconnect buffer and replayed after register.ack, in order, ahead of
new frames. A terminal transition (success/failed/cancelled) reported the
instant the connection drops is therefore delivered on reconnect rather than lost —
the same replay guarantee log lines have. The orchestrator treats a re-delivered
terminal frame idempotently (a completed dispatch row is not reopened). Only
connection-liveness frames like job.heartbeat remain unbuffered.
The upstream tier closes the complementary gap: if a run’s terminal transition is lost on a still-live connection (never disconnected, so no reconnect replay fires), a periodic reconciliation sweep re-reads the run’s current state from the owning orchestrator and settles the run. A run therefore reaches its terminal state even when the deciding frame is dropped mid-flight, not only when the connection drops and reconnects.
job.ack
Section titled “job.ack”Positive dispatch acknowledgment. Sent the moment the agent receives a job.dispatch and accepts it — after the drain and busy checks pass, before execution begins. It resolves the orchestrator’s dispatch-ack deadline (see below) so a dispatch that actually arrived is never mistaken for a lost one.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "job.ack" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—jobAckSchema
job.reject
Section titled “job.reject”Explicit dispatch rejection. Sent when the agent cannot accept a job.dispatch — it is already running a job (busy) or is draining (draining). Every job.dispatch is answered: accepted with job.ack (a job.status with state running also resolves the deadline) or refused with this message. On receiving it the orchestrator undoes its dispatch accounting and requeues the job for another agent. An unanswered dispatch is recovered by the dispatch-ack deadline (below) and by disconnect-time triage.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "job.reject" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| reason | enum | Yes | One of: busy, draining |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—jobRejectSchema
Dispatch acknowledgment deadline
Section titled “Dispatch acknowledgment deadline”A job.dispatch message can be lost in transit — most visibly when a scaler-managed agent tears down inside its post-job idle window while a dispatch is in flight. To make “dispatched but never received” detectable, every job.dispatch carries a deadline:
- When the orchestrator sends a dispatch it stamps a deadline (
dispatch_queue.ack_deadline) and arms an in-memory timer. The deadline starts when the message is actually sent, not before the secret-merge and token-mint preparation. - The deadline is resolved by any answer:
job.ack(accept),job.reject(refuse), or ajob.statuswith staterunning(which doubles as an ack in case the ack itself was lost). - If no answer arrives in time, the dispatch is treated as lost: the orchestrator requeues the job (reusing the same attempt budget and scaler-consult machinery as a rejected dispatch) and disconnects the unresponsive agent with the
4031close code. A scaler-managed agent is then destroyed by its normal lifecycle; a static agent reconnects and re-syncs. - A requeued job that no connected agent takes on the immediate re-dispatch is not stranded: a periodic per-coordinator sweep re-attempts delivery of every still-pending job onto a connected idle agent whose labels match, through the same atomic claim a normal dispatch uses. So a re-dispatch that transiently missed an eligible agent — because the agent was momentarily busy, or connected to a different coordinator — still lands the job on the next sweep rather than waiting for the job to expire.
The deadline survives an orchestrator restart or leader switch: it is persisted in dispatch_queue, re-armed from the persisted rows on boot, and any deadline that elapsed while no coordinator was watching is swept on the leader. The default is 10 seconds (KICI_DISPATCH_ACK_TIMEOUT_MS), overridable per org via org_settings.dispatch_ack_timeout_ms (kici-admin org-settings dispatch-ack).
step.status
Section titled “step.status”Reports a step-level execution state transition. Sent for each step within a job.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "step.status" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| stepIndex | number (int >= 0) | Yes | Zero-based step index |
| stepName | string | Yes | Human-readable step name |
| state | enum | Yes | One of: running, success, failed, skipped, pending, cancelled |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
| data | Record<string, unknown> | No | Optional state-specific data (error, duration, etc.) |
| step_type | enum | No | Distinguishes regular steps from hook executions: step, hook:onCancel, hook:cleanup, hook:onSuccess, hook:onFailure, hook:beforeStep, hook:afterStep |
| secretsAccessed | string[] | No | Secret key names accessed by this step via ctx.secrets.get()/expose(). Never contains values |
| concurrencyKind | enum | No | Step concurrency role: sequential, parallel-child, parallel-group. Absent means an ordinary sequential step |
| groupId | string | No | Parallel-group correlation id shared by a group’s children (e.g. g0) |
| logBytesStreamed | number (int >= 0) | No | Total raw bytes streamed by this step’s LogStreamer at terminal time. Set on terminal step states only; reused by the orchestrator to accumulate per-job and per-run totals for the kici_org_log_bytes capacity-planning gauge |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—agentStepStatusSchema
log.chunk (Agent direction)
Section titled “log.chunk (Agent direction)”Streams log output from step execution to the orchestrator. The orchestrator may forward these upstream for dashboard display.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "log.chunk" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| stepIndex | number | Yes | Zero-based step index |
| lines | string[] | Yes | Array of log output lines |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
| stream | enum | No | Stream these lines came from: stdout, stderr. Absent (older agent) reads as stdout |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—agentLogChunkSchema
job.heartbeat
Section titled “job.heartbeat”Periodic heartbeat sent by agents for each running job. Used by the orchestrator’s stale run detector to identify jobs that have stopped progressing. Unlike connection-level heartbeats, this is per-job.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "job.heartbeat" | Yes | Message discriminator |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
Note: job.heartbeat has no messageId field — it is a lightweight fire-and-forget message.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—jobHeartbeatSchema
agent.log
Section titled “agent.log”Operational log lines streamed from stateful/external agents to the orchestrator via WebSocket. Used for agents whose logs cannot be captured by the host (e.g., Firecracker VMs, external agents). Lines are batched (up to 50 lines or 100ms debounce) for efficiency.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "agent.log" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| agentId | string | Yes | Agent identifier |
| lines | string[] | Yes | Array of log output lines |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—agentLogSchema
config.ack
Section titled “config.ack”Sent by the agent after receiving and applying register.ack config. Signals the orchestrator that MMDS data can be cleared for Firecracker agents. In Firecracker/scaler-managed mode, the agent also blocks MMDS access via iptables before sending this acknowledgment.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "config.ack" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| agentId | string | Yes | Agent identifier acknowledging config |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—configAckSchema
Event emit messages
Section titled “Event emit messages”These messages support custom event emission from running workflow steps. When a step calls ctx.emit(), the agent sends an event.emit message to the orchestrator, which responds with a delivery receipt.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”event.emit
Section titled “event.emit”Emits a custom event from a running workflow step. The orchestrator stores the event, performs fan-out matching against lock file subscriptions, and returns a delivery receipt.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "event.emit" | Yes | Message discriminator |
| jobId | string | Yes | Job that is emitting the event |
| requestId | string | Yes | Correlates request to response for routing |
| eventName | string | Yes | Custom event name (e.g., "deploy-complete") |
| payload | Record<string, unknown> | Yes | Event payload data |
| target | object | No | Cross-repo targeting: { repos?: string[] } |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—eventEmitSchema
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”event.emit.response
Section titled “event.emit.response”Response confirming event delivery. Contains either a deliveryId on success or an error on failure.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "event.emit.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the original event.emit requestId |
| deliveryId | string | No | Delivery ID assigned by orchestrator (on success) |
| error | string | No | Error description (on failure) |
error is either a deliberate author-facing reason — an unknown job context — or a safe fixed string classifying an internal failure. It is never raw exception text.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—eventEmitResponseSchema
Cache upload messages
Section titled “Cache upload messages”These messages support agent direct-to-S3 cache uploads. The agent requests a pre-signed upload URL from the orchestrator, performs the upload directly to object storage, then confirms completion so the orchestrator can update cache metadata.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”cache.upload.request
Section titled “cache.upload.request”Request a pre-signed upload URL for cache storage (source bundle or dependency tarball).
| Field | Type | Required | Description |
|---|---|---|---|
| type | "cache.upload.request" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job that produced the artifact |
| cacheType | enum | Yes | One of: source, deps |
| contentHash | string | No | Content hash for cache keying |
| lockfileHash | string | No | Lockfile hash for cache keying |
| platform | string | Yes | OS platform (e.g., linux) |
| arch | string | Yes | CPU architecture (e.g., x64, arm64) |
| depsHash | string | No | SHA-256 of the dep tarball, so the orchestrator can sign a content-addressed URL. Deps uploads only. Absent on an older agent, which then gets a lockfile-keyed URL |
| sourceTarDigest | string | No | SHA-256 of the source tarball, so the orchestrator can sign a content-addressed URL. Source uploads only. Absent on an older agent, which then gets a lockfile-keyed URL |
| siblingsDigest | string | No | In-repo workspace: sibling closure digest; part of the dependency pointer key |
cache.upload.complete
Section titled “cache.upload.complete”Confirm that an upload finished so the orchestrator can update metadata.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "cache.upload.complete" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job that produced the artifact |
| cacheType | enum | Yes | One of: source, deps |
| contentHash | string | No | Content hash for cache keying |
| lockfileHash | string | No | Lockfile hash for cache keying |
| platform | string | Yes | OS platform (e.g., linux) |
| arch | string | Yes | CPU architecture (e.g., x64, arm64) |
| depsHash | string | No | SHA-256 of the dep tarball (for agent-side integrity checking) |
| sourceTarDigest | string | No | SHA-256 of the source tarball (for agent-side integrity checking) |
| siblingsDigest | string | No | In-repo workspace: sibling closure digest; part of the dependency pointer key |
cache.user.restore.request
Section titled “cache.user.restore.request”Request a user-cache restore (declarative or imperative ctx.cache) — the orchestrator returns a pre-signed download URL for the matching entry.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "cache.user.restore.request" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job requesting the restore |
| key | string | Yes | Exact cache key |
| restoreKeys | string[] | No | Ordered prefix fallbacks (newest matching entry wins) |
cache.user.save.request
Section titled “cache.user.save.request”Request a pre-signed upload slot for a user-cache save. Keys are immutable, so the orchestrator may decline when the key already exists.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "cache.user.save.request" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job saving the cache |
| key | string | Yes | Exact cache key |
cache.user.save.complete
Section titled “cache.user.save.complete”Confirm a user-cache upload finished so the orchestrator can commit the temp object to its final key and record metadata.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "cache.user.save.complete" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job that produced the artifact |
| key | string | Yes | Exact cache key |
| tarHash | string | Yes | SHA-256 of the tarball bytes |
| sizeBytes | number | Yes | Tarball size in bytes (drives quota accounting) |
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”cache.upload.response
Section titled “cache.upload.response”Return the pre-signed upload URL for the agent to upload directly to object storage.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "cache.upload.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the original request |
| uploadUrl | string | Yes | Pre-signed URL for direct S3 upload |
cache.user.restore.response
Section titled “cache.user.restore.response”Return the matched user-cache entry’s pre-signed download URL, or signal a miss.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "cache.user.restore.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the original request |
| hit | boolean | Yes | True when an entry matched (exact or prefix) |
| matchedKey | string | No | Full key that matched (exact or the matched prefix entry) |
| downloadUrl | string | No | Pre-signed GET URL for the matched tarball (present only on hit) |
| tarHash | string | No | SHA-256 of the tarball bytes for integrity verification |
cache.user.save.response
Section titled “cache.user.save.response”Return the pre-signed upload URL, or signal skip when the immutable key already exists.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "cache.user.save.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the original request |
| uploadUrl | string | No | Pre-signed PUT URL to the temp object (absent when skip is true) |
| skip | boolean | Yes | True when the exact key already exists (immutable no-op) |
Provenance attestation messages
Section titled “Provenance attestation messages”These messages mirror the cache-upload handshake for build provenance bundles: the agent requests a pre-signed PUT URL keyed by the artifact’s subject digest, uploads the attestation directly to object storage, then confirms completion so the orchestrator records an attestations row. When the signed statement cannot be minted at build time, the agent instead sends provenance.upload.defer carrying the frozen DSSE envelope so the mint can complete later.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”provenance.upload.request
Section titled “provenance.upload.request”Request a pre-signed PUT URL for a provenance bundle. The orchestrator resolves the run server-side and ownership-checks the job.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "provenance.upload.request" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job producing the attestation (ownership-checked) |
| subjectDigest | string | Yes | Primary subject digest (lowercase hex); storage-key discriminator |
provenance.upload.complete
Section titled “provenance.upload.complete”Confirm a provenance bundle upload so the orchestrator records an attestations row.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "provenance.upload.complete" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job producing the attestation |
| subjectName | string | Yes | Caller-supplied artifact name |
| subjectDigest | string | Yes | Primary subject digest (lowercase hex) |
| mediaType | string | Yes | Bundle media type |
provenance.upload.defer
Section titled “provenance.upload.defer”Hand a frozen, DSSE-signed statement to the orchestrator for a later mint when the attestation cannot be signed at build time. The envelope and its ephemeral public key are carried inline; statementHash binds the deferred mint to this exact payload.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "provenance.upload.defer" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job producing the attestation |
| subjectName | string | Yes | Caller-supplied artifact name |
| subjectDigest | string | Yes | Primary subject digest (lowercase hex) |
| audience | string | Yes | Requested token audience for the later mint |
| mediaType | string | Yes | Bundle media type |
| statementHash | string | Yes | SHA-256 of the frozen DSSE statement payload |
| dsseEnvelope | object | Yes | The frozen, DSSE-signed statement envelope |
| publicKey | object | Yes | Ephemeral public key JWK the envelope was signed with |
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”provenance.upload.response
Section titled “provenance.upload.response”Return the pre-signed PUT URL for the agent to upload the bundle directly to object storage.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "provenance.upload.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the original request |
| uploadUrl | string | Yes | Pre-signed PUT URL, or "" when storage is unavailable |
Artifact messages
Section titled “Artifact messages”These messages carry the run-scoped artifact upload/download handshake. When a step uploads a named artifact, the agent requests a pre-signed PUT URL; the orchestrator validates the artifact name and runs the enforcement gates (duplicate name, per-artifact/per-run size cap, org quota) before minting the URL, so a granted URL is always safe to upload to. Downloads resolve a named artifact within the same run to a pre-signed GET.
The orchestrator checks the artifact name itself rather than trusting the agent’s own check, because the name becomes a path segment of the storage key. It must be a short token of letters, digits, ., _, and -, up to 128 characters, and not made only of dots. Under that contract the name maps to its storage segment unchanged, so two distinct names can never produce the same key — a name outside it would be folded onto another name’s key, letting a second upload overwrite the first object while both records keep their own hash. The same check runs on the upload request and on the upload completion, since either message can carry a name. The key also carries a hash of the exact name, so two names differing only by case differ by more than case and address distinct objects on a case-insensitive object store as well.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”artifacts.upload.request
Section titled “artifacts.upload.request”Request a pre-signed PUT URL for a named artifact upload.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "artifacts.upload.request" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job producing the artifact (ownership-checked; run + org resolved server-side) |
| name | string | Yes | Artifact name (immutability + storage-key discriminator within the run) |
| declaredSizeBytes | number (int >= 0) | Yes | Packed tarball size in bytes; drives the size/run/quota enforcement gates |
artifacts.upload.complete
Section titled “artifacts.upload.complete”Confirm an artifact upload finished so the orchestrator records the artifacts row. The orchestrator replies with an artifacts.upload.complete.ack when it advertises the artifactCompleteAck capability, and the agent waits for it before the upload step returns.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "artifacts.upload.complete" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job producing the artifact |
| name | string | Yes | Artifact name |
| sha256 | string | Yes | SHA-256 (hex) of the tarball bytes |
The object is strict. The orchestrator derives the storage key from the run and artifact name it resolved server-side, and it records the real object size it reads back from storage. A frame carrying storageKey, sizeBytes, or any other field outside this table is refused.
artifacts.download.request
Section titled “artifacts.download.request”Request a pre-signed GET URL for a named artifact of this run.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "artifacts.download.request" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| jobId | string | Yes | Job requesting the download (ownership-checked; run resolved server-side) |
| name | string | Yes | Artifact name to resolve within the run |
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”artifacts.upload.response
Section titled “artifacts.upload.response”Return a pre-signed PUT (granted) or a refusal (rejected). All enforcement happens before minting, so a granted URL is safe to upload to.
A rejected carries either reason (an enforcement gate was hit) or error (the request could not be serviced at all — the name violates the artifact-name contract, artifact storage is not configured, the job’s run could not be resolved, the job is not owned by this agent, or the grant failed internally), never both. Keeping the two apart is what stops a bad name or an orchestrator-side problem from reaching the workflow author as a storage-quota rejection. error is a safe fixed string, never raw exception text; an agent that predates the field falls back to a generic rejection message.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "artifacts.upload.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the original request |
| outcome | enum | Yes | One of: granted, rejected |
| uploadUrl | string | No | Pre-signed PUT URL (present only on granted) |
| storageKey | string | No | Storage key of the object the grant covers (present only on granted) |
| reason | enum | No | Enforcement refusal reason (present only on an enforcement rejected): duplicate_name, size_cap, run_cap, org_quota |
| error | string | No | Failure detail — invalid name or internal failure (present only on a rejected with no reason) |
artifacts.upload.complete.ack
Section titled “artifacts.upload.complete.ack”Report the outcome of committing an artifacts.upload.complete. The commit is retried a few times before failed is reported, because it is idempotent and a momentary storage/database blip should not fail a run. An agent that sees the artifactCompleteAck capability on register.ack waits for this message and fails the workflow step on failed (or on a 25-second timeout, deliberately inside the step’s own artifact-request timeout so the specific reason is what surfaces), so a commit that never landed surfaces as a failed step instead of a green run with a missing artifact. An orchestrator that does not advertise the capability sends nothing, and the agent then treats the complete message as fire-and-forget and does not wait.
Losing the connection while an ack is in flight does not fail the step. Because the commit is idempotent, the agent parks the in-flight complete instead of failing it, and re-sends it under a fresh messageId after the next register.ack — up to twice, and only while the reconnected orchestrator still advertises the capability. The correlation id is re-minted on every resend, so a late ack for the pre-disconnect send is ignored rather than answering for the new one. The original 25-second deadline is never restarted, so an orchestrator that never comes back still fails the step on the same schedule; when the resends run out, when the reconnected orchestrator no longer acks, or when the agent’s token is rejected outright so no reconnect will be attempted, the step fails with an error saying the artifact may nevertheless have been committed.
A failed ack carries reason, a safe fixed string classifying the failure — artifact storage is not configured, the job’s run could not be resolved, the job is not owned by this agent, the uploaded object was missing at commit time, the name violates the artifact-name contract, or the commit failed internally. As on the upload and download responses, reason is never raw exception text: the exception stays in the orchestrator’s own logs, so a database endpoint or a storage key never reaches the workflow author.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "artifacts.upload.complete.ack" | Yes | Message discriminator |
| requestId | string | Yes | Echoes the complete message’s messageId |
| outcome | enum | Yes | One of: committed, failed |
| reason | string | No | Safe fixed failure classification (present only on failed) |
artifacts.download.response
Section titled “artifacts.download.response”Return a pre-signed GET plus size/sha256 for the named artifact, or not_found when the run never uploaded an artifact by that name.
not_found is also the outcome when the orchestrator could not perform the lookup at all — artifact storage is not configured, the job’s run could not be resolved, the job is not owned by this agent, or the lookup failed internally. Those replies carry error, which is what separates “this artifact does not exist” from “this orchestrator could not look it up”; a genuine miss carries no error. As on the upload response, error is a safe fixed string, never raw exception text, and an agent that predates the field renders the plain not-found message.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "artifacts.download.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the original request |
| outcome | enum | Yes | One of: found, not_found |
| downloadUrl | string | No | Pre-signed GET URL (present only on found) |
| sizeBytes | number (int >= 0) | No | Artifact size in bytes (present only on found) |
| sha256 | string | No | SHA-256 (hex) of the tarball bytes for integrity verification (present only on found) |
| error | string | No | Internal-failure detail (present only on a not_found caused by an orchestrator failure) |
Fleet log collection messages
Section titled “Fleet log collection messages”These messages support debug-bundle collection across a cluster: the orchestrator asks each agent for a log/diagnostic mini-bundle, and the agent streams the ZIP back in base64 frames (or reports an error).
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”fleet.logs.request
Section titled “fleet.logs.request”Ask an agent for its log/diagnostic mini-bundle.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "fleet.logs.request" | Yes | Message discriminator |
| requestId | string | Yes | UUID correlating the chunked response |
| logWindowHours | number | Yes | Hours of log history to include |
| maxBytes | number | Yes | Per-node cap on raw log bytes |
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”fleet.bundle.chunk
Section titled “fleet.bundle.chunk”One base64 frame of an agent’s mini-bundle ZIP.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "fleet.bundle.chunk" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the originating request |
| seq | number | Yes | Zero-based frame sequence number |
| isLast | boolean | Yes | True on the final frame |
| dataB64 | string | Yes | Base64-encoded ZIP frame |
fleet.bundle.error
Section titled “fleet.bundle.error”Reported when an agent fails to build or stream its mini-bundle.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "fleet.bundle.error" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the originating request |
| message | string | Yes | Failure description |
A fleet.bundle.chunk whose requestId names a collection the orchestrator is still waiting on from that agent is exempt from the connection’s rate limiter, the same way a heartbeat is. The agent streams its whole bundle as one burst, and maxBytes lets that bundle exceed the limiter’s byte burst many times over. A throttled frame would leave a gap the assembler rejects as out of order. A chunk that names no such collection is rate-limited like any other frame.
Connection rate limiting
Section titled “Connection rate limiting”Every agent connection carries a per-connection rate limiter on message count and byte throughput. Heartbeats and solicited fleet bundle chunks are exempt.
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”rate.limit.warning
Section titled “rate.limit.warning”Sent instead of processing a frame the limiter refused. The frame is dropped; the agent should wait before sending more. A sustained overrun closes the connection instead of warning.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "rate.limit.warning" | Yes | Message discriminator |
| retryAfterMs | number | No | Estimated wait until the limiter admits another frame of that size |
Scaler credential-claim messages
Section titled “Scaler credential-claim messages”These messages carry the event-scaler provisioning claim. The event scaler emits a kici.scaler.scale-up event carrying a single-use claimCode — never a token. The provisioning workflow forwards that code to the instance it boots. The agent exchanges the code over this round-trip for a freshly minted ephemeral agent token, then registers with it. A workflow can also exchange the code itself and deliver the token to the instance. The token is therefore minted lazily, only when a real provision claims it, and it appears in the response frame only — never in the persisted event log.
The workflow-facing surface is ctx.kici.scaler.claimAgentCredentials(claimCode); see Event scaler.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”scaler.claim-credentials
Section titled “scaler.claim-credentials”Exchanges a single-use claim code for ephemeral agent credentials. The claim code is the authorization, so a fresh agent can send this message before it authenticates or registers. An already-registered agent can also send it.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "scaler.claim-credentials" | Yes | Message discriminator |
| requestId | string | Yes | Correlates the response |
| claimCode | string | Yes | Single-use code from the scale-up event payload |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—scalerClaimCredentialsSchema
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”scaler.claim-credentials.response
Section titled “scaler.claim-credentials.response”The minted credentials for a claimed provision, or an error. Exactly one of credentials / error is present.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "scaler.claim-credentials.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the originating scaler.claim-credentials |
| credentials | { agentToken, agentId, orchestratorUrl, labels } | No | Minted credentials (on success) |
| error | string | No | Failure description (on failure) |
error carries the deliberate claim verdicts verbatim — the code is unknown, already consumed, or past its claimTtlSeconds window — because each names only the caller’s own claim. It also carries a fixed string when the orchestrator has no event scaler configured. Any other failure is a safe fixed string, never raw exception text.
The claim is consumed before the token is minted, so a concurrent replay of the same code sees it consumed and cannot double-mint.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—scalerClaimCredentialsResponseSchema
Agent private API messages
Section titled “Agent private API messages”These messages carry a generic request/response envelope over the existing agent connection. The orchestrator registers each callable method by name and required role; the agent names a method and passes its parameters, and the orchestrator replies with the method’s result or an error. Adding a method is a server-side registration, not a new message type.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”agent.api.request
Section titled “agent.api.request”Calls a registered method on the orchestrator.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "agent.api.request" | Yes | Message discriminator |
| requestId | string | Yes | Correlates the response |
| method | string | Yes | Dot-namespaced method name (e.g. infrastructure.list) |
| params | object | No | Method-specific parameters (default: empty object) |
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—agentApiRequestSchema
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”agent.api.response
Section titled “agent.api.response”Result of one agent.api.request.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "agent.api.response" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the originating agent.api.request |
| result | unknown | No | The method’s return value (on success) |
| error | string | No | Failure description (on failure) |
error carries the two deliberate rejections verbatim — the method is not registered, or the caller lacks the role the method requires — because both name only the caller’s own request. Any other failure is a safe fixed string, never raw exception text.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts—agentApiResponseSchema
Registered methods
Section titled “Registered methods”Each method carries a required role — read or write — checked against the calling agent’s own roles before the handler runs. Several are registered only when the orchestrator has the dependency the method needs. An agent that calls one on an orchestrator which did not register it gets the ordinary “unknown method” error, never a partial result.
| Method | Role | Registered when | Purpose |
|---|---|---|---|
infrastructure.list | read | Always | Lists the orchestrator’s configured scaler backends |
inventory.query | read | A host roster is configured | Queries the cluster’s host roster (backs ctx.kici.inventory) |
inventory.get | read | A host roster is configured | Reads one host’s roster entry |
host.requestReboot | write | A host roster is configured | Marks a host reboot-pending so the imminent disconnect is expected, not a failure |
host.cancelReboot | write | A host roster is configured | Clears the reboot-pending flag |
kici.ensureInitRunner | write | A host roster is configured and the token store, secret resolver and access log are all present | Bring-up: ensures the init runner exists on a target host |
kici.preBootSend | write | Same as above | Bring-up: returns the scoped transport material for pushing a fresh box |
kici.presignAgentPackage | write | Same as above | Bring-up: pre-signs the agent package download |
kici.agentVersionStatus | write | Same as above | Fleet upgrade: reports a host’s target and staged agent version |
kici.restageAgent | write | Same as above | Fleet upgrade: re-stages a host’s agent once availability allows |
oidc.token.request | read | An identity-token mint is available (see below) | Mints a short-lived build-provenance ID token bound to the calling job |
git.credential.request | read | A secret resolver is configured | Mints a scoped, short-lived git credential for one network operation |
The bring-up and roster methods take write because they change host state; every read-only lookup and every mint bound to the caller’s own job takes read.
oidc.token.request
Section titled “oidc.token.request”Mints a build-provenance ID token bound to the job the calling agent actually owns. The agent supplies only its job and the requested audience — every identity claim (repository, ref, workflow) is derived on the orchestrator side, so a workflow can never assert an identity it does not have.
Which mint serves the call is the anti-forgery choke point. An orchestrator that owns a signing key and its own issuer signs locally, because the orchestrator is the root of trust. The offline local dev plane mints locally under the kici-local issuer. An orchestrator with no key registers nothing, so the method is unknown.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
| jobId | string | Yes | The job the calling agent is executing |
| audience | string | Yes | Requested token audience (1-255 characters) |
Result — either a minted token, or a deferral:
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | The minted short-lived JWT |
| expiresIn | number | Yes | Token lifetime in seconds |
| jti | string | Yes | Token identifier, used to correlate the mint downstream |
| Field | Type | Required | Description |
|---|---|---|---|
| deferred | true | Yes | The mint failed transiently; the agent freezes the statement and reports it for later fulfilment |
| code | enum | Yes | unavailable or failed |
A permanent rejection is thrown rather than returned, so it never appears as a deferral.
Authoritative source:
packages/engine/src/protocol/messages/oidc-token-relay.ts
git.credential.request
Section titled “git.credential.request”Mints a git credential for a single network operation. The agent’s credential helper calls this on every fetch and push, so a token is always minted seconds before use. That is what lets an arbitrarily long job keep pushing past the point where a credential captured at checkout would have expired.
A repository must be named explicitly: wildcards are rejected by the schema. A write grant is therefore always a specific repository written down in the workflow source and reviewable in a diff.
Authorization runs in two layers before anything is minted. Job ownership comes first — an agent may only request credentials for a job it was actually dispatched, resolved from server truth rather than from the parameters. Then the repository fence, which applies to write only. Every named repository must sit inside the organisation that owns the job’s source repository. One out-of-organisation name refuses the whole request, rather than minting a token that covers both. A read needs no fence, because naming a repository the credential’s own installation does not cover yields nothing.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
| jobId | string | Yes | The job the calling agent is executing |
| repositories | string[] | Yes | Every repository the credential must cover, most-specific first. A list because one app installation token can name several repositories at once |
| ref | GitCredentialRef | No | Which credential to mint from. Absent falls back to the source credential the orchestrator already holds for the job |
| permissions | Record<string, string> | No | Requested permissions, passed through verbatim for the forge to accept or reject |
GitCredentialRef is one of three shapes, each naming its material as either a qualified <context>:<secret-name> reference or a runtime value, exactly one per pair:
| kind | Fields |
|---|---|
app | appId*, installationId*, privateKey* |
token | token*, optional user |
ssh | privateKey* |
The schema rejects a reference that looks like the credential itself rather than the name of a secret holding it. Pasting a key or token where a name belongs is the easy, silent mistake, so it is caught on the wire as well as in the workflow source.
Result:
| Field | Type | Required | Description |
|---|---|---|---|
| kind | enum | Yes | basic (HTTPS) or ssh |
| user | string | No | Basic-auth username. Omitted for SSH |
| secret | string | Yes | The minted credential |
| grant | object | Yes | { scoped: false }, or { scoped: true, permissions } naming exactly what the credential was scoped to |
| expiresAt | string or null | Yes | Expiry timestamp, or null for a credential with no expiry |
Authoritative source:
packages/engine/src/protocol/messages/git-credential-relay.ts
Step approval messages
Section titled “Step approval messages”These messages carry the step-level approval round-trip. When a step declares approval, the agent blocks its step loop and sends a step.approval-request; the orchestrator creates a step-scoped held-run from the requirement and replies with step.approval-resolved once the hold is approved, rejected, or expired. The agent keeps heartbeats flowing during the wait so it is not reaped as stale.
Authoritative source:
packages/engine/src/protocol/messages/orchestrator-agent.ts
Agent -> Orchestrator
Section titled “Agent -> Orchestrator”step.approval-request
Section titled “step.approval-request”A step carrying approval is about to run and the agent is blocking until the orchestrator resolves the approval. For a when: 'drift' gate the request carries the computed drift payload.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "step.approval-request" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID (correlated by the resolution’s requestId) |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| stepIndex | number | Yes | Zero-based step index |
| stepName | string | Yes | Human-readable step name |
| clauses | ApproverClause[] | Yes | AND-list of approver clauses (empty = any approval-capable member) |
| reason | string | Yes | Human label for the gate (from the SDK approval reason) |
| timeoutSeconds | number | No | Per-gate timeout override; absent falls back to the org-default expiry |
| payload | { summaryMarkdown, drift } | No | Computed drift, present only for a when: 'drift' gate; persisted on the hold and rendered in the approval queue + CLI |
Orchestrator -> Agent
Section titled “Orchestrator -> Agent”step.approval-resolved
Section titled “step.approval-resolved”Resolution of a step-level approval hold. On approved the agent runs the step with its live workspace intact; on rejected/expired it fails the job with a clear reason.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "step.approval-resolved" | Yes | Message discriminator |
| requestId | string | Yes | Correlates to the originating step.approval-request messageId |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| stepIndex | number | Yes | Zero-based step index |
| outcome | enum | Yes | One of: approved, rejected, expired |
| reason | string | No | Optional human reason (e.g. the reject reason) |
On rejected, reason is either deliberate rejection wording — an invalid per-gate timeout, or the approver’s own decline note — or a safe fixed string classifying an internal failure. It is never raw exception text.
Execution status messages
Section titled “Execution status messages”These messages flow from the orchestrator upstream to KiCI for execution metadata tracking and real-time dashboard updates.
Authoritative source:
packages/engine/src/protocol/messages/execution-status.ts
execution.status
Section titled “execution.status”Structured execution status update sent by the orchestrator when an execution run starts, completes, or changes status. The upstream tier stores this metadata for dashboard queries.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "execution.status" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| workflowName | string | Yes | Name of the workflow being executed |
| status | enum | Yes | One of: pending, running, success, failed, cancelled, cancelling, held |
| routingKey | string | No | Routing key of the run’s own source, so each run is attributed to its real source when one connection serves multiple sources (absent → the upstream falls back to the connection’s first routing key) |
| repoIdentifier | string | No | Repository identifier (e.g., owner/repo) |
| workflowRepoIdentifier | string | No | Repository that DEFINES the workflow, when that is not repoIdentifier — a global workflow authored in one repository and dispatched against another. Absent for every per-repository run |
| isGlobalEvalRound | boolean | No | True when the run records a global evaluation round rather than a workflow. Mirrored upstream so a re-run of a round re-evaluates the original event instead of resolving a workflow out of the acted-on repository’s lock file. Absent means an ordinary run |
| repoProvider | string | No | Run-level repo provider (origin host: github / gitlab / bitbucket / local); drives provider-aware repo links in the dashboard |
| localWorkingTree | boolean | No | True when the run executed a developer’s uploaded local working tree (kici run remote) |
| sha | string | No | Commit SHA |
| ref | string | No | Git branch or tag (e.g., main, refs/tags/v1.0) |
| triggerEvent | string | No | Trigger event type (e.g., push, pr:open) |
| commitMessage | string | No | First line of the commit message |
| jobCount | number | No | Total number of jobs in this execution |
| startedAt | number | Yes | Unix timestamp when execution started |
| completedAt | number | No | Unix timestamp when execution completed |
| durationMs | number | No | Total execution duration in milliseconds |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
| parentRunId | string or null | No | Parent run ID for re-run lineage (null/undefined for originals) |
| originalRunId | string or null | No | Root ancestor run ID (always points to first run in chain) |
| triggeredBy | string or null | No | User identity that triggered this re-run (null for webhook) |
| triggeredByAgentLabel | string or null | No | Agent provenance label when the run was triggered through an agent credential |
| triggerActorProvider | string or null | No | Provider of the triggering actor captured from the provider event (the pusher / PR author), distinct from triggeredBy |
| triggerActorUsername | string or null | No | Provider login of the triggering actor |
| triggerActorUserId | string or null | No | Provider user ID of the triggering actor |
| failureReason | string | No | Human-readable reason why the run failed (only present for failed runs) |
| logBytes | number | No | Total raw log bytes accumulated across all jobs of this run; only set on terminal run states |
| initFailure | object | No | Structured init-failure signal (scope, category, message, optional jobName) set when the run never executed a single step; only present when status is failed |
| failureClass | enum | No | Why a terminal run failed: never_started, timed_out, dead_orchestrator, step_failure, cancelled (only present for failed/cancelled runs) |
Authoritative source:
packages/engine/src/protocol/messages/execution-status.ts—executionStatusSchema
step.status.forward
Section titled “step.status.forward”Per-step status forwarded from agent through the orchestrator upstream in real-time. Enables live step-by-step progress in the dashboard.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "step.status.forward" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| jobName | string | Yes | Human-readable job name |
| stepIndex | number | Yes | Zero-based step index |
| stepName | string | Yes | Human-readable step name |
| state | enum | Yes | One of: running, success, failed, skipped, pending, cancelled |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
| data | Record<string, unknown> | No | Optional state-specific data |
| secretsAccessed | string[] | No | Secret key names accessed by this step. Forwarded from agent for dashboard display |
| concurrencyKind | enum | No | Step concurrency role: sequential, parallel-child, parallel-group; absent means an ordinary sequential step |
| groupId | string | No | Parallel-group correlation ID shared by a group’s children (e.g., g0) |
Authoritative source:
packages/engine/src/protocol/messages/execution-status.ts—stepStatusForwardSchema
job.status.forward
Section titled “job.status.forward”Per-job status forwarded from orchestrator upstream in real-time. Enables live job-level progress in the dashboard.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "job.status.forward" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runId | string | Yes | Execution run ID |
| jobId | string | Yes | Job ID within the run |
| jobName | string | Yes | Human-readable job name |
| status | enum | Yes | One of: pending, queued, running, recovering, cancelling, success, failed, cancelled, skipped, timed_out_stale, drift_dropped, unroutable |
| matrixValues | Record<string, unknown> | No | Matrix parameter values for this job instance |
| startedAt | number | No | Unix timestamp when job started |
| completedAt | number | No | Unix timestamp when job completed |
| durationMs | number | No | Job duration in milliseconds |
| errorMessage | string or null | No | Error message if job failed |
| agentId | string or null | No | Agent ID executing this job |
| orchestratorId | string or null | No | Orchestrator ID that dispatched this job |
| runsOnLabels | string[] | No | Labels used for agent routing |
| contexts | string[] | No | Ordered bound context names for this job (multi-context jobs) |
| logBytes | number | No | Total raw log bytes accumulated across all steps of this job; only set on terminal job states |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
| initFailure | object | No | Structured init-failure signal (scope, category, message, optional jobName) — set for synthetic rejected / init-failed jobs |
| jobKind | enum | No | What this job is, orthogonal to its status: standard (ordinary job), gate (an invoke gate), proxy (a per-run child mirroring one summoned run). Absent reads as standard, so an older peer that ignores it is unaffected |
| summonedRunId | string | No | For a proxy job, the id of the summoned run it mirrors |
Authoritative source:
packages/engine/src/protocol/messages/execution-status.ts—jobStatusForwardSchema
state.replay
Section titled “state.replay”State replay sent on orchestrator reconnection. Contains a full snapshot of all active runs and their jobs so the upstream tier can reconstruct the current state without requiring the orchestrator to resend individual status messages.
| Field | Type | Required | Description |
|---|---|---|---|
| type | "state.replay" | Yes | Message discriminator |
| messageId | string | Yes | Unique message ID |
| runs | RunSnapshot[] | Yes | Array of active run snapshots |
| timestamp | number | Yes | Unix timestamp (milliseconds) |
Each RunSnapshot:
| Field | Type | Required | Description |
|---|---|---|---|
| runId | string | Yes | Execution run ID |
| workflowName | string | Yes | Workflow name |
| status | enum | Yes | One of: pending, running, success, failed, cancelled, cancelling |
| routingKey | string | No | Provider routing key |
| repoIdentifier | string | No | Repository identifier (e.g., owner/repo) |
| workflowRepoIdentifier | string | No | Repository that DEFINES the workflow, when that is not repoIdentifier |
| sha | string | No | Commit SHA |
| ref | string | No | Git branch or tag |
| triggerEvent | string | No | Trigger event type |
| commitMessage | string | No | First line of the commit message |
| jobCount | number | Yes | Total number of jobs in this run |
| startedAt | number | Yes | Unix timestamp when run started |
| completedAt | number | No | Unix timestamp when run completed |
| durationMs | number | No | Run duration in milliseconds |
| parentRunId | string or null | No | Parent run ID for re-run lineage |
| originalRunId | string or null | No | Root ancestor run ID (first run in the chain) |
| triggeredBy | string or null | No | User identity that triggered this re-run |
| failureReason | string | No | Human-readable reason why the run failed (only present for failed runs) |
| jobs | JobSnapshot[] | Yes | Array of job snapshots within the run |
Each JobSnapshot:
| Field | Type | Required | Description |
|---|---|---|---|
| jobId | string | Yes | Job ID |
| jobName | string | Yes | Human-readable job name |
| status | string | Yes | Job status |
| startedAt | number | No | Unix timestamp job started |
| completedAt | number | No | Unix timestamp job ended |
| durationMs | number | No | Job duration (ms) |
| errorMessage | string or null | No | Error message if job failed |
| agentId | string or null | No | Agent ID executing this job |
| runsOnLabels | string[] | No | Labels used for agent routing |
Authoritative source:
packages/engine/src/protocol/messages/execution-status.ts—stateReplaySchema
See also
Section titled “See also”- Dashboard, metrics & wire format — concurrency, agent metrics, agent authentication, agent private API, join, peer-to-peer, the test-relay control plane, and wire-format messages
- Protocol overview — message flow diagram, common envelopes (
heartbeat,ack,nack,error), and authentication messages