Three YAML formats, and which one is real
Three YAML formats for a policy exist in this toolkit, they are mutually incompatible, and the one its own tooling writes is not the one the loader reads, so a file that looks governed can be silently empty.
Last updated: 14 Oct, 2026 · Agent Governance Toolkit 4.1.0
The Policy scope lesson showed one field quietly doing nothing. This lesson is larger and sharper: the loader drops what it does not recognise and fails open, the file the command line generates loads as empty, and the linter and the loader disagree on what a valid rule even is. Each is proven with a run.
The loader drops what it does not recognise
PolicyDocument.from_yaml keeps the fields it knows and silently discards the rest. This file carries a bogus_top key at the top and a severity key on its rule. Both vanish, and with no defaults block the policy fails open.
version: "1.0"
name: drops
bogus_top: hello
rules:
- name: allow-refund
condition: {field: action, operator: eq, value: refund}
action: allow
severity: criticalimport warnings; warnings.simplefilter("ignore")
from agent_os.policies.schema import PolicyDocument
doc = PolicyDocument.from_yaml("drops.yaml")
# the unknown top-level key and the unknown rule key are both gone
print("has bogus_top:", hasattr(doc, "bogus_top"))
print("rule fields :", list(doc.rules[0].model_dump().keys()))
print("default set? :", doc.defaults.action.value)has bogus_top: False rule fields : ['name', 'condition', 'action', 'priority', 'message', 'override'] default set? : allow
Nothing told you the two keys were ignored. A typo in a field name, or a key from a different format, is dropped in the same silence, and a file with no default lets unmatched calls through.
The file the command line writes loads as empty
Running agentmesh init writes a starter policy to policies/default.yaml. It uses a nested policies list, each with its own rules that carry a list of conditions. That shape belongs to a different engine.
policies:
- enabled: true
id: default-security
name: Default Security Policy
rules:
- action: block
conditions:
- output contains 'password'
- output contains 'api_key'
- output contains 'secret'
id: no-secrets-in-output
message: Potential secret detected in output
- action: block
conditions:
- peer_trust_score < 500
id: require-peer-trust
message: Peer trust score below thresholdLoaded by from_yaml, every one of those keys is unknown at the top level, so all of it is dropped. The document comes back named unnamed, with zero rules, and the fail-open allow default, a policy that blocks nothing.
from agent_os.policies.schema import PolicyDocument
# load the exact file agentmesh wrote (shown above)
doc = PolicyDocument.from_yaml("default.yaml")
print("name :", doc.name)
print("rules :", len(doc.rules))
print("default:", doc.defaults.action.value)name : unnamed rules : 0 default: allow
The linter does not agree it is empty. Run agt lint-policy on the very same file and it reports three hard errors, for the version, name and rules fields it cannot find at the top level.
$ agt lint-policy policies/default.yaml
policies/default.yaml:1: error: Missing required field 'version'
policies/default.yaml:1: error: Missing required field 'name'
policies/default.yaml:1: error: Missing required field 'rules'
3 error(s) found.The linter and the loader disagree on a rule
The two tools do not even share a schema. This file writes its one condition as a plain string instead of a {field, operator, value} dictionary. The linter accepts it and reports no issues.
version: "1.0"
name: strcond
rules:
- name: r1
condition: "action == refund"
action: allow
defaults:
action: deny$ agt lint-policy strcond.yaml
No issues found.The loader rejects the identical file. from_yaml validates the condition against its schema, finds a string where a dictionary belongs, and raises before the policy can be used.
from agent_os.policies.schema import PolicyDocument
try:
doc = PolicyDocument.from_yaml("strcond.yaml")
print("loaded:", doc.name)
except Exception as e:
# the loader demands a dict condition; the linter was happy with a string
lines = str(e).splitlines()
print(type(e).__name__, "at", lines[1].strip())
print(lines[2].strip().split(" [")[0])ValidationError at rules.0.condition Input should be a valid dictionary or instance of PolicyCondition
What the three formats mean for you
- The loader's format is the real one. A top-level
version,name,rulesanddefaults, with each rule's condition a{field, operator, value}dictionary, is whatPolicyDocument.from_yamlreads. - The generated format is a trap. The nested
policiesshape thatagentmesh initwrites loads as an empty, fail-open policy, so a file that looks configured governs nothing. - The linter is not the loader.
agt lint-policypassing a file, or failing it, tells you nothing reliable about whetherfrom_yamlwill read it the way you meant.
Three shapes, one reader
| Shape | from_yaml reads it as | agt lint-policy says |
|---|---|---|
Top-level rules, dict conditions | The real policy | Passes a correct file |
Nested policies (generated) | Empty, fail-open | Three missing-field errors |
| A rule with a string condition | Raises a validation error | No issues found |
Where this bites
- Trusting a generated starter file, where the
agentmesh initoutput needs rewriting into the loader's format before it governs anything. - Trusting a green lint, where a file the linter passes can still fail to load or load as empty.
- Reviewing a policy, where you load it with
from_yamland print the rule count, rather than reading the YAML and assuming it parsed.
from_yaml and asserting it has the rules and the default you expect. A clean lint and a plausible-looking file both pass while the loaded policy governs nothing, which is the worst way for a gate to fail.Related
- Previous: Policy scope
- Next: Permissions that depend on the context
- See also: Governance is opt-in: the fail-open default
- Add your own unknown key to
drops.yaml, reload, and confirm it is dropped with no warning. - Rewrite the
agentmesh initfile into the loader's format and confirmfrom_yamlnow reports the rules. - Fix the string condition into a
{field, operator, value}dictionary and watchfrom_yamlload it.
This is what real progress feels like.