10 KiB
deployctl protocol v1 — read-only foundation
Supported commands
The runner supports version, plan, verify-plan, inspect, preflight, plan-environment, verify-repository, verify-artifacts, verify-package
and check-package.
It does not connect to a remote server, invoke Docker, install software, change system
configuration, deploy applications, or restore data. apply, upgrade and restore
fail closed. verify-repository and verify-artifacts create private temporary snapshots and invoke
local Linux GnuPG; it does not modify APT or persistent keyrings.
Build and try
For staged official Docker metadata authentication, see
verify-repository contract. Its output is not
accepted as execution authorization by plan-environment.
Requires Go 1.26 or newer. No third-party Go dependencies.
go test ./... -count=1
go vet ./...
go build -o dist/deployctl.exe ./cmd/deployctl
Get-Content -Raw protocol/examples/plan-request.json | ./dist/deployctl.exe plan
node --test tests/cli-smoke.test.mjs
go build -o dist/deployctl ./cmd/deployctl
./dist/deployctl plan < protocol/examples/plan-request.json
The example hashes are synthetic, not deployable package/image references.
Input
plan, verify-plan, verify-package, check-package, plan-environment, verify-repository and verify-artifacts receive one UTF-8 JSON value through
stdin, maximum 1 MiB. version, inspect and preflight require no stdin request.
Unknown fields, case aliases, duplicate keys, missing fields, nulls, additional
JSON values, excessive nesting and malformed types are rejected.
Errors go to stderr without echoing input; exit status is nonzero. stdout only
contains a JSON response on success (an output transport failure can truncate it).
plan requires exactly the fields in examples/plan-request.json:
- protocolVersion: integer 1.
- hostId, instanceId, appId: lower-case ASCII identifiers, first character a-z, remaining characters a-z, 0-9 or hyphen; 1–48 characters.
- packageDigest, imageDigest, observedStateDigest:
sha256:and 64 lowercase hex characters. - domain: explicit lowercase ASCII DNS name, at least two labels, not an IP, wildcard, URL, port or trailing-dot name. IDNs must already be in ASCII form.
imageDigest is a preliminary single-image binding. The deployable package
contract will bind all component images (including private databases); this
foundation does not claim to represent a complete multi-container release.
Plan and verification
plan returns protocolVersion, mode=offline-preview, executable=false and plan.
The plan contains intent, projectName, dataPath, createdAt, expiresAt and hash.
Times are UTC RFC3339 with second precision; the validity window is 15 minutes.
Project name is sd-<instanceId>, data path is
/var/lib/server-deploy/instances/<instanceId>. The path is only a preview;
symlink-safe filesystem access is a separate executor responsibility.
Hash is SHA-256 over Go JSON encoding of Plan with an empty hash field, in its declared field order. The CLI is the canonical plan producer; clients transport the returned plan without rebuilding its hash. This is content binding, NOT a signature, authorization, approval token or package trust check.
verify-plan receives exactly { "plan": <returned plan>, "current": <intent> }.
It rejects changed identities, state digests, derived resources, hashes, future
creation times and expiry (including the exact expiration timestamp).
Success returns protocolVersion, mode, valid=true and executable=false.
Host identity and state digest are caller-supplied offline inputs. Verification does NOT prove anything about a real server. Future apply must independently inspect the host under the host lock, resolve the entire trusted package and revalidate it; offline preview is never permission to deploy.
Local prerequisite inspection
inspect takes no stdin request and examines the machine where deployctl runs.
It reports OS/architecture, the presence of /run/systemd/system, and an executable
regular Docker client at /usr/bin/docker or /usr/local/bin/docker. It does not
run that executable or read Docker configuration/credentials. Access failures
appear as unknown, not missing. Non-Linux targets do not probe Linux paths.
supportedPlatform only describes the initial Linux amd64/arm64 runtime family,
not distro certification. deploymentReady is always false in this foundation.
Daemon connectivity, Compose version, host identity, distro acceptance, permissions,
disk space, ports, DNS, firewall and gateway validation are listed as unchecked.
This report is not the authoritative observed state required by a future apply.
Environment proposal
preflight adds local Ubuntu release, effective privilege, Linux /var/lib
filesystem available bytes and conservative existing-resource observations.
Output includes protocolVersion=1, mode=local-environment-proposal, observedAt,
report and proposal. It does not accept caller-supplied observations over stdin.
The proposal reports blockers, candidate changes and impacts; executable is always false. Existing/unknown resources are never silently adopted or removed. Candidate steps, when present, are not an executable installation plan: package inventory, version locks, repository trust, host identity and network checks remain unresolved. Exit 0 means the report was produced, not that the host is ready. See the preflight contract for exact boundaries.
Version-locked environment draft
plan-environment receives { "lock": <Docker package lock> }. See the
lock contract for all required fields.
Local observations are collected by the CLI, not supplied by the caller.
Output includes protocolVersion=1, mode=local-environment-draft, observedAt,
report and draft. The draft includes requestedPackages, lockDigest,
observationDigest, impacts, blockers, executable=false and
repositoryAuthenticated=false. No shell command, download or installation runs.
Preflight now includes a local dpkg inventory observation. Relevant existing or residual packages prevent fresh-install candidates; unknown status/journal cannot be treated as absence. The lock is only syntactically pinned to an allowed source: repository signature, signed metadata chain, package bytes, freshness and dependency resolution remain blockers. This is not a complete or authorized APT transaction.
Local application package verification
verify-package reads { "directory": "<absolute package directory>", "expectedDigest": "sha256:<64 lowercase hex>" } from stdin.
The expected digest pins the exact manifest.json bytes; the manifest pins every
payload file and every component's image reference. Obtain the expected digest
through a trusted channel, not from the untrusted package being checked.
The output includes the verified manifest, manifest digest, payload file count, verified=true, executable=false and publisherAuthenticated=false. File contents are never echoed. Integrity is not publisher authentication, Compose-policy validation, container-image availability, backup compatibility or deployment safety.
Packages must be immutable, trusted staging directories during verification. The internal verifier returns the verified payload snapshot for future consumers; execution must not reopen mutable source files after a check. No package extraction, download, signature trust-store management or image pull is implemented yet.
Manifest contract
Every field is required; unknown fields are rejected:
protocolVersion: 1;runtime:compose.appId: the same lowercase identifier rules as the offline intent.version: three numeric components separated by dots (no prerelease suffix).entrypoint: the path of one declared payload file.platforms: a nonempty unique subset oflinux/amd64,linux/arm64.components: 1–32 unique{name, image}entries. Names use identifier rules; images use lowercase repository paths followed by@sha256:<64 lowercase hex>. This initial lexical subset excludes tags and registry ports; it is not a complete Docker reference parser or a check against the Compose services.files: 1–128 unique{path, digest}entries. Digests pin payload bytes;manifest.jsonitself must not appear in this list.
Paths are lowercase portable relative slash paths, at most 240 bytes overall and
100 bytes per segment. Segments begin with ASCII alphanumeric characters and use
only ASCII alphanumeric, dot, underscore or hyphen. Dotfiles, traversal, backslashes,
colons, Windows device names and trailing dots are rejected. The directory must
contain only manifest.json, listed payload files and their required parents.
Limits are 1 MiB for the manifest, 4 MiB per payload and 16 MiB for total payloads.
Symlinks and special files are rejected. See internal/appbundle/README.md for
filesystem trust boundaries and Linux-specific rejection tests.
Restricted Compose policy check
check-package accepts the same request as verify-package. It performs the
complete integrity check first, then validates the returned entrypoint bytes
against isolated-compose-v1. No files are reopened and no Docker command runs.
Success returns protocolVersion=1, policyPassed=true, profile, the manifest digest,
executable=false and publisherAuthenticated=false. Failure returns no JSON result
and a fixed redacted diagnostic. verify-package remains integrity-only; a package
may pass that command while failing check-package.
The initial profile accepts only strict JSON, pinned manifest-matching services, explicit non-root identities, read-only roots, dropped capabilities, no new privileges, a private internal backend network and plain project-local named volumes. Every field in the profile is required; other fields are rejected. See exact shape and limitations.
This is not a general YAML validator or completed application adapter. Traefik routes, environment/secrets, application-specific permissions, Docker version compatibility, volume ownership and resource limits are not implemented here. Passing never establishes that an application can start or its data can be safely upgraded/restored. Future execution still requires locked host/resource checks, trusted images, a fixed Compose invocation and exact snapshot binding.