settings: three tiers - wikitool.json (instance), .wikitool-local.json (checkout), secrets by reference incl. 1Password CLI; new command config set (#207)
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
This commit is contained in:
1 parent
059a0a1840
commit
f6aaadc413
40 files changed
+2246
-31
No files matched your search
@@ -18,5 +18,7 @@ lives in `AGENTS.md` or a `CONTRACT.md`, and a page here only gives its reasons.
|
||||
and a migration question separately
|
||||
- [knowledge-and-commitment.md](knowledge-and-commitment.md) - why commitments live in a task
|
||||
tracker rather than in `kb/`
|
||||
- [configuration-tiers.md](configuration-tiers.md) - why settings sit in three tiers, and what
|
||||
keeping secrets out of the tree does not protect against
|
||||
- [model-and-effort-selection.md](model-and-effort-selection.md) - which model and effort suit
|
||||
which kind of session, for the harness that has to choose
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user