Skip to content

Instantly share code, notes, and snippets.

@shykes
Last active February 13, 2026 19:30
Show Gist options
  • Select an option

  • Save shykes/640319ddca0ee8efdfffa7ffbd4b7626 to your computer and use it in GitHub Desktop.

Select an option

Save shykes/640319ddca0ee8efdfffa7ffbd4b7626 to your computer and use it in GitHub Desktop.
Workspace design briefing: implementation vs. design discussion

Workspace design briefing: implementation vs. design discussion

Workspace Design Briefing: Implementation vs. Discussion

Companion to the ongoing Discord design thread. Based on a review of the implementation on the workspace branch against the points raised in discussion.


1. "Every client always has a workspace"

Status: Implemented, clean.

Detection in core/workspace/detect.go has a 4-step fallback chain that always returns a Workspace struct — it never returns nil:

  1. Find .dagger/ upward → read config.toml → workspace with config
  2. Find dagger.json upward → check migration triggers → error or ignore
  3. Find .git/ upward → empty workspace rooted at repo root
  4. Nothing found → empty workspace rooted at CWD

This is solid and consistent with the stated design.

2. Workspace Detection

Status: Implemented, one subtlety.

FindUpAll does a single upward pass looking for .dagger/, dagger.json, and .git/ simultaneously. The first .dagger/ directory found wins — even if it doesn't contain a config.toml. You get an empty workspace rooted there.

Previously flagged as a question: A stale or empty .dagger/ directory anywhere in the path will "capture" the workspace and shadow a .git root higher up. This is intentional: .dagger/ is a jurisdiction marker. Even without config, it delineates "this subtree is someone else's scope" (see sub-workspace boundaries in section 4).

3. Workspace Initialization — "Where is .dagger created?"

Status: Not implemented. This is the biggest gap.

There is no dagger workspace init command. The CLI has:

  • dagger workspace info — read-only
  • dagger workspace config — reads/writes config values
  • dagger module init — creates a module at .dagger/modules/<name>/ and writes config.toml
  • dagger install — adds a module to config.toml

The .dagger/ directory gets created implicitly as a side-effect of dagger install or dagger module init, which write config.toml via exportConfigToHost. But there's no explicit "create a workspace here" gesture.

The design question is unresolved in code: cd ./subdir/of/my/repo && dagger workspace init → does that create the workspace in the current directory, or the repo root?

What actually happens today:

  • dagger install foo calls currentWorkspace() → runs detection → falls back to .git root or CWD → writes config.toml there.
  • So if you're in a subdirectory of a git repo with no existing .dagger/, the workspace is created at the git root, not the current directory.

There's no way to explicitly say "I want the workspace here." You're always at the mercy of detection. If you want a workspace at ./subdir/ but there's a .git at ../, you get the workspace at ../ instead.

Two possible behaviors for a future dagger workspace init:

  1. Create at detected root (git root fallback) — consistent with detection, but means init in a subdirectory creates .dagger/ somewhere else. Surprising.
  2. Create at CWD — gives explicit control, but means detection and initialization follow different rules. The init overrides what detection would have chosen.

Neither is obviously wrong. The discussion needs to pick one.

4. Namespacing vs. Access Control

Status: Partially implemented. The namespacing model needs revision; the access control layer is not yet built.

This is the central design insight from the discussion thread: filesystem namespacing and filesystem access control are orthogonal concerns. The current implementation conflates them.

The two concerns

Namespacing answers: what can a path refer to? This defines the coordinate system — the set of valid addresses a module can construct.

Access control answers: what is a module allowed to read/write? This is policy, enforced at the workspace level. A future [access] section in config.toml can restrict what modules can touch — files, env vars, network, exec — even within the wider namespace. The workspace is the gate.

Two scopes of intent

A workspace represents the end-user's jurisdiction — the slice of the repo they care about. Paths encode scope of intent:

  • Relative paths (src, ./src, .): resolve relative to the workspace root. This means "within the user's jurisdiction." The default scope for well-behaved modules.
  • Absolute paths (/src, /libs/shared, /): resolve relative to the git root (fallback: workspace root). This means "within the whole repo." An explicit escape hatch for cross-cutting concerns.
  • .. is always an error: no path traversal. Simple and predictable.

The distinction matters most for discovery operations. A Go toolchain scanning for modules:

// "Find go modules in the user's workspace" — respects their jurisdiction
ws.Directory(".").Glob("**/go.mod")

// "Find go modules across the whole repo" — explicit wider scope
ws.Directory("/").Glob("**/go.mod")

A team with a workspace at apps/frontend/ expects the first to find their Go modules, not every Go module across 50 teams. The path style makes the intent visible.

Workspace at repo/apps/frontend/ Path Resolves to Scope
Relative src repo/apps/frontend/src Workspace
Relative . repo/apps/frontend/ Workspace
Absolute /libs/shared repo/libs/shared Repo
Absolute / repo/ Repo
Traversal ../backend error

Edge cases degrade cleanly:

  • No git root → absolute = relative (scope collapses to workspace root)
  • Workspace at git root → absolute = relative (same scope, no surprise)

Convention for module developers: default to relative paths. A well-behaved module respects the user's workspace scope. Use absolute paths only when you deliberately need repo-wide reach (CI modules, cross-project dependency scanners, etc.).

Why two scopes?

Two pragmatic constraints pull in opposite directions:

  1. Modules need repo-wide file access — monorepos are entangled, a CI module may need to read multiple apps, shared libs, root config files.
  2. Teams need subdirectory workspaces — in a big monorepo, you don't want to ask 50 people for permission to add a file at the root. Easier to drop .dagger/ in your team's area.

Relative paths respect the user's jurisdiction by default. Absolute paths provide an explicit escape hatch for repo-wide operations. The intent is visible in the code.

Sub-workspace boundaries (future refinement)

When a subdirectory of the current workspace is itself a workspace (contains .dagger/), it represents a sub-jurisdiction. Discovery operations (directory() listing, glob, search(), findUp()) should respect these boundaries by default:

  • Default: sub-workspace directories are excluded from scans. A parent workspace scanning with ws.Directory(".") sees shared code but not individual apps' internals.
  • Opt-in: an includeSubWorkspaces parameter allows crossing sub-jurisdiction boundaries when needed.
  • Direct access always works: requesting a specific file by path inside a sub-workspace is not blocked. The filtering applies to discovery, not to explicit access.

This makes .dagger/ a jurisdiction marker — even an empty .dagger/ directory delineates "this subtree is someone else's scope." The directory structure itself encodes the organizational boundaries, with no explicit configuration needed.

The workspace is still the gate

This does NOT mean the workspace has no security role. The workspace is the policy enforcement point. Future workspace config can tightly control what modules are and aren't allowed to access:

# Future: workspace-level access control (not yet implemented)
[access.files]
allow = ["/apps/frontend/**", "/libs/shared/**"]
deny = ["/apps/backend/secrets/**"]

[access.env]
allow = ["CI", "NODE_ENV"]

[access.network]
allow = ["registry.npmjs.org"]

The distinction:

  • Namespacing (git root): what paths are addressable. An ergonomics concern.
  • Access control (workspace config): what paths are permitted. A security concern.

Current state and security note

Today there is no access control layer. The namespace boundary is the effective trust boundary. This means installing a third-party module gives it read access to your entire repo (via the git root namespace). This is already true with the existing +defaultPath mechanism, so it's not a regression — but the future workspace-level access controls are what makes this model safe long-term. This gap should be named explicitly in the design so it doesn't get forgotten.

Implementation changes needed

The current resolveWorkspacePath() resolves everything against ws.Root. To implement the two-coordinate-system model:

  1. detect.go — detect and return both workspace root and git root (already found during detection via .git lookup)
  2. core/workspace.go — add a RepoRoot field (or NamespaceRoot) alongside Root
  3. resolveWorkspacePath() — relative paths resolve against Root; absolute paths resolve against RepoRoot
  4. Both are clamped to RepoRoot — no path can escape the outer boundary

5. Relative Paths and Client Workdir

Status: Implemented simply. The deeper question is deferred.

Both relative and absolute paths currently resolve to workspace root (this changes with section 4 above). There is no concept of "relative to the client workdir."

A module calling ws.Directory("src") always gets <workspace-root>/src, regardless of where the user's terminal was. This is correct — module behavior should be deterministic, not dependent on where the user happened to cd.

For the "5 apps" scenario (cd apps/frontend && dagger call build), the module needs CWD context — but it should be opt-in, not baked into path resolution:

  • A future ws.ClientWorkdir() method could return the CWD path relative to workspace root
  • The module author chooses to use it as a default: "if no path argument given, use client workdir"
  • Modules that don't call ClientWorkdir() behave identically regardless of where the user runs them

Predictability is the default; CWD-sensitivity is opt-in.

6. Sub-workspaces and Part 3 Entanglement

Status: Not implemented. Design direction clarified.

The implementation has no concept of sub-workspaces, nested workspaces, or dynamic artifacts. Workspace is flat: one root, one config, no hierarchy.

The jurisdiction model from section 4 clarifies how sub-workspaces relate:

  • Sub-workspaces share a namespace (git root) — any sub-workspace's modules can address files across the repo via absolute paths
  • Sub-workspaces define jurisdiction boundaries — parent workspace scans skip sub-workspace directories by default
  • Sub-workspaces enforce their own access policy — each workspace config controls what its modules are allowed to do
  • Sub-workspaces are a configuration, jurisdiction, and policy boundary, not a naming boundary

The unresolved question remains: do sub-workspaces handle the "5 apps" pattern, or does Part 3's "dynamic artifacts" concept? The jurisdiction model is compatible with either answer.

Summary

Topic Implementation Status
Every client has a workspace Always returns a Workspace ✅ Clean
Detection fallback chain .dagger/.git → CWD ✅ Works (empty .dagger/ shadowing is intentional — jurisdiction marker)
dagger workspace init No explicit command ❌ Missing
FS namespacing (two scopes) Workspace root only ⚠️ Needs: relative → workspace root (jurisdiction), absolute → git root (repo)
FS access control Not implemented ❌ Future — workspace as policy gate
Sub-workspace boundaries Not implemented ❌ Future — discovery ops skip sub-jurisdictions by default
Client workdir exposure Not available to modules ❌ Future — opt-in ws.ClientWorkdir()
Sub-workspaces / Part 3 Flat model, no nesting ❌ Not yet addressed
findUp() / search() Not implemented ❌ In Part 2 design, not in code

Key decisions needed

  1. Where does dagger workspace init create .dagger/? At CWD or at detected root?
  2. Confirm the two-scope model: relative → workspace root (jurisdiction), absolute → git root (repo). (Proposed above, needs sign-off.)
  3. When to build ws.ClientWorkdir()? And does it block on Part 3?
  4. Sub-workspaces vs. dynamic artifacts for multi-app repos — which mechanism owns this?
  5. When to build workspace-level access controls? The namespace model is safe for now (same trust level as +defaultPath) but long-term security depends on the access control layer.
  6. When to build sub-workspace boundary filtering? API should be designed now so includeSubWorkspaces can be added later without breaking changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment