@tinycloud/cli 1.5.0-beta.8 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ This release has **Commander coverage tracked, not complete parity**:
5
5
 
6
6
  - 4 migrated registration(s).
7
7
  - 1 partially migrated registration(s).
8
- - 136 legacy registration(s) remain Commander-owned.
8
+ - 150 legacy registration(s) remain Commander-owned.
9
9
 
10
10
  - `auth import [source]` → `tinycloud.auth.import@1` (partial; legacy inputs: v1 delegation artifact, v1 permission artifact without command, bare portable delegation, stored delegation wrapper, cross-user delegation persisted with activated=false).
11
11
  - `secrets get <name>` → `tinycloud.secrets.get@1` (migrated).
@@ -16,13 +16,10 @@ This release has **Commander coverage tracked, not complete parity**:
16
16
 
17
17
  ## Calling the CLI
18
18
 
19
- `/usr/sbin/tc` (iproute2 traffic control) shadows the older `tc` name on many Linux hosts, so the CLI is called `tinycloud`. Call the CLI by path, preferring the private install from https://agents.tinycloud.xyz/cli (set `TC_BIN` to use another one):
19
+ Run the installed CLI as `tinycloud`. On Linux, `tc` is iproute2 traffic control at `/usr/sbin/tc`.
20
20
 
21
21
  ```bash
22
- TC="${TC_BIN:-$HOME/.local/share/tinycloud/cli/node_modules/.bin/tinycloud}" # TC_BIN, else where agents.tinycloud.xyz/cli installs it
23
- [ -n "${TC_BIN:-}" ] || [ -x "$TC" ] || ! command -v npm >/dev/null || TC="$(npm prefix --global)/bin/tinycloud" # else a global npm install
24
- [ -x "$TC" ] || { echo "TC_NOT_FOUND: $TC is not executable; install the CLI from https://agents.tinycloud.xyz/cli or set TC_BIN" >&2; exit 1; }
25
- "$TC" --version
22
+ tinycloud --version
26
23
  ```
27
24
 
28
25
  ## Global Options
@@ -41,10 +38,9 @@ TC="${TC_BIN:-$HOME/.local/share/tinycloud/cli/node_modules/.bin/tinycloud}" #
41
38
  ## Context and Login
42
39
 
43
40
  ```bash
44
- "$TC" profile list # use a new profile name; keep existing profiles
45
- "$TC" init --name publisher --key-only
46
- "$TC" --profile publisher auth login --device --manifest builtin:share-publishing --expiry 7d
47
- "$TC" --profile publisher context --json
41
+ tinycloud onboard # once; every later task reuses this profile
42
+ tinycloud auth request --cap SPEC --grant --paste --expiry 30d # more access: ask once
43
+ tinycloud context --json
48
44
  ```
49
45
 
50
46
  `context` always emits JSON: profile, `ownerDid`, `sessionDid`, host, `spaceId` and local session expiry, with `access: "not-tested"`. It never prints keys or signed material.
@@ -53,15 +49,17 @@ TC="${TC_BIN:-$HOME/.local/share/tinycloud/cli/node_modules/.bin/tinycloud}" #
53
49
  |------|-------------|
54
50
  | `--device` | Approve on another device (phone) through OpenKey device authorization. Requires a KV-scoped `--manifest` (no `tinycloud.sql`, no secret decrypt). Prints `Approve on your phone: URL (code XXXX-XXXX)` to stderr and waits for the whole approval window |
55
51
  | `--manifest <file>` | Request only this manifest's permissions: one space, plus raw `tinycloud.encryption` network entries (space `encryption`) such as a secrets decrypt grant. Every scoped login adds `tinycloud.capabilities/read` on path `""` in that space when missing (OpenKey requires it to sign). File path, `base64:<json>`, or `builtin:share-publishing` |
56
- | `--expiry <duration>` | Session lifetime (`1h`, `7d`, milliseconds; at least `1m`) counted from approval; ISO dates are refused because OpenKey signs approval time plus a lifetime. Enforced against the signed session on every login path. Device login: at most `30d`, default `30d` |
52
+ | `--expiry <duration>` | Session lifetime (`1h`, `7d`, milliseconds; at least `1m`) counted from approval; ISO dates are refused because OpenKey signs approval time plus a lifetime. Enforced against the signed session on every login path. Normal OpenKey login defaults to `7d`; onboarding explicitly requests `30d`. Device login: at most `30d`, default `30d` |
57
53
  | `--owner <did>` | Refuse approval by any identity other than this `did:pkh`; must agree with an owner the profile already recorded. OpenKey preselects that owner's key, so this is how to sign in with a key that is not the account's primary key; it works on a plain login without `--manifest` too. Also names the secrets owner for a manifest's `secrets` when the profile has no recorded owner |
58
54
  | `--replace-session` | Scoped or device login: replace a live session the new scope would narrow, change or shorten (otherwise `SESSION_IN_USE`); renewals and widenings that last at least as long need no flag |
59
55
  | `--replication-prefix <prefix>` | Repeatable login request; adds `get` + `sync` only when an unrestricted covering `get` is already requested. Saves prefixes on the profile and reapplies them on later logins and rotation |
60
56
  | `--replication-allow-secrets` | Explicitly allow prefixes in the `secrets` space or `vault` tree; replication stores ciphertext only |
61
57
  | `--method openkey\|local` | Browser OpenKey flow or local Ethereum key |
62
- | `--paste`, `--no-popup` | `--paste` waits up to five minutes for the one-line OpenKey signed code on stdin; `--no-popup` only prints the approval URL. An interactive CLI accepts paste if a browser callback is blocked. Safari, and Chromium browsers enforcing Local Network Access without granting it (including headless Chromium), may require paste for replica-enabled prepared-SIWE login because HTTPS-to-HTTP loopback is blocked. Non-TTY `--no-popup` cannot accept paste: if callback fails it expires nonzero with a `--paste` retry hint; the old signed code cannot be reused. An EOF in `--paste` mode returns `PASTE_CODE_MISSING` without saving a key or session. Ctrl-C, SIGINT or SIGTERM before the signed code arrives cancels the OpenKey handoff (best effort) and exits 130 `CANCELLED`. |
58
+ | `--paste`, `--no-popup` | Normal OpenKey login prints a short approval link on stderr, waits for approval, then automatically verifies, durably saves and acknowledges the result. It works in a non-interactive terminal without a localhost callback or return code. `--no-popup` skips opening a local browser, not waiting. Re-run the same command after interruption to resume. `--paste` instead selects the explicit manual fallback with a five-minute, single-use return code on stdin; EOF without a code returns `PASTE_CODE_MISSING` without saving a key or session. Automatic links do not produce a manual code. |
63
59
 
64
- Device login JSON lists the signed, approved `permissions`, owner-unchecked `declined` capabilities, `ownerDid`, `spaceId` and `expiresAt`. `tinycloud auth request --manifest FILE --grant --device` adds another space to a logged-in profile the same way (default lifetime 7d). `tinycloud enable share [--replace-session]` is shorthand for device login with `builtin:share-publishing`. OpenKey refuses `tinycloud.sql` and other abilities outside its device policy; use browser login (`--method openkey --manifest`) for those. Non-interactive browser login without `--paste` or `--no-popup` fails fast with `INTERACTIVE_LOGIN_REQUIRED`. Only an explicit `--host` is stored on the profile. Errors: `MANIFEST_REQUIRED`, `SCOPE_REJECTED` (OpenKey refused a capability; the message names it), `SESSION_IN_USE` (the request or the approved scope would narrow a live session; use a new profile or `--replace-session`), `PROFILE_CHANGED_DURING_LOGIN` (another login, key rotation or logout changed the profile while approval was pending; nothing saved), `PROFILE_STATE_INCONSISTENT` (profile, key and session disagree after an interrupted write; `--replace-session` or a new profile), `PROFILE_LOCK_TIMEOUT` (another process held the profile lock past 45 s), `LOCAL_OWNER_PROFILE` (scoped or device login on a local-owner-key profile; use a separate profile), `OPENKEY_UNREACHABLE`, `DEVICE_AUTH_DENIED`, `DEVICE_AUTH_EXPIRED`, `DEVICE_AUTH_BINDING_MISMATCH` (OpenKey's approval metadata disagrees with the signed grant), `OPENKEY_OWNER_MISMATCH`, `OPENKEY_GRANT_BROADENED`, `OPENKEY_EXPIRY_EXCEEDED` (the signed session outlives `--expiry`). Owner addresses compare case-insensitively (EIP-55 or lowercase); chain id and space name compare exactly. See [AUTH.md](AUTH.md).
60
+ `auth login --method openkey` on a profile that does not exist creates a key-only profile first (as `init --key-only` does), then continues into the login; nothing is granted until the owner approves. A `--manifest` or `--device` login still needs the profile to exist (use `tinycloud init --key-only --name X` first). `auth logout` on a missing profile still fails with `PROFILE_NOT_FOUND` and creates nothing.
61
+
62
+ Device login JSON lists the signed, approved `permissions`, owner-unchecked `declined` capabilities, `ownerDid`, `spaceId` and `expiresAt`. `tinycloud auth request --manifest FILE --grant --paste` adds another space to the onboarding profile: send the owner the link and pass their code to the CLI exactly as sent (default lifetime 7d; `--grant --device` also works for KV-scoped manifests). `tinycloud enable share [--replace-session]` is shorthand for device login with `builtin:share-publishing`. OpenKey refuses `tinycloud.sql` and other abilities outside its device policy; use normal OpenKey login (`--method openkey --manifest`) for those. Explicit `auth login` can wait for approval non-interactively; data commands never initiate consent to repair missing authority. Only an explicit `--host` is stored on the profile. Errors: `MANIFEST_REQUIRED`, `SCOPE_REJECTED` (OpenKey refused a capability; the message names it), `SESSION_IN_USE` (the request or the approved scope would narrow a live session; use a new profile or `--replace-session`), `PROFILE_CHANGED_DURING_LOGIN` (another login, key rotation or logout changed the profile while approval was pending; nothing saved), `PROFILE_STATE_INCONSISTENT` (profile, key and session disagree after an interrupted write; `--replace-session` or a new profile), `PROFILE_LOCK_TIMEOUT` (another process held the profile lock past 45 s), `LOCAL_OWNER_PROFILE` (scoped or device login on a local-owner-key profile; use a separate profile), `OPENKEY_UNREACHABLE`, `DEVICE_AUTH_DENIED`, `DEVICE_AUTH_EXPIRED`, `DEVICE_AUTH_BINDING_MISMATCH` (OpenKey's approval metadata disagrees with the signed grant), `OPENKEY_OWNER_MISMATCH`, `OPENKEY_GRANT_BROADENED`, `OPENKEY_EXPIRY_EXCEEDED` (the signed session outlives `--expiry`). Owner addresses compare case-insensitively (EIP-55 or lowercase); chain id and space name compare exactly. See [AUTH.md](AUTH.md).
65
63
 
66
64
  `builtin:share-publishing` requests, in the owner's `default` space, exactly:
67
65
 
@@ -83,17 +81,17 @@ No `del` anywhere and no `list` on the bearer prefix: no `tinycloud share` comma
83
81
  ## Delegations
84
82
 
85
83
  ```bash
86
- "$TC" delegation create \
84
+ tinycloud delegation create \
87
85
  --to did:pkh:eip155:1:0xRecipient... \
88
86
  --path kv/shared \
89
87
  --actions kv/get,kv/put \
90
88
  --expiry 24h
91
89
 
92
- "$TC" delegation list # All
93
- "$TC" delegation list --granted # Granted by me
94
- "$TC" delegation list --received # Received by me
95
- "$TC" delegation info <cid>
96
- "$TC" delegation revoke <cid>
90
+ tinycloud delegation list # All
91
+ tinycloud delegation list --granted # Granted by me
92
+ tinycloud delegation list --received # Received by me
93
+ tinycloud delegation info <cid>
94
+ tinycloud delegation revoke <cid>
97
95
  ```
98
96
 
99
97
  Actions are auto-prefixed with `tinycloud.` if not already.
@@ -104,16 +102,18 @@ warning that revocation signs you out. After the node confirms that exact CID,
104
102
  the CLI runs its normal durable logout, removes the restorable session and key,
105
103
  and blocks implicit reauthentication. Unrelated CIDs do not sign you out.
106
104
 
107
- A grant the owner signed through `auth grant` can be revoked from any of the owner's profiles, including one that never recorded it (another CLI profile or machine). When this profile has no local record of the CID, the CLI reads the owner's delegation records on the node and uses that record's space only if the grant was issued by the signed-in owner (the issuer is the owner or the signed-in session key, or a key that holds a delegation recorded on the node whose own issuer is, in turn, one of those: any holder along such an owner-rooted chain counts, up to 8 hops) and every space it covers is one plain space id (`tinycloud:pkh:eip155:<chain>:<address>:<name>`) that belongs to that owner. The lookup reads at most 5,000 records (pages of 100; a larger page is refused) and refuses rather than decides on a partial list. A CID the node does not list, one issued by someone else, a space the owner does not hold, or an ambiguous record is refused (`RAW_RECAP_RESOURCE_UNSUPPORTED`, "this CLI profile has no record of this grant and the node's record does not match the signed-in owner"). An OpenKey owner uses the same space-scoped request as for a locally recorded grant; OpenKey profiles hold no wallet signer, so there is no wallet-signed revoke for them. After the revoke, the agent's next call is refused (`GRANT_REVOKED`, exit 5, the same as `tc replica`: "This grant was revoked. Ask the owner for a new grant") as soon as the node has recorded it.
105
+ From a delegate (agent) profile, `delegation list --granted` lists the delegations that profile's own key issued: every page of the node's issued-delegations listing, each with its `status`, sorted by delegation ID rather than by date (the output's `note` says so). A node that does not offer the listing yet answers `ISSUED_DELEGATIONS_UNAVAILABLE` (exit 7). `delegation revoke <cid>` from a delegate profile revokes a delegation its own key issued with a revocation signed by that key, so it needs no revoke grant from the owner. Revoking one that's already revoked also succeeds. A delegation the key did not issue is refused with the node's message (`REVOKE_UNAUTHORIZED`, exit 5). A chain too long for the node to check right now answers `CHAIN_TOO_DEEP` (exit 7); retry later. Owner profiles behave as described above.
106
+
107
+ A grant the owner signed through `auth grant` can be revoked from any of the owner's profiles, including one that never recorded it (another CLI profile or machine). When this profile has no local record of the CID, the CLI reads the owner's delegation records on the node and uses that record's space only if the grant was issued by the signed-in owner (the issuer is the owner or the signed-in session key, or a key that holds a delegation recorded on the node whose own issuer is, in turn, one of those: any holder along such an owner-rooted chain counts, up to 8 hops) and every space it covers is one plain space id (`tinycloud:pkh:eip155:<chain>:<address>:<name>`) that belongs to that owner. The lookup reads at most 5,000 records (pages of 100; a larger page is refused) and refuses rather than decides on a partial list. A CID the node does not list, one issued by someone else, a space the owner does not hold, or an ambiguous record is refused (`RAW_RECAP_RESOURCE_UNSUPPORTED`, "this CLI profile has no record of this grant and the node's record does not match the signed-in owner"). An OpenKey owner uses the same space-scoped request as for a locally recorded grant; OpenKey profiles hold no wallet signer, so there is no wallet-signed revoke for them. After the revoke, the agent's next call is refused (`GRANT_REVOKED`, exit 5, the same as `tinycloud replica`: "This grant was revoked. Ask the owner for a new grant") as soon as the node has recorded it.
108
108
 
109
109
  `tinycloud auth import` stores delegations in `additional-delegations.json`. Every replay path installs only a grant with a locally verified proof and a binding containing its signed authority; unbound and proof-less grants never install, including while migration is deferred. [AUTH.md](AUTH.md#imported-delegations-and-their-request-bindings) covers migration and recovery.
110
110
 
111
111
  ### Stored grants
112
112
 
113
113
  ```bash
114
- "$TC" --profile PROFILE auth grants list --json
115
- "$TC" --profile PROFILE auth grants remove <cid>
116
- "$TC" --profile PROFILE auth grants remove <cid> --revoke --yes
114
+ tinycloud --profile PROFILE auth grants list --json
115
+ tinycloud --profile PROFILE auth grants remove <cid>
116
+ tinycloud --profile PROFILE auth grants remove <cid> --revoke --yes
117
117
  ```
118
118
 
119
119
  `list` is local and does not contact the node. Its JSON contains `profile`, `session` (`cid` recomputed from stored header and `expiresAt`, or `null`), `localVerification` (`available`, `session_missing`, `session_expired`, `session_invalid`), and `grants`. Each indexed grant row contains `cid` (CID-shaped or `null`), `expiry`, `expired`, `local` (`verified`, `refused`, `not_checked`, `for_another_key`), `reason` (fixed skip code or `null`), `node: "not_checked"`, `bound`, `proof` (`siwe`, `compact`, `missing`, or `null`), `source`, `scope`, and `scopeSource` (`signed` or `stored`). `for_another_key` means the stored audience names another key; its scope is unverified stored metadata and it cannot install in this session. Local verification checks signed scope and binding, not the node's delegation chain or revocation status. Human output shows `CID | LOCAL | EXPIRES | SOURCE | BOUND | SCOPE` and the session CID.
@@ -133,25 +133,25 @@ A concurrent local removal after revocation reports `removed: 0` as success. Oth
133
133
  ## Spaces
134
134
 
135
135
  ```bash
136
- "$TC" space list
137
- "$TC" space create myspace
138
- "$TC" space info # Current space
139
- "$TC" space info <space-id> # Specific space
140
- "$TC" space switch myspace
136
+ tinycloud space list
137
+ tinycloud space create myspace
138
+ tinycloud space info # Current space
139
+ tinycloud space info <space-id> # Specific space
140
+ tinycloud space switch myspace
141
141
  ```
142
142
 
143
- `space list` reads the owner's account registry (`account/spaces/`) when the session holds `tinycloud.kv/get` and `tinycloud.kv/list` on the account space's `spaces/` prefix, and falls back to `tinycloud.space/list` only when the session holds that. A session with neither (including the default OpenKey owner sign-in, which covers only the `default` space) fails with `PERMISSION_DENIED` (exit 5) naming both; an owner grants the registry read with `"$TC" auth request --grant --cap tinycloud.kv:account:spaces/:get,list`.
143
+ `space list` reads the owner's account registry (`account/spaces/`) when the session holds `tinycloud.kv/get` and `tinycloud.kv/list` on the account space's `spaces/` prefix, and falls back to `tinycloud.space/list` only when the session holds that. A session with neither (including the default OpenKey owner sign-in, which covers only the `default` space) fails with `PERMISSION_DENIED` (exit 5) naming both; an owner grants the registry read with `tinycloud auth request --grant --cap tinycloud.kv:account:spaces/:get,list`.
144
144
 
145
145
  ## Profiles
146
146
 
147
147
  Multiple identities/environments with isolated keys and sessions.
148
148
 
149
149
  ```bash
150
- "$TC" profile list
151
- "$TC" profile create staging --host https://staging.tinycloud.xyz
152
- "$TC" profile show # Current profile
153
- "$TC" profile switch staging # Change default
154
- "$TC" profile delete old-profile
150
+ tinycloud profile list
151
+ tinycloud profile create staging --host https://staging.tinycloud.xyz
152
+ tinycloud profile show # Current profile
153
+ tinycloud profile switch staging # Change default
154
+ tinycloud profile delete old-profile
155
155
  ```
156
156
 
157
157
  Per-command override: `tinycloud kv get mykey --profile staging`
@@ -168,33 +168,49 @@ network grant` takes the short name, resolves the network, and issues
168
168
  separately from any KV or SQL reads the app also needs.
169
169
 
170
170
  ```bash
171
- "$TC" secrets network init
172
- "$TC" secrets network show
173
- "$TC" secrets network grant did:pkh:eip155:1:0xRecipient...
174
-
175
- printf '%s' "$ANTHROPIC_API_KEY" | "$TC" secrets put ANTHROPIC_API_KEY --stdin # keeps the value out of argv and shell history
176
- "$TC" secrets get ANTHROPIC_API_KEY
177
- "$TC" secrets get ANTHROPIC_API_KEY -o key.txt # atomic owner-only (0600) replacement
178
- "$TC" secrets list
179
- "$TC" secrets delete ANTHROPIC_API_KEY
171
+ tinycloud secrets network init
172
+ tinycloud secrets network show
173
+ tinycloud secrets network grant did:pkh:eip155:1:0xRecipient...
174
+
175
+ printf '%s' "$ANTHROPIC_API_KEY" | tinycloud secrets put ANTHROPIC_API_KEY --stdin # keeps the value out of argv and shell history
176
+ tinycloud secrets get ANTHROPIC_API_KEY
177
+ tinycloud secrets get ANTHROPIC_API_KEY -o key.txt # atomic owner-only (0600) replacement
178
+ tinycloud secrets list
179
+ tinycloud secrets delete ANTHROPIC_API_KEY
180
180
  ```
181
181
 
182
182
  A missing secret makes `tinycloud secrets get` exit 4 (`NOT_FOUND`). For an identity that is not local, `error.setup` carries the owner's request link; a local identity (or a name/scope no link can express) gets only a hint naming `tinycloud secrets put NAME --stdin`. `tinycloud secrets doctor NAME` hints `tinycloud secrets request NAME` for a missing secret, or `tinycloud secrets put NAME --stdin` for a local identity. `tinycloud secrets get --delegation` follows the same rule, and a delegated read the node refuses (403, a capability 401, `AUTH_UNAUTHORIZED`) exits 5.
183
183
 
184
+ ### Running a command with secrets: `run`
185
+
186
+ ```bash
187
+ tinycloud secrets run NAME... [--map NAME=ENV_VAR]... [--scope S] [--space S] [--delegation SOURCE] [--private-key HEX] -- <command> [args...]
188
+ tinycloud --profile agent secrets run OPENAI_API_KEY -- python train.py
189
+ tinycloud --profile agent secrets run --map ANTHROPIC_KEY=ANTHROPIC_API_KEY -- node server.js
190
+ ```
191
+
192
+ `tinycloud secrets run` reads every named secret as `tinycloud secrets get` does, then starts `<command>` directly, without a shell, with each value in its environment: under the secret's name, or under `ENV_VAR` for `--map NAME=ENV_VAR`. A value replaces an inherited variable of the same name. The CLI doesn't pass its own `TC_PRIVATE_KEY` to the command. To give the command a key from a secret, use `--map NAME=TC_PRIVATE_KEY`. Everything after the first `--`, including any further `--`, goes to the command unchanged. The command uses the CLI's stdin, stdout and stderr; the CLI itself writes nothing to stdout, and its own messages go to stderr.
193
+
194
+ - Name at least one secret, as an argument or with `--map`. `ENV_VAR` must match `[A-Za-z_][A-Za-z0-9_]*`; two secrets mapped to the same variable, no `--`, or nothing after it exit 2 (`USAGE_ERROR`) before any read.
195
+ - If any secret cannot be read, the command does not start and the CLI exits with the code and error `tinycloud secrets get` gives for that secret. A value containing a NUL character exits 1 (`SECRET_NOT_EXPORTABLE`) without starting the command.
196
+ - Otherwise the CLI exits with the command's exit code. If the command was ended by SIGINT, SIGTERM, SIGHUP, SIGKILL, SIGALRM or SIGUSR2, the CLI ends by the same signal (or exits with 128 plus the signal number if that signal doesn't end it). If any other signal ended it, such as SIGQUIT or SIGSEGV, the CLI exits with 128 plus the signal number instead, so it does not cause a core dump (SIGUSR1 and SIGPIPE are left out too: Node uses SIGUSR1 for its inspector and ignores SIGPIPE).
197
+ - SIGINT, SIGTERM, SIGHUP and SIGQUIT sent to the CLI go to the command. When the CLI runs in the foreground of a terminal, it doesn't pass on SIGINT, SIGQUIT or SIGHUP: the terminal already sends them (Ctrl-C, Ctrl-\, hangup) to the command, so the command normally gets each one once. The CLI checks whether it's in the foreground using `/proc`, or `ps` where `/proc` isn't available. If neither works, it passes the signals on. When the CLI runs in the background, it passes them on, so a signal sent to the whole job (for example `kill -INT %1`) can reach the command twice.
198
+ - From a script, stop the command by sending SIGTERM to the CLI's process ID. The CLI passes it on, waits for the command to exit, then exits the way the command did (the command's own exit code if it handles SIGTERM itself). If the command ignores SIGTERM, the CLI keeps waiting.
199
+ - A command that does not exist exits 127; one that cannot be executed, or cannot be started for another reason, exits 126. These cases print one plain stderr line even with `--json`: `tinycloud secrets run: <command>: command not found`, `…: permission denied` or `…: cannot run (<error code>)`.
200
+
201
+ Limits: `tinycloud secrets run` keeps values out of the command line, your shell history and the CLI's own output. It doesn't hide them from the command: the command, and anything it starts, receives the values and can print, log or pass them on. Other programs running as the same user can often read a process's environment too.
202
+
184
203
  ### Asking the owner for a secret: `request`, `wait`, `manage`
185
204
 
186
205
  `/usr/sbin/tc` (iproute2 traffic control) can shadow the older `tc` name on `PATH`, so scripts call the CLI by absolute path:
187
206
 
188
207
  ```bash
189
- TC="${TC_BIN:-$HOME/.local/share/tinycloud/cli/node_modules/.bin/tinycloud}" # TC_BIN, else where agents.tinycloud.xyz/cli installs it
190
- [ -n "${TC_BIN:-}" ] || [ -x "$TC" ] || ! command -v npm >/dev/null || TC="$(npm prefix --global)/bin/tinycloud" # else a global npm install
191
- [ -x "$TC" ] || { echo "TC_NOT_FOUND: $TC is not executable; install the CLI from https://agents.tinycloud.xyz/cli or set TC_BIN" >&2; exit 1; }
192
- "$TC" --profile agent secrets request OPENAI_API_KEY --requester "agent@host" --json
208
+ tinycloud --profile agent secrets request OPENAI_API_KEY --requester "agent@host" --json
193
209
  # {"url":"https://secrets.tinycloud.xyz/request?owner=…&name=OPENAI_API_KEY&requester=…&v=1","names":["OPENAI_API_KEY"],"owner":"did:pkh:eip155:1:0x…"}
194
210
  # send the url to the owner once, then:
195
- "$TC" --profile agent secrets wait OPENAI_API_KEY --timeout 9m --json
196
- "$TC" --profile agent secrets get OPENAI_API_KEY --raw
197
- "$TC" secrets manage OPENAI_API_KEY --no-open --json # {"url":"…","opened":false}: the replace/manage page
211
+ tinycloud --profile agent secrets wait OPENAI_API_KEY --timeout 9m --json
212
+ tinycloud --profile agent secrets get OPENAI_API_KEY --raw
213
+ tinycloud secrets manage OPENAI_API_KEY --no-open --json # {"url":"…","opened":false}: the replace/manage page
198
214
  ```
199
215
 
200
216
  - **`secrets request <NAME...> [--scope S | --app ID] [--owner DID] [--requester LABEL] [--open]`** prints `{url, names, scope?, owner}` and exits 0. It is offline: no session, no node request. 1–10 names (`^[A-Z][A-Z0-9_]*$`, ≤ 64, duplicates dropped). `--app ID` derives the scope from an app id (`xyz.tinycloud.feed` → `xyz-tinycloud-feed`); `--app` with `--scope` exits 2. `--requester` defaults to `user@host`. `--open` also opens the link; a failure to open only adds a `BROWSER_OPEN_FAILED` warning on stderr. Errors: 2 for `INVALID_SECRET_NAME`, `TOO_MANY_SECRET_NAMES`, `INVALID_SECRET_SCOPE`, `INVALID_REQUESTER`, `INVALID_OWNER`, `OWNER_MISMATCH`, `LOCAL_OWNER_PROFILE`; 3 for `OWNER_DID_UNKNOWN` (pass `--owner`).
@@ -234,11 +250,11 @@ An agent profile gets read access to named secrets through a scoped OpenKey past
234
250
 
235
251
  ```bash
236
252
  echo '{ "app_id": "my.agent", "space": "secrets", "secrets": { "OPENAI_API_KEY": true } }' > agent.json
237
- "$TC" init --name agent --key-only
238
- "$TC" --profile agent auth login --method openkey --paste --manifest agent.json --owner did:pkh:eip155:1:0xOWNER < /dev/null
253
+ tinycloud init --name agent --key-only
254
+ tinycloud --profile agent auth login --method openkey --paste --manifest agent.json --owner did:pkh:eip155:1:0xOWNER < /dev/null
239
255
  # exits 3 (PASTE_CODE_MISSING) and names the approval URL; the owner approves and returns a code
240
- printf '%s\n' "$CODE" | "$TC" --profile agent auth login --method openkey --paste --manifest agent.json --owner did:pkh:eip155:1:0xOWNER
241
- "$TC" --profile agent secrets get OPENAI_API_KEY --raw
256
+ printf '%s\n' "$CODE" | tinycloud --profile agent auth login --method openkey --paste --manifest agent.json --owner did:pkh:eip155:1:0xOWNER
257
+ tinycloud --profile agent secrets get OPENAI_API_KEY --raw
242
258
  ```
243
259
 
244
260
  `secrets: { NAME: true }` requests, in the owner's `secrets` space, `kv/get` on `vault/secrets/NAME` and `capabilities/read` on `""`, plus the raw network entry `{ "service": "tinycloud.encryption", "space": "encryption", "path": "urn:tinycloud:encryption:<ownerDid>:default", "actions": ["tinycloud.encryption/decrypt"] }`. The owner DID comes from the profile's recorded owner, else `--owner`, and its address is EIP-55 checksummed in the URN; with neither the login fails with `OWNER_DID_UNKNOWN`. The JSON result lists approved `permissions` and `declined` entries. A decrypt signed inside the owner's space by an older OpenKey deployment is not a raw grant: scoped login reports it in `declined` and omits it from saved permissions. Browser `auth request --grant` or secret-read escalation refuses that old nested proof before activating or storing a grant (`OPENKEY_GRANT_BROADENED`); a renewed approval must sign the raw network grant.
@@ -247,23 +263,23 @@ When neither stdin nor stderr is a terminal, `secrets get|list|put|delete` on an
247
263
 
248
264
  ## Local Replicas
249
265
 
250
- `tinycloud replica` keeps a durable, read-only copy of one KV prefix on this device. `sync` pulls the node's `tinycloud.kv/sync` change feed (node feature `kv-sync-v1`) and fetches each changed value, accepting bytes only if they hash to the ETag the node attested. `get`, `list`, `status` and `reset` never touch the network, so they work with the host offline. Reads report observed state from one source host, not global finality. Needs Node.js 22.13+ or Bun (built-in SQLite); on older Node every `tinycloud replica` command fails with `RUNTIME_UNSUPPORTED` (exit 1) and other commands are unaffected.
266
+ `tinycloud replica` keeps a durable, read-only copy of one KV prefix on this device. `sync` pulls the node's `tinycloud.kv/sync` change feed (node feature `kv-sync-v1`) and fetches each changed value, accepting bytes only if they hash to the ETag the node attested. `get`, `list`, `status` and `reset` never touch the network, so they work with the host offline. Reads report observed state from one source host, not global finality. Needs Node.js 22.20 or later, or Bun (built-in SQLite); on older Node every `tinycloud replica` command fails with `RUNTIME_UNSUPPORTED` (exit 1) and other commands are unaffected.
251
267
 
252
268
  A replica needs a device grant carrying `get` and `sync` on the prefix. Neither `*` nor `tinycloud.kv/*` implies `sync`, and the owner's default session does not hold it; `tinycloud auth grant` acquires it for that grant only:
253
269
 
254
270
  ```bash
255
271
  # device
256
- "$TC" --profile device auth request --cap tinycloud.kv:SPACE:notes/:get,list,metadata,sync --expiry 30d --emit req.json
272
+ tinycloud --profile device auth request --cap tinycloud.kv:SPACE:notes/:get,list,metadata,sync --expiry 30d --emit req.json
257
273
  # owner
258
- "$TC" --profile owner auth grant req.json > grant.json
274
+ tinycloud --profile owner auth grant req.json > grant.json
259
275
  # device
260
- "$TC" --profile device auth import grant.json
261
- "$TC" --profile device replica sync --prefix notes/ # first sync creates the replica "notes"
262
- "$TC" --profile device replica get notes/todo.md --raw # offline
263
- "$TC" --profile device replica list notes/ --json
264
- "$TC" --profile device replica status
265
- "$TC" --profile device replica sync # catch up: updates and deletes
266
- "$TC" --profile device replica reset [--purge]
276
+ tinycloud --profile device auth import grant.json
277
+ tinycloud --profile device replica sync --prefix notes/ # first sync creates the replica "notes"
278
+ tinycloud --profile device replica get notes/todo.md --raw # offline
279
+ tinycloud --profile device replica list notes/ --json
280
+ tinycloud --profile device replica status
281
+ tinycloud --profile device replica sync # catch up: updates and deletes
282
+ tinycloud --profile device replica reset [--purge]
267
283
  ```
268
284
 
269
285
  - The first `sync` needs `--prefix` (and `--space` when grants cover several spaces); it pins the source host (`--host` or the profile host, never a discovered local node). Later runs use the stored configuration; a different `--prefix`, `--space` or `--host` is `REPLICA_CONFIG_MISMATCH` (exit 2). `--replica <name>` selects or names a replica (default: named after the prefix, or the profile's only replica).
@@ -327,8 +343,8 @@ session, logout signs that session out and removes only the profile's legacy
327
343
  `replicas/` tree; the other account's gate and flag-owned replicas stay. An
328
344
  interrupted earlier sign-out is reported, and a second `auth logout` completes
329
345
  it. A gate that no longer matches its logout record (for example, restored
330
- from an older copy) refuses logout; run `tc profile delete <name>` (after
331
- `tc profile switch <other>` if it is the default) and sign in again.
346
+ from an older copy) refuses logout; run `tinycloud profile delete <name>` (after
347
+ `tinycloud profile switch <other>` if it is the default) and sign in again.
332
348
 
333
349
  `--keep-replicas` retains legacy files only when no replica gate, flag-owned
334
350
  partition or pending purge exists; gated profiles refuse before mutation and
@@ -343,49 +359,51 @@ replayed and the covered read is served locally. If explicit `kv/get` plus
343
359
  ## Node Health
344
360
 
345
361
  ```bash
346
- "$TC" node health # Ping + latency
347
- "$TC" node version # Server version
348
- "$TC" node status # Combined
362
+ tinycloud node health # Ping + latency
363
+ tinycloud node version # Server version
364
+ tinycloud node status # Combined
349
365
  ```
350
366
 
351
367
  ## Shell Completions
352
368
 
353
369
  ```bash
354
- eval "$("$TC" completion bash)"
355
- eval "$("$TC" completion zsh)"
356
- "$TC" completion fish | source
370
+ eval "$(tinycloud completion bash)"
371
+ eval "$(tinycloud completion zsh)"
372
+ tinycloud completion fish | source
357
373
  ```
358
374
 
359
375
  ## Share Publish, Inspect, Receive, List, Revoke
360
376
 
361
- Publishing needs the publishing scope on a dedicated profile (`tinycloud --profile publisher auth login --device --manifest builtin:share-publishing`, or `tinycloud --profile publisher enable share`). Publish with `tinycloud --profile publisher share publish ...`.
377
+ Publishing needs the publishing scope in the owner's `default` space, on a dedicated profile: `tinycloud init --name publisher --key-only`, then `tinycloud --profile publisher auth login --method openkey --paste --manifest builtin:share-publishing --expiry 30d`. Publish with `tinycloud --profile publisher share publish ...`.
378
+
379
+ Sharing to an email address, domain or DID isn't available in this release; link shares are.
362
380
 
363
381
  ```bash
364
- "$TC" share publish ./note.md --json # bearer link; JSON has no URL: use share show <id> --reveal-link
365
- "$TC" share publish ./note.md --to email:alice@example.com --notify # addressed: exact email, emailed invitation (read required)
366
- "$TC" share publish ./note.md --to did:pkh:eip155:1:0xRecipient... # addressed: one DID
367
- "$TC" share publish ./note.md --to domain:example.com # addressed: any verified address at the domain
368
- "$TC" share publish ./note.md --to domain:example.com --notify --notify-to alice@example.com # invite one matching mailbox; node 1.17.3+
369
- cat note.md | "$TC" share publish - --name note.md
370
- "$TC" share publish ./site/ --json # folder with index.html -> one self-contained page, bearer link
371
-
372
- printf '%s' "$SHARE_URL" | "$TC" share inspect - --json # verify, print safe metadata
373
- printf '%s' "$SHARE_URL" | "$TC" share receive - --stdout # bearer link: verified bytes
374
- printf '%s' "$SHARE_URL" | "$TC" share receive - --output .
375
-
376
- "$TC" share list --json # sender history, no complete URLs; human rows are shareId, target, expiresAt, status (active, revoked or expired), tab-separated
377
- "$TC" share show <id> [--reveal-link]
378
- "$TC" share notify <id> --to alice@example.com # retry email delivery within 5 minutes of publication
379
- "$TC" share revoke <id> # bearer or addressed share this profile created
380
- "$TC" share history recover [--json] # move an unreadable sender history aside (renamed, never deleted)
382
+ tinycloud share publish ./note.md --json # bearer link; JSON has no URL: use share show <id> --reveal-link
383
+ tinycloud share publish ./note.md --to email:alice@example.com --notify # addressed: exact email, emailed invitation (read required)
384
+ tinycloud share publish ./note.md --to did:pkh:eip155:1:0xRecipient... # addressed: one DID
385
+ tinycloud share publish ./note.md --to domain:example.com # addressed: any verified address at the domain
386
+ tinycloud share publish ./note.md --to domain:example.com --notify --notify-to alice@example.com # invite one matching mailbox; node 1.17.3+
387
+ cat note.md | tinycloud share publish - --name note.md
388
+ tinycloud share publish ./site/ --json # folder with index.html -> one self-contained page, bearer link
389
+
390
+ printf '%s' "$SHARE_URL" | tinycloud share inspect - --json # verify, print safe metadata
391
+ printf '%s' "$SHARE_URL" | tinycloud share receive - --stdout # bearer link: verified bytes
392
+ printf '%s' "$SHARE_URL" | tinycloud share receive - --output .
393
+
394
+ tinycloud share list --json # sender history, no complete URLs; human rows are shareId, target, expiresAt, status (active, revoked or expired), tab-separated
395
+ tinycloud share show <id> [--reveal-link]
396
+ tinycloud share notify <id> --to alice@example.com # retry email delivery within 5 minutes of publication
397
+ tinycloud share revoke <id> # bearer or addressed share this profile created
398
+ tinycloud share history recover [--json] # move an unreadable sender history aside (renamed, never deleted)
381
399
  ```
382
400
 
383
401
  **Artifact versions (pinned shares).** `--artifact <id|new>` stores the file as the next write-once version, `artifacts/<id>/versions/<n>/<file>` plus a `manifest.json` written last (file sha256 and size, parent, `--note`), in the owner's space. The share is pinned to that version: its delegation covers only that file and never carries `put`. Publishing the same bytes again to a bearer link returns the same version; changed content makes the next one. Publishing never overwrites an earlier version, but the node has no write-once ACL for the owner, so the owner (or anyone holding the owner's session) can overwrite a version file afterwards; the CLI will not, and the writes it makes use `If-None-Match: *`. Only addressed artifact shares detect such an overwrite (see Pin below); a bearer artifact share carries no digest and would deliver the new bytes. An addressed share stores ciphertext under a fresh random key each time, so every addressed publish is a new version, and `manifest.json` records only the sha256 of the stored ciphertext, never of the plaintext (an unsalted plaintext hash next to the ciphertext would let a reader of the space confirm a guess). `--artifact new` mints an id. JSON output adds `artifactId`, `version` and `mode` (`pinned`). `--action edit` on an artifact share is refused (a pinned version is read-only). An artifact share takes one file. Every artifact share pins one version; shares that follow later versions are not available yet. Profiles signed in before this feature need `auth login --device --manifest builtin:share-publishing` again, because the manifest now includes `artifacts/`.
384
402
 
385
403
  ```bash
386
- "$TC" share publish ./report.md --artifact new --to email:alice@example.com --json # v1, pinned
387
- cat report-v2.md | "$TC" share publish - --name report.md --artifact art_0123456789abcdef --to email:alice@example.com --note "second draft"
388
- "$TC" share versions art_0123456789abcdef --json # versions and the shares (from local history) pinned to each
404
+ tinycloud share publish ./report.md --artifact new --to email:alice@example.com --json # v1, pinned
405
+ cat report-v2.md | tinycloud share publish - --name report.md --artifact art_0123456789abcdef --to email:alice@example.com --note "second draft"
406
+ tinycloud share versions art_0123456789abcdef --json # versions and the shares (from local history) pinned to each
389
407
  ```
390
408
 
391
409
  Versions are kept by default and count against the owner's storage. The node does not reclaim overwritten or deleted values; byte reclamation depends on history GC (TC-1277).
@@ -447,17 +465,17 @@ Style with the TinyCloud tokens so the artifact follows the viewer's light and d
447
465
 
448
466
  ```bash
449
467
  # 1. Publish; the JSON carries the share id and the artifact id, never the link
450
- "$TC" --profile publisher share publish ./report.md --to anyone --artifact new --json
468
+ tinycloud --profile publisher share publish ./report.md --to anyone --artifact new --json
451
469
  # {"metadata":{"shareId":"…"},"artifactId":"art_…","version":1,"mode":"pinned", …}
452
470
 
453
471
  # 2. Get the link: a second call. Write it straight to a private file
454
- "$TC" --profile publisher share show <shareId> --reveal-link
472
+ tinycloud --profile publisher share show <shareId> --reveal-link
455
473
 
456
474
  # 3. Revise: publish the next pinned version; it gets its own link, and earlier links keep showing the version they pinned
457
- "$TC" --profile publisher share publish ./report-v2.md --to anyone --artifact art_0123456789abcdef --json
475
+ tinycloud --profile publisher share publish ./report-v2.md --to anyone --artifact art_0123456789abcdef --json
458
476
 
459
477
  # 4. Take a link down; the node confirms before it reports revoked
460
- "$TC" --profile publisher share revoke <shareId>
478
+ tinycloud --profile publisher share revoke <shareId>
461
479
  ```
462
480
 
463
481
  - Default to `--to anyone` for artifacts unless the person you are working for named a narrower audience. Choose `--expires` deliberately (the default is `7d`). Anyone holding the complete URL can open it until it expires or is revoked, and revoking does not recall copies already received. For content that only named people may read, use `--to email:` and tell the recipient it will download rather than render.
@@ -528,6 +546,8 @@ Use `--json` for machine-readable output; interactive commands can render human
528
546
 
529
547
  General storage/auth exit codes: 0 success, 1 operation error (including `PROFILE_MIGRATION_REQUIRED` and `PROBE_FAILED`), 2 invalid input, 3 authentication required, 4 not found, 5 permission denied, 6 network error, 7 node error, 8 pending (`tinycloud secrets wait` only), 10 storage full, 130 interrupted (`CANCELLED`). `tinycloud share` uses its own: 1 profile lock timeout, sender-history retry or unconfirmed revocation (`PROFILE_LOCK_TIMEOUT`, `SHARE_HISTORY_RETRY`, `SHARE_REVOKE_UNCONFIRMED`), 3 upload authority required, 4 unavailable, expired, revoked bearer link, upload failed or revocation refused (`SHARE_LINK_UNAVAILABLE`, `SHARE_REVOKED`, `SHARE_REVOKE_REJECTED`), 5 permission denied (`PERMISSION_DENIED`, `SHARE_ISSUER_KEY_UNAVAILABLE`) or verification failed, 6 recipient authorization required or network error, 7 byte limit, 8 output conflict or unsafe filename, 9 partial success, 10 storage full.
530
548
 
549
+ `tinycloud secrets run` exits with its command's status once the command has started. Before that it uses the codes above, plus 126 (the command cannot be executed or started) and 127 (the command does not exist).
550
+
531
551
  For `tinycloud kv get|put|head|list|delete`, `tinycloud space list|host`, `tinycloud sql`, `tinycloud vault`, `tinycloud vars`, `tinycloud duckdb`, `tinycloud secrets list|put|delete`, `tinycloud delegation`, and `tinycloud account`, service-result errors use typed authorization: a plain HTTP 401 is `AUTH_REQUIRED` (exit 3); a 403 is `PERMISSION_DENIED` (exit 5). A 401 with a validated missing capability, or `AUTH_UNAUTHORIZED` without an HTTP status, is `PERMISSION_DENIED` (exit 5); a validated capability also produces a `tinycloud auth request --cap` hint. This changes the former exit 1 for status-less `AUTH_UNAUTHORIZED` in the listed commands. Other HTTP failures retain their service-specific codes. `tinycloud share` has separate exit mappings below.
532
552
 
533
553
  Every command that writes reports full TinyCloud storage the same way: `STORAGE_QUOTA_EXCEEDED` (storage is full) or `STORAGE_LIMIT_REACHED` (this write is larger than what is left), exit 10, with nothing written (`tinycloud sql copy` instead names the table and the rows it had already copied). The hint gives the account totals when the node reports them (`371.7 MiB used of 100 MiB (free plan)`), says reading still works, and links to https://account.tinycloud.xyz/storage. Retrying cannot succeed until the owner frees up space or upgrades.
@@ -50,7 +50,7 @@ const { delegation } = await tc.delegateTo("did:pkh:eip155:1:0x...", [
50
50
  { service: "tinycloud.kv", space: "default", path: "kv/shared", actions: ["tinycloud.kv/get"] },
51
51
  ], { expiry: "1h" });
52
52
 
53
- // Account delegation query, POST /delegation/query (what `tc delegation list` uses). It includes
53
+ // Account delegation query, POST /delegation/query (what `tinycloud delegation list` uses). It includes
54
54
  // delegations signed by the session key; account.delegations.list() reads the SQL index and does not.
55
55
  await tc.account.delegations.query({ direction: "all" });
56
56
  await tc.delegationManager.revoke(cid);