data-olympus 0.3.0__py3-none-any.whl
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.
- data_olympus/__init__.py +14 -0
- data_olympus/_bin/_kb_detect_workspace.sh +57 -0
- data_olympus/_bin/_kb_enforce.py +619 -0
- data_olympus/_bin/_kb_fallback.py +361 -0
- data_olympus/_bin/kb +957 -0
- data_olympus/_bin/kb-enforce-hook +337 -0
- data_olympus/_bin/opencode/data-olympus-gate.ts +102 -0
- data_olympus/audit_log.py +275 -0
- data_olympus/audit_trailers.py +42 -0
- data_olympus/auth.py +139 -0
- data_olympus/cli/__init__.py +1 -0
- data_olympus/cli/import_cmd.py +115 -0
- data_olympus/cli/indexgen.py +60 -0
- data_olympus/cli/main.py +151 -0
- data_olympus/cli/report_cmd.py +181 -0
- data_olympus/config.py +261 -0
- data_olympus/cooccurrence.py +393 -0
- data_olympus/dedup.py +57 -0
- data_olympus/durable.py +51 -0
- data_olympus/embeddings.py +317 -0
- data_olympus/enforce_policy.py +297 -0
- data_olympus/format/__init__.py +16 -0
- data_olympus/format/document.py +39 -0
- data_olympus/format/frontmatter.py +35 -0
- data_olympus/format/lint.py +92 -0
- data_olympus/format/validate.py +71 -0
- data_olympus/git_ops.py +397 -0
- data_olympus/health.py +114 -0
- data_olympus/importer/__init__.py +13 -0
- data_olympus/importer/adr.py +286 -0
- data_olympus/importer/flat.py +170 -0
- data_olympus/importer/model.py +106 -0
- data_olympus/importer/okf.py +192 -0
- data_olympus/importer/run.py +416 -0
- data_olympus/importer/stamp.py +227 -0
- data_olympus/index.py +1745 -0
- data_olympus/markdown_parse.py +103 -0
- data_olympus/models.py +480 -0
- data_olympus/onboarding.py +131 -0
- data_olympus/onboarding_inflight.py +137 -0
- data_olympus/onboarding_playbook.py +99 -0
- data_olympus/pending.py +533 -0
- data_olympus/principals.py +168 -0
- data_olympus/prompts.py +35 -0
- data_olympus/push_queue.py +261 -0
- data_olympus/query_expansion.py +200 -0
- data_olympus/rate_limit.py +81 -0
- data_olympus/refresh.py +329 -0
- data_olympus/report.py +133 -0
- data_olympus/rest_api.py +845 -0
- data_olympus/safe_id.py +36 -0
- data_olympus/search_gate.py +64 -0
- data_olympus/search_shortcut.py +146 -0
- data_olympus/server.py +1115 -0
- data_olympus/session_metrics.py +303 -0
- data_olympus/setup_wizard.py +751 -0
- data_olympus/thin_pointer.py +20 -0
- data_olympus/tools_audit.py +41 -0
- data_olympus/tools_enforce.py +198 -0
- data_olympus/tools_onboarding.py +585 -0
- data_olympus/tools_read.py +230 -0
- data_olympus/tools_write.py +878 -0
- data_olympus/trigram.py +126 -0
- data_olympus/viewer/__init__.py +1 -0
- data_olympus/viewer/generator.py +375 -0
- data_olympus/worktrees.py +147 -0
- data_olympus/write_gate.py +382 -0
- data_olympus-0.3.0.dist-info/METADATA +97 -0
- data_olympus-0.3.0.dist-info/RECORD +73 -0
- data_olympus-0.3.0.dist-info/WHEEL +4 -0
- data_olympus-0.3.0.dist-info/entry_points.txt +3 -0
- data_olympus-0.3.0.dist-info/licenses/LICENSE +202 -0
- data_olympus-0.3.0.dist-info/licenses/NOTICE +8 -0
data_olympus/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""data-olympus: governance-grade knowledge-base format, CLI, and MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError
|
|
6
|
+
from importlib.metadata import version as _pkg_version
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
# Single source of truth: the installed distribution's version (declared in
|
|
10
|
+
# pyproject [project].version). Reading it here keeps `data_olympus.__version__`
|
|
11
|
+
# from drifting out of sync with the packaging metadata the release chain tags on.
|
|
12
|
+
__version__ = _pkg_version("data-olympus")
|
|
13
|
+
except PackageNotFoundError: # pragma: no cover - only when running from a raw tree
|
|
14
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced by: hooks (agent-hooks/*) and bin/kb onboarding-check.
|
|
3
|
+
# Resolves CWD to (WORKSPACE, COMPONENT?, WORKSPACE_REMOTE_URL?, COMPONENT_REMOTE_URL?)
|
|
4
|
+
# via upward path traversal.
|
|
5
|
+
|
|
6
|
+
detect_workspace_and_component() {
|
|
7
|
+
local start="${1:-$PWD}"
|
|
8
|
+
local cwd
|
|
9
|
+
cwd=$(cd "$start" && pwd -P) || return 1
|
|
10
|
+
|
|
11
|
+
# Walk up until we find a directory directly under the workspaces root.
|
|
12
|
+
# The root is configurable via KB_WORKSPACES_ROOT (default: ~/projects).
|
|
13
|
+
local workspaces_root="${KB_WORKSPACES_ROOT:-$HOME/projects}"
|
|
14
|
+
local workspace_root=""
|
|
15
|
+
local cur="$cwd"
|
|
16
|
+
while [ "$cur" != "/" ]; do
|
|
17
|
+
if [ "$(dirname "$cur")" = "$workspaces_root" ]; then
|
|
18
|
+
workspace_root="$cur"
|
|
19
|
+
break
|
|
20
|
+
fi
|
|
21
|
+
cur=$(dirname "$cur")
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
if [ -z "$workspace_root" ]; then
|
|
25
|
+
return 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
WORKSPACE=$(basename "$workspace_root")
|
|
29
|
+
WORKSPACE_REMOTE_URL=""
|
|
30
|
+
COMPONENT=""
|
|
31
|
+
COMPONENT_REMOTE_URL=""
|
|
32
|
+
|
|
33
|
+
if [ -d "$workspace_root/.git" ]; then
|
|
34
|
+
WORKSPACE_REMOTE_URL=$(git -C "$workspace_root" remote get-url origin 2>/dev/null || true)
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ "$cwd" != "$workspace_root" ]; then
|
|
38
|
+
local rel="${cwd#$workspace_root/}"
|
|
39
|
+
local probe="$workspace_root"
|
|
40
|
+
local seg
|
|
41
|
+
local IFS_save="$IFS"
|
|
42
|
+
IFS=/
|
|
43
|
+
for seg in $rel; do
|
|
44
|
+
IFS="$IFS_save"
|
|
45
|
+
probe="$probe/$seg"
|
|
46
|
+
if [ -d "$probe/.git" ]; then
|
|
47
|
+
COMPONENT=$(basename "$probe")
|
|
48
|
+
COMPONENT_REMOTE_URL=$(git -C "$probe" remote get-url origin 2>/dev/null || true)
|
|
49
|
+
break
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
IFS="$IFS_save"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
export WORKSPACE COMPONENT WORKSPACE_REMOTE_URL COMPONENT_REMOTE_URL
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""kb enforce installer: per-agent providers for the data-olympus enforcement gate.
|
|
3
|
+
|
|
4
|
+
Each provider idempotently installs/removes MARKER-tagged enforcement wiring for
|
|
5
|
+
one coding agent, backs up before editing, and reports status/doctor. Subcommands:
|
|
6
|
+
install | uninstall | status | doctor. Select an agent with --agent.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import urllib.request
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
MARKER = "data-olympus-enforce"
|
|
20
|
+
SHIM_VERSION = "1"
|
|
21
|
+
HOOK_BIN = str(Path(__file__).resolve().parent / "kb-enforce-hook")
|
|
22
|
+
PLUGIN_SRC = Path(__file__).resolve().parent / "opencode" / "data-olympus-gate.ts"
|
|
23
|
+
PLUGIN_NAME = "data-olympus-gate.ts"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _home() -> str:
|
|
27
|
+
override = os.getenv("KB_ENFORCE_HOME")
|
|
28
|
+
return override if override else os.path.expanduser("~")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _backup(target: Path) -> None:
|
|
32
|
+
if target.exists():
|
|
33
|
+
ts = time.strftime("%Y%m%d-%H%M%S")
|
|
34
|
+
shutil.copy2(target, target.with_suffix(target.suffix + f".kb-bak-{ts}"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_json(target: Path) -> dict:
|
|
38
|
+
if target.exists() and target.read_text().strip():
|
|
39
|
+
return json.loads(target.read_text())
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _doctor_endpoint() -> tuple[bool, str]:
|
|
44
|
+
endpoint = os.getenv("KB_ENDPOINT", "http://localhost:8080")
|
|
45
|
+
try:
|
|
46
|
+
with urllib.request.urlopen(f"{endpoint}/api/v1/health", timeout=5) as r:
|
|
47
|
+
ok = r.status == 200
|
|
48
|
+
except Exception as exc: # noqa: BLE001 - report any failure
|
|
49
|
+
return False, f"cannot reach {endpoint}: {exc}"
|
|
50
|
+
return ok, f"endpoint {endpoint} reachable={ok}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _hook_bin_in_worktree(hook_bin: str) -> bool:
|
|
54
|
+
"""True when the dispatcher path resolves inside a git worktree checkout
|
|
55
|
+
(a `.worktrees/` or `.claude/worktrees/` segment). An install performed from
|
|
56
|
+
such a checkout dangles after the worktree is pruned and then silently fails
|
|
57
|
+
open, so doctor warns about it."""
|
|
58
|
+
parts = Path(hook_bin).resolve().parts
|
|
59
|
+
if ".worktrees" in parts:
|
|
60
|
+
return True
|
|
61
|
+
# `.claude/worktrees/<...>`: a `.claude` segment immediately followed by
|
|
62
|
+
# `worktrees`.
|
|
63
|
+
return any(
|
|
64
|
+
parts[i] == ".claude" and parts[i + 1] == "worktrees"
|
|
65
|
+
for i in range(len(parts) - 1)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _doctor_hook_bin() -> tuple[bool, list[str]]:
|
|
70
|
+
"""Verify the installed hook dispatcher exists and is executable, and warn
|
|
71
|
+
when it resolves inside a worktree. Returns (ok, messages)."""
|
|
72
|
+
msgs: list[str] = []
|
|
73
|
+
exists = os.path.isfile(HOOK_BIN)
|
|
74
|
+
executable = exists and os.access(HOOK_BIN, os.X_OK)
|
|
75
|
+
if not exists:
|
|
76
|
+
msgs.append(f"hook command MISSING at {HOOK_BIN}")
|
|
77
|
+
elif not executable:
|
|
78
|
+
msgs.append(f"hook command present but NOT executable at {HOOK_BIN}")
|
|
79
|
+
else:
|
|
80
|
+
msgs.append(f"hook command present and executable at {HOOK_BIN}")
|
|
81
|
+
if _hook_bin_in_worktree(HOOK_BIN):
|
|
82
|
+
msgs.append(
|
|
83
|
+
f"WARNING: hook command resolves inside a worktree ({HOOK_BIN}); this "
|
|
84
|
+
"install will dangle and silently fail open once the worktree is "
|
|
85
|
+
"pruned. Re-run `kb enforce install` from the main checkout."
|
|
86
|
+
)
|
|
87
|
+
return executable, msgs
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _managed_versions_in_hooks(data: dict) -> set:
|
|
91
|
+
"""The set of managed-marker versions present in a JSON hooks map."""
|
|
92
|
+
return {
|
|
93
|
+
h[MARKER]
|
|
94
|
+
for blocks in data.get("hooks", {}).values()
|
|
95
|
+
for block in blocks
|
|
96
|
+
for h in block.get("hooks", [])
|
|
97
|
+
if MARKER in h
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class HookFileProvider:
|
|
102
|
+
"""A provider that writes MARKER-tagged hook entries into a JSON hooks map
|
|
103
|
+
inside a target file (the map lives under the top-level 'hooks' key).
|
|
104
|
+
|
|
105
|
+
events: list of (event_name, dispatcher_mode, matcher_or_None).
|
|
106
|
+
dialect: passed to kb-enforce-hook as '--dialect <dialect>'; omitted when
|
|
107
|
+
'claude' so Claude's slice-1 command form ('<hook> <mode>') is preserved.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
tier = "hard"
|
|
111
|
+
|
|
112
|
+
def __init__(self, name: str, default_target: Path, events: list,
|
|
113
|
+
dialect: str = "claude", note: str = "") -> None:
|
|
114
|
+
self.name = name
|
|
115
|
+
self._default_target = default_target
|
|
116
|
+
self._events = events
|
|
117
|
+
self._dialect = dialect
|
|
118
|
+
self._note = note
|
|
119
|
+
self.enforce_mode = "hard"
|
|
120
|
+
|
|
121
|
+
def default_target(self) -> Path:
|
|
122
|
+
return self._default_target
|
|
123
|
+
|
|
124
|
+
def _command(self, mode: str) -> str:
|
|
125
|
+
# Always thread --agent so the consult audit records the correct
|
|
126
|
+
# per-agent identity. --dialect is still suppressed for claude (default)
|
|
127
|
+
# to keep its slice-1 command form.
|
|
128
|
+
dialect = "" if self._dialect == "claude" else f" --dialect {self._dialect}"
|
|
129
|
+
return f"{HOOK_BIN} {mode}{dialect} --agent {self.name}"
|
|
130
|
+
|
|
131
|
+
def _managed_block(self, mode: str, matcher: str | None) -> dict:
|
|
132
|
+
entry = {"type": "command", "command": self._command(mode), MARKER: SHIM_VERSION}
|
|
133
|
+
block: dict = {"hooks": [entry]}
|
|
134
|
+
if matcher is not None:
|
|
135
|
+
block["matcher"] = matcher
|
|
136
|
+
return block
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _strip_managed(hooks: dict) -> dict:
|
|
140
|
+
out: dict = {}
|
|
141
|
+
for event, blocks in hooks.items():
|
|
142
|
+
kept = []
|
|
143
|
+
for block in blocks:
|
|
144
|
+
kh = [h for h in block.get("hooks", []) if MARKER not in h]
|
|
145
|
+
if kh:
|
|
146
|
+
nb = dict(block)
|
|
147
|
+
nb["hooks"] = kh
|
|
148
|
+
kept.append(nb)
|
|
149
|
+
if kept:
|
|
150
|
+
out[event] = kept
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
def install(self, target: Path) -> int:
|
|
154
|
+
data = _load_json(target)
|
|
155
|
+
_backup(target)
|
|
156
|
+
hooks = self._strip_managed(data.get("hooks", {}))
|
|
157
|
+
events = self._events
|
|
158
|
+
if self.enforce_mode == "soft":
|
|
159
|
+
events = [(e, m, mt) for (e, m, mt) in self._events if m != "pre-tool"]
|
|
160
|
+
for event, mode, matcher in events:
|
|
161
|
+
hooks.setdefault(event, []).append(self._managed_block(mode, matcher))
|
|
162
|
+
data["hooks"] = hooks
|
|
163
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
target.write_text(json.dumps(data, indent=2) + "\n")
|
|
165
|
+
print(
|
|
166
|
+
f"installed data-olympus enforcement (v{SHIM_VERSION}) into {target} "
|
|
167
|
+
f"[{self.name}, tier={self.tier}]"
|
|
168
|
+
)
|
|
169
|
+
if self._note:
|
|
170
|
+
print(self._note)
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
def uninstall(self, target: Path) -> int:
|
|
174
|
+
data = _load_json(target)
|
|
175
|
+
if "hooks" not in data:
|
|
176
|
+
print("nothing to uninstall")
|
|
177
|
+
return 0
|
|
178
|
+
_backup(target)
|
|
179
|
+
data["hooks"] = self._strip_managed(data["hooks"])
|
|
180
|
+
if not data["hooks"]:
|
|
181
|
+
del data["hooks"]
|
|
182
|
+
target.write_text(json.dumps(data, indent=2) + "\n")
|
|
183
|
+
print(f"uninstalled data-olympus enforcement from {target} [{self.name}]")
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
def status(self, target: Path) -> int:
|
|
187
|
+
data = _load_json(target)
|
|
188
|
+
versions = _managed_versions_in_hooks(data)
|
|
189
|
+
if not versions:
|
|
190
|
+
print(f"{self.name}: not installed")
|
|
191
|
+
return 0
|
|
192
|
+
stale = " (stale; run `kb enforce install`)" if SHIM_VERSION not in versions else ""
|
|
193
|
+
print(f"{self.name}: installed, tier={self.tier}, versions={sorted(versions)}{stale}")
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
def doctor(self, target: Path) -> int:
|
|
197
|
+
"""Doctor now checks three things beyond endpoint reachability:
|
|
198
|
+
the managed marker/version is present in the LIVE settings file, the hook
|
|
199
|
+
dispatcher exists and is executable, and the dispatcher is not installed
|
|
200
|
+
from a worktree (which dangles after pruning and fails open)."""
|
|
201
|
+
ok_ep, msg_ep = _doctor_endpoint()
|
|
202
|
+
print(f"doctor [{self.name}]: {msg_ep}")
|
|
203
|
+
|
|
204
|
+
data = _load_json(target)
|
|
205
|
+
versions = _managed_versions_in_hooks(data)
|
|
206
|
+
if not versions:
|
|
207
|
+
print(f"doctor [{self.name}]: managed hook NOT installed in {target}")
|
|
208
|
+
ok_marker = False
|
|
209
|
+
elif SHIM_VERSION not in versions:
|
|
210
|
+
print(f"doctor [{self.name}]: managed hook in {target} is STALE "
|
|
211
|
+
f"(found {sorted(versions)}, want {SHIM_VERSION}); run "
|
|
212
|
+
"`kb enforce install`")
|
|
213
|
+
ok_marker = False
|
|
214
|
+
else:
|
|
215
|
+
print(f"doctor [{self.name}]: managed hook v{SHIM_VERSION} present in {target}")
|
|
216
|
+
ok_marker = True
|
|
217
|
+
|
|
218
|
+
ok_bin, bin_msgs = _doctor_hook_bin()
|
|
219
|
+
for m in bin_msgs:
|
|
220
|
+
print(f"doctor [{self.name}]: {m}")
|
|
221
|
+
|
|
222
|
+
# A worktree-installed hook is a real problem even if the file is currently
|
|
223
|
+
# present and executable, so fail doctor on it.
|
|
224
|
+
ok_worktree = not _hook_bin_in_worktree(HOOK_BIN)
|
|
225
|
+
return 0 if (ok_ep and ok_marker and ok_bin and ok_worktree) else 1
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _claude_provider() -> HookFileProvider:
|
|
229
|
+
return HookFileProvider(
|
|
230
|
+
name="claude-code",
|
|
231
|
+
default_target=Path(_home()) / ".claude" / "settings.json",
|
|
232
|
+
events=[
|
|
233
|
+
("SessionStart", "session-start", None),
|
|
234
|
+
("UserPromptSubmit", "user-prompt", None),
|
|
235
|
+
# Anchored: the matcher is a regex, so an un-anchored alternation
|
|
236
|
+
# substring-matches unrelated tools (e.g. "Bash" matches "BashOutput",
|
|
237
|
+
# "Edit" matches "NotebookEditOther"). ^(...)$ gates exactly these.
|
|
238
|
+
("PreToolUse", "pre-tool", "^(Edit|Write|MultiEdit|NotebookEdit|Bash)$"),
|
|
239
|
+
("Stop", "stop", None),
|
|
240
|
+
],
|
|
241
|
+
dialect="claude",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
CODEX_TRUST_NOTE = (
|
|
246
|
+
"NOTE (codex): Codex requires this hook to be trusted before it runs. On the "
|
|
247
|
+
"next `codex` start you will be prompted to trust it, or run codex with "
|
|
248
|
+
"`--dangerously-bypass-hook-trust` for vetted automation. The trust hash is "
|
|
249
|
+
"persisted under [hooks.state] in ~/.codex/config.toml."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _codex_provider() -> HookFileProvider:
|
|
254
|
+
return HookFileProvider(
|
|
255
|
+
name="codex",
|
|
256
|
+
default_target=Path(_home()) / ".codex" / "hooks.json",
|
|
257
|
+
events=[
|
|
258
|
+
("SessionStart", "session-start", None),
|
|
259
|
+
("UserPromptSubmit", "user-prompt", None),
|
|
260
|
+
# Anchored (see the claude provider): keep the alternation exact so
|
|
261
|
+
# "Bash" does not also gate "BashOutput".
|
|
262
|
+
("PreToolUse", "pre-tool", "^(Edit|Write|MultiEdit|Bash)$"),
|
|
263
|
+
("Stop", "stop", None),
|
|
264
|
+
],
|
|
265
|
+
dialect="claude", # Codex shares Claude's exit-2 deny contract
|
|
266
|
+
note=CODEX_TRUST_NOTE,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _gemini_provider() -> HookFileProvider:
|
|
271
|
+
return HookFileProvider(
|
|
272
|
+
name="gemini",
|
|
273
|
+
default_target=Path(_home()) / ".gemini" / "settings.json",
|
|
274
|
+
events=[
|
|
275
|
+
("SessionStart", "session-start", None),
|
|
276
|
+
("BeforeAgent", "user-prompt", None), # BeforeAgent carries `prompt`
|
|
277
|
+
# Anchored to avoid substring matches against other Gemini tools.
|
|
278
|
+
("BeforeTool", "pre-tool", "^(write_file|replace|run_shell_command)$"),
|
|
279
|
+
("Stop", "stop", None),
|
|
280
|
+
],
|
|
281
|
+
dialect="gemini",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class OpenCodeProvider:
|
|
286
|
+
"""OpenCode provider. Unlike the hard shell-hook agents, OpenCode is wired by
|
|
287
|
+
dropping a managed TypeScript plugin file into the plugin directory. Install
|
|
288
|
+
copies the bundled template; uninstall removes it only if it still carries the
|
|
289
|
+
managed marker, so operator-authored plugins in the same dir are untouched.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
name = "opencode"
|
|
293
|
+
tier = "hard"
|
|
294
|
+
|
|
295
|
+
def default_target(self) -> Path:
|
|
296
|
+
return Path(_home()) / ".config" / "opencode" / "plugin"
|
|
297
|
+
|
|
298
|
+
def install(self, target: Path) -> int:
|
|
299
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
300
|
+
dest = target / PLUGIN_NAME
|
|
301
|
+
if dest.exists():
|
|
302
|
+
_backup(dest)
|
|
303
|
+
shutil.copy2(PLUGIN_SRC, dest)
|
|
304
|
+
print(
|
|
305
|
+
f"installed data-olympus enforcement (v{SHIM_VERSION}) into {dest} "
|
|
306
|
+
f"[opencode, tier=hard]"
|
|
307
|
+
)
|
|
308
|
+
return 0
|
|
309
|
+
|
|
310
|
+
def uninstall(self, target: Path) -> int:
|
|
311
|
+
dest = target / PLUGIN_NAME
|
|
312
|
+
if dest.exists() and "data-olympus-enforce (managed)" in dest.read_text():
|
|
313
|
+
dest.unlink()
|
|
314
|
+
print(f"uninstalled data-olympus enforcement from {dest} [opencode]")
|
|
315
|
+
else:
|
|
316
|
+
print("nothing to uninstall")
|
|
317
|
+
return 0
|
|
318
|
+
|
|
319
|
+
def status(self, target: Path) -> int:
|
|
320
|
+
import re
|
|
321
|
+
dest = target / PLUGIN_NAME
|
|
322
|
+
if not (dest.exists() and "data-olympus-enforce (managed)" in dest.read_text()):
|
|
323
|
+
print("opencode: not installed")
|
|
324
|
+
return 0
|
|
325
|
+
m = re.search(r"data-olympus-enforce \(managed\) v(\d+)", dest.read_text())
|
|
326
|
+
ver = m.group(1) if m else "?"
|
|
327
|
+
stale = " (stale; run `kb enforce install`)" if ver != SHIM_VERSION else ""
|
|
328
|
+
print(f"opencode: installed, tier=hard, versions=['{ver}']{stale}")
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
def doctor(self, target: Path) -> int:
|
|
332
|
+
ok_ep, msg = _doctor_endpoint()
|
|
333
|
+
print(f"doctor [opencode]: {msg}")
|
|
334
|
+
dest = target / PLUGIN_NAME
|
|
335
|
+
installed = dest.exists() and "data-olympus-enforce (managed)" in dest.read_text()
|
|
336
|
+
if installed:
|
|
337
|
+
print(f"doctor [opencode]: managed plugin present at {dest}")
|
|
338
|
+
else:
|
|
339
|
+
print(f"doctor [opencode]: managed plugin NOT installed at {dest}")
|
|
340
|
+
# The plugin source lives beside this installer; warn if it (and so the
|
|
341
|
+
# install source) resolves inside a worktree.
|
|
342
|
+
ok_worktree = not _hook_bin_in_worktree(str(PLUGIN_SRC))
|
|
343
|
+
if not ok_worktree:
|
|
344
|
+
print(f"doctor [opencode]: WARNING: plugin source resolves inside a "
|
|
345
|
+
f"worktree ({PLUGIN_SRC}); re-install from the main checkout.")
|
|
346
|
+
return 0 if (ok_ep and installed and ok_worktree) else 1
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
IBEGIN = "<!-- >>> data-olympus enforce (managed) >>> -->"
|
|
350
|
+
IEND = "<!-- <<< data-olympus enforce <<< -->"
|
|
351
|
+
INSTRUCTIONS_BODY = f"""{IBEGIN}
|
|
352
|
+
<!-- data-olympus-enforce v{SHIM_VERSION} -->
|
|
353
|
+
## Governed decisions (data-olympus)
|
|
354
|
+
|
|
355
|
+
Before making a code or architectural decision (choosing a library, pattern,
|
|
356
|
+
migration, API design, schema, or auth approach), you MUST call the `kb_consult`
|
|
357
|
+
MCP tool for the current workspace and follow the governing rules it returns.
|
|
358
|
+
Treat those rules as authoritative, not advisory. This agent cannot be hard-gated
|
|
359
|
+
locally, so compliance is observed via the data-olympus audit log.
|
|
360
|
+
{IEND}"""
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class InstructionsProvider:
|
|
364
|
+
"""A soft provider that writes a MARKER-delimited managed block into a
|
|
365
|
+
Markdown instructions file. Unlike the hard hook providers, it cannot block a
|
|
366
|
+
tool call; it instructs the agent to consult the KB and relies on the
|
|
367
|
+
data-olympus audit log to observe compliance. The block is delimited by the
|
|
368
|
+
IBEGIN/IEND HTML comments so install is idempotent (one block, replaced on
|
|
369
|
+
re-install) and uninstall is surgical (operator content preserved).
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
tier = "soft"
|
|
373
|
+
|
|
374
|
+
def __init__(self, name: str, default_target: Path) -> None:
|
|
375
|
+
self.name = name
|
|
376
|
+
self._default_target = default_target
|
|
377
|
+
|
|
378
|
+
def default_target(self) -> Path:
|
|
379
|
+
return self._default_target
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def _strip_block(text: str) -> str:
|
|
383
|
+
if IBEGIN in text and IEND in text:
|
|
384
|
+
pre = text.split(IBEGIN, 1)[0].rstrip("\n")
|
|
385
|
+
post = text.split(IEND, 1)[1].lstrip("\n")
|
|
386
|
+
joined = "\n".join(p for p in (pre, post) if p)
|
|
387
|
+
return (joined + "\n") if joined else ""
|
|
388
|
+
return text
|
|
389
|
+
|
|
390
|
+
def install(self, target: Path) -> int:
|
|
391
|
+
existing = target.read_text() if target.exists() else ""
|
|
392
|
+
if target.exists():
|
|
393
|
+
_backup(target)
|
|
394
|
+
base = self._strip_block(existing).rstrip("\n")
|
|
395
|
+
new = (base + "\n\n" if base else "") + INSTRUCTIONS_BODY + "\n"
|
|
396
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
397
|
+
target.write_text(new)
|
|
398
|
+
print(
|
|
399
|
+
f"installed data-olympus enforcement (v{SHIM_VERSION}) into {target} "
|
|
400
|
+
f"[{self.name}, tier=soft]"
|
|
401
|
+
)
|
|
402
|
+
return 0
|
|
403
|
+
|
|
404
|
+
def uninstall(self, target: Path) -> int:
|
|
405
|
+
if not target.exists():
|
|
406
|
+
print("nothing to uninstall")
|
|
407
|
+
return 0
|
|
408
|
+
_backup(target)
|
|
409
|
+
target.write_text(self._strip_block(target.read_text()))
|
|
410
|
+
print(f"uninstalled data-olympus enforcement from {target} [{self.name}]")
|
|
411
|
+
return 0
|
|
412
|
+
|
|
413
|
+
def status(self, target: Path) -> int:
|
|
414
|
+
if target.exists() and IBEGIN in target.read_text():
|
|
415
|
+
print(f"{self.name}: installed, tier=soft, versions=['{SHIM_VERSION}']")
|
|
416
|
+
else:
|
|
417
|
+
print(f"{self.name}: not installed")
|
|
418
|
+
return 0
|
|
419
|
+
|
|
420
|
+
def doctor(self, _target: Path) -> int:
|
|
421
|
+
ok, msg = _doctor_endpoint()
|
|
422
|
+
print(f"doctor [{self.name}]: {msg}")
|
|
423
|
+
return 0 if ok else 1
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class UnsupportedProvider:
|
|
427
|
+
"""A documented-unsupported provider stub. The agent has no local hook,
|
|
428
|
+
instructions, or MCP surface we can wire, so install/uninstall/doctor report
|
|
429
|
+
the reason to stderr and exit non-zero (69 == EX_UNAVAILABLE); status reports
|
|
430
|
+
unsupported but exits 0 (querying state is not itself a failure).
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
tier = "unsupported"
|
|
434
|
+
|
|
435
|
+
def __init__(self, name: str, reason: str) -> None:
|
|
436
|
+
self.name = name
|
|
437
|
+
self._reason = reason
|
|
438
|
+
|
|
439
|
+
def default_target(self) -> Path:
|
|
440
|
+
return Path("/dev/null")
|
|
441
|
+
|
|
442
|
+
def _report(self) -> int:
|
|
443
|
+
print(f"{self.name}: unsupported -- {self._reason}", file=sys.stderr)
|
|
444
|
+
return 69 # EX_UNAVAILABLE
|
|
445
|
+
|
|
446
|
+
def install(self, _target: Path) -> int:
|
|
447
|
+
return self._report()
|
|
448
|
+
|
|
449
|
+
def uninstall(self, _target: Path) -> int:
|
|
450
|
+
return self._report()
|
|
451
|
+
|
|
452
|
+
def status(self, _target: Path) -> int:
|
|
453
|
+
print(f"{self.name}: unsupported -- {self._reason}")
|
|
454
|
+
return 0
|
|
455
|
+
|
|
456
|
+
def doctor(self, _target: Path) -> int:
|
|
457
|
+
return self._report()
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
HOOK_BEGIN = "# >>> data-olympus enforce (managed) >>>"
|
|
461
|
+
HOOK_END = "# <<< data-olympus enforce <<<"
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _git_managed_block(block: bool) -> str:
|
|
465
|
+
if block:
|
|
466
|
+
body = (
|
|
467
|
+
'data-olympus report --staged --fail-on-unverified || {\n'
|
|
468
|
+
' echo "[KB] commit blocked: governed change without a consultation '
|
|
469
|
+
'(set KB_ENFORCE_FAIL_MODE or run kb_consult)." >&2; exit 1;\n'
|
|
470
|
+
'}'
|
|
471
|
+
)
|
|
472
|
+
else:
|
|
473
|
+
body = (
|
|
474
|
+
'data-olympus report --range HEAD~1..HEAD --emit-events || true'
|
|
475
|
+
)
|
|
476
|
+
return f"{HOOK_BEGIN}\n# data-olympus-enforce v{SHIM_VERSION}\n{body}\n{HOOK_END}\n"
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class GitHookProvider:
|
|
480
|
+
name = "git"
|
|
481
|
+
tier = "detect"
|
|
482
|
+
|
|
483
|
+
def __init__(self, *, block: bool = False) -> None:
|
|
484
|
+
self._block = block
|
|
485
|
+
|
|
486
|
+
def default_target(self) -> Path:
|
|
487
|
+
hook = "pre-commit" if self._block else "post-commit"
|
|
488
|
+
return Path(".git") / "hooks" / hook
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def _strip(text: str) -> str:
|
|
492
|
+
if HOOK_BEGIN in text and HOOK_END in text:
|
|
493
|
+
pre = text.split(HOOK_BEGIN, 1)[0].rstrip("\n")
|
|
494
|
+
post = text.split(HOOK_END, 1)[1].lstrip("\n")
|
|
495
|
+
joined = "\n".join(p for p in (pre, post) if p)
|
|
496
|
+
return (joined + "\n") if joined else ""
|
|
497
|
+
return text
|
|
498
|
+
|
|
499
|
+
def install(self, target: Path) -> int:
|
|
500
|
+
existing = target.read_text() if target.exists() else ""
|
|
501
|
+
if target.exists():
|
|
502
|
+
_backup(target)
|
|
503
|
+
base = self._strip(existing).rstrip("\n")
|
|
504
|
+
if not base:
|
|
505
|
+
base = "#!/bin/sh"
|
|
506
|
+
new = base + "\n\n" + _git_managed_block(self._block) + "\n"
|
|
507
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
508
|
+
target.write_text(new)
|
|
509
|
+
target.chmod(0o755)
|
|
510
|
+
kind = "pre-commit (block)" if self._block else "post-commit (warn)"
|
|
511
|
+
print(f"installed data-olympus enforcement (v{SHIM_VERSION}) into {target} [git, {kind}]")
|
|
512
|
+
return 0
|
|
513
|
+
|
|
514
|
+
def uninstall(self, target: Path) -> int:
|
|
515
|
+
if not target.exists():
|
|
516
|
+
print("nothing to uninstall")
|
|
517
|
+
return 0
|
|
518
|
+
_backup(target)
|
|
519
|
+
target.write_text(self._strip(target.read_text()))
|
|
520
|
+
print(f"uninstalled data-olympus enforcement from {target} [git]")
|
|
521
|
+
return 0
|
|
522
|
+
|
|
523
|
+
def status(self, target: Path) -> int:
|
|
524
|
+
if target.exists() and HOOK_BEGIN in target.read_text():
|
|
525
|
+
print(f"git: installed, tier=detect, versions=['{SHIM_VERSION}']")
|
|
526
|
+
else:
|
|
527
|
+
print("git: not installed")
|
|
528
|
+
return 0
|
|
529
|
+
|
|
530
|
+
def doctor(self, target: Path) -> int:
|
|
531
|
+
ok = target.exists() and HOOK_BEGIN in target.read_text() and os.access(target, os.X_OK)
|
|
532
|
+
print(f"doctor [git]: hook {'present and executable' if ok else 'missing'} at {target}")
|
|
533
|
+
return 0 if ok else 1
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def registry() -> dict:
|
|
537
|
+
return {
|
|
538
|
+
"claude-code": _claude_provider(),
|
|
539
|
+
"codex": _codex_provider(),
|
|
540
|
+
"gemini": _gemini_provider(),
|
|
541
|
+
"opencode": OpenCodeProvider(),
|
|
542
|
+
# copilot-cli: the GitHub Copilot CLI loads "custom instructions from
|
|
543
|
+
# AGENTS.md and related files" (verified via `copilot --help`:
|
|
544
|
+
# `--no-custom-instructions` disables exactly that). No instructions file
|
|
545
|
+
# exists in ~/.copilot/ on this laptop, so we default to the documented
|
|
546
|
+
# global custom-instructions path ~/.copilot/copilot-instructions.md (one
|
|
547
|
+
# of the "related files"). Operators can override with --settings.
|
|
548
|
+
"copilot-cli": InstructionsProvider(
|
|
549
|
+
"copilot-cli", Path(_home()) / ".copilot" / "copilot-instructions.md"),
|
|
550
|
+
"copilot-ide": InstructionsProvider(
|
|
551
|
+
"copilot-ide", Path(".github/copilot-instructions.md")),
|
|
552
|
+
"antigravity": UnsupportedProvider(
|
|
553
|
+
"antigravity",
|
|
554
|
+
"no documented local hook/instructions/MCP surface as of 2026-06; "
|
|
555
|
+
"revisit when Google publishes an extensibility API"),
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def main(argv: list[str]) -> int:
|
|
560
|
+
p = argparse.ArgumentParser(prog="kb enforce")
|
|
561
|
+
p.add_argument("command", choices=["install", "uninstall", "status", "doctor"])
|
|
562
|
+
p.add_argument("--agent", default=None)
|
|
563
|
+
p.add_argument("--all", action="store_true")
|
|
564
|
+
p.add_argument("--settings", default=None)
|
|
565
|
+
p.add_argument("--block", action="store_true",
|
|
566
|
+
help="git provider: install a blocking pre-commit hook "
|
|
567
|
+
"instead of post-commit warn")
|
|
568
|
+
p.add_argument("--mode", choices=["off", "soft", "hard"], default="hard",
|
|
569
|
+
help="off uninstalls; soft installs inject-only (no blocking "
|
|
570
|
+
"gate); hard (default) installs the full gate")
|
|
571
|
+
args = p.parse_args(argv)
|
|
572
|
+
reg = registry()
|
|
573
|
+
|
|
574
|
+
if args.all or (args.command == "status" and not args.agent and not args.settings):
|
|
575
|
+
rc = 0
|
|
576
|
+
for name, provider in reg.items():
|
|
577
|
+
if args.all and getattr(provider, "tier", "") == "unsupported":
|
|
578
|
+
print(f"{name}: skipped (unsupported)")
|
|
579
|
+
continue
|
|
580
|
+
cmd = args.command
|
|
581
|
+
if args.command == "install" and args.mode == "off":
|
|
582
|
+
cmd = "uninstall"
|
|
583
|
+
elif args.command == "install" and args.mode == "soft":
|
|
584
|
+
if hasattr(provider, "enforce_mode"):
|
|
585
|
+
provider.enforce_mode = "soft"
|
|
586
|
+
else:
|
|
587
|
+
print(f"note: {name} has a fixed tier; --mode soft has no "
|
|
588
|
+
f"effect (use --mode off to uninstall)")
|
|
589
|
+
target = provider.default_target()
|
|
590
|
+
rc |= {
|
|
591
|
+
"install": provider.install, "uninstall": provider.uninstall,
|
|
592
|
+
"status": provider.status, "doctor": provider.doctor,
|
|
593
|
+
}[cmd](target)
|
|
594
|
+
return rc
|
|
595
|
+
|
|
596
|
+
agent = args.agent or "claude-code"
|
|
597
|
+
provider = GitHookProvider(block=args.block) if agent == "git" else reg.get(agent)
|
|
598
|
+
if provider is None:
|
|
599
|
+
print(f"kb enforce: unknown agent '{agent}' (known: {', '.join(sorted(reg))}, git)",
|
|
600
|
+
file=sys.stderr)
|
|
601
|
+
return 64
|
|
602
|
+
command = args.command
|
|
603
|
+
if args.command == "install" and args.mode == "off":
|
|
604
|
+
command = "uninstall"
|
|
605
|
+
elif args.command == "install" and args.mode == "soft":
|
|
606
|
+
if hasattr(provider, "enforce_mode"):
|
|
607
|
+
provider.enforce_mode = "soft"
|
|
608
|
+
else:
|
|
609
|
+
print(f"note: {agent} has a fixed tier; --mode soft has no effect "
|
|
610
|
+
f"(use --mode off to uninstall)")
|
|
611
|
+
target = Path(args.settings) if args.settings else provider.default_target()
|
|
612
|
+
return {
|
|
613
|
+
"install": provider.install, "uninstall": provider.uninstall,
|
|
614
|
+
"status": provider.status, "doctor": provider.doctor,
|
|
615
|
+
}[command](target)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
if __name__ == "__main__":
|
|
619
|
+
raise SystemExit(main(sys.argv[1:]))
|