healthcloud-cli 3.5.0__tar.gz → 3.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. healthcloud_cli-3.6.0/PKG-INFO +1641 -0
  2. healthcloud_cli-3.6.0/README.md +1623 -0
  3. healthcloud_cli-3.6.0/healthcloud_cli/admins/__init__.py +5 -0
  4. healthcloud_cli-3.6.0/healthcloud_cli/admins/commands.py +147 -0
  5. healthcloud_cli-3.6.0/healthcloud_cli/admins/group.py +22 -0
  6. healthcloud_cli-3.6.0/healthcloud_cli/admins/operations.py +169 -0
  7. healthcloud_cli-3.6.0/healthcloud_cli/ams/agents/commands.py +370 -0
  8. healthcloud_cli-3.6.0/healthcloud_cli/ams/agents/group.py +44 -0
  9. healthcloud_cli-3.6.0/healthcloud_cli/ams/agents/operations.py +1048 -0
  10. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/agents/projections.py +15 -2
  11. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/auth.py +45 -6
  12. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/compatibility.py +29 -21
  13. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/context.py +143 -16
  14. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/data_sources/commands.py +2 -2
  15. healthcloud_cli-3.6.0/healthcloud_cli/ams/departments/commands.py +263 -0
  16. healthcloud_cli-3.6.0/healthcloud_cli/ams/departments/group.py +34 -0
  17. healthcloud_cli-3.6.0/healthcloud_cli/ams/departments/operations.py +234 -0
  18. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/departments/projections.py +4 -1
  19. healthcloud_cli-3.6.0/healthcloud_cli/ams/domain_errors.py +36 -0
  20. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/errors.py +24 -5
  21. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/group.py +1 -1
  22. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/guardrails/__init__.py +1 -1
  23. healthcloud_cli-3.6.0/healthcloud_cli/ams/guardrails/commands.py +225 -0
  24. healthcloud_cli-3.6.0/healthcloud_cli/ams/guardrails/group.py +30 -0
  25. healthcloud_cli-3.6.0/healthcloud_cli/ams/guardrails/operations.py +633 -0
  26. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/organization/commands.py +1 -1
  27. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/output.py +30 -0
  28. healthcloud_cli-3.6.0/healthcloud_cli/ams/policies/commands.py +270 -0
  29. healthcloud_cli-3.6.0/healthcloud_cli/ams/policies/group.py +34 -0
  30. healthcloud_cli-3.6.0/healthcloud_cli/ams/policies/operations.py +269 -0
  31. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/pretty.py +36 -0
  32. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/registry.py +2 -0
  33. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/results.py +29 -0
  34. healthcloud_cli-3.6.0/healthcloud_cli/ams/sessions/__init__.py +5 -0
  35. healthcloud_cli-3.6.0/healthcloud_cli/ams/sessions/commands.py +551 -0
  36. healthcloud_cli-3.6.0/healthcloud_cli/ams/sessions/group.py +75 -0
  37. healthcloud_cli-3.6.0/healthcloud_cli/ams/sessions/operations.py +1790 -0
  38. healthcloud_cli-3.6.0/healthcloud_cli/ams/sessions/projections.py +241 -0
  39. healthcloud_cli-3.6.0/healthcloud_cli/ams/skills/commands.py +195 -0
  40. healthcloud_cli-3.6.0/healthcloud_cli/ams/skills/group.py +30 -0
  41. healthcloud_cli-3.6.0/healthcloud_cli/ams/skills/operations.py +201 -0
  42. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/target.py +9 -3
  43. healthcloud_cli-3.6.0/healthcloud_cli/ams/teams/commands.py +205 -0
  44. healthcloud_cli-3.6.0/healthcloud_cli/ams/teams/group.py +30 -0
  45. healthcloud_cli-3.6.0/healthcloud_cli/ams/teams/operations.py +208 -0
  46. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/tools/commands.py +2 -2
  47. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/tuning.py +3 -3
  48. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/cli.py +18 -78
  49. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/contracts/mcp-tools.manifest.json +2 -2
  50. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/output.py +56 -8
  51. healthcloud_cli-3.6.0/healthcloud_cli/tenants/__init__.py +5 -0
  52. healthcloud_cli-3.6.0/healthcloud_cli/tenants/commands.py +166 -0
  53. healthcloud_cli-3.6.0/healthcloud_cli/tenants/group.py +22 -0
  54. healthcloud_cli-3.6.0/healthcloud_cli/tenants/operations.py +180 -0
  55. healthcloud_cli-3.6.0/healthcloud_cli/util.py +110 -0
  56. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/pyproject.toml +1 -1
  57. healthcloud_cli-3.5.0/PKG-INFO +0 -790
  58. healthcloud_cli-3.5.0/README.md +0 -772
  59. healthcloud_cli-3.5.0/healthcloud_cli/ams/agents/commands.py +0 -92
  60. healthcloud_cli-3.5.0/healthcloud_cli/ams/agents/group.py +0 -18
  61. healthcloud_cli-3.5.0/healthcloud_cli/ams/agents/operations.py +0 -95
  62. healthcloud_cli-3.5.0/healthcloud_cli/ams/departments/commands.py +0 -61
  63. healthcloud_cli-3.5.0/healthcloud_cli/ams/departments/group.py +0 -18
  64. healthcloud_cli-3.5.0/healthcloud_cli/ams/departments/operations.py +0 -73
  65. healthcloud_cli-3.5.0/healthcloud_cli/ams/domain_errors.py +0 -9
  66. healthcloud_cli-3.5.0/healthcloud_cli/ams/guardrails/commands.py +0 -57
  67. healthcloud_cli-3.5.0/healthcloud_cli/ams/guardrails/group.py +0 -18
  68. healthcloud_cli-3.5.0/healthcloud_cli/ams/guardrails/operations.py +0 -70
  69. healthcloud_cli-3.5.0/healthcloud_cli/ams/policies/commands.py +0 -57
  70. healthcloud_cli-3.5.0/healthcloud_cli/ams/policies/group.py +0 -18
  71. healthcloud_cli-3.5.0/healthcloud_cli/ams/policies/operations.py +0 -68
  72. healthcloud_cli-3.5.0/healthcloud_cli/ams/skills/commands.py +0 -57
  73. healthcloud_cli-3.5.0/healthcloud_cli/ams/skills/group.py +0 -18
  74. healthcloud_cli-3.5.0/healthcloud_cli/ams/skills/operations.py +0 -68
  75. healthcloud_cli-3.5.0/healthcloud_cli/ams/teams/commands.py +0 -57
  76. healthcloud_cli-3.5.0/healthcloud_cli/ams/teams/group.py +0 -18
  77. healthcloud_cli-3.5.0/healthcloud_cli/ams/teams/operations.py +0 -68
  78. healthcloud_cli-3.5.0/healthcloud_cli/util.py +0 -29
  79. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/.gitignore +0 -0
  80. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/hatch_build.py +0 -0
  81. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/__init__.py +0 -0
  82. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/__init__.py +0 -0
  83. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/agents/__init__.py +0 -0
  84. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/commands.py +0 -0
  85. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
  86. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/data_sources/group.py +0 -0
  87. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/data_sources/operations.py +0 -0
  88. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/data_sources/projections.py +0 -0
  89. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/departments/__init__.py +0 -0
  90. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/guardrails/projections.py +0 -0
  91. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/organization/__init__.py +0 -0
  92. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/organization/group.py +0 -0
  93. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/organization/operations.py +0 -0
  94. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/organization/projections.py +0 -0
  95. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/policies/__init__.py +0 -0
  96. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/policies/projections.py +0 -0
  97. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/session.py +0 -0
  98. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/skills/__init__.py +0 -0
  99. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/skills/projections.py +0 -0
  100. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/teams/__init__.py +0 -0
  101. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/teams/projections.py +0 -0
  102. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/tools/__init__.py +0 -0
  103. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/tools/group.py +0 -0
  104. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/tools/operations.py +0 -0
  105. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/tools/projections.py +0 -0
  106. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/ui.py +0 -0
  107. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/ams/windows_files.py +0 -0
  108. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/context.py +0 -0
  109. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/contracts/__init__.py +0 -0
  110. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/main.py +0 -0
  111. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/manifest.py +0 -0
  112. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/healthcloud_cli/params.py +0 -0
  113. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/scripts/__init__.py +0 -0
  114. {healthcloud_cli-3.5.0 → healthcloud_cli-3.6.0}/scripts/bundle_manifest.py +0 -0
@@ -0,0 +1,1641 @@
1
+ Metadata-Version: 2.5
2
+ Name: healthcloud-cli
3
+ Version: 3.6.0
4
+ Summary: Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest.
5
+ Project-URL: Issues, https://github.com/healthcloudservices/healthcloud-services/issues
6
+ Project-URL: Repository, https://github.com/healthcloudservices/healthcloud-services
7
+ Author: Healthcheck Systems Inc
8
+ License-Expression: MIT
9
+ Keywords: cli,health-cloud,healthcare,healthcloud,mcp
10
+ Requires-Python: >=3.14
11
+ Requires-Dist: click>=8.1
12
+ Requires-Dist: healthcloud-sdk>=3.14.0
13
+ Requires-Dist: healthcloud-tenantadmin>=1.1.1
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
16
+ Requires-Dist: respx>=0.21; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # healthcloud-cli
20
+
21
+ Requires **Python 3.14 or newer**.
22
+
23
+ `hc` is an SDK-backed CLI: the primary surface is `hc ams`, which reads and
24
+ tunes one explicitly selected AMS tenant (catalog domains -- skills, policies,
25
+ guardrails, teams, departments, organization, data-sources, tools -- plus
26
+ read-only agent operations). The `hc mcp` subtree is the gateway fallback and
27
+ has exactly one subcommand per
28
+ MCP tool -- generated dynamically at startup from a shared manifest across
29
+ audiences such as `patient`, `fieldagent`, `ehr`, `vitals`, `diagnostics`,
30
+ `appointments`, `graph`, `person`, `provider`, `tenantadmin`, `connectors`,
31
+ and `telehealth`. Run `hc mcp tools list` to see the exact, current set for
32
+ your installed version. Nothing here is hand-written per tool: the same
33
+ manifest also drives the sibling NPM CLI (`@healthcloud CLI` in
34
+ `packages/cli/npm`), so command names match exactly between the two.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install healthcloud-cli
40
+ ```
41
+
42
+ This installs the `hc` console script plus its dependency, the
43
+ [`healthcloud-sdk`](../../pip) package, which does the actual HTTP/JSON-RPC
44
+ work against the MCP gateway.
45
+
46
+ ## Local development
47
+
48
+ Use uv 0.12.5 and Python 3.14. The package's uv source override installs the
49
+ SDK checkout in editable mode; its committed lock includes the test and build
50
+ tools. Released package consumers can still use pip.
51
+
52
+ ```bash
53
+ cd packages/cli/pip
54
+ uv lock --check
55
+ uv sync --frozen --extra dev
56
+ uv run --frozen --extra dev python -m pytest
57
+ uv run --frozen hc --help
58
+ uv run --frozen hc mcp tools list --audience patient
59
+ uv run --frozen hc mcp patient get-patient --help
60
+ ```
61
+
62
+ AMS help and shell-completion discovery are credential-free and offline: they
63
+ do not resolve root profiles, read target files, create SDK clients, or log in.
64
+ You can inspect the group, a leaf command, and completion candidates before
65
+ configuring credentials or a target:
66
+
67
+ ```bash
68
+ uv run --frozen hc ams --help
69
+ uv run --frozen hc ams inspect --help
70
+ _HC_COMPLETE=bash_complete COMP_WORDS="hc ams in" COMP_CWORD=2 uv run --frozen hc
71
+ _HC_COMPLETE=bash_complete COMP_WORDS="hc ams inspect --o" COMP_CWORD=3 uv run --frozen hc
72
+ ```
73
+
74
+ Operational validation starts only when Click invokes an AMS leaf callback.
75
+ `discover`, `agents`, `departments`, `teams`, `organization`, `skills`, `policies`, and `sessions`
76
+ (with `attachments` and `messages`) then
77
+ validate their explicit environment and tenant arguments; the remaining leaves
78
+ require `--target-file`. All apply the root, target, and authentication checks
79
+ described below. Deferring preflight does not relax those safety requirements.
80
+
81
+ The bundled tool manifest (`healthcloud_cli/contracts/mcp-tools.manifest.json`)
82
+ is a copy of `packages/cli/contracts/mcp-tools.manifest.json`. Re-sync it
83
+ after pulling manifest changes with:
84
+
85
+ ```bash
86
+ python scripts/bundle_manifest.py
87
+ ```
88
+
89
+ This also runs automatically as a hatchling build hook whenever you `python
90
+ -m build`, so a built wheel/sdist is always self-contained and never depends
91
+ on a relative path outside the installed package.
92
+
93
+ ## Authentication & configuration
94
+
95
+ Every command accepts these global options (place them before the `mcp`
96
+ subcommand):
97
+
98
+ | Flag | Env var | Meaning |
99
+ |-------------------|--------------------|-----------------------------------------------|
100
+ | `--access-token` | `HC_ACCESS_TOKEN` | Bearer token sent to the MCP gateway |
101
+ | `--base-url` | `HC_MCP_BASE_URL` | Override the MCP gateway base URL |
102
+ | `--profile` | `HC_PROFILE` | Named profile (see below) |
103
+ | `--output` | -- | `pretty` (default) \| `json` \| `raw` |
104
+ | `--raw` | -- | Shortcut for `--output raw` |
105
+ | `--verbose` | -- | Print request diagnostics to stderr |
106
+
107
+ Precedence, highest wins: **explicit CLI flag > environment variable >
108
+ profile file value > built-in default** (`environment` defaults to `"dev"`;
109
+ `accessToken`/`baseUrl` default to unset). The resolved access token is never
110
+ written to logs, generated files, or test snapshots.
111
+
112
+ ### Saved `hc login` session fallback
113
+
114
+ When no explicit token is given (`--access-token`, `HC_ACCESS_TOKEN`, or a
115
+ profile `accessToken`), `hc mcp` uses the saved `hc login` tenant-admin
116
+ session as the bearer, with the same locked refresh-near-expiry behavior as
117
+ `hc ams` (the refreshed session is persisted for the next command):
118
+
119
+ ```bash
120
+ hc login --environment dev --tenant-id TENANT_ID # saves/refreshes the session
121
+ hc mcp tenantadmin get-current-tenant-admin # uses the saved session bearer
122
+ ```
123
+
124
+ On `HTTP 401` when no explicit token and no saved session exist, the CLI
125
+ prints one hint line — `Hint: not logged in; run 'hc login', or pass
126
+ --access-token / HC_ACCESS_TOKEN.` — after the failure line, and exits `3`.
127
+ An explicit token always wins over the saved session; `hc logout` removes it
128
+ (local only).
129
+
130
+ ### Optional profile file
131
+
132
+ `~/.healthcloud/cli/profiles.json`:
133
+
134
+ ```json
135
+ {
136
+ "dev-mia": {
137
+ "baseUrl": "https://dev-api-mcp.health.cloud",
138
+ "accessToken": "eyJ...",
139
+ "environment": "dev"
140
+ }
141
+ }
142
+ ```
143
+
144
+ A missing profiles file is not an error -- profiles are entirely optional. If
145
+ `--profile`/`HC_PROFILE` names a profile and the file (or that entry) is
146
+ missing, `hc` exits with a clear usage error rather than silently ignoring
147
+ it.
148
+
149
+ ```bash
150
+ # get-patient is self-service — the patient is resolved from the access
151
+ # token, so no --patient-id flag exists.
152
+ hc --profile dev-mia mcp patient get-patient
153
+ ```
154
+
155
+ ## Listing and inspecting tools
156
+
157
+ ```bash
158
+ # Local (bundled manifest, no network call)
159
+ hc mcp tools list
160
+ hc mcp tools list --audience patient
161
+
162
+ # Live gateway (requires --audience)
163
+ hc mcp tools list --audience patient --remote
164
+
165
+ # Full manifest entry for one tool (description, inputSchema, method/path)
166
+ hc mcp tools show patient add_insurance --output json
167
+ ```
168
+
169
+ ## Calling a tool
170
+
171
+ Every manifest tool is its own subcommand, grouped by audience:
172
+
173
+ ```bash
174
+ hc mcp patient get-patient
175
+ hc mcp appointments book-appointment --patient-id patient-123 --slot-id slot-456
176
+
177
+ # Every Diagnostics tool takes an explicit --patient-id — providers and
178
+ # field agents legitimately look up or record diagnostics for a patient
179
+ # other than themselves. When the caller holds a patient token, the backend
180
+ # enforces server-side that --patient-id must be their own (403 otherwise).
181
+ hc mcp diagnostics create-rapid-test --patient-id patient-123 --test-type covid-19 --result negative
182
+ ```
183
+
184
+ Run `--help` on any generated command to see its real manifest description
185
+ and arguments:
186
+
187
+ ```bash
188
+ hc mcp patient get-patient --help
189
+ ```
190
+
191
+ ### Argument flags
192
+
193
+ - **Primitive properties** (`string`/`number`/`integer`/`boolean`) become a
194
+ normal flag in kebab-case, e.g. `patient_id` -> `--patient-id`.
195
+ - `enum` values are validated; invalid values are rejected before any
196
+ network call.
197
+ - `minimum`/`maximum`/`exclusiveMinimum`/`exclusiveMaximum` are enforced
198
+ the same way.
199
+ - Boolean properties are a tri-state flag pair: `--flag/--no-flag`.
200
+ Passing neither leaves the value unset (falls back to the manifest
201
+ default or an `--input`/`--stdin` value, if any); passing one sets it
202
+ explicitly to `true`/`false`.
203
+ - **Object/array properties are never flattened.** They get a single
204
+ `--<prop>-json` flag that takes a raw JSON string, e.g.:
205
+
206
+ ```bash
207
+ # create-medication is self-service — no --patient-id flag; the patient is
208
+ # resolved server-side from the access token.
209
+ hc mcp patient create-medication \
210
+ --name "Metformin" \
211
+ --dosage-json '{"value":10,"unit":"mg","frequency":"once_daily"}'
212
+ ```
213
+ - Path parameters (from `{placeholders}` in the tool's HTTP path) are always
214
+ required, in addition to whatever `inputSchema.required` lists.
215
+
216
+ ### Bulk input: `--input` / `--stdin`
217
+
218
+ ```bash
219
+ hc mcp appointments book-appointment --input request.json
220
+ cat request.json | hc mcp appointments book-appointment --stdin
221
+ ```
222
+
223
+ `--input <file>` and `--stdin` both supply a *base* JSON object of
224
+ arguments. `--input` and `--stdin` cannot be combined. Merge precedence
225
+ (highest wins): **manifest defaults < `--input`/`--stdin` object < explicit
226
+ flags** -- i.e. an explicit flag you actually pass always overrides the same
227
+ key coming from a file or stdin, which in turn overrides the schema default.
228
+
229
+ After merging, the final object is validated against the tool's
230
+ `inputSchema` (required fields present, correct types, enum membership,
231
+ numeric bounds). Any validation failure prints a clear message to stderr and
232
+ exits **2**, without making a network call.
233
+
234
+ ### `hc mcp call` -- generic escape hatch
235
+
236
+ `hc mcp call` dispatches to any MCP tool by its exact snake_case tool name --
237
+ including one not yet present in the bundled manifest (the tool name itself
238
+ is never validated against the manifest, it's just forwarded):
239
+
240
+ ```bash
241
+ # get_patient is self-service and takes no arguments — {} is a complete,
242
+ # valid call.
243
+ hc mcp call patient get_patient --input '{}'
244
+ hc mcp call patient get_patient --input-file request.json
245
+ cat request.json | hc mcp call patient get_patient --stdin
246
+ ```
247
+
248
+ Note the deliberate inconsistency versus named commands: here `--input` takes
249
+ an inline JSON **value**, not a file path -- use `--input-file` for a file.
250
+ Exactly one of `--input` / `--input-file` / `--stdin` may be given (or none,
251
+ meaning `{}`).
252
+
253
+ ## Output modes
254
+
255
+ The MCP tool-call result is `{ content: [{type:"text", text: "..."}],
256
+ is_error: bool }` from the JSON-RPC response's `.result`, or the JSON-RPC
257
+ `.error` object if the gateway returned one.
258
+
259
+ - **`pretty`** (default): if `content[0].text` parses as JSON, pretty-prints
260
+ the parsed value; otherwise prints the raw text. On a JSON-RPC error,
261
+ prints `error.message` (and `error.code` with `--verbose`) to stderr.
262
+ - **`json`**: prints the tool result's parsed JSON (or raw text if
263
+ unparseable) to stdout, with no envelope.
264
+ - **`raw`**: prints the complete JSON-RPC envelope (`id`, `jsonrpc`,
265
+ `result`, `error`) as returned by the gateway.
266
+
267
+ Diagnostics and `--verbose` logs always go to stderr -- stdout stays
268
+ pipeable in every mode.
269
+
270
+ ## Exit codes
271
+
272
+ | Code | Meaning |
273
+ |------|-----------------------------------------------------------------------|
274
+ | `0` | Success |
275
+ | `2` | Local input validation failure (bad JSON, missing required field, invalid enum/number, `--input`+`--stdin` both given, unknown option) -- no network call made |
276
+ | `3` | HTTP/network failure talking to the MCP gateway |
277
+ | `4` | JSON-RPC error response (`.error` is not `None`) |
278
+ | `5` | MCP tool result reported `is_error: true` |
279
+
280
+ ## Building
281
+
282
+ ```bash
283
+ uv lock --check
284
+ uv run --frozen python -m build --no-isolation
285
+ uv run --frozen python -m zipfile -l dist/*.whl # confirm the bundled manifest is included
286
+ ```
287
+
288
+ ## Deployed AMS administration (source checkout)
289
+
290
+ The reviewed AMS administration SDK is not published yet. Use this checkout's
291
+ editable SDK and tenant-admin sources; no released SDK version is claimed to
292
+ contain `ams_admin`. An installed SDK missing that capability is refused before
293
+ login. Ordinary MCP commands keep their existing behavior.
294
+
295
+ Prepare ignored SDK build READMEs from tracked inputs, then install the locked
296
+ editable packages (the CLI README itself is tracked):
297
+
298
+ ```bash
299
+ node tooling/docs-compiler/prepare-package-readmes.mjs
300
+ cd packages/cli/pip
301
+ uv sync --frozen --extra dev
302
+ uv run --frozen hc ams --help
303
+ ```
304
+
305
+ Create one private tenant-admin session before interactive AMS work, inspect its
306
+ non-secret binding, and remove it when finished:
307
+
308
+ ```bash
309
+ uv run --frozen hc login --environment dev --tenant-id TENANT_ID
310
+ uv run --frozen hc login --status
311
+ uv run --frozen hc logout
312
+ ```
313
+
314
+ `hc login` reads `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` when present,
315
+ otherwise it prompts (with hidden password input). It stores only the email,
316
+ tenant/environment binding, access/refresh tokens and expiry in the strict
317
+ owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
318
+
319
+ For CI and agents, log in non-interactively: pass `--email` and pipe the password
320
+ to `--password-stdin` (read as exactly one stdin line, never echoed, never a
321
+ flag value — there is no `--password` option):
322
+
323
+ ```bash
324
+ printf '%s\n' "$HC_AMS_ADMIN_PASSWORD" | \
325
+ uv run --frozen hc login --environment dev --tenant-id TENANT_ID \
326
+ --email "$HC_AMS_ADMIN_EMAIL" --password-stdin
327
+ ```
328
+
329
+ `--email` beats `HC_AMS_ADMIN_EMAIL` and `--password-stdin` beats
330
+ `HC_AMS_ADMIN_PASSWORD`. With `--password-stdin`, the piped line is always the
331
+ password: it is read before any prompt, so it can never be consumed as the
332
+ email, and the email must come from `--email`, `HC_AMS_ADMIN_EMAIL` or (only on
333
+ an interactive terminal) the email prompt. When stdin is not an interactive
334
+ terminal and neither `--password-stdin` nor `HC_AMS_ADMIN_PASSWORD` supplies the
335
+ password, `hc login` refuses with that guidance instead of attempting a hidden
336
+ prompt (no `GetPassWarning`, no echoed password); a missing email on a
337
+ non-interactive stdin (or a process with no stdin stream at all) is refused the
338
+ same way, naming `--email`/`--password-stdin`; every refusal happens before any
339
+ login request. `--status` cannot be combined with `--email` or `--password-stdin`.
340
+ Even on an interactive terminal, `--password-stdin` suppresses terminal echo
341
+ while the single line is read and restores it afterwards; a stdin stream that is
342
+ closed or errors mid-read is refused cleanly (exit 2, never a traceback).
343
+ AMS commands may omit both target flags while this session is present, except
344
+ that a `prod` session still needs `--environment prod` on every command. Explicit
345
+ flags and target files must match it exactly. A session within 60 seconds of
346
+ expiry is refreshed once before the operation, under a lock so concurrent
347
+ commands never spend the same refresh token (a second command waits up to 30 seconds, then stops), and
348
+ atomically replaced. Refresh
349
+ failure stops without an AMS call or password fallback and asks for a new login.
350
+ If the refreshed session cannot be saved, the current command still finishes and
351
+ prints a warning to run `hc login` again.
352
+
353
+ `hc login` works before the admin's email is verified. The current-admin
354
+ profile reports `email_verified` (invited admins start verified); confirm the
355
+ emailed code with `hc tenants verify-email`.
356
+
357
+ `~/.healthcloud` and `~/.healthcloud/cli` must be directories you own with mode
358
+ 700; the CLI creates them that way, and refuses (naming the directory and the
359
+ `chmod 700` fix) if an existing one is shared. `hc logout` deletes the local file
360
+ only and reports `"server_session_revoked": false`: issued tokens stay valid
361
+ until they expire.
362
+
363
+ ## Tenant registration: `hc tenants register`
364
+
365
+ Creates a tenant and its first (primary) tenant admin through the tenant-admin
366
+ SDK (`POST /tenants/register`) — CLI-first, never an MCP passthrough:
367
+
368
+ ```bash
369
+ printf '%s\n' "$ADMIN_PASSWORD" | \
370
+ uv run --frozen hc tenants register \
371
+ --tenant-id acmeclinic --name "Acme Clinic" \
372
+ --admin-first-name Ana --admin-last-name Doe \
373
+ --admin-email ana@example.test --password-stdin
374
+ ```
375
+
376
+ - Requires a saved `hc login` session — the session token is attached to the
377
+ SDK before the call; without a session the command refuses with
378
+ "run `hc login` before registering a tenant" and makes no network call.
379
+ `--environment` must match the saved session's environment.
380
+ - `--tenant-id` is validated locally (lowercase a-z only, e.g. `acmeclinic`)
381
+ before any network call; anything else is refused without a request.
382
+ - The admin password comes only from a hidden interactive prompt or
383
+ `--password-stdin` (exactly one stdin line, echo-suppressed). There is no
384
+ `--password` flag, and the password is never echoed or logged.
385
+ - Output: a `tenants.register` envelope with the tenant ID, status, tier,
386
+ admin ID and `email_otp_sent`, plus a next-step hint pointing at
387
+ `hc tenants verify-email`. A 409 is reported as "A tenant with this ID
388
+ already exists"; other API errors surface the backend's safe message.
389
+
390
+ ## Tenant email verification: `hc tenants verify-email` and `hc tenants resend-code`
391
+
392
+ Confirms (or re-sends) the registration OTP through the tenant-admin SDK
393
+ (`POST /auth/verify-email` / `POST /auth/resend-verification-code`) —
394
+ CLI-first, never an MCP passthrough:
395
+
396
+ ```bash
397
+ uv run --frozen hc tenants verify-email \
398
+ --environment dev --tenant-id acmeclinic --email ana@example.test --otp 123456
399
+ uv run --frozen hc tenants resend-code \
400
+ --environment dev --tenant-id acmeclinic --email ana@example.test
401
+ ```
402
+
403
+ - No saved `hc login` session is required: the emailed OTP is the proof, and
404
+ no bearer token is sent.
405
+ - `--tenant-id` is validated locally (lowercase a-z only) before any network
406
+ call; `--email` and `--otp` must be nonblank.
407
+ - `verify-email` output: a `tenants.verify-email` envelope with the verified
408
+ email and the exact next command to run —
409
+ `hc login --environment dev --tenant-id acmeclinic --email ana@example.test`
410
+ (the password is prompted, never a flag value). `email_verified` is `true`
411
+ only when the service answers `verified: true`; any other answer reports `false`.
412
+ - `resend-code` output: a `tenants.resend-code` envelope with `sent: true`.
413
+ - A wrong or expired code is reported as the server's safe message (e.g.
414
+ "Verification code is invalid or expired"); an unknown email is a
415
+ not-found error. Both are bounded JSON envelopes, never stack traces.
416
+
417
+ ## Managing tenant admins: `hc admins create`, `hc admins list`, `hc admins get`
418
+
419
+ Adds and reads a tenant's admins through the tenant-admin SDK
420
+ (`POST /auth/register` mode=self, `GET /tenant-admins`, `GET /admins/{admin_id}`)
421
+ — CLI-first, never an MCP passthrough:
422
+
423
+ ```bash
424
+ printf '%s\n' "$ADMIN_PASSWORD" | \
425
+ uv run --frozen hc admins create --first-name Bo --last-name Nguyen \
426
+ --email bo@example.test --password-stdin
427
+ uv run --frozen hc admins list
428
+ uv run --frozen hc admins get --admin-id ADMIN_ID
429
+ ```
430
+
431
+ - All three commands require a saved `hc login` session; the session's tenant
432
+ is always the target (there is no `--tenant-id` flag). Without a session the
433
+ command refuses with "run `hc login` before managing tenant admins" and
434
+ makes no network call. `--environment` (optional) must match the saved
435
+ session's environment.
436
+ - `create` adds another admin to the saved session's tenant. The new admin's
437
+ password comes only from a hidden interactive prompt or `--password-stdin`
438
+ (exactly one stdin line, echo-suppressed); there is no `--password` flag and
439
+ the password is never echoed or logged. Its success envelope carries the
440
+ new `admin_id`, `email_otp_sent` and a next-step hint pointing at
441
+ `hc tenants verify-email`. A 409 is reported as "A tenant admin with this
442
+ email already exists for the tenant".
443
+ - `list` and `get` are read-only, unpaged and bounded: each admin is projected
444
+ to `id`, `name`, `email`, `status` and `is_primary` only — never phone
445
+ numbers, Cognito subs, permissions or audit fields. `list` emits
446
+ `data.admins` plus an unpaged `page` block; `get` emits `data.admin`. Both
447
+ omit the `verification` block, like every read.
448
+ - Failures are the shared bounded error envelopes: not-found (`get` on an
449
+ unknown ID), conflict (duplicate email on `create`), validation (blank
450
+ inputs or an environment mismatch, refused before any network call),
451
+ authentication (no saved session) and transport — never stack traces.
452
+
453
+ Bootstrap candidate IDs with the public SDK discovery endpoint. Without a saved
454
+ session both flags are required; a saved session supplies both together. No MCP
455
+ defaults, admin login, or automatic candidate selection are used:
456
+
457
+ ```bash
458
+ uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
459
+ ```
460
+
461
+ Read-only agent listing, inspection and export take flags instead of a target
462
+ file; both may be omitted together when a matching saved session exists. Without
463
+ one, they log in as tenant admin using `HC_AMS_ADMIN_EMAIL` /
464
+ `HC_AMS_ADMIN_PASSWORD`. They emit the same versioned result envelope as the
465
+ other read domains (`operation`, `outcome`, `schema_version`, `target`, `data`;
466
+ no `verification` block on reads); failures emit one categorized JSON error
467
+ envelope on stderr with the same categories (not found, target mismatch,
468
+ refusal, logged out), not the plain-text `Error: …` lines of earlier releases.
469
+ `agents list` carries bounded projections under `data.agents` plus a `page`
470
+ block; `agents get` carries the bounded projection under `data.agent`, whose
471
+ `assigned_references` holds both halves of an assignment: the nullable
472
+ `department_id`/`team_id` organization references and the five capability lists
473
+ (`tool_ids`, `skill_ids`, `guardrail_ids`, `policy_ids`, `data_source_ids`). The
474
+ legacy top-level `{"agents": …, "environment": …, "tenant_id": …}` shape is
475
+ intentionally replaced by this envelope — scripts must read the new shape.
476
+ Stdout carries bounded metadata only; `adapter` shows
477
+ whether an agent runs from the `file` or `db` config adapter (`unknown` when the
478
+ server does not say). `export` first re-checks the agent/tenant identity, then
479
+ saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
480
+ file (exact bytes preserved), refusing (and leaving no file) when the body is
481
+ not valid UTF-8; its stdout reports the envelope with the file `path`, `format`
482
+ and `sha256` under `data` and the `agent_id` in `target`. `export`
483
+ needs an AMS build that serves `/agents/{agent_id}/configuration/export`; that
484
+ route is not yet deployed to dev:
485
+
486
+ ```bash
487
+ uv run --frozen hc ams agents list --environment dev --tenant-id TENANT_ID
488
+ uv run --frozen hc ams agents get --environment dev --tenant-id TENANT_ID --agent-id AGENT_ID
489
+ uv run --frozen hc ams agents export --environment dev --tenant-id TENANT_ID --agent-id AGENT_ID \
490
+ --format yaml --out agent.yaml
491
+ ```
492
+
493
+ For example, `hc ams agents list` succeeds with (formatted for readability):
494
+
495
+ ```json
496
+ {"schema_version": 1, "operation": "agents.list", "outcome": "succeeded",
497
+ "target": {"environment": "dev", "tenant_id": "TENANT_ID"},
498
+ "data": {"agents": [{"id": "AGENT_ID", "name": "…", "adapter": "file", "…": "…"}],
499
+ "page": {"kind": "unpaged", "returned_count": 1, "continuation": null}}}
500
+ ```
501
+
502
+ and a missing `--agent-id` fails with one envelope on stderr and exit 1:
503
+
504
+ ```json
505
+ {"schema_version": 1, "operation": "agents.get", "outcome": "failed",
506
+ "target": {"environment": "dev", "tenant_id": "TENANT_ID", "agent_id": "AGENT_ID"},
507
+ "error": {"category": "not_found", "message": "Agent 'AGENT_ID' was not found."}}
508
+ ```
509
+
510
+ Agent lifecycle changes are two first-class, send-once write commands. They
511
+ require a saved `hc login` session (env-var credentials are not accepted for
512
+ writes) and an explicit `--expected-config-version`, `--confirm` acknowledgement:
513
+
514
+ ```bash
515
+ uv run --frozen hc ams agents deactivate --environment dev --tenant-id TENANT_ID \
516
+ --agent-id AGENT_ID --expected-config-version N --confirm
517
+ uv run --frozen hc ams agents activate --environment dev --tenant-id TENANT_ID \
518
+ --agent-id AGENT_ID --expected-config-version N --confirm
519
+ ```
520
+
521
+ Each command issues exactly one `GET /agents/{agent_id}`, local pre-checks,
522
+ exactly one `PUT /agents/{agent_id}`, and exactly one `GET` readback. The PUT
523
+ body is exactly `{"status": …, "active": …}` and nothing else — the server's
524
+ update schema accepts ~40 fields, but a lifecycle command is not a generic CRUD
525
+ fallback, so there is no `agents update`, no `--status`/`--active` flag, and no
526
+ `archive` here (deletion is its own reviewed command, below). `deactivate` writes `status: "disabled"` **and**
527
+ `active: false`; `activate` writes `status: "active"` **and** `active: true`.
528
+ Writing one field and reporting the other is a false claim, so both are written
529
+ and both are read back.
530
+
531
+ The result envelope reports three facts separately under `data`:
532
+ `data.evidence.saved` (`confirmed` only when the readback shows the exact pair
533
+ that was sent, for this agent in this tenant), `data.evidence.side_effects`
534
+ (always `unverified`: no side-effect readback route exists) and
535
+ `data.evidence.runtime` (always `unverified`: no serving route exists). Exit 0
536
+ requires that readback confirmation; a readback mismatch exits nonzero with
537
+ `"outcome": "failed"` and `verification.saved_state: "failed"` and says the new
538
+ state is **not** confirmed. A readback that is missing, cannot be read as an
539
+ agent object, describes a different agent, or lands in a different tenant is the
540
+ same unconfirmed failure (never an internal error): `data.eligibility.fields_tested_by_read_paths` is then `null`
541
+ because those fields could not be observed. `data.eligibility` states that the
542
+ fields the read-eligibility paths test now read the new pair, and warns
543
+ explicitly that
544
+ `get_default_agent_for_tenant` / `get_orchestrator_for_tenant` never test these
545
+ fields — a disabled agent can still be selected by those paths — and that
546
+ in-flight runs, WSS sessions and schedules are untouched by this route.
547
+
548
+ Pre-checks refuse with exit 1 and **no write**: a missing `--confirm` (exit 2
549
+ usage error, before login), an agent outside the explicit tenant, an already
550
+ in-effect state, an unsupported `archived`/`draft` target (named as having no
551
+ supported reversible transition here), or a `config_version` that no longer
552
+ matches `--expected-config-version`. That version check is a local best-effort
553
+ guard only: the route has no `If-Match` and the server never compares the
554
+ version, so it cannot prove atomicity and a request could still race between
555
+ the read and the PUT.
556
+
557
+ A transport error or `5xx` from the PUT is reported as `outcome: "unknown"`
558
+ with `mutation_replayed: false`, `verification.saved_state: "unknown"` and a
559
+ `reconciliation: {"status": "not_read", …}` block; the CLI sends no retry and
560
+ does not re-read automatically — run `hc ams agents get` before doing anything
561
+ else. A failed readback after an accepted PUT is reported the same way (unknown,
562
+ not success). A `2xx` PUT answer whose body cannot be read as an agent object is
563
+ also `outcome: "unknown"` (sent once, saved state unverified), never an internal
564
+ error and never a success.
565
+
566
+ Capability and organization assignments are one first-class, send-once write
567
+ command. It requires a saved `hc login` session (env-var credentials are not
568
+ accepted for writes), an explicit `--expected-config-version`, and the
569
+ `--confirm` acknowledgement of the target and the printed reviewed diff:
570
+
571
+ ```bash
572
+ uv run --frozen hc ams agents assignments set --environment dev --tenant-id TENANT_ID \
573
+ --agent-id AGENT_ID --expected-config-version N --confirm \
574
+ --tool-ids TOOL_ID --tool-ids ANOTHER_TOOL_ID \
575
+ --clear-team
576
+ ```
577
+
578
+ There is **no** assignment route on the server: assignments are plain fields on
579
+ the one `PUT /agents/{agent_id}` route, so this command issues exactly one
580
+ `GET /agents/{agent_id}`, local pre-checks, exactly one `PUT`, and exactly one
581
+ `GET` readback, and the PUT body is **exactly** the reference fields the
582
+ reviewed diff changes — never the whole agent payload, never a generic
583
+ `agents update`, never the legacy `tools`/`data_sources` name aliases, and never
584
+ a lifecycle, model or instruction field.
585
+
586
+ Each family is either omitted (unchanged) or given exactly once:
587
+ `--department-id`, `--team-id`, and the repeatable `--skill-ids`, `--tool-ids`,
588
+ `--policy-ids`, `--guardrail-ids`, `--data-source-ids` flags replace that family
589
+ exactly; `--clear-department`, `--clear-team` and `--clear-skill-ids`,
590
+ `--clear-tool-ids`, `--clear-policy-ids`, `--clear-guardrail-ids`,
591
+ `--clear-data-source-ids` empty or clear it (`[]` for a list family, `null` for
592
+ the two nullable scalars). The server de-duplicates ids, so the CLI does too.
593
+ `health_service_id`, `custom_tools`, `artifact_kinds`, lifecycle and `workflow`
594
+ are not part of this command: a health-service assignment has no `hc` read
595
+ surface, so it would not be reviewable.
596
+
597
+ The reviewed diff is printed to stderr before the single write (ids only, never
598
+ instruction text): each changed field with its `before` -> `after` value, plus
599
+ sorted `added`/`removed` id lists for the list families. It is omitted under
600
+ `--output json`/`raw` so those modes keep stderr machine-parsable; the same diff
601
+ is carried in the envelope under `data.changed` as
602
+ `{field: {before, after, added, removed}}` (`added`/`removed` are `null` for the
603
+ two scalar references).
604
+
605
+ The envelope reports `data.agent_id`, `data.changed`,
606
+ `data.config_version: {before, after}` (a version advance is informational
607
+ only), and three facts separately under `data.evidence`: `saved` (`confirmed`
608
+ only when the readback is this agent in this tenant and **every** changed
609
+ reference equals the intended after value), `side_effects` (always `unverified`)
610
+ and `runtime` (always `unverified`). `data.mutation_replayed` is always `false`.
611
+ The three side effects are disclosed, not verified: this route does **not**
612
+ regenerate or publish the agent's Application file (only the plan-apply path
613
+ publishes), `disable_direct_agent_mutations` does not gate this `PUT`, and the
614
+ server audit log records an `UPDATE` with the payload. Runtime enforcement of
615
+ the new references is unobservable: no serving readback route exists, so this
616
+ command never claims it.
617
+
618
+ `tool_ids` and `data_source_ids` address **unscoped global catalogs** by design;
619
+ the server checks tenant scope only for the department, team, skill, policy and
620
+ guardrail references. This command does not imply otherwise, and it does not
621
+ re-implement the server's reference checks.
622
+
623
+ Pre-checks refuse with exit 1 and **no write**: a missing `--confirm` (exit 2
624
+ usage error, before login and before any request), no assignment option at all
625
+ (exit 2), setting and clearing the same family together (exit 2), an agent
626
+ outside the explicit tenant, a request where every reference already equals the
627
+ read state ("already in effect"), or a `config_version` that no longer matches
628
+ `--expected-config-version`. That version check is a local best-effort guard
629
+ only: the route has no `If-Match` and the server never compares the version, so
630
+ it cannot prove atomicity and a request could still race between the read and
631
+ the PUT. Operation-specific approval here is the CLI's own `--confirm` plus the
632
+ reviewed diff, deliberately separate from the tuning/plan-apply approval path;
633
+ the server enforces tenant isolation, reference existence and the audit log, and
634
+ it has **no** assignment approval or consent entity.
635
+
636
+ A transport error or `5xx` from the PUT is reported as `outcome: "unknown"` with
637
+ `mutation_replayed: false`, `verification.saved_state: "unknown"` and a
638
+ `reconciliation: {"status": "not_read", …}` block; the CLI sends no retry and
639
+ does not re-read automatically — run `hc ams agents get` before doing anything
640
+ else. A `2xx` PUT answer whose body cannot be read as an agent object is also
641
+ `outcome: "unknown"`. A readback that is missing, cannot be read as an agent, is
642
+ a different agent or a different tenant, or does not show exactly the references
643
+ that were sent, is `outcome: "failed"` with `verification.saved_state: "failed"`
644
+ and a re-read pointer — never an internal error, never a replay.
645
+
646
+ Assignments are **not** reachable through the tuning path: a generated plan that
647
+ carries a reference field is refused by field name by
648
+ `hc ams sessions plans apply`, so a plan/tuning approval can never grant
649
+ capabilities.
650
+
651
+ Deleting an agent is one first-class, send-once destructive command. It requires
652
+ a saved `hc login` session (env-var credentials are not accepted for writes) and
653
+ the `--confirm` acknowledgement of the target and disclosure block it prints:
654
+
655
+ ```bash
656
+ uv run --frozen hc ams agents delete --environment dev --tenant-id TENANT_ID \
657
+ --agent-id AGENT_ID --confirm
658
+ ```
659
+
660
+ Before the delete it reads the agent once and prints the resolved destructive
661
+ target (`agent_id`, `name`, `type`, `status`, `active`, `config_version`) and the
662
+ disclosure block to stderr, so the operator approves an actual record, not a
663
+ phrase. The same disclosure is carried under `data.disclosure` in the envelope
664
+ (`irreversible`, `generated_file`, `dependency_guard`, `restore`,
665
+ `authorization`, `recovery_trail`), and it is the honest server contract:
666
+
667
+ - the delete is a **hard delete** of the agent row: no soft delete, no
668
+ tombstone, no retention window and no undelete route;
669
+ - the generated agent code file is removed from the runtime;
670
+ - the server guards **one** reference only — schedules that point at the agent
671
+ answer `409` and nothing is deleted. Default-agent and orchestrator selection,
672
+ engine bindings (`default_agent_id`), sessions and utility agents are **not**
673
+ guarded (there is no foreign key), so this command cannot detect them;
674
+ - `hc ams restore` **cannot** restore a deleted agent (it replays tuning fields
675
+ onto an existing agent). Export the configuration first with
676
+ `hc ams agents export` if you may need it;
677
+ - the route needs an authenticated tenant user only: no admin/role check is
678
+ applied;
679
+ - only a server audit row (`AuditAction.DELETE`) remains.
680
+
681
+ The sequence is exactly one `GET /agents/{agent_id}`, exactly one
682
+ `DELETE /agents/{agent_id}` (204, no body) and exactly one confirmation `GET`.
683
+ Only a `404` from that confirmation read proves the row is gone: the envelope
684
+ then reports `data.deleted: true`, `data.evidence.deleted.status: "confirmed"`
685
+ with `confirmation: "not_found"` and `confirmation_status: 404`, plus
686
+ `data.evidence.side_effects` and `data.evidence.runtime` (both always
687
+ `unverified`) and `data.mutation_replayed: false`. The command prints no promise
688
+ of restore and has no `--force`, no `--dry-run`, no interactive prompt and no
689
+ `--expected-config-version` — the route has no `If-Match` or version check, so a
690
+ version flag would advertise protection the server does not offer.
691
+
692
+ Refusals claim nothing and send nothing extra: a missing `--confirm` is an exit
693
+ 2 usage error before login or any request; an agent outside the explicit tenant,
694
+ the tenant identity mismatch and a delete target that no longer exists are
695
+ refused before the `DELETE`; a `409` is a `conflict` refusal naming the referring
696
+ schedules; a `404` from the `DELETE` is `not_found`; and a `405` is the server's
697
+ refusal (`unsupported`). None of them is retried.
698
+
699
+ A transport error or `5xx` from the `DELETE` is `outcome: "unknown"` with
700
+ `mutation_replayed: false`, `verification.saved_state: "unknown"` and a
701
+ `reconciliation: {"status": "not_read", …}` block; nothing is replayed and the
702
+ agent is never restored — run `hc ams agents list` before doing anything else.
703
+ The same unknown outcome applies when the delete was accepted but the
704
+ confirmation read could not be read. A confirmation read that still returns the
705
+ agent (`200`) — or a `2xx` body that cannot be read as an agent — is
706
+ `outcome: "failed"` with `data.deleted: false` and
707
+ `verification.saved_state: "failed"`, never success.
708
+
709
+ Read-only department listing and inspection use the same explicit tenant-admin
710
+ targeting. They emit one versioned JSON result on stdout; failures emit one
711
+ categorized JSON result on stderr. Department descriptions, creator identities,
712
+ and evaluation-suite content are never included in the projection:
713
+
714
+ ```bash
715
+ uv run --frozen hc ams departments list --environment dev --tenant-id TENANT_ID
716
+ uv run --frozen hc ams departments get --environment dev --tenant-id TENANT_ID \
717
+ --department-id DEPARTMENT_ID
718
+ ```
719
+
720
+ Department reads are complete and unpaged.
721
+
722
+ Department writes are first-class, send-once commands that require a saved
723
+ `hc login` session (run `hc login` first; env-var credentials are not accepted
724
+ for writes). Each write is sent exactly once — the CLI never retries a write —
725
+ and the result envelope carries the server's authoritative readback of the
726
+ department (same bounded projection as reads):
727
+
728
+ ```bash
729
+ uv run --frozen hc ams departments create --environment dev --tenant-id TENANT_ID --name "Clinical Operations" --created-by-user-id ADMIN_USER_ID [--description TEXT] [--parent-department-id PARENT_ID] [--key KEY] [--required-eval-suite-ids SUITE_ID ...]
730
+ 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 ...]
731
+ uv run --frozen hc ams departments set-active --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
732
+ uv run --frozen hc ams departments deactivate --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
733
+ uv run --frozen hc ams departments delete --environment dev --tenant-id TENANT_ID --department-id DEPARTMENT_ID
734
+ ```
735
+
736
+ - `create` succeeds with `operation: "departments.create"` and
737
+ `data.department` = the server's 201 readback. `--name` and
738
+ `--created-by-user-id` are required by the server contract (the tenant is
739
+ taken from the saved session's authenticated admin, never from a flag).
740
+ - `update` sends only the options you pass (an empty `--description ""`
741
+ clears it); it refuses with exit 1 (`validation`) when no field option is
742
+ given. `set-active` and `deactivate` mark the department active/inactive.
743
+ - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
744
+ with no body) and is irreversible.
745
+ - A 404 for the requested department is a `not_found` refusal naming the ID
746
+ (on `create`, a 404 names the referenced department — typically the missing
747
+ `--parent-department-id`); a target/tenant or identity mismatch is a
748
+ `validation` refusal; both exit 1
749
+ without changes. The one exception is a tenant mismatch on the `create`
750
+ readback: the department has already been created server-side, so the CLI
751
+ reports a `validation` refusal that names the department, the tenant that
752
+ received it, and tells you to run `hc ams departments get` — it never claims
753
+ a landed write was a stopped no-op.
754
+ - If the transport times out or the connection drops mid-write, the write's
755
+ outcome is unknown: the CLI reports one `transport` error whose message says
756
+ the outcome is unknown and to check `hc ams departments get` before
757
+ retrying. It never re-sends the write on its own.
758
+
759
+ Read-only team listing and inspection are also complete and unpaged. The bounded
760
+ projection omits descriptions and creator identities; it includes evaluation-suite
761
+ IDs but never suite content:
762
+
763
+ ```bash
764
+ uv run --frozen hc ams teams list --environment dev --tenant-id TENANT_ID
765
+ uv run --frozen hc ams teams get --environment dev --tenant-id TENANT_ID --team-id TEAM_ID
766
+ ```
767
+
768
+ Team writes are first-class, send-once commands that require a saved `hc login`
769
+ session (run `hc login` first; env-var credentials are not accepted for writes).
770
+ Each write is sent exactly once — the CLI never retries a write — and the result
771
+ envelope carries the server's authoritative readback of the team (same bounded
772
+ projection as reads). Teams have no active/inactive lifecycle, so there are no
773
+ `set-active`/`deactivate` commands:
774
+
775
+ ```bash
776
+ 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 ...]
777
+ 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 ...]
778
+ uv run --frozen hc ams teams delete --environment dev --tenant-id TENANT_ID --team-id TEAM_ID
779
+ ```
780
+
781
+ - `create` succeeds with `operation: "teams.create"` and `data.team` = the
782
+ server's 201 readback. `--name`, `--department-id` and
783
+ `--created-by-user-id` are required by the server contract (the tenant is
784
+ taken from the saved session's authenticated admin, never from a flag).
785
+ - `update` sends only the options you pass (an empty `--description ""`
786
+ clears it; `--clear-required-eval-suite-ids` sends an empty list to clear
787
+ the required evaluation suites; `--key` accepts a nonblank key, it cannot be
788
+ blanked); it refuses with exit 1 (`validation`) when no field option is
789
+ given.
790
+ - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
791
+ with no body) and is irreversible. When AI agents still reference the team,
792
+ the server refuses with 409 and the CLI reports one `conflict` refusal that
793
+ names the team and says agents reference it; no changes were made.
794
+ - A 404 for the requested team is a `not_found` refusal naming the ID (on
795
+ `create`, a 404 names the referenced department — the missing
796
+ `--department-id`); a target/tenant mismatch on an update readback is a
797
+ `validation` refusal that reports the landed write truthfully: it names the
798
+ team and the tenant that actually received it and points to `hc ams teams
799
+ get` — it never claims a landed write was stopped.
800
+ - If the transport times out or the connection drops mid-write, the write's
801
+ outcome is unknown: the CLI reports one `transport` error whose message says
802
+ the outcome is unknown and to check `hc ams teams get` before retrying. It
803
+ never re-sends the write on its own.
804
+
805
+ The organization hierarchy is the backend-built department/team/agent tree. The
806
+ CLI preserves its order and nesting without issuing entity reads, joining records,
807
+ or inferring missing relationships:
808
+
809
+ ```bash
810
+ uv run --frozen hc ams organization hierarchy --environment dev --tenant-id TENANT_ID
811
+ ```
812
+
813
+ Read-only skill catalog listing and inspection are complete and unpaged. The
814
+ bounded projection includes only skill IDs, names, tool IDs, and audit timestamps;
815
+ it omits descriptions, tenant identity, creator identity, arbitrary response
816
+ fields, and tool details. Tool-ID order is preserved:
817
+
818
+ ```bash
819
+ uv run --frozen hc ams skills list --environment dev --tenant-id TENANT_ID
820
+ uv run --frozen hc ams skills get --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID
821
+ ```
822
+
823
+ The CLI does not assign skills. Skill commands do not
824
+ read guardrails. They make exactly one catalog GET after tenant-admin login
825
+ and does not paginate, retry, cache, join, or infer additional catalog data.
826
+ Skill reads refuse target files, root credentials or base URLs, raw output,
827
+ blank or dot-segment skill IDs, and installed SDKs that do not provide both
828
+ skill list and get methods. These local refusals happen before tenant-admin login
829
+ or catalog reads.
830
+
831
+ Skill writes are first-class, send-once commands that require a saved `hc login`
832
+ session (env-var credentials are not accepted for writes). Each write is sent
833
+ exactly once — the CLI never retries a write — and the result envelope carries
834
+ the server's authoritative readback of the skill (same bounded projection as
835
+ reads). Skills have no active/inactive lifecycle, so there are no
836
+ `set-active`/`deactivate` commands:
837
+
838
+ ```bash
839
+ uv run --frozen hc ams skills create --environment dev --tenant-id TENANT_ID --name "Schedule Lookup" --created-by-user-id ADMIN_USER_ID [--description TEXT] [--tool-ids TOOL_ID ...]
840
+ uv run --frozen hc ams skills update --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID [--name NAME] [--description TEXT] [--tool-ids TOOL_ID ... | --clear-tool-ids]
841
+ uv run --frozen hc ams skills delete --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID
842
+ ```
843
+
844
+ - `create` succeeds with `operation: "skills.create"` and `data.skill` = the
845
+ server's 201 readback. `--name` and `--created-by-user-id` are required by
846
+ the server contract; the tenant is taken from the saved session's
847
+ authenticated admin, never from a flag. The server does not validate that
848
+ `--tool-ids` reference real tools (blank tool IDs are rejected client-side).
849
+ - `update` sends only the options you pass (an empty `--description ""`
850
+ clears it; `--clear-tool-ids` sends an explicit empty list to clear the
851
+ skill's tool bindings); it refuses with exit 1 (`validation`) when no field
852
+ option is given.
853
+ - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
854
+ with no body) and is irreversible. When AI agents still reference the skill
855
+ (via their `skill_ids`), the server refuses with 409 and the CLI reports one
856
+ `conflict` refusal that names the skill and says agents reference it; no
857
+ changes were made.
858
+ - A 404 for the requested skill is a `not_found` refusal naming the ID (on
859
+ `create`, a 404 is a generic referenced-record refusal — the server does not
860
+ validate tool references and the CLI does not either); a target/tenant
861
+ mismatch on a write readback is a `validation` refusal that reports the
862
+ landed write truthfully: it names the skill and the tenant that actually
863
+ received it and points to `hc ams skills get` — it never claims a landed
864
+ write was stopped.
865
+ - If the transport times out or the connection drops mid-write, the write's
866
+ outcome is unknown: the CLI reports one `transport` error whose message says
867
+ the outcome is unknown and to check `hc ams skills get` before retrying. It
868
+ never re-sends the write on its own.
869
+
870
+ Read-only policy catalog listing and inspection are complete and unpaged. The
871
+ bounded projection includes only policy IDs, names, department IDs, guardrail
872
+ IDs, active status, and audit timestamps. It never emits protected policy
873
+ content, descriptions, tenant or creator identity, arbitrary response fields,
874
+ or joined department/guardrail details. Guardrail-ID order is preserved:
875
+
876
+ ```bash
877
+ uv run --frozen hc ams policies list --environment dev --tenant-id TENANT_ID
878
+ uv run --frozen hc ams policies get --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
879
+ ```
880
+
881
+ The CLI does not export protected policy content and never emits it: policy
882
+ `--content` is write-only — it is sent to the server but never appears in any
883
+ command output, error message, or readback. Read commands make exactly one
884
+ catalog GET after tenant-admin login and do not paginate, retry, cache, join,
885
+ or infer referenced resources. Policy reads refuse target files, root
886
+ credentials or base URLs, raw output, blank or dot-segment policy IDs, and
887
+ installed SDKs without both policy methods before tenant-admin login or
888
+ catalog reads. Successful reads make no assignment, deployment, serving,
889
+ enforcement, or effectiveness claim.
890
+
891
+ Policy writes are first-class, send-once commands that require a saved `hc login`
892
+ session (env-var credentials are not accepted for writes). Each write is sent
893
+ exactly once — the CLI never retries a write — and the result envelope carries
894
+ the server's authoritative readback of the policy (same bounded projection as
895
+ reads: never content, description, tenant identity, or creator identity).
896
+ Policies have an `active` lifecycle flag, so `activate` and `deactivate`
897
+ commands exist (they PUT the `active` field):
898
+
899
+ ```bash
900
+ uv run --frozen hc ams policies create --environment dev --tenant-id TENANT_ID --name "Appointment Safety" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID [--description TEXT] [--content TEXT] [--guardrail-ids GUARDRAIL_ID ...]
901
+ uv run --frozen hc ams policies update --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID [--name NAME] [--description TEXT] [--content TEXT] [--guardrail-ids GUARDRAIL_ID ... | --clear-guardrail-ids]
902
+ uv run --frozen hc ams policies activate --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
903
+ uv run --frozen hc ams policies deactivate --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
904
+ uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
905
+ ```
906
+
907
+ - `create` succeeds with `operation: "policies.create"` and `data.policy` =
908
+ the server's 201 readback. `--name`, `--department-id` and
909
+ `--created-by-user-id` are required by the server contract; the tenant is
910
+ taken from the saved session's authenticated admin, never from a flag. The
911
+ server does not validate that `--guardrail-ids` reference real guardrails
912
+ (blank guardrail IDs are rejected client-side). `--content` is write-only:
913
+ it is sent but never echoed back.
914
+ - `update` sends only the options you pass (an empty `--description ""` or
915
+ `--content ""` clears it; `--clear-guardrail-ids` sends an explicit empty
916
+ list to clear the policy's guardrail bindings); it refuses with exit 1
917
+ (`validation`) when no field option is given.
918
+ - `activate`/`deactivate` PUT `active: true|false` and succeed only when the
919
+ readback confirms the requested flag.
920
+ - `delete` succeeds with `data: {"deleted": true}` (the server answers 204
921
+ with no body) and is irreversible. If the server refuses with 409, the CLI
922
+ reports one `conflict` refusal naming the policy; no changes were made.
923
+ - Any other 409 on `create`, `update`, `activate` or `deactivate` is one
924
+ write-worded `conflict` refusal (naming the policy where one was given) that
925
+ says the server refused the write and to review the policy's current server
926
+ state before retrying; no changes were made. Writes are never refused with
927
+ the read-era conflict wording.
928
+ - A 404 for the requested policy is a `not_found` refusal naming the ID (on
929
+ `create`, a 404 names the referenced `--department-id`); a target/tenant
930
+ mismatch on a write readback is a `validation` refusal that reports the
931
+ landed write truthfully: it names the policy and the tenant that actually
932
+ received it and points to `hc ams policies get` — it never claims a landed
933
+ write was stopped.
934
+ - If the transport times out or the connection drops mid-write, the write's
935
+ outcome is unknown: the CLI reports one `transport` error whose message says
936
+ the outcome is unknown and to check `hc ams policies get` before retrying.
937
+ It never re-sends the write on its own.
938
+
939
+ Governed authoring sessions and their messages are first-class commands over the
940
+ VibeCoder session routes. Creating a session and appending a message make no model
941
+ call; they only record authoring state. Sessions are tenant- and user-scoped by the
942
+ server, newest first, and expired sessions are not returned. `--limit` is the only
943
+ pagination the server offers (sessions default 50, maximum 100; messages default
944
+ 100, maximum 200) — there are no offsets or cursors, and a limit outside that range
945
+ is refused locally:
946
+
947
+ ```bash
948
+ uv run --frozen hc ams sessions list --environment dev --tenant-id TENANT_ID [--limit N]
949
+ uv run --frozen hc ams sessions get --environment dev --tenant-id TENANT_ID --session-id SESSION_ID
950
+ uv run --frozen hc ams sessions create --environment dev --tenant-id TENANT_ID [--agent-id AGENT_ID] [--mode plan|implement] [--initial-prompt TEXT]
951
+ uv run --frozen hc ams sessions messages list --environment dev --tenant-id TENANT_ID --session-id SESSION_ID [--limit N]
952
+ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENANT_ID --session-id SESSION_ID --content TEXT [--mode plan|implement] [--idempotency-key KEY]
953
+ ```
954
+
955
+ - `list` emits `operation: "sessions.list"`, `data.sessions` (bounded projections
956
+ of session ID, agent ID, operation, mode, status and audit timestamps) and a
957
+ `page` block (`{"kind": "limit", "limit": N, "returned_count": N,
958
+ "continuation": null}`). `get` emits `operation: "sessions.get"` and
959
+ `data.session`. The projections never include the session's protected
960
+ `initial_prompt`, tenant or user identity, or arbitrary response fields.
961
+ - `create` and `messages add` require a saved `hc login` session (env-var
962
+ credentials are not accepted for writes). Each write is sent exactly once and
963
+ the envelope carries the server's authoritative readback:
964
+ `operation: "sessions.create"` with `data.session`, and
965
+ `operation: "sessions.messages.add"` with `data.message` (message ID, role,
966
+ mode, plan ID, model, timestamps) plus `data.idempotency_key` when a key was
967
+ supplied. Protected free text — `--initial-prompt` and `--content` — is sent to
968
+ the server but never appears in the envelope, a refusal, a diagnostic or a log.
969
+ - `--initial-prompt` becomes the session's first `user` message in the same
970
+ server transaction; `hc ams sessions messages list` shows it afterwards.
971
+ - `--mode implement` is not available from the server yet: `create` reports one
972
+ `unsupported` refusal (the server answers 501) and creates nothing. A message
973
+ whose `--mode` differs from its session's mode is refused by the server with
974
+ 409 and reported as one write-worded `conflict` refusal that says a message's
975
+ mode must match its session's mode and points at `hc ams sessions get`; no
976
+ message is added, and the read-era conflict wording is never used.
977
+ - A missing, expired or other user's session is a `not_found` refusal naming the
978
+ session on `get`, `messages list` and `messages add`; on `create`, a 404 names
979
+ the requested `--agent-id`.
980
+ - Messages with an `--idempotency-key` are replay-safe: the server answers 201
981
+ with the existing message instead of inserting a second one, and the command
982
+ reports that message (its original ID) — a replay is never reported as a new
983
+ write, and the command itself never re-sends a write.
984
+ - If the transport times out or the connection drops mid-write, the write's
985
+ outcome is unknown: the CLI reports one `transport` error whose message says so
986
+ and points at `hc ams sessions list`/`hc ams sessions messages list` before
987
+ retrying. A write readback that lands in a different tenant is reported
988
+ truthfully as `validation`, naming the tenant that actually received it — a
989
+ landed write is never reported as stopped.
990
+ - Reads refuse target files, root credentials or base URLs, raw output, blank or
991
+ dot-segment session IDs, out-of-range limits, and installed SDKs without the
992
+ session list/get and message list/add methods before tenant-admin login or any
993
+ read. The existing `hc ams plan|show|apply|restore` tuning commands and their
994
+ signatures are unchanged.
995
+ - These session and message commands do not attach files, apply or roll back
996
+ configuration, or send session history to a model. Plan review is its own
997
+ command group (below).
998
+
999
+ Session attachments are three commands over the live VibeCoder attachment routes.
1000
+ There is no download command: the server exposes no attachment content route, so
1001
+ the CLI never fetches, prints or reconstructs a file's bytes:
1002
+
1003
+ ```bash
1004
+ uv run --frozen hc ams sessions attachments list --environment dev --tenant-id TENANT_ID --session-id SESSION_ID
1005
+ uv run --frozen hc ams sessions attachments upload --environment dev --tenant-id TENANT_ID --session-id SESSION_ID --file PATH
1006
+ uv run --frozen hc ams sessions attachments delete --environment dev --tenant-id TENANT_ID --session-id SESSION_ID --attachment-id ATTACHMENT_ID
1007
+ ```
1008
+
1009
+ - `list` emits `operation: "sessions.attachments.list"` and `data.attachments`,
1010
+ each a bounded projection of `id`, `filename`, `content_type`, `size_bytes`,
1011
+ `index_status` and `created_at`. The server offers no pagination on this
1012
+ route, so there is no `page` block and no `--limit`. The projections never
1013
+ include `storage_key`, `content_hash`, tenant or user identity, or any file
1014
+ content. An agent-backed session lists the agent attachments linked to the
1015
+ session; a session with no agent lists its draft attachments. The server
1016
+ guarantees no ordering, so the CLI claims none.
1017
+ - `upload` reads `--file` locally, reads the session once first, then sends the
1018
+ file exactly once as the multipart `file` part under its bare filename, and
1019
+ reports the server's authoritative readback as
1020
+ `operation: "sessions.attachments.upload"` with `data.attachment` (the same
1021
+ projection as `list`). It requires a saved `hc login` session. The file's
1022
+ bytes are never written to stdout, stderr, an error envelope, a diagnostic or
1023
+ a log; only its name and size appear. An agent-backed session derives the
1024
+ content type from the extension and stores an agent attachment (with an
1025
+ indexing status); a session with no agent stores a draft attachment and
1026
+ accepts any extension.
1027
+ - Before sending, `upload` refuses a missing or unreadable file and a file
1028
+ larger than 5 MiB without any request: those bounds apply to both paths. The
1029
+ extension allow-list (`txt`, `md`, `pdf`, `docx`) is the agent path's only,
1030
+ because that path derives the content type from the extension, so it is
1031
+ applied after the session read and only when the session has an `agent_id`; a
1032
+ draft upload of any other extension is sent, and the server answers. The
1033
+ session read is a separate route, so the upload POST itself is still sent
1034
+ exactly once. These local pre-checks mirror the server's own bounds; they
1035
+ never replace the server's answer. When the server
1036
+ does refuse, its status is reported as sent and never normalised: `413` (the
1037
+ route's stream cap) and `422` (the attachment service's size check, an unsafe
1038
+ or blank filename, or an unsupported extension on the agent path) both become
1039
+ one `validation` refusal naming the status and the server's own detail, and
1040
+ nothing is attached. A missing, expired or other user's session is a
1041
+ `not_found` refusal naming the session.
1042
+ - `delete` removes an attachment with a `204` (no body, so the envelope is built
1043
+ from the request identity, never a fabricated response). It requires a saved
1044
+ `hc login` session and reads the session once first, because the route has two
1045
+ different effects and the wording must be truthful: for an agent-backed
1046
+ session the file is only **detached** from the session and the agent
1047
+ attachment and its stored object survive (`data.effect: "detached"`), while a
1048
+ session with no agent has its draft attachment **deleted**
1049
+ (`data.effect: "deleted"`). `data` also carries `attachment_id`, and `detail`
1050
+ states the effect in words. The DELETE itself is sent exactly once. A missing
1051
+ session, or an attachment ID that is not attached to the session, is a
1052
+ `not_found` refusal; nothing is removed.
1053
+ - If the transport times out or the connection drops mid-write, the upload's or
1054
+ delete's outcome is unknown: the CLI reports one `transport` error saying so
1055
+ and pointing at `hc ams sessions attachments list` for that session before
1056
+ retrying. It never re-sends automatically; `list` is the reconciliation step.
1057
+ A refused or failed upload creates no session link, sends no message,
1058
+ generates no plan and makes no model call, and a landed write readback in
1059
+ another tenant is reported truthfully as `validation`.
1060
+ - Attachment commands refuse target files, root credentials or base URLs, raw
1061
+ output, blank or dot-segment session and attachment IDs, and installed SDKs
1062
+ without the attachment list/upload/delete methods before tenant-admin login
1063
+ or any request. Uploads are multipart requests; `APIClient.request` forwards
1064
+ `files`/`data` for them and every existing JSON caller's wire bytes are
1065
+ unchanged.
1066
+
1067
+ Exact create/configure plans are five commands over the existing VibeCoder plan
1068
+ routes: two review commands, one send-once apply, one send-once rollback and one
1069
+ read-only reconciliation. There is no approval receipt and no direct agent write:
1070
+
1071
+ ```bash
1072
+ uv run --frozen hc ams sessions plans generate --environment dev --tenant-id TENANT_ID --session-id SESSION_ID [--operation modify|create] [--agent-type Application|Utility] [--prompt-file PATH] [--idempotency-key KEY] --out PATH
1073
+ uv run --frozen hc ams sessions plans get --environment dev --tenant-id TENANT_ID --plan-id PLAN_ID --out PATH
1074
+ uv run --frozen hc ams sessions plans apply --environment dev --tenant-id TENANT_ID --plan-id PLAN_ID --confirm-hash 64_HEX_PLAN_HASH [--expected-config-version N] --confirm
1075
+ uv run --frozen hc ams sessions plans rollback --environment dev --tenant-id TENANT_ID --plan-id PLAN_ID --expected-config-version N [--reason TEXT] --confirm
1076
+ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TENANT_ID --plan-id PLAN_ID
1077
+ ```
1078
+
1079
+ - `generate` sends exactly one `POST /vibecoder/sessions/SESSION_ID/plan` with
1080
+ the explicit session ID; it makes no preflight read and no other request, no
1081
+ message write, no `PUT` and no apply. The server runs the planner once. It
1082
+ requires a saved `hc login` session and reports
1083
+ `operation: "sessions.plans.generate"` with `data.plan` and `data.out`.
1084
+ `get` sends exactly one `GET /vibecoder/plans/PLAN_ID` and reports
1085
+ `operation: "sessions.plans.get"`; a reload is immutable, so it returns the
1086
+ same `plan_hash` and diff however the session changed afterwards. `get` is a
1087
+ read and needs no saved `hc login` session.
1088
+ - `--out` is required and must be a new path: the review is written to a
1089
+ newly created owner-only file (mode `0600`, no symlinks, never overwritten).
1090
+ A refusal removes the reservation again, so a refused review never leaves a
1091
+ file behind.
1092
+ - The review file carries the exact target (`environment`, `tenant_id`,
1093
+ `session_id`, `agent_id` — explicit `null` for a creation plan — and
1094
+ `plan_id`), `operation`, `status`, `base_config_version` (`null` for create),
1095
+ `plan_hash`, `model`, `summary`, the advisory `review_warnings` with their
1096
+ own `severity`, `file_actions` as attachment **IDs only**, and the diff:
1097
+ `changes` with each returned field's `before` and `after` for a configure
1098
+ plan, or `definition` (name, description, type, model, `is_orchestrator`,
1099
+ department/team, instructions, greetings, opening suggestions) plus
1100
+ `references` (catalog IDs grouped by department/team/tool/skill/policy/
1101
+ guardrail/data-source) for a creation plan. `plan_json["original_prompt"]`
1102
+ is never projected, and `warnings`/`skipped_references` are reported as
1103
+ counts, not bodies.
1104
+ - stdout stays bounded and free-text-free: plan ID, `plan_hash`, operation,
1105
+ status, base config version, agent ID, model, sorted changed-field **names**
1106
+ (create: reference counts by kind) and finding counts. No prompt,
1107
+ instructions, greeting, summary, message content or plan value is printed to
1108
+ stdout, stderr, a diagnostic or an error string.
1109
+ - The prompt is protected free text and never comes from argv: `--prompt-file`
1110
+ reads one owner-only regular text file (no symlinks, at most 20000
1111
+ characters) and sends it once. `--prompt-file` is optional — the server falls
1112
+ back to the session's initial prompt or last message — and `--agent-type`
1113
+ applies to `--operation create` only (with `modify` it is a usage error
1114
+ before login or any request). `--prompt-file` is not mirrored as a local
1115
+ "prompt required" rule: a creation plan with no prompt anywhere keeps the
1116
+ server's own `422 "a prompt is required for agent creation"`.
1117
+ - It refuses rather than endorses. A configure plan that changes anything
1118
+ outside the existing tuning field set (`instructions`, `greetings`) is
1119
+ refused by field name — in particular capability/permission expansion
1120
+ (`tool_ids`, `skill_ids`, `policy_ids`, `guardrail_ids`, `data_source_ids`,
1121
+ `department_id`, `team_id`, `is_orchestrator`), lifecycle/`status`, model and
1122
+ file actions: permission assignment stays a separately reviewed capability.
1123
+ It also refuses a plan whose `status` is not `proposed`, whose `plan_hash` is
1124
+ not 64 hex characters, whose `session_id`/`tenant_id`/`operation` differ from
1125
+ the explicit target, whose `plan_json` carries unknown keys, or whose file
1126
+ actions name anything but an attachment ID. A `status: "invalid"` plan is
1127
+ refused instead of reviewed, and its `validation_errors` are reported by
1128
+ count and never quoted — that server text can contain protected field
1129
+ values, so read it in the AMS console. Non-`advisory` review findings are
1130
+ refused the same way; advisory findings are displayed in the review file.
1131
+ A creation plan that claims an `agent_id` is refused, and no existing agent
1132
+ ID is ever fabricated or required.
1133
+ - Server answers are preserved, not replaced: a missing, expired or other
1134
+ user's session or plan is a `not_found` refusal naming it, a `422` from the
1135
+ plan route is a `validation` refusal carrying the server's own bounded
1136
+ detail, and an unavailable planner (`503`) is a `transport` refusal with no
1137
+ file written. A dropped connection on `generate` reports one `transport`
1138
+ error saying the plan's outcome is unknown and points at
1139
+ `hc ams sessions messages list`/`plans get` before retrying; the request is
1140
+ never re-sent automatically.
1141
+ - Both commands refuse target files, root credentials or base URLs, raw output,
1142
+ blank or dot-segment session and plan IDs, a prompt file that is not a
1143
+ readable owner-only regular file, and installed SDKs without the plan
1144
+ generate/get methods before tenant-admin login (or, for `generate`, before
1145
+ any request). The existing `hc ams plan|show|apply|restore` tuning commands
1146
+ and their signatures are unchanged.
1147
+
1148
+ Applying, rolling back and reconciling one plan are separate capabilities on the
1149
+ same routes. The commands never retry, never replay, never send a second
1150
+ mutation and never fall back to `PUT /agents/{id}`:
1151
+
1152
+ - `apply` requires a saved `hc login` session, `--confirm` and the exact
1153
+ 64-hex `--confirm-hash` from the review. It reads the plan **once**, runs its
1154
+ local pre-checks, sends **exactly one**
1155
+ `POST /vibecoder/plans/PLAN_ID/apply` with the reviewed `plan_hash`, the
1156
+ version the plan itself carries and `confirm: true`, then reads the plan (and
1157
+ the agent, when the plan names one) back. Approving a plan binds exactly what
1158
+ was reviewed: a configure plan whose changes fall outside `instructions`/
1159
+ `greetings`, or which carries file actions, is refused by field name even
1160
+ though the server would accept it, and a creation plan that claims an agent
1161
+ ID, reports existing-field changes or carries a file write/path action is
1162
+ refused the same way. A plan that is not `proposed`, carries
1163
+ `validation_errors`, has blocking review findings, or whose hash no longer
1164
+ matches `--confirm-hash` is refused before anything is sent.
1165
+ - A **configure** `apply` requires `--expected-config-version` equal to the
1166
+ plan's own `base_config_version`; a mismatch is a local refusal, never a stale
1167
+ send. A **creation** plan has no baseline (`base_config_version` is `null`),
1168
+ so `--expected-config-version` must **not** be supplied: the request sends `1`
1169
+ only because the pinned route schema requires `expected_config_version >= 1`
1170
+ while the server's creation branch never reads it. That `1` is a documented
1171
+ **protocol filler, not a baseline claim**, and the CLI never reports it as
1172
+ evidence.
1173
+ - `apply` reports three evidence sections independently and never collapses
1174
+ them into "applied": `data.evidence.saved` (the plan reloaded after the write
1175
+ is `applied`/`publish_failed`; for a configure plan the agent's
1176
+ `config_version` advanced by exactly one from the plan's own
1177
+ `base_config_version` and every reviewed field equals its `after` value; for
1178
+ a creation plan the created `agent_id` was read back in this tenant and no
1179
+ version baseline is claimed), `data.evidence.side_effects` (files are
1180
+ `unverified` because the pinned contract exposes no attachment/file readback,
1181
+ and publication is `not_required` when the server reports
1182
+ `publication_status: not_applicable`, otherwise `unknown`/`failed` — never
1183
+ "serving"), and `data.evidence.runtime` (always `unverified`, because there is
1184
+ no serving/runtime route). `data.mutation_replayed` is always `false`.
1185
+ - Exit codes are strict: `0` **only** when the write is provably saved and
1186
+ readback-confirmed (the `verification` block then reads `saved_state:
1187
+ confirmed`, `side_effects: unverified`, `runtime: unverified`). A readback
1188
+ mismatch, a saved-but-unpublished result, or an unknown outcome exits nonzero
1189
+ with the bounded JSON envelope on stderr.
1190
+ - `apply` surfaces the server's own answers and never its free text: a missing
1191
+ plan is a `not_found` refusal naming it; a `409` names only the structured
1192
+ reason code (`plan_hash_mismatch`, `already_applied`,
1193
+ `plan_not_applicable`, `attachments_changed`, `stale_baseline`,
1194
+ `concurrent_edit`) with bounded local wording; a `422` is reported by
1195
+ validation-error **count** only, because that server text can quote protected
1196
+ field values.
1197
+ - **Unknown outcome is never success and never a replay.** On a transport
1198
+ failure or any `5xx` from `/apply` (the mutation may have committed) the
1199
+ command sends nothing further, records `outcome: "unknown"`, runs the same
1200
+ read-only reconciliation `reconcile` performs, prints it together with
1201
+ `mutation_replayed: false`, and exits nonzero telling the operator to inspect
1202
+ before acting. The only supported follow-up is `plans reconcile`, or
1203
+ `plans rollback` once the state is known.
1204
+ - `rollback` requires a saved `hc login` session, `--confirm`,
1205
+ `--expected-config-version` equal to the agent's **current**
1206
+ `config_version`, and reads the plan and agent first. It requires the plan to
1207
+ be an applied configure plan (`status` `applied` or `publish_failed`, with an
1208
+ `agent_id`); a mismatch is refused locally, never sent. It then sends
1209
+ **exactly one** `POST /vibecoder/plans/PLAN_ID/rollback` (the pinned request
1210
+ schema requires `reason`, so `--reason` is sent as a bounded string, `""`
1211
+ when omitted, and is never echoed), reads both back, and reports the same
1212
+ three evidence sections. `--reason` is protected free text: it never reaches
1213
+ stdout, stderr, a diagnostic or an error string.
1214
+ - **Rollback is not restore and neither is deletion recovery.** `hc ams
1215
+ restore` is the tuning contract's selected-field restore: receipt-bound fields
1216
+ put back on the same agent. `hc ams sessions plans rollback` asks the server to
1217
+ revert the fields **one applied plan** changed, version-checked. A creation
1218
+ plan changed no existing field, so it is refused locally: deleting an agent a
1219
+ creation plan created is a different capability this CLI does not add. The
1220
+ existing `hc ams plan|show|apply|restore` tuning commands and their
1221
+ signatures, output and receipts are unchanged.
1222
+ - `reconcile` is the read-only recovery path: it sends no `POST`, no `PUT` and no
1223
+ `DELETE`. It reloads the plan, reads its agent when it names one, and reports
1224
+ the plan `status` and `plan_hash`, the `agent_id`, the agent's
1225
+ `config_version`, which planned field values are `present`/`absent`/`other`,
1226
+ and `mutation_replayed: false`; side effects and runtime stay `unverified`.
1227
+ It is what an operator runs after any uncertain outcome, before any further
1228
+ write.
1229
+ - All three refuse target files, root credentials or base URLs, raw output,
1230
+ blank or dot-segment plan IDs, an apply/rollback without `--confirm`, a
1231
+ malformed `--confirm-hash`, an out-of-range `--expected-config-version`, a
1232
+ `--reason` longer than 2000 characters, and installed SDKs without the plan
1233
+ get/apply/rollback methods before tenant-admin login or any request.
1234
+
1235
+ Guardrail catalog listing and inspection are complete and unpaged. The bounded
1236
+ projection includes only guardrail IDs, names, and audit timestamps. It never
1237
+ emits protected rules, descriptions, tenant or creator identity, arbitrary
1238
+ response fields, or joined policy/agent details:
1239
+
1240
+ ```bash
1241
+ uv run --frozen hc ams guardrails list --environment dev --tenant-id TENANT_ID
1242
+ uv run --frozen hc ams guardrails get --environment dev --tenant-id TENANT_ID \
1243
+ --guardrail-id GUARDRAIL_ID
1244
+ ```
1245
+
1246
+ Reads make exactly one catalog GET after tenant-admin login and do not paginate,
1247
+ retry, cache, join, or infer additional data. Guardrail reads refuse target
1248
+ files, root credentials or base URLs, raw output, blank or dot-segment guardrail
1249
+ IDs, and installed SDKs without both catalog methods before tenant-admin login or
1250
+ catalog reads.
1251
+
1252
+ Guardrail writes are first-class, send-once commands that require a saved
1253
+ `hc login` session (env-var credentials are not accepted for writes). Each write
1254
+ is sent exactly once — the CLI never retries a write — and the result envelope
1255
+ carries the server's authoritative readback of the guardrail (same bounded
1256
+ projection as reads: never rules, description, tenant identity, or creator
1257
+ identity). A guardrail has no `active` flag and no `config_version`, so there is
1258
+ no `activate`/`deactivate` command and no expected-version option: the server's
1259
+ write routes carry no `If-Match`, and the CLI will not advertise a guard the
1260
+ server does not offer.
1261
+
1262
+ ```bash
1263
+ uv run --frozen hc ams guardrails create --environment dev --tenant-id TENANT_ID --name NAME --created-by-user-id ADMIN_USER_ID [--description TEXT] [--rules-file RULES_JSON_FILE]
1264
+ uv run --frozen hc ams guardrails update --environment dev --tenant-id TENANT_ID --guardrail-id GUARDRAIL_ID [--name NAME] [--description TEXT] [--rules-file RULES_JSON_FILE]
1265
+ uv run --frozen hc ams guardrails delete --environment dev --tenant-id TENANT_ID --guardrail-id GUARDRAIL_ID --confirm
1266
+ ```
1267
+
1268
+ - `create` succeeds with `operation: "guardrails.create"` and
1269
+ `data.guardrail` = the server's 201 readback. `--name` and
1270
+ `--created-by-user-id` are required by the server contract; the tenant is
1271
+ taken from the saved session's authenticated admin, never from a flag.
1272
+ `--rules-file` names a regular JSON file holding the rules object with string
1273
+ keys. Rules never ride argv (so they stay out of the process list) and are
1274
+ never echoed: an unreadable, symlinked, duplicate-keyed or non-object file is
1275
+ refused with one fixed message that does not quote its content. Rules are
1276
+ write-only — they are sent but never appear in any command output, error
1277
+ message or readback.
1278
+ - `update` sends only the options you pass (an empty `--description ""` clears
1279
+ it; a `--rules-file` holding `{}` sends an empty rule set); it refuses with exit 1
1280
+ (`validation`) when no field option is given.
1281
+ - `delete` is a reviewed one-shot destructor and requires `--confirm`; without
1282
+ it the command exits 2 before login and sends nothing. With it the CLI makes
1283
+ exactly one `GET` for the guardrail, one `GET /agents` and one `GET /policies`
1284
+ to compute the affected references, prints the resolved target, those
1285
+ references and the disclosure block to stderr (pretty mode only; `--output
1286
+ json|raw` keeps stderr machine-parsable), sends exactly one `DELETE`, then
1287
+ makes exactly one confirmation `GET`. Only a 404 there proves the deletion:
1288
+ the command reports `data.deleted: true` and `verification.saved_state:
1289
+ confirmed` for a 404, `data.deleted: false` with `saved_state: failed` when
1290
+ the guardrail is still served, and `outcome: unknown` with
1291
+ `data.mutation_replayed: false` and a `not_read` reconciliation when the
1292
+ answer is 5xx or the transport fails. It never replays, retries or re-reads on
1293
+ its own, and it never claims a restore: `hc ams restore` replays tuning fields
1294
+ onto an existing agent and can never restore a deleted guardrail.
1295
+ - The server guards one reference only: an agent whose `guardrail_ids` contain
1296
+ the guardrail answers 409 and nothing is deleted (the CLI reports one
1297
+ `conflict` refusal). A policy whose `guardrail_ids` still name it is NOT
1298
+ guarded and is not repaired, so a later agent configuration read that resolves
1299
+ that policy fails; the disclosure says so, and the printed affected references
1300
+ name every policy that still points at the guardrail. When the agent or policy
1301
+ catalog cannot be read, the delete is refused (`nothing was changed`) rather
1302
+ than executed without its disclosure.
1303
+ - Any other 409 on `create` or `update` is one write-worded `conflict` refusal
1304
+ that says the server refused the write and to review the guardrail's current
1305
+ server state before retrying; no changes were made.
1306
+ - A 404 for the requested guardrail on `update` is a `not_found` refusal naming
1307
+ the ID; a target/tenant mismatch on a write readback is a `validation` refusal
1308
+ that reports the landed write truthfully: it names the guardrail and the
1309
+ tenant that actually received it and points to `hc ams guardrails get`.
1310
+ - If the transport times out or the connection drops mid-write, the write's
1311
+ outcome is unknown: the CLI reports one `transport` error whose message says
1312
+ the outcome is unknown and to check `hc ams guardrails get` before retrying.
1313
+ It never re-sends the write on its own.
1314
+
1315
+ Read-only tool catalog listing and inspection are complete and unpaged. The
1316
+ bounded projection includes only IDs, names, read-only and confirmation flags,
1317
+ agent types, required scopes, audit events, and activity labels:
1318
+
1319
+ ```bash
1320
+ uv run --frozen hc ams tools list --environment dev --tenant-id TENANT_ID
1321
+ uv run --frozen hc ams tools get --environment dev --tenant-id TENANT_ID \
1322
+ --tool-id TOOL_ID
1323
+ ```
1324
+
1325
+ When both target flags are omitted, these commands may use the matching saved
1326
+ tenant-admin session. Explicit flags must match that session. Tool output never
1327
+ includes descriptions, parameters, input schemas, PIP or connector routing, or
1328
+ arbitrary response fields. The CLI does not execute, assign, create, update, or
1329
+ delete tools; join agents or skills; or claim assignment, availability,
1330
+ authorization, deployment, serving, or runtime effectiveness. It makes exactly
1331
+ one catalog GET after tenant-admin login and does not paginate, retry, cache,
1332
+ join, or infer additional data. Tool reads refuse target files, root credentials
1333
+ or base URLs, raw output, blank or dot-segment IDs, and installed SDKs without
1334
+ both tool methods before tenant-admin login or catalog reads.
1335
+
1336
+ Read-only data-source catalog listing and inspection are complete and unpaged.
1337
+ The bounded projection includes only stable IDs and names:
1338
+
1339
+ ```bash
1340
+ uv run --frozen hc ams data-sources list --environment dev --tenant-id TENANT_ID
1341
+ uv run --frozen hc ams data-sources get --environment dev --tenant-id TENANT_ID \
1342
+ --data-source-id DATA_SOURCE_ID
1343
+ ```
1344
+
1345
+ When both target flags are omitted, these commands may use the matching saved
1346
+ tenant-admin session. Explicit flags must match that session. Data-source output
1347
+ never includes descriptions or arbitrary response fields and does not imply
1348
+ access, patient consent, record availability, assignment, authorization, or
1349
+ runtime effectiveness. The CLI does not create, update, delete, assign, access
1350
+ records through, or execute data sources. It makes exactly one catalog GET after
1351
+ tenant-admin login and does not paginate, retry, cache, truncate, join, or infer
1352
+ additional data. Data-source reads refuse target files, root credentials or base
1353
+ URLs, raw output, blank or dot-segment IDs, and installed SDKs without both
1354
+ data-source methods before tenant-admin login or catalog reads.
1355
+
1356
+ ### AMS output modes
1357
+
1358
+ The read commands (`hc ams skills|tools|policies|guardrails|data-sources|teams|departments list|get`
1359
+ and `hc ams organization hierarchy`) render in two modes (`--output` on the root
1360
+ `hc` command; `pretty` is the default):
1361
+
1362
+ - **`pretty`** (default): a compact human view. Lists print an `ID`/`NAME`
1363
+ table with an entry count; a single record prints `key: value` lines; the
1364
+ organization hierarchy prints an indented tree. Each failed AMS command
1365
+ prints one stderr line, `error: <message> (<category>)` (flag/argument
1366
+ usage errors from Click print Click's standard usage message instead).
1367
+ - **`json`**: the stable result envelope (below) on stdout. When stdout is not
1368
+ a terminal, the envelope is printed as one compact line; on a terminal it is
1369
+ indented. Errors keep the structured JSON envelope on stderr.
1370
+
1371
+ The JSON envelope carries `schema_version`, `operation`, `target`, `outcome`,
1372
+ and either `data` (succeeded) or `error: {category, message}` (failed). On
1373
+ read-only operations the envelope no longer includes the `verification` block:
1374
+ every field there is `not_applicable` on a read, so it was boilerplate. If a
1375
+ future mutating operation reintroduces it, `verification` reappears only with
1376
+ non-`not_applicable` content. Everything else about the envelope — field names,
1377
+ ordering semantics, categories — is unchanged and stays parseable.
1378
+
1379
+ `--raw`/`--output raw` stays refused for AMS (`Raw output is refused for AMS.`),
1380
+ and refusals keep the JSON error envelope on stderr.
1381
+
1382
+ ```bash
1383
+ hc ams skills list --environment dev --tenant-id TENANT_ID # pretty table
1384
+ hc ams skills get --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID # key: value
1385
+ hc --output json ams skills list --environment dev --tenant-id TENANT_ID # compact envelope
1386
+ hc --output json ams skills list --environment dev --tenant-id TENANT_ID | jq .data
1387
+ ```
1388
+
1389
+ ### What `hc ams` errors report
1390
+
1391
+ In `json` and `raw` modes each failed AMS command prints one JSON object on stderr with
1392
+ an `error.category` and a message naming the real cause; in `pretty` mode the
1393
+ same message is exactly one stderr line, `error: <message> (<category>)`:
1394
+
1395
+ - Local refusals keep their specific message and exit `1` with category
1396
+ `validation`: `Raw output is refused for AMS.`, `AMS target does not match the
1397
+ saved session; stopped before authentication.`, identity mismatches
1398
+ (`Skill/tenant identity mismatch; stopped.`), and dot-segment ID refusals.
1399
+ - No saved session and no target flags: category `authentication` with
1400
+ `Not logged in; run \`hc login --environment <env> --tenant-id <tenant>\`.`
1401
+ - A `get` whose requested ID does not exist (HTTP 404): category `not_found`
1402
+ naming the ID, e.g. `Skill 'nosuch' was not found.` (a 404 on `list` stays
1403
+ `validation`; departments keeps its legacy contract for all read statuses,
1404
+ while department write 404s are `not_found` refusals naming the ID).
1405
+ - HTTP 401/403 map to `authentication`/`authorization`; 409 to `conflict`;
1406
+ network failures to `transport`; local output failures to `local_io`.
1407
+ - Any unexpected internal error is category `internal` with a generic message;
1408
+ it is never labeled `validation` and never leaks exception details.
1409
+
1410
+ ```bash
1411
+ hc --raw ams skills list --environment dev --tenant-id TENANT_ID # validation: raw refused
1412
+ hc ams skills list --tenant-id OTHER_TENANT # validation: target mismatch
1413
+ hc ams skills list # authentication: not logged in
1414
+ hc ams skills get --skill-id NOSUCH # not_found: names the ID
1415
+ ```
1416
+
1417
+ Every other command requires an explicit local, non-secret target JSON file.
1418
+ Only the canonical HTTPS AMS origin for that environment is accepted; custom
1419
+ origins require separately verified support. Target and nested qualification keys
1420
+ are strict. Unknown/unavailable qualification may be represented by omitted
1421
+ records or empty qualification `{}` for inspection/UI, but blocks planning.
1422
+
1423
+ The following is a **schema example**, not evidence of deployed CareCo eligibility.
1424
+ Replace its assertions only after an operator has established the corresponding
1425
+ proof. `deployed_build` and `proof_ref` are references, never credentials:
1426
+
1427
+ ```json
1428
+ {
1429
+ "schema_version": 1,
1430
+ "environment": "dev",
1431
+ "tenant_id": "TENANT_ID",
1432
+ "agent_id": "EXPLICIT_SELECTED_AGENT_ID",
1433
+ "api_origin": "https://dev-amsservices.health.cloud",
1434
+ "qualification": {
1435
+ "runtime": {"source": "OPERATOR_VERIFIED_RUNTIME", "proof_ref": "RUNTIME_PROOF"},
1436
+ "instructions": {"source": "agent.instructions", "proof_ref": "INSTRUCTION_SOURCE_PROOF"},
1437
+ "greetings": {"source": "agent.greetings", "proof_ref": "GREETING_SOURCE_PROOF"},
1438
+ "backend_model_context": {
1439
+ "deployed_build": "OPERATOR_VERIFIED_DEPLOYED_REVISION",
1440
+ "config_version": 7,
1441
+ "proof_ref": "FULL_INPUT_AND_WRITER_COVERAGE_REVIEW_PROOF",
1442
+ "current_build_confirmed": true,
1443
+ "fresh_session": true,
1444
+ "no_attachments_or_history": true,
1445
+ "full_inputs_approved": true,
1446
+ "change_path_without_logs": true
1447
+ }
1448
+ }
1449
+ }
1450
+ ```
1451
+
1452
+ Qualification is an operator assertion with evidence, never authorization.
1453
+ Server-side tenant-admin authorization remains mandatory. No current-build API is
1454
+ established: the CLI cannot automatically detect an unreported deployment, verify
1455
+ actual runtime consumption, attest deployed all-writer coverage, or audit model
1456
+ inputs. A changed/unavailable build or agent configuration invalidates the review;
1457
+ the operator must refresh the record. The recorded config version must equal the
1458
+ inspected version before planning/apply/restore.
1459
+ `full_inputs_approved` means an operator approved non-sensitive **full planner and
1460
+ reviewer inputs**, including custom_tools, catalogs, all agent fields, and all other
1461
+ server context. Output projection does not sanitize server-side model context.
1462
+ `change_path_without_logs` requires evidence that the exact deterministic modify
1463
+ request uses the change path, avoiding the direct-answer branch's recent logs.
1464
+ Unknown effective sources and file-backed/ignored instructions block tuning.
1465
+
1466
+ Before `plan`, `apply` or selected-field `restore`, the operator must review evidence
1467
+ that the actual serving build includes **all writer version coverage**: ordinary
1468
+ agent updates and startup/migration repair writers advance `config_version`
1469
+ atomically with their changes, and tuning apply uses transactional compare-and-set
1470
+ against the expected version. Bind this coverage evidence and the full-input
1471
+ review together in the existing `qualification.backend_model_context.proof_ref`
1472
+ review bundle for its `deployed_build`, inspected `config_version` and
1473
+ `current_build_confirmed` assertion. AMS source prerequisites
1474
+ [#488](https://github.com/healthcloudservices/healthcloud-ams-services/pull/488) and
1475
+ [#497](https://github.com/healthcloudservices/healthcloud-ams-services/pull/497)
1476
+ are evidence leads; source tests, PR merges, or a numeric `config_version` alone
1477
+ cannot prove coverage in the serving build. If deployed identity or any writer's
1478
+ coverage is unknown, or a writer still bypasses version advancement, stop
1479
+ `plan`/`apply`/`restore`; bounded read-only inspection/UI may continue under their
1480
+ existing limits. Refresh this evidence after a changed/unknown build or agent
1481
+ configuration and recheck it before apply and selected-field restore. This is an
1482
+ operator prerequisite: the current CLI does not automatically attest deployment
1483
+ or all-writer coverage, and the record adds no server authorization boundary.
1484
+
1485
+ For CI and scripts with no saved session, credentials may still enter through
1486
+ `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` in the current process, intended
1487
+ for composition with the existing `onepassword-env` wrapper; that compatibility
1488
+ path logs in once per invocation. There is no fallback identity, secret argument,
1489
+ arbitrary token import, resolved env file, keychain, or browser credential bridge. Root MCP
1490
+ `--access-token`, `--base-url`, their environment equivalents, and token/base-URL
1491
+ profiles are refused for AMS. Root profile environment values do not select AMS.
1492
+
1493
+ ```bash
1494
+ uv run --frozen hc ams --target-file TARGET.json inspect
1495
+ uv run --frozen hc ams --target-file TARGET.json inspect --out PRIVATE_INSPECTION.json
1496
+ uv run --frozen hc ams --target-file TARGET.json plan --changes-file PRIVATE_CHANGES.json --out PRIVATE_RECEIPT.json
1497
+ uv run --frozen hc ams --target-file TARGET.json show --receipt PRIVATE_RECEIPT.json --out PRIVATE_DIFF.json
1498
+ uv run --frozen hc ams --target-file TARGET.json apply --receipt PRIVATE_RECEIPT.json --confirm-plan-hash DISPLAYED_HASH
1499
+ uv run --frozen hc ams --target-file TARGET.json restore --receipt PRIVATE_RECEIPT.json --out PRIVATE_RESTORE_RECEIPT.json
1500
+ uv run --frozen hc ams --target-file TARGET.json ui --repo EXISTING_AMS_WORKTREE
1501
+ ```
1502
+
1503
+ Changes JSON accepts only nonempty subsets of `instructions` (string) and
1504
+ `greetings` (language-key to string map). Input changes/receipts must be regular,
1505
+ owner-only files (for example create under `umask 077`). Outputs use exclusive
1506
+ 0600 creation on POSIX and an owner-only protected DACL on Windows, and refuse
1507
+ overwrite/symlinks; failed operations remove unfinished
1508
+ outputs. No whole-agent snapshot is saved. Free text appears only in explicitly
1509
+ requested private files for human review before any AI use. Default output in
1510
+ pretty/json/verbose modes contains bounded metadata and hashes; AMS refuses raw.
1511
+
1512
+ The planner receives `Update the agent configuration to exactly these field
1513
+ values: ` followed by sorted, compact requested JSON. Extra fields, files,
1514
+ validation errors, warnings/skipped references, blocking or unknown review
1515
+ findings, and approximate wording are refused. Each immutable field change must
1516
+ include `before` and exact `after` values so a modified receipt cannot redefine a
1517
+ restore. A server plan omitting `before` cannot safely support this workflow;
1518
+ review/replan rather than synthesizing server provenance. `show`/`apply` reload the
1519
+ immutable backend plan/hash. Before apply, recheck the deployed coverage and
1520
+ full-input qualification above. Apply requires the displayed hash, matching
1521
+ version and selected before-values, and sends `confirm=True` once. HTTP 401/403
1522
+ and 409 never trigger identity fallback or mutation replay.
1523
+
1524
+ On transport failure or HTTP 5xx, apply records an unknown outcome and reconciles through GET
1525
+ plan/agent without retrying the mutation. Confirmed saves require applied status,
1526
+ selected-field readback and exactly the next config version. The current public
1527
+ response omits publication_status: **saved/readback confirmed is reported separately
1528
+ from publication unknown and runtime unverified**, with nonzero exit status for
1529
+ complete success. `publish_failed` is reported separately. Do not replay a saved
1530
+ apply to resolve publication. A fresh invocation refuses an already consumed plan.
1531
+
1532
+ Restore creates a new reviewed plan from only the selected fields' authoritative
1533
+ before-values against the latest config version. Unrelated edits are preserved;
1534
+ a current same-field mismatch blocks restore. There is no force restore. Refresh
1535
+ operator qualification, including deployed all-writer coverage and full-input
1536
+ evidence, for the latest build/version first, then review the new
1537
+ restore receipt with `show` and confirm its new hash with `apply`.
1538
+
1539
+ UI launch validates the existing `test-client-ui/package.json` Vite script,
1540
+ refuses lifecycle hooks, and invokes `npm run dev -- --host 127.0.0.1` by argv
1541
+ with `shell=False`. It installs nothing and launches no backend. The allowlisted
1542
+ child environment includes toolchain essentials plus only these public target
1543
+ variables: `VITE_API_URL`, `VITE_HEALTHCLOUD_ENVIRONMENT`, `VITE_TUNING_TENANT_ID`,
1544
+ `VITE_TUNING_AGENT_ID`. Injected HC/OP credentials and other VITE/NODE/NPM options
1545
+ are excluded. It prints `/app/agents/{agent_id}`; browser login remains separate.
1546
+ The supplied existing worktree and its local frontend configuration remain
1547
+ operator-controlled.
1548
+
1549
+ AMS failures exit nonzero with safe categories, never backend exception bodies.
1550
+ In particular, apply may exit nonzero after a confirmed save because publication
1551
+ is unknown: inspect the emitted outcome before any next action.
1552
+
1553
+ On Windows, `chmod` does not establish file privacy. Create the private-input
1554
+ directory with an owner-only ACL before saving change JSON or receipts there:
1555
+
1556
+ ```powershell
1557
+ New-Item -ItemType Directory -Path .\ams-private
1558
+ $amsUser = whoami
1559
+ icacls .\ams-private /inheritance:r /grant:r "${amsUser}:(OI)(CI)F"
1560
+ ```
1561
+
1562
+ Native file handling checks the opened handle's owner and DACL, rejects grants to
1563
+ other principals and reparse points, and creates outputs with their restricted
1564
+ DACL already in place. Filesystems without usable ACLs fail closed.
1565
+
1566
+ The `Python CLI` and `Python CLI (Windows)` CI jobs run the non-live suite on
1567
+ Linux and Windows, including native file permissions and apply reconciliation.
1568
+ Both feed `SDK package checks`; a job failure or unexpected job skip blocks that aggregate.
1569
+ Run locally, sequentially, from `packages/cli/pip` with
1570
+ `uv run --frozen --extra dev python -m pytest -q -m 'not live'`.
1571
+ These synthetic checks do not establish live CareCo acceptance. That still needs
1572
+ an explicitly selected and qualified DEV agent, scoped credentials, and the
1573
+ attended before/after/restore procedure above.
1574
+
1575
+ For an optional real-HTTP reproduction with Postman CLI 1.75.0, start
1576
+ `postman simulation run postman/simulations/ams-tuning-failure.sim.yaml --port 4900 --no-history --output ndjson`
1577
+ from the repository root. In another terminal, run the same SDK verification
1578
+ helper used by the required loopback HTTP tests:
1579
+
1580
+ ```bash
1581
+ uv run --project packages/cli/pip --frozen --extra dev python -c "import sys; sys.path.insert(0, 'packages/cli/pip/tests'); from test_ams_http import verify_uncertain_apply; print(verify_uncertain_apply('http://127.0.0.1:4900/ams/api/ams'))"
1582
+ ```
1583
+
1584
+ The simulation log must show GET plan, GET agent, one POST apply returning 503,
1585
+ then GET plan and GET agent. Restart the stateful simulation before repeating;
1586
+ stop it with Ctrl+C afterward. This is a source-backed synthetic failure fixture,
1587
+ not the deployed service. The required CI tests exercise both pre-commit and
1588
+ post-commit 503 over loopback HTTP without requiring a Postman login or installation.
1589
+
1590
+
1591
+ ### Installed CLI integration
1592
+
1593
+ From the repository root, with Python 3.14, uv 0.12.5 and Node.js available:
1594
+
1595
+ ```bash
1596
+ python -m unittest discover -s tooling/cli-integration -p 'test_*.py' -v
1597
+ python tooling/cli-integration/run.py --output /tmp/hc-cli-integration
1598
+ ```
1599
+
1600
+ The runner installs the SDK, tenant-admin SDK and CLI from the frozen CLI lock
1601
+ with `uv sync --no-editable` into a fresh temporary environment outside the
1602
+ checkout. It verifies installed module ownership, version, help for every AMS
1603
+ command, native shell completion, discovery and the
1604
+ inspect/plan/show/apply/restore journey. Those three local packages keep one
1605
+ version across the stacked release series, so the install passes
1606
+ `--refresh-package` for each of them: without it uv can restore a cached wheel
1607
+ built from an earlier commit and the suite silently exercises old code.
1608
+
1609
+ Negative journeys exercise denied login, identity/qualification/plan rejection,
1610
+ malformed responses, failed readback and uncertain writes without mutation
1611
+ replay.
1612
+
1613
+ The journey suite journals itself as it runs: the output directory holds
1614
+ `uv/journeys.json` (the ordered journey list), `uv/progress.jsonl` (each
1615
+ completed journey, flushed) and `uv/journeys/` (per-journey results and
1616
+ diagnostics). If the suite hits its timeout, the run adds `uv/timeout.json` plus
1617
+ a console summary naming the journeys that were in flight, so the retained
1618
+ evidence is never an empty directory that cannot say where the run stopped.
1619
+
1620
+ The `Installed CLI integration` workflow selects its uv-installed Linux job at
1621
+ job level for `packages/cli/**`, `packages/pip/**`,
1622
+ `packages/pip-tenantadmin/**`, its own tooling/workflow, docs compiler inputs
1623
+ (`tooling/docs-compiler/**`, `docs-src/**`, `docs/sdk/**`), MCP contracts
1624
+ (`contracts/mcp/**`), and root package/lock, Python/lock and Ruff inputs. Backend-only changes skip the install job;
1625
+ the small `CLI integration` result check still runs and verifies applicability.
1626
+ Invalid selection or an applicable failed/cancelled/skipped job fails it.
1627
+ The job retains only sanitized consumer/JUnit results for seven days.
1628
+
1629
+ All scenario records and credentials are synthetic. A test-only Python startup
1630
+ adapter preserves the canonical DEV AMS/tenant-admin origins for product
1631
+ validation while routing only those exact origins over real HTTP to an owned
1632
+ loopback fixture; other origins are refused before network access. Child
1633
+ processes use an isolated home and an allowlisted environment. The adapter is
1634
+ outside all shipped distributions. This boundary does not verify TLS or deployed
1635
+ service conformance. A verified save still exits 1 with `outcome="saved"` and
1636
+ `publication="unknown"`; publication and runtime acceptance remain unverified.
1637
+
1638
+ Required-check activation is tracked separately in
1639
+ [services #724](https://github.com/healthcloudservices/healthcloud-services/issues/724).
1640
+ Require `CLI integration` only after the workflow is merged and both relevant
1641
+ and unrelated-change Actions results and effective settings are verified.