Skip to content

Policy and locked config

Before this shipped, a repo-local config overrode the user's own settings, and nothing overrode either. There was no scope above the person at the keyboard, so an organisation could not state "isolation is on and you may not turn it off."

Two changes fixed that: a scope above the user, and consent before executing a config that is code.

Precedence

Scopes merge in this order, later winning, except for keys the system scope has locked:

system    /etc/moxxy/config.yaml
          %PROGRAMDATA%\moxxy\config.yaml
          $MOXXY_SYSTEM_CONFIG

user      ~/.moxxy/config.yaml

project   ./moxxy.config.{yaml,yml,ts,js,mjs,cjs}   (searched upward)

explicit  --config <path>

flags     per-invocation

moxxy config path prints which files are actually in effect on a machine.

Locked keys

The system scope may carry a locked: list of dot-paths. Those paths are stripped out of the user, project and explicit layers before the merge, so a lower layer cannot set them at all, and an attempted override is logged.

yaml
security:
  enabled: true
  strict: true

plugins:
  isolator:
    default: subprocess

locked:
  - security.enabled
  - security.strict
  - plugins.isolator
  - audit

A locked path can name a leaf (security.enabled) or a subtree (audit, which locks everything under it).

Lock deliberately

Lock what a user must not be able to switch off. Everything else — the model, the theme, the density, which channels they use — is a preference, and locking preferences generates support tickets without improving your posture.

The system scope is YAML only. An executable file at a root-owned path would run as whoever starts moxxy, which is a worse hole than the one this feature closes.

Executable project configs

moxxy.config.ts is code. It is executed with your full privileges before the permission engine, the vault or any isolator exists, and the project search walks upward through parent directories. That meant cloning a repository and typing moxxy inside it silently ran that repository's config.

moxxy now asks first, and records the approval against the file's contents, so editing the file asks again.

sh
moxxy config trust                # approve ./moxxy.config.ts
moxxy config trust ./path/to.ts   # approve a specific file
moxxy config trust --list         # what is approved
moxxy config untrust <file>       # withdraw approval

Non-interactive runs (CI, daemons, containers) skip an unapproved executable config rather than executing unreviewed code. Pre-approve with moxxy config trust when building an image.

To forbid them outright across an estate:

yaml
config:
  allowExecutable: false
locked:
  - config.allowExecutable

YAML configs are unaffected by any of this. They are data.

Behaviour change

A project moxxy.config.ts that used to load silently now requires one-time approval.

Permission policy

Permission rules are policy too, and they follow the same idea. Config-supplied rules apply as a managed layer rather than being copied into the user's writable ~/.moxxy/permissions.json, so a rule pushed from the system scope does not become something the user can simply delete.

yaml
permissions:
  deny:
    - name: Write
      inputPathPrefix: { path: /etc }
      reason: system files are managed by configuration
    - name: Bash
      inputGlob: { command: '* curl *' }
  allow:
    - name: Read
      inputPathPrefix: { path: /srv/app }
    - name: Bash
      inputGlob: { command: 'git *' }

locked:
  - permissions

Order is: managed deny, file deny, managed allow, file allow. The managed layer is checked first on purpose — checking the user's file first would let an "allow always" answer, which the agent itself can provoke, silently defeat a rule the operator pushed.

Prefer the anchored matchers

Three matchers exist, and for operator-written policy only two of them are safe:

MatcherSemantics
inputPathPrefixAnchored, path-aware. /srv/app matches /srv/app/x, not /srv/apple. Paths are normalised first, so .. cannot walk out.
inputGlobAnchored whole-value. * within a segment, ** across, ? one character.
inputMatchesRaw regex, substring match, unanchored.

inputMatches is unanchored by deliberate, documented contract: existing user files depend on it and silently anchoring would break them. That makes it a trap in an org policy. The obvious { path: '/etc' } reads like "under /etc" and actually means "contains /etc anywhere", which over-blocks as a deny rule and grants far more than intended as an allow rule.

Use inputPathPrefix and inputGlob when you are writing rules other people will live under.

See permissions for the full engine.

Next

Open source · self-hosted · MIT