healthcloud-cli 3.6.0__tar.gz → 3.7.1__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.6.0 → healthcloud_cli-3.7.1}/.gitignore +2 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/PKG-INFO +84 -21
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/README.md +83 -20
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/departments/operations.py +74 -5
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/output.py +56 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/policies/commands.py +29 -5
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/policies/operations.py +110 -6
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/results.py +53 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/sessions/commands.py +30 -4
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/sessions/operations.py +32 -7
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/skills/operations.py +58 -4
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/teams/operations.py +58 -4
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/contracts/mcp-tools.manifest.json +207 -3
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/pyproject.toml +1 -1
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/hatch_build.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/admins/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/admins/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/admins/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/admins/operations.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/agents/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/agents/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/agents/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/agents/operations.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/agents/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/auth.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/compatibility.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/context.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/data_sources/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/data_sources/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/data_sources/operations.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/data_sources/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/departments/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/departments/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/departments/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/departments/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/domain_errors.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/errors.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/guardrails/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/guardrails/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/guardrails/operations.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/guardrails/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/organization/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/organization/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/organization/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/organization/operations.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/organization/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/policies/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/policies/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/policies/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/pretty.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/registry.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/session.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/sessions/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/sessions/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/sessions/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/skills/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/skills/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/skills/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/skills/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/target.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/teams/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/teams/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/teams/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/teams/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/tools/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/tools/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/tools/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/tools/operations.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/tools/projections.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/tuning.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/ui.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/windows_files.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/cli.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/context.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/contracts/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/main.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/manifest.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/output.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/params.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/tenants/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/tenants/commands.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/tenants/group.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/tenants/operations.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/util.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/scripts/__init__.py +0 -0
- {healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/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.7.1
|
|
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
|
|
@@ -751,10 +751,14 @@ uv run --frozen hc ams departments delete --environment dev --tenant-id TENANT_I
|
|
|
751
751
|
reports a `validation` refusal that names the department, the tenant that
|
|
752
752
|
received it, and tells you to run `hc ams departments get` — it never claims
|
|
753
753
|
a landed write was a stopped no-op.
|
|
754
|
-
- If the transport times out
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
754
|
+
- If the transport times out, the connection drops mid-write, or the server
|
|
755
|
+
answers an HTTP 5xx/408 before a readable result, the write's outcome is
|
|
756
|
+
unknown: the CLI exits 1 with an `unknown` envelope
|
|
757
|
+
(`verification.saved_state: unknown`, `verification.side_effects:
|
|
758
|
+
unverified`, `data.mutation_replayed: false`, and
|
|
759
|
+
`data.reconciliation.status: not_read`) whose message says the outcome is
|
|
760
|
+
unknown and to check `hc ams departments get` before retrying. It never
|
|
761
|
+
re-sends the write and never re-reads on its own.
|
|
758
762
|
|
|
759
763
|
Read-only team listing and inspection are also complete and unpaged. The bounded
|
|
760
764
|
projection omits descriptions and creator identities; it includes evaluation-suite
|
|
@@ -797,10 +801,14 @@ uv run --frozen hc ams teams delete --environment dev --tenant-id TENANT_ID --
|
|
|
797
801
|
`validation` refusal that reports the landed write truthfully: it names the
|
|
798
802
|
team and the tenant that actually received it and points to `hc ams teams
|
|
799
803
|
get` — it never claims a landed write was stopped.
|
|
800
|
-
- If the transport times out
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
+
- If the transport times out, the connection drops mid-write, or the server
|
|
805
|
+
answers an HTTP 5xx/408 before a readable result, the write's outcome is
|
|
806
|
+
unknown: the CLI exits 1 with an `unknown` envelope
|
|
807
|
+
(`verification.saved_state: unknown`, `verification.side_effects:
|
|
808
|
+
unverified`, `data.mutation_replayed: false`, and
|
|
809
|
+
`data.reconciliation.status: not_read`) whose message says the outcome is
|
|
810
|
+
unknown and to check `hc ams teams get` before retrying. It never re-sends
|
|
811
|
+
the write and never re-reads on its own.
|
|
804
812
|
|
|
805
813
|
The organization hierarchy is the backend-built department/team/agent tree. The
|
|
806
814
|
CLI preserves its order and nesting without issuing entity reads, joining records,
|
|
@@ -862,10 +870,14 @@ uv run --frozen hc ams skills delete --environment dev --tenant-id TENANT_ID --s
|
|
|
862
870
|
landed write truthfully: it names the skill and the tenant that actually
|
|
863
871
|
received it and points to `hc ams skills get` — it never claims a landed
|
|
864
872
|
write was stopped.
|
|
865
|
-
- If the transport times out
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
873
|
+
- If the transport times out, the connection drops mid-write, or the server
|
|
874
|
+
answers an HTTP 5xx/408 before a readable result, the write's outcome is
|
|
875
|
+
unknown: the CLI exits 1 with an `unknown` envelope
|
|
876
|
+
(`verification.saved_state: unknown`, `verification.side_effects:
|
|
877
|
+
unverified`, `data.mutation_replayed: false`, and
|
|
878
|
+
`data.reconciliation.status: not_read`) whose message says the outcome is
|
|
879
|
+
unknown and to check `hc ams skills get` before retrying. It never re-sends
|
|
880
|
+
the write and never re-reads on its own.
|
|
869
881
|
|
|
870
882
|
Read-only policy catalog listing and inspection are complete and unpaged. The
|
|
871
883
|
bounded projection includes only policy IDs, names, department IDs, guardrail
|
|
@@ -897,8 +909,8 @@ Policies have an `active` lifecycle flag, so `activate` and `deactivate`
|
|
|
897
909
|
commands exist (they PUT the `active` field):
|
|
898
910
|
|
|
899
911
|
```bash
|
|
900
|
-
uv run --frozen hc ams policies create --environment dev --tenant-id TENANT_ID --name "Appointment Safety" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID [--description TEXT] [--content TEXT] [--guardrail-ids GUARDRAIL_ID ...]
|
|
901
|
-
uv run --frozen hc ams policies update --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID [--name NAME] [--description TEXT] [--content TEXT] [--guardrail-ids GUARDRAIL_ID ... | --clear-guardrail-ids]
|
|
912
|
+
uv run --frozen hc ams policies create --environment dev --tenant-id TENANT_ID --name "Appointment Safety" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID [--description TEXT] [--content TEXT | --content-file PATH] [--guardrail-ids GUARDRAIL_ID ...]
|
|
913
|
+
uv run --frozen hc ams policies update --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID [--name NAME] [--description TEXT] [--content TEXT | --content-file PATH] [--guardrail-ids GUARDRAIL_ID ... | --clear-guardrail-ids]
|
|
902
914
|
uv run --frozen hc ams policies activate --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
|
|
903
915
|
uv run --frozen hc ams policies deactivate --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
|
|
904
916
|
uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
|
|
@@ -911,6 +923,29 @@ uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID -
|
|
|
911
923
|
server does not validate that `--guardrail-ids` reference real guardrails
|
|
912
924
|
(blank guardrail IDs are rejected client-side). `--content` is write-only:
|
|
913
925
|
it is sent but never echoed back.
|
|
926
|
+
- The protected policy content can also arrive from an owner-only file, which
|
|
927
|
+
keeps it out of the command line entirely (argv, the process table under
|
|
928
|
+
`ps`/`/proc/*/cmdline`, shell history and CI logs all see only the path):
|
|
929
|
+
|
|
930
|
+
```bash
|
|
931
|
+
uv run --frozen hc ams policies create --environment dev --tenant-id TENANT_ID --name "Appointment Safety" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID --content-file PATH
|
|
932
|
+
uv run --frozen hc ams policies update --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID --content-file PATH
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
The file is read locally, before the login check and any request, by the same
|
|
936
|
+
reader the sessions commands use: it must be a readable owner-only regular
|
|
937
|
+
text file (no symlinks, no directories, not group- or world-readable) and
|
|
938
|
+
must hold 1-20000 characters. Its content is never printed, and an invalid,
|
|
939
|
+
empty or oversized file is refused locally with a message that names the
|
|
940
|
+
discipline, never the text. The output is identical to the inline form:
|
|
941
|
+
`policies.create` / `policies.update` with `data.policy`.
|
|
942
|
+
- Exactly one content source is accepted per write: passing `--content` with
|
|
943
|
+
`--content-file` is a usage error (exit 2) that sends nothing. The inline
|
|
944
|
+
`--content` flag remains supported for convenience and remains exposed in
|
|
945
|
+
argv, the process table and shell history — use the file channel for
|
|
946
|
+
anything sensitive. An empty `--content ""` on `update` is still the
|
|
947
|
+
documented clear; an empty `--content-file` is a `validation` refusal, so a
|
|
948
|
+
truncated or misdirected file can never clear protected content by accident.
|
|
914
949
|
- `update` sends only the options you pass (an empty `--description ""` or
|
|
915
950
|
`--content ""` clears it; `--clear-guardrail-ids` sends an explicit empty
|
|
916
951
|
list to clear the policy's guardrail bindings); it refuses with exit 1
|
|
@@ -931,10 +966,14 @@ uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID -
|
|
|
931
966
|
landed write truthfully: it names the policy and the tenant that actually
|
|
932
967
|
received it and points to `hc ams policies get` — it never claims a landed
|
|
933
968
|
write was stopped.
|
|
934
|
-
- If the transport times out
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
969
|
+
- If the transport times out, the connection drops mid-write, or the server
|
|
970
|
+
answers an HTTP 5xx/408 before a readable result, the write's outcome is
|
|
971
|
+
unknown: the CLI exits 1 with an `unknown` envelope
|
|
972
|
+
(`verification.saved_state: unknown`, `verification.side_effects:
|
|
973
|
+
unverified`, `data.mutation_replayed: false`, and
|
|
974
|
+
`data.reconciliation.status: not_read`) whose message says the outcome is
|
|
975
|
+
unknown and to check `hc ams policies get` before retrying. It never
|
|
976
|
+
re-sends the write and never re-reads on its own.
|
|
938
977
|
|
|
939
978
|
Governed authoring sessions and their messages are first-class commands over the
|
|
940
979
|
VibeCoder session routes. Creating a session and appending a message make no model
|
|
@@ -947,9 +986,9 @@ is refused locally:
|
|
|
947
986
|
```bash
|
|
948
987
|
uv run --frozen hc ams sessions list --environment dev --tenant-id TENANT_ID [--limit N]
|
|
949
988
|
uv run --frozen hc ams sessions get --environment dev --tenant-id TENANT_ID --session-id SESSION_ID
|
|
950
|
-
uv run --frozen hc ams sessions create --environment dev --tenant-id TENANT_ID [--agent-id AGENT_ID] [--mode plan|implement] [--initial-prompt TEXT]
|
|
989
|
+
uv run --frozen hc ams sessions create --environment dev --tenant-id TENANT_ID [--agent-id AGENT_ID] [--mode plan|implement] [--initial-prompt TEXT | --initial-prompt-file PATH]
|
|
951
990
|
uv run --frozen hc ams sessions messages list --environment dev --tenant-id TENANT_ID --session-id SESSION_ID [--limit N]
|
|
952
|
-
uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENANT_ID --session-id SESSION_ID --content TEXT [--mode plan|implement] [--idempotency-key KEY]
|
|
991
|
+
uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENANT_ID --session-id SESSION_ID [--content TEXT | --content-file PATH] [--mode plan|implement] [--idempotency-key KEY]
|
|
953
992
|
```
|
|
954
993
|
|
|
955
994
|
- `list` emits `operation: "sessions.list"`, `data.sessions` (bounded projections
|
|
@@ -966,6 +1005,30 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
|
|
|
966
1005
|
mode, plan ID, model, timestamps) plus `data.idempotency_key` when a key was
|
|
967
1006
|
supplied. Protected free text — `--initial-prompt` and `--content` — is sent to
|
|
968
1007
|
the server but never appears in the envelope, a refusal, a diagnostic or a log.
|
|
1008
|
+
- The protected free text can also arrive from an owner-only file, which keeps it
|
|
1009
|
+
out of the command line entirely (argv, the process table under
|
|
1010
|
+
`ps`/`/proc/*/cmdline`, shell history and CI logs all see only the path):
|
|
1011
|
+
|
|
1012
|
+
```bash
|
|
1013
|
+
uv run --frozen hc ams sessions create --environment dev --tenant-id TENANT_ID --initial-prompt-file PATH
|
|
1014
|
+
uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENANT_ID --session-id SESSION_ID --content-file PATH
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
The file is read locally, before the login check and any request, by the same
|
|
1018
|
+
reader `hc ams sessions plans generate --prompt-file` uses: it must be a
|
|
1019
|
+
readable owner-only regular text file (no symlinks, no directories, not
|
|
1020
|
+
group- or world-readable), and must hold 1-20000 characters — the same bound
|
|
1021
|
+
the inline flag is now held to, so neither channel can carry an empty or
|
|
1022
|
+
over-long value past the local check. Its content is never printed, and an
|
|
1023
|
+
invalid, empty or oversized file is refused locally with a message that
|
|
1024
|
+
names the discipline, never the text. The output is identical to the inline form: `sessions.create`
|
|
1025
|
+
with `data.session`, `sessions.messages.add` with `data.message`.
|
|
1026
|
+
- Exactly one source is accepted per field. Passing `--initial-prompt` with
|
|
1027
|
+
`--initial-prompt-file`, or `--content` with `--content-file`, is a usage
|
|
1028
|
+
error (exit 2) that sends nothing; `messages add` with neither is the same
|
|
1029
|
+
usage error. The inline `--initial-prompt` and `--content` flags remain
|
|
1030
|
+
supported for convenience, and remain exposed in argv, the process table and
|
|
1031
|
+
shell history — use the file channel for anything sensitive.
|
|
969
1032
|
- `--initial-prompt` becomes the session's first `user` message in the same
|
|
970
1033
|
server transaction; `hc ams sessions messages list` shows it afterwards.
|
|
971
1034
|
- `--mode implement` is not available from the server yet: `create` reports one
|
|
@@ -733,10 +733,14 @@ uv run --frozen hc ams departments delete --environment dev --tenant-id TENANT_I
|
|
|
733
733
|
reports a `validation` refusal that names the department, the tenant that
|
|
734
734
|
received it, and tells you to run `hc ams departments get` — it never claims
|
|
735
735
|
a landed write was a stopped no-op.
|
|
736
|
-
- If the transport times out
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
736
|
+
- If the transport times out, the connection drops mid-write, or the server
|
|
737
|
+
answers an HTTP 5xx/408 before a readable result, the write's outcome is
|
|
738
|
+
unknown: the CLI exits 1 with an `unknown` envelope
|
|
739
|
+
(`verification.saved_state: unknown`, `verification.side_effects:
|
|
740
|
+
unverified`, `data.mutation_replayed: false`, and
|
|
741
|
+
`data.reconciliation.status: not_read`) whose message says the outcome is
|
|
742
|
+
unknown and to check `hc ams departments get` before retrying. It never
|
|
743
|
+
re-sends the write and never re-reads on its own.
|
|
740
744
|
|
|
741
745
|
Read-only team listing and inspection are also complete and unpaged. The bounded
|
|
742
746
|
projection omits descriptions and creator identities; it includes evaluation-suite
|
|
@@ -779,10 +783,14 @@ uv run --frozen hc ams teams delete --environment dev --tenant-id TENANT_ID --
|
|
|
779
783
|
`validation` refusal that reports the landed write truthfully: it names the
|
|
780
784
|
team and the tenant that actually received it and points to `hc ams teams
|
|
781
785
|
get` — it never claims a landed write was stopped.
|
|
782
|
-
- If the transport times out
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
+
- If the transport times out, the connection drops mid-write, or the server
|
|
787
|
+
answers an HTTP 5xx/408 before a readable result, the write's outcome is
|
|
788
|
+
unknown: the CLI exits 1 with an `unknown` envelope
|
|
789
|
+
(`verification.saved_state: unknown`, `verification.side_effects:
|
|
790
|
+
unverified`, `data.mutation_replayed: false`, and
|
|
791
|
+
`data.reconciliation.status: not_read`) whose message says the outcome is
|
|
792
|
+
unknown and to check `hc ams teams get` before retrying. It never re-sends
|
|
793
|
+
the write and never re-reads on its own.
|
|
786
794
|
|
|
787
795
|
The organization hierarchy is the backend-built department/team/agent tree. The
|
|
788
796
|
CLI preserves its order and nesting without issuing entity reads, joining records,
|
|
@@ -844,10 +852,14 @@ uv run --frozen hc ams skills delete --environment dev --tenant-id TENANT_ID --s
|
|
|
844
852
|
landed write truthfully: it names the skill and the tenant that actually
|
|
845
853
|
received it and points to `hc ams skills get` — it never claims a landed
|
|
846
854
|
write was stopped.
|
|
847
|
-
- If the transport times out
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
855
|
+
- If the transport times out, the connection drops mid-write, or the server
|
|
856
|
+
answers an HTTP 5xx/408 before a readable result, the write's outcome is
|
|
857
|
+
unknown: the CLI exits 1 with an `unknown` envelope
|
|
858
|
+
(`verification.saved_state: unknown`, `verification.side_effects:
|
|
859
|
+
unverified`, `data.mutation_replayed: false`, and
|
|
860
|
+
`data.reconciliation.status: not_read`) whose message says the outcome is
|
|
861
|
+
unknown and to check `hc ams skills get` before retrying. It never re-sends
|
|
862
|
+
the write and never re-reads on its own.
|
|
851
863
|
|
|
852
864
|
Read-only policy catalog listing and inspection are complete and unpaged. The
|
|
853
865
|
bounded projection includes only policy IDs, names, department IDs, guardrail
|
|
@@ -879,8 +891,8 @@ Policies have an `active` lifecycle flag, so `activate` and `deactivate`
|
|
|
879
891
|
commands exist (they PUT the `active` field):
|
|
880
892
|
|
|
881
893
|
```bash
|
|
882
|
-
uv run --frozen hc ams policies create --environment dev --tenant-id TENANT_ID --name "Appointment Safety" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID [--description TEXT] [--content TEXT] [--guardrail-ids GUARDRAIL_ID ...]
|
|
883
|
-
uv run --frozen hc ams policies update --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID [--name NAME] [--description TEXT] [--content TEXT] [--guardrail-ids GUARDRAIL_ID ... | --clear-guardrail-ids]
|
|
894
|
+
uv run --frozen hc ams policies create --environment dev --tenant-id TENANT_ID --name "Appointment Safety" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID [--description TEXT] [--content TEXT | --content-file PATH] [--guardrail-ids GUARDRAIL_ID ...]
|
|
895
|
+
uv run --frozen hc ams policies update --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID [--name NAME] [--description TEXT] [--content TEXT | --content-file PATH] [--guardrail-ids GUARDRAIL_ID ... | --clear-guardrail-ids]
|
|
884
896
|
uv run --frozen hc ams policies activate --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
|
|
885
897
|
uv run --frozen hc ams policies deactivate --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
|
|
886
898
|
uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID
|
|
@@ -893,6 +905,29 @@ uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID -
|
|
|
893
905
|
server does not validate that `--guardrail-ids` reference real guardrails
|
|
894
906
|
(blank guardrail IDs are rejected client-side). `--content` is write-only:
|
|
895
907
|
it is sent but never echoed back.
|
|
908
|
+
- The protected policy content can also arrive from an owner-only file, which
|
|
909
|
+
keeps it out of the command line entirely (argv, the process table under
|
|
910
|
+
`ps`/`/proc/*/cmdline`, shell history and CI logs all see only the path):
|
|
911
|
+
|
|
912
|
+
```bash
|
|
913
|
+
uv run --frozen hc ams policies create --environment dev --tenant-id TENANT_ID --name "Appointment Safety" --department-id DEPARTMENT_ID --created-by-user-id ADMIN_USER_ID --content-file PATH
|
|
914
|
+
uv run --frozen hc ams policies update --environment dev --tenant-id TENANT_ID --policy-id POLICY_ID --content-file PATH
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
The file is read locally, before the login check and any request, by the same
|
|
918
|
+
reader the sessions commands use: it must be a readable owner-only regular
|
|
919
|
+
text file (no symlinks, no directories, not group- or world-readable) and
|
|
920
|
+
must hold 1-20000 characters. Its content is never printed, and an invalid,
|
|
921
|
+
empty or oversized file is refused locally with a message that names the
|
|
922
|
+
discipline, never the text. The output is identical to the inline form:
|
|
923
|
+
`policies.create` / `policies.update` with `data.policy`.
|
|
924
|
+
- Exactly one content source is accepted per write: passing `--content` with
|
|
925
|
+
`--content-file` is a usage error (exit 2) that sends nothing. The inline
|
|
926
|
+
`--content` flag remains supported for convenience and remains exposed in
|
|
927
|
+
argv, the process table and shell history — use the file channel for
|
|
928
|
+
anything sensitive. An empty `--content ""` on `update` is still the
|
|
929
|
+
documented clear; an empty `--content-file` is a `validation` refusal, so a
|
|
930
|
+
truncated or misdirected file can never clear protected content by accident.
|
|
896
931
|
- `update` sends only the options you pass (an empty `--description ""` or
|
|
897
932
|
`--content ""` clears it; `--clear-guardrail-ids` sends an explicit empty
|
|
898
933
|
list to clear the policy's guardrail bindings); it refuses with exit 1
|
|
@@ -913,10 +948,14 @@ uv run --frozen hc ams policies delete --environment dev --tenant-id TENANT_ID -
|
|
|
913
948
|
landed write truthfully: it names the policy and the tenant that actually
|
|
914
949
|
received it and points to `hc ams policies get` — it never claims a landed
|
|
915
950
|
write was stopped.
|
|
916
|
-
- If the transport times out
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
951
|
+
- If the transport times out, the connection drops mid-write, or the server
|
|
952
|
+
answers an HTTP 5xx/408 before a readable result, the write's outcome is
|
|
953
|
+
unknown: the CLI exits 1 with an `unknown` envelope
|
|
954
|
+
(`verification.saved_state: unknown`, `verification.side_effects:
|
|
955
|
+
unverified`, `data.mutation_replayed: false`, and
|
|
956
|
+
`data.reconciliation.status: not_read`) whose message says the outcome is
|
|
957
|
+
unknown and to check `hc ams policies get` before retrying. It never
|
|
958
|
+
re-sends the write and never re-reads on its own.
|
|
920
959
|
|
|
921
960
|
Governed authoring sessions and their messages are first-class commands over the
|
|
922
961
|
VibeCoder session routes. Creating a session and appending a message make no model
|
|
@@ -929,9 +968,9 @@ is refused locally:
|
|
|
929
968
|
```bash
|
|
930
969
|
uv run --frozen hc ams sessions list --environment dev --tenant-id TENANT_ID [--limit N]
|
|
931
970
|
uv run --frozen hc ams sessions get --environment dev --tenant-id TENANT_ID --session-id SESSION_ID
|
|
932
|
-
uv run --frozen hc ams sessions create --environment dev --tenant-id TENANT_ID [--agent-id AGENT_ID] [--mode plan|implement] [--initial-prompt TEXT]
|
|
971
|
+
uv run --frozen hc ams sessions create --environment dev --tenant-id TENANT_ID [--agent-id AGENT_ID] [--mode plan|implement] [--initial-prompt TEXT | --initial-prompt-file PATH]
|
|
933
972
|
uv run --frozen hc ams sessions messages list --environment dev --tenant-id TENANT_ID --session-id SESSION_ID [--limit N]
|
|
934
|
-
uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENANT_ID --session-id SESSION_ID --content TEXT [--mode plan|implement] [--idempotency-key KEY]
|
|
973
|
+
uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENANT_ID --session-id SESSION_ID [--content TEXT | --content-file PATH] [--mode plan|implement] [--idempotency-key KEY]
|
|
935
974
|
```
|
|
936
975
|
|
|
937
976
|
- `list` emits `operation: "sessions.list"`, `data.sessions` (bounded projections
|
|
@@ -948,6 +987,30 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
|
|
|
948
987
|
mode, plan ID, model, timestamps) plus `data.idempotency_key` when a key was
|
|
949
988
|
supplied. Protected free text — `--initial-prompt` and `--content` — is sent to
|
|
950
989
|
the server but never appears in the envelope, a refusal, a diagnostic or a log.
|
|
990
|
+
- The protected free text can also arrive from an owner-only file, which keeps it
|
|
991
|
+
out of the command line entirely (argv, the process table under
|
|
992
|
+
`ps`/`/proc/*/cmdline`, shell history and CI logs all see only the path):
|
|
993
|
+
|
|
994
|
+
```bash
|
|
995
|
+
uv run --frozen hc ams sessions create --environment dev --tenant-id TENANT_ID --initial-prompt-file PATH
|
|
996
|
+
uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENANT_ID --session-id SESSION_ID --content-file PATH
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
The file is read locally, before the login check and any request, by the same
|
|
1000
|
+
reader `hc ams sessions plans generate --prompt-file` uses: it must be a
|
|
1001
|
+
readable owner-only regular text file (no symlinks, no directories, not
|
|
1002
|
+
group- or world-readable), and must hold 1-20000 characters — the same bound
|
|
1003
|
+
the inline flag is now held to, so neither channel can carry an empty or
|
|
1004
|
+
over-long value past the local check. Its content is never printed, and an
|
|
1005
|
+
invalid, empty or oversized file is refused locally with a message that
|
|
1006
|
+
names the discipline, never the text. The output is identical to the inline form: `sessions.create`
|
|
1007
|
+
with `data.session`, `sessions.messages.add` with `data.message`.
|
|
1008
|
+
- Exactly one source is accepted per field. Passing `--initial-prompt` with
|
|
1009
|
+
`--initial-prompt-file`, or `--content` with `--content-file`, is a usage
|
|
1010
|
+
error (exit 2) that sends nothing; `messages add` with neither is the same
|
|
1011
|
+
usage error. The inline `--initial-prompt` and `--content` flags remain
|
|
1012
|
+
supported for convenience, and remain exposed in argv, the process table and
|
|
1013
|
+
shell history — use the file channel for anything sensitive.
|
|
951
1014
|
- `--initial-prompt` becomes the session's first `user` message in the same
|
|
952
1015
|
server transaction; `hc ams sessions messages list` shows it afterwards.
|
|
953
1016
|
- `--mode implement` is not available from the server yet: `create` reports one
|
{healthcloud_cli-3.6.0 → healthcloud_cli-3.7.1}/healthcloud_cli/ams/departments/operations.py
RENAMED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from ..context import AmsDependencies
|
|
6
6
|
from ..domain_errors import DomainReadRefused
|
|
7
|
-
from ..results import succeeded
|
|
7
|
+
from ..results import is_unknown_write_status, succeeded, unknown_write
|
|
8
8
|
from ..target import TenantTarget
|
|
9
9
|
from .projections import department_projection
|
|
10
10
|
|
|
@@ -27,6 +27,18 @@ UNKNOWN_OUTCOME = (
|
|
|
27
27
|
"department's current server state before deciding whether to retry."
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
UNKNOWN_HTTP_OUTCOME = (
|
|
31
|
+
"The department write was sent once and its outcome is now unknown: the server answered with an "
|
|
32
|
+
"error status before a readable result, so the request may have been processed. Run "
|
|
33
|
+
"`hc ams departments get` to check the department's current server state before deciding whether "
|
|
34
|
+
"to retry."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
UNKNOWN_RECONCILIATION = (
|
|
38
|
+
"This command did not re-read the department after the uncertain write; run "
|
|
39
|
+
"`hc ams departments list` before doing anything else."
|
|
40
|
+
)
|
|
41
|
+
|
|
30
42
|
|
|
31
43
|
def _department_id(value: str) -> str:
|
|
32
44
|
if not isinstance(value, str) or not value.strip():
|
|
@@ -140,11 +152,26 @@ def create_department(
|
|
|
140
152
|
)
|
|
141
153
|
except HealthCloudNetworkError:
|
|
142
154
|
# Sent once: the request may have landed; the outcome is unknown, never retried here.
|
|
143
|
-
|
|
155
|
+
unknown_write(
|
|
156
|
+
"departments.create",
|
|
157
|
+
_target(target),
|
|
158
|
+
UNKNOWN_OUTCOME,
|
|
159
|
+
{"department": None},
|
|
160
|
+
reconciliation_detail=UNKNOWN_RECONCILIATION,
|
|
161
|
+
)
|
|
144
162
|
except HealthCloudHTTPError as error:
|
|
145
163
|
refusal = _create_not_found(error, parent_department_id)
|
|
146
164
|
if refusal is not None:
|
|
147
165
|
raise refusal from error
|
|
166
|
+
if is_unknown_write_status(getattr(error, "status_code", None)):
|
|
167
|
+
# A 5xx/408 may have committed server-side: report it as unknown, never a retry shape.
|
|
168
|
+
unknown_write(
|
|
169
|
+
"departments.create",
|
|
170
|
+
_target(target),
|
|
171
|
+
UNKNOWN_HTTP_OUTCOME,
|
|
172
|
+
{"department": None},
|
|
173
|
+
reconciliation_detail=UNKNOWN_RECONCILIATION,
|
|
174
|
+
)
|
|
148
175
|
raise
|
|
149
176
|
# The write has already landed server-side; a mismatching readback must never be
|
|
150
177
|
# reported as a stopped no-op. Name the tenant that actually received it.
|
|
@@ -174,11 +201,25 @@ def update_department(
|
|
|
174
201
|
try:
|
|
175
202
|
department = resource.update(department_id, **fields)
|
|
176
203
|
except HealthCloudNetworkError:
|
|
177
|
-
|
|
204
|
+
unknown_write(
|
|
205
|
+
"departments.update",
|
|
206
|
+
_target(target, department_id),
|
|
207
|
+
UNKNOWN_OUTCOME,
|
|
208
|
+
{"department": None},
|
|
209
|
+
reconciliation_detail=UNKNOWN_RECONCILIATION,
|
|
210
|
+
)
|
|
178
211
|
except HealthCloudHTTPError as error:
|
|
179
212
|
refusal = _not_found(error, department_id)
|
|
180
213
|
if refusal is not None:
|
|
181
214
|
raise refusal from error
|
|
215
|
+
if is_unknown_write_status(getattr(error, "status_code", None)):
|
|
216
|
+
unknown_write(
|
|
217
|
+
"departments.update",
|
|
218
|
+
_target(target, department_id),
|
|
219
|
+
UNKNOWN_HTTP_OUTCOME,
|
|
220
|
+
{"department": None},
|
|
221
|
+
reconciliation_detail=UNKNOWN_RECONCILIATION,
|
|
222
|
+
)
|
|
182
223
|
raise
|
|
183
224
|
_identity(department, target, department_id)
|
|
184
225
|
return _write_result("departments.update", target, department_id, {"department": department_projection(department)})
|
|
@@ -193,11 +234,25 @@ def _lifecycle(dependencies: AmsDependencies, target: TenantTarget, department_i
|
|
|
193
234
|
try:
|
|
194
235
|
department = getattr(resource, method)(department_id)
|
|
195
236
|
except HealthCloudNetworkError:
|
|
196
|
-
|
|
237
|
+
unknown_write(
|
|
238
|
+
f"departments.{method}",
|
|
239
|
+
_target(target, department_id),
|
|
240
|
+
UNKNOWN_OUTCOME,
|
|
241
|
+
{"department": None},
|
|
242
|
+
reconciliation_detail=UNKNOWN_RECONCILIATION,
|
|
243
|
+
)
|
|
197
244
|
except HealthCloudHTTPError as error:
|
|
198
245
|
refusal = _not_found(error, department_id)
|
|
199
246
|
if refusal is not None:
|
|
200
247
|
raise refusal from error
|
|
248
|
+
if is_unknown_write_status(getattr(error, "status_code", None)):
|
|
249
|
+
unknown_write(
|
|
250
|
+
f"departments.{method}",
|
|
251
|
+
_target(target, department_id),
|
|
252
|
+
UNKNOWN_HTTP_OUTCOME,
|
|
253
|
+
{"department": None},
|
|
254
|
+
reconciliation_detail=UNKNOWN_RECONCILIATION,
|
|
255
|
+
)
|
|
201
256
|
raise
|
|
202
257
|
_identity(department, target, department_id)
|
|
203
258
|
return _write_result(
|
|
@@ -225,10 +280,24 @@ def delete_department(dependencies: AmsDependencies, target: TenantTarget, depar
|
|
|
225
280
|
try:
|
|
226
281
|
resource.delete(department_id)
|
|
227
282
|
except HealthCloudNetworkError:
|
|
228
|
-
|
|
283
|
+
unknown_write(
|
|
284
|
+
"departments.delete",
|
|
285
|
+
_target(target, department_id),
|
|
286
|
+
UNKNOWN_OUTCOME,
|
|
287
|
+
{"deleted": None},
|
|
288
|
+
reconciliation_detail=UNKNOWN_RECONCILIATION,
|
|
289
|
+
)
|
|
229
290
|
except HealthCloudHTTPError as error:
|
|
230
291
|
refusal = _not_found(error, department_id)
|
|
231
292
|
if refusal is not None:
|
|
232
293
|
raise refusal from error
|
|
294
|
+
if is_unknown_write_status(getattr(error, "status_code", None)):
|
|
295
|
+
unknown_write(
|
|
296
|
+
"departments.delete",
|
|
297
|
+
_target(target, department_id),
|
|
298
|
+
UNKNOWN_HTTP_OUTCOME,
|
|
299
|
+
{"deleted": None},
|
|
300
|
+
reconciliation_detail=UNKNOWN_RECONCILIATION,
|
|
301
|
+
)
|
|
233
302
|
raise
|
|
234
303
|
return _write_result("departments.delete", target, department_id, {"deleted": True})
|
|
@@ -6,11 +6,16 @@ import hashlib
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import stat
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from contextlib import contextmanager
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
import click
|
|
13
14
|
|
|
15
|
+
# One bound for every protected free-text field the CLI accepts, whichever
|
|
16
|
+
# channel it arrives on (#834): the same value the server enforces.
|
|
17
|
+
PROTECTED_TEXT_MAX_CHARS = 20000
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
def canonical(value: object) -> str:
|
|
16
21
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
|
|
@@ -91,6 +96,57 @@ def read_private_text(path: Path, *, maximum: int) -> str:
|
|
|
91
96
|
) from None
|
|
92
97
|
|
|
93
98
|
|
|
99
|
+
def protected_text(
|
|
100
|
+
inline: str | None,
|
|
101
|
+
path: str | None,
|
|
102
|
+
*,
|
|
103
|
+
flag: str,
|
|
104
|
+
file_flag: str,
|
|
105
|
+
refuse: Callable[[str, str], BaseException],
|
|
106
|
+
required: bool = False,
|
|
107
|
+
allow_empty_inline: bool = False,
|
|
108
|
+
) -> str | None:
|
|
109
|
+
"""Resolve one protected free-text field from exactly one input channel.
|
|
110
|
+
|
|
111
|
+
``--<field>`` keeps working for convenience but lands in argv, the process
|
|
112
|
+
table and shell history; ``--<field>-file`` reads the same text from one
|
|
113
|
+
owner-only file instead. Exactly one source is allowed, and both channels
|
|
114
|
+
are bounded identically, so neither can carry an over-long value past the
|
|
115
|
+
local check the other applies.
|
|
116
|
+
|
|
117
|
+
``required`` makes "no source at all" a refusal here, in the Click-free
|
|
118
|
+
layer, so the invariant does not depend on the adapter. Empty text is
|
|
119
|
+
refused on both channels unless the caller documents an empty inline value
|
|
120
|
+
as a clear (``hc ams policies update --content ""``); an empty file is
|
|
121
|
+
always refused, so a truncated or misdirected file can never clear a
|
|
122
|
+
protected field by accident. ``refuse`` builds the caller's own categorized
|
|
123
|
+
refusal, and no refusal quotes the text itself.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
if inline is not None and path is not None:
|
|
127
|
+
raise refuse("validation", f"Pass {flag} or {file_flag}, not both; nothing was sent.")
|
|
128
|
+
if inline is None and path is None:
|
|
129
|
+
if not required:
|
|
130
|
+
return None
|
|
131
|
+
raise refuse("validation", f"Pass {flag} or {file_flag}; nothing was sent.")
|
|
132
|
+
if path is not None:
|
|
133
|
+
if not isinstance(path, str) or not path.strip():
|
|
134
|
+
raise refuse("validation", f"Explicit {file_flag} file path required.")
|
|
135
|
+
text = read_private_text(Path(path), maximum=PROTECTED_TEXT_MAX_CHARS)
|
|
136
|
+
if not text:
|
|
137
|
+
raise refuse("validation", f"The file given to {file_flag} is empty; nothing was sent.")
|
|
138
|
+
return text
|
|
139
|
+
if len(inline) > PROTECTED_TEXT_MAX_CHARS:
|
|
140
|
+
raise refuse(
|
|
141
|
+
"validation",
|
|
142
|
+
f"{flag} is longer than {PROTECTED_TEXT_MAX_CHARS} characters; nothing was sent. "
|
|
143
|
+
f"Pass a shorter value, or the same text with {file_flag}.",
|
|
144
|
+
)
|
|
145
|
+
if not inline and not allow_empty_inline:
|
|
146
|
+
raise refuse("validation", f"{flag} is empty; nothing was sent. Pass the text, or {file_flag} holding it.")
|
|
147
|
+
return inline
|
|
148
|
+
|
|
149
|
+
|
|
94
150
|
@contextmanager
|
|
95
151
|
def _private_stream(path: Path, newline: str | None = None):
|
|
96
152
|
"""Reserve output before network calls, never overwrite, clean failed work."""
|
|
@@ -88,7 +88,15 @@ def create_create_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
88
88
|
@click.option(
|
|
89
89
|
"--content",
|
|
90
90
|
default=None,
|
|
91
|
-
help="Policy content (write-only; never echoed in command output)."
|
|
91
|
+
help="Policy content (write-only; never echoed in command output). Exposed in argv, the process "
|
|
92
|
+
"table and shell history: prefer --content-file.",
|
|
93
|
+
)
|
|
94
|
+
@click.option(
|
|
95
|
+
"--content-file",
|
|
96
|
+
default=None,
|
|
97
|
+
type=click.Path(dir_okay=False),
|
|
98
|
+
help="Owner-only text file holding the policy content, so the text never enters argv or shell "
|
|
99
|
+
"history (1-20000 characters). Cannot be combined with --content.",
|
|
92
100
|
)
|
|
93
101
|
@click.option(
|
|
94
102
|
"--guardrail-ids",
|
|
@@ -106,7 +114,10 @@ def create_create_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
106
114
|
description: str | None,
|
|
107
115
|
content: str | None,
|
|
108
116
|
guardrail_ids: tuple[str, ...],
|
|
117
|
+
content_file: str | None,
|
|
109
118
|
) -> None:
|
|
119
|
+
if content is not None and content_file is not None:
|
|
120
|
+
raise click.UsageError("Pass --content or --content-file, not both; nothing was sent.")
|
|
110
121
|
target_data = {"environment": environment, "tenant_id": tenant_id, "name": name}
|
|
111
122
|
|
|
112
123
|
def execute():
|
|
@@ -123,6 +134,7 @@ def create_create_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
123
134
|
description=description,
|
|
124
135
|
content=content,
|
|
125
136
|
guardrail_ids=list(guardrail_ids) or None,
|
|
137
|
+
content_file=content_file,
|
|
126
138
|
)
|
|
127
139
|
|
|
128
140
|
run_domain("policies.create", target_data, execute, read_only=False)
|
|
@@ -140,7 +152,16 @@ def create_update_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
140
152
|
@click.option(
|
|
141
153
|
"--content",
|
|
142
154
|
default=None,
|
|
143
|
-
help="New policy content; an empty value clears it (write-only; never echoed)."
|
|
155
|
+
help="New policy content; an empty value clears it (write-only; never echoed). Exposed in argv, "
|
|
156
|
+
"the process table and shell history: prefer --content-file.",
|
|
157
|
+
)
|
|
158
|
+
@click.option(
|
|
159
|
+
"--content-file",
|
|
160
|
+
default=None,
|
|
161
|
+
type=click.Path(dir_okay=False),
|
|
162
|
+
help="Owner-only text file holding the new policy content, so the text never enters argv or "
|
|
163
|
+
"shell history (1-20000 characters). An empty file is refused. Cannot be combined with "
|
|
164
|
+
"--content.",
|
|
144
165
|
)
|
|
145
166
|
@click.option(
|
|
146
167
|
"--guardrail-ids",
|
|
@@ -163,7 +184,10 @@ def create_update_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
163
184
|
content: str | None,
|
|
164
185
|
guardrail_ids: tuple[str, ...],
|
|
165
186
|
clear_guardrail_ids: bool,
|
|
187
|
+
content_file: str | None,
|
|
166
188
|
) -> None:
|
|
189
|
+
if content is not None and content_file is not None:
|
|
190
|
+
raise click.UsageError("Pass --content or --content-file, not both; nothing was sent.")
|
|
167
191
|
target_data = {
|
|
168
192
|
"environment": environment,
|
|
169
193
|
"tenant_id": tenant_id,
|
|
@@ -181,14 +205,14 @@ def create_update_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
181
205
|
if description is not None:
|
|
182
206
|
fields["description"] = description or None
|
|
183
207
|
if content is not None:
|
|
184
|
-
fields["content"] = content
|
|
208
|
+
fields["content"] = content
|
|
185
209
|
if guardrail_ids:
|
|
186
210
|
fields["guardrail_ids"] = list(guardrail_ids)
|
|
187
211
|
elif clear_guardrail_ids:
|
|
188
212
|
fields["guardrail_ids"] = []
|
|
189
|
-
if not fields:
|
|
213
|
+
if not fields and content_file is None:
|
|
190
214
|
raise click.UsageError("Nothing to update; pass at least one field option.")
|
|
191
|
-
return update_policy(dependencies, target, policy_id, fields=fields)
|
|
215
|
+
return update_policy(dependencies, target, policy_id, fields=fields, content_file=content_file)
|
|
192
216
|
|
|
193
217
|
run_domain("policies.update", target_data, execute, read_only=False)
|
|
194
218
|
|