workspacetrust

package
v0.0.20 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package workspacetrust is the adapter-layer reader/writer for workspace trust (Workspace-Trust feature, Phases 1 + 2b). It serves composition from two user-global sources under the XDG config dir:

  • Phase 1 (DECLARATIVE, this file): the operator-authored, read-only `trustedWorkspaces:` list in <xdg>/mecatl/settings.yaml. IsDeclared answers "is this workspace declared-trusted?"
  • Phase 2b (REMEMBERED, registry.go): the machine-written <xdg>/mecatl/trust.yaml registry. Remembered answers "is this workspace remembered-trusted, and has its identity anchor DRIFTED?"; Remember persists an entry; the identity-anchor hash (anchor.go) is what drift is measured against. The registry is a SIBLING of settings.yaml, never inside it (the settings-vs-state split).

What it is (and is NOT)

`trustedWorkspaces` is config DATA, not a permission Rule. It NEVER produces a governance.Rule and NEVER touches the permission evaluator's deny-dominance. It only feeds the composition-level trust decision (internal/app), which gates whether a project's ALLOW rules and project soul are honoured — exactly the same admission gate as the --trust-project flag. Trust here is MONOTONIC-POSITIVE: a declared entry can only GRANT trust; it can never override a Deny or a configured Ask anywhere (those are honoured regardless).

This is the Phase 1 declarative half of the trust feature. The machine-written trust.yaml registry, the interactive prompt, and identity-anchor drift are Phase 2 and live elsewhere; this leaf only READS the human-authored settings.yaml.

Path keying (security, MUST-FIX 5.1)

Both declared entries and the workspace under test are keyed by their cleaned + realpath'd absolute path (filepath.Abs then filepath.EvalSymlinks), so a moved/symlinked path cannot forge or inherit another workspace's trust. A path that cannot be resolved (e.g. a broken symlink, or a declared entry pointing at a nonexistent dir) is treated as UNTRUSTED / non-matching (fail-safe). A malformed entry is ignored; the rest of the list is honoured.

Layering

workspacetrust is an adapter LEAF: stdlib + the shared xdgconfig env seam + the YAML parser. It imports no domain package (session, prompt, governance, tool), and no domain package imports it. It mirrors permconfig/soul. The TRUST DECISION that folds this together with the --trust-project flag is composition (internal/app), not here.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader resolves workspace trust from the user-global config dir: the DECLARATIVE `trustedWorkspaces:` list in settings.yaml (Phase 1, IsDeclared) AND the MACHINE-WRITTEN trust.yaml registry (Phase 2b, Remembered/Remember).

Construct it with New (real env + real registry write seam) or NewWithEnv (faked env + the real write seam) or NewWithEnvIO (faked env + an injected write seam, so the registry write runs fully offline in tests). A Reader built without a write seam still serves every READ path; only Remember requires the seam.

func New

func New() *Reader

New binds a Reader to the real process environment + filesystem, with the real O_NOFOLLOW/0o600/temp-rename registry write seam.

func NewWithEnv

func NewWithEnv(env xdgconfig.ResolveEnv) *Reader

NewWithEnv binds a Reader to an injected env so the XDG/home resolution and the file read run fully offline against a fake — never the developer's real ~/.config. It uses the REAL registry write seam (so a test that writes a registry against a faked XDG dir exercises the actual O_NOFOLLOW/temp-rename path). Tests that need to SPY on or REJECT the write inject their own seam via NewWithEnvIO.

func NewWithEnvIO

func NewWithEnvIO(env xdgconfig.ResolveEnv, writeFile registryWriteFunc) *Reader

NewWithEnvIO binds a Reader to an injected env AND an injected registry write seam, so the write is fully observable/controllable offline (e.g. a spy that records writes to assert "mecated never writes", or a stub that always errors). A nil writeFile leaves the Reader read-only (Remember then errors).

func (*Reader) AnchorHash

func (*Reader) AnchorHash(workspace string) string

AnchorHash computes the identity-anchor hash for workspace using the real filesystem. workspace is realpath'd first (symmetric with the registry keying); an unresolvable workspace yields "" (the fold then treats a remembered entry as drifted/untrusted — fail-safe). The returned hash is lowercase-hex SHA-256.

It is a method on Reader (not a free function) so the anchor computation is reached through the same handle the fold already holds; the receiver carries no state the computation needs (the IO seam is the real binding), hence the blank receiver.

func (*Reader) HasProjectAuthority

func (*Reader) HasProjectAuthority(workspace string) bool

HasProjectAuthority reports whether workspace carries a project authority set worth gating behind a trust prompt (see file doc). It is a method on Reader so it is reached through the same handle the fold holds; the receiver carries no state the probe needs (the IO seam is the real binding). It is FAIL-SAFE in the quiet direction: an unresolvable workspace or any IO/parse failure simply means "no authority found here" for that member, never an error — at worst the prompt is skipped, which only ever leaves a workspace UNTRUSTED (the safe default).

func (*Reader) IsDeclared

func (r *Reader) IsDeclared(workspace string) bool

IsDeclared reports whether workspace is in the operator-authored `trustedWorkspaces:` list, comparing on the cleaned + realpath'd absolute path of BOTH sides (MUST-FIX 5.1). It returns false (fail-safe) when:

  • workspace is empty, or cannot be resolved to a realpath (broken symlink);
  • the settings.yaml is absent, unreadable, oversized, or unparseable;
  • no declared entry's realpath matches the workspace's realpath.

A single malformed/unresolvable entry is skipped; the rest of the list is still honoured. This NEVER errors out of band: a corrupt config simply yields "not declared" — trust is monotonic-positive, so the absence of a grant is the safe default.

func (*Reader) Remember

func (r *Reader) Remember(workspace, anchorHash string, trustedAt time.Time) error

Remember persists (or updates) the remembered-trust entry for workspace with the given identity-anchor hash and trustedAt timestamp. The workspace is realpath'd (symmetric with Remembered) so the entry is keyed identically to how it will be looked up. trustedAt is INJECTED by the caller (composition supplies time.Now()) so the write is deterministic in tests — this adapter never calls time.Now().

The write is read-modify-write: it loads the current registry (preserving other entries), upserts this one, and atomically replaces the file via a 0o600 + O_NOFOLLOW temp file then rename (the soulguard discipline). It returns an error only on a genuine IO failure (the caller logs it); a corrupt EXISTING registry is treated as empty (the upsert starts fresh rather than failing) so a remembered write is never blocked by an unrelated corrupt file.

This is the ONLY write path for trust.yaml. mecated never calls it. The path is derived solely from the user XDG config dir (never a repo-controlled path), so a repo cannot redirect the write (no self-trust).

func (*Reader) Remembered

func (r *Reader) Remembered(workspace, currentAnchorHash string) (remembered, drifted bool)

Remembered reports whether the workspace has a remembered-trust entry and, if so, whether its stored identity-anchor hash has DRIFTED from currentAnchorHash.

It returns (false, false) — not remembered, fail-safe — when:

  • the Reader is nil, or workspace is empty, or workspace cannot be realpath'd;
  • the trust.yaml is absent, unreadable, oversized, unparseable, or a wrong/unknown schema version;
  • no entry's realpath matches the workspace's realpath.

When an entry IS found: remembered=true, and drifted = (stored hash != currentAnchorHash). A drifted entry is reported remembered+drifted so the fold (internal/app) can FAIL SAFE to untrusted (mecated) or re-prompt (mecatui, 2c). A blank stored hash is treated as drift (it can never match a real anchor hash), so a hand-corrupted entry never silently grants.

func (*Reader) WithDiagnostics

func (r *Reader) WithDiagnostics(d port.Diagnostics) *Reader

WithDiagnostics injects the operational-logging sink the Reader writes its fail-safe Warn lines through and returns the receiver for fluent wiring. A nil sink is ignored (the NopDiagnostics default stands), so the Reader never nil-panics. The composition layer calls this immediately after a constructor.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL