@tinycloud/cli 1.0.0-beta.22 → 1.0.0-beta.23

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.0.0-beta.22",
3
+ "version": "1.0.0-beta.23",
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.0.0-beta.9",
33
33
  "@tinycloud/share-envelope": "1.0.0-beta.5",
34
- "@tinycloud/node-sdk": "3.0.0-beta.19",
34
+ "@tinycloud/node-sdk": "3.0.0-beta.20",
35
35
  "@tinycloud/node-sdk-wasm": "1.7.6",
36
36
  "@tinycloud/sdk-core": "3.0.0-beta.19",
37
- "@tinycloud/operations": "0.3.3-beta.17",
37
+ "@tinycloud/operations": "0.3.3-beta.18",
38
38
  "chalk": "^5.3.0",
39
39
  "commander": "^14.0.0",
40
40
  "open": "^11.0.0",
@@ -17,7 +17,7 @@ TC="$(npm prefix --global)/bin/tc"
17
17
  ```
18
18
 
19
19
  - `--manifest FILE` (required with `--device`) is an app manifest (`app_id`, `space`, `permissions`) given as a file path or `base64:<json>`. `builtin:share-publishing` is the built-in manifest for `tc share publish`; [REFERENCE.md](REFERENCE.md#context-and-login) lists exactly what it requests, and the owner's consent page shows every capability. One space per login.
20
- - Device login is for KV-scoped manifests, and every device manifest must also request `{ "service": "tinycloud.capabilities", "path": "", "skipPrefix": true, "actions": ["read"] }` in the same space: OpenKey requires it to sign any delegation, shows it as required, and rejects a request without it with `SCOPE_REJECTED`. `builtin:share-publishing` already includes it. OpenKey also refuses `tinycloud.sql` and any other ability its device policy excludes with `SCOPE_REJECTED`; request SQL through [browser login with a manifest](#browser-login-with-a-manifest) on a machine with a browser.
20
+ - Device login is for KV-scoped manifests. Every scoped login (device, browser, paste) adds `tinycloud.capabilities/read` on the space root (`""`) when the manifest lacks it: OpenKey requires it to sign any delegation and shows it as required. The CLI refuses `tinycloud.encryption` and the `secrets` space before a device request (`DEVICE_AUTH_UNSUPPORTED_SCOPE`); OpenKey also refuses `tinycloud.sql` and other excluded abilities with `SCOPE_REJECTED`. Request those through [browser or paste login with a manifest](#browser-login-with-a-manifest). Device login cannot grant secret reads.
21
21
  - `--expiry` sets the session lifetime as a duration counted from approval (`1h`, `7d`, or milliseconds; at least `1m`; device login at most `30d`, default `30d`). OpenKey signs approval time plus the lifetime, so ISO dates are refused up front (`INVALID_EXPIRY`): an absolute deadline cannot survive an approval that takes longer than the 30 s clock-skew allowance. A signed session that would outlive the requested lifetime is refused.
22
22
  - `--owner did:pkh:eip155:CHAIN:ADDRESS` refuses an approval by any other identity. Every login on a profile that already recorded an owner DID is held to that owner automatically, whatever wrote it (`tc init`, an earlier login, or `profile.json` with any posture), and `--owner` must agree with it. Only a local-owner-key profile is not pinned. Owner addresses compare case-insensitively; chain id and space name compare exactly. The owner, permissions and expiry recorded for a session only ever come from a verified signed proof; callback-supplied values are dropped.
23
23
  - Scoped and device logins refuse a profile that holds a local owner key (`LOCAL_OWNER_PROFILE`): a local-owner-key posture, `authMethod: "local"`, or a stored private key, whatever the recorded posture. Use a separate profile.
@@ -61,7 +61,33 @@ On a machine with a browser, request the same manifest through the OpenKey brows
61
61
  tc --profile PROFILE auth login --method openkey --manifest /absolute/path/to/manifest.json --expiry 7d
62
62
  ```
63
63
 
64
- `--paste` prints a URL and waits for a return code in the terminal; `--no-popup` prints the callback URL without opening a browser. The same signed-proof, owner, live-session, local-owner and commit-time checks apply. Older OpenKey responses without a signed proof are rejected for scoped login; do not drop the manifest to bypass that failure. `--expiry` reaches OpenKey as `<seconds>s`; dates and lifetimes under a minute are refused before the browser opens.
64
+ `--paste` prints the approval URL on stderr and reads the owner's return code from stdin: a terminal paste, or piped text. End the code with a newline; a final line without one is also accepted. If stdin ends with no code, the command fails with `PASTE_CODE_MISSING` (exit 3), names the approval URL, and saves nothing. `--no-popup` prints the callback URL without opening a browser. The same signed-proof, owner, live-session, local-owner and commit-time checks apply, and the JSON result lists `permissions` (approved) and `declined` (unchecked). Older OpenKey responses without a signed proof are rejected for scoped login; do not drop the manifest to bypass that failure. `--expiry` reaches OpenKey as `<seconds>s`; dates and lifetimes under a minute are refused before the browser opens.
65
+
66
+ Browser approval uses stderr for the URL and accepts a code on terminal stdin even when stdout is redirected to a file or pipe. If neither stdin nor stderr is a terminal, use `--paste` instead of waiting for a browser. `--device --manifest` is available for eligible KV scopes, but cannot approve a secrets or encryption manifest.
67
+
68
+ ## Secret reads for an agent
69
+
70
+ An agent reads named secrets through its own profile. The owner approves a scoped paste login whose manifest names the secrets. Device login cannot grant secrets.
71
+
72
+ ```bash
73
+ cat > agent-secrets.json <<'EOF'
74
+ { "app_id": "my.agent", "name": "My agent", "space": "secrets", "secrets": { "OPENAI_API_KEY": true } }
75
+ EOF
76
+ tc init --name agent-secrets --key-only
77
+ LOGIN=(tc --profile agent-secrets auth login --method openkey --paste --manifest agent-secrets.json
78
+ --owner did:pkh:eip155:1:0xOWNER_ADDRESS --expiry 7d)
79
+ "${LOGIN[@]}" < /dev/null # exits 3 with PASTE_CODE_MISSING; its stderr names the approval URL
80
+ # Send the owner that URL. After they approve and send back the code:
81
+ printf '%s\n' "$CODE" | "${LOGIN[@]}"
82
+ tc --profile agent-secrets secrets get OPENAI_API_KEY --raw
83
+ ```
84
+
85
+ - The approval URL depends only on the profile's session key and the request, so the same command run again accepts the owner's code from stdin. Write the code followed by a newline; a final line without one is also accepted. Nothing is saved until a code verifies.
86
+ - `secrets: { NAME: true }` requests `tinycloud.kv/get` on `vault/secrets/NAME` in the `secrets` space, `tinycloud.capabilities/read` on that space, and decrypt on the owner's default secrets network. The decrypt entry is a raw network resource, `{ "service": "tinycloud.encryption", "space": "encryption", "path": "urn:tinycloud:encryption:<ownerDid>:default", "actions": ["tinycloud.encryption/decrypt"] }`; it does not count as a second space.
87
+ - The network belongs to the owner, so a profile with no recorded owner needs `--owner`, the did:pkh of the account that will approve (OpenKey refuses a network owned by anyone else). Without it the login fails with `OWNER_DID_UNKNOWN`; it never falls back to the profile's session `did:key`. A recorded or explicit owner address is EIP-55 checksummed in the network URN.
88
+ - If the owner unchecks decrypt, the login still succeeds and lists the decrypt entry in `declined`; reads of that secret then fail. Older OpenKey deployments sign decrypt inside the owner's secrets space rather than as a top-level network grant. That does not grant raw decrypt: it is listed in `declined` with an old-deployment warning and omitted from saved approved permissions. A later `auth request --grant` or secret-read escalation refuses that old nested proof before activating or storing a delegation, with `OPENKEY_GRANT_BROADENED`; obtain a new approval from a deployment that signs the raw network grant.
89
+ - A secrets command (`get`, `list`, `put`, `delete`) on an OpenKey profile without a grant for the name fails fast only when neither stdin nor stderr is a terminal: `PERMISSION_DENIED` (exit 5) with a hint naming this scoped login. Redirecting stdout (`|`, `>`, or command substitution) does not prevent a person at a terminal from seeing the browser approval prompt. If the session is missing or expired, a headless secrets command, including the canonical `secrets get` operation, instead fails with `AUTH_REQUIRED` (exit 3) and the scoped paste-login hint before invoking the node or starting an unscoped browser refresh.
90
+ - `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.
65
91
 
66
92
  ## More spaces after login
67
93
 
@@ -71,7 +97,7 @@ Login covers one space. Request further spaces on the logged-in profile:
71
97
  tc --profile PROFILE auth request --manifest FILE --grant --device
72
98
  ```
73
99
 
74
- `auth request --device` defaults to a 7-day lifetime (`--expiry` to change it, at most `30d`). Without `--device`, `--grant` opens the OpenKey browser flow.
100
+ `auth request --device` defaults to a 7-day lifetime (`--expiry` to change it, at most `30d`). Without `--device`, `--grant` opens the OpenKey browser flow. Browser grants require a signed proof for the profile's session key and owner. Only the permissions and expiry in that proof can be activated and stored; an older space-nested decrypt proof, missing or foreign-owner proof, broader grant, wrong space, or too-long expiry is refused without storing a delegation.
75
101
 
76
102
  ## Unscoped login and sign-out
77
103
 
@@ -39,13 +39,13 @@ tc --profile publisher context --json
39
39
 
40
40
  | `auth login` flag | Description |
41
41
  |------|-------------|
42
- | `--device` | Approve on another device (phone) through OpenKey device authorization. Requires `--manifest`: KV abilities plus `tinycloud.capabilities/read` on path `""` in the same space (OpenKey requires it to sign; no `tinycloud.sql`). Prints `Approve on your phone: URL (code XXXX-XXXX)` to stderr and waits for the whole approval window |
43
- | `--manifest <file>` | Request only this manifest's permissions (one space). File path, `base64:<json>`, or `builtin:share-publishing` |
42
+ | `--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 |
43
+ | `--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` |
44
44
  | `--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
- | `--owner <did>` | Refuse approval by any identity other than this `did:pkh`; must agree with an owner the profile already recorded |
45
+ | `--owner <did>` | Refuse approval by any identity other than this `did:pkh`; must agree with an owner the profile already recorded. Also names the secrets owner for a manifest's `secrets` when the profile has no recorded owner |
46
46
  | `--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 |
47
47
  | `--method openkey\|local` | Browser OpenKey flow or local Ethereum key |
48
- | `--paste`, `--no-popup` | Browser flow without a local callback / without opening a browser |
48
+ | `--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
49
 
50
50
  Device login JSON lists the signed, approved `permissions`, owner-unchecked `declined` capabilities, `ownerDid`, `spaceId` and `expiresAt`. `tc auth request --manifest FILE --grant --device` adds another space to a logged-in profile the same way (default lifetime 7d). `tc 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).
51
51
 
@@ -122,10 +122,28 @@ tc secrets network grant did:pkh:eip155:1:0xRecipient...
122
122
 
123
123
  tc secrets put ANTHROPIC_API_KEY "sk-..."
124
124
  tc secrets get ANTHROPIC_API_KEY
125
+ tc secrets get ANTHROPIC_API_KEY -o key.txt # atomic owner-only (0600) replacement
125
126
  tc secrets list
126
127
  tc secrets delete ANTHROPIC_API_KEY
127
128
  ```
128
129
 
130
+ ### Scoped secret reads for agents
131
+
132
+ 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.
133
+
134
+ ```bash
135
+ echo '{ "app_id": "my.agent", "space": "secrets", "secrets": { "OPENAI_API_KEY": true } }' > agent.json
136
+ tc init --name agent --key-only
137
+ tc --profile agent auth login --method openkey --paste --manifest agent.json --owner did:pkh:eip155:1:0xOWNER < /dev/null
138
+ # exits 3 (PASTE_CODE_MISSING) and names the approval URL; the owner approves and returns a code
139
+ printf '%s\n' "$CODE" | tc --profile agent auth login --method openkey --paste --manifest agent.json --owner did:pkh:eip155:1:0xOWNER
140
+ tc --profile agent secrets get OPENAI_API_KEY --raw
141
+ ```
142
+
143
+ `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.
144
+
145
+ When neither stdin nor stderr is a terminal, `secrets get|list|put|delete` on an OpenKey profile that lacks the grant fails with `PERMISSION_DENIED` (exit 5) and a scoped paste-login hint. Missing or expired sessions instead fail with `AUTH_REQUIRED` (exit 3) before a canonical read invokes the node or starts an unscoped browser refresh. Redirected stdout alone does not disable an owner's browser approval; its URL appears on stderr and terminal stdin accepts a code. `secrets get -o` validates the destination and existing directory before any read or decrypt, then syncs a fresh 0600 inode and atomically replaces an existing regular file. Directory sync is best-effort after replacement and cannot turn a completed write into a reported failure. It refuses symlinks, directories, devices and absent parent directories (`INVALID_ARGUMENT`); an unreplaceable filesystem target fails with a sanitized error naming the destination, without a non-atomic fallback. A node that refuses decrypt (HTTP 401/403) is reported as missing authority on the invoked network, not as an undecryptable secret.
146
+
129
147
  ## Node Health
130
148
 
131
149
  ```bash