@topolo/cli 0.13.12 → 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
@@ -453,7 +606,7 @@ silently become version 2 CLI plans.
453
606
 
454
607
  `topolo agent-packs --store /owned/AgentPacks runtime-adoption plan --manifest /review/adoption.json` produces a nonwriting configuration plan. Review its physical targets and digest, then use `runtime-adoption apply --manifest /review/adoption.json --plan-digest <sha256> --confirm` for one configuration-only attempt.
455
608
 
456
- The closed owner manifest binds the actual consumer set and effective local Topolo transport origins, physical file preimages, shared aliases, private profile metadata, immutable CLI 0.13.11/MCP 0.14.26/SDK 0.23.21 targets and the separately pinned successor Python adapter. It must include every server in each physical config as either a targeted transport or an explicitly untouched server. Empty or incomplete manifests refuse. The command never installs skills, rebinds a home, changes existing instructions/permissions, starts a consumer or probes Auth.
609
+ The closed owner manifest binds the actual consumer set and effective local Topolo transport origins, physical file preimages, shared aliases, private profile metadata, one of the two reviewed immutable consumer tuples (CLI 0.13.11/MCP 0.14.26/SDK 0.23.21 or CLI 0.13.12/MCP 0.14.27/SDK 0.23.22) and the separately pinned successor Python adapter. Consumer tuple versions are independent of the driver package version; mixed tuples and other versions refuse. It must include every server in each physical config as either a targeted transport or an explicitly untouched server. Empty or incomplete manifests refuse. The command never installs skills, rebinds a home, changes existing instructions/permissions, starts a consumer or probes Auth.
457
610
 
458
611
  A successful apply means configuration is prepared. It does not mean existing processes reloaded it, the private account is healthy, permissions were granted or either native harness completed a task. Starting or refreshing consumers is a separate owner-authorized step and can contact private Auth and renew the selected private profile. Public credential-store repair is not a blanket prerequisite for independent private profiles. See [the manifest and effect boundaries](agent-packs/README.md#private-runtime-adoption).
459
612
 
@@ -710,7 +710,7 @@ The manifest is a local owner-reviewed configuration scope, not native provenanc
710
710
  - `ownerGeneration: {path, sha256}`: the owner's current bounded nonsecret effective-transport/generation record, inside that library. The owner must establish its native currency separately.
711
711
  - `consumers`: the complete unique existing native IDs in scope. Target rows must account for every ID; no new native IDs are created.
712
712
  - `profile: {path, publicPath, metadata, environment, contextKey, principalId}`: absolute private and public credential-file references, explicit organization context, and metadata only. Files must be physically distinct; the private profile must be owned, single-link, regular and mode 0600. Neither profile's contents are read or hashed. A renewal that changes private file metadata invalidates the descriptor and requires owner reconciliation.
713
- - `runtime: {node, cli, mcp, cohort, pythonSource, adapterFiles, osEnvironment}`. Each executable/entry and each `cohort` value is `{path, sha256}`. Cohort keys `cli`, `mcp`, `sdk` pin their package JSON; required target versions are 0.13.11, 0.14.26 and 0.23.21. CLI and MCP entries must be their pinned package's `dist/index.js`. `pythonSource` is the physical successor source directory; `adapterFiles` pins its `topolo_agent_pack/{cli,live,topolo}.py`. Verify the complete installed cohort independently before admitting its pins. `osEnvironment` permits only HOME/PATH and optional USER/LOGNAME/LANG/LC_ALL/TMPDIR, with no credential, service-origin, Node or installation overrides.
713
+ - `runtime: {node, cli, mcp, cohort, pythonSource, adapterFiles, osEnvironment}`. Each executable/entry and each `cohort` value is `{path, sha256}`. Cohort keys `cli`, `mcp`, `sdk` pin their package JSON; the exact reviewed consumer tuples are CLI 0.13.11/MCP 0.14.26/SDK 0.23.21 and CLI 0.13.12/MCP 0.14.27/SDK 0.23.22. Each tuple is coupled: mixed or unreviewed versions refuse, and both CLI and MCP must depend on that tuple's exact SDK version. The consumer tuple is independent of the driver package version. CLI and MCP entries must be their pinned package's `dist/index.js`. `pythonSource` is the physical successor source directory; `adapterFiles` pins its `topolo_agent_pack/{cli,live,topolo}.py`. Verify the complete installed cohort independently before admitting its pins. `osEnvironment` permits only HOME/PATH and optional USER/LOGNAME/LANG/LC_ALL/TMPDIR, with no credential, service-origin, Node or installation overrides.
714
714
  - `targets`: one row per unique physical configuration file, with `{path, before, aliases, format, rootKey, consumers, transports, untouchedServers}`. Supported formats are JSON `mcpServers` and simple TOML `mcp_servers` tables. Each transport is `{name, origin, consumers}` and must already exist as a local command; URL, inherited `env_vars`, installation-mode, ambiguous names or unsupported TOML layouts refuse. `origin` is the exact physical file. The complete physical server map must equal the targeted names plus `untouchedServers`. The owner must independently bind all effective native layers; a zero-table query or a guessed server name is insufficient.
715
715
  - `launcher: {path, before}`: exactly the owned generated `launchers/topolo-agent-pack-backend`; its interpreter, owner and mode are preserved.
716
716
  - `preserve`: nonsecret `{path, sha256}` references to existing roster, model/effort, permission, charter/context, routing and lifecycle records. Include every protected fixed and dynamic record in the approved scope. These may not overlap destinations. No transcript or credential contents belong in a manifest.
@@ -2086,12 +2086,19 @@ def adoption_runtime(runtime,profile):
2086
2086
  adoption_read_guard(profile,adoption_runtime_paths(runtime))
2087
2087
  for key in ['node','cli','mcp']:adoption_pin(runtime[key],profile)
2088
2088
  adoption_keys(runtime['cohort'],['cli','mcp','sdk'])
2089
- for key,name,version in [('cli','@topolo/cli','0.13.11'),('mcp','@topolo/mcp','0.14.26'),('sdk','@topolo/sdk','0.23.21')]:
2089
+ packages={}
2090
+ for key,name in [('cli','@topolo/cli'),('mcp','@topolo/mcp'),('sdk','@topolo/sdk')]:
2090
2091
  package=json.loads(adoption_pin(runtime['cohort'][key],profile))
2091
- adoption_require(package.get('name')==name and package.get('version')==version,'immutable target cohort differs')
2092
- if key!='sdk':
2093
- adoption_require(runtime[key]['path']==str(pathlib.Path(runtime['cohort'][key]['path']).parent/'dist/index.js') and
2094
- package.get('dependencies',{}).get('@topolo/sdk')=='0.23.21','entry/dependency cohort differs')
2092
+ adoption_require(package.get('name')==name,'immutable target cohort differs')
2093
+ packages[key]=package
2094
+ versions=tuple(packages[key].get('version') for key in ['cli','mcp','sdk'])
2095
+ # Reviewed consumer tuples are independent of this adapter driver's version.
2096
+ # Membership is coupled: independently allowed versions would admit mixtures.
2097
+ adoption_require(versions in (('0.13.11','0.14.26','0.23.21'),('0.13.12','0.14.27','0.23.22')),
2098
+ 'immutable target cohort differs')
2099
+ for key in ['cli','mcp']:
2100
+ adoption_require(runtime[key]['path']==str(pathlib.Path(runtime['cohort'][key]['path']).parent/'dist/index.js') and
2101
+ packages[key].get('dependencies',{}).get('@topolo/sdk')==versions[2],'entry/dependency cohort differs')
2095
2102
  source=pathlib.Path(runtime['pythonSource'])
2096
2103
  adoption_require(source.is_absolute() and source==source.resolve(),'physical adapter source required')
2097
2104
  adoption_require(isinstance(runtime['adapterFiles'],list) and len(runtime['adapterFiles'])==3 and
@@ -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: