@ngockhoale/ukit 2.6.11 → 2.7.0
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 +70 -0
- package/manifests/documentation.yaml +24 -2
- package/manifests/instructionRules.yaml +62 -0
- package/package.json +1 -1
- package/scripts/perf/audit-perf.mjs +920 -0
- package/src/cli/commands/doctor.js +23 -4
- package/src/core/diffPlan.js +8 -0
- package/src/core/ompConfigMerge.js +222 -0
- package/src/core/runInstallPipeline.js +11 -0
- package/src/core/runtimeConfig.js +11 -0
- package/src/core/unattendedDoctor.js +227 -0
- package/templates/.claude/hooks/block-dangerous.sh +76 -9
- package/templates/.claude/hooks/context-hardcap-gate.sh +26 -8
- package/templates/.claude/hooks/project-important.sh +70 -9
- package/templates/.claude/hooks/protect-files.sh +24 -7
- package/templates/.claude/hooks/sensitive-data-guard.sh +57 -5
- package/templates/.claude/settings.json +22 -117
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +217 -10
- package/templates/.claude/ukit/runtime/hook-input.sh +119 -0
- package/templates/.omp/config.yml +32 -4
- package/templates/.omp/hooks/pre/ukit-bridge.js +12 -1
- package/templates/AGENTS.md +22 -10
- package/templates/CLAUDE.md +22 -10
- package/templates/adapter-presets/opencode/opencode.template.json +1 -1
- package/templates/instructions/core.md +22 -10
- package/templates/instructions/layout.yaml +12 -12
- package/templates/ukit/storage/config.json +2 -0
|
@@ -16,10 +16,22 @@ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
|
16
16
|
if [ "$__ukit_main_stage_rc" -ne 0 ]; then
|
|
17
17
|
# Infra failure during staging (mktemp/truncate): payload was never
|
|
18
18
|
# inspected — announce the degrade (SPEC §8), never a silent pass.
|
|
19
|
-
|
|
19
|
+
# TASK-003: `deny`, not `ask` — ask auto-approves under bypassPermissions.
|
|
20
|
+
ukit_emit_permission_decision deny "UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound), so dangerous-command gate cannot prove it safe."
|
|
20
21
|
fi
|
|
21
|
-
# BUG-C21-01: a truncated/stalled staged payload
|
|
22
|
-
|
|
22
|
+
# BUG-C21-01 / TASK-003: a truncated/stalled staged payload used to be announced
|
|
23
|
+
# as a blanket `ask` — but `ask` auto-approves under bypassPermissions (direct
|
|
24
|
+
# host) = the gate silently skipped. Now the degraded path first attempts a
|
|
25
|
+
# bounded salvage of tool_input.command: a COMPLETE recovered command still gets
|
|
26
|
+
# the normal verdict below; an unrecoverable/incomplete one is refused with
|
|
27
|
+
# `deny` (the only verdict both hosts treat as closed — review R1-1).
|
|
28
|
+
if ukit_input_degraded; then
|
|
29
|
+
if UKIT_SALVAGED_COMMAND="$(ukit_salvage_tool_field "$UKIT_INPUT_FILE" "tool_input.command")"; then
|
|
30
|
+
UKIT_SALVAGE_ACTIVE=1
|
|
31
|
+
else
|
|
32
|
+
ukit_emit_permission_decision deny "UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound) and the command could not be recovered, so dangerous-command gate cannot prove it safe."
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
23
35
|
else
|
|
24
36
|
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
25
37
|
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
@@ -52,16 +64,22 @@ else
|
|
|
52
64
|
# path's ukit_emit_input_degraded.
|
|
53
65
|
rm -f "$UKIT_INPUT_FILE"
|
|
54
66
|
UKIT_INPUT_FILE=""
|
|
55
|
-
# TASK-223: same emit shape as ukit_emit_permission_decision —
|
|
56
|
-
# direct host the JSON only reaches the permission pipeline on exit
|
|
57
|
-
# omp chain needs exit 2 (marker exported by hook-chain-runner.mjs).
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
# TASK-223 + TASK-003: same emit shape as ukit_emit_permission_decision —
|
|
68
|
+
# under a direct host the JSON only reaches the permission pipeline on exit
|
|
69
|
+
# 0; the omp chain needs exit 2 (marker exported by hook-chain-runner.mjs).
|
|
70
|
+
# The decision is `deny`, never `ask`: bypassPermissions auto-approves ask.
|
|
71
|
+
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound), so dangerous-command gate cannot prove it safe."}}'
|
|
72
|
+
echo "BLOCKED: dangerous-command gate could not inspect a truncated/stalled payload; refused fail-closed." >&2
|
|
60
73
|
if [ -n "${UKIT_HOOK_CHAIN_RUNNER:-}" ]; then exit 2; fi
|
|
61
74
|
exit 0
|
|
62
75
|
fi
|
|
63
76
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
64
77
|
fi
|
|
78
|
+
if [ "${UKIT_SALVAGE_ACTIVE:-0}" = "1" ]; then
|
|
79
|
+
# TASK-003: degraded path already recovered a COMPLETE command — skip the
|
|
80
|
+
# normal extraction (the staged prefix would not parse anyway).
|
|
81
|
+
COMMAND="$UKIT_SALVAGED_COMMAND"
|
|
82
|
+
else
|
|
65
83
|
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
66
84
|
# jq is not installed on stock macOS. Keep jq as the low-latency normal path, but use
|
|
67
85
|
# UKit's required Node runtime as a fallback so its absence cannot silently disable the gate.
|
|
@@ -81,6 +99,7 @@ process.stdin.on("end", () => {
|
|
|
81
99
|
});
|
|
82
100
|
' 2>/dev/null)
|
|
83
101
|
fi
|
|
102
|
+
fi
|
|
84
103
|
|
|
85
104
|
if [ -z "$COMMAND" ]; then
|
|
86
105
|
exit 0
|
|
@@ -168,6 +187,37 @@ SAFE_ONE_TARGET_REGEX='^(\./)?(dist|build|coverage|\.next|\.nuxt|\.turbo|tmp|tem
|
|
|
168
187
|
UNSAFE_ONE_TARGET_REGEX='^(/.*|~|~/.*|\.\.|\.\./.*|\.)$'
|
|
169
188
|
RM_WORD_REGEX=$'(^|[;&|[:space:]"\x27])rm([[:space:]"\x27]|$)'
|
|
170
189
|
|
|
190
|
+
# TASK-013 / SPEC §10: canonical-path containment for the name allowlist. A
|
|
191
|
+
# target that passes SAFE_ONE_TARGET_REGEX must ALSO resolve inside
|
|
192
|
+
# realpath(projectRoot) — a `dist` symlinked outside the project (or a `..`
|
|
193
|
+
# segment) downgrades the segment to the generic verdict, never allow.
|
|
194
|
+
#
|
|
195
|
+
# ukit_canonical_path mirrors GNU `realpath -m` semantics portably:
|
|
196
|
+
# 1. `realpath -m` (GNU/coreutils) when supported;
|
|
197
|
+
# 2. ONE documented fallback — stock macOS `realpath` cannot resolve
|
|
198
|
+
# nonexistent paths, so resolve the deepest existing ancestor and append
|
|
199
|
+
# the lexical remainder (equivalent for containment);
|
|
200
|
+
# 3. `realpath` itself absent → empty output → fail closed (unresolved-
|
|
201
|
+
# generic), never silently allow.
|
|
202
|
+
ukit_canonical_path() {
|
|
203
|
+
__u_p="$1"
|
|
204
|
+
__u_out=$(realpath -m -- "$__u_p" 2>/dev/null) && [ -n "$__u_out" ] && { printf '%s' "$__u_out"; return 0; }
|
|
205
|
+
__u_rest=""
|
|
206
|
+
while [ -n "$__u_p" ] && [ "$__u_p" != "/" ] && [ ! -e "$__u_p" ] && [ ! -L "$__u_p" ]; do
|
|
207
|
+
__u_rest="/${__u_p##*/}${__u_rest}"
|
|
208
|
+
__u_p="${__u_p%/*}"
|
|
209
|
+
done
|
|
210
|
+
[ -z "$__u_p" ] && __u_p="/"
|
|
211
|
+
__u_out=$(realpath -- "$__u_p" 2>/dev/null) || return 1
|
|
212
|
+
printf '%s%s' "$__u_out" "$__u_rest"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Project-root canonical prefix, resolved once before the segment loop. Targets
|
|
216
|
+
# are interpreted relative to the project root (the host's rm cwd), not the
|
|
217
|
+
# hook's own cwd. An unresolvable root fails closed: no allowlist target can
|
|
218
|
+
# prove containment against an unknown root.
|
|
219
|
+
UKIT_PROJECT_ROOT_CANON=$(ukit_canonical_path "${CLAUDE_PROJECT_DIR:-$PWD}")
|
|
220
|
+
|
|
171
221
|
RM_VERDICT_UNSAFE=0
|
|
172
222
|
RM_VERDICT_GENERIC=0
|
|
173
223
|
RM_DIRECT_SEEN=0
|
|
@@ -201,7 +251,24 @@ while IFS= read -r segment; do
|
|
|
201
251
|
seg_any_unsafe=1
|
|
202
252
|
seg_all_safe=0
|
|
203
253
|
elif printf '%s' "$target" | grep -qE "$SAFE_ONE_TARGET_REGEX"; then
|
|
204
|
-
:
|
|
254
|
+
# Containment gate (TASK-013): the allowlisted NAME is not enough — the
|
|
255
|
+
# canonical target must live inside the canonical project root. `..`
|
|
256
|
+
# segments and unresolvable/missing realpath fail closed to generic.
|
|
257
|
+
case "$target" in
|
|
258
|
+
*..*) seg_all_safe=0 ;;
|
|
259
|
+
*)
|
|
260
|
+
case "$target" in /*) __t_abs="$target" ;; *) __t_abs="$UKIT_PROJECT_ROOT_CANON/$target" ;; esac
|
|
261
|
+
__t_canon=$(ukit_canonical_path "$__t_abs")
|
|
262
|
+
if [ -z "$UKIT_PROJECT_ROOT_CANON" ] || [ -z "$__t_canon" ]; then
|
|
263
|
+
seg_all_safe=0
|
|
264
|
+
else
|
|
265
|
+
case "$__t_canon" in
|
|
266
|
+
"$UKIT_PROJECT_ROOT_CANON"/*) : ;;
|
|
267
|
+
*) seg_all_safe=0 ;;
|
|
268
|
+
esac
|
|
269
|
+
fi
|
|
270
|
+
;;
|
|
271
|
+
esac
|
|
205
272
|
else
|
|
206
273
|
seg_all_safe=0
|
|
207
274
|
fi
|
|
@@ -44,10 +44,19 @@ source "$HOOK_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
|
44
44
|
if [ "$__ukit_main_stage_rc" -ne 0 ]; then
|
|
45
45
|
# Infra failure during staging (mktemp/truncate): payload was never
|
|
46
46
|
# inspected — announce the degrade (SPEC §8), never a silent pass.
|
|
47
|
-
|
|
47
|
+
# TASK-003: `deny`, not `ask` — ask auto-approves under bypassPermissions.
|
|
48
|
+
ukit_emit_permission_decision deny "UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound), so context hard-cap gate cannot prove it safe."
|
|
49
|
+
fi
|
|
50
|
+
# BUG-C21-01 / TASK-003: degraded input first salvages tool_name — a COMPLETE
|
|
51
|
+
# recovered name takes the normal verdict path below; an unrecoverable one is
|
|
52
|
+
# refused with `deny` (never `ask`: bypassPermissions auto-approves ask).
|
|
53
|
+
if ukit_input_degraded; then
|
|
54
|
+
if UKIT_SALVAGED_TOOL_NAME="$(ukit_salvage_tool_field "$UKIT_INPUT_FILE" "tool_name")"; then
|
|
55
|
+
UKIT_SALVAGE_ACTIVE=1
|
|
56
|
+
else
|
|
57
|
+
ukit_emit_permission_decision deny "UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound) and the tool name could not be recovered, so context hard-cap gate cannot prove it safe."
|
|
58
|
+
fi
|
|
48
59
|
fi
|
|
49
|
-
# BUG-C21-01: a truncated/stalled staged payload must be announced (SPEC §8), never silently passed.
|
|
50
|
-
ukit_input_degraded && ukit_emit_input_degraded failclosed "context hard-cap gate"
|
|
51
60
|
else
|
|
52
61
|
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
53
62
|
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
@@ -80,17 +89,18 @@ else
|
|
|
80
89
|
# path's ukit_emit_input_degraded.
|
|
81
90
|
rm -f "$UKIT_INPUT_FILE"
|
|
82
91
|
UKIT_INPUT_FILE=""
|
|
83
|
-
# TASK-223: same emit shape as ukit_emit_permission_decision —
|
|
84
|
-
# direct host (non-zero discards stdout there), exit 2 inside
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
# TASK-223 + TASK-003: same emit shape as ukit_emit_permission_decision —
|
|
93
|
+
# exit 0 on the direct host (non-zero discards stdout there), exit 2 inside
|
|
94
|
+
# the omp chain. `deny`, never `ask`: bypassPermissions auto-approves ask.
|
|
95
|
+
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound), so context hard-cap gate cannot prove it safe."}}'
|
|
96
|
+
echo "BLOCKED: context hard-cap gate could not inspect a truncated/stalled payload; refused fail-closed." >&2
|
|
87
97
|
if [ -n "${UKIT_HOOK_CHAIN_RUNNER:-}" ]; then exit 2; fi
|
|
88
98
|
exit 0
|
|
89
99
|
fi
|
|
90
100
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
91
101
|
fi
|
|
92
102
|
|
|
93
|
-
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" HOOK_DIR="$HOOK_DIR" node <<'NODE'
|
|
103
|
+
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" HOOK_DIR="$HOOK_DIR" UKIT_SALVAGE_ACTIVE="${UKIT_SALVAGE_ACTIVE:-0}" UKIT_SALVAGED_TOOL_NAME="${UKIT_SALVAGED_TOOL_NAME:-}" node <<'NODE'
|
|
94
104
|
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 3000;
|
|
95
105
|
// SPEC §8: a timed-out gate silently passes work it never evaluated — announce the
|
|
96
106
|
// degrade (fail-open posture kept: still exit 0).
|
|
@@ -164,6 +174,14 @@ async function readRunCursor() {
|
|
|
164
174
|
return {};
|
|
165
175
|
}
|
|
166
176
|
})();
|
|
177
|
+
// TASK-003: on the degraded path the staged prefix cannot parse, so the shell
|
|
178
|
+
// handed us the salvaged tool_name — reconstruct just enough payload for the
|
|
179
|
+
// gate's normal verdict.
|
|
180
|
+
if (process.env.UKIT_SALVAGE_ACTIVE === '1'
|
|
181
|
+
&& typeof process.env.UKIT_SALVAGED_TOOL_NAME === 'string'
|
|
182
|
+
&& process.env.UKIT_SALVAGED_TOOL_NAME) {
|
|
183
|
+
payload.tool_name = process.env.UKIT_SALVAGED_TOOL_NAME;
|
|
184
|
+
}
|
|
167
185
|
const toolName = payload?.tool_name || '';
|
|
168
186
|
if (!GATED_TOOLS.has(toolName)) {
|
|
169
187
|
process.exit(0);
|
|
@@ -6,6 +6,58 @@
|
|
|
6
6
|
# session. No renderer logic lives here — the shared runtime module renders.
|
|
7
7
|
|
|
8
8
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
10
|
+
SCRIPT="$PROJECT_ROOT/.claude/ukit/runtime/project-important.mjs"
|
|
11
|
+
UNREADABLE_WARNING='[UKit] PROJECT_IMPORTANT.md was not injected: the file could not be read. Run `ukit doctor`.'
|
|
12
|
+
PROJECT_IMPORTANT_FILE="$PROJECT_ROOT/PROJECT_IMPORTANT.md"
|
|
13
|
+
PROJECT_IMPORTANT_CACHE_DIR="$PROJECT_ROOT/.ukit/storage/cache"
|
|
14
|
+
PROJECT_IMPORTANT_CACHE="$PROJECT_IMPORTANT_CACHE_DIR/project-important.cache"
|
|
15
|
+
|
|
16
|
+
# Cache invalidation uses both metadata and a cheap content checksum. Metadata
|
|
17
|
+
# avoids reading a stable file twice in the common case, while the checksum
|
|
18
|
+
# catches in-place edits that preserve size and timestamp.
|
|
19
|
+
ukit_pi_fingerprint() {
|
|
20
|
+
local metadata checksum
|
|
21
|
+
# GNU coreutils first: it rejects '%m:%z:%i' outright, while BSD stat would
|
|
22
|
+
# accept the GNU format string and silently print a filesystem summary.
|
|
23
|
+
metadata="$(/usr/bin/stat -c '%Y:%s:%i' "$PROJECT_IMPORTANT_FILE" 2>/dev/null)"
|
|
24
|
+
if [ -z "$metadata" ]; then
|
|
25
|
+
metadata="$(/usr/bin/stat -f '%m:%z:%i' "$PROJECT_IMPORTANT_FILE" 2>/dev/null)"
|
|
26
|
+
fi
|
|
27
|
+
[ -n "$metadata" ] || return 1
|
|
28
|
+
checksum="$(cksum < "$PROJECT_IMPORTANT_FILE" 2>/dev/null)" || return 1
|
|
29
|
+
[ -n "$checksum" ] || return 1
|
|
30
|
+
printf '%s|%s' "$metadata" "$checksum"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Resolve the runtime before consulting the cache. A stale cache must never
|
|
34
|
+
# hide a missing install or renderer, while a valid hit intentionally skips
|
|
35
|
+
# stdin staging: this advisory hook does not inspect its SessionStart payload,
|
|
36
|
+
# and waiting on a producer that holds the pipe open is the stall this hook is
|
|
37
|
+
# being fixed for (auto-prune-bash.sh, wired beside it, never reads stdin either).
|
|
38
|
+
if [ ! -f "$SCRIPT" ]; then
|
|
39
|
+
printf '%s\n' "$UNREADABLE_WARNING"
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [ -f "$PROJECT_IMPORTANT_FILE" ] && [ -f "$PROJECT_IMPORTANT_CACHE" ]; then
|
|
44
|
+
__ukit_pi_key="$(ukit_pi_fingerprint 2>/dev/null)"
|
|
45
|
+
if [ -n "$__ukit_pi_key" ]; then
|
|
46
|
+
__ukit_pi_cache="$(cat "$PROJECT_IMPORTANT_CACHE" 2>/dev/null)"
|
|
47
|
+
# Key on the first line; the rest is the renderer's exact stdout bytes.
|
|
48
|
+
# Anything malformed (empty, key-only, wrong key) falls through to the
|
|
49
|
+
# cold path, so a corrupt cache is a re-render, never a wrong answer.
|
|
50
|
+
if [ "${__ukit_pi_cache%%$'\n'*}" = "$__ukit_pi_key" ] \
|
|
51
|
+
&& [ "$__ukit_pi_cache" != "$__ukit_pi_key" ]; then
|
|
52
|
+
__ukit_pi_hit="${__ukit_pi_cache#"$__ukit_pi_key"$'\n'}"
|
|
53
|
+
if [ -n "$__ukit_pi_hit" ]; then
|
|
54
|
+
printf '%s\n' "$__ukit_pi_hit"
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
9
61
|
# shellcheck source=/dev/null
|
|
10
62
|
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
11
63
|
# Arm advisory latency telemetry when available; a missing runtime (pre-install
|
|
@@ -60,15 +112,6 @@ else
|
|
|
60
112
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
61
113
|
fi
|
|
62
114
|
|
|
63
|
-
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
64
|
-
SCRIPT="$PROJECT_ROOT/.claude/ukit/runtime/project-important.mjs"
|
|
65
|
-
UNREADABLE_WARNING='[UKit] PROJECT_IMPORTANT.md was not injected: the file could not be read. Run `ukit doctor`.'
|
|
66
|
-
|
|
67
|
-
if [ ! -f "$SCRIPT" ]; then
|
|
68
|
-
printf '%s\n' "$UNREADABLE_WARNING"
|
|
69
|
-
exit 0
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
115
|
# The wrapped runtime self-arms on UKIT_HOOK_DEADLINE_MS (SCRIPT_ARM/EXIT_ARM),
|
|
73
116
|
# so a wedged read exits 0 inside the deadline instead of orphaning a node child.
|
|
74
117
|
INPUT="$(cat "$UKIT_INPUT_FILE" 2>/dev/null)"
|
|
@@ -80,5 +123,23 @@ if [ "$STATUS" -ne 0 ] || [ -z "$OUTPUT" ]; then
|
|
|
80
123
|
exit 0
|
|
81
124
|
fi
|
|
82
125
|
|
|
126
|
+
# Cache writes are advisory and atomic. Recompute the key after rendering so a
|
|
127
|
+
# file edit racing the renderer cannot publish bytes under an old fingerprint.
|
|
128
|
+
__ukit_pi_render_key=""
|
|
129
|
+
if [ -f "$PROJECT_IMPORTANT_FILE" ]; then
|
|
130
|
+
__ukit_pi_render_key="$(ukit_pi_fingerprint 2>/dev/null)"
|
|
131
|
+
fi
|
|
132
|
+
if [ -n "$__ukit_pi_render_key" ]; then
|
|
133
|
+
if mkdir -p "$PROJECT_IMPORTANT_CACHE_DIR" 2>/dev/null; then
|
|
134
|
+
__ukit_pi_tmp="$PROJECT_IMPORTANT_CACHE.tmp.$$"
|
|
135
|
+
if printf '%s\n%s' "$__ukit_pi_render_key" "$OUTPUT" > "$__ukit_pi_tmp" 2>/dev/null \
|
|
136
|
+
&& mv -f "$__ukit_pi_tmp" "$PROJECT_IMPORTANT_CACHE" 2>/dev/null; then
|
|
137
|
+
:
|
|
138
|
+
else
|
|
139
|
+
rm -f "$__ukit_pi_tmp" 2>/dev/null || true
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
83
144
|
printf '%s\n' "$OUTPUT"
|
|
84
145
|
exit 0
|
|
@@ -16,10 +16,20 @@ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
|
16
16
|
if [ "$__ukit_main_stage_rc" -ne 0 ]; then
|
|
17
17
|
# Infra failure during staging (mktemp/truncate): payload was never
|
|
18
18
|
# inspected — announce the degrade (SPEC §8), never a silent pass.
|
|
19
|
-
|
|
19
|
+
# TASK-003: `deny`, not `ask` — ask auto-approves under bypassPermissions.
|
|
20
|
+
ukit_emit_permission_decision deny "UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound), so protected-file gate cannot prove it safe."
|
|
20
21
|
fi
|
|
21
|
-
# BUG-C21-01
|
|
22
|
-
|
|
22
|
+
# BUG-C21-01 / TASK-003: degraded input first attempts a bounded salvage of
|
|
23
|
+
# tool_input.file_path — a COMPLETE recovered path still gets the normal verdict
|
|
24
|
+
# below; an unrecoverable/incomplete one is refused with `deny` (never `ask`:
|
|
25
|
+
# bypassPermissions auto-approves ask on the direct host — review R1-1).
|
|
26
|
+
if ukit_input_degraded; then
|
|
27
|
+
if UKIT_SALVAGED_FILE_PATH="$(ukit_salvage_tool_field "$UKIT_INPUT_FILE" "tool_input.file_path")"; then
|
|
28
|
+
UKIT_SALVAGE_ACTIVE=1
|
|
29
|
+
else
|
|
30
|
+
ukit_emit_permission_decision deny "UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound) and the file path could not be recovered, so protected-file gate cannot prove it safe."
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
23
33
|
else
|
|
24
34
|
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
25
35
|
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
@@ -52,15 +62,21 @@ else
|
|
|
52
62
|
# path's ukit_emit_input_degraded.
|
|
53
63
|
rm -f "$UKIT_INPUT_FILE"
|
|
54
64
|
UKIT_INPUT_FILE=""
|
|
55
|
-
# TASK-223: same emit shape as ukit_emit_permission_decision —
|
|
56
|
-
# direct host (non-zero discards stdout there), exit 2 inside
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
# TASK-223 + TASK-003: same emit shape as ukit_emit_permission_decision —
|
|
66
|
+
# exit 0 on the direct host (non-zero discards stdout there), exit 2 inside
|
|
67
|
+
# the omp chain. `deny`, never `ask`: bypassPermissions auto-approves ask.
|
|
68
|
+
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound), so protected-file gate cannot prove it safe."}}'
|
|
69
|
+
echo "BLOCKED: protected-file gate could not inspect a truncated/stalled payload; refused fail-closed." >&2
|
|
59
70
|
if [ -n "${UKIT_HOOK_CHAIN_RUNNER:-}" ]; then exit 2; fi
|
|
60
71
|
exit 0
|
|
61
72
|
fi
|
|
62
73
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
63
74
|
fi
|
|
75
|
+
if [ "${UKIT_SALVAGE_ACTIVE:-0}" = "1" ]; then
|
|
76
|
+
# TASK-003: degraded path already recovered a COMPLETE file_path — skip the
|
|
77
|
+
# normal extraction (the staged prefix would not parse anyway).
|
|
78
|
+
FILE_PATH="$UKIT_SALVAGED_FILE_PATH"
|
|
79
|
+
else
|
|
64
80
|
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
65
81
|
# jq is absent on stock macOS. Keep jq as the low-latency normal path, but fall back to
|
|
66
82
|
# UKit's required Node runtime so a missing optional binary cannot disable this gate.
|
|
@@ -80,6 +96,7 @@ process.stdin.on("end", () => {
|
|
|
80
96
|
});
|
|
81
97
|
' 2>/dev/null)
|
|
82
98
|
fi
|
|
99
|
+
fi
|
|
83
100
|
|
|
84
101
|
if [ -z "$FILE_PATH" ]; then
|
|
85
102
|
exit 0
|
|
@@ -30,9 +30,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
30
30
|
HOOK_DIR="$SCRIPT_DIR"
|
|
31
31
|
SCRIPT_PATH="$HOOK_DIR/../ukit/runtime/sensitive-value-scanner.mjs"
|
|
32
32
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
33
|
-
# Bounded stdin (H01)
|
|
34
|
-
#
|
|
35
|
-
#
|
|
33
|
+
# Bounded stdin (H01). TASK-003: staging uses `truncate` so a cut payload can be
|
|
34
|
+
# SALVAGED — when the decision-relevant field is provably complete the guard
|
|
35
|
+
# runs the real scan on it; when it cannot be recovered the gate refuses
|
|
36
|
+
# fail-closed (exit 2 + §8 systemMessage, never `ask`: bypassPermissions
|
|
37
|
+
# auto-approves `ask` on the direct host — review R1-1).
|
|
36
38
|
# shellcheck source=/dev/null
|
|
37
39
|
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
38
40
|
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
@@ -40,7 +42,7 @@ if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
|
40
42
|
# runtime (pre-install tree) simply leaves telemetry off.
|
|
41
43
|
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
42
44
|
trap ukit_cleanup_hook_input EXIT
|
|
43
|
-
ukit_stage_hook_input 2097152
|
|
45
|
+
ukit_stage_hook_input 2097152 truncate
|
|
44
46
|
STAGE_RC=$?
|
|
45
47
|
if [ "$STAGE_RC" -ne 0 ]; then
|
|
46
48
|
# BUG-C21-02: a fail-closed refusal must never be a SILENT block — emit the
|
|
@@ -51,6 +53,40 @@ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
|
51
53
|
echo 'BLOCKED (sensitive data gate): payload could not be staged within bounds; never scanned (fail-closed).' >&2
|
|
52
54
|
exit 2
|
|
53
55
|
fi
|
|
56
|
+
# TASK-003 degraded-path salvage: recover hook_event_name + the channel's
|
|
57
|
+
# scanned field (prompt for UserPromptSubmit; tool_name + the decision field
|
|
58
|
+
# for PreToolUse Read/Grep/Bash). Every salvage step must prove its field
|
|
59
|
+
# COMPLETE; any failure refuses fail-closed — a partial field is never
|
|
60
|
+
# scanned (a truncated dangerous shape could parse benign = fail-open).
|
|
61
|
+
if ukit_input_degraded; then
|
|
62
|
+
__ukit_salvage_fail() {
|
|
63
|
+
printf '%s\n' '{"systemMessage":"UKit sensitive-data gate blocked this call: the tool-call payload was truncated or stalled during stdin staging and its scanned field could not be recovered intact, so it was never scanned. Fail-closed — reduce the payload or check for a stalled producer."}'
|
|
64
|
+
echo 'BLOCKED (sensitive data gate): truncated/stalled payload; scanned field unrecoverable — refused fail-closed.' >&2
|
|
65
|
+
exit 2
|
|
66
|
+
}
|
|
67
|
+
UKIT_SALVAGED_EVENT="$(ukit_salvage_tool_field "$UKIT_INPUT_FILE" "hook_event_name")" || __ukit_salvage_fail
|
|
68
|
+
UKIT_SALVAGED_TOOL_NAME=""
|
|
69
|
+
UKIT_SALVAGED_FIELD_NAME=""
|
|
70
|
+
UKIT_SALVAGED_FIELD_VALUE=""
|
|
71
|
+
case "$UKIT_SALVAGED_EVENT" in
|
|
72
|
+
UserPromptSubmit)
|
|
73
|
+
UKIT_SALVAGED_FIELD_NAME="prompt"
|
|
74
|
+
UKIT_SALVAGED_FIELD_VALUE="$(ukit_salvage_tool_field "$UKIT_INPUT_FILE" "prompt")" || __ukit_salvage_fail
|
|
75
|
+
;;
|
|
76
|
+
PreToolUse)
|
|
77
|
+
UKIT_SALVAGED_TOOL_NAME="$(ukit_salvage_tool_field "$UKIT_INPUT_FILE" "tool_name")" || __ukit_salvage_fail
|
|
78
|
+
case "$UKIT_SALVAGED_TOOL_NAME" in
|
|
79
|
+
Read) UKIT_SALVAGED_FIELD_NAME="file_path" ;;
|
|
80
|
+
Grep) UKIT_SALVAGED_FIELD_NAME="path" ;;
|
|
81
|
+
Bash) UKIT_SALVAGED_FIELD_NAME="command" ;;
|
|
82
|
+
*) __ukit_salvage_fail ;;
|
|
83
|
+
esac
|
|
84
|
+
UKIT_SALVAGED_FIELD_VALUE="$(ukit_salvage_tool_field "$UKIT_INPUT_FILE" "tool_input.$UKIT_SALVAGED_FIELD_NAME")" || __ukit_salvage_fail
|
|
85
|
+
;;
|
|
86
|
+
*) __ukit_salvage_fail ;;
|
|
87
|
+
esac
|
|
88
|
+
UKIT_SALVAGE_ACTIVE=1
|
|
89
|
+
fi
|
|
54
90
|
else
|
|
55
91
|
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
56
92
|
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
@@ -90,7 +126,7 @@ else
|
|
|
90
126
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
91
127
|
fi
|
|
92
128
|
|
|
93
|
-
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" SCRIPT_PATH="$SCRIPT_PATH" UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node <<'NODE'
|
|
129
|
+
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" SCRIPT_PATH="$SCRIPT_PATH" UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" UKIT_SALVAGE_ACTIVE="${UKIT_SALVAGE_ACTIVE:-0}" UKIT_SALVAGED_EVENT="${UKIT_SALVAGED_EVENT:-}" UKIT_SALVAGED_TOOL_NAME="${UKIT_SALVAGED_TOOL_NAME:-}" UKIT_SALVAGED_FIELD_NAME="${UKIT_SALVAGED_FIELD_NAME:-}" UKIT_SALVAGED_FIELD_VALUE="${UKIT_SALVAGED_FIELD_VALUE:-}" node <<'NODE'
|
|
94
130
|
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 3000;
|
|
95
131
|
// BUG-C21-03 (chosen posture: FAIL-CLOSED, parity with the staging refusal):
|
|
96
132
|
// a wedged scan must never pass an uninspected payload. The deadline emits a
|
|
@@ -127,6 +163,22 @@ const payload = (() => {
|
|
|
127
163
|
}
|
|
128
164
|
})();
|
|
129
165
|
|
|
166
|
+
// TASK-003: on the degraded path the staged prefix cannot parse, so the shell
|
|
167
|
+
// handed us the provably-COMPLETE salvaged fields — reconstruct just enough
|
|
168
|
+
// payload for the normal channel scan below.
|
|
169
|
+
if (process.env.UKIT_SALVAGE_ACTIVE === '1' && process.env.UKIT_SALVAGED_EVENT) {
|
|
170
|
+
payload.hook_event_name = process.env.UKIT_SALVAGED_EVENT;
|
|
171
|
+
if (process.env.UKIT_SALVAGED_EVENT === 'PreToolUse' && process.env.UKIT_SALVAGED_TOOL_NAME) {
|
|
172
|
+
payload.tool_name = process.env.UKIT_SALVAGED_TOOL_NAME;
|
|
173
|
+
payload.tool_input = {};
|
|
174
|
+
if (process.env.UKIT_SALVAGED_FIELD_NAME) {
|
|
175
|
+
payload.tool_input[process.env.UKIT_SALVAGED_FIELD_NAME] = process.env.UKIT_SALVAGED_FIELD_VALUE;
|
|
176
|
+
}
|
|
177
|
+
} else if (process.env.UKIT_SALVAGED_EVENT === 'UserPromptSubmit') {
|
|
178
|
+
payload.prompt = process.env.UKIT_SALVAGED_FIELD_VALUE;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
130
182
|
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
131
183
|
|
|
132
184
|
async function readJsonSafe(filePath, fallback) {
|