healthcloud-cli 3.7.6__tar.gz → 3.7.8__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.6 → healthcloud_cli-3.7.8}/PKG-INFO +58 -15
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/README.md +57 -14
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/context.py +25 -9
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/output.py +59 -13
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/sessions/commands.py +2 -2
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/sessions/operations.py +136 -33
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/sessions/projections.py +26 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/windows_files.py +19 -3
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/pyproject.toml +1 -1
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/.gitignore +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/hatch_build.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/admins/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/admins/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/admins/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/admins/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/agents/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/agents/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/agents/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/agents/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/agents/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/auth.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/compatibility.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/departments/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/departments/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/departments/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/departments/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/departments/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/domain_errors.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/errors.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/guardrails/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/guardrails/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/guardrails/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/guardrails/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/organization/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/organization/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/organization/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/organization/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/organization/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/policies/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/policies/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/policies/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/policies/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/policies/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/pretty.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/registry.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/results.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/session.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/sessions/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/sessions/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/skills/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/skills/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/skills/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/skills/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/skills/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/target.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/teams/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/teams/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/teams/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/teams/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/teams/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/tools/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/tools/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/tools/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/tools/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/tools/projections.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/tuning.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/ui.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/cli.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/context.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/contracts/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/main.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/manifest.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/output.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/params.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/tenants/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/tenants/commands.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/tenants/group.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/tenants/operations.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/util.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/scripts/__init__.py +0 -0
- {healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/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.8
|
|
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
|
|
@@ -1082,9 +1082,14 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
|
|
|
1082
1082
|
|
|
1083
1083
|
- `list` emits `operation: "sessions.list"`, `data.sessions` (bounded projections
|
|
1084
1084
|
of session ID, agent ID, operation, mode, status and audit timestamps) and a
|
|
1085
|
-
`page` block
|
|
1086
|
-
|
|
1087
|
-
`
|
|
1085
|
+
`page` block. These routes page with a limit only, so the block always says
|
|
1086
|
+
which limit was in force: with `--limit N` it is
|
|
1087
|
+
`{"kind": "limit", "limit": N, "returned_count": N, "continuation": null}`,
|
|
1088
|
+
and without it the server's own default page was applied, which the block
|
|
1089
|
+
reports as `{"kind": "server_default", "server_default_limit": 50,
|
|
1090
|
+
"returned_count": N, "continuation": null}` (50 sessions, 100 messages) — so
|
|
1091
|
+
a truncated page is never mistaken for a complete list. `get` emits
|
|
1092
|
+
`operation: "sessions.get"` and `data.session`. The projections never include the session's protected
|
|
1088
1093
|
`initial_prompt`, tenant or user identity, or arbitrary response fields.
|
|
1089
1094
|
- Default `pretty` output prints `list` as a table with honest headers —
|
|
1090
1095
|
`ID`, `AGENT ID`, `OPERATION`, `STATUS`, `EXPIRES AT` for sessions and `ID`,
|
|
@@ -1133,7 +1138,11 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
|
|
|
1133
1138
|
- `--initial-prompt` becomes the session's first `user` message in the same
|
|
1134
1139
|
server transaction; `hc ams sessions messages list` shows it afterwards.
|
|
1135
1140
|
- `--mode implement` is not available from the server yet: `create` reports one
|
|
1136
|
-
`unsupported` refusal (the server answers 501) and creates nothing
|
|
1141
|
+
`unsupported` refusal (the server answers 501) and creates nothing, and a
|
|
1142
|
+
`messages add` the server answers 501 for is the same `unsupported` refusal
|
|
1143
|
+
with no message added. Both refusals report the 501 the server actually sent
|
|
1144
|
+
and name implement mode as the documented reason, rather than asserting the
|
|
1145
|
+
requested mode was the cause. A message
|
|
1137
1146
|
whose `--mode` differs from its session's mode is refused by the server with
|
|
1138
1147
|
409 and reported as one write-worded `conflict` refusal that names the
|
|
1139
1148
|
session, mentions a mode mismatch as one possible cause, and points at
|
|
@@ -1156,9 +1165,14 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
|
|
|
1156
1165
|
- Reads refuse target files, root credentials or base URLs, raw output, blank or
|
|
1157
1166
|
dot-segment session IDs, out-of-range limits, and installed SDKs without the
|
|
1158
1167
|
session list/get and message list methods before tenant-admin login or any
|
|
1159
|
-
read
|
|
1160
|
-
not required for the read
|
|
1161
|
-
|
|
1168
|
+
read. `messages list` needs only `messages.list` — the write method `add` is
|
|
1169
|
+
not required for the read. The writes check their own methods in the same
|
|
1170
|
+
preflight — each command requires exactly the write method it calls, so
|
|
1171
|
+
`create` refuses an installed SDK without `sessions.create` and `messages add`
|
|
1172
|
+
one without `messages.add` before tenant-admin login and before any read or
|
|
1173
|
+
write, and neither is refused for the write the other command performs. A single-object route whose 2xx body is not a JSON
|
|
1174
|
+
object is refused as a `validation` error, never reported as an internal
|
|
1175
|
+
failure. The existing `hc ams plan|show|apply|restore` tuning commands and their
|
|
1162
1176
|
signatures are unchanged.
|
|
1163
1177
|
- These session and message commands do not attach files, apply or roll back
|
|
1164
1178
|
configuration, or send session history to a model. Plan review is its own
|
|
@@ -1255,7 +1269,12 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
|
|
|
1255
1269
|
`get` sends exactly one `GET /vibecoder/plans/PLAN_ID` and reports
|
|
1256
1270
|
`operation: "sessions.plans.get"`; a reload is immutable, so it returns the
|
|
1257
1271
|
same `plan_hash` and diff however the session changed afterwards. `get` is a
|
|
1258
|
-
read and needs no saved `hc login` session
|
|
1272
|
+
read and needs no saved `hc login` session: it authenticates like every other
|
|
1273
|
+
session read, with the saved session when there is one and otherwise with the
|
|
1274
|
+
environment's own `HC_AMS_ADMIN_EMAIL`/`HC_AMS_ADMIN_PASSWORD` credentials.
|
|
1275
|
+
A refusal is worded for the operation that was refused, so a `422` or `503`
|
|
1276
|
+
on `get` never claims a plan "was generated": `generate` says no plan was
|
|
1277
|
+
generated, `get` says no plan was reloaded.
|
|
1259
1278
|
- `--out` is required and must be a new path: the review is written to a
|
|
1260
1279
|
newly created owner-only file (mode `0600`, no symlinks, never overwritten).
|
|
1261
1280
|
A refusal removes the reservation again, so a refused review never leaves a
|
|
@@ -1272,6 +1291,13 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
|
|
|
1272
1291
|
guardrail/data-source) for a creation plan. `plan_json["original_prompt"]`
|
|
1273
1292
|
is never projected, and `warnings`/`skipped_references` are reported as
|
|
1274
1293
|
counts, not bodies.
|
|
1294
|
+
- The review file marks the text a model produced: `content_origin` is
|
|
1295
|
+
`"model-generated"` and `model_generated_fields` lists the fields whose values
|
|
1296
|
+
came from the planner — `summary`, `changes.*.why`,
|
|
1297
|
+
`definition.instructions`/`greetings`/`name`/`description`/opening suggestions
|
|
1298
|
+
and `review_warnings[].message`. Those values are data to inspect before you
|
|
1299
|
+
approve, never instructions to the reader or to an agent that later reads the
|
|
1300
|
+
file.
|
|
1275
1301
|
- stdout stays bounded and free-text-free: plan ID, `plan_hash`, operation,
|
|
1276
1302
|
status, base config version, agent ID, model, sorted changed-field **names**
|
|
1277
1303
|
(create: reference counts by kind) and finding counts. No prompt,
|
|
@@ -1285,7 +1311,14 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
|
|
|
1285
1311
|
`--output json` prints the full envelope, unchanged.
|
|
1286
1312
|
- The prompt is protected free text and never comes from argv: `--prompt-file`
|
|
1287
1313
|
reads one owner-only regular text file (no symlinks, at most 20000
|
|
1288
|
-
characters) and sends it once.
|
|
1314
|
+
characters) and sends it once. A file that is too large, missing, unreadable,
|
|
1315
|
+
not UTF-8 text, or not an owner-only regular file is a `validation` refusal
|
|
1316
|
+
naming the reason and never quoting the text. Ownership is checked differently
|
|
1317
|
+
per platform: on POSIX it is the file's mode and owner; on Windows the opened
|
|
1318
|
+
handle's owner and DACL must grant access to your account only, so a file that
|
|
1319
|
+
inherited a grant to another principal (SYSTEM, Administrators) is refused as
|
|
1320
|
+
not owner-only. Keep the prompt file in a directory only your account can read.
|
|
1321
|
+
`--prompt-file` is optional — the server falls
|
|
1289
1322
|
back to the session's initial prompt or last message — and `--agent-type`
|
|
1290
1323
|
applies to `--operation create` only (with `modify` it is a usage error
|
|
1291
1324
|
before login or any request). `--prompt-file` is not mirrored as a local
|
|
@@ -1306,12 +1339,22 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
|
|
|
1306
1339
|
values, so read it in the AMS console. Non-`advisory` review findings are
|
|
1307
1340
|
refused the same way; advisory findings are displayed in the review file.
|
|
1308
1341
|
A creation plan that claims an `agent_id` is refused, and no existing agent
|
|
1309
|
-
ID is ever fabricated or required.
|
|
1342
|
+
ID is ever fabricated or required. So is a creation plan whose proposed agent
|
|
1343
|
+
carries a field outside the documented definition and reference set
|
|
1344
|
+
(`name`, `description`, `type`, `model`, `preferred_model`,
|
|
1345
|
+
`is_orchestrator`, `department_id`, `team_id`, `instructions`, `greetings`,
|
|
1346
|
+
`opening_suggestions`, `opening_suggestions_options`, and the catalog
|
|
1347
|
+
`*_ids`), or whose reference field holds anything but non-blank catalog IDs:
|
|
1348
|
+
the operator never approves a definition the review did not show.
|
|
1310
1349
|
- Server answers are preserved, not replaced: a missing, expired or other
|
|
1311
|
-
user's session or plan is a `not_found` refusal naming it,
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1350
|
+
user's session or plan is a `not_found` refusal naming it, and an unavailable
|
|
1351
|
+
planner (`503`) is a `transport` refusal with no file written. A `422` from
|
|
1352
|
+
the plan route is a `validation` refusal, but its detail is only quoted when
|
|
1353
|
+
it is a known static contract message that carries no field values — the
|
|
1354
|
+
documented `"a prompt is required for agent creation"`. Any other `422`
|
|
1355
|
+
detail can quote protected field values, exactly like `validation_errors`
|
|
1356
|
+
and blocking review findings, so it is reported by category only and you read
|
|
1357
|
+
it in the AMS console. A dropped connection on `generate` reports one `transport`
|
|
1315
1358
|
error saying the plan's outcome is unknown and points at
|
|
1316
1359
|
`hc ams sessions messages list`/`plans get` before retrying; the request is
|
|
1317
1360
|
never re-sent automatically.
|
|
@@ -1064,9 +1064,14 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
|
|
|
1064
1064
|
|
|
1065
1065
|
- `list` emits `operation: "sessions.list"`, `data.sessions` (bounded projections
|
|
1066
1066
|
of session ID, agent ID, operation, mode, status and audit timestamps) and a
|
|
1067
|
-
`page` block
|
|
1068
|
-
|
|
1069
|
-
`
|
|
1067
|
+
`page` block. These routes page with a limit only, so the block always says
|
|
1068
|
+
which limit was in force: with `--limit N` it is
|
|
1069
|
+
`{"kind": "limit", "limit": N, "returned_count": N, "continuation": null}`,
|
|
1070
|
+
and without it the server's own default page was applied, which the block
|
|
1071
|
+
reports as `{"kind": "server_default", "server_default_limit": 50,
|
|
1072
|
+
"returned_count": N, "continuation": null}` (50 sessions, 100 messages) — so
|
|
1073
|
+
a truncated page is never mistaken for a complete list. `get` emits
|
|
1074
|
+
`operation: "sessions.get"` and `data.session`. The projections never include the session's protected
|
|
1070
1075
|
`initial_prompt`, tenant or user identity, or arbitrary response fields.
|
|
1071
1076
|
- Default `pretty` output prints `list` as a table with honest headers —
|
|
1072
1077
|
`ID`, `AGENT ID`, `OPERATION`, `STATUS`, `EXPIRES AT` for sessions and `ID`,
|
|
@@ -1115,7 +1120,11 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
|
|
|
1115
1120
|
- `--initial-prompt` becomes the session's first `user` message in the same
|
|
1116
1121
|
server transaction; `hc ams sessions messages list` shows it afterwards.
|
|
1117
1122
|
- `--mode implement` is not available from the server yet: `create` reports one
|
|
1118
|
-
`unsupported` refusal (the server answers 501) and creates nothing
|
|
1123
|
+
`unsupported` refusal (the server answers 501) and creates nothing, and a
|
|
1124
|
+
`messages add` the server answers 501 for is the same `unsupported` refusal
|
|
1125
|
+
with no message added. Both refusals report the 501 the server actually sent
|
|
1126
|
+
and name implement mode as the documented reason, rather than asserting the
|
|
1127
|
+
requested mode was the cause. A message
|
|
1119
1128
|
whose `--mode` differs from its session's mode is refused by the server with
|
|
1120
1129
|
409 and reported as one write-worded `conflict` refusal that names the
|
|
1121
1130
|
session, mentions a mode mismatch as one possible cause, and points at
|
|
@@ -1138,9 +1147,14 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
|
|
|
1138
1147
|
- Reads refuse target files, root credentials or base URLs, raw output, blank or
|
|
1139
1148
|
dot-segment session IDs, out-of-range limits, and installed SDKs without the
|
|
1140
1149
|
session list/get and message list methods before tenant-admin login or any
|
|
1141
|
-
read
|
|
1142
|
-
not required for the read
|
|
1143
|
-
|
|
1150
|
+
read. `messages list` needs only `messages.list` — the write method `add` is
|
|
1151
|
+
not required for the read. The writes check their own methods in the same
|
|
1152
|
+
preflight — each command requires exactly the write method it calls, so
|
|
1153
|
+
`create` refuses an installed SDK without `sessions.create` and `messages add`
|
|
1154
|
+
one without `messages.add` before tenant-admin login and before any read or
|
|
1155
|
+
write, and neither is refused for the write the other command performs. A single-object route whose 2xx body is not a JSON
|
|
1156
|
+
object is refused as a `validation` error, never reported as an internal
|
|
1157
|
+
failure. The existing `hc ams plan|show|apply|restore` tuning commands and their
|
|
1144
1158
|
signatures are unchanged.
|
|
1145
1159
|
- These session and message commands do not attach files, apply or roll back
|
|
1146
1160
|
configuration, or send session history to a model. Plan review is its own
|
|
@@ -1237,7 +1251,12 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
|
|
|
1237
1251
|
`get` sends exactly one `GET /vibecoder/plans/PLAN_ID` and reports
|
|
1238
1252
|
`operation: "sessions.plans.get"`; a reload is immutable, so it returns the
|
|
1239
1253
|
same `plan_hash` and diff however the session changed afterwards. `get` is a
|
|
1240
|
-
read and needs no saved `hc login` session
|
|
1254
|
+
read and needs no saved `hc login` session: it authenticates like every other
|
|
1255
|
+
session read, with the saved session when there is one and otherwise with the
|
|
1256
|
+
environment's own `HC_AMS_ADMIN_EMAIL`/`HC_AMS_ADMIN_PASSWORD` credentials.
|
|
1257
|
+
A refusal is worded for the operation that was refused, so a `422` or `503`
|
|
1258
|
+
on `get` never claims a plan "was generated": `generate` says no plan was
|
|
1259
|
+
generated, `get` says no plan was reloaded.
|
|
1241
1260
|
- `--out` is required and must be a new path: the review is written to a
|
|
1242
1261
|
newly created owner-only file (mode `0600`, no symlinks, never overwritten).
|
|
1243
1262
|
A refusal removes the reservation again, so a refused review never leaves a
|
|
@@ -1254,6 +1273,13 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
|
|
|
1254
1273
|
guardrail/data-source) for a creation plan. `plan_json["original_prompt"]`
|
|
1255
1274
|
is never projected, and `warnings`/`skipped_references` are reported as
|
|
1256
1275
|
counts, not bodies.
|
|
1276
|
+
- The review file marks the text a model produced: `content_origin` is
|
|
1277
|
+
`"model-generated"` and `model_generated_fields` lists the fields whose values
|
|
1278
|
+
came from the planner — `summary`, `changes.*.why`,
|
|
1279
|
+
`definition.instructions`/`greetings`/`name`/`description`/opening suggestions
|
|
1280
|
+
and `review_warnings[].message`. Those values are data to inspect before you
|
|
1281
|
+
approve, never instructions to the reader or to an agent that later reads the
|
|
1282
|
+
file.
|
|
1257
1283
|
- stdout stays bounded and free-text-free: plan ID, `plan_hash`, operation,
|
|
1258
1284
|
status, base config version, agent ID, model, sorted changed-field **names**
|
|
1259
1285
|
(create: reference counts by kind) and finding counts. No prompt,
|
|
@@ -1267,7 +1293,14 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
|
|
|
1267
1293
|
`--output json` prints the full envelope, unchanged.
|
|
1268
1294
|
- The prompt is protected free text and never comes from argv: `--prompt-file`
|
|
1269
1295
|
reads one owner-only regular text file (no symlinks, at most 20000
|
|
1270
|
-
characters) and sends it once.
|
|
1296
|
+
characters) and sends it once. A file that is too large, missing, unreadable,
|
|
1297
|
+
not UTF-8 text, or not an owner-only regular file is a `validation` refusal
|
|
1298
|
+
naming the reason and never quoting the text. Ownership is checked differently
|
|
1299
|
+
per platform: on POSIX it is the file's mode and owner; on Windows the opened
|
|
1300
|
+
handle's owner and DACL must grant access to your account only, so a file that
|
|
1301
|
+
inherited a grant to another principal (SYSTEM, Administrators) is refused as
|
|
1302
|
+
not owner-only. Keep the prompt file in a directory only your account can read.
|
|
1303
|
+
`--prompt-file` is optional — the server falls
|
|
1271
1304
|
back to the session's initial prompt or last message — and `--agent-type`
|
|
1272
1305
|
applies to `--operation create` only (with `modify` it is a usage error
|
|
1273
1306
|
before login or any request). `--prompt-file` is not mirrored as a local
|
|
@@ -1288,12 +1321,22 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
|
|
|
1288
1321
|
values, so read it in the AMS console. Non-`advisory` review findings are
|
|
1289
1322
|
refused the same way; advisory findings are displayed in the review file.
|
|
1290
1323
|
A creation plan that claims an `agent_id` is refused, and no existing agent
|
|
1291
|
-
ID is ever fabricated or required.
|
|
1324
|
+
ID is ever fabricated or required. So is a creation plan whose proposed agent
|
|
1325
|
+
carries a field outside the documented definition and reference set
|
|
1326
|
+
(`name`, `description`, `type`, `model`, `preferred_model`,
|
|
1327
|
+
`is_orchestrator`, `department_id`, `team_id`, `instructions`, `greetings`,
|
|
1328
|
+
`opening_suggestions`, `opening_suggestions_options`, and the catalog
|
|
1329
|
+
`*_ids`), or whose reference field holds anything but non-blank catalog IDs:
|
|
1330
|
+
the operator never approves a definition the review did not show.
|
|
1292
1331
|
- Server answers are preserved, not replaced: a missing, expired or other
|
|
1293
|
-
user's session or plan is a `not_found` refusal naming it,
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1332
|
+
user's session or plan is a `not_found` refusal naming it, and an unavailable
|
|
1333
|
+
planner (`503`) is a `transport` refusal with no file written. A `422` from
|
|
1334
|
+
the plan route is a `validation` refusal, but its detail is only quoted when
|
|
1335
|
+
it is a known static contract message that carries no field values — the
|
|
1336
|
+
documented `"a prompt is required for agent creation"`. Any other `422`
|
|
1337
|
+
detail can quote protected field values, exactly like `validation_errors`
|
|
1338
|
+
and blocking review findings, so it is reported by category only and you read
|
|
1339
|
+
it in the AMS console. A dropped connection on `generate` reports one `transport`
|
|
1297
1340
|
error saying the plan's outcome is unknown and points at
|
|
1298
1341
|
`hc ams sessions messages list`/`plans get` before retrying; the request is
|
|
1299
1342
|
never re-sent automatically.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from collections.abc import Callable
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
6
|
from contextlib import AbstractContextManager
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from pathlib import Path
|
|
@@ -236,8 +236,20 @@ def preflight_policies(invocation: AmsContext, target: TenantTarget) -> None:
|
|
|
236
236
|
sdk.close()
|
|
237
237
|
|
|
238
238
|
|
|
239
|
-
def preflight_sessions(
|
|
240
|
-
|
|
239
|
+
def preflight_sessions(
|
|
240
|
+
invocation: AmsContext,
|
|
241
|
+
target: TenantTarget,
|
|
242
|
+
*,
|
|
243
|
+
write_methods: Sequence[str] = (),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Validate local session compatibility before login or any request.
|
|
246
|
+
|
|
247
|
+
``write_methods`` names exactly the write methods the calling command will
|
|
248
|
+
call, as ``"<resource>.<method>"`` (``sessions.create``, ``messages.add``),
|
|
249
|
+
so a write command is refused here — before tenant-admin login and before any
|
|
250
|
+
read or write — only for a method it actually needs; a write it never
|
|
251
|
+
performs is never required of the installed SDK.
|
|
252
|
+
"""
|
|
241
253
|
|
|
242
254
|
_check_root(invocation)
|
|
243
255
|
if invocation.target_file is not None:
|
|
@@ -245,12 +257,16 @@ def preflight_sessions(invocation: AmsContext, target: TenantTarget) -> None:
|
|
|
245
257
|
sdk = _public_sdk(target.environment, target.tenant_id)
|
|
246
258
|
try:
|
|
247
259
|
resource = getattr(getattr(sdk, "ams_admin", None), "sessions", None)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
260
|
+
messages = getattr(resource, "messages", None)
|
|
261
|
+
resources = {"sessions": resource, "messages": messages}
|
|
262
|
+
required = (resource, "list"), (resource, "get"), (messages, "list")
|
|
263
|
+
missing = any(not callable(getattr(holder, method, None)) for holder, method in required)
|
|
264
|
+
for qualified in write_methods:
|
|
265
|
+
owner, _, method = qualified.partition(".")
|
|
266
|
+
holder = resources.get(owner)
|
|
267
|
+
if not method or not callable(getattr(holder, method, None)):
|
|
268
|
+
missing = True
|
|
269
|
+
if missing:
|
|
254
270
|
raise click.ClickException(
|
|
255
271
|
"SDK missing ams_admin sessions; use the reviewed SDK version required by this CLI."
|
|
256
272
|
)
|
|
@@ -66,34 +66,75 @@ def read_json(path: Path, *, private: bool = False) -> dict:
|
|
|
66
66
|
) from None
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
class PrivateInputRefused(ValueError):
|
|
70
|
+
"""A protected input file the reader refused, with a value-free reason.
|
|
71
|
+
|
|
72
|
+
``reason`` names the failed property — ``too_large``, ``not_owner_only``,
|
|
73
|
+
``not_text`` or ``unreadable`` — never the file's path or content, so a
|
|
74
|
+
caller can map it to its own categorized refusal (a bare click error is not
|
|
75
|
+
the command's error contract) without quoting protected text.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, reason: str) -> None:
|
|
79
|
+
super().__init__(reason)
|
|
80
|
+
self.reason = reason
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def private_input_detail(reason: str, *, maximum: int) -> str:
|
|
84
|
+
"""A bounded, value-free sentence explaining why a protected file was refused."""
|
|
85
|
+
|
|
86
|
+
if reason == "too_large":
|
|
87
|
+
return f"it is larger than {maximum} characters"
|
|
88
|
+
if reason == "not_text":
|
|
89
|
+
return "it is not readable UTF-8 text"
|
|
90
|
+
if reason in ("not_owner_only", "not_regular"):
|
|
91
|
+
return "it is not an owner-only regular file"
|
|
92
|
+
return "it cannot be read"
|
|
93
|
+
|
|
94
|
+
|
|
69
95
|
def read_private_text(path: Path, *, maximum: int) -> str:
|
|
70
96
|
"""Read one owner-only plain-text input without quoting its content.
|
|
71
97
|
|
|
72
98
|
``maximum`` is a character bound checked before the text is handed on, so
|
|
73
99
|
an oversized protected input is refused locally instead of being sent. The
|
|
74
|
-
text itself is never included in the refusal: only the reason
|
|
100
|
+
text itself is never included in the refusal: only the reason, as a
|
|
101
|
+
:class:`PrivateInputRefused` carrying one of ``too_large``,
|
|
102
|
+
``not_owner_only``, ``not_regular``, ``not_text`` or ``unreadable``.
|
|
103
|
+
|
|
104
|
+
The owner-only check is a POSIX mode/ownership check. On Windows the same
|
|
105
|
+
guarantee comes from ``windows_files``, which validates the opened handle's
|
|
106
|
+
owner and DACL; its refusal carries its own reason (``not_owner_only`` or
|
|
107
|
+
``not_regular``) and is reported unchanged here, so a non-private file is not
|
|
108
|
+
misreported as unreadable. An inherited ACL cannot be made owner-only by the
|
|
109
|
+
creator, so the caller must still keep the file in a directory only the
|
|
110
|
+
operator can read.
|
|
75
111
|
"""
|
|
76
112
|
try:
|
|
77
113
|
if os.name == "nt":
|
|
78
|
-
from .windows_files import open_file
|
|
114
|
+
from .windows_files import PrivateHandleRefused, open_file
|
|
79
115
|
|
|
80
|
-
|
|
116
|
+
try:
|
|
117
|
+
fd = open_file(path, private=True)
|
|
118
|
+
except PrivateHandleRefused as error:
|
|
119
|
+
raise PrivateInputRefused(error.reason) from None
|
|
81
120
|
else:
|
|
82
121
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
83
122
|
with os.fdopen(fd, encoding="utf-8") as stream:
|
|
84
123
|
info = os.fstat(stream.fileno())
|
|
85
|
-
if not stat.S_ISREG(info.st_mode)
|
|
86
|
-
|
|
87
|
-
):
|
|
88
|
-
raise
|
|
124
|
+
if not stat.S_ISREG(info.st_mode):
|
|
125
|
+
raise PrivateInputRefused("not_regular")
|
|
126
|
+
if os.name != "nt" and (info.st_mode & 0o077 or info.st_uid != os.getuid()):
|
|
127
|
+
raise PrivateInputRefused("not_owner_only")
|
|
89
128
|
text = stream.read(maximum + 1)
|
|
90
129
|
if len(text) > maximum:
|
|
91
|
-
raise
|
|
130
|
+
raise PrivateInputRefused("too_large")
|
|
92
131
|
return text
|
|
93
|
-
except
|
|
94
|
-
raise
|
|
95
|
-
|
|
96
|
-
) from None
|
|
132
|
+
except PrivateInputRefused:
|
|
133
|
+
raise
|
|
134
|
+
except UnicodeError:
|
|
135
|
+
raise PrivateInputRefused("not_text") from None
|
|
136
|
+
except OSError, ValueError:
|
|
137
|
+
raise PrivateInputRefused("unreadable") from None
|
|
97
138
|
|
|
98
139
|
|
|
99
140
|
def protected_text(
|
|
@@ -132,7 +173,12 @@ def protected_text(
|
|
|
132
173
|
if path is not None:
|
|
133
174
|
if not isinstance(path, str) or not path.strip():
|
|
134
175
|
raise refuse("validation", f"Explicit {file_flag} file path required.")
|
|
135
|
-
|
|
176
|
+
try:
|
|
177
|
+
text = read_private_text(Path(path), maximum=PROTECTED_TEXT_MAX_CHARS)
|
|
178
|
+
except PrivateInputRefused:
|
|
179
|
+
raise click.ClickException(
|
|
180
|
+
"Invalid input file; require a readable, owner-only regular text file without symlinks."
|
|
181
|
+
) from None
|
|
136
182
|
if not text:
|
|
137
183
|
raise refuse("validation", f"The file given to {file_flag} is empty; nothing was sent.")
|
|
138
184
|
return text
|
|
@@ -125,7 +125,7 @@ def create_create_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
125
125
|
_require_login()
|
|
126
126
|
target = resolve_tenant_target(environment, tenant_id)
|
|
127
127
|
target_data.update(environment=target.environment, tenant_id=target.tenant_id)
|
|
128
|
-
preflight_sessions(invocation, target)
|
|
128
|
+
preflight_sessions(invocation, target, write_methods=("sessions.create",))
|
|
129
129
|
return create_session(
|
|
130
130
|
dependencies,
|
|
131
131
|
target,
|
|
@@ -561,7 +561,7 @@ def create_messages_add_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
561
561
|
_require_login()
|
|
562
562
|
target = resolve_tenant_target(environment, tenant_id)
|
|
563
563
|
target_data.update(environment=target.environment, tenant_id=target.tenant_id)
|
|
564
|
-
preflight_sessions(invocation, target)
|
|
564
|
+
preflight_sessions(invocation, target, write_methods=("messages.add",))
|
|
565
565
|
return add_message(
|
|
566
566
|
dependencies,
|
|
567
567
|
target,
|
|
@@ -7,7 +7,14 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from ..context import AmsDependencies
|
|
9
9
|
from ..domain_errors import DomainOutcomeUnknown, DomainReadRefused
|
|
10
|
-
from ..output import
|
|
10
|
+
from ..output import (
|
|
11
|
+
PROTECTED_TEXT_MAX_CHARS,
|
|
12
|
+
PrivateInputRefused,
|
|
13
|
+
private_input_detail,
|
|
14
|
+
private_output,
|
|
15
|
+
protected_text,
|
|
16
|
+
read_private_text,
|
|
17
|
+
)
|
|
11
18
|
from ..results import succeeded, unknown
|
|
12
19
|
from ..target import TenantTarget
|
|
13
20
|
from .projections import (
|
|
@@ -25,7 +32,11 @@ from .projections import (
|
|
|
25
32
|
)
|
|
26
33
|
|
|
27
34
|
SESSION_LIMIT_MAX = 100
|
|
35
|
+
# The server's own defaults when ``--limit`` is omitted: an omitted limit is a
|
|
36
|
+
# real, truncating page, so the envelope reports which default was applied.
|
|
37
|
+
SESSION_LIMIT_DEFAULT = 50
|
|
28
38
|
MESSAGE_LIMIT_MAX = 200
|
|
39
|
+
MESSAGE_LIMIT_DEFAULT = 100
|
|
29
40
|
# Mirror the server's own bounds so an obviously unacceptable file is never
|
|
30
41
|
# put on the wire; the server's refusal still wins when it disagrees.
|
|
31
42
|
ATTACHMENT_MAX_BYTES = 5 * 1024 * 1024
|
|
@@ -288,16 +299,21 @@ def _plan_operation(value: str) -> str:
|
|
|
288
299
|
|
|
289
300
|
|
|
290
301
|
def _prompt_file(path: str) -> str:
|
|
291
|
-
"""Read the protected prompt from one owner-only text file, never argv.
|
|
302
|
+
"""Read the protected prompt from one owner-only text file, never argv.
|
|
303
|
+
|
|
304
|
+
``read_private_text`` applies the size bound itself, so oversize, unsafe and
|
|
305
|
+
unreadable files are all refused here in the plan's own ``validation``
|
|
306
|
+
category instead of a bare click error. There is deliberately no second
|
|
307
|
+
length check: a check after that read could never run.
|
|
308
|
+
"""
|
|
292
309
|
|
|
293
310
|
if not isinstance(path, str) or not path.strip():
|
|
294
311
|
raise PlanReviewRefused("validation", "Explicit prompt file path required.")
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
300
|
-
return text
|
|
312
|
+
try:
|
|
313
|
+
return read_private_text(Path(path), maximum=PROMPT_MAX_CHARS)
|
|
314
|
+
except PrivateInputRefused as error:
|
|
315
|
+
detail = private_input_detail(error.reason, maximum=PROMPT_MAX_CHARS)
|
|
316
|
+
raise PlanReviewRefused("validation", f"The prompt file was refused: {detail}; nothing was sent.") from None
|
|
301
317
|
|
|
302
318
|
|
|
303
319
|
def _plans(sdk, methods, *, write: bool = False, what: str | None = None):
|
|
@@ -467,15 +483,34 @@ def _modify_diff(body: dict) -> dict:
|
|
|
467
483
|
|
|
468
484
|
|
|
469
485
|
def _references(agent: dict) -> dict:
|
|
486
|
+
"""Group the proposal's catalog references by kind, refusing anything but IDs.
|
|
487
|
+
|
|
488
|
+
Field **names** only ever reach a refusal: a non-string or blank item is
|
|
489
|
+
refused rather than dropped, because a dropped item is a reference the
|
|
490
|
+
operator would approve without having seen it.
|
|
491
|
+
"""
|
|
492
|
+
|
|
470
493
|
references = {}
|
|
471
494
|
for field, kind in PLAN_REFERENCE_FIELDS.items():
|
|
472
495
|
value = agent.get(field)
|
|
473
|
-
if
|
|
496
|
+
if value is None:
|
|
497
|
+
continue
|
|
498
|
+
if isinstance(value, str):
|
|
499
|
+
if not value.strip():
|
|
500
|
+
raise PlanReviewRefused(
|
|
501
|
+
"validation", f"Creation plan reference '{field}' is blank; it is not a catalog ID. Refused."
|
|
502
|
+
)
|
|
474
503
|
references[kind] = [value]
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
504
|
+
continue
|
|
505
|
+
if not isinstance(value, list) or not all(isinstance(item, str) and item.strip() for item in value):
|
|
506
|
+
raise PlanReviewRefused(
|
|
507
|
+
"validation",
|
|
508
|
+
f"Creation plan reference '{field}' must be a catalog ID or a list of catalog IDs; "
|
|
509
|
+
"anything else is refused by field name. No review file was written.",
|
|
510
|
+
)
|
|
511
|
+
ids = sorted(set(value))
|
|
512
|
+
if ids:
|
|
513
|
+
references[kind] = ids
|
|
479
514
|
return references
|
|
480
515
|
|
|
481
516
|
|
|
@@ -492,6 +527,13 @@ def _create_diff(plan, body: dict) -> dict:
|
|
|
492
527
|
raise PlanReviewRefused(
|
|
493
528
|
"validation", "A creation plan must carry the proposed agent definition; the server returned none."
|
|
494
529
|
)
|
|
530
|
+
unknown = sorted(set(agent) - set(PLAN_DEFINITION_FIELDS) - set(PLAN_REFERENCE_FIELDS))
|
|
531
|
+
if unknown:
|
|
532
|
+
raise PlanReviewRefused(
|
|
533
|
+
"validation",
|
|
534
|
+
f"Creation plan carries agent fields this review does not cover: {', '.join(unknown)}. "
|
|
535
|
+
"Refused by field name; no review file was written.",
|
|
536
|
+
)
|
|
495
537
|
return {
|
|
496
538
|
"definition": {name: agent[name] for name in PLAN_DEFINITION_FIELDS if name in agent},
|
|
497
539
|
"references": _references(agent),
|
|
@@ -519,9 +561,41 @@ def _plan_review(
|
|
|
519
561
|
return actual, review, plan_metadata(plan, actual, review)
|
|
520
562
|
|
|
521
563
|
|
|
564
|
+
# The only `422` details this CLI prints: static route-contract messages that
|
|
565
|
+
# name a rule and carry no field value. Everything else the server sends in a
|
|
566
|
+
# `422` detail is its own text, which can quote protected field values — the
|
|
567
|
+
# same reason `validation_errors` and blocking review findings are reported by
|
|
568
|
+
# count only. Extend this set only with a detail verified to be static.
|
|
569
|
+
PLAN_SAFE_422_DETAILS = frozenset({"a prompt is required for agent creation"})
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _plan_safe_detail(error: BaseException) -> str | None:
|
|
573
|
+
"""The server's own `422` detail, but only when it is a known value-free message."""
|
|
574
|
+
|
|
575
|
+
parsed = getattr(error, "parsed_body", None)
|
|
576
|
+
if not isinstance(parsed, dict):
|
|
577
|
+
return None
|
|
578
|
+
for key in ("detail", "error", "message"):
|
|
579
|
+
value = parsed.get(key)
|
|
580
|
+
if isinstance(value, str) and value.strip():
|
|
581
|
+
detail = " ".join(value.split())
|
|
582
|
+
return detail if detail in PLAN_SAFE_422_DETAILS else None
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _plan_not_done(kind: str) -> str:
|
|
587
|
+
"""What did not happen, worded for the operation that was refused."""
|
|
588
|
+
|
|
589
|
+
if kind == "generate":
|
|
590
|
+
return "No plan was generated."
|
|
591
|
+
return "No plan was reloaded; nothing was reviewed."
|
|
592
|
+
|
|
593
|
+
|
|
522
594
|
def _plan_write_refusal(
|
|
523
|
-
error: BaseException, *, session_id: str | None, plan_id: str | None
|
|
595
|
+
error: BaseException, *, kind: str, session_id: str | None, plan_id: str | None
|
|
524
596
|
) -> PlanReviewRefused | None:
|
|
597
|
+
"""Map one plan-route HTTP error for `generate` (`kind="generate"`) or `get`."""
|
|
598
|
+
|
|
525
599
|
status = getattr(error, "status_code", None)
|
|
526
600
|
if status == 404:
|
|
527
601
|
if plan_id is not None:
|
|
@@ -532,14 +606,20 @@ def _plan_write_refusal(
|
|
|
532
606
|
"not_found", f"Session '{session_id}' was not found in this tenant; no plan was generated."
|
|
533
607
|
)
|
|
534
608
|
if status == 422:
|
|
609
|
+
detail = _plan_safe_detail(error)
|
|
610
|
+
if detail is not None:
|
|
611
|
+
return PlanReviewRefused(
|
|
612
|
+
"validation", f"The plan request was refused by the server (422): {detail}. {_plan_not_done(kind)}"
|
|
613
|
+
)
|
|
535
614
|
return PlanReviewRefused(
|
|
536
615
|
"validation",
|
|
537
|
-
|
|
616
|
+
"The plan request was refused by the server (422). Its detail can quote protected field values, "
|
|
617
|
+
f"so it is not printed; review it in the AMS console. {_plan_not_done(kind)}",
|
|
538
618
|
)
|
|
539
619
|
if status == 503:
|
|
540
620
|
return PlanReviewRefused(
|
|
541
621
|
"transport",
|
|
542
|
-
"The plan service is unavailable (the server answered 503);
|
|
622
|
+
f"The plan service is unavailable (the server answered 503); {_plan_not_done(kind)} Retry later.",
|
|
543
623
|
)
|
|
544
624
|
return None
|
|
545
625
|
|
|
@@ -562,6 +642,26 @@ def _identity(session, target: TenantTarget, session_id: str | None) -> None:
|
|
|
562
642
|
)
|
|
563
643
|
|
|
564
644
|
|
|
645
|
+
def _page(limit: int | None, server_default: int, returned: int) -> dict:
|
|
646
|
+
"""The page block, honest about an omitted ``--limit``.
|
|
647
|
+
|
|
648
|
+
These routes page with a limit only: an omitted limit is not "unpaged", it
|
|
649
|
+
is the server's own default page, which can truncate the list. The block
|
|
650
|
+
therefore reports either the limit that was sent, or the server default
|
|
651
|
+
that was applied instead, so a consumer can always tell whether more rows
|
|
652
|
+
may exist.
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
if limit is None:
|
|
656
|
+
return {
|
|
657
|
+
"kind": "server_default",
|
|
658
|
+
"server_default_limit": server_default,
|
|
659
|
+
"returned_count": returned,
|
|
660
|
+
"continuation": None,
|
|
661
|
+
}
|
|
662
|
+
return {"kind": "limit", "limit": limit, "returned_count": returned, "continuation": None}
|
|
663
|
+
|
|
664
|
+
|
|
565
665
|
def list_sessions(dependencies: AmsDependencies, target: TenantTarget, *, limit: int | None = None) -> dict:
|
|
566
666
|
"""`hc ams sessions list`: the caller's non-expired sessions, newest first."""
|
|
567
667
|
|
|
@@ -576,12 +676,7 @@ def list_sessions(dependencies: AmsDependencies, target: TenantTarget, *, limit:
|
|
|
576
676
|
_target(target),
|
|
577
677
|
{
|
|
578
678
|
"sessions": projections,
|
|
579
|
-
"page":
|
|
580
|
-
"kind": "limit",
|
|
581
|
-
"limit": limit,
|
|
582
|
-
"returned_count": len(projections),
|
|
583
|
-
"continuation": None,
|
|
584
|
-
},
|
|
679
|
+
"page": _page(limit, SESSION_LIMIT_DEFAULT, len(projections)),
|
|
585
680
|
},
|
|
586
681
|
read_only=True,
|
|
587
682
|
)
|
|
@@ -653,13 +748,26 @@ def _message_conflict(error: BaseException, session_id: str) -> SessionWriteRefu
|
|
|
653
748
|
|
|
654
749
|
|
|
655
750
|
def _not_implemented(error: BaseException, *, session: bool) -> SessionWriteRefused | None:
|
|
751
|
+
"""A 501 is the server saying it does not serve this write yet.
|
|
752
|
+
|
|
753
|
+
Both routes report the 501 the server actually sent rather than asserting
|
|
754
|
+
the requested mode was the cause; implement mode is named as the documented
|
|
755
|
+
reason, not as a certainty.
|
|
756
|
+
"""
|
|
757
|
+
|
|
656
758
|
if getattr(error, "status_code", None) != 501:
|
|
657
759
|
return None
|
|
658
|
-
|
|
760
|
+
if session:
|
|
761
|
+
return SessionWriteRefused(
|
|
762
|
+
"unsupported",
|
|
763
|
+
"The server does not serve this session write yet (it answered 501, not implemented); the "
|
|
764
|
+
"write was refused and no changes were made. Implement mode is the documented reason: "
|
|
765
|
+
"implement-mode authoring is not available from this server yet.",
|
|
766
|
+
)
|
|
659
767
|
return SessionWriteRefused(
|
|
660
768
|
"unsupported",
|
|
661
|
-
|
|
662
|
-
"
|
|
769
|
+
"The server does not serve this message write yet (it answered 501, not implemented); no "
|
|
770
|
+
"message was added. Implement-mode messages are not available from this server yet.",
|
|
663
771
|
)
|
|
664
772
|
|
|
665
773
|
|
|
@@ -748,12 +856,7 @@ def list_messages(
|
|
|
748
856
|
_target(target, session_id),
|
|
749
857
|
{
|
|
750
858
|
"messages": projections,
|
|
751
|
-
"page":
|
|
752
|
-
"kind": "limit",
|
|
753
|
-
"limit": limit,
|
|
754
|
-
"returned_count": len(projections),
|
|
755
|
-
"continuation": None,
|
|
756
|
-
},
|
|
859
|
+
"page": _page(limit, MESSAGE_LIMIT_DEFAULT, len(projections)),
|
|
757
860
|
},
|
|
758
861
|
read_only=True,
|
|
759
862
|
)
|
|
@@ -982,7 +1085,7 @@ def generate_plan(
|
|
|
982
1085
|
except HealthCloudNetworkError:
|
|
983
1086
|
raise PlanReviewRefused("transport", UNKNOWN_PLAN_OUTCOME) from None
|
|
984
1087
|
except HealthCloudHTTPError as error:
|
|
985
|
-
refusal = _plan_write_refusal(error, session_id=session_id, plan_id=None)
|
|
1088
|
+
refusal = _plan_write_refusal(error, kind="generate", session_id=session_id, plan_id=None)
|
|
986
1089
|
if refusal is not None:
|
|
987
1090
|
raise refusal from error
|
|
988
1091
|
raise
|
|
@@ -1016,7 +1119,7 @@ def get_plan(dependencies: AmsDependencies, target: TenantTarget, plan_id: str,
|
|
|
1016
1119
|
"was lost. Nothing was written; retry the read when the service is reachable.",
|
|
1017
1120
|
) from None
|
|
1018
1121
|
except HealthCloudHTTPError as error:
|
|
1019
|
-
refusal = _plan_write_refusal(error, session_id=None, plan_id=plan_id)
|
|
1122
|
+
refusal = _plan_write_refusal(error, kind="get", session_id=None, plan_id=plan_id)
|
|
1020
1123
|
if refusal is not None:
|
|
1021
1124
|
raise refusal from error
|
|
1022
1125
|
raise
|
|
@@ -50,6 +50,25 @@ def attachment_projection(attachment: TuningAttachment) -> dict:
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
# Every review field whose value is model-generated text: the planner writes it
|
|
54
|
+
# from the operator's prompt and the session context, so it is data to inspect,
|
|
55
|
+
# never an instruction to follow. The review file carries this list beside the
|
|
56
|
+
# values it describes (an LLM01/LLM07 marker for the human approval decision).
|
|
57
|
+
MODEL_GENERATED_FIELDS = {
|
|
58
|
+
"modify": ("changes.*.why", "review_warnings[].message", "summary"),
|
|
59
|
+
"create": (
|
|
60
|
+
"definition.description",
|
|
61
|
+
"definition.greetings",
|
|
62
|
+
"definition.instructions",
|
|
63
|
+
"definition.name",
|
|
64
|
+
"definition.opening_suggestions",
|
|
65
|
+
"definition.opening_suggestions_options",
|
|
66
|
+
"review_warnings[].message",
|
|
67
|
+
"summary",
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
53
72
|
def plan_review(target: TenantTarget, plan: TuningPlan, operation: str, diff: dict) -> dict:
|
|
54
73
|
"""The owner-only review artifact for one exact plan.
|
|
55
74
|
|
|
@@ -57,10 +76,17 @@ def plan_review(target: TenantTarget, plan: TuningPlan, operation: str, diff: di
|
|
|
57
76
|
owner-only ``--out`` file and never printed: ``plan_json["original_prompt"]``
|
|
58
77
|
is not projected at all, ``warnings``/``skipped_references`` are reported as
|
|
59
78
|
counts rather than bodies, and file actions keep attachment IDs only.
|
|
79
|
+
|
|
80
|
+
The summary, the per-change ``why``, the proposed instructions/greetings and
|
|
81
|
+
the advisory findings are model-generated free text: the file marks them as
|
|
82
|
+
such with ``content_origin`` and ``model_generated_fields`` so the operator
|
|
83
|
+
approving it treats them as untrusted data, not as instructions.
|
|
60
84
|
"""
|
|
61
85
|
|
|
62
86
|
review = {
|
|
63
87
|
"schema_version": 1,
|
|
88
|
+
"content_origin": "model-generated",
|
|
89
|
+
"model_generated_fields": sorted(MODEL_GENERATED_FIELDS[operation]),
|
|
64
90
|
"target": {
|
|
65
91
|
"environment": target.environment,
|
|
66
92
|
"tenant_id": target.tenant_id,
|
|
@@ -95,6 +95,20 @@ def _user_sid() -> str:
|
|
|
95
95
|
_close(token)
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
class PrivateHandleRefused(ValueError):
|
|
99
|
+
"""A Windows handle check refused the file, with a value-free reason.
|
|
100
|
+
|
|
101
|
+
A distinct type (not a plain ``ValueError``) so a reader can report the real
|
|
102
|
+
reason — ``not_owner_only`` or ``not_regular``, the same vocabulary the POSIX
|
|
103
|
+
reader uses — instead of collapsing every refusal into "cannot be read".
|
|
104
|
+
Neither the message nor the reason ever names the path, the owner or an ACE.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, reason: str, message: str) -> None:
|
|
108
|
+
super().__init__(message)
|
|
109
|
+
self.reason = reason
|
|
110
|
+
|
|
111
|
+
|
|
98
112
|
def _require_private(handle, user_sid: str) -> None:
|
|
99
113
|
owner, acl, descriptor = _P(), _P(), _P()
|
|
100
114
|
status = _get_security(
|
|
@@ -104,7 +118,7 @@ def _require_private(handle, user_sid: str) -> None:
|
|
|
104
118
|
raise ctypes.WinError(status)
|
|
105
119
|
try:
|
|
106
120
|
if not owner or _sid_string(owner) != user_sid or not acl:
|
|
107
|
-
raise
|
|
121
|
+
raise PrivateHandleRefused("not_owner_only", "Owner-only Windows DACL required")
|
|
108
122
|
count = ctypes.cast(acl, ctypes.POINTER(_Acl)).contents.count
|
|
109
123
|
for index in range(count):
|
|
110
124
|
ace = _P()
|
|
@@ -114,7 +128,9 @@ def _require_private(handle, user_sid: str) -> None:
|
|
|
114
128
|
continue
|
|
115
129
|
# Only ordinary ACCESS_ALLOWED_ACE grants to the current user are accepted.
|
|
116
130
|
if kind != 0 or _sid_string(ace.value + 8) != user_sid:
|
|
117
|
-
raise
|
|
131
|
+
raise PrivateHandleRefused(
|
|
132
|
+
"not_owner_only", "Private Windows files must not grant access to other principals"
|
|
133
|
+
)
|
|
118
134
|
finally:
|
|
119
135
|
_free(descriptor)
|
|
120
136
|
|
|
@@ -195,7 +211,7 @@ def open_file(path: Path, *, private: bool = False, create: bool = False) -> int
|
|
|
195
211
|
info = (w.DWORD * 2)() # FILE_ATTRIBUTE_TAG_INFO
|
|
196
212
|
_check(_file_info(handle, 9, info, ctypes.sizeof(info)))
|
|
197
213
|
if _file_type(handle) != 1 or info[0] & (0x10 | 0x400): # Disk files only; no directories or reparse points.
|
|
198
|
-
raise
|
|
214
|
+
raise PrivateHandleRefused("not_regular", "Regular non-reparse Windows file required")
|
|
199
215
|
if user is not None:
|
|
200
216
|
_require_private(handle, user)
|
|
201
217
|
fd = msvcrt.open_osfhandle(handle, (os.O_WRONLY if create else os.O_RDONLY) | os.O_BINARY)
|
|
@@ -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.8"
|
|
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.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/__init__.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/data_sources/projections.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/departments/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/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.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/guardrails/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/guardrails/projections.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/organization/__init__.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/organization/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/healthcloud_cli/ams/organization/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/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
|
{healthcloud_cli-3.7.6 → healthcloud_cli-3.7.8}/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
|