Every settings field declares its tier; a value in the wrong file is an error naming
file, key and tier. A field naming a program or affecting a gate cannot be declared
instance. Secrets are references only: {"ref": "<name>"} via WIKITOOL_SECRET_<NAME> or
a user-only secrets file, {"ref": "op://..."} via op read (locked 1Password: exit 42).
The preflight records op as an optional tool. doctor gains one line per tier, docs
verify checks a committed wikitool.json for plaintext secrets, CI resolves a real
op:// value through 1password/load-secrets-action. No feature reads its settings
this way yet; without the two files every other command is unchanged.
Files changed:
- .gitea/workflows/ci.yml
- .gitignore
- AGENTS.md
- CHANGES.md
- INSTALL.md
- VERSION
- docs/explanation/README.md
- docs/explanation/configuration-tiers.md
- docs/how-to/README.md
- docs/how-to/use-1password-for-settings.md
- docs/reference/commands/README.md
- docs/reference/commands/configuration.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/types-instructions-and-docs.md
- docs/reference/configuration.md
- docs/reference/exit-codes.md
- instructions/dev/doc-pull-through.md
- instructions/dev/testing-conventions.md
- instructions/preflight.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/_util.py
- tools/chemenu/commands/config_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/config.py
- tools/chemenu/config_shapes.py
- tools/chemenu/prerequisites.py
- tools/chemenu/settings.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/tests/test_settings.py
- tools/chemenu/toolpaths.py
- tools/preflight.ps1
- tools/preflight.sh
- tools/prerequisites.txt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
86 lines
5.0 KiB
Markdown
86 lines
5.0 KiB
Markdown
# Configuration tiers
|
|
|
|
Settings are split into three tiers: what belongs to the instance (`wikitool.json`, committed),
|
|
what belongs to one checkout (`.wikitool-local.json`, gitignored), and secrets, which sit
|
|
outside the working tree and are reached only by reference. The files, their fields and the
|
|
command that writes them are in [the configuration reference](../reference/configuration.md#tiered-settings);
|
|
the rules an agent follows are in [AGENTS.md § Settings](../../AGENTS.md#settings). This page is
|
|
about why it is built this way, and what it does not do.
|
|
|
|
## Three kinds of data that used to share files
|
|
|
|
Before the tiers, every feature kept its settings in a gitignored file of its own. Three kinds of
|
|
data ended up mixed in them:
|
|
|
|
- **What belongs to the instance.** The weekly review's thresholds are a decision about the
|
|
corpus, and every clone of it should make the same one. Gitignoring them meant each clone had
|
|
to be told again.
|
|
- **What belongs to one checkout.** A tool path, the push URLs a checkout may publish to, the
|
|
consent to record telemetry on this disk. Committing any of these would hand every other clone
|
|
an answer that is wrong for it rather than missing.
|
|
- **Secrets.** A tracker token sat in plain text in the working tree, protected only by
|
|
`.gitignore` - one wrong pattern away from a commit, and in plain view of every agent that read
|
|
the file in the course of ordinary work.
|
|
|
|
The tiers give each kind one place, declared per field, so the question "where does this go" is
|
|
answered once, by whoever declares the field, rather than each time by whoever writes a value.
|
|
|
|
## One place per value, not an override
|
|
|
|
A familiar design lets the local file override the committed one. It is deliberately not this
|
|
one: a value in the wrong file is an error. With two possible sources for one value, every
|
|
reader first has to work out which one wins, and the answer differs between clones - a reviewer
|
|
looking at `wikitool.json` would no longer know what the instance actually runs with. One place
|
|
per value keeps the committed file a true statement about every clone.
|
|
|
|
## What runs, and where it pushes, never travels with a pull
|
|
|
|
A field that names a program to run, or that arms or loosens a gate, is never in the committed
|
|
file - not even as a reference, since a resolved reference is still a value someone else chose.
|
|
Otherwise a `git pull` from another clone could change what `wikitool` executes on this machine,
|
|
or where `publish` sends this checkout's content. The rule is checked when a field is declared,
|
|
in code, so no reader has to remember it; the reasoning is the same as for the gates
|
|
([why-gates-are-code.md](why-gates-are-code.md)). The same fields are also not written by
|
|
`config set`: like the Publish-Remote Gate's allowlist, they change by the user's own edit.
|
|
|
|
## References instead of values
|
|
|
|
A secret is only ever a reference, and a private value - a host name, an internal URL - may be
|
|
one. A reference names where the value lives: a name, which resolves through a
|
|
`WIKITOOL_SECRET_<NAME>` variable or a secrets file in the user's own configuration directory, or
|
|
an `op://` URI, which resolves through the 1Password CLI.
|
|
|
|
That is what makes the committed file portable. An instance that is public commits
|
|
`wikitool.json` publicly, so a private value can never sit there as text; as an `op://`
|
|
reference it can, and every machine signed in to the same 1Password account resolves it to the
|
|
same value. In CI, `op` runs with a service-account token and resolves the same `op://`
|
|
references. Alternatively, 1Password's own action resolves values into variables before
|
|
`wikitool` starts, which serves references by name. This repository's CI does the second.
|
|
|
|
A reference is resolved only when a command asks for that one value, never when the files are
|
|
read, and the value stays in that process's memory. `doctor` reports whether each reference
|
|
resolves, never what it resolves to.
|
|
|
|
## What this does not protect against
|
|
|
|
Keeping secrets out of the tree protects against three things: a secret being committed, a
|
|
secret sitting in a file an agent reads while doing ordinary work, and a secret appearing in
|
|
`wikitool`'s output.
|
|
|
|
It does not protect against an agent with shell access that sets out to read one. If 1Password
|
|
is unlocked in the terminal, the agent can run `op read` itself. It can read the secrets file. In
|
|
general, whatever the `wikitool` process can read in a user account, an agent running in that same
|
|
account can read too. The stack does not claim otherwise.
|
|
|
|
What limits the exposure without preventing it:
|
|
|
|
- a 1Password vault of its own for this instance's entries, so an unlocked `op` reaches nothing
|
|
else;
|
|
- the approval prompt of the 1Password desktop app, which asks a human before each new process
|
|
reads a value;
|
|
- in CI, a service account that sees only that vault.
|
|
|
|
The rule in [AGENTS.md § Settings](../../AGENTS.md#settings) - a secret never passes through the
|
|
agent - is an instruction, not a barrier. The tiers make following it the easy path. They do not
|
|
make breaking it impossible.
|