Skip to content

Scope managed posture to declared owned sources - #319

Merged
chubes4 merged 1 commit into
mainfrom
fix/318-scoped-managed-sources
Aug 3, 2026
Merged

Scope managed posture to declared owned sources#319
chubes4 merged 1 commit into
mainfrom
fix/318-scoped-managed-sources

Conversation

@chubes4

@chubes4 chubes4 commented Aug 2, 2026

Copy link
Copy Markdown
Member

Closes #318.

--posture managed (shipped in v1.11.0) granted edit on all of wp-content/plugins/ and wp-content/themes/. On h44lacrosse.com that put WooCommerce, the Stripe gateway, Data Machine, and the agent's own data-machine-code runtime inside an autonomous agent's write surface — on a site taking real card payments.

Three paths there are actually the site's own (h44-core, h44-forms, h44-lacrosse-theme), and those three are exactly what its harvest.yml captures.

Why this was worse than "too broad"

The managed guidance tells the agent its edits are captured as restore points. Outside the harvested set that is false — an edit to WooCommerce is never recorded and is erased by the next update. The agent was told a safety property that did not hold for most of what it was granted.

Root cause is the prose-vs-permission split #314 existed to eliminate, reintroduced one level down: the guidance enumerated ("this site's theme and plugins"), the policy generalised (two directories).

The invariant

The editable set must equal the set the operator's capture records.

A path the agent can edit but nothing records is a path where it silently loses work, invisibly, until an update overwrites it. There is no reliable way to infer which plugins a site "owns", and guessing wrong on production is not an acceptable default — so it is declared:

./setup.sh --posture managed \
  --managed-source wp-content/themes/acme \
  --managed-source wp-content/plugins/acme-core

Recorded as wp_coding_agents_managed_sources so upgrades converge. Managed now denies all three roots exactly as engineering does, then emits one allow per declared path.

Fails closed. Managed with nothing declared grants no editable source, and the guidance says so explicitly rather than implying a directory is editable. Malformed declarations (outside wp-content, or a file inside a component) are rejected with a reason.

Rule order is now load-bearing

Verified against opencode 1.18.4 source, not the docs — the two disagree, which is the subject of several open upstream issues (#7029, #13872, #24335, #24999, #36765):

// packages/opencode/src/permission/index.ts
.findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern))
// fromConfig(): iterates Object.entries(value) — JSON key order

findLast over a ruleset built in config key order, with no specificity sorting in evaluate (Wildcard.all's length sort is used elsewhere). So a narrower allow written after a broad deny wins. Both the generator and the reconciler preserve deny-before-allow, and the tests assert the ordering rather than comparing sorted keys — sort_keys=True would have destroyed the property under test.

Runtime support narrowed, deliberately

Managed is now opencode-only. Claude Code treats deny as absolute so an allow cannot carve an exception out of it; Codex has no documented precedence for overlapping filesystem entries. Both now refuse managed posture with a clear error rather than emit a permission set whose behavior on a production site I could not verify. Their engineering paths are untouched.

Guidance rewritten

Enumerates the declared paths by name, states the list is exhaustive, and marks the remainder of wp-content read-only — naming commerce/payment code and the agent's own runtime as things it must not touch, with the reason (an update erases the change anyway). The heading no longer labels the whole category editable.

Verification

  • Engineering output unchanged — generated opencode.json diffed byte-for-byte against generation from v1.11.0: identical.
  • tests/posture.sh extended: ordering (deny index < allow index), fail-closed with nothing declared, normalization rejecting bad paths, the three runtimes' managed support, prose enumerating every declared path, prose never presenting a directory as editable, and fail-closed prose.
  • Reconciler covered for insertion order, operator-rule preservation, and dropping a stale allow when a source is undeclared.
  • Full tests/*.sh: no new failures (same 7 pre-existing environment failures as main, none in shell.yml). bash -n clean.

Follow-up

Re-applying to h44lacrosse.com is an operator action, not part of this PR. That box currently has the over-broad grant live.

--posture managed granted edit on all of wp-content/plugins and
wp-content/themes. On h44lacrosse.com that meant WooCommerce, the Stripe
gateway, Data Machine, and the agent's own data-machine-code runtime were
writable by an autonomous agent on a site taking real card payments.

Only three paths there are the site's own, and those three are exactly what
its harvest workflow captures. The managed guidance promises that edits are
captured as restore points; for everything else in those directories that is
false, so the agent was told a safety property that did not hold for most of
the surface it was granted. An edit to WooCommerce is never recorded and is
destroyed by the next update.

This is the prose-vs-permission split #314 existed to kill, reintroduced one
level down: the guidance enumerated ("this site's theme and plugins") while
the policy generalised (two directories).

The invariant: the editable set must equal the set the operator's capture
records. There is no reliable way to infer which plugins a site owns, and
guessing wrong on production is not an acceptable default, so it is declared:

  --managed-source wp-content/themes/acme --managed-source wp-content/plugins/acme-core

Recorded as wp_coding_agents_managed_sources so upgrades converge. Managed now
denies all three roots exactly as engineering does, then emits one allow per
declared path. Declaring nothing fails closed — no editable source, and the
guidance says so instead of implying a directory is editable.

Rule ORDER is now load-bearing. Verified against opencode 1.18.4 source, not
the docs, which several open upstream issues contradict: evaluate() is
findLast over a ruleset built in config key order with no specificity sorting,
so a narrower allow written after a broad deny wins. The generator and the
reconciler both preserve deny-before-allow.

Claude Code treats deny as absolute so an allow cannot carve an exception, and
Codex has no documented precedence for overlapping filesystem entries. Both
now refuse managed posture rather than emit a permission set whose behavior on
a production site is unverified.

The guidance section is rewritten to enumerate the declared paths by name and
to mark the rest of wp-content read-only, naming commerce code and the agent's
own runtime as things it must not touch. The heading no longer calls the whole
category editable.

Engineering output is unchanged: generated opencode.json verified
byte-identical to v1.11.0.
@chubes4
chubes4 merged commit e130e96 into main Aug 3, 2026
18 checks passed
@chubes4
chubes4 deleted the fix/318-scoped-managed-sources branch August 3, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

managed posture grants edit on all of wp-content/plugins and themes, including commerce plugins and the agent's own runtime

1 participant