agentgov-cli 0.1.0__tar.gz → 0.1.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.
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/PKG-INFO +1 -1
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/commands/install.py +43 -5
- agentgov_cli-0.1.1/agentgov_cli/commands/login.py +159 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/pyproject.toml +17 -1
- agentgov_cli-0.1.0/agentgov_cli/commands/login.py +0 -74
- agentgov_cli-0.1.0/tests/test_pathguard.py +0 -84
- agentgov_cli-0.1.0/uv.lock +0 -886
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/.gitignore +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/README.md +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/__init__.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/commands/__init__.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/commands/doctor.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/commands/register_device.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/commands/status.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/commands/workitem.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/commands/wrap.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/loopback.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/main.py +0 -0
- {agentgov_cli-0.1.0 → agentgov_cli-0.1.1}/agentgov_cli/port_resolver.py +0 -0
|
@@ -14,6 +14,7 @@ import shutil
|
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
import typer
|
|
17
18
|
from rich.console import Console
|
|
18
19
|
|
|
19
20
|
console = Console()
|
|
@@ -28,10 +29,22 @@ ASSET_ROOTS = [
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def run(
|
|
31
|
-
gateway_url: str =
|
|
32
|
-
|
|
32
|
+
gateway_url: str = typer.Option(
|
|
33
|
+
"http://localhost:8000",
|
|
34
|
+
"--gateway-url",
|
|
35
|
+
"--gateway", # alias — earlier docs called it --gateway
|
|
36
|
+
envvar="AGENTGOV_GATEWAY_URL",
|
|
37
|
+
help="URL of the local AgentGov gateway (usually http://localhost:8000).",
|
|
38
|
+
),
|
|
39
|
+
force: bool = typer.Option(False, "--force", help="Overwrite existing hooks/statusline."),
|
|
33
40
|
) -> None:
|
|
34
|
-
"""Install hooks + statusline + slash commands into ~/.claude/.
|
|
41
|
+
"""Install hooks + statusline + slash commands into ~/.claude/.
|
|
42
|
+
|
|
43
|
+
Also writes a ready-to-copy managed-settings.json to
|
|
44
|
+
~/.agentgov/managed-settings.json — save that file at the OS-managed path
|
|
45
|
+
(needs sudo/admin) to force ALL Claude Code processes on this machine —
|
|
46
|
+
terminal AND VS Code extension — through the gateway.
|
|
47
|
+
"""
|
|
35
48
|
src = _find_assets()
|
|
36
49
|
dest = Path.home() / ".claude"
|
|
37
50
|
dest.mkdir(exist_ok=True)
|
|
@@ -62,9 +75,34 @@ def run(
|
|
|
62
75
|
console.print(f"[green]Copied[/] /workitem slash command → {commands_dest / 'workitem.md'}")
|
|
63
76
|
|
|
64
77
|
settings = _build_managed_settings(gateway_url, hooks_dest, statusline_dest)
|
|
78
|
+
settings_json = json.dumps(settings, indent=2)
|
|
79
|
+
|
|
80
|
+
# Write to a stable local path so the admin can `sudo cp` it into place
|
|
81
|
+
# without having to redirect stdout carefully.
|
|
82
|
+
local_settings_path = Path.home() / ".agentgov" / "managed-settings.json"
|
|
83
|
+
local_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
local_settings_path.write_text(settings_json + "\n")
|
|
85
|
+
local_settings_path.chmod(0o644)
|
|
86
|
+
|
|
87
|
+
target_path = _managed_settings_path()
|
|
65
88
|
console.rule("[bold]managed-settings.json[/]")
|
|
66
|
-
console.print(f"
|
|
67
|
-
console.print(
|
|
89
|
+
console.print(f"Written locally to: [cyan]{local_settings_path}[/]")
|
|
90
|
+
console.print(f"Target install path: [cyan]{target_path}[/]\n")
|
|
91
|
+
console.print("[bold]To govern the VS Code extension too, run:[/]")
|
|
92
|
+
if platform.system() == "Windows":
|
|
93
|
+
console.print(
|
|
94
|
+
f' Copy-Item "{local_settings_path}" "{target_path}" -Force'
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
parent = str(Path(target_path).parent)
|
|
98
|
+
console.print(
|
|
99
|
+
f" sudo mkdir -p {parent} && sudo cp {local_settings_path} {target_path}"
|
|
100
|
+
)
|
|
101
|
+
console.print(
|
|
102
|
+
"\nThen fully quit VS Code and reopen — the extension now routes through the gateway."
|
|
103
|
+
)
|
|
104
|
+
console.print("\n[dim]Contents (for reference):[/]")
|
|
105
|
+
console.print(settings_json)
|
|
68
106
|
|
|
69
107
|
|
|
70
108
|
def _find_assets() -> Path:
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""`agentgov login` — obtain an AgentGov dashboard session (CLAUDE.md §13.5).
|
|
2
|
+
|
|
3
|
+
Two paths, mutually exclusive:
|
|
4
|
+
|
|
5
|
+
1. --invite <inv_...> Accept an emailed invite: prompts for a username,
|
|
6
|
+
POSTs /api/invites/<token>, and stores the returned
|
|
7
|
+
sess_ token as the identity.
|
|
8
|
+
|
|
9
|
+
2. --token <sess_...|agentgov-dev-admin>
|
|
10
|
+
Paste-in a pre-obtained token. Verified by hitting
|
|
11
|
+
/api/devices with it (any authenticated GET works).
|
|
12
|
+
|
|
13
|
+
The Auth0 device-code flow lands in Phase 5 and slots into path (2).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
import typer
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run(
|
|
30
|
+
tower_url: str = typer.Option(
|
|
31
|
+
None, "--tower", envvar="CONTROL_TOWER_URL", help="Control tower URL"
|
|
32
|
+
),
|
|
33
|
+
token: str = typer.Option(
|
|
34
|
+
None,
|
|
35
|
+
"--token",
|
|
36
|
+
help="Paste a session token (sess_...) or 'agentgov-dev-admin' in dev.",
|
|
37
|
+
),
|
|
38
|
+
invite: str = typer.Option(
|
|
39
|
+
None,
|
|
40
|
+
"--invite",
|
|
41
|
+
help="Accept an invite token (inv_...) from an email link.",
|
|
42
|
+
),
|
|
43
|
+
username: str = typer.Option(
|
|
44
|
+
None,
|
|
45
|
+
"--username",
|
|
46
|
+
help="Username to claim when accepting an invite. Prompted if omitted.",
|
|
47
|
+
),
|
|
48
|
+
display_name: str = typer.Option(
|
|
49
|
+
None, "--display-name", help="Optional display name when accepting an invite."
|
|
50
|
+
),
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Attach this machine to a user in the AgentGov control tower."""
|
|
53
|
+
tower = (tower_url or os.environ.get("CONTROL_TOWER_URL") or "http://localhost:3000").rstrip("/")
|
|
54
|
+
|
|
55
|
+
if invite and token:
|
|
56
|
+
console.print("[red]--invite and --token are mutually exclusive.[/]")
|
|
57
|
+
raise typer.Exit(2)
|
|
58
|
+
|
|
59
|
+
if invite:
|
|
60
|
+
resolved = _accept_invite(tower, invite.strip(), username, display_name)
|
|
61
|
+
else:
|
|
62
|
+
resolved = _paste_token(tower, token)
|
|
63
|
+
|
|
64
|
+
identity_path = Path.home() / ".agentgov" / "identity.json"
|
|
65
|
+
identity_path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
identity_path.write_text(json.dumps(resolved, indent=2))
|
|
67
|
+
identity_path.chmod(0o600)
|
|
68
|
+
console.print(f"[green]✅ Logged in.[/] Identity saved to {identity_path}")
|
|
69
|
+
console.print("Next: [bold]agentgov register-device --name 'my-laptop'[/]")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _paste_token(tower: str, token: str | None) -> dict[str, object]:
|
|
73
|
+
if not token:
|
|
74
|
+
console.print(
|
|
75
|
+
"[bold]Paste your session token[/] (sess_... from signup/invite, or 'agentgov-dev-admin' in dev)."
|
|
76
|
+
)
|
|
77
|
+
token = typer.prompt("token", hide_input=True).strip()
|
|
78
|
+
else:
|
|
79
|
+
token = token.strip()
|
|
80
|
+
|
|
81
|
+
# Verify.
|
|
82
|
+
try:
|
|
83
|
+
r = httpx.get(
|
|
84
|
+
f"{tower}/api/devices",
|
|
85
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
86
|
+
timeout=10.0,
|
|
87
|
+
)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
console.print(f"[red]Cannot reach control tower at {tower}: {e}[/]")
|
|
90
|
+
raise typer.Exit(2) from e
|
|
91
|
+
if r.status_code == 401:
|
|
92
|
+
console.print("[red]Token rejected by control tower.[/]")
|
|
93
|
+
raise typer.Exit(2)
|
|
94
|
+
if r.status_code >= 400:
|
|
95
|
+
console.print(f"[red]Unexpected response: {r.status_code} {r.text}[/]")
|
|
96
|
+
raise typer.Exit(2)
|
|
97
|
+
|
|
98
|
+
return {"tower_url": tower, "auth0_token": token}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _accept_invite(
|
|
102
|
+
tower: str,
|
|
103
|
+
invite_token: str,
|
|
104
|
+
username: str | None,
|
|
105
|
+
display_name: str | None,
|
|
106
|
+
) -> dict[str, object]:
|
|
107
|
+
# Peek so the user sees what they're joining.
|
|
108
|
+
try:
|
|
109
|
+
peek = httpx.get(f"{tower}/api/invites/{invite_token}", timeout=10.0)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
console.print(f"[red]Cannot reach control tower at {tower}: {e}[/]")
|
|
112
|
+
raise typer.Exit(2) from e
|
|
113
|
+
if peek.status_code != 200:
|
|
114
|
+
console.print(f"[red]Invite not valid: {peek.status_code} {peek.text}[/]")
|
|
115
|
+
raise typer.Exit(2)
|
|
116
|
+
preview = peek.json()
|
|
117
|
+
console.print(
|
|
118
|
+
f"Invite for [bold]{preview.get('email')}[/] to join "
|
|
119
|
+
f"[bold]{preview.get('tenant_name')}[/] as {preview.get('role')}."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if not username:
|
|
123
|
+
username = typer.prompt("Choose a username").strip()
|
|
124
|
+
if not display_name:
|
|
125
|
+
display_name = typer.prompt("Display name (optional)", default="").strip() or None
|
|
126
|
+
|
|
127
|
+
body: dict[str, object] = {"username": username}
|
|
128
|
+
if display_name:
|
|
129
|
+
body["display_name"] = display_name
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
r = httpx.post(
|
|
133
|
+
f"{tower}/api/invites/{invite_token}",
|
|
134
|
+
json=body,
|
|
135
|
+
timeout=15.0,
|
|
136
|
+
)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
console.print(f"[red]Accept failed: {e}[/]")
|
|
139
|
+
raise typer.Exit(2) from e
|
|
140
|
+
if r.status_code >= 400:
|
|
141
|
+
console.print(f"[red]Accept rejected: {r.status_code} {r.text}[/]")
|
|
142
|
+
raise typer.Exit(2)
|
|
143
|
+
|
|
144
|
+
resp = r.json()
|
|
145
|
+
session_token = resp.get("session_token")
|
|
146
|
+
if not session_token:
|
|
147
|
+
console.print("[red]Server did not return a session token.[/]")
|
|
148
|
+
raise typer.Exit(2)
|
|
149
|
+
|
|
150
|
+
console.print(
|
|
151
|
+
f"[green]Joined {resp.get('tenant_name')} as {resp.get('user', {}).get('username')}.[/]"
|
|
152
|
+
)
|
|
153
|
+
return {
|
|
154
|
+
"tower_url": tower,
|
|
155
|
+
"auth0_token": session_token,
|
|
156
|
+
"tenant_id": resp.get("tenant_id"),
|
|
157
|
+
"tenant_slug": resp.get("tenant_slug"),
|
|
158
|
+
"username": resp.get("user", {}).get("username"),
|
|
159
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# PyPI: `agentgov` was rejected as too similar to existing `agent-gov`.
|
|
3
3
|
# Package name is agentgov-cli; the console command it installs is still `agentgov`.
|
|
4
4
|
name = "agentgov-cli"
|
|
5
|
-
version = "0.1.
|
|
5
|
+
version = "0.1.1"
|
|
6
6
|
description = "AgentGov CLI — wrap Claude Code, bind work items, check gateway health."
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
license = { text = "Apache-2.0" }
|
|
@@ -37,6 +37,22 @@ build-backend = "hatchling.build"
|
|
|
37
37
|
[tool.hatch.build.targets.wheel]
|
|
38
38
|
packages = ["agentgov_cli"]
|
|
39
39
|
|
|
40
|
+
# Ship client_assets/ (sibling dir of cli/) as shared data. Installs to
|
|
41
|
+
# <venv>/share/agentgov/client_assets/, which is exactly where
|
|
42
|
+
# commands/install.py:ASSET_ROOTS[1] looks. Without this, a pip-installed CLI
|
|
43
|
+
# can't find hooks/statusline/slash-command and `agentgov install` fails with
|
|
44
|
+
# FileNotFoundError.
|
|
45
|
+
[tool.hatch.build.targets.wheel.shared-data]
|
|
46
|
+
"../client_assets" = "share/agentgov/client_assets"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = [
|
|
50
|
+
"agentgov_cli/**",
|
|
51
|
+
"../client_assets/**",
|
|
52
|
+
"README.md",
|
|
53
|
+
"pyproject.toml",
|
|
54
|
+
]
|
|
55
|
+
|
|
40
56
|
[tool.ruff]
|
|
41
57
|
line-length = 100
|
|
42
58
|
target-version = "py310"
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"""`agentgov login` — obtain an AgentGov session token (CLAUDE.md §13.5).
|
|
2
|
-
|
|
3
|
-
v1 implements a DEV-ONLY stub: the CLI accepts a bearer token you paste
|
|
4
|
-
(from the dashboard) and stores it in ~/.agentgov/identity.json (0600).
|
|
5
|
-
The real OAuth device-code flow lands with Auth0 in Phase 5; the file
|
|
6
|
-
layout + tower endpoint are already device-code-shaped so the swap is
|
|
7
|
-
transparent to `wrap`.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
import httpx
|
|
17
|
-
import typer
|
|
18
|
-
from rich.console import Console
|
|
19
|
-
|
|
20
|
-
console = Console()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def run(
|
|
24
|
-
tower_url: str = typer.Option(
|
|
25
|
-
None, "--tower", envvar="CONTROL_TOWER_URL", help="Control tower URL"
|
|
26
|
-
),
|
|
27
|
-
token: str = typer.Option(
|
|
28
|
-
None,
|
|
29
|
-
"--token",
|
|
30
|
-
help="Paste your dev-admin token (development mode only). Prompts if omitted.",
|
|
31
|
-
prompt=False,
|
|
32
|
-
),
|
|
33
|
-
) -> None:
|
|
34
|
-
"""Attach this machine to a user in the AgentGov control tower."""
|
|
35
|
-
tower = tower_url or os.environ.get("CONTROL_TOWER_URL") or "http://localhost:3000"
|
|
36
|
-
|
|
37
|
-
if not token:
|
|
38
|
-
console.print(
|
|
39
|
-
"[bold]Dev-mode login[/] — paste a dev-admin token from the dashboard, or "
|
|
40
|
-
"the literal string 'agentgov-dev-admin' to use the seeded ADMIN.",
|
|
41
|
-
)
|
|
42
|
-
token = typer.prompt("token", hide_input=True).strip()
|
|
43
|
-
else:
|
|
44
|
-
token = token.strip()
|
|
45
|
-
|
|
46
|
-
# Verify the token can list /api/devices (a low-risk authenticated call).
|
|
47
|
-
try:
|
|
48
|
-
r = httpx.get(
|
|
49
|
-
f"{tower.rstrip('/')}/api/devices",
|
|
50
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
51
|
-
timeout=10.0,
|
|
52
|
-
)
|
|
53
|
-
except Exception as e:
|
|
54
|
-
console.print(f"[red]Cannot reach control tower at {tower}: {e}[/]")
|
|
55
|
-
raise typer.Exit(2) from e
|
|
56
|
-
if r.status_code == 401:
|
|
57
|
-
console.print("[red]Token rejected by control tower.[/]")
|
|
58
|
-
raise typer.Exit(2)
|
|
59
|
-
if r.status_code >= 400:
|
|
60
|
-
console.print(f"[red]Unexpected response from control tower: {r.status_code} {r.text}[/]")
|
|
61
|
-
raise typer.Exit(2)
|
|
62
|
-
|
|
63
|
-
# Persist.
|
|
64
|
-
identity_path = Path.home() / ".agentgov" / "identity.json"
|
|
65
|
-
identity_path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
-
payload = {
|
|
67
|
-
"tower_url": tower,
|
|
68
|
-
"auth0_token": token,
|
|
69
|
-
# user_id + tenant_id populated on register-device response.
|
|
70
|
-
}
|
|
71
|
-
identity_path.write_text(json.dumps(payload, indent=2))
|
|
72
|
-
identity_path.chmod(0o600)
|
|
73
|
-
console.print(f"[green]✅ Logged in.[/] Identity saved to {identity_path}")
|
|
74
|
-
console.print("Next: [bold]agentgov register-device --name 'my-laptop'[/]")
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""PreToolUse path-guard hook tests (§R-CC1 acceptance).
|
|
2
|
-
|
|
3
|
-
The hook is a subprocess boundary; we invoke it as a subprocess with crafted
|
|
4
|
-
stdin (like Claude Code does) and assert exit codes.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
HOOK = (
|
|
16
|
-
Path(__file__).resolve().parent.parent.parent
|
|
17
|
-
/ "client_assets"
|
|
18
|
-
/ "hooks"
|
|
19
|
-
/ "pretooluse_pathguard.py"
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _run(payload: dict, repo_root: Path) -> tuple[int, str]:
|
|
24
|
-
env = os.environ.copy()
|
|
25
|
-
env["AGENTGOV_REPO_ROOT"] = str(repo_root)
|
|
26
|
-
p = subprocess.run(
|
|
27
|
-
[sys.executable, str(HOOK)],
|
|
28
|
-
input=json.dumps(payload).encode(),
|
|
29
|
-
capture_output=True,
|
|
30
|
-
env=env,
|
|
31
|
-
timeout=5,
|
|
32
|
-
)
|
|
33
|
-
return p.returncode, p.stderr.decode()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_allows_file_inside_repo(tmp_path: Path) -> None:
|
|
37
|
-
(tmp_path / "src").mkdir()
|
|
38
|
-
target = tmp_path / "src" / "hello.py"
|
|
39
|
-
target.write_text("# ok")
|
|
40
|
-
rc, err = _run({"tool_name": "Read", "tool_input": {"file_path": str(target)}}, tmp_path)
|
|
41
|
-
assert rc == 0, err
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_blocks_path_traversal(tmp_path: Path) -> None:
|
|
45
|
-
rc, err = _run({"tool_name": "Read", "tool_input": {"file_path": "../../etc/passwd"}}, tmp_path)
|
|
46
|
-
assert rc == 2
|
|
47
|
-
assert "outside" in err
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_blocks_absolute_escape(tmp_path: Path) -> None:
|
|
51
|
-
rc, err = _run({"tool_name": "Read", "tool_input": {"file_path": "/etc/passwd"}}, tmp_path)
|
|
52
|
-
assert rc == 2
|
|
53
|
-
assert "outside" in err
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_bash_cd_root_blocked(tmp_path: Path) -> None:
|
|
57
|
-
rc, _ = _run({"tool_name": "Bash", "tool_input": {"command": "cd / && ls"}}, tmp_path)
|
|
58
|
-
assert rc == 2
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_bash_relative_command_ok(tmp_path: Path) -> None:
|
|
62
|
-
rc, _err = _run(
|
|
63
|
-
{"tool_name": "Bash", "tool_input": {"command": "pytest tests/"}},
|
|
64
|
-
tmp_path,
|
|
65
|
-
)
|
|
66
|
-
assert rc == 0
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_tmp_claude_scratch_allowed(tmp_path: Path) -> None:
|
|
70
|
-
rc, _err = _run(
|
|
71
|
-
{"tool_name": "Read", "tool_input": {"file_path": "/tmp/claude-scratch/x.txt"}},
|
|
72
|
-
tmp_path,
|
|
73
|
-
)
|
|
74
|
-
assert rc == 0
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def test_fail_closed_on_bad_input(tmp_path: Path) -> None:
|
|
78
|
-
# Missing tool_input entirely — hook should not accept.
|
|
79
|
-
# Not-a-dict tool_input:
|
|
80
|
-
rc, _err = _run(
|
|
81
|
-
{"tool_name": "Read", "tool_input": "just a string"}, # type: ignore[dict-item]
|
|
82
|
-
tmp_path,
|
|
83
|
-
)
|
|
84
|
-
assert rc == 2
|