healthcloud-cli 3.4.0__tar.gz → 3.5.0__tar.gz

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.
Files changed (81) hide show
  1. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/.gitignore +5 -0
  2. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/PKG-INFO +157 -3
  3. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/README.md +156 -2
  4. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/agents/commands.py +29 -20
  5. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/agents/operations.py +43 -20
  6. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/context.py +42 -0
  7. healthcloud_cli-3.5.0/healthcloud_cli/ams/data_sources/__init__.py +5 -0
  8. healthcloud_cli-3.5.0/healthcloud_cli/ams/data_sources/commands.py +61 -0
  9. healthcloud_cli-3.5.0/healthcloud_cli/ams/data_sources/group.py +18 -0
  10. healthcloud_cli-3.5.0/healthcloud_cli/ams/data_sources/operations.py +71 -0
  11. healthcloud_cli-3.5.0/healthcloud_cli/ams/data_sources/projections.py +12 -0
  12. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/departments/operations.py +2 -0
  13. healthcloud_cli-3.5.0/healthcloud_cli/ams/errors.py +215 -0
  14. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/guardrails/operations.py +4 -1
  15. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/organization/operations.py +1 -0
  16. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/policies/operations.py +2 -1
  17. healthcloud_cli-3.5.0/healthcloud_cli/ams/pretty.py +133 -0
  18. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/registry.py +4 -0
  19. healthcloud_cli-3.5.0/healthcloud_cli/ams/results.py +42 -0
  20. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/session.py +2 -0
  21. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/skills/operations.py +2 -1
  22. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/teams/operations.py +2 -1
  23. healthcloud_cli-3.5.0/healthcloud_cli/ams/tools/__init__.py +5 -0
  24. healthcloud_cli-3.5.0/healthcloud_cli/ams/tools/commands.py +57 -0
  25. healthcloud_cli-3.5.0/healthcloud_cli/ams/tools/group.py +18 -0
  26. healthcloud_cli-3.5.0/healthcloud_cli/ams/tools/operations.py +66 -0
  27. healthcloud_cli-3.5.0/healthcloud_cli/ams/tools/projections.py +21 -0
  28. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/cli.py +107 -4
  29. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/pyproject.toml +1 -1
  30. healthcloud_cli-3.4.0/healthcloud_cli/ams/errors.py +0 -97
  31. healthcloud_cli-3.4.0/healthcloud_cli/ams/results.py +0 -31
  32. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/hatch_build.py +0 -0
  33. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/__init__.py +0 -0
  34. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/__init__.py +0 -0
  35. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/agents/__init__.py +0 -0
  36. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/agents/group.py +0 -0
  37. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/agents/projections.py +0 -0
  38. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/auth.py +0 -0
  39. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/commands.py +0 -0
  40. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/compatibility.py +0 -0
  41. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/departments/__init__.py +0 -0
  42. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/departments/commands.py +0 -0
  43. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/departments/group.py +0 -0
  44. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/departments/projections.py +0 -0
  45. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/domain_errors.py +0 -0
  46. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/group.py +0 -0
  47. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
  48. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/guardrails/commands.py +0 -0
  49. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/guardrails/group.py +0 -0
  50. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/guardrails/projections.py +0 -0
  51. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/organization/__init__.py +0 -0
  52. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/organization/commands.py +0 -0
  53. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/organization/group.py +0 -0
  54. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/organization/projections.py +0 -0
  55. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/output.py +0 -0
  56. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/policies/__init__.py +0 -0
  57. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/policies/commands.py +0 -0
  58. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/policies/group.py +0 -0
  59. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/policies/projections.py +0 -0
  60. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/skills/__init__.py +0 -0
  61. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/skills/commands.py +0 -0
  62. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/skills/group.py +0 -0
  63. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/skills/projections.py +0 -0
  64. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/target.py +0 -0
  65. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/teams/__init__.py +0 -0
  66. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/teams/commands.py +0 -0
  67. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/teams/group.py +0 -0
  68. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/teams/projections.py +0 -0
  69. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/tuning.py +0 -0
  70. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/ui.py +0 -0
  71. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/ams/windows_files.py +0 -0
  72. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/context.py +0 -0
  73. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/contracts/__init__.py +0 -0
  74. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
  75. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/main.py +0 -0
  76. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/manifest.py +0 -0
  77. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/output.py +0 -0
  78. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/params.py +0 -0
  79. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/healthcloud_cli/util.py +0 -0
  80. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/scripts/__init__.py +0 -0
  81. {healthcloud_cli-3.4.0 → healthcloud_cli-3.5.0}/scripts/bundle_manifest.py +0 -0
@@ -255,3 +255,8 @@ packages/pip-tenantadmin/README.md
255
255
 
256
256
  # Live SDK test assets stay in the archived SDK history, outside this source import.
257
257
  integration-tests/shared/test-assets/
258
+
259
+ # Ralph loop runtime state
260
+ .ralph/
261
+ /PROMPT.md
262
+ .blind-peer-review/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: healthcloud-cli
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Summary: Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest.
5
5
  Project-URL: Issues, https://github.com/healthcloudservices/healthcloud-services/issues
6
6
  Project-URL: Repository, https://github.com/healthcloudservices/healthcloud-services
@@ -293,6 +293,31 @@ uv run --frozen hc logout
293
293
  otherwise it prompts (with hidden password input). It stores only the email,
294
294
  tenant/environment binding, access/refresh tokens and expiry in the strict
295
295
  owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
296
+
297
+ For CI and agents, log in non-interactively: pass `--email` and pipe the password
298
+ to `--password-stdin` (read as exactly one stdin line, never echoed, never a
299
+ flag value — there is no `--password` option):
300
+
301
+ ```bash
302
+ printf '%s\n' "$HC_AMS_ADMIN_PASSWORD" | \
303
+ uv run --frozen hc login --environment dev --tenant-id TENANT_ID \
304
+ --email "$HC_AMS_ADMIN_EMAIL" --password-stdin
305
+ ```
306
+
307
+ `--email` beats `HC_AMS_ADMIN_EMAIL` and `--password-stdin` beats
308
+ `HC_AMS_ADMIN_PASSWORD`. With `--password-stdin`, the piped line is always the
309
+ password: it is read before any prompt, so it can never be consumed as the
310
+ email, and the email must come from `--email`, `HC_AMS_ADMIN_EMAIL` or (only on
311
+ an interactive terminal) the email prompt. When stdin is not an interactive
312
+ terminal and neither `--password-stdin` nor `HC_AMS_ADMIN_PASSWORD` supplies the
313
+ password, `hc login` refuses with that guidance instead of attempting a hidden
314
+ prompt (no `GetPassWarning`, no echoed password); a missing email on a
315
+ non-interactive stdin (or a process with no stdin stream at all) is refused the
316
+ same way, naming `--email`/`--password-stdin`; every refusal happens before any
317
+ login request. `--status` cannot be combined with `--email` or `--password-stdin`.
318
+ Even on an interactive terminal, `--password-stdin` suppresses terminal echo
319
+ while the single line is read and restores it afterwards; a stdin stream that is
320
+ closed or errors mid-read is refused cleanly (exit 2, never a traceback).
296
321
  AMS commands may omit both target flags while this session is present, except
297
322
  that a `prod` session still needs `--environment prod` on every command. Explicit
298
323
  flags and target files must match it exactly. A session within 60 seconds of
@@ -320,11 +345,22 @@ uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
320
345
  Read-only agent listing, inspection and export take flags instead of a target
321
346
  file; both may be omitted together when a matching saved session exists. Without
322
347
  one, they log in as tenant admin using `HC_AMS_ADMIN_EMAIL` /
323
- `HC_AMS_ADMIN_PASSWORD`. Stdout carries bounded metadata only; `adapter` shows
348
+ `HC_AMS_ADMIN_PASSWORD`. They emit the same versioned result envelope as the
349
+ other read domains (`operation`, `outcome`, `schema_version`, `target`, `data`;
350
+ no `verification` block on reads); failures emit one categorized JSON error
351
+ envelope on stderr with the same categories (not found, target mismatch,
352
+ refusal, logged out), not the plain-text `Error: …` lines of earlier releases.
353
+ `agents list` carries bounded projections under `data.agents` plus a `page`
354
+ block; `agents get` carries the bounded projection under `data.agent`. The
355
+ legacy top-level `{"agents": …, "environment": …, "tenant_id": …}` shape is
356
+ intentionally replaced by this envelope — scripts must read the new shape.
357
+ Stdout carries bounded metadata only; `adapter` shows
324
358
  whether an agent runs from the `file` or `db` config adapter (`unknown` when the
325
359
  server does not say). `export` first re-checks the agent/tenant identity, then
326
360
  saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
327
- file, refusing (and leaving no file) when the body is not valid UTF-8. `export`
361
+ file (exact bytes preserved), refusing (and leaving no file) when the body is
362
+ not valid UTF-8; its stdout reports the envelope with the file `path`, `format`
363
+ and `sha256` under `data` and the `agent_id` in `target`. `export`
328
364
  needs an AMS build that serves `/agents/{agent_id}/configuration/export`; that
329
365
  route is not yet deployed to dev:
330
366
 
@@ -335,6 +371,23 @@ uv run --frozen hc ams agents export --environment dev --tenant-id TENANT_ID --a
335
371
  --format yaml --out agent.yaml
336
372
  ```
337
373
 
374
+ For example, `hc ams agents list` succeeds with (formatted for readability):
375
+
376
+ ```json
377
+ {"schema_version": 1, "operation": "agents.list", "outcome": "succeeded",
378
+ "target": {"environment": "dev", "tenant_id": "TENANT_ID"},
379
+ "data": {"agents": [{"id": "AGENT_ID", "name": "…", "adapter": "file", "…": "…"}],
380
+ "page": {"kind": "unpaged", "returned_count": 1, "continuation": null}}}
381
+ ```
382
+
383
+ and a missing `--agent-id` fails with one envelope on stderr and exit 1:
384
+
385
+ ```json
386
+ {"schema_version": 1, "operation": "agents.get", "outcome": "failed",
387
+ "target": {"environment": "dev", "tenant_id": "TENANT_ID", "agent_id": "AGENT_ID"},
388
+ "error": {"category": "not_found", "message": "Agent 'AGENT_ID' was not found."}}
389
+ ```
390
+
338
391
  Read-only department listing and inspection use the same explicit tenant-admin
339
392
  targeting. They emit one versioned JSON result on stdout; failures emit one
340
393
  categorized JSON result on stderr. Department descriptions, creator identities,
@@ -421,6 +474,107 @@ additional data. Guardrail reads refuse target files, root credentials or base
421
474
  URLs, raw output, blank or dot-segment guardrail IDs, and installed SDKs without
422
475
  both guardrail methods before tenant-admin login or catalog reads.
423
476
 
477
+ Read-only tool catalog listing and inspection are complete and unpaged. The
478
+ bounded projection includes only IDs, names, read-only and confirmation flags,
479
+ agent types, required scopes, audit events, and activity labels:
480
+
481
+ ```bash
482
+ uv run --frozen hc ams tools list --environment dev --tenant-id TENANT_ID
483
+ uv run --frozen hc ams tools get --environment dev --tenant-id TENANT_ID \
484
+ --tool-id TOOL_ID
485
+ ```
486
+
487
+ When both target flags are omitted, these commands may use the matching saved
488
+ tenant-admin session. Explicit flags must match that session. Tool output never
489
+ includes descriptions, parameters, input schemas, PIP or connector routing, or
490
+ arbitrary response fields. The CLI does not execute, assign, create, update, or
491
+ delete tools; join agents or skills; or claim assignment, availability,
492
+ authorization, deployment, serving, or runtime effectiveness. It makes exactly
493
+ one catalog GET after tenant-admin login and does not paginate, retry, cache,
494
+ join, or infer additional data. Tool reads refuse target files, root credentials
495
+ or base URLs, raw output, blank or dot-segment IDs, and installed SDKs without
496
+ both tool methods before tenant-admin login or catalog reads.
497
+
498
+ Read-only data-source catalog listing and inspection are complete and unpaged.
499
+ The bounded projection includes only stable IDs and names:
500
+
501
+ ```bash
502
+ uv run --frozen hc ams data-sources list --environment dev --tenant-id TENANT_ID
503
+ uv run --frozen hc ams data-sources get --environment dev --tenant-id TENANT_ID \
504
+ --data-source-id DATA_SOURCE_ID
505
+ ```
506
+
507
+ When both target flags are omitted, these commands may use the matching saved
508
+ tenant-admin session. Explicit flags must match that session. Data-source output
509
+ never includes descriptions or arbitrary response fields and does not imply
510
+ access, patient consent, record availability, assignment, authorization, or
511
+ runtime effectiveness. The CLI does not create, update, delete, assign, access
512
+ records through, or execute data sources. It makes exactly one catalog GET after
513
+ tenant-admin login and does not paginate, retry, cache, truncate, join, or infer
514
+ additional data. Data-source reads refuse target files, root credentials or base
515
+ URLs, raw output, blank or dot-segment IDs, and installed SDKs without both
516
+ data-source methods before tenant-admin login or catalog reads.
517
+
518
+ ### AMS output modes
519
+
520
+ The read commands (`hc ams skills|tools|policies|guardrails|data-sources|teams|departments list|get`
521
+ and `hc ams organization hierarchy`) render in two modes (`--output` on the root
522
+ `hc` command; `pretty` is the default):
523
+
524
+ - **`pretty`** (default): a compact human view. Lists print an `ID`/`NAME`
525
+ table with an entry count; a single record prints `key: value` lines; the
526
+ organization hierarchy prints an indented tree. Each failed AMS command
527
+ prints one stderr line, `error: <message> (<category>)` (flag/argument
528
+ usage errors from Click print Click's standard usage message instead).
529
+ - **`json`**: the stable result envelope (below) on stdout. When stdout is not
530
+ a terminal, the envelope is printed as one compact line; on a terminal it is
531
+ indented. Errors keep the structured JSON envelope on stderr.
532
+
533
+ The JSON envelope carries `schema_version`, `operation`, `target`, `outcome`,
534
+ and either `data` (succeeded) or `error: {category, message}` (failed). On
535
+ read-only operations the envelope no longer includes the `verification` block:
536
+ every field there is `not_applicable` on a read, so it was boilerplate. If a
537
+ future mutating operation reintroduces it, `verification` reappears only with
538
+ non-`not_applicable` content. Everything else about the envelope — field names,
539
+ ordering semantics, categories — is unchanged and stays parseable.
540
+
541
+ `--raw`/`--output raw` stays refused for AMS (`Raw output is refused for AMS.`),
542
+ and refusals keep the JSON error envelope on stderr.
543
+
544
+ ```bash
545
+ hc ams skills list --environment dev --tenant-id TENANT_ID # pretty table
546
+ hc ams skills get --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID # key: value
547
+ hc --output json ams skills list --environment dev --tenant-id TENANT_ID # compact envelope
548
+ hc --output json ams skills list --environment dev --tenant-id TENANT_ID | jq .data
549
+ ```
550
+
551
+ ### What `hc ams` errors report
552
+
553
+ In `json` and `raw` modes each failed AMS command prints one JSON object on stderr with
554
+ an `error.category` and a message naming the real cause; in `pretty` mode the
555
+ same message is exactly one stderr line, `error: <message> (<category>)`:
556
+
557
+ - Local refusals keep their specific message and exit `1` with category
558
+ `validation`: `Raw output is refused for AMS.`, `AMS target does not match the
559
+ saved session; stopped before authentication.`, identity mismatches
560
+ (`Skill/tenant identity mismatch; stopped.`), and dot-segment ID refusals.
561
+ - No saved session and no target flags: category `authentication` with
562
+ `Not logged in; run \`hc login --environment <env> --tenant-id <tenant>\`.`
563
+ - A `get` whose requested ID does not exist (HTTP 404): category `not_found`
564
+ naming the ID, e.g. `Skill 'nosuch' was not found.` (a 404 on `list` stays
565
+ `validation`; departments keeps its legacy contract for all read statuses).
566
+ - HTTP 401/403 map to `authentication`/`authorization`; 409 to `conflict`;
567
+ network failures to `transport`; local output failures to `local_io`.
568
+ - Any unexpected internal error is category `internal` with a generic message;
569
+ it is never labeled `validation` and never leaks exception details.
570
+
571
+ ```bash
572
+ hc --raw ams skills list --environment dev --tenant-id TENANT_ID # validation: raw refused
573
+ hc ams skills list --tenant-id OTHER_TENANT # validation: target mismatch
574
+ hc ams skills list # authentication: not logged in
575
+ hc ams skills get --skill-id NOSUCH # not_found: names the ID
576
+ ```
577
+
424
578
  Every other command requires an explicit local, non-secret target JSON file.
425
579
  Only the canonical HTTPS AMS origin for that environment is accepted; custom
426
580
  origins require separately verified support. Target and nested qualification keys
@@ -275,6 +275,31 @@ uv run --frozen hc logout
275
275
  otherwise it prompts (with hidden password input). It stores only the email,
276
276
  tenant/environment binding, access/refresh tokens and expiry in the strict
277
277
  owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
278
+
279
+ For CI and agents, log in non-interactively: pass `--email` and pipe the password
280
+ to `--password-stdin` (read as exactly one stdin line, never echoed, never a
281
+ flag value — there is no `--password` option):
282
+
283
+ ```bash
284
+ printf '%s\n' "$HC_AMS_ADMIN_PASSWORD" | \
285
+ uv run --frozen hc login --environment dev --tenant-id TENANT_ID \
286
+ --email "$HC_AMS_ADMIN_EMAIL" --password-stdin
287
+ ```
288
+
289
+ `--email` beats `HC_AMS_ADMIN_EMAIL` and `--password-stdin` beats
290
+ `HC_AMS_ADMIN_PASSWORD`. With `--password-stdin`, the piped line is always the
291
+ password: it is read before any prompt, so it can never be consumed as the
292
+ email, and the email must come from `--email`, `HC_AMS_ADMIN_EMAIL` or (only on
293
+ an interactive terminal) the email prompt. When stdin is not an interactive
294
+ terminal and neither `--password-stdin` nor `HC_AMS_ADMIN_PASSWORD` supplies the
295
+ password, `hc login` refuses with that guidance instead of attempting a hidden
296
+ prompt (no `GetPassWarning`, no echoed password); a missing email on a
297
+ non-interactive stdin (or a process with no stdin stream at all) is refused the
298
+ same way, naming `--email`/`--password-stdin`; every refusal happens before any
299
+ login request. `--status` cannot be combined with `--email` or `--password-stdin`.
300
+ Even on an interactive terminal, `--password-stdin` suppresses terminal echo
301
+ while the single line is read and restores it afterwards; a stdin stream that is
302
+ closed or errors mid-read is refused cleanly (exit 2, never a traceback).
278
303
  AMS commands may omit both target flags while this session is present, except
279
304
  that a `prod` session still needs `--environment prod` on every command. Explicit
280
305
  flags and target files must match it exactly. A session within 60 seconds of
@@ -302,11 +327,22 @@ uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
302
327
  Read-only agent listing, inspection and export take flags instead of a target
303
328
  file; both may be omitted together when a matching saved session exists. Without
304
329
  one, they log in as tenant admin using `HC_AMS_ADMIN_EMAIL` /
305
- `HC_AMS_ADMIN_PASSWORD`. Stdout carries bounded metadata only; `adapter` shows
330
+ `HC_AMS_ADMIN_PASSWORD`. They emit the same versioned result envelope as the
331
+ other read domains (`operation`, `outcome`, `schema_version`, `target`, `data`;
332
+ no `verification` block on reads); failures emit one categorized JSON error
333
+ envelope on stderr with the same categories (not found, target mismatch,
334
+ refusal, logged out), not the plain-text `Error: …` lines of earlier releases.
335
+ `agents list` carries bounded projections under `data.agents` plus a `page`
336
+ block; `agents get` carries the bounded projection under `data.agent`. The
337
+ legacy top-level `{"agents": …, "environment": …, "tenant_id": …}` shape is
338
+ intentionally replaced by this envelope — scripts must read the new shape.
339
+ Stdout carries bounded metadata only; `adapter` shows
306
340
  whether an agent runs from the `file` or `db` config adapter (`unknown` when the
307
341
  server does not say). `export` first re-checks the agent/tenant identity, then
308
342
  saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
309
- file, refusing (and leaving no file) when the body is not valid UTF-8. `export`
343
+ file (exact bytes preserved), refusing (and leaving no file) when the body is
344
+ not valid UTF-8; its stdout reports the envelope with the file `path`, `format`
345
+ and `sha256` under `data` and the `agent_id` in `target`. `export`
310
346
  needs an AMS build that serves `/agents/{agent_id}/configuration/export`; that
311
347
  route is not yet deployed to dev:
312
348
 
@@ -317,6 +353,23 @@ uv run --frozen hc ams agents export --environment dev --tenant-id TENANT_ID --a
317
353
  --format yaml --out agent.yaml
318
354
  ```
319
355
 
356
+ For example, `hc ams agents list` succeeds with (formatted for readability):
357
+
358
+ ```json
359
+ {"schema_version": 1, "operation": "agents.list", "outcome": "succeeded",
360
+ "target": {"environment": "dev", "tenant_id": "TENANT_ID"},
361
+ "data": {"agents": [{"id": "AGENT_ID", "name": "…", "adapter": "file", "…": "…"}],
362
+ "page": {"kind": "unpaged", "returned_count": 1, "continuation": null}}}
363
+ ```
364
+
365
+ and a missing `--agent-id` fails with one envelope on stderr and exit 1:
366
+
367
+ ```json
368
+ {"schema_version": 1, "operation": "agents.get", "outcome": "failed",
369
+ "target": {"environment": "dev", "tenant_id": "TENANT_ID", "agent_id": "AGENT_ID"},
370
+ "error": {"category": "not_found", "message": "Agent 'AGENT_ID' was not found."}}
371
+ ```
372
+
320
373
  Read-only department listing and inspection use the same explicit tenant-admin
321
374
  targeting. They emit one versioned JSON result on stdout; failures emit one
322
375
  categorized JSON result on stderr. Department descriptions, creator identities,
@@ -403,6 +456,107 @@ additional data. Guardrail reads refuse target files, root credentials or base
403
456
  URLs, raw output, blank or dot-segment guardrail IDs, and installed SDKs without
404
457
  both guardrail methods before tenant-admin login or catalog reads.
405
458
 
459
+ Read-only tool catalog listing and inspection are complete and unpaged. The
460
+ bounded projection includes only IDs, names, read-only and confirmation flags,
461
+ agent types, required scopes, audit events, and activity labels:
462
+
463
+ ```bash
464
+ uv run --frozen hc ams tools list --environment dev --tenant-id TENANT_ID
465
+ uv run --frozen hc ams tools get --environment dev --tenant-id TENANT_ID \
466
+ --tool-id TOOL_ID
467
+ ```
468
+
469
+ When both target flags are omitted, these commands may use the matching saved
470
+ tenant-admin session. Explicit flags must match that session. Tool output never
471
+ includes descriptions, parameters, input schemas, PIP or connector routing, or
472
+ arbitrary response fields. The CLI does not execute, assign, create, update, or
473
+ delete tools; join agents or skills; or claim assignment, availability,
474
+ authorization, deployment, serving, or runtime effectiveness. It makes exactly
475
+ one catalog GET after tenant-admin login and does not paginate, retry, cache,
476
+ join, or infer additional data. Tool reads refuse target files, root credentials
477
+ or base URLs, raw output, blank or dot-segment IDs, and installed SDKs without
478
+ both tool methods before tenant-admin login or catalog reads.
479
+
480
+ Read-only data-source catalog listing and inspection are complete and unpaged.
481
+ The bounded projection includes only stable IDs and names:
482
+
483
+ ```bash
484
+ uv run --frozen hc ams data-sources list --environment dev --tenant-id TENANT_ID
485
+ uv run --frozen hc ams data-sources get --environment dev --tenant-id TENANT_ID \
486
+ --data-source-id DATA_SOURCE_ID
487
+ ```
488
+
489
+ When both target flags are omitted, these commands may use the matching saved
490
+ tenant-admin session. Explicit flags must match that session. Data-source output
491
+ never includes descriptions or arbitrary response fields and does not imply
492
+ access, patient consent, record availability, assignment, authorization, or
493
+ runtime effectiveness. The CLI does not create, update, delete, assign, access
494
+ records through, or execute data sources. It makes exactly one catalog GET after
495
+ tenant-admin login and does not paginate, retry, cache, truncate, join, or infer
496
+ additional data. Data-source reads refuse target files, root credentials or base
497
+ URLs, raw output, blank or dot-segment IDs, and installed SDKs without both
498
+ data-source methods before tenant-admin login or catalog reads.
499
+
500
+ ### AMS output modes
501
+
502
+ The read commands (`hc ams skills|tools|policies|guardrails|data-sources|teams|departments list|get`
503
+ and `hc ams organization hierarchy`) render in two modes (`--output` on the root
504
+ `hc` command; `pretty` is the default):
505
+
506
+ - **`pretty`** (default): a compact human view. Lists print an `ID`/`NAME`
507
+ table with an entry count; a single record prints `key: value` lines; the
508
+ organization hierarchy prints an indented tree. Each failed AMS command
509
+ prints one stderr line, `error: <message> (<category>)` (flag/argument
510
+ usage errors from Click print Click's standard usage message instead).
511
+ - **`json`**: the stable result envelope (below) on stdout. When stdout is not
512
+ a terminal, the envelope is printed as one compact line; on a terminal it is
513
+ indented. Errors keep the structured JSON envelope on stderr.
514
+
515
+ The JSON envelope carries `schema_version`, `operation`, `target`, `outcome`,
516
+ and either `data` (succeeded) or `error: {category, message}` (failed). On
517
+ read-only operations the envelope no longer includes the `verification` block:
518
+ every field there is `not_applicable` on a read, so it was boilerplate. If a
519
+ future mutating operation reintroduces it, `verification` reappears only with
520
+ non-`not_applicable` content. Everything else about the envelope — field names,
521
+ ordering semantics, categories — is unchanged and stays parseable.
522
+
523
+ `--raw`/`--output raw` stays refused for AMS (`Raw output is refused for AMS.`),
524
+ and refusals keep the JSON error envelope on stderr.
525
+
526
+ ```bash
527
+ hc ams skills list --environment dev --tenant-id TENANT_ID # pretty table
528
+ hc ams skills get --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID # key: value
529
+ hc --output json ams skills list --environment dev --tenant-id TENANT_ID # compact envelope
530
+ hc --output json ams skills list --environment dev --tenant-id TENANT_ID | jq .data
531
+ ```
532
+
533
+ ### What `hc ams` errors report
534
+
535
+ In `json` and `raw` modes each failed AMS command prints one JSON object on stderr with
536
+ an `error.category` and a message naming the real cause; in `pretty` mode the
537
+ same message is exactly one stderr line, `error: <message> (<category>)`:
538
+
539
+ - Local refusals keep their specific message and exit `1` with category
540
+ `validation`: `Raw output is refused for AMS.`, `AMS target does not match the
541
+ saved session; stopped before authentication.`, identity mismatches
542
+ (`Skill/tenant identity mismatch; stopped.`), and dot-segment ID refusals.
543
+ - No saved session and no target flags: category `authentication` with
544
+ `Not logged in; run \`hc login --environment <env> --tenant-id <tenant>\`.`
545
+ - A `get` whose requested ID does not exist (HTTP 404): category `not_found`
546
+ naming the ID, e.g. `Skill 'nosuch' was not found.` (a 404 on `list` stays
547
+ `validation`; departments keeps its legacy contract for all read statuses).
548
+ - HTTP 401/403 map to `authentication`/`authorization`; 409 to `conflict`;
549
+ network failures to `transport`; local output failures to `local_io`.
550
+ - Any unexpected internal error is category `internal` with a generic message;
551
+ it is never labeled `validation` and never leaks exception details.
552
+
553
+ ```bash
554
+ hc --raw ams skills list --environment dev --tenant-id TENANT_ID # validation: raw refused
555
+ hc ams skills list --tenant-id OTHER_TENANT # validation: target mismatch
556
+ hc ams skills list # authentication: not logged in
557
+ hc ams skills get --skill-id NOSUCH # not_found: names the ID
558
+ ```
559
+
406
560
  Every other command requires an explicit local, non-secret target JSON file.
407
561
  Only the canonical HTTPS AMS origin for that environment is accepted; custom
408
562
  origins require separately verified support. Target and nested qualification keys
@@ -6,11 +6,10 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
 
9
- from ..compatibility import safe_errors
10
9
  from ..context import AmsDependencies, pass_ams, preflight_agents
11
- from ..output import metadata
10
+ from ..errors import run_domain
12
11
  from ..session import resolve_tenant_target
13
- from .operations import AgentReadRefused, export_agent, get_agent, list_agents
12
+ from .operations import export_agent, get_agent, list_agents
14
13
 
15
14
 
16
15
  def _environment(function):
@@ -25,22 +24,21 @@ def _agent(function):
25
24
  return click.option("--agent-id", required=True)(function)
26
25
 
27
26
 
28
- def _run(operation, *args):
29
- try:
30
- return operation(*args)
31
- except AgentReadRefused as exc:
32
- raise click.ClickException(str(exc)) from None
33
-
34
-
35
27
  def create_list_command(dependencies: AmsDependencies) -> click.Command:
36
28
  @click.command("list")
37
29
  @_environment
38
30
  @_tenant
39
31
  @pass_ams
40
- @safe_errors
41
32
  def command(invocation, environment: str | None, tenant_id: str | None) -> None:
42
- preflight_agents(invocation)
43
- metadata(_run(list_agents, dependencies, resolve_tenant_target(environment, tenant_id)))
33
+ target_data = {"environment": environment, "tenant_id": tenant_id}
34
+
35
+ def execute():
36
+ target = resolve_tenant_target(environment, tenant_id)
37
+ target_data.update(environment=target.environment, tenant_id=target.tenant_id)
38
+ preflight_agents(invocation)
39
+ return list_agents(dependencies, target)
40
+
41
+ run_domain("agents.list", target_data, execute)
44
42
 
45
43
  return command
46
44
 
@@ -51,10 +49,16 @@ def create_get_command(dependencies: AmsDependencies) -> click.Command:
51
49
  @_tenant
52
50
  @_agent
53
51
  @pass_ams
54
- @safe_errors
55
52
  def command(invocation, environment: str | None, tenant_id: str | None, agent_id: str) -> None:
56
- preflight_agents(invocation)
57
- metadata(_run(get_agent, dependencies, resolve_tenant_target(environment, tenant_id), agent_id))
53
+ target_data = {"environment": environment, "tenant_id": tenant_id, "agent_id": agent_id}
54
+
55
+ def execute():
56
+ target = resolve_tenant_target(environment, tenant_id)
57
+ target_data.update(environment=target.environment, tenant_id=target.tenant_id)
58
+ preflight_agents(invocation)
59
+ return get_agent(dependencies, target, agent_id)
60
+
61
+ run_domain("agents.get", target_data, execute)
58
62
 
59
63
  return command
60
64
 
@@ -67,7 +71,6 @@ def create_export_command(dependencies: AmsDependencies) -> click.Command:
67
71
  @click.option("--out", required=True, type=click.Path(path_type=Path, dir_okay=False))
68
72
  @click.option("--format", "export_format", type=click.Choice(["json", "yaml"]), default="json", show_default=True)
69
73
  @pass_ams
70
- @safe_errors
71
74
  def command(
72
75
  invocation,
73
76
  environment: str | None,
@@ -76,8 +79,14 @@ def create_export_command(dependencies: AmsDependencies) -> click.Command:
76
79
  out: Path,
77
80
  export_format: str,
78
81
  ) -> None:
79
- preflight_agents(invocation)
80
- target = resolve_tenant_target(environment, tenant_id)
81
- metadata(_run(export_agent, dependencies, target, agent_id, out, export_format))
82
+ target_data = {"environment": environment, "tenant_id": tenant_id, "agent_id": agent_id}
83
+
84
+ def execute():
85
+ target = resolve_tenant_target(environment, tenant_id)
86
+ target_data.update(environment=target.environment, tenant_id=target.tenant_id)
87
+ preflight_agents(invocation)
88
+ return export_agent(dependencies, target, agent_id, out, export_format)
89
+
90
+ run_domain("agents.export", target_data, execute)
82
91
 
83
92
  return command
@@ -7,18 +7,29 @@ from pathlib import Path
7
7
  from typing import Literal
8
8
 
9
9
  from ..context import AmsDependencies
10
+ from ..domain_errors import DomainReadRefused
10
11
  from ..output import private_text_output
12
+ from ..results import succeeded
11
13
  from ..target import TenantTarget
12
14
  from .projections import detail, summary
13
15
 
14
16
 
15
- class AgentReadRefused(ValueError):
16
- """A stable refusal safe for the CLI adapter to expose."""
17
+ class AgentReadRefused(DomainReadRefused):
18
+ """A categorized agent refusal safe for the CLI adapter to expose."""
19
+
20
+ pass
17
21
 
18
22
 
19
23
  def _agent_id(value: str) -> str:
20
24
  if not isinstance(value, str) or not value.strip():
21
- raise AgentReadRefused("Explicit agent ID required.")
25
+ raise AgentReadRefused("validation", "Explicit agent ID required.")
26
+ return value
27
+
28
+
29
+ def _target(target: TenantTarget, agent_id: str | None = None) -> dict:
30
+ value = {"environment": target.environment, "tenant_id": target.tenant_id}
31
+ if agent_id is not None:
32
+ value["agent_id"] = agent_id
22
33
  return value
23
34
 
24
35
 
@@ -26,18 +37,23 @@ def list_agents(dependencies: AmsDependencies, target: TenantTarget) -> dict:
26
37
  with dependencies.authenticated_sdk_factory(target) as sdk:
27
38
  agents = sdk.ams_admin.list_agents()
28
39
  if any(agent.tenant_id != target.tenant_id for agent in agents):
29
- raise AgentReadRefused("Agent/tenant identity mismatch; stopped.")
30
- return {
31
- "environment": target.environment,
32
- "tenant_id": target.tenant_id,
33
- "agents": [summary(agent) for agent in agents],
34
- }
40
+ raise AgentReadRefused("validation", "Agent/tenant identity mismatch; stopped.")
41
+ projections = [summary(agent) for agent in agents]
42
+ return succeeded(
43
+ "agents.list",
44
+ _target(target),
45
+ {
46
+ "agents": projections,
47
+ "page": {"kind": "unpaged", "returned_count": len(projections), "continuation": None},
48
+ },
49
+ read_only=True,
50
+ )
35
51
 
36
52
 
37
53
  def _verified_agent(sdk, target: TenantTarget, agent_id: str):
38
54
  agent = sdk.ams_admin.get_agent(agent_id)
39
55
  if agent is None or agent.id != agent_id or agent.tenant_id != target.tenant_id:
40
- raise AgentReadRefused("Agent/tenant identity mismatch; stopped.")
56
+ raise AgentReadRefused("validation", "Agent/tenant identity mismatch; stopped.")
41
57
  return agent
42
58
 
43
59
 
@@ -45,7 +61,12 @@ def get_agent(dependencies: AmsDependencies, target: TenantTarget, agent_id: str
45
61
  agent_id = _agent_id(agent_id)
46
62
  with dependencies.authenticated_sdk_factory(target) as sdk:
47
63
  agent = _verified_agent(sdk, target, agent_id)
48
- return {"environment": target.environment, "tenant_id": target.tenant_id} | detail(agent)
64
+ return succeeded(
65
+ "agents.get",
66
+ _target(target, agent_id),
67
+ {"agent": detail(agent)},
68
+ read_only=True,
69
+ )
49
70
 
50
71
 
51
72
  def export_agent(
@@ -57,16 +78,18 @@ def export_agent(
57
78
  ) -> dict:
58
79
  agent_id = _agent_id(agent_id)
59
80
  if export_format not in ("json", "yaml"):
60
- raise AgentReadRefused("Export format must be json or yaml.")
81
+ raise AgentReadRefused("validation", "Export format must be json or yaml.")
61
82
  with private_text_output(output) as write, dependencies.authenticated_sdk_factory(target) as sdk:
62
83
  _verified_agent(sdk, target, agent_id)
63
84
  text = sdk.ams_admin.export_agent_configuration(agent_id, format=export_format)
64
85
  write(text)
65
- return {
66
- "environment": target.environment,
67
- "tenant_id": target.tenant_id,
68
- "agent_id": agent_id,
69
- "path": str(output),
70
- "format": export_format,
71
- "sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
72
- }
86
+ return succeeded(
87
+ "agents.export",
88
+ _target(target, agent_id),
89
+ {
90
+ "path": str(output),
91
+ "format": export_format,
92
+ "sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
93
+ },
94
+ read_only=True,
95
+ )
@@ -197,6 +197,48 @@ def preflight_guardrails(invocation: AmsContext, target: TenantTarget) -> None:
197
197
  sdk.close()
198
198
 
199
199
 
200
+ def preflight_tools(invocation: AmsContext, target: TenantTarget) -> None:
201
+ """Validate local tool-read compatibility before login or data reads."""
202
+
203
+ _check_root(invocation)
204
+ if invocation.target_file is not None:
205
+ raise click.ClickException("Tools requires explicit environment/tenant flags, without a target file.")
206
+ sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
207
+ try:
208
+ resource = getattr(getattr(sdk, "ams_admin", None), "tools", None)
209
+ if (
210
+ resource is None
211
+ or not callable(getattr(resource, "list", None))
212
+ or not callable(getattr(resource, "get", None))
213
+ ):
214
+ raise click.ClickException(
215
+ "SDK missing ams_admin tools; use the reviewed SDK version required by this CLI."
216
+ )
217
+ finally:
218
+ sdk.close()
219
+
220
+
221
+ def preflight_data_sources(invocation: AmsContext, target: TenantTarget) -> None:
222
+ """Validate local data-source-read compatibility before login or data reads."""
223
+
224
+ _check_root(invocation)
225
+ if invocation.target_file is not None:
226
+ raise click.ClickException("Data sources requires explicit environment/tenant flags, without a target file.")
227
+ sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
228
+ try:
229
+ resource = getattr(getattr(sdk, "ams_admin", None), "data_sources", None)
230
+ if (
231
+ resource is None
232
+ or not callable(getattr(resource, "list", None))
233
+ or not callable(getattr(resource, "get", None))
234
+ ):
235
+ raise click.ClickException(
236
+ "SDK missing ams_admin data sources; use the reviewed SDK version required by this CLI."
237
+ )
238
+ finally:
239
+ sdk.close()
240
+
241
+
200
242
  def require_target(invocation: AmsContext) -> AmsTarget:
201
243
  """Validate root options and load the explicitly selected AMS target."""
202
244
 
@@ -0,0 +1,5 @@
1
+ """Read-only AMS data-source CLI domain."""
2
+
3
+ from .group import create_data_sources_group
4
+
5
+ __all__ = ["create_data_sources_group"]