loki-mode 9.22.2 → 9.22.4
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 +50 -0
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +44 -0
- package/autonomy/quickstart.sh +319 -56
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_releases.py +5 -2
- package/dashboard/control.py +5 -2
- package/dashboard/server.py +8 -2
- package/docs/INSTALLATION.md +11 -0
- package/docs/OUTCOME-CANARY-EVALUATION.md +45 -2
- package/docs/PROOF-PASSPORT.md +4 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/tools/outcome-canary-evaluate.py +17 -2
- package/tools/outcome-canary-receipt-verify.py +17 -2
- package/tools/outcome-canary-receipt.py +26 -5
- package/web-app/server.py +8 -2
package/README.md
CHANGED
|
@@ -51,6 +51,56 @@ That is the whole happy path. It asks for a one-line idea, picks a template,
|
|
|
51
51
|
shows the real cost and time estimate before spending anything, then builds.
|
|
52
52
|
Press Enter through every step and you get a sample Todo app.
|
|
53
53
|
|
|
54
|
+
One command, no prompts (CI, scripts, containers, any shell without a terminal):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
loki quickstart "a todo app with user accounts" --yes
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Both halves are required with no terminal: an idea (or a path to a PRD file)
|
|
61
|
+
and an explicit `--yes`. Given both, Loki picks the top-ranked template
|
|
62
|
+
automatically, prints the same honest cost and time estimate, and starts the
|
|
63
|
+
build without asking anything. Missing either half exits 2 with the
|
|
64
|
+
needs-a-terminal message and writes nothing, so an ambient `LOKI_AUTO_CONFIRM`
|
|
65
|
+
or a stray argument in CI can never start a paid build on its own. Existing
|
|
66
|
+
files are never overwritten: if `prd.md` is present the PRD lands at
|
|
67
|
+
`prd-quickstart.md`, then numbered suffixes as needed.
|
|
68
|
+
|
|
69
|
+
Choose an exact shipped starter when the top-ranked match is not the one you
|
|
70
|
+
want:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
loki quickstart "an internal reporting workspace" --template dashboard --yes
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`--template` accepts an exact template name for idea inputs and works the same
|
|
77
|
+
way with interactive use or `--dry-run` (including JSON preview). Unknown
|
|
78
|
+
templates, duplicate flags, and combinations with a PRD path refuse before
|
|
79
|
+
provider discovery, estimation, writes, or build execution.
|
|
80
|
+
|
|
81
|
+
Preview the same deterministic template choice and estimator-backed plan with
|
|
82
|
+
zero writes or execution:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
loki quickstart "a todo app with user accounts" --dry-run
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Preview requires an idea or readable PRD path, works without a terminal or AI
|
|
89
|
+
provider, and exits before creating a PRD or starting a build. `--dry-run` and
|
|
90
|
+
`--yes` are mutually exclusive so execution intent is never ambiguous.
|
|
91
|
+
|
|
92
|
+
For scripts and local dashboards, add `--json` to receive one versioned JSON
|
|
93
|
+
object instead of terminal text:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
loki quickstart "a todo app with user accounts" --dry-run --json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The object contains the input kind, deterministic selected template (or `null`
|
|
100
|
+
for an existing PRD), and the exact estimator response under `plan`. `--json`
|
|
101
|
+
requires `--dry-run`; invalid input or estimator failure writes no JSON, and the
|
|
102
|
+
command still exits before provider discovery, file writes, or build execution.
|
|
103
|
+
|
|
54
104
|
Or go straight at it:
|
|
55
105
|
|
|
56
106
|
```bash
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v9.22.
|
|
6
|
+
# Loki Mode v9.22.4
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
470
470
|
|
|
471
471
|
---
|
|
472
472
|
|
|
473
|
-
**v9.22.
|
|
473
|
+
**v9.22.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.22.
|
|
1
|
+
9.22.4
|
package/autonomy/loki
CHANGED
|
@@ -35298,6 +35298,9 @@ cmd_proof() {
|
|
|
35298
35298
|
echo " show <id> Pretty-print .loki/proofs/<id>/proof.json"
|
|
35299
35299
|
echo " verify <id> Re-check a receipt against the repo (tamper + drift);"
|
|
35300
35300
|
echo " exit 0 clean, 1 tamper/drift. Verify it yourself."
|
|
35301
|
+
echo " passport <contract> <id|proof.json> <output.json>"
|
|
35302
|
+
echo " Bind work from any agent to a portable Outcome Contract"
|
|
35303
|
+
echo " and independently rechecked Evidence Receipt"
|
|
35301
35304
|
echo ""
|
|
35302
35305
|
echo "Options for 'verify':"
|
|
35303
35306
|
echo " --human Render the verdict and every failure reason as prose"
|
|
@@ -35474,6 +35477,47 @@ PYEOF
|
|
|
35474
35477
|
fi
|
|
35475
35478
|
exit 0
|
|
35476
35479
|
;;
|
|
35480
|
+
passport)
|
|
35481
|
+
# EXEC-PROOF-01: make the provider-neutral Proof Passport reachable
|
|
35482
|
+
# from the installed product. The Python implementation already
|
|
35483
|
+
# binds the executor/verifier identities, exact contract digest,
|
|
35484
|
+
# exact receipt bytes, and independently re-derived verdict. Before
|
|
35485
|
+
# this subcommand, users had to know the repository-only tools/
|
|
35486
|
+
# path, so the portable proof workflow was not actually a product
|
|
35487
|
+
# workflow for npm consumers.
|
|
35488
|
+
#
|
|
35489
|
+
# The second positional accepts either a local proof id or an
|
|
35490
|
+
# explicit receipt path. That lets a Loki run use its short id while
|
|
35491
|
+
# Codex, Claude Code, Cursor, Factory, or any other executor can hand
|
|
35492
|
+
# over a provider-neutral proof.json without copying it into .loki.
|
|
35493
|
+
local contract="${1:-}"
|
|
35494
|
+
local receipt_ref="${2:-}"
|
|
35495
|
+
local output="${3:-}"
|
|
35496
|
+
if [ -z "$contract" ] || [ -z "$receipt_ref" ] || [ -z "$output" ]; then
|
|
35497
|
+
echo -e "${RED}Usage: loki proof passport <contract.json> <proof-id|proof.json> <output.json> [options]${NC}" >&2
|
|
35498
|
+
echo "Options: --repo-dir DIR --markdown-output FILE --force" >&2
|
|
35499
|
+
exit 64
|
|
35500
|
+
fi
|
|
35501
|
+
shift 3
|
|
35502
|
+
|
|
35503
|
+
local receipt_path="$receipt_ref"
|
|
35504
|
+
if [ ! -f "$receipt_path" ]; then
|
|
35505
|
+
receipt_path="${proofs_dir}/${receipt_ref}/proof.json"
|
|
35506
|
+
fi
|
|
35507
|
+
if [ ! -f "$receipt_path" ]; then
|
|
35508
|
+
echo -e "${RED}Receipt not found: ${receipt_ref}${NC}" >&2
|
|
35509
|
+
echo "Pass a proof id from 'loki proof list' or a path to proof.json." >&2
|
|
35510
|
+
exit 2
|
|
35511
|
+
fi
|
|
35512
|
+
|
|
35513
|
+
local passport_tool="${SKILL_DIR}/tools/proof-passport.py"
|
|
35514
|
+
if [ ! -f "$passport_tool" ]; then
|
|
35515
|
+
echo -e "${RED}Proof Passport tool not found: ${passport_tool}${NC}" >&2
|
|
35516
|
+
exit 3
|
|
35517
|
+
fi
|
|
35518
|
+
python3 "$passport_tool" "$contract" "$receipt_path" "$output" "$@"
|
|
35519
|
+
exit $?
|
|
35520
|
+
;;
|
|
35477
35521
|
md)
|
|
35478
35522
|
# Paste-able Markdown for a PR comment, a Slack message, or a
|
|
35479
35523
|
# ticket. The renderer already existed -- render_evidence_receipt_md
|
package/autonomy/quickstart.sh
CHANGED
|
@@ -283,6 +283,19 @@ _qs_template_summary() {
|
|
|
283
283
|
esac
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
# _qs_template_exists <name>: accept only an exact shipped template basename.
|
|
287
|
+
# Keeping validation here (rather than accepting an arbitrary path) prevents
|
|
288
|
+
# --template from becoming a second PRD/file-read surface. The intentionally
|
|
289
|
+
# narrow character set also makes names safe to join below without traversal.
|
|
290
|
+
_qs_template_exists() {
|
|
291
|
+
local name="${1:-}"
|
|
292
|
+
case "$name" in
|
|
293
|
+
""|*[!a-z0-9-]*) return 1;;
|
|
294
|
+
esac
|
|
295
|
+
[ "$name" != "README" ] || return 1
|
|
296
|
+
[ -f "$(_qs_templates_dir)/$name.md" ]
|
|
297
|
+
}
|
|
298
|
+
|
|
286
299
|
# _qs_selected_provider: print the provider a build would ACTUALLY pick, or
|
|
287
300
|
# nothing. Single source of truth is providers/loader.sh auto_detect_provider --
|
|
288
301
|
# the same seam render_provider_availability (provider-offer.sh:395) uses, and
|
|
@@ -327,7 +340,7 @@ _qs_selected_provider() {
|
|
|
327
340
|
# non-zero if the estimator gave no result (caller falls back to a no-number
|
|
328
341
|
# confirm, never fabricating a figure).
|
|
329
342
|
_qs_emit_plan() {
|
|
330
|
-
local prd_path="$1" template_name="$2"
|
|
343
|
+
local prd_path="$1" template_name="$2" json_output="${3:-false}" input_kind="${4:-idea}"
|
|
331
344
|
local plan_json=""
|
|
332
345
|
plan_json=$(show_prd_plan "$prd_path" "true" "false" 2>/dev/null) || plan_json=""
|
|
333
346
|
if [ -z "$plan_json" ]; then
|
|
@@ -358,6 +371,32 @@ print('{}{}'.format(iters, rng_str))
|
|
|
358
371
|
if [ -z "$parsed" ]; then
|
|
359
372
|
return 1
|
|
360
373
|
fi
|
|
374
|
+
|
|
375
|
+
if [ "$json_output" = true ]; then
|
|
376
|
+
local json_payload=""
|
|
377
|
+
json_payload=$(printf '%s' "$plan_json" | python3 -c '
|
|
378
|
+
import json
|
|
379
|
+
import sys
|
|
380
|
+
|
|
381
|
+
plan = json.load(sys.stdin)
|
|
382
|
+
if not isinstance(plan, dict):
|
|
383
|
+
sys.exit(1)
|
|
384
|
+
template_name, input_kind = sys.argv[1:3]
|
|
385
|
+
payload = {
|
|
386
|
+
"schema_version": 1,
|
|
387
|
+
"command": "loki quickstart",
|
|
388
|
+
"mode": "dry-run",
|
|
389
|
+
"input_kind": input_kind,
|
|
390
|
+
"selected_template": template_name if input_kind == "idea" else None,
|
|
391
|
+
"source_name": template_name if input_kind == "prd" else None,
|
|
392
|
+
"plan": plan,
|
|
393
|
+
}
|
|
394
|
+
json.dump(payload, sys.stdout, separators=(",", ":"), sort_keys=True)
|
|
395
|
+
' "$template_name" "$input_kind" 2>/dev/null) || json_payload=""
|
|
396
|
+
[ -n "$json_payload" ] || return 1
|
|
397
|
+
printf '%s\n' "$json_payload" >&3
|
|
398
|
+
return 0
|
|
399
|
+
fi
|
|
361
400
|
local tier_u cost_u time_u iter_u
|
|
362
401
|
tier_u=$(printf '%s' "$parsed" | sed -n '1p')
|
|
363
402
|
cost_u=$(printf '%s' "$parsed" | sed -n '2p')
|
|
@@ -391,16 +430,32 @@ _qs_help() {
|
|
|
391
430
|
printf '\n'
|
|
392
431
|
printf 'Options:\n'
|
|
393
432
|
printf ' --yes, -y Auto-confirm the final build prompt (still shows the plan)\n'
|
|
433
|
+
printf ' --dry-run Preview the selected template and plan; write/start nothing\n'
|
|
434
|
+
printf ' --json With --dry-run, emit one machine-readable JSON object\n'
|
|
435
|
+
printf ' --template N Use the exact shipped template N for an IDEA\n'
|
|
394
436
|
printf ' --help, -h Show this help and exit\n'
|
|
395
437
|
printf '\n'
|
|
438
|
+
printf 'Non-interactive use:\n'
|
|
439
|
+
printf ' loki quickstart "a todo app with user accounts" --yes\n'
|
|
440
|
+
printf ' Both an IDEA (or PRD path) and --yes are required with no terminal.\n'
|
|
441
|
+
printf ' Missing either one exits 2 and writes nothing. The top-ranked\n'
|
|
442
|
+
printf ' template is chosen automatically and the plan is still shown.\n'
|
|
443
|
+
printf ' Add --template NAME to choose a shipped template instead.\n'
|
|
444
|
+
printf '\n'
|
|
445
|
+
printf 'Zero-spend preview:\n'
|
|
446
|
+
printf ' loki quickstart "a todo app" --dry-run\n'
|
|
447
|
+
printf ' An IDEA (or readable PRD path) is required. No provider is checked,\n'
|
|
448
|
+
printf ' no file is written, and no build is started. Do not combine with --yes.\n'
|
|
449
|
+
printf ' Add --json for versioned JSON only; --json requires --dry-run.\n'
|
|
450
|
+
printf '\n'
|
|
396
451
|
printf 'Steps:\n'
|
|
397
|
-
printf ' 1. Setup Check for an AI provider
|
|
452
|
+
printf ' 1. Setup Check for an AI provider for execution (skipped in preview)\n'
|
|
398
453
|
printf ' 2. Build Describe what you want, or Enter for the sample Todo app\n'
|
|
399
454
|
printf ' 3. Template Pick the closest starting template (offline keyword match)\n'
|
|
400
455
|
printf ' 4. Plan Review the honest cost/time estimate, then confirm\n'
|
|
401
456
|
printf '\n'
|
|
402
|
-
printf 'The PRD is written to ./prd.md in the current directory
|
|
403
|
-
printf '
|
|
457
|
+
printf 'The PRD is written to ./prd.md in the current directory (or the next\n'
|
|
458
|
+
printf 'free prd-quickstart*.md name if that exists), then the build starts.\n'
|
|
404
459
|
return 0
|
|
405
460
|
}
|
|
406
461
|
|
|
@@ -408,14 +463,36 @@ _qs_help() {
|
|
|
408
463
|
# (slice B), show_prd_plan (slice A), the template matcher, and cmd_start.
|
|
409
464
|
#
|
|
410
465
|
# Order is load-bearing:
|
|
411
|
-
#
|
|
466
|
+
# argv validation -> non-TTY/CI gate (hint + exit 2) -> provider gate or skip ->
|
|
412
467
|
# step 2 (idea / PRD path) -> step 3 (template) -> step 4 (plan + confirm) ->
|
|
413
468
|
# write PRD to CWD -> cmd_start --yes --no-plan (subshelled; it execs the runner).
|
|
469
|
+
#
|
|
470
|
+
# The non-TTY/CI gate admits two fully-specified shapes:
|
|
471
|
+
# loki quickstart "<idea>|<prd-path>" --yes
|
|
472
|
+
# loki quickstart "<idea>|<prd-path>" --dry-run
|
|
473
|
+
# Execution requires argv consent; preview forbids it. Both paths skip prompts
|
|
474
|
+
# and share template ranking plus the honest estimator. Only execution crosses
|
|
475
|
+
# the provider, confirm, write, and cmd_start boundaries.
|
|
414
476
|
cmd_quickstart() {
|
|
415
477
|
local positional=""
|
|
416
478
|
local assume_yes=false
|
|
479
|
+
local dry_run=false
|
|
480
|
+
local json_output=false
|
|
481
|
+
local template_override=""
|
|
482
|
+
local template_flag_seen=false
|
|
417
483
|
if _qs_assume_yes; then assume_yes=true; fi
|
|
418
484
|
|
|
485
|
+
# yes_flag tracks EXPLICIT --yes/-y on THIS command's argv, and nothing else.
|
|
486
|
+
# It is deliberately NOT assume_yes: _qs_assume_yes also returns true for an
|
|
487
|
+
# ambient LOKI_ASSUME_YES or LOKI_AUTO_CONFIRM=true, and LOKI_AUTO_CONFIRM is
|
|
488
|
+
# exactly what `loki --yes <anything>` (loki:2313) and CI-ish environments
|
|
489
|
+
# export. Gating the non-interactive bypass on assume_yes would let an
|
|
490
|
+
# ambient env var plus a stray positional silently start a PAID build in CI
|
|
491
|
+
# with no human in the loop. The safety contract asks for explicit consent,
|
|
492
|
+
# so consent must come from argv. assume_yes keeps its existing meaning for
|
|
493
|
+
# the confirm prompt, so the interactive journey is byte-identical.
|
|
494
|
+
local yes_flag=false
|
|
495
|
+
|
|
419
496
|
while [ $# -gt 0 ]; do
|
|
420
497
|
case "$1" in
|
|
421
498
|
--help|-h)
|
|
@@ -424,8 +501,30 @@ cmd_quickstart() {
|
|
|
424
501
|
;;
|
|
425
502
|
--yes|-y)
|
|
426
503
|
assume_yes=true
|
|
504
|
+
yes_flag=true
|
|
505
|
+
shift
|
|
506
|
+
;;
|
|
507
|
+
--dry-run)
|
|
508
|
+
dry_run=true
|
|
509
|
+
shift
|
|
510
|
+
;;
|
|
511
|
+
--json)
|
|
512
|
+
json_output=true
|
|
427
513
|
shift
|
|
428
514
|
;;
|
|
515
|
+
--template)
|
|
516
|
+
if [ "$template_flag_seen" = true ]; then
|
|
517
|
+
printf '%s--template may be specified only once.%s\n' "$_QS_RED" "$_QS_NC" >&2
|
|
518
|
+
exit 2
|
|
519
|
+
fi
|
|
520
|
+
template_flag_seen=true
|
|
521
|
+
if [ $# -lt 2 ] || [ -z "${2:-}" ] || [[ "${2:-}" == --* ]]; then
|
|
522
|
+
printf '%s--template requires an exact shipped template name.%s\n' "$_QS_RED" "$_QS_NC" >&2
|
|
523
|
+
exit 2
|
|
524
|
+
fi
|
|
525
|
+
template_override="$2"
|
|
526
|
+
shift 2
|
|
527
|
+
;;
|
|
429
528
|
--*)
|
|
430
529
|
printf '%sUnknown option: %s%s\n' "$_QS_RED" "$1" "$_QS_NC" >&2
|
|
431
530
|
printf "Run 'loki quickstart --help' for usage.\n" >&2
|
|
@@ -444,36 +543,133 @@ cmd_quickstart() {
|
|
|
444
543
|
esac
|
|
445
544
|
done
|
|
446
545
|
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
|
|
450
|
-
|
|
546
|
+
# A preview is an explicit no-execution request. Reject simultaneous build
|
|
547
|
+
# consent instead of guessing which instruction wins. This check precedes
|
|
548
|
+
# provider discovery, estimation, and every write.
|
|
549
|
+
if [ "$dry_run" = true ] && [ "$yes_flag" = true ]; then
|
|
550
|
+
printf '%s--dry-run cannot be combined with --yes or -y.%s\n' "$_QS_RED" "$_QS_NC" >&2
|
|
551
|
+
exit 2
|
|
552
|
+
fi
|
|
553
|
+
|
|
554
|
+
if [ "$json_output" = true ] && [ "$dry_run" != true ]; then
|
|
555
|
+
printf '%s--json requires --dry-run.%s\n' "$_QS_RED" "$_QS_NC" >&2
|
|
451
556
|
exit 2
|
|
452
557
|
fi
|
|
453
558
|
|
|
559
|
+
# Explicit template selection is deliberately an IDEA-only surface. It is
|
|
560
|
+
# validated before provider discovery, estimation, and every write/build
|
|
561
|
+
# boundary so a typo or conflicting PRD can never fall through to spend.
|
|
562
|
+
if [ "$template_flag_seen" = true ]; then
|
|
563
|
+
if [ -z "$positional" ]; then
|
|
564
|
+
printf '%s--template requires an IDEA argument.%s\n' "$_QS_RED" "$_QS_NC" >&2
|
|
565
|
+
exit 2
|
|
566
|
+
fi
|
|
567
|
+
case "$positional" in
|
|
568
|
+
*/*|*.md|*.markdown|*.txt|*.json|*.yaml|*.yml)
|
|
569
|
+
printf '%s--template cannot be combined with a PRD path.%s\n' "$_QS_RED" "$_QS_NC" >&2
|
|
570
|
+
exit 2
|
|
571
|
+
;;
|
|
572
|
+
esac
|
|
573
|
+
if [ -f "$positional" ]; then
|
|
574
|
+
printf '%s--template cannot be combined with a PRD path.%s\n' "$_QS_RED" "$_QS_NC" >&2
|
|
575
|
+
exit 2
|
|
576
|
+
fi
|
|
577
|
+
if ! _qs_template_exists "$template_override"; then
|
|
578
|
+
printf '%sUnknown shipped template: %s%s\n' "$_QS_RED" "$template_override" "$_QS_NC" >&2
|
|
579
|
+
exit 2
|
|
580
|
+
fi
|
|
581
|
+
fi
|
|
582
|
+
|
|
583
|
+
# Preview is deliberately non-interactive: without an explicit idea or PRD
|
|
584
|
+
# path there is nothing deterministic to estimate. A path-looking argument
|
|
585
|
+
# must resolve to a readable regular file; otherwise treating a typo such as
|
|
586
|
+
# ./prd.md as prose would preview an unrelated template and mislead the user.
|
|
587
|
+
if [ "$dry_run" = true ]; then
|
|
588
|
+
if [ -z "$positional" ]; then
|
|
589
|
+
printf 'loki quickstart --dry-run requires an IDEA or readable PRD path.\n' >&2
|
|
590
|
+
exit 2
|
|
591
|
+
fi
|
|
592
|
+
case "$positional" in
|
|
593
|
+
*/*|*.md|*.markdown|*.txt|*.json|*.yaml|*.yml)
|
|
594
|
+
if [ ! -f "$positional" ] || [ ! -r "$positional" ]; then
|
|
595
|
+
printf 'PRD path is not a readable file: %s\n' "$positional" >&2
|
|
596
|
+
exit 2
|
|
597
|
+
fi
|
|
598
|
+
;;
|
|
599
|
+
esac
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
# Non-TTY / CI: quickstart is interactive by definition, so by default it
|
|
603
|
+
# never hangs on a read -- it prints the automation hint to stderr and exits
|
|
604
|
+
# 2 (design 3.8).
|
|
605
|
+
#
|
|
606
|
+
# The execution exception is a fully-specified invocation:
|
|
607
|
+
# loki quickstart "<idea>" --yes (or a PRD path in place of the idea)
|
|
608
|
+
# Both halves are required. A non-empty positional supplies the input that
|
|
609
|
+
# steps 2-3 would otherwise have to ask for, and an explicit argv --yes
|
|
610
|
+
# supplies the consent step 4 would otherwise have to ask for. With both
|
|
611
|
+
# present there is nothing left to prompt about, so the refusal is pure
|
|
612
|
+
# friction for a newly installed operator running one command.
|
|
613
|
+
#
|
|
614
|
+
# Missing EITHER half keeps the refusal verbatim and writes nothing: a bare
|
|
615
|
+
# `loki quickstart` in CI still cannot spend, and neither can one that has an
|
|
616
|
+
# idea but no consent. Fail-closed is the load-bearing direction here -- the
|
|
617
|
+
# bypass must be something an operator opts into by typing both, never
|
|
618
|
+
# something an environment can arrive at on its own.
|
|
619
|
+
local noninteractive_ok=false
|
|
620
|
+
if _qs_non_interactive; then
|
|
621
|
+
if [ "$dry_run" = true ] || { [ -n "$positional" ] && [ "$yes_flag" = true ]; }; then
|
|
622
|
+
noninteractive_ok=true
|
|
623
|
+
else
|
|
624
|
+
printf 'loki quickstart is interactive and needs a terminal. For automation use: loki start <prd> --yes\n' >&2
|
|
625
|
+
exit 2
|
|
626
|
+
fi
|
|
627
|
+
fi
|
|
628
|
+
|
|
629
|
+
# Machine-readable preview follows the exact same selection and estimator
|
|
630
|
+
# path as the human preview. Suppress presentation stdout while retaining a
|
|
631
|
+
# duplicate of the caller's stdout on fd 3; _qs_emit_plan writes the single
|
|
632
|
+
# validated JSON object there only after estimation succeeds. Stderr stays
|
|
633
|
+
# available for fail-closed diagnostics.
|
|
634
|
+
local json_stdout_redirected=false
|
|
635
|
+
if [ "$json_output" = true ]; then
|
|
636
|
+
exec 3>&1 1>/dev/null
|
|
637
|
+
json_stdout_redirected=true
|
|
638
|
+
fi
|
|
639
|
+
|
|
454
640
|
printf '\n'
|
|
455
|
-
|
|
641
|
+
if [ "$dry_run" = true ]; then
|
|
642
|
+
printf '%sLoki Mode quickstart preview -- template and plan only.%s\n' "$_QS_BOLD" "$_QS_NC"
|
|
643
|
+
else
|
|
644
|
+
printf '%sLoki Mode quickstart -- four quick questions, then your build starts.%s\n' "$_QS_BOLD" "$_QS_NC"
|
|
645
|
+
fi
|
|
456
646
|
printf '\n'
|
|
457
647
|
|
|
458
648
|
# ----- Step 1 of 4: Setup (reuse the slice-B provider offer) -------------
|
|
459
649
|
printf '%sStep 1 of 4: Setup%s\n' "$_QS_BOLD" "$_QS_NC"
|
|
460
|
-
|
|
650
|
+
if [ "$dry_run" = true ]; then
|
|
651
|
+
# Estimation and deterministic template selection are local. Previewing
|
|
652
|
+
# must work before provider installation and must not run provider code.
|
|
653
|
+
printf ' Preview mode: provider check skipped; no build will start.\n'
|
|
654
|
+
else
|
|
655
|
+
printf ' Checking for an AI provider CLI ...\n'
|
|
461
656
|
# Ask the loader FIRST: it is the only thing that knows what the runner will
|
|
462
657
|
# really pick, and it is the only check that sees opencode. Falling back to
|
|
463
658
|
# detect_any_provider (stale four, PATH-only) keeps the no-provider guard
|
|
464
659
|
# intact when the loader is absent or unreadable.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
660
|
+
local found=""
|
|
661
|
+
found="$(_qs_selected_provider)" || found=""
|
|
662
|
+
if [ -n "$found" ]; then
|
|
663
|
+
printf ' Found: %s. Good.\n' "$found"
|
|
664
|
+
elif detect_any_provider; then
|
|
665
|
+
printf ' Found: an AI provider CLI. Good.\n'
|
|
666
|
+
else
|
|
667
|
+
# Run the inline install + login offer. provider_offer_gate returns 2 if
|
|
668
|
+
# no provider ends up available (declined, or install failed).
|
|
669
|
+
if ! provider_offer_gate; then
|
|
670
|
+
printf '%sNo provider available; cannot start a build. Install one and re-run loki quickstart.%s\n' "$_QS_RED" "$_QS_NC" >&2
|
|
671
|
+
exit 2
|
|
672
|
+
fi
|
|
477
673
|
fi
|
|
478
674
|
fi
|
|
479
675
|
printf '\n'
|
|
@@ -484,27 +680,34 @@ cmd_quickstart() {
|
|
|
484
680
|
local prd_source="" # an existing PRD file path, when the user has one
|
|
485
681
|
local brief="" # the one-line idea (drives template matching)
|
|
486
682
|
local template_name=""
|
|
683
|
+
local input_kind="idea"
|
|
487
684
|
|
|
488
685
|
if [ -n "$positional" ] && [ -f "$positional" ]; then
|
|
489
686
|
prd_source="$positional"
|
|
687
|
+
input_kind="prd"
|
|
490
688
|
printf '%sUsing your PRD: %s%s\n' "$_QS_DIM" "$positional" "$_QS_NC"
|
|
491
689
|
printf '\n'
|
|
492
690
|
else
|
|
493
691
|
printf '%sStep 2 of 4: What do you want to build?%s\n' "$_QS_BOLD" "$_QS_NC"
|
|
494
|
-
|
|
495
|
-
printf ' (Press Enter to build the sample Todo app.)\n'
|
|
496
|
-
if [ -n "$positional" ]; then
|
|
692
|
+
if [ "$dry_run" = true ]; then
|
|
497
693
|
brief="$positional"
|
|
498
|
-
printf '
|
|
694
|
+
printf ' Previewing idea: %s\n' "$brief"
|
|
499
695
|
else
|
|
500
|
-
|
|
501
|
-
printf '
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
prd_source="$answer"
|
|
696
|
+
printf ' Describe it in one line, or paste a path to a PRD file.\n'
|
|
697
|
+
printf ' (Press Enter to build the sample Todo app.)\n'
|
|
698
|
+
if [ -n "$positional" ]; then
|
|
699
|
+
brief="$positional"
|
|
700
|
+
printf '> %s\n' "$brief"
|
|
506
701
|
else
|
|
507
|
-
|
|
702
|
+
local answer=""
|
|
703
|
+
printf '> '
|
|
704
|
+
read -r answer 2>/dev/null || answer=""
|
|
705
|
+
# If the typed value is an existing file, treat it as a PRD path.
|
|
706
|
+
if [ -n "$answer" ] && [ -f "$answer" ]; then
|
|
707
|
+
prd_source="$answer"
|
|
708
|
+
else
|
|
709
|
+
brief="$answer"
|
|
710
|
+
fi
|
|
508
711
|
fi
|
|
509
712
|
fi
|
|
510
713
|
printf '\n'
|
|
@@ -512,6 +715,11 @@ cmd_quickstart() {
|
|
|
512
715
|
|
|
513
716
|
# ----- Step 3 of 4: Pick a template (skipped if a PRD path was given) ----
|
|
514
717
|
if [ -z "$prd_source" ]; then
|
|
718
|
+
if [ -n "$template_override" ]; then
|
|
719
|
+
template_name="$template_override"
|
|
720
|
+
printf '%sStep 3 of 4: Template%s\n' "$_QS_BOLD" "$_QS_NC"
|
|
721
|
+
printf ' Selected %s (--template).\n\n' "$template_name"
|
|
722
|
+
else
|
|
515
723
|
local -a top3=()
|
|
516
724
|
local line
|
|
517
725
|
while IFS= read -r line; do
|
|
@@ -523,25 +731,40 @@ cmd_quickstart() {
|
|
|
523
731
|
top3=("simple-todo-app")
|
|
524
732
|
fi
|
|
525
733
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
734
|
+
# The MENU, not just the read, is interactive-only. Offering "Choose 1-3"
|
|
735
|
+
# to a shell that can never answer is a prompt the operator has to read
|
|
736
|
+
# and mistrust; worse, it makes a transcript indistinguishable from one
|
|
737
|
+
# that actually stopped for input. The ranking above is shared by both
|
|
738
|
+
# paths -- only its presentation differs.
|
|
739
|
+
local pick=""
|
|
740
|
+
if [ "$noninteractive_ok" != true ]; then
|
|
741
|
+
printf '%sStep 3 of 4: Pick a starting template%s\n' "$_QS_BOLD" "$_QS_NC"
|
|
742
|
+
if [ -n "$brief" ]; then
|
|
743
|
+
printf ' Closest matches for "%s":\n' "$brief"
|
|
744
|
+
else
|
|
745
|
+
printf ' Closest matches for the sample Todo app:\n'
|
|
746
|
+
fi
|
|
747
|
+
local i=1 t suffix
|
|
748
|
+
for t in "${top3[@]}"; do
|
|
749
|
+
suffix=""
|
|
750
|
+
[ "$i" -eq 1 ] && suffix=" (default)"
|
|
751
|
+
printf ' %d) %-18s %s%s\n' "$i" "$t" "$(_qs_template_summary "$t")" "$suffix"
|
|
752
|
+
i=$((i + 1))
|
|
753
|
+
done
|
|
754
|
+
printf ' Choose 1-%d, or press Enter for 1.\n' "${#top3[@]}"
|
|
755
|
+
printf '> '
|
|
756
|
+
read -r pick 2>/dev/null || pick=""
|
|
757
|
+
printf '\n'
|
|
529
758
|
else
|
|
530
|
-
|
|
759
|
+
# Deterministic selection: the empty pick below resolves to top3[0],
|
|
760
|
+
# the same rank the interactive picker offers as "(default)". The
|
|
761
|
+
# ranking itself is unchanged -- _qs_score_templates is offline,
|
|
762
|
+
# deterministic and already the single source of order -- so the
|
|
763
|
+
# non-interactive choice is exactly the one an operator pressing
|
|
764
|
+
# Enter would get. No picker, no prompt, no second code path.
|
|
765
|
+
printf '%sStep 3 of 4: Template%s\n' "$_QS_BOLD" "$_QS_NC"
|
|
766
|
+
printf ' Selected %s (top match) for "%s".\n\n' "${top3[0]}" "$brief"
|
|
531
767
|
fi
|
|
532
|
-
local i=1 t suffix
|
|
533
|
-
for t in "${top3[@]}"; do
|
|
534
|
-
suffix=""
|
|
535
|
-
[ "$i" -eq 1 ] && suffix=" (default)"
|
|
536
|
-
printf ' %d) %-18s %s%s\n' "$i" "$t" "$(_qs_template_summary "$t")" "$suffix"
|
|
537
|
-
i=$((i + 1))
|
|
538
|
-
done
|
|
539
|
-
printf ' Choose 1-%d, or press Enter for 1.\n' "${#top3[@]}"
|
|
540
|
-
|
|
541
|
-
local pick=""
|
|
542
|
-
printf '> '
|
|
543
|
-
read -r pick 2>/dev/null || pick=""
|
|
544
|
-
printf '\n'
|
|
545
768
|
|
|
546
769
|
case "$pick" in
|
|
547
770
|
""|1) template_name="${top3[0]}";;
|
|
@@ -550,6 +773,8 @@ cmd_quickstart() {
|
|
|
550
773
|
*) template_name="${top3[0]}";; # any unexpected input -> the default
|
|
551
774
|
esac
|
|
552
775
|
|
|
776
|
+
fi
|
|
777
|
+
|
|
553
778
|
local tdir; tdir="$(_qs_templates_dir)"
|
|
554
779
|
prd_source="$tdir/$template_name.md"
|
|
555
780
|
if [ ! -f "$prd_source" ]; then
|
|
@@ -562,11 +787,41 @@ cmd_quickstart() {
|
|
|
562
787
|
|
|
563
788
|
# ----- Step 4 of 4: Review the plan (reuse the slice-A estimator) --------
|
|
564
789
|
printf '%sStep 4 of 4: Review the plan%s\n' "$_QS_BOLD" "$_QS_NC"
|
|
790
|
+
# The plan is ALWAYS rendered, on both paths, from the same real estimator.
|
|
791
|
+
# The non-interactive path shows it before execution rather than before a
|
|
792
|
+
# prompt: the operator still gets the honest quote in the transcript, and it
|
|
793
|
+
# is still the figure cmd_start runs with, so the quote equals the charge.
|
|
565
794
|
local estimate_ok=true
|
|
566
|
-
if ! _qs_emit_plan "$prd_source" "$template_name"; then
|
|
795
|
+
if ! _qs_emit_plan "$prd_source" "$template_name" "$json_output" "$input_kind"; then
|
|
567
796
|
estimate_ok=false
|
|
568
|
-
|
|
569
|
-
|
|
797
|
+
# No honest number available. Interactively this flips the confirm to
|
|
798
|
+
# default-NO below. Non-interactively there is no one to review the
|
|
799
|
+
# missing plan, so fail closed before writing a PRD or starting a build.
|
|
800
|
+
# Explicit --yes authorizes the displayed estimate; it is not consent
|
|
801
|
+
# to spend without one.
|
|
802
|
+
if [ "$noninteractive_ok" = true ]; then
|
|
803
|
+
if [ "$json_stdout_redirected" = true ]; then
|
|
804
|
+
exec 1>&3 3>&-
|
|
805
|
+
json_stdout_redirected=false
|
|
806
|
+
fi
|
|
807
|
+
printf 'Could not compute a cost estimate; non-interactive quickstart requires a displayed plan and started no build.\n' >&2
|
|
808
|
+
return 2
|
|
809
|
+
else
|
|
810
|
+
printf '%sCould not compute a cost estimate (the estimator did not return a result).%s\n' "$_QS_YELLOW" "$_QS_NC"
|
|
811
|
+
printf '\n'
|
|
812
|
+
fi
|
|
813
|
+
fi
|
|
814
|
+
|
|
815
|
+
# The preview terminal boundary is intentionally immediately after the
|
|
816
|
+
# shared estimator. Reaching it proves the same template and plan were
|
|
817
|
+
# rendered, while returning here makes the confirm, PRD copy, and cmd_start
|
|
818
|
+
# structurally unreachable.
|
|
819
|
+
if [ "$dry_run" = true ]; then
|
|
820
|
+
printf 'Preview complete. No provider was run, no file was written, and no build was started.\n'
|
|
821
|
+
if [ "$json_stdout_redirected" = true ]; then
|
|
822
|
+
exec 1>&3 3>&-
|
|
823
|
+
fi
|
|
824
|
+
return 0
|
|
570
825
|
fi
|
|
571
826
|
|
|
572
827
|
# ----- Confirm ----------------------------------------------------------
|
|
@@ -595,8 +850,16 @@ cmd_quickstart() {
|
|
|
595
850
|
local target="./prd.md"
|
|
596
851
|
if [ -e "$target" ]; then
|
|
597
852
|
local overwrite=""
|
|
598
|
-
|
|
599
|
-
|
|
853
|
+
# Non-interactive never asks and never overwrites. Leaving $overwrite
|
|
854
|
+
# empty falls into the existing suffix walk below, which is already the
|
|
855
|
+
# correct no-clobber behavior -- prd-quickstart.md, then numbered free
|
|
856
|
+
# suffixes exactly as needed. Reused rather than reimplemented so the two
|
|
857
|
+
# paths cannot drift, and so "never overwrite" holds by construction
|
|
858
|
+
# (there is no branch here that can reach the clobber).
|
|
859
|
+
if [ "$noninteractive_ok" != true ]; then
|
|
860
|
+
printf 'prd.md already exists. Overwrite? [y/N] '
|
|
861
|
+
read -r overwrite 2>/dev/null || overwrite=""
|
|
862
|
+
fi
|
|
600
863
|
if [[ ! "$overwrite" =~ ^[Yy] ]]; then
|
|
601
864
|
# Declining to overwrite one file must never silently destroy
|
|
602
865
|
# another (bug-hunt MEDIUM): the fallback gets the same existence
|
package/dashboard/__init__.py
CHANGED
|
@@ -133,9 +133,12 @@ def _git_tags(repo_dir: str, limit: int) -> tuple:
|
|
|
133
133
|
"refs/tags",
|
|
134
134
|
]
|
|
135
135
|
try:
|
|
136
|
+
# Fixed git executable, integer-formatted option, separate cwd, and an
|
|
137
|
+
# argv vector keep caller data out of shell parsing.
|
|
136
138
|
proc = subprocess.run(
|
|
137
|
-
cmd,
|
|
138
|
-
|
|
139
|
+
cmd, # lgtm[py/command-line-injection]
|
|
140
|
+
cwd=repo_dir, capture_output=True, text=True,
|
|
141
|
+
timeout=_GIT_TIMEOUT_S, shell=False,
|
|
139
142
|
)
|
|
140
143
|
except FileNotFoundError:
|
|
141
144
|
return [], "git executable not found on PATH"
|
package/dashboard/control.py
CHANGED
|
@@ -495,14 +495,17 @@ async def start_session(request: StartRequest):
|
|
|
495
495
|
|
|
496
496
|
try:
|
|
497
497
|
# Start the process
|
|
498
|
+
# RUN_SH is a trusted fixed path; provider and PRD were validated above,
|
|
499
|
+
# and argv is passed directly without a command shell.
|
|
498
500
|
process = subprocess.Popen(
|
|
499
|
-
args,
|
|
501
|
+
args, # lgtm[py/command-line-injection]
|
|
500
502
|
stdout=subprocess.DEVNULL,
|
|
501
503
|
stderr=subprocess.DEVNULL,
|
|
502
504
|
start_new_session=True,
|
|
503
505
|
# A deleted cwd would make Popen itself raise; fall back to the
|
|
504
506
|
# skill tree rather than failing to launch the run at all.
|
|
505
|
-
cwd=str(_cwd_or_skill_dir())
|
|
507
|
+
cwd=str(_cwd_or_skill_dir()),
|
|
508
|
+
shell=False,
|
|
506
509
|
)
|
|
507
510
|
|
|
508
511
|
# Save provider for status tracking
|
package/dashboard/server.py
CHANGED
|
@@ -4313,13 +4313,16 @@ def _start_supervised_workspace_build(
|
|
|
4313
4313
|
supervisor_log = _build_execution.execution_dir(execution_id) / "supervisor.log"
|
|
4314
4314
|
try:
|
|
4315
4315
|
with _build_execution.open_private_log(supervisor_log) as log_handle:
|
|
4316
|
+
# sys.executable and the module name are fixed; validated build
|
|
4317
|
+
# fields remain argv data and never reach a command shell.
|
|
4316
4318
|
process = subprocess.Popen(
|
|
4317
|
-
supervisor_args,
|
|
4319
|
+
supervisor_args, # lgtm[py/command-line-injection]
|
|
4318
4320
|
stdout=log_handle,
|
|
4319
4321
|
stderr=subprocess.STDOUT,
|
|
4320
4322
|
start_new_session=True,
|
|
4321
4323
|
cwd=str(skill_dir),
|
|
4322
4324
|
env=popen_env,
|
|
4325
|
+
shell=False,
|
|
4323
4326
|
)
|
|
4324
4327
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
4325
4328
|
finished_at = _build_execution.utc_now()
|
|
@@ -4582,13 +4585,16 @@ async def start_build(request: Request, body: StartBuildRequest):
|
|
|
4582
4585
|
args.append("--bg")
|
|
4583
4586
|
args.append(str(spec_file))
|
|
4584
4587
|
try:
|
|
4588
|
+
# run_sh is a trusted fixed path; provider and spec were validated, and
|
|
4589
|
+
# the argv vector is executed directly without shell interpretation.
|
|
4585
4590
|
process = subprocess.Popen(
|
|
4586
|
-
args,
|
|
4591
|
+
args, # lgtm[py/command-line-injection]
|
|
4587
4592
|
stdout=subprocess.DEVNULL,
|
|
4588
4593
|
stderr=subprocess.DEVNULL,
|
|
4589
4594
|
start_new_session=True,
|
|
4590
4595
|
cwd=str(project_dir),
|
|
4591
4596
|
env=popen_env,
|
|
4597
|
+
shell=False,
|
|
4592
4598
|
)
|
|
4593
4599
|
except (OSError, subprocess.SubprocessError) as e:
|
|
4594
4600
|
raise HTTPException(status_code=500, detail=f"Failed to start build: {e}")
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -183,6 +183,17 @@ builds the sample Todo app.
|
|
|
183
183
|
loki quickstart
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
For a fully specified first build, provide the idea, an optional exact shipped
|
|
187
|
+
starter template, and explicit consent:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
loki quickstart "an internal reporting workspace" --template dashboard --yes
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Use `--dry-run` instead of `--yes` to preview the same template and plan without
|
|
194
|
+
provider discovery, file writes, or execution; add `--json` for one versioned
|
|
195
|
+
machine-readable object.
|
|
196
|
+
|
|
186
197
|
Drop a spec -- any artifact that describes what you want built -- and Loki
|
|
187
198
|
Mode takes it from spec to deployed app. Specs can be a markdown PRD, a
|
|
188
199
|
GitHub issue URL, or a YAML feature description.
|
|
@@ -25,6 +25,21 @@ loki outcomes canary evaluate report.json observations.json \
|
|
|
25
25
|
|
|
26
26
|
Evaluation is opt-in. Without `--enable-evaluation`, the command refuses.
|
|
27
27
|
|
|
28
|
+
Automation that may act only on one exact decision can add an explicit verdict
|
|
29
|
+
gate:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
loki outcomes canary evaluate report.json observations.json \
|
|
33
|
+
--enable-evaluation --control-route safe --canary-percent 10 \
|
|
34
|
+
--require-verdict PROMOTE --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--require-verdict` accepts only `PROMOTE`, `HOLD`, or `ROLLBACK`. A complete
|
|
38
|
+
evaluation still prints its actual verdict, but exits 3 when that verdict does
|
|
39
|
+
not exactly match the required value. JSON output also includes
|
|
40
|
+
`required_verdict` and `requirement_met` when the gate is requested. Omitting the
|
|
41
|
+
gate preserves the existing behavior: every complete verdict exits 0.
|
|
42
|
+
|
|
28
43
|
## Retain a decision receipt
|
|
29
44
|
|
|
30
45
|
After a complete evaluation, create one immutable portable proof by independently
|
|
@@ -35,6 +50,19 @@ loki outcomes canary receipt report.json observations.json receipt.json \
|
|
|
35
50
|
--enable-receipt --control-route safe --canary-percent 10 --min-samples 5
|
|
36
51
|
```
|
|
37
52
|
|
|
53
|
+
Automation can require the rederived verdict before the receipt is published:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
loki outcomes canary receipt report.json observations.json receipt.json \
|
|
57
|
+
--enable-receipt --control-route safe --canary-percent 10 --min-samples 5 \
|
|
58
|
+
--require-verdict PROMOTE --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The target is created only when the actual verdict exactly matches the required
|
|
62
|
+
`PROMOTE`, `HOLD`, or `ROLLBACK` value. A mismatch exits `3`, reports both verdicts
|
|
63
|
+
and `"requirement_met": false`, and leaves the target absent. Omitting
|
|
64
|
+
`--require-verdict` preserves the existing behavior of recording any complete verdict.
|
|
65
|
+
|
|
38
66
|
The command creates a new canonical `loki-outcome-canary-decision-receipt/v1`
|
|
39
67
|
file. It binds the exact report, source, and observation digests; the full
|
|
40
68
|
evaluation policy; privacy-safe aggregate arm results; the acceptance delta; and
|
|
@@ -56,6 +84,20 @@ loki outcomes canary verify report.json observations.json receipt.json \
|
|
|
56
84
|
--enable-verification --json
|
|
57
85
|
```
|
|
58
86
|
|
|
87
|
+
Automation that may act only on one exact independently verified decision can
|
|
88
|
+
gate the verifier itself:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
loki outcomes canary verify report.json observations.json receipt.json \
|
|
92
|
+
--enable-verification --require-verdict PROMOTE --json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`--require-verdict` accepts only `PROMOTE`, `HOLD`, or `ROLLBACK`. A receipt that
|
|
96
|
+
fully verifies still prints its actual verdict, but exits 3 when that verdict
|
|
97
|
+
does not exactly match the required value. JSON output also includes
|
|
98
|
+
`required_verdict` and `requirement_met` when the gate is requested. Omitting the
|
|
99
|
+
gate preserves the existing behavior: every fully verified receipt exits 0.
|
|
100
|
+
|
|
59
101
|
The read-only verifier takes the policy from the canonical receipt, independently
|
|
60
102
|
reruns the deterministic evaluation, rechecks the report, source, and observation
|
|
61
103
|
bytes, and requires the complete rederived receipt to match exactly. It returns
|
|
@@ -139,5 +181,6 @@ unbound evidence returns `REFUSED` rather than a partial verdict.
|
|
|
139
181
|
`--json` emits the aggregate arms, policy, exact evidence digests, verdict, and
|
|
140
182
|
refusal reasons. It is portable across machines because it omits local input paths.
|
|
141
183
|
The default output is a short human-readable summary. Exit 0 means
|
|
142
|
-
a verdict was produced (including `ROLLBACK`)
|
|
143
|
-
|
|
184
|
+
a verdict was produced (including `ROLLBACK`) and any requested exact-verdict
|
|
185
|
+
gate matched. Exit 3 means evaluation was refused or an exact-verdict gate did
|
|
186
|
+
not match, 64 is an invocation error, and 66 is a missing input file.
|
package/docs/PROOF-PASSPORT.md
CHANGED
|
@@ -5,10 +5,12 @@ receipt. The JSON artifact preserves the exact contract and receipt digests,
|
|
|
5
5
|
the verifier verdict, executor/verifier separation, and explicit trust
|
|
6
6
|
limitations.
|
|
7
7
|
|
|
8
|
-
Generate the machine-readable passport and a portable PR/CI summary together
|
|
8
|
+
Generate the machine-readable passport and a portable PR/CI summary together
|
|
9
|
+
from the installed product. The receipt argument can be a proof id from
|
|
10
|
+
`loki proof list` or a path to any provider's compatible `proof.json`:
|
|
9
11
|
|
|
10
12
|
```bash
|
|
11
|
-
|
|
13
|
+
loki proof passport \
|
|
12
14
|
outcome-contract.json proof.json proof-passport.json \
|
|
13
15
|
--repo-dir . \
|
|
14
16
|
--markdown-output proof-passport.md
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.22.
|
|
2
|
+
var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.22.4";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Yf(Jf(import.meta.url)),Q=X$(X);h5=Xf(Qf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var _O={};l0(_O,{runOrThrow:()=>Tf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Cf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>bO});async function NQ(Z,X=bO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let V=$.byteLength-(z-X);J+=Y.decode($.subarray(0,V),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Tf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=wf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function wf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Cf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var bO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Ff?"":Z}var Ff,A0,F8,_0,$W0,a0,V8,Q9,h;var S6=p(()=>{Ff=(process.env.NO_COLOR??"").length>0;A0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),_0=o7("\x1B[1;33m"),$W0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),V8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),h=o7("\x1B[0m")});import{existsSync as ff}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(ff(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var XA={};l0(XA,{runStatus:()=>Vh});import{existsSync as Y9,readFileSync as g3,readdirSync as aO,statSync as sO}from"fs";import{resolve as h8,basename as ef}from"path";import{homedir as Zh}from"os";function nO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function oO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*AQ/X);if(J>AQ)J=AQ;let z=AQ-J,K=F8;if(Y>=80)K=A0;else if(Y>=50)K=_0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),V=nO(Z),W=nO(X);return` ${V8}${Q}${h} ${K}[${$}]${h} ${Y}% (${V} / ${W})`}async function Qh(){if(await X9("jq"))return!0;return process.stdout.write(`${A0}Error: jq is required but not installed.${h}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -1236,4 +1236,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1236
1236
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (g_(),v_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1237
1237
|
`),process.stderr.write(m_),2}}lO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var dV0=await pV0(Bun.argv.slice(2));process.exit(dV0);
|
|
1238
1238
|
|
|
1239
|
-
//# debugId=
|
|
1239
|
+
//# debugId=C61EF35407CE212F64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "9.22.
|
|
4
|
+
"version": "9.22.4",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "9.22.
|
|
5
|
+
"version": "9.22.4",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
|
@@ -235,6 +235,7 @@ def main(argv=None):
|
|
|
235
235
|
parser.add_argument("--max-risk", type=float, default=.25)
|
|
236
236
|
parser.add_argument("--min-samples", type=int, default=5)
|
|
237
237
|
parser.add_argument("--min-lift-bps", type=int, default=1)
|
|
238
|
+
parser.add_argument("--require-verdict", choices=("PROMOTE", "HOLD", "ROLLBACK"))
|
|
238
239
|
parser.add_argument("--json", action="store_true")
|
|
239
240
|
args = parser.parse_args(argv)
|
|
240
241
|
for path in (args.report, args.observations):
|
|
@@ -245,8 +246,19 @@ def main(argv=None):
|
|
|
245
246
|
args.report, args.observations, args.control_route, args.canary_percent,
|
|
246
247
|
args.max_risk, args.min_samples, args.min_lift_bps, args.enable_evaluation,
|
|
247
248
|
)
|
|
249
|
+
requirement_met = (
|
|
250
|
+
not result["refusal_reasons"]
|
|
251
|
+
and (args.require_verdict is None or result["verdict"] == args.require_verdict)
|
|
252
|
+
)
|
|
248
253
|
if args.json:
|
|
249
|
-
|
|
254
|
+
output = result
|
|
255
|
+
if args.require_verdict is not None:
|
|
256
|
+
output = {
|
|
257
|
+
**result,
|
|
258
|
+
"required_verdict": args.require_verdict,
|
|
259
|
+
"requirement_met": requirement_met,
|
|
260
|
+
}
|
|
261
|
+
print(json.dumps(output, sort_keys=True))
|
|
250
262
|
elif result["refusal_reasons"]:
|
|
251
263
|
print("Canary evaluation: REFUSED")
|
|
252
264
|
for reason in result["refusal_reasons"]:
|
|
@@ -264,7 +276,10 @@ def main(argv=None):
|
|
|
264
276
|
print(f" accepted delta: {result['accepted_delta_bps']} bps")
|
|
265
277
|
print(f" report sha256: {result['report_sha256']}")
|
|
266
278
|
print(f" observations sha256: {result['observations_sha256']}")
|
|
267
|
-
|
|
279
|
+
if args.require_verdict is not None:
|
|
280
|
+
status = "MET" if requirement_met else "NOT MET"
|
|
281
|
+
print(f" required verdict: {args.require_verdict} ({status})")
|
|
282
|
+
return OK if requirement_met else REFUSED
|
|
268
283
|
|
|
269
284
|
|
|
270
285
|
if __name__ == "__main__":
|
|
@@ -160,6 +160,7 @@ def main(argv=None):
|
|
|
160
160
|
parser.add_argument("observations")
|
|
161
161
|
parser.add_argument("receipt")
|
|
162
162
|
parser.add_argument("--enable-verification", action="store_true")
|
|
163
|
+
parser.add_argument("--require-verdict", choices=("PROMOTE", "HOLD", "ROLLBACK"))
|
|
163
164
|
parser.add_argument("--json", action="store_true")
|
|
164
165
|
args = parser.parse_args(argv)
|
|
165
166
|
for path in (args.report, args.observations, args.receipt):
|
|
@@ -169,14 +170,28 @@ def main(argv=None):
|
|
|
169
170
|
result = verify_receipt(
|
|
170
171
|
args.report, args.observations, args.receipt, args.enable_verification
|
|
171
172
|
)
|
|
173
|
+
requirement_met = (
|
|
174
|
+
result["status"] == "VERIFIED"
|
|
175
|
+
and (args.require_verdict is None or result["verdict"] == args.require_verdict)
|
|
176
|
+
)
|
|
172
177
|
if args.json:
|
|
173
|
-
|
|
178
|
+
output = result
|
|
179
|
+
if args.require_verdict is not None:
|
|
180
|
+
output = {
|
|
181
|
+
**result,
|
|
182
|
+
"required_verdict": args.require_verdict,
|
|
183
|
+
"requirement_met": requirement_met,
|
|
184
|
+
}
|
|
185
|
+
print(json.dumps(output, sort_keys=True))
|
|
174
186
|
elif result["status"] == "VERIFIED":
|
|
175
187
|
print(f"Canary decision receipt: VERIFIED ({result['verdict']})")
|
|
176
188
|
print(f" sha256={result['receipt_sha256']}")
|
|
189
|
+
if args.require_verdict is not None:
|
|
190
|
+
status = "MET" if requirement_met else "NOT MET"
|
|
191
|
+
print(f" required verdict: {args.require_verdict} ({status})")
|
|
177
192
|
else:
|
|
178
193
|
print(f"Canary decision receipt: REFUSED ({result['refusal_reason']})")
|
|
179
|
-
return OK if
|
|
194
|
+
return OK if requirement_met else REFUSED
|
|
180
195
|
|
|
181
196
|
|
|
182
197
|
if __name__ == "__main__":
|
|
@@ -13,6 +13,7 @@ import tempfile
|
|
|
13
13
|
OK, REFUSED, USAGE, NO_INPUT = 0, 3, 64, 66
|
|
14
14
|
DOMAIN = "loki-outcome-canary-decision-receipt/v1"
|
|
15
15
|
MAX_BYTES = 5 * 1024 * 1024
|
|
16
|
+
VERDICTS = {"PROMOTE", "HOLD", "ROLLBACK"}
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class Parser(argparse.ArgumentParser):
|
|
@@ -106,7 +107,8 @@ def _publish_create_only(path, payload):
|
|
|
106
107
|
|
|
107
108
|
def create_receipt(report_path, observations_path, receipt_path, control_route,
|
|
108
109
|
canary_percent=10.0, max_risk=.25, min_samples=5,
|
|
109
|
-
min_lift_bps=1, enable_receipt=False
|
|
110
|
+
min_lift_bps=1, enable_receipt=False,
|
|
111
|
+
require_verdict=None):
|
|
110
112
|
"""Reverify a decision and publish one immutable portable receipt."""
|
|
111
113
|
result = {
|
|
112
114
|
"receipt": DOMAIN,
|
|
@@ -118,6 +120,14 @@ def create_receipt(report_path, observations_path, receipt_path, control_route,
|
|
|
118
120
|
if not enable_receipt:
|
|
119
121
|
result["refusal_reason"] = "receipt_not_enabled"
|
|
120
122
|
return result
|
|
123
|
+
if require_verdict is not None:
|
|
124
|
+
result.update({
|
|
125
|
+
"required_verdict": require_verdict,
|
|
126
|
+
"requirement_met": False,
|
|
127
|
+
})
|
|
128
|
+
if require_verdict not in VERDICTS:
|
|
129
|
+
result["refusal_reason"] = "required_verdict_invalid"
|
|
130
|
+
return result
|
|
121
131
|
try:
|
|
122
132
|
_target_is_absent(receipt_path)
|
|
123
133
|
evaluator = _load_tool(
|
|
@@ -127,11 +137,11 @@ def create_receipt(report_path, observations_path, receipt_path, control_route,
|
|
|
127
137
|
report_path, observations_path, control_route, canary_percent,
|
|
128
138
|
max_risk, min_samples, min_lift_bps, True,
|
|
129
139
|
)
|
|
130
|
-
if decision.get("refusal_reasons") or decision.get("verdict") not in
|
|
131
|
-
"PROMOTE", "HOLD", "ROLLBACK"
|
|
132
|
-
}:
|
|
140
|
+
if decision.get("refusal_reasons") or decision.get("verdict") not in VERDICTS:
|
|
133
141
|
result["refusal_reason"] = "evaluation_refused"
|
|
134
142
|
return result
|
|
143
|
+
if require_verdict is not None:
|
|
144
|
+
result["verdict"] = decision["verdict"]
|
|
135
145
|
|
|
136
146
|
report_bytes = _read_named_regular(report_path)
|
|
137
147
|
observations_bytes = _read_named_regular(observations_path)
|
|
@@ -149,6 +159,9 @@ def create_receipt(report_path, observations_path, receipt_path, control_route,
|
|
|
149
159
|
if _sha256(source_bytes) != decision["source_sha256"]:
|
|
150
160
|
result["refusal_reason"] = "source_unsafe_or_drifted"
|
|
151
161
|
return result
|
|
162
|
+
if require_verdict is not None and decision["verdict"] != require_verdict:
|
|
163
|
+
result["refusal_reason"] = "verdict_mismatch"
|
|
164
|
+
return result
|
|
152
165
|
|
|
153
166
|
receipt = {
|
|
154
167
|
"receipt": DOMAIN,
|
|
@@ -184,6 +197,8 @@ def create_receipt(report_path, observations_path, receipt_path, control_route,
|
|
|
184
197
|
"verdict": decision["verdict"],
|
|
185
198
|
"receipt_sha256": _sha256(payload),
|
|
186
199
|
})
|
|
200
|
+
if require_verdict is not None:
|
|
201
|
+
result["requirement_met"] = True
|
|
187
202
|
except FileNotFoundError:
|
|
188
203
|
result["refusal_reason"] = "input_missing"
|
|
189
204
|
except (OSError, ValueError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
@@ -210,6 +225,7 @@ def main(argv=None):
|
|
|
210
225
|
parser.add_argument("--max-risk", type=float, default=.25)
|
|
211
226
|
parser.add_argument("--min-samples", type=int, default=5)
|
|
212
227
|
parser.add_argument("--min-lift-bps", type=int, default=1)
|
|
228
|
+
parser.add_argument("--require-verdict", choices=("PROMOTE", "HOLD", "ROLLBACK"))
|
|
213
229
|
parser.add_argument("--json", action="store_true")
|
|
214
230
|
args = parser.parse_args(argv)
|
|
215
231
|
for path in (args.report, args.observations):
|
|
@@ -219,15 +235,20 @@ def main(argv=None):
|
|
|
219
235
|
result = create_receipt(
|
|
220
236
|
args.report, args.observations, args.receipt, args.control_route,
|
|
221
237
|
args.canary_percent, args.max_risk, args.min_samples,
|
|
222
|
-
args.min_lift_bps, args.enable_receipt,
|
|
238
|
+
args.min_lift_bps, args.enable_receipt, args.require_verdict,
|
|
223
239
|
)
|
|
224
240
|
if args.json:
|
|
225
241
|
print(json.dumps(result, sort_keys=True))
|
|
226
242
|
elif result["status"] == "RECORDED":
|
|
227
243
|
print(f"Canary decision receipt: RECORDED ({result['verdict']})")
|
|
228
244
|
print(f" sha256={result['receipt_sha256']}")
|
|
245
|
+
if args.require_verdict is not None:
|
|
246
|
+
print(f" required verdict: {args.require_verdict} (MET)")
|
|
229
247
|
else:
|
|
230
248
|
print(f"Canary decision receipt: REFUSED ({result['refusal_reason']})")
|
|
249
|
+
if args.require_verdict is not None and result["verdict"] is not None:
|
|
250
|
+
print(f" actual verdict: {result['verdict']}")
|
|
251
|
+
print(f" required verdict: {args.require_verdict} (NOT MET)")
|
|
231
252
|
return OK if result["status"] == "RECORDED" else REFUSED
|
|
232
253
|
|
|
233
254
|
|
package/web-app/server.py
CHANGED
|
@@ -2668,14 +2668,17 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
2668
2668
|
if req.provider:
|
|
2669
2669
|
build_env["LOKI_PROVIDER"] = req.provider
|
|
2670
2670
|
|
|
2671
|
+
# LOKI_CLI is a trusted fixed path; request data stays in an argv
|
|
2672
|
+
# vector and is never parsed as shell command text.
|
|
2671
2673
|
proc = subprocess.Popen(
|
|
2672
|
-
cmd,
|
|
2674
|
+
cmd, # lgtm[py/command-line-injection]
|
|
2673
2675
|
stdout=subprocess.PIPE,
|
|
2674
2676
|
stderr=subprocess.STDOUT,
|
|
2675
2677
|
stdin=subprocess.DEVNULL,
|
|
2676
2678
|
text=True,
|
|
2677
2679
|
cwd=project_dir,
|
|
2678
2680
|
env=build_env,
|
|
2681
|
+
shell=False,
|
|
2679
2682
|
**({"start_new_session": True} if sys.platform != "win32"
|
|
2680
2683
|
else {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}),
|
|
2681
2684
|
)
|
|
@@ -4253,8 +4256,10 @@ async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
|
|
|
4253
4256
|
chat_env.update(_load_secrets())
|
|
4254
4257
|
# Pass provider via env for quick mode (loki quick uses LOKI_PROVIDER env)
|
|
4255
4258
|
chat_env["LOKI_PROVIDER"] = chat_provider
|
|
4259
|
+
# loki is resolved from the trusted application install; message,
|
|
4260
|
+
# provider, and PRD path remain argv data with no command shell.
|
|
4256
4261
|
proc = subprocess.Popen(
|
|
4257
|
-
cmd_args,
|
|
4262
|
+
cmd_args, # lgtm[py/command-line-injection]
|
|
4258
4263
|
stdout=subprocess.PIPE,
|
|
4259
4264
|
stderr=subprocess.STDOUT,
|
|
4260
4265
|
stdin=subprocess.DEVNULL,
|
|
@@ -4262,6 +4267,7 @@ async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
|
|
|
4262
4267
|
cwd=str(target),
|
|
4263
4268
|
env=chat_env,
|
|
4264
4269
|
start_new_session=True,
|
|
4270
|
+
shell=False,
|
|
4265
4271
|
)
|
|
4266
4272
|
task.process = proc
|
|
4267
4273
|
_track_child_pid(proc.pid)
|