Skip to content

A right that is system-authored on one host and user-authored on another cannot be declared no-op on both #114

Closed

Description

@2000game

What happens

normalizeActual drops rows with meta.modifiedPid === -1 as "system baseline — invisible to reconciliation" (src/permissions/grants.ts:81). That is the right call for a single host. Across two hosts of the same instance it produces a config that cannot be a clean no-op on both, because the same right can be system-authored on one and user-authored on the other.

Observed on a group_role domain that exists on both our hosts. Three roles of the same group, right churchgroup:view (authId 1101), isInherited: false on both:

Host meta.modifiedPid how ct sees it
prod -1 (system) invisible
dev 1 (a person) user-authored, ct's to own

The dev rows are not hand-made — that host is a copy, and the copy stamped a person id onto rows that are system rows upstream.

Why it has no good answer today

Adopt from prod, then plan both:

Config prod dev
as ct adopt grants emits it (right omitted, because prod's row is invisible) ✅ no-op ❌ revokes the right — dev ends up less permissive than prod
right declared explicitly ❌ +1 grant on each of the three roles ✅ no-op

Neither is acceptable if "prod plan stays a clean no-op" is the rule, so the group has to be left out of adoption entirely — even though nothing about it is genuinely undeclarable.

Suggestion

Make a declaration satisfiable by a system-baseline row instead of invisible to it: when a declared grant matches a live row with modifiedPid === -1, treat it as already present and plan no PUT. Reconciliation would still never author or revoke a baseline row, so the "never fight the platform" property in the module header holds — the change is only that the tool stops proposing to re-author something the platform already grants.

With that, the second row of the table above becomes ✅ / ✅ and the domain is declarable.

Why it is hard to see coming

isInherited is the field that looks like it answers "is this grant mine to own", and it says false on both hosts. The field that actually decides is meta.modifiedPid, and nothing in plan, coverage or adopt grants surfaces it — the divergence only shows up as an unexplained revoke in the other host's plan, after the config has already been written. Even a warning from adopt grants ("this domain has N system-baseline rows that will not be emitted") would have turned an afternoon of bisecting into a one-liner.

Found on ct-cli 1.7.0 while adopting Local Lead group_role grants in an estate repo.

Activity

  1. removed
    triageUnsorted intake — decide in the weekly sweep
    on Aug 17, 2026
  2. added a commit that references this issue on Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions