Agent-First Config v0.2: An Edit Changes One Thing
v0.2 makes every config mutation a targeted source edit — set/unset/add/remove touch only the addressed span and leave comments, ordering, quoting, and datetimes byte-identical, or fail before writing. Plus first-class dotenv and INI backends, four secret input sources, and a CLI that is an Agent-First Data client end to end.
Setting one field in a config file should change one field. v0.1 didn’t quite
promise that. Under the hood every write loaded the whole file into a generic
Value, mutated one node, and re-serialized the entire thing back out. For JSON
that reordered keys and reset your whitespace; for TOML it dropped comments and
turned a datetime into a quoted string; for YAML it flattened anchors and could
retype an untouched float into a string. The field you asked for changed — and so
did everything around it.
Agent-First Config v0.2 makes the mutation surface honest. set, unset,
add, and remove now edit only the span you addressed and leave every other
byte exactly as it was — or they fail before touching the file. It is a breaking
release; config is still pre-1.0 and carries no compatibility layer.
One field, one span
Given this TOML:
# database connection
[db]
host = "localhost" # primary
port = 5432
started = 2026-01-01T00:00:00Z
afconfig db.toml set db.port 5433 used to round-trip the whole document: the
comments vanished, started came back as a quoted string, and the keys could
reorder. In v0.2 the same command changes exactly one span:
# database connection
[db]
host = "localhost" # primary
port = 5433
started = 2026-01-01T00:00:00Z
Everything else is byte-identical. Each backend now holds the original source and edits in place:
- JSON — a span-tracking document. Replace, insert, or remove only the target member; a new key follows its neighbours’ indentation.
- TOML —
toml_edit, so comments, table order, and datetime syntax survive. - YAML — the
noyaliblossless CST: comments, anchors, block/flow style, and scalar style are all preserved. - dotenv & INI — source editors that rewrite only the one affected line.
When a change can’t be made without losing fidelity — a collection shape the
editor can’t express, a value a format can’t represent — it is a structured
unsupported error, not a silent rewrite. The rule underneath:
An edit changes the thing you addressed. Nothing else moves, and if it can’t be done cleanly, nothing is written at all.
Two new formats, same rule
.env and INI join JSON/TOML/YAML as first-class, source-preserving backends —
and both ship with a written-down dialect instead of “whatever some parser does.”
dotenv is deliberately side-effect free: every value is a string, $VAR and
${VAR} stay literal, and nothing expands variables, reads the process
environment, or merges overlay files. TOKEN=abc#def keeps its #; a comment
only starts at # with leading space. Duplicate keys are an error, not
last-wins.
INI Core v1 is a small, deterministic subset: [section] plus key=value,
one level deep, exact case, string values, #/; comments only at line start.
No interpolation, includes, bare keys, or duplicate sections — the ambiguous
corners of the format are simply rejected.
Each format is its own Cargo feature (json, toml, yaml, dotenv, ini),
all on by default, so a library consumer can compile just the backend it needs.
Secrets are inputs, not arguments
Writing a secret used to mean putting it in argv, where it lands in shell
history, process listings, and the success payload echoed back to stdout. v0.2
gives secrets four dedicated, mutually-exclusive sources — pick by how the value
reaches you:
afconfig app.json set api_key_secret --value-secret-stdin < key.txt # a pipe
afconfig app.json set api_key_secret --value-secret-prompt # a human
afconfig app.json set api_key_secret --value-secret-fd 3 3< key.txt # automation
afconfig app.json set api_key_secret --value-secret "$K" # convenient, weakest
The value never enters the argv diagnostics, logs, errors, or the success event.
A targeted get api_key_secret still returns the real value — you explicitly
asked for it — while show and every non-targeted surface redact _secret fields
and any exact name you name with --secret-name.
The CLI is an AFDATA client, end to end
Every path — success, domain error, argument error, help, version, broken pipe —
now speaks Agent-First Data:
structured events, --output json|yaml|plain, conventional -V, and predictable
exit codes. The one deliberate exception is get --value-only, which prints a
bare scalar for shell command substitution:
value=$(afconfig config.json get service.host --value-only) && curl -- "$value"
The assignment carries the exit status, so a failed read short-circuits the &&
instead of feeding a broken value downstream.
Upgrading
- Keyed-list
add/removenow require--slug-field <FIELD>. The old hard-codedidentities → identityspecial case is gone; pass the identity field explicitly, and nested paths likecfg.usersnow work. default,doc, andschemacommands were removed. They returnedimplemented:falsewith a zero exit; the renderer stays as a library API.- Dot-paths use
\.and\\escaping. Greedy two-segment key matching is gone; address a literal dot inside a key with\.. - Explicit type prefixes fail loudly.
b:maybe,i:1.5, and malformedj:JSON now error instead of silently falling back to a string orfalse. - Mutations refuse symlink and hardlink targets and write atomically (temp + fsync + rename, preserving mode). Reads still follow links.
Install:
cargo add agent-first-config # library, all backends
cargo install agent-first-config --features cli # the afconfig binary
brew install agentfirstkit/tap/afconfig
An edit changes one thing now. The rest of your file — the comments you wrote, the order you chose, the datetime you typed — stays exactly where you left it.