@topolo/cli 0.13.13 → 0.13.15

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
@@ -114,6 +114,88 @@ The local installer does not edit JetBrains AI Assistant settings or web-only
114
114
  connectors because those are managed through their own UI or remote connector
115
115
  configuration. Use the generated native MCP entry as the import source there.
116
116
 
117
+ ### Shared local MCP hub (opt-in)
118
+
119
+ Every stdio registration starts its own Node process in every agent thread, so
120
+ N threads x M pinned identities means N x M MCP processes. The shared hub runs
121
+ **one** loopback process that serves every identity at
122
+ `http://127.0.0.1:<port>/mcp/<name>`; Claude Code and Codex connect to it by URL.
123
+ Plain `topolo setup` keeps the stdio path; `--shared` is opt-in.
124
+
125
+ ```bash
126
+ topolo --env production setup --shared --name topolo-acme --context org:org_acme
127
+ topolo --env staging setup --shared --name topolo-acme-stg --context org:org_acme
128
+ topolo setup --shared --import-stdio # move every existing stdio topolo/topolo-* entry onto the hub
129
+ topolo doctor --shared # hub, token, unit, registrations, then MCP whoami per identity
130
+ topolo mcp serve [--port 7337] # what the keep-alive unit runs; foreground for debugging
131
+ ```
132
+
133
+ `setup --shared`:
134
+
135
+ 1. Adds the identity (`{name, env, context}`, exactly what a stdio entry pins
136
+ with `TOPOLO_ENV` + `TOPOLO_CONTEXT`; omit `--context` only for the default
137
+ `topolo` identity that follows the active context) to `mcp-hub.json` next to
138
+ the credential store (`~/.config/topolo/mcp-hub.json`, or the directory of
139
+ `TOPOLO_CONFIG_PATH`). `--import-stdio` converts existing stdio entries and
140
+ skips ones the hub cannot serve (private `TOPOLO_CONFIG_PATH` profiles,
141
+ agent installations, invalid or conflicting definitions).
142
+ 2. Creates `mcp-hub.token` (256-bit, mode 0600) once and reuses it.
143
+ 3. Registers **every** hub identity with Claude Code and Codex at user scope,
144
+ replacing same-name stdio entries. `--project` is refused so the token can
145
+ never land in a repository file. Other hosts stay on stdio.
146
+ 4. Installs a keep-alive unit running the exact Node binary and CLI entry that
147
+ ran setup (`--no-service` to skip): `~/Library/LaunchAgents/io.topolo.mcp-hub.plist`
148
+ (launchd, `KeepAlive`, logs to `~/Library/Logs/topolo-mcp-hub.log`) or
149
+ `~/.config/systemd/user/topolo-mcp-hub.service` (`Restart=always`). Only
150
+ `TOPOLO_CONFIG_PATH`/`XDG_CONFIG_HOME` are passed through; PATH and secrets
151
+ are not. With `--agent-home` the unit is written there but not loaded.
152
+
153
+ Exact entries written (token shown as `<token>`):
154
+
155
+ ```jsonc
156
+ // ~/.claude.json (written 0600)
157
+ "mcpServers": {
158
+ "topolo-acme": {
159
+ "type": "http",
160
+ "url": "http://127.0.0.1:7337/mcp/topolo-acme",
161
+ "headers": { "Authorization": "Bearer <token>", "X-Topolo-Agent": "claude-code" }
162
+ }
163
+ }
164
+ ```
165
+
166
+ ```toml
167
+ # ~/.codex/config.toml (written 0600)
168
+ [mcp_servers.topolo-acme]
169
+ url = "http://127.0.0.1:7337/mcp/topolo-acme"
170
+ http_headers = { "Authorization" = "Bearer <token>", "X-Topolo-Agent" = "codex" }
171
+ ```
172
+
173
+ Codex streamable-HTTP servers accept `url`, `bearer_token_env_var`,
174
+ `http_headers` and `env_http_headers`; an inline `bearer_token` is rejected.
175
+ `http_headers` is used so the hub works when Codex is launched from a GUI with
176
+ no shell environment. To keep the token out of `config.toml`, replace the
177
+ `Authorization` header with `bearer_token_env_var = "TOPOLO_MCP_HUB_TOKEN"` and
178
+ export that variable (contents of `mcp-hub.token`) into Codex's environment.
179
+
180
+ Hub behaviour and security:
181
+
182
+ - Binds `127.0.0.1` only; there is no host option.
183
+ - Refuses any request whose `Host` is not `127.0.0.1`, `localhost` or `[::1]`
184
+ on the hub port, or that carries a non-loopback `Origin` (DNS rebinding and
185
+ browser cross-origin calls), before checking the bearer token
186
+ (constant-time). Only then is `/mcp/<name>` routed; `GET /health` lists
187
+ identity names.
188
+ - Each identity has its own handler (`@topolo/mcp/local`) that resolves and
189
+ refreshes its credential from the local store exactly as a pinned stdio MCP
190
+ does. Process-wide `TOPOLO_API_KEY`/`TOPOLO_ACCESS_TOKEN`/`TOPOLO_CONTEXT`,
191
+ request headers and other identities are never consulted; the hub token is
192
+ stripped before the request reaches an identity.
193
+ - Stateless Streamable HTTP (no session ids), so restarts never strand a client.
194
+ - `mcp-hub.json` is re-read when it changes; adding an identity needs no restart.
195
+ - Logs carry method, path, status and duration only.
196
+ - Rotating the token: delete `mcp-hub.token`, rerun `topolo setup --shared`,
197
+ then restart the agents.
198
+
117
199
  ## Cross-org isolation
118
200
 
119
201
  The CLI **never accepts an `orgId` argument**. Every command is scoped to the
@@ -224,6 +306,77 @@ Both resource flags are required together. This supports concurrent agent
224
306
  operations across many app-local workspaces without changing the CLI/MCP org
225
307
  context or the user's web switcher.
226
308
 
309
+ ### `topolo credentials write-custody`
310
+
311
+ Local CLI and stdio MCP replace credentials in action results with opaque
312
+ `topolo-secret:<uuid>` references held in the OS protected store (macOS
313
+ Keychain, Linux Secret Service, Windows DPAPI). Both use the same store service
314
+ and the same binding (environment, verified principal, organization and API key
315
+ identity), so a reference returned by a stdio MCP call can be used by the CLI
316
+ pinned to the same identity and environment. References expire after 30 minutes.
317
+
318
+ When a service such as the release controller needs a scoped API key in a
319
+ custody file, write it straight from the reference so no person or agent sees
320
+ the plaintext:
321
+
322
+ ```bash
323
+ export TOPOLO_CONTEXT=org:<organizationId>
324
+ created=$(topolo --env production actions call app_topolo_auth.api_keys.create \
325
+ --data '{"appId":"<appId>","name":"<label>","scopes":["<scope>"],"expiresInDays":90}' \
326
+ --confirm --detail --json) # data.key.secret is a topolo-secret reference
327
+ topolo --env production credentials write-custody "$(jq -r .data.key.secret <<<"$created")" \
328
+ --api-key-id "$(jq -r .data.key.id <<<"$created")" --app <appId> \
329
+ --out /absolute/custody/dir/production --confirm --json
330
+ ```
331
+
332
+ The command:
333
+
334
+ 1. Requires `--confirm`, an absolute `--out`, `--app` and `--api-key-id`.
335
+ Keep one key per environment in its own subdirectory (for example
336
+ `.../catalog-publication-credentials/<environment>/`). When the last path
337
+ component is an environment name it must equal the credential's environment
338
+ (`custody_environment_mismatch` otherwise), and `custody.json.environment`
339
+ is always the environment the credential, protected store and Auth calls
340
+ are bound to.
341
+ 2. Creates `--out` (and any missing parents) with mode 0700, or accepts an existing 0700 directory owned
342
+ by the current user. Every path component must be a real directory (no
343
+ symbolic links); parents must be owned by root or the current user and must
344
+ not be group- or world-writable (root-owned sticky directories such as
345
+ `/tmp` are allowed).
346
+ 3. Refuses to overwrite `api-key.json` or `custody.json` without `--replace`,
347
+ and refuses symbolic-link or non-regular destinations even with it.
348
+ 4. Reads the key's metadata from Auth `app_topolo_auth.api_keys.list` with the
349
+ pinned credential and requires the same organization, the `--app`
350
+ application, status `active` and an unexpired key.
351
+ 5. Resolves the reference through the SDK protected store with its binding
352
+ checks, then verifies the key against Auth introspection: it must be that
353
+ API key id, application, organization and scope set.
354
+ 6. Writes, each 0600 through a temporary file and rename in the same directory:
355
+ - `api-key.json`: `{"schemaVersion":1,"apiKey":"<secret>"}`
356
+ - `custody.json`: `{schemaVersion, appId, scopes, organizationId, environment,
357
+ apiKeyId, expiresAt, keyDigest, createdAt}`. `expiresAt` and `createdAt`
358
+ are Auth's values unchanged (epoch seconds, `expiresAt` null for no
359
+ expiry); `keyDigest` is `sha256:<hex>` of the secret.
360
+ 7. Deletes the local protected reference, like `actions release-secret`. This
361
+ does **not** revoke the upstream key; use `app_topolo_auth.api_keys.revoke`.
362
+
363
+ Output (JSON or text) carries only paths, metadata and the digest. Refusals use
364
+ stable `error.code` values: `custody_confirmation_required`,
365
+ `custody_invalid_input`, `custody_path_not_absolute`, `custody_environment_mismatch`, `custody_platform_unsupported`,
366
+ `custody_path_symlink`, `custody_path_not_directory`, `custody_path_not_regular`,
367
+ `custody_parent_insecure`, `custody_dir_insecure`, `custody_file_exists`,
368
+ `custody_organization_required`, `custody_metadata_unavailable`,
369
+ `custody_key_not_found`, `custody_app_mismatch`, `custody_organization_mismatch`,
370
+ `custody_key_inactive`, `custody_key_expired`, `custody_secret_unsupported_type`,
371
+ `custody_key_verification_failed`, `custody_key_mismatch`,
372
+ `custody_metadata_mismatch`, `custody_reference_release_failed` and
373
+ `custody_error_withheld`, plus the protected store's `invalid_action_secret_reference`,
374
+ `action_secret_unavailable` (unknown, or bound to another principal,
375
+ organization, API key or environment), `action_secret_expired`,
376
+ `action_secret_invalid` and `action_secret_identity_required`. A refusal before
377
+ step 6 writes no custody file and keeps the reference (step 2 may already have
378
+ created the empty 0700 directory).
379
+
227
380
  ### `topolo workspaces`
228
381
 
229
382
  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: