Note
This doc describes shipped code in
packages/computer/src/artifacts/ and the artifacts custom
command in packages/computer/src/backends/worker-shell/.
Cloudflare Artifacts
is versioned, Git-speaking repository storage. A Worker reaches it
through a namespace binding (env.ARTIFACTS) that can create,
inspect, import, and delete repositories and mint Git tokens scoped
to a repository.
@cloudflare/computer/artifacts wraps that binding with optional
session scoping. createArtifact(binding, sessionId) returns a
client whose every operation is implicitly scoped to that session.
import { createArtifact } from "@cloudflare/computer/artifacts";
const artifacts = createArtifact(env.ARTIFACTS, agentId);
const repo = await artifacts.create("build-cache", {
description: "CI artifacts for this agent",
});
// repo.name -> "build-cache" (local, unscoped)
// repo.remote -> "https://.../<agentId>__build-cache.git"
// repo.token -> initial git token (a secret)The session id is optional. createArtifact(binding) returns a
client over the namespace as a whole — see
without a session id.
The session id is a name prefix. A repository the caller names
build-cache is stored in the namespace as
${sessionId}__build-cache. The caller never types the prefix: it
is added on the way into the binding and stripped on the way back
out, so every name a caller passes or receives is local.
create("foo")storessessionId__foo; the returnednameisfoo.get("foo"),delete("foo"), and the token methods all addresssessionId__foo.list()returns only the repositories that belong to the session, each with the prefix removed. Repositories from other sessions in the same namespace are filtered out.
Artifacts repository names may contain letters, digits, ., _,
and -, but not /. The scope separator is therefore a double
underscore (__). Both the session id and the local name forbid
that separator, so the split is unambiguous. An empty, malformed, or
separator-bearing session id throws InvalidSessionIdError at
construction; the same constraint on a repo name throws
InvalidRepoNameError.
This lets one namespace host many isolated sessions — one per agent, user, or task — without the caller managing prefixes by hand, and without one session enumerating or colliding with another's repos.
createArtifact(binding) builds a client with no session at all.
Nothing is prefixed on the way in, nothing is stripped or filtered
on the way out, and the names it works in are the ones the
namespace stores.
const artifacts = createArtifact(env.ARTIFACTS);
artifacts.sessionId; // undefined
await artifacts.list(); // every repo in the namespace
await artifacts.get("agent-7__build-cache"); // another session's repo
await artifacts.create("shared-cache"); // stored as "shared-cache"A name here may carry the scope separator, because the repositories
a session owns are stored under <session>__<name> and reaching
them is the point. Names are still held to the binding's charset,
so assertRepoName rejects the same empty and /-bearing names it
always did.
The reach covers writes as well as reads: create,
import, delete, and the token methods all take the stored name
literally, so an unscoped client can mint a token for, or delete,
a repository a session created. Give a session id to any client
that represents one tenant, and leave it off only for a caller that
administers the namespace.
Only an omitted or null session id means "no session"; an empty
string is still an InvalidSessionIdError, so an accidentally empty
id fails instead of silently widening the client.
Like workspace.git, the artifacts surface has a typed API and an
argv-driven CLI backed by one implementation, so they cannot drift.
- A typed JavaScript API —
artifacts.create({...}),artifacts.createToken(...), and so on. Object-options in, structured values out. - An argv-driven entry point —
artifacts.cli({ argv, env }). Every flag-shape decision lives inartifacts/cli.ts; the typed methods and the CLI route to the same client.
The exceptions are the CLI's top-level shorthands. create composes
create and createToken and then registers a git remote; share
composes get and createToken and prints a single credentialed
URL. These compositions are CLI-only — there is no single typed
method for them, and create's git step rides on an injected seam
(below) rather than a client method. The pieces they compose are
still the same client methods, so the two doors do not drift on the
parts they share.
createArtifact(binding, sessionId?) returns an ArtifactClient.
The table describes a client with a session id; without one, every
row reads the same with "the session's" replaced by "the
namespace's" and "local name" by "stored name":
| Method | Purpose |
|---|---|
create(name, opts?) |
Create a repo. Returns { name, remote, defaultBranch, token } with a local name. |
get(name) |
Resolve full ArtifactsRepoInfo metadata with a local name. Throws if missing. |
list() |
The session's repo summaries: Omit<ArtifactsRepoInfo, "remote">[], each with a local name. Walks every page. Unscoped, this is the whole namespace. |
import(name, source, opts?) |
Import an external git remote into a session repo. |
delete(name) |
Delete a repo. Returns false when it does not exist. |
createToken(name, scope?, ttl?) |
Mint a git token. Returns { id, plaintext, scope, expiresAt }. |
listTokens(name) |
A repo's token page (metadata only). |
getToken(name, id) |
One token's metadata from that page. Throws NotFoundError on a miss. |
revokeToken(name, tokenOrId) |
Revoke a token. Returns false on a miss. |
cli(input) |
The argv door (below). input may also carry a remoteAdd seam used only by the CLI create shorthand. |
sessionId on the client is the session it is scoped to, or
undefined when it spans the namespace.
opts for create carries description, readOnly, and
setDefaultBranch. source for import carries url, branch,
and depth; its opts carries description and readOnly. scope
is "read" or "write" (default "write"); ttl is in seconds.
list() exposes no cursor. It walks the binding's pages internally
until they are exhausted and returns the full set: the session's
repositories under a session id, every repository in the namespace
without one. The page size is an internal constant, not a
caller-facing cap.
With a session id the walk also does the filtering, so a namespace shared by many sessions costs the same number of round trips either way.
The binding has no direct token accessor. It also exposes
listTokens() without a caller-supplied cursor, even though the
result type carries a page of tokens plus a total. So
getToken(name, id) filters the returned page and raises
NotFoundError when no token in that page matches.
artifacts.cli({ argv }) dispatches two top-level shorthands,
create and share, plus two groups, repo and token.
artifacts help # top-level help
artifacts --help | -h # alias for help
artifacts repo --help # repo group help
artifacts token --help # token group help
artifacts create <name> [--scope read|write] [--ttl DUR] [--remote NAME] \
[--default-branch B] [--description D] [--force]
artifacts share <name> [--scope read|write] [--ttl DUR]
artifacts repo create <name> [--description D] [--default-branch B] [--read-only]
artifacts repo get <name>
artifacts repo list
artifacts repo delete <name>
artifacts repo import <name> --url U [--branch B] [--depth N] [--read-only] [--description D]
artifacts token create <repo> [--scope read|write] [--ttl DUR]
artifacts token list <repo>
artifacts token get <repo> <id>
artifacts token delete <repo> <id|plaintext> # alias: revoke
Output is machine-first. Reads and data-producing mutations
(create, repo create, get, list, import,
token create/list/get) print JSON on stdout. share prints a
single credentialed remote URL. delete and token delete print a
one-line confirmation.
artifacts create <name> composes the three steps a caller
otherwise runs by hand: it creates the repo, mints a git token, and
registers a git remote whose URL carries that token. It is a
convenience over the repo and token primitives, which remain for
the uncomposed cases.
--scopedefaults towrite: the point of the shorthand is a remote you can push to. Areaddefault would register an origin that rejects the first push.--remotenames the git remote to register; it defaults to<name>, so--remote originis the common override.--ttlaccepts either bare seconds or a unit-suffixed duration —30s,5m,1h,2h30m,1d. The same grammar applies totoken create --ttl. A bare integer is still seconds, so existing invocations keep working.
The printed JSON carries the bare remote (non-secret), the
credentialedRemote (the push/clone-ready URL with the token folded
in as basic-auth — a secret), defaultBranch, the gitRemote name,
the scope, the token plaintext, and remoteRegistered. When the
shell wires no git seam, remoteRegistered is false and a
remoteAddCommand field carries a ready-to-run git remote add
line instead.
The three steps are sequential side effects, so a failure can leave
a repo (and token) behind. Re-running a bare create then fails
because the repo already exists. --force is the recovery path: it
reuses an existing repo rather than treating it as a collision, and
updates an existing git remote rather than refusing it. Without
--force, either pre-existing piece is a hard error (exit 1) whose
message names --force. Each --force run mints a fresh token; the
prior token keeps working until its TTL.
The git step is injected. The artifacts package owns no git: the
worker backend hands the CLI a remoteAdd closure backed by the
same workspace.git.cli(...) the built-in git command uses. The
typed create/createToken methods never learn about git, so the
JS API and the CLI cannot drift.
artifacts share <name> is the read-side counterpart to create.
It mints a git token for an existing repo and prints just the
credentialed remote URL on stdout — one clone/push-ready string, no
JSON envelope — so a caller can hand off a link without parsing
output or hand-building the URL.
--scopedefaults toread: the common case is handing a fetch-only link to a consumer. Pass--scope writefor a pushable URL.--ttltakes the same duration grammar ascreate.
The repo must already exist; a missing repo is a hard error (exit 1) and no token is minted. The whole printed URL is a secret — it carries a live token. Each call mints a fresh token, so revoking one shared link does not disturb others.
Help is a first-class, agent-readable surface. help, --help,
-h, and each group's --help print documentation that spells out
the secret-handling rules and the client's actual state: local names
under a session, stored names and a namespace-wide repo list
without one, or a clear configuration notice when no binding is
available. A bare artifacts prints the top-level help and exits
non-zero, the way git with no args does.
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
The operation failed (repo not found, name collision, unknown subcommand, token miss). |
129 |
Malformed command line (unknown flag, missing required value or positional). |
token create and the create shorthand print a token's
plaintext; share prints a remote URL with a live token embedded;
and repo create / import return an initial token.
The create shorthand additionally prints a credentialedRemote
URL with that token embedded — treat the whole URL as a secret.
token list and token get show metadata only. Capture a token's
plaintext when it is minted; it is not retrievable afterward.
The worker backend's shell isolate always exposes an artifacts
command. The command forwards through the WorkspaceStub returned by
getWorkspace() and calls workspace.artifacts.cli(...), matching
the built-in git command's workspace.git.cli(...) path.
A host durable object wires the command by passing its
env.ARTIFACTS binding to Workspace:
export class MyAgent extends DurableObject<Env> {
#workspace: Workspace;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.#workspace = new Workspace({
storage: ctx.storage,
sessionId: ctx.id.toString(),
artifacts: { binding: env.ARTIFACTS },
backends: [new WorkerShellBackend(/* ... */)],
});
}
}The client is scoped to a session when one is available. The session
id defaults to WorkspaceOptions.sessionId, and
artifacts.sessionId overrides it. With neither — or with an
explicit artifacts: { binding, sessionId: null }, the opt-out for
a workspace that has a session id but wants artifacts across every
session — workspace.artifacts spans the namespace.
An omitted or undefined session id intentionally gives the
workspace access to the whole namespace. An empty string is
different: the constructor throws InvalidSessionIdError. This
catches blank input and code that normalizes a missing value to
"", such as sessionId: request.sessionId ?? "", rather than
quietly widening that tenant's access.
When artifacts is omitted from Workspace, the command still
exists, but operations fail with a clear "Workspace Artifacts binding
is not configured" error. Its help prints the same notice instead of
describing session or namespace access that is not available.
The binding stanza in the consumer's Wrangler config:
Inside bash.exec, artifacts repo list then forwards across the
loopback to the client's cli(...). The shell isolate has no
network of its own; the binding call happens host-side, the same way
network-bound git subcommands do.
The create shorthand's git step rides the same wiring. The shell's
artifacts command hands the CLI a remoteAdd closure backed by
workspace.git.cli(...), bound to the shell's working directory, so
the remote is registered host-side in the repo the caller is sitting
in. No extra binding is needed beyond the git surface the
WorkspaceStub already exposes.
The binding and its wire shapes are the global types from
@cloudflare/workers-types: Artifacts (the namespace binding),
ArtifactsRepo (the repo handle), ArtifactsCreateRepoResult,
ArtifactsRepoInfo, ArtifactsTokenInfo, ArtifactsError, and so
on. createArtifact(binding, sessionId?) takes an Artifacts and
returns metadata in those same shapes — the wrapper adds session
scoping, it does not redeclare the protocol. Workers consumers get
the globals from their own @cloudflare/workers-types setup, and
this package's typecheck uses the same source of truth.
The in-memory FakeArtifactsBinding the tests run against
implements Artifacts, so the type checker holds the fake to the
real interface; a drift in the published shape fails the build
rather than passing green.
The wrapper covers the repository and token lifecycle. The binding's
fork is intentionally out of scope for now.
{ "artifacts": [{ "binding": "ARTIFACTS", "namespace": "default" }] }