Agent-First Config v0.2: An Edit Changes One Thing

by Agent-First Kit Contributors

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:

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

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.