stacklok / stacklok/mecatl

Consolidate workspace-authority enforcement: one gate per surface + make the config state unrepresentable

Open
#760 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Go
Stars
152
Forks
16
Avg merge
14h 48m
Merged PRs (30d)
536

Description

Background

ADR 0237 (listener-scoped workspace authority) is enforced by several checks scattered across the server adapter. Panel review of the accumulator branch found four places where the enforcement was missing, weaker than intended, or self-contradicting (see "Findings that motivated this"). Each was fixed, but the pattern — remember to call the right gate at every entry point — is the underlying risk, and it recurred four times. This issue proposes the structural changes that make it hard to reintroduce.

This is a follow-up refactor, not a live vulnerability: the four concrete gaps are fixed on the branch. Scope here is hardening + simplification.

The mental model (now true after the branch fixes)

There are four distinct authority surfaces, each operating on a different type at a different lifecycle point, so no single function can cover all four:

Surface Type gated Gate function Entry points
Create request string workspace + profile workspaceForCreate createSession, adoption (adoptionBindingsForAuthority), schedule create/update (manager hook)
Loaded session *session.Session validatePersistedWorkspace reopenLoadedSession (shared choke point: loadAndReopen, startRunContent, ForkSession) + resumeFromAwaiting (bypasses the funnel by design)
Persisted schedule port.ScheduleSpec validatePersistedScheduleWorkspace CanProcessSchedule (claim), FireNow
Environment override tool.Environment validateEnvironmentOverride engineAndEnvironmentFor

The invariant we want to hold going forward: every entry point that resolves a workspace-bearing thing for use routes through its surface's single gate. The loaded-session surface now has a structural choke point (reopenLoadedSession); the other three are single-gated but rely on each call site remembering to call the gate.

Proposal

1. Make the config state unrepresentable (folds in the AuthoritativeWorkspace split-brain finding)

Today authority is a WorkspaceAuthority enum plus a bare AuthoritativeWorkspace string, on both server.Config and app.Config. The pairing can disagree; validateWorkspaceAuthorityConfig catches the illegal combinations at NewService, but the type still admits them, and app.Config carries two loosely-coupled fields a future composition root can set inconsistently (sessions rooted at B while trust/ingestion/Bash cwd pin to A).

Replace the enum + bare string with an opaque value carrying its own root, constructed only through validating constructors:

type WorkspaceAuthority struct { mode uint8; root string } // zero value = client-selected
func ServerAssignedWorkspace(root string) (WorkspaceAuthority, error) // errors unless root is clean+absolute
func FilelessWorkspaces() WorkspaceAuthority                          // no root, by construction

Consequences:

  • Deletes AuthoritativeWorkspace from both server.Config and app.Config.
  • Collapses validateWorkspaceAuthorityConfig into the constructors: "server-assigned with no root" and "file-less with a root" become unconstructable rather than rejected.
  • app.Config carries one authority value verbatim instead of two fields to keep in sync; AuthoritativeWorkspace stops being a second copy of the launch root.
  • The architect review costed this as roughly break-even in lines; the payoff is the deleted field + the illegal states, not the three enum cases.

Cost to weigh: server.Config literals in tests can no longer set the root by field assignment (they call a constructor), and the getter for the root moves behind a method.

2. Keep the loaded-session choke point; document the surface map

reopenLoadedSession is now the structural gate for the loaded-session surface. Record the four-surface map above in docs/design/IMPLEMENTATION-NOTES.md (or the ADR) so the next person adding an entry point knows which gate to route through, and keep the "route every entry point through its surface gate" line as a stated invariant.

3. (Investigate) a guard against an unrouted new entry point

The one axis a value type does not cover: someone adds a new method that loads a session / builds an environment and forgets to call the gate. Worth investigating whether a lightweight test can enumerate the entry points (e.g. every exported Service method that reaches the workspace factory) and assert each routes through a gate. This may not be cleanly expressible statically; if not, say so and rely on #1 + #2 + review. Do not build a heavy framework for it.

Findings that motivated this (all fixed on the branch)

  • CreateTeam accepted a client-selected filesystem root, bypassing authority entirely — CRITICAL, filed separately as #759 (fixed on its own branch).
  • Filesystem schedules could never fire under server-assigned authority (the persisted resolved root was rejected by the fire-time create gate).
  • ForkSession skipped validatePersistedWorkspace — fixed by moving the check to the reopenLoadedSession choke point.
  • validateEnvironmentOverride used a raw string compare instead of the shared isAuthoritativeWorkspace identity rule (fail-open under file-less authority).

Out of scope

Multi-workspace authorization (opaque scoped grants / handles) — ADR 0237 is explicitly a single-root deployment policy. This issue does not change that; it only consolidates the single-root enforcement.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ADR 0237 and the workspace-authority paths named in the issue, especially server.Config, app.Config, reopenLoadedSession, CanProcessSchedule, FireNow, and engineAndEnvironmentFor. Trace how the four authority surfaces are currently gated, then assess the validating authority constructors and the lightweight unrouted-entry-point check. Done means illegal config states are unrepresentable, the four-surface map is documented, and the guard is either justified or explicitly rejected.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
authorization, backend, documentation
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.