agentgov-cli 0.1.5__tar.gz → 0.1.6__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.5 → agentgov_cli-0.1.6}/PKG-INFO +1 -1
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +80 -15
- agentgov_cli-0.1.6/agentgov_cli/commands/uninstall.py +149 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/main.py +15 -1
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/pyproject.toml +5 -1
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/.gitignore +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/README.md +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/__init__.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/commands/workitem.md +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/hooks/pretooluse_pathguard.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/hooks/sessionstart_register.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/managed-settings.json.tmpl +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/statusline/agentgov_statusline.sh +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/__init__.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/doctor.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/gateway.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/install.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/login.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/register_device.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/status.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/workitem.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/commands/wrap.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/gateway_process.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/loopback.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/port_resolver.py +0 -0
- {agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/hatch_build.py +0 -0
|
@@ -29,8 +29,10 @@ from __future__ import annotations
|
|
|
29
29
|
|
|
30
30
|
import json
|
|
31
31
|
import os
|
|
32
|
+
import subprocess
|
|
32
33
|
import sys
|
|
33
34
|
import urllib.request
|
|
35
|
+
from typing import NoReturn
|
|
34
36
|
|
|
35
37
|
GATEWAY_URL = os.environ.get("AGENTGOV_GATEWAY_URL", "http://127.0.0.1:8000")
|
|
36
38
|
TIMEOUT_SEC = 4.0
|
|
@@ -40,15 +42,75 @@ TIMEOUT_SEC = 4.0
|
|
|
40
42
|
ESCAPE_PREFIXES = ("/workitem", "/agentgov", "/help", "/status", "/doctor")
|
|
41
43
|
|
|
42
44
|
|
|
43
|
-
def _allow() ->
|
|
45
|
+
def _allow() -> NoReturn:
|
|
44
46
|
sys.exit(0)
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
def _block(message: str) ->
|
|
49
|
+
def _block(message: str) -> NoReturn:
|
|
48
50
|
sys.stderr.write(message)
|
|
49
51
|
sys.exit(2)
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
def _binding(session_id: str) -> dict | None:
|
|
55
|
+
"""Ask the gateway about this session. None = gateway unreachable."""
|
|
56
|
+
try:
|
|
57
|
+
with urllib.request.urlopen( # noqa: S310 — loopback only
|
|
58
|
+
f"{GATEWAY_URL.rstrip('/')}/admin/session/{session_id}/binding",
|
|
59
|
+
timeout=TIMEOUT_SEC,
|
|
60
|
+
) as resp:
|
|
61
|
+
result = json.loads(resp.read() or b"{}")
|
|
62
|
+
return result if isinstance(result, dict) else {}
|
|
63
|
+
except Exception:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _register(session_id: str, cwd: str) -> bool:
|
|
68
|
+
"""Re-register a session the gateway has forgotten (e.g. after a restart).
|
|
69
|
+
|
|
70
|
+
The gateway holds sessions in memory, so any restart drops every editor
|
|
71
|
+
window that is already open. Re-registering here means the developer never
|
|
72
|
+
has to know that happened.
|
|
73
|
+
"""
|
|
74
|
+
if not cwd:
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
repo_root = subprocess.run(
|
|
78
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
79
|
+
cwd=cwd,
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
timeout=5,
|
|
83
|
+
check=False,
|
|
84
|
+
)
|
|
85
|
+
if repo_root.returncode != 0:
|
|
86
|
+
return False
|
|
87
|
+
root = repo_root.stdout.strip()
|
|
88
|
+
remote = subprocess.run(
|
|
89
|
+
["git", "remote", "get-url", "origin"],
|
|
90
|
+
cwd=cwd,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
text=True,
|
|
93
|
+
timeout=5,
|
|
94
|
+
check=False,
|
|
95
|
+
)
|
|
96
|
+
body = json.dumps(
|
|
97
|
+
{
|
|
98
|
+
"session_id": session_id,
|
|
99
|
+
"repo": remote.stdout.strip() if remote.returncode == 0 else "",
|
|
100
|
+
"repo_root": root,
|
|
101
|
+
}
|
|
102
|
+
).encode()
|
|
103
|
+
req = urllib.request.Request(
|
|
104
|
+
f"{GATEWAY_URL.rstrip('/')}/admin/session/register",
|
|
105
|
+
data=body,
|
|
106
|
+
headers={"Content-Type": "application/json"},
|
|
107
|
+
)
|
|
108
|
+
urllib.request.urlopen(req, timeout=TIMEOUT_SEC).read() # noqa: S310 — loopback only
|
|
109
|
+
return True
|
|
110
|
+
except Exception:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
52
114
|
def main() -> None:
|
|
53
115
|
try:
|
|
54
116
|
payload = json.load(sys.stdin)
|
|
@@ -57,6 +119,7 @@ def main() -> None:
|
|
|
57
119
|
|
|
58
120
|
session_id = str(payload.get("session_id") or "")
|
|
59
121
|
user_input = str(payload.get("user_input") or "").strip()
|
|
122
|
+
cwd = str(payload.get("cwd") or "")
|
|
60
123
|
|
|
61
124
|
if not session_id:
|
|
62
125
|
_allow()
|
|
@@ -65,13 +128,8 @@ def main() -> None:
|
|
|
65
128
|
if user_input.startswith(ESCAPE_PREFIXES):
|
|
66
129
|
_allow()
|
|
67
130
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
f"{GATEWAY_URL.rstrip('/')}/admin/session/{session_id}/binding",
|
|
71
|
-
timeout=TIMEOUT_SEC,
|
|
72
|
-
) as resp:
|
|
73
|
-
data = json.loads(resp.read() or b"{}")
|
|
74
|
-
except Exception:
|
|
131
|
+
data = _binding(session_id)
|
|
132
|
+
if data is None:
|
|
75
133
|
# Gateway down or unreachable. Do NOT block here: Claude Code cannot
|
|
76
134
|
# reach the model either way, and blocking would bury the real error
|
|
77
135
|
# ("connection refused") under a confusing governance message.
|
|
@@ -81,12 +139,19 @@ def main() -> None:
|
|
|
81
139
|
_allow()
|
|
82
140
|
|
|
83
141
|
if not data.get("known"):
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
142
|
+
# The gateway does not recognise this session. This is almost always
|
|
143
|
+
# TRANSIENT, not an attack: the gateway keeps sessions in memory, so a
|
|
144
|
+
# restart or reinstall forgets every editor window that is already open.
|
|
145
|
+
#
|
|
146
|
+
# Blocking here was a serious mistake. It bricked every open Claude Code
|
|
147
|
+
# window after a gateway restart, with a message telling the developer to
|
|
148
|
+
# start a new session — while the API plane would have enforced anyway.
|
|
149
|
+
# Self-heal instead: re-register, then re-check.
|
|
150
|
+
if not _register(session_id, cwd):
|
|
151
|
+
_allow() # Could not re-register — let the gateway be the judge.
|
|
152
|
+
data = _binding(session_id)
|
|
153
|
+
if data is None or data.get("bound"):
|
|
154
|
+
_allow()
|
|
90
155
|
|
|
91
156
|
repo = data.get("repo") or "this repository"
|
|
92
157
|
_block(
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""`agentgov uninstall` — remove AgentGov from this machine, safely.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS
|
|
4
|
+
---------------
|
|
5
|
+
`agentgov install` writes hook registrations into ~/.claude/settings.json that
|
|
6
|
+
point at ~/.claude/agentgov-hooks/*.py. Nothing removed them, so the documented
|
|
7
|
+
teardown —
|
|
8
|
+
|
|
9
|
+
rm -rf ~/.agentgov ~/.claude/agentgov-hooks
|
|
10
|
+
|
|
11
|
+
— left Claude Code registering three hooks whose files no longer existed. Every
|
|
12
|
+
session and every prompt then tried to execute a missing command. Deleting the
|
|
13
|
+
files made Claude Code WORSE, not neutral, and no amount of reinstalling fixed
|
|
14
|
+
it because the stale registrations were never the thing being reinstalled.
|
|
15
|
+
|
|
16
|
+
Order matters and is the whole point of this command: **deregister first, delete
|
|
17
|
+
second**. A half-removed install must never be able to break the editor.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import typer
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
CLAUDE_DIR = Path.home() / ".claude"
|
|
33
|
+
SETTINGS = CLAUDE_DIR / "settings.json"
|
|
34
|
+
AGENTGOV_DIR = Path.home() / ".agentgov"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(
|
|
38
|
+
keep_credentials: bool = typer.Option(
|
|
39
|
+
False,
|
|
40
|
+
"--keep-credentials",
|
|
41
|
+
help="Keep ~/.agentgov (device key, identity, logs). Only remove Claude Code wiring.",
|
|
42
|
+
),
|
|
43
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Do not prompt for confirmation."),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Remove AgentGov's Claude Code hooks, statusline, and local state."""
|
|
46
|
+
if not yes:
|
|
47
|
+
console.print("This will remove AgentGov's hooks and statusline from Claude Code.")
|
|
48
|
+
if not keep_credentials:
|
|
49
|
+
console.print(
|
|
50
|
+
f"It will also delete [cyan]{AGENTGOV_DIR}[/] "
|
|
51
|
+
"(device key, identity, gateway logs)."
|
|
52
|
+
)
|
|
53
|
+
if not typer.confirm("Continue?", default=False):
|
|
54
|
+
raise typer.Abort()
|
|
55
|
+
|
|
56
|
+
# 1. STOP the gateway service first, so nothing restarts mid-teardown.
|
|
57
|
+
_stop_service()
|
|
58
|
+
|
|
59
|
+
# 2. DEREGISTER before deleting. This is the step whose absence broke
|
|
60
|
+
# Claude Code: settings.json outlives the files it points at.
|
|
61
|
+
_deregister()
|
|
62
|
+
|
|
63
|
+
# 3. Now the files are safe to delete — nothing references them.
|
|
64
|
+
for path in (CLAUDE_DIR / "agentgov-hooks", CLAUDE_DIR / "agentgov-statusline"):
|
|
65
|
+
if path.exists():
|
|
66
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
67
|
+
console.print(f"[green]Removed[/] {path}")
|
|
68
|
+
workitem = CLAUDE_DIR / "commands" / "workitem.md"
|
|
69
|
+
if workitem.exists():
|
|
70
|
+
workitem.unlink()
|
|
71
|
+
console.print(f"[green]Removed[/] {workitem}")
|
|
72
|
+
|
|
73
|
+
if not keep_credentials and AGENTGOV_DIR.exists():
|
|
74
|
+
shutil.rmtree(AGENTGOV_DIR, ignore_errors=True)
|
|
75
|
+
console.print(f"[green]Removed[/] {AGENTGOV_DIR}")
|
|
76
|
+
|
|
77
|
+
console.print("\n[bold green]AgentGov removed.[/]")
|
|
78
|
+
console.print("Claude Code is unaffected — restart it to clear the old session.")
|
|
79
|
+
console.print(
|
|
80
|
+
"\n[dim]Machine-wide managed-settings.json (if you installed it) needs sudo:[/]\n"
|
|
81
|
+
" sudo rm -f /etc/claude-code/managed-settings.json"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _stop_service() -> None:
|
|
86
|
+
"""Stop + disable the gateway service, ignoring 'not installed'."""
|
|
87
|
+
unit = Path.home() / ".config" / "systemd" / "user" / "agentgov-gateway.service"
|
|
88
|
+
if not unit.exists():
|
|
89
|
+
return
|
|
90
|
+
for args in (["stop", "agentgov-gateway"], ["disable", "agentgov-gateway"]):
|
|
91
|
+
subprocess.run(
|
|
92
|
+
["systemctl", "--user", *args], capture_output=True, check=False, timeout=30
|
|
93
|
+
)
|
|
94
|
+
unit.unlink(missing_ok=True)
|
|
95
|
+
subprocess.run(
|
|
96
|
+
["systemctl", "--user", "daemon-reload"], capture_output=True, check=False, timeout=30
|
|
97
|
+
)
|
|
98
|
+
console.print(f"[green]Stopped[/] gateway service ({unit.name})")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _deregister() -> None:
|
|
102
|
+
"""Strip AgentGov's entries from ~/.claude/settings.json, leaving the rest."""
|
|
103
|
+
if not SETTINGS.exists():
|
|
104
|
+
return
|
|
105
|
+
try:
|
|
106
|
+
data = json.loads(SETTINGS.read_text() or "{}")
|
|
107
|
+
except json.JSONDecodeError:
|
|
108
|
+
console.print(
|
|
109
|
+
f"[yellow]Could not parse {SETTINGS}[/] — leaving it alone. "
|
|
110
|
+
"Remove any 'agentgov-hooks' entries by hand."
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
if not isinstance(data, dict):
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
removed = 0
|
|
117
|
+
hooks = data.get("hooks")
|
|
118
|
+
if isinstance(hooks, dict):
|
|
119
|
+
for event in list(hooks):
|
|
120
|
+
entries = hooks[event]
|
|
121
|
+
if not isinstance(entries, list):
|
|
122
|
+
continue
|
|
123
|
+
kept = [e for e in entries if not _is_agentgov(e)]
|
|
124
|
+
removed += len(entries) - len(kept)
|
|
125
|
+
if kept:
|
|
126
|
+
hooks[event] = kept
|
|
127
|
+
else:
|
|
128
|
+
del hooks[event]
|
|
129
|
+
if hooks:
|
|
130
|
+
data["hooks"] = hooks
|
|
131
|
+
else:
|
|
132
|
+
data.pop("hooks", None)
|
|
133
|
+
|
|
134
|
+
if "agentgov" in str(data.get("statusLine", "")):
|
|
135
|
+
data.pop("statusLine", None)
|
|
136
|
+
removed += 1
|
|
137
|
+
|
|
138
|
+
if removed:
|
|
139
|
+
SETTINGS.write_text(json.dumps(data, indent=2) + "\n")
|
|
140
|
+
console.print(f"[green]Deregistered[/] {removed} entr(ies) from {SETTINGS}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _is_agentgov(entry: object) -> bool:
|
|
144
|
+
if not isinstance(entry, dict):
|
|
145
|
+
return False
|
|
146
|
+
return any(
|
|
147
|
+
isinstance(h, dict) and "agentgov-hooks" in str(h.get("command", ""))
|
|
148
|
+
for h in (entry.get("hooks") or [])
|
|
149
|
+
)
|
|
@@ -4,7 +4,17 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
|
-
from .commands import
|
|
7
|
+
from .commands import (
|
|
8
|
+
doctor,
|
|
9
|
+
gateway,
|
|
10
|
+
install,
|
|
11
|
+
login,
|
|
12
|
+
register_device,
|
|
13
|
+
status,
|
|
14
|
+
uninstall,
|
|
15
|
+
workitem,
|
|
16
|
+
wrap,
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
app = typer.Typer(
|
|
10
20
|
name="agentgov",
|
|
@@ -19,6 +29,10 @@ app.command(
|
|
|
19
29
|
app.command(
|
|
20
30
|
"install", help="Install client assets into ~/.claude/ and print managed-settings.json."
|
|
21
31
|
)(install.run)
|
|
32
|
+
app.command(
|
|
33
|
+
"uninstall",
|
|
34
|
+
help="Remove AgentGov hooks, statusline and local state (deregisters before deleting).",
|
|
35
|
+
)(uninstall.run)
|
|
22
36
|
app.add_typer(gateway.app, name="gateway")
|
|
23
37
|
app.command("wrap", help="Wrap `claude` — start a governed Claude Code session.")(wrap.run)
|
|
24
38
|
app.command("workitem", help="Bind the current session to a work item (JIRA/Notion/PRJ-...).")(
|
|
@@ -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.6"
|
|
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" }
|
|
@@ -65,6 +65,7 @@ include = [
|
|
|
65
65
|
|
|
66
66
|
[tool.ruff]
|
|
67
67
|
line-length = 100
|
|
68
|
+
extend-exclude = ["agentgov_cli/client_assets"]
|
|
68
69
|
target-version = "py310"
|
|
69
70
|
[tool.ruff.lint]
|
|
70
71
|
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "RUF"]
|
|
@@ -73,6 +74,9 @@ select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "RUF"]
|
|
|
73
74
|
ignore = ["B008"]
|
|
74
75
|
|
|
75
76
|
[tool.mypy]
|
|
77
|
+
# agentgov_cli/client_assets is a build artifact vendored by hatch_build.py;
|
|
78
|
+
# the real source lives at the repo root and is checked there.
|
|
79
|
+
exclude = ['agentgov_cli/client_assets/']
|
|
76
80
|
python_version = "3.10"
|
|
77
81
|
strict = true
|
|
78
82
|
disallow_untyped_defs = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/hooks/pretooluse_pathguard.py
RENAMED
|
File without changes
|
{agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/hooks/sessionstart_register.py
RENAMED
|
File without changes
|
{agentgov_cli-0.1.5 → agentgov_cli-0.1.6}/agentgov_cli/client_assets/managed-settings.json.tmpl
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|