Declare your own Alibaba Cloud OSS buckets in an Environment's config.storage.buckets[]. Sessions created with that Environment mount each bucket (or one prefix of it) at /mnt/buckets/<name>, and the Agent can read and write it like a local directory.
Bucket mounts are environment-level configuration: a Session snapshots the configuration when it is created, and later Environment changes do not affect existing Sessions.
- An Environment can declare at most 8 buckets.
- Only Alibaba Cloud OSS is supported (
provider: "aliyun-oss").
- Both read-only and read-write are supported.
- Reads download from OSS through a presigned URL; writes upload directly to OSS with multipart upload (writes smaller than 1 MiB are relayed by Qoder).
- Your RAM role or AccessKey never enters the sandbox.
This feature is in Beta. Whenever the resulting config.storage.buckets is non-empty on create or update, the request must carry x-qoder-beta: oss-bucket-2026-09-23. Adding buckets to an Environment for the first time also requires the account to be eligible for the Beta (allowlist or full rollout); otherwise the request returns 503 feature_not_available. Removing buckets (the result contains none) does not require the header.
Configuration fields
config.storage accepts a single field, buckets, and is only valid for cloud Environments:
{
"type": "cloud",
"storage": {
"buckets": []
}
}
Fields of one bucket:
| Field | Type | Required | Description |
|---|
name | string | Yes | Mount directory name, i.e. /mnt/buckets/<name>. 1–63 characters of letters, digits, ., _, -, starting with a letter or digit; unique within the Environment |
provider | string | Yes | Always "aliyun-oss" |
region | string | Yes | OSS region ID such as cn-hangzhou or ap-southeast-1. Only a region ID is accepted; custom endpoints are not supported |
bucket | string | Yes | Bucket name; must not contain /, *, or ? |
prefix | string | No | Mount only the objects under this prefix. Leading and trailing / are ignored; omitted means the whole bucket. Must not contain *, ?, \, NUL, or empty, ., or .. segments |
access | string | No | read_only (default) or read_write |
accelerator_endpoint | string | No | OSS accelerator host in the form <label>.oss-data-acc.aliyuncs.com; use only when the bucket is in the sandbox's region |
credential | object | Yes | Credentials for the bucket; see Credentials |
Within one request, name must be unique, and two buckets with the same region + bucket + prefix are rejected.
Request fields are validated strictly: unknown fields, keys with different casing, and wrong value types return 400.
Example
Create an Environment with two buckets:
curl -s -X POST "https://api.qoder.com.cn/api/v1/cloud/environments" \
-H "Authorization: Bearer $QODER_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "x-qoder-beta: oss-bucket-2026-09-23" \
-d '{
"name": "with-oss-buckets",
"config": {
"type": "cloud",
"storage": {
"buckets": [
{
"name": "datasets",
"provider": "aliyun-oss",
"region": "cn-hangzhou",
"bucket": "my-bucket",
"prefix": "team/data/",
"access": "read_only",
"credential": {
"type": "ram_role",
"role_arn": "acs:ram::1234567890123456:role/qoder-oss-mount"
}
},
{
"name": "outputs",
"provider": "aliyun-oss",
"region": "cn-hangzhou",
"bucket": "my-bucket",
"prefix": "team/outputs/",
"access": "read_write",
"credential": {
"type": "access_key",
"access_key_id": "LTAI5tExampleAccessKey",
"access_key_secret": "ExampleSecret"
}
}
]
}
}
}'
Credentials are returned in their read view only:
{
"id": "env_019e44eb66bb748cabcd1489f6fa4428",
"type": "environment",
"name": "with-oss-buckets",
"description": "",
"config": {
"type": "cloud",
"storage": {
"buckets": [
{
"name": "datasets",
"provider": "aliyun-oss",
"region": "cn-hangzhou",
"bucket": "my-bucket",
"prefix": "team/data/",
"access": "read_only",
"credential": {
"type": "ram_role"
}
},
{
"name": "outputs",
"provider": "aliyun-oss",
"region": "cn-hangzhou",
"bucket": "my-bucket",
"prefix": "team/outputs/",
"access": "read_write",
"credential": {
"type": "access_key",
"access_key_id": "LTAI****sKey"
}
}
]
}
},
"metadata": {},
"archived_at": null,
"created_at": "2026-10-21T08:00:00.000000Z",
"updated_at": "2026-10-21T08:00:00.000000Z"
}
Credentials
credential.type is either ram_role (recommended) or access_key.
RAM role (recommended)
- Create a RAM role in your Alibaba Cloud account for Qoder to assume. Its trust policy must allow the Qoder service role to assume it; contact support for the Qoder service role.
- Restrict who can assume it with
sts:ExternalId:
| Session owner | sts:ExternalId |
|---|
| Individual user | User ID |
| Enterprise (service account) | <organization ID>@<workspace ID> |
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"RAM": ["<Qoder service role ARN>"]
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": ["<your user ID or organization ID@workspace ID>"]
}
}
}
]
}
- Put the role ARN in
credential.role_arn.
Qoder also attaches a session policy that narrows the credentials to the declared bucket, prefix, and access mode. The role itself must also grant the actions in Permissions; the effective permission is the intersection of the two.
AccessKey
Use the AccessKey of a dedicated RAM user, never the account's root AccessKey, and grant that user the policy below.
- The AccessKey is stored with the Environment configuration and snapshotted into Sessions created afterwards.
- Reading an Environment returns only a masked
access_key_id (for example LTAI****sKey); access_key_secret is never returned.
- Rotating the AccessKey only affects Sessions created afterwards.
- There is no STS session policy: Qoder enforces the prefix and read-only limits, while the storage-side limit is the AccessKey's own RAM policy.
Keeping credentials on update
config is replaced as a whole, but storage.buckets[].credential is write-only and preserved per bucket name:
- Omitting a bucket's
credential, or echoing the masked credential you read back, keeps the stored credential.
- Renaming a bucket creates a new bucket and requires a full credential.
- Changing
credential.type or sending "credential": null requires a full new credential.
- Removing a bucket from the array, or sending
"buckets": [], removes it from Sessions created afterwards; existing Sessions keep their snapshot.
Authorizing a bucket
The bucket owner must explicitly allow Qoder to mount it. Authorization consists of bucket tags and an optional access list file; at least one of them must name who may mount, otherwise the mount is refused.
Add tags to the bucket in the OSS console. You can find your user ID, organization ID, and workspace ID in the Qoder console:
| Tag | Required | Description |
|---|
qoder-mount | Yes | Must be enabled, allowing Qoder to mount the bucket |
qoder-mount-users | No | Comma-separated individual user IDs allowed to mount |
qoder-mount-organizations | No | Comma-separated organization IDs allowed to mount |
qoder-mount-workspaces | No | Comma-separated workspace IDs allowed to mount |
Rules:
- A tag value is limited to 128 characters; use tags only for a small number of IDs.
- Individual sessions match only
qoder-mount-users; enterprise sessions match only qoder-mount-organizations. They are different namespaces, so an identical spelling does not grant access across them.
- When
qoder-mount-workspaces is set, the Session's workspace must match.
- When any restricting tag is present, every dimension that is set must match.
- Tags are cached for about 1 minute; on read failures the previous value is used for up to 10 minutes; an explicit denial (403) takes effect immediately.
Access list (optional)
For finer control by Agent or Session, put an access list file next to the mounted directory. It lives outside the mounted directory and is neither visible nor writable from the sandbox.
- Name:
<prefix without the trailing slash>.qoder-acl.json. For example, a prefix of team/data/ uses team/data.qoder-acl.json.
- A whole-bucket mount (no
prefix) has no access list and relies on tags only.
- Only the file at the mounted directory's own level is read; parent directories are not consulted.
{
"version": 1,
"grants": [
{"users": ["<user ID>"]},
{"organizations": ["<organization ID>"], "workspaces": ["<workspace ID>"], "agents": ["<agent ID>"], "sessions": ["<session ID>"]}
]
}
Rules:
- The supported dimensions are
users, organizations, workspaces, agents, and sessions.
- Within one grant, every dimension that is set must match; unset dimensions are unrestricted; an empty list matches nobody.
- Any matching grant allows the mount; if none matches, the mount is refused with 403.
- When both tags and an access list are present, both must match.
- A file larger than 64 KB, invalid version 1 JSON, unknown keys, a
null grant, or a dimension that is not an array of non-empty strings is rejected.
agents and sessions are checked at mount time, not when the Environment is saved.
- The list is cached for about 1 minute; after it expires, read failures reuse the previous value for up to 10 minutes; 403 is never reused.
- A missing file is not an error, but in that case the tags must name who may mount.
Permissions
Grant the RAM role or RAM user the minimum permissions:
| Action | Purpose | Needed by |
|---|
oss:ListObjects | List directories | Read-only / read-write |
oss:GetBucketTagging | Read bucket tags (mount authorization) | Read-only / read-write |
oss:GetObject | Read files; read the access list; read sources during rename | Read-only / read-write |
oss:HeadObject | Stat files | Read-only / read-write |
oss:PutObject | Write files, multipart upload, create directories, write rename targets | Read-write only |
oss:DeleteObject | Delete files and empty directories, delete rename sources | Read-write only |
oss:AbortMultipartUpload | Clean up interrupted uploads | Read-write only |
oss:ListParts | Verify parts before completing an upload; without it large writes fail | Read-write only |
Example read-only policy (prefix is team/data/):
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": ["oss:ListObjects", "oss:GetBucketTagging"],
"Resource": ["acs:oss:*:*:my-bucket"]
},
{
"Effect": "Allow",
"Action": ["oss:GetObject", "oss:HeadObject"],
"Resource": ["acs:oss:*:*:my-bucket/team/data/*"]
},
{
"Effect": "Allow",
"Action": ["oss:GetObject"],
"Resource": ["acs:oss:*:*:my-bucket/team/data.qoder-acl.json"]
}
]
}
A read-write mount also needs oss:PutObject, oss:DeleteObject, oss:AbortMultipartUpload, and oss:ListParts in the second statement's Action.
Validation when saving
When an Environment is saved (on create, or when an update adds or changes a bucket), Qoder tries the mount as the saving user with the submitted credentials:
- Obtain credentials (assume the role or use the AccessKey).
- Read the bucket tags.
- Read the access list, if present.
- List the prefix once (
max-keys=1).
Only new or changed buckets are checked; unchanged buckets are not checked again. Validation times out after 15 seconds.
- A configuration or permission refusal returns 400
invalid_request_error with a message such as Bucket '<name>' cannot be mounted: <reason>.
- An OSS / STS outage returns 503
api_error and can be retried.
- The
agents / sessions dimensions of the access list are not checked when saving; they are checked at mount time.
Behavior in the sandbox
Mount point: /mnt/buckets/<name>. With access: read_only it is a read-only filesystem; with read_write files can be created, modified, and deleted.
- Directories: object storage has no real directories. A directory is the prefix of object keys; an empty directory is kept alive by a zero-byte
<path>/ placeholder object, the same convention as the OSS console and ossutil.
- Reads: Qoder returns a 302 to a presigned URL and the sandbox downloads directly from OSS.
- Writes: files smaller than 1 MiB use a regular upload relayed by Qoder; files of 1 MiB or more use multipart upload (the sandbox PUTs each part to a presigned URL and then commits).
- Rename: a single-file rename is copy + delete and is not atomic. Concurrent Sessions modifying the same path can lose writes; Qoder does not arbitrate across Sessions.
- Directory rename: not natively supported; the client falls back to moving files one by one.
- Deleting directories: a non-empty directory returns 409; delete its files first.
- Listing limit: a single directory with more than 100,000 entries returns 507 rather than a truncated listing.
- File size limit: a direct upload is at most 10,000 parts × 64 MiB, i.e. 625 GiB; the bucket's own quotas and limits still apply.
- Interrupted uploads: an interrupted write leaves unfinished parts in the bucket. Configure a lifecycle rule on the bucket to clean them up.
- Regions: when the bucket is in the sandbox's region, the OSS internal endpoint is used; otherwise traffic goes over the public endpoint with higher latency. The OSS accelerator works only in the same region.
- Networking: the sandbox accesses OSS directly for reads and for writes of 1 MiB or more. If the Environment uses restricted networking, allow the corresponding OSS domain.
Errors
| HTTP | type | Description |
|---|
| 400 | invalid_request_error | Invalid bucket field, missing credential, missing x-qoder-beta header, or the save-time probe was refused |
| 401 | authentication_error | PAT or SAT invalid or expired |
| 403 | permission_error | Not authorized for the target Environment |
| 503 | feature_not_available | The account is not eligible for the oss_bucket Beta |
| 503 | api_error | OSS / STS was temporarily unavailable during the save-time probe; retry |
Refusals at mount time (bucket tags do not authorize the caller, the access list does not match, the credentials lack permissions, and so on) surface as EACCES in the sandbox and are not returned by the Environment API. Check the bucket tags, access list, and RAM policy first.
Limitations
- Only Alibaba Cloud OSS is supported; S3, COS, MinIO, and custom endpoints are not.
- Qoder does not manage object versions, lifecycle rules, bucket ACLs, or other OSS settings.
- One
prefix of one bucket can appear only once per Environment; to mount several directories of the same bucket, use different name and prefix values.
- Mounts use the Environment snapshot; changing an Environment does not alter existing Sessions.