loki-mode 5.42.2 → 5.46.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 +4 -3
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +684 -0
- package/autonomy/checklist-verify.py +368 -0
- package/autonomy/completion-council.sh +49 -0
- package/autonomy/loki +83 -0
- package/autonomy/playwright-verify.sh +350 -0
- package/autonomy/prd-analyzer.py +457 -0
- package/autonomy/prd-checklist.sh +223 -0
- package/autonomy/run.sh +164 -4
- package/completions/loki.bash +6 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +134 -1
- package/dashboard/static/index.html +804 -265
- package/docs/INSTALLATION.md +1 -1
- package/docs/audit-logging.md +600 -0
- package/docs/authentication.md +374 -0
- package/docs/authorization.md +455 -0
- package/docs/git-workflow.md +446 -0
- package/docs/metrics.md +527 -0
- package/docs/network-security.md +275 -0
- package/docs/openclaw-integration.md +572 -0
- package/docs/siem-integration.md +579 -0
- package/learning/__init__.py +1 -1
- package/mcp/__init__.py +1 -1
- package/memory/__init__.py +2 -0
- package/package.json +2 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#===============================================================================
|
|
3
|
+
# Playwright Smoke Test Module (v5.46.0)
|
|
4
|
+
#
|
|
5
|
+
# Runs lightweight smoke tests against a running user application to verify
|
|
6
|
+
# it loads correctly. Advisory only - failures do NOT block iterations or
|
|
7
|
+
# council approval.
|
|
8
|
+
#
|
|
9
|
+
# Functions:
|
|
10
|
+
# playwright_verify_init() - Check/install Playwright
|
|
11
|
+
# playwright_verify_app(url) - Run smoke test against URL
|
|
12
|
+
# playwright_verify_should_run() - Check if verification should run
|
|
13
|
+
# playwright_verify_summary() - One-line summary for prompt injection
|
|
14
|
+
# playwright_verify_as_evidence() - Formatted output for council evidence
|
|
15
|
+
#
|
|
16
|
+
# Environment Variables:
|
|
17
|
+
# LOKI_PLAYWRIGHT_ENABLED - Enable/disable (default: true)
|
|
18
|
+
# LOKI_PLAYWRIGHT_INTERVAL - Run every N iterations (default: 5)
|
|
19
|
+
# LOKI_PLAYWRIGHT_TIMEOUT - Page load timeout in ms (default: 15000)
|
|
20
|
+
#
|
|
21
|
+
# Data:
|
|
22
|
+
# .loki/verification/playwright-results.json - Last results
|
|
23
|
+
# .loki/verification/screenshots/ - Captured screenshots
|
|
24
|
+
#
|
|
25
|
+
#===============================================================================
|
|
26
|
+
|
|
27
|
+
# Configuration
|
|
28
|
+
PLAYWRIGHT_ENABLED=${LOKI_PLAYWRIGHT_ENABLED:-true}
|
|
29
|
+
PLAYWRIGHT_INTERVAL=${LOKI_PLAYWRIGHT_INTERVAL:-5}
|
|
30
|
+
# Guard against zero/negative interval (division by zero in modulo)
|
|
31
|
+
if [ "$PLAYWRIGHT_INTERVAL" -le 0 ] 2>/dev/null; then
|
|
32
|
+
PLAYWRIGHT_INTERVAL=5
|
|
33
|
+
fi
|
|
34
|
+
PLAYWRIGHT_TIMEOUT=${LOKI_PLAYWRIGHT_TIMEOUT:-15000}
|
|
35
|
+
# Derive seconds for the outer timeout wrapper (add buffer for browser startup)
|
|
36
|
+
PLAYWRIGHT_TIMEOUT_SEC=$(( (PLAYWRIGHT_TIMEOUT / 1000) + 15 ))
|
|
37
|
+
|
|
38
|
+
# Internal state
|
|
39
|
+
PLAYWRIGHT_VERIFY_DIR=""
|
|
40
|
+
PLAYWRIGHT_RESULTS_FILE=""
|
|
41
|
+
PLAYWRIGHT_LAST_VERIFY_ITERATION=0
|
|
42
|
+
PLAYWRIGHT_AVAILABLE=false
|
|
43
|
+
PLAYWRIGHT_MAX_SCREENSHOTS=10
|
|
44
|
+
|
|
45
|
+
#===============================================================================
|
|
46
|
+
# Initialization
|
|
47
|
+
#===============================================================================
|
|
48
|
+
|
|
49
|
+
playwright_verify_init() {
|
|
50
|
+
# Check if npx playwright is available; attempt chromium install if needed.
|
|
51
|
+
# Returns 0 if playwright available, 1 if not.
|
|
52
|
+
|
|
53
|
+
if [ "$PLAYWRIGHT_ENABLED" != "true" ]; then
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
PLAYWRIGHT_VERIFY_DIR=".loki/verification"
|
|
58
|
+
PLAYWRIGHT_RESULTS_FILE="${PLAYWRIGHT_VERIFY_DIR}/playwright-results.json"
|
|
59
|
+
mkdir -p "${PLAYWRIGHT_VERIFY_DIR}/screenshots"
|
|
60
|
+
|
|
61
|
+
# Check if playwright is accessible via npx
|
|
62
|
+
if ! npx playwright --version &>/dev/null; then
|
|
63
|
+
log_warn "Playwright not found via npx - smoke tests disabled"
|
|
64
|
+
PLAYWRIGHT_AVAILABLE=false
|
|
65
|
+
return 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Check if chromium browser is installed; attempt install if not
|
|
69
|
+
if ! npx playwright install --dry-run chromium &>/dev/null; then
|
|
70
|
+
log_info "Installing Playwright chromium browser..."
|
|
71
|
+
if ! timeout 60 npx playwright install chromium &>/dev/null; then
|
|
72
|
+
log_warn "Failed to install Playwright chromium - smoke tests disabled"
|
|
73
|
+
PLAYWRIGHT_AVAILABLE=false
|
|
74
|
+
return 1
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
PLAYWRIGHT_AVAILABLE=true
|
|
79
|
+
log_info "Playwright smoke tests initialized (every ${PLAYWRIGHT_INTERVAL} iterations, ${PLAYWRIGHT_TIMEOUT}ms timeout)"
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#===============================================================================
|
|
84
|
+
# Interval Control
|
|
85
|
+
#===============================================================================
|
|
86
|
+
|
|
87
|
+
playwright_verify_should_run() {
|
|
88
|
+
# Returns 0 (true) if verification should run this iteration
|
|
89
|
+
if [ "$PLAYWRIGHT_ENABLED" != "true" ]; then
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [ "$PLAYWRIGHT_AVAILABLE" != "true" ]; then
|
|
94
|
+
return 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
local current_iteration="${ITERATION_COUNT:-0}"
|
|
98
|
+
if [ "$current_iteration" -eq 0 ]; then
|
|
99
|
+
return 1
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if [ $((current_iteration % PLAYWRIGHT_INTERVAL)) -ne 0 ]; then
|
|
103
|
+
return 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Don't verify same iteration twice
|
|
107
|
+
if [ "$current_iteration" -eq "$PLAYWRIGHT_LAST_VERIFY_ITERATION" ]; then
|
|
108
|
+
return 1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#===============================================================================
|
|
115
|
+
# Smoke Test
|
|
116
|
+
#===============================================================================
|
|
117
|
+
|
|
118
|
+
playwright_verify_app() {
|
|
119
|
+
local url="$1"
|
|
120
|
+
|
|
121
|
+
if [ -z "$url" ]; then
|
|
122
|
+
log_warn "playwright_verify_app: no URL provided"
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
local verify_dir="${PLAYWRIGHT_VERIFY_DIR:-.loki/verification}"
|
|
127
|
+
local screenshots_dir="${verify_dir}/screenshots"
|
|
128
|
+
mkdir -p "$screenshots_dir"
|
|
129
|
+
|
|
130
|
+
local timestamp
|
|
131
|
+
timestamp=$(date -u +%Y-%m-%dT%H%M%SZ)
|
|
132
|
+
local screenshot_path="${screenshots_dir}/verify-${timestamp}.png"
|
|
133
|
+
local results_file="${verify_dir}/playwright-results.json"
|
|
134
|
+
local script_file="${verify_dir}/.smoke-test.js"
|
|
135
|
+
|
|
136
|
+
log_step "Running Playwright smoke test against ${url}..."
|
|
137
|
+
|
|
138
|
+
# Generate inline smoke test script
|
|
139
|
+
cat > "$script_file" << 'SMOKE_SCRIPT'
|
|
140
|
+
const { chromium } = require('playwright');
|
|
141
|
+
|
|
142
|
+
(async () => {
|
|
143
|
+
const url = process.argv[2];
|
|
144
|
+
const screenshotPath = process.argv[3];
|
|
145
|
+
const resultsPath = process.argv[4];
|
|
146
|
+
const pageTimeout = parseInt(process.argv[5] || '15000', 10);
|
|
147
|
+
const startTime = Date.now();
|
|
148
|
+
|
|
149
|
+
const results = {
|
|
150
|
+
verified_at: new Date().toISOString(),
|
|
151
|
+
url: url,
|
|
152
|
+
passed: false,
|
|
153
|
+
checks: {
|
|
154
|
+
page_loads: false,
|
|
155
|
+
no_5xx: false,
|
|
156
|
+
no_console_errors: true,
|
|
157
|
+
has_title: false,
|
|
158
|
+
has_content: false
|
|
159
|
+
},
|
|
160
|
+
screenshot: screenshotPath,
|
|
161
|
+
errors: [],
|
|
162
|
+
duration_ms: 0
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let browser;
|
|
166
|
+
try {
|
|
167
|
+
browser = await chromium.launch({ headless: true });
|
|
168
|
+
const page = await browser.newPage();
|
|
169
|
+
|
|
170
|
+
// Collect console errors
|
|
171
|
+
const consoleErrors = [];
|
|
172
|
+
page.on('console', msg => {
|
|
173
|
+
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Navigate to URL
|
|
177
|
+
const response = await page.goto(url, {
|
|
178
|
+
waitUntil: 'domcontentloaded',
|
|
179
|
+
timeout: pageTimeout
|
|
180
|
+
});
|
|
181
|
+
results.checks.page_loads = true;
|
|
182
|
+
|
|
183
|
+
// Check HTTP status (no 5xx)
|
|
184
|
+
const status = response ? response.status() : 0;
|
|
185
|
+
results.checks.no_5xx = status < 500;
|
|
186
|
+
if (status >= 500) results.errors.push('HTTP ' + status);
|
|
187
|
+
|
|
188
|
+
// Check console errors
|
|
189
|
+
if (consoleErrors.length > 0) {
|
|
190
|
+
results.checks.no_console_errors = false;
|
|
191
|
+
results.errors.push(...consoleErrors.slice(0, 5));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check page title
|
|
195
|
+
const title = await page.title();
|
|
196
|
+
results.checks.has_title = title.length > 0;
|
|
197
|
+
|
|
198
|
+
// Check visible content
|
|
199
|
+
const bodyText = await page.evaluate(() => document.body?.innerText?.trim() || '');
|
|
200
|
+
results.checks.has_content = bodyText.length > 0;
|
|
201
|
+
|
|
202
|
+
// Capture screenshot
|
|
203
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
204
|
+
|
|
205
|
+
// Determine overall pass
|
|
206
|
+
results.passed = Object.values(results.checks).every(v => v === true);
|
|
207
|
+
|
|
208
|
+
} catch (err) {
|
|
209
|
+
results.errors.push(err.message);
|
|
210
|
+
} finally {
|
|
211
|
+
if (browser) await browser.close();
|
|
212
|
+
results.duration_ms = Date.now() - startTime;
|
|
213
|
+
|
|
214
|
+
// Atomic write: temp file then rename
|
|
215
|
+
const fs = require('fs');
|
|
216
|
+
const tmp = resultsPath + '.tmp';
|
|
217
|
+
fs.writeFileSync(tmp, JSON.stringify(results, null, 2));
|
|
218
|
+
fs.renameSync(tmp, resultsPath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
process.exit(results.passed ? 0 : 1);
|
|
222
|
+
})();
|
|
223
|
+
SMOKE_SCRIPT
|
|
224
|
+
|
|
225
|
+
# Run with outer timeout (never block iteration)
|
|
226
|
+
timeout "${PLAYWRIGHT_TIMEOUT_SEC}" node "$script_file" \
|
|
227
|
+
"$url" "$screenshot_path" "$results_file" "$PLAYWRIGHT_TIMEOUT" 2>/dev/null
|
|
228
|
+
local exit_code=$?
|
|
229
|
+
|
|
230
|
+
# Clean up generated script
|
|
231
|
+
rm -f "$script_file"
|
|
232
|
+
|
|
233
|
+
# Update tracking state
|
|
234
|
+
PLAYWRIGHT_LAST_VERIFY_ITERATION="${ITERATION_COUNT:-0}"
|
|
235
|
+
|
|
236
|
+
# Rotate screenshots: keep only the most recent N
|
|
237
|
+
_playwright_rotate_screenshots "$screenshots_dir"
|
|
238
|
+
|
|
239
|
+
# Log result
|
|
240
|
+
if [ -f "$results_file" ]; then
|
|
241
|
+
local summary
|
|
242
|
+
summary=$(playwright_verify_summary 2>/dev/null || true)
|
|
243
|
+
if [ -n "$summary" ]; then
|
|
244
|
+
log_info "Playwright: $summary"
|
|
245
|
+
fi
|
|
246
|
+
elif [ "$exit_code" -eq 124 ]; then
|
|
247
|
+
log_warn "Playwright smoke test timed out after ${PLAYWRIGHT_TIMEOUT_SEC}s"
|
|
248
|
+
else
|
|
249
|
+
log_warn "Playwright smoke test failed (exit $exit_code)"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Never fail the iteration
|
|
253
|
+
return 0
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#===============================================================================
|
|
257
|
+
# Screenshot Rotation
|
|
258
|
+
#===============================================================================
|
|
259
|
+
|
|
260
|
+
_playwright_rotate_screenshots() {
|
|
261
|
+
local dir="$1"
|
|
262
|
+
local max=${PLAYWRIGHT_MAX_SCREENSHOTS}
|
|
263
|
+
|
|
264
|
+
# Count existing screenshots
|
|
265
|
+
local count
|
|
266
|
+
count=$(find "$dir" -maxdepth 1 -name 'verify-*.png' 2>/dev/null | wc -l | tr -d ' ')
|
|
267
|
+
|
|
268
|
+
if [ "$count" -gt "$max" ]; then
|
|
269
|
+
local to_remove=$((count - max))
|
|
270
|
+
# Remove oldest files (sorted by name, which is timestamp-based)
|
|
271
|
+
find "$dir" -maxdepth 1 -name 'verify-*.png' 2>/dev/null \
|
|
272
|
+
| sort | head -n "$to_remove" \
|
|
273
|
+
| xargs rm -f 2>/dev/null || true
|
|
274
|
+
fi
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#===============================================================================
|
|
278
|
+
# Summary (for prompt injection)
|
|
279
|
+
#===============================================================================
|
|
280
|
+
|
|
281
|
+
playwright_verify_summary() {
|
|
282
|
+
# Returns one-line summary string
|
|
283
|
+
if [ ! -f "$PLAYWRIGHT_RESULTS_FILE" ]; then
|
|
284
|
+
echo ""
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
_PW_RESULTS="$PLAYWRIGHT_RESULTS_FILE" python3 -c "
|
|
289
|
+
import json, os
|
|
290
|
+
try:
|
|
291
|
+
data = json.load(open(os.environ['_PW_RESULTS']))
|
|
292
|
+
checks = data.get('checks', {})
|
|
293
|
+
passed = sum(1 for v in checks.values() if v)
|
|
294
|
+
total = len(checks)
|
|
295
|
+
status = 'PASS' if data.get('passed') else 'FAIL'
|
|
296
|
+
duration = data.get('duration_ms', 0)
|
|
297
|
+
url = data.get('url', '?')
|
|
298
|
+
errors = data.get('errors', [])
|
|
299
|
+
err_detail = ''
|
|
300
|
+
if errors:
|
|
301
|
+
err_detail = ' Errors: ' + '; '.join(errors[:3])
|
|
302
|
+
print(f'{status} {passed}/{total} checks ({duration}ms) {url}{err_detail}')
|
|
303
|
+
except Exception:
|
|
304
|
+
print('')
|
|
305
|
+
" 2>/dev/null || echo ""
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#===============================================================================
|
|
309
|
+
# Council Evidence (for completion-council.sh)
|
|
310
|
+
#===============================================================================
|
|
311
|
+
|
|
312
|
+
playwright_verify_as_evidence() {
|
|
313
|
+
# Writes formatted smoke test evidence to stdout or appends to file
|
|
314
|
+
local evidence_file="${1:-}"
|
|
315
|
+
|
|
316
|
+
if [ ! -f "$PLAYWRIGHT_RESULTS_FILE" ]; then
|
|
317
|
+
return 0
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
{
|
|
321
|
+
echo ""
|
|
322
|
+
echo "## Playwright Smoke Test"
|
|
323
|
+
echo ""
|
|
324
|
+
|
|
325
|
+
_PW_RESULTS="$PLAYWRIGHT_RESULTS_FILE" python3 -c "
|
|
326
|
+
import json, os
|
|
327
|
+
try:
|
|
328
|
+
data = json.load(open(os.environ['_PW_RESULTS']))
|
|
329
|
+
checks = data.get('checks', {})
|
|
330
|
+
status = 'PASS' if data.get('passed') else 'FAIL'
|
|
331
|
+
print(f'Overall: {status} | URL: {data.get(\"url\", \"?\")} | Duration: {data.get(\"duration_ms\", 0)}ms')
|
|
332
|
+
print()
|
|
333
|
+
for name, result in checks.items():
|
|
334
|
+
label = '[PASS]' if result else '[FAIL]'
|
|
335
|
+
print(f' {label} {name}')
|
|
336
|
+
errors = data.get('errors', [])
|
|
337
|
+
if errors:
|
|
338
|
+
print()
|
|
339
|
+
print('Errors:')
|
|
340
|
+
for e in errors[:5]:
|
|
341
|
+
print(f' - {e}')
|
|
342
|
+
screenshot = data.get('screenshot', '')
|
|
343
|
+
if screenshot:
|
|
344
|
+
print()
|
|
345
|
+
print(f'Screenshot: {screenshot}')
|
|
346
|
+
except Exception:
|
|
347
|
+
print('Playwright data unavailable')
|
|
348
|
+
" 2>/dev/null || echo "Playwright data unavailable"
|
|
349
|
+
} >> "${evidence_file:-/dev/stdout}"
|
|
350
|
+
}
|