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.
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/PKG-INFO +50 -21
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/README.md +49 -20
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/auth.py +25 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/commands.py +58 -11
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/operations.py +108 -1
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/commands.py +2 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/operations.py +38 -7
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/commands.py +2 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/commands.py +4 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/pyproject.toml +1 -1
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/.gitignore +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/hatch_build.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/admins/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/admins/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/admins/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/admins/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/agents/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/compatibility.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/context.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/domain_errors.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/errors.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/output.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/policies/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/pretty.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/registry.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/results.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/session.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/sessions/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/skills/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/target.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/teams/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tools/projections.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/tuning.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/ui.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/windows_files.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/cli.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/context.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/contracts/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/main.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/manifest.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/output.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/params.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/tenants/__init__.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/tenants/commands.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/tenants/group.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/tenants/operations.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/util.py +0 -0
- {healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/scripts/__init__.py +0 -0
- {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
|
+
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"
|
|
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`
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
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
|
|
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`
|
|
1085
|
-
|
|
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
|
|
1117
|
-
mode
|
|
1118
|
-
message is added, and the read-era conflict wording
|
|
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
|
|
1135
|
-
read
|
|
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"
|
|
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`
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
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
|
|
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`
|
|
1067
|
-
|
|
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
|
|
1099
|
-
mode
|
|
1100
|
-
message is added, and the read-era conflict wording
|
|
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
|
|
1117
|
-
read
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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)
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/operations.py
RENAMED
|
@@ -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
|
-
|
|
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
|
|
626
|
-
"message's mode
|
|
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
|
-
|
|
1071
|
-
|
|
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.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/__init__.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/data_sources/projections.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/departments/projections.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/guardrails/projections.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/__init__.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/ams/organization/projections.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.3 → healthcloud_cli-3.7.6}/healthcloud_cli/contracts/mcp-tools.manifest.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|