The FC Agent is a small service you run in your own environment — typically as a single pod in your Kubernetes cluster. It connects outbound to FixControl over HTTPS and executes signed, expiring, single-use operations: approve or reject a paused CI deployment, promote a paused Argo rollout, or run a validation inside your cluster.
FixControl connects to private customer infrastructure through this outbound agent. Kubernetes, Argo, Jenkins and GitLab do not need to be exposed publicly to FixControl.
You install and operate the agent yourself. The install package is a public release — plain Kubernetes manifests with a scripted installer — at github.com/FixControl-AI/fixcontrol-agent, and Settings → DevOps → fc-agents links the pinned version alongside the agent identity and the signing keys to pin, at the moment you enrol.
Architecture
FixControl Cloud
(gates, approvals, evidence, audit)
▲
│ outbound HTTPS (initiated by the agent)
│
FC Agent ← runs in your environment
/ | \
/ | \
Kubernetes Argo Jenkins / GitLab
(private — none of these are exposed to FixControl)- The agent polls FixControl on a short, server-advertised interval. There is no push channel, no listener, no inbound socket: the agent's manifest ships no Service, no Ingress and no inbound network policy, and its health endpoint binds to localhost.
- FixControl registers an operation — the verdict of a decided gate, or a request to run a validation. The agent fetches it on its next poll, verifies it locally, executes it, and reports the result back — also outbound. Every execution verdict (approve, reject, promote) is a recorded human decision.
- Under the agent connection model FixControl opens no connection toward your network and holds no credential for your infrastructure.
Prerequisites
- The FC Agent enabled for your workspace. It is off by default; your FixControl operator switches it on. With it off, no operation is registered and no agent request is accepted.
- A Kubernetes cluster (the agent runs as a Deployment; the install package is plain Kubernetes manifests rendered with Kustomize — no Helm required).
- Outbound HTTPS from the agent's namespace to FixControl (typically port 443), through your existing egress or proxy rules.
- The Kubernetes integration connected first — an agent registration is attached to that connection, and enrolment refuses without one.
- An enrolled agent identity: created under Settings → DevOps → fc-agents (or
POST /api/integrations/devops/agents). Enrolment shows the agent secret once and lists the FixControl signing keys to pin. - The capability armed: agent carriage stays off until an organization admin arms Hand the verdict to an fc-agent in your cluster under Settings → AI & approvals → Approvals → Deployment capabilities. Every deployment capability on that card is off by default. Arming asks for a reason and is audited; disarming is one click. Until it is armed, gates open and decisions are recorded, but nothing is registered for the agent.
Networking
The agent's egress, in full:
allow egress agent namespace → FixControl (HTTPS, 443)
(for the hosted service: api.fixcontrol.ai —
the enrolment panel shows the exact URL as
"FixControl URL")
agent namespace → your manifests git remote
(only with the default GitOps promotion path)
agent namespace → your private GitLab / Jenkins
(only with CI verdict carriage)
agent namespace → kube-apiserver + cluster DNS
(the installer's NetworkPolicy adds these itself)
inbound rules needed: noneThe pipeline event still reaches FixControl through the CI host's own signed webhook — the agent carries the verdict back, not the event in. So a private CI host needs outbound HTTPS to FixControl too (or a proxy that provides it).
- The agent refuses to start against a plaintext
http://FixControl URL. TLS is verified; there is deliberately no "skip TLS verification" option. - Standard egress proxies work as long as the agent can reach the FixControl origin; the operation signature covers the request path, so intermediaries cannot repoint a request undetected.
- The agent's own health/metrics endpoint binds to
127.0.0.1and is probed in-pod.
Authentication and operation signing
Two independent mechanisms, with two independent keys:
- Transport authentication — every request the agent makes carries an HMAC-SHA256 signature under the agent's own per-agent secret, plus a timestamp with a bounded skew window. The secret is shown once at enrolment, stored by FixControl only in encrypted form, and can be rotated or revoked at any time; revocation takes effect on the next request.
- Operation authority — every operation is signed by FixControl with an Ed25519 key whose public half you pin in the agent's configuration. The agent contains no signing code for this key — it can verify operations, never mint them. A pinned agent refuses operations signed any other way, so the authority to order an action and the ability to talk to the API are cryptographically separate.
Operation lifecycle
Every operation is single-use, expiring and idempotent:
- Expiry — operations expire minutes after registration (default 5). An approval decided at 10:00 can never promote something at 16:00 because an agent was down; the expired operation is recorded as expired, visibly.
- Replay protection — each operation carries a random nonce. The agent keeps a persistent nonce ledger in its own namespace and refuses a nonce it has seen, reporting the refusal back with reason
replayrather than dropping it. - Idempotent settlement — an operation settles once. A conflicting second result is recorded as a conflict and does not rewrite the first.
- Addressed, not broadcast — an operation names one cluster and one capability, and CI verdicts are additionally pinned to the specific agent named on the connection. The pin travels inside the signature.
Capabilities and local allowlists
The operation vocabulary is closed: approve/reject a CI deployment, promote/abort/read an Argo rollout, run/cancel a validation. There is no "run this command" operation.
What an agent may actually execute is decided in three places, and the last one is entirely yours:
- The registration in FixControl grants the agent a capability set.
- Every poll intersects that grant with what the running agent advertises — neither side can widen the other.
- The agent's local allowlists — the namespaces it may act in, the rollouts it may touch, and the set of capabilities it switches on — live in the agent's configuration in your cluster (the
values.envthe installer reads; the package README names each entry). FixControl never sends them and cannot override them. An empty namespace allowlist means nothing is allowed — the installer refuses an install that leaves it empty by accident.
Runner capabilities — what the machine can actually run
An agent's operations (above) say what it is permitted to do. That is a different question from what the machine it runs on is able to do, and FixControl keeps the two apart on purpose.
A runner describes itself: operating system family, processor architecture, container runtime, the language runtimes, compilers, build systems and SDKs it has, system libraries, runtime extensions, tools, any proprietary capability, hardware, and which service implementations (or arbitrary service images) it can start. That description arrives one of two ways — advertised by the running agent, or declared by you for something the agent build cannot detect. A declaration outranks an advertisement and is never overwritten by a poll.
Two rules follow, and both are refusals rather than substitutions:
- No silent substitution. GCC is not MSVC, Linux is not Windows, an open-source compiler is not a proprietary SDK, PHP 8.3 does not satisfy
>=8.4. A project whose environment needs something no runner offers is refused with the exact missing capability named, per family and per runner. - Capability and reachability are separate answers. A runner can hold every capability a project needs and still be unusable because this build has no transport to it. You are told which of the two it is — a runner that lacks a compiler and a runner FixControl cannot carry work to are different problems with different fixes.
A runner with no description matches nothing. That is deliberate: an undescribed runner would otherwise silently become the answer to every question.
Validating on your own runner
A project can have its changes validated by a runner in your cluster instead of on FixControl's infrastructure, or in addition to it. This moves execution of the build and tests into your environment; it does not change where your source is stored — FixControl still clones the repository on its own servers and sends source to its AI providers to work on it.
All of these must hold before a run goes to your runner:
- The FC Agent is enabled for your workspace and a registered agent is connected (see Prerequisites).
- The agent advertises the `test.run` capability. An agent that does not offer it is never handed a validation.
- Remote validation is allowed for the project. The workspace default is off, and your FixControl operator changes that default. Each project can decide for itself under Project → Environments → How changes are validated → Remote validation for this project: Inherit workspace setting, Allowed for this project or Not allowed for this project. The setting only appears for a strategy that has a run in your environment: Your own Kubernetes environment or Docker first, then your environment.
If one of these is missing, the run is refused with the missing piece named, rather than quietly running somewhere else.
One proof your runner cannot make: that the application still starts. Before a change is applied, FixControl normally starts the application and waits for it to answer. A customer-hosted runner cannot make that check; the run records it as unsupported. A project whose verified environment declares a start command is therefore blocked from applying changes until one of two things is true:
- the strategy is Docker first, then your environment, so the start check is made on FixControl's own lane first; or
- an admin or manager has switched on Allow changes without this proof under Proof that the application still starts on the same page. That is an explicit acceptance, recorded with their name and the date next to every approval that relies on it.
Windows, .NET Framework and WinDev
FixControl detects .NET Framework projects (net48 and friends), modern .NET projects that target Windows (a -windows target framework, such as WinForms or WPF), and WinDev projects, and plans their real build and test commands — NuGet restore, MSBuild and VSTest for .NET Framework. Modern .NET that does not need Windows runs on Linux like any other stack.
Windows builds are not verified today. A hosted Windows lane for .NET Framework and -windows targets is built, but it is not enabled by default and not yet proven: its image has never been built and no Windows task has run. Until FixControl enables it, such a project stops with a refusal that names exactly what is missing — MSBuild (Visual Studio Build Tools), nuget.exe, vstest.console — and goes no further.
Once enabled, that lane has two limits worth knowing now:
- It cannot start the application, so it cannot make the start proof described above.
- It cannot run sidecar services. A Windows task cannot carry the Linux database containers other lanes use, so a .NET Framework repository that declares a database is refused, with the service named.
WinDev is a refusal in every case. Building a WinDev project needs a licensed PC SOFT installation, which FixControl does not ship, and there is no WinDev runner. The refusal names what is missing.
This is the honest outcome and the intended one. The alternative — verifying a net48 solution with a Linux .NET SDK — would produce a green verdict about software that is not the software you ship. A refusal you can act on is worth more than a pass you cannot trust.
If Windows verification matters to you, tell your FixControl contact.
Kubernetes permissions (RBAC)
The agent runs under a minimal Role, and validation runs under a separate ServiceAccount:
- Agent —
get/liston Argo Rollouts in the namespaces you allow, plusget/updateon exactly one named ConfigMap (its nonce ledger). By default it holds no write verbs on rollouts at all: the default promotion path is a GitOps marker commit that your own in-cluster tooling verifies and applies. The pod is hardened (non-root, read-only root filesystem, all capabilities dropped, digest-pinned images). - Validation runner — a separate ServiceAccount in its own namespace that creates short-lived test namespaces, deployments, services and jobs, and cannot reach Argo resources at all. A ValidatingAdmissionPolicy (enforced by the API server, applied by the installer by default) rejects any write by this identity outside its own
fc-test-*namespaces.
Compromising the component that runs test code must never yield rollout control — that is why they are two identities.
Argo and CI connectivity
- Argo Rollouts — the agent observes rollout state and, after an approved gate, promotes through the configured path. The default is a GitOps marker commit verified by your own promotion tooling against the live rollout; granting the agent direct rollout-API access is a separate, off-by-default choice.
- Jenkins / GitLab (private) — the CI credential lives in a secret in your cluster, deliberately separate from the agent's FixControl identity, so the two rotate independently. The operation envelope for CI verdicts carries no target URL — the agent only talks to the CI host address configured on your side, so a compromised cloud cannot point the agent at an arbitrary internal address.
- Details of what a gate does per CI host are in DevOps & CI/CD.
Secrets
| Secret | Lives | Rotated by |
|---|---|---|
| Agent transport secret | Your cluster + encrypted in FixControl | You, via rotate (shown once) |
| FixControl signing keys (public) | Pinned in the agent's config | Updated at enrolment/rotation |
| Kubernetes access | The agent's in-cluster ServiceAccount | Kubernetes |
| GitLab / Jenkins credential | A secret in your cluster | You |
| Validation-run secrets | Not passed by FixControl at all — a validation run receives no secret from us; whatever a run needs must already exist in the runner's own namespace | You |
Secret values for validation runs never appear in FixControl's stores, operation payloads, logs or evidence — there are none to appear. A per-project binding model (reference a secret by name, resolve it runner-side) exists in the schema, but no executor reads it yet, so do not plan around it.
Evidence and health
- Every operation result — success, failure, refusal, expiry — is reported back and lines up with the approval that caused it in one audit trail.
- Agent connectivity is a first-class health capability: a missed heartbeat shows as degraded within minutes, per agent, rather than hiding behind a generic "connected" state.
- Validation runs report the same evidence contract as FixControl's isolated Docker validation: environment hash, image digests, executed plan, result. See Change validation.
Failure modes
- FixControl unreachable — your workloads keep running; the agent simply collects no new operations. It retries transient failures with backoff and re-signs each attempt with a fresh timestamp; permanent (4xx) failures are not retried blindly.
- Agent down — pending operations expire visibly; the gate's approval stays recorded, and the timeline shows that execution did not happen. Nothing executes late.
- Refusals — an operation outside the local allowlist, an unknown capability, a replayed nonce or a downgrade attempt is refused, and the refusal is reported and recorded as a governance finding.
- Degraded nonce storage — if the agent cannot write its nonce ledger, it falls back to in-memory tracking with a loud warning and a metric; the replay window is then bounded by the operation expiry (minutes).
Install, upgrade, uninstall
Getting the package. Enrolment shows a download link for the pinned release alongside the agent's identity, and the same link stays on the fc-agents card afterwards — an upgrade, a second cluster and a re-install all start there. Each release carries the install tarball, its SHA-256 checksum, an image-pins.env with the two digest-pinned image references the values file wants, CycloneDX SBOMs, and the image digests.
Verify before you run. The images are signed with a keyless Sigstore signature tied to the release workflow, so there is no key of ours to store or trust:
cosign verify ghcr.io/fixcontrol-ai/fc-agent@sha256:... \
--certificate-identity-regexp '^https://github.com/FixControl-AI/fixcontrol-agent/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comcosign verify-attestation --type cyclonedx checks the SBOM the same way — an attested SBOM is tied to the digest, where a downloaded file is not. The exact commands are in the package README.
Building the images yourself is supported and the sources are in the tarball — an air-gapped estate, or a policy of running only images built in-house, needs build-images.sh, your own registry and the digests it prints, and nothing else changes.
What is in it. The manifests, an idempotent install.sh driven by a single values.env, a verify.sh that checks the result against the live cluster, plus rotate/revoke/uninstall scripts.
Upgrading is downloading the new release, carrying your values.env across, pasting the new image-pins.env lines, and re-running the installer. The agent and FixControl negotiate capabilities on every poll, so a newer server and an older agent keep working within what both understand; a release that requires a newer agent says so in its notes.
Uninstalling is the uninstall script plus revoking the agent in Settings → DevOps, after which its requests are refused.
Troubleshooting
- Agent unhealthy right after install — the health probe reports ready only after the first successful poll; check outbound reachability to FixControl and the enrolment values.
- Operations expire without being executed — the agent is not polling (egress blocked, secret revoked, pod down) or is enrolled against a different cluster ID than the operation targets. Both surface in connectivity health and the audit trail.
- A verdict was approved but nothing moved — check the operation on the approval's timeline: an expiry, a refusal (with its reason) or a delivery retry is recorded there rather than lost.
See also: Verification and results · DevOps & CI/CD · Change validation · Security & audit.