@tinycloud/cli 1.1.0-beta.9 → 1.2.0-beta.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.
- package/dist/index.js +303 -114
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +10738 -6668
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +8 -7
- package/skills/tc-cli/AUTH.md +22 -7
- package/skills/tc-cli/REFERENCE.md +55 -7
- package/skills/tc-cli/SKILL.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinycloud/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-beta.0",
|
|
4
4
|
"description": "TinyCloud CLI - manage data, delegations, and nodes from the terminal",
|
|
5
5
|
"author": "TinyCloud, Inc.",
|
|
6
6
|
"license": "EGPL",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"check:skill-pack": "node scripts/check-skill-pack.mjs"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@tinycloud/share-sdk": "1.1.0
|
|
33
|
-
"@tinycloud/share-envelope": "1.1.0
|
|
34
|
-
"@tinycloud/node-sdk": "3.
|
|
35
|
-
"@tinycloud/node-sdk-wasm": "1.7.
|
|
36
|
-
"@tinycloud/sdk-core": "3.
|
|
37
|
-
"@tinycloud/operations": "0.
|
|
32
|
+
"@tinycloud/share-sdk": "1.1.0",
|
|
33
|
+
"@tinycloud/share-envelope": "1.1.0",
|
|
34
|
+
"@tinycloud/node-sdk": "3.2.0-beta.0",
|
|
35
|
+
"@tinycloud/node-sdk-wasm": "1.7.7",
|
|
36
|
+
"@tinycloud/sdk-core": "3.2.0-beta.0",
|
|
37
|
+
"@tinycloud/operations": "0.5.0-beta.0",
|
|
38
38
|
"chalk": "^5.3.0",
|
|
39
39
|
"commander": "^14.0.0",
|
|
40
40
|
"open": "^11.0.0",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"siwe": "^3.0.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@tinycloud/replica": "0.1.0",
|
|
45
46
|
"@types/node": "^20",
|
|
46
47
|
"tsup": "^8.0.0",
|
|
47
48
|
"typescript": "^5.9.3"
|
package/skills/tc-cli/AUTH.md
CHANGED
|
@@ -20,6 +20,7 @@ TC="$(npm prefix --global)/bin/tc"
|
|
|
20
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
|
+
- An OpenKey account can hold several keys. Each key is a separate owner with its own spaces and data, and one of them is the account's **primary key**. OpenKey preselects the primary key unless the request names an owner. OpenKey reports whether the approving key is primary. The profile records that as `ownerKeyPrimary`, and `tc auth status` shows it (`null` when OpenKey did not say). A login approved by another key succeeds, but prints a warning on stderr. To use a key that is not primary on purpose, pass `--owner` with its did:pkh, also on a plain `auth login` without `--manifest`: OpenKey then preselects that key. The flag is unsigned metadata, never authority.
|
|
23
24
|
- 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.
|
|
24
25
|
- A live, unexpired session is replaced only by a scope that keeps everything it holds for the same owner and lasts at least as long: renewing the same manifest, or widening a session the owner narrowed earlier. Anything else, including an approval the owner narrowed, one that signs a caveat (a restriction such as a tenant) onto an action the live session holds unrestricted, or a renewal that would end earlier, refuses with `SESSION_IN_USE` and saves nothing. Use a new profile; pass `--replace-session` only when replacing that session is intended. The check runs on the request before consent and again on the approved scope before saving.
|
|
25
26
|
- Approval can take minutes. Before saving, the CLI re-reads the profile, key and session under the profile lock; if another login, key rotation or logout changed them meanwhile, it refuses with `PROFILE_CHANGED_DURING_LOGIN` and saves nothing. The CLI's profile, key and session writers take the same lock, and a failed commit write restores all three before the lock is released. If that restore fails too, the login fails with `PROFILE_STATE_INCONSISTENT` naming each failure; a crash between writes surfaces the same code on the next scoped login; check `tc --profile PROFILE context`, then use `--replace-session` or a new profile. The commit waits up to 45 s for the lock (a crashed holder's lock is reclaimed after 30 s), else `PROFILE_LOCK_TIMEOUT`; other commands that time out on the lock report the same code. Wait for the other tc or MCP process and retry.
|
|
@@ -88,6 +89,8 @@ tc --profile agent-secrets secrets get OPENAI_API_KEY --raw
|
|
|
88
89
|
- 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.
|
|
89
90
|
- 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.
|
|
90
91
|
- 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.
|
|
92
|
+
- At a terminal, the owner approves the missing grant in OpenKey. The request carries the missing entries, `tinycloud.capabilities/read` on the space root (the deployed OpenKey refuses a request without it; newer OpenKey releases require it only when it is requested) and raw decrypt in the `encryption` pseudo-space with the owner's address EIP-55 checksummed; a decrypt-only request is placed in the `secrets` space (or `--space`), since OpenKey needs exactly one space. The signed `capabilities/read` counts as requested; anything else beyond the request is refused (`OPENKEY_GRANT_BROADENED`). The grant is stored with its signed SIWE proof (`siweProof`) and a request binding to exactly its signed capabilities (`cli-grant:<cid>`; see below). `secrets get` and other operations (including MCP), in this process or a new one, re-verify that proof against the profile's session key and owner, and hold the grant to its binding, before using it. A stored grant that fails that check (for example one approved by an earlier release, which stored no proof) is not used by them, and a read that needed it asks for approval again. Each such grant is reported by a fixed reason code (`proof_missing`, `unbound`, `outside_request`, `host_mismatch`, `activation_rejected`, ...) and, when the stored CID is the CID of the stored authorization bytes, that CID; never by error text. In JSON mode (`--json`, or stdout not a terminal) the warnings go inside the error's `warnings` array (beside any validated `meta`) when the command fails, or as one `{"warnings": [...]}` line on stderr after the command's output when it succeeds, and stderr carries nothing else; otherwise they are one stderr line saying to approve access again. Expired grants are dropped without a warning. A stale grant needs no cleanup: it expires on its own.
|
|
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. 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. This needs a signed session: a delegate session created by `tc auth import` stores no SIWE or expiry, so its expiry is detected only when the node refuses it.
|
|
91
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.
|
|
92
95
|
|
|
93
96
|
## More spaces after login
|
|
@@ -98,20 +101,32 @@ Login covers one space. Request further spaces on the logged-in profile:
|
|
|
98
101
|
tc --profile PROFILE auth request --manifest FILE --grant --device
|
|
99
102
|
```
|
|
100
103
|
|
|
101
|
-
`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.
|
|
104
|
+
`auth request --device` defaults to a 7-day lifetime (`--expiry` to change it, at most `30d`). Without `--device`, `--grant` opens the OpenKey browser flow. Each space's request adds `tinycloud.capabilities/read` on that space's root, which OpenKey requires; a request of only raw decrypt is placed in the profile's primary space. 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.
|
|
102
105
|
|
|
103
106
|
## Imported delegations and their request bindings
|
|
104
107
|
|
|
105
|
-
Delegations stored in a profile's `additional-delegations.json`
|
|
108
|
+
Delegations stored in a profile's `additional-delegations.json` replay only into that profile's restored session. Compact-UCAN and signed-login (CACAO) grants must carry a verifiable proof and a valid `authorityRequest` binding whose `requested` capabilities contain all of their signed authority. The CLI, `tc secrets get`, and MCP tools all apply this rule; expired records and cross-user compact grants with a verified CID, signature, and signed audience matching the stored transport audience are skipped silently, while refused replay candidates produce fixed-code `STORED_GRANT_SKIPPED` warnings. A stored record whose `delegationHeader` is anything but a single string `Authorization` installs nothing.
|
|
106
109
|
|
|
107
110
|
- **Request-bound import.** `tc auth import` of an artifact with a `requestId` (from `tc auth grant`, or MCP `tinycloud_auth_import`) binds the delegation to the stored request it was checked against. Importing a stored delegation again this way replaces its record and binding.
|
|
108
|
-
- **Import without a request.** `tc auth import` of a bare portable delegation, a stored record, or an envelope with no `requestId`, addressed to this profile's session key,
|
|
109
|
-
- **
|
|
110
|
-
- **
|
|
111
|
+
- **Import without a request.** `tc auth import` of a bare portable delegation, a stored record, or an envelope with no `requestId`, addressed to this profile's session key, verifies it and binds it to exactly its own signed capabilities: request ID `unbound-import:<cid>`, with an `authorityRequestAudit` note. A signed-login grant must carry its SIWE message and signature; an unproved session-addressed grant fails with `INVALID_AUTH_IMPORT`, and one with an invalid proof fails with `DELEGATION_REJECTED` without being saved. No request limits a valid import: the profile gets the delegation's full signed authority, whatever the artifact's `permissions` say. To limit authority, import against a request instead (`tc auth request --emit`, then `tc auth grant` by the owner, then `tc auth import`). This route never replaces or weakens an existing binding for the same CID.
|
|
112
|
+
- **Proofless cross-user imports.** A portable grant with no signed proof is refused with `INVALID_AUTH_IMPORT` even when addressed to another key. It is not written to the profile or grant history and cannot produce repeated replay warnings.
|
|
113
|
+
- **One-time migration.** Records stored by earlier releases have no binding. The first command that restores the profile's own session binds each verifiable record to exactly its own signed capabilities (`migrated:<cid>`, audit note `source: "migration"`) and writes `delegation-binding-migration.json`, listing bound and unbound CIDs. Records without proof cannot be migrated. Migration is best-effort and restartable under the profile lock: an interrupted run or a busy lock defers remaining records to the next command. The runtime never installs an unbound record during that wait or afterward; it reports `unbound` (or `proof_missing` for records without proof).
|
|
111
114
|
- Older releases ignore the new fields. They do not enforce bindings, and an older `tc auth import` of a CID that is already stored replaces its record with an unbound one, which this release then refuses until it is imported again.
|
|
112
|
-
- Signed-login grants
|
|
115
|
+
- **CLI grants.** Signed-login grants from `tc auth request --grant` (OpenKey or local key) and secret-read approvals are checked against the request they answer, then stored with their `siweProof` and bound to their signed capabilities (`cli-grant:<cid>`). A local-key grant of only raw decrypt on the owner's network names no space; it is verified like any other grant. A new grant never replaces an existing bound record for the same CID. Earlier CLI releases stored signed-login grants without a proof: these are now refused with `proof_missing` on every replay path, even if a binding was later added. A grant with proof but without a valid binding reports `unbound`; cross-user delegations addressed to the profile's identity rather than its session key remain separate and are not installed by replay.
|
|
113
116
|
|
|
114
|
-
The binding is local profile data. It stops records written by other paths from granting authority. It does not stop someone who can write the profile directory, who already holds the session key and the signed bytes.
|
|
117
|
+
The binding is local profile data. It stops records written by other paths from granting authority. A request binding limits a stored grant to its signed authority, but it does not prove where the record came from: anyone who can write the profile store and holds a valid grant for this session key can add one. It does not stop someone who can write the profile directory, who already holds the session key and the signed bytes.
|
|
118
|
+
|
|
119
|
+
## Stored grants
|
|
120
|
+
|
|
121
|
+
`tc --profile PROFILE auth grants list --json` reads only the local profile; it makes no node request. Its `session` includes the recomputed session CID when available. Each row reports the stored CID (only if CID-shaped), expiry, source, binding, proof kind, effective scope or stored scope, and `local` verification separately from `node: "not_checked"`. `for_another_key` means the stored audience names another key: this session cannot install it, and its displayed scope is stored metadata rather than locally verified authority. For session-targeted grants, local verification checks compact signatures, CIDs, audience, expiry, resources and binding; for CACAO it checks the owner's signature, audience, lifetime, ReCap, CID and binding. The node still checks delegation chains and revocation, so `local: "verified"` alone is not proof of access. Neither host, request ID nor audit note appears in this output.
|
|
122
|
+
|
|
123
|
+
The `for_another_key` label requires a valid signed compact audience and CID matching the stored Authorization, plus a matching stored `delegateDID`; an edited transport audience or invalid proof is refused with a reason instead. A double `Bearer` prefix is refused; a single prefix is removed before activation.
|
|
124
|
+
|
|
125
|
+
`tc --profile PROFILE auth grants remove <cid>` removes all local rows with that exact CID, without contacting the node; delegates may do this. An owner can add `--revoke --yes` to discover the verified local node (unless a host was explicitly selected), revoke there, then remove local rows only after successful revocation. Delegate-session profiles cannot use `--revoke`. If revocation succeeds but local removal fails, `GRANT_REVOKED_LOCAL_REMOVE_FAILED` includes a hint to retry `auth grants remove <cid>` **without** `--revoke`. `tc delegation revoke <cid>` also revokes a CID directly when its target is known. OpenKey cannot sign raw `urn:cid` ReCap authority today: owner OpenKey revocation falls back to command-scoped `delegation/revoke` on an owner-signed CID-bound space, and fails immediately when no verified fallback space exists. The JavaScript SDK cannot currently issue a delegatee self-revocation, although the node accepts one. Local removal is not revocation.
|
|
126
|
+
|
|
127
|
+
If another process already removed the row after the node revoke, `--revoke` succeeds with `removed: 0`. Other post-revoke local removal errors include the underlying `error.meta.causeCode` (for example `PROFILE_LOCK_TIMEOUT`) so automation can distinguish lock contention from a node failure.
|
|
128
|
+
|
|
129
|
+
To replace an old grant without a proof, an owner can approve again with `tc auth request --grant [--device]`, then remove the old CID. A delegate can emit a request with `tc auth request --emit`, have the owner run `tc auth grant`, import the signed result with `tc auth import`, then remove the old CID. If the original artifact still includes `siweProof`, importing it again is also possible.
|
|
115
130
|
|
|
116
131
|
## Unscoped login and sign-out
|
|
117
132
|
|
|
@@ -5,7 +5,7 @@ This release has **Commander coverage tracked, not complete parity**:
|
|
|
5
5
|
|
|
6
6
|
- 1 migrated registration(s).
|
|
7
7
|
- 1 partially migrated registration(s).
|
|
8
|
-
-
|
|
8
|
+
- 125 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).
|
|
@@ -42,7 +42,7 @@ tc --profile publisher context --json
|
|
|
42
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
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. Also names the secrets owner for a manifest's `secrets` when the profile has no recorded owner |
|
|
45
|
+
| `--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
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
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 |
|
|
@@ -80,7 +80,23 @@ tc delegation revoke <cid>
|
|
|
80
80
|
|
|
81
81
|
Actions are auto-prefixed with `tinycloud.` if not already.
|
|
82
82
|
|
|
83
|
-
`tc auth import` stores delegations in
|
|
83
|
+
`tc 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.
|
|
84
|
+
|
|
85
|
+
### Stored grants
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
tc --profile PROFILE auth grants list --json
|
|
89
|
+
tc --profile PROFILE auth grants remove <cid>
|
|
90
|
+
tc --profile PROFILE auth grants remove <cid> --revoke --yes
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`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.
|
|
94
|
+
|
|
95
|
+
`for_another_key` is decided from a verified signed compact audience, CID, and matching transport DID, not an unsigned `delegateDID` alone. Tampered records retain their refusal reason, and proofless records retain `proof_missing`.
|
|
96
|
+
|
|
97
|
+
Local `remove` deletes every row matching that exact CID, and is available to delegates. `--revoke` alone is owner-only: it uses normal verified local-node discovery unless a host is explicit, revokes on the node, and removes the rows only after success. If the local removal then fails, `GRANT_REVOKED_LOCAL_REMOVE_FAILED` directs a local-only retry without `--revoke`. OpenKey cannot sign raw `urn:cid` authority on the deployed version; owner OpenKey revocation uses a verified CID-bound owner-space fallback, or immediately fails `RAW_RECAP_RESOURCE_UNSUPPORTED`. Local removal does not revoke the delegation; delegated sessions cannot use `--revoke` because this SDK cannot issue a delegatee self-revocation.
|
|
98
|
+
|
|
99
|
+
A concurrent local removal after revocation reports `removed: 0` as success. Other local cleanup failures include `error.meta.causeCode` (for example `PROFILE_LOCK_TIMEOUT`).
|
|
84
100
|
|
|
85
101
|
## Spaces
|
|
86
102
|
|
|
@@ -144,7 +160,37 @@ tc --profile agent secrets get OPENAI_API_KEY --raw
|
|
|
144
160
|
|
|
145
161
|
`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.
|
|
146
162
|
|
|
147
|
-
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.
|
|
163
|
+
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. On any profile kind, a stored signed session that has expired or no longer verifies fails with `AUTH_REQUIRED` (exit 3) and a sign-in hint before any node request, never as a retryable `NODE_ERROR`; operations and MCP report a non-retryable `AUTH_REQUIRED`. Any service result coded `AUTH_REQUIRED` or `PERMISSION_DENIED` without an HTTP status now exits 3 or 5 instead of 1 in commands using `cliErrorFromService` (account, delegation, duckdb, kv, secrets list/put/delete, space, sql, vars and vault); for example, `tc secrets list` without a grant returns the NodeSecretsService's status-less `PERMISSION_DENIED` with exit 5. `tc secrets get` and `tc auth import` additionally supply the profile kind's sign-in hint for `AUTH_REQUIRED`. A delegate session bootstrapped by `tc auth import` stores no SIWE or expiry, so its expiry is found only when the node refuses it. At a terminal, the owner approves a missing grant in OpenKey: the request adds `tinycloud.capabilities/read` on the space root and keeps raw decrypt in the `encryption` pseudo-space, and a decrypt-only request is placed in the secrets space. The grant is stored with its signed SIWE proof and bound to exactly its signed capabilities, and `secrets get` (like every operation, including MCP) re-verifies it against the session key and owner, and holds it to that binding, in the same process or a new one; a stored grant that fails (such as one from an earlier release, stored without a proof) is skipped and reported as a `STORED_GRANT_SKIPPED` warning with a fixed reason code and its CID when that CID is the CID of the stored bytes: inside the JSON error's `warnings` (including a failed `-o` write), as one `{"warnings": [...]}` stderr line after a successful read's output, or as one human-readable stderr line. `--json` selects the JSON forms even with stdout on a terminal, and suppresses progress spinners. Expired grants are dropped silently. Operations and MCP results carry the same `warnings` array. A JSON error can carry both `meta` (validated authorization fields) and `warnings`. A canonical `secrets get` that still lacks authority fails with `PERMISSION_DENIED` (exit 5). 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.
|
|
164
|
+
|
|
165
|
+
## Local Replicas
|
|
166
|
+
|
|
167
|
+
`tc 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 `tc replica` command fails with `RUNTIME_UNSUPPORTED` (exit 1) and other commands are unaffected.
|
|
168
|
+
|
|
169
|
+
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; `tc auth grant` acquires it for that grant only:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# device
|
|
173
|
+
tc --profile device auth request --cap tinycloud.kv:SPACE:notes/:get,list,metadata,sync --expiry 30d --emit req.json
|
|
174
|
+
# owner
|
|
175
|
+
tc --profile owner auth grant req.json > grant.json
|
|
176
|
+
# device
|
|
177
|
+
tc --profile device auth import grant.json
|
|
178
|
+
tc --profile device replica sync --prefix notes/ # first sync creates the replica "notes"
|
|
179
|
+
tc --profile device replica get notes/todo.md --raw # offline
|
|
180
|
+
tc --profile device replica list notes/ --json
|
|
181
|
+
tc --profile device replica status
|
|
182
|
+
tc --profile device replica sync # catch up: updates and deletes
|
|
183
|
+
tc --profile device replica reset [--purge]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- 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).
|
|
187
|
+
- The grant must be a compact UCAN issued to this device's key; SIWE/CACAO sessions cannot back a replica. A newer covering grant is installed as pending and takes over after the node accepts a sync under it. If the node refuses it, the current grant keeps serving and `status.device.pendingDelegationError` says why; a revoked replacement is discarded without touching the replica.
|
|
188
|
+
- Reads are allowed only inside the authority window the node attested at the last successful sync (the earliest expiry across the whole delegation chain, which can be before the grant's own `exp`), checked again when each page commits. After expiry, reads fail with `GRANT_EXPIRED` (exit 5) and no new sync starts. An authority bound that is not an RFC 3339 timestamp is `PROTOCOL_ERROR` (exit 7) and changes nothing. A revocation is learned on the next online sync, only from the node's typed revocation answer (never from text such as a key name): the replica purges its entries and content and later reads fail with `GRANT_REVOKED`, including after restart. If a content file cannot be removed, `status.purgePending` is `true` and every later `tc replica` command retries; reads stay blocked meanwhile. A revoked replica stays blocked; remove it with `tc replica reset --purge` and sync again under a new grant.
|
|
189
|
+
- `--retention-grant <cid>` presents an owner-issued `tinycloud.kv/retain` grant on the prefix (never invoked; nothing implies it). Reads then continue after the sync grant expires, marked `authority: "expired"`, until the node-attested `retainUntil`. A revocation of the retain grant a sync presented drops that grant (one set meanwhile with `--retention-grant` is kept): once the sync grant expires, reads fail with `GRANT_REVOKED`. Once the sync grant has expired the device cannot learn that the retain grant was revoked.
|
|
190
|
+
- Syncing the `secrets` space or a prefix in the `vault` namespace needs `--allow-secrets`.
|
|
191
|
+
- `get` statuses: present; `KEY_DELETED`, `KEY_ABSENT` (only once the first sync completed), `CONTENT_MISSING` (known key whose bytes are not local yet; the next sync repairs it) and `COVERAGE_INCOMPLETE` exit 4; `NOT_COVERED` (outside the prefix) exits 2. `-o FILE` writes atomically with mode 0600; stored content is re-hashed on every read unless `--no-verify`.
|
|
192
|
+
- 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
|
+
- 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.
|
|
148
194
|
|
|
149
195
|
## Node Health
|
|
150
196
|
|
|
@@ -194,7 +240,7 @@ The fragment after `#` is the read authority. It never reaches a server in a que
|
|
|
194
240
|
|
|
195
241
|
**Concurrent history changes.** A Share command resolves its profile once for authentication and every sender-history operation, including retries; a later default-profile change cannot redirect a share link into another profile. If a write waits more than about 2 seconds to acquire the profile lock, the CLI writes one waiting notice to stderr naming that profile. A profile removed after an earlier history read or during an operation, or repeated key/salt changes, returns `SHARE_HISTORY_RETRY` (exit 1) with a retry hint; a profile missing from the outset still returns `PROFILE_NOT_FOUND` with the setup hint. If a history retry follows a revoke, check the node state before trying again: revocation might already have succeeded even though sender history could not record it.
|
|
196
242
|
|
|
197
|
-
Share publication without `--expires` requests a seven-day lifetime. For a session-only profile, the CLI clamps that request to the verified SIWE session expiry, prints a notice to stderr, and reports `"expiryClamped": true` in JSON. Explicit lifetimes beyond the session end, or with less than 60 seconds left after second-precision rounding, fail with `SESSION_LIFETIME_EXCEEDED`. Expired/invalid restored sessions and missing owner authority return `AUTH_REQUIRED`; rejected KV upload or delegation scopes return `PERMISSION_DENIED`. A session-only profile whose authority for an anyone-with-link share carries signed restrictions (caveats) is refused with `PERMISSION_DENIED` before anything is stored, because the link's delegation cannot carry the caveats; approve Share publishing without restrictions on a new profile (`tc init --name publisher --key-only && tc --profile publisher enable share`).
|
|
243
|
+
Share publication without `--expires` requests a seven-day lifetime. For a session-only profile, the CLI clamps that request to the verified SIWE session expiry, prints a notice to stderr, and reports `"expiryClamped": true` in JSON. Explicit lifetimes beyond the session end, or with less than 60 seconds left after second-precision rounding, fail with `SESSION_LIFETIME_EXCEEDED`. Expired/invalid restored sessions and missing owner authority return `AUTH_REQUIRED`; rejected KV upload or delegation scopes return `PERMISSION_DENIED`. A session-only profile whose authority for an anyone-with-link share carries signed restrictions (caveats) is refused with `PERMISSION_DENIED` before anything is stored, because the link's delegation cannot carry the caveats; approve Share publishing without restrictions on a new profile (`tc init --name publisher --key-only && tc --profile publisher enable share`). Full TinyCloud storage returns the general storage-full error (`STORAGE_QUOTA_EXCEEDED` or `STORAGE_LIMIT_REACHED`, exit 10), and any other failed upload returns `UPLOAD_FAILED`; nothing is shared in either case. A filename that has characters other than `A-Z a-z 0-9 . _ -`, starts with anything but a letter or digit, contains `..`, or is longer than 128 characters is stored under a readable URI-safe name. It keeps its extension when that is 1-16 ASCII letters or digits (`Q3 plan (draft).md` is stored as `Q3-plan-draft.md`, `.env` as `share.env`); otherwise its dots become dashes, so it never gains one. Bearer links show the stored name and `receive` writes it, while addressed links keep the original. JSON publish output includes `expiryClamped: false` when no clamp was needed.
|
|
198
244
|
|
|
199
245
|
### Share Publish Options
|
|
200
246
|
|
|
@@ -220,7 +266,7 @@ Share publication without `--expires` requests a seven-day lifetime. For a sessi
|
|
|
220
266
|
| File | `tc kv put key --file ./data.txt` |
|
|
221
267
|
| Stdin | `echo "data" \| tc kv put key --stdin` |
|
|
222
268
|
|
|
223
|
-
KV keys cannot contain spaces or control characters: the SDK sends keys unescaped in the node resource URI, so `get`, `put`, `head`, `delete` and `list --prefix` refuse them with `USAGE_ERROR` (exit 2) before authenticating. A full storage
|
|
269
|
+
KV keys cannot contain spaces or control characters: the SDK sends keys unescaped in the node resource URI, so `get`, `put`, `head`, `delete` and `list --prefix` refuse them with `USAGE_ERROR` (exit 2) before authenticating. A `put` on full storage returns the general storage-full error (exit 10).
|
|
224
270
|
|
|
225
271
|
## Profile Directory Structure
|
|
226
272
|
|
|
@@ -260,6 +306,8 @@ On either recovery path, an orphaned `.recover-*` claim can hold a replacement o
|
|
|
260
306
|
|
|
261
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.
|
|
262
308
|
|
|
263
|
-
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. `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
|
|
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.
|
|
264
310
|
|
|
265
311
|
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
|
+
|
|
313
|
+
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 (`tc 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/billing. Retrying cannot succeed until the owner frees up space or upgrades.
|
package/skills/tc-cli/SKILL.md
CHANGED
|
@@ -44,6 +44,8 @@ tc --profile publisher auth login --device --manifest builtin:share-publishing
|
|
|
44
44
|
|
|
45
45
|
SQL commands need a profile whose session the owner granted through browser login; a device-login session has no SQL authority.
|
|
46
46
|
|
|
47
|
+
To read a KV prefix while the host is unreachable, keep a local replica: `tc replica sync --prefix PREFIX` under a device grant with `sync`, then `tc replica get|list` read only local storage. See [REFERENCE.md](REFERENCE.md#local-replicas) for the grant flow, expiry and exit codes.
|
|
48
|
+
|
|
47
49
|
```bash
|
|
48
50
|
tc --profile PROFILE kv get KEY --space SPACE --json
|
|
49
51
|
tc --profile PROFILE kv get KEY --space SPACE --raw -o ./resource.txt
|