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.
Files changed (53) hide show
  1. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/PKG-INFO +49 -1
  2. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/README.md +48 -0
  3. healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/__init__.py +5 -0
  4. healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/commands.py +83 -0
  5. healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/group.py +18 -0
  6. healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/operations.py +72 -0
  7. healthcloud_cli-3.3.0/healthcloud_cli/ams/agents/projections.py +37 -0
  8. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/auth.py +6 -2
  9. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/commands.py +2 -0
  10. healthcloud_cli-3.3.0/healthcloud_cli/ams/context.py +143 -0
  11. healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/__init__.py +5 -0
  12. healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/commands.py +59 -0
  13. healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/group.py +18 -0
  14. healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/operations.py +71 -0
  15. healthcloud_cli-3.3.0/healthcloud_cli/ams/departments/projections.py +19 -0
  16. healthcloud_cli-3.3.0/healthcloud_cli/ams/domain_errors.py +9 -0
  17. healthcloud_cli-3.3.0/healthcloud_cli/ams/errors.py +87 -0
  18. healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/__init__.py +5 -0
  19. healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/commands.py +28 -0
  20. healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/group.py +17 -0
  21. healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/operations.py +33 -0
  22. healthcloud_cli-3.3.0/healthcloud_cli/ams/organization/projections.py +29 -0
  23. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/output.py +17 -3
  24. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/registry.py +8 -0
  25. healthcloud_cli-3.3.0/healthcloud_cli/ams/results.py +31 -0
  26. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/target.py +14 -0
  27. healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/__init__.py +5 -0
  28. healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/commands.py +55 -0
  29. healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/group.py +18 -0
  30. healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/operations.py +67 -0
  31. healthcloud_cli-3.3.0/healthcloud_cli/ams/teams/projections.py +21 -0
  32. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/pyproject.toml +1 -1
  33. healthcloud_cli-3.2.5/healthcloud_cli/ams/context.py +0 -74
  34. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/.gitignore +0 -0
  35. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/hatch_build.py +0 -0
  36. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/__init__.py +0 -0
  37. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/__init__.py +0 -0
  38. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/compatibility.py +0 -0
  39. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/group.py +0 -0
  40. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/tuning.py +0 -0
  41. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/ui.py +0 -0
  42. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/ams/windows_files.py +0 -0
  43. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/cli.py +0 -0
  44. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/context.py +0 -0
  45. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/contracts/__init__.py +0 -0
  46. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
  47. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/main.py +0 -0
  48. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/manifest.py +0 -0
  49. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/output.py +0 -0
  50. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/params.py +0 -0
  51. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/healthcloud_cli/util.py +0 -0
  52. {healthcloud_cli-3.2.5 → healthcloud_cli-3.3.0}/scripts/__init__.py +0 -0
  53. {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.2.5
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,5 @@
1
+ """Read-only AMS agent command domain."""
2
+
3
+ from .group import create_agents_group
4
+
5
+ __all__ = ["create_agents_group"]
@@ -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,5 @@
1
+ """Read-only AMS department CLI domain."""
2
+
3
+ from .group import create_departments_group
4
+
5
+ __all__ = ["create_departments_group"]
@@ -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
+ )