@topolo/cli 0.13.13 → 0.13.16

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/README.md CHANGED
@@ -74,27 +74,50 @@ OAuth errors, and never extends the original device-code expiry.
74
74
  ### Contexts
75
75
 
76
76
  A user can act in their personal space or any organization they belong to. Each
77
- context has its own credential; the CLI (and the MCP) run in the active one.
77
+ context has its own credential. Name the one to act as with `--context <key>` or
78
+ `TOPOLO_CONTEXT`; without a pin the CLI (and the MCP) use the stored context only
79
+ while it is the only one signed in for the environment.
78
80
  These credentials are independent from the shared web preference: switching a
79
81
  CLI/MCP context never moves the browser session, and a web switch never changes
80
82
  an already-issued CLI/MCP token or its refresh chain.
81
83
 
82
84
  ```bash
83
- topolo context ls # personal + your orgs, and which is active
84
- topolo context use <organization|personal> # switch (validated + a fresh token minted server-side)
85
+ topolo context ls # personal + your orgs, and which is the stored default
86
+ topolo context use <organization|personal> # set the stored default (validated + a fresh token minted server-side)
87
+ topolo --context org:org_acme whoami # pin one command to one context
85
88
  ```
86
89
 
90
+ **No implicit default context.** Once several contexts are signed in for an
91
+ environment, every account command without `--context`/`TOPOLO_CONTEXT` refuses
92
+ with "the context is no longer safe to assume", exactly like the
93
+ multi-environment `--env` guard. `topolo context use` stays available for anyone
94
+ who wants a stored default, but nothing relies on it implicitly.
95
+
96
+ `topolo auth login` stores the approved sign-in under its own context key and
97
+ never moves the stored default (`--use` opts in; the environment's first sign-in
98
+ becomes the default). It prints which user and organization were approved. To
99
+ re-authorise one context, name it:
100
+
101
+ ```bash
102
+ topolo --env production --context org:org_acme auth login
103
+ ```
104
+
105
+ If the browser approves a different user or organization, the CLI warns that
106
+ `org:org_acme` was not renewed and exits 1.
107
+
87
108
  ### Wire into agent hosts
88
109
 
89
110
  ```bash
90
- topolo setup # detects installed local hosts and registers the exact bundled MCP
111
+ topolo setup # registers the exact bundled MCP, pinned to the only signed-in context
91
112
  topolo setup --all # configure every supported local host
92
113
  topolo doctor # verifies auth, handshake, tool discovery, and identity
93
114
  ```
94
115
 
95
116
  The MCP reads the same credential `topolo auth login` writes — no API key to paste.
96
- For concurrent agents pinned to different organizations, use an exact context
97
- key returned by `topolo context ls --json`:
117
+ Every registration setup writes pins `TOPOLO_ENV` and `TOPOLO_CONTEXT`. Without
118
+ `--context`, setup pins the only signed-in context, and refuses when there are
119
+ none or several. For each additional organization, use an exact context key
120
+ returned by `topolo context ls --json`:
98
121
 
99
122
  ```bash
100
123
  topolo setup --name topolo-acme --context org:org_acme --env production
@@ -114,11 +137,96 @@ The local installer does not edit JetBrains AI Assistant settings or web-only
114
137
  connectors because those are managed through their own UI or remote connector
115
138
  configuration. Use the generated native MCP entry as the import source there.
116
139
 
140
+ ### Shared local MCP hub (opt-in)
141
+
142
+ Every stdio registration starts its own Node process in every agent thread, so
143
+ N threads x M pinned identities means N x M MCP processes. The shared hub runs
144
+ **one** loopback process that serves every identity at
145
+ `http://127.0.0.1:<port>/mcp/<name>`; Claude Code and Codex connect to it by URL.
146
+ Plain `topolo setup` keeps the stdio path; `--shared` is opt-in.
147
+
148
+ ```bash
149
+ topolo --env production setup --shared --name topolo-acme --context org:org_acme
150
+ topolo --env staging setup --shared --name topolo-acme-stg --context org:org_acme
151
+ topolo setup --shared --import-stdio # move every existing stdio topolo/topolo-* entry onto the hub
152
+ topolo doctor --shared # hub, token, unit, registrations, then MCP whoami per identity
153
+ topolo mcp serve [--port 7337] # what the keep-alive unit runs; foreground for debugging
154
+ ```
155
+
156
+ `setup --shared`:
157
+
158
+ 1. Adds the identity (`{name, env, context}`, exactly what a stdio entry pins
159
+ with `TOPOLO_ENV` + `TOPOLO_CONTEXT`; both are always pinned, and without
160
+ `--context` the only signed-in context is pinned) to `mcp-hub.json` next to
161
+ the credential store (`~/.config/topolo/mcp-hub.json`, or the directory of
162
+ `TOPOLO_CONFIG_PATH`). `--import-stdio` converts existing stdio entries and
163
+ skips ones the hub cannot serve (entries with no `TOPOLO_CONTEXT` pin,
164
+ private `TOPOLO_CONFIG_PATH` profiles, agent installations, invalid or
165
+ conflicting definitions). Add a skipped unpinned entry back with
166
+ `--name <name> --context <context>`.
167
+ 2. Creates `mcp-hub.token` (256-bit, mode 0600) once and reuses it.
168
+ 3. Registers **every** hub identity with Claude Code and Codex at user scope,
169
+ replacing same-name stdio entries. `--project` is refused so the token can
170
+ never land in a repository file. Other hosts stay on stdio.
171
+ 4. Installs a keep-alive unit running the exact Node binary and CLI entry that
172
+ ran setup (`--no-service` to skip): `~/Library/LaunchAgents/io.topolo.mcp-hub.plist`
173
+ (launchd, `KeepAlive`, logs to `~/Library/Logs/topolo-mcp-hub.log`) or
174
+ `~/.config/systemd/user/topolo-mcp-hub.service` (`Restart=always`). Only
175
+ `TOPOLO_CONFIG_PATH`/`XDG_CONFIG_HOME` are passed through; PATH and secrets
176
+ are not. With `--agent-home` the unit is written there but not loaded.
177
+
178
+ Exact entries written (token shown as `<token>`):
179
+
180
+ ```jsonc
181
+ // ~/.claude.json (written 0600)
182
+ "mcpServers": {
183
+ "topolo-acme": {
184
+ "type": "http",
185
+ "url": "http://127.0.0.1:7337/mcp/topolo-acme",
186
+ "headers": { "Authorization": "Bearer <token>", "X-Topolo-Agent": "claude-code" }
187
+ }
188
+ }
189
+ ```
190
+
191
+ ```toml
192
+ # ~/.codex/config.toml (written 0600)
193
+ [mcp_servers.topolo-acme]
194
+ url = "http://127.0.0.1:7337/mcp/topolo-acme"
195
+ http_headers = { "Authorization" = "Bearer <token>", "X-Topolo-Agent" = "codex" }
196
+ ```
197
+
198
+ Codex streamable-HTTP servers accept `url`, `bearer_token_env_var`,
199
+ `http_headers` and `env_http_headers`; an inline `bearer_token` is rejected.
200
+ `http_headers` is used so the hub works when Codex is launched from a GUI with
201
+ no shell environment. To keep the token out of `config.toml`, replace the
202
+ `Authorization` header with `bearer_token_env_var = "TOPOLO_MCP_HUB_TOKEN"` and
203
+ export that variable (contents of `mcp-hub.token`) into Codex's environment.
204
+
205
+ Hub behaviour and security:
206
+
207
+ - Binds `127.0.0.1` only; there is no host option.
208
+ - Refuses any request whose `Host` is not `127.0.0.1`, `localhost` or `[::1]`
209
+ on the hub port, or that carries a non-loopback `Origin` (DNS rebinding and
210
+ browser cross-origin calls), before checking the bearer token
211
+ (constant-time). Only then is `/mcp/<name>` routed; `GET /health` lists
212
+ identity names.
213
+ - Each identity has its own handler (`@topolo/mcp/local`) that resolves and
214
+ refreshes its credential from the local store exactly as a pinned stdio MCP
215
+ does. Process-wide `TOPOLO_API_KEY`/`TOPOLO_ACCESS_TOKEN`/`TOPOLO_CONTEXT`,
216
+ request headers and other identities are never consulted; the hub token is
217
+ stripped before the request reaches an identity.
218
+ - Stateless Streamable HTTP (no session ids), so restarts never strand a client.
219
+ - `mcp-hub.json` is re-read when it changes; adding an identity needs no restart.
220
+ - Logs carry method, path, status and duration only.
221
+ - Rotating the token: delete `mcp-hub.token`, rerun `topolo setup --shared`,
222
+ then restart the agents.
223
+
117
224
  ## Cross-org isolation
118
225
 
119
226
  The CLI **never accepts an `orgId` argument**. Every command is scoped to the
120
- active context embedded in the credential (personal or a specific org); to act
121
- in a different organization you switch context with `topolo context use`, which
227
+ context embedded in the credential (personal or a specific org); to act
228
+ in a different organization you pin that context's own stored credential
229
+ (`--context`), or create one with `topolo context use`, which
122
230
  mints a new context-scoped credential — you can never target another org by
123
231
  passing an id. The context remains bound through refresh rotation and is not
124
232
  re-hydrated from the user's current web selection. This is enforced in two places:
@@ -224,6 +332,77 @@ Both resource flags are required together. This supports concurrent agent
224
332
  operations across many app-local workspaces without changing the CLI/MCP org
225
333
  context or the user's web switcher.
226
334
 
335
+ ### `topolo credentials write-custody`
336
+
337
+ Local CLI and stdio MCP replace credentials in action results with opaque
338
+ `topolo-secret:<uuid>` references held in the OS protected store (macOS
339
+ Keychain, Linux Secret Service, Windows DPAPI). Both use the same store service
340
+ and the same binding (environment, verified principal, organization and API key
341
+ identity), so a reference returned by a stdio MCP call can be used by the CLI
342
+ pinned to the same identity and environment. References expire after 30 minutes.
343
+
344
+ When a service such as the release controller needs a scoped API key in a
345
+ custody file, write it straight from the reference so no person or agent sees
346
+ the plaintext:
347
+
348
+ ```bash
349
+ export TOPOLO_CONTEXT=org:<organizationId>
350
+ created=$(topolo --env production actions call app_topolo_auth.api_keys.create \
351
+ --data '{"appId":"<appId>","name":"<label>","scopes":["<scope>"],"expiresInDays":90}' \
352
+ --confirm --detail --json) # data.key.secret is a topolo-secret reference
353
+ topolo --env production credentials write-custody "$(jq -r .data.key.secret <<<"$created")" \
354
+ --api-key-id "$(jq -r .data.key.id <<<"$created")" --app <appId> \
355
+ --out /absolute/custody/dir/production --confirm --json
356
+ ```
357
+
358
+ The command:
359
+
360
+ 1. Requires `--confirm`, an absolute `--out`, `--app` and `--api-key-id`.
361
+ Keep one key per environment in its own subdirectory (for example
362
+ `.../catalog-publication-credentials/<environment>/`). When the last path
363
+ component is an environment name it must equal the credential's environment
364
+ (`custody_environment_mismatch` otherwise), and `custody.json.environment`
365
+ is always the environment the credential, protected store and Auth calls
366
+ are bound to.
367
+ 2. Creates `--out` (and any missing parents) with mode 0700, or accepts an existing 0700 directory owned
368
+ by the current user. Every path component must be a real directory (no
369
+ symbolic links); parents must be owned by root or the current user and must
370
+ not be group- or world-writable (root-owned sticky directories such as
371
+ `/tmp` are allowed).
372
+ 3. Refuses to overwrite `api-key.json` or `custody.json` without `--replace`,
373
+ and refuses symbolic-link or non-regular destinations even with it.
374
+ 4. Reads the key's metadata from Auth `app_topolo_auth.api_keys.list` with the
375
+ pinned credential and requires the same organization, the `--app`
376
+ application, status `active` and an unexpired key.
377
+ 5. Resolves the reference through the SDK protected store with its binding
378
+ checks, then verifies the key against Auth introspection: it must be that
379
+ API key id, application, organization and scope set.
380
+ 6. Writes, each 0600 through a temporary file and rename in the same directory:
381
+ - `api-key.json`: `{"schemaVersion":1,"apiKey":"<secret>"}`
382
+ - `custody.json`: `{schemaVersion, appId, scopes, organizationId, environment,
383
+ apiKeyId, expiresAt, keyDigest, createdAt}`. `expiresAt` and `createdAt`
384
+ are Auth's values unchanged (epoch seconds, `expiresAt` null for no
385
+ expiry); `keyDigest` is `sha256:<hex>` of the secret.
386
+ 7. Deletes the local protected reference, like `actions release-secret`. This
387
+ does **not** revoke the upstream key; use `app_topolo_auth.api_keys.revoke`.
388
+
389
+ Output (JSON or text) carries only paths, metadata and the digest. Refusals use
390
+ stable `error.code` values: `custody_confirmation_required`,
391
+ `custody_invalid_input`, `custody_path_not_absolute`, `custody_environment_mismatch`, `custody_platform_unsupported`,
392
+ `custody_path_symlink`, `custody_path_not_directory`, `custody_path_not_regular`,
393
+ `custody_parent_insecure`, `custody_dir_insecure`, `custody_file_exists`,
394
+ `custody_organization_required`, `custody_metadata_unavailable`,
395
+ `custody_key_not_found`, `custody_app_mismatch`, `custody_organization_mismatch`,
396
+ `custody_key_inactive`, `custody_key_expired`, `custody_secret_unsupported_type`,
397
+ `custody_key_verification_failed`, `custody_key_mismatch`,
398
+ `custody_metadata_mismatch`, `custody_reference_release_failed` and
399
+ `custody_error_withheld`, plus the protected store's `invalid_action_secret_reference`,
400
+ `action_secret_unavailable` (unknown, or bound to another principal,
401
+ organization, API key or environment), `action_secret_expired`,
402
+ `action_secret_invalid` and `action_secret_identity_required`. A refusal before
403
+ step 6 writes no custody file and keeps the reference (step 2 may already have
404
+ created the empty 0700 directory).
405
+
227
406
  ### `topolo workspaces`
228
407
 
229
408
  Discovers canonical workspaces and the caller-specific creation policy for a
@@ -270,6 +270,13 @@ Local deletion does not revoke the upstream credential; use its published
270
270
  revocation action separately when required. Other SDK/remote hosts keep
271
271
  redaction unless they explicitly provide a protected store.
272
272
 
273
+ The only supported way to put a referenced API key on disk is an
274
+ operator-approved custody file:
275
+ `topolo credentials write-custody <reference> --out <absolute-dir> --app <appId> --api-key-id <id> --confirm --json`
276
+ verifies the key against Auth, writes 0600 `api-key.json` and `custody.json`
277
+ (metadata and a SHA-256 digest only) into a 0700 directory, prints no secret,
278
+ and deletes the local reference without revoking the upstream key.
279
+
273
280
  ## Generic public API escape hatch
274
281
 
275
282
  Only when public docs provide a complete contract:
@@ -270,6 +270,13 @@ Local deletion does not revoke the upstream credential; use its published
270
270
  revocation action separately when required. Other SDK/remote hosts keep
271
271
  redaction unless they explicitly provide a protected store.
272
272
 
273
+ The only supported way to put a referenced API key on disk is an
274
+ operator-approved custody file:
275
+ `topolo credentials write-custody <reference> --out <absolute-dir> --app <appId> --api-key-id <id> --confirm --json`
276
+ verifies the key against Auth, writes 0600 `api-key.json` and `custody.json`
277
+ (metadata and a SHA-256 digest only) into a 0700 directory, prints no secret,
278
+ and deletes the local reference without revoking the upstream key.
279
+
273
280
  ## Generic public API escape hatch
274
281
 
275
282
  Only when public docs provide a complete contract:
@@ -270,6 +270,13 @@ Local deletion does not revoke the upstream credential; use its published
270
270
  revocation action separately when required. Other SDK/remote hosts keep
271
271
  redaction unless they explicitly provide a protected store.
272
272
 
273
+ The only supported way to put a referenced API key on disk is an
274
+ operator-approved custody file:
275
+ `topolo credentials write-custody <reference> --out <absolute-dir> --app <appId> --api-key-id <id> --confirm --json`
276
+ verifies the key against Auth, writes 0600 `api-key.json` and `custody.json`
277
+ (metadata and a SHA-256 digest only) into a 0700 directory, prints no secret,
278
+ and deletes the local reference without revoking the upstream key.
279
+
273
280
  ## Generic public API escape hatch
274
281
 
275
282
  Only when public docs provide a complete contract:
@@ -270,6 +270,13 @@ Local deletion does not revoke the upstream credential; use its published
270
270
  revocation action separately when required. Other SDK/remote hosts keep
271
271
  redaction unless they explicitly provide a protected store.
272
272
 
273
+ The only supported way to put a referenced API key on disk is an
274
+ operator-approved custody file:
275
+ `topolo credentials write-custody <reference> --out <absolute-dir> --app <appId> --api-key-id <id> --confirm --json`
276
+ verifies the key against Auth, writes 0600 `api-key.json` and `custody.json`
277
+ (metadata and a SHA-256 digest only) into a 0700 directory, prints no secret,
278
+ and deletes the local reference without revoking the upstream key.
279
+
273
280
  ## Generic public API escape hatch
274
281
 
275
282
  Only when public docs provide a complete contract:
@@ -270,6 +270,13 @@ Local deletion does not revoke the upstream credential; use its published
270
270
  revocation action separately when required. Other SDK/remote hosts keep
271
271
  redaction unless they explicitly provide a protected store.
272
272
 
273
+ The only supported way to put a referenced API key on disk is an
274
+ operator-approved custody file:
275
+ `topolo credentials write-custody <reference> --out <absolute-dir> --app <appId> --api-key-id <id> --confirm --json`
276
+ verifies the key against Auth, writes 0600 `api-key.json` and `custody.json`
277
+ (metadata and a SHA-256 digest only) into a 0700 directory, prints no secret,
278
+ and deletes the local reference without revoking the upstream key.
279
+
273
280
  ## Generic public API escape hatch
274
281
 
275
282
  Only when public docs provide a complete contract:
@@ -270,6 +270,13 @@ Local deletion does not revoke the upstream credential; use its published
270
270
  revocation action separately when required. Other SDK/remote hosts keep
271
271
  redaction unless they explicitly provide a protected store.
272
272
 
273
+ The only supported way to put a referenced API key on disk is an
274
+ operator-approved custody file:
275
+ `topolo credentials write-custody <reference> --out <absolute-dir> --app <appId> --api-key-id <id> --confirm --json`
276
+ verifies the key against Auth, writes 0600 `api-key.json` and `custody.json`
277
+ (metadata and a SHA-256 digest only) into a 0700 directory, prints no secret,
278
+ and deletes the local reference without revoking the upstream key.
279
+
273
280
  ## Generic public API escape hatch
274
281
 
275
282
  Only when public docs provide a complete contract:
@@ -270,6 +270,13 @@ Local deletion does not revoke the upstream credential; use its published
270
270
  revocation action separately when required. Other SDK/remote hosts keep
271
271
  redaction unless they explicitly provide a protected store.
272
272
 
273
+ The only supported way to put a referenced API key on disk is an
274
+ operator-approved custody file:
275
+ `topolo credentials write-custody <reference> --out <absolute-dir> --app <appId> --api-key-id <id> --confirm --json`
276
+ verifies the key against Auth, writes 0600 `api-key.json` and `custody.json`
277
+ (metadata and a SHA-256 digest only) into a 0700 directory, prints no secret,
278
+ and deletes the local reference without revoking the upstream key.
279
+
273
280
  ## Generic public API escape hatch
274
281
 
275
282
  Only when public docs provide a complete contract:
@@ -270,6 +270,13 @@ Local deletion does not revoke the upstream credential; use its published
270
270
  revocation action separately when required. Other SDK/remote hosts keep
271
271
  redaction unless they explicitly provide a protected store.
272
272
 
273
+ The only supported way to put a referenced API key on disk is an
274
+ operator-approved custody file:
275
+ `topolo credentials write-custody <reference> --out <absolute-dir> --app <appId> --api-key-id <id> --confirm --json`
276
+ verifies the key against Auth, writes 0600 `api-key.json` and `custody.json`
277
+ (metadata and a SHA-256 digest only) into a 0700 directory, prints no secret,
278
+ and deletes the local reference without revoking the upstream key.
279
+
273
280
  ## Generic public API escape hatch
274
281
 
275
282
  Only when public docs provide a complete contract: