Skip to content

feat: Programme B — server siblings machine surface - #69

Merged
im-tyler merged 4 commits into
mainfrom
programme-b-siblings
Nov 5, 2026
Merged

im-tyler merged 4 commits into
mainfrom
programme-b-siblings

Conversation

@im-tyler

@im-tyler im-tyler commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Programme B — Siblings (teploy-cli's slice; plan _internal/DASH_AUTOCONFIG_PLAN_2026-11-03.md section 10, owner-approved). Branch from main 6ffdb60 (v0.1.53).

What

teploy server siblings <server> --json — the read-only discovery read Dash runs before offering to connect the products that share a server. Addressed like server status/server audit (servers.yml name or raw host; global --host/--user/--key override).

Envelope (contracts/schema/siblings-envelope.schema.json, corpus rev 20): {machine_interface, server, host, observed_at, state, error?, siblings[], unknown_labeled[]}; each row {container, product, version?, app?, state, image, port_hints[], source}.

  • product — the container's dev.teploy.product label (source: label), else the image's (image_label), else the forgejo family by the image's OCI labels (org.opencontainers.image.title/source naming forgejo/forgejo or gitea → product: forgejo, source: oci_label).
  • forward-compat — an unrecognised dev.teploy.product value passes through verbatim under unknown_labeled; Dash decides what it knows.
  • version hints — dev.teploy.version, else teploy.version when the container belongs to a teploy app, else the image's org.opencontainers.image.version; app — the teploy app label; image — the ps ref with a 12-hex ID resolved to its repo tag.
  • port_hints — docker inspect's published binds (loopback/tailnet included as-is): where /.well-known/teploy.json may live. The command OBSERVES only: no DNS, no HTTP, no URL guessing, no env, nothing secret-adjacent. Credential minting / env writes need a confirmed instance_id + admin consent — consumer's surface.
  • state ok | partial | unreachable — an unlistable server or a wholesale inspect failure is partial with empty arrays, never a clean "no siblings". --json exits 0 whenever the envelope was produced.

Bounded + read-only

At most two SSH round trips: the shared batched docker ps (labels already ride along via containersFormat) plus one batched inspect (docker.InspectSiblings) fetching every container's port binds and every distinct image's labels + tags in a single command — image-label candidates get their hints without a third trip. TestServerSiblings_EveryCommandOnlyReads (after the audit pin) refuses any mutating verb/docker subcommand/stdin/upload and pins ≤ 2 commands.

Capability + contracts

  • siblings-v1 in the registry and version --json (regenerated mi2.json).
  • siblings-envelope schema derived from the DTO; fixtures generated from the real collectServerSiblings over a mock executor: mixed (one of each detection class + pass-through) and none; both validated against the schema. MANIFEST rev 20.

Commits

  1. feat(docker): batched sibling inspect (port binds + image labels, one command)
  2. feat(siblings): the command, detection fallbacks, capability token
  3. contracts: siblings-envelope, corpus rev 20
  4. docs: README sibling-discovery section, CHANGELOG, AUDIT_OPEN

Verification

go build ./..., go vet ./..., GOOS=linux go vet ./..., go test ./... all green; gofmt clean on touched files; -race on the touched packages; every intermediate commit builds and tests green. Hermetic tests cover: all detection sources and fallbacks, container-label-beats-image, version priority, port-hint extraction (dual-stack, loopback, tailnet, udp, exposed-but-unpublished), unknown-product pass-through, ID-created image tag resolution, determinism, JSON round-trip, partial vs ok states, and the read-only pin. Deferred items and the forgejo substring trade-off recorded in AUDIT_OPEN.md.

…one command

InspectSiblings fetches, in one remote command, every named container's
published port binds (docker inspect's NetworkSettings.Ports: host_ip,
host_port, container_port, proto; loopback/tailnet binds kept as-is,
exposed-but-unpublished ports yield nothing) and every named image's config
labels + repo tags, for the Programme B siblings command. Both halves are
read-only inspects. Objects docker cannot find are skipped (the
ResolveImageTags tolerance); containers with no binds still get their key,
so 'inspect answered' stays distinguishable from 'inspect never mentioned
it' — the caller's wholesale-failure tripwire depends on it. ID-form refs
(docker ps's 12-hex) resolve to the image's facts by prefix, name/tag refs
by their own RepoTags entry. Port binds are sorted deterministically (the
port map is a Go map on the wire).
…y products and forgejo (siblings-v1)

`teploy server siblings <server> [--json]`, addressed like server
status/audit (servers.yml name or host, --host/--user/--key override).
Programme B's discovery read, plan section 10: candidates from what the
containers and their images SAY, never from probing.

- product: the container's dev.teploy.product label (source label), else
  the image's (image_label), else — for the forgejo family, whose upstream
  images carry no teploy label — the image's org.opencontainers.image.
  title/source naming forgejo/forgejo or gitea (oci_label). Substring
  matching is the rule there on purpose: it is a candidate for the consumer
  to probe (forgejo answers /api/v1/version), never a confirmed identity.
- a dev.teploy.product value this CLI does not recognise passes through
  verbatim under unknown_labeled — a product newer than the CLI stays
  visible; Dash decides what it knows.
- version hints: dev.teploy.version, else the teploy release label when the
  container belongs to a teploy app, else the image's
  org.opencontainers.image.version. app: the teploy app label. image: the
  ps ref, with a 12-hex ID resolved to its first repo tag.
- port_hints: the container's published binds — where
  /.well-known/teploy.json may live. No DNS, no HTTP, no URL guessing, no
  env, nothing secret-adjacent.

At most two remote round trips: the shared batched docker ps (labels ride
along) plus one batched inspect for binds + image labels. Read-only pinned
by test after TestServerAudit_EveryCommandOnlyReads: no mutating verb or
docker subcommand, no stdin, no uploads, <=2 commands. An unlistable server
or a wholesale inspect failure is state partial/unreachable with empty
arrays — never a clean 'no siblings'. Capability token siblings-v1 (the
regenerated mi2.json lands here with the code that added it).
Schema derived from the serverSiblingsDTO encoder; fixtures GENERATED from
the real collectServerSiblings over a mock executor (the corpus rule —
never hand-authored where a producer exists): 'mixed' carries one of each
detection class (observe by container label across web+worker, dash by
image label, forgejo by OCI labels, an unrecognised product passed through
verbatim) and 'none' a server with nothing on it. MANIFEST rev 20 and the
artifact table row; both fixtures validated against the schema.
README: a Sibling discovery section (what the labels are, what a row
carries, what deliberately is NOT there — probing, DNS, credentials — and
the state vocabulary) plus the command-list entry. CHANGELOG Unreleased.
AUDIT_OPEN: the new slice's deliberate report-only boundary, the forgejo
substring trade-off, the one-row-per-container decision, the every-binds
read, and the not-yet-live caveat.
@im-tyler
im-tyler merged commit 555aa57 into main Nov 5, 2026
1 check passed
@im-tyler
im-tyler deleted the programme-b-siblings branch November 6, 2026 00:36
@im-tyler im-tyler mentioned this pull request Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant