healthcloud-cli 3.7.3__tar.gz → 3.7.6__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.3 → healthcloud_cli-3.7.6}/PKG-INFO +50 -21
  2. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/README.md +49 -20
  3. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/auth.py +25 -0
  4. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/commands.py +58 -11
  5. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/operations.py +108 -1
  6. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/commands.py +2 -0
  7. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/operations.py +38 -7
  8. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/commands.py +2 -0
  9. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/commands.py +4 -0
  10. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/pyproject.toml +1 -1
  11. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/.gitignore +0 -0
  12. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/hatch_build.py +0 -0
  13. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/__init__.py +0 -0
  14. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/admins/__init__.py +0 -0
  15. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/admins/commands.py +0 -0
  16. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/admins/group.py +0 -0
  17. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/admins/operations.py +0 -0
  18. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/__init__.py +0 -0
  19. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/__init__.py +0 -0
  20. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/commands.py +0 -0
  21. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/group.py +0 -0
  22. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/operations.py +0 -0
  23. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/projections.py +0 -0
  24. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/commands.py +0 -0
  25. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/compatibility.py +0 -0
  26. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/context.py +0 -0
  27. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
  28. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/commands.py +0 -0
  29. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/group.py +0 -0
  30. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/operations.py +0 -0
  31. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/projections.py +0 -0
  32. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/__init__.py +0 -0
  33. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/group.py +0 -0
  34. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/projections.py +0 -0
  35. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/domain_errors.py +0 -0
  36. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/errors.py +0 -0
  37. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/group.py +0 -0
  38. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
  39. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/commands.py +0 -0
  40. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/group.py +0 -0
  41. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/operations.py +0 -0
  42. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/projections.py +0 -0
  43. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/__init__.py +0 -0
  44. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/commands.py +0 -0
  45. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/group.py +0 -0
  46. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/operations.py +0 -0
  47. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/projections.py +0 -0
  48. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/output.py +0 -0
  49. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/__init__.py +0 -0
  50. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/group.py +0 -0
  51. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/operations.py +0 -0
  52. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/projections.py +0 -0
  53. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/pretty.py +0 -0
  54. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/registry.py +0 -0
  55. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/results.py +0 -0
  56. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/session.py +0 -0
  57. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/__init__.py +0 -0
  58. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/commands.py +0 -0
  59. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/group.py +0 -0
  60. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/projections.py +0 -0
  61. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/__init__.py +0 -0
  62. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/group.py +0 -0
  63. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/operations.py +0 -0
  64. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/projections.py +0 -0
  65. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/target.py +0 -0
  66. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/__init__.py +0 -0
  67. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/group.py +0 -0
  68. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/operations.py +0 -0
  69. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/projections.py +0 -0
  70. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/__init__.py +0 -0
  71. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/commands.py +0 -0
  72. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/group.py +0 -0
  73. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/operations.py +0 -0
  74. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/projections.py +0 -0
  75. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tuning.py +0 -0
  76. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/ui.py +0 -0
  77. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/windows_files.py +0 -0
  78. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/cli.py +0 -0
  79. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/context.py +0 -0
  80. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/contracts/__init__.py +0 -0
  81. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
  82. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/main.py +0 -0
  83. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/manifest.py +0 -0
  84. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/output.py +0 -0
  85. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/params.py +0 -0
  86. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/tenants/__init__.py +0 -0
  87. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/tenants/commands.py +0 -0
  88. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/tenants/group.py +0 -0
  89. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/tenants/operations.py +0 -0
  90. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/util.py +0 -0
  91. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/scripts/__init__.py +0 -0
  92. {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/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.3
3
+ Version: 3.7.6
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
@@ -797,25 +797,39 @@ and the result envelope carries the server's authoritative readback of the
797
797
  department (same bounded projection as reads):
798
798
 
799
799
  ```bash
800
- uv run --frozen hc ams departments create --environment dev --tenant-id TENANT_ID --name "Clinical Operations" --created-by-user-id ADMIN_USER_ID [--description TEXT] [--parent-department-id PARENT_ID] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
800
+ uv run --frozen hc ams departments create --environment dev --tenant-id TENANT_ID --name "Clinical Operations" [--description TEXT] [--parent-department-id PARENT_ID] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
801
801
  uv run --frozen hc ams departments update --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID [--name NAME] [--description TEXT] [--parent-department-id PARENT_ID] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
802
802
  uv run --frozen hc ams departments set-active --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
803
803
  uv run --frozen hc ams departments deactivate --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
804
- uv run --frozen hc ams departments delete --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
804
+ uv run --frozen hc ams departments delete --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID [--confirm]
805
805
  ```
806
806
 
807
807
  - `create` succeeds with `operation: "departments.create"` and
808
- `data.department` = the server's 201 readback. `--name` and
809
- `--created-by-user-id` are required by the server contract (the tenant is
810
- taken from the saved session's authenticated admin, never from a flag).
808
+ `data.department` = the server's 201 readback. `--name` is required. There is
809
+ no creator flag: before the write the CLI reads the saved session's own
810
+ identity (`GET /admins/me`) and sends it as the server contract's
811
+ `created_by_user_id`, so a create can never be attributed to another admin
812
+ and the tenant is taken from that same authenticated admin. A failed identity
813
+ read is a bounded failure and the write is never sent — it is not reported as
814
+ an unknown outcome.
811
815
  - `update` sends only the options you pass (an empty `--description ""`
812
816
  clears it); it refuses with exit 1 (`validation`) when no field option is
813
817
  given. `set-active` and `deactivate` mark the department active/inactive.
814
- - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
815
- with no body) and is irreversible.
818
+ - `delete --confirm` succeeds with `data: {"deleted": true}` (the server
819
+ answers 204 with no body) and is irreversible: the CLI keeps no restore.
820
+ Without `--confirm` the command is a **preview**: it reads the department
821
+ exactly once (one bounded `GET`), prints `preview: --confirm was not passed,
822
+ so nothing has been sent.` plus the resolved target and the disclosure block
823
+ to stderr, and exits 2 having sent no `DELETE`. With `--confirm` the confirmed
824
+ run makes the same single `GET` before its single `DELETE`, and the result
825
+ envelope is the same, so a preview is never an approval token. The preview
826
+ text is suppressed under `--output json`/`raw`, which keep stderr
827
+ machine-parsable (the exit-2 refusal message itself is plain text on stderr;
828
+ see #890).
816
829
  - A 404 for the requested department is a `not_found` refusal naming the ID
817
830
  (on `create`, a 404 names the referenced department — typically the missing
818
- `--parent-department-id`); a target/tenant or identity mismatch is a
831
+ `--parent-department-id`; on `delete`, the reviewed read refuses identically
832
+ and nothing is sent); a target/tenant or identity mismatch is a
819
833
  `validation` refusal; both exit 1
820
834
  without changes. The one exception is a tenant mismatch on the `create`
821
835
  readback: the department has already been created server-side, so the CLI
@@ -849,7 +863,7 @@ projection as reads). Teams have no active/inactive lifecycle, so there are no
849
863
 
850
864
  ```bash
851
865
  uv run --frozen hc ams teams create --environment dev --tenant-id TENANT_ID --name "Escalation Team" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID [--description TEXT] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
852
- uv run --frozen hc ams teams update --environment dev --tenant-id TENANT_ID --team-id TEAM_ID [--name NAME] [--description TEXT] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
866
+ uv run --frozen hc ams teams update --environment dev --tenant-id TENANT_ID --team-id TEAM_ID [--name NAME] [--description TEXT] [--key KEY] [--required-eval-suite-ids SUITE_ID ... | --clear-required-eval-suite-ids]
853
867
  uv run --frozen hc ams teams delete --environment dev --tenant-id TENANT_ID --team-id TEAM_ID
854
868
  ```
855
869
 
@@ -861,7 +875,8 @@ uv run --frozen hc ams teams delete --environment dev --tenant-id TENANT_ID --
861
875
  clears it; `--clear-required-eval-suite-ids` sends an empty list to clear
862
876
  the required evaluation suites; `--key` accepts a nonblank key, it cannot be
863
877
  blanked); it refuses with exit 1 (`validation`) when no field option is
864
- given.
878
+ given. `--required-eval-suite-ids` and `--clear-required-eval-suite-ids` are
879
+ alternatives: passing both is a usage error (exit 2) and nothing is sent.
865
880
  - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
866
881
  with no body) and is irreversible. When AI agents still reference the team,
867
882
  the server refuses with 409 and the CLI reports one `conflict` refusal that
@@ -928,7 +943,8 @@ uv run --frozen hc ams skills delete --environment dev --tenant-id TENANT_ID --s
928
943
  - `update` sends only the options you pass (an empty `--description ""`
929
944
  clears it; `--clear-tool-ids` sends an explicit empty list to clear the
930
945
  skill's tool bindings); it refuses with exit 1 (`validation`) when no field
931
- option is given.
946
+ option is given. `--tool-ids` and `--clear-tool-ids` are alternatives:
947
+ passing both is a usage error (exit 2) and nothing is sent.
932
948
  - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
933
949
  with no body) and is irreversible. When AI agents still reference the skill
934
950
  (via their `skill_ids`), the server refuses with 409 and the CLI reports one
@@ -1020,7 +1036,9 @@ uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID -
1020
1036
  - `update` sends only the options you pass (an empty `--description ""` or
1021
1037
  `--content ""` clears it; `--clear-guardrail-ids` sends an explicit empty
1022
1038
  list to clear the policy's guardrail bindings); it refuses with exit 1
1023
- (`validation`) when no field option is given.
1039
+ (`validation`) when no field option is given. `--guardrail-ids` and
1040
+ `--clear-guardrail-ids` are alternatives: passing both is a usage error
1041
+ (exit 2) and nothing is sent.
1024
1042
  - `activate`/`deactivate` PUT `active: true|false` and succeed only when the
1025
1043
  readback confirms the requested flag.
1026
1044
  - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
@@ -1081,8 +1099,12 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1081
1099
  the envelope carries the server's authoritative readback:
1082
1100
  `operation: "sessions.create"` with `data.session`, and
1083
1101
  `operation: "sessions.messages.add"` with `data.message` (message ID, role,
1084
- mode, plan ID, model, timestamps) plus `data.idempotency_key` when a key was
1085
- supplied. Protected free text — `--initial-prompt` and `--content` — is sent to
1102
+ mode, plan ID, model, timestamps) plus `data.idempotency_key` — the key the
1103
+ server read back, when the returned message carries one. When the readback
1104
+ does not echo a key, the field is omitted: the envelope never asserts a
1105
+ key→message association the server did not confirm. An `--idempotency-key`
1106
+ longer than 128 characters (or an empty one) is refused locally with a
1107
+ `validation` error naming the flag, before anything is sent. Protected free text — `--initial-prompt` and `--content` — is sent to
1086
1108
  the server but never appears in the envelope, a refusal, a diagnostic or a log.
1087
1109
  - The protected free text can also arrive from an owner-only file, which keeps it
1088
1110
  out of the command line entirely (argv, the process table under
@@ -1113,16 +1135,18 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1113
1135
  - `--mode implement` is not available from the server yet: `create` reports one
1114
1136
  `unsupported` refusal (the server answers 501) and creates nothing. A message
1115
1137
  whose `--mode` differs from its session's mode is refused by the server with
1116
- 409 and reported as one write-worded `conflict` refusal that says a message's
1117
- mode must match its session's mode and points at `hc ams sessions get`; no
1118
- message is added, and the read-era conflict wording is never used.
1138
+ 409 and reported as one write-worded `conflict` refusal that names the
1139
+ session, mentions a mode mismatch as one possible cause, and points at
1140
+ `hc ams sessions get`; no message is added, and the read-era conflict wording
1141
+ is never used.
1119
1142
  - A missing, expired or other user's session is a `not_found` refusal naming the
1120
1143
  session on `get`, `messages list` and `messages add`; on `create`, a 404 names
1121
1144
  the requested `--agent-id`.
1122
1145
  - Messages with an `--idempotency-key` are replay-safe: the server answers 201
1123
1146
  with the existing message instead of inserting a second one, and the command
1124
1147
  reports that message (its original ID) — a replay is never reported as a new
1125
- write, and the command itself never re-sends a write.
1148
+ write, and the command itself never re-sends a write. The envelope echoes the
1149
+ server's read-back key, never a key the server did not confirm.
1126
1150
  - If the transport times out or the connection drops mid-write, the write's
1127
1151
  outcome is unknown: the CLI reports one `transport` error whose message says so
1128
1152
  and points at `hc ams sessions list`/`hc ams sessions messages list` before
@@ -1131,8 +1155,10 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1131
1155
  landed write is never reported as stopped.
1132
1156
  - Reads refuse target files, root credentials or base URLs, raw output, blank or
1133
1157
  dot-segment session IDs, out-of-range limits, and installed SDKs without the
1134
- session list/get and message list/add methods before tenant-admin login or any
1135
- read. The existing `hc ams plan|show|apply|restore` tuning commands and their
1158
+ session list/get and message list methods before tenant-admin login or any
1159
+ read; `messages list` needs only `messages.list` — the write method `add` is
1160
+ not required for the read, and `messages add` still refuses `unsupported`
1161
+ without it. The existing `hc ams plan|show|apply|restore` tuning commands and their
1136
1162
  signatures are unchanged.
1137
1163
  - These session and message commands do not attach files, apply or roll back
1138
1164
  configuration, or send session history to a model. Plan review is its own
@@ -1223,6 +1249,9 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
1223
1249
  message write, no `PUT` and no apply. The server runs the planner once. It
1224
1250
  requires a saved `hc login` session and reports
1225
1251
  `operation: "sessions.plans.generate"` with `data.plan` and `data.out`.
1252
+ An `--idempotency-key` longer than 128 characters (or an empty one) is
1253
+ refused locally with a `validation` error naming the flag, before anything
1254
+ is sent.
1226
1255
  `get` sends exactly one `GET /vibecoder/plans/PLAN_ID` and reports
1227
1256
  `operation: "sessions.plans.get"`; a reload is immutable, so it returns the
1228
1257
  same `plan_hash` and diff however the session changed afterwards. `get` is a
@@ -779,25 +779,39 @@ and the result envelope carries the server's authoritative readback of the
779
779
  department (same bounded projection as reads):
780
780
 
781
781
  ```bash
782
- uv run --frozen hc ams departments create --environment dev --tenant-id TENANT_ID --name "Clinical Operations" --created-by-user-id ADMIN_USER_ID [--description TEXT] [--parent-department-id PARENT_ID] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
782
+ uv run --frozen hc ams departments create --environment dev --tenant-id TENANT_ID --name "Clinical Operations" [--description TEXT] [--parent-department-id PARENT_ID] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
783
783
  uv run --frozen hc ams departments update --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID [--name NAME] [--description TEXT] [--parent-department-id PARENT_ID] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
784
784
  uv run --frozen hc ams departments set-active --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
785
785
  uv run --frozen hc ams departments deactivate --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
786
- uv run --frozen hc ams departments delete --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
786
+ uv run --frozen hc ams departments delete --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID [--confirm]
787
787
  ```
788
788
 
789
789
  - `create` succeeds with `operation: "departments.create"` and
790
- `data.department` = the server's 201 readback. `--name` and
791
- `--created-by-user-id` are required by the server contract (the tenant is
792
- taken from the saved session's authenticated admin, never from a flag).
790
+ `data.department` = the server's 201 readback. `--name` is required. There is
791
+ no creator flag: before the write the CLI reads the saved session's own
792
+ identity (`GET /admins/me`) and sends it as the server contract's
793
+ `created_by_user_id`, so a create can never be attributed to another admin
794
+ and the tenant is taken from that same authenticated admin. A failed identity
795
+ read is a bounded failure and the write is never sent — it is not reported as
796
+ an unknown outcome.
793
797
  - `update` sends only the options you pass (an empty `--description ""`
794
798
  clears it); it refuses with exit 1 (`validation`) when no field option is
795
799
  given. `set-active` and `deactivate` mark the department active/inactive.
796
- - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
797
- with no body) and is irreversible.
800
+ - `delete --confirm` succeeds with `data: {"deleted": true}` (the server
801
+ answers 204 with no body) and is irreversible: the CLI keeps no restore.
802
+ Without `--confirm` the command is a **preview**: it reads the department
803
+ exactly once (one bounded `GET`), prints `preview: --confirm was not passed,
804
+ so nothing has been sent.` plus the resolved target and the disclosure block
805
+ to stderr, and exits 2 having sent no `DELETE`. With `--confirm` the confirmed
806
+ run makes the same single `GET` before its single `DELETE`, and the result
807
+ envelope is the same, so a preview is never an approval token. The preview
808
+ text is suppressed under `--output json`/`raw`, which keep stderr
809
+ machine-parsable (the exit-2 refusal message itself is plain text on stderr;
810
+ see #890).
798
811
  - A 404 for the requested department is a `not_found` refusal naming the ID
799
812
  (on `create`, a 404 names the referenced department — typically the missing
800
- `--parent-department-id`); a target/tenant or identity mismatch is a
813
+ `--parent-department-id`; on `delete`, the reviewed read refuses identically
814
+ and nothing is sent); a target/tenant or identity mismatch is a
801
815
  `validation` refusal; both exit 1
802
816
  without changes. The one exception is a tenant mismatch on the `create`
803
817
  readback: the department has already been created server-side, so the CLI
@@ -831,7 +845,7 @@ projection as reads). Teams have no active/inactive lifecycle, so there are no
831
845
 
832
846
  ```bash
833
847
  uv run --frozen hc ams teams create --environment dev --tenant-id TENANT_ID --name "Escalation Team" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID [--description TEXT] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
834
- uv run --frozen hc ams teams update --environment dev --tenant-id TENANT_ID --team-id TEAM_ID [--name NAME] [--description TEXT] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
848
+ uv run --frozen hc ams teams update --environment dev --tenant-id TENANT_ID --team-id TEAM_ID [--name NAME] [--description TEXT] [--key KEY] [--required-eval-suite-ids SUITE_ID ... | --clear-required-eval-suite-ids]
835
849
  uv run --frozen hc ams teams delete --environment dev --tenant-id TENANT_ID --team-id TEAM_ID
836
850
  ```
837
851
 
@@ -843,7 +857,8 @@ uv run --frozen hc ams teams delete --environment dev --tenant-id TENANT_ID --
843
857
  clears it; `--clear-required-eval-suite-ids` sends an empty list to clear
844
858
  the required evaluation suites; `--key` accepts a nonblank key, it cannot be
845
859
  blanked); it refuses with exit 1 (`validation`) when no field option is
846
- given.
860
+ given. `--required-eval-suite-ids` and `--clear-required-eval-suite-ids` are
861
+ alternatives: passing both is a usage error (exit 2) and nothing is sent.
847
862
  - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
848
863
  with no body) and is irreversible. When AI agents still reference the team,
849
864
  the server refuses with 409 and the CLI reports one `conflict` refusal that
@@ -910,7 +925,8 @@ uv run --frozen hc ams skills delete --environment dev --tenant-id TENANT_ID --s
910
925
  - `update` sends only the options you pass (an empty `--description ""`
911
926
  clears it; `--clear-tool-ids` sends an explicit empty list to clear the
912
927
  skill's tool bindings); it refuses with exit 1 (`validation`) when no field
913
- option is given.
928
+ option is given. `--tool-ids` and `--clear-tool-ids` are alternatives:
929
+ passing both is a usage error (exit 2) and nothing is sent.
914
930
  - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
915
931
  with no body) and is irreversible. When AI agents still reference the skill
916
932
  (via their `skill_ids`), the server refuses with 409 and the CLI reports one
@@ -1002,7 +1018,9 @@ uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID -
1002
1018
  - `update` sends only the options you pass (an empty `--description ""` or
1003
1019
  `--content ""` clears it; `--clear-guardrail-ids` sends an explicit empty
1004
1020
  list to clear the policy's guardrail bindings); it refuses with exit 1
1005
- (`validation`) when no field option is given.
1021
+ (`validation`) when no field option is given. `--guardrail-ids` and
1022
+ `--clear-guardrail-ids` are alternatives: passing both is a usage error
1023
+ (exit 2) and nothing is sent.
1006
1024
  - `activate`/`deactivate` PUT `active: true|false` and succeed only when the
1007
1025
  readback confirms the requested flag.
1008
1026
  - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
@@ -1063,8 +1081,12 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1063
1081
  the envelope carries the server's authoritative readback:
1064
1082
  `operation: "sessions.create"` with `data.session`, and
1065
1083
  `operation: "sessions.messages.add"` with `data.message` (message ID, role,
1066
- mode, plan ID, model, timestamps) plus `data.idempotency_key` when a key was
1067
- supplied. Protected free text — `--initial-prompt` and `--content` — is sent to
1084
+ mode, plan ID, model, timestamps) plus `data.idempotency_key` — the key the
1085
+ server read back, when the returned message carries one. When the readback
1086
+ does not echo a key, the field is omitted: the envelope never asserts a
1087
+ key→message association the server did not confirm. An `--idempotency-key`
1088
+ longer than 128 characters (or an empty one) is refused locally with a
1089
+ `validation` error naming the flag, before anything is sent. Protected free text — `--initial-prompt` and `--content` — is sent to
1068
1090
  the server but never appears in the envelope, a refusal, a diagnostic or a log.
1069
1091
  - The protected free text can also arrive from an owner-only file, which keeps it
1070
1092
  out of the command line entirely (argv, the process table under
@@ -1095,16 +1117,18 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1095
1117
  - `--mode implement` is not available from the server yet: `create` reports one
1096
1118
  `unsupported` refusal (the server answers 501) and creates nothing. A message
1097
1119
  whose `--mode` differs from its session's mode is refused by the server with
1098
- 409 and reported as one write-worded `conflict` refusal that says a message's
1099
- mode must match its session's mode and points at `hc ams sessions get`; no
1100
- message is added, and the read-era conflict wording is never used.
1120
+ 409 and reported as one write-worded `conflict` refusal that names the
1121
+ session, mentions a mode mismatch as one possible cause, and points at
1122
+ `hc ams sessions get`; no message is added, and the read-era conflict wording
1123
+ is never used.
1101
1124
  - A missing, expired or other user's session is a `not_found` refusal naming the
1102
1125
  session on `get`, `messages list` and `messages add`; on `create`, a 404 names
1103
1126
  the requested `--agent-id`.
1104
1127
  - Messages with an `--idempotency-key` are replay-safe: the server answers 201
1105
1128
  with the existing message instead of inserting a second one, and the command
1106
1129
  reports that message (its original ID) — a replay is never reported as a new
1107
- write, and the command itself never re-sends a write.
1130
+ write, and the command itself never re-sends a write. The envelope echoes the
1131
+ server's read-back key, never a key the server did not confirm.
1108
1132
  - If the transport times out or the connection drops mid-write, the write's
1109
1133
  outcome is unknown: the CLI reports one `transport` error whose message says so
1110
1134
  and points at `hc ams sessions list`/`hc ams sessions messages list` before
@@ -1113,8 +1137,10 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1113
1137
  landed write is never reported as stopped.
1114
1138
  - Reads refuse target files, root credentials or base URLs, raw output, blank or
1115
1139
  dot-segment session IDs, out-of-range limits, and installed SDKs without the
1116
- session list/get and message list/add methods before tenant-admin login or any
1117
- read. The existing `hc ams plan|show|apply|restore` tuning commands and their
1140
+ session list/get and message list methods before tenant-admin login or any
1141
+ read; `messages list` needs only `messages.list` — the write method `add` is
1142
+ not required for the read, and `messages add` still refuses `unsupported`
1143
+ without it. The existing `hc ams plan|show|apply|restore` tuning commands and their
1118
1144
  signatures are unchanged.
1119
1145
  - These session and message commands do not attach files, apply or roll back
1120
1146
  configuration, or send session history to a model. Plan review is its own
@@ -1205,6 +1231,9 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
1205
1231
  message write, no `PUT` and no apply. The server runs the planner once. It
1206
1232
  requires a saved `hc login` session and reports
1207
1233
  `operation: "sessions.plans.generate"` with `data.plan` and `data.out`.
1234
+ An `--idempotency-key` longer than 128 characters (or an empty one) is
1235
+ refused locally with a `validation` error naming the flag, before anything
1236
+ is sent.
1208
1237
  `get` sends exactly one `GET /vibecoder/plans/PLAN_ID` and reports
1209
1238
  `operation: "sessions.plans.get"`; a reload is immutable, so it returns the
1210
1239
  same `plan_hash` and diff however the session changed afterwards. `get` is a
@@ -121,6 +121,31 @@ def _saved_session_token(target: AmsTarget | TenantTarget) -> str:
121
121
  return session.access_token
122
122
 
123
123
 
124
+ def session_admin_id(target: AmsTarget | TenantTarget) -> str:
125
+ """The saved ``hc login`` session's own tenant-admin id (``GET /admins/me``).
126
+
127
+ The server resolves the identity from the verified access token, so nothing
128
+ here is caller-supplied. This is a read of the saved session only: there is
129
+ no environment-variable credential fallback and no password path, so a write
130
+ that needs the creator identity can never borrow another admin's id.
131
+ """
132
+
133
+ token = _saved_session_token(target)
134
+ with _tenant_admin_sdk(target.environment, target.tenant_id) as admin:
135
+ admin.set_access_token(token)
136
+ try:
137
+ profile = admin.admin.get_current_tenant_admin()
138
+ finally:
139
+ admin.clear_access_token()
140
+ admin_id = getattr(profile, "admin_id", None)
141
+ if not isinstance(admin_id, str) or not admin_id.strip():
142
+ raise click.ClickException("Saved AMS session identity unavailable; run hc login.")
143
+ profile_tenant_id = getattr(profile, "tenant_id", None)
144
+ if profile_tenant_id is not None and profile_tenant_id != target.tenant_id:
145
+ raise click.ClickException("Saved AMS session identity does not match the AMS target; run hc login.")
146
+ return admin_id
147
+
148
+
124
149
  def selected_agent(sdk, target: AmsTarget):
125
150
  agent = sdk.ams_admin.get_agent(target.agent_id)
126
151
  if agent is None or agent.id != target.agent_id or agent.tenant_id != target.tenant_id:
@@ -4,8 +4,9 @@ from __future__ import annotations
4
4
 
5
5
  import click
6
6
 
7
+ from ..auth import session_admin_id
7
8
  from ..context import AmsDependencies, pass_ams, preflight_departments
8
- from ..errors import run_domain
9
+ from ..errors import run_domain, run_preview
9
10
  from ..session import resolve_tenant_target
10
11
  from .operations import (
11
12
  activate_department,
@@ -14,10 +15,12 @@ from .operations import (
14
15
  delete_department,
15
16
  get_department,
16
17
  list_departments,
18
+ preview_delete,
17
19
  update_department,
18
20
  )
19
21
 
20
22
  LOGIN_HINT = "Not logged in; run `hc login` before writing department changes."
23
+ DELETE_LOGIN_HINT = "Not logged in; run `hc login` before deleting a department."
21
24
 
22
25
 
23
26
  def _environment(function):
@@ -71,23 +74,45 @@ def create_get_command(dependencies: AmsDependencies) -> click.Command:
71
74
  return command
72
75
 
73
76
 
74
- def _require_login() -> None:
77
+ def _require_login(hint: str = LOGIN_HINT) -> None:
75
78
  """Department writes need a saved `hc login` session, not env-var credentials."""
76
79
 
77
80
  from ..session import load_session
78
81
 
79
82
  if load_session() is None:
80
- raise click.ClickException(LOGIN_HINT)
83
+ raise click.ClickException(hint)
84
+
85
+
86
+ def _preview_then_refuse(invocation, target_data: dict, preview, refusal: str) -> None:
87
+ """Read the target once, print the reviewed delete, then refuse with exit 2.
88
+
89
+ Nothing is written before the refusal, and the preview is still a read: a
90
+ failed read (not logged in, wrong tenant, 404, transport) keeps the bounded
91
+ error envelope instead of escaping as a traceback. The confirmed run makes
92
+ the same read before its single DELETE, so a preview is never an approval
93
+ token.
94
+ """
95
+
96
+ def read() -> None:
97
+ _require_login(DELETE_LOGIN_HINT)
98
+ target = resolve_tenant_target(target_data["environment"], target_data["tenant_id"])
99
+ target_data.update(environment=target.environment, tenant_id=target.tenant_id)
100
+ preflight_departments(invocation, target)
101
+ preview(target)
102
+
103
+ run_preview("departments.delete", target_data, read)
104
+ raise click.UsageError(refusal)
81
105
 
82
106
 
83
107
  def create_create_command(dependencies: AmsDependencies) -> click.Command:
84
- @click.command("create", help="Create one department (send-once write; requires `hc login`).")
108
+ @click.command(
109
+ "create",
110
+ help="Create one department (send-once write; requires `hc login`). The creator is the saved "
111
+ "session's own admin, resolved from the verified token.",
112
+ )
85
113
  @_environment
86
114
  @_tenant
87
115
  @click.option("--name", required=True, help="Department name.")
88
- @click.option(
89
- "--created-by-user-id", required=True, help="Calling admin's user ID (required by the server contract)."
90
- )
91
116
  @click.option("--description", default=None, help="Optional department description.")
92
117
  @click.option("--parent-department-id", default=None, help="Optional parent department ID.")
93
118
  @click.option("--key", default=None, help="Optional department key.")
@@ -102,7 +127,6 @@ def create_create_command(dependencies: AmsDependencies) -> click.Command:
102
127
  environment: str | None,
103
128
  tenant_id: str | None,
104
129
  name: str,
105
- created_by_user_id: str,
106
130
  description: str | None,
107
131
  parent_department_id: str | None,
108
132
  key: str | None,
@@ -115,6 +139,10 @@ def create_create_command(dependencies: AmsDependencies) -> click.Command:
115
139
  target = resolve_tenant_target(environment, tenant_id)
116
140
  target_data.update(environment=target.environment, tenant_id=target.tenant_id)
117
141
  preflight_departments(invocation, target)
142
+ # Attribution is a read of the saved session's own identity (GET /admins/me):
143
+ # the server derives it from the verified token, so it is resolved before
144
+ # the write and never supplied by the caller.
145
+ created_by_user_id = session_admin_id(target)
118
146
  return create_department(
119
147
  dependencies,
120
148
  target,
@@ -239,20 +267,39 @@ def create_deactivate_command(dependencies: AmsDependencies) -> click.Command:
239
267
 
240
268
 
241
269
  def create_delete_command(dependencies: AmsDependencies) -> click.Command:
242
- @click.command("delete", help="Delete one department (send-once, irreversible; requires `hc login`).")
270
+ help_text = (
271
+ "Delete one department (irreversible; send-once; requires `hc login` and --confirm). The CLI keeps "
272
+ "no restore and cannot undo the delete."
273
+ )
274
+
275
+ @click.command("delete", help=help_text)
243
276
  @_environment
244
277
  @_tenant
245
278
  @click.option("--department-id", required=True)
279
+ @click.option(
280
+ "--confirm",
281
+ is_flag=True,
282
+ default=False,
283
+ help="Required acknowledgement of the printed target and disclosure block; without it the command "
284
+ "reads the department, prints both and sends nothing.",
285
+ )
246
286
  @pass_ams
247
- def command(invocation, environment: str | None, tenant_id: str | None, department_id: str) -> None:
287
+ def command(invocation, environment: str | None, tenant_id: str | None, department_id: str, confirm: bool) -> None:
248
288
  target_data = {
249
289
  "environment": environment,
250
290
  "tenant_id": tenant_id,
251
291
  "department_id": department_id,
252
292
  }
293
+ if not confirm:
294
+ _preview_then_refuse(
295
+ invocation,
296
+ target_data,
297
+ lambda target: preview_delete(dependencies, target, department_id),
298
+ "--confirm is required; no delete was sent. Re-run with --confirm to send it.",
299
+ )
253
300
 
254
301
  def execute():
255
- _require_login()
302
+ _require_login(DELETE_LOGIN_HINT)
256
303
  target = resolve_tenant_target(environment, tenant_id)
257
304
  target_data.update(environment=target.environment, tenant_id=target.tenant_id)
258
305
  preflight_departments(invocation, target)
@@ -270,12 +270,119 @@ def deactivate_department(dependencies: AmsDependencies, target: TenantTarget, d
270
270
  return _lifecycle(dependencies, target, department_id, "deactivate")
271
271
 
272
272
 
273
+ DELETE_DISCLOSURE = {
274
+ "irreversible": "the server deletes the department; the CLI has no restore and no retention window.",
275
+ "no_retry": "exactly one DELETE is sent, and an uncertain outcome is never replayed.",
276
+ }
277
+
278
+
279
+ def _review_enabled() -> bool:
280
+ """A reviewed write rendering is human: json/raw keep stderr machine-parsable."""
281
+
282
+ import click
283
+
284
+ from ...context import CliConfig
285
+
286
+ ctx = click.get_current_context(silent=True)
287
+ config = ctx.find_object(CliConfig) if ctx is not None else None
288
+ return getattr(config, "output", None) not in ("json", "raw")
289
+
290
+
291
+ def _echo_preview_notice() -> None:
292
+ """Say plainly that what follows is a preview and nothing has been sent."""
293
+
294
+ import click
295
+
296
+ if not _review_enabled():
297
+ return
298
+ click.echo("preview: --confirm was not passed, so nothing has been sent.", err=True)
299
+
300
+
301
+ def _delete_target(department, department_id: str) -> dict:
302
+ """The exact record the operator is approving: ids and bounded fields only."""
303
+
304
+ return {
305
+ "department_id": department_id,
306
+ "name": getattr(department, "name", None),
307
+ "key": getattr(department, "key", None),
308
+ "active": getattr(department, "active", None),
309
+ "parent_department_id": getattr(department, "parent_department_id", None),
310
+ }
311
+
312
+
313
+ def _echo_delete_review(department, department_id: str, *, preview: bool = False) -> None:
314
+ """Print the destructive target and the disclosure before the single DELETE.
315
+
316
+ ``preview`` is the unconfirmed half: it prints the same target and the same
317
+ disclosure, worded as what the confirmed run would do.
318
+ """
319
+
320
+ import click
321
+
322
+ if not _review_enabled():
323
+ return
324
+ target = _delete_target(department, department_id)
325
+ click.echo(
326
+ f"{'would delete' if preview else 'deleting'} department '{department_id}' "
327
+ f"(name {target['name']!r}, key {target['key']!r}, active {target['active']}, "
328
+ f"parent_department_id {target['parent_department_id']!r}):",
329
+ err=True,
330
+ )
331
+ for key, fact in DELETE_DISCLOSURE.items():
332
+ click.echo(f" {key}: {fact}", err=True)
333
+ click.echo(f"one DELETE /departments/{department_id}", err=True)
334
+
335
+
336
+ def _delete_review_target(sdk, target: TenantTarget, department_id: str):
337
+ """The one bounded read a delete reviews: a 404 is its own refusal.
338
+
339
+ Used by both the unconfirmed preview and the confirmed run, so the target the
340
+ operator reviewed and the target that is deleted cannot drift.
341
+ """
342
+
343
+ from healthcloud.errors import HealthCloudHTTPError
344
+
345
+ try:
346
+ department = _resource(sdk, ("get",)).get(department_id)
347
+ except HealthCloudHTTPError as error:
348
+ if getattr(error, "status_code", 0) == 404:
349
+ raise DepartmentWriteRefused(
350
+ "not_found",
351
+ f"Department '{department_id}' was not found in this tenant; nothing was sent.",
352
+ ) from None
353
+ raise
354
+ if (
355
+ department is None
356
+ or getattr(department, "id", None) != department_id
357
+ or getattr(department, "tenant_id", None) != target.tenant_id
358
+ ):
359
+ raise DepartmentWriteRefused("validation", "Department/tenant identity mismatch; stopped.")
360
+ return department
361
+
362
+
363
+ def preview_delete(dependencies: AmsDependencies, target: TenantTarget, department_id: str) -> None:
364
+ """Print the delete target and its disclosure without sending the DELETE."""
365
+
366
+ department_id = _department_id(department_id)
367
+ with dependencies.authenticated_sdk_factory(target) as sdk:
368
+ before = _delete_review_target(sdk, target, department_id)
369
+ _echo_preview_notice()
370
+ _echo_delete_review(before, department_id, preview=True)
371
+
372
+
273
373
  def delete_department(dependencies: AmsDependencies, target: TenantTarget, department_id: str) -> dict:
274
- """Delete one department (204, no body); no retry on unknown outcome."""
374
+ """Delete one department (204, no body); no retry on unknown outcome.
375
+
376
+ Sequence: exactly one ``GET /departments/{id}`` (the reviewed target, the same
377
+ read the unconfirmed preview makes) and exactly one ``DELETE``. There is never
378
+ a retry, a replay, a second delete or a restore claim.
379
+ """
275
380
  from healthcloud.errors import HealthCloudHTTPError, HealthCloudNetworkError
276
381
 
277
382
  department_id = _department_id(department_id)
278
383
  with dependencies.authenticated_sdk_factory(target) as sdk:
384
+ before = _delete_review_target(sdk, target, department_id)
385
+ _echo_delete_review(before, department_id)
279
386
  resource = _resource(sdk, ("delete",))
280
387
  try:
281
388
  resource.delete(department_id)
@@ -188,6 +188,8 @@ def create_update_command(dependencies: AmsDependencies) -> click.Command:
188
188
  ) -> None:
189
189
  if content is not None and content_file is not None:
190
190
  raise click.UsageError("Pass --content or --content-file, not both; nothing was sent.")
191
+ if guardrail_ids and clear_guardrail_ids:
192
+ raise click.UsageError("--guardrail-ids and --clear-guardrail-ids cannot be combined; nothing was sent.")
191
193
  target_data = {
192
194
  "environment": environment,
193
195
  "tenant_id": tenant_id,
@@ -87,6 +87,28 @@ def _session_id(value: str) -> str:
87
87
  return value
88
88
 
89
89
 
90
+ IDEMPOTENCY_KEY_MAX_CHARS = 128
91
+
92
+
93
+ def _idempotency_key(value, refuse):
94
+ """Mirror the SDK's 128-character bound locally (the help text advertises it).
95
+
96
+ Refused before login or any request, so the bound holds regardless of the
97
+ installed SDK version. A non-empty string within the bound passes through
98
+ unchanged; the caller keeps its own refusal class (message write vs plan
99
+ review).
100
+ """
101
+ if value is None:
102
+ return None
103
+ if not isinstance(value, str) or not value or len(value) > IDEMPOTENCY_KEY_MAX_CHARS:
104
+ raise refuse(
105
+ "validation",
106
+ f"--idempotency-key must be a non-empty string of at most {IDEMPOTENCY_KEY_MAX_CHARS} "
107
+ "characters; nothing was sent.",
108
+ )
109
+ return value
110
+
111
+
90
112
  def _limit(value: int | None, maximum: int, name: str) -> int | None:
91
113
  if value is None:
92
114
  return None
@@ -572,7 +594,9 @@ def get_session(dependencies: AmsDependencies, target: TenantTarget, session_id:
572
594
  with dependencies.authenticated_sdk_factory(target) as sdk:
573
595
  session = _sessions(sdk).get(session_id)
574
596
  if session is None:
575
- raise SessionReadRefused("validation", "Session/tenant identity mismatch; stopped.")
597
+ # A 200 with a JSON null body is the server saying "no such session
598
+ # here" — not an identity mismatch.
599
+ raise SessionReadRefused("not_found", f"Session '{session_id}' was not found in this tenant.")
576
600
  _identity(session, target, session_id)
577
601
  return succeeded(
578
602
  "sessions.get", _target(target, session_id), {"session": session_projection(session)}, read_only=True
@@ -622,9 +646,9 @@ def _message_conflict(error: BaseException, session_id: str) -> SessionWriteRefu
622
646
  return None
623
647
  return SessionWriteRefused(
624
648
  "conflict",
625
- f"The message was refused for session '{session_id}': the server reported a conflict. A "
626
- "message's mode must match its session's mode; read the session with `hc ams sessions get` "
627
- "before retrying. No message was added.",
649
+ f"The message was refused for session '{session_id}': the server reported a conflict — for "
650
+ "example, a message's mode not matching its session's mode. Read the session with "
651
+ "`hc ams sessions get` before retrying. No message was added.",
628
652
  )
629
653
 
630
654
 
@@ -709,7 +733,7 @@ def list_messages(
709
733
  limit = _limit(limit, MESSAGE_LIMIT_MAX, "--limit")
710
734
  with dependencies.authenticated_sdk_factory(target) as sdk:
711
735
  try:
712
- messages = _messages(sdk).list(session_id, **({} if limit is None else {"limit": limit}))
736
+ messages = _messages(sdk, ("list",)).list(session_id, **({} if limit is None else {"limit": limit}))
713
737
  except HealthCloudHTTPError as error:
714
738
  if getattr(error, "status_code", None) == 404:
715
739
  raise SessionReadRefused(
@@ -944,6 +968,7 @@ def generate_plan(
944
968
  session_id = _session_id(session_id)
945
969
  operation = _plan_operation(operation)
946
970
  prompt = None if prompt_file is None else _prompt_file(prompt_file)
971
+ idempotency_key = _idempotency_key(idempotency_key, PlanReviewRefused)
947
972
  with private_output(out) as write, dependencies.authenticated_sdk_factory(target) as sdk:
948
973
  resource = _plans(sdk, ("generate",), write=True)
949
974
  try:
@@ -1038,6 +1063,7 @@ def add_message(
1038
1063
  refuse=SessionWriteRefused,
1039
1064
  required=True,
1040
1065
  )
1066
+ idempotency_key = _idempotency_key(idempotency_key, SessionWriteRefused)
1041
1067
  with dependencies.authenticated_sdk_factory(target) as sdk:
1042
1068
  resource = _messages(sdk, ("add",), write=True)
1043
1069
  try:
@@ -1067,8 +1093,13 @@ def add_message(
1067
1093
  "anything else.",
1068
1094
  )
1069
1095
  data: dict = {"message": message_projection(message)}
1070
- if idempotency_key is not None:
1071
- data["idempotency_key"] = idempotency_key
1096
+ # Echo the server's readback key when the message carries one, so the
1097
+ # envelope never asserts a key→message association the server did not
1098
+ # confirm. When the readback omits the key entirely the field is omitted
1099
+ # too — the caller can see the key they passed in their own command line.
1100
+ readback_key = getattr(message, "idempotency_key", None)
1101
+ if readback_key is not None:
1102
+ data["idempotency_key"] = readback_key
1072
1103
  return succeeded("sessions.messages.add", _target(target, session_id), data, read_only=False)
1073
1104
 
1074
1105
 
@@ -141,6 +141,8 @@ def create_update_command(dependencies: AmsDependencies) -> click.Command:
141
141
  tool_ids: tuple[str, ...],
142
142
  clear_tool_ids: bool,
143
143
  ) -> None:
144
+ if tool_ids and clear_tool_ids:
145
+ raise click.UsageError("--tool-ids and --clear-tool-ids cannot be combined; nothing was sent.")
144
146
  target_data = {
145
147
  "environment": environment,
146
148
  "tenant_id": tenant_id,
@@ -149,6 +149,10 @@ def create_update_command(dependencies: AmsDependencies) -> click.Command:
149
149
  required_eval_suite_ids: tuple[str, ...],
150
150
  clear_required_eval_suite_ids: bool,
151
151
  ) -> None:
152
+ if required_eval_suite_ids and clear_required_eval_suite_ids:
153
+ raise click.UsageError(
154
+ "--required-eval-suite-ids and --clear-required-eval-suite-ids cannot be combined; nothing was sent."
155
+ )
152
156
  target_data = {
153
157
  "environment": environment,
154
158
  "tenant_id": tenant_id,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "healthcloud-cli"
7
- version = "3.7.3"
7
+ version = "3.7.6"
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"