loki-mode 7.129.5 → 8.0.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/README.md +81 -5
- package/SKILL.md +66 -5
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +37 -0
- package/autonomy/completion-council.sh +862 -28
- package/autonomy/council-v2.sh +39 -0
- package/autonomy/crash.sh +3 -2
- package/autonomy/grill.sh +42 -0
- package/autonomy/hooks/validate-bash.sh +301 -4
- package/autonomy/lib/cockpit-render.sh +8 -2
- package/autonomy/lib/cr-rematerialize.py +101 -4
- package/autonomy/lib/deadline.py +957 -0
- package/autonomy/lib/dependency-setup.sh +205 -0
- package/autonomy/lib/done-recognition.sh +45 -0
- package/autonomy/lib/efficiency_cost.py +13 -6
- package/autonomy/lib/no_mock_scan.py +793 -0
- package/autonomy/lib/prd-enrich.sh +42 -0
- package/autonomy/lib/proof-analytics-props.py +69 -0
- package/autonomy/lib/proof-generator.py +282 -95
- package/autonomy/lib/proof-template.html +15 -12
- package/autonomy/lib/proof-verify.py +151 -102
- package/autonomy/lib/requirements_contract.py +848 -0
- package/autonomy/lib/sdk-mode.sh +103 -0
- package/autonomy/lib/secret-scan.sh +139 -0
- package/autonomy/lib/spec-expand.sh +208 -0
- package/autonomy/lib/tree_digest.py +266 -0
- package/autonomy/lib/voter-agents.sh +58 -8
- package/autonomy/lib/workspace_diff.py +124 -0
- package/autonomy/loki +189 -17
- package/autonomy/playwright-verify.sh +501 -0
- package/autonomy/prd-checklist.sh +17 -8
- package/autonomy/provider-offer.sh +111 -0
- package/autonomy/run.sh +4417 -676
- package/autonomy/sandbox.sh +42 -0
- package/autonomy/spec-interrogation.sh +148 -5
- package/autonomy/spec.sh +118 -1
- package/autonomy/telemetry.sh +133 -7
- package/autonomy/verify.sh +107 -0
- package/bin/loki +110 -3
- package/completions/_loki +10 -0
- package/completions/loki.bash +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +96 -7
- package/dashboard/build_supervisor.py +1619 -0
- package/dashboard/control.py +8 -0
- package/dashboard/prompt_optimizer.py +7 -0
- package/dashboard/server.py +577 -22
- package/dashboard/static/trust.html +1 -1
- package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
- package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
- package/docs/INSTALLATION.md +19 -2
- package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
- package/docs/PLANS-INDEX.md +33 -0
- package/docs/PRIVACY.md +29 -1
- package/docs/SIGNED-RECEIPTS.md +102 -0
- package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
- package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
- package/docs/V8-AGENT-SDK-PLAN.md +692 -0
- package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
- package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
- package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
- package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
- package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
- package/docs/alternative-installations.md +4 -4
- package/loki-ts/dist/loki.js +674 -285
- package/loki-ts/package.json +2 -0
- package/mcp/__init__.py +1 -1
- package/package.json +7 -6
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +75 -0
- package/providers/codex.sh +85 -13
- package/references/sdk-mode.md +106 -0
- package/skills/model-selection.md +1 -1
- package/skills/quality-gates.md +22 -0
package/autonomy/council-v2.sh
CHANGED
|
@@ -273,6 +273,45 @@ Respond ONLY with a valid JSON object. No markdown fencing."
|
|
|
273
273
|
local result
|
|
274
274
|
case "${PROVIDER_NAME:-claude}" in
|
|
275
275
|
claude)
|
|
276
|
+
# v8 RAW-SDK JUDGE PATH (opt-in LOKI_SDK_COUNCIL_V2=1). Run this
|
|
277
|
+
# reviewer verdict via the pure-HTTPS @anthropic-ai/sdk (no claude
|
|
278
|
+
# binary) through the loki-ts `internal sdk-judge` bridge, same schema
|
|
279
|
+
# (council-v2-schema.json), same JSON output the extractor below
|
|
280
|
+
# consumes -> parity by construction. Fail-closed: any miss (flag off,
|
|
281
|
+
# no ANTHROPIC_API_KEY, non-zero, empty) falls through to the claude
|
|
282
|
+
# path. Runs BEFORE the claude-binary guard so the SDK path is
|
|
283
|
+
# genuinely binary-free. Default OFF = zero behavior change.
|
|
284
|
+
if [ "${LOKI_SDK_COUNCIL_V2:-0}" = "1" ]; then
|
|
285
|
+
local _c2_sdk_root _c2_sdk_loki _c2_sdk_schema _c2_sdk_pf _c2_sdk_out _c2_sdk_rc
|
|
286
|
+
_c2_sdk_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
287
|
+
_c2_sdk_loki="${_c2_sdk_root}/bin/loki"
|
|
288
|
+
_c2_sdk_schema="${_c2_sdk_root}/loki-ts/data/council-v2-schema.json"
|
|
289
|
+
if [ -x "$_c2_sdk_loki" ] && [ -f "$_c2_sdk_schema" ] && command -v bun >/dev/null 2>&1; then
|
|
290
|
+
_c2_sdk_pf="$(mktemp 2>/dev/null)" || _c2_sdk_pf=""
|
|
291
|
+
if [ -n "$_c2_sdk_pf" ]; then
|
|
292
|
+
printf '%s' "$full_prompt" > "$_c2_sdk_pf"
|
|
293
|
+
_c2_sdk_rc=0
|
|
294
|
+
# Explicit in-process timeout + OS-level ceiling on the bun
|
|
295
|
+
# subprocess (council review: default 120s + maxRetries could
|
|
296
|
+
# stack, and a cold-start could hang with no cap). Bound both.
|
|
297
|
+
local _c2_to_s="${LOKI_SDK_REVIEW_TIMEOUT:-180}"
|
|
298
|
+
local _c2_wrap
|
|
299
|
+
if command -v timeout >/dev/null 2>&1; then _c2_wrap="timeout $(( _c2_to_s + 15 ))"
|
|
300
|
+
elif command -v gtimeout >/dev/null 2>&1; then _c2_wrap="gtimeout $(( _c2_to_s + 15 ))"
|
|
301
|
+
else _c2_wrap=""; fi
|
|
302
|
+
_c2_sdk_out="$($_c2_wrap "$_c2_sdk_loki" internal sdk-judge \
|
|
303
|
+
--prompt-file "$_c2_sdk_pf" --schema-file "$_c2_sdk_schema" \
|
|
304
|
+
--model "${LOKI_SDK_JUDGE_MODEL:-claude-haiku-4-5}" --effort low \
|
|
305
|
+
--timeout-ms "$(( _c2_to_s * 1000 ))" 2>/dev/null)" || _c2_sdk_rc=$?
|
|
306
|
+
rm -f "$_c2_sdk_pf" 2>/dev/null || true
|
|
307
|
+
if [ "$_c2_sdk_rc" -eq 0 ] && [ -n "$_c2_sdk_out" ]; then
|
|
308
|
+
echo "$_c2_sdk_out" > "$output_file"
|
|
309
|
+
return 0
|
|
310
|
+
fi
|
|
311
|
+
fi
|
|
312
|
+
fi
|
|
313
|
+
# fall through to the claude path (fail-closed)
|
|
314
|
+
fi
|
|
276
315
|
if command -v claude &>/dev/null; then
|
|
277
316
|
# EMBED 2 + 3 (v7.33.0). Council-v2 reviewer verdict. $full_prompt
|
|
278
317
|
# is self-contained (evidence + PRD + strict JSON output, via
|
package/autonomy/crash.sh
CHANGED
|
@@ -207,11 +207,12 @@ loki_crash_friction() {
|
|
|
207
207
|
# Kept as a callable no-op so existing callers (run.sh) do not break, and still
|
|
208
208
|
# records the DISCLOSURE_SHOWN sentinel for back-compat with any reader of it.
|
|
209
209
|
loki_show_disclosure_once() {
|
|
210
|
-
local
|
|
210
|
+
local config_root="${LOKI_DIR:-${HOME}/.loki}"
|
|
211
|
+
local config="${config_root}/config"
|
|
211
212
|
if [ -f "$config" ] && grep -q "^DISCLOSURE_SHOWN=true" "$config" 2>/dev/null; then
|
|
212
213
|
return 0
|
|
213
214
|
fi
|
|
214
|
-
mkdir -p "$
|
|
215
|
+
mkdir -p "$config_root" 2>/dev/null || return 0
|
|
215
216
|
echo "DISCLOSURE_SHOWN=true" >> "$config" 2>/dev/null || true
|
|
216
217
|
return 0
|
|
217
218
|
}
|
package/autonomy/grill.sh
CHANGED
|
@@ -138,6 +138,14 @@ grill_check_provider() {
|
|
|
138
138
|
local provider="${LOKI_PROVIDER:-claude}"
|
|
139
139
|
case "$provider" in
|
|
140
140
|
claude)
|
|
141
|
+
# v8: the raw-SDK grill path (LOKI_SDK_GRILL=1) needs no claude binary,
|
|
142
|
+
# so the precondition passes when that path is viable (bridge + bun).
|
|
143
|
+
# grill_invoke_provider still fails closed to claude on an SDK miss.
|
|
144
|
+
if [ "${LOKI_SDK_GRILL:-0}" = "1" ] \
|
|
145
|
+
&& [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/bin/loki" ] \
|
|
146
|
+
&& command -v bun >/dev/null 2>&1; then
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
141
149
|
if ! command -v claude >/dev/null 2>&1; then
|
|
142
150
|
_grill_err "Claude Code CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
|
|
143
151
|
return $GRILL_EXIT_ERROR
|
|
@@ -181,6 +189,40 @@ grill_invoke_provider() {
|
|
|
181
189
|
|
|
182
190
|
case "$provider" in
|
|
183
191
|
claude)
|
|
192
|
+
# v8 RAW-SDK TEXT PATH (opt-in LOKI_SDK_GRILL=1). Free-form grill via
|
|
193
|
+
# the pure-HTTPS @anthropic-ai/sdk (no claude binary) through the
|
|
194
|
+
# loki-ts `internal sdk-text` bridge. Runs BEFORE the claude-binary
|
|
195
|
+
# check so the no-binary deploy win holds. Fail-closed: on ANY miss
|
|
196
|
+
# (flag off, no key, bun/entrypoint absent, non-zero, empty) fall
|
|
197
|
+
# through to the claude path below. Output is the free-form question
|
|
198
|
+
# text loki-grill parses, same as the claude path.
|
|
199
|
+
if [ "${LOKI_SDK_GRILL:-0}" = "1" ]; then
|
|
200
|
+
local _gs_root _gs_loki _gs_pf _gs_out _gs_rc
|
|
201
|
+
_gs_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
202
|
+
_gs_loki="${_gs_root}/bin/loki"
|
|
203
|
+
if [ -x "$_gs_loki" ] && command -v bun >/dev/null 2>&1; then
|
|
204
|
+
_gs_pf="$(mktemp 2>/dev/null)" || _gs_pf=""
|
|
205
|
+
if [ -n "$_gs_pf" ]; then
|
|
206
|
+
printf '%s' "$prompt" > "$_gs_pf"
|
|
207
|
+
_gs_rc=0
|
|
208
|
+
local _gs_to_s="${LOKI_GRILL_TIMEOUT:-180}"
|
|
209
|
+
local _gs_wrap
|
|
210
|
+
if command -v timeout >/dev/null 2>&1; then _gs_wrap="timeout $(( _gs_to_s + 15 ))"
|
|
211
|
+
elif command -v gtimeout >/dev/null 2>&1; then _gs_wrap="gtimeout $(( _gs_to_s + 15 ))"
|
|
212
|
+
else _gs_wrap=""; fi
|
|
213
|
+
_gs_out="$($_gs_wrap "$_gs_loki" internal sdk-text \
|
|
214
|
+
--prompt-file "$_gs_pf" \
|
|
215
|
+
--model "${LOKI_SDK_GRILL_MODEL:-claude-sonnet-5}" --effort high \
|
|
216
|
+
--timeout-ms "$(( _gs_to_s * 1000 ))" 2>/dev/null)" || _gs_rc=$?
|
|
217
|
+
rm -f "$_gs_pf" 2>/dev/null || true
|
|
218
|
+
if [ "$_gs_rc" -eq 0 ] && [ -n "$_gs_out" ]; then
|
|
219
|
+
printf '%s\n' "$_gs_out"
|
|
220
|
+
return 0
|
|
221
|
+
fi
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
# fall through to the claude path (fail-closed)
|
|
225
|
+
fi
|
|
184
226
|
if ! command -v claude >/dev/null 2>&1; then
|
|
185
227
|
_grill_err "Claude Code CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
|
|
186
228
|
return $GRILL_EXIT_ERROR
|
|
@@ -5,8 +5,306 @@
|
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
7
7
|
INPUT=$(cat)
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
deny() {
|
|
10
|
+
local reason="${1:-Blocked by Loki command policy}"
|
|
11
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
12
|
+
printf '%s' "$reason" | python3 -c '
|
|
13
|
+
import json, sys
|
|
14
|
+
print(json.dumps({"hookSpecificOutput": {
|
|
15
|
+
"hookEventName": "PreToolUse",
|
|
16
|
+
"permissionDecision": "deny",
|
|
17
|
+
"permissionDecisionReason": sys.stdin.read(),
|
|
18
|
+
}}))
|
|
19
|
+
'
|
|
20
|
+
else
|
|
21
|
+
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked by Loki command policy"}}'
|
|
22
|
+
fi
|
|
23
|
+
exit 2
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
HOST_GUARD=false
|
|
27
|
+
case "${LOKI_HOST_GUARD:-0}" in
|
|
28
|
+
1|true|yes|on) HOST_GUARD=true ;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
31
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
32
|
+
"$HOST_GUARD" && deny "LOKI_HOST_GUARD: python3 is unavailable, so the Bash command cannot be inspected safely."
|
|
33
|
+
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if ! COMMAND=$(printf '%s' "$INPUT" | python3 -c '
|
|
38
|
+
import json, sys
|
|
39
|
+
try:
|
|
40
|
+
data = json.load(sys.stdin)
|
|
41
|
+
command = (data.get("tool_input") or {}).get("command")
|
|
42
|
+
except Exception:
|
|
43
|
+
raise SystemExit(2)
|
|
44
|
+
if not isinstance(command, str) or not command.strip():
|
|
45
|
+
raise SystemExit(2)
|
|
46
|
+
sys.stdout.write(command)
|
|
47
|
+
'); then
|
|
48
|
+
"$HOST_GUARD" && deny "LOKI_HOST_GUARD: malformed or missing Bash command input was denied."
|
|
49
|
+
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
CWD=$(printf '%s' "$INPUT" | python3 -c '
|
|
54
|
+
import json, sys
|
|
55
|
+
try:
|
|
56
|
+
cwd = json.load(sys.stdin).get("cwd", "")
|
|
57
|
+
except Exception:
|
|
58
|
+
cwd = ""
|
|
59
|
+
sys.stdout.write(cwd if isinstance(cwd, str) else "")
|
|
60
|
+
' 2>/dev/null || true)
|
|
61
|
+
|
|
62
|
+
if "$HOST_GUARD"; then
|
|
63
|
+
GUARD_ROOT="${LOKI_TARGET_DIR:-$PWD}"
|
|
64
|
+
if [ -z "$CWD" ] || ! _LOKI_GUARD_CWD="$CWD" _LOKI_GUARD_ROOT="$GUARD_ROOT" python3 -c '
|
|
65
|
+
import os
|
|
66
|
+
cwd = os.path.realpath(os.environ["_LOKI_GUARD_CWD"])
|
|
67
|
+
root = os.path.realpath(os.environ["_LOKI_GUARD_ROOT"])
|
|
68
|
+
raise SystemExit(0 if cwd == root or cwd.startswith(root + os.sep) else 1)
|
|
69
|
+
'; then
|
|
70
|
+
deny "LOKI_HOST_GUARD: Bash command cwd is outside the assigned workspace."
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
CWD="${CWD:-${LOKI_TARGET_DIR:-$PWD}}"
|
|
74
|
+
|
|
75
|
+
# Hosted builds run on a shared host. This guard prevents recognizable direct
|
|
76
|
+
# command mistakes involving host containers, processes, and listening ports.
|
|
77
|
+
# It is not process isolation: an allowed package script or interpreter can
|
|
78
|
+
# perform the same operations internally. Production builds still require a
|
|
79
|
+
# separate container or microVM with no host Docker socket, restricted
|
|
80
|
+
# credentials, and network policy. This parser only inspects text.
|
|
81
|
+
if "$HOST_GUARD"; then
|
|
82
|
+
HOST_GUARD_REASON=$(_LOKI_GUARD_COMMAND="$COMMAND" python3 - <<'PY'
|
|
83
|
+
import os
|
|
84
|
+
import re
|
|
85
|
+
import shlex
|
|
86
|
+
|
|
87
|
+
command = os.environ.get("_LOKI_GUARD_COMMAND", "")
|
|
88
|
+
blocked = {
|
|
89
|
+
"docker": "host Docker access",
|
|
90
|
+
"docker-compose": "host Docker access",
|
|
91
|
+
"kill": "host process control",
|
|
92
|
+
"killall": "host process control",
|
|
93
|
+
"pkill": "host process control",
|
|
94
|
+
"sudo": "host privilege escalation",
|
|
95
|
+
"doas": "host privilege escalation",
|
|
96
|
+
"launchctl": "host service control",
|
|
97
|
+
"service": "host service control",
|
|
98
|
+
"systemctl": "host service control",
|
|
99
|
+
}
|
|
100
|
+
wrappers = {"command", "env", "exec", "nohup", "time"}
|
|
101
|
+
wrapper_value_options = {
|
|
102
|
+
"env": {"-u", "--unset", "-C", "--chdir"},
|
|
103
|
+
"exec": {"-a"},
|
|
104
|
+
"time": {"-f", "--format", "-o", "--output"},
|
|
105
|
+
}
|
|
106
|
+
shells = {"bash", "dash", "ksh", "sh", "zsh"}
|
|
107
|
+
separators = {";", "&", "&&", "|", "||", "(", ")"}
|
|
108
|
+
assignment = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$", re.S)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def basename(value):
|
|
112
|
+
return value.rstrip("/").rsplit("/", 1)[-1]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def words(value):
|
|
116
|
+
lexer = shlex.shlex(value.replace("\n", " ; "), posix=True,
|
|
117
|
+
punctuation_chars=";&|()")
|
|
118
|
+
lexer.whitespace_split = True
|
|
119
|
+
lexer.commenters = ""
|
|
120
|
+
return list(lexer)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def first_command(segment):
|
|
124
|
+
index = 0
|
|
125
|
+
while index < len(segment) and assignment.match(segment[index]):
|
|
126
|
+
index += 1
|
|
127
|
+
while index < len(segment):
|
|
128
|
+
name = basename(segment[index])
|
|
129
|
+
if name not in wrappers:
|
|
130
|
+
return name, index
|
|
131
|
+
index += 1
|
|
132
|
+
while index < len(segment):
|
|
133
|
+
token = segment[index]
|
|
134
|
+
if assignment.match(token):
|
|
135
|
+
index += 1
|
|
136
|
+
continue
|
|
137
|
+
if token == "--":
|
|
138
|
+
index += 1
|
|
139
|
+
break
|
|
140
|
+
if name == "env" and token in {"-S", "--split-string"}:
|
|
141
|
+
return "__uninspectable_wrapper__", index
|
|
142
|
+
if token in wrapper_value_options.get(name, set()):
|
|
143
|
+
index += 2
|
|
144
|
+
continue
|
|
145
|
+
if token.startswith("-"):
|
|
146
|
+
index += 1
|
|
147
|
+
continue
|
|
148
|
+
break
|
|
149
|
+
return "", index
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def classify(value, depth=0):
|
|
153
|
+
if depth > 2:
|
|
154
|
+
return "command nesting exceeds the safe inspection limit"
|
|
155
|
+
try:
|
|
156
|
+
tokens = words(value)
|
|
157
|
+
except ValueError:
|
|
158
|
+
return "command text could not be parsed safely"
|
|
159
|
+
|
|
160
|
+
segment = []
|
|
161
|
+
segments = []
|
|
162
|
+
for token in tokens:
|
|
163
|
+
if token in separators:
|
|
164
|
+
if segment:
|
|
165
|
+
segments.append(segment)
|
|
166
|
+
segment = []
|
|
167
|
+
else:
|
|
168
|
+
segment.append(token)
|
|
169
|
+
if segment:
|
|
170
|
+
segments.append(segment)
|
|
171
|
+
|
|
172
|
+
for part in segments:
|
|
173
|
+
name, index = first_command(part)
|
|
174
|
+
if name == "__uninspectable_wrapper__":
|
|
175
|
+
return "command wrapper cannot be inspected safely"
|
|
176
|
+
if name in blocked:
|
|
177
|
+
return blocked[name]
|
|
178
|
+
|
|
179
|
+
args = part[index + 1:]
|
|
180
|
+
if name in shells:
|
|
181
|
+
for pos, arg in enumerate(args[:-1]):
|
|
182
|
+
if arg.startswith("-") and "c" in arg:
|
|
183
|
+
nested = classify(args[pos + 1], depth + 1)
|
|
184
|
+
if nested:
|
|
185
|
+
return nested
|
|
186
|
+
|
|
187
|
+
if name == "xargs":
|
|
188
|
+
for arg in args:
|
|
189
|
+
if arg.startswith("-"):
|
|
190
|
+
continue
|
|
191
|
+
target = basename(arg)
|
|
192
|
+
if target in blocked:
|
|
193
|
+
return blocked[target]
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
if name == "find":
|
|
197
|
+
for pos, arg in enumerate(args[:-1]):
|
|
198
|
+
if arg in {"-exec", "-execdir", "-ok", "-okdir"}:
|
|
199
|
+
target = basename(args[pos + 1])
|
|
200
|
+
if target in blocked:
|
|
201
|
+
return blocked[target]
|
|
202
|
+
|
|
203
|
+
if name in {"bunx", "npx"} and args:
|
|
204
|
+
target = basename(next((arg for arg in args if not arg.startswith("-")), ""))
|
|
205
|
+
if target in blocked:
|
|
206
|
+
return blocked[target]
|
|
207
|
+
if name in {"npm", "pnpm", "yarn"} and args and args[0] in {"exec", "x"}:
|
|
208
|
+
target = basename(next((arg for arg in args[1:] if not arg.startswith("-")), ""))
|
|
209
|
+
if target in blocked:
|
|
210
|
+
return blocked[target]
|
|
211
|
+
|
|
212
|
+
if name == "brew" and args and args[0] == "services":
|
|
213
|
+
return "host service control"
|
|
214
|
+
if name in {"nc", "ncat", "netcat"} and any(arg == "-l" or arg.startswith("-l") for arg in args):
|
|
215
|
+
return "host port listener"
|
|
216
|
+
if name == "socat" and any("LISTEN:" in arg.upper() for arg in args):
|
|
217
|
+
return "host port listener"
|
|
218
|
+
|
|
219
|
+
port_assignment = re.search(
|
|
220
|
+
r"(?<![A-Za-z0-9_])(?:PORT|HOST_PORT|LISTEN_PORT)\s*=\s*['\"]?([0-9]+)",
|
|
221
|
+
command,
|
|
222
|
+
)
|
|
223
|
+
if port_assignment and int(port_assignment.group(1)) != 0:
|
|
224
|
+
return "explicit host port claim"
|
|
225
|
+
|
|
226
|
+
port_flag = re.search(
|
|
227
|
+
r"(?:^|\s)(?:--port|--publish)(?:=|\s+)([0-9]+)",
|
|
228
|
+
command,
|
|
229
|
+
)
|
|
230
|
+
if port_flag and int(port_flag.group(1)) != 0:
|
|
231
|
+
return "explicit host port claim"
|
|
232
|
+
|
|
233
|
+
http_server = re.search(r"\bhttp\.server\s+([0-9]+)", command)
|
|
234
|
+
if http_server and int(http_server.group(1)) != 0:
|
|
235
|
+
return "host port listener"
|
|
236
|
+
return ""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
reason = classify(command)
|
|
240
|
+
if reason:
|
|
241
|
+
print("LOKI_HOST_GUARD: refusing " + reason + ". Use the platform sandbox and lifecycle APIs instead.")
|
|
242
|
+
PY
|
|
243
|
+
)
|
|
244
|
+
if [ -n "$HOST_GUARD_REASON" ]; then
|
|
245
|
+
deny "$HOST_GUARD_REASON"
|
|
246
|
+
fi
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
# The supervised simple-web worker prepares dependencies and owns preview,
|
|
250
|
+
# browser, proof, and process lifecycle. Model-launched installs, watchers, dev
|
|
251
|
+
# servers, and Git loops only add latency or escape the bounded execution plan.
|
|
252
|
+
# Enforce that contract at the command boundary so it applies to every model,
|
|
253
|
+
# not only to models that follow the prompt perfectly.
|
|
254
|
+
if [ "${LOKI_SUPERVISED_BUILD:-0}" = "1" ] \
|
|
255
|
+
&& [ "${LOKI_BUILD_PROFILE:-}" = "simple-web" ]; then
|
|
256
|
+
SIMPLE_WEB_GUARD_REASON=$(_LOKI_SIMPLE_COMMAND="$COMMAND" \
|
|
257
|
+
_LOKI_SIMPLE_CWD="$CWD" python3 - <<'PY'
|
|
258
|
+
import json
|
|
259
|
+
import os
|
|
260
|
+
import re
|
|
261
|
+
|
|
262
|
+
command = os.environ.get("_LOKI_SIMPLE_COMMAND", "")
|
|
263
|
+
cwd = os.environ.get("_LOKI_SIMPLE_CWD", "")
|
|
264
|
+
|
|
265
|
+
rules = (
|
|
266
|
+
(
|
|
267
|
+
re.compile(r"(?:^|[;&|()]|&&|\|\|)\s*(?:env\s+[^;&|()]*\s+)?(?:npm|pnpm|yarn|bun)\s+(?:i|install|add|update|upgrade)\b", re.I),
|
|
268
|
+
"dependency changes are owned by the prepared scaffold",
|
|
269
|
+
),
|
|
270
|
+
(
|
|
271
|
+
re.compile(r"(?:^|[;&|()]|&&|\|\|)\s*(?:npm\s+run\s+dev|pnpm\s+(?:run\s+)?dev|yarn\s+dev|bun\s+run\s+dev|next\s+dev|vite(?:\s|$))", re.I),
|
|
272
|
+
"development servers are owned by the preview harness",
|
|
273
|
+
),
|
|
274
|
+
(
|
|
275
|
+
re.compile(r"(?:^|[;&|()]|&&|\|\|)\s*git(?:\s|$)", re.I),
|
|
276
|
+
"Git operations are outside the hosted simple-web implementation step",
|
|
277
|
+
),
|
|
278
|
+
(
|
|
279
|
+
re.compile(r"(?:^|\s)--watch(?:All)?(?:=\S+)?(?:\s|$)", re.I),
|
|
280
|
+
"watch-mode tests do not terminate",
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
for pattern, reason in rules:
|
|
285
|
+
if pattern.search(command):
|
|
286
|
+
print(reason)
|
|
287
|
+
raise SystemExit(0)
|
|
288
|
+
|
|
289
|
+
# `npm test` can hide a watcher in package.json. Inspect only the assigned
|
|
290
|
+
# workspace manifest and direct the model to a one-shot script when available.
|
|
291
|
+
if re.search(r"(?:^|[;&|()]|&&|\|\|)\s*npm\s+(?:run\s+)?test(?:\s|$)", command, re.I):
|
|
292
|
+
try:
|
|
293
|
+
with open(os.path.join(cwd, "package.json"), encoding="utf-8") as handle:
|
|
294
|
+
script = str((json.load(handle).get("scripts") or {}).get("test") or "")
|
|
295
|
+
except (OSError, ValueError, TypeError):
|
|
296
|
+
script = ""
|
|
297
|
+
if re.search(r"(?:^|\s)--watch(?:All)?(?:=\S+)?(?:\s|$)", script, re.I):
|
|
298
|
+
print("the package test script starts a watcher; use test:ci or a one-shot runner")
|
|
299
|
+
raise SystemExit(0)
|
|
300
|
+
|
|
301
|
+
print("")
|
|
302
|
+
PY
|
|
303
|
+
)
|
|
304
|
+
if [ -n "$SIMPLE_WEB_GUARD_REASON" ]; then
|
|
305
|
+
deny "LOKI_SIMPLE_WEB_GUARD: ${SIMPLE_WEB_GUARD_REASON}."
|
|
306
|
+
fi
|
|
307
|
+
fi
|
|
10
308
|
|
|
11
309
|
# Dangerous command patterns (matched anywhere in the command string)
|
|
12
310
|
# Safe paths like /tmp/ and relative paths (./) are excluded below
|
|
@@ -62,8 +360,7 @@ for pattern in "${BLOCKED_PATTERNS[@]}"; do
|
|
|
62
360
|
fi
|
|
63
361
|
done
|
|
64
362
|
"$is_safe" && continue
|
|
65
|
-
|
|
66
|
-
exit 2
|
|
363
|
+
deny "Blocked: potentially dangerous command pattern detected"
|
|
67
364
|
fi
|
|
68
365
|
done
|
|
69
366
|
|
|
@@ -373,12 +373,18 @@ PYEV
|
|
|
373
373
|
|
|
374
374
|
# Print a steering hint for the focused run. Steering is opt-in prompt injection;
|
|
375
375
|
# this only tells the operator how, it does not inject anything.
|
|
376
|
+
# RUN-25 iter 22 (Wave D #3): the hint used to name .loki/steering.md, which
|
|
377
|
+
# NOTHING reads -- a control the UI advertised silently no-op'd (a quiet lie to
|
|
378
|
+
# the operator). The file the loop actually reads is .loki/HUMAN_INPUT.md, gated
|
|
379
|
+
# behind LOKI_PROMPT_INJECTION=1. Name the REAL file + flag (or the `loki steer`
|
|
380
|
+
# helper) so the advertised control genuinely steers the run.
|
|
376
381
|
cockpit_steer_hint() {
|
|
377
382
|
local focus_repo="$1"
|
|
378
383
|
local repo="${focus_repo:-$(pwd)}"
|
|
379
384
|
printf '\n'
|
|
380
385
|
echo "Steer this run"
|
|
381
|
-
echo " Drop a note the next iteration will read:"
|
|
382
|
-
echo "
|
|
386
|
+
echo " Drop a note the next iteration will read (needs LOKI_PROMPT_INJECTION=1):"
|
|
387
|
+
echo " loki steer 'your guidance' # writes it for you"
|
|
388
|
+
echo " echo 'your guidance' > $repo/.loki/HUMAN_INPUT.md"
|
|
383
389
|
echo " Or open the dashboard chat: loki dashboard"
|
|
384
390
|
}
|
|
@@ -32,8 +32,13 @@ import sys
|
|
|
32
32
|
_BLOCKING = {"CRITICAL", "HIGH"}
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def
|
|
36
|
-
"""Return
|
|
35
|
+
def _payload(raw):
|
|
36
|
+
"""Return the structured payload from a CLI envelope or bare SDK result."""
|
|
37
|
+
raw = raw.strip()
|
|
38
|
+
if raw.startswith("```") and raw.endswith("```"):
|
|
39
|
+
first_newline = raw.find("\n")
|
|
40
|
+
if first_newline != -1:
|
|
41
|
+
raw = raw[first_newline + 1 : -3].strip()
|
|
37
42
|
try:
|
|
38
43
|
env = json.loads(raw)
|
|
39
44
|
except Exception:
|
|
@@ -54,14 +59,92 @@ def rematerialize(raw):
|
|
|
54
59
|
payload = json.loads(res)
|
|
55
60
|
except Exception:
|
|
56
61
|
payload = None
|
|
62
|
+
# v8 raw-SDK path: `loki internal sdk-judge` emits the BARE payload object
|
|
63
|
+
# (no CLI envelope), so there is no 'structured_output'/'result' wrapper. If
|
|
64
|
+
# the top-level dict itself carries the payload's 'verdict' key, it IS the
|
|
65
|
+
# payload. The stop_reason guard above already passes for a bare object
|
|
66
|
+
# (no 'stop_reason' key -> None -> allowed).
|
|
67
|
+
if not isinstance(payload, dict) and "verdict" in env:
|
|
68
|
+
payload = env
|
|
57
69
|
if not isinstance(payload, dict):
|
|
58
70
|
raise ValueError(5)
|
|
59
71
|
|
|
72
|
+
return payload
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _requirement_contract(payload, manifest_source):
|
|
76
|
+
if isinstance(manifest_source, dict):
|
|
77
|
+
manifest = manifest_source
|
|
78
|
+
else:
|
|
79
|
+
try:
|
|
80
|
+
with open(manifest_source, encoding="utf-8") as handle:
|
|
81
|
+
manifest = json.load(handle)
|
|
82
|
+
except (OSError, json.JSONDecodeError):
|
|
83
|
+
raise ValueError(8)
|
|
84
|
+
if not isinstance(manifest, dict):
|
|
85
|
+
raise ValueError(8)
|
|
86
|
+
expected = manifest.get("requirements")
|
|
87
|
+
actual = payload.get("requirements")
|
|
88
|
+
findings = payload.get("findings")
|
|
89
|
+
if (
|
|
90
|
+
set(payload)
|
|
91
|
+
!= {"schema", "spec_sha256", "verdict", "requirements", "findings"}
|
|
92
|
+
or manifest.get("schema") != "loki-requirements-manifest/v1"
|
|
93
|
+
or payload.get("schema") != "loki-requirements-verdict/v1"
|
|
94
|
+
or payload.get("spec_sha256") != manifest.get("spec_sha256")
|
|
95
|
+
or not isinstance(expected, list)
|
|
96
|
+
or not expected
|
|
97
|
+
or not isinstance(actual, list)
|
|
98
|
+
or len(actual) != len(expected)
|
|
99
|
+
or not isinstance(findings, list)
|
|
100
|
+
):
|
|
101
|
+
raise ValueError(8)
|
|
102
|
+
|
|
103
|
+
expected_ids = [item.get("id") for item in expected if isinstance(item, dict)]
|
|
104
|
+
actual_ids = [item.get("id") for item in actual if isinstance(item, dict)]
|
|
105
|
+
if len(expected_ids) != len(expected) or actual_ids != expected_ids:
|
|
106
|
+
raise ValueError(8)
|
|
107
|
+
for item in actual:
|
|
108
|
+
if (
|
|
109
|
+
set(item) != {"id", "status", "evidence"}
|
|
110
|
+
or item.get("status") not in ("PASS", "FAIL")
|
|
111
|
+
or not isinstance(item.get("evidence"), str)
|
|
112
|
+
or not item["evidence"].strip()
|
|
113
|
+
or len(item["evidence"]) > 2000
|
|
114
|
+
):
|
|
115
|
+
raise ValueError(8)
|
|
116
|
+
|
|
117
|
+
for finding in findings:
|
|
118
|
+
if (
|
|
119
|
+
not isinstance(finding, dict)
|
|
120
|
+
or set(finding) != {"severity", "description"}
|
|
121
|
+
or finding.get("severity")
|
|
122
|
+
not in ("Critical", "High", "Medium", "Low")
|
|
123
|
+
or not isinstance(finding.get("description"), str)
|
|
124
|
+
or not finding["description"].strip()
|
|
125
|
+
or len(finding["description"]) > 2000
|
|
126
|
+
):
|
|
127
|
+
raise ValueError(8)
|
|
128
|
+
|
|
60
129
|
verdict = str(payload.get("verdict", "")).strip().upper()
|
|
61
130
|
if verdict not in ("PASS", "FAIL"):
|
|
62
131
|
raise ValueError(6)
|
|
132
|
+
if findings or any(item["status"] != "PASS" for item in actual):
|
|
133
|
+
verdict = "FAIL"
|
|
134
|
+
return verdict, findings
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def rematerialize(raw, manifest_source=""):
|
|
138
|
+
"""Return legacy VERDICT/FINDINGS text, or raise ValueError(exit_code)."""
|
|
139
|
+
payload = _payload(raw)
|
|
140
|
+
if manifest_source:
|
|
141
|
+
verdict, findings = _requirement_contract(payload, manifest_source)
|
|
142
|
+
else:
|
|
143
|
+
verdict = str(payload.get("verdict", "")).strip().upper()
|
|
144
|
+
findings = payload.get("findings")
|
|
145
|
+
if verdict not in ("PASS", "FAIL"):
|
|
146
|
+
raise ValueError(6)
|
|
63
147
|
|
|
64
|
-
findings = payload.get("findings")
|
|
65
148
|
if not isinstance(findings, list):
|
|
66
149
|
findings = []
|
|
67
150
|
|
|
@@ -101,7 +184,21 @@ def main():
|
|
|
101
184
|
return 2
|
|
102
185
|
|
|
103
186
|
try:
|
|
104
|
-
|
|
187
|
+
manifest_source = os.environ.get(
|
|
188
|
+
"_LOKI_CR_REQUIREMENTS_MANIFEST_JSON", ""
|
|
189
|
+
)
|
|
190
|
+
if manifest_source:
|
|
191
|
+
try:
|
|
192
|
+
manifest_source = json.loads(manifest_source)
|
|
193
|
+
except json.JSONDecodeError:
|
|
194
|
+
return 8
|
|
195
|
+
else:
|
|
196
|
+
manifest_source = os.environ.get(
|
|
197
|
+
"_LOKI_CR_REQUIREMENTS_MANIFEST", ""
|
|
198
|
+
)
|
|
199
|
+
text = rematerialize(
|
|
200
|
+
raw, manifest_source
|
|
201
|
+
)
|
|
105
202
|
except ValueError as e:
|
|
106
203
|
return int(e.args[0])
|
|
107
204
|
|