Configuration

jawbd's config.toml: the purchase gate, approvals, and local-network access.

jawbd reads one optional TOML file. It sets whether purchases need approval, how purchases are detected, how approvals are collected, and a default for local-network access.

Where the file is

<data_dir>/config.toml, next to daemon.token and your profiles:

OSDefault path
Windows%LOCALAPPDATA%\jawb\config.toml
Linux$XDG_DATA_HOME/jawb/config.toml, else ~/.local/share/jawb/config.toml
macOS~/.local/share/jawb/config.toml unless XDG_DATA_HOME is set

There is no separate override variable. A different --data-dir or JAWB_DATA_DIR moves the config file with everything else, so a scratch daemon never reads your real configuration.

The file is optional: without it, every key has its default. jawbd creates it the first time you change a key (jawb config set, or the API below), with a first line that links back to this page. It reads the file when it starts and when you ask it to reload (below); it does not watch the file, so a hand edit takes effect on the next reload or start.

A broken file never stops the daemon and never makes it weaker. An unparseable file gives every key its default and the warning W_CONFIG_INVALID (with line and column). An unknown key is ignored with W_CONFIG_UNKNOWN_KEY. A known key with a bad value takes its default, with W_CONFIG_INVALID. The warnings appear in GET /v1/status. No flag or environment variable overrides a key.

# jawb configuration. Docs: https://jawb.app/docs/config
version = 1

[purchase]
gate = "approve"        # deny | approve | allow
detection = "strict"    # strict | standard

[approval]
assurance = "verified-if-available"
channels = ["desktop", "prompt", "terminal"]
timeout_s = 120

[egress]
allow_private_default = false

[profiles.shopping.purchase]
gate = "deny"           # this profile never buys

Keys

KeyValuesDefaultSafety key
version11no
purchase.gatedeny, approve, allowapproveyes
purchase.detectionstrict, standardstrictyes
approval.assuranceverified, verified-if-available, assertedverified-if-availableyes
approval.channelsan ordered list drawn from desktop, prompt, terminal, no duplicates; may be empty["desktop", "prompt", "terminal"]no
approval.timeout_sinteger, 30 to 600120no
egress.allow_private_defaulttrue or falsefalseno
profiles.<name>.purchase.gateas purchase.gateunset: the profile uses the global valueyes
profiles.<name>.purchase.detectionas purchase.detectionunset: the profile uses the global valueyes
  • purchase.gate and purchase.detection: see the purchase gate. A profile's value overrides the global one; incognito sessions use the global one.
  • approval.assurance: whether approvals must be verified by the operating system. See assurance labels.
  • approval.channels: the order in which approval channels are tried. A channel only requests verification; approval.assurance decides whether verification is required. An empty list means nothing can approve, which has the same effect as deny.
  • approval.timeout_s: how long a held action or setting change waits for a human.
  • egress.allow_private_default: whether a public page may reach local and private-network addresses (the --allow-private rule) for a profile that has no setting of its own, and for incognito sessions. A profile's own setting (jawb profile settings) wins. The addresses that are always refused (link-local, cloud metadata, jawbd's own control port) stay refused whatever this says.

The floor rule

For each safety key, the floor is its built-in default.

  • A file value equal to or stronger than the default applies immediately. The order, strongest first, is deny > approve > allow for purchase.gate; strict > standard for purchase.detection; and verified > verified-if-available > asserted for approval.assurance.
  • A value weaker than the default is unconfirmed. The default stays in force, and jawbd holds one pending confirmation of class config. A human approves or denies it through the same channels as a purchase (see Policy and confirmation). Once approved, the weaker values apply until the daemon exits or the file is reloaded with different values. A deny, or the timeout, leaves the defaults in force.
  • This happens on every daemon start with a weaker-than-default file. That is deliberate: nothing stored on disk is trusted to say "a human agreed", so an agent that can edit config.toml and restart the daemon still cannot switch the gate off without a person.

Known limit. The floor is the default, not your last choice. If you set purchase.gate = "deny", a program running as you can edit it back to approve without asking, because approve is not below the floor. You would still be asked about every purchase. And on a host with no OS verifier (macOS and Linux today), the confirmation of a weakening is only asserted, so a program running as you can confirm it.

GET /v1/status reports the effective purchase.gate and purchase.detection, lists unconfirmed keys, and warns W_CONFIG_UNCONFIRMED while any are waiting and W_PURCHASE_UNGATED while the effective gate is allow.

The /v1/config API

Every route needs the bearer token and refuses an Origin header, like all /v1 routes (daemon API level 11 or later).

RouteBodyResult
GET /v1/confignone{path, version, keys, pending, warnings}. Each key row has key, default, file, effective, safety and state (default, set or unconfirmed). pending is the waiting config confirmation or null. Profile overrides follow the global keys.
PUT /v1/config{"set": {<key>: <value>}, "unset": [<key>]}, both optional; unknown fields are refusedWrites the file and applies it. Returns the GET shape, or, when a change is weaker than its floor, E_CONFIRMATION_REQUIRED (HTTP 428) with details: {class: "config", pending, summary: {changes}, digest, expires_in_ms, hint}. The other changes in the same request are already applied.
POST /v1/config/reload{}Re-reads the file, with the same reply as PUT.

An unknown key is E_USAGE with reason: "unknown_key", a bad value E_USAGE with reason: "invalid_value", and in both cases nothing is written. Booleans also accept "true", "false", "on" and "off", integers a numeric string, and approval.channels a comma-separated string. unset returns a key to its default. A newer PUT or reload replaces a waiting config confirmation, and the old one is reported as denied with reason: "superseded".

$ curl -s -H "Authorization: Bearer $(cat "$JAWB_DATA_DIR/daemon.token")" \
    http://127.0.0.1:4180/v1/config

The jawb config command

Available from jawb 0.1.1. Release 0.1.0 has no jawb config; use the API above or edit the file.

CommandWhat it does
jawb config pathPrints <data_dir>/config.toml. Local; does not contact the daemon.
jawb config showShows every key: the effective value, the default, whether it is a safety key, and a note with the file value when it differs. Unconfirmed keys are flagged with their pending ID. Profile overrides follow the global keys. -q prints key=value lines.
jawb config get <key>The effective value, plus the file value and pending ID when they differ. A profile override that is not set reports the global value it uses. -q prints the value only.
jawb config set <key> <value>Sets a key. Booleans accept true, false, on, off; integers a number; lists are comma-separated, for example desktop,terminal (an empty string is the empty list). Prints the key's new line.
jawb config unset <key>Returns a key to its default. Never needs approval.
jawb config reloadRe-reads the file.

Every command takes --json for the usual envelope. The --json result of show is the GET /v1/config shape; get returns that key's row (plus pending while it is unconfirmed); set and unset add changed (the key) to the GET shape.

$ jawb config get purchase.gate
purchase.gate = approve (file: allow, unconfirmed, pending pc_7m2kq4x9d1ab)

When set or reload would weaken a safety setting, the file is saved but the default stays in force, and jawb shows the changes. On an interactive terminal it asks you to type weaken to approve, or deny:

$ jawb config set purchase.gate allow
jawb: this change WEAKENS a safety setting and needs a human (pc_7m2kq4x9d1ab):
  purchase.gate: approve -> allow
  the file is saved; until you approve, the default stays in force
  approving applies it until jawbd exits; denying keeps the default
Type `weaken` to approve it, or `deny` to decline it: weaken
jawb: Verify with Windows Hello on this computer to finish the approval (up to 120s)...
approved pc_7m2kq4x9d1ab (confirmed via tty-verified: the operating system verified you)
purchase.gate = allow

After weaken, the approval goes through the normal confirmation flow: where jawbd has an OS verifier (Windows Hello), it raises the prompt and jawb waits for it, and the result shows the assurance label. deny declines it (exit 5; the default stays in force). Any other answer leaves it pending until it expires (exit 5). --yes never answers this prompt. In --json mode, without a terminal, or with --yes, jawb exits 5 (E_CONFIRMATION_REQUIRED) with the pending ID in details.pending, and a human runs jawb confirm <pc_id> in their own terminal and types weaken. The CLI refuses a jawbd older than the configuration API (API level 11) with E_VERSION_MISMATCH (exit 3).