Skip to content

Make artifacts scope optional - #109

Merged
aron-cf merged 11 commits into
mainfrom
artifacts-optional-scope
Aug 18, 2026
Merged

Make artifacts scope optional#109
aron-cf merged 11 commits into
mainfrom
artifacts-optional-scope

Conversation

@aron-cf

@aron-cf aron-cf commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

The artifacts client always needed a session id. That was fine for an agent that owns one corner of a namespace, but it left no way to look at the namespace as a whole, and a Workspace configured with an Artifacts binding and no session id threw from its constructor rather than giving you a client at all.

The session id is now optional. Leave it out and the client works in the names the namespace stores: list() returns every repository, including the ones sessions own under <session>__<name>, and each of those can be read, written to, or deleted by that name. Pass a session id and nothing changes. A Workspace picks the session up from its own sessionId, and artifacts: { binding, sessionId: null } asks for the namespace-wide client even when the workspace has a session of its own.

const mine = createArtifact(env.ARTIFACTS, "agent-7");
await mine.list(); // [{ name: "build-cache" }]

const all = createArtifact(env.ARTIFACTS);
await all.list();  // [{ name: "agent-7__build-cache" }, ...]
await all.delete("agent-7__build-cache");

That reach covers writes and deletes, so the unscoped client is for a caller that administers a namespace, not one that lives in it. To keep the two apart, an empty session id stays an error: only an omitted or null id means "no session", so an id that came out blank by accident fails instead of quietly opening up every session.

Names get one concession. A scoped client still refuses __ in a name, since that separator is what makes the split unambiguous. An unscoped client has to accept it, because the stored names it works in contain it.

The scoping rules, the client, and the workspace wiring each landed with their own tests, and the in-shell artifacts command now prints help that matches the client it is bound to instead of promising a session prefix that may not be applied. Run npm test --workspace @cloudflare/computer to see the lot.

Two documentation notes. docs/15_artifacts_interface.md gained a section on the namespace-wide client, covering what its names mean and why a blank id is rejected. Separately, the word "facade" is gone from the prose across the repository in favor of "wrapper", which says the same thing without borrowing a pattern name.

@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

馃 Changeset detected

Latest commit: 0025615

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@cloudflare/computer Minor
@cloudflare/dofs Minor
@cloudflare/computer-rpc Minor
@cloudflare/computerd Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

devin-ai-integration[bot]

This comment was marked as resolved.

The scoping helpers took a session id as a required string, so
there was no way to express a client that addresses the namespace
as a whole. They now accept an undefined session id and pass names
through untouched in both directions, filtering nothing.

An unscoped name is held only to the charset the binding enforces,
not to the stricter local-name rule. The names such a client works
in are the ones the namespace stores, and those include the
separator-bearing names scoped sessions mint, so rejecting the
separator would put every scoped repository out of reach.

Absence has to be spelled out. An empty string stays a validation
error rather than a synonym for an omitted id, so an id that came
out empty by accident fails loudly instead of quietly widening a
client from one session to all of them.
createArtifact required a session id, so the only client it could
build was one confined to a single session. A caller administering
a namespace had no way to reach across it, and a Workspace with an
Artifacts binding but no session id threw from its constructor.

The session id is now optional. Omit it and the client addresses
the namespace as a whole: names are the ones the binding stores,
list returns every repository including those a session owns, and
each one can be read, written to, or deleted under its stored
name. Passing a session id behaves exactly as before.

ArtifactClient.sessionId widens to string | undefined to say which
of the two a client is. The scope is resolved once at construction,
so a malformed id still fails there rather than on first use.
The CLI help stated the session-scoping contract unconditionally:
names are local, a prefix is added on the way in, and repo list
shows one session's repositories. None of that holds for a client
built without a session id, and help is the contract a shell
consumer reads before choosing a name.

Each help path now takes the scoping the client actually applies
and prints the paragraph that is true of it. The namespace-wide
wording also spells out that the listing spans sessions and that a
stored name reaches into one, so nobody has to infer the reach of
the client they were handed.
A Workspace configured with an Artifacts binding and no session id
threw from its constructor. The session id fell through to the
empty string, and the facade rejected that, so the only way to get
an artifacts surface at all was to have a session to scope it by.

An absent session id now yields a client over the whole namespace.
`artifacts.sessionId: null` asks for the same thing explicitly,
for a workspace that carries a session id of its own but wants
artifacts across every session.

WorkspaceOptions.sessionId documents the empty string as its unset
value, so an empty id is read here as "no session to scope by".
The facade stays stricter and rejects an empty id outright: a
caller reaching it directly has no such convention, and an id that
came out empty by accident should not quietly widen a client from
one session to all of them.
The artifacts documentation described one kind of client: bound to
a session, working in local names, listing one session's
repositories. A client built without a session id does none of
that, and a reader following the old text would pick names that
address the wrong repository or none at all.

The interface spec gains a section on the namespace-wide client,
covering what its names mean, that its reach includes writes and
deletes, and why an empty session id is still an error rather than
a way to ask for it. The package README, the docs index, and the
module comments pick up the same qualification.
The line describing repo get promised metadata carrying a local
name, which holds only for a client scoped to a session. An
unscoped client returns the name the namespace stores.
The introduction said the wrapper is "session-scoped when you ask
for it" and then described binding a binding, which took five lines
to say what three say plainly. The paragraph on the empty session
id opened with a drum roll and spent three lines on a rationale the
code comments already carry.

The changeset summary becomes one sentence and a pointer to the
interface document, since it is read as a single line of release
notes rather than as a description of the change.
"Facade" is a design-pattern name standing in for a plain word.
Every prose use across the documentation, the package readmes, and
the source comments becomes "wrapper", along with the one local
variable in the computerd command-line tests that carried the term
in its name.

Three sentences needed more than the word swap to keep reading
well: an observability comment that would have wrapped a span in a
wrapper, a workspace comment whose wrappers wrapped their own entry
points, and a project-layout line that listed the root export among
the public entrypoints.
The artifacts wrapper rejects an empty session id so a blank value
cannot quietly turn one tenant's client into a client over every
repository in the namespace. The Workspace path read an empty id as
an absent one instead, which walked around that check: a caller
deriving the session id from an unset variable or a missing request
field got the namespace-wide client rather than an error.

An empty id now reaches the wrapper and fails there, as it did
before the session id became optional. Omitting the id, or passing
null, remains the way to ask for a client over the whole namespace.
@aron-cf
aron-cf force-pushed the artifacts-optional-scope branch from 5877e1b to 8aec189 Compare August 18, 2026 13:50
@pkg-pr-new

pkg-pr-new Bot commented Aug 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@109

commit: 0025615

devin-ai-integration[bot]

This comment was marked as resolved.

An omitted property or an unset variable is undefined in
JavaScript, so it intentionally selects the namespace-wide client.
The Workspace comments and interface document incorrectly named
those values as cases that raise InvalidSessionIdError.

State the actual boundary instead: blank input and code that
normalizes a missing value to an empty string fail, while omitted
and undefined values use the documented default. Keep separate tests
for an omitted property, an explicit undefined value, and an empty
string so the distinction stays visible.
devin-ai-integration[bot]

This comment was marked as resolved.

The disabled artifacts client carries an undefined session id, so
the scope-aware help mistook it for a configured client over the
whole namespace. Repository and token operations still failed, but
their help promised access that did not exist.

Give the dispatcher a third help state for a missing binding. Top
level, repository, and token help now print the same configuration
notice as failed operations instead of describing either session or
namespace access. Direct artifacts clients keep their existing two
states.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +1352 to +1356
const configured = options.artifacts?.sessionId;
if (configured !== undefined) return configured;
return options.sessionId;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃煥 Missing session id now silently grants namespace-wide artifacts access instead of failing

artifactsSessionId (packages/computer/src/workspace.ts:1352-1356) resolves to undefined whenever neither artifacts.sessionId nor WorkspaceOptions.sessionId is set, and createArtifact then builds an unscoped client. An unscoped client takes stored names literally and applies no prefixing or filtering (packages/computer/src/artifacts/scope.ts:96-98, packages/computer/src/artifacts/scope.ts:115-116), so list, get, createToken, and delete reach every repository in the namespace, including those other tenants own. Before this change the same configuration threw at construction, so a workspace that forgot to pass a session id failed loudly; now it comes up with namespace-wide authority. Because the worker-shell backend always exposes the artifacts command and forwards to workspace.artifacts.cli(...), an agent running in that shell of a session-less workspace can enumerate, mint write tokens for, and delete other sessions' repositories.

Open in Devin Review

Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

@aron-cf
aron-cf merged commit 9b9156e into main Aug 18, 2026
19 checks passed
@aron-cf
aron-cf deleted the artifacts-optional-scope branch August 18, 2026 15:15
@github-actions github-actions Bot mentioned this pull request Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant