FixControl/Documentation

Admin Guide

Change validation

How FixControl validates a proposed change — isolated validation in its own environment (on by default), automatic environment discovery, the application-start check, previews, the pilots for multi-container and in-cluster validation, and the evidence a run leaves behind.

Before a proposed change reaches your repository or your release process, FixControl validates it — and records exactly what that validation proved. This page is the detailed, administrator view: which environments exist, how the choice between them is made, what evidence a run leaves behind, and which parts are a pilot. For how to read a result as a user, see Verification and results.

PartAvailability
Validation in FixControl's own isolated environment, automatic environment discovery, the application-start check, tests the change brings with itAvailable, on by default
PreviewsAvailable, on by default; a workspace can switch them off
Investigating a failed run inside its containerAvailable, off until you switch it on
Multi-container validation from your Compose filePilot, off by default, switched on per organization
Validation inside your own Kubernetes environment (FC Agent runner)Pilot, off by default, switched on per organization
.NET Framework (net48) and WinDevNot verified today — see below
You can ask for this instead of navigating to it. Command Center answers "how is this project verified?", "why did it fail?" (in plain language, including what the agent found inside the failed container), "does it still start?" and "why can't this change be applied?" — and it can start a verification or bring the application up as a preview, and change how a project is verified: on or off, where it runs, and what you know about starting the application. Only what you name is changed; the card shows what it is changing from as well as to.
Change (proposed by a developer or by FixControl's AI)
   ↓
isolated validation in FixControl's environment
   ↓
optional validation in your own cluster   (policy-driven, via the FC Agent)
   ↓
evidence frozen into the approval
   ↓
your existing CI/CD, Kubernetes and Argo controls

Isolated Docker validation

Every run gets its own temporary, isolated container on FixControl's infrastructure, cleaned up afterwards. Where multi-container validation is configured (pilot: a per-organization setting, off by default), FixControl additionally runs the change next to the services the application actually needs:

  • The repository's Compose file is read as configuration — never executed. Every service, image and option passes a strict policy: unsupported or dangerous constructs (host mounts, privileged containers, port publishing, build instructions and more) are refused, not skipped quietly. The rules in customer language are listed under Projects → Multi-container environments.
  • Services start in one isolated network per run, without internet access. Only the container that runs your tests gets the checkout — the supporting services never have the repository mounted. The run is cleaned up afterwards, and a sweeper removes anything a crashed run leaves behind.
  • Image references are resolved to digests before the run, so the evidence names the exact software versions the change passed against.

This lane answers: does the change build, pass its tests and behave next to its database, cache and background services? It is fast, reproducible and disposable.

Validation in your own environment (pilot)

Where configured — a per-organization opt-in, off by default — validation continues inside your own Kubernetes cluster, executed by a validation runner that the FC Agent install package deploys (you install that package yourself; it is a signed public release):

  • The instruction to run arrives as a signed, expiring, single-use operation the agent fetches outbound; by default the workspace travels over that same channel. Your cluster is never exposed to FixControl.
  • The runner creates a short-lived test namespace, starts the same declared services, runs the same plan, and cleans up. It is a separate identity from the agent, cannot touch rollouts, and an admission policy blocks its writes outside its own test namespaces.
  • Secret values never appear in FixControl's stores, operation payloads, logs or evidence. In fact FixControl passes no secret at all into a validation run today: a run that needs one relies on what the runner's own namespace already provides, arranged on your side during onboarding. (A per-project binding model — reference a secret by name, resolved runner-side — exists in the schema but no executor reads it yet, so do not plan around it.)

This lane answers a different question: does the change behave in the environment it will actually ship to — your cluster's runtime, policies and dependencies. It is not the Docker run duplicated.

What FixControl can actually verify

A verdict means something only if a real toolchain ran. This is what ships, stack by stack — including where the honest answer is "not here".

StackWhat runsVerdict comes from
Node / TypeScriptdependency install, Prisma or Drizzle migrations, tsc --noEmit, the detected test runner (Jest, Vitest, Mocha, node --test)type-check and tests; lint and build are advisory
Angularinstall, migrations, ng build, ng test in a headless browser the image suppliesbuild and tests — the build is required here because it type-checks templates
PythonPoetry / Pipenv / pip install, Django or Alembic migrations, mypy, pytest or unittesttype-check and tests
dbtinstall, a synthesised offline profile, dbt deps, dbt parsedbt parse. dbt compile runs as advice because it needs a reachable warehouse, and dbt run / dbt test are not planned at all
PHPComposer install, Laravel or Doctrine migrations, Psalm or PHPStan, Pest / PHPUnit / artisan testanalyser and tests; on a legacy tree with neither, a recursive php -l syntax sweep
Gogo mod download, go build ./..., go test ./...build and tests
Rustcargo fetch, cargo build, cargo testbuild and tests
JavaMaven or Gradle, compile then test (Maven warms its repository once, then runs offline)build and tests
C# / .NETdotnet restore, EF Core migrations, dotnet build, dotnet testbuild and tests; dotnet format is advisory and only when an .editorconfig is committed
C / C++CMake or Meson configure, build, ctest / meson testbuild and tests
Rubybundle install, rails db:migrate, RSpec or Minitesttests
.NET Framework (net48)planned as NuGet restore → MSBuild → VSTest, and routed to a Windows environmentNothing verifies it today. See below.
WINDEVdetected, and routed to a WINDEV runnerNothing verifies it today. See below.

A component with no test runner, type-checker or build configured produces an unsupported plan and says so — "No verification steps could be derived" — rather than a green run that proved nothing. Migration steps are dropped when the project has no database wired, so a required migration step can never wedge the gate forever.

Windows toolchains: what ships, honestly

A net48 project is not refused as unknown. FixControl detects it, plans the real commands (NuGet restore, MSBuild, VSTest) and routes the whole solution to a Windows environment. A hosted Windows environment for this, running on FixControl's cloud infrastructure, has been built — but it is not switched on and not yet proven: its image has not been built into service and no Windows run has taken place. Until it is, the run stops with a refusal that names exactly what is missing (MSBuild or VS Build Tools, nuget.exe and vstest.console). Even once switched on, that Windows environment will not start the application (so the start check reads not verified here) and cannot run supporting services such as a database; a net48 repository that declares one is refused with the service named.

WinDev is detected and planned too, but compiling and testing it requires a licensed PC SOFT installation, which FixControl neither ships nor may obtain. A WinDev run always stops with a refusal.

This is deliberate. The alternative — silently verifying a net48 solution with a Linux .NET SDK — would produce a green run that proves nothing about the software that actually ships. A refusal that names the missing capability is the honest answer, and it is the one you get.

Which validation runs

The choice is policy-driven and explicit, never inferred from the repository. It is set per project under Environments → How changes are validated; new projects use Isolated Docker. Validation in your own environment additionally needs the FC Agent switched on for your workspace, a runner that advertises it can run tests, and remote validation allowed — your FixControl operator sets the workspace default, and the project setting Remote validation for this project (Inherit workspace setting, Allowed for this project, Not allowed for this project) can override it.

On screenWhat it does
Isolated DockerFast, isolated validation on FixControl-managed infrastructure.
Your own Kubernetes environmentValidate against the environment the change will actually ship to.
Docker first, then your environmentThe isolated run happens first; only after it passes does the run in your own environment start. The gate then requires both.

In run lists the two lanes are labelled local (Docker) and remote (cluster), and a run's detail page shows Where it ran next to Validation.

Two honesty rules hold throughout:

  • A required in-cluster validation is satisfied only by in-cluster evidence — a Docker-only pass is never substituted for it.
  • If a required lane is unavailable (for example, the agent is disabled), the run fails closed with a clear reason. It does not fall back to a weaker validation without saying so.

FixControl also detects when a repository clearly targets Kubernetes (Helm charts, Kustomize, manifests) and records that signal with the run, so a Docker-only validation of a cluster-heavy application is visible for what it is. The signal informs; it never silently changes the configured strategy.

Automatic environment discovery

Validation only works if FixControl knows how your application is built and started. For repositories that follow a common shape this is derived from the repository itself. Where that is not enough, FixControl can work it out — and then prove it (a per-organization setting, on by default; turn it off to keep every run on the deterministic detection path):

  1. Facts are read from the repository, without AI: manifests and their version constraints, lockfiles, runtime version files, Dockerfile instructions, Compose services, CI runtime configuration, build and start scripts, declared health endpoints. Environment templates contribute names only, never values.
  2. AI proposes an environment from those facts, in a fixed schema. The proposal can only express runtimes, services, install/build/validation steps, start commands and environment references. It cannot express a Docker flag, a mount, a published port, privileged mode or a secret value — those are not fields it has.
  3. Policy checks the proposal before anything runs: the same command rules, the same image rules, the same resource budgets as every other validation. A proposal that contradicts an explicit fact in the repository (a required runtime version, for example) is refused rather than rounded off.
  4. The sandbox proves it by really running it. Only a run that succeeds produces a verified environment — a versioned, read-only record of the runtimes, services, images, steps and start commands that worked, together with which discovery run and which sandbox run proved it.

Attempts are bounded: a fixed maximum number of attempts, a wall-clock limit and a token budget, all recorded on the discovery next to what was actually spent. If the attempts run out, the result is not a vague red run: FixControl stops with what it found, what it tried, the last failure, and the one question a person needs to answer to unblock it.

Later changes reuse the verified environment as-is. A fingerprint over the configuration-relevant files (manifests, lockfiles, Dockerfiles, Compose, CI, environment templates, Kubernetes manifests) decides when that stops being true: a change to your code leaves it alone, a change to the configuration invalidates the verified environment and a new verification is due. The same applies to the project's own runtime pins under Residency & validation: change a pinned version or image and the verified environment is retired, so the next run is proven under the new pin instead of reusing an image the pin no longer names. A verified environment is never edited in place — a new proof is a new version — and it can only be withdrawn, never re-blessed without a new successful run.

Environment failures are treated as environment failures. A runtime that cannot be resolved, a service that will not start or a dependency that will not install never turns into an AI-proposed change to your application code.

What you can tell it yourself

Discovery reads the repository. Some things are not in the repository: how the application is started in your deployment, which port it listens on, which readiness path answers, an environment variable that is not a secret, a system package, an extra install step.

Under Environment settings, on the same card as the verified environment, a project administrator can write those down. The hint on screen states the contract exactly: "What discovery cannot read from the repository: how the application starts, its port and readiness path, extra environment, system packages and install steps. Applied to every verification, judged by the same policy as the planner's own commands, and still proven by the run."

Three properties make this safe to hand to an operator rather than to an engineer:

  • The same policy judges your commands and the AI's. A command you type goes through the identical allowlist; a system package you name is looked up in the identical recipe allowlist. There is no operator bypass.
  • It is still proven. Settings are an input to the verification, never a substitute for it. Save and verify re-runs discovery with them applied; the environment is verified only if the run succeeds.
  • It is bounded. At most four start commands, thirty-two environment entries, eight extra install steps, and a free-text note for the planner. Credentials do not belong here — the screen says so.

When nothing is set, the card says "No environment settings — discovery derives everything from the repository." When something is, it says "Environment settings are in force for every verification."

Most projects never have to fill this in. The first, model-free verification attempt now derives the start command from the repository itself where the repository says so out loud — the framework's marker file, its dependency, its own declared port and health path. Evidence beats convention, and convention never beats silence: a repository that says nothing gets no start command rather than a guess, and a wrong derivation costs one attempt rather than producing a bad contract, because an environment whose start target did not answer is not accepted.

A verified environment with no start command is therefore a normal, honest outcome — the tests ran, nothing was launched. The card says so and offers Find a start command, which asks the planner to work one out rather than leaving you to guess.

Where discovery runs. By default it runs on FixControl's own isolated Docker lane. For a project whose residency policy requires customer-hosted execution, the local Docker target is removed from the selection entirely and the discovery runs on your own runner instead; the evidence says where it ran. That is not a fallback with a shrug: if remote validation is switched off for the project, or no reachable runner advertises the capabilities the discovery needs, the request is refused with exactly that reason and the remedy — register a container-capable runner, or switch remote validation on for the project.

Does the application still start?

A green test suite is not a green change. A patch can leave every test passing and still break the boot: a missing environment variable, a route table that no longer compiles, a service the application opens on start-up.

So when the project's verified environment declares how the application starts, every run on FixControl's own Linux infrastructure launches it and waits for it to answer on its readiness path — in the isolated Docker environment and on FixControl's managed cloud environment, where the application is started, probed and stopped inside the same short-lived task that ran the tests. That result is its own line on the run:

On screenMeans
Started and answeredThe application launched and answered within the readiness budget.
Did not startIt launched and died, or never answered.
Not startedNothing was attempted.
Not verified hereThis environment declares nothing to start, so there was nothing to check. The tests say nothing about whether the application runs.

The run detail page says it plainly: "The tests can pass while the application no longer boots. This is the check that tells them apart."

It blocks the apply. If the verified environment claims a start command and no run proved it, the patch gate refuses with: "The tests passed but nothing proved the application still starts; rerun the verification so its start target is exercised." An approval that was already given stays given — the patch is shown as Approved — application start unproven rather than silently applying.

Two boundaries worth knowing. A start that fails because the contract is wrong (no start target, the command was refused by policy) is an environment failure and sends the environment back for re-verification. A start that fails because the application crashed or timed out is an application failure and feeds the revision loop.

The proof is required wherever the run happened. A runner in your own environment cannot start the application and watch it come up (the operation channel has no start or probe step), so on that lane the check is recorded as not verified here and the apply gate still refuses. A project validated only in your own environment therefore needs one of two things: the strategy Docker first, then your environment, where the isolated run produces the proof, or the waiver below.

Waiving it, by name and by date

Some projects genuinely cannot answer. A project validated on your own runner may have no way to start the application and watch it come up. For those, a project admin can set Proof that the application still starts to not required, in the project's validation settings under Advanced.

The screen states the trade in full before you confirm: "You are accepting that a change can be applied while nobody has confirmed the application still comes up. Your name and today's date are recorded next to every approval that relies on it."

So a waiver is not a silence in the code — it is an operator who accepted a risk, on a date, in a row an auditor can read. The project's settings then say Not required — changes are applied on the test results alone, with Accepted by {who} on {date} beneath it. Only admins and managers can set it, and an unrelated save never grants or revokes one.

On the approval card

The approver sees this as its own check, Application start, alongside QA verification, security review, sandbox validation and CI on the delivered commit. It has three states and the middle one has its own word:

  • passed — the application came up and answered, with this change in it;
  • waived — nobody proved it, and somebody accepted that; the check names who and when;
  • failed — FixControl could not start the application with this change.

Waived is deliberately not passed. A reader skimming the state column can never mistake one for the other, and the proof always wins where it exists: a project that could prove it and did never renders as merely waived.

Tests the change brings with it

A patch often ships its own tests. Two kinds used to be written and then never run; both are run now:

  • Tests your framework's runner never picks up. An Odoo module collects only Odoo's own test classes, so a plain Python unittest file in a patch loaded and ran nothing. FixControl now reads such a file (without importing it) and runs its test classes directly, next to the framework's own step. A Node test file written for node --test inside a Jest, Vitest or Mocha project is run with node --test instead of failing the project runner.
  • UI tests, in a headless browser. An Odoo QUnit suite the patch registers (only the patch's own QUnit modules), an Odoo tour started from a Python test, a Playwright or Cypress spec in a project that already uses that tool, or a self-contained Node test script. The developer agent is asked to turn goals about the interface into exactly such tests, per stack.

These steps prove the patch; they do not replace the project's own required steps. They fail closed: when the test environment cannot run a browser, the run says "UI tests could not run in this test environment" instead of passing silently, and a UI step that executed nothing is reported as not run. A goal about the interface counts as verified only by a passing browser test of this version of the patch that covers it — a Node script never verifies a UI goal. A failing UI test is a verdict on the change and feeds the revision loop like any other failing test.

When a run fails, the agent goes back into the container

A failed run used to hand the next attempt a log tail. Now, where it is switched on, the developer agent gets a bounded shell inside the very container the run failed in — the same image, the same services, the same working tree — runs a few commands, and writes down what it observed.

  • It has exactly two verbs: run one command, and conclude. It has no way to edit a file; it cannot "fix" anything from in there.
  • It is bounded by a command count and a time budget (defaults: twelve commands, two minutes), both set by your FixControl operator, and neither raisable by the agent.
  • The container is destroyed the moment the session ends. Nothing outlives its run, there is no lease and no keep-alive.
  • The conclusion carries a summary, a likely cause, the evidence it saw, a suggested direction and a confidence. "Not reproduced, low confidence" is an allowed and useful answer; a confident guess is not.

On the run you see it as What the agent found in the failed container, with the commands it ran. The finding is put at the top of the next revision's brief, above the log summary — so the next patch is written against something observed rather than something inferred.

It is off until you switch it on. That is deliberate, and the reason is worth stating: what it enables is a model running commands of its own choosing inside the sandbox — a hardened, egress-free, throwaway box on an already-failed run, but still a capability whose blast radius is yours to weigh, not ours to assume. A failed run offers it where you meet the gap it fills, with what it costs: "It adds a minute or so to a run that already failed, and nothing at all to one that passed."

You switch it on yourself. The switch is offered in three places, all writing the same workspace setting: on the failed run itself (Switch this on), on the project's Code & environments card, and in Settings → Overview under Settings for this workspace. You can also just ask — "laat FixControl mislukte runs onderzoeken" — and Command Center proposes it on a card that states the consequence before you confirm. Admins and managers only, behind the two-factor wall, and every change is written to the audit trail with the value before and after.

Once on, it engages only when the run has a patch, ran in FixControl's own environment (the isolated Docker environment or the managed cloud environment), and the developer agent holds the capability for it.

Previews

Live previews are on by default; an admin or manager can switch them off for the workspace under Settings → Overview & progress → Settings for this workspace (Let people open a running preview). They depend on automatic environment discovery as well — a preview can only be opened on top of a verified environment, and a verified environment is what discovery produces. A preview holds a sandbox environment alive after the run has finished, so at most three run at the same time per workspace; each stops after 30 minutes without use or after 4 hours, and must become ready within 20 minutes.

Someone with project administration rights can open a preview: FixControl starts the application in the sandbox, waits until it answers its own health check, and gives that one application to authenticated people in your organization for a limited time.

Starting a preview takes a few minutes, so you do not have to keep the page open: the person who started it gets a notification when it is ready — or when it did not start, with the reason — linking back to where it was started.

What that does and does not open up:

  • No port is published. The sandbox network stays closed, and FixControl reaches the running application over the internal per-run network. Databases, caches and other services in the environment are not reachable through a preview; exactly one application target is.
  • A preview is not a shareable link. Viewers must be signed in, in the right organization, with settings-read rights on the project (creating a preview and recording a verdict both need settings-manage). There is no public share link. Where each preview has its own hostname, the first request carries a one-time hand-off that is valid for sixty seconds, is bound to that single preview, and is exchanged immediately for an ordinary session cookie scoped to that host alone — it cannot be pasted to anyone or reused.
  • It ends by itself. Every preview has a time limit and a maximum number of previews per organization; expiry, a manual stop or a crashed application all end it, and the environment is cleaned up afterwards. Creating, viewing, stopping and reviewing a preview are all recorded as audit events.
  • You can use the application, not just look at it. The gateway forwards GET and HEAD everywhere, and forwards form submissions and other writes (POST, PUT, PATCH, DELETE) when the preview is served on its own hostname — so you can log in and click through a flow. WebSocket upgrades are still not forwarded, so a page that depends on a live socket degrades to whatever it does without one.
  • Previews run in FixControl's own isolated environment, including its managed cloud environment. Previewing inside your own Kubernetes cluster is not part of this — it would open a path toward your cluster that does not exist today. For the same reason, a project whose residency policy requires customer-hosted execution gets no preview at all: the request is refused with that reason rather than quietly falling back to a FixControl-side run.
  • Per-preview hostnames are an operator prerequisite, not a default. Until the FixControl operator configures a preview-origin template (it needs wildcard DNS and a wildcard certificate on the FixControl deployment), previews are served under a shared path and only render correctly for applications whose assets are addressed relatively. An application that emits root-absolute asset paths — most PHP and Rails applications do — needs the per-preview hostname before its stylesheets and scripts load. The same hostname is what unlocks form submissions in the point above.

After looking at a preview, a reviewer can record a verdict: this looks right or this does not look right. That verdict is human evidence and is recorded next to the automated validation, with the name and the moment. It never replaces a test result, it never opens an approval gate on its own, and it stops counting as evidence when the verified environment it was formed on is replaced.

Who can run one

Three separate permissions, because they are three different powers:

  • Reading a run, its detail, its live stream and a change's CI status — admins, managers and operators.
  • Starting and cancelling a run — admins, managers and operators too. Reading a report and spending a container are different things, but an operator watching work go through legitimately needs to be able to re-run a check without asking someone else.
  • Editing the verification verdict — admins and managers only. That is the gate that reading must never be able to clear.

Archiving and restoring a run sit with the general archive permission: it is visibility only, and reversible.

Managing the run list

Sandbox runs accumulate. The Sandbox runs page filters them by outcome — passed, failed, errored (error or timeout, bundled because the mental model is "did not pass, did not fail cleanly"), cancelled, running — and lets you clear the list without losing anything:

  • Archive a finished run, from its row or in bulk. Archiving is visibility only: the verdict, the logs, the evidence and every gate that cited that run are untouched, and the run stays openable by its id. Who archived it and when are recorded on the run, so the trace still answers both questions after the audit event has aged out.
  • Archived is its own view, from which the same runs can be restored in bulk or one at a time.
  • A run that is still executing cannot be archived — the server refuses it, so the action is hidden rather than offered and then rejected.

Evidence

Every green run records:

  • the normalized environment configuration (a hash of the validated topology),
  • the exact image digests the services ran with,
  • the executed plan (which steps ran),
  • for in-cluster runs: where it ran — agent, cluster, namespace, operation.

Together these form the run's validation fingerprint. A changed patch, image, configuration or plan produces a different fingerprint, so stale evidence never carries over. The result — including the fingerprint and, where applicable, the per-lane results — is frozen into the approval that relies on it, and appears in the deployment provenance trail alongside the approval and the rollout it led to.

Where your governance policy requires it, the patch gate refuses to apply a change whose current version lacks matching green evidence (see Governance & policy, verification policies).

When validation fails

A failure produces structured evidence, classified by kind:

  • Application failures — a red test, a crash at startup, a service that never became ready. These can feed a new AI-prepared revision of the change, which goes through validation and approval again from the start.
  • Infrastructure failures — an image that would not pull, an unreachable runner, an expired operation. These surface as health findings for an operator and deliberately do not trigger a code change: a broken environment is not a bug in your application.

In both cases governance stays between analysis and execution: a failure never means an unreviewed change reaches your systems.

See also: FC Agent · DevOps & CI/CD · Governance & policy.

Anything unclear or wrong?Let us know →

FixControl is a trade name of FixControl B.V.