healthcloud-cli 3.3.0__tar.gz → 3.4.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 (69) hide show
  1. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/PKG-INFO +101 -15
  2. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/README.md +100 -14
  3. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/commands.py +10 -10
  4. healthcloud_cli-3.4.0/healthcloud_cli/ams/auth.py +91 -0
  5. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/compatibility.py +6 -4
  6. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/context.py +70 -1
  7. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/commands.py +9 -7
  8. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/errors.py +12 -2
  9. healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/__init__.py +5 -0
  10. healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/commands.py +57 -0
  11. healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/group.py +18 -0
  12. healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/operations.py +67 -0
  13. healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/projections.py +18 -0
  14. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/commands.py +6 -5
  15. healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/__init__.py +5 -0
  16. healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/commands.py +57 -0
  17. healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/group.py +18 -0
  18. healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/operations.py +67 -0
  19. healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/projections.py +21 -0
  20. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/registry.py +6 -0
  21. healthcloud_cli-3.4.0/healthcloud_cli/ams/session.py +345 -0
  22. healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/__init__.py +5 -0
  23. healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/commands.py +57 -0
  24. healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/group.py +18 -0
  25. healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/operations.py +67 -0
  26. healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/projections.py +19 -0
  27. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/commands.py +9 -7
  28. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/windows_files.py +51 -2
  29. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/cli.py +61 -1
  30. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/pyproject.toml +1 -1
  31. healthcloud_cli-3.3.0/healthcloud_cli/ams/auth.py +0 -53
  32. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/.gitignore +0 -0
  33. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/hatch_build.py +0 -0
  34. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/__init__.py +0 -0
  35. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/__init__.py +0 -0
  36. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/__init__.py +0 -0
  37. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/group.py +0 -0
  38. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/operations.py +0 -0
  39. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/projections.py +0 -0
  40. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/commands.py +0 -0
  41. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/__init__.py +0 -0
  42. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/group.py +0 -0
  43. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/operations.py +0 -0
  44. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/projections.py +0 -0
  45. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/domain_errors.py +0 -0
  46. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/group.py +0 -0
  47. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/__init__.py +0 -0
  48. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/group.py +0 -0
  49. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/operations.py +0 -0
  50. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/projections.py +0 -0
  51. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/output.py +0 -0
  52. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/results.py +0 -0
  53. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/target.py +0 -0
  54. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/__init__.py +0 -0
  55. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/group.py +0 -0
  56. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/operations.py +0 -0
  57. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/projections.py +0 -0
  58. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/tuning.py +0 -0
  59. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/ui.py +0 -0
  60. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/context.py +0 -0
  61. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/contracts/__init__.py +0 -0
  62. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
  63. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/main.py +0 -0
  64. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/manifest.py +0 -0
  65. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/output.py +0 -0
  66. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/params.py +0 -0
  67. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/util.py +0 -0
  68. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/scripts/__init__.py +0 -0
  69. {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/scripts/bundle_manifest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: healthcloud-cli
3
- Version: 3.3.0
3
+ Version: 3.4.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
@@ -69,10 +69,10 @@ _HC_COMPLETE=bash_complete COMP_WORDS="hc ams inspect --o" COMP_CWORD=3 uv run -
69
69
  ```
70
70
 
71
71
  Operational validation starts only when Click invokes an AMS leaf callback.
72
- `discover` then validates its explicit environment and tenant arguments; the
73
- other leaves require `--target-file` and apply the existing root, target, and
74
- authentication checks described below. Deferring preflight does not relax
75
- those safety requirements.
72
+ `discover`, `agents`, `departments`, `teams`, `organization`, `skills`, and `policies` then
73
+ validate their explicit environment and tenant arguments; the remaining leaves
74
+ require `--target-file`. All apply the root, target, and authentication checks
75
+ described below. Deferring preflight does not relax those safety requirements.
76
76
 
77
77
  The bundled tool manifest (`healthcloud_cli/contracts/mcp-tools.manifest.json`)
78
78
  is a copy of `packages/cli/contracts/mcp-tools.manifest.json`. Re-sync it
@@ -280,16 +280,47 @@ uv sync --frozen --extra dev
280
280
  uv run --frozen hc ams --help
281
281
  ```
282
282
 
283
- Bootstrap candidate IDs with the public SDK discovery endpoint. Both flags are
284
- required; no MCP defaults, admin login, or automatic candidate selection are used:
283
+ Create one private tenant-admin session before interactive AMS work, inspect its
284
+ non-secret binding, and remove it when finished:
285
+
286
+ ```bash
287
+ uv run --frozen hc login --environment dev --tenant-id TENANT_ID
288
+ uv run --frozen hc login --status
289
+ uv run --frozen hc logout
290
+ ```
291
+
292
+ `hc login` reads `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` when present,
293
+ otherwise it prompts (with hidden password input). It stores only the email,
294
+ tenant/environment binding, access/refresh tokens and expiry in the strict
295
+ owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
296
+ AMS commands may omit both target flags while this session is present, except
297
+ that a `prod` session still needs `--environment prod` on every command. Explicit
298
+ flags and target files must match it exactly. A session within 60 seconds of
299
+ expiry is refreshed once before the operation, under a lock so concurrent
300
+ commands never spend the same refresh token (a second command waits up to 30 seconds, then stops), and
301
+ atomically replaced. Refresh
302
+ failure stops without an AMS call or password fallback and asks for a new login.
303
+ If the refreshed session cannot be saved, the current command still finishes and
304
+ prints a warning to run `hc login` again.
305
+
306
+ `~/.healthcloud` and `~/.healthcloud/cli` must be directories you own with mode
307
+ 700; the CLI creates them that way, and refuses (naming the directory and the
308
+ `chmod 700` fix) if an existing one is shared. `hc logout` deletes the local file
309
+ only and reports `"server_session_revoked": false`: issued tokens stay valid
310
+ until they expire.
311
+
312
+ Bootstrap candidate IDs with the public SDK discovery endpoint. Without a saved
313
+ session both flags are required; a saved session supplies both together. No MCP
314
+ defaults, admin login, or automatic candidate selection are used:
285
315
 
286
316
  ```bash
287
317
  uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
288
318
  ```
289
319
 
290
- Read-only agent listing, inspection and export also take explicit flags instead
291
- of a target file, and log in as tenant admin (`HC_AMS_ADMIN_EMAIL` /
292
- `HC_AMS_ADMIN_PASSWORD`). Stdout carries bounded metadata only; `adapter` shows
320
+ Read-only agent listing, inspection and export take flags instead of a target
321
+ file; both may be omitted together when a matching saved session exists. Without
322
+ one, they log in as tenant admin using `HC_AMS_ADMIN_EMAIL` /
323
+ `HC_AMS_ADMIN_PASSWORD`. Stdout carries bounded metadata only; `adapter` shows
293
324
  whether an agent runs from the `file` or `db` config adapter (`unknown` when the
294
325
  server does not say). `export` first re-checks the agent/tenant identity, then
295
326
  saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
@@ -335,6 +366,61 @@ or inferring missing relationships:
335
366
  uv run --frozen hc ams organization hierarchy --environment dev --tenant-id TENANT_ID
336
367
  ```
337
368
 
369
+ Read-only skill catalog listing and inspection are complete and unpaged. The
370
+ bounded projection includes only skill IDs, names, tool IDs, and audit timestamps;
371
+ it omits descriptions, tenant identity, creator identity, arbitrary response
372
+ fields, and tool details. Tool-ID order is preserved:
373
+
374
+ ```bash
375
+ uv run --frozen hc ams skills list --environment dev --tenant-id TENANT_ID
376
+ uv run --frozen hc ams skills get --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID
377
+ ```
378
+
379
+ The CLI does not create, update, delete, or assign skills. Skill commands do not
380
+ read guardrails. They make exactly one catalog GET after tenant-admin login
381
+ and does not paginate, retry, cache, join, or infer additional catalog data.
382
+ Skill reads refuse target files, root credentials or base URLs, raw output,
383
+ blank or dot-segment skill IDs, and installed SDKs that do not provide both
384
+ skill list and get methods. These local refusals happen before tenant-admin login
385
+ or catalog reads.
386
+
387
+ Read-only policy catalog listing and inspection are complete and unpaged. The
388
+ bounded projection includes only policy IDs, names, department IDs, guardrail
389
+ IDs, active status, and audit timestamps. It never emits protected policy
390
+ content, descriptions, tenant or creator identity, arbitrary response fields,
391
+ or joined department/guardrail details. Guardrail-ID order is preserved:
392
+
393
+ ```bash
394
+ uv run --frozen hc ams policies list --environment dev --tenant-id TENANT_ID
395
+ uv run --frozen hc ams policies get --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
396
+ ```
397
+
398
+ The CLI does not export protected policy content; create, update, delete, or
399
+ assign policies; or join guardrail records. It makes exactly one catalog GET after
400
+ tenant-admin login and does not paginate, retry, cache, join, or infer referenced
401
+ resources. Policy reads refuse target files, root credentials or base URLs, raw
402
+ output, blank or dot-segment policy IDs, and installed SDKs without both policy
403
+ methods before tenant-admin login or catalog reads. Successful reads make no
404
+ assignment, deployment, serving, enforcement, or effectiveness claim.
405
+
406
+ Read-only guardrail catalog listing and inspection are complete and unpaged.
407
+ The bounded projection includes only guardrail IDs, names, and audit timestamps.
408
+ It never emits protected rules, descriptions, tenant or creator identity,
409
+ arbitrary response fields, or joined policy/agent details:
410
+
411
+ ```bash
412
+ uv run --frozen hc ams guardrails list --environment dev --tenant-id TENANT_ID
413
+ uv run --frozen hc ams guardrails get --environment dev --tenant-id TENANT_ID \
414
+ --guardrail-id GUARDRAIL_ID
415
+ ```
416
+
417
+ The CLI does not export rules; create, update, delete, assign, deploy, or enforce
418
+ guardrails; or claim they are active or effective. It makes exactly one catalog
419
+ GET after tenant-admin login and does not paginate, retry, cache, join, or infer
420
+ additional data. Guardrail reads refuse target files, root credentials or base
421
+ URLs, raw output, blank or dot-segment guardrail IDs, and installed SDKs without
422
+ both guardrail methods before tenant-admin login or catalog reads.
423
+
338
424
  Every other command requires an explicit local, non-secret target JSON file.
339
425
  Only the canonical HTTPS AMS origin for that environment is accepted; custom
340
426
  origins require separately verified support. Target and nested qualification keys
@@ -403,11 +489,11 @@ configuration and recheck it before apply and selected-field restore. This is an
403
489
  operator prerequisite: the current CLI does not automatically attest deployment
404
490
  or all-writer coverage, and the record adds no server authorization boundary.
405
491
 
406
- Credentials enter only through `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD`
407
- in the current process, intended for composition with the existing
408
- `onepassword-env` wrapper. Each invocation logs in through `HCTenantAdminSDK`;
409
- tokens remain private in memory. There is no token cache, fallback identity,
410
- secret argument, resolved env file, or browser credential bridge. Root MCP
492
+ For CI and scripts with no saved session, credentials may still enter through
493
+ `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` in the current process, intended
494
+ for composition with the existing `onepassword-env` wrapper; that compatibility
495
+ path logs in once per invocation. There is no fallback identity, secret argument,
496
+ arbitrary token import, resolved env file, keychain, or browser credential bridge. Root MCP
411
497
  `--access-token`, `--base-url`, their environment equivalents, and token/base-URL
412
498
  profiles are refused for AMS. Root profile environment values do not select AMS.
413
499
 
@@ -51,10 +51,10 @@ _HC_COMPLETE=bash_complete COMP_WORDS="hc ams inspect --o" COMP_CWORD=3 uv run -
51
51
  ```
52
52
 
53
53
  Operational validation starts only when Click invokes an AMS leaf callback.
54
- `discover` then validates its explicit environment and tenant arguments; the
55
- other leaves require `--target-file` and apply the existing root, target, and
56
- authentication checks described below. Deferring preflight does not relax
57
- those safety requirements.
54
+ `discover`, `agents`, `departments`, `teams`, `organization`, `skills`, and `policies` then
55
+ validate their explicit environment and tenant arguments; the remaining leaves
56
+ require `--target-file`. All apply the root, target, and authentication checks
57
+ described below. Deferring preflight does not relax those safety requirements.
58
58
 
59
59
  The bundled tool manifest (`healthcloud_cli/contracts/mcp-tools.manifest.json`)
60
60
  is a copy of `packages/cli/contracts/mcp-tools.manifest.json`. Re-sync it
@@ -262,16 +262,47 @@ uv sync --frozen --extra dev
262
262
  uv run --frozen hc ams --help
263
263
  ```
264
264
 
265
- Bootstrap candidate IDs with the public SDK discovery endpoint. Both flags are
266
- required; no MCP defaults, admin login, or automatic candidate selection are used:
265
+ Create one private tenant-admin session before interactive AMS work, inspect its
266
+ non-secret binding, and remove it when finished:
267
+
268
+ ```bash
269
+ uv run --frozen hc login --environment dev --tenant-id TENANT_ID
270
+ uv run --frozen hc login --status
271
+ uv run --frozen hc logout
272
+ ```
273
+
274
+ `hc login` reads `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` when present,
275
+ otherwise it prompts (with hidden password input). It stores only the email,
276
+ tenant/environment binding, access/refresh tokens and expiry in the strict
277
+ owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
278
+ AMS commands may omit both target flags while this session is present, except
279
+ that a `prod` session still needs `--environment prod` on every command. Explicit
280
+ flags and target files must match it exactly. A session within 60 seconds of
281
+ expiry is refreshed once before the operation, under a lock so concurrent
282
+ commands never spend the same refresh token (a second command waits up to 30 seconds, then stops), and
283
+ atomically replaced. Refresh
284
+ failure stops without an AMS call or password fallback and asks for a new login.
285
+ If the refreshed session cannot be saved, the current command still finishes and
286
+ prints a warning to run `hc login` again.
287
+
288
+ `~/.healthcloud` and `~/.healthcloud/cli` must be directories you own with mode
289
+ 700; the CLI creates them that way, and refuses (naming the directory and the
290
+ `chmod 700` fix) if an existing one is shared. `hc logout` deletes the local file
291
+ only and reports `"server_session_revoked": false`: issued tokens stay valid
292
+ until they expire.
293
+
294
+ Bootstrap candidate IDs with the public SDK discovery endpoint. Without a saved
295
+ session both flags are required; a saved session supplies both together. No MCP
296
+ defaults, admin login, or automatic candidate selection are used:
267
297
 
268
298
  ```bash
269
299
  uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
270
300
  ```
271
301
 
272
- Read-only agent listing, inspection and export also take explicit flags instead
273
- of a target file, and log in as tenant admin (`HC_AMS_ADMIN_EMAIL` /
274
- `HC_AMS_ADMIN_PASSWORD`). Stdout carries bounded metadata only; `adapter` shows
302
+ Read-only agent listing, inspection and export take flags instead of a target
303
+ file; both may be omitted together when a matching saved session exists. Without
304
+ one, they log in as tenant admin using `HC_AMS_ADMIN_EMAIL` /
305
+ `HC_AMS_ADMIN_PASSWORD`. Stdout carries bounded metadata only; `adapter` shows
275
306
  whether an agent runs from the `file` or `db` config adapter (`unknown` when the
276
307
  server does not say). `export` first re-checks the agent/tenant identity, then
277
308
  saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
@@ -317,6 +348,61 @@ or inferring missing relationships:
317
348
  uv run --frozen hc ams organization hierarchy --environment dev --tenant-id TENANT_ID
318
349
  ```
319
350
 
351
+ Read-only skill catalog listing and inspection are complete and unpaged. The
352
+ bounded projection includes only skill IDs, names, tool IDs, and audit timestamps;
353
+ it omits descriptions, tenant identity, creator identity, arbitrary response
354
+ fields, and tool details. Tool-ID order is preserved:
355
+
356
+ ```bash
357
+ uv run --frozen hc ams skills list --environment dev --tenant-id TENANT_ID
358
+ uv run --frozen hc ams skills get --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID
359
+ ```
360
+
361
+ The CLI does not create, update, delete, or assign skills. Skill commands do not
362
+ read guardrails. They make exactly one catalog GET after tenant-admin login
363
+ and does not paginate, retry, cache, join, or infer additional catalog data.
364
+ Skill reads refuse target files, root credentials or base URLs, raw output,
365
+ blank or dot-segment skill IDs, and installed SDKs that do not provide both
366
+ skill list and get methods. These local refusals happen before tenant-admin login
367
+ or catalog reads.
368
+
369
+ Read-only policy catalog listing and inspection are complete and unpaged. The
370
+ bounded projection includes only policy IDs, names, department IDs, guardrail
371
+ IDs, active status, and audit timestamps. It never emits protected policy
372
+ content, descriptions, tenant or creator identity, arbitrary response fields,
373
+ or joined department/guardrail details. Guardrail-ID order is preserved:
374
+
375
+ ```bash
376
+ uv run --frozen hc ams policies list --environment dev --tenant-id TENANT_ID
377
+ uv run --frozen hc ams policies get --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
378
+ ```
379
+
380
+ The CLI does not export protected policy content; create, update, delete, or
381
+ assign policies; or join guardrail records. It makes exactly one catalog GET after
382
+ tenant-admin login and does not paginate, retry, cache, join, or infer referenced
383
+ resources. Policy reads refuse target files, root credentials or base URLs, raw
384
+ output, blank or dot-segment policy IDs, and installed SDKs without both policy
385
+ methods before tenant-admin login or catalog reads. Successful reads make no
386
+ assignment, deployment, serving, enforcement, or effectiveness claim.
387
+
388
+ Read-only guardrail catalog listing and inspection are complete and unpaged.
389
+ The bounded projection includes only guardrail IDs, names, and audit timestamps.
390
+ It never emits protected rules, descriptions, tenant or creator identity,
391
+ arbitrary response fields, or joined policy/agent details:
392
+
393
+ ```bash
394
+ uv run --frozen hc ams guardrails list --environment dev --tenant-id TENANT_ID
395
+ uv run --frozen hc ams guardrails get --environment dev --tenant-id TENANT_ID \
396
+ --guardrail-id GUARDRAIL_ID
397
+ ```
398
+
399
+ The CLI does not export rules; create, update, delete, assign, deploy, or enforce
400
+ guardrails; or claim they are active or effective. It makes exactly one catalog
401
+ GET after tenant-admin login and does not paginate, retry, cache, join, or infer
402
+ additional data. Guardrail reads refuse target files, root credentials or base
403
+ URLs, raw output, blank or dot-segment guardrail IDs, and installed SDKs without
404
+ both guardrail methods before tenant-admin login or catalog reads.
405
+
320
406
  Every other command requires an explicit local, non-secret target JSON file.
321
407
  Only the canonical HTTPS AMS origin for that environment is accepted; custom
322
408
  origins require separately verified support. Target and nested qualification keys
@@ -385,11 +471,11 @@ configuration and recheck it before apply and selected-field restore. This is an
385
471
  operator prerequisite: the current CLI does not automatically attest deployment
386
472
  or all-writer coverage, and the record adds no server authorization boundary.
387
473
 
388
- Credentials enter only through `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD`
389
- in the current process, intended for composition with the existing
390
- `onepassword-env` wrapper. Each invocation logs in through `HCTenantAdminSDK`;
391
- tokens remain private in memory. There is no token cache, fallback identity,
392
- secret argument, resolved env file, or browser credential bridge. Root MCP
474
+ For CI and scripts with no saved session, credentials may still enter through
475
+ `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` in the current process, intended
476
+ for composition with the existing `onepassword-env` wrapper; that compatibility
477
+ path logs in once per invocation. There is no fallback identity, secret argument,
478
+ arbitrary token import, resolved env file, keychain, or browser credential bridge. Root MCP
393
479
  `--access-token`, `--base-url`, their environment equivalents, and token/base-URL
394
480
  profiles are refused for AMS. Root profile environment values do not select AMS.
395
481
 
@@ -9,16 +9,16 @@ import click
9
9
  from ..compatibility import safe_errors
10
10
  from ..context import AmsDependencies, pass_ams, preflight_agents
11
11
  from ..output import metadata
12
- from ..target import TenantTarget
12
+ from ..session import resolve_tenant_target
13
13
  from .operations import AgentReadRefused, export_agent, get_agent, list_agents
14
14
 
15
15
 
16
16
  def _environment(function):
17
- return click.option("--environment", type=click.Choice(["dev", "uat", "prod"]), required=True)(function)
17
+ return click.option("--environment", type=click.Choice(["dev", "uat", "prod"]))(function)
18
18
 
19
19
 
20
20
  def _tenant(function):
21
- return click.option("--tenant-id", required=True)(function)
21
+ return click.option("--tenant-id")(function)
22
22
 
23
23
 
24
24
  def _agent(function):
@@ -38,9 +38,9 @@ def create_list_command(dependencies: AmsDependencies) -> click.Command:
38
38
  @_tenant
39
39
  @pass_ams
40
40
  @safe_errors
41
- def command(invocation, environment: str, tenant_id: str) -> None:
41
+ def command(invocation, environment: str | None, tenant_id: str | None) -> None:
42
42
  preflight_agents(invocation)
43
- metadata(_run(list_agents, dependencies, TenantTarget(environment, tenant_id)))
43
+ metadata(_run(list_agents, dependencies, resolve_tenant_target(environment, tenant_id)))
44
44
 
45
45
  return command
46
46
 
@@ -52,9 +52,9 @@ def create_get_command(dependencies: AmsDependencies) -> click.Command:
52
52
  @_agent
53
53
  @pass_ams
54
54
  @safe_errors
55
- def command(invocation, environment: str, tenant_id: str, agent_id: str) -> None:
55
+ def command(invocation, environment: str | None, tenant_id: str | None, agent_id: str) -> None:
56
56
  preflight_agents(invocation)
57
- metadata(_run(get_agent, dependencies, TenantTarget(environment, tenant_id), agent_id))
57
+ metadata(_run(get_agent, dependencies, resolve_tenant_target(environment, tenant_id), agent_id))
58
58
 
59
59
  return command
60
60
 
@@ -70,14 +70,14 @@ def create_export_command(dependencies: AmsDependencies) -> click.Command:
70
70
  @safe_errors
71
71
  def command(
72
72
  invocation,
73
- environment: str,
74
- tenant_id: str,
73
+ environment: str | None,
74
+ tenant_id: str | None,
75
75
  agent_id: str,
76
76
  out: Path,
77
77
  export_format: str,
78
78
  ) -> None:
79
79
  preflight_agents(invocation)
80
- target = TenantTarget(environment, tenant_id)
80
+ target = resolve_tenant_target(environment, tenant_id)
81
81
  metadata(_run(export_agent, dependencies, target, agent_id, out, export_format))
82
82
 
83
83
  return command
@@ -0,0 +1,91 @@
1
+ """Saved-session or environment tenant-admin authentication."""
2
+
3
+ import os
4
+ from contextlib import contextmanager
5
+ from datetime import UTC, datetime
6
+
7
+ import click
8
+
9
+ from healthcloud import HCSDK
10
+ from healthcloud_tenantadmin import HCTenantAdminSDK
11
+
12
+ from .session import load_session, refreshed_session, save_session, session_lock
13
+ from .target import AmsTarget, TenantTarget
14
+
15
+
16
+ @contextmanager
17
+ def authenticated_sdk(target: AmsTarget | TenantTarget):
18
+ sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
19
+ try:
20
+ if not hasattr(sdk, "ams_admin"):
21
+ raise click.ClickException(
22
+ "SDK missing ams_admin; use the reviewed local editable SDK checkout (see CLI README)."
23
+ )
24
+ if isinstance(target, TenantTarget) and not hasattr(sdk.ams_admin, "list_agents"):
25
+ raise click.ClickException(
26
+ "SDK missing ams_admin agent listing; use the reviewed local editable SDK checkout (see CLI README)."
27
+ )
28
+ session = load_session()
29
+ if session is not None:
30
+ if (target.environment, target.tenant_id) != (session.environment, session.tenant_id):
31
+ raise click.ClickException(
32
+ "AMS target does not match the saved session; stopped before authentication."
33
+ )
34
+ token = _saved_session_token(target)
35
+ else:
36
+ email = os.environ.get("HC_AMS_ADMIN_EMAIL")
37
+ password = os.environ.get("HC_AMS_ADMIN_PASSWORD")
38
+ if not email or not password:
39
+ raise click.ClickException(
40
+ "Run hc login or inject HC_AMS_ADMIN_EMAIL and HC_AMS_ADMIN_PASSWORD into this process."
41
+ )
42
+ with HCTenantAdminSDK(environment=target.environment, tenant_id=target.tenant_id) as admin:
43
+ result = admin.admin.login_tenant_admin(email=email, password=password, tenant_id=target.tenant_id)
44
+ token = result.get("access_token") if isinstance(result, dict) else None
45
+ if not isinstance(token, str) or not token.strip():
46
+ raise click.ClickException("Tenant-admin login returned no access token; stopped without fallback.")
47
+ sdk.set_access_token(token)
48
+ try:
49
+ yield sdk
50
+ finally:
51
+ sdk.set_access_token(None)
52
+
53
+ finally:
54
+ sdk.close()
55
+
56
+
57
+ def _saved_session_token(target: AmsTarget | TenantTarget) -> str:
58
+ with session_lock():
59
+ # Re-read under the lock: another command may have refreshed while this one waited.
60
+ session = load_session()
61
+ if session is None or (target.environment, target.tenant_id) != (session.environment, session.tenant_id):
62
+ raise click.ClickException("Saved AMS session changed; run hc login.")
63
+ if not session.needs_refresh(datetime.now(UTC)):
64
+ return session.access_token
65
+ try:
66
+ with HCTenantAdminSDK(environment=target.environment, tenant_id=target.tenant_id) as admin:
67
+ result = admin.admin.refresh_tenant_admin_token(
68
+ refresh_token=session.refresh_token, tenant_id=target.tenant_id
69
+ )
70
+ except Exception:
71
+ raise click.ClickException("Saved AMS session refresh failed; run hc login.") from None
72
+ session = refreshed_session(session, result, datetime.now(UTC))
73
+ try:
74
+ save_session(session)
75
+ except click.ClickException:
76
+ # The refresh already succeeded; the old refresh token may be spent. Finish this
77
+ # command with the new access token and say the saved session is no longer usable.
78
+ click.echo(
79
+ "Warning: refreshed AMS session could not be saved; run hc login before the next command.",
80
+ err=True,
81
+ )
82
+ return session.access_token
83
+
84
+
85
+ def selected_agent(sdk, target: AmsTarget):
86
+ agent = sdk.ams_admin.get_agent(target.agent_id)
87
+ if agent is None or agent.id != target.agent_id or agent.tenant_id != target.tenant_id:
88
+ raise click.ClickException("Agent/tenant identity mismatch; stopped.")
89
+ if type(agent.config_version) is not int or agent.config_version < 1:
90
+ raise click.ClickException("Agent configuration version unavailable; stopped.")
91
+ return agent
@@ -71,15 +71,17 @@ def default_dependencies() -> AmsDependencies:
71
71
 
72
72
  def create_discover_command(dependencies: AmsDependencies) -> click.Command:
73
73
  @click.command("discover")
74
- @click.option("--environment", type=click.Choice(["dev", "uat", "prod"]), required=True)
75
- @click.option("--tenant-id", required=True)
74
+ @click.option("--environment", type=click.Choice(["dev", "uat", "prod"]))
75
+ @click.option("--tenant-id")
76
76
  @pass_ams
77
77
  @safe_errors
78
78
  def discover(invocation, environment, tenant_id):
79
79
  """Public candidate IDs only; never infer or select an agent."""
80
80
  preflight_discovery(invocation)
81
- if not tenant_id.strip():
82
- raise click.ClickException("Explicit tenant ID required.")
81
+ from .session import resolve_tenant_target
82
+
83
+ target = resolve_tenant_target(environment, tenant_id)
84
+ environment, tenant_id = target.environment, target.tenant_id
83
85
  with dependencies.public_sdk_factory(environment=environment) as sdk:
84
86
  agents = sdk.ams.get_agents_for_tenant(tenant_id)
85
87
  if not isinstance(agents, dict) or any(
@@ -134,10 +134,79 @@ def preflight_organization(invocation: AmsContext, target: TenantTarget) -> None
134
134
  sdk.close()
135
135
 
136
136
 
137
+ def preflight_skills(invocation: AmsContext, target: TenantTarget) -> None:
138
+ """Validate local skill-read compatibility before login or data reads."""
139
+
140
+ _check_root(invocation)
141
+ if invocation.target_file is not None:
142
+ raise click.ClickException("Skills requires explicit environment/tenant flags, without a target file.")
143
+ sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
144
+ try:
145
+ resource = getattr(getattr(sdk, "ams_admin", None), "skills", None)
146
+ if (
147
+ resource is None
148
+ or not callable(getattr(resource, "list", None))
149
+ or not callable(getattr(resource, "get", None))
150
+ ):
151
+ raise click.ClickException(
152
+ "SDK missing ams_admin skills; use the reviewed SDK version required by this CLI."
153
+ )
154
+ finally:
155
+ sdk.close()
156
+
157
+
158
+ def preflight_policies(invocation: AmsContext, target: TenantTarget) -> None:
159
+ """Validate local policy-read compatibility before login or data reads."""
160
+
161
+ _check_root(invocation)
162
+ if invocation.target_file is not None:
163
+ raise click.ClickException("Policies requires explicit environment/tenant flags, without a target file.")
164
+ sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
165
+ try:
166
+ resource = getattr(getattr(sdk, "ams_admin", None), "policies", None)
167
+ if (
168
+ resource is None
169
+ or not callable(getattr(resource, "list", None))
170
+ or not callable(getattr(resource, "get", None))
171
+ ):
172
+ raise click.ClickException(
173
+ "SDK missing ams_admin policies; use the reviewed SDK version required by this CLI."
174
+ )
175
+ finally:
176
+ sdk.close()
177
+
178
+
179
+ def preflight_guardrails(invocation: AmsContext, target: TenantTarget) -> None:
180
+ """Validate local guardrail-read compatibility before login or data reads."""
181
+
182
+ _check_root(invocation)
183
+ if invocation.target_file is not None:
184
+ raise click.ClickException("Guardrails requires explicit environment/tenant flags, without a target file.")
185
+ sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
186
+ try:
187
+ resource = getattr(getattr(sdk, "ams_admin", None), "guardrails", None)
188
+ if (
189
+ resource is None
190
+ or not callable(getattr(resource, "list", None))
191
+ or not callable(getattr(resource, "get", None))
192
+ ):
193
+ raise click.ClickException(
194
+ "SDK missing ams_admin guardrails; use the reviewed SDK version required by this CLI."
195
+ )
196
+ finally:
197
+ sdk.close()
198
+
199
+
137
200
  def require_target(invocation: AmsContext) -> AmsTarget:
138
201
  """Validate root options and load the explicitly selected AMS target."""
139
202
 
140
203
  _check_root(invocation)
141
204
  if invocation.target_file is None:
142
205
  raise click.ClickException("AMS requires an explicit --target-file.")
143
- return load_target(invocation.target_file)
206
+ target = load_target(invocation.target_file)
207
+ from .session import load_session
208
+
209
+ session = load_session()
210
+ if session is not None and (target.environment, target.tenant_id) != (session.environment, session.tenant_id):
211
+ raise click.ClickException("AMS target does not match the saved session; stopped before authentication.")
212
+ return target
@@ -6,16 +6,16 @@ import click
6
6
 
7
7
  from ..context import AmsDependencies, pass_ams, preflight_departments
8
8
  from ..errors import run_domain
9
- from ..target import TenantTarget
9
+ from ..session import resolve_tenant_target
10
10
  from .operations import get_department, list_departments
11
11
 
12
12
 
13
13
  def _environment(function):
14
- return click.option("--environment", type=click.Choice(["dev", "uat", "prod"]), required=True)(function)
14
+ return click.option("--environment", type=click.Choice(["dev", "uat", "prod"]))(function)
15
15
 
16
16
 
17
17
  def _tenant(function):
18
- return click.option("--tenant-id", required=True)(function)
18
+ return click.option("--tenant-id")(function)
19
19
 
20
20
 
21
21
  def create_list_command(dependencies: AmsDependencies) -> click.Command:
@@ -23,11 +23,12 @@ def create_list_command(dependencies: AmsDependencies) -> click.Command:
23
23
  @_environment
24
24
  @_tenant
25
25
  @pass_ams
26
- def command(invocation, environment: str, tenant_id: str) -> None:
26
+ def command(invocation, environment: str | None, tenant_id: str | None) -> None:
27
27
  target_data = {"environment": environment, "tenant_id": tenant_id}
28
28
 
29
29
  def execute():
30
- target = TenantTarget(environment, tenant_id)
30
+ target = resolve_tenant_target(environment, tenant_id)
31
+ target_data.update(environment=target.environment, tenant_id=target.tenant_id)
31
32
  preflight_departments(invocation, target)
32
33
  return list_departments(dependencies, target)
33
34
 
@@ -42,7 +43,7 @@ def create_get_command(dependencies: AmsDependencies) -> click.Command:
42
43
  @_tenant
43
44
  @click.option("--department-id", required=True)
44
45
  @pass_ams
45
- def command(invocation, environment: str, tenant_id: str, department_id: str) -> None:
46
+ def command(invocation, environment: str | None, tenant_id: str | None, department_id: str) -> None:
46
47
  target_data = {
47
48
  "environment": environment,
48
49
  "tenant_id": tenant_id,
@@ -50,7 +51,8 @@ def create_get_command(dependencies: AmsDependencies) -> click.Command:
50
51
  }
51
52
 
52
53
  def execute():
53
- target = TenantTarget(environment, tenant_id)
54
+ target = resolve_tenant_target(environment, tenant_id)
55
+ target_data.update(environment=target.environment, tenant_id=target.tenant_id)
54
56
  preflight_departments(invocation, target)
55
57
  return get_department(dependencies, target, department_id)
56
58
 
@@ -28,7 +28,13 @@ _MESSAGE_TEMPLATES = {
28
28
 
29
29
  def _message(operation: str, category: str) -> str:
30
30
  prefix = operation.split(".", 1)[0]
31
- domain = {"teams": "Team", "organization": "Organization"}.get(prefix, "Department")
31
+ domain = {
32
+ "teams": "Team",
33
+ "organization": "Organization",
34
+ "skills": "Skill",
35
+ "policies": "Policy",
36
+ "guardrails": "Guardrail",
37
+ }.get(prefix, "Department")
32
38
  return _MESSAGE_TEMPLATES[category].format(domain=domain, domain_lower=domain.lower())
33
39
 
34
40
 
@@ -38,7 +44,11 @@ def _http_category(operation: str, status: object) -> str:
38
44
  403: "authorization",
39
45
  409: "conflict",
40
46
  }
41
- if operation.split(".", 1)[0] in {"teams", "organization"} and status in {400, 404, 422}:
47
+ if operation.split(".", 1)[0] in {"teams", "organization", "skills", "policies", "guardrails"} and status in {
48
+ 400,
49
+ 404,
50
+ 422,
51
+ }:
42
52
  return "validation"
43
53
  return shared_categories.get(status, "transport")
44
54
 
@@ -0,0 +1,5 @@
1
+ """Read-only AMS guardrail catalog command domain."""
2
+
3
+ from .group import create_guardrails_group
4
+
5
+ __all__ = ["create_guardrails_group"]