Airframe has a front end an agent can use cold. It has a schema, a contract that says what every kind returns and how it fails, a validator, a verifier, and a gateway that decides what an agent may touch. Glidepath has none of that. What it has is about sixty routes inside Tower's backend that answer every CI/CD question and take every CI/CD action, for Tower alone. This spec moves that surface into Glidepath, builds it the way Airframe is built, and lets Tower and agents share it.
cicd.yaml, promoting, rolling back and pinning all open a pull request. Only short-lived things, such as a re-run or a preview environment, are API calls.cicd.yaml by pull request, limited by field. The whole-file block goes. Each field is open, tighten only or human only, and both Clearance and CI enforce that.glidepath-control-plane and has no Hangar dependency. Tower and Clearance are only clients.Airframe's API is its schema. Twenty kinds in catalog.hangar.io/v1alpha1, a versioning policy, and a
sidecar per kind that lists what the kind returns, the conditions it reports with a fixed set of reasons, how to verify it
and how it is known to fail. All of that is generated into one contract file. An agent asks airframe-capabilities
what it can do, runs airframe-validate before it opens a pull request, and runs airframe-verify
afterwards. Clearance decides which of those writes the agent may make at all.
Glidepath has the schema half: cicd.yaml and cicd.schema.json. Everything an agent needs at
run time is spread over six stores: PipelineRuns, Tekton Results, the release records, Testkube, Chains and Rekor, and
GitHub. The only thing that joins them is Tower's backend: glidepathProvenance.ts alone is 2,994 lines with
60 routes, and four browser hooks read Tekton directly through the Kubernetes proxy. Some of that is CI/CD logic, such as
working out a flow's chain slug or which releases a rollback may target, and it lives in a Backstage plugin. Clearance
offers agents exactly one CI/CD tool, pipeline.rerun.
That has three costs. An agent can't ask Glidepath why a flow failed. Glidepath, which has to stay installable on its own, can't be operated without Tower. And every rule about who may promote or roll back is written in Tower's code, not in Glidepath's.
A new sidecar, schemas/cicd.meta.yaml, does for cicd.yaml what xrds/<kind>.meta.yaml
does for an XRD. A generator folds it, the schema, the gate registry and Glidepath's state vocabularies into
contract/glidepath-contract.json. CI runs the generator with --check, the same way Airframe's does.
| Section | What it says | Where it comes from |
|---|---|---|
| fields | Every cicd.yaml field with its type, description, owner (team or admin), risk and agent scope: open, tighten or human | cicd.schema.json + cicd.meta.yaml |
| gates | Each governance gate: what it verifies, whether it is required, which status check it posts | the gate registry |
| states | Fixed sets of stage, gate and release states with their meanings. The release set already exists: proposed, merged, progressing, healthy, superseded, rolled-back | ADR-0021, ADR-0022 |
| knownFailures | Stable ids with a match and a hint, e.g. SourceVolumeFull (raise build.sourceVolume.size), ProvenanceSignerNotAllowed, ItsmChangeNotApproved, BuildKitOOM | known-gaps.md and incidents, curated |
| actions | Every API action with its tier, arguments, effects and dry-run output | the OpenAPI spec |
glidepath validate checks a cicd.yaml in one pass: the schema, then GP-* rules for what a schema
can't say (an environment named after a stage, a Flight environment with no release stage). With --base <ref>
it also checks the GP-SCOPE rules on what changed, so the field scope below holds in CI even for a pull request that never
went through Clearance. Glidepath gets an AGENTS.md and an llms.txt, as Airframe has.
One service, glidepath-api, deployed by glidepath-control-plane on the dev cluster where the
pipelines already run. Its nouns use identities Glidepath already has: a flow is one chain-id (and one
Tempo trace), a release is one releaseId (<chain-id>:<cluster>/<env>), and
an image is a digest. Custom verbs follow the resource:verb style.
| Call | Tier | What it does |
|---|---|---|
GET /v1/contract | T0 | The contract, so a client never hard-codes a field or a state |
GET /v1/apps/{app}/flows | T0 | Flows by state and time, live and archived (Results) |
GET /v1/flows/{chainId} | T0 | Stages, gates, runs, the trace link |
GET /v1/runs/{run}/logs | T0 | Step logs, live or archived |
GET /v1/flows/{chainId}:explain | T0 | The failing step classified against knownFailures, with the hint and the field to change |
GET /v1/apps/{app}/releases, /v1/releases/{releaseId} | T0 | Release state, PR, change request, rollback eligibility |
GET /v1/images/{digest}/evidence | T0 | Provenance, SBOM, test evidence, signer |
POST /v1/apps/{app}/cicd:validate | T0 | glidepath validate as a call |
POST /v1/apps/{app}/cicd:propose | T1 | Opens a pull request to cicd.yaml, after the scope check |
POST /v1/runs/{run}:rerun, :cancel | T1 | What Tower's Re-run and Cancel do today |
POST /v1/apps/{app}/builds | T1 | Builds a branch |
POST|DELETE /v1/apps/{app}/previews | T1 | Creates, extends or deletes an ephemeral environment |
POST /v1/apps/{app}/environments/{env}:promote | T1 Ground · T2 Flight | Ground: the deploy commit. Flight: runs the app's release pipeline, which opens the gitops PR |
POST …/environments/{env}:rollback, :pin | T2 | A release of an eligible earlier image, or a cloud pin PR |
GET /v1/events | T0 | The CDEvents stream, filtered by app (server-sent events; AMQP topics for Autopilot) |
Pipelines and agents present a ServiceAccount token, checked with TokenReview the way the broker checks them (ADR-0002). People present an OIDC token. Tower presents tower-actor's minted token and names the user it acts for; only that subject may name someone else.
Each call is a SubjectAccessReview against app-<name>-cicd on a virtual resource (glidepath.hangar.io flows, releases, environments) with verbs such as rerun or promote. glidepath-app renders the RoleBindings from the app's owners. No second permission model.
Every write takes dryRun and an idempotency key, and records an audit event with the caller, who it acts for, the tier and the decision. A denial returns a rule id and a hint, in the shape Clearance's decisions already use.
Agents don't get a second MCP server. Clearance registers glidepath.* tools that call the API and keeps doing
what it does for every tool: profile limits, CEL rules and its hash-chained audit. The API checks permissions again itself,
so a tool that bypasses Clearance is refused there too.
One rule is specific to CI/CD: an agent never satisfies a gate on its own change. A Flight change is always a pull request; the human who merges it is also who satisfies the GitHub-merge provenance check (ADR-0027). The signing policy is admin-owned and never reachable from an agent.
| New Clearance tool | Tier | Calls |
|---|---|---|
glidepath.flows.list, .flow.get, .logs.get, .explain | T0 | flows, runs, logs, explain |
glidepath.releases.get, .evidence.get, .contract | T0 | releases, evidence, contract |
glidepath.run.rerun, .run.cancel, .build, .preview | T1 | replaces today's pipeline.rerun |
glidepath.cicd.propose | T1 | cicd:propose, after the field-scope check |
glidepath.promote, .rollback, .pin | T2 | propose only; T2 tools aren't capped by the tier ceiling |
Today Clearance's BASELINE_DENY_PATHS denies cicd.yaml for every agent. That keeps agents out
of their own controls, but it also stops an agent from turning on a build cache, adding a Ground environment or fixing the
source volume size it was just told is too small. Even Autopilot's own planner patches cicd.yaml. The
file-level deny goes. In its place is the field-level scope Clearance already uses for values files (AF-9a), with each
field's class read from the contract.
| Change | Where |
|---|---|
Drop cicd.yaml and **/cicd.yaml from BASELINE_DENY_PATHS; keep .tekton/** | autopilot profile.py |
Make the baseline field denies file-qualified (cicd.yaml#/secrets), since /release means something else in a values file | autopilot profile.py, scope.py |
| Generate the human-only list from the contract instead of hard-coding it; tighten-only fields need a before and after comparison, not a glob | autopilot, from glidepath-contract.json |
| GP-SCOPE-001 human-only field changed, GP-SCOPE-002 a gate loosened, GP-SCOPE-003 a Flight environment added or retiered | glidepath glidepath validate --base, in each app's PR checks |
| Agent PRs need a human approval and a human merge | the app repo's ruleset, unchanged |
The rule the old deny protected still holds: an agent never edits a control that constrains it. Secrets, signers, cloud deploy identity and the gates are those controls. A build flag isn't.
Tower keeps every route, every screen and every URL. What changes is where its backend gets each answer. Tower's
backend plugin glidepath (/api/glidepath/*) gains one client for glidepath-api, and
each group of routes gets a switch in config:
direct: today's code path, unchanged. The default for every group.
shadow: Tower serves the direct answer, calls the API in the background and records any difference as a metric (tower_glidepath_api_shadow_mismatch_total{group}) and a log line. Writes are never run twice. A shadowed write calls the API with dryRun and compares the plan with what Tower is about to do. This is the same live and shadow approach glidepath-relay used for ADR-0021.
api: the route calls the API and the direct code becomes the fallback. Going back is a config change.
Until P6 (Oct 30, 2026). P6 removed the switches, shadow mode and its mismatch metric from Tower's backend (backstage#215); only tower_glidepath_api_request_duration{group, op, outcome} remains. Every group now calls the API, and there is no direct code left to fall back to: an API error or timeout goes back to the person, a 4xx with its own status and anything else as a 502. InfraServices (infra-<name>-cicd) re-run, cancel, promote and roll back through the API too.
| Group | Tower today | After | Phase |
|---|---|---|---|
| Runs and logs | /pipeline-history, /runs, /run, /log; browser hooks useTektonPipelineRuns, useTaskRunLogs, useFleetPipelineRuns, useFleetPipelineHistory through the proxy; chain slugs worked out in chainSlug.ts | flows, runs and logs from the API. Hooks call Tower's backend, not the proxy. The chain slug comes with the flow. Since P6 the hooks read live runs and step logs through new API reads (/v1/apps/{app}/live, /v1/live, /v1/runs); Tower's own Results cache is gone | P1 shadow · P2 api · P6 hooks |
| Supply chain and tests | /provenance, /head, /images, /tests, /tests/report (Rekor, GHCR, Testkube through the service proxy) | /v1/images/{digest}/evidence, tests and test reports from the API. Since P6 Tower verifies no provenance itself. /head and /images stay Tower's own (GitHub) | P1 shadow · P2 api · P6 |
| Release state | /release-records and the record reads behind /release-run and /deploy-history (release-tracking ConfigMaps, read as the ingestor) | /v1/apps/{app}/releases | P1 shadow · P2 api |
| Run control | /tekton/rerun, /tekton/cancel as tower-actor, held to shape by the tower-pipelinerun-shape policy | runs:rerun, :cancel. Since P6 tower-actor can't write a PipelineRun at all | P3 · P6 |
| cicd.yaml editor | /config/cicd, /config/cicd/schema: Tower checks the patch and opens the PR | cicd:validate, cicd:propose; the editor shows each field's owner and scope from the contract | P3 |
| Promote, roll back, pin | /promote, /release/rollback-plan, /release/rollback, /pin; eligibility worked out in Tower | environments:promote, :rollback, :pin; eligibility from the API. A Ground promotion stays Tower's own, a direct commit to the gitops repo, because the API refuses Ground targets (GP-API-422) | P4 · P6 |
Argo CD actions (/argo/*, a delegated system). Config and values (/config, flight-base, env-xr, components, chart, platform-env: Airframe's contract). Break-glass, the cluster registry, pull requests, agent definitions.
The Release Record and the release flow (/release-record*, /release-draft, /release-mode, /release-planned, /release-calendar, /release-signoff). They read release state from the API but are written by Tower. Image prune stays and reads "in use" from the API later.
tower-actor's PipelineRun create and patch binding on the dev cluster (backstage-tekton-rerun), the reader's Rekor, Testkube and tekton-results-relay service-proxy grants, Tower's Results cache, its own provenance checks and rollback plan, and the four proxy hooks. The tower-pipelinerun-shape policy became glidepath-api-pipelinerun-shape: it refuses any PipelineRun write from tower-actor and holds glidepath-api to Glidepath's run shape. Tower writes nothing on the dev cluster itself except break-glass; its CI/CD actions are API calls as tower-actor on the person's behalf.
Still not through the API after P6. The Ops Wall's release records and release Events, which Tower reads through the Kubernetes proxy (useFleetReleases.ts). Argo CD, Rollouts, pod and Crossplane reads also go through the proxy; they aren't CI/CD and were never in scope. Also still Tower's own: a Ground promotion, the Release notification for a Flight promotion, following a rollback run to its PR, /head and /images, and break-glass.
| Risk | Answer |
|---|---|
| The API is down | Until P6 every group fell back to direct. Since P6 (Oct 30, 2026) there is no fallback: Tower's CI/CD views depend on the API, the way they depended on Tekton Results, and an API error reaches the person as an error. The API runs two replicas by default. |
| The two authorization checks disagree | Tower's owner check (towerAuthz.ts) keeps running in front of the API. Shadow mode also compares the API's decision with Tower's, and a mismatch blocks the cut-over for that group. |
| An extra hop is slower | The Results cache Tower built in pipelineHistory.ts moves into the API: a finished run never changes. Shadow mode records how long each side takes. Since P6 Tower's cache is gone and tower_glidepath_api_request_duration times each call. |
| Two copies drift during the move | Each group is shadowed and then cut over on its own. Only groups that are switched to api are frozen in Tower; the rest keep shipping as usual. |
cicd.meta.yaml, the contract generator, glidepath validate with GP-* and GP-SCOPE rules, AGENTS.md and llms.txt. Clearance swaps the cicd.yaml file deny for field scope from the contract. No service yet. Done when: an agent PR that turns on build.cache passes, and one that turns off governance.sast or touches secrets is refused by Clearance and fails CI.
glidepath-api with flows, runs, logs, releases and evidence, its OpenAPI spec, authentication and SubjectAccessReview. Tower's runs, supply chain and release-state groups go to shadow. Done when: seven days with no shadow mismatches on every app on the dev cluster.
Those three groups switch to api. Clearance gets the T0 glidepath.* tools. explain ships with the first knownFailures. Done when: an agent starting cold names the right failure id for a seeded failed flow, using only MCP.
Re-run, cancel, build a branch, previews, cicd:propose. Tower's run-control and cicd-editor groups go through dry-run shadow, then api. pipeline.rerun is retired. Done when: a re-run from Tower and one from an agent each create exactly one PipelineRun, and each is audited with both identities.
Promote, roll back and pin, each opened by Glidepath's release pipeline as today. Tower's promote group moves. Done when: an agent proposes a staging promotion, a human merges it, the release record reaches healthy, and the agent had no way to merge it.
/v1/events and AMQP topics. An Autopilot event agent triages a failed flow and opens a cicd.yaml or code PR. Done when: a seeded failure produces a correct triage PR without anyone stepping in.
Remove Tower's direct paths, the proxy hooks, tower-actor's Tekton bindings and the shape policy. Done when: kubectl auth can-i shows Tower's identities can't create or patch a PipelineRun. Met Oct 30, 2026: kubectl auth can-i create|patch pipelineruns answers no for hangar:hangar-prod:tower-actor and hangar:hangar-prod:backstage-ingestor, in a namespace and with -A, and yes for glidepath-api's ServiceAccount. Shipped as P6a, the backend's direct paths (backstage#215); P6b, the hooks on new live reads (glidepath#188, backstage#216, tower v0.107.0); and P6c, the cluster grants and the policy (apron#108, gitops-hangar-dev#72).
Which GitHub identity opens an agent's pull request. This only matters for the content an agent writes, which is
cicd.yaml and code. Promote, rollback and pin PRs are opened by Glidepath's release pipeline, as they are today.
| Option | Verdict | Why |
|---|---|---|
| A separate GitHub App for agents | Leaning | The PR author shows it was an agent. It is installed only on the repos it needs, with contents and pull requests and never checks. Each call mints a one-hour installation token, so nothing is stored. It isn't on any ruleset bypass list, so it can't merge. This is Autopilot's D4. |
| Glidepath's existing bot or the Pipelines-as-Code app | No | Blurs who wrote the change, and the Pipelines-as-Code app can write status checks, so an agent holding it could post a passing gate. |
| The person's own token | No | Correct attribution, but the agent inherits that person's merge rights and their token has to be held somewhere. |
tower-actor./retest on a PR keeps working, and the API is a second way to do the same thing.