# Antifailure manifest schema

URL: https://antifailure.dev/docs/reference/schemas/manifest-v1

The file antifailure.yaml at the root of a repository.

---

The file antifailure.yaml at the root of a repository. It describes what to build, where the database comes from, what the environment may reach on the network, who the agents log in as, and what they do. It is the whole configuration surface: nothing about an environment is configured anywhere else.

:::note
This page is generated from `schemas/manifest.v1.json`. Edit the schema, then run `just generate`.
:::

## The document

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `auth` | [auth](#auth) | no | How personas come to exist. |
| `change` | [Change](#change) | no | How a pull request's diff is classified. |
| `database` | [Database](#database) | no | Where the environment's Postgres comes from, and how the production copy is made safe before anyone can branch from it. |
| `egress` | [Egress](#egress) | no | What the environment may reach on the network. |
| `explore` | [Explore](#explore) | no | Agents that pursue a goal with no declared workflow, discover the paths an application offers, and report where it costs somebody effort without failing. |
| `fidelity` | [Fidelity](#fidelity) | no | The component inventory: what the environment reproduces, what stands in for something, and what it could not reproduce at all. |
| `github` | [GitHub](#github) | no | How Antifailure appears on a pull request: what runs it, whether it comments, what it does with forks, and when it tears the environment down. |
| `insights` | [Insights](#insights) | no | The Postgres native checks that turn a preview environment into a database review. |
| `invariants` | list of [Invariant](#invariant) | no | Read only statements that must hold after every workflow. They are the assertions a test cannot make from the outside: no orphaned rows, no negative balances, no subscription without a customer. Max items 100. |
| `load` | [Load](#load) | no | Traffic shaped like production, compared between the base branch and this one. |
| `name` | string | no | A short name for this application, used in environment hostnames and in the control plane. Defaults to the repository directory name. Max length 40, matches `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`. |
| `oracle` | [Oracle](#oracle) | no | Deploy a baseline version alongside the candidate, send both the same requests, and report every difference in what came back and in what ended up in the database. |
| `personas` | list of [Persona](#persona) | no | The accounts agents log in as. Each is created or reconciled in the golden by the authentication adapter, so a persona is a real user of the application rather than a bypass. Max items 50. |
| `policy` | [Policy](#policy) | no | What each class of finding does to the pull request check. |
| `runtime` | [Runtime](#runtime) | no | Where and how long the environment runs. |
| `services` | list of [Service](#service) | no | Every process the environment runs: web servers, API servers, background workers, and scheduled jobs. Min items 1, max items 50. |
| `version` | `1` | **yes** | The manifest schema version. Increment only for a breaking change; the engine refuses a version it does not understand rather than guessing. |
| `workflows` | list of [Workflow](#workflow) | no | What the agents do, written as sentences. A workflow is a goal, not a script: the runner decides the actions and verifies the outcome. Max items 200. |

## auth

How personas come to exist. Absent from most manifests, because detection answers it; present when detection is wrong, when the users table has names nothing could guess, or when the application's users live somewhere only a script can reach.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `adapter` | `auto`, `direct`, `supabase`, `supabase_api`, `nextauth`, `clerk`, `auth0`, `workos`, `seed` | no | Which authentication scheme personas are created in. auto picks it from the dependency list and the live schema. Defaults to `auto`. |
| `connection` | string | no | The Auth0 database connection users are created in. Defaults to Username-Password-Authentication. Max length 128. |
| `domain` | string | no | The tenant, for Auth0, for example dev-abc123.us.auth0.com. Max length 253. |
| `password` | [password rules](#password-rules) | no | The application's password policy, so the generated password satisfies it. |
| `sandbox` | boolean | no | That the configured tenant is a sandbox, development or staging tenant rather than the production one. A hosted adapter refuses to create anybody without this, because the only tenant it could otherwise fall back to is the real one. Defaults to `false`. |
| `seed` | string | no | The command the seed adapter runs, once per persona, with the persona in the environment as AF_PERSONA_NAME, AF_PERSONA_EMAIL, AF_PERSONA_PASSWORD, AF_PERSONA_TOTP_SECRET, AF_PERSONA_ROLE, AF_PERSONA_LOGIN and AF_PERSONA_ATTRIBUTES. It must be idempotent, because it runs again on every branch. Max length 2000. |
| `sessions` | list of string | no | Extra tables holding sessions or tokens, emptied so that no real session survives into a branch. Masking does not touch them, because a session token is not personal data by any rule a scanner applies. Max items 50. |
| `table` | [auth table](#auth-table) | no | The columns of an application's own users table, for the direct adapter. |
| `token_env` | string | no | The variable holding the provider's admin credential. The variable name, never the credential. Max length 128. |
| `url` | string | no | The project's API root, for Supabase. Max length 2048. |

## auth table

The columns of an application's own users table, for the direct adapter. Named rather than guessed, because guessing a column name is how provisioning writes a row the application cannot read.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `attributes` | object | no | Maps a persona attribute name to the column it is stored in. Max properties 50. |
| `email` | string | no | Defaults to `email`. Max length 63. |
| `id` | string | no | Defaults to `id`. Max length 63. |
| `json` | string | no | A JSONB column that persona attributes with no column of their own are written into. Max length 63. |
| `name` | string | **yes** | Max length 63. |
| `password` | string | no | The column the bcrypt hash goes in. Absent for a table that keeps no password. Max length 63. |
| `role` | string | no | Max length 63. |
| `schema` | string | no | Defaults to `public`. Max length 63. |
| `timestamps` | list of string | no | Columns set to now() on insert, and on update where the name contains 'updated'. Max items 10. |

## Build

How to turn the service directory into an image. Omitted means detect: a Dockerfile if there is one, otherwise a buildpack.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `allow_hosts` | list of string | no | Hosts the build is declared to reach, such as a package registry or an engine download. DECLARED RATHER THAN ENFORCED in this release: the list is validated and shown by af explain, and the local builder does not yet seal a build or apply it. Write it as the record of what your build needs, and do not rely on it as a control. Max items 50. |
| `args` | object | no | Build arguments. Never secrets: build arguments are recorded in image metadata and are visible to anyone who can pull the image. Secrets are mounted, and the linter rejects a secret shaped argument. Max properties 50. |
| `context` | string | no | Build context directory, relative to the repository root. Defaults to the repository root so that a service can copy from a shared package. Max length 512. |
| `dockerfile` | string | no | Path to the Dockerfile, relative to the repository root. Max length 512. |
| `image` | string | no | A prebuilt image reference, used with the image strategy. Pinned by digest is strongly preferred. Max length 512. |
| `strategy` | `auto`, `dockerfile`, `buildpack`, `image` | no | Defaults to `auto`. |
| `target` | string | no | Stage to build in a multi stage Dockerfile. Max length 128. |

## Change

How a pull request's diff is classified. The built in rules cover the layouts most projects use; these are for the ones they do not. A rule says what a path is, never which checks to run: an unrecognised path always selects every check, and no rule here can take a check away.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `rules` | list of [Change rule](#change-rule) | no | Path patterns this repository wants classified its own way. The longest matching pattern wins, so order does not decide. Max items 100. |

## Change rule

One path pattern and what the paths it matches are. It says what a file IS, never which checks to run.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `note` | string | no | The sentence the report prints for this rule, replacing the default one that restates the pattern. Max length 200. |
| `path` | string | **yes** | A glob against the repository relative path. A single star does not cross a slash and a double star does. A pattern that matches everything is refused, because it would defeat the rule that an unrecognised path selects every check. Min length 1, max length 256. |
| `surface` | `schema`, `code`, `asset`, `build`, `dependency`, `config`, `infrastructure`, `pipeline`, `test`, `docs` | **yes** | What the matched paths are. Surfaces the engine assigns from the manifest itself, such as a service or the masking rules file, cannot be set here. |

## Database

Where the environment's Postgres comes from, and how the production copy is made safe before anyone can branch from it.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `api_key_env` | string | no | The name of the variable holding the provider's API key. Named rather than carried: a manifest is committed and a key is not. Defaults to NEON_API_KEY for the neon provider. |
| `golden` | [Golden](#golden) | no | The masked, verified copy every environment branches from. |
| `masking_rules` | string | no | Path to the masking rules file, relative to the repository root. Defaults to `masking.yaml`. Max length 512. |
| `max_branches` | integer | no | The plan's concurrent branch limit, where the provider has one it cannot read from its own API. Reaching it fails with AF-DB-006 rather than hanging. Minimum 1. |
| `project` | string | no | The account-side project a hosted provider creates branches in, such as a Neon project. Not a secret, which is why it lives here and the key that reaches it does not. |
| `provider` | `docker`, `neon`, `supabase`, `dblab` | no | Which provider creates branches. docker is local and needs nothing; neon, supabase, and dblab talk to a service. Defaults to `docker`. |
| `seed` | string | no | Command that fills the golden with data, for a project with no production database yet. It runs once per refresh with DATABASE_URL set, and every branch is a copy of what it made, so the cost is paid once rather than per environment. Mutually exclusive with source_url_env. Max length 1024. |
| `source_url_env` | string | no | Name of the environment variable holding the read only connection string of the production database. The value is read once, during a golden refresh, on the operator's machine or runner, and never stored. Max length 128, matches `^[A-Za-z_][A-Za-z0-9_]*$`. |
| `subset` | [Subset](#subset) | no | Take a production shaped slice rather than the whole database. |
| `url_env` | string | no | Name of the environment variable to inject into services with the branch's connection string. Defaults to `DATABASE_URL`. Max length 128, matches `^[A-Za-z_][A-Za-z0-9_]*$`. |
| `version` | `14`, `15`, `16`, `17`, `18` | no | Postgres major version. Match it to the source: a golden built on a different major is an environment running a Postgres your application does not. Defaults to `17`. |

## Egress

What the environment may reach on the network. Everything leaves through the sidecar, and everything not named here is blocked.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `allow_ipv6` | boolean | no | Whether the environment may open IPv6 connections. Off by default, because an IPv6 path that bypasses the proxy is the most common way an egress control is silently defeated. Defaults to `false`. |
| `default` | `block`, `allow`, `capture`, `mock`, `sandbox`, `synth` | no | What happens to a host with no rule. Changing this away from block is a deliberate act with a real cost: it is how a preview environment emails a real customer. Defaults to `block`. |
| `rules` | list of [Egress rule](#egress-rule) | no | What the environment may do with one host. Max items 500. |

## Egress rule

What the environment may do with one host. A rule is per host because that is the unit a person can reason about: allowed, blocked, answered from a fixture, or sent to the provider's own sandbox.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `credential` | string | no | Name of the environment variable holding the sandbox credential for this host. Max length 128, matches `^[A-Za-z_][A-Za-z0-9_]*$`. |
| `fixtures` | string | no | Path to a fixture pack or an OpenAPI document for mock mode, relative to the repository root. Max length 512. |
| `host` | string | **yes** | Host to match. A leading *. matches one or more labels. An IP literal matches only itself. Max length 253. |
| `methods` | list of string | no | Restrict the rule to these HTTP methods. Max items 10. |
| `mode` | `block`, `allow`, `capture`, `mock`, `sandbox`, `synth` | **yes** | block refuses with a readable decision. allow passes through with a rate limit. sandbox substitutes test credentials and forwards to the provider's sandbox. capture records the message into the inbox and returns the provider's success shape. mock answers from a fixture or an offline pack. synth asks a model to invent a response and marks every result that touched it as unverified. |
| `note` | string | no | Why this rule exists. Rendered in the network policy view, because a rule nobody can explain is a rule nobody dares remove. Max length 512. |
| `paths` | list of string | no | Restrict the rule to these path prefixes. Anything else on the same host falls through to the next rule. Max items 100. |
| `rate_limit` | string | no | Token bucket rate, for example 10/s or 600/m. Applies to allow and sandbox. Matches `^[0-9]+/(s\|m\|h)$`. |
| `webhook_path` | string | no | Path on the application that this provider posts webhooks to. The sandbox forwarder and the offline pack both deliver here. Max length 512. |

## Environment variable

One variable a service needs. The manifest declares the name and where the value comes from; it never holds the value itself, which is why the file is safe to commit.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `from` | string | no | Where to read the value: a secrets adapter name, or the name of a different variable to copy. Max length 256. |
| `name` | string | **yes** | Max length 128, matches `^[A-Za-z_][A-Za-z0-9_]*$`. |
| `required` | boolean | no | Whether the environment fails to start without it. Defaults to true, because a service silently missing configuration is the failure this product exists to prevent. Defaults to `true`. |
| `sandbox` | boolean | no | Marks a credential that must be a sandbox one. The secrets subsystem refuses a value carrying a known live prefix, and the proxy trips a wire if one reaches the network anyway. Defaults to `false`. |
| `value` | string | no | A literal value for a variable that is configuration rather than a secret, such as a feature flag or a public URL. A value that looks like a credential is rejected. Max length 2048. |

## Explore

Agents that pursue a goal with no declared workflow, discover the paths an application offers, and report where it costs somebody effort without failing. An exploration is reproducible from its seed and never counts against the change.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | no | Defaults to `false`. |
| `goals` | list of [Goal](#goal) | no | One thing an exploratory agent tries to achieve. Max items 50. |

## Fidelity

The component inventory: what the environment reproduces, what stands in for something, and what it could not reproduce at all.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | no | Defaults to `true`. |
| `require` | list of string | no | Dimensions every component of which must be reproduced. A dimension that could not be measured neither satisfies a requirement nor breaks one. |

## GitHub

How Antifailure appears on a pull request: what runs it, whether it comments, what it does with forks, and when it tears the environment down.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `comment` | boolean | no | Whether to maintain a single comment on the pull request. It is updated in place rather than appended, so a busy pull request does not accumulate twenty bot comments. Set false and af change and af ci write comment=false to GITHUB_OUTPUT for the workflow to gate its comment step on. The report files are still written, because the report is also the job summary and the payload a control plane is sent. Defaults to `true`. |
| `fork_policy` | `never`, `label`, `always` | no | What to do with a pull request from a fork. label requires a maintainer to add antifailure:allow first, which is the only safe default: a fork's code would otherwise run with the environment's credentials. Enforced by af ci, af up, af test and af load run before an environment is created, on pull_request and pull_request_target, and read from the base branch rather than from the pull request, because the pull request's copy of this file belongs to the contributor. Defaults to `label`. |
| `mode` | `actions`, `app`, `off` | no | actions runs everything inside a workflow with no server. app uses the GitHub App and the control plane. Defaults to `actions`. |
| `teardown_on` | list of string | no | Accepted and read by nothing. Teardown is unconditional: af ci tears down whatever the outcome, and the control plane asks for teardown on close, merge, supersession and timeout without reading your manifest. The lifetime ceiling is runtime.max_ttl. Defaults to `[close merge ttl]`. Max items 5. |

## Goal

One thing an exploratory agent tries to achieve. Unlike a workflow this declares no outcome, so it cannot fail: what it produces is the path it took and the friction it met on the way.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `budget` | object | no | Hard caps. An exploration that exhausts its budget reports what it found up to that point and says the budget ran out. |
| `goal` | string | **yes** | What somebody is trying to do, in one sentence. The agent has no script, so this is the only thing telling it where to go, and its words are what decide whether the goal was reached. Min length 10, max length 1000. |
| `name` | string | **yes** | Max length 64, matches `^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$`. |
| `persona` | string | no | Which persona explores. Defaults to the first persona. Max length 40. |
| `seed` | string | no | Decides every choice the agent makes. The same seed against the same application takes the same path, step for step, which is what lets a finding be replayed. Defaults to the goal's name. Max length 64. |
| `slow_ms` | integer | no | How long one step may take before it is reported as friction. Defaults to `3000`. Minimum 1, maximum 600000. |
| `start_path` | string | no | Where to begin. Defaults to the application root. Defaults to `/`. Max length 512. |

## Golden

The masked, verified copy every environment branches from.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `max_age` | string | no | How stale a golden may be before af up refreshes it first. Defaults to `168h`. Matches `^[0-9]+(h\|d)$`. |
| `retain` | integer | no | How many versions to keep. A referenced version is never collected regardless of this. Defaults to `5`. Minimum 1, maximum 100. |
| `schedule` | string | no | Cron expression for automatic refreshes, with an optional CRON_TZ prefix. A refresh that would overlap a running one is skipped with an event rather than queued. Max length 128. |
| `storage` | `local`, `azure_blob`, `s3` | no | Where dumps and attestations live. Defaults to `local`. |
| `storage_url` | string | no | Container or bucket URL for a remote store. Credentials come from the secrets subsystem, never from this URL. Max length 1024. |

## Insights

The Postgres native checks that turn a preview environment into a database review.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | no | Defaults to `true`. |
| `large_table_rows` | integer | no | Row count above which a migration lint treats a table as large, where a rewrite or an exclusive lock is an outage rather than a pause. Defaults to `100000`. Minimum 0. |
| `migration_rehearsal` | boolean | no | Apply pending migrations to a fresh branch, recording per statement duration and the strongest lock held per table. Defaults to `true`. |
| `plan_diff` | boolean | no | Compare query plans between branches to catch an index that stopped being used. Defaults to `true`. |
| `query_regression` | boolean | no | Diff pg_stat_statements between the base branch and this one after running the same workflows, to catch a query loop before it reaches production. Defaults to `true`. |
| `regression_factor` | number | no | How much slower a query may get before it is reported. Defaults to `1.5`. Minimum 1. |
| `regression_min_ms` | number | no | Minimum absolute change in mean milliseconds before a regression is reported, so that a query going from 0.1 to 0.2 milliseconds is not news. Defaults to `5`. Minimum 0. |
| `rolling_compatibility` | [Rolling compatibility](#rolling-compatibility) | no | Run the previous release against the migrated schema and see whether its workflows still pass, which is the invariant a rolling deploy actually depends on. |

## Invariant

One read only statement that must hold after every workflow. Invariants are the assertions a test cannot make from the outside, checked against the database rather than the interface.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `description` | string | no | What is wrong when this fails, in one sentence. It becomes the failure message. Max length 512. |
| `name` | string | **yes** | Max length 64, matches `^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$`. |
| `sql` | string | **yes** | A single read only statement. It runs inside a read only transaction with a statement timeout, so a write is refused by Postgres as well as by validation. The invariant fails when the statement returns any row, so write it to select the violations. Min length 6, max length 4000. |

## Load

Traffic shaped like production, compared between the base branch and this one. Results are always deltas, never absolute capacity claims.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `duration` | string | no | How long to run. Capped at fifteen minutes. Defaults to `2m`. Matches `^[0-9]+(s\|m)$`. |
| `enabled` | boolean | no | Defaults to `false`. |
| `safe_routes` | list of string | no | Routes that may be called freely because they do not mutate state. Max items 500. |
| `scale` | number | no | Fraction of production arrival rate to reproduce. Defaults to `0.05`. Minimum 0.001, maximum 1. |
| `scenarios` | list of [Load scenario](#load-scenario) | no | Declared journeys run against the environment beside the mix. Each entry names a scenario document in the repository. Max items 50. |
| `source` | `none`, `otel`, `access_log` | no | Where the endpoint mix comes from. An OpenTelemetry trace export or a combined format access log, both read from a file named in source_config.path. Defaults to `none`. |
| `source_config` | object | no | Adapter specific settings. Both sources take a path: the OTLP/JSON trace export, or the access log. Credentials come from the secrets subsystem. Max properties 20. |
| `thresholds` | object | no | Deltas that fail the run. Applied to the difference against the base branch, never to absolute numbers. |
| `unsafe_routes` | list of string | no | Routes that mutate state destructively. They are included only against a fresh branch that is reset afterwards. Max items 500. |

## Load scenario

One journey document and how hard to run it.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `iterations` | integer | no | How many times each session walks it. Defaults to `1`. Minimum 1, maximum 1000. |
| `path` | string | **yes** | The scenario document, relative to the repository root. Max length 512. |
| `sessions` | integer | no | How many sessions walk the journey at once. Defaults to `1`. Minimum 1, maximum 1000. |
| `start_after` | string | no | Delay before this scenario starts, so one journey can burst while another is already running. Matches `^[0-9]+(ms\|s\|m)$`. |

## Oracle

Deploy a baseline version alongside the candidate, send both the same requests, and report every difference in what came back and in what ended up in the database.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `base_ref` | string | no | The git ref the baseline comes from, or the ref the merge base is taken against. Empty tries origin/HEAD, then origin/main, then origin/master, and says which it used. Max length 256. |
| `baseline` | `merge_base`, `ref` | no | How the version to compare against is chosen. merge_base answers what this branch changes; ref answers what changes when it ships. Defaults to `merge_base`. |
| `compare_timestamps` | boolean | no | Compare timestamp strings exactly instead of treating two well formed timestamps as equal. Turn it on when timestamps come from the data rather than from the clock. Defaults to `false`. |
| `compare_uuids` | boolean | no | Compare UUIDs exactly instead of treating two well formed UUIDs as equal. Turn it on when identifiers are stored rather than generated per request. Defaults to `false`. |
| `database` | [Oracle database](#oracle-database) | no | The comparison of the two branches' contents. |
| `enabled` | boolean | no | Whether the comparison runs. Present but false is how a project keeps its probe plan and turns the check off for a while. Defaults to `true`. |
| `fail_on` | `none`, `minor`, `major`, `critical` | no | The lowest severity that fails the command. critical is a request the baseline served and the candidate did not, a status that fell into an error class, or a row the baseline wrote and the candidate did not. Defaults to `critical`. |
| `ignore` | [Oracle ignore](#oracle-ignore) | no | What the comparison is told not to look at. |
| `probes` | list of [Probe](#probe) | no | The requests sent to both versions, in order, byte for byte the same on each side. Max items 200. |

## Oracle database

The comparison of the two branches' contents.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | no | Defaults to `true`. |
| `exclude` | list of string | no | Tables to leave out, applied after tables. Max items 500. |
| `max_rows` | integer | no | How many rows a table may hold and still be compared. A table over the bound is reported as not compared, never silently skipped. Defaults to `10000`. Minimum 1, maximum 1e+06. |
| `tables` | list of string | no | Tables to compare. Empty compares every table. A pattern is schema.table, and either half may be an asterisk. Max items 500. |

## Oracle ignore

What the comparison is told not to look at. Everything here is printed in the report along with the defaults, because an oracle that silently ignores a field is worse than one that reports it.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `fields` | list of string | no | JSON paths to skip, in a response body and in a table row alike. $.token, $.orders[*].placed_at and $..created_at are all accepted. Max items 200. |
| `headers` | list of string | no | Response headers to skip, in addition to the defaults. Max items 100. |

## password rules

The application's password policy, so the generated password satisfies it. Without this, an application stricter than the generator refuses a correct password at sign in and the run reports a login failure that looks like the application's fault.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `forbid` | string | no | Characters the application will not accept. Max length 32. |
| `min_length` | integer | no | Minimum 1, maximum 128. |
| `symbols` | string | no | Replaces the default symbol set, for an application that rejects the ones it uses. Max length 32. |

## Persona

One account an agent logs in as. Personas are created or reconciled in the golden by the authentication adapter, so an agent signs in the way a person does rather than through a bypass.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `attributes` | object | no | Extra columns to set on the persona's row, for a schema with application specific fields. Max properties 50. |
| `email` | string | no | Login address. Defaults to name@example.test, which is a reserved domain that can never receive mail. Max length 254. |
| `login` | `none`, `password`, `magic_link`, `email_code`, `sms_code`, `totp`, `session` | no | How this persona signs in. none is for an application with no sign in, or a page that is public: the agent goes straight to start_path. magic_link and email_code read the message from the captured inbox, so they work with no mail provider at all. The runner drives none, password, magic_link, email_code and sms_code today; a persona set to totp or session is reported as blocked with the reason, rather than failing the change. Defaults to `password`. |
| `mfa` | boolean | no | Whether to enroll a time based one time password secret, which the runner holds so that it can complete a challenge. Defaults to `false`. |
| `name` | string | **yes** | Max length 40, matches `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`. |
| `phone` | string | no | Number an SMS code is sent to. Defaults to a number in the +1 555 0100 block, which is reserved for fictional use and can never reach a real handset. Only sms_code uses it. Max length 32. |
| `role` | string | no | Application role to provision, for example admin or member. Interpreted by the authentication adapter. Max length 64. |

## Policy

What each class of finding does to the pull request check. A finding at 'fail' fails the check, one at 'warn' is reported and the check still passes, and one at 'ignore' is not reported at all. Every key here is read when the report is built, so the answer to why a check failed is always one of these keys.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `cleanup` | `ignore`, `warn`, `fail` | no | Teardown left a resource behind. The journal remembers what is left, so 'af down' can finish the job. Defaults to `fail`. |
| `egress_surprise` | `ignore`, `warn`, `fail` | no | The environment tried to reach a host the manifest does not mention. The request was refused either way; this decides whether the attempt stops the merge. Defaults to `fail`. |
| `load_regression` | `ignore`, `warn`, `fail` | no | A load threshold from the load block being exceeded. Defaults to `warn`. |
| `masking` | `ignore`, `warn`, `fail` | no | The environment's own branch read back with something in it that still parses as real data. Defaults to `fail`. |
| `migration_failed` | `ignore`, `warn`, `fail` | no | A migration that did not apply to a branch with production's shape in it. A migration that fails here is one that would have failed in production. Defaults to `fail`. |
| `migration_lint` | `ignore`, `warn`, `fail` | no | Any of the seventeen migration lint rules. They share one setting because the rules are already scoped by table size. Defaults to `warn`. |
| `migration_lock` | object | no | How long a migration may hold a lock on a table. Both figures are compared against a sampled lower bound, so a breach really did hold the lock at least that long. |
| `migration_rewrite` | `ignore`, `warn`, `fail` | no | A statement Postgres reported as rewriting a table, which copies every row under a lock nothing can read through. Defaults to `warn`. |
| `plan_regression` | `ignore`, `warn`, `fail` | no | A query plan that got worse in one of three plan regressions: a table is now read end to end, an index is no longer used, or the planner's estimate grew. Defaults to `warn`. |
| `query_regression` | `ignore`, `warn`, `fail` | no | A statement that runs more often, or slower, than the saved baseline did. Needs a baseline to compare against. Defaults to `warn`. |
| `workflows_unverified` | `ignore`, `warn`, `fail` | no | A run in which no workflow reached a verdict about the application, because every one was blocked or unverified or because none was declared. Distinct from a single blocked workflow, which is never counted against the application: one gap in the tooling is not evidence, and a run where every workflow was a gap has tested nothing at all, so reporting it as a pass says the application was checked when it was not. Set it to warn if the project has no workflows yet and you would rather record that choice than be told about it. Defaults to `fail`. |

## Probe

One request sent to both versions.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `body` | string | no | The request body, sent byte for byte to both sides. Max length 65536. |
| `headers` | object | no | Headers sent on both sides. Credentials come from the secrets subsystem, never from here. Max properties 20. |
| `method` | `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` | no | Defaults to `GET`. |
| `name` | string | **yes** | Identifies the request in the report. Min length 1, max length 64, matches `^[a-z0-9][a-z0-9-]*$`. |
| `path` | string | **yes** | The path and query, starting with a slash. Min length 1, max length 2048. |

## Resources

What one replica of a service is allowed to use. Absent means the runtime decides, which locally means no limit and on a cluster means the namespace default.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `cpu` | string | no | CPU limit, in cores or millicores. Defaults to `1`. Matches `^[0-9]+(\.[0-9]+)?m?$`. |
| `memory` | string | no | Memory limit. Defaults to `1Gi`. Matches `^[0-9]+(Mi\|Gi\|M\|G)$`. |

## Rolling compatibility

Run the previous release against the migrated schema and see whether its workflows still pass, which is the invariant a rolling deploy actually depends on.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `against` | string | no | Which commit the previous release is: merge-base, previous-commit, or any revision git can resolve, such as a release tag. Defaults to `merge-base`. Max length 256. |
| `when` | `never`, `risky`, `always` | no | risky runs the check only when the pending migrations contain a change the previous release could notice, such as a dropped or renamed column. always runs it for every migration, and costs a second image build and a second environment every time. Defaults to `risky`. |

## Runtime

Where and how long the environment runs. The provider decides the machinery; the rest is the lifetime, the address, and the naming the environment gets.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `domain` | string | no | Wildcard domain for environment hostnames. Defaults to localhost, which needs no DNS at all. Defaults to `localhost`. Max length 253. |
| `idle_sleep` | string | no | How long an environment may sit idle before it is scaled to zero. It wakes on the next request. Defaults to `30m`. Matches `^[0-9]+(m\|h)$`. |
| `kubeconfig_context` | string | no | Which kubeconfig context to use. Naming it prevents an environment landing on whatever cluster happened to be current. Max length 253. |
| `max_ttl` | string | no | The furthest af env extend may push an environment's expiry, measured from when it was created. A lifetime that can be extended forever is not a lifetime, and this is the bound. Defaults to `168h`. Matches `^[0-9]+(h\|d)$`. |
| `namespace_prefix` | string | no | Prefix for Kubernetes namespaces. Defaults to `af`. Max length 40. |
| `provider` | `local`, `kubernetes` | no | Defaults to `local`. |
| `ttl` | string | no | How long an environment lives before the reaper tears it down. Extend one you are still using with af env extend, up to max_ttl. Defaults to `24h`. Matches `^[0-9]+(h\|d)$`. |

## Service

One process the environment runs. A service is built from the repository, given the variables it declared, attached to the environment's private network, and started in dependency order.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `build` | [Build](#build) | no | How to turn the service directory into an image. |
| `command` | string | no | Command that starts the service, overriding the image's own. Executed with an argument vector, never through a shell. Max length 1024. |
| `depends_on` | list of string | no | Services that must be ready first. A cycle is rejected at validation. Max items 50. |
| `env` | list of [Environment variable](#environment-variable) | no | Names of environment variables this service needs. Names only. Values come from the secrets subsystem, and a name with no value anywhere fails with AF-SEC-001 rather than starting a service that will misbehave. Max items 200. |
| `health_path` | string | no | HTTP path that reports readiness. A service is not considered up until this returns a 2xx or 3xx status. Defaults to `/`. Max length 512. |
| `health_timeout` | string | no | How long to wait for readiness before failing with AF-RUN-004. Defaults to `180s`. Matches `^[0-9]+(ms\|s\|m)$`. |
| `kind` | `web`, `worker`, `cron` | no | What the service is. A web service gets a hostname and a readiness check; a worker gets neither; a cron service is invoked on a schedule instead of run continuously. Defaults to `web`. |
| `migrate` | string | no | Command that applies pending migrations. Run once against a fresh branch before the services start, and rehearsed with timing and lock analysis when insights are on. Max length 1024. |
| `name` | string | **yes** | Unique within the manifest. Appears in hostnames, logs, and container names. Max length 40, matches `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`. |
| `path` | string | no | Directory containing the service, relative to the repository root. Defaults to the root. A path outside the repository is rejected. Max length 512. |
| `port` | integer | no | Port the service listens on. Required for a web service unless detection found it. Minimum 1, maximum 65535. |
| `replicas` | integer | no | How many instances to run. Defaults to `1`. Minimum 1, maximum 10. |
| `resources` | [Resources](#resources) | no | What one replica of a service is allowed to use. |
| `schedule` | string | no | Cron expression for a cron service, with an optional CRON_TZ prefix. Evaluated in the declared zone. Max length 128. |

## Subset

Take a production shaped slice rather than the whole database. The closure is computed over foreign keys, so a subset always satisfies every constraint the schema declares.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | no | Defaults to `false`. |
| `follow_dependents` | integer | no | How many levels of rows that reference the seed to include. Zero includes only what the seed rows reference, which is the minimum that satisfies foreign keys. Defaults to `1`. Minimum 0, maximum 5. |
| `max_rows` | integer | no | Upper bound on rows per table, applied deterministically so two runs produce the same subset. Defaults to `1e+06`. Minimum 1. |
| `seed_table` | string | no | Table the selection starts from, for example the tenant or account table. Max length 128. |
| `seed_where` | string | no | A SQL predicate selecting the seed rows, for example created_at > now() - interval '90 days'. Max length 2048. |
| `virtual_relationships` | list of object | no | Relationships the schema does not declare as foreign keys but the application relies on. Without these, a subset can look complete and still break the application. Max items 200. |

## Workflow

One thing the agents do, written as a goal rather than a script. The runner decides the actions and verifies the outcome, so a workflow survives a redesign of the page it happens on.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `budget` | object | no | Hard caps. A workflow that exhausts its budget ends as blocked with the reason, never as a partial pass. |
| `description` | string | **yes** | What a person would do, in sentences. Say the goal and what proves it happened, not the selectors. Min length 10, max length 4000. |
| `expect` | list of string | no | Observations that must hold for a pass, written as sentences. These are assertions about what the user can see, not about the DOM. Max items 50. |
| `independent` | boolean | no | Whether this workflow can run at the same time as others. Workflows that share an environment run one at a time unless this says otherwise, because two agents mutating the same data produce failures nobody can reproduce. Defaults to `false`. |
| `name` | string | **yes** | Max length 64, matches `^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$`. |
| `persona` | string | no | Which persona runs it. Defaults to the first persona. Max length 40. |
| `start_path` | string | no | Where to begin. Defaults to the application root. Defaults to `/`. Max length 512. |
| `tags` | list of string | no | Max items 20. |
