S3VFS in two runtimes:
@struktoai/mirage-node, signs requests server-side using @aws-sdk/client-s3.@struktoai/mirage-browser, stays secret-free: your backend hands out presigned URLs and the browser fetches them withfetch().
Node (server-side)
~/.aws/credentials is configured, you can omit accessKeyId/secretAccessKey and the AWS SDK picks up the default profile.
Under write: conditional a write carries the version of the file mirage read, and S3 refuses it when the file has changed since. The endpoint decides which writes can be conditioned: AWS’s conditions every write, copy and delete; any other endpoint (or AWS_ENDPOINT_URL_S3/AWS_ENDPOINT_URL) only a write, so mirage refuses what would copy or delete there. An endpoint set only in an AWS profile is not read: give a non-AWS server its endpoint. A browser mount signed by presigned URLs cannot carry a condition, so it cannot declare write: conditional. If the session last saw no file at the path, the write sends If-None-Match: * and fails if someone created the file in the meantime.
A conditional single-file move attempts rollback when source deletion is refused. The move still fails and warns if the destination may remain. Cleanup uses the copy’s S3 version or, for a new destination, its ETag. Unversioned overwrites, same-ETag replacements, retried versioned copies, and uncertain delete outcomes limit recovery; inspect both paths after a failure.
An absent destination must remain absent until copying. This guard may add one metadata request; cleanup adds one delete and may require s3:DeleteObjectVersion. Directory and cross-mount moves do not use rollback.
A directory mv, rm -r or rmdir looks at the directory again when it is done: a file written into it that the command never listed stays, and the command fails with Directory not empty. Under write: conditional, a directory mv that finds a file changed before it could copy it removes nothing from the source; the copies it made stay at the destination.
An append or another rewrite that observes a missing file uses create-only
commit under either write policy. S3 checks If-None-Match: * at PUT or final
multipart completion, so a delayed creation cannot replace a competing
writer’s bytes. A lost condition returns an error without automatic retry.
Existing-file appends under unconditional retain last-write-wins behavior.
Config
Fields marked secret are redacted in snapshots.
Browser (presigned URLs)
S3VFS never sees AWS credentials. You implement one function, presignedUrlProvider, that your frontend calls to get a signed URL for each S3 operation. The actual signing happens on your server, using any S3-signing SDK you prefer.
Config
Fields marked secret are redacted in snapshots.
The browser
S3VFS can also sign requests itself, without presignedUrlProvider: it then takes the credential fields of the Node config, which puts the keys in the browser.
1. Server: sign URLs on demand
Using @aws-sdk/s3-request-presigner:2. Browser: wire it up
Customize a builtin command
The generic command definitions are available before a workspace is created. Copy the definition with the behavior you want to change, then register that copy on one mount:generic export from @struktoai/mirage-node for a Node
workspace. The builtin definition and other mounts stay unchanged.
Registration is a host application action; an agent can discover and run the
resulting cat command, but cannot alter the command table.
3. Configure CORS on the bucket
Presigned URLs reach the S3 host directly from the browser. Your bucket must allow cross-origin requests from your app’s origin, otherwise everyfetch() fails with TypeError: Failed to fetch (the browser drops the response at the CORS preflight).
PutBucketCors call with your existing env credentials and applies the same policy to all configured buckets in one shot:
Refused keys
When credentials cannot read an S3 object (S3 returnsAccessDenied, or a bodiless HTTP 403 response on a HEAD request), Mirage reports EACCES (Permission denied) for that file operand.
Shell commands follow standard POSIX conventions:
cat a b cprints the contents ofaandc, writes a permission error forbtostderr, and exits with a non-zero status.grep -rlogs an error on inaccessible files and exits with status2.cp b x || fallbacktriggers the fallback branch.test -e bevaluates to false.
IAM permissions and existence checks
- Scoped grants: When access is granted below a specific prefix (for example,
s3:GetObjectonbucket/dir/*alongsides3:ListBucketon the bucket), Mirage treatsdiras a directory duringstat. - Missing
ListBucket: Withouts3:ListBucketpermission, S3 returns HTTP 403 even for non-existent keys to avoid leaking existence. As a result, non-existent files reportPermission denied(EACCES) instead ofNo such file or directory(ENOENT). - Listed but unreadable keys: If
ListObjectsV2includes an object that your credentials cannot read,statandtest -ereport that the file exists, while read attempts (cat) fail withPermission denied. - Browser presigned URLs: Mirage does not parse the HTTP error body when reading through presigned URLs in the browser. Any HTTP 403 status (including expired or misconfigured URLs) is handled as a permission refusal.
Scoping a VFS to a key prefix
PasskeyPrefix to scope every operation to a subpath of the bucket:
/data/notes.md; the underlying bucket key is users/${userId}/data/notes.md. Useful for multi-tenant systems. Pairs naturally with STS AssumeRole session policies for AWS-side enforcement. Unset behavior is unchanged.
Normalization: leading slashes are stripped and a trailing slash is added automatically. Both undefined and an empty string are treated as “no prefix.”
In YAML-based configs the snake-case spelling key_prefix is also accepted and is automatically mapped to keyPrefix at construction.
VFS.md
The VFS.md an agent reads with S3 mounted at/<path>.
- Rendered
- Raw