@ngockhoale/ukit 2.4.1 → 2.4.3
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.
- package/CHANGELOG.md +65 -0
- package/manifests/platform.full.yaml +19 -111
- package/package.json +2 -1
- package/scripts/index/refresh-index.mjs +48 -18
- package/src/cli/commands/doctor.js +59 -2
- package/src/core/compact/threshold.js +36 -6
- package/src/core/gatewayProbe.js +143 -15
- package/src/core/gatewayResilienceEnv.js +136 -7
- package/src/diagnostics/classifyHang.js +246 -0
- package/src/index/buildIndex.js +1096 -75
- package/templates/.claude/hooks/auto-allow-bash.sh +99 -87
- package/templates/.claude/hooks/auto-prune-bash.sh +4 -0
- package/templates/.claude/hooks/block-dangerous.sh +46 -1
- package/templates/.claude/hooks/completion-gate.sh +65 -7
- package/templates/.claude/hooks/compress-output.sh +49 -2
- package/templates/.claude/hooks/context-hardcap-gate.sh +52 -4
- package/templates/.claude/hooks/context-window-guard.sh +204 -71
- package/templates/.claude/hooks/handoff-model-guard.sh +50 -3
- package/templates/.claude/hooks/handoff-resume.sh +47 -3
- package/templates/.claude/hooks/post-edit-verify.sh +45 -2
- package/templates/.claude/hooks/pre-edit-backup.sh +45 -2
- package/templates/.claude/hooks/protect-files.sh +46 -1
- package/templates/.claude/hooks/record-execution.sh +46 -2
- package/templates/.claude/hooks/reinject-context.sh +1 -1
- package/templates/.claude/hooks/reset-compact-pressure.sh +4 -0
- package/templates/.claude/hooks/sensitive-data-guard.sh +101 -18
- package/templates/.claude/hooks/skill-router.sh +59 -5
- package/templates/.claude/hooks/stale-spec-guard.sh +47 -2
- package/templates/.claude/hooks/task-watchdog.sh +129 -126
- package/templates/.claude/hooks/verification-guard.sh +136 -106
- package/templates/.claude/hooks/vision-router.sh +138 -18
- package/templates/.claude/settings.json +0 -5
- package/templates/.claude/ukit/index/lib/index-core.mjs +1027 -68
- package/templates/.claude/ukit/index/post-edit-verify.mjs +8 -0
- package/templates/.claude/ukit/index/pre-edit-backup.mjs +8 -0
- package/templates/.claude/ukit/index/refresh-index.mjs +48 -18
- package/templates/.claude/ukit/index/route-task.mjs +610 -4
- package/templates/.claude/ukit/index/stale-spec-check.mjs +8 -0
- package/templates/.claude/ukit/runtime/async-lock.mjs +340 -0
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +73 -24
- package/templates/.claude/ukit/runtime/context-capacity.mjs +144 -0
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +672 -170
- package/templates/.claude/ukit/runtime/hook-chain-budget.mjs +92 -0
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +84 -29
- package/templates/.claude/ukit/runtime/hook-input.mjs +120 -0
- package/templates/.claude/ukit/runtime/hook-input.sh +140 -0
- package/templates/.claude/ukit/runtime/hook-payload-store.mjs +160 -0
- package/templates/.claude/ukit/runtime/hook-process.mjs +250 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +255 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.sh +60 -0
- package/templates/.claude/ukit/runtime/output-compression.mjs +8 -0
- package/templates/.claude/ukit/runtime/reinject-context.mjs +8 -0
- package/templates/.claude/ukit/runtime/stop-coordinator.mjs +509 -0
- package/templates/.claude/ukit/runtime/task-watchdog.mjs +180 -6
- package/templates/.claude/ukit/runtime/transcript-tail.mjs +107 -0
- package/templates/.omp/hooks/pre/ukit-bridge.js +171 -57
|
@@ -2,13 +2,58 @@
|
|
|
2
2
|
# PreToolUse hook: Block edits to sensitive/protected files
|
|
3
3
|
# Matched on: Edit|Write
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# shellcheck source=/dev/null
|
|
7
|
+
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
8
|
+
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
9
|
+
# shared cleanup path before the staged payload is removed. A missing
|
|
10
|
+
# runtime (pre-install tree) simply leaves telemetry off.
|
|
11
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
12
|
+
trap ukit_cleanup_hook_input EXIT
|
|
13
|
+
# Bounded stdin (H01): stage before anything reads it; the shell var is capped.
|
|
14
|
+
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
15
|
+
else
|
|
16
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
17
|
+
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
18
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
19
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
20
|
+
if [ -e /dev/fd/0 ]; then
|
|
21
|
+
exec 8<&0
|
|
22
|
+
head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
23
|
+
else
|
|
24
|
+
head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
25
|
+
fi
|
|
26
|
+
__ukit_reader=$!
|
|
27
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
28
|
+
case "$__ukit_stage_ms" in
|
|
29
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
30
|
+
esac
|
|
31
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
32
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
33
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
34
|
+
__ukit_waiter=$!
|
|
35
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
36
|
+
__ukit_stage_rc=$?
|
|
37
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
38
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
39
|
+
exec 8<&-
|
|
40
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
41
|
+
# A bounded cut (rc != 0) leaves a short payload that the JSON consumer
|
|
42
|
+
# already degrades on - the shared helper's truncate posture (R4.5).
|
|
43
|
+
if [ "$__ukit_size" -gt 2097152 ]; then
|
|
44
|
+
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
45
|
+
fi
|
|
46
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
47
|
+
fi
|
|
48
|
+
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
6
49
|
# jq is absent on stock macOS. Keep jq as the low-latency normal path, but fall back to
|
|
7
50
|
# UKit's required Node runtime so a missing optional binary cannot disable this gate.
|
|
8
51
|
if command -v jq >/dev/null 2>&1 && jq --version >/dev/null 2>&1; then
|
|
9
52
|
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
10
53
|
else
|
|
11
54
|
FILE_PATH=$(printf '%s' "$INPUT" | node -e '
|
|
55
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || "", 10) || 3000;
|
|
56
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
12
57
|
const chunks = [];
|
|
13
58
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
14
59
|
process.stdin.on("end", () => {
|
|
@@ -2,7 +2,51 @@
|
|
|
2
2
|
# PostToolUse hook: persist session-scoped source/write/verification receipts.
|
|
3
3
|
# ADVISORY ONLY — always exit 0. A missing or failing runtime must be loud, not a silent pass.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# shellcheck source=/dev/null
|
|
7
|
+
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
8
|
+
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
9
|
+
# shared cleanup path before the staged payload is removed. A missing
|
|
10
|
+
# runtime (pre-install tree) simply leaves telemetry off.
|
|
11
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
12
|
+
trap ukit_cleanup_hook_input EXIT
|
|
13
|
+
# Bounded stdin (H01): receipts can legitimately be large, so the staged file
|
|
14
|
+
# is the transport and the shell var only mirrors the capped payload.
|
|
15
|
+
ukit_stage_hook_input 33554432 temp-file || exit 0
|
|
16
|
+
else
|
|
17
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
18
|
+
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
19
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
20
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
21
|
+
if [ -e /dev/fd/0 ]; then
|
|
22
|
+
exec 8<&0
|
|
23
|
+
head -c 33554433 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
24
|
+
else
|
|
25
|
+
head -c 33554433 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
26
|
+
fi
|
|
27
|
+
__ukit_reader=$!
|
|
28
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
29
|
+
case "$__ukit_stage_ms" in
|
|
30
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
31
|
+
esac
|
|
32
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
33
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
34
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
35
|
+
__ukit_waiter=$!
|
|
36
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
37
|
+
__ukit_stage_rc=$?
|
|
38
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
39
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
40
|
+
exec 8<&-
|
|
41
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
42
|
+
# A bounded cut (rc != 0) leaves a short payload that the JSON consumer
|
|
43
|
+
# already degrades on - the shared helper's truncate posture (R4.5).
|
|
44
|
+
if [ "$__ukit_size" -gt 33554432 ]; then
|
|
45
|
+
truncate -s 33554432 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
46
|
+
fi
|
|
47
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
48
|
+
fi
|
|
49
|
+
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
6
50
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
7
51
|
SCRIPT="$PROJECT_ROOT/.claude/ukit/runtime/execution-ledger.mjs"
|
|
8
52
|
|
|
@@ -11,7 +55,7 @@ if [ ! -f "$SCRIPT" ]; then
|
|
|
11
55
|
exit 0
|
|
12
56
|
fi
|
|
13
57
|
|
|
14
|
-
OUTPUT=$(printf '%s' "$INPUT" | UKIT_HARNESS=claude-code node "$SCRIPT" --record)
|
|
58
|
+
OUTPUT=$(printf '%s' "$INPUT" | UKIT_HARNESS=claude-code UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT" --record)
|
|
15
59
|
STATUS=$?
|
|
16
60
|
|
|
17
61
|
if [ "$STATUS" -ne 0 ]; then
|
|
@@ -10,7 +10,7 @@ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
10
10
|
SCRIPT_PATH="$HOOK_DIR/../ukit/runtime/reinject-context.mjs"
|
|
11
11
|
|
|
12
12
|
if [ -f "$SCRIPT_PATH" ]; then
|
|
13
|
-
node "$SCRIPT_PATH"
|
|
13
|
+
UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT_PATH"
|
|
14
14
|
exit $?
|
|
15
15
|
fi
|
|
16
16
|
|
|
@@ -26,6 +26,10 @@ fi
|
|
|
26
26
|
|
|
27
27
|
# The lock directory lives beside the state file; its parent exists because the file does.
|
|
28
28
|
node -e '
|
|
29
|
+
// Deadline must exceed the bounded lock wait in this block (maxWaitMs = 5000) so the
|
|
30
|
+
// watchdog can never fire while the mutation is still legally waiting for the lock.
|
|
31
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || "", 10) || 8000;
|
|
32
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
29
33
|
const crypto = require("crypto");
|
|
30
34
|
const fs = require("fs");
|
|
31
35
|
const path = require("path");
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
# PreToolUse (Read|Grep|Bash) + UserPromptSubmit hook: sensitive-data gate.
|
|
3
3
|
#
|
|
4
4
|
# Blocks sensitive data — API keys, private keys, credentials, secret files —
|
|
5
|
-
# from being sent to the AI.
|
|
6
|
-
#
|
|
5
|
+
# from being sent to the AI. Detection is format-true only (owner directive
|
|
6
|
+
# "chặn khi thấy rõ" after repeated false positives): a text match must itself
|
|
7
|
+
# BE a credential — a vendor-formatted key, a private key block, or a JWT.
|
|
8
|
+
# Keyword-assignment guessing and loose bearer matching are gone on purpose;
|
|
9
|
+
# code, config snippets, hashes and encoded blobs flow. Detection covers three
|
|
7
10
|
# egress channels into model context:
|
|
8
11
|
# 1. Read/Grep of secret files (.env*, *.pem, id_rsa, credentials.json, ...)
|
|
9
12
|
# 2. Bash commands that dump secrets (cat .env, bare `env`, curl -u user:pass,
|
|
10
13
|
# URL-embedded credentials, any high-confidence token in the command text)
|
|
11
|
-
# 3. UserPromptSubmit prompt text containing high-confidence secret patterns
|
|
14
|
+
# 3. UserPromptSubmit prompt text containing high-confidence secret patterns;
|
|
15
|
+
# complete machine-generated envelopes are delivered with a redacted advisory
|
|
12
16
|
#
|
|
13
17
|
# FAIL-CLOSED on detection (exit 2). Never echoes the detected value — only a
|
|
14
18
|
# redacted preview plus a truncated sha256 (12 hex chars: enough for the human
|
|
@@ -22,17 +26,70 @@
|
|
|
22
26
|
# Fails open only when there is nothing to scan (malformed stdin, no channel):
|
|
23
27
|
# a broken gate must not brick every session, but every real detection blocks.
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
30
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
31
|
+
# Bounded stdin (H01) — fail-closed overflow: an oversized payload is REFUSED
|
|
32
|
+
# (silent exit 2, nothing staged, nothing echoed). The gate never truncates
|
|
33
|
+
# unknown overflow and never materializes it in a shell variable.
|
|
34
|
+
# shellcheck source=/dev/null
|
|
35
|
+
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
36
|
+
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
37
|
+
# shared cleanup path before the staged payload is removed. A missing
|
|
38
|
+
# runtime (pre-install tree) simply leaves telemetry off.
|
|
39
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
40
|
+
trap ukit_cleanup_hook_input EXIT
|
|
41
|
+
ukit_stage_hook_input 2097152 refuse
|
|
42
|
+
STAGE_RC=$?
|
|
43
|
+
if [ "$STAGE_RC" -ne 0 ]; then
|
|
44
|
+
exit "$STAGE_RC"
|
|
45
|
+
fi
|
|
46
|
+
else
|
|
47
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
48
|
+
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
49
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
50
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
51
|
+
if [ -e /dev/fd/0 ]; then
|
|
52
|
+
exec 8<&0
|
|
53
|
+
head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
54
|
+
else
|
|
55
|
+
head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
56
|
+
fi
|
|
57
|
+
__ukit_reader=$!
|
|
58
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
59
|
+
case "$__ukit_stage_ms" in
|
|
60
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
61
|
+
esac
|
|
62
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
63
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
64
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
65
|
+
__ukit_waiter=$!
|
|
66
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
67
|
+
__ukit_stage_rc=$?
|
|
68
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
69
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
70
|
+
exec 8<&-
|
|
71
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
72
|
+
if [ "$__ukit_stage_rc" -ne 0 ] || [ "$__ukit_size" -gt 2097152 ]; then
|
|
73
|
+
# Fail-closed: an unstaged/oversized payload was never inspected
|
|
74
|
+
# (exit 2 blocks; 1 would be a non-blocking hook error).
|
|
75
|
+
rm -f "$UKIT_INPUT_FILE"
|
|
76
|
+
exit 2
|
|
77
|
+
fi
|
|
78
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
79
|
+
fi
|
|
27
80
|
|
|
28
|
-
|
|
81
|
+
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" node <<'NODE'
|
|
82
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 3000;
|
|
83
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
29
84
|
const fs = require('fs');
|
|
30
85
|
const path = require('path');
|
|
31
86
|
const { createHash } = require('crypto');
|
|
32
87
|
|
|
88
|
+
let rawInput = '';
|
|
89
|
+
try { rawInput = fs.readFileSync(process.env.INPUT_FILE || '', 'utf8'); } catch {}
|
|
33
90
|
const payload = (() => {
|
|
34
91
|
try {
|
|
35
|
-
const parsed = JSON.parse(
|
|
92
|
+
const parsed = JSON.parse(rawInput);
|
|
36
93
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
37
94
|
} catch {
|
|
38
95
|
return {};
|
|
@@ -76,22 +133,26 @@ function pathAllowed(filePath) {
|
|
|
76
133
|
}
|
|
77
134
|
|
|
78
135
|
// --- high-confidence secret value patterns (shared by prompt + bash scanning) ---
|
|
136
|
+
// AKIA/ASIA/AIza prefixes are pure base64-compatible text, so an encoded blob
|
|
137
|
+
// can contain a coincidental key-shaped substring (round-17 live case: ASIA…
|
|
138
|
+
// inside a base64 payload). Those rules must stand alone — no base64/base64url
|
|
139
|
+
// character on either side. Padding "=" is a boundary: it only ends a blob.
|
|
79
140
|
const TOKEN_PATTERNS = [
|
|
80
141
|
{ label: 'OpenAI/Anthropic-style API key', re: /\bsk-(?:proj-|ant-|svc-|acct-|admin-)?[A-Za-z0-9_-]{20,}/g },
|
|
81
|
-
{ label: 'AWS access key id', re:
|
|
142
|
+
{ label: 'AWS access key id', re: /(?<![A-Za-z0-9+/_-])(?:AKIA|ASIA)[0-9A-Z]{16}(?![A-Za-z0-9+/_-])/g },
|
|
82
143
|
{ label: 'GitHub token', re: /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g },
|
|
83
144
|
{ label: 'GitHub fine-grained token', re: /\bgithub_pat_[A-Za-z0-9_]{20,}/g },
|
|
84
145
|
{ label: 'GitLab token', re: /\bglpat-[A-Za-z0-9_-]{20,}/g },
|
|
85
146
|
{ label: 'Slack token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
86
|
-
{ label: 'Google API key', re:
|
|
147
|
+
{ label: 'Google API key', re: /(?<![A-Za-z0-9+/_-])AIza[0-9A-Za-z_-]{20,}(?![A-Za-z0-9+/_-])/g },
|
|
87
148
|
{ label: 'Stripe live key', re: /\b[srp]k_live_[A-Za-z0-9]{20,}/g },
|
|
88
149
|
{ label: 'JWT', re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
89
150
|
{ label: 'private key block', re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY-----/g },
|
|
90
|
-
{ label: 'bearer token', re: /\bBearer\s+[A-Za-z0-9._+/=-]{20,}/gi },
|
|
91
151
|
];
|
|
92
152
|
|
|
93
|
-
|
|
94
|
-
|
|
153
|
+
// No keyword-assignment lane ("password: …", "token = …") on purpose — it was
|
|
154
|
+
// the source of every false positive: it guessed at whatever followed a
|
|
155
|
+
// keyword, blocking code, prose and shell syntax that is never a secret.
|
|
95
156
|
function scanSecretText(text) {
|
|
96
157
|
const found = [];
|
|
97
158
|
for (const { label, re } of TOKEN_PATTERNS) {
|
|
@@ -101,11 +162,6 @@ function scanSecretText(text) {
|
|
|
101
162
|
found.push({ label, value: match[0] });
|
|
102
163
|
}
|
|
103
164
|
}
|
|
104
|
-
ASSIGNMENT_RE.lastIndex = 0;
|
|
105
|
-
let match;
|
|
106
|
-
while ((match = ASSIGNMENT_RE.exec(text)) !== null) {
|
|
107
|
-
found.push({ label: `${match[2].toLowerCase()} assignment`, value: match[3] });
|
|
108
|
-
}
|
|
109
165
|
return found;
|
|
110
166
|
}
|
|
111
167
|
|
|
@@ -253,6 +309,32 @@ if (findings.length === 0) {
|
|
|
253
309
|
process.exit(0);
|
|
254
310
|
}
|
|
255
311
|
|
|
312
|
+
// --- machine-generated envelope lane (UserPromptSubmit only) ---
|
|
313
|
+
// Harness plumbing (<task-notification>, <system-reminder>,
|
|
314
|
+
// <cross-session-message>) legitimately carries fixture/log content through
|
|
315
|
+
// the prompt channel — test tokens quoted in task bodies, CI logs, agent
|
|
316
|
+
// results. Blocking it silently drops the notification and stalls pipelines
|
|
317
|
+
// ("Waiting for N background agents"). These envelopes flow with a redacted
|
|
318
|
+
// advisory: labels + counts only, never values. Everything a human types —
|
|
319
|
+
// and every other event/channel — stays fail-closed below.
|
|
320
|
+
const MACHINE_ENVELOPE_RE = /^(?:task-notification|system-reminder|cross-session-message)$/i;
|
|
321
|
+
function isCompleteMachineEnvelope(text) {
|
|
322
|
+
const trimmed = String(text || '').trim();
|
|
323
|
+
const opening = /^<([a-z-]+)(?:\s[^>]*)?>/i.exec(trimmed);
|
|
324
|
+
if (!opening || !MACHINE_ENVELOPE_RE.test(opening[1])) return false;
|
|
325
|
+
const tag = opening[1];
|
|
326
|
+
const closing = new RegExp(`</${tag}>$`, 'i');
|
|
327
|
+
return closing.test(trimmed);
|
|
328
|
+
}
|
|
329
|
+
const promptText = textChannels.length > 0 ? textChannels[0].text : null;
|
|
330
|
+
if (event === 'UserPromptSubmit' && promptText && isCompleteMachineEnvelope(promptText)) {
|
|
331
|
+
const labels = [...new Set(findings.map((f) => f.label))];
|
|
332
|
+
process.stdout.write(`${JSON.stringify({
|
|
333
|
+
systemMessage: `UKit sensitive-data gate: ${findings.length} secret-shaped value(s) inside a machine-generated notification (${labels.slice(0, 3).join('; ')}${labels.length > 3 ? '; …' : ''}). Delivered without blocking — treat as fixture/log plumbing, not a typed secret. Never repeat the values in your reply.`,
|
|
334
|
+
})}\n`);
|
|
335
|
+
process.exit(0);
|
|
336
|
+
}
|
|
337
|
+
|
|
256
338
|
// --- block message: redacted previews only, never the secret itself ---
|
|
257
339
|
function describeFinding(finding) {
|
|
258
340
|
const where = finding.preview
|
|
@@ -264,8 +346,9 @@ function describeFinding(finding) {
|
|
|
264
346
|
const lines = [
|
|
265
347
|
`BLOCKED (sensitive data): ${findings.length} potential secret(s) detected. Nothing was sent to the AI.`,
|
|
266
348
|
...findings.map(describeFinding),
|
|
267
|
-
'
|
|
268
|
-
'
|
|
349
|
+
'Detection is format-true only: a match must itself be a vendor-formatted key,',
|
|
350
|
+
'a private key block, a JWT, or a credentials file. Code, prose, hashes and',
|
|
351
|
+
'encoded blobs are not blocked. To proceed, the USER can choose one of:',
|
|
269
352
|
' 1. Redact the secret (placeholder like <API_KEY>) and retry.',
|
|
270
353
|
' 2. Approve it explicitly: add the full sha256 (shasum -a 256 of the value) or the file path',
|
|
271
354
|
' to .ukit/storage/security/allowlist.json — that file is protected from AI edits.',
|
|
@@ -2,16 +2,63 @@
|
|
|
2
2
|
# Hook: route prompt/tool signals to the smallest useful installed skill set.
|
|
3
3
|
# Used from UserPromptSubmit and PreToolUse so end users do not need to name skills.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# shellcheck source=/dev/null
|
|
7
|
+
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
8
|
+
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
9
|
+
# shared cleanup path before the staged payload is removed. A missing
|
|
10
|
+
# runtime (pre-install tree) simply leaves telemetry off.
|
|
11
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
12
|
+
trap ukit_cleanup_hook_input EXIT
|
|
13
|
+
# Bounded stdin (H01): stage before anything reads it; the shell var is capped,
|
|
14
|
+
# env passes only the staged-file path — never the payload.
|
|
15
|
+
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
16
|
+
else
|
|
17
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
18
|
+
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
19
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
20
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
21
|
+
if [ -e /dev/fd/0 ]; then
|
|
22
|
+
exec 8<&0
|
|
23
|
+
head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
24
|
+
else
|
|
25
|
+
head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
26
|
+
fi
|
|
27
|
+
__ukit_reader=$!
|
|
28
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
29
|
+
case "$__ukit_stage_ms" in
|
|
30
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
31
|
+
esac
|
|
32
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
33
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
34
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
35
|
+
__ukit_waiter=$!
|
|
36
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
37
|
+
__ukit_stage_rc=$?
|
|
38
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
39
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
40
|
+
exec 8<&-
|
|
41
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
42
|
+
# A bounded cut (rc != 0) leaves a short payload that the JSON consumer
|
|
43
|
+
# already degrades on - the shared helper's truncate posture (R4.5).
|
|
44
|
+
if [ "$__ukit_size" -gt 2097152 ]; then
|
|
45
|
+
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
46
|
+
fi
|
|
47
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
48
|
+
fi
|
|
49
|
+
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
6
50
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
7
51
|
|
|
8
52
|
# Internal orchestration envelopes are not user requests. Ignore them before routing or
|
|
9
53
|
# compact-pressure bookkeeping so task results cannot replace the active completion contract.
|
|
10
54
|
case "$INPUT" in
|
|
11
55
|
*"<task-notification"*|*"<tool-result"*|*"<system-reminder"*|*"<local-command-caveat"*)
|
|
12
|
-
INTERNAL_ORCHESTRATION=$(
|
|
56
|
+
INTERNAL_ORCHESTRATION=$(INPUT_FILE="$UKIT_INPUT_FILE" node -e '
|
|
57
|
+
// wall-clock watchdog: never let a wedged parse outlive the hook (same arm as the router heredoc below)
|
|
58
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || "", 10) || 3000;
|
|
59
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
13
60
|
try {
|
|
14
|
-
const payload = JSON.parse(process.env.
|
|
61
|
+
const payload = JSON.parse(require("fs").readFileSync(process.env.INPUT_FILE || "", "utf8") || "{}");
|
|
15
62
|
const candidates = [payload?.prompt, payload?.user_prompt, payload?.text, payload?.message, payload?.input]
|
|
16
63
|
.filter((value) => typeof value === "string")
|
|
17
64
|
.map((value) => value.trim());
|
|
@@ -44,6 +91,9 @@ esac
|
|
|
44
91
|
case "$INPUT" in
|
|
45
92
|
*".worktrees/"*)
|
|
46
93
|
if printf '%s' "$INPUT" | node -e '
|
|
94
|
+
// wall-clock watchdog: never let a wedged stdin read outlive the hook (same arm as the router heredoc below)
|
|
95
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || "", 10) || 3000;
|
|
96
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
47
97
|
const chunks = [];
|
|
48
98
|
process.stdin.on("data", (c) => chunks.push(c));
|
|
49
99
|
process.stdin.on("end", () => {
|
|
@@ -79,6 +129,9 @@ THRESHOLD_SCRIPT="$HOOK_DIR/../ukit/runtime/compact-threshold.mjs"
|
|
|
79
129
|
case "$INPUT" in
|
|
80
130
|
*'"agent_id"'*|*'"agent_type"'*)
|
|
81
131
|
if printf '%s' "$INPUT" | node -e '
|
|
132
|
+
// wall-clock watchdog: never let a wedged stdin read outlive the hook (same arm as the router heredoc below)
|
|
133
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || "", 10) || 3000;
|
|
134
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
82
135
|
const chunks = [];
|
|
83
136
|
process.stdin.on("data", (c) => chunks.push(c));
|
|
84
137
|
process.stdin.on("end", () => {
|
|
@@ -96,7 +149,7 @@ process.stdin.on("end", () => {
|
|
|
96
149
|
;;
|
|
97
150
|
esac
|
|
98
151
|
|
|
99
|
-
|
|
152
|
+
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" STATE_FILE="$STATE_FILE" HOOK_DIR="$HOOK_DIR" node <<'NODE'
|
|
100
153
|
const fs = require('fs');
|
|
101
154
|
const path = require('path');
|
|
102
155
|
const crypto = require('crypto');
|
|
@@ -2566,7 +2619,8 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2566
2619
|
return compactState;
|
|
2567
2620
|
}
|
|
2568
2621
|
|
|
2569
|
-
|
|
2622
|
+
let rawInput = '';
|
|
2623
|
+
try { rawInput = fs.readFileSync(process.env.INPUT_FILE || '', 'utf8'); } catch {}
|
|
2570
2624
|
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
2571
2625
|
const statePath = process.env.STATE_FILE || path.join(projectRoot, '.claude', 'ukit', 'skill-router-state.json');
|
|
2572
2626
|
const routeCachePath = path.join(projectRoot, '.claude', 'ukit', 'route-cache.json');
|
|
@@ -2,7 +2,50 @@
|
|
|
2
2
|
# PreToolUse hook: prevent stale/ambiguous risky Edit specs and whole-file risky Write.
|
|
3
3
|
# Matched on: Edit|Write
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# shellcheck source=/dev/null
|
|
7
|
+
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
8
|
+
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
9
|
+
# shared cleanup path before the staged payload is removed. A missing
|
|
10
|
+
# runtime (pre-install tree) simply leaves telemetry off.
|
|
11
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
12
|
+
trap ukit_cleanup_hook_input EXIT
|
|
13
|
+
# Bounded stdin (H01): stage before anything reads it; the shell var is capped.
|
|
14
|
+
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
15
|
+
else
|
|
16
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
17
|
+
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
18
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
19
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
20
|
+
if [ -e /dev/fd/0 ]; then
|
|
21
|
+
exec 8<&0
|
|
22
|
+
head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
23
|
+
else
|
|
24
|
+
head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
25
|
+
fi
|
|
26
|
+
__ukit_reader=$!
|
|
27
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
28
|
+
case "$__ukit_stage_ms" in
|
|
29
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
30
|
+
esac
|
|
31
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
32
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
33
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
34
|
+
__ukit_waiter=$!
|
|
35
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
36
|
+
__ukit_stage_rc=$?
|
|
37
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
38
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
39
|
+
exec 8<&-
|
|
40
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
41
|
+
# A bounded cut (rc != 0) leaves a short payload that the JSON consumer
|
|
42
|
+
# already degrades on - the shared helper's truncate posture (R4.5).
|
|
43
|
+
if [ "$__ukit_size" -gt 2097152 ]; then
|
|
44
|
+
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
45
|
+
fi
|
|
46
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
47
|
+
fi
|
|
48
|
+
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
6
49
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
7
50
|
SCRIPT="$PROJECT_ROOT/.claude/ukit/index/stale-spec-check.mjs"
|
|
8
51
|
|
|
@@ -21,6 +64,8 @@ fi
|
|
|
21
64
|
case "$INPUT" in
|
|
22
65
|
*".worktrees/"*)
|
|
23
66
|
if printf '%s' "$INPUT" | node -e '
|
|
67
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || "", 10) || 3000;
|
|
68
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
24
69
|
const chunks = [];
|
|
25
70
|
process.stdin.on("data", (c) => chunks.push(c));
|
|
26
71
|
process.stdin.on("end", () => {
|
|
@@ -42,4 +87,4 @@ process.stdin.on("end", () => {
|
|
|
42
87
|
;;
|
|
43
88
|
esac
|
|
44
89
|
|
|
45
|
-
printf '%s' "$INPUT" | node "$SCRIPT"
|
|
90
|
+
printf '%s' "$INPUT" | UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT"
|