healthcloud-cli 3.2.2__tar.gz → 3.2.4__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.2 → healthcloud_cli-3.2.4}/PKG-INFO +41 -3
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/README.md +40 -2
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/ams/output.py +18 -5
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/ams/tuning.py +6 -4
- healthcloud_cli-3.2.4/healthcloud_cli/ams/windows_files.py +162 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/pyproject.toml +1 -1
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/.gitignore +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/hatch_build.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/__init__.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/ams/__init__.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/ams/auth.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/ams/commands.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/ams/target.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/ams/ui.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/cli.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/context.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/contracts/__init__.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/main.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/manifest.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/output.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/params.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/util.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/scripts/__init__.py +0 -0
- {healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/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.
|
|
3
|
+
Version: 3.2.4
|
|
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
|
|
@@ -358,7 +358,8 @@ uv run --frozen hc ams --target-file TARGET.json ui --repo EXISTING_AMS_WORKTREE
|
|
|
358
358
|
Changes JSON accepts only nonempty subsets of `instructions` (string) and
|
|
359
359
|
`greetings` (language-key to string map). Input changes/receipts must be regular,
|
|
360
360
|
owner-only files (for example create under `umask 077`). Outputs use exclusive
|
|
361
|
-
0600 creation and
|
|
361
|
+
0600 creation on POSIX and an owner-only protected DACL on Windows, and refuse
|
|
362
|
+
overwrite/symlinks; failed operations remove unfinished
|
|
362
363
|
outputs. No whole-agent snapshot is saved. Free text appears only in explicitly
|
|
363
364
|
requested private files for human review before any AI use. Default output in
|
|
364
365
|
pretty/json/verbose modes contains bounded metadata and hashes; AMS refuses raw.
|
|
@@ -375,7 +376,7 @@ full-input qualification above. Apply requires the displayed hash, matching
|
|
|
375
376
|
version and selected before-values, and sends `confirm=True` once. HTTP 401/403
|
|
376
377
|
and 409 never trigger identity fallback or mutation replay.
|
|
377
378
|
|
|
378
|
-
On transport failure, apply records an unknown outcome and reconciles through GET
|
|
379
|
+
On transport failure or HTTP 5xx, apply records an unknown outcome and reconciles through GET
|
|
379
380
|
plan/agent without retrying the mutation. Confirmed saves require applied status,
|
|
380
381
|
selected-field readback and exactly the next config version. The current public
|
|
381
382
|
response omits publication_status: **saved/readback confirmed is reported separately
|
|
@@ -403,3 +404,40 @@ operator-controlled.
|
|
|
403
404
|
AMS failures exit nonzero with safe categories, never backend exception bodies.
|
|
404
405
|
In particular, apply may exit nonzero after a confirmed save because publication
|
|
405
406
|
is unknown: inspect the emitted outcome before any next action.
|
|
407
|
+
|
|
408
|
+
On Windows, `chmod` does not establish file privacy. Create the private-input
|
|
409
|
+
directory with an owner-only ACL before saving change JSON or receipts there:
|
|
410
|
+
|
|
411
|
+
```powershell
|
|
412
|
+
New-Item -ItemType Directory -Path .\ams-private
|
|
413
|
+
$amsUser = whoami
|
|
414
|
+
icacls .\ams-private /inheritance:r /grant:r "${amsUser}:(OI)(CI)F"
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Native file handling checks the opened handle's owner and DACL, rejects grants to
|
|
418
|
+
other principals and reparse points, and creates outputs with their restricted
|
|
419
|
+
DACL already in place. Filesystems without usable ACLs fail closed.
|
|
420
|
+
|
|
421
|
+
The `Python CLI` and `Python CLI (Windows)` CI jobs run the non-live suite on
|
|
422
|
+
Linux and Windows, including native file permissions and apply reconciliation.
|
|
423
|
+
Both feed `SDK package checks`; a job failure or unexpected job skip blocks that aggregate.
|
|
424
|
+
Run locally, sequentially, from `packages/cli/pip` with
|
|
425
|
+
`uv run --frozen --extra dev python -m pytest -q -m 'not live'`.
|
|
426
|
+
These synthetic checks do not establish live CareCo acceptance. That still needs
|
|
427
|
+
an explicitly selected and qualified DEV agent, scoped credentials, and the
|
|
428
|
+
attended before/after/restore procedure above.
|
|
429
|
+
|
|
430
|
+
For an optional real-HTTP reproduction with Postman CLI 1.75.0, start
|
|
431
|
+
`postman simulation run postman/simulations/ams-tuning-failure.sim.yaml --port 4900 --no-history --output ndjson`
|
|
432
|
+
from the repository root. In another terminal, run the same SDK verification
|
|
433
|
+
helper used by the required loopback HTTP tests:
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
uv run --project packages/cli/pip --frozen --extra dev python -c "import sys; sys.path.insert(0, 'packages/cli/pip/tests'); from test_ams_http import verify_uncertain_apply; print(verify_uncertain_apply('http://127.0.0.1:4900/ams/api/ams'))"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
The simulation log must show GET plan, GET agent, one POST apply returning 503,
|
|
440
|
+
then GET plan and GET agent. Restart the stateful simulation before repeating;
|
|
441
|
+
stop it with Ctrl+C afterward. This is a source-backed synthetic failure fixture,
|
|
442
|
+
not the deployed service. The required CI tests exercise both pre-commit and
|
|
443
|
+
post-commit 503 over loopback HTTP without requiring a Postman login or installation.
|
|
@@ -340,7 +340,8 @@ uv run --frozen hc ams --target-file TARGET.json ui --repo EXISTING_AMS_WORKTREE
|
|
|
340
340
|
Changes JSON accepts only nonempty subsets of `instructions` (string) and
|
|
341
341
|
`greetings` (language-key to string map). Input changes/receipts must be regular,
|
|
342
342
|
owner-only files (for example create under `umask 077`). Outputs use exclusive
|
|
343
|
-
0600 creation and
|
|
343
|
+
0600 creation on POSIX and an owner-only protected DACL on Windows, and refuse
|
|
344
|
+
overwrite/symlinks; failed operations remove unfinished
|
|
344
345
|
outputs. No whole-agent snapshot is saved. Free text appears only in explicitly
|
|
345
346
|
requested private files for human review before any AI use. Default output in
|
|
346
347
|
pretty/json/verbose modes contains bounded metadata and hashes; AMS refuses raw.
|
|
@@ -357,7 +358,7 @@ full-input qualification above. Apply requires the displayed hash, matching
|
|
|
357
358
|
version and selected before-values, and sends `confirm=True` once. HTTP 401/403
|
|
358
359
|
and 409 never trigger identity fallback or mutation replay.
|
|
359
360
|
|
|
360
|
-
On transport failure, apply records an unknown outcome and reconciles through GET
|
|
361
|
+
On transport failure or HTTP 5xx, apply records an unknown outcome and reconciles through GET
|
|
361
362
|
plan/agent without retrying the mutation. Confirmed saves require applied status,
|
|
362
363
|
selected-field readback and exactly the next config version. The current public
|
|
363
364
|
response omits publication_status: **saved/readback confirmed is reported separately
|
|
@@ -385,3 +386,40 @@ operator-controlled.
|
|
|
385
386
|
AMS failures exit nonzero with safe categories, never backend exception bodies.
|
|
386
387
|
In particular, apply may exit nonzero after a confirmed save because publication
|
|
387
388
|
is unknown: inspect the emitted outcome before any next action.
|
|
389
|
+
|
|
390
|
+
On Windows, `chmod` does not establish file privacy. Create the private-input
|
|
391
|
+
directory with an owner-only ACL before saving change JSON or receipts there:
|
|
392
|
+
|
|
393
|
+
```powershell
|
|
394
|
+
New-Item -ItemType Directory -Path .\ams-private
|
|
395
|
+
$amsUser = whoami
|
|
396
|
+
icacls .\ams-private /inheritance:r /grant:r "${amsUser}:(OI)(CI)F"
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Native file handling checks the opened handle's owner and DACL, rejects grants to
|
|
400
|
+
other principals and reparse points, and creates outputs with their restricted
|
|
401
|
+
DACL already in place. Filesystems without usable ACLs fail closed.
|
|
402
|
+
|
|
403
|
+
The `Python CLI` and `Python CLI (Windows)` CI jobs run the non-live suite on
|
|
404
|
+
Linux and Windows, including native file permissions and apply reconciliation.
|
|
405
|
+
Both feed `SDK package checks`; a job failure or unexpected job skip blocks that aggregate.
|
|
406
|
+
Run locally, sequentially, from `packages/cli/pip` with
|
|
407
|
+
`uv run --frozen --extra dev python -m pytest -q -m 'not live'`.
|
|
408
|
+
These synthetic checks do not establish live CareCo acceptance. That still needs
|
|
409
|
+
an explicitly selected and qualified DEV agent, scoped credentials, and the
|
|
410
|
+
attended before/after/restore procedure above.
|
|
411
|
+
|
|
412
|
+
For an optional real-HTTP reproduction with Postman CLI 1.75.0, start
|
|
413
|
+
`postman simulation run postman/simulations/ams-tuning-failure.sim.yaml --port 4900 --no-history --output ndjson`
|
|
414
|
+
from the repository root. In another terminal, run the same SDK verification
|
|
415
|
+
helper used by the required loopback HTTP tests:
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
uv run --project packages/cli/pip --frozen --extra dev python -c "import sys; sys.path.insert(0, 'packages/cli/pip/tests'); from test_ams_http import verify_uncertain_apply; print(verify_uncertain_apply('http://127.0.0.1:4900/ams/api/ams'))"
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The simulation log must show GET plan, GET agent, one POST apply returning 503,
|
|
422
|
+
then GET plan and GET agent. Restart the stateful simulation before repeating;
|
|
423
|
+
stop it with Ctrl+C afterward. This is a source-backed synthetic failure fixture,
|
|
424
|
+
not the deployed service. The required CI tests exercise both pre-commit and
|
|
425
|
+
post-commit 503 over loopback HTTP without requiring a Postman login or installation.
|
|
@@ -37,10 +37,17 @@ def _pairs(pairs):
|
|
|
37
37
|
def read_json(path: Path, *, private: bool = False) -> dict:
|
|
38
38
|
"""Reject links, non-regular files and ambiguous JSON without quoting input."""
|
|
39
39
|
try:
|
|
40
|
-
|
|
40
|
+
if os.name == "nt":
|
|
41
|
+
from .windows_files import open_file
|
|
42
|
+
|
|
43
|
+
fd = open_file(path, private=private)
|
|
44
|
+
else:
|
|
45
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
41
46
|
with os.fdopen(fd, encoding="utf-8") as stream:
|
|
42
47
|
info = os.fstat(stream.fileno())
|
|
43
|
-
if not stat.S_ISREG(info.st_mode) or (
|
|
48
|
+
if not stat.S_ISREG(info.st_mode) or (
|
|
49
|
+
private and os.name != "nt" and (info.st_mode & 0o077 or info.st_uid != os.getuid())
|
|
50
|
+
):
|
|
44
51
|
raise ValueError("Private input must be an owner-only regular file")
|
|
45
52
|
value = json.load(
|
|
46
53
|
stream, object_pairs_hook=_pairs, parse_constant=lambda _: (_ for _ in ()).throw(ValueError())
|
|
@@ -58,12 +65,18 @@ def read_json(path: Path, *, private: bool = False) -> dict:
|
|
|
58
65
|
def private_output(path: Path):
|
|
59
66
|
"""Reserve output before network calls, never overwrite, clean failed work."""
|
|
60
67
|
try:
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
if os.name == "nt":
|
|
69
|
+
from .windows_files import open_file
|
|
70
|
+
|
|
71
|
+
fd = open_file(path, create=True)
|
|
72
|
+
else:
|
|
73
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
74
|
+
except OSError, ValueError:
|
|
63
75
|
raise click.ClickException("Cannot create private output; choose a new file, without symlinks.") from None
|
|
64
76
|
try:
|
|
65
77
|
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
66
|
-
os.
|
|
78
|
+
if os.name != "nt":
|
|
79
|
+
os.fchmod(stream.fileno(), 0o600)
|
|
67
80
|
yield lambda value: stream.write(canonical(value) + "\n")
|
|
68
81
|
except BaseException:
|
|
69
82
|
path.unlink(missing_ok=True)
|
|
@@ -9,7 +9,7 @@ from uuid import uuid4
|
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
|
|
12
|
-
from healthcloud.errors import HealthCloudNetworkError
|
|
12
|
+
from healthcloud.errors import HealthCloudHTTPError, HealthCloudNetworkError
|
|
13
13
|
from healthcloud.models.base import JsonValue
|
|
14
14
|
|
|
15
15
|
from .auth import selected_agent
|
|
@@ -199,7 +199,7 @@ def _outcome(sdk, target, receipt, *, unknown: bool = False):
|
|
|
199
199
|
metadata(result)
|
|
200
200
|
if unknown:
|
|
201
201
|
raise click.ClickException(
|
|
202
|
-
"Mutation outcome was unknown after transport failure; GET reconciliation recorded. Review saved state; do not replay."
|
|
202
|
+
"Mutation outcome was unknown after transport or server failure; GET reconciliation recorded. Review saved state; do not replay."
|
|
203
203
|
)
|
|
204
204
|
if not saved:
|
|
205
205
|
raise click.ClickException("Apply status or selected-field readback failed; inspect before further action.")
|
|
@@ -226,8 +226,10 @@ def apply_receipt(sdk, target, receipt, confirm_hash: str):
|
|
|
226
226
|
result = sdk.ams_admin.apply_tuning_plan(
|
|
227
227
|
plan.id, plan_hash=plan.plan_hash, expected_config_version=plan.base_config_version, confirm=True
|
|
228
228
|
)
|
|
229
|
-
except HealthCloudNetworkError:
|
|
230
|
-
|
|
229
|
+
except (HealthCloudNetworkError, HealthCloudHTTPError) as exc:
|
|
230
|
+
if isinstance(exc, HealthCloudHTTPError) and not 500 <= exc.status_code < 600:
|
|
231
|
+
raise
|
|
232
|
+
# A timeout or server/proxy error can occur after the mutation committed.
|
|
231
233
|
metadata(dict(plan_id=plan.id, outcome="unknown", mutation_replayed=False))
|
|
232
234
|
_outcome(sdk, target, receipt, unknown=True)
|
|
233
235
|
if result is None or result.id != plan.id or result.plan_hash != plan.plan_hash:
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Windows handle-based JSON access with an owner-only, protected DACL.
|
|
2
|
+
|
|
3
|
+
Keep this module Windows-only: the public file helpers import it lazily.
|
|
4
|
+
Security is checked on the opened handle, before reading or writing content.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ctypes
|
|
8
|
+
import msvcrt
|
|
9
|
+
import os
|
|
10
|
+
from ctypes import wintypes as w
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
14
|
+
_security = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
15
|
+
_P = ctypes.c_void_p
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _bind(library, name, result, *arguments):
|
|
19
|
+
function = getattr(library, name)
|
|
20
|
+
function.restype = result
|
|
21
|
+
function.argtypes = arguments
|
|
22
|
+
return function
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_close = _bind(_kernel, "CloseHandle", w.BOOL, w.HANDLE)
|
|
26
|
+
_free = _bind(_kernel, "LocalFree", _P, _P)
|
|
27
|
+
_process = _bind(_kernel, "GetCurrentProcess", w.HANDLE)
|
|
28
|
+
_open_token = _bind(_security, "OpenProcessToken", w.BOOL, w.HANDLE, w.DWORD, ctypes.POINTER(w.HANDLE))
|
|
29
|
+
_token_info = _bind(_security, "GetTokenInformation", w.BOOL, w.HANDLE, w.DWORD, _P, w.DWORD, ctypes.POINTER(w.DWORD))
|
|
30
|
+
_sid_text = _bind(_security, "ConvertSidToStringSidW", w.BOOL, _P, ctypes.POINTER(_P))
|
|
31
|
+
_descriptor = _bind(
|
|
32
|
+
_security,
|
|
33
|
+
"ConvertStringSecurityDescriptorToSecurityDescriptorW",
|
|
34
|
+
w.BOOL,
|
|
35
|
+
w.LPCWSTR,
|
|
36
|
+
w.DWORD,
|
|
37
|
+
ctypes.POINTER(_P),
|
|
38
|
+
_P,
|
|
39
|
+
)
|
|
40
|
+
_get_security = _bind(
|
|
41
|
+
_security,
|
|
42
|
+
"GetSecurityInfo",
|
|
43
|
+
w.DWORD,
|
|
44
|
+
w.HANDLE,
|
|
45
|
+
w.DWORD,
|
|
46
|
+
w.DWORD,
|
|
47
|
+
ctypes.POINTER(_P),
|
|
48
|
+
_P,
|
|
49
|
+
ctypes.POINTER(_P),
|
|
50
|
+
_P,
|
|
51
|
+
ctypes.POINTER(_P),
|
|
52
|
+
)
|
|
53
|
+
_get_ace = _bind(_security, "GetAce", w.BOOL, _P, w.DWORD, ctypes.POINTER(_P))
|
|
54
|
+
_create = _bind(_kernel, "CreateFileW", w.HANDLE, w.LPCWSTR, w.DWORD, w.DWORD, _P, w.DWORD, w.DWORD, w.HANDLE)
|
|
55
|
+
_file_type = _bind(_kernel, "GetFileType", w.DWORD, w.HANDLE)
|
|
56
|
+
_file_info = _bind(_kernel, "GetFileInformationByHandleEx", w.BOOL, w.HANDLE, w.DWORD, _P, w.DWORD)
|
|
57
|
+
_set_file_info = _bind(_kernel, "SetFileInformationByHandle", w.BOOL, w.HANDLE, w.DWORD, _P, w.DWORD)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _SecurityAttributes(ctypes.Structure):
|
|
61
|
+
_fields_ = [("length", w.DWORD), ("descriptor", _P), ("inherit", w.BOOL)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Acl(ctypes.Structure):
|
|
65
|
+
_fields_ = [("revision", w.BYTE), ("reserved", w.BYTE), ("size", w.WORD), ("count", w.WORD), ("pad", w.WORD)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _check(ok):
|
|
69
|
+
if not ok:
|
|
70
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _sid_string(sid) -> str:
|
|
74
|
+
text = _P()
|
|
75
|
+
_check(_sid_text(sid, ctypes.byref(text)))
|
|
76
|
+
try:
|
|
77
|
+
return ctypes.wstring_at(text)
|
|
78
|
+
finally:
|
|
79
|
+
_free(text)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _user_sid() -> str:
|
|
83
|
+
token = w.HANDLE()
|
|
84
|
+
_check(_open_token(_process(), 0x0008, ctypes.byref(token))) # TOKEN_QUERY
|
|
85
|
+
try:
|
|
86
|
+
size = w.DWORD()
|
|
87
|
+
_token_info(token, 1, None, 0, ctypes.byref(size)) # TokenUser
|
|
88
|
+
if not size.value:
|
|
89
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
90
|
+
buffer = ctypes.create_string_buffer(size.value)
|
|
91
|
+
_check(_token_info(token, 1, buffer, size, ctypes.byref(size)))
|
|
92
|
+
return _sid_string(_P.from_buffer(buffer).value) # TOKEN_USER starts with the user SID pointer.
|
|
93
|
+
finally:
|
|
94
|
+
_close(token)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _require_private(handle, user_sid: str) -> None:
|
|
98
|
+
owner, acl, descriptor = _P(), _P(), _P()
|
|
99
|
+
status = _get_security(
|
|
100
|
+
handle, 1, 0x00000005, ctypes.byref(owner), None, ctypes.byref(acl), None, ctypes.byref(descriptor)
|
|
101
|
+
)
|
|
102
|
+
if status:
|
|
103
|
+
raise ctypes.WinError(status)
|
|
104
|
+
try:
|
|
105
|
+
if not owner or _sid_string(owner) != user_sid or not acl:
|
|
106
|
+
raise ValueError("Owner-only Windows DACL required")
|
|
107
|
+
count = ctypes.cast(acl, ctypes.POINTER(_Acl)).contents.count
|
|
108
|
+
for index in range(count):
|
|
109
|
+
ace = _P()
|
|
110
|
+
_check(_get_ace(acl, index, ctypes.byref(ace)))
|
|
111
|
+
kind, flags = (w.BYTE * 2).from_address(ace.value)
|
|
112
|
+
if flags & 0x08 or kind == 1: # INHERIT_ONLY_ACE or ACCESS_DENIED_ACE grants no file access.
|
|
113
|
+
continue
|
|
114
|
+
# Only ordinary ACCESS_ALLOWED_ACE grants to the current user are accepted.
|
|
115
|
+
if kind != 0 or _sid_string(ace.value + 8) != user_sid:
|
|
116
|
+
raise ValueError("Private Windows files must not grant access to other principals")
|
|
117
|
+
finally:
|
|
118
|
+
_free(descriptor)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def open_file(path: Path, *, private: bool = False, create: bool = False) -> int:
|
|
122
|
+
"""Return a CRT fd, transferring ownership only after native checks succeed."""
|
|
123
|
+
user = _user_sid() if private or create else None
|
|
124
|
+
descriptor = _P()
|
|
125
|
+
attributes = None
|
|
126
|
+
if create:
|
|
127
|
+
# Protected DACL: no inherited grants and no public interval before ACL setup.
|
|
128
|
+
_check(_descriptor(f"O:{user}D:P(A;;FA;;;{user})", 1, ctypes.byref(descriptor), None))
|
|
129
|
+
attributes = _SecurityAttributes(ctypes.sizeof(_SecurityAttributes), descriptor, False)
|
|
130
|
+
try:
|
|
131
|
+
handle = _create(
|
|
132
|
+
str(path.absolute()),
|
|
133
|
+
(0x40010000 if create else 0x80000000) | 0x00020000, # GENERIC_WRITE + DELETE / READ + READ_CONTROL
|
|
134
|
+
0 if create else 1, # No sharing for outputs; share reads only for inputs.
|
|
135
|
+
ctypes.byref(attributes) if attributes else None,
|
|
136
|
+
1 if create else 3, # CREATE_NEW / OPEN_EXISTING
|
|
137
|
+
0x00200000 | 0x02000000, # OPEN_REPARSE_POINT | BACKUP_SEMANTICS
|
|
138
|
+
None,
|
|
139
|
+
)
|
|
140
|
+
finally:
|
|
141
|
+
if descriptor:
|
|
142
|
+
_free(descriptor)
|
|
143
|
+
if handle == w.HANDLE(-1).value:
|
|
144
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
145
|
+
try:
|
|
146
|
+
info = (w.DWORD * 2)() # FILE_ATTRIBUTE_TAG_INFO
|
|
147
|
+
_check(_file_info(handle, 9, info, ctypes.sizeof(info)))
|
|
148
|
+
if _file_type(handle) != 1 or info[0] & (0x10 | 0x400): # Disk files only; no directories or reparse points.
|
|
149
|
+
raise ValueError("Regular non-reparse Windows file required")
|
|
150
|
+
if user is not None:
|
|
151
|
+
_require_private(handle, user)
|
|
152
|
+
fd = msvcrt.open_osfhandle(handle, (os.O_WRONLY if create else os.O_RDONLY) | os.O_BINARY)
|
|
153
|
+
except BaseException:
|
|
154
|
+
try:
|
|
155
|
+
if create:
|
|
156
|
+
# Delete this exact handle's new file, without a path replacement race.
|
|
157
|
+
delete = w.BOOL(True)
|
|
158
|
+
_check(_set_file_info(handle, 4, ctypes.byref(delete), ctypes.sizeof(delete)))
|
|
159
|
+
finally:
|
|
160
|
+
_close(handle)
|
|
161
|
+
raise
|
|
162
|
+
return fd
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "healthcloud-cli"
|
|
7
|
-
version = "3.2.
|
|
7
|
+
version = "3.2.4"
|
|
8
8
|
description = "Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.2.2 → healthcloud_cli-3.2.4}/healthcloud_cli/contracts/mcp-tools.manifest.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|