Conversation
The assets module needs a short, URL-safe token to make every share unique. A hyphenated UUID is 36 characters; Crockford base32 over the same 16 random bytes is 26 lowercase characters with no hyphens and no visually ambiguous letters. Add encodeBase32 / decodeBase32 and a randomId helper that pulls 16 bytes from crypto.getRandomValues. The random source is injectable so tests can pin the output. Decoding folds the ambiguous letters back so a transcribed token still resolves.
Shared assets need a Content-Type so a browser opening the presigned URL renders them correctly. A full mime database is overkill for the file kinds an agent shares, so ship a small table covering images, text, common documents, archives, and media, with an application/octet-stream fallback. The lookup keys off the basename's extension and is case-insensitive. Dotfiles and extensionless paths take the fallback.
Presigning an R2 GET URL is a handful of HMAC-SHA256 steps over deterministic strings, so implement SigV4 in house rather than pull a dependency. The presigned-GET path uses UNSIGNED-PAYLOAD, so the URL is a pure function of the inputs and the clock and no request body is read. Keep a sha256HexStream helper that hashes a ReadableStream incrementally in constant space, for the future signed-upload path a buffering library can't serve. Tests validate the signing-key derivation and empty-string digest against AWS's published vectors.
The constructor already accepts sessionId and forwards it to mount factories, but never surfaced it. The assets module tags each shared object with the originating session, so expose a read-only getter that returns the configured id (empty string when unset).
createAssets({ ws, bucket, s3 }) binds a workspace and an R2 bucket
and returns a client whose share(path, opts) uploads a VFS file to R2
and returns a presigned GET URL.
The object key is prefix/id/basename: a fresh base32 id per share, so
the same file shared twice yields two distinct keys, and only the
basename lands in the key so the full VFS path is never exposed. The
body streams straight from ws.fs.readFile into bucket.put without
buffering. Content-Type is inferred from the extension, the
Content-Disposition carries the filename, and custom metadata records
the source path, session id, and expiry.
Credentials and the account-derived endpoint resolve from the standard
R2 / Cloudflare env vars when not passed explicitly. The tsconfig
gains the node types entry dofs already carries, matching the
node:crypto already externalised in the rolldown config.
Add the ./assets entry to the package exports and the rolldown input map so consumers can import createAssets from @cloudflare/workspace/assets. The module builds to its own chunk; node:crypto stays external, as it already was in the config.
Document the assets module: createAssets, the share options, the prefix/id/basename key shape and why every share is unique, the object metadata, the credential resolution from the environment, and the expiry-versus-cleanup distinction. Link it from the docs index and the workspace package README.
Give the triage agent a share tool that uploads a workspace file to R2 and returns a time-limited link, so it can hand the user an artifact it produced. The tool binds the bucket and credentials with createAssets and exposes only a path and an optional lifetime to the model. The presigner needs R2 S3 credentials the bucket binding can't surface, so the tool is registered only when R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY are present. Without them the agent runs unchanged and the tool is omitted. Add the ASSETS bucket binding and the credential vars to the wrangler config.
Add a Worker plus Durable Object example that turns a one-shot POST /prompt into an image. The Durable Object runs the prompt through a Workers AI text-to-image model, writes the generated PNG into a backend-less Workspace, then uploads the file to R2 and returns a presigned link through createAssets. The example is production-only: the presigner needs R2 S3 credentials set as secrets, and the image model runs on Cloudflare's network, so a local dev stack can't produce a working link. The README covers the bucket, credentials, deploy, and request shape.
List the R2 S3 credential names the presigner needs in a .dev.vars.example file so the credentials are discoverable without reading the source. Ignore real .dev.vars files while keeping the example tracked, and point the README at it.
The model call extracted env.AI.run into a local and invoked it unbound, so the binding's run() ran with an undefined `this` and threw "Cannot set properties of undefined (setting '#options')" from inside the binding. Keep the call on env.AI through a typed view of the binding so run() retains its receiver.
Fix the assets example comment so the documented response shape matches the code, and document the think example's optional share tool setup: the ASSETS bucket, the R2 S3 credential secrets, and the fact that the tool is omitted when credentials are absent. Add tests for live assets error paths that reviewers identified as uncovered: NaN expiry, the seven-day presigned URL cap, missing-file rejection without uploading, and the remaining resolveS3 failure branches.
R2 rejects an arbitrary ReadableStream body because it cannot infer a content length. Size the VFS file with fs.stat, pipe the read stream through a FixedLengthStream, and pass the readable half to bucket.put so the binding receives a known-length stream without buffering the file. The regression test installs a Workers-compatible FixedLengthStream stand-in for the Node runner and asserts the fake R2 bucket receives the fixed-length readable with the file's byte length.
Expose an optional assets client on Workspace and WorkspaceStub so the worker-backend shell can publish files without receiving R2 bindings or signing secrets in the Dynamic Worker. The new `assets publish <path> [<expiry>]` just-bash command forwards to that host-side capability and prints the share URL to stdout. The command resolves relative paths against cwd, defaults expiry to one hour, and accepts bare millisecond values plus ms, s, m, and h suffixes. When assets are not configured it exits with a clear error. Wire the think example to configure WorkspaceOptions.assets when the R2 credentials are present, and document the shell command in the worker backend and assets docs.
|
|
||
| function errorJSON(error: unknown, status: number): Response { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| return new Response(JSON.stringify({ error: message }), { |
Require an endpoint source before wiring the think example's assets publisher or share tool. R2 credentials alone are not enough because the assets signer derives its endpoint from CLOUDFLARE_ACCOUNT_ID or uses R2_ENDPOINT when one is provided. Without this guard, a partially configured deployment could throw during Workspace construction or while building the toolset. Also restore the WorkspaceGitStub documentation to the git stub and add a separate comment for WorkspaceAssetsStub so each RPC surface is described by the right block.
aron-cf
marked this pull request as ready for review
June 16, 2026 19:42
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.
This change adds an assets publishing surface for
@cloudflare/workspace. A Workspace can now upload a file from its virtual filesystem to R2 and return a short-lived presigned URL. The URL key is unique per share, includes only the filename, and carries the right content type, disposition, and metadata.The public API lives at
@cloudflare/workspace/assetsand follows the same factory shape as the git client: bind the workspace, bucket, and signing configuration once, then callshare()for each file.R2 bucket bindings can upload objects, but they cannot mint presigned URLs. The signer therefore uses R2's S3-compatible credentials, resolved from explicit
s3options or from the usual environment values:CLOUDFLARE_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY, andR2_ENDPOINTwhen set. Uploads still go through the R2 binding; only the presignedGETURL uses the S3 signing path.The object key has this shape:
The random id is 16 bytes encoded as Crockford base32, so sharing the same file twice creates two distinct objects. Only the filename appears in the URL path; the full workspace path is stored only in R2 custom metadata. The body is streamed from the virtual filesystem through
FixedLengthStreambefore callingbucket.put(), because R2 requires stream uploads to have a known length.Workspaces can also expose the publisher to the worker backend's just-bash shell by attaching the assets client at construction time:
That makes a new shell command available inside the Dynamic Worker:
The command prints the share URL to stdout. It accepts absolute paths or paths relative to the current working directory. The optional expiry defaults to one hour; a bare number is milliseconds, and
ms,s,m, andhsuffixes are supported. The Dynamic Worker never receives the R2 binding or signing secrets; it forwards to the host workspace over the existing RPC loopback.This also adds an
examples/assetsproject that turns a one-shot prompt into an image link. The Worker acceptsPOST /prompt, runs the FLUX.2 [klein] 9B Workers AI image model, writes the generated PNG into a backend-less Workspace, publishes it through the assets client, and returns{ path, url }.{ "path": "/workspace/0d58d8c2-7c5c-48a2-bd22-c25f9dd5c824.png", "url": "https://<account>.r2.cloudflarestorage.com/..." }The example is production-only. It needs an R2 bucket, Workers AI, and R2 S3 credentials set as secrets. A
.dev.vars.exampledocuments the required values, but a local dev stack will not produce a working public link.Manual verification:
Then call
POST /promptand open the returned URL before it expires.The change is covered by unit and integration tests for key generation, MIME inference, SigV4 signing, fixed-length R2 uploads,
share()behavior against a real SQLite-backed Workspace,WorkspaceStub.assets, and the worker-backendassets publishcommand. The validation run was:npm run format npx biome check . npx vitest run --workspace @cloudflare/workspace npx vitest run --workspace @cloudflare/workspace -- --config vitest.config.worker-backend.ts npx tsc -p packages/workspace/tsconfig.build.json --noEmit npm run build --workspace @cloudflare/workspace npm run typecheck --workspace @cloudflare/example-think npm run typecheck --workspace @example/workspace-assetsDocumentation was added for the assets interface, the worker backend shell command, and the new example. The think example now wires the same assets publisher into both its model-facing
sharetool and the worker shell'sassets publishcommand when the R2 credentials are present.