Files changed: - CHANGES.md - INSTALL.md - VERSION - docs/explanation/configuration-tiers.md - docs/how-to/README.md - docs/how-to/back-up-to-a-synced-folder.md - docs/reference/commands/README.md - docs/reference/commands/git.md - docs/reference/commands/instance-health.md - docs/reference/configuration.md - instructions/publish-cycle.md - instructions/setup-instance.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/backup.py - tools/chemenu/cli.py - tools/chemenu/cli_contract.py - tools/chemenu/commands/backup_cmd.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/git_publish.py - tools/chemenu/config_shapes.py - tools/chemenu/reference_doc.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_backup.py - tools/chemenu/tests/test_config_shapes.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
114 lines
6.7 KiB
Markdown
114 lines
6.7 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.
|
|
|
|
<!-- wikitool:toc -->
|
|
## Contents
|
|
|
|
- [Three kinds of data that used to share files](#three-kinds-of-data-that-used-to-share-files)
|
|
- [One place per value, not an override](#one-place-per-value-not-an-override)
|
|
- [What runs, and where it pushes, never travels with a pull](#what-runs-and-where-it-pushes-never-travels-with-a-pull)
|
|
- [References instead of values](#references-instead-of-values)
|
|
- [What this does not protect against](#what-this-does-not-protect-against)
|
|
<!-- /wikitool:toc -->
|
|
|
|
## 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 plain private value in `.wikitool-local.json` stays on this machine only as long as that file
|
|
does. A backup into a synced folder (the `backup` section) copies the file as it is, because it is
|
|
the one record of how this checkout is set up, and a restore without it starts from nothing. That
|
|
trade is the operator's, made by writing the section by hand. A reference keeps the value out of
|
|
that copy as well, and the secrets file is never in it: it lies outside the instance, and a backup
|
|
reads nothing outside it.
|
|
|
|
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.
|
|
|
|
One secret has no reference at all: the session `raw login` stores for a website behind a login.
|
|
The user never types it, so nobody could put it into a secrets source by hand. `raw login` writes
|
|
it into the secrets file itself, under a name derived from the host, and `raw fetch` finds it by
|
|
that name. It never goes to 1Password, because it changes with every sign-in and the stack only
|
|
ever reads from there. So it sits in the one source the 1Password safeguards below do not cover.
|
|
|
|
## 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 the CLI may read
|
|
anything - per terminal session, not per value, so it stops a session nobody approved and not
|
|
one that already was;
|
|
- in CI, a service account that sees only that vault.
|
|
|
|
None of these covers a website session from `raw login`, which lives in the secrets file only.
|
|
Whoever can read that file can use the website as the user, until the session expires or the user
|
|
signs out there.
|
|
|
|
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.
|