guide/05-config.md
Config: the contract
Standing: a contract, 2026-11-13. It describes what design c146 (Config File) built in this app as qnative c11: on main as 313bd7c, with the guide's wording fixes in 9daa5f8. Facts about qcontrol were read from crates/qconfig on qcontrol main (8677b44ed). Where something is not built, or waits on someone, the line says so. The rulings behind it are in design cycles/c146.config-file/plan.md (R1–R8, Q1–Q6) and its eval in eval.md.
The Config page answers one question: where does this machine's sensor configuration come from? It is a V1 page under This machine, beside Sensor (Route::Config, crates/app/src/pages/config.rs), not a Dev page (c146 R3 overrules c138 ruling 6). This page stays in the developer-mode guide because it began here.
What qcontrol has today
- One file,
config.yaml(the app reads it at~/.config/qcontrol/config.yaml). Every section is strict: an unknown key is an error. - An
includelist of local globs, for exampleconf.d/*.yaml, merged into it (crates/qconfig/src/include.rs). An included file may holdtargetsandbehaviorsonly. - A built-in stock document, used when the file is missing (
crates/qconfig/src/stock.rs). It defines three behaviors (audit,io-log,openshell-enforce) and applies none. $__<store>{KEY}names a secret (qsecrets). A string is resolved only when it is exactly the reference.- Nothing that loads configuration from a URL, and no entry point that checks a document without loading it.
The three modes
The labels are Policy Config's vocabulary (c146 R2) and were kept at review stop 1 (Q1). They are Mode::label() in crates/app/src/config/mod.rs; the ids are what config-state returns.
| Label | Id | The person's sentence | What the app does |
|---|---|---|---|
| Edit here | manual | "I keep my configuration on this machine." | Shows the file as written and as it takes effect, in an editor with a check on every keystroke. Starters fill the editor. |
| Managed by your company | managed | "My configuration comes from my company's plane." | Shows the file the plane sends, read-only, and says who manages it. The other modes are locked. |
| From a published file | published | "Start me from a file published at an address." | Fetches the file once, shows it, checks it, and writes it when the person confirms. Then it is this machine's file to edit. |
A connected plane decides the mode. Otherwise the mode is the person's pick among the three tiles.
Edit here
States: no file (the stock document is in force, and the page opens on Pick a starter: three cards or an empty file), valid, invalid (each problem marked on its line), unsaved edits, applied ("Applied as v3", with Revert), and an agent's draft (below).
- The editor is gpui-component's code editor (YAML, line numbers) inside QGUIkit's
EditorFrame. Nothing custom (R8). YAML highlighting (the tree-sitter feature) is not turned on yet. - Two views of one file: Written and Effective. Effective lists each target with its behaviors and what they run, where events are sent, the tray and the dashboard.
- The starters sit beside the editor (Q3): one library hosted by Qpoint, of which the app highlights three. Each is a whole
config.yaml, checked:Id Name For Does recommendedRecommended Most people Audits every AI harness. developerDeveloper laptop People who run coding agents Adds command and file capture, and holds coding agents' model traffic to an allowlist (OpenShell). dataData or analyst laptop People who work with customer data Keeps a full trace of what every harness sends and writes, in a local file.
The library's address ishttps://qpoint.io/configs/<file>, a fixture until the docs site hosts it. The files are incrates/app/config/starters/. - Versions (Q4): every apply is a new version, newest first, saying what changed and who made it ("you", or the agent whose draft it was). Each has Restore, which loads that version into the editor; applying it makes a new version.
Managed by your company
States: not connected (an address and a token), signing in, connected and read-only (saying who manages it), refused, not answering, and an agent refused.
- Connected is read-only. The page names the company and who to ask.
- An agent's proposal is refused with: "Managed by your company. Changes are made there." This is C9's first pass (below).
- How an endpoint enrols and what credential it holds are not decided (design c131 D13, c134). The mode runs against the fixture plane: Meridian's file, labelled "Policy Config v13" (
crates/app/config/hq-meridian.yaml). That label is an assumption until Tyler answers Q6.
From a published file
Fetched once (c138 ruling 4, kept as c146 R1):
- The person gives an address, or picks a file from Qpoint's library.
- The app fetches the file and shows all of it, with its hash.
- The app checks it. An invalid file stops here, with every problem, and nothing is written.
- The person confirms.
- The file replaces
config.yamlwhole (Q2). The file it replaces is kept as a version, so Restore brings it back. Its record of origin is a comment block at the top:# source: https://qpoint.io/configs/developer-laptop.yaml # fetched: 2026-11-13T16:20:00Z # hash: 9f2c… - From then on it is this machine's file, edited as in Edit here. Nothing follows the address. Check for changes fetches it again and shows the difference before the person confirms; when nothing changed, it says so.
Why the whole file and not an include: an include file may hold only targets and behaviors, so a published file couldn't carry sinks, secrets or ui (Q2 supersedes this page's earlier conf.d/<name>.yaml landing). Why once, and not followed: a configuration decides what the sensor intercepts and enforces. An address that could change it later, with nobody looking, would be a remote control.
Not built yet: fetching any https address waits for the docs site's library; today only the library's own files fetch. The hash is a stand-in (config::digest) until the real fetch hashes with SHA-256.
The agent drafts, the person applies
An agent can change the file only by proposing a draft (R4):
- The agent reads, checks and proposes, over the app's own MCP server,
qpoint-guide-native(R5). It never writes the file, and there is no write tool. - The draft is always the whole
config.yaml(R7), shown on the page as a difference against the file in force, with who proposed it, what they were asked, and one sentence on what changes. Include files are left as they are. - The person presses Apply (a new version: "Proposed by
, applied by you") or Discard. Apply is disabled while the draft doesn't check. - A new proposal replaces any earlier draft from an agent.
- The agent points at Apply and never presses it, the same rule as the guide's tours and Simulation mode. It points with region id
config.draft.
The server instructions carry this (crates/app/src/config/tools.rs, INSTRUCTIONS, and one line at the top of crates/app/src/guide/mcp.rs). In short: asks about what qcontrol watches, audits or enforces, where its events go, or its tray and dashboard are config asks, so use the config tools, not find-in-app. Read config-state and config-read first, and config-reference for the sections you'll touch. Write the whole file, keep what the person had, run config-check, fix every error, then config-propose. Say what changes in the person's words, never "I changed it".
The six tools
All six are on qpoint-guide-native, beside the guide's own seventeen (crates/app/src/config/tools.rs).
| Tool | Takes | Returns |
|---|---|---|
config-state | nothing | mode and modeLabel; path; file (found, none with the stock document in force, or sent by the plane); managedBy (org, ask) when managed; draftWaiting (by, asked); origin (url, fetched) for a published file; versions (n, what, by, at, newest first); agentsCan; writesTo |
config-read | draft (optional, boolean) | path; written (the file as written, or null when the stock document applies); stock (the stock document in words, when there's no file); effective (targets with title, if and behaviors with what each runs; sinks; tray; dashboard); check (the file's own check). With draft: true, the draft waiting for the person instead, or an error when none is. On a managed machine, the plane's file. |
config-reference | section (optional: include, secrets, sinks, ui, behaviors, targets) | The reference read from qconfig's schema: each section's type, meaning, keys and an example; rules (the fields, functions and operators of an if: rule, with examples); harness ids (qcontrol's: claude-cli, claude-desktop, cursor-cli, codex-cli, aider, windsurf, ollama). One section comes with rules and harness ids. |
config-starters | nothing | Each starter's id, name, for, does, url and full text |
config-check | text (the whole file) | { ok, issues }, in the shape below. Changes nothing. |
config-propose | text, asked (the person's words), summary (one sentence) | proposed: true, warnings, diff (folded to two lines of context), and next ("point with id config.draft, don't press it"). The window opens the Config page. Refused, as an error, when the document has errors, when it is the file already in force, or when the plane manages the file. |
The check
One checker serves the page and the agent (R5), crates/app/src/config/check.rs. It returns:
{
"ok": false,
"issues": [
{
"line": 13,
"path": "behaviors.audit.plugin",
"message": "Unknown key `plugin` in behaviors.audit.",
"hint": "Did you mean `plugins`?",
"level": "error"
}
]
}
lineis 1-based in the document.pathis the dotted key path.hintis the nearest valid key, or what was expected ("Expected one of: …", "Defined: audit, io-log."), and is left out when there's nothing to say.leveliserrororwarning. An error stops Apply and refuses a proposal; a warning doesn't.okis true when there are no errors.- What it checks: the strict schema (unknown keys, wrong types), required fields, an empty target title, and targets that name a behavior not defined under
behaviors. The issues are sorted by line. - A refused proposal is an error whose text says how many problems there are and carries the issues: "Not proposed: one problem. Fix them and propose again." followed by the issues as JSON.
It is a stand-in for qcontrol's check (R6). It parses with qconfig's own parser (serde-saphyr) into structs that mirror qconfig's schema on main. The eval found that it drifts (c146 eval.md, F1): of fifteen documents, twelve that qconfig refuses pass it (among them a relative file:// sink, malformed if: rules, a ~ in a secrets path, and behavior env, which qconfig doesn't have), and two that qconfig accepts are refused (a plugin's path, inject.wait / tree_wait). None of the eval's drafts hit a gap, and every clean draft also loaded in qconfig. The promise that an invalid draft never reaches the person holds only as far as the mirror goes. The recommended fix is to link qconfig itself into the app by git, which is a qnative cycle of its own and is not ruled yet.
What the eval measured
Twelve plain instructions (c146 intents.md), each given to a fresh headless Claude Code on Opus 5.5 with only this server, on synthetic world meridian-day90:
- On the merged build (
313bd7c), 9 of 12 passed, and every proposal the agent made checked clean the first time (8 of 8 that drafted; the one meant to be refused was refused). - The three misses: two asks were routed to find-in-app and never reached the config tools ("turn the tray off", "stop watching Aider"), and one draft passed both checkers but put a prefix before a secret reference, which qsecrets would send as literal text.
- Wording fixes (F2–F4, F6's W2, F7–F9) made all three pass, and landed as
9daa5f8.
On synthetic data
With the Sensor page set to synthetic, all three modes act on the world's configuration. Nothing is read from or written to this machine's real configuration (c138 R4's guard).
Still open
Engineering (c146 plan.md § Asks; until they are answered, the page says honestly when a change takes effect):
- A check entry point that takes a document and returns each error with its key path and line, without applying it (R6). Until then, the stand-in above.
- Who may write
config.yamlwhen the monitor runs elevated. If the app can't, what applies a change: a command, the control socket? Until this is answered, Apply writes to the app's own record only (config-statesays so inwritesTo). - Whether the monitor picks up a changed file without a restart.
- One icon, one switch (the walk's W1, with c131 D7):
ui.traygoverns the single menu-bar icon, whoever draws it. The app honours it, and qtray never runs beside the app. Engineering is asked to confirm. - The effective configuration as the monitor loaded it: design c131 D17 names
EffectiveConfigandConfigFilefor/api/config. qcontrol doesn't serve it yet; the app reads the file.
Others:
- Tyler (Q6): does the company's Policy Config reach the sensor as this
config.yaml, or alongside it? What in the file says a plane is expected (also design c145's question)? - Mark Peterson (C9): on a managed machine, may the agent explain a change instead of drafting it ("tell me first, so I can fix it on my own")? Until he answers, the proposal is refused with "Managed by your company. Changes are made there.", and the agent can still read and explain the file.
- The docs site: hosting the starter library at a public address.
Here, in qnative:
- F1: link qconfig as the check, or fill the mirror's gaps.
- c13 carries two of the walk's lines: the guide's tray region gets its full title, "Show messages in the tray" (W3), and the sentence in
config-reference'sui.traythat names Messages › Settings is updated to the switch's new place (W2).
Settled: the sensor never fetches a configuration from an address. Fetching is the app's job, and it fetches once (R1).