Conversation
Signed-off-by: Yves Brissaud <yves@dagger.io>
The clients directory is a convention, and existing modules keep a temporary global client behind a pyproject.toml flag. Signed-off-by: Yves Brissaud <yves@dagger.io>
A scope is one pyproject.toml, a uv workspace root. Each client is a workspace member, so a consumer installs only the clients it names. Signed-off-by: Yves Brissaud <yves@dagger.io>
Record the decided questions, build on contextModuleSource without [[dependencies]], keep the runtime copy in sdk/, and describe the shared default session. Signed-off-by: Yves Brissaud <yves@dagger.io>
The SDK never edits [project] dependencies except for the global client, a module calls itself through a self client, and exported signatures name only core types and the module's own types. Signed-off-by: Yves Brissaud <yves@dagger.io>
Every question is decided: a client project keeps its clients at the scope root, a module keeps them in clients/, and a module gets a self client only when the user declares one. Signed-off-by: Yves Brissaud <yves@dagger.io>
One structure in every scope: src, sdk and clients. A client is generated inside the scope that uses it, and dagger.toml joins a scope to its clients. Signed-off-by: Yves Brissaud <yves@dagger.io>
The SDK writes the client dependency into the project file, and a client loads its module with one serveModule call, surfaced through core. Signed-off-by: Yves Brissaud <yves@dagger.io>
The generator passes the field name, so the SDK never derives it from a class name and cannot drift from the engine's naming rule. Signed-off-by: Yves Brissaud <yves@dagger.io>
The engine marks what a module contributes with @sourcemap(module:). Read it to tell core from a client, to name the package of a client, and to digest core alone, so that the digest stays the same whatever clients the schema holds. Signed-off-by: Yves Brissaud <yves@dagger.io>
`from dagger import X` takes a name from the package init, which star-imports the generated bindings. The query builder and the session then depend on generated code only to reach hand-written exceptions. Import them from `dagger._exceptions`, where they live. Signed-off-by: Yves Brissaud <yves@dagger.io>
A GraphQL enum literal is bare, and the query builder only rendered an `enum.Enum` that way. The SDK files can't import a generated enum, so they need to send a value they only know by its schema name. `EnumName` marks such a string. It is rendered bare, and refused when it is not a GraphQL name, because it would otherwise inject query syntax. Signed-off-by: Yves Brissaud <yves@dagger.io>
The message hard-coded `dagger.client.gen`, so the SDK files named a generated module, and bindings generated anywhere else would report the wrong place. Generated code passes only the class and method, so read the module from the calling frame. Today's message is unchanged. Signed-off-by: Yves Brissaud <yves@dagger.io>
Split the rendering of types from the flat client, so that a package can render the types it owns, and leave out the fields a client contributes to a core type. A field can now say what it selects on and how, so that a module-level function can reuse the method rendering. The output of generate is unchanged. Signed-off-by: Yves Brissaud <yves@dagger.io>
Type registration, error reporting and the function call protocol went through the generated `dag`, `TypeDef`, `TypeDefKind`, `FunctionCachePolicy` and `JSON`. The SDK files may import nothing generated. `dagger.mod._api` selects the handful of fields module support needs by name, with the same arguments and defaults as the generated bindings, so the queries do not change. A TypeDefKind travels as its schema name, as `describe_json` already sends it; a cache policy and a JSON scalar go the same way. Input arguments now arrive in one query instead of one per argument, as the comment there always claimed. The tests compare the selections of the hand-written calls with those of the generated bindings, field by field. Signed-off-by: Yves Brissaud <yves@dagger.io>
`dagger.provisioning` imported `Client` to hand it out from `dagger.Connection` and to check the engine version. The version check is one raw query. The isolated client stays the generated root, because that is what `async with dagger.Connection()` yields today, but the package init now registers that root with `dagger.client.base`, the one file that may name generated code being the init itself. The package exports do not change. Signed-off-by: Yves Brissaud <yves@dagger.io>
Three checks over `sdk/src/dagger`, with `__init__.py` as the one exception: every module imports in an interpreter that refuses `dagger.client.gen`, `dagger_gen` and `dagger_clients`; no file names those packages; and no file imports a name the package init provides, since such a name may be generated. Signed-off-by: Yves Brissaud <yves@dagger.io>
Core becomes dagger_clients.core: its types, the entry function core(), and CORE_DIGEST. A client becomes dagger_clients.<name>: its own types, an entry function named after the client, and one module-level function per field it contributes to a core type, with the core receiver first; one name on two receivers gets an overload per receiver. The descriptor is plain data in _target.py. A client's files depend only on the client and on core, never on the other clients in the schema. The generated code targets Session, Target, client_root and client_select in dagger.client, which a later slice adds. Signed-off-by: Yves Brissaud <yves@dagger.io>
generate keeps its behaviour, because mod.dang still calls it. generate-core and generate-client write one package each into the dagger_clients namespace of an output directory, with py.typed. A client name that can't become a package is a usage error. Signed-off-by: Yves Brissaud <yves@dagger.io>
…less The descriptor is plain data, so a client package carries nothing it must resolve at import beyond the core digest it checks. Signed-off-by: Yves Brissaud <yves@dagger.io>
The entry function hands client_root the GraphQL field name, so the SDK never derives it from the class or the target. Each client checks its CORE_DIGEST against the installed core on import, through check_core. The caller passes --core-digest from the core it generated, because the two must match for the client to import. Signed-off-by: Yves Brissaud <yves@dagger.io>
The guard matched generated names with a line regex and only looked at module-level `import dagger` / `from dagger import X`. A lazy `from .gen import Client` inside a function passed all 69 tests. Walk every AST node, resolve relative imports to absolute names against the file's package, and reject anything landing on dagger.client.gen, dagger_gen or dagger_clients. The lazy relative import is kept as a regression case, along with `from . import gen`, TYPE_CHECKING blocks and method bodies. The submodule check compared paths, which a case-insensitive filesystem resolves `Client` to `client/`; it now compares listed names. `dagger/__init__.py` is the one file still allowed to name generated code. Pin that to the single star-import block so a second coupling fails, until the later slice swaps it for the optional dagger_global import. Signed-off-by: Yves Brissaud <yves@dagger.io>
The digest lied in both directions. Legacy ID names were computed from the whole schema, so a client type named DirectoryID took the compatibility class out of core while the digest stayed the same. The raw schema version was hashed, so two modern versions that render one core gave two digests. Legacy names now come from the type map of the part being rendered, and the digest hashes the canonical core surface plus only the compatibility mode, which is what changes generation. Signed-off-by: Yves Brissaud <yves@dagger.io>
A field of module A's type attributed to module B was emitted in A, queried with A's target, and B got nothing. A @sourcemap on a core enum value or input field was ignored. Only a field of a core object or interface type can be contributed; anything else is now an error at generation that names the type, the member and the two modules. Signed-off-by: Yves Brissaud <yves@dagger.io>
The inherited convert_id branch ran the query straight through the receiver's context, so TARGET was never attached and the field ran before its module was served. The selection now goes through client_select like every other contributed field, executes for the ID, and reloads the receiver from it. Signed-off-by: Yves Brissaud <yves@dagger.io>
--core-digest recorded any value, including an empty string or the digest of another schema, so check_core could pass on a client generated against different core types. The client's schema holds core, so its digest is computed and a given value that differs is refused, naming both. An empty --name or --ref is refused at parse time. Signed-off-by: Yves Brissaud <yves@dagger.io>
Two tests pinned whole literal fragments, including blank-line layout and the structure of the private dispatcher. They now assert on the imported names, the public signatures, the client_select arguments, the overloads and compilation, which is what a client relies on. Signed-off-by: Yves Brissaud <yves@dagger.io>
A client type named Arg replaced the imported query builder's Arg, and the entry function then failed while building its arguments. A package now imports every runtime and generator helper as _<name> and emits that alias everywhere. A generated type never starts with an underscore, and the per-receiver function of an overload now carries the GraphQL type name, so no generated name can spell an alias. The names in the contract don't change. The temporary one-file client keeps its plain names. Signed-off-by: Yves Brissaud <yves@dagger.io>
Core symbols were imported in the same statement as the digest, so a stale core that dropped a symbol raised a plain ImportError before check_core ran, and the message to regenerate was never seen. The client now imports the digest, runs the check, then imports the symbols. Signed-off-by: Yves Brissaud <yves@dagger.io>
A generated interface is a runtime-checkable protocol, so an object that implements it matched the interface's branch structurally and the first branch won: a Zebra took the Animal hook, which rejected its own argument. The dispatcher now goes by the receiver's GraphQL type name first, and falls back to the structural match only for an object with no hook of its own. The tests load the generated core and client against a fake of the runtime, so the dispatch is checked by calling it. Signed-off-by: Yves Brissaud <yves@dagger.io>
…missed A nested array in members made the runtime refuse the whole array, and generation then reported that it "misses sdk, clients/core, ...": the members that were fine, which sends the user to inspect the wrong entries. When the runtime refuses the file, the message now gives its own diagnosis, "members is not an array of strings". The list of missed or extra members stays for the case it is meant for, a runtime that read some members and disagrees about which. Signed-off-by: Yves Brissaud <yves@dagger.io>
The stricter array reader still scanned raw lines: it decoded no escapes and did not skip multi-line strings, so a dependency such as `tomli; python_version < "3.11"` made the runtime refuse a valid pyproject.toml, telling its author to write what they had written. [project] dependencies are the user's content, which generation cannot shape, so they must be read the way TOML defines them. The members, the project's dependencies and each member's are now read by tomllib, in one exec in the pinned default base image over only the scope's pyproject.toml files, so it is cached until one of them changes. The same script picks the members to install. It is inline, so the shared entrypoint's copy of the build carries it. The Go helper, which already parses TOML, is out of reach there: the shared entrypoint can read none of its own non-Dang files. The regular expressions keep the single-line keys the templates and `mod config set` write. An array holding anything but strings is still refused, and generation's message names the key the runtime refused, members or dependencies. The runtime now reads a quoted [tool.uv.workspace] header, so that no longer stops generation, and the message no longer lists it as unreadable. Signed-off-by: Yves Brissaud <yves@dagger.io>
Signed-off-by: Yves Brissaud <yves@dagger.io>
The comment on the refusal's wording claimed it matched generation's refusal of a file the runtime cannot read. The runtime now reads the workspace with tomllib, so that refusal no longer exists; the forms the editor asks for are its own limits, and the comment now says so. Signed-off-by: Yves Brissaud <yves@dagger.io>
…ly candidates The workspace reader caught a decode error only in the scope's own file. A malformed member, or a UTF-16 file, surfaced as a traceback from an inline script, with a line and column but no path, so the user had to guess which member to open. Every file is now read in one place that names it: "tools/m/pyproject.toml is not valid TOML: ...". Tables and names that are not what they should be are read as absent rather than raising. The mount took every pyproject.toml in the tree, a virtualenv's hundreds included, all in the read's cache key. .venv, __pycache__, node_modules and .git are left out: no member lives there. Signed-off-by: Yves Brissaud <yves@dagger.io>
* uc-fixer-dang-session-9176327a: (70 commits) runtime: name the pyproject.toml the reader cannot read, and mount only candidates pyproject: say whose limits the editor's refusal describes runtime: read workspace members and dependencies as TOML generate: when the runtime refuses the members, say why, not what it missed generate: refuse members the runtime and uv read differently, either way sdk: a module says it is one, so it never provisions an engine e2e: an unreadable marker licenses neither a delete nor an overwrite pyproject: say what to write when the editor refuses a scope file e2e: refuse a quoted workspace header in a scope generation wrote generate: name the scope-file shapes the runtime does not read engine-e2e: let a revert of the pin leave checks that can pass generate: refuse a scope file the module runtime would read differently runtime: install only the workspace members the project depends on sdk: say what to do with a src/dagger_gen.py generation keeps hack: run e2e-local.sh with CDPATH set pyproject: read a source written with dotted keys pyproject: keep a comment with the line it sits on sdk: provision the engine for the default session of a plain program generate: replace only the sdk/ an earlier SDK vendored generate: read the member marker as TOML, and only the kinds the SDK writes ...
…s it Signed-off-by: Yves Brissaud <yves@dagger.io>
Signed-off-by: Yves Brissaud <yves@dagger.io>
… hands over Under a Dang entrypoint, a module's code runs in an exec the entrypoint starts. The engine gives that exec no module context, so the process is a plain nested client whose current workspace is the one the engine finds in its own container. serveModule resolves a workspace path there, and a local client fails with "dir module source does not contain a dagger config file". The entrypoint holds the module's workspace: the engine passes it to every call. It now sends it with the call, and `python -m dagger.mod call` keeps it for the process. A local target then loads through that workspace: node(id) -> moduleSource(path) -> asModule -> serve. A git target keeps serveModule, which depends on no workspace, and so does every target in a process no entrypoint handed a workspace. The descriptor does not change. Signed-off-by: Yves Brissaud <yves@dagger.io>
…ace with the call Both entrypoints, the shared one and the one generated with a static entrypoint, found the module they serve through the workspace the engine hands them, and built it from there. That workspace is the caller's. It holds the module when the module sits in it, and not when the module was loaded from git, or served into another module's process: there the caller is that process, and its workspace is its own container. A module on an entrypoint that another module loaded as a client failed to build, with "workspace file .../dagger-module.toml: no such file or directory". Inside an entrypoint `currentModule` is the module it serves, so both now build from `currentModule.source`, and the static entrypoint no longer bakes in the module's workspace path: a moved module needs no regeneration. Both also send the workspace with each call, which the SDK now resolves a local client's target in. Signed-off-by: Yves Brissaud <yves@dagger.io>
A generated manifest named the builtin runtime, `[runtime] source = "python"`, beside the shared entrypoint, for engines that predate entrypoints. The floor is now one that runs them, and one that follows the entrypoint over the runtime, so the runtime table went unread and misled: its engineVersion looked like the view the module ran in, which on an entrypoint is always the engine's own. Generation now writes the entrypoint alone. The runtime this SDK wrote goes, with what only a runtime reads: engineVersion and [[dependencies]]. A runtime the user named is kept as it is, with no entrypoint added, because the engine would follow the entrypoint instead. What a runtime manifest can say and an entrypoint manifest cannot, include, exclude and a source elsewhere, is refused before anything is written rather than dropped; the keys are read with TOML and JSON parsers, since the manifest is the user's. Scope used a non-empty engineVersion to mean "a module's scope", and with it gone the check that the runtime reads the members uv reads stopped running for modules. Scope now takes isModule, and engineVersion only chooses the view. Signed-off-by: Yves Brissaud <yves@dagger.io>
The walkthrough pointed each module's [runtime] at this checkout. With generation writing the entrypoint alone, and an engine following the entrypoint over a runtime anyway, that ran the published @v1 entrypoint, which predates this layout. Each module now gets a copy of this checkout's entrypoint/, named by path, since the engine takes an entrypoint only from a git ref or from inside the module. The client loads through serveModule on a released engine, so the pointer to the development engine goes. Signed-off-by: Yves Brissaud <yves@dagger.io>
…h entrypoint engine-e2e existed because released engines lacked serveModule: its checks ran in a playground of an engine built from dagger/dagger. v1.0.0-beta.14 has serveModule and runs Dang entrypoints, so the e2e suite runs on the local engine again, and dagger.toml is its workspace config once more, as before 3955025. e2e-local.sh no longer swaps it in. devSdkCheck becomes cliSdkCheck, on the CLI of the beta.14 engine image, without the `dagger check` it ran, which would now recurse into itself. devClientCallCheck, which ran the caller on the runtime of this checkout, is replaced by entrypointClientCallCheck: nothing ran a module through an entrypoint end to end before, which is how a module on one could not load a local client. It does now, in both forms, the shared entrypoint of this checkout and the one --dang-entrypoint generates, from `dagger module init` to `dagger call`, with the client's target on an entrypoint too. Signed-off-by: Yves Brissaud <yves@dagger.io>
The first release that runs Dang entrypoints and serves serveModule. The SDK module declares it, and a plain program that provisions its own engine downloads that CLI instead of beta.10, which had neither. The README says how a module runs now, and what happens to a manifest that names a runtime. Signed-off-by: Yves Brissaud <yves@dagger.io>
It served engines released before serveModule. The floor is now v1.0.0-beta.14, which has the field, so an engine without it is below the floor, and its load fails as a ClientLoadError whose cause names the field. It is no stale client: regenerating a client cannot give the engine a field, so the message does not tell the user to. The staleness signal keeps its narrow reading: only the validator's own answer, with no path, GRAPHQL_VALIDATION_FAILED and the field and parent named, marks a stale client, and its tests are unchanged. The tests of the fallback's wire shape go with it, and so does the parametrized one that proved which load errors did not trigger the fallback: with no fallback, no load error triggers anything, and it no longer tested a decision. Signed-off-by: Yves Brissaud <yves@dagger.io>
…st match Signed-off-by: Yves Brissaud <yves@dagger.io>
Both entrypoints sent the caller's Workspace ID into the module's process, and module code could read any caller file through it. In Dagger an ID is a capability, and a module is third-party code. The entrypoint now resolves the local clients the caller's dagger.toml declares on the module's scope, and each call carries them by name. The scope is the one registered under the module's name that holds the module's own files, so a module loaded from elsewhere under that name gets nothing. Each client goes over as a directory-kind module source built from the files the engine loaded for it, plus its local dependencies'. Not the source Workspace.moduleSource returns, nor a Module: on beta.14 both keep the workspace, and withIncludes(["../secret"]) reloads the context from it, which reads the caller's file just as the workspace did. The loader looks a local target up by name and serves it. A name not handed over fails naming the client and pointing at `dagger generate`. A process an entrypoint runs never falls back to serveModule for a local target, since that resolves the path in its own container; an entrypoint that sends no clients says so. entrypointClientCallCheck proves it in both forms: module code tries every ID the loader holds as a workspace, a module source and a module, and its own current workspace, against a file only the caller has, and reads nothing, while a function declaring `ws: Workspace` still reads it. Against the workspace handover the same code reads the file. Signed-off-by: Yves Brissaud <yves@dagger.io>
Generation took any local Dang entrypoint for its own static one and replaced it with the shared entrypoint, so a user's `source = "./my-entrypoint"` was lost. And the manifest reader matched double-quoted values only, so `kind = 'dang'` read as no kind and a single-quoted fork was dropped as a stale entrypoint. The SDK's own static entrypoint is now the one at the path generation writes, ./sdk/entrypoint; every other entrypoint is the user's and is kept as written. Kind and source are read with TOML.decode, and the entrypoint table header is recognised however it is spaced or quoted. The static path's objections are read the same way: a builtin runtime written `source = 'python'` was refused as a runtime of the user's own. Signed-off-by: Yves Brissaud <yves@dagger.io>
Generation kept any Dang entrypoint already in the manifest. One that does not send the handover, an older pin or a fork, leaves a local client with nothing to load through, and the module failed at its first call to it. Refuse, not repair: repairing means replacing an entrypoint table the user wrote, and generation cannot tell whether a given pin or fork sends the handover. The refusal is narrow. It applies only to an entrypoint this SDK does not write, the shared source or its own static one, in a scope that declares a local client; without one, any entrypoint that runs the module will do. The message names the entrypoint and the clients, and says to remove the [entrypoint] table or the local clients. At run time the module process already says what happened when an entrypoint hands over nothing. Signed-off-by: Yves Brissaud <yves@dagger.io>
A manifest naming both a custom runtime and a Dang entrypoint was kept whole. The engine runs the entrypoint, on the session's version, while generation took the core it generates against from the runtime's engineVersion: the core/session skew the digest check exists to prevent. Beside this SDK's static entrypoint it was worse: that branch rebuilt the manifest and dropped the runtime without a word. Both tables are the user's and they contradict each other, so generation refuses before either branch runs. `dagger generate`, `dagger module init` and `dagger module client add` on such a module stop with: dagger-module.toml names both a runtime (<source>) and an entrypoint (<source>); the engine runs the entrypoint and never calls the runtime. Remove [entrypoint] to run on your runtime, or [runtime] to run on the entrypoint Signed-off-by: Yves Brissaud <yves@dagger.io>
A static entrypoint could carry the client list from generation. It must not: a baked list lives in the module's own files, which the module's author controls, while the caller's workspace config is what the caller agreed to. The comment says so, so the next change does not simplify it back. Signed-off-by: Yves Brissaud <yves@dagger.io>
static-scope-init-check still expected the static entrypoint to send `workspace: (workspace.id :: String!)`, the handover 41b5aad removed. It now expects the declared clients, refuses any workspace ID, and checks the static entrypoint carries the shared entrypoint's handover.dang byte for byte. Signed-off-by: Yves Brissaud <yves@dagger.io>
The checks that drive the floor release's CLI run it as a nested client of whatever engine runs the e2e module, so a pass on a later engine was taken for a pass on beta.14. The playground now asks the engine for its version first, and every check through it fails unless the engine is v1.0.0-beta.14 (a release's "+<build>" suffix aside), naming the command that runs the checks on the floor engine. This replaces the `dagger check` the old devSdkCheck ran rather than restoring it. From inside the e2e module it would run the whole suite inside itself on the same engine: repetition that doubles a run already near its timeouts. Pinning the engine the suite runs on proves what that nested run proved. Signed-off-by: Yves Brissaud <yves@dagger.io>
Five checks went through sdk-sdk, whose pinned harness (its latest commit) drives a v1.0.0-beta.10 CLI. On the beta.14 engine that CLI's `dagger sdk install` connects and then hangs until sdk-sdk's 10-minute timeout kills it. It does the same for a two-file Dang SDK module with nothing of this repository in it, so the hang is the CLI/engine pairing, not this SDK. A later CLI cannot stand in: sdk-sdk passes `-y` and `--progress` to `dagger sdk`, which beta.14 refuses. None of the five needed sdk-sdk for more than running `dagger call` in a workspace holding this checkout, which the e2e playground already is: the floor release's CLI, on an engine the playground checks is the floor. The checks keep their assertions, and the missing-generated-files one still asserts the failure and its message. The vendored-layout detour the client call needed is gone, and so is the sdk-sdk dependency. Signed-off-by: Yves Brissaud <yves@dagger.io>
… workspace Signed-off-by: Yves Brissaud <yves@dagger.io>
The refusal of an entrypoint this SDK does not write, in a scope with a local client, offered two ways out: drop the entrypoint or drop the clients. The one a fork's author would take was missing: make the fork hand clients over. Offering it alone would not have helped, since generation refused every such entrypoint whatever it did. Generation now reads the entrypoint from where the engine resolves it (a path in the module, an address, or a module reference) and accepts it when it carries this SDK's handover.dang unchanged and another of its files sends `ClientHandover(workspace: workspace).clients` with the call. The refusal names that as its first option, with the line to add. A pin of the shared entrypoint is accepted the same way once its release carries the handover. One that cannot be read, or carries the file without calling it, is still refused. Signed-off-by: Yves Brissaud <yves@dagger.io>
4a819e2 made every check that drives the floor release's CLI refuse an engine that is not exactly v1.0.0-beta.14. That is the right condition for a job that claims the floor, and the wrong one for running the suite at all: the day beta.15 exists, the ordinary suite stops running, a cost paid by everyone every day to protect one property. The playground no longer asserts anything about its engine. The floor job, hack/e2e-floor.sh, provisions the floor's engine with DAGGER_ENGINE=image://registry.dagger.io/engine:v<floorVersion>, reads the version from .dagger/modules/e2e/main.dang so there is one to change, and runs `assert-floor-engine` before any check: the engine must report the floor (a release's "+<build>" aside) or the job fails, naming itself as the command to run. hack/e2e-local.sh runs the same checks on whatever engine it finds. Signed-off-by: Yves Brissaud <yves@dagger.io>
The independent correctness re-check of the handover never completed, so the probe stands in for it. It walked three routes; it now walks every route on a ModuleSource that could rebuild a directory from outside the source's own loaded files, which is the shape that defeated attempt two (a Workspace.moduleSource result reloading its context on withIncludes). Added: generatedContextDirectory, updatedConfigDirectory, generatedContextChangeset.layer, withSourceSubpath, and the withName / withSDK / withEngineVersion mutations, each into contextDirectory; one level of recursion through dependencies and asModule.source. The docstring lists what it rejects and why (scalars, schema JSON, generate(workspace:), the with* attach/mutate fields whose result is still walked). Two corrections the reviewer's instinct demanded. A climb leak lands on the workspace root only when the number of "../" equals the module's depth: too few stays inside, too many escapes and the engine refuses the pattern. A fixed depth of 8 overshot and missed the exact-depth read, so every climbing route is now tried at depths 1..12. And a route that fails is only reassuring if a resolver refused it, not if the field is absent from the engine; the probe tells a validation error from a refusal and reports absent=0, which the check asserts. Verified: against attempt two's shape (handing over the workspace- retaining source) the probe reads the caller file at withIncludes(../x3), the module's depth, and the check fails; against the fix it walks 4305 routes with absent=0 and reads nothing, under both entrypoint forms. Signed-off-by: Yves Brissaud <yves@dagger.io>
* uc-fixer-entrypoint-handover-32e9c924: e2e: extend the handover probe with every context-rebuilding route e2e: prove the floor in its own job, and run the suite on any engine python-sdk: let a forked entrypoint hand clients over, and say how e2e: run the runtime checks on the floor CLI, not sdk-sdk's e2e: refuse to prove the floor on an engine that is not the floor e2e: the static entrypoint hands over clients, not the workspace entrypoint: say why the handover is read at every call, not baked python-sdk: refuse a runtime of the user's own beside an entrypoint python-sdk: refuse an entrypoint that cannot hand a local client over python-sdk: replace only the entrypoint generation wrote, read as TOML entrypoint: hand the module its declared clients, never the workspace client: remove the load path for engines without serveModule python-sdk: move the floor to v1.0.0-beta.14 e2e: fold engine-e2e into the e2e suite, and run a module through each entrypoint hack: run the walkthrough on this checkout's entrypoint python-sdk: generate a module on its Dang entrypoint alone entrypoint: build the module from its own source, and send the workspace with the call client: load a local target through the workspace a module entrypoint hands over
Signed-off-by: Yves Brissaud <yves@dagger.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft, for review of the approach as much as the code.
One generated client artifact per module, used by a module and by a plain Python program alike. It replaces the per-dependency bindings and
[[dependencies]].The design is committed with the code:
hack/designs/2026-09-15-unified-clients.md.What a scope looks like
Any directory that uses clients — a module's own directory, or a plain project:
Each client is a PEP 420 namespace package under
dagger_clients, a uv workspace member with no path of its own, so the same artifact is byte-identical in two different scopes.What works
Verified by hand with the released CLI, outside the check harness.
hack/try-unified-clients.shbuilds the whole walkthrough from nothing in a temporary workspace.v1.0.0-beta.14, throughQuery.serveModuleand under both entrypoint forms.[project]table at all, calling a client throughdagger.connection().dag.container()anddagger.Containerkeep working behind[tool.dagger] global-client = true, and clearing the flag gives back exactly the tree of a module that never had one.What is open, and not fixable here
A changed client target does not invalidate its caller's cached result.
[[dependencies]]used to put the target into the caller's identity; a unified client records a path and loads the module at run time, so nothing about the target reaches the caller's cache key. Change the target, call the caller with the same arguments, and the old answer comes back; call it with a different argument and the change appears.This reproduces on a released engine and on
284cd849alike, so it is a property of loading a module at run time rather than of the new field. Every SDK that drops[[dependencies]]inherits it. Written up language-neutrally, with the measured behaviour kept separate from the inferred mechanism:hack/designs/2026-09-18-serve-module-cache.md.A module driven by a Dang entrypoint cannot reach a second hop. In a chain
demo → lib → leaf,lib's entrypoint is handed the caller's container-derived workspace rescoped to/.dagger/modules/lib, solibcannot load its own local client. One hop works; two do not.core/sdk/entrypoint/entrypoint.go,Workspace()at beta.14.The SDK that generates a module must be the one that runs it. A generated manifest names the published shared entrypoint, which predates this layout and refuses the module. An entrypoint source may name only a git ref or a path inside the module, so a checkout under development cannot point at its own. Written up in
hack/designs/2026-09-18-workspace-sdk-runtime.md. Before release, tagentrypoint/v1.xfrom this branch.Smaller, also engine-side:
dagger module client add ../librecords./.dagger/modules/lib, butdagger module client rm ../librefuses the same argument, becausewithoutClientdoes not normalise the address the waywithClientdoes.To try this branch today:
hack/try-unified-clients.shbuilds the walkthrough from nothing and copies this checkout's entrypoint into each module, which is what makes a development checkout runnable.Modules run on a Dang entrypoint, and are handed only their clients
Generated manifests carry no
[runtime]: a module runs through the shared Dang entrypoint, or the one--static-entrypointgenerates inside it. The floor is released v1.0.0-beta.14, which drives entrypoints and hasserveModule; the load path for engines without it is gone.That exposed a defect worth recording. A Dang entrypoint runs the module's Python in an ordinary nested exec, and the engine attaches module context only to execs it starts itself — so the process reports its own container as its workspace and no
currentModuleat all. Whatever module context the code needs, the entrypoint must hand over.What it hands over is the least thing that works: one module source per declared client, built from the files the engine already loaded, detached from the workspace they came from, re-read from the caller's config at every call. Not the workspace, and not a handle that leads back to one — both were tried and both leaked, the second because a
ModuleSourcefromWorkspace.moduleSourcekeeps its origin andwithIncludes("../…")climbs back out.A probe check runs module code that walks every ID the loader holds and every
ModuleSourceroute that could rebuild a directory — at every climb depth, distinguishing an absent field from a refused capability — and reads nothing: 4,305 routes,reads=[],absent=0, under both entrypoint forms. Against the leaking shape the same probe reads the caller's file, so the check has a negative control.The rule most of the review time went into
Generation writes into a directory the user also owns, so it must touch only what it made. Making that true took one critical defect and eleven more:
[tool.dagger] generatedmarker is read as TOML, never as text, and onlyclient,coreandruntimecount. A regex over raw text once deleted a user's directory because a multi-line string in it happened to quote a marker.clients/corestops generation rather than being replaced.[tool.uv.workspace] membersand[project] dependenciesare parsed withtomllibin the SDK's pinned image. A hand-written reader failed this twice in opposite directions: first too generous, then refusing"tomli; python_version < \"3.11\""as "not an array of strings".Checks
sdk: 546 passed.helpers/pyproject: 39 Go tests.hack/e2e-local.shruns them against your engine;hack/e2e-floor.shpins the floor release and asserts it before any check runs.Phases
This is phase 1. Phase 2 publishes the SDK files and ends the migration: the
sdk/copy leaves the scope, and the global client is removed after a deprecation period.