healthcloud-cli 3.2.5__tar.gz → 3.3.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.2.5 → healthcloud_cli-3.3.0}/PKG-INFO +49 -1
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/README.md +48 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/__init__.py +5 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/commands.py +83 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/group.py +18 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/operations.py +72 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/projections.py +37 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/auth.py +6 -2
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/commands.py +2 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/context.py +143 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/__init__.py +5 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/commands.py +59 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/group.py +18 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/operations.py +71 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/projections.py +19 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/domain_errors.py +9 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/errors.py +87 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/__init__.py +5 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/commands.py +28 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/group.py +17 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/operations.py +33 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/projections.py +29 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/output.py +17 -3
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/registry.py +8 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/results.py +31 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/target.py +14 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/__init__.py +5 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/commands.py +55 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/group.py +18 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/operations.py +67 -0
- healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/projections.py +21 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/pyproject.toml +1 -1
- healthcloud_cli-3.2.5/healthcloud_cli/ams/context.py +0 -74
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/.gitignore +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/hatch_build.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/__init__.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/__init__.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/compatibility.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/group.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/tuning.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/ui.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/windows_files.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/cli.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/context.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/contracts/__init__.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/main.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/manifest.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/output.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/params.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/util.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/scripts/__init__.py +0 -0
- {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.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.3.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
|
|
@@ -287,6 +287,54 @@ required; no MCP defaults, admin login, or automatic candidate selection are use
|
|
|
287
287
|
uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
+
Read-only agent listing, inspection and export also take explicit flags instead
|
|
291
|
+
of a target file, and log in as tenant admin (`HC_AMS_ADMIN_EMAIL` /
|
|
292
|
+
`HC_AMS_ADMIN_PASSWORD`). Stdout carries bounded metadata only; `adapter` shows
|
|
293
|
+
whether an agent runs from the `file` or `db` config adapter (`unknown` when the
|
|
294
|
+
server does not say). `export` first re-checks the agent/tenant identity, then
|
|
295
|
+
saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
|
|
296
|
+
file, refusing (and leaving no file) when the body is not valid UTF-8. `export`
|
|
297
|
+
needs an AMS build that serves `/agents/{agent_id}/configuration/export`; that
|
|
298
|
+
route is not yet deployed to dev:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
uv run --frozen hc ams agents list --environment dev --tenant-id TENANT_ID
|
|
302
|
+
uv run --frozen hc ams agents get --environment dev --tenant-id TENANT_ID --agent-id AGENT_ID
|
|
303
|
+
uv run --frozen hc ams agents export --environment dev --tenant-id TENANT_ID --agent-id AGENT_ID \
|
|
304
|
+
--format yaml --out agent.yaml
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Read-only department listing and inspection use the same explicit tenant-admin
|
|
308
|
+
targeting. They emit one versioned JSON result on stdout; failures emit one
|
|
309
|
+
categorized JSON result on stderr. Department descriptions, creator identities,
|
|
310
|
+
and evaluation-suite content are never included in the projection:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
uv run --frozen hc ams departments list --environment dev --tenant-id TENANT_ID
|
|
314
|
+
uv run --frozen hc ams departments get --environment dev --tenant-id TENANT_ID \
|
|
315
|
+
--department-id DEPARTMENT_ID
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Department reads are complete and unpaged. The CLI does not expose department
|
|
319
|
+
create, update, delete, activate, or deactivate operations.
|
|
320
|
+
|
|
321
|
+
Read-only team listing and inspection are also complete and unpaged. The bounded
|
|
322
|
+
projection omits descriptions and creator identities; it includes evaluation-suite
|
|
323
|
+
IDs but never suite content. The CLI does not expose team mutations:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
uv run --frozen hc ams teams list --environment dev --tenant-id TENANT_ID
|
|
327
|
+
uv run --frozen hc ams teams get --environment dev --tenant-id TENANT_ID --team-id TEAM_ID
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The organization hierarchy is the backend-built department/team/agent tree. The
|
|
331
|
+
CLI preserves its order and nesting without issuing entity reads, joining records,
|
|
332
|
+
or inferring missing relationships:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
uv run --frozen hc ams organization hierarchy --environment dev --tenant-id TENANT_ID
|
|
336
|
+
```
|
|
337
|
+
|
|
290
338
|
Every other command requires an explicit local, non-secret target JSON file.
|
|
291
339
|
Only the canonical HTTPS AMS origin for that environment is accepted; custom
|
|
292
340
|
origins require separately verified support. Target and nested qualification keys
|
|
@@ -269,6 +269,54 @@ required; no MCP defaults, admin login, or automatic candidate selection are use
|
|
|
269
269
|
uv run --frozen hc ams discover --environment dev --tenant-id TENANT_ID
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
+
Read-only agent listing, inspection and export also take explicit flags instead
|
|
273
|
+
of a target file, and log in as tenant admin (`HC_AMS_ADMIN_EMAIL` /
|
|
274
|
+
`HC_AMS_ADMIN_PASSWORD`). Stdout carries bounded metadata only; `adapter` shows
|
|
275
|
+
whether an agent runs from the `file` or `db` config adapter (`unknown` when the
|
|
276
|
+
server does not say). `export` first re-checks the agent/tenant identity, then
|
|
277
|
+
saves the server's own JSON/YAML export byte-for-byte to a new owner-only `--out`
|
|
278
|
+
file, refusing (and leaving no file) when the body is not valid UTF-8. `export`
|
|
279
|
+
needs an AMS build that serves `/agents/{agent_id}/configuration/export`; that
|
|
280
|
+
route is not yet deployed to dev:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
uv run --frozen hc ams agents list --environment dev --tenant-id TENANT_ID
|
|
284
|
+
uv run --frozen hc ams agents get --environment dev --tenant-id TENANT_ID --agent-id AGENT_ID
|
|
285
|
+
uv run --frozen hc ams agents export --environment dev --tenant-id TENANT_ID --agent-id AGENT_ID \
|
|
286
|
+
--format yaml --out agent.yaml
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Read-only department listing and inspection use the same explicit tenant-admin
|
|
290
|
+
targeting. They emit one versioned JSON result on stdout; failures emit one
|
|
291
|
+
categorized JSON result on stderr. Department descriptions, creator identities,
|
|
292
|
+
and evaluation-suite content are never included in the projection:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
uv run --frozen hc ams departments list --environment dev --tenant-id TENANT_ID
|
|
296
|
+
uv run --frozen hc ams departments get --environment dev --tenant-id TENANT_ID \
|
|
297
|
+
--department-id DEPARTMENT_ID
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Department reads are complete and unpaged. The CLI does not expose department
|
|
301
|
+
create, update, delete, activate, or deactivate operations.
|
|
302
|
+
|
|
303
|
+
Read-only team listing and inspection are also complete and unpaged. The bounded
|
|
304
|
+
projection omits descriptions and creator identities; it includes evaluation-suite
|
|
305
|
+
IDs but never suite content. The CLI does not expose team mutations:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
uv run --frozen hc ams teams list --environment dev --tenant-id TENANT_ID
|
|
309
|
+
uv run --frozen hc ams teams get --environment dev --tenant-id TENANT_ID --team-id TEAM_ID
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The organization hierarchy is the backend-built department/team/agent tree. The
|
|
313
|
+
CLI preserves its order and nesting without issuing entity reads, joining records,
|
|
314
|
+
or inferring missing relationships:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
uv run --frozen hc ams organization hierarchy --environment dev --tenant-id TENANT_ID
|
|
318
|
+
```
|
|
319
|
+
|
|
272
320
|
Every other command requires an explicit local, non-secret target JSON file.
|
|
273
321
|
Only the canonical HTTPS AMS origin for that environment is accepted; custom
|
|
274
322
|
origins require separately verified support. Target and nested qualification keys
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Fresh Click adapters for read-only agent operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ..compatibility import safe_errors
|
|
10
|
+
from ..context import AmsDependencies, pass_ams, preflight_agents
|
|
11
|
+
from ..output import metadata
|
|
12
|
+
from ..target import TenantTarget
|
|
13
|
+
from .operations import AgentReadRefused, export_agent, get_agent, list_agents
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _environment(function):
|
|
17
|
+
return click.option("--environment", type=click.Choice(["dev", "uat", "prod"]), required=True)(function)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _tenant(function):
|
|
21
|
+
return click.option("--tenant-id", required=True)(function)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _agent(function):
|
|
25
|
+
return click.option("--agent-id", required=True)(function)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run(operation, *args):
|
|
29
|
+
try:
|
|
30
|
+
return operation(*args)
|
|
31
|
+
except AgentReadRefused as exc:
|
|
32
|
+
raise click.ClickException(str(exc)) from None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_list_command(dependencies: AmsDependencies) -> click.Command:
|
|
36
|
+
@click.command("list")
|
|
37
|
+
@_environment
|
|
38
|
+
@_tenant
|
|
39
|
+
@pass_ams
|
|
40
|
+
@safe_errors
|
|
41
|
+
def command(invocation, environment: str, tenant_id: str) -> None:
|
|
42
|
+
preflight_agents(invocation)
|
|
43
|
+
metadata(_run(list_agents, dependencies, TenantTarget(environment, tenant_id)))
|
|
44
|
+
|
|
45
|
+
return command
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_get_command(dependencies: AmsDependencies) -> click.Command:
|
|
49
|
+
@click.command("get")
|
|
50
|
+
@_environment
|
|
51
|
+
@_tenant
|
|
52
|
+
@_agent
|
|
53
|
+
@pass_ams
|
|
54
|
+
@safe_errors
|
|
55
|
+
def command(invocation, environment: str, tenant_id: str, agent_id: str) -> None:
|
|
56
|
+
preflight_agents(invocation)
|
|
57
|
+
metadata(_run(get_agent, dependencies, TenantTarget(environment, tenant_id), agent_id))
|
|
58
|
+
|
|
59
|
+
return command
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_export_command(dependencies: AmsDependencies) -> click.Command:
|
|
63
|
+
@click.command("export")
|
|
64
|
+
@_environment
|
|
65
|
+
@_tenant
|
|
66
|
+
@_agent
|
|
67
|
+
@click.option("--out", required=True, type=click.Path(path_type=Path, dir_okay=False))
|
|
68
|
+
@click.option("--format", "export_format", type=click.Choice(["json", "yaml"]), default="json", show_default=True)
|
|
69
|
+
@pass_ams
|
|
70
|
+
@safe_errors
|
|
71
|
+
def command(
|
|
72
|
+
invocation,
|
|
73
|
+
environment: str,
|
|
74
|
+
tenant_id: str,
|
|
75
|
+
agent_id: str,
|
|
76
|
+
out: Path,
|
|
77
|
+
export_format: str,
|
|
78
|
+
) -> None:
|
|
79
|
+
preflight_agents(invocation)
|
|
80
|
+
target = TenantTarget(environment, tenant_id)
|
|
81
|
+
metadata(_run(export_agent, dependencies, target, agent_id, out, export_format))
|
|
82
|
+
|
|
83
|
+
return command
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Fresh agent-domain group assembly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..context import AmsDependencies
|
|
8
|
+
from .commands import create_export_command, create_get_command, create_list_command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_agents_group(dependencies: AmsDependencies) -> click.Group:
|
|
12
|
+
@click.group("agents")
|
|
13
|
+
def group() -> None:
|
|
14
|
+
"""Read-only agent listing, inspection and export for one explicit tenant."""
|
|
15
|
+
|
|
16
|
+
for factory in (create_list_command, create_get_command, create_export_command):
|
|
17
|
+
group.add_command(factory(dependencies))
|
|
18
|
+
return group
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Click-free orchestration for agent list, get, and exact-text export."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from ..context import AmsDependencies
|
|
10
|
+
from ..output import private_text_output
|
|
11
|
+
from ..target import TenantTarget
|
|
12
|
+
from .projections import detail, summary
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentReadRefused(ValueError):
|
|
16
|
+
"""A stable refusal safe for the CLI adapter to expose."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _agent_id(value: str) -> str:
|
|
20
|
+
if not isinstance(value, str) or not value.strip():
|
|
21
|
+
raise AgentReadRefused("Explicit agent ID required.")
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_agents(dependencies: AmsDependencies, target: TenantTarget) -> dict:
|
|
26
|
+
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
27
|
+
agents = sdk.ams_admin.list_agents()
|
|
28
|
+
if any(agent.tenant_id != target.tenant_id for agent in agents):
|
|
29
|
+
raise AgentReadRefused("Agent/tenant identity mismatch; stopped.")
|
|
30
|
+
return {
|
|
31
|
+
"environment": target.environment,
|
|
32
|
+
"tenant_id": target.tenant_id,
|
|
33
|
+
"agents": [summary(agent) for agent in agents],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _verified_agent(sdk, target: TenantTarget, agent_id: str):
|
|
38
|
+
agent = sdk.ams_admin.get_agent(agent_id)
|
|
39
|
+
if agent is None or agent.id != agent_id or agent.tenant_id != target.tenant_id:
|
|
40
|
+
raise AgentReadRefused("Agent/tenant identity mismatch; stopped.")
|
|
41
|
+
return agent
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_agent(dependencies: AmsDependencies, target: TenantTarget, agent_id: str) -> dict:
|
|
45
|
+
agent_id = _agent_id(agent_id)
|
|
46
|
+
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
47
|
+
agent = _verified_agent(sdk, target, agent_id)
|
|
48
|
+
return {"environment": target.environment, "tenant_id": target.tenant_id} | detail(agent)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def export_agent(
|
|
52
|
+
dependencies: AmsDependencies,
|
|
53
|
+
target: TenantTarget,
|
|
54
|
+
agent_id: str,
|
|
55
|
+
output: Path,
|
|
56
|
+
export_format: Literal["json", "yaml"],
|
|
57
|
+
) -> dict:
|
|
58
|
+
agent_id = _agent_id(agent_id)
|
|
59
|
+
if export_format not in ("json", "yaml"):
|
|
60
|
+
raise AgentReadRefused("Export format must be json or yaml.")
|
|
61
|
+
with private_text_output(output) as write, dependencies.authenticated_sdk_factory(target) as sdk:
|
|
62
|
+
_verified_agent(sdk, target, agent_id)
|
|
63
|
+
text = sdk.ams_admin.export_agent_configuration(agent_id, format=export_format)
|
|
64
|
+
write(text)
|
|
65
|
+
return {
|
|
66
|
+
"environment": target.environment,
|
|
67
|
+
"tenant_id": target.tenant_id,
|
|
68
|
+
"agent_id": agent_id,
|
|
69
|
+
"path": str(output),
|
|
70
|
+
"format": export_format,
|
|
71
|
+
"sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
|
72
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Explicit bounded projections for agent reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from healthcloud.models.ams_tuning import AgentConfig
|
|
6
|
+
|
|
7
|
+
from ..output import digest
|
|
8
|
+
|
|
9
|
+
ASSIGNED_REFERENCES = ("tool_ids", "skill_ids", "guardrail_ids", "policy_ids", "data_source_ids")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _metadata_string(value: object) -> str:
|
|
13
|
+
return value if isinstance(value, str) and value.strip() else "unknown"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def summary(agent: AgentConfig) -> dict:
|
|
17
|
+
runtime = agent.runtime_configuration if isinstance(agent.runtime_configuration, dict) else {}
|
|
18
|
+
return {
|
|
19
|
+
"id": agent.id,
|
|
20
|
+
"name": agent.name,
|
|
21
|
+
"key": agent.key,
|
|
22
|
+
"type": agent.type,
|
|
23
|
+
"status": agent.status,
|
|
24
|
+
"active": agent.active,
|
|
25
|
+
"config_version": agent.config_version,
|
|
26
|
+
"adapter": _metadata_string(runtime.get("adapter")),
|
|
27
|
+
"runtime": _metadata_string(runtime.get("runtime")),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def detail(agent: AgentConfig) -> dict:
|
|
32
|
+
values = {"instructions": agent.instructions, "greetings": agent.greetings}
|
|
33
|
+
return summary(agent) | {
|
|
34
|
+
"fields": sorted(values),
|
|
35
|
+
"field_hashes": {key: digest(value) for key, value in values.items()},
|
|
36
|
+
"assigned_references": {key: getattr(agent, key) for key in ASSIGNED_REFERENCES},
|
|
37
|
+
}
|
|
@@ -8,17 +8,21 @@ import click
|
|
|
8
8
|
from healthcloud import HCSDK
|
|
9
9
|
from healthcloud_tenantadmin import HCTenantAdminSDK
|
|
10
10
|
|
|
11
|
-
from .target import AmsTarget
|
|
11
|
+
from .target import AmsTarget, TenantTarget
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@contextmanager
|
|
15
|
-
def authenticated_sdk(target: AmsTarget):
|
|
15
|
+
def authenticated_sdk(target: AmsTarget | TenantTarget):
|
|
16
16
|
sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
|
|
17
17
|
try:
|
|
18
18
|
if not hasattr(sdk, "ams_admin"):
|
|
19
19
|
raise click.ClickException(
|
|
20
20
|
"SDK missing ams_admin; use the reviewed local editable SDK checkout (see CLI README)."
|
|
21
21
|
)
|
|
22
|
+
if isinstance(target, TenantTarget) and not hasattr(sdk.ams_admin, "list_agents"):
|
|
23
|
+
raise click.ClickException(
|
|
24
|
+
"SDK missing ams_admin agent listing; use the reviewed local editable SDK checkout (see CLI README)."
|
|
25
|
+
)
|
|
22
26
|
email = os.environ.get("HC_AMS_ADMIN_EMAIL")
|
|
23
27
|
password = os.environ.get("HC_AMS_ADMIN_PASSWORD")
|
|
24
28
|
if not email or not password:
|
|
@@ -4,6 +4,7 @@ from .compatibility import safe_errors
|
|
|
4
4
|
from .group import create_ams_group
|
|
5
5
|
|
|
6
6
|
ams = create_ams_group()
|
|
7
|
+
agents = ams.commands["agents"]
|
|
7
8
|
discover = ams.commands["discover"]
|
|
8
9
|
inspect = ams.commands["inspect"]
|
|
9
10
|
plan = ams.commands["plan"]
|
|
@@ -14,6 +15,7 @@ ui = ams.commands["ui"]
|
|
|
14
15
|
|
|
15
16
|
__all__ = [
|
|
16
17
|
"ams",
|
|
18
|
+
"agents",
|
|
17
19
|
"apply",
|
|
18
20
|
"create_ams_group",
|
|
19
21
|
"discover",
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Typed invocation configuration and deferred AMS operational preflight."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from contextlib import AbstractContextManager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from healthcloud import HCSDK
|
|
14
|
+
|
|
15
|
+
from ..context import CliConfig, resolve_config
|
|
16
|
+
from .target import AmsTarget, TenantTarget, load_target
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class AmsDependencies:
|
|
21
|
+
"""Process-local effect adapters used by one assembled AMS command tree."""
|
|
22
|
+
|
|
23
|
+
public_sdk_factory: Callable[..., Any]
|
|
24
|
+
authenticated_sdk_factory: Callable[[AmsTarget | TenantTarget], AbstractContextManager[Any]]
|
|
25
|
+
process_runner: Callable[..., Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class AmsContext:
|
|
30
|
+
"""Configuration recorded for one AMS command invocation."""
|
|
31
|
+
|
|
32
|
+
root_config: CliConfig
|
|
33
|
+
target_file: Path | None
|
|
34
|
+
dependencies: AmsDependencies
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
pass_ams = click.make_pass_decorator(AmsContext, ensure=False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _check_root(invocation: AmsContext) -> CliConfig:
|
|
41
|
+
cfg = invocation.root_config
|
|
42
|
+
if cfg.access_token is not None or cfg.base_url is not None:
|
|
43
|
+
raise click.ClickException("AMS refuses root MCP credentials and base URL settings.")
|
|
44
|
+
try:
|
|
45
|
+
resolved = resolve_config(
|
|
46
|
+
cfg.access_token,
|
|
47
|
+
cfg.base_url,
|
|
48
|
+
cfg.profile,
|
|
49
|
+
cfg.output,
|
|
50
|
+
False,
|
|
51
|
+
cfg.verbose,
|
|
52
|
+
)
|
|
53
|
+
except click.UsageError, OSError, ValueError, TypeError:
|
|
54
|
+
raise click.ClickException("AMS root profile is unavailable or invalid; use an explicit target.") from None
|
|
55
|
+
if resolved.access_token is not None or resolved.base_url is not None:
|
|
56
|
+
raise click.ClickException("AMS refuses root MCP credentials and base URL settings.")
|
|
57
|
+
if resolved.output == "raw":
|
|
58
|
+
raise click.ClickException("Raw output is refused for AMS.")
|
|
59
|
+
return resolved
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def preflight_discovery(invocation: AmsContext) -> None:
|
|
63
|
+
"""Validate root options for discovery without loading a target."""
|
|
64
|
+
|
|
65
|
+
_check_root(invocation)
|
|
66
|
+
if invocation.target_file is not None:
|
|
67
|
+
raise click.ClickException("Discover requires explicit environment/tenant flags, without a target file.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def preflight_agents(invocation: AmsContext) -> None:
|
|
71
|
+
"""Validate root options without loading a tuning target."""
|
|
72
|
+
|
|
73
|
+
_check_root(invocation)
|
|
74
|
+
if invocation.target_file is not None:
|
|
75
|
+
raise click.ClickException("Agents requires explicit environment/tenant flags, without a target file.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def preflight_departments(invocation: AmsContext, target: TenantTarget) -> None:
|
|
79
|
+
"""Validate root options without loading a tuning target."""
|
|
80
|
+
|
|
81
|
+
_check_root(invocation)
|
|
82
|
+
if invocation.target_file is not None:
|
|
83
|
+
raise click.ClickException("Departments requires explicit environment/tenant flags, without a target file.")
|
|
84
|
+
sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
|
|
85
|
+
try:
|
|
86
|
+
resource = getattr(getattr(sdk, "ams_admin", None), "departments", None)
|
|
87
|
+
if (
|
|
88
|
+
resource is None
|
|
89
|
+
or not callable(getattr(resource, "list", None))
|
|
90
|
+
or not callable(getattr(resource, "get", None))
|
|
91
|
+
):
|
|
92
|
+
raise click.ClickException(
|
|
93
|
+
"SDK missing ams_admin departments; use the reviewed SDK version required by this CLI."
|
|
94
|
+
)
|
|
95
|
+
finally:
|
|
96
|
+
sdk.close()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def preflight_teams(invocation: AmsContext, target: TenantTarget) -> None:
|
|
100
|
+
"""Validate local team-read compatibility before login or data reads."""
|
|
101
|
+
|
|
102
|
+
_check_root(invocation)
|
|
103
|
+
if invocation.target_file is not None:
|
|
104
|
+
raise click.ClickException("Teams requires explicit environment/tenant flags, without a target file.")
|
|
105
|
+
sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
|
|
106
|
+
try:
|
|
107
|
+
resource = getattr(getattr(sdk, "ams_admin", None), "teams", None)
|
|
108
|
+
if (
|
|
109
|
+
resource is None
|
|
110
|
+
or not callable(getattr(resource, "list", None))
|
|
111
|
+
or not callable(getattr(resource, "get", None))
|
|
112
|
+
):
|
|
113
|
+
raise click.ClickException(
|
|
114
|
+
"SDK missing ams_admin teams; use the reviewed SDK version required by this CLI."
|
|
115
|
+
)
|
|
116
|
+
finally:
|
|
117
|
+
sdk.close()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def preflight_organization(invocation: AmsContext, target: TenantTarget) -> None:
|
|
121
|
+
"""Validate local hierarchy-read compatibility before login or data reads."""
|
|
122
|
+
|
|
123
|
+
_check_root(invocation)
|
|
124
|
+
if invocation.target_file is not None:
|
|
125
|
+
raise click.ClickException("Organization requires explicit environment/tenant flags, without a target file.")
|
|
126
|
+
sdk = HCSDK(environment=target.environment, tenant_id=target.tenant_id)
|
|
127
|
+
try:
|
|
128
|
+
resource = getattr(getattr(sdk, "ams_admin", None), "organization", None)
|
|
129
|
+
if resource is None or not callable(getattr(resource, "get_hierarchy", None)):
|
|
130
|
+
raise click.ClickException(
|
|
131
|
+
"SDK missing ams_admin organization; use the reviewed SDK version required by this CLI."
|
|
132
|
+
)
|
|
133
|
+
finally:
|
|
134
|
+
sdk.close()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def require_target(invocation: AmsContext) -> AmsTarget:
|
|
138
|
+
"""Validate root options and load the explicitly selected AMS target."""
|
|
139
|
+
|
|
140
|
+
_check_root(invocation)
|
|
141
|
+
if invocation.target_file is None:
|
|
142
|
+
raise click.ClickException("AMS requires an explicit --target-file.")
|
|
143
|
+
return load_target(invocation.target_file)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Fresh Click adapters for department reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..context import AmsDependencies, pass_ams, preflight_departments
|
|
8
|
+
from ..errors import run_domain
|
|
9
|
+
from ..target import TenantTarget
|
|
10
|
+
from .operations import get_department, list_departments
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _environment(function):
|
|
14
|
+
return click.option("--environment", type=click.Choice(["dev", "uat", "prod"]), required=True)(function)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _tenant(function):
|
|
18
|
+
return click.option("--tenant-id", required=True)(function)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_list_command(dependencies: AmsDependencies) -> click.Command:
|
|
22
|
+
@click.command("list")
|
|
23
|
+
@_environment
|
|
24
|
+
@_tenant
|
|
25
|
+
@pass_ams
|
|
26
|
+
def command(invocation, environment: str, tenant_id: str) -> None:
|
|
27
|
+
target_data = {"environment": environment, "tenant_id": tenant_id}
|
|
28
|
+
|
|
29
|
+
def execute():
|
|
30
|
+
target = TenantTarget(environment, tenant_id)
|
|
31
|
+
preflight_departments(invocation, target)
|
|
32
|
+
return list_departments(dependencies, target)
|
|
33
|
+
|
|
34
|
+
run_domain("departments.list", target_data, execute)
|
|
35
|
+
|
|
36
|
+
return command
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_get_command(dependencies: AmsDependencies) -> click.Command:
|
|
40
|
+
@click.command("get")
|
|
41
|
+
@_environment
|
|
42
|
+
@_tenant
|
|
43
|
+
@click.option("--department-id", required=True)
|
|
44
|
+
@pass_ams
|
|
45
|
+
def command(invocation, environment: str, tenant_id: str, department_id: str) -> None:
|
|
46
|
+
target_data = {
|
|
47
|
+
"environment": environment,
|
|
48
|
+
"tenant_id": tenant_id,
|
|
49
|
+
"department_id": department_id,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def execute():
|
|
53
|
+
target = TenantTarget(environment, tenant_id)
|
|
54
|
+
preflight_departments(invocation, target)
|
|
55
|
+
return get_department(dependencies, target, department_id)
|
|
56
|
+
|
|
57
|
+
run_domain("departments.get", target_data, execute)
|
|
58
|
+
|
|
59
|
+
return command
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Fresh department-domain group assembly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..context import AmsDependencies
|
|
8
|
+
from .commands import create_get_command, create_list_command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_departments_group(dependencies: AmsDependencies) -> click.Group:
|
|
12
|
+
@click.group("departments")
|
|
13
|
+
def group() -> None:
|
|
14
|
+
"""Read-only department listing and inspection for one explicit tenant."""
|
|
15
|
+
|
|
16
|
+
for factory in (create_list_command, create_get_command):
|
|
17
|
+
group.add_command(factory(dependencies))
|
|
18
|
+
return group
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Click-free orchestration for department list and get."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..context import AmsDependencies
|
|
6
|
+
from ..domain_errors import DomainReadRefused
|
|
7
|
+
from ..results import succeeded
|
|
8
|
+
from ..target import TenantTarget
|
|
9
|
+
from .projections import department_projection
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DepartmentReadRefused(DomainReadRefused):
|
|
13
|
+
"""A categorized department refusal safe to expose in a bounded error."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _department_id(value: str) -> str:
|
|
19
|
+
if not isinstance(value, str) or not value.strip():
|
|
20
|
+
raise DepartmentReadRefused("validation", "Explicit department ID required.")
|
|
21
|
+
if value in {".", ".."}:
|
|
22
|
+
raise DepartmentReadRefused("validation", "Department ID must not be a dot segment.")
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resource(sdk):
|
|
27
|
+
admin = getattr(sdk, "ams_admin", None)
|
|
28
|
+
resource = getattr(admin, "departments", None)
|
|
29
|
+
if (
|
|
30
|
+
resource is None
|
|
31
|
+
or not callable(getattr(resource, "list", None))
|
|
32
|
+
or not callable(getattr(resource, "get", None))
|
|
33
|
+
):
|
|
34
|
+
raise DepartmentReadRefused("unsupported", "Installed SDK does not support department reads.")
|
|
35
|
+
return resource
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _target(target: TenantTarget, department_id: str | None = None) -> dict:
|
|
39
|
+
value = {"environment": target.environment, "tenant_id": target.tenant_id}
|
|
40
|
+
if department_id is not None:
|
|
41
|
+
value["department_id"] = department_id
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def list_departments(dependencies: AmsDependencies, target: TenantTarget) -> dict:
|
|
46
|
+
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
47
|
+
departments = _resource(sdk).list()
|
|
48
|
+
if any(department.tenant_id != target.tenant_id for department in departments):
|
|
49
|
+
raise DepartmentReadRefused("validation", "Department/tenant identity mismatch; stopped.")
|
|
50
|
+
projections = [department_projection(department) for department in departments]
|
|
51
|
+
return succeeded(
|
|
52
|
+
"departments.list",
|
|
53
|
+
_target(target),
|
|
54
|
+
{
|
|
55
|
+
"departments": projections,
|
|
56
|
+
"page": {"kind": "unpaged", "returned_count": len(projections), "continuation": None},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_department(dependencies: AmsDependencies, target: TenantTarget, department_id: str) -> dict:
|
|
62
|
+
department_id = _department_id(department_id)
|
|
63
|
+
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
64
|
+
department = _resource(sdk).get(department_id)
|
|
65
|
+
if department is None or department.id != department_id or department.tenant_id != target.tenant_id:
|
|
66
|
+
raise DepartmentReadRefused("validation", "Department/tenant identity mismatch; stopped.")
|
|
67
|
+
return succeeded(
|
|
68
|
+
"departments.get",
|
|
69
|
+
_target(target, department_id),
|
|
70
|
+
{"department": department_projection(department)},
|
|
71
|
+
)
|