@tinycloud/cli 1.2.0-beta.1 → 1.2.0-beta.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinycloud/cli",
3
- "version": "1.2.0-beta.1",
3
+ "version": "1.2.0-beta.3",
4
4
  "description": "TinyCloud CLI - manage data, delegations, and nodes from the terminal",
5
5
  "author": "TinyCloud, Inc.",
6
6
  "license": "EGPL",
@@ -31,10 +31,10 @@
31
31
  "dependencies": {
32
32
  "@tinycloud/share-sdk": "1.1.0",
33
33
  "@tinycloud/share-envelope": "1.1.0",
34
- "@tinycloud/node-sdk": "3.2.0-beta.1",
34
+ "@tinycloud/node-sdk": "3.2.0-beta.3",
35
35
  "@tinycloud/node-sdk-wasm": "1.7.7",
36
- "@tinycloud/sdk-core": "3.2.0-beta.0",
37
- "@tinycloud/operations": "0.5.0-beta.1",
36
+ "@tinycloud/sdk-core": "3.2.0-beta.3",
37
+ "@tinycloud/operations": "0.5.0-beta.3",
38
38
  "chalk": "^5.3.0",
39
39
  "commander": "^14.0.0",
40
40
  "open": "^11.0.0",
@@ -42,7 +42,7 @@
42
42
  "siwe": "^3.0.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@tinycloud/replica": "0.1.0",
45
+ "@tinycloud/replica": "0.1.1-beta.0",
46
46
  "@types/node": "^20",
47
47
  "tsup": "^8.0.0",
48
48
  "typescript": "^5.9.3"
@@ -93,6 +93,25 @@ tc --profile agent-secrets secrets get OPENAI_API_KEY --raw
93
93
  - A stored session that has expired, or no longer verifies for the profile's key, fails every command that needs it with `AUTH_REQUIRED` (exit 3) before any node request, whatever the profile kind; secrets commands on an owner OpenKey profile whose session has passed its expiry refresh it in the browser at a terminal instead. A delegate session created by `tc auth import` has no SIWE; its lifetime is the `exp` its compact-UCAN delegation signs, read from the stored authorization bytes, so it is refused the same way, including sessions imported by earlier releases. Expiry is judged by this machine's clock. `context` reports that expiry, and an expired imported session does not block the scoped paste login the hint names. A session imported from a CACAO (wallet-signed) delegation is still refused only by the node. The hint names a new scoped paste login for a delegate profile, `auth login --method local` for a local owner key, and `auth login --method openkey` for an OpenKey owner; Share commands keep their `auth login --device --manifest builtin:share-publishing` guidance. It is never reported as a retryable node error. Operations and MCP report it as `AUTH_REQUIRED` (`retryable: false`), the code they also use for a node's 401.
94
94
  - `secrets get -o FILE` validates the destination and its existing directory before fetching or decrypting secret bytes. It refuses symlinks, directories and devices, syncs a new owner-only (0600) file, then atomically replaces the destination. Directory sync is attempted after replacement where supported; its failure does not report a failed write when the destination was already replaced. A destination that cannot be replaced fails with a sanitized filesystem error naming the destination; there is no unsafe fallback.
95
95
 
96
+ ### Asking the owner to add a missing secret
97
+
98
+ When the secret does not exist yet, the agent sends the owner one message with a request link, redeems the consent short code if the owner approved a scoped login, then waits. Call the CLI by absolute path: a bare `tc` on `PATH` can be iproute2's `/usr/sbin/tc` (traffic control), not TinyCloud.
99
+
100
+ ```bash
101
+ TC_BIN="$(npm prefix --global)/bin/tc"
102
+ "$TC_BIN" --profile agent-secrets secrets request OPENAI_API_KEY --requester "my-agent@$(hostname -s)" --json # offline; prints {url, names, owner}
103
+ # One message to the owner: the url (plus the scoped-login approval URL above if the profile has no grant yet).
104
+ # If the owner sends back a consent short code, redeem it first:
105
+ printf '%s\n' "$CODE" | "${LOGIN[@]}"
106
+ "$TC_BIN" --profile agent-secrets secrets wait OPENAI_API_KEY --timeout 9m --json # 0 ready, 8 still pending
107
+ "$TC_BIN" --profile agent-secrets secrets get OPENAI_API_KEY --raw
108
+ ```
109
+
110
+ - `wait` never signs in: it needs a stored session and doesn't accept `TC_PRIVATE_KEY` (exit 3). Exit 3 carries `error.recovery.command`, the persistent login to run once.
111
+ - After a CLI upgrade, run one normal command per profile first (for example `"$TC_BIN" --profile agent-secrets auth caps`). Otherwise the first `wait` returns `PROFILE_MIGRATION_REQUIRED` (exit 1) once, with that command in `error.recovery.command`.
112
+ - Exit 8 means the owner has not added it yet: rerun `wait` without sending the link again. Exit 5 means the grant does not cover a name: `error.recovery.names` lists them. [REFERENCE.md](REFERENCE.md#asking-the-owner-for-a-secret-request-wait-manage) has the full exit table.
113
+ - A local identity (local-owner profile, a profile with a stored `privateKey`, `--private-key`, `TC_PRIVATE_KEY`) owns its secrets and gets `LOCAL_OWNER_PROFILE` from `request`: store them with `secrets put NAME --stdin`.
114
+
96
115
  ## More spaces after login
97
116
 
98
117
  Login covers one space. Request further spaces on the logged-in profile:
@@ -3,12 +3,13 @@
3
3
  <!-- BEGIN GENERATED TINYCloud operations coverage -->
4
4
  This release has **Commander coverage tracked, not complete parity**:
5
5
 
6
- - 1 migrated registration(s).
6
+ - 2 migrated registration(s).
7
7
  - 1 partially migrated registration(s).
8
- - 125 legacy registration(s) remain Commander-owned.
8
+ - 128 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).
12
+ - `secrets probe <name>` → `tinycloud.secrets.status@1` (migrated).
12
13
  <!-- END GENERATED TINYCloud operations coverage -->
13
14
 
14
15
  ## Calling the CLI
@@ -25,6 +26,8 @@ This release has **Commander coverage tracked, not complete parity**:
25
26
  | `-q, --quiet` | Suppress non-essential output |
26
27
  | `--no-cache` | Disable caching |
27
28
  | `--json` | Force machine-readable output |
29
+ | `--replication` / `--no-replication` | Enable/disable read-through replication for this invocation; `TC_REPLICATION=1` enables it by default, with command flags taking precedence |
30
+ | `--replication-debug` | Write per-read, write and sync diagnostics to stderr |
28
31
 
29
32
  ## Context and Login
30
33
 
@@ -44,6 +47,8 @@ tc --profile publisher context --json
44
47
  | `--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` |
45
48
  | `--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 |
46
49
  | `--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 |
50
+ | `--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 |
51
+ | `--replication-allow-secrets` | Explicitly allow prefixes in the `secrets` space or `vault` tree; replication stores ciphertext only |
47
52
  | `--method openkey\|local` | Browser OpenKey flow or local Ethereum key |
48
53
  | `--paste`, `--no-popup` | Browser flow without a local callback / without opening a browser. `--paste` reads the owner's code from stdin (newline-terminated; a final unterminated line is accepted); stdin ending without a code fails with `PASTE_CODE_MISSING` (exit 3) naming the approval URL |
49
54
 
@@ -138,13 +143,60 @@ tc secrets network init
138
143
  tc secrets network show
139
144
  tc secrets network grant did:pkh:eip155:1:0xRecipient...
140
145
 
141
- tc secrets put ANTHROPIC_API_KEY "sk-..."
146
+ printf '%s' "$ANTHROPIC_API_KEY" | tc secrets put ANTHROPIC_API_KEY --stdin # keeps the value out of argv and shell history
142
147
  tc secrets get ANTHROPIC_API_KEY
143
148
  tc secrets get ANTHROPIC_API_KEY -o key.txt # atomic owner-only (0600) replacement
144
149
  tc secrets list
145
150
  tc secrets delete ANTHROPIC_API_KEY
146
151
  ```
147
152
 
153
+ A missing secret makes `tc 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 `tc secrets put NAME --stdin`. `tc secrets doctor NAME` hints `tc secrets request NAME` for a missing secret, or `tc secrets put NAME --stdin` for a local identity. `tc secrets get --delegation` follows the same rule, and a delegated read the node refuses (403, a capability 401, `AUTH_UNAUTHORIZED`) exits 5.
154
+
155
+ ### Asking the owner for a secret: `request`, `wait`, `manage`
156
+
157
+ `/usr/sbin/tc` (iproute2 traffic control) can shadow TinyCloud's `tc` on `PATH`, so scripts call the CLI by absolute path:
158
+
159
+ ```bash
160
+ TC_BIN="$(npm prefix --global)/bin/tc"
161
+ "$TC_BIN" --profile agent secrets request OPENAI_API_KEY --requester "agent@host" --json
162
+ # {"url":"https://secrets.tinycloud.xyz/request?owner=…&name=OPENAI_API_KEY&requester=…&v=1","names":["OPENAI_API_KEY"],"owner":"did:pkh:eip155:1:0x…"}
163
+ # send the url to the owner once, then:
164
+ "$TC_BIN" --profile agent secrets wait OPENAI_API_KEY --timeout 9m --json
165
+ "$TC_BIN" --profile agent secrets get OPENAI_API_KEY --raw
166
+ "$TC_BIN" secrets manage OPENAI_API_KEY --no-open --json # {"url":"…","opened":false}: the replace/manage page
167
+ ```
168
+
169
+ - **`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`).
170
+ - **`secrets wait <NAME...> [--scope S | --app ID] [--owner DID] [--timeout D]`** polls until every name is readable. `D` is `0`, `Ns` or `Nm`, default `5m`, at most `9m`; `--timeout 0` makes exactly one check (≤ 21 s). The whole wait is bounded by timeout + 21 s. Each check runs in a child process that is killed if it overruns.
171
+ - **`secrets manage [NAME...] [--scope S | --app ID] [--no-open]`** prints `{url, opened}` for the Secret Manager page, opening a browser unless `--no-open`.
172
+
173
+ **`wait` never signs in: it needs a stored session and doesn't accept `TC_PRIVATE_KEY`** (an explicit key exits 3). It never writes profile state, never saves a consent request, never prints a value and never prompts. After a CLI upgrade, run one normal command per profile first (for example `tc --profile agent auth caps`); otherwise the first `wait` returns `PROFILE_MIGRATION_REQUIRED` once, with that command in `error.recovery.command`.
174
+
175
+ | `wait` exit | Meaning |
176
+ |---|---|
177
+ | 0 | stdout `{status:"ready", names, scope?}` |
178
+ | 8 | `PENDING`: stdout `{status:"pending", pending, present, url}` (or `hint` instead of `url` for a local identity). Rerun `wait` without re-sending the link |
179
+ | 5 | `PERMISSION_DENIED`: `error.recovery` `{action:"request_access", names, owner, scope?}` names the denied names; `error.setup` covers names still pending |
180
+ | 3 | `AUTH_REQUIRED`: no stored session, an expired one, a 401, or `TC_PRIVATE_KEY` set. `error.recovery.command` is the persistent login to run once |
181
+ | 2 | usage, `OWNER_MISMATCH` |
182
+ | 6 | `NETWORK_ERROR` after repeated node failures; `error.progress` `{present, pending}` from the last successful check |
183
+ | 1 | `PROFILE_MIGRATION_REQUIRED` (run `error.recovery.command`, `tc --profile P auth caps`, once, then retry) or `PROBE_FAILED` |
184
+ | 130 | `CANCELLED` (SIGINT/SIGTERM); `error.progress` when a check had succeeded |
185
+
186
+ **Which identity gets a link (posture table):**
187
+
188
+ | Identity | `request` | missing secret on `get` / `wait` |
189
+ |---|---|---|
190
+ | `owner-openkey` (recorded owner) | link for the recorded owner | `error.setup` / `url` |
191
+ | `delegate-session` | link for the owner of its space (or `--owner`) | `error.setup` / `url` |
192
+ | key-only `did:key` with no recorded owner | `OWNER_DID_UNKNOWN` (3) unless `--owner` | `wait`: needs a session (3) |
193
+ | `local-owner-key`, a profile holding a `privateKey`, `--private-key`, `TC_PRIVATE_KEY` | `LOCAL_OWNER_PROFILE` (2) | hint `tc secrets put NAME --stdin`, no link |
194
+
195
+ **Error JSON fields** (stderr, JSON mode; only these keys ever appear in `error`: `code`, `message`, `hint`, `recovery`, `setup`, `progress`, `meta`, `warnings`):
196
+ - `error.setup` `{url, names, scope?, owner, action:"add_secret"}`: the canonical request link, on `NOT_FOUND` and on `wait`'s `PERMISSION_DENIED` for pending names.
197
+ - `error.recovery`: `{action:"sign_in", command?}` (a persistent login), `{action:"complete_migration", command?, operationId:"tinycloud.auth.capabilities"}`, or `{action:"request_access", names, owner, scope?}`. `command` is omitted for profile names that are not shell-safe; sign in to that profile, or run `auth caps` for it, yourself.
198
+ - `error.progress` `{present, pending}`: on `NETWORK_ERROR` and `CANCELLED` from `wait`, only after at least one successful check.
199
+
148
200
  ### Scoped secret reads for agents
149
201
 
150
202
  An agent profile gets read access to named secrets through a scoped OpenKey paste login with a manifest that names them; [AUTH.md](AUTH.md#secret-reads-for-an-agent) has the full flow. Device login cannot grant secrets.
@@ -192,6 +244,29 @@ tc --profile device replica reset [--purge]
192
244
  - Exit codes: 0 ok; 1 busy, runtime or storage error; 2 usage, `NOT_COVERED`, `SECRETS_OPT_IN_REQUIRED`; 3 grant missing; 4 key absent, deleted, content missing or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network; 7 node, protocol or integrity error (`SOURCE_CHANGED`, `SCOPE_VIOLATION`, `CONTENT_MISMATCH`); 10 storage full.
193
245
  - Store: `profiles/<profile>/replicas/<name>/` (honours `TC_HOME`): `replica.db` (SQLite WAL, `synchronous=FULL`; each feed page and its cursor commit in one transaction) and content-addressed `blobs/`, directories 0700 and files 0600. Only one process syncs a replica at a time (`REPLICA_BUSY`); a process whose sync lease expired or was taken over writes nothing more. `reset --purge` removes the replica only under a live lease on the database it opened, so it never removes a replica another process took over or recreated. A node answer `410 RESET_REQUIRED` resets the replica in place and re-bootstraps from the same source; `status.lastReset` records it. A feed from a different node is `SOURCE_CHANGED` until `tc replica reset` clears the pin. A profile deleted before or while any `tc replica` command runs ends it with `REPLICA_NOT_FOUND`; nothing is written into the profile and it is never recreated.
194
246
 
247
+ ## Read-through Replication Diagnostics
248
+
249
+ `--replication` enables the read-through adapter for one command;
250
+ `--no-replication` overrides `TC_REPLICATION=1`. It is off by default.
251
+ `--replication-debug` (or `TC_REPLICATION_DEBUG=1`) writes per-read, write
252
+ and sync details to stderr. The event log is
253
+ `profiles/<profile>/replication/events.jsonl` under `TC_HOME`; it records
254
+ operational metadata, not values.
255
+
256
+ `tc replica report [--since 24h]` renders read source/reason counts, hit ratio,
257
+ latency and staleness percentiles, sync/write outcomes, recent divergences,
258
+ replica/grant status, and pinned keys. `--json` emits the aggregate as JSON.
259
+ `--clear-pending` clears pending replica writes and prints a warning: commits
260
+ that arrive later can be observed as stale until a subsequent sync.
261
+ `auth logout` purges legacy and flag-owned replicas unless `--keep-replicas`
262
+ is used.
263
+
264
+ B-int release acceptance: test `tc auth request --grant` against a real
265
+ OpenKey test account without production writes. Approve the request, confirm
266
+ the grant is stored, start a fresh process, and verify saved prefixes are
267
+ replayed and the covered read is served locally. If explicit `kv/get` plus
268
+ `sync` is rejected, stop before release and resolve the protocol contract.
269
+
195
270
  ## Node Health
196
271
 
197
272
  ```bash
@@ -304,9 +379,9 @@ On either recovery path, an orphaned `.recover-*` claim can hold a replacement o
304
379
  - **Session key**: `did:key:z6Mk...#z6Mk...` — generated at init
305
380
  - **Owner DID**: `did:pkh:eip155:{chainId}:{address}` — after auth
306
381
 
307
- Use `--json` for machine-readable output; interactive commands can render human output. General command errors go to stderr as `{error: {code, message, hint?, meta?}}`. `meta` contains a typed HTTP `status` and, only when validated as a TinyCloud capability (and against the KV request when available), a `resource` and `requiredAction`. Inspect the structured code, not only the exit status.
382
+ Use `--json` for machine-readable output; interactive commands can render human output. General command errors go to stderr as `{error: {code, message, hint?, recovery?, setup?, progress?, meta?, warnings?}}` (`recovery`, `setup` and `progress` are described under [Secrets](#asking-the-owner-for-a-secret-request-wait-manage)). `meta` contains a typed HTTP `status` and, only when validated as a TinyCloud capability (and against the KV request when available), a `resource` and `requiredAction`. Inspect the structured code, not only the exit status.
308
383
 
309
- General storage/auth exit codes: 0 success, 1 operation error, 2 invalid input, 3 authentication required, 4 not found, 5 permission denied, 6 network error, 7 node error, 10 storage full. `tc share` uses its own: 1 profile lock timeout or sender-history retry (`PROFILE_LOCK_TIMEOUT`, `SHARE_HISTORY_RETRY`), 3 upload authority required, 4 unavailable, expired or upload failed, 5 permission denied (`PERMISSION_DENIED`) 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.
384
+ 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 (`tc secrets wait` only), 10 storage full, 130 interrupted (`CANCELLED`). `tc share` uses its own: 1 profile lock timeout or sender-history retry (`PROFILE_LOCK_TIMEOUT`, `SHARE_HISTORY_RETRY`), 3 upload authority required, 4 unavailable, expired or upload failed, 5 permission denied (`PERMISSION_DENIED`) 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.
310
385
 
311
386
  For `tc kv get|put|head|list|delete`, `tc space list|host`, `tc sql`, `tc vault`, `tc vars`, `tc duckdb`, `tc secrets list|put|delete`, `tc delegation`, and `tc 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 `tc 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. `tc share` has separate exit mappings below.
312
387