Skip to content

feat(sdks): expose user-defined file metadata on sandbox.files#1383

Open
mishushakov wants to merge 8 commits into
mainfrom
mishushakov/files-metadata-sdk
Open

feat(sdks): expose user-defined file metadata on sandbox.files#1383
mishushakov wants to merge 8 commits into
mainfrom
mishushakov/files-metadata-sdk

Conversation

@mishushakov

@mishushakov mishushakov commented Jun 4, 2026

Copy link
Copy Markdown
Member

Adds a metadata option to file uploads and surfaces persisted metadata on every EntryInfo / WriteInfo returned by getInfo, list, rename, and write responses, across the JS and Python (sync + async) SDKs.

Metadata is sent as X-Metadata-<key>: <value> request headers and persisted by envd as user.e2b.* extended attributes; the same map is applied to every file in a multi-file upload. Keys and values must be printable US-ASCII and keys are lowercased by the sandbox, so they may differ in case when read back. Requires envd 0.6.2 or later.

This syncs the envd OpenAPI spec and filesystem proto with infra#2732 and regenerates the JS/Python clients.

Usage

JavaScript / TypeScript

// Single file
const info = await sandbox.files.write('report.txt', 'hello', {
  metadata: { author: 'mish', purpose: 'demo' },
})
console.log(info.metadata) // { author: 'mish', purpose: 'demo' }

// Multiple files (same metadata applied to each)
await sandbox.files.writeFiles(
  [
    { path: 'a.txt', data: 'A' },
    { path: 'b.txt', data: 'B' },
  ],
  { metadata: { source: 'import' } }
)

// Read it back
const stat = await sandbox.files.getInfo('report.txt')
console.log(stat.metadata) // { author: 'mish', purpose: 'demo' }

Python

# Single file
info = sandbox.files.write("report.txt", "hello", metadata={"author": "mish"})
print(info.metadata)  # {"author": "mish"}

# Multiple files (same metadata applied to each)
sandbox.files.write_files(
    [
        WriteEntry(path="a.txt", data="A"),
        WriteEntry(path="b.txt", data="B"),
    ],
    metadata={"source": "import"},
)

# Read it back
stat = sandbox.files.get_info("report.txt")
print(stat.metadata)  # {"author": "mish"}

The async Python API is identical with await.

Tests

Integration tests cover the round-trip across write / getInfo / list / rename, octet-stream uploads, multi-file uploads, overwrite-clears-stale-metadata, and metadata written directly as user.e2b.* xattrs via sandbox.commands.run surfacing in getInfo. They require a sandbox running envd 0.6.2+.

🤖 Generated with Claude Code

Adds a metadata option to file uploads and surfaces persisted metadata
on EntryInfo/WriteInfo returned by getInfo, list, rename, and write.
Metadata is sent as X-Metadata-<key> headers and persisted by envd as
user.e2b.* xattrs. Syncs the envd OpenAPI spec and filesystem proto,
regenerates the JS and Python clients, and adds integration tests.

Requires envd 0.5.26 or later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cla-bot cla-bot Bot added the cla-signed label Jun 4, 2026
@cursor

cursor Bot commented Jun 4, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches core filesystem upload/read APIs and version gating across both SDKs; behavior depends on coordinated envd 0.6.2+ rollout but changes are additive with integration test coverage.

Overview
Adds user-defined file metadata to JS and Python SDKs (sandbox.files): uploads accept an optional metadata map on write / writeFiles / write_files, encoded as X-Metadata-* headers and requiring envd 0.6.2+ (clients error if metadata is used on older envd).

Read paths now expose metadata on WriteInfo / EntryInfo from upload responses, getInfo, list, and rename, with empty maps normalized away. Spec and generated clients are updated (EntryInfo.metadata on OpenAPI + filesystem proto).

Integration tests cover round-trip uploads (multipart and octet-stream), multi-file shared metadata, overwrite clearing stale metadata, and user.e2b.* xattrs surfacing via getInfo.

Reviewed by Cursor Bugbot for commit 923f2be. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from fb43a35. Download artifacts from this workflow run.

JS SDK (e2b@2.28.3-mishushakov-files-metadata-sdk.0):

npm install ./e2b-2.28.3-mishushakov-files-metadata-sdk.0.tgz

CLI (@e2b/cli@2.11.1-mishushakov-files-metadata-sdk.0):

npm install ./e2b-cli-2.11.1-mishushakov-files-metadata-sdk.0.tgz

Python SDK (e2b==2.27.1+mishushakov-files-metadata-sdk):

pip install ./e2b-2.27.1+mishushakov.files.metadata.sdk-py3-none-any.whl

Comment thread packages/js-sdk/src/sandbox/filesystem/index.ts Outdated
@changeset-bot

changeset-bot Bot commented Jun 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 923f2be

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

This PR includes changesets to release 2 packages
Name Type
e2b Minor
@e2b/python-sdk 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

….6.2

Re-syncs the envd OpenAPI spec and filesystem proto with the latest
infra#2732 (refined upload description and user.e2b. xattr namespace
note), regenerates the JS client, and pins the file-metadata version
gate to envd 0.6.2 (the version that ships the feature).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py Outdated
Comment thread packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py Outdated
Comment thread packages/js-sdk/tests/sandbox/files/metadata.test.ts
Comment thread packages/js-sdk/tests/sandbox/files/metadata.test.ts
…_dict

- Move file metadata to WriteEntry so each file in a multi-file write can
  carry its own; keep the metadata option on single-file write/write.
- Upload per file when metadata is set (X-Metadata-* headers are
  request-scoped), preserving the batched multipart path otherwise.
- Replace the module-level _write_info_from_dict helper with a
  WriteInfo.from_dict() classmethod.
- Trim the version-gate error message to a single sentence.
- Add tests that set metadata via xattrs (user.e2b.*) through
  sandbox.commands.run and assert it surfaces in getInfo (JS + Python).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit effe883. Configure here.

Comment thread packages/js-sdk/src/sandbox/filesystem/index.ts Outdated
Revert per-WriteEntry metadata; metadata is again an option on
write/writeFiles/write_files and is applied to every file in the upload
(sent as request-scoped X-Metadata-* headers). Keeps the earlier review
fixes: WriteInfo.from_dict(), the trimmed version-gate message, and the
xattr-via-commands tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mishushakov mishushakov marked this pull request as ready for review June 4, 2026 15:12
Comment on lines 78 to +86
* Path to the filesystem object.
*/
path: string
/**
* User-defined metadata persisted on the file as extended attributes.
* Only populated when metadata was supplied on upload and the sandbox's
* envd supports it. `undefined` when no metadata is set.
*/
metadata?: Record<string, string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The docstring on WriteInfo.metadata says it is "Only populated when metadata was supplied on upload and the sandbox's envd supports it." Since EntryInfo extends WriteInfo and inherits this doc, it also describes EntryInfo.metadata returned by getInfo/list/rename — which is populated from any user.e2b.* xattr on the inode, including ones set out-of-band (as the PR's own "metadata set via xattrs is surfaced in getInfo" test demonstrates). Consider dropping the "Only" qualifier or noting that read responses also reflect xattrs set externally. Same wording is duplicated in packages/python-sdk/e2b/sandbox/filesystem/filesystem.py:52-57.

Extended reasoning...

What the docstring says vs. what actually happens

The new metadata field on WriteInfo is documented as:

User-defined metadata persisted on the file as extended attributes. Only populated when metadata was supplied on upload and the sandbox's envd supports it. undefined when no metadata is set.

(packages/js-sdk/src/sandbox/filesystem/index.ts:81-85, mirrored verbatim in packages/python-sdk/e2b/sandbox/filesystem/filesystem.py:52-57.)

The word "Only" makes that statement incorrect because EntryInfo extends WriteInfo (index.ts:89) and inherits this docstring. EntryInfo is returned by Filesystem.getInfo, Filesystem.list, and Filesystem.rename, all of which call into envd RPCs (stat, listDir, move) that surface the proto EntryInfo.metadata field. The proto comment on filesystem.proto:65-68 is the authoritative description and correctly says the field reflects any user.e2b.* xattr on the inode — regardless of whether the SDK set it on upload.

Step-by-step proof from the PR's own test

The PR adds packages/js-sdk/tests/sandbox/files/metadata.test.ts:155 (metadata set via xattrs is surfaced in getInfo):

  1. Write a file with sandbox.files.write(filename, 'content') — no metadata option, so no X-Metadata-* header is sent. Per the docstring, metadata should remain undefined.
  2. Resolve the absolute path with realpath.
  3. Run setfattr -n user.e2b.author -v mish <path> via sandbox.commands.run — this sets a user.e2b.* xattr directly, bypassing the SDK upload entirely.
  4. Call sandbox.files.getInfo(filename) and assert info.metadata deep-equals { author: 'mish' }.

The assertion passes, which means EntryInfo.metadata (the inherited field) is populated from xattrs that were never "supplied on upload". The same scenario is repeated in the Python test suites (packages/python-sdk/tests/{async,sync}/.../test_metadata.py:test_metadata_set_via_xattrs_surfaced_in_get_info). The docstring's "Only" is therefore factually wrong for the EntryInfo use site.

Why this is still only a nit

The refuters are correct that this is purely a documentation cleanup, not a behavior bug — the runtime is right, the OpenAPI description block (envd.yaml:104-124) and the proto comment both document the xattr semantics properly, and any developer looking at EntryInfo.metadata in an IDE would see the cross-cutting proto docs alongside the inherited TS/Python comment. So this should not block the PR.

Suggested fix

Either drop the "Only" qualifier and mention reads, e.g.:

User-defined metadata persisted on the file as extended attributes
(`user.e2b.*` xattrs). On uploads, populated when metadata was supplied
and the sandbox's envd supports it. On reads (`getInfo`/`list`/`rename`),
populated from any `user.e2b.*` xattr on the inode, including ones set
out-of-band. `undefined`/`None` when no such metadata is set.

Apply to both packages/js-sdk/src/sandbox/filesystem/index.ts:78-86 and packages/python-sdk/e2b/sandbox/filesystem/filesystem.py:52-57 so the JS/Python copies stay in sync.

mishushakov and others added 3 commits June 9, 2026 22:37
…on gate

Use SandboxError (JS) / SandboxException (Python) instead of
TemplateError/TemplateException when metadata is set against envd older
than 0.6.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on gate

The sandbox is healthy — it's the template's envd that is too old — so
revert the gate back to TemplateError (JS) / TemplateException (Python),
matching the recursive-watch version gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@matthewlouisbrockman matthewlouisbrockman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

approving, may want to address claudes comment, also the instructions for the allowed values aren't quite accurate because it's not all ascii allowed because it's passed in the headers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants