feat: add deployment foundation and cross-device handoff
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# 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](../internal/aptrepo/README.md). Its output is not
|
||||
accepted as execution authorization by `plan-environment`.
|
||||
|
||||
Requires Go 1.26 or newer. No third-party Go dependencies.
|
||||
|
||||
```powershell
|
||||
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
|
||||
```
|
||||
|
||||
```sh
|
||||
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](../internal/preflight/README.md) for exact boundaries.
|
||||
|
||||
### Version-locked environment draft
|
||||
|
||||
`plan-environment` receives `{ "lock": <Docker package lock> }`. See the
|
||||
[lock contract](../internal/installplan/README.md) 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 of `linux/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.json` itself 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](../internal/composepolicy/README.md).
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user