HIPsHanzo Proposals
Back to HIPs
HIP-1230FinalStandards TrackInfrastructure

Platform — The Container Plane

Hanzo AI
Created: 2026-08-20
Requires: HIP-0026, HIP-0027, HIP-0106, HIP-0135, HIP-0139

HIP-1230: Platform — The Container Plane

Abstract

platform is the per-org container platform: projects, applications, builds, deploys, environments, releases, logs and verified custom domains, each application reconciled as an operator Service CR into the caller's own tenant-<org> Kubernetes namespace (apps/platform/platform.go:1-27). It is implemented in hanzoai/cloud at apps/platform. This HIP states the target surface — one address, /v1/platform — and carries two pieces previously specified apart: the forge push endpoint (formerly HIP-1070) and the pipelines board (formerly HIP-1071), both platform addresses because their implementation is this package.

Motivation

The capability's routes grew at eight top-level addresses while its store, its process and its name were one: a generated client offered a BuildsApi, a RunnerApi and a PlatformApi for one subsystem, and one root carried a name the grammar refuses outright (git-webhook). One store is one capability however many nouns it answers for (HIP-0139 §7.1).

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

One store

The capability owns one store: the system-namespace platform SQLite database (apps/platform/store.go:131), holding project, application, build, deployment, environment and release rows. Tenancy is the org column on every table, enforced in every WHERE clause; the store exposes no method that reads a row without the org (apps/platform/store.go:22-28).

The addresses

Every route is under /v1/platform: apps, builds, cd, ci, environments, fleet, health, hook, pipelines, projects, releases, run and runner. /v1/platform/ci answers 501 naming what is missing — an address the fleet publishes and routes nowhere is the defect the manifest exists to prevent (manifest/apps.go:140-145). The router today still serves seven of these families at the root (/v1/builds, /v1/environments, /v1/git-webhook, /v1/pipelines, /v1/releases, /v1/run, /v1/runner); each pair is a line in cloud's openapi/misfiled.txt and folds here, with git-webhook — a hyphenated compound — renamed to hook in the same move.

Operations are typed zip operations except the push endpoint, which cannot be: its authentication is an HMAC over the raw bytes checked before the payload is parsed, and a typed operation decodes first (apps/platform/hook.go:19-22).

The push endpoint

POST /v1/platform/hook is where the forge delivers a push. The receiver MUST live in this process because the deploy trigger has exactly one registrant, and it is platform's: the endpoint once lived in git's process, where that registrant is nil, so every delivery was signed, accepted, answered 204 and built nothing (apps/platform/hook.go:1-17).

The forge holds no platform session, so the address is public at the identity layer and authenticated by the HMAC signature; the bytes verified MUST be the bytes acted on, and the event kind is taken from the payload, never a header. Four bounds keep an unauthenticated endpoint from being a lever: an encoded body is refused 415 before it is touched, a body cap bounds what is hashed, the verifying secret refreshes on a bounded window from KMS (HIP-0027), and that key-store read times out below the forge's delivery timeout. An unreadable secret answers 503, not 401 — the fault is ours. Duplicates are remembered by fact (namespace, repository, ref, commit) for a bounded window, and a failed dispatch gives the memory back. The forge half is one forge-wide system webhook signed with the value at the configured KMS reference; a repository opts in by having an application that tracks it, not by owning a hook.

The pipelines board

GET /v1/platform/pipelines is a projection, not a record: one application's build and deploy configuration joined with its latest run (apps/platform/console.go:227). The surface MUST be read-only — a pipeline comes into existence only through the one existing write path — and where an application has a latest deployment, that deployment's status replaces the configuration's. The same rule governs the sibling boards it sits beside: an environment is a scope derived from the applications that target it, a build row is a real record or an honest empty, a release is a deployment that was actually applied.

Tenancy

The org is the gateway-minted, IAM-validated claim (HIP-0026), read as c.Org(); the deploy namespace is derived from it as tenant-<org>, never taken from the request, and cross-tenant identifiers are structurally not inputs to any handler (apps/platform/platform.go:20-26). Console reads additionally require the identity to carry a user, refusing the one forgeable path — a caller reaching a pod directly with an asserted org and no bearer. Two endpoints differ by audience: /v1/platform/fleet is the operator's drift board, admitted only for a SuperAdmin or an org-confined OrgAdmin (apps/platform/fleet.go:31-34, HIP-0135), and the push endpoint authenticates by signature as above. POST /v1/platform/runner — the privileged build trigger hanzo build and the push hook call — is gated by a constant-time shared token plus an image-ref allowlist confined to the registries we own (apps/platform/runner.go:1-13).

Metered

The capability is metered (plugin/platform/main.go:21), and every debit lands in integer cents through the shared cloud.ResourceMeter on the caller's org ledger:

  • build minutes — wall-clock from build creation to observed Job completion, once per completed build, at CLOUD_BUILD_MINUTE_CENTS (policy default $1.00/min; 0 makes builds free) (apps/platform/buildmeter.go:1-20);
  • running compute — each tick charges every live app's org for the span since its compare-and-set watermark, at the app's SBOM compute rate, so a double-tick or restart never double-charges (apps/platform/computemeter.go:1-30);
  • the run fee — a flat gate-then-meter-on-success debit per /v1/platform/run deploy at CLOUD_PLATFORM_RUN_FEE_CENTS (apps/platform/run.go:36).

Events, observability, stage

The capability publishes no events on the bus, so a customer's webhooks receive nothing from it. It emits lifecycle facts — push.landed, build.started, deploy.live, deploy.failed (build.go:582-585) — on the in-process stream (apps/platform/hook.go:628, apps/platform/deploy.go:290, apps/platform/applylive.go:94), fanned to registered reactors: the deploy subscriber, mirror-out, and chat notification. Beyond the request span it emits structured log lines only (apps/platform/run.go:227). Its stage is ga: the manifest row carries no stage field, and absent means ga (HIP-0139 §8).

Upstreams

The capability forks nothing. It links go-git/go-git v5 (Apache-2.0) to read pushed repositories in memory, k8s.io/apimachinery (Apache-2.0) to write operator CRs, and Masterminds/sprig v3 (MIT) for template functions. Builds execute in an in-cluster BuildKit (Apache-2.0) Job launched by image; nothing of BuildKit is linked into the binary.

Rationale

The alternative to carrying the hook and the board in this HIP is two more specification files for two things that are single routes of this package — one capability sliced into three specs, the inversion of the defect HIP-0139 §6 names.

Security Considerations

The wrong implementation hands an attacker the build plane. An unsigned or parse-before-verify hook lets an unauthenticated caller mint builds in the process that owns deploys; an encoded body accepted at that endpoint buys megabytes of allocation for kilobytes on the wire. A deploy namespace taken from the request is a cross-tenant deploy; it is derived from the validated org instead. A leaked runner token without the image-ref allowlist pushes to an arbitrary registry; with it, only to ours. The fleet board without its admin guard is a fleet-wide rollout lever exposed to every tenant.

References

  • HIP-0026 — Identity and Access Management
  • HIP-0027 — Secrets Management
  • HIP-0106 — The Hanzo Plugin Contract
  • HIP-0135 — What Is Public
  • HIP-0139 — Capability

Copyright

Released under CC0 1.0 Universal Public Domain Dedication.