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.
Files changed (73) hide show
  1. data_olympus/__init__.py +14 -0
  2. data_olympus/_bin/_kb_detect_workspace.sh +57 -0
  3. data_olympus/_bin/_kb_enforce.py +619 -0
  4. data_olympus/_bin/_kb_fallback.py +361 -0
  5. data_olympus/_bin/kb +957 -0
  6. data_olympus/_bin/kb-enforce-hook +337 -0
  7. data_olympus/_bin/opencode/data-olympus-gate.ts +102 -0
  8. data_olympus/audit_log.py +275 -0
  9. data_olympus/audit_trailers.py +42 -0
  10. data_olympus/auth.py +139 -0
  11. data_olympus/cli/__init__.py +1 -0
  12. data_olympus/cli/import_cmd.py +115 -0
  13. data_olympus/cli/indexgen.py +60 -0
  14. data_olympus/cli/main.py +151 -0
  15. data_olympus/cli/report_cmd.py +181 -0
  16. data_olympus/config.py +261 -0
  17. data_olympus/cooccurrence.py +393 -0
  18. data_olympus/dedup.py +57 -0
  19. data_olympus/durable.py +51 -0
  20. data_olympus/embeddings.py +317 -0
  21. data_olympus/enforce_policy.py +297 -0
  22. data_olympus/format/__init__.py +16 -0
  23. data_olympus/format/document.py +39 -0
  24. data_olympus/format/frontmatter.py +35 -0
  25. data_olympus/format/lint.py +92 -0
  26. data_olympus/format/validate.py +71 -0
  27. data_olympus/git_ops.py +397 -0
  28. data_olympus/health.py +114 -0
  29. data_olympus/importer/__init__.py +13 -0
  30. data_olympus/importer/adr.py +286 -0
  31. data_olympus/importer/flat.py +170 -0
  32. data_olympus/importer/model.py +106 -0
  33. data_olympus/importer/okf.py +192 -0
  34. data_olympus/importer/run.py +416 -0
  35. data_olympus/importer/stamp.py +227 -0
  36. data_olympus/index.py +1745 -0
  37. data_olympus/markdown_parse.py +103 -0
  38. data_olympus/models.py +480 -0
  39. data_olympus/onboarding.py +131 -0
  40. data_olympus/onboarding_inflight.py +137 -0
  41. data_olympus/onboarding_playbook.py +99 -0
  42. data_olympus/pending.py +533 -0
  43. data_olympus/principals.py +168 -0
  44. data_olympus/prompts.py +35 -0
  45. data_olympus/push_queue.py +261 -0
  46. data_olympus/query_expansion.py +200 -0
  47. data_olympus/rate_limit.py +81 -0
  48. data_olympus/refresh.py +329 -0
  49. data_olympus/report.py +133 -0
  50. data_olympus/rest_api.py +845 -0
  51. data_olympus/safe_id.py +36 -0
  52. data_olympus/search_gate.py +64 -0
  53. data_olympus/search_shortcut.py +146 -0
  54. data_olympus/server.py +1115 -0
  55. data_olympus/session_metrics.py +303 -0
  56. data_olympus/setup_wizard.py +751 -0
  57. data_olympus/thin_pointer.py +20 -0
  58. data_olympus/tools_audit.py +41 -0
  59. data_olympus/tools_enforce.py +198 -0
  60. data_olympus/tools_onboarding.py +585 -0
  61. data_olympus/tools_read.py +230 -0
  62. data_olympus/tools_write.py +878 -0
  63. data_olympus/trigram.py +126 -0
  64. data_olympus/viewer/__init__.py +1 -0
  65. data_olympus/viewer/generator.py +375 -0
  66. data_olympus/worktrees.py +147 -0
  67. data_olympus/write_gate.py +382 -0
  68. data_olympus-0.3.0.dist-info/METADATA +97 -0
  69. data_olympus-0.3.0.dist-info/RECORD +73 -0
  70. data_olympus-0.3.0.dist-info/WHEEL +4 -0
  71. data_olympus-0.3.0.dist-info/entry_points.txt +3 -0
  72. data_olympus-0.3.0.dist-info/licenses/LICENSE +202 -0
  73. data_olympus-0.3.0.dist-info/licenses/NOTICE +8 -0
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env bash
2
+ # kb-enforce-hook - data-olympus enforcement hook dispatcher.
3
+ #
4
+ # Invoked by a coding agent's hooks. Reads the hook payload as JSON on stdin and
5
+ # talks to the data-olympus REST endpoints. Modes:
6
+ # session-start warm-up note (stdout -> context)
7
+ # user-prompt POST /consult, print governing rules (stdout -> context)
8
+ # pre-tool POST /gate/check, block (exit 2) on consult_required
9
+ # stop no-op
10
+ #
11
+ # Environment:
12
+ # KB_ENDPOINT REST endpoint (default http://localhost:8080)
13
+ # KB_ENFORCE_FAIL_MODE open|closed (default open) behaviour when unreachable
14
+ # KB_AUTH_TOKEN when set, sent as Authorization: Bearer on POSTs
15
+ set -uo pipefail
16
+
17
+ KB_ENDPOINT="${KB_ENDPOINT:-http://localhost:8080}"
18
+ KB_ENFORCE_FAIL_MODE="${KB_ENFORCE_FAIL_MODE:-open}"
19
+ KB_AUTH_TOKEN="${KB_AUTH_TOKEN:-}"
20
+ MODE="${1:-}"
21
+ HERE="$(cd "$(dirname "$0")" && pwd)"
22
+
23
+ # Optional --dialect after the mode. Default "claude" keeps the current
24
+ # (claude == codex) output contract byte-for-byte; "gemini" switches deny/inject
25
+ # to its JSON-on-stdout contract.
26
+ shift || true
27
+ DIALECT="claude"
28
+ # AGENT is reported as agent_identity to the consult audit so the per-agent
29
+ # compliance view is correct. The same dispatcher serves claude/codex/gemini, so
30
+ # a hardcoded identity would mis-attribute every non-claude consultation.
31
+ AGENT="unknown"
32
+ POSITIONAL=()
33
+ while [[ $# -gt 0 ]]; do
34
+ case "$1" in
35
+ --dialect) DIALECT="${2:-claude}"; shift; shift || true ;;
36
+ --agent) AGENT="${2:-unknown}"; shift; shift || true ;;
37
+ *) POSITIONAL+=("$1"); shift ;;
38
+ esac
39
+ done
40
+
41
+ # Read entire stdin payload. Skip for resolve-workspace: it is a stdin-free
42
+ # diagnostic (resolve and print a workspace label), so reading stdin would hang.
43
+ PAYLOAD=""
44
+ if [[ "$MODE" != "resolve-workspace" ]]; then
45
+ PAYLOAD="$(cat || true)"
46
+ fi
47
+
48
+ json_field() {
49
+ # json_field <dotted.path> ; reads $PAYLOAD, prints value or empty.
50
+ # JSON null and missing keys BOTH emit an empty string (never the literal
51
+ # "None"): a null session_id must not become the ledger key "None".
52
+ python3 -c 'import json,sys
53
+ p=sys.argv[1].split(".")
54
+ try:
55
+ d=json.loads(sys.stdin.read() or "{}")
56
+ for k in p:
57
+ d=d.get(k, "") if isinstance(d, dict) else ""
58
+ if d is None:
59
+ print("")
60
+ else:
61
+ print(d if isinstance(d,str) else json.dumps(d))
62
+ except Exception:
63
+ print("")' "$1" <<<"$PAYLOAD"
64
+ }
65
+
66
+ json_field_from() {
67
+ # json_field_from <json-string> <key> ; null/missing -> empty string.
68
+ python3 -c 'import json,sys
69
+ try:
70
+ d=json.loads(sys.argv[1] or "{}")
71
+ v=d.get(sys.argv[2], "")
72
+ if v is None:
73
+ print("")
74
+ else:
75
+ print(v if isinstance(v,str) else json.dumps(v))
76
+ except Exception:
77
+ print("")' "$1" "$2"
78
+ }
79
+
80
+ resolve_workspace() {
81
+ # Echo a stable workspace label for cwd. Preference order:
82
+ # 1. the MAIN git worktree basename, which is identical from the main
83
+ # checkout and any linked worktree (so one consult clears both the
84
+ # pre-tool and pre-commit gates, and matches `report`'s default);
85
+ # 2. the workspaces-root detector (KB_WORKSPACES_ROOT), for non-git dirs;
86
+ # 3. the raw cwd, as a last resort.
87
+ # Git resolution is tried FIRST (not the detector) so the two gates never
88
+ # disagree on a linked worktree, even when KB_WORKSPACES_ROOT resolves that
89
+ # worktree to its own basename. `git worktree list` reports the main worktree
90
+ # as its first entry and is correct for separate-git-dir layouts.
91
+ local cwd="$1"
92
+ local main_wt
93
+ # First NON-bare worktree record. The main worktree is listed first, but a bare
94
+ # repo's first record is the bare git dir (marked `bare`), not a real checkout;
95
+ # skip it so the key is a worktree basename, not `repo.git`.
96
+ main_wt="$(git -C "$cwd" worktree list --porcelain 2>/dev/null | awk '
97
+ /^worktree /{p=substr($0,10); b=0}
98
+ /^bare$/{b=1}
99
+ /^$/{ if(p!="" && b==0){print p; exit} p=""; b=0 }
100
+ END{ if(p!="" && b==0) print p }')"
101
+ if [[ -n "$main_wt" ]]; then
102
+ basename "$main_wt"; return 0
103
+ fi
104
+ if [[ -r "$HERE/_kb_detect_workspace.sh" ]]; then
105
+ # shellcheck disable=SC1091
106
+ . "$HERE/_kb_detect_workspace.sh"
107
+ if detect_workspace_and_component "$cwd" 2>/dev/null; then
108
+ echo "$WORKSPACE"; return 0
109
+ fi
110
+ fi
111
+ echo "$cwd"
112
+ }
113
+
114
+ post_json() {
115
+ # post_json <url> <json-body>
116
+ # On a successful HTTP round-trip, sets globals RESP_BODY (response body) and
117
+ # RESP_CODE (numeric HTTP status) and returns 0. Returns non-zero ONLY when the
118
+ # curl CONNECTION itself fails (DNS/connect/timeout); callers must additionally
119
+ # check RESP_CODE for 2xx, since curl --silent exits 0 on 4xx/5xx.
120
+ #
121
+ # When KB_AUTH_TOKEN is set, pass the Authorization header via a curl config on
122
+ # a process-substitution FD so the token is NOT exposed in process arguments
123
+ # (visible via `ps`). Mirrors bin/kb's do_curl_post.
124
+ local url="$1"
125
+ local body="$2"
126
+ local tmpfile
127
+ tmpfile="$(mktemp)"
128
+ local auth_k_arg=()
129
+ if [[ -n "$KB_AUTH_TOKEN" ]]; then
130
+ auth_k_arg=(-K <(printf 'header = "Authorization: Bearer %s"\n' "$KB_AUTH_TOKEN"))
131
+ fi
132
+ # Timeouts: the pre-tool gate is on the agent's critical path (it blocks every
133
+ # governed edit), so it sets tight CURL_MAX_TIME/CURL_CONNECT_TIMEOUT to fail
134
+ # fast to the fail-mode switch rather than stalling the tool call. Other callers
135
+ # (user-prompt injection) keep the looser default.
136
+ local max_time="${CURL_MAX_TIME:-5}"
137
+ local connect_arg=()
138
+ if [[ -n "${CURL_CONNECT_TIMEOUT:-}" ]]; then
139
+ connect_arg=(--connect-timeout "$CURL_CONNECT_TIMEOUT")
140
+ fi
141
+ # Guard the array expansion: under `set -u`, "${arr[@]}" on an empty array is
142
+ # an unbound-variable error on bash 3.2 (macOS default).
143
+ if RESP_CODE=$(curl --silent --max-time "$max_time" \
144
+ ${connect_arg[@]+"${connect_arg[@]}"} \
145
+ -H "Content-Type: application/json" \
146
+ ${auth_k_arg[@]+"${auth_k_arg[@]}"} \
147
+ --data "$body" \
148
+ --output "$tmpfile" --write-out "%{http_code}" \
149
+ -X POST "$url" 2>/dev/null); then
150
+ RESP_BODY="$(cat "$tmpfile")"
151
+ rm -f "$tmpfile"
152
+ return 0
153
+ fi
154
+ rm -f "$tmpfile"
155
+ RESP_BODY=""
156
+ RESP_CODE=""
157
+ return 1
158
+ }
159
+
160
+ is_2xx() {
161
+ # is_2xx <http-code> ; true when the code is 200-299.
162
+ [[ "$1" =~ ^2[0-9][0-9]$ ]]
163
+ }
164
+
165
+ emit_context() {
166
+ # emit_context <text> ; print injected context per dialect.
167
+ local text="$1"
168
+ if [[ "$DIALECT" == "gemini" ]]; then
169
+ python3 -c 'import json,sys; print(json.dumps({"hookSpecificOutput":{"additionalContext":sys.argv[1]}}))' "$text"
170
+ else
171
+ printf '%s\n' "$text"
172
+ fi
173
+ }
174
+
175
+ emit_deny() {
176
+ # emit_deny <reason> ; signal a blocked tool call per dialect, then exit.
177
+ local reason="$1"
178
+ if [[ "$DIALECT" == "gemini" ]]; then
179
+ python3 -c 'import json,sys; print(json.dumps({"decision":"deny","reason":sys.argv[1]}))' "$reason"
180
+ exit 0
181
+ fi
182
+ echo "$reason" >&2
183
+ exit 2
184
+ }
185
+
186
+ emit_fail_closed() {
187
+ # emit_fail_closed <reason> ; block due to gate-unavailable under fail-mode=closed.
188
+ emit_deny "$1"
189
+ }
190
+
191
+ record_degraded() {
192
+ # record_degraded <workspace> <session> <reason> ; best-effort, ignores failure.
193
+ # Records a gate_degraded audit event when the gate was REACHED but degraded.
194
+ # Never changes the gate's allow/block/fail decision.
195
+ local body
196
+ body="$(python3 -c 'import json,sys
197
+ print(json.dumps({"event_type":"gate_degraded","workspace":sys.argv[1],
198
+ "agent_identity":sys.argv[2],"source_session":sys.argv[3],"reason":sys.argv[4]}))' \
199
+ "$1" "$AGENT" "$2" "$3")"
200
+ post_json "$KB_ENDPOINT/api/v1/audit/event" "$body" >/dev/null 2>&1 || true
201
+ }
202
+
203
+ case "$MODE" in
204
+ session-start)
205
+ emit_context "[KB] data-olympus enforcement active (endpoint: $KB_ENDPOINT)."
206
+ exit 0
207
+ ;;
208
+ user-prompt)
209
+ SESSION="$(json_field session_id)"
210
+ CWD="$(json_field cwd)"
211
+ PROMPT="$(json_field prompt)"
212
+ WS="$(resolve_workspace "$CWD")"
213
+ # trigger=prompt_hook: this consult is the installer's per-prompt auto-consult
214
+ # (UserPromptSubmit / Gemini BeforeAgent), NOT a deliberate agent kb_consult.
215
+ # It is recorded for audit/compliance and its rules are still injected, but it
216
+ # does NOT clear the gate: only an explicit kb_consult call does. Without this
217
+ # marker every user prompt would refresh the ledger and the gate would fire
218
+ # only during autonomous stretches longer than the TTL.
219
+ BODY="$(python3 -c 'import json,sys
220
+ print(json.dumps({"workspace":sys.argv[1],"intent":sys.argv[2],
221
+ "source_session":sys.argv[3],"agent_identity":sys.argv[4],"trigger":"prompt_hook"}))' "$WS" "$PROMPT" "$SESSION" "$AGENT")"
222
+ # Prompt-rule injection is best-effort and never blocks: any failure
223
+ # (connection error OR non-2xx) just means no injected rules this turn.
224
+ if ! post_json "$KB_ENDPOINT/api/v1/consult" "$BODY" || ! is_2xx "$RESP_CODE"; then
225
+ echo "[KB] warn: consult endpoint unreachable; proceeding without injected rules." >&2
226
+ exit 0
227
+ fi
228
+ GOV="$(json_field_from "$RESP_BODY" is_governed_decision)"
229
+ if [[ "$GOV" == "true" ]]; then
230
+ RULES="$(echo "$RESP_BODY" | python3 -c 'import json,sys
231
+ d=json.load(sys.stdin)
232
+ lines=["=== GOVERNING RULES (data-olympus) ==="]
233
+ for r in d.get("rules",[]):
234
+ lines.append(f"- {r.get('"'"'id'"'"')}: {r.get('"'"'title'"'"')}")
235
+ lines.append("=== consult these before deciding ===")
236
+ print("\n".join(lines))')"
237
+ emit_context "$RULES"
238
+ fi
239
+ exit 0
240
+ ;;
241
+ pre-tool)
242
+ # Single parse of the hook payload: emit session_id, cwd, tool_name, file_path
243
+ # and the capped action_diff, each base64-encoded on its own line (base64 is
244
+ # newline/tab-safe, so a multi-line diff or command survives the read). This
245
+ # replaces four separate json_field spawns plus the ADIFF spawn with one
246
+ # python3 invocation on the pre-tool critical path.
247
+ # action_diff: the change content (Write.content, Edit.new_string,
248
+ # Bash.command), capped at 4000 chars so the gate body stays bounded, so the
249
+ # classifier can use diff signals and classify shell commands.
250
+ {
251
+ IFS= read -r SESSION_B64
252
+ IFS= read -r CWD_B64
253
+ IFS= read -r TOOL_B64
254
+ IFS= read -r FPATH_B64
255
+ IFS= read -r ADIFF_B64
256
+ } < <(printf '%s' "$PAYLOAD" | python3 -c 'import base64,json,sys
257
+ def enc(v):
258
+ return base64.b64encode((v or "").encode("utf-8","replace")).decode("ascii")
259
+ try:
260
+ d=json.loads(sys.stdin.read() or "{}")
261
+ if not isinstance(d,dict): d={}
262
+ except Exception:
263
+ d={}
264
+ ti=d.get("tool_input",{}) if isinstance(d.get("tool_input"),dict) else {}
265
+ diff=ti.get("content") or ti.get("new_string") or ti.get("command") or ""
266
+ if not isinstance(diff,str): diff=""
267
+ for v in (d.get("session_id"), d.get("cwd"), d.get("tool_name"),
268
+ ti.get("file_path"), diff[:4000]):
269
+ print(enc(v if isinstance(v,str) else ""))')
270
+ b64d() { printf '%s' "$1" | base64 --decode 2>/dev/null || true; }
271
+ SESSION="$(b64d "$SESSION_B64")"
272
+ CWD="$(b64d "$CWD_B64")"
273
+ TOOL="$(b64d "$TOOL_B64")"
274
+ FPATH="$(b64d "$FPATH_B64")"
275
+ ADIFF="$(b64d "$ADIFF_B64")"
276
+ WS="$(resolve_workspace "$CWD")"
277
+ BODY="$(python3 -c 'import json,sys
278
+ print(json.dumps({"workspace":sys.argv[1],"session_id":sys.argv[2],
279
+ "tool_name":sys.argv[3],"action_path":sys.argv[4],"action_diff":sys.argv[5]}))' "$WS" "$SESSION" "$TOOL" "$FPATH" "$ADIFF")"
280
+ # Gate is "unavailable" when the connection fails OR the HTTP status is not
281
+ # 2xx (curl --silent exits 0 on 4xx/5xx). Unavailable routes through the
282
+ # fail-mode switch; it must NEVER implicitly allow. REACHED distinguishes a
283
+ # full connection failure (cannot record) from a reachable-but-degraded gate.
284
+ GATE_AVAILABLE=1
285
+ REACHED=1
286
+ # Tight timeouts on the critical-path gate: fail fast to the fail-mode switch
287
+ # instead of stalling every governed edit for up to 5s on a slow/down server.
288
+ CURL_MAX_TIME=2 CURL_CONNECT_TIMEOUT=1
289
+ export CURL_MAX_TIME CURL_CONNECT_TIMEOUT
290
+ if ! post_json "$KB_ENDPOINT/api/v1/gate/check" "$BODY"; then
291
+ GATE_AVAILABLE=0; REACHED=0
292
+ elif ! is_2xx "$RESP_CODE"; then
293
+ GATE_AVAILABLE=0
294
+ fi
295
+ VERDICT=""
296
+ if [[ "$GATE_AVAILABLE" == "1" ]]; then
297
+ VERDICT="$(json_field_from "$RESP_BODY" verdict)"
298
+ fi
299
+ # A reachable 2xx with no parseable verdict is also "unavailable": do not
300
+ # silently allow on an unexpected/degraded response.
301
+ if [[ "$GATE_AVAILABLE" == "0" || ( "$VERDICT" != "allow" && "$VERDICT" != "consult_required" ) ]]; then
302
+ # Reachable-but-degraded (server up, non-2xx or unparseable) is recorded;
303
+ # a full connection failure cannot be recorded (phone is down).
304
+ if [[ "$REACHED" == "1" ]]; then
305
+ record_degraded "$WS" "$SESSION" "gate degraded (code=$RESP_CODE)"
306
+ fi
307
+ if [[ "$KB_ENFORCE_FAIL_MODE" == "closed" ]]; then
308
+ emit_fail_closed "[KB] gate unavailable and fail-mode=closed; blocking."
309
+ fi
310
+ echo "[KB] warn: gate endpoint unavailable; failing open (enforcement gap)." >&2
311
+ exit 0
312
+ fi
313
+ if [[ "$VERDICT" == "consult_required" ]]; then
314
+ # Actionable deny: include the exact workspace key AND the session id (the
315
+ # one value the agent cannot guess) plus a copy-pasteable kb_consult call.
316
+ # A null/empty session_id echoes as empty (json_field already emits "" for
317
+ # JSON null), never the literal "None".
318
+ emit_deny "[KB] BLOCKED: this is a governed change and no explicit consultation is on record. Call kb_consult(workspace='$WS', source_session='$SESSION', intent='<what you are doing>') then retry."
319
+ fi
320
+ # Only reached when 2xx AND verdict == allow.
321
+ exit 0
322
+ ;;
323
+ stop)
324
+ exit 0
325
+ ;;
326
+ resolve-workspace)
327
+ # Diagnostic: print the workspace label this hook resolves for a directory
328
+ # (default: $PWD). Same resolution the pre-tool gate applies, so operators
329
+ # can see exactly which key a consult must target.
330
+ resolve_workspace "${POSITIONAL[0]:-$PWD}"
331
+ exit 0
332
+ ;;
333
+ *)
334
+ echo "kb-enforce-hook: unknown mode '$MODE' (want session-start|user-prompt|pre-tool|stop|resolve-workspace)" >&2
335
+ exit 64
336
+ ;;
337
+ esac
@@ -0,0 +1,102 @@
1
+ // data-olympus-enforce (managed) v1 -- installed by `kb enforce install --agent opencode`.
2
+ // Gates file-mutating tools through the data-olympus gate. Remove via
3
+ // `kb enforce uninstall --agent opencode`.
4
+ import { execFileSync } from "node:child_process"
5
+ import { basename } from "node:path"
6
+ import type { Plugin, Hooks } from "@opencode-ai/plugin"
7
+
8
+ const ENDPOINT = process.env.KB_ENDPOINT ?? "http://localhost:8080"
9
+ const FAIL_MODE = process.env.KB_ENFORCE_FAIL_MODE ?? "open"
10
+ const TOKEN = process.env.KB_AUTH_TOKEN ?? ""
11
+ // Gate mutating tools. "bash" is included because shell-driven writes surface as bash.
12
+ const GATED = new Set(["edit", "write", "patch", "multiedit", "bash"])
13
+
14
+ // Resolve the workspace key the SAME way bin/kb-enforce-hook's resolve_workspace
15
+ // does: the MAIN git worktree basename (worktree-invariant, so a consult recorded
16
+ // from the main checkout clears the gate from any linked worktree too), matching
17
+ // the basename key used by every other surface. Falls back to the basename of the
18
+ // worktree/directory path, then the raw path, so the key is never empty.
19
+ // Previously this sent the raw absolute `worktree ?? directory` path, which could
20
+ // never match consults recorded under the basename key everywhere else.
21
+ export function resolveWorkspace(dir: string): string {
22
+ try {
23
+ const out = execFileSync("git", ["-C", dir, "worktree", "list", "--porcelain"], {
24
+ encoding: "utf-8",
25
+ stdio: ["ignore", "pipe", "ignore"],
26
+ })
27
+ // First NON-bare worktree record; the main worktree is listed first. Skip a
28
+ // leading bare record (a bare repo's first entry is the bare git dir).
29
+ let path: string | undefined
30
+ let bare = false
31
+ for (const line of out.split("\n")) {
32
+ if (line.startsWith("worktree ")) {
33
+ path = line.slice("worktree ".length)
34
+ bare = false
35
+ } else if (line === "bare") {
36
+ bare = true
37
+ } else if (line === "") {
38
+ if (path && !bare) return basename(path)
39
+ path = undefined
40
+ bare = false
41
+ }
42
+ }
43
+ if (path && !bare) return basename(path)
44
+ } catch {
45
+ // not a git repo / git unavailable -> fall through to basename of the path
46
+ }
47
+ return basename(dir) || dir
48
+ }
49
+
50
+ export const DataOlympusGate: Plugin = async ({ directory, worktree }) => {
51
+ const workspace = resolveWorkspace(worktree ?? directory)
52
+ return {
53
+ "tool.execute.before": async (input, output) => {
54
+ if (!GATED.has(input.tool)) return
55
+ const headers: Record<string, string> = { "content-type": "application/json" }
56
+ if (TOKEN) headers["authorization"] = `Bearer ${TOKEN}`
57
+ // action_diff: the change content, so the gate classifier sees a real signal
58
+ // for bash (the command) and patch (the diff) which carry no file path.
59
+ // Capped at 4000 chars to bound the request body (mirrors kb-enforce-hook).
60
+ const args = output.args ?? {}
61
+ const rawDiff =
62
+ (args.content ?? args.newString ?? args.new_string ?? args.command ?? args.patch ?? "") as string
63
+ const actionDiff = typeof rawDiff === "string" ? rawDiff.slice(0, 4000) : ""
64
+ let verdict: string | undefined
65
+ try {
66
+ const res = await fetch(`${ENDPOINT}/api/v1/gate/check`, {
67
+ method: "POST",
68
+ headers,
69
+ body: JSON.stringify({
70
+ workspace,
71
+ session_id: input.sessionID,
72
+ tool_name: input.tool,
73
+ action_path: (args.filePath ?? args.path) ?? "",
74
+ action_diff: actionDiff,
75
+ }),
76
+ signal: AbortSignal.timeout(5000),
77
+ })
78
+ if (!res.ok) {
79
+ if (FAIL_MODE === "closed")
80
+ throw new Error(`data-olympus gate HTTP ${res.status}; blocking (fail-closed)`)
81
+ return
82
+ }
83
+ verdict = ((await res.json()) as { verdict?: string }).verdict
84
+ } catch (err) {
85
+ if (err instanceof Error && err.message.startsWith("data-olympus gate")) throw err
86
+ if (FAIL_MODE === "closed")
87
+ throw new Error(`data-olympus gate unreachable; blocking (fail-closed)`)
88
+ return // fail open
89
+ }
90
+ if (verdict === "consult_required") {
91
+ // Actionable deny: echo the exact workspace key and session id (the one
92
+ // value the agent cannot guess) so the clearing call is copy-pasteable.
93
+ throw new Error(
94
+ `BLOCKED by data-olympus: '${input.tool}' is a governed change with no ` +
95
+ `explicit consultation on record. Call ` +
96
+ `kb_consult(workspace='${workspace}', source_session='${input.sessionID}', ` +
97
+ `intent='<what you are doing>') then retry.`,
98
+ )
99
+ }
100
+ },
101
+ } satisfies Hooks
102
+ }