Skip to content

feat(guardrails): add a configurable identity-domain isolation guard #5945

Closed
8 of 8 issues completed
Closed

Description

@kyle-sexton

Part of #5943

Problem

A machine that holds several identity domains (for example a work tree and a personal tree, each
with its own git author, signing key, SSH key, gh account and az login) has no plugin guard that
keeps a session rooted in one domain from acting on another. Permission rules cannot express this:
permissions.deny is not cwd-aware, so a global rule that blocks one tree's path also blocks a
session rooted in that tree, and built-in read-only Bash commands never reach the auto-mode
classifier. On origin/main no guardrails guard classifies the session or the command by domain
(plugins/guardrails/README.md, guard table).

Evidence

Verified this pass:

  • No guard on main reads a domain map (git grep over plugins/*/hooks for domain, identity and
    cross-domain terms found nothing).
  • plugins/harness-ops/skills/machine-profile/SKILL.md lines 22 and 48: the machine profile already
    discovers identity domains from includeIf "gitdir:..." entries and records each domain's tree,
    git include and gh config directory. That is a ready source for the guard's domain map.
  • plugins/guardrails/hooks/block-exported-msys-pathconv.sh records that a guard keyed on path shape
    across every Bash command fired on 45.7% of 14,234 real commands and was rejected on that
    measurement. A domain guard is path-shape matching by nature, so it inherits that risk.

Behavior of the working local guard (read for behavior only; its code does not cross into this repo):

  1. Classify the session cwd into a domain, or neutral.
  2. Cross-domain path: deny a Bash or PowerShell command that names another domain's root in any
    spelling (Windows drive path with either slash, MSYS /c/..., WSL /mnt/c/..., escaped
    backslashes, any case), or a ~/src/github.com/<owner>/... / ~/worktrees/github.com/<owner>/...
    path whose owner belongs to another domain. Reads are denied too.
  3. Shared exception: one named directory (a cross-domain drop box) is readable and writable from
    the other domain, only when every foreign path in the command is inside it and the command has
    no ... The exception is one-directional.
  4. Cross-domain identity: deny a command that carries another domain's identity marker in an
    identity position (git remote add/set-url to the other org, a user.email= or --author
    with the other domain's address, the other domain's gh/az config dir name).
  5. gh / az config-dir mismatch: in a domain that needs a non-default config dir, deny gh/az
    without it (in the command text or the hook's environment); in the default domain deny them
    with another domain's dir. --version is exempt.
  6. Global identity immutability (all domains, including neutral): deny a git config write with
    --global, --system or --file <identity file> touching user.email, user.signingkey,
    credential.helper, core.sshCommand, core.hooksPath, user.useConfigOnly or includeIf,
    and any shell write to an identity file (~/.gitconfig*, *.gitconfig, a git identities
    directory). Reads (--get, --list, config get) pass.
  7. Fail-open on any internal error; every exit 0 with the decision in stdout JSON.

The source item reports 170 local test cases including WSL spellings (not re-run this pass).

Proposed approach

A new guard on the guardrails Bash|PowerShell dispatcher row, off unless a domain map is
configured, with its own kill switch and telemetry like every other guard.

Config shape (exact keys are a design decision; follow docs/conventions/plugin-option-naming
and docs/conventions/config-cascade):

domains:
  <name>:
    roots: [<per-OS absolute roots: Windows drive path, /mnt/<d>/..., POSIX>]
    owners: [<GitHub owners mapped from ~/src|~/worktrees/github.com/<owner>/...>]
    identity_markers: [<strings that identify this domain: org names, account names, config dir names>]
    gh_config_dir: <optional>
    az_config_dir: <optional>
identity_files: [<globs>]
shared_exceptions: [{path: <dir>, from: [<domain>], to: <domain>}]
messages: {<rule>: {windows: ..., posix: ...}}

Open decisions:

  • Where the domain map lives: derived from the machine profile, hand-written userConfig, or both
    with the profile as default. The map is machine-specific and must never sit in a tracked repo file.
  • Owner plugin: guardrails, or harness-ops beside the machine profile.
  • Implementation: bash in the dispatcher, or a node guard; port behavior, not the Python.
  • Prefilter: a cheap substring test on configured roots and markers before any parse, per the
    45.7% precedent.

Alternatives considered: permission deny rules (not cwd-aware, rejected above); a sandbox profile
(OS-level, does not express identity markers or the shared exception).

Acceptance criteria

  • With no domain map configured the guard emits nothing and costs no parse.
  • Each rule 2 to 6 above has deny and allow test cases, fed as stdin JSON, in Windows, MSYS and
    WSL spellings, including the one-way shared exception and its .. refusal.
  • Rule 6 applies from a neutral cwd; reads of identity config pass.
  • Measured firing rate on a recorded command corpus is reported in the PR, and false positives
    found are added as regression cases (docs/conventions/hook-precision).
  • Row cost stays within docs/conventions/hook-budget.
  • README documents the config shape, the kill switch and the declared gaps.

Constraints and gotchas

  • The guard reads command text; it is a deterrent, not a sandbox. Declare the gaps (computed paths,
    scripts run by name).
  • Do not ship any real org, account or path in defaults, fixtures or docs; use placeholders.
  • gh/az checks must honor the config dir set in the hook's own environment, not only in the
    command text.
  • WSL: drive roots appear as /mnt/<d>/..., and on Windows as C:, /c/, /cygdrive/c/.

Context

Source: local handoff item 20260928-generalize-local-guards-into-plugins.md, candidate 1. Related:
#4666 (machine profile), #1203 and #1204 (per-domain scoping in babysit config).

Activity

  1. added
    status: needs-decisionAwaiting a human or maintainer judgment call.
    priority: mediumReal value, no hard deadline; normal backlog flow.
    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.
    wayfind: designWayfind decision item: design-space or domain-model decision; human in the loop.
    work-class: structuralRefactors, migrations, contract changes; cross-cutting and hard to reverse.
    on Oct 2, 2026
  2. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 15, 2026
  3. kyle-sexton commented on Oct 24, 2026

    @kyle-sexton
    MemberAuthor

    Split by rule, adopting #5943's defaults: the guard lives in guardrails as a bash guard on the dispatcher, and the domain map is derived from /harness-ops:machine-profile and kept in the plugin data directory, never in a tracked file. Order: the foundation first; then rules 2, 4, 5 and 6 and the setup writer in parallel; the shared exception after rule 2; the needs-human corpus and parity slice last.

    Each agent slice is labelled status: ready and admission-deferred, with Size, Complexity and Uncertainty set; admission takes unblocked slices oldest first.

    Posted by AI while splitting the backlog (owner decisions Q93 to Q95 and C12, 2026-10-24).

  4. removed
    status: needs-decisionAwaiting a human or maintainer judgment call.
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 24, 2026
  5. github-actions commented on Oct 25, 2026

    @github-actions
    Contributor

    The triage sweep dispatched intake triage for this issue (attempt 1 of 2).

  6. github-actions commented on Oct 25, 2026

    @github-actions
    Contributor

    Proposed work class: work-class: structural
    Basis: automated intake triage; the lanes App applied this class.

    Size: Large
    Complexity: High
    Uncertainty: Knowable
    Flags: none
    Blocked by: none

    Understood: a configurable guardrails guard that classifies a session's cwd into an identity domain. It would deny cross-domain paths, identity markers and gh/az config-dir mismatches, and refuse global git identity writes. It stays off unless a domain map is configured.

    The maintainer's split comment already settles the open design decisions. The guard lives in guardrails as a bash dispatcher guard, and the map comes from the machine profile and is kept in the plugin data directory. The work is tracked in #9743 to #9750, so this issue acts as the umbrella. No likely duplicates beyond those slices.

    Automated triage, applied by the lanes App.

  7. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 25, 2026
  8. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 25, 2026
  9. melodic-automation-lanes commented on Oct 25, 2026

    @melodic-automation-lanes
    Contributor

    Not admitted to the issue lane: Size large: every sub-issue is closed, but it is still open, so an item no sub-issue covers or a sub-issue's close keeps it from closing as a finished split (owner Q93). Admission takes the issue once each of those is met, when intake triages it again (.github/actions/resolve-config/admission-rules.json).

  10. added
    status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.
    and removed
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    on Nov 3, 2026
  11. kyle-sexton commented on Nov 3, 2026

    @kyle-sexton
    MemberAuthor

    Delivered: closing as completed.
    Agent choice, not an owner answer.

  12. kyle-sexton commented on Nov 3, 2026

    @kyle-sexton
    MemberAuthor

    Closing on the verified delivery record above.

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

    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.priority: mediumReal value, no hard deadline; normal backlog flow.status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.wayfind: designWayfind decision item: design-space or domain-model decision; human in the loop.work-class: structuralRefactors, migrations, contract changes; cross-cutting and hard to reverse.

    Fields

    Work status

    confirmed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions