healthcloud-cli 3.3.0__tar.gz → 3.4.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.
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/PKG-INFO +101 -15
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/README.md +100 -14
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/commands.py +10 -10
- healthcloud_cli-3.4.0/healthcloud_cli/ams/auth.py +91 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/compatibility.py +6 -4
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/context.py +70 -1
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/commands.py +9 -7
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/errors.py +12 -2
- healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/__init__.py +5 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/commands.py +57 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/group.py +18 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/operations.py +67 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/guardrails/projections.py +18 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/commands.py +6 -5
- healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/__init__.py +5 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/commands.py +57 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/group.py +18 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/operations.py +67 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/policies/projections.py +21 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/registry.py +6 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/session.py +345 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/__init__.py +5 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/commands.py +57 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/group.py +18 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/operations.py +67 -0
- healthcloud_cli-3.4.0/healthcloud_cli/ams/skills/projections.py +19 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/commands.py +9 -7
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/windows_files.py +51 -2
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/cli.py +61 -1
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/pyproject.toml +1 -1
- healthcloud_cli-3.3.0/healthcloud_cli/ams/auth.py +0 -53
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/.gitignore +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/hatch_build.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/__init__.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/__init__.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/__init__.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/group.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/operations.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/agents/projections.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/commands.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/__init__.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/group.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/operations.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/departments/projections.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/domain_errors.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/group.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/__init__.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/group.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/operations.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/organization/projections.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/output.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/results.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/target.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/__init__.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/group.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/operations.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/teams/projections.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/tuning.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/ams/ui.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/context.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/contracts/__init__.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/main.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/manifest.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/output.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/params.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/healthcloud_cli/util.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/scripts/__init__.py +0 -0
- {healthcloud_cli-3.3.0 → healthcloud_cli-3.4.0}/scripts/bundle_manifest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: healthcloud-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest.
|
|
5
5
|
Project-URL: Issues, https://github.com/healthcloudservices/healthcloud-services/issues
|
|
6
6
|
Project-URL: Repository, https://github.com/healthcloudservices/healthcloud-services
|
|
@@ -69,10 +69,10 @@ _HC_COMPLETE=bash_complete COMP_WORDS="hc ams inspect --o" COMP_CWORD=3 uv run -
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
Operational validation starts only when Click invokes an AMS leaf callback.
|
|
72
|
-
`discover`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
those safety requirements.
|
|
72
|
+
`discover`, `agents`, `departments`, `teams`, `organization`, `skills`, and `policies` then
|
|
73
|
+
validate their explicit environment and tenant arguments; the remaining leaves
|
|
74
|
+
require `--target-file`. All apply the root, target, and authentication checks
|
|
75
|
+
described below. Deferring preflight does not relax those safety requirements.
|
|
76
76
|
|
|
77
77
|
The bundled tool manifest (`healthcloud_cli/contracts/mcp-tools.manifest.json`)
|
|
78
78
|
is a copy of `packages/cli/contracts/mcp-tools.manifest.json`. Re-sync it
|
|
@@ -280,16 +280,47 @@ uv sync --frozen --extra dev
|
|
|
280
280
|
uv run --frozen hc ams --help
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
Create one private tenant-admin session before interactive AMS work, inspect its
|
|
284
|
+
non-secret binding, and remove it when finished:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
uv run --frozen hc login --environment dev --tenant-id TENANT_ID
|
|
288
|
+
uv run --frozen hc login --status
|
|
289
|
+
uv run --frozen hc logout
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`hc login` reads `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` when present,
|
|
293
|
+
otherwise it prompts (with hidden password input). It stores only the email,
|
|
294
|
+
tenant/environment binding, access/refresh tokens and expiry in the strict
|
|
295
|
+
owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
|
|
296
|
+
AMS commands may omit both target flags while this session is present, except
|
|
297
|
+
that a `prod` session still needs `--environment prod` on every command. Explicit
|
|
298
|
+
flags and target files must match it exactly. A session within 60 seconds of
|
|
299
|
+
expiry is refreshed once before the operation, under a lock so concurrent
|
|
300
|
+
commands never spend the same refresh token (a second command waits up to 30 seconds, then stops), and
|
|
301
|
+
atomically replaced. Refresh
|
|
302
|
+
failure stops without an AMS call or password fallback and asks for a new login.
|
|
303
|
+
If the refreshed session cannot be saved, the current command still finishes and
|
|
304
|
+
prints a warning to run `hc login` again.
|
|
305
|
+
|
|
306
|
+
`~/.healthcloud` and `~/.healthcloud/cli` must be directories you own with mode
|
|
307
|
+
700; the CLI creates them that way, and refuses (naming the directory and the
|
|
308
|
+
`chmod 700` fix) if an existing one is shared. `hc logout` deletes the local file
|
|
309
|
+
only and reports `"server_session_revoked": false`: issued tokens stay valid
|
|
310
|
+
until they expire.
|
|
311
|
+
|
|
312
|
+
Bootstrap candidate IDs with the public SDK discovery endpoint. Without a saved
|
|
313
|
+
session both flags are required; a saved session supplies both together. No MCP
|
|
314
|
+
defaults, admin login, or automatic candidate selection are used:
|
|
285
315
|
|
|
286
316
|
```bash
|
|
287
317
|
uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
|
|
288
318
|
```
|
|
289
319
|
|
|
290
|
-
Read-only agent listing, inspection and export
|
|
291
|
-
|
|
292
|
-
|
|
320
|
+
Read-only agent listing, inspection and export take flags instead of a target
|
|
321
|
+
file; both may be omitted together when a matching saved session exists. Without
|
|
322
|
+
one, they log in as tenant admin using `HC_AMS_ADMIN_EMAIL` /
|
|
323
|
+
`HC_AMS_ADMIN_PASSWORD`. Stdout carries bounded metadata only; `adapter` shows
|
|
293
324
|
whether an agent runs from the `file` or `db` config adapter (`unknown` when the
|
|
294
325
|
server does not say). `export` first re-checks the agent/tenant identity, then
|
|
295
326
|
saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
|
|
@@ -335,6 +366,61 @@ or inferring missing relationships:
|
|
|
335
366
|
uv run --frozen hc ams organization hierarchy --environment dev --tenant-id TENANT_ID
|
|
336
367
|
```
|
|
337
368
|
|
|
369
|
+
Read-only skill catalog listing and inspection are complete and unpaged. The
|
|
370
|
+
bounded projection includes only skill IDs, names, tool IDs, and audit timestamps;
|
|
371
|
+
it omits descriptions, tenant identity, creator identity, arbitrary response
|
|
372
|
+
fields, and tool details. Tool-ID order is preserved:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
uv run --frozen hc ams skills list --environment dev --tenant-id TENANT_ID
|
|
376
|
+
uv run --frozen hc ams skills get --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The CLI does not create, update, delete, or assign skills. Skill commands do not
|
|
380
|
+
read guardrails. They make exactly one catalog GET after tenant-admin login
|
|
381
|
+
and does not paginate, retry, cache, join, or infer additional catalog data.
|
|
382
|
+
Skill reads refuse target files, root credentials or base URLs, raw output,
|
|
383
|
+
blank or dot-segment skill IDs, and installed SDKs that do not provide both
|
|
384
|
+
skill list and get methods. These local refusals happen before tenant-admin login
|
|
385
|
+
or catalog reads.
|
|
386
|
+
|
|
387
|
+
Read-only policy catalog listing and inspection are complete and unpaged. The
|
|
388
|
+
bounded projection includes only policy IDs, names, department IDs, guardrail
|
|
389
|
+
IDs, active status, and audit timestamps. It never emits protected policy
|
|
390
|
+
content, descriptions, tenant or creator identity, arbitrary response fields,
|
|
391
|
+
or joined department/guardrail details. Guardrail-ID order is preserved:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
uv run --frozen hc ams policies list --environment dev --tenant-id TENANT_ID
|
|
395
|
+
uv run --frozen hc ams policies get --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The CLI does not export protected policy content; create, update, delete, or
|
|
399
|
+
assign policies; or join guardrail records. It makes exactly one catalog GET after
|
|
400
|
+
tenant-admin login and does not paginate, retry, cache, join, or infer referenced
|
|
401
|
+
resources. Policy reads refuse target files, root credentials or base URLs, raw
|
|
402
|
+
output, blank or dot-segment policy IDs, and installed SDKs without both policy
|
|
403
|
+
methods before tenant-admin login or catalog reads. Successful reads make no
|
|
404
|
+
assignment, deployment, serving, enforcement, or effectiveness claim.
|
|
405
|
+
|
|
406
|
+
Read-only guardrail catalog listing and inspection are complete and unpaged.
|
|
407
|
+
The bounded projection includes only guardrail IDs, names, and audit timestamps.
|
|
408
|
+
It never emits protected rules, descriptions, tenant or creator identity,
|
|
409
|
+
arbitrary response fields, or joined policy/agent details:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
uv run --frozen hc ams guardrails list --environment dev --tenant-id TENANT_ID
|
|
413
|
+
uv run --frozen hc ams guardrails get --environment dev --tenant-id TENANT_ID \
|
|
414
|
+
--guardrail-id GUARDRAIL_ID
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The CLI does not export rules; create, update, delete, assign, deploy, or enforce
|
|
418
|
+
guardrails; or claim they are active or effective. It makes exactly one catalog
|
|
419
|
+
GET after tenant-admin login and does not paginate, retry, cache, join, or infer
|
|
420
|
+
additional data. Guardrail reads refuse target files, root credentials or base
|
|
421
|
+
URLs, raw output, blank or dot-segment guardrail IDs, and installed SDKs without
|
|
422
|
+
both guardrail methods before tenant-admin login or catalog reads.
|
|
423
|
+
|
|
338
424
|
Every other command requires an explicit local, non-secret target JSON file.
|
|
339
425
|
Only the canonical HTTPS AMS origin for that environment is accepted; custom
|
|
340
426
|
origins require separately verified support. Target and nested qualification keys
|
|
@@ -403,11 +489,11 @@ configuration and recheck it before apply and selected-field restore. This is an
|
|
|
403
489
|
operator prerequisite: the current CLI does not automatically attest deployment
|
|
404
490
|
or all-writer coverage, and the record adds no server authorization boundary.
|
|
405
491
|
|
|
406
|
-
|
|
407
|
-
in the current process, intended
|
|
408
|
-
`onepassword-env` wrapper
|
|
409
|
-
|
|
410
|
-
|
|
492
|
+
For CI and scripts with no saved session, credentials may still enter through
|
|
493
|
+
`HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` in the current process, intended
|
|
494
|
+
for composition with the existing `onepassword-env` wrapper; that compatibility
|
|
495
|
+
path logs in once per invocation. There is no fallback identity, secret argument,
|
|
496
|
+
arbitrary token import, resolved env file, keychain, or browser credential bridge. Root MCP
|
|
411
497
|
`--access-token`, `--base-url`, their environment equivalents, and token/base-URL
|
|
412
498
|
profiles are refused for AMS. Root profile environment values do not select AMS.
|
|
413
499
|
|
|
@@ -51,10 +51,10 @@ _HC_COMPLETE=bash_complete COMP_WORDS="hc ams inspect --o" COMP_CWORD=3 uv run -
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Operational validation starts only when Click invokes an AMS leaf callback.
|
|
54
|
-
`discover`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
those safety requirements.
|
|
54
|
+
`discover`, `agents`, `departments`, `teams`, `organization`, `skills`, and `policies` then
|
|
55
|
+
validate their explicit environment and tenant arguments; the remaining leaves
|
|
56
|
+
require `--target-file`. All apply the root, target, and authentication checks
|
|
57
|
+
described below. Deferring preflight does not relax those safety requirements.
|
|
58
58
|
|
|
59
59
|
The bundled tool manifest (`healthcloud_cli/contracts/mcp-tools.manifest.json`)
|
|
60
60
|
is a copy of `packages/cli/contracts/mcp-tools.manifest.json`. Re-sync it
|
|
@@ -262,16 +262,47 @@ uv sync --frozen --extra dev
|
|
|
262
262
|
uv run --frozen hc ams --help
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
Create one private tenant-admin session before interactive AMS work, inspect its
|
|
266
|
+
non-secret binding, and remove it when finished:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
uv run --frozen hc login --environment dev --tenant-id TENANT_ID
|
|
270
|
+
uv run --frozen hc login --status
|
|
271
|
+
uv run --frozen hc logout
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`hc login` reads `HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` when present,
|
|
275
|
+
otherwise it prompts (with hidden password input). It stores only the email,
|
|
276
|
+
tenant/environment binding, access/refresh tokens and expiry in the strict
|
|
277
|
+
owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
|
|
278
|
+
AMS commands may omit both target flags while this session is present, except
|
|
279
|
+
that a `prod` session still needs `--environment prod` on every command. Explicit
|
|
280
|
+
flags and target files must match it exactly. A session within 60 seconds of
|
|
281
|
+
expiry is refreshed once before the operation, under a lock so concurrent
|
|
282
|
+
commands never spend the same refresh token (a second command waits up to 30 seconds, then stops), and
|
|
283
|
+
atomically replaced. Refresh
|
|
284
|
+
failure stops without an AMS call or password fallback and asks for a new login.
|
|
285
|
+
If the refreshed session cannot be saved, the current command still finishes and
|
|
286
|
+
prints a warning to run `hc login` again.
|
|
287
|
+
|
|
288
|
+
`~/.healthcloud` and `~/.healthcloud/cli` must be directories you own with mode
|
|
289
|
+
700; the CLI creates them that way, and refuses (naming the directory and the
|
|
290
|
+
`chmod 700` fix) if an existing one is shared. `hc logout` deletes the local file
|
|
291
|
+
only and reports `"server_session_revoked": false`: issued tokens stay valid
|
|
292
|
+
until they expire.
|
|
293
|
+
|
|
294
|
+
Bootstrap candidate IDs with the public SDK discovery endpoint. Without a saved
|
|
295
|
+
session both flags are required; a saved session supplies both together. No MCP
|
|
296
|
+
defaults, admin login, or automatic candidate selection are used:
|
|
267
297
|
|
|
268
298
|
```bash
|
|
269
299
|
uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
|
|
270
300
|
```
|
|
271
301
|
|
|
272
|
-
Read-only agent listing, inspection and export
|
|
273
|
-
|
|
274
|
-
|
|
302
|
+
Read-only agent listing, inspection and export take flags instead of a target
|
|
303
|
+
file; both may be omitted together when a matching saved session exists. Without
|
|
304
|
+
one, they log in as tenant admin using `HC_AMS_ADMIN_EMAIL` /
|
|
305
|
+
`HC_AMS_ADMIN_PASSWORD`. Stdout carries bounded metadata only; `adapter` shows
|
|
275
306
|
whether an agent runs from the `file` or `db` config adapter (`unknown` when the
|
|
276
307
|
server does not say). `export` first re-checks the agent/tenant identity, then
|
|
277
308
|
saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
|
|
@@ -317,6 +348,61 @@ or inferring missing relationships:
|
|
|
317
348
|
uv run --frozen hc ams organization hierarchy --environment dev --tenant-id TENANT_ID
|
|
318
349
|
```
|
|
319
350
|
|
|
351
|
+
Read-only skill catalog listing and inspection are complete and unpaged. The
|
|
352
|
+
bounded projection includes only skill IDs, names, tool IDs, and audit timestamps;
|
|
353
|
+
it omits descriptions, tenant identity, creator identity, arbitrary response
|
|
354
|
+
fields, and tool details. Tool-ID order is preserved:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
uv run --frozen hc ams skills list --environment dev --tenant-id TENANT_ID
|
|
358
|
+
uv run --frozen hc ams skills get --environment dev --tenant-id TENANT_ID --skill-id SKILL_ID
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The CLI does not create, update, delete, or assign skills. Skill commands do not
|
|
362
|
+
read guardrails. They make exactly one catalog GET after tenant-admin login
|
|
363
|
+
and does not paginate, retry, cache, join, or infer additional catalog data.
|
|
364
|
+
Skill reads refuse target files, root credentials or base URLs, raw output,
|
|
365
|
+
blank or dot-segment skill IDs, and installed SDKs that do not provide both
|
|
366
|
+
skill list and get methods. These local refusals happen before tenant-admin login
|
|
367
|
+
or catalog reads.
|
|
368
|
+
|
|
369
|
+
Read-only policy catalog listing and inspection are complete and unpaged. The
|
|
370
|
+
bounded projection includes only policy IDs, names, department IDs, guardrail
|
|
371
|
+
IDs, active status, and audit timestamps. It never emits protected policy
|
|
372
|
+
content, descriptions, tenant or creator identity, arbitrary response fields,
|
|
373
|
+
or joined department/guardrail details. Guardrail-ID order is preserved:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
uv run --frozen hc ams policies list --environment dev --tenant-id TENANT_ID
|
|
377
|
+
uv run --frozen hc ams policies get --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The CLI does not export protected policy content; create, update, delete, or
|
|
381
|
+
assign policies; or join guardrail records. It makes exactly one catalog GET after
|
|
382
|
+
tenant-admin login and does not paginate, retry, cache, join, or infer referenced
|
|
383
|
+
resources. Policy reads refuse target files, root credentials or base URLs, raw
|
|
384
|
+
output, blank or dot-segment policy IDs, and installed SDKs without both policy
|
|
385
|
+
methods before tenant-admin login or catalog reads. Successful reads make no
|
|
386
|
+
assignment, deployment, serving, enforcement, or effectiveness claim.
|
|
387
|
+
|
|
388
|
+
Read-only guardrail catalog listing and inspection are complete and unpaged.
|
|
389
|
+
The bounded projection includes only guardrail IDs, names, and audit timestamps.
|
|
390
|
+
It never emits protected rules, descriptions, tenant or creator identity,
|
|
391
|
+
arbitrary response fields, or joined policy/agent details:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
uv run --frozen hc ams guardrails list --environment dev --tenant-id TENANT_ID
|
|
395
|
+
uv run --frozen hc ams guardrails get --environment dev --tenant-id TENANT_ID \
|
|
396
|
+
--guardrail-id GUARDRAIL_ID
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The CLI does not export rules; create, update, delete, assign, deploy, or enforce
|
|
400
|
+
guardrails; or claim they are active or effective. It makes exactly one catalog
|
|
401
|
+
GET after tenant-admin login and does not paginate, retry, cache, join, or infer
|
|
402
|
+
additional data. Guardrail reads refuse target files, root credentials or base
|
|
403
|
+
URLs, raw output, blank or dot-segment guardrail IDs, and installed SDKs without
|
|
404
|
+
both guardrail methods before tenant-admin login or catalog reads.
|
|
405
|
+
|
|
320
406
|
Every other command requires an explicit local, non-secret target JSON file.
|
|
321
407
|
Only the canonical HTTPS AMS origin for that environment is accepted; custom
|
|
322
408
|
origins require separately verified support. Target and nested qualification keys
|
|
@@ -385,11 +471,11 @@ configuration and recheck it before apply and selected-field restore. This is an
|
|
|
385
471
|
operator prerequisite: the current CLI does not automatically attest deployment
|
|
386
472
|
or all-writer coverage, and the record adds no server authorization boundary.
|
|
387
473
|
|
|
388
|
-
|
|
389
|
-
in the current process, intended
|
|
390
|
-
`onepassword-env` wrapper
|
|
391
|
-
|
|
392
|
-
|
|
474
|
+
For CI and scripts with no saved session, credentials may still enter through
|
|
475
|
+
`HC_AMS_ADMIN_EMAIL` and `HC_AMS_ADMIN_PASSWORD` in the current process, intended
|
|
476
|
+
for composition with the existing `onepassword-env` wrapper; that compatibility
|
|
477
|
+
path logs in once per invocation. There is no fallback identity, secret argument,
|
|
478
|
+
arbitrary token import, resolved env file, keychain, or browser credential bridge. Root MCP
|
|
393
479
|
`--access-token`, `--base-url`, their environment equivalents, and token/base-URL
|
|
394
480
|
profiles are refused for AMS. Root profile environment values do not select AMS.
|
|
395
481
|
|
|
@@ -9,16 +9,16 @@ import click
|
|
|
9
9
|
from ..compatibility import safe_errors
|
|
10
10
|
from ..context import AmsDependencies, pass_ams, preflight_agents
|
|
11
11
|
from ..output import metadata
|
|
12
|
-
from ..
|
|
12
|
+
from ..session import resolve_tenant_target
|
|
13
13
|
from .operations import AgentReadRefused, export_agent, get_agent, list_agents
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _environment(function):
|
|
17
|
-
return click.option("--environment", type=click.Choice(["dev", "uat", "prod"])
|
|
17
|
+
return click.option("--environment", type=click.Choice(["dev", "uat", "prod"]))(function)
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _tenant(function):
|
|
21
|
-
return click.option("--tenant-id"
|
|
21
|
+
return click.option("--tenant-id")(function)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _agent(function):
|
|
@@ -38,9 +38,9 @@ def create_list_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
38
38
|
@_tenant
|
|
39
39
|
@pass_ams
|
|
40
40
|
@safe_errors
|
|
41
|
-
def command(invocation, environment: str, tenant_id: str) -> None:
|
|
41
|
+
def command(invocation, environment: str | None, tenant_id: str | None) -> None:
|
|
42
42
|
preflight_agents(invocation)
|
|
43
|
-
metadata(_run(list_agents, dependencies,
|
|
43
|
+
metadata(_run(list_agents, dependencies, resolve_tenant_target(environment, tenant_id)))
|
|
44
44
|
|
|
45
45
|
return command
|
|
46
46
|
|
|
@@ -52,9 +52,9 @@ def create_get_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
52
52
|
@_agent
|
|
53
53
|
@pass_ams
|
|
54
54
|
@safe_errors
|
|
55
|
-
def command(invocation, environment: str, tenant_id: str, agent_id: str) -> None:
|
|
55
|
+
def command(invocation, environment: str | None, tenant_id: str | None, agent_id: str) -> None:
|
|
56
56
|
preflight_agents(invocation)
|
|
57
|
-
metadata(_run(get_agent, dependencies,
|
|
57
|
+
metadata(_run(get_agent, dependencies, resolve_tenant_target(environment, tenant_id), agent_id))
|
|
58
58
|
|
|
59
59
|
return command
|
|
60
60
|
|
|
@@ -70,14 +70,14 @@ def create_export_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
70
70
|
@safe_errors
|
|
71
71
|
def command(
|
|
72
72
|
invocation,
|
|
73
|
-
environment: str,
|
|
74
|
-
tenant_id: str,
|
|
73
|
+
environment: str | None,
|
|
74
|
+
tenant_id: str | None,
|
|
75
75
|
agent_id: str,
|
|
76
76
|
out: Path,
|
|
77
77
|
export_format: str,
|
|
78
78
|
) -> None:
|
|
79
79
|
preflight_agents(invocation)
|
|
80
|
-
target =
|
|
80
|
+
target = resolve_tenant_target(environment, tenant_id)
|
|
81
81
|
metadata(_run(export_agent, dependencies, target, agent_id, out, export_format))
|
|
82
82
|
|
|
83
83
|
return command
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Saved-session or environment tenant-admin authentication."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from healthcloud import HCSDK
|
|
10
|
+
from healthcloud_tenantadmin import HCTenantAdminSDK
|
|
11
|
+
|
|
12
|
+
from .session import load_session, refreshed_session, save_session, session_lock
|
|
13
|
+
from .target import AmsTarget, TenantTarget
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def authenticated_sdk(target: AmsTarget | TenantTarget):
|
|
18
|
+
sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
|
|
19
|
+
try:
|
|
20
|
+
if not hasattr(sdk, "ams_admin"):
|
|
21
|
+
raise click.ClickException(
|
|
22
|
+
"SDK missing ams_admin; use the reviewed local editable SDK checkout (see CLI README)."
|
|
23
|
+
)
|
|
24
|
+
if isinstance(target, TenantTarget) and not hasattr(sdk.ams_admin, "list_agents"):
|
|
25
|
+
raise click.ClickException(
|
|
26
|
+
"SDK missing ams_admin agent listing; use the reviewed local editable SDK checkout (see CLI README)."
|
|
27
|
+
)
|
|
28
|
+
session = load_session()
|
|
29
|
+
if session is not None:
|
|
30
|
+
if (target.environment, target.tenant_id) != (session.environment, session.tenant_id):
|
|
31
|
+
raise click.ClickException(
|
|
32
|
+
"AMS target does not match the saved session; stopped before authentication."
|
|
33
|
+
)
|
|
34
|
+
token = _saved_session_token(target)
|
|
35
|
+
else:
|
|
36
|
+
email = os.environ.get("HC_AMS_ADMIN_EMAIL")
|
|
37
|
+
password = os.environ.get("HC_AMS_ADMIN_PASSWORD")
|
|
38
|
+
if not email or not password:
|
|
39
|
+
raise click.ClickException(
|
|
40
|
+
"Run hc login or inject HC_AMS_ADMIN_EMAIL and HC_AMS_ADMIN_PASSWORD into this process."
|
|
41
|
+
)
|
|
42
|
+
with HCTenantAdminSDK(environment=target.environment, tenant_id=target.tenant_id) as admin:
|
|
43
|
+
result = admin.admin.login_tenant_admin(email=email, password=password, tenant_id=target.tenant_id)
|
|
44
|
+
token = result.get("access_token") if isinstance(result, dict) else None
|
|
45
|
+
if not isinstance(token, str) or not token.strip():
|
|
46
|
+
raise click.ClickException("Tenant-admin login returned no access token; stopped without fallback.")
|
|
47
|
+
sdk.set_access_token(token)
|
|
48
|
+
try:
|
|
49
|
+
yield sdk
|
|
50
|
+
finally:
|
|
51
|
+
sdk.set_access_token(None)
|
|
52
|
+
|
|
53
|
+
finally:
|
|
54
|
+
sdk.close()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _saved_session_token(target: AmsTarget | TenantTarget) -> str:
|
|
58
|
+
with session_lock():
|
|
59
|
+
# Re-read under the lock: another command may have refreshed while this one waited.
|
|
60
|
+
session = load_session()
|
|
61
|
+
if session is None or (target.environment, target.tenant_id) != (session.environment, session.tenant_id):
|
|
62
|
+
raise click.ClickException("Saved AMS session changed; run hc login.")
|
|
63
|
+
if not session.needs_refresh(datetime.now(UTC)):
|
|
64
|
+
return session.access_token
|
|
65
|
+
try:
|
|
66
|
+
with HCTenantAdminSDK(environment=target.environment, tenant_id=target.tenant_id) as admin:
|
|
67
|
+
result = admin.admin.refresh_tenant_admin_token(
|
|
68
|
+
refresh_token=session.refresh_token, tenant_id=target.tenant_id
|
|
69
|
+
)
|
|
70
|
+
except Exception:
|
|
71
|
+
raise click.ClickException("Saved AMS session refresh failed; run hc login.") from None
|
|
72
|
+
session = refreshed_session(session, result, datetime.now(UTC))
|
|
73
|
+
try:
|
|
74
|
+
save_session(session)
|
|
75
|
+
except click.ClickException:
|
|
76
|
+
# The refresh already succeeded; the old refresh token may be spent. Finish this
|
|
77
|
+
# command with the new access token and say the saved session is no longer usable.
|
|
78
|
+
click.echo(
|
|
79
|
+
"Warning: refreshed AMS session could not be saved; run hc login before the next command.",
|
|
80
|
+
err=True,
|
|
81
|
+
)
|
|
82
|
+
return session.access_token
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def selected_agent(sdk, target: AmsTarget):
|
|
86
|
+
agent = sdk.ams_admin.get_agent(target.agent_id)
|
|
87
|
+
if agent is None or agent.id != target.agent_id or agent.tenant_id != target.tenant_id:
|
|
88
|
+
raise click.ClickException("Agent/tenant identity mismatch; stopped.")
|
|
89
|
+
if type(agent.config_version) is not int or agent.config_version < 1:
|
|
90
|
+
raise click.ClickException("Agent configuration version unavailable; stopped.")
|
|
91
|
+
return agent
|
|
@@ -71,15 +71,17 @@ def default_dependencies() -> AmsDependencies:
|
|
|
71
71
|
|
|
72
72
|
def create_discover_command(dependencies: AmsDependencies) -> click.Command:
|
|
73
73
|
@click.command("discover")
|
|
74
|
-
@click.option("--environment", type=click.Choice(["dev", "uat", "prod"])
|
|
75
|
-
@click.option("--tenant-id"
|
|
74
|
+
@click.option("--environment", type=click.Choice(["dev", "uat", "prod"]))
|
|
75
|
+
@click.option("--tenant-id")
|
|
76
76
|
@pass_ams
|
|
77
77
|
@safe_errors
|
|
78
78
|
def discover(invocation, environment, tenant_id):
|
|
79
79
|
"""Public candidate IDs only; never infer or select an agent."""
|
|
80
80
|
preflight_discovery(invocation)
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
from .session import resolve_tenant_target
|
|
82
|
+
|
|
83
|
+
target = resolve_tenant_target(environment, tenant_id)
|
|
84
|
+
environment, tenant_id = target.environment, target.tenant_id
|
|
83
85
|
with dependencies.public_sdk_factory(environment=environment) as sdk:
|
|
84
86
|
agents = sdk.ams.get_agents_for_tenant(tenant_id)
|
|
85
87
|
if not isinstance(agents, dict) or any(
|
|
@@ -134,10 +134,79 @@ def preflight_organization(invocation: AmsContext, target: TenantTarget) -> None
|
|
|
134
134
|
sdk.close()
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
def preflight_skills(invocation: AmsContext, target: TenantTarget) -> None:
|
|
138
|
+
"""Validate local skill-read compatibility before login or data reads."""
|
|
139
|
+
|
|
140
|
+
_check_root(invocation)
|
|
141
|
+
if invocation.target_file is not None:
|
|
142
|
+
raise click.ClickException("Skills requires explicit environment/tenant flags, without a target file.")
|
|
143
|
+
sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
|
|
144
|
+
try:
|
|
145
|
+
resource = getattr(getattr(sdk, "ams_admin", None), "skills", None)
|
|
146
|
+
if (
|
|
147
|
+
resource is None
|
|
148
|
+
or not callable(getattr(resource, "list", None))
|
|
149
|
+
or not callable(getattr(resource, "get", None))
|
|
150
|
+
):
|
|
151
|
+
raise click.ClickException(
|
|
152
|
+
"SDK missing ams_admin skills; use the reviewed SDK version required by this CLI."
|
|
153
|
+
)
|
|
154
|
+
finally:
|
|
155
|
+
sdk.close()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def preflight_policies(invocation: AmsContext, target: TenantTarget) -> None:
|
|
159
|
+
"""Validate local policy-read compatibility before login or data reads."""
|
|
160
|
+
|
|
161
|
+
_check_root(invocation)
|
|
162
|
+
if invocation.target_file is not None:
|
|
163
|
+
raise click.ClickException("Policies requires explicit environment/tenant flags, without a target file.")
|
|
164
|
+
sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
|
|
165
|
+
try:
|
|
166
|
+
resource = getattr(getattr(sdk, "ams_admin", None), "policies", None)
|
|
167
|
+
if (
|
|
168
|
+
resource is None
|
|
169
|
+
or not callable(getattr(resource, "list", None))
|
|
170
|
+
or not callable(getattr(resource, "get", None))
|
|
171
|
+
):
|
|
172
|
+
raise click.ClickException(
|
|
173
|
+
"SDK missing ams_admin policies; use the reviewed SDK version required by this CLI."
|
|
174
|
+
)
|
|
175
|
+
finally:
|
|
176
|
+
sdk.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def preflight_guardrails(invocation: AmsContext, target: TenantTarget) -> None:
|
|
180
|
+
"""Validate local guardrail-read compatibility before login or data reads."""
|
|
181
|
+
|
|
182
|
+
_check_root(invocation)
|
|
183
|
+
if invocation.target_file is not None:
|
|
184
|
+
raise click.ClickException("Guardrails requires explicit environment/tenant flags, without a target file.")
|
|
185
|
+
sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
|
|
186
|
+
try:
|
|
187
|
+
resource = getattr(getattr(sdk, "ams_admin", None), "guardrails", None)
|
|
188
|
+
if (
|
|
189
|
+
resource is None
|
|
190
|
+
or not callable(getattr(resource, "list", None))
|
|
191
|
+
or not callable(getattr(resource, "get", None))
|
|
192
|
+
):
|
|
193
|
+
raise click.ClickException(
|
|
194
|
+
"SDK missing ams_admin guardrails; use the reviewed SDK version required by this CLI."
|
|
195
|
+
)
|
|
196
|
+
finally:
|
|
197
|
+
sdk.close()
|
|
198
|
+
|
|
199
|
+
|
|
137
200
|
def require_target(invocation: AmsContext) -> AmsTarget:
|
|
138
201
|
"""Validate root options and load the explicitly selected AMS target."""
|
|
139
202
|
|
|
140
203
|
_check_root(invocation)
|
|
141
204
|
if invocation.target_file is None:
|
|
142
205
|
raise click.ClickException("AMS requires an explicit --target-file.")
|
|
143
|
-
|
|
206
|
+
target = load_target(invocation.target_file)
|
|
207
|
+
from .session import load_session
|
|
208
|
+
|
|
209
|
+
session = load_session()
|
|
210
|
+
if session is not None and (target.environment, target.tenant_id) != (session.environment, session.tenant_id):
|
|
211
|
+
raise click.ClickException("AMS target does not match the saved session; stopped before authentication.")
|
|
212
|
+
return target
|
|
@@ -6,16 +6,16 @@ import click
|
|
|
6
6
|
|
|
7
7
|
from ..context import AmsDependencies, pass_ams, preflight_departments
|
|
8
8
|
from ..errors import run_domain
|
|
9
|
-
from ..
|
|
9
|
+
from ..session import resolve_tenant_target
|
|
10
10
|
from .operations import get_department, list_departments
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _environment(function):
|
|
14
|
-
return click.option("--environment", type=click.Choice(["dev", "uat", "prod"])
|
|
14
|
+
return click.option("--environment", type=click.Choice(["dev", "uat", "prod"]))(function)
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _tenant(function):
|
|
18
|
-
return click.option("--tenant-id"
|
|
18
|
+
return click.option("--tenant-id")(function)
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def create_list_command(dependencies: AmsDependencies) -> click.Command:
|
|
@@ -23,11 +23,12 @@ def create_list_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
23
23
|
@_environment
|
|
24
24
|
@_tenant
|
|
25
25
|
@pass_ams
|
|
26
|
-
def command(invocation, environment: str, tenant_id: str) -> None:
|
|
26
|
+
def command(invocation, environment: str | None, tenant_id: str | None) -> None:
|
|
27
27
|
target_data = {"environment": environment, "tenant_id": tenant_id}
|
|
28
28
|
|
|
29
29
|
def execute():
|
|
30
|
-
target =
|
|
30
|
+
target = resolve_tenant_target(environment, tenant_id)
|
|
31
|
+
target_data.update(environment=target.environment, tenant_id=target.tenant_id)
|
|
31
32
|
preflight_departments(invocation, target)
|
|
32
33
|
return list_departments(dependencies, target)
|
|
33
34
|
|
|
@@ -42,7 +43,7 @@ def create_get_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
42
43
|
@_tenant
|
|
43
44
|
@click.option("--department-id", required=True)
|
|
44
45
|
@pass_ams
|
|
45
|
-
def command(invocation, environment: str, tenant_id: str, department_id: str) -> None:
|
|
46
|
+
def command(invocation, environment: str | None, tenant_id: str | None, department_id: str) -> None:
|
|
46
47
|
target_data = {
|
|
47
48
|
"environment": environment,
|
|
48
49
|
"tenant_id": tenant_id,
|
|
@@ -50,7 +51,8 @@ def create_get_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
def execute():
|
|
53
|
-
target =
|
|
54
|
+
target = resolve_tenant_target(environment, tenant_id)
|
|
55
|
+
target_data.update(environment=target.environment, tenant_id=target.tenant_id)
|
|
54
56
|
preflight_departments(invocation, target)
|
|
55
57
|
return get_department(dependencies, target, department_id)
|
|
56
58
|
|
|
@@ -28,7 +28,13 @@ _MESSAGE_TEMPLATES = {
|
|
|
28
28
|
|
|
29
29
|
def _message(operation: str, category: str) -> str:
|
|
30
30
|
prefix = operation.split(".", 1)[0]
|
|
31
|
-
domain = {
|
|
31
|
+
domain = {
|
|
32
|
+
"teams": "Team",
|
|
33
|
+
"organization": "Organization",
|
|
34
|
+
"skills": "Skill",
|
|
35
|
+
"policies": "Policy",
|
|
36
|
+
"guardrails": "Guardrail",
|
|
37
|
+
}.get(prefix, "Department")
|
|
32
38
|
return _MESSAGE_TEMPLATES[category].format(domain=domain, domain_lower=domain.lower())
|
|
33
39
|
|
|
34
40
|
|
|
@@ -38,7 +44,11 @@ def _http_category(operation: str, status: object) -> str:
|
|
|
38
44
|
403: "authorization",
|
|
39
45
|
409: "conflict",
|
|
40
46
|
}
|
|
41
|
-
if operation.split(".", 1)[0] in {"teams", "organization"} and status in {
|
|
47
|
+
if operation.split(".", 1)[0] in {"teams", "organization", "skills", "policies", "guardrails"} and status in {
|
|
48
|
+
400,
|
|
49
|
+
404,
|
|
50
|
+
422,
|
|
51
|
+
}:
|
|
42
52
|
return "validation"
|
|
43
53
|
return shared_categories.get(status, "transport")
|
|
44
54
|
|