OSS assets
OSS assets connect to an S3-compatible object storage endpoint (AWS S3, MinIO, Aliyun OSS,
…). The asset names the endpoint and credentials only — the bucket is part of every
command's target, not part of the asset.
Command syntax
<family> <verb> [target] [--flags] — family is bucket or object, verb is the
operation, and target is <bucket>/<key> (no leading slash).
Every example below is literally executable as written. Optional flags are not marked with
brackets; each useful combination is spelled out on its own line.
bucket
object
object list backups
object list backups/2026/
object list backups/2026/ --max-keys=100
object list backups/2026/ --max-keys=100 --after=2026/03/db.sql.gz
object stat backups/2026/db.sql.gz
object get backups/app.conf (returns the content inline, truncated to 64 KiB)
object get backups/app.conf --max-bytes=4096
object get backups/2026/db.sql.gz --file=/tmp/db.sql.gz (streams to a local file instead)
object put backups/2026/db.sql.gz --file=/tmp/db.sql.gz
object put backups/app.conf --file=/tmp/app.conf --content-type=text/plain
object copy backups/2026/db.sql.gz --to=archive/db-2026.sql.gz
object move backups/2026/db.sql.gz --to=archive/db-2026.sql.gz
object delete backups/2026/old.log
object presign backups/2026/db.sql.gz
object presign backups/2026/db.sql.gz --expiry=600
object presign uploads/inbox/report.pdf --method=put --expiry=600 (denied by the default policy, see Notes)
Flag reference
Every flag each verb accepts — anything else is rejected rather than ignored. Bold
flags are required: omitting one does not default it, the command is rejected before it
runs. Verbs not listed here take no flags at all.
object list: --max-keys, --after
object get: --file, --max-bytes
object put: --file, --content-type
object copy: --to
object move: --to
object presign: --expiry, --method
Flag values
Checked before any approval dialog, so a wrong value costs nothing:
--max-keys, --max-bytes and --expiry must be plain decimal integers: 1000, not
1,000, 1_000, 1e3 or 3.0.
--method is get or put, lower case exactly. Nothing else is accepted, and there is
no case folding: GET is an error rather than a synonym. Omitting it means get, so a
bare object presign requests object.presign.read.
--expiry defaults to 3600 seconds when omitted.
--file must be an absolute local path. The approved command string is all that
identifies the file, and a relative path resolves against a working directory that is not
part of it, so it would land somewhere unpredictable.
--file and --max-bytes cannot be given together: they select two different behaviors
of object get (stream the whole object to disk, or return the first bytes inline), so
one of them would have to be ignored.
--max-bytes defaults to 64 KiB and is capped at 1 MiB. A larger value is reduced to the
cap rather than rejected, and truncated then reports that the object was longer.
--max-keys is capped at 1000. A larger value is rejected, not reduced: silently
returning fewer keys than asked would break --after pagination against what the caller
expected to page through.
--expiry is capped at 604800 seconds (7 days) and is also rejected rather than
reduced: a longer-lived presigned URL is what S3-compatible backends refuse to sign in
the first place, so a value over the cap would otherwise be approved and then fail.
--to is a second <bucket>/<key> and follows the same rules as the target, including
the requirement that it name a single object.
Not checked on this side at all — the storage service is the only thing that will reject a
wrong value, after approval and a round trip:
--content-type is passed through verbatim as the object's MIME type; omitting it lets
the service decide.
--after is a continuation cursor, not a filter: pass back the nextContinuationToken
from the previous object list result to read the next page.
Results
bucket list returns {"buckets":[…]}.
object list returns the raw listing: prefixes (one level of "folders"),
objects, plus isTruncated and nextContinuationToken for paging.
object stat returns the object's metadata.
object get without --file returns content together with encoding
(utf-8, or base64 when the bytes are not valid UTF-8) and truncated. When
truncated is true you read only the first bytes — re-run with --file rather than
guessing at the rest.
object get --file returns the byte count, the object and the local file path;
object put returns the byte count and the object (bucket/key), with no file. The
content never passes through the conversation either way.
object copy, object move and object delete return {"status":"ok",…}.
object presign returns the signed url, its method and expiresIn seconds. The URL
is fully usable. Under raw-by-default Audit the complete signed URL — signature parameters
included — enters the Audit result unchanged, subject only to the existing result
capture/truncation; nothing is redacted, so treat the URL as a capability that grants
access until it expires.
Approval and policy rules
Approval is granted per <action> <resource>, where resource is <bucket>/<key>:
| command |
permission(s) requested |
bucket list |
bucket.list * |
object list B/P |
object.list B/P |
object stat B/K, object get B/K |
object.read B/K |
object get B/K --file=P |
object.read B/K and a local-write check on P (the same gate cp uses when writing to this machine) |
object put B/K |
object.write B/K |
object copy S --to=D |
object.read S and object.write D |
object move S --to=D |
object.read S, object.write D and object.delete S |
object delete B/K |
object.delete B/K |
object presign B/K --method=get |
object.presign.read B/K |
object presign B/K --method=put |
object.presign.write B/K |
copy and move request every one of their resources: any one of them being denied
rejects the whole command, and where an allow list exists all of them must match.
Rules are written the same way, with * wildcards that do not cross / inside a key:
| rule |
meaning |
* |
everything |
object.read mybucket or object.read mybucket/ |
every object in the bucket, at any depth |
object.read mybucket/logs/ |
everything under the logs/ prefix, at any depth |
object.read mybucket/logs/*.gz |
only *.gz directly under logs/ |
object.* mybucket/ |
any object operation on that bucket |
object.presign.* * |
any presigned URL on any bucket |
Notes
- Presigned PUT URLs are denied by the default policy and cannot be enabled by an
allow rule: a signed upload URL moves write access outside this app entirely — anyone
holding it can write that key until it expires, with no further policy, approval or
audit. Use
object put instead — no asset or group policy change can enable it, because
the deny is a floor merged into every effective policy.
- Object keys may contain spaces; quote the whole target when they do, e.g.
object stat 'mybucket/My Report.pdf'. Leading or trailing whitespace is rejected —
permission rules split <action> <resource> at the first whitespace, so a padded name
could never be authorized.
- Every
object verb takes exactly one target and it must name a real key:
object stat mybucket is rejected, because mybucket as a rule would authorize the
whole bucket rather than one object. object list is the exception — listing is always
by prefix, so a bare bucket name is normalized to mybucket/.
- A trailing
/ names the zero-byte "folder marker" object, so object delete mybucket/logs/
deletes exactly that marker. Because the same string read as a rule means the whole
prefix, such an approval is never turned into a standing grant: repeating it asks again.
object list is the exception — a listing approval on mybucket/logs/ does become a
standing grant, because there the command and the rule cover the same range.
- Targets must not start with
/ or --, and the bucket is part of the target rather
than of the asset — there is no per-asset default bucket.
object copy and object move are single-object and server-side; both endpoints must
be on this same asset. Use the cp tool to move data between different assets or
between object storage and a server, and to copy whole prefixes.
- Unknown flags are rejected rather than ignored, so a typo such as
--max_keys=100 fails
instead of silently listing the default page size.
- The command line is shell-tokenized but never shell-executed:
$, |, > and &
produce an error rather than expanding or redirecting.
- The
scope parameter is not used by OSS assets; the target names bucket and key.
Asset config (for put_asset)
| field |
type |
required |
notes |
endpoint |
string |
yes |
Host, or scheme://host[:port] |
access_key_id |
string |
yes |
|
secret_access_key |
string |
no |
Write-only. Encrypted in the asset; does not create a credential |
credential_id |
number |
no |
Existing managed password credential ID; mutually exclusive with secret_access_key |
provider |
string |
no |
UI provider preset label (e.g. "s3", "minio"); does not change connection behavior |
region |
string |
no |
|
use_path_style |
bool |
no |
true to force path-style addressing (needed by most self-hosted S3-compatible services) |
use_ssl |
bool |
no |
true to connect over HTTPS |
connect_timeout |
number |
no |
Seconds; 0 uses the default |
Plaintext is never returned, is encrypted in the asset, and never creates a managed credential.
Example:
put_asset(name="backups-bucket", type="oss", config={"endpoint":"s3.us-east-1.amazonaws.com","region":"us-east-1","access_key_id":"AKIAEXAMPLE","secret_access_key":"...","use_ssl":"true"})
1---2name: oss3description: List, read, upload, copy, move, delete and presign objects in S3-compatible object storage via exec, using a family + verb + target command syntax.4---56# OSS assets78OSS assets connect to an S3-compatible object storage endpoint (AWS S3, MinIO, Aliyun OSS,9…). The asset names the endpoint and credentials only — the bucket is part of every10command's target, not part of the asset.1112## Command syntax1314`<family> <verb> [target] [--flags]` — `family` is `bucket` or `object`, `verb` is the15operation, and `target` is `<bucket>/<key>` (no leading slash).1617Every example below is literally executable as written. Optional flags are not marked with18brackets; each useful combination is spelled out on its own line.1920### bucket2122- `bucket list`2324### object2526- `object list backups`27- `object list backups/2026/`28- `object list backups/2026/ --max-keys=100`29- `object list backups/2026/ --max-keys=100 --after=2026/03/db.sql.gz`30- `object stat backups/2026/db.sql.gz`31- `object get backups/app.conf` (returns the content inline, truncated to 64 KiB)32- `object get backups/app.conf --max-bytes=4096`33- `object get backups/2026/db.sql.gz --file=/tmp/db.sql.gz` (streams to a local file instead)34- `object put backups/2026/db.sql.gz --file=/tmp/db.sql.gz`35- `object put backups/app.conf --file=/tmp/app.conf --content-type=text/plain`36- `object copy backups/2026/db.sql.gz --to=archive/db-2026.sql.gz`37- `object move backups/2026/db.sql.gz --to=archive/db-2026.sql.gz`38- `object delete backups/2026/old.log`39- `object presign backups/2026/db.sql.gz`40- `object presign backups/2026/db.sql.gz --expiry=600`41- `object presign uploads/inbox/report.pdf --method=put --expiry=600` (denied by the default policy, see Notes)4243## Flag reference4445Every flag each verb accepts — anything else is rejected rather than ignored. **Bold**46flags are required: omitting one does not default it, the command is rejected before it47runs. Verbs not listed here take no flags at all.4849- `object list`: `--max-keys`, `--after`50- `object get`: `--file`, `--max-bytes`51- `object put`: **`--file`**, `--content-type`52- `object copy`: **`--to`**53- `object move`: **`--to`**54- `object presign`: `--expiry`, `--method`5556## Flag values5758Checked before any approval dialog, so a wrong value costs nothing:5960- `--max-keys`, `--max-bytes` and `--expiry` must be plain decimal integers: `1000`, not61 `1,000`, `1_000`, `1e3` or `3.0`.62- `--method` is `get` or `put`, lower case exactly. Nothing else is accepted, and there is63 no case folding: `GET` is an error rather than a synonym. Omitting it means `get`, so a64 bare `object presign` requests `object.presign.read`.65- `--expiry` defaults to 3600 seconds when omitted.66- `--file` must be an **absolute** local path. The approved command string is all that67 identifies the file, and a relative path resolves against a working directory that is not68 part of it, so it would land somewhere unpredictable.69- `--file` and `--max-bytes` cannot be given together: they select two different behaviors70 of `object get` (stream the whole object to disk, or return the first bytes inline), so71 one of them would have to be ignored.72- `--max-bytes` defaults to 64 KiB and is capped at 1 MiB. A larger value is reduced to the73 cap rather than rejected, and `truncated` then reports that the object was longer.74- `--max-keys` is capped at 1000. A larger value is **rejected**, not reduced: silently75 returning fewer keys than asked would break `--after` pagination against what the caller76 expected to page through.77- `--expiry` is capped at 604800 seconds (7 days) and is also **rejected** rather than78 reduced: a longer-lived presigned URL is what S3-compatible backends refuse to sign in79 the first place, so a value over the cap would otherwise be approved and then fail.80- `--to` is a second `<bucket>/<key>` and follows the same rules as the target, including81 the requirement that it name a single object.8283Not checked on this side at all — the storage service is the only thing that will reject a84wrong value, after approval and a round trip:8586- `--content-type` is passed through verbatim as the object's MIME type; omitting it lets87 the service decide.88- `--after` is a continuation cursor, not a filter: pass back the `nextContinuationToken`89 from the previous `object list` result to read the next page.9091## Results9293- `bucket list` returns `{"buckets":[…]}`.94- `object list` returns the raw listing: `prefixes` (one level of "folders"),95 `objects`, plus `isTruncated` and `nextContinuationToken` for paging.96- `object stat` returns the object's metadata.97- `object get` without `--file` returns `content` together with `encoding`98 (`utf-8`, or `base64` when the bytes are not valid UTF-8) and `truncated`. When99 `truncated` is true you read only the first bytes — re-run with `--file` rather than100 guessing at the rest.101- `object get --file` returns the byte count, the object and the local file path;102 `object put` returns the byte count and the object (bucket/key), with no `file`. The103 content never passes through the conversation either way.104- `object copy`, `object move` and `object delete` return `{"status":"ok",…}`.105- `object presign` returns the signed `url`, its `method` and `expiresIn` seconds. The URL106 is fully usable. Under raw-by-default Audit the complete signed URL — signature parameters107 included — enters the Audit result unchanged, subject only to the existing result108 capture/truncation; nothing is redacted, so treat the URL as a capability that grants109 access until it expires.110111## Approval and policy rules112113Approval is granted per `<action> <resource>`, where resource is `<bucket>/<key>`:114115| command | permission(s) requested |116|---|---|117| `bucket list` | `bucket.list *` |118| `object list B/P` | `object.list B/P` |119| `object stat B/K`, `object get B/K` | `object.read B/K` |120| `object get B/K --file=P` | `object.read B/K` **and** a local-write check on `P` (the same gate `cp` uses when writing to this machine) |121| `object put B/K` | `object.write B/K` |122| `object copy S --to=D` | `object.read S` **and** `object.write D` |123| `object move S --to=D` | `object.read S`, `object.write D` **and** `object.delete S` |124| `object delete B/K` | `object.delete B/K` |125| `object presign B/K --method=get` | `object.presign.read B/K` |126| `object presign B/K --method=put` | `object.presign.write B/K` |127128`copy` and `move` request every one of their resources: any one of them being denied129rejects the whole command, and where an allow list exists all of them must match.130131Rules are written the same way, with `*` wildcards that do not cross `/` inside a key:132133| rule | meaning |134|---|---|135| `*` | everything |136| `object.read mybucket` or `object.read mybucket/` | every object in the bucket, at any depth |137| `object.read mybucket/logs/` | everything under the `logs/` prefix, at any depth |138| `object.read mybucket/logs/*.gz` | only `*.gz` directly under `logs/` |139| `object.* mybucket/` | any object operation on that bucket |140| `object.presign.* *` | any presigned URL on any bucket |141142## Notes143144- Presigned **PUT** URLs are denied by the default policy and cannot be enabled by an145 allow rule: a signed upload URL moves write access outside this app entirely — anyone146 holding it can write that key until it expires, with no further policy, approval or147 audit. Use `object put` instead — no asset or group policy change can enable it, because148 the deny is a floor merged into every effective policy.149- Object keys may contain spaces; quote the whole target when they do, e.g.150 `object stat 'mybucket/My Report.pdf'`. Leading or trailing whitespace is rejected —151 permission rules split `<action> <resource>` at the first whitespace, so a padded name152 could never be authorized.153- Every `object` verb takes exactly one target and it must name a real key:154 `object stat mybucket` is rejected, because `mybucket` as a rule would authorize the155 whole bucket rather than one object. `object list` is the exception — listing is always156 by prefix, so a bare bucket name is normalized to `mybucket/`.157- A trailing `/` names the zero-byte "folder marker" object, so `object delete mybucket/logs/`158 deletes exactly that marker. Because the same string read as a *rule* means the whole159 prefix, such an approval is never turned into a standing grant: repeating it asks again.160 `object list` is the exception — a listing approval on `mybucket/logs/` *does* become a161 standing grant, because there the command and the rule cover the same range.162- Targets must not start with `/` or `--`, and the bucket is part of the target rather163 than of the asset — there is no per-asset default bucket.164- `object copy` and `object move` are single-object and server-side; both endpoints must165 be on this same asset. Use the `cp` tool to move data between different assets or166 between object storage and a server, and to copy whole prefixes.167- Unknown flags are rejected rather than ignored, so a typo such as `--max_keys=100` fails168 instead of silently listing the default page size.169- The command line is shell-tokenized but never shell-executed: `$`, `|`, `>` and `&`170 produce an error rather than expanding or redirecting.171- The `scope` parameter is not used by OSS assets; the target names bucket and key.172173## Asset config (for put_asset)174175| field | type | required | notes |176|---|---|---|---|177| `endpoint` | string | yes | Host, or `scheme://host[:port]` |178| `access_key_id` | string | yes | |179| `secret_access_key` | string | no | **Write-only.** Encrypted in the asset; does not create a credential |180| `credential_id` | number | no | Existing managed password credential ID; mutually exclusive with `secret_access_key` |181| `provider` | string | no | UI provider preset label (e.g. `"s3"`, `"minio"`); does not change connection behavior |182| `region` | string | no | |183| `use_path_style` | bool | no | `true` to force path-style addressing (needed by most self-hosted S3-compatible services) |184| `use_ssl` | bool | no | `true` to connect over HTTPS |185| `connect_timeout` | number | no | Seconds; 0 uses the default |186187Plaintext is never returned, is encrypted in the asset, and never creates a managed credential.188189Example:190191 put_asset(name="backups-bucket", type="oss", config={"endpoint":"s3.us-east-1.amazonaws.com","region":"us-east-1","access_key_id":"AKIAEXAMPLE","secret_access_key":"...","use_ssl":"true"})