Skip to content

Policy

Policy decides what may execute. It is evaluated before every operation, before authentication, and it is never optional and never skippable.

Location: wherever you keep it — point $RUNTIME_POLICY_FILE at it. Ownership: yours. Runtime never creates it, and never overwrites it.

Policy has no environment-variable overrides. An env var must never be able to widen governance — point RUNTIME_POLICY_FILE at a different file instead.

Schema version

Policy written by this release is schema v2. Absence still means legacy v1, unknown keys fail with the key and line, and the parse window is independent of the executable floor. A supported v1 document is migrated in memory without changing its bytes. The running binary publishes both the migration window in <Runtime Home>/specs/schema-contract.json and the strict v2 shape in <Runtime Home>/specs/policy-schema.json.

runtime config migrate --surface policy --file ~/.config/engineering-runtime/policy.yaml

Dry-run by default. --write is the guarded rewrite; it refuses a path inside the Runtime Home. The published window is <Runtime Home>/specs/schema-contract.json. See Upgrading.

Three layers, and only one of them is yours

Layer What it is Can you change it?
Compiled safety profile Invariants built into the binary: an operation no provider claims, a path that skips the audit write, a context-aware operation with no resolved target No. Not by any file, flag or Control Plane value
Enterprise policy A signed generation your Control Plane delivered Only from the Control Plane. Runtime verifies it on every read
Your declarative policy The document below Yes — this page is about it

The profile is a floor, not a stricter default. If it denies, the operation is denied, and nothing above it can vote otherwise. runtime config validate prints which version of it is in force:

Safety:   compiled-safety-profile/v3 (b4671f4efe53) — invariants no document can disable

Enterprise policy outranks your document rather than merging with it, and invalid enterprise state stops execution rather than falling back to yours — otherwise corrupting the bundle would be a way onto more permissive rules.

There is no off switch

A top-level enabled: false used to disable provider, binary and subcommand governance in one line. Policy schema v2 removes that key entirely, because a floor a file can remove is not a floor.

A legacy v1 document with enabled absent or true migrates safely in memory; Runtime does not rewrite it. A legacy enabled: false document gets a blocking message because removing it changes the document's stated meaning and needs an operator decision. Remove the key deliberately (or delete the document), then validate again. A v2 document containing top-level enabled is invalid.

Three governance surfaces

Surface Governs Block
Provider Governance runtime <provider> <operation> providers:
Binary Governance runtime command run <binary> allowed_binaries: + command_policy:
File Governance runtime files <operation> <path> file_policy:

When a provider chooses the CLI transport for an operation, both of the first two apply — the provider rule first, then allowed_binaries and command_policy on top.

File Governance

New in 0.6.0, and it is a breaking change. File access is default-deny: a policy document with no file_policy block permits no read, list, write, append or delete at all.

file_policy:
  read_roots:
    - "."                    # the working directory, resolved per request
  write_roots:
    - "."

  # Optional. These may only narrow the compiled budgets, never raise them.
  max_read_bytes: 1048576
  max_list_entries: 1000
  max_depth: 32
  max_edit_files: 1000
  max_edit_batch_bytes: 33554432
  max_edit_matches: 10000
  max_edit_diff_bytes: 1048576
  max_edit_seconds: 30

Read and write are separate authorities: read_roots permits read and list, write_roots permits write, append and delete, and neither implies the other. A path must resolve inside a granted root, so .. and an absolute path elsewhere reach nothing.

A root that does not exist on a given machine is skipped rather than failing the document — one policy can name the trees several installs use.

Choosing roots

"." is resolved per request, not once

The sample above uses "." because it works anywhere. It is resolved against the working directory of each command, so the grant moves with wherever you happen to be standing: the same files read succeeds in one directory and is refused in another. Convenient for a first run; a source of confusing denials once capabilities run from CI, a scheduler, or an agent.

Write absolute paths in a policy you intend to keep.

Grant read widely, write narrowly. They are separate authorities precisely so they can differ, and the consequences are not symmetric — a wrong read exposes a file, a wrong write destroys one.

file_policy:
  # Reading across the working tree is cheap.
  read_roots:
    - "/Users/you/projects"

  # Writing is not. Name the directories capabilities may change.
  write_roots:
    - "/Users/you/projects/scratch"
    - "/Users/you/projects/reports"

Do not grant write to a home directory or a tree of git checkouts. A root covers everything beneath it, so /Users/you includes ~/.ssh, ~/.aws, and the shell profile holding your tokens; a repository root lets a capability rewrite tracked source with nobody reviewing the change. Runtime refuses its own governance documents and the native credential stores, but it cannot know which of your files matter.

If a capability needs to write into a repository, name that repository — not the tree containing it.

Budgets

The three read/list fields and the five max_edit_* fields bound what a single operation may discover, read, transform and return. They may only be narrowed by a document — a larger value is ignored, because a limit a document can raise is a default rather than a limit.

Read/list result limits can return a stated truncation. Edit limits instead refuse the batch before writing, because a partially selected estate is not the operation the caller requested. Exact defaults, output behavior and the redaction pass applied on the way out are on the files provider page.

Provider Governance

providers:
  github:
    enabled: true
    denied:
      - api DELETE
  files:
    enabled: true
    denied:
      - delete

How rules match

Operations are discovered from the Provider that owns them, not from a list here. The consequences:

  • A provider with no entry is fully allowed. runtime github repo list works without a policy edit, and a new operation in a later runtime version never requires one.
  • denied prefixes match against "<operation> <args...>". A rule can deny a whole operation (delete) or one narrow case (workflow run deploy-prod.yml, api DELETE).
  • allowed, when non-empty, restricts the provider to those prefixes.
  • enabled: false disables the provider entirely.
  • Rules are evaluated on the operation, never the transport. A denial cannot be sidestepped by a provider moving an operation from REST to the CLI.

Governance is evaluated on what will actually execute

If a provider normalizes an argument, policy matches the canonical form. This closed a real bypass: the github provider upper-cases an api method, so api delete once slipped past the api DELETE denial and sent a genuine DELETE. Any normalization performed after the policy check is a bypass by definition.

A rule must name an operation that exists

The compiled default policy denies api DELETE rather than repo delete — because there is no repo delete operation. That rule would read like governance while enforcing nothing, and the destructive path is actually the api escape hatch.

runtime config validate

The report ends with policy warnings for any rule that can never match: a providers.<name> rule naming an operation the provider doesn't expose, a block for an unregistered provider, or a command_policy.rules.<binary> entry for a binary absent from allowed_binaries.

A denial that matches nothing is worse than no denial — it produces false confidence. Check the warnings after every policy edit.

Locking a provider down to an allow-list

providers:
  github:
    enabled: true
    allowed:
      - repo list
      - repo view
      - user get

Anything not prefixed by one of those is refused.

Allow-list → deny-list was a deliberate change

Earlier versions used a global allowed_commands list, which no longer exists. Installations that need an explicit allow-list must set providers.<name>.allowed. See Migration Guides.

Binary Governance

allowed_binaries:
  - gh
  - git
  - terraform
  - gcloud
  - kubectl
  - oc
  - aws
  - az
  - vault
  - helm
  - flux
  - gsutil
  - bq
  - docker
  - podman
  - kustomize
  - argocd
  - istioctl
  - pulumi
  - packer
  - sops

Only these binaries may execute at all through runtime command run. A binary absent from this list is refused, whatever the subcommand rules say.

Subcommand rules

admitted_helpers — letting a tool reach its credential helper

Some tools delegate authentication to a separate executable. docker finds its registry credentials by launching docker-credential-gcloud; git can be configured the same way.

Runtime resolves and pins the binary you asked for, then runs it in a bounded environment with a fixed minimal PATH — the caller's PATH is deliberately not forwarded, because that is what a substitution attack edits. A helper living in /opt/homebrew/bin is therefore unreachable, and docker push fails inside Runtime while the identical push succeeds outside it.

admitted_helpers opens exactly that door and no wider:

command_policy:
  admitted_helpers:
    docker:
      - /opt/homebrew/bin/docker-credential-gcloud

Four properties, each deliberate:

  • Absolute paths only. A relative path is resolved against something Runtime does not control. A non-absolute entry is rejected when the policy loads, not when a push fails later.
  • Scoped to one parent binary. A helper admitted for docker is not reachable by terraform. The admission is a pair, never a global allowance.
  • Only the named files are exposed. Runtime stages each admitted helper into a directory it controls and puts that on PATH. The helper's own directory is never added — admitting one binary in /opt/homebrew/bin must not admit the several hundred beside it.
  • Empty by default. With no admitted_helpers, nothing is admitted and the push still fails. Widening this is always a deliberate act.

Requires schema_version: 3 (Runtime 0.9.1 or later).

command_policy:
  # Refused outright even if they were ever added to allowed_binaries
  denied_binaries:
    - rm
    - sudo
    - chmod
    - curl
    - wget
    - ssh

  rules:
    git:
      denied:
        - push --force
        - push -f
        - reset --hard
        - clean -fd
        - filter-branch
        - update-ref -d
    terraform:
      denied:
        - destroy
    kubectl:
      denied:
        - delete namespace
    helm:
      denied:
        - uninstall

Matching semantics, which differ between the two lists:

List Matching
denied A contiguous run of argument tokens anywhere in the command
allowed Prefix match

Denials always win.

Because denied matches anywhere, a leading global flag cannot bypass a rule: git -C /some/dir push --force is caught exactly like a bare git push --force.

Subcommand rules are evaluated before Runtime Context injection, so an injected flag (--project=, -n <namespace>) can never be used to slip past a denial.

What the compiled default rules protect

The shipped defaults target one thing consistently: irreversible actions.

Binary Denied Why
git force-push, hard reset, clean, filter-branch, ref deletion Rewrites or discards history
terraform, pulumi destroy Tears down real infrastructure
helm, istioctl uninstall Removes a release or the mesh control plane
flux delete, uninstall Removes GitOps-managed resources or Flux itself
kubectl / oc delete namespace / delete project Bulk resource destruction
gcloud project and service-account deletion Identity and account loss
aws s3 rb --force, ec2 terminate-instances, iam delete-user, rds delete-db-instance Bulk data and identity deletion
az vm delete, group delete, ad user delete, keyvault delete Same profile as aws
vault kv delete, kv metadata delete, secrets disable Permanent secret removal
gsutil rm -r, rb Recursive object and bucket deletion
bq rm -f, rm -r -f, rm -f -r -f skips the confirmation prompt
docker, podman system prune -a, volume rm, run --privileged Bulk pruning; privileged run is a container-escape risk
argocd app delete, cluster rm, repo rm Detaches or deletes GitOps state

kustomize, packer and sops carry no denial rules — none of them has a subcommand that destroys existing infrastructure. kustomize only renders YAML to stdout, packer only builds new images, and sops encrypts and decrypts files in place.

Two denials that are not in your document

The compiled floor refuses two families of command regardless of what any policy — local or enterprise — says about them, because a denial a document can remove is a suggestion.

Credential export (secret-export-operation): the operation's output is a secret. gcloud auth print-access-token, kubectl get secret, vault kv get, aws secretsmanager get-secret-value, sops -d, git credential fill. A tool using a credential is untouched — that is the whole native-credential model.

Tool reconfiguration (tool-lifecycle-mutation): the operation changes the tool rather than the system you are operating on. gcloud config set, gcloud components install, kubectl config use-context, oc login, git config --global, helm plugin install, docker login, gh extension install. Reading the same configuration is fine (kubectl config view, gcloud config get-value project, git config --get).

The reason these are a floor rather than a rule: Runtime decides whether an operation is allowed by pinning the exact binary and reading where your tool is pointed. A command that installs a component into that install root or switches the current context changes what every later governed operation does. Do those in your own shell, deliberately.

See command run for the full lists and the exact refusal text.

Denials are audited

A denial is recorded exactly like a success or a failure. That is the point: the audit log answers "was this attempted and refused", not just "what happened".

runtime command run git push --force
runtime audit tail -n 1 --output json

The runtime tells you when it is the one refusing:

policy denied github <operation>: ...

Common tasks

providers:
  files:
    enabled: true
    denied: []          # remove the `delete` rule
providers:
  github:
    enabled: true
    denied:
      - api DELETE
      - workflow run deploy-prod.yml
allowed_binaries:
  - gh
  - git
  - my-internal-cli

command_policy:
  rules:
    my-internal-cli:
      denied:
        - teardown
providers:
  github:
    enabled: false
file_policy:
  read_roots:
    - /srv/checkouts
  write_roots:
    - /srv/checkouts/scratch   # read the tree, write only one part of it
providers:
  github:
    enabled: true
    identities:
      - release-bot

Checked after authentication, because the ordinary rules are evaluated before any credential is touched — at that point nobody has asked GitHub who is acting. An unresolved identity never satisfies the list.

After any edit:

runtime config validate     # confirms the rule can actually match something

New policy defaults never reach an existing install

An env-selected community policy is yours, so a release adding allowed_binaries entries changes nothing for a machine that already selected a file. runtime config validate summarises the difference; runtime config diff policy lists every binary. See the upgrade trap.

Operational examples on this site were verified against Runtime 0.9.8. After bootstrap, the version-exact files in Runtime Home win.