#14148·dagger

Module sessions can reach `currentWorkspace`, so the codegen-level restriction is not a boundary

Author: TomChvCreated Sep 14, 2026Updated Sep 14, 2026

Summary

currentWorkspace is deliberately left out of the bindings generated for a module, so that a module — and in particular a dependency — cannot reach the user's local machine.

The omission does not achieve that. The field is present in the schema the nested module session actually serves, and getGQLClient() is public on the generated Client, so any module author reaches it in three lines.

This matters now, and it matters more once unified clients land: at that point these bindings are generated anyway, so the codegen-level omission disappears entirely and there is nothing left in front of the field.

Reproduction

A module that is loaded as a dependency of another module (app depends on dep; the call below is dep's code, reached via dag.dep().probeWorkspace() from app):

typescript
@func()
async probeWorkspace(): Promise<string> {
  const gql = (dag as any).getGQLClient()
  const cwd = await gql.request(`{ currentWorkspace { cwd } }`)
  const dir = await gql.request(`{ currentWorkspace { directory(path: "/") { entries } } }`)
  return `cwd=${cwd.currentWorkspace.cwd} entries=${dir.currentWorkspace.directory.entries}`
}

Result:

cwd=/.dagger/modules/eptest-dev | entries=.dagger/,.git/,dagger.toml

So a dependency can:

  • obtain the caller's Workspace
  • see the caller's module directory as its cwd
  • enumerate the workspace root, including .git/

I verified directory enumeration. I did not run a file read, but Workspace.file(path).contents is on the same object that is already in hand, so I see no reason it would behave differently.

Tested on manifest-v2 (3f729ec9), but nothing about the reproduction is specific to that branch — the same getGQLClient() escape hatch exists on the current generated clients.

Why the current placement does not hold

The restriction lives in SDK codegen, which decides which fields become methods. It does not live in the schema the session serves. Anything that can issue a raw GraphQL query is unaffected by it, and every generated client hands you one.

Two consequences:

  1. Today it is a speed bump. An author who wants workspace access has it.
  2. After unified clients, when a module's bindings are the same generated client as everyone else's, the field is simply present as a typed method. There is no longer even a speed bump.

If module isolation from the host workspace is intended, it needs to be enforced in the schema the nested session serves, per client kind — not in codegen.

One thing to preserve if this is fixed

Under manifest v2 a module has no [[dependencies]] table, so there is nowhere in the manifest to declare the modules it depends on. The TypeScript SDK now establishes them at run time instead: the generated dispatcher serves its scope's client targets into its own session before dispatching, using exactly this query:

graphql
{ currentWorkspace { moduleSource(path: "/.dagger/modules/dep") { asModule { serve } } } }

Serving from the Dang entrypoint that spawned the container does not work — a nested exec takes its schema from the active module's dependencies at the moment it is created, as future/module-manifest-v2/compat-bridge.md notes — so it has to happen in the dispatcher's session.

That is the top-level module serving modules it was configured with, which seems like the legitimate case. A fix that blocks currentWorkspace wholesale would break it. Distinguishing the top-level module from a transitively-loaded dependency, or providing another way for a module to serve its declared clients, would keep both properties.

Suggested shape

  • Gate currentWorkspace (and anything else host-reaching) in the schema served to a nested module session, not in codegen.
  • Decide explicitly whether the top-level module differs from a dependency here, since manifest v2 currently depends on the top-level case working.