agentgov-cli 0.1.9__tar.gz → 0.1.12__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.9 → agentgov_cli-0.1.12}/.gitignore +3 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/PKG-INFO +2 -2
- agentgov_cli-0.1.12/agentgov_cli/asset_manifest.py +205 -0
- agentgov_cli-0.1.12/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +792 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/statusline/agentgov_statusline.sh +43 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/doctor.py +71 -2
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/install.py +76 -48
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/workitem.py +86 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/pyproject.toml +4 -4
- agentgov_cli-0.1.9/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +0 -206
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/README.md +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/__init__.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/commands/workitem.md +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/hooks/pretooluse_pathguard.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/hooks/sessionstart_register.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/managed-settings.json.tmpl +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/__init__.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/gateway.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/login.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/register_device.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/status.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/uninstall.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/wrap.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/gateway_process.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/loopback.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/main.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/port_resolver.py +0 -0
- {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/hatch_build.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentgov-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
4
4
|
Summary: AgentGov CLI — wrap Claude Code, bind work items, check gateway health.
|
|
5
5
|
Author: AgentGov Contributors
|
|
6
6
|
License: Apache-2.0
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
8
|
Requires-Dist: httpx>=0.27.2
|
|
9
9
|
Requires-Dist: pydantic>=2.9.0
|
|
10
10
|
Requires-Dist: pynacl>=1.5.0
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Provenance for the client assets AgentGov installs into ~/.claude/.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS
|
|
4
|
+
---------------
|
|
5
|
+
Nothing in the product could tell "installed" from "current", and five separate
|
|
6
|
+
layers all needed that one fact:
|
|
7
|
+
|
|
8
|
+
* `agentgov install` skipped the hooks directory whenever it already existed,
|
|
9
|
+
so the documented upgrade (`pip install -U` + `agentgov install`) refreshed
|
|
10
|
+
the CLI and left the OLD hooks in place. Every hook fix we shipped stopped
|
|
11
|
+
at the machines that already had AgentGov.
|
|
12
|
+
* The setup page's Step 0 preflight printed `READY -> nothing to do` on the
|
|
13
|
+
strength of three hook REGISTRATIONS, never asking whether the registered
|
|
14
|
+
files were current — so it actively confirmed a broken machine as healthy.
|
|
15
|
+
* `agentgov doctor` checked presence, executability and registration. Never
|
|
16
|
+
content.
|
|
17
|
+
* `smoke-package` only ever installed into a pristine HOME, so the upgrade
|
|
18
|
+
path that every existing user takes was untested.
|
|
19
|
+
|
|
20
|
+
That combination shipped a `UserPromptSubmit` hook whose block message went
|
|
21
|
+
only to stderr — which the VS Code extension does not surface — so a governed
|
|
22
|
+
session with no work item bound erased the prompt and displayed nothing at all.
|
|
23
|
+
The fix existed in the repo for a while and could not reach anyone.
|
|
24
|
+
|
|
25
|
+
A recorded hash per installed file answers the question all five were asking.
|
|
26
|
+
|
|
27
|
+
WHY THE HASHES AND NOT JUST A VERSION STRING
|
|
28
|
+
--------------------------------------------
|
|
29
|
+
A version alone cannot separate the two reasons an installed file differs from
|
|
30
|
+
the shipped one:
|
|
31
|
+
|
|
32
|
+
STALE we wrote it, and a newer version has since shipped -> overwrite,
|
|
33
|
+
silently, because it is ours and it is out of date.
|
|
34
|
+
MODIFIED it matches neither what we shipped nor what we last wrote -> the
|
|
35
|
+
developer edited it. Back it up before overwriting.
|
|
36
|
+
|
|
37
|
+
Getting that distinction wrong in either direction is bad: silently eating a
|
|
38
|
+
local edit, or burying every upgrade in `.bak` files and scary warnings.
|
|
39
|
+
|
|
40
|
+
WHERE IT LIVES
|
|
41
|
+
--------------
|
|
42
|
+
Inside `~/.claude/agentgov-hooks/`, alongside the assets it describes, so that
|
|
43
|
+
`agentgov uninstall` (which rmtrees that directory) disposes of it without
|
|
44
|
+
needing to know it exists. The leading dot keeps it out of the `*.py` globs
|
|
45
|
+
that install and doctor use to chmod and enumerate hooks.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import hashlib
|
|
51
|
+
import json
|
|
52
|
+
import sys
|
|
53
|
+
from datetime import UTC, datetime
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
MANIFEST_NAME = ".agentgov-manifest.json"
|
|
57
|
+
|
|
58
|
+
# Asset source is discovered relative to the installed package (client_assets is
|
|
59
|
+
# a sibling of the CLI package). In wheel installs we ship it as package data;
|
|
60
|
+
# in a source checkout we look up-tree.
|
|
61
|
+
ASSET_ROOTS = [
|
|
62
|
+
# Vendored into the package at build time by cli/hatch_build.py. This is the
|
|
63
|
+
# path that works for pip/pipx installs: it is relative to __file__, so it
|
|
64
|
+
# survives relocated venvs and `pip install --user`, unlike sys.prefix.
|
|
65
|
+
Path(__file__).resolve().parent / "client_assets",
|
|
66
|
+
# Source checkout: client_assets/ sits at the repo root (CLAUDE.md §4).
|
|
67
|
+
Path(__file__).resolve().parent.parent.parent / "client_assets",
|
|
68
|
+
# Legacy shared-data location, kept so older installs keep working.
|
|
69
|
+
Path(sys.prefix) / "share" / "agentgov" / "client_assets",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# Which source subdirectory lands in which directory under ~/.claude, and the
|
|
73
|
+
# glob that selects the files install is responsible for. `managed-settings.json.tmpl`
|
|
74
|
+
# sits at the source root and is deliberately absent: it is rendered into
|
|
75
|
+
# ~/.agentgov/ for an admin to place, never copied into ~/.claude.
|
|
76
|
+
_ASSET_DIRS: tuple[tuple[str, str, str], ...] = (
|
|
77
|
+
("hooks", "agentgov-hooks", "*.py"),
|
|
78
|
+
("statusline", "agentgov-statusline", "*.sh"),
|
|
79
|
+
("commands", "commands", "*.md"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def managed_assets(src: Path) -> list[tuple[str, str]]:
|
|
84
|
+
"""(source-relative path, destination dir) for everything install owns.
|
|
85
|
+
|
|
86
|
+
Enumerated from the source tree rather than hardcoded so that adding an
|
|
87
|
+
asset makes it managed automatically. A hardcoded list is precisely what
|
|
88
|
+
let the statusline drift out of date without anything noticing — it was
|
|
89
|
+
never in whatever each layer happened to believe the asset set was.
|
|
90
|
+
"""
|
|
91
|
+
out: list[tuple[str, str]] = []
|
|
92
|
+
for sub, dest_dir, pattern in _ASSET_DIRS:
|
|
93
|
+
for path in sorted((src / sub).glob(pattern)):
|
|
94
|
+
out.append((f"{sub}/{path.name}", dest_dir))
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def find_assets() -> Path:
|
|
99
|
+
for root in ASSET_ROOTS:
|
|
100
|
+
if (root / "hooks").is_dir():
|
|
101
|
+
return root
|
|
102
|
+
raise FileNotFoundError(f"client_assets/ not found in any of: {ASSET_ROOTS}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cli_version() -> str:
|
|
106
|
+
try:
|
|
107
|
+
from importlib.metadata import version
|
|
108
|
+
|
|
109
|
+
return version("agentgov-cli")
|
|
110
|
+
except Exception:
|
|
111
|
+
return "unknown"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sha256_file(path: Path) -> str:
|
|
115
|
+
h = hashlib.sha256()
|
|
116
|
+
with path.open("rb") as fh:
|
|
117
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
118
|
+
h.update(chunk)
|
|
119
|
+
return h.hexdigest()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def hooks_dir(home: Path | None = None) -> Path:
|
|
123
|
+
return (home or Path.home()) / ".claude" / "agentgov-hooks"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def manifest_path(home: Path | None = None) -> Path:
|
|
127
|
+
return hooks_dir(home) / MANIFEST_NAME
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def installed_path(rel: str, dest_dir: str, home: Path | None = None) -> Path:
|
|
131
|
+
"""Where a shipped asset lands on disk."""
|
|
132
|
+
return (home or Path.home()) / ".claude" / dest_dir / Path(rel).name
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load(home: Path | None = None) -> dict[str, str]:
|
|
136
|
+
"""Recorded hash per asset from the last install. Empty when absent.
|
|
137
|
+
|
|
138
|
+
Absent is the normal state for every install predating this file, so it
|
|
139
|
+
must degrade quietly rather than raise.
|
|
140
|
+
"""
|
|
141
|
+
path = manifest_path(home)
|
|
142
|
+
try:
|
|
143
|
+
data = json.loads(path.read_text())
|
|
144
|
+
except Exception:
|
|
145
|
+
return {}
|
|
146
|
+
assets = data.get("assets")
|
|
147
|
+
return assets if isinstance(assets, dict) else {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_version(home: Path | None = None) -> str:
|
|
151
|
+
try:
|
|
152
|
+
data = json.loads(manifest_path(home).read_text())
|
|
153
|
+
except Exception:
|
|
154
|
+
return ""
|
|
155
|
+
return str(data.get("version") or "")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def save(assets: dict[str, str], home: Path | None = None) -> None:
|
|
159
|
+
path = manifest_path(home)
|
|
160
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
path.write_text(
|
|
162
|
+
json.dumps(
|
|
163
|
+
{
|
|
164
|
+
"version": cli_version(),
|
|
165
|
+
"installed_at": datetime.now(UTC).isoformat(),
|
|
166
|
+
"assets": assets,
|
|
167
|
+
},
|
|
168
|
+
indent=2,
|
|
169
|
+
)
|
|
170
|
+
+ "\n"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def classify(rel: str, dest_dir: str, src: Path, home: Path | None = None) -> str:
|
|
175
|
+
"""One of MISSING | OK | STALE | MODIFIED for a single installed asset."""
|
|
176
|
+
shipped = src / rel
|
|
177
|
+
target = installed_path(rel, dest_dir, home)
|
|
178
|
+
if not target.exists():
|
|
179
|
+
return "MISSING"
|
|
180
|
+
if not shipped.exists():
|
|
181
|
+
# Nothing to compare against — treat as fine rather than invent a fault.
|
|
182
|
+
return "OK"
|
|
183
|
+
current = sha256_file(target)
|
|
184
|
+
if current == sha256_file(shipped):
|
|
185
|
+
return "OK"
|
|
186
|
+
recorded = load(home).get(rel)
|
|
187
|
+
# No manifest (pre-manifest install) means we cannot prove we wrote it, so
|
|
188
|
+
# it is reported as MODIFIED and install will back it up. That is the safe
|
|
189
|
+
# direction: it happens at most once per machine, and it never destroys an
|
|
190
|
+
# edit we cannot account for.
|
|
191
|
+
return "STALE" if recorded and recorded == current else "MODIFIED"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def drift(home: Path | None = None) -> list[tuple[str, str]]:
|
|
195
|
+
"""(relative path, state) for every managed asset that is not OK."""
|
|
196
|
+
try:
|
|
197
|
+
src = find_assets()
|
|
198
|
+
except FileNotFoundError:
|
|
199
|
+
return []
|
|
200
|
+
out: list[tuple[str, str]] = []
|
|
201
|
+
for rel, dest_dir in managed_assets(src):
|
|
202
|
+
state = classify(rel, dest_dir, src, home)
|
|
203
|
+
if state != "OK":
|
|
204
|
+
out.append((rel, state))
|
|
205
|
+
return out
|