healthcloud-cli 3.7.2__tar.gz → 3.7.3__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 (92) hide show
  1. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/PKG-INFO +43 -16
  2. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/README.md +42 -15
  3. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/commands.py +79 -10
  4. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/operations.py +149 -24
  5. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/errors.py +46 -23
  6. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/pyproject.toml +1 -1
  7. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/.gitignore +0 -0
  8. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/hatch_build.py +0 -0
  9. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/__init__.py +0 -0
  10. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/admins/__init__.py +0 -0
  11. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/admins/commands.py +0 -0
  12. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/admins/group.py +0 -0
  13. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/admins/operations.py +0 -0
  14. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/__init__.py +0 -0
  15. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/__init__.py +0 -0
  16. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/group.py +0 -0
  17. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/projections.py +0 -0
  18. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/auth.py +0 -0
  19. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/commands.py +0 -0
  20. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/compatibility.py +0 -0
  21. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/context.py +0 -0
  22. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
  23. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/commands.py +0 -0
  24. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/group.py +0 -0
  25. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/operations.py +0 -0
  26. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/projections.py +0 -0
  27. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/__init__.py +0 -0
  28. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/commands.py +0 -0
  29. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/group.py +0 -0
  30. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/operations.py +0 -0
  31. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/projections.py +0 -0
  32. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/domain_errors.py +0 -0
  33. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/group.py +0 -0
  34. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
  35. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/commands.py +0 -0
  36. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/group.py +0 -0
  37. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/operations.py +0 -0
  38. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/projections.py +0 -0
  39. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/__init__.py +0 -0
  40. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/commands.py +0 -0
  41. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/group.py +0 -0
  42. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/operations.py +0 -0
  43. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/projections.py +0 -0
  44. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/output.py +0 -0
  45. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/__init__.py +0 -0
  46. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/commands.py +0 -0
  47. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/group.py +0 -0
  48. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/operations.py +0 -0
  49. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/projections.py +0 -0
  50. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/pretty.py +0 -0
  51. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/registry.py +0 -0
  52. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/results.py +0 -0
  53. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/session.py +0 -0
  54. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/__init__.py +0 -0
  55. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/commands.py +0 -0
  56. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/group.py +0 -0
  57. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/operations.py +0 -0
  58. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/projections.py +0 -0
  59. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/__init__.py +0 -0
  60. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/commands.py +0 -0
  61. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/group.py +0 -0
  62. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/operations.py +0 -0
  63. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/projections.py +0 -0
  64. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/target.py +0 -0
  65. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/__init__.py +0 -0
  66. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/commands.py +0 -0
  67. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/group.py +0 -0
  68. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/operations.py +0 -0
  69. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/projections.py +0 -0
  70. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/__init__.py +0 -0
  71. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/commands.py +0 -0
  72. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/group.py +0 -0
  73. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/operations.py +0 -0
  74. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/projections.py +0 -0
  75. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tuning.py +0 -0
  76. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/ui.py +0 -0
  77. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/windows_files.py +0 -0
  78. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/cli.py +0 -0
  79. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/context.py +0 -0
  80. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/contracts/__init__.py +0 -0
  81. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
  82. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/main.py +0 -0
  83. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/manifest.py +0 -0
  84. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/output.py +0 -0
  85. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/params.py +0 -0
  86. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/tenants/__init__.py +0 -0
  87. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/tenants/commands.py +0 -0
  88. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/tenants/group.py +0 -0
  89. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/tenants/operations.py +0 -0
  90. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/util.py +0 -0
  91. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/scripts/__init__.py +0 -0
  92. {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/scripts/bundle_manifest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: healthcloud-cli
3
- Version: 3.7.2
3
+ Version: 3.7.3
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
@@ -576,11 +576,23 @@ raw JSON line. `--output json` prints the full envelope above, byte-identical to
576
576
  before. An unknown outcome still prints its one-line `error:` plus the
577
577
  reconciliation on stderr.
578
578
 
579
- Pre-checks refuse with exit 1 and **no write**: a missing `--confirm` (exit 2
580
- usage error, before login), an agent outside the explicit tenant, an already
581
- in-effect state, an unsupported `archived`/`draft` target (named as having no
582
- supported reversible transition here), or a `config_version` that no longer
583
- matches `--expected-config-version`. That version check is a local best-effort
579
+ Without `--confirm` the command is a **preview**: it re-reads the agent exactly
580
+ once (the same one bounded `GET`), runs the same local pre-checks, prints
581
+ `preview: --confirm was not passed, so nothing has been sent.` and then the
582
+ resolved change to stderr — the target (`agent_id`, `name`, `type`,
583
+ `config_version`), `status` and `active` before -> after, and
584
+ `one PUT /agents/{agent_id} with body keys: status, active` — and exits 2 having
585
+ sent nothing. With `--confirm` the write path is unchanged: it re-reads before
586
+ the single `PUT`, so a preview is never an approval token. The preview text is
587
+ suppressed under `--output json`/`raw` (the same rule the confirmed path's
588
+ review follows) so those modes keep stderr machine-parsable.
589
+
590
+ Pre-checks refuse with exit 1 and **no write**: an agent outside the explicit
591
+ tenant, an already in-effect state, an unsupported `archived`/`draft` target
592
+ (named as having no supported reversible transition here), or a
593
+ `config_version` that no longer matches `--expected-config-version` — all of
594
+ these are also enforced by the preview, which still sends nothing. A missing
595
+ `--confirm` exits 2. That version check is a local best-effort
584
596
  guard only: the route has no `If-Match` and the server never compares the
585
597
  version, so it cannot prove atomicity and a request could still race between
586
598
  the read and the PUT.
@@ -657,15 +669,23 @@ the server checks tenant scope only for the department, team, skill, policy and
657
669
  guardrail references. This command does not imply otherwise, and it does not
658
670
  re-implement the server's reference checks.
659
671
 
660
- Pre-checks refuse with exit 1 and **no write**: a missing `--confirm` (exit 2
661
- usage error, before login and before any request), no assignment option at all
672
+ Without `--confirm` the command is a **preview**: it reads the agent exactly once
673
+ (the same one bounded `GET`), computes the same reviewed diff and runs the same
674
+ local pre-checks, prints `preview: --confirm was not passed, so nothing has been
675
+ sent.` followed by the reviewed diff, and exits 2 having sent no `PUT`. With
676
+ `--confirm` the write path is unchanged: it re-reads before the single `PUT`, so
677
+ a preview is never an approval token. The preview text is suppressed under
678
+ `--output json`/`raw`, like the confirmed path's review.
679
+
680
+ Pre-checks refuse with exit 1 and **no write**: no assignment option at all
662
681
  (exit 2), setting and clearing the same family together (exit 2), an agent
663
682
  outside the explicit tenant, a request where every reference already equals the
664
683
  read state ("already in effect"), or a `config_version` that no longer matches
665
- `--expected-config-version`. That version check is a local best-effort guard
666
- only: the route has no `If-Match` and the server never compares the version, so
667
- it cannot prove atomicity and a request could still race between the read and
668
- the PUT. Operation-specific approval here is the CLI's own `--confirm` plus the
684
+ `--expected-config-version` — all of these are also enforced by the preview,
685
+ which still sends nothing. A missing `--confirm` exits 2. That version check is
686
+ a local best-effort guard only: the route has no `If-Match` and the server never
687
+ compares the version, so it cannot prove atomicity and a request could still
688
+ race between the read and the PUT. Operation-specific approval here is the CLI's own `--confirm` plus the
669
689
  reviewed diff, deliberately separate from the tuning/plan-apply approval path;
670
690
  the server enforces tenant isolation, reference existence and the audit log, and
671
691
  it has **no** assignment approval or consent entity.
@@ -733,10 +753,17 @@ disclosure: `agent_id`, `deleted: true`, `target`, `disclosure`, `evidence`
733
753
  reviewed target and disclosure above are still printed to stderr in `pretty`
734
754
  mode only; `--output json`/`raw` keep stderr machine-parsable.
735
755
 
736
- Refusals claim nothing and send nothing extra: a missing `--confirm` is an exit
737
- 2 usage error before login or any request; an agent outside the explicit tenant,
738
- the tenant identity mismatch and a delete target that no longer exists are
739
- refused before the `DELETE`; a `409` is a `conflict` refusal naming the referring
756
+ Without `--confirm` the command is a **preview**: it reads the agent exactly once
757
+ (the same one bounded `GET`), prints `preview: --confirm was not passed, so
758
+ nothing has been sent.` plus the resolved destructive target and the whole
759
+ disclosure block to stderr, and exits 2 having sent no `DELETE`. With `--confirm`
760
+ the sequence above is unchanged: it re-reads before the single `DELETE`, so a
761
+ preview is never an approval token. The preview text is suppressed under
762
+ `--output json`/`raw`, like the confirmed path's review.
763
+
764
+ Refusals claim nothing and send nothing extra: an agent outside the explicit
765
+ tenant, the tenant identity mismatch and a delete target that no longer exists
766
+ are refused before the `DELETE` (the preview reports the same `not_found`); a `409` is a `conflict` refusal naming the referring
740
767
  schedules; a `404` from the `DELETE` is `not_found`; and a `405` is the server's
741
768
  refusal (`unsupported`). None of them is retried.
742
769
 
@@ -558,11 +558,23 @@ raw JSON line. `--output json` prints the full envelope above, byte-identical to
558
558
  before. An unknown outcome still prints its one-line `error:` plus the
559
559
  reconciliation on stderr.
560
560
 
561
- Pre-checks refuse with exit 1 and **no write**: a missing `--confirm` (exit 2
562
- usage error, before login), an agent outside the explicit tenant, an already
563
- in-effect state, an unsupported `archived`/`draft` target (named as having no
564
- supported reversible transition here), or a `config_version` that no longer
565
- matches `--expected-config-version`. That version check is a local best-effort
561
+ Without `--confirm` the command is a **preview**: it re-reads the agent exactly
562
+ once (the same one bounded `GET`), runs the same local pre-checks, prints
563
+ `preview: --confirm was not passed, so nothing has been sent.` and then the
564
+ resolved change to stderr — the target (`agent_id`, `name`, `type`,
565
+ `config_version`), `status` and `active` before -> after, and
566
+ `one PUT /agents/{agent_id} with body keys: status, active` — and exits 2 having
567
+ sent nothing. With `--confirm` the write path is unchanged: it re-reads before
568
+ the single `PUT`, so a preview is never an approval token. The preview text is
569
+ suppressed under `--output json`/`raw` (the same rule the confirmed path's
570
+ review follows) so those modes keep stderr machine-parsable.
571
+
572
+ Pre-checks refuse with exit 1 and **no write**: an agent outside the explicit
573
+ tenant, an already in-effect state, an unsupported `archived`/`draft` target
574
+ (named as having no supported reversible transition here), or a
575
+ `config_version` that no longer matches `--expected-config-version` — all of
576
+ these are also enforced by the preview, which still sends nothing. A missing
577
+ `--confirm` exits 2. That version check is a local best-effort
566
578
  guard only: the route has no `If-Match` and the server never compares the
567
579
  version, so it cannot prove atomicity and a request could still race between
568
580
  the read and the PUT.
@@ -639,15 +651,23 @@ the server checks tenant scope only for the department, team, skill, policy and
639
651
  guardrail references. This command does not imply otherwise, and it does not
640
652
  re-implement the server's reference checks.
641
653
 
642
- Pre-checks refuse with exit 1 and **no write**: a missing `--confirm` (exit 2
643
- usage error, before login and before any request), no assignment option at all
654
+ Without `--confirm` the command is a **preview**: it reads the agent exactly once
655
+ (the same one bounded `GET`), computes the same reviewed diff and runs the same
656
+ local pre-checks, prints `preview: --confirm was not passed, so nothing has been
657
+ sent.` followed by the reviewed diff, and exits 2 having sent no `PUT`. With
658
+ `--confirm` the write path is unchanged: it re-reads before the single `PUT`, so
659
+ a preview is never an approval token. The preview text is suppressed under
660
+ `--output json`/`raw`, like the confirmed path's review.
661
+
662
+ Pre-checks refuse with exit 1 and **no write**: no assignment option at all
644
663
  (exit 2), setting and clearing the same family together (exit 2), an agent
645
664
  outside the explicit tenant, a request where every reference already equals the
646
665
  read state ("already in effect"), or a `config_version` that no longer matches
647
- `--expected-config-version`. That version check is a local best-effort guard
648
- only: the route has no `If-Match` and the server never compares the version, so
649
- it cannot prove atomicity and a request could still race between the read and
650
- the PUT. Operation-specific approval here is the CLI's own `--confirm` plus the
666
+ `--expected-config-version` — all of these are also enforced by the preview,
667
+ which still sends nothing. A missing `--confirm` exits 2. That version check is
668
+ a local best-effort guard only: the route has no `If-Match` and the server never
669
+ compares the version, so it cannot prove atomicity and a request could still
670
+ race between the read and the PUT. Operation-specific approval here is the CLI's own `--confirm` plus the
651
671
  reviewed diff, deliberately separate from the tuning/plan-apply approval path;
652
672
  the server enforces tenant isolation, reference existence and the audit log, and
653
673
  it has **no** assignment approval or consent entity.
@@ -715,10 +735,17 @@ disclosure: `agent_id`, `deleted: true`, `target`, `disclosure`, `evidence`
715
735
  reviewed target and disclosure above are still printed to stderr in `pretty`
716
736
  mode only; `--output json`/`raw` keep stderr machine-parsable.
717
737
 
718
- Refusals claim nothing and send nothing extra: a missing `--confirm` is an exit
719
- 2 usage error before login or any request; an agent outside the explicit tenant,
720
- the tenant identity mismatch and a delete target that no longer exists are
721
- refused before the `DELETE`; a `409` is a `conflict` refusal naming the referring
738
+ Without `--confirm` the command is a **preview**: it reads the agent exactly once
739
+ (the same one bounded `GET`), prints `preview: --confirm was not passed, so
740
+ nothing has been sent.` plus the resolved destructive target and the whole
741
+ disclosure block to stderr, and exits 2 having sent no `DELETE`. With `--confirm`
742
+ the sequence above is unchanged: it re-reads before the single `DELETE`, so a
743
+ preview is never an approval token. The preview text is suppressed under
744
+ `--output json`/`raw`, like the confirmed path's review.
745
+
746
+ Refusals claim nothing and send nothing extra: an agent outside the explicit
747
+ tenant, the tenant identity mismatch and a delete target that no longer exists
748
+ are refused before the `DELETE` (the preview reports the same `not_found`); a `409` is a `conflict` refusal naming the referring
722
749
  schedules; a `404` from the `DELETE` is `not_found`; and a `405` is the server's
723
750
  refusal (`unsupported`). None of them is retried.
724
751
 
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  import click
8
8
 
9
9
  from ..context import AmsDependencies, pass_ams, preflight_agents
10
- from ..errors import run_domain
10
+ from ..errors import run_domain, run_preview
11
11
  from ..session import resolve_tenant_target
12
12
  from .operations import (
13
13
  ASSIGNMENT_FAMILIES,
@@ -16,6 +16,9 @@ from .operations import (
16
16
  export_agent,
17
17
  get_agent,
18
18
  list_agents,
19
+ preview_assignments,
20
+ preview_delete,
21
+ preview_lifecycle,
19
22
  set_assignments,
20
23
  set_lifecycle,
21
24
  )
@@ -37,6 +40,34 @@ def _agent(function):
37
40
  return click.option("--agent-id", required=True)(function)
38
41
 
39
42
 
43
+ def _preview_then_refuse(
44
+ invocation,
45
+ operation: str,
46
+ target_data: dict,
47
+ preview,
48
+ refusal: str,
49
+ *,
50
+ login_hint: str,
51
+ ) -> None:
52
+ """Read the target once, print the reviewed change, then refuse with exit 2.
53
+
54
+ Nothing is written before the refusal, and the preview is still a read: a
55
+ failed read (not logged in, wrong tenant, 404, transport) keeps the bounded
56
+ error envelope instead of escaping as a traceback. The confirmed run
57
+ re-reads before its single write, so a preview is never an approval token.
58
+ """
59
+
60
+ def read() -> None:
61
+ _require_login(login_hint)
62
+ target = resolve_tenant_target(target_data["environment"], target_data["tenant_id"])
63
+ target_data.update(environment=target.environment, tenant_id=target.tenant_id)
64
+ preflight_agents(invocation, target, methods=("get_agent",))
65
+ preview(target)
66
+
67
+ run_preview(operation, target_data, read)
68
+ raise click.UsageError(refusal)
69
+
70
+
40
71
  def _require_login(hint: str = LOGIN_HINT) -> None:
41
72
  """Agent writes need a saved `hc login` session, not env-var credentials."""
42
73
 
@@ -71,7 +102,8 @@ def create_lifecycle_command(dependencies: AmsDependencies, transition: str) ->
71
102
  "--confirm",
72
103
  is_flag=True,
73
104
  default=False,
74
- help="Required acknowledgement of the exact target and state; without it nothing is sent.",
105
+ help="Required acknowledgement of the exact target and state; without it the command reads the "
106
+ "target, prints the change and sends nothing.",
75
107
  )
76
108
  @pass_ams
77
109
  def command(
@@ -82,14 +114,27 @@ def create_lifecycle_command(dependencies: AmsDependencies, transition: str) ->
82
114
  expected_config_version: int,
83
115
  confirm: bool,
84
116
  ) -> None:
85
- if not confirm:
86
- raise click.UsageError("--confirm is required; no lifecycle write was sent.")
87
117
  target_data = {
88
118
  "environment": environment,
89
119
  "tenant_id": tenant_id,
90
120
  "agent_id": agent_id,
91
121
  "expected_config_version": expected_config_version,
92
122
  }
123
+ if not confirm:
124
+ _preview_then_refuse(
125
+ invocation,
126
+ f"agents.{transition}",
127
+ target_data,
128
+ lambda target: preview_lifecycle(
129
+ dependencies,
130
+ target,
131
+ agent_id,
132
+ transition=transition,
133
+ expected_config_version=expected_config_version,
134
+ ),
135
+ "--confirm is required; no lifecycle write was sent. Re-run with --confirm to send it.",
136
+ login_hint=LOGIN_HINT,
137
+ )
93
138
 
94
139
  def execute():
95
140
  _require_login()
@@ -123,13 +168,21 @@ def create_delete_command(dependencies: AmsDependencies) -> click.Command:
123
168
  "--confirm",
124
169
  is_flag=True,
125
170
  default=False,
126
- help="Required acknowledgement of the printed target and disclosure block; without it nothing is sent.",
171
+ help="Required acknowledgement of the printed target and disclosure block; without it the command "
172
+ "prints both and sends nothing.",
127
173
  )
128
174
  @pass_ams
129
175
  def command(invocation, environment: str | None, tenant_id: str | None, agent_id: str, confirm: bool) -> None:
130
- if not confirm:
131
- raise click.UsageError("--confirm is required; no delete was sent.")
132
176
  target_data = {"environment": environment, "tenant_id": tenant_id, "agent_id": agent_id}
177
+ if not confirm:
178
+ _preview_then_refuse(
179
+ invocation,
180
+ "agents.delete",
181
+ target_data,
182
+ lambda target: preview_delete(dependencies, target, agent_id),
183
+ "--confirm is required; no delete was sent. Re-run with --confirm to send it.",
184
+ login_hint=DELETE_LOGIN_HINT,
185
+ )
133
186
 
134
187
  def execute():
135
188
  _require_login(DELETE_LOGIN_HINT)
@@ -221,7 +274,10 @@ CONFIG_VERSION_HELP = (
221
274
  "The config_version read before the write. A local best-effort guard only: the route has no "
222
275
  "If-Match, so it never proves atomicity."
223
276
  )
224
- CONFIRM_HELP = "Required acknowledgement of the target and the printed reviewed diff; without it nothing is sent."
277
+ CONFIRM_HELP = (
278
+ "Required acknowledgement of the target and the printed reviewed diff; without it the command prints "
279
+ "the diff and sends nothing."
280
+ )
225
281
 
226
282
 
227
283
  def _assignment_changes(values: dict, cleared: dict) -> dict:
@@ -320,8 +376,6 @@ def create_assignments_set_command(dependencies: AmsDependencies) -> click.Comma
320
376
  clear_data_source_ids: bool,
321
377
  confirm: bool,
322
378
  ) -> None:
323
- if not confirm:
324
- raise click.UsageError("--confirm is required; no assignment write was sent.")
325
379
  changes = _assignment_changes(
326
380
  {
327
381
  "department_id": department_id,
@@ -351,6 +405,21 @@ def create_assignments_set_command(dependencies: AmsDependencies) -> click.Comma
351
405
  "expected_config_version": expected_config_version,
352
406
  "changed_fields": sorted(changes),
353
407
  }
408
+ if not confirm:
409
+ _preview_then_refuse(
410
+ invocation,
411
+ "agents.assignments.set",
412
+ target_data,
413
+ lambda target: preview_assignments(
414
+ dependencies,
415
+ target,
416
+ agent_id,
417
+ changes,
418
+ expected_config_version=expected_config_version,
419
+ ),
420
+ "--confirm is required; no assignment write was sent. Re-run with --confirm to send it.",
421
+ login_hint=ASSIGNMENT_LOGIN_HINT,
422
+ )
354
423
 
355
424
  def execute():
356
425
  _require_login(ASSIGNMENT_LOGIN_HINT)
@@ -136,6 +136,14 @@ def _lifecycle_target(target: TenantTarget, agent_id: str) -> dict:
136
136
  return _target(target, agent_id)
137
137
 
138
138
 
139
+ def _lifecycle_write_pair(transition: str) -> tuple[str, bool]:
140
+ """The fixed status/active pair for one transition, or a local refusal."""
141
+
142
+ if transition not in LIFECYCLE_TRANSITIONS:
143
+ raise AgentWriteRefused("validation", "Unsupported lifecycle transition; nothing was sent.")
144
+ return LIFECYCLE_TRANSITIONS[transition]
145
+
146
+
139
147
  def _state(agent) -> dict:
140
148
  return {
141
149
  "status_field": agent.status,
@@ -325,9 +333,7 @@ def set_lifecycle(
325
333
  from healthcloud.errors import HealthCloudHTTPError, HealthCloudNetworkError
326
334
 
327
335
  agent_id = _agent_id(agent_id)
328
- if transition not in LIFECYCLE_TRANSITIONS:
329
- raise AgentWriteRefused("validation", "Unsupported lifecycle transition; nothing was sent.")
330
- status, active = LIFECYCLE_TRANSITIONS[transition]
336
+ status, active = _lifecycle_write_pair(transition)
331
337
  operation = f"agents.{transition}"
332
338
  with dependencies.authenticated_sdk_factory(target) as sdk:
333
339
  before = _verified_agent(sdk, target, agent_id)
@@ -478,6 +484,16 @@ def _assignment_diff(before, changes: dict) -> dict:
478
484
  return changed
479
485
 
480
486
 
487
+ def _echo_preview_notice() -> None:
488
+ """Say plainly that the review below is a preview and nothing has been sent."""
489
+
490
+ import click
491
+
492
+ if not _review_enabled():
493
+ return
494
+ click.echo("preview: --confirm was not passed, so nothing has been sent.", err=True)
495
+
496
+
481
497
  def _review_enabled() -> bool:
482
498
  """A reviewed write rendering is human: json/raw keep stderr machine-parsable."""
483
499
 
@@ -509,6 +525,39 @@ def _echo_assignment_review(agent_id: str, before, changed: dict) -> None:
509
525
  click.echo(f"one PUT /agents/{agent_id} with body keys: {', '.join(changed)}", err=True)
510
526
 
511
527
 
528
+ def _echo_lifecycle_review(agent, agent_id: str, transition: str, status: str, active: bool) -> None:
529
+ """Print the exact lifecycle change the confirmed run would send."""
530
+
531
+ import click
532
+
533
+ if not _review_enabled():
534
+ return
535
+ target = _delete_target(agent, agent_id)
536
+ click.echo(
537
+ f"{transition} agent '{agent_id}' would change (name {target['name']!r}, type {target['type']!r}, "
538
+ f"config_version {target['config_version']}):",
539
+ err=True,
540
+ )
541
+ click.echo(f" status: {getattr(agent, 'status', None)!r} -> {status!r}", err=True)
542
+ click.echo(f" active: {getattr(agent, 'active', None)!r} -> {active!r}", err=True)
543
+ click.echo(f"one PUT /agents/{agent_id} with body keys: status, active", err=True)
544
+
545
+
546
+ def _assignment_plan(sdk, target: TenantTarget, agent_id: str, changes: dict, expected_config_version: int):
547
+ """One verified read, the reviewed diff, and the local version guard."""
548
+
549
+ before = _verified_agent(sdk, target, agent_id)
550
+ changed = _assignment_diff(before, changes)
551
+ if not changed:
552
+ raise AgentWriteRefused(
553
+ "validation",
554
+ f"No assignment option changes agent '{agent_id}'; every requested reference already equals "
555
+ "the read state and no write was sent.",
556
+ )
557
+ _assignment_precheck(before, agent_id, expected_config_version, changed)
558
+ return before, changed
559
+
560
+
512
561
  def _assignment_precheck(before, agent_id: str, expected_config_version: int, changed: dict) -> None:
513
562
  if before.config_version != expected_config_version:
514
563
  raise AgentWriteRefused(
@@ -642,15 +691,7 @@ def set_assignments(
642
691
  except ValueError as error:
643
692
  raise AgentWriteRefused("validation", f"{error}; nothing was sent.") from None
644
693
  with dependencies.authenticated_sdk_factory(target) as sdk:
645
- before = _verified_agent(sdk, target, agent_id)
646
- changed = _assignment_diff(before, changes)
647
- if not changed:
648
- raise AgentWriteRefused(
649
- "validation",
650
- f"No assignment option changes agent '{agent_id}'; every requested reference already equals "
651
- "the read state and no write was sent.",
652
- )
653
- _assignment_precheck(before, agent_id, expected_config_version, changed)
694
+ before, changed = _assignment_plan(sdk, target, agent_id, changes, expected_config_version)
654
695
  _echo_assignment_review(agent_id, before, changed)
655
696
  body = {field: entry["after"] for field, entry in changed.items()}
656
697
  try:
@@ -775,6 +816,22 @@ _DELETE_RUNTIME_UNVERIFIED = (
775
816
  )
776
817
 
777
818
 
819
+ def _delete_review_target(sdk, target: TenantTarget, agent_id: str):
820
+ """The one bounded read a delete reviews: a 404 is its own refusal."""
821
+
822
+ from healthcloud.errors import HealthCloudHTTPError
823
+
824
+ try:
825
+ return _verified_agent(sdk, target, agent_id)
826
+ except HealthCloudHTTPError as error:
827
+ if getattr(error, "status_code", 0) == 404:
828
+ raise AgentWriteRefused(
829
+ "not_found",
830
+ f"Agent '{agent_id}' was not found in this tenant; nothing was sent.",
831
+ ) from None
832
+ raise
833
+
834
+
778
835
  def _delete_target(agent, agent_id: str) -> dict:
779
836
  """The exact record the operator is approving: ids and bounded fields only."""
780
837
 
@@ -788,8 +845,12 @@ def _delete_target(agent, agent_id: str) -> dict:
788
845
  }
789
846
 
790
847
 
791
- def _echo_delete_review(agent, agent_id: str) -> None:
792
- """Print the destructive target and the disclosure before the single DELETE."""
848
+ def _echo_delete_review(agent, agent_id: str, *, preview: bool = False) -> None:
849
+ """Print the destructive target and the disclosure before the single DELETE.
850
+
851
+ ``preview`` is the unconfirmed half: it prints the same target and the same
852
+ disclosure, worded as what the confirmed run would do.
853
+ """
793
854
 
794
855
  import click
795
856
 
@@ -797,7 +858,8 @@ def _echo_delete_review(agent, agent_id: str) -> None:
797
858
  return
798
859
  target = _delete_target(agent, agent_id)
799
860
  click.echo(
800
- f"deleting agent '{agent_id}' (name {target['name']!r}, type {target['type']!r}, "
861
+ f"{'would delete' if preview else 'deleting'} agent '{agent_id}' "
862
+ f"(name {target['name']!r}, type {target['type']!r}, "
801
863
  f"status {target['status']!r}, active {target['active']}, "
802
864
  f"config_version {target['config_version']}):",
803
865
  err=True,
@@ -929,15 +991,7 @@ def delete_agent(dependencies: AmsDependencies, target: TenantTarget, agent_id:
929
991
 
930
992
  agent_id = _agent_id(agent_id)
931
993
  with dependencies.authenticated_sdk_factory(target) as sdk:
932
- try:
933
- before = _verified_agent(sdk, target, agent_id)
934
- except HealthCloudHTTPError as error:
935
- if getattr(error, "status_code", 0) == 404:
936
- raise AgentWriteRefused(
937
- "not_found",
938
- f"Agent '{agent_id}' was not found in this tenant; nothing was sent.",
939
- ) from None
940
- raise
994
+ before = _delete_review_target(sdk, target, agent_id)
941
995
  _echo_delete_review(before, agent_id)
942
996
  try:
943
997
  sdk.ams_admin.delete_agent(agent_id)
@@ -1046,3 +1100,74 @@ def delete_agent(dependencies: AmsDependencies, target: TenantTarget, agent_id:
1046
1100
  "before doing anything else."
1047
1101
  ),
1048
1102
  )
1103
+
1104
+
1105
+ # --- previews: read the target and print the change, send nothing -----------
1106
+ #
1107
+ # A write is reviewed before it is sent, and `--confirm` acknowledges that
1108
+ # review. Without `--confirm` the command therefore reads the same target and
1109
+ # prints the same reviewed change, then refuses with exit 2. A preview is a
1110
+ # read, so it performs exactly one GET and never a write, a preflight write or a
1111
+ # second read: the confirmed run re-reads before its single write, so a preview
1112
+ # is never an approval token and never a TOCTOU shortcut.
1113
+
1114
+
1115
+ def preview_lifecycle(
1116
+ dependencies: AmsDependencies,
1117
+ target: TenantTarget,
1118
+ agent_id: str,
1119
+ *,
1120
+ transition: Literal["activate", "deactivate"],
1121
+ expected_config_version: int,
1122
+ ) -> None:
1123
+ """Print the exact lifecycle change without sending it.
1124
+
1125
+ Same one bounded ``GET``, same local pre-checks and same projected target as
1126
+ ``set_lifecycle``; the only differences are that nothing is written and the
1127
+ reviewed change goes to stderr.
1128
+ """
1129
+
1130
+ agent_id = _agent_id(agent_id)
1131
+ status, active = _lifecycle_write_pair(transition)
1132
+ with dependencies.authenticated_sdk_factory(target) as sdk:
1133
+ before = _verified_agent(sdk, target, agent_id)
1134
+ _lifecycle_precheck(before, agent_id, status, active, expected_config_version)
1135
+ _echo_preview_notice()
1136
+ _echo_lifecycle_review(before, agent_id, transition, status, active)
1137
+
1138
+
1139
+ def preview_assignments(
1140
+ dependencies: AmsDependencies,
1141
+ target: TenantTarget,
1142
+ agent_id: str,
1143
+ changes: dict,
1144
+ *,
1145
+ expected_config_version: int,
1146
+ ) -> None:
1147
+ """Print the reviewed assignment diff without sending it.
1148
+
1149
+ Same bounded body validation, one ``GET``, the same diff and local version
1150
+ guard as ``set_assignments``; no PUT is sent.
1151
+ """
1152
+
1153
+ from healthcloud.clients.ams_admin import prepare_assignment_update
1154
+
1155
+ agent_id = _agent_id(agent_id)
1156
+ try:
1157
+ changes = prepare_assignment_update(**changes)
1158
+ except ValueError as error:
1159
+ raise AgentWriteRefused("validation", f"{error}; nothing was sent.") from None
1160
+ with dependencies.authenticated_sdk_factory(target) as sdk:
1161
+ before, changed = _assignment_plan(sdk, target, agent_id, changes, expected_config_version)
1162
+ _echo_preview_notice()
1163
+ _echo_assignment_review(agent_id, before, changed)
1164
+
1165
+
1166
+ def preview_delete(dependencies: AmsDependencies, target: TenantTarget, agent_id: str) -> None:
1167
+ """Print the delete target and its disclosure without sending the DELETE."""
1168
+
1169
+ agent_id = _agent_id(agent_id)
1170
+ with dependencies.authenticated_sdk_factory(target) as sdk:
1171
+ before = _delete_review_target(sdk, target, agent_id)
1172
+ _echo_preview_notice()
1173
+ _echo_delete_review(before, agent_id, preview=True)
@@ -135,6 +135,25 @@ def emit_compact_json(value: dict, *, err: bool = False) -> None:
135
135
  click.echo(json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True), err=err)
136
136
 
137
137
 
138
+ def run_preview(operation: str, target: dict, callback: Callable[[], Any]) -> None:
139
+ """Run the read half of an unconfirmed write, printing no success envelope.
140
+
141
+ A preview is a read: a failure (not logged in, wrong tenant, 404, transport,
142
+ SDK missing) renders the same bounded error envelope as any other read and
143
+ exits nonzero, so nothing escapes as a traceback. On success the caller has
144
+ already printed the reviewed target to stderr, so this renders nothing: the
145
+ adapter then refuses with exit 2 and a message saying nothing was sent.
146
+ """
147
+
148
+ mode = output_mode()
149
+ try:
150
+ callback()
151
+ except KeyboardInterrupt as error:
152
+ _bounded_failure(operation, target, error, mode, read_only=True)
153
+ except Exception as error:
154
+ _bounded_failure(operation, target, error, mode, read_only=True)
155
+
156
+
138
157
  def _render_success(envelope: dict, mode: str) -> None:
139
158
  if mode != "json":
140
159
  click.echo(success_text(envelope))
@@ -169,6 +188,32 @@ def _error_message(operation: str, category: str, target: dict, error: Exception
169
188
  return _message(operation, category, target)
170
189
 
171
190
 
191
+ def _bounded_failure(operation: str, target: dict, error: BaseException, mode: str, *, read_only: bool) -> None:
192
+ """Classify ``error``, render the error envelope, and exit bounded."""
193
+
194
+ # ``error`` may be a KeyboardInterrupt (BaseException): classification
195
+ # maps it to transport/130 and the bounded envelope, never a traceback.
196
+
197
+ category, exit_code = _classification(operation, error)
198
+ # An unknown mutation outcome carries its own reconciliation envelope:
199
+ # the write may have committed, so it renders as ``unknown`` (never a
200
+ # plain failure and never success) and still exits nonzero.
201
+ envelope = getattr(error, "envelope", None)
202
+ _render_failure(
203
+ envelope
204
+ if isinstance(envelope, dict)
205
+ else failed(
206
+ operation,
207
+ target,
208
+ category,
209
+ _error_message(operation, category, target, error),
210
+ read_only=read_only,
211
+ ),
212
+ mode,
213
+ )
214
+ raise SystemExit(exit_code) from None
215
+
216
+
172
217
  def run_domain(operation: str, target: dict, callback: Callable[[], Any], *, read_only: bool = True) -> None:
173
218
  """Run one AMS operation and render its envelope in the active output mode.
174
219
 
@@ -180,29 +225,7 @@ def run_domain(operation: str, target: dict, callback: Callable[[], Any], *, rea
180
225
  mode = output_mode()
181
226
 
182
227
  def _fail(error: BaseException) -> None:
183
- """Classify ``error``, render the error envelope, and exit bounded."""
184
-
185
- # ``error`` may be a KeyboardInterrupt (BaseException): classification
186
- # maps it to transport/130 and the bounded envelope, never a traceback.
187
-
188
- category, exit_code = _classification(operation, error)
189
- # An unknown mutation outcome carries its own reconciliation envelope:
190
- # the write may have committed, so it renders as ``unknown`` (never a
191
- # plain failure and never success) and still exits nonzero.
192
- envelope = getattr(error, "envelope", None)
193
- _render_failure(
194
- envelope
195
- if isinstance(envelope, dict)
196
- else failed(
197
- operation,
198
- target,
199
- category,
200
- _error_message(operation, category, target, error),
201
- read_only=read_only,
202
- ),
203
- mode,
204
- )
205
- raise SystemExit(exit_code) from None
228
+ _bounded_failure(operation, target, error, mode, read_only=read_only)
206
229
 
207
230
  try:
208
231
  envelope = callback()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "healthcloud-cli"
7
- version = "3.7.2"
7
+ version = "3.7.3"
8
8
  description = "Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"