loki-mode 9.54.2 → 9.55.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 +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +110 -34
- package/autonomy/crash.sh +6 -6
- package/autonomy/lib/done-recognition.sh +7 -0
- package/autonomy/lib/prd-enrich.sh +7 -0
- package/autonomy/lib/proof-verify.py +93 -21
- package/autonomy/lib/voter-agents.sh +7 -0
- package/autonomy/loki +404 -119
- package/autonomy/quickstart.sh +7 -0
- package/autonomy/run.sh +605 -30
- package/autonomy/verify.sh +114 -8
- package/bin/loki +12 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +6 -5
- package/docs/air-gapped.md +39 -17
- package/docs/enterprise/security.md +10 -13
- package/docs/environment-variables.md +1 -0
- package/docs/exit-codes.md +76 -2
- package/docs/v10/BACKLOG.md +23 -0
- package/docs/v10/BOARD.md +61 -32
- package/docs/v10/CONTROL.md +36 -0
- package/docs/v10/DECISIONS.md +31 -0
- package/docs/v10/METRICS.md +78 -1
- package/docs/v10/PROGRESS.md +197 -0
- package/loki-ts/dist/loki.js +7 -4
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/loader.sh +10 -0
- package/tools/receipt-attest.py +1 -1
package/autonomy/verify.sh
CHANGED
|
@@ -1689,14 +1689,26 @@ verify_gate_runtime() {
|
|
|
1689
1689
|
local boot_log="$artifact_dir/boot.log"
|
|
1690
1690
|
|
|
1691
1691
|
# Boot the app in the background, bounded by the timeout wrapper. The whole
|
|
1692
|
-
# process group is killed on teardown. Env is inherited (no secrets injected)
|
|
1693
|
-
#
|
|
1694
|
-
#
|
|
1695
|
-
# it
|
|
1692
|
+
# process group is killed on teardown. Env is inherited (no secrets injected)
|
|
1693
|
+
# PLUS one new marker: LOKI_VERIFY_RUN_TOKEN, a per-run random value used
|
|
1694
|
+
# ONLY as an ownership proof at teardown (see _verify_runtime_teardown) --
|
|
1695
|
+
# it identifies "a process this exact verify run launched" even after a
|
|
1696
|
+
# setsid()'d daemon detaches into its own session/process group, which
|
|
1697
|
+
# defeats every pgid/ppid-based check (round-4 council finding: v7.109.0's
|
|
1698
|
+
# daemonize-leak fixture uses child_process.spawn({detached:true}), and no
|
|
1699
|
+
# pgid/ppid arm can ever match a setsid'd process). Environment variables
|
|
1700
|
+
# survive setsid() and fork()/spawn() regardless of process-group changes,
|
|
1701
|
+
# so this closes exactly the gap those checks cannot. PORT is exported to
|
|
1702
|
+
# the resolved port so 12-factor apps (node, many python frameworks) listen
|
|
1703
|
+
# where the probe looks; apps that hardcode a port ignore it harmlessly.
|
|
1704
|
+
# This keeps boot-port and probe-port consistent.
|
|
1705
|
+
local run_token=""
|
|
1706
|
+
run_token="verify-$$-${RANDOM}-${RANDOM}-$(date +%s 2>/dev/null || echo 0)"
|
|
1696
1707
|
local app_pid=""
|
|
1697
1708
|
(
|
|
1698
1709
|
cd "$tree" || exit 127
|
|
1699
1710
|
export PORT="$port"
|
|
1711
|
+
export LOKI_VERIFY_RUN_TOKEN="$run_token"
|
|
1700
1712
|
exec "$timeout_bin" "$boot_timeout" sh -c "$method"
|
|
1701
1713
|
) >"$boot_log" 2>&1 &
|
|
1702
1714
|
app_pid=$!
|
|
@@ -1769,7 +1781,7 @@ verify_gate_runtime() {
|
|
|
1769
1781
|
# Reclaim BOTH the detected port and the actually-bound port (scraped from the
|
|
1770
1782
|
# boot log) so a server that daemonized onto a different port than we guessed
|
|
1771
1783
|
# does not leak. scraped_port may be empty (no banner) -- teardown handles it.
|
|
1772
|
-
_verify_runtime_teardown "$app_pid" "$port" "$scraped_port"
|
|
1784
|
+
_verify_runtime_teardown "$app_pid" "$port" "$scraped_port" "$run_token"
|
|
1773
1785
|
|
|
1774
1786
|
# Interpret the result.
|
|
1775
1787
|
if [ "$answered" != "true" ]; then
|
|
@@ -1907,8 +1919,34 @@ SMOKE_SCRIPT
|
|
|
1907
1919
|
|
|
1908
1920
|
# Teardown: terminate the boot launcher and any process still holding the port.
|
|
1909
1921
|
# Bounded and best effort; never blocks the gate.
|
|
1922
|
+
# Does pid's environment carry LOKI_VERIFY_RUN_TOKEN=<tok> exactly? Portable
|
|
1923
|
+
# ownership proof that survives setsid() (see the fourth arm below for why
|
|
1924
|
+
# pgid/ppid checks cannot). Fails CLOSED: an empty token, an unreadable
|
|
1925
|
+
# environment, or any ambiguity returns "no match" -- never a false positive
|
|
1926
|
+
# that would authorize a kill. Checked directly (not assumed) on this
|
|
1927
|
+
# codebase's target platforms: Linux via /proc/<pid>/environ (NUL-separated,
|
|
1928
|
+
# same-uid readable); macOS/BSD via `ps -E -ww -o command=` (the `-E` flag
|
|
1929
|
+
# specifically requests the environment block; `-e` on BSD ps means "all
|
|
1930
|
+
# processes" and shows nothing extra -- do not confuse the two).
|
|
1931
|
+
_verify_runtime_pid_has_token() {
|
|
1932
|
+
local pid="$1" tok="$2"
|
|
1933
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
1934
|
+
[ -n "$tok" ] || return 1
|
|
1935
|
+
if [ -r "/proc/$pid/environ" ]; then
|
|
1936
|
+
tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -qxF "LOKI_VERIFY_RUN_TOKEN=$tok"
|
|
1937
|
+
return $?
|
|
1938
|
+
fi
|
|
1939
|
+
if command -v ps >/dev/null 2>&1; then
|
|
1940
|
+
case " $(ps -E -ww -o command= -p "$pid" 2>/dev/null) " in
|
|
1941
|
+
*" LOKI_VERIFY_RUN_TOKEN=$tok "*) return 0 ;;
|
|
1942
|
+
*) return 1 ;;
|
|
1943
|
+
esac
|
|
1944
|
+
fi
|
|
1945
|
+
return 1
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1910
1948
|
_verify_runtime_teardown() {
|
|
1911
|
-
local app_pid="$1" port="$2" scraped_port="${3:-}"
|
|
1949
|
+
local app_pid="$1" port="$2" scraped_port="${3:-}" run_token="${4:-}"
|
|
1912
1950
|
if [ -n "$app_pid" ]; then
|
|
1913
1951
|
# Best: kill the launcher's whole process GROUP so a server it spawned in
|
|
1914
1952
|
# a subshell (e.g. `vite &`) dies too. GUARDED against self-suicide: in a
|
|
@@ -1943,6 +1981,22 @@ _verify_runtime_teardown() {
|
|
|
1943
1981
|
# Reclaim BOTH the detected port and the actually-bound (scraped) port from
|
|
1944
1982
|
# any orphan that outlived the launcher -- a daemonized server that bound a
|
|
1945
1983
|
# different port than we guessed would otherwise leak. Bounded, best effort.
|
|
1984
|
+
#
|
|
1985
|
+
# SCOPING (D14/D15/D16 class): `lsof -ti tcp:$port` alone, with no
|
|
1986
|
+
# -sTCP:LISTEN filter, matches EVERY socket referencing that port number,
|
|
1987
|
+
# including a CLIENT with an established connection whose remote port
|
|
1988
|
+
# happens to be $port (e.g. a browser tab open to localhost:$port) -- that
|
|
1989
|
+
# process gets SIGKILL'd even though it never bound anything. And with no
|
|
1990
|
+
# ownership check at all, ANY process (this run's or not) listening on the
|
|
1991
|
+
# reused port gets killed. Fixed to (1) filter to LISTEN sockets only, and
|
|
1992
|
+
# (2) verify each candidate is actually part of THIS launcher's tree before
|
|
1993
|
+
# killing it: its pgid matches $child_pgid (the group this teardown just
|
|
1994
|
+
# tried to signal above), or its direct parent is $app_pid. A holder that
|
|
1995
|
+
# matches neither is left alone -- this run never bound that port, so it
|
|
1996
|
+
# never reclaims it. Trade-off: a server that called setsid() into a fresh
|
|
1997
|
+
# session AND was reparented away from app_pid (neither condition holds)
|
|
1998
|
+
# will leak here; failing closed on an unverified kill is the safer
|
|
1999
|
+
# direction (see docs/v10/DECISIONS.md D14/D15/D16).
|
|
1946
2000
|
if command -v lsof >/dev/null 2>&1; then
|
|
1947
2001
|
# Build a unique, non-empty port list (scraped only if it differs).
|
|
1948
2002
|
local _ports="$port"
|
|
@@ -1951,10 +2005,62 @@ _verify_runtime_teardown() {
|
|
|
1951
2005
|
for _rp in $_ports; do
|
|
1952
2006
|
[ -z "$_rp" ] && continue
|
|
1953
2007
|
local holders
|
|
1954
|
-
holders="$(lsof -ti tcp:"$_rp" 2>/dev/null || true)"
|
|
2008
|
+
holders="$(lsof -ti tcp:"$_rp" -sTCP:LISTEN 2>/dev/null || true)"
|
|
1955
2009
|
if [ -n "$holders" ]; then
|
|
1956
2010
|
printf '%s\n' "$holders" | while IFS= read -r pid; do
|
|
1957
|
-
[ -
|
|
2011
|
+
[ -z "$pid" ] && continue
|
|
2012
|
+
local _h_pgid="" _h_ppid=""
|
|
2013
|
+
_h_pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ' || true)"
|
|
2014
|
+
_h_ppid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ' || true)"
|
|
2015
|
+
# THIRD ARM (council round 2, REJECT reproduced and fixed):
|
|
2016
|
+
# GNU `timeout` (this codebase's launcher, verify.sh:1699)
|
|
2017
|
+
# makes ITSELF the process-group leader, so the group's
|
|
2018
|
+
# pgid equals timeout's own pid, which equals app_pid (the
|
|
2019
|
+
# subshell execs straight into timeout with no fork). If
|
|
2020
|
+
# the launched command daemonizes -- forks a server and
|
|
2021
|
+
# the parent (timeout, then sh -c) exits -- by teardown
|
|
2022
|
+
# time: child_pgid is EMPTY (timeout already exited, so
|
|
2023
|
+
# `ps -o pgid= -p "$app_pid"` finds nothing); the orphan's
|
|
2024
|
+
# ppid is 1, not app_pid; but its pgid is STILL app_pid,
|
|
2025
|
+
# the value it inherited when timeout created the group.
|
|
2026
|
+
# Neither of the two arms above can ever match this case,
|
|
2027
|
+
# so the classic "daemonized" leak this function's own
|
|
2028
|
+
# comments describe was never reclaimed. Reproduced
|
|
2029
|
+
# directly: own daemon listener LEAKED pre-this-arm,
|
|
2030
|
+
# reclaimed after. Safe as an ownership proof, not a
|
|
2031
|
+
# loosening: a process group's pgid cannot be reassigned
|
|
2032
|
+
# to an unrelated group while any member of the original
|
|
2033
|
+
# group (here, the orphan itself, still alive and
|
|
2034
|
+
# reporting this exact pgid) still exists -- POSIX pgid
|
|
2035
|
+
# reuse is blocked until the whole group is empty.
|
|
2036
|
+
# FOURTH ARM (council round 4, 2 of 4 reviewers REJECTed
|
|
2037
|
+
# with an identical reproduction; verified independently
|
|
2038
|
+
# before applying): a process launched via
|
|
2039
|
+
# child_process.spawn({detached:true}) (Node's setsid()
|
|
2040
|
+
# wrapper) becomes its OWN session and process group
|
|
2041
|
+
# leader -- its pgid equals its OWN pid, not app_pid or
|
|
2042
|
+
# child_pgid, and its ppid is 1. None of the three arms
|
|
2043
|
+
# above can ever match that shape, which is exactly
|
|
2044
|
+
# test-runtime-gate.sh case I's fixture (the v7.109.0
|
|
2045
|
+
# daemonize-leak regression test) and a real pattern many
|
|
2046
|
+
# servers use to daemonize. Environment variables survive
|
|
2047
|
+
# setsid()/fork()/exec() regardless of process-group
|
|
2048
|
+
# changes, so LOKI_VERIFY_RUN_TOKEN (exported into the
|
|
2049
|
+
# launched app's env at boot, see the launch site above)
|
|
2050
|
+
# is checked here as a portability-verified ownership
|
|
2051
|
+
# proof (see _verify_runtime_pid_has_token; confirmed
|
|
2052
|
+
# directly on macOS with `ps -E -ww`, not assumed).
|
|
2053
|
+
# Residual, disclosed gap: a daemon that scrubs its own
|
|
2054
|
+
# environment (env -i, some process supervisors) before
|
|
2055
|
+
# detaching still leaks -- no ownership proof survives
|
|
2056
|
+
# that, and failing closed on an unverified kill remains
|
|
2057
|
+
# the safer direction.
|
|
2058
|
+
if { [ -n "$child_pgid" ] && [ -n "$_h_pgid" ] && [ "$_h_pgid" = "$child_pgid" ]; } \
|
|
2059
|
+
|| { [ -n "$app_pid" ] && [ -n "$_h_ppid" ] && [ "$_h_ppid" = "$app_pid" ]; } \
|
|
2060
|
+
|| { [ -n "$app_pid" ] && [ -n "$_h_pgid" ] && [ "$_h_pgid" = "$app_pid" ]; } \
|
|
2061
|
+
|| _verify_runtime_pid_has_token "$pid" "$run_token"; then
|
|
2062
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
2063
|
+
fi
|
|
1958
2064
|
done
|
|
1959
2065
|
fi
|
|
1960
2066
|
done
|
package/bin/loki
CHANGED
|
@@ -348,6 +348,18 @@ if [ "${1:-}" = "trust" ]; then
|
|
|
348
348
|
done
|
|
349
349
|
fi
|
|
350
350
|
|
|
351
|
+
# `doctor --airgap` (egress audit) lives only in bash (_loki_airgap_audit).
|
|
352
|
+
# The Bun doctor rejected the flag, so the default route had no audit at all.
|
|
353
|
+
# Routed here, flag-anywhere, so the committed dist needs no rebuild and bash
|
|
354
|
+
# stays the single implementation; bash main() fires the one cli_command event.
|
|
355
|
+
if [ "${1:-}" = "doctor" ]; then
|
|
356
|
+
for _doctor_arg in "${@:2}"; do
|
|
357
|
+
if [ "$_doctor_arg" = "--airgap" ]; then
|
|
358
|
+
exec "$BASH_CLI" "$@"
|
|
359
|
+
fi
|
|
360
|
+
done
|
|
361
|
+
fi
|
|
362
|
+
|
|
351
363
|
# CLI consolidation (Phase B): `report kpis` is the canonical form of the
|
|
352
364
|
# Bun-only KPI snapshot. The `report` noun is otherwise bash-owned (every other
|
|
353
365
|
# subcommand -- session/metrics/cost/export/share/dogfood -- routes to bash
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -115,11 +115,12 @@ commands and forward-compat with v8.0.0.
|
|
|
115
115
|
- Installs the `loki` CLI binary to your PATH (`bin/loki` shim)
|
|
116
116
|
- Subsequent `loki setup-skill` creates symlinks at `~/.claude/skills/loki-mode`, `~/.codex/skills/loki-mode`
|
|
117
117
|
|
|
118
|
-
**Anonymous telemetry is
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`
|
|
122
|
-
opt-out into your base image
|
|
118
|
+
**Anonymous telemetry is ON by default for an individual interactive install**
|
|
119
|
+
and auto-off in CI, non-interactive sessions, and when `LOKI_ENTERPRISE=true` or
|
|
120
|
+
`LOKI_AIRGAP=true` is set. It is disclosed once on first use. Turn it off with
|
|
121
|
+
`loki telemetry off`; `loki doctor --airgap` shows its current state. To make
|
|
122
|
+
opting in impossible across a fleet, bake an opt-out into your base image
|
|
123
|
+
(opt-out always wins):
|
|
123
124
|
```bash
|
|
124
125
|
# Hard-disable everywhere (belt and suspenders; opt-out always wins):
|
|
125
126
|
LOKI_TELEMETRY_DISABLED=true npm install -g loki-mode
|
package/docs/air-gapped.md
CHANGED
|
@@ -45,31 +45,53 @@ That is the property competitors' dashboard-bound verification cannot have.
|
|
|
45
45
|
## The one required egress, stated plainly
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
|
-
|
|
48
|
+
loki doctor --airgap
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
default install.
|
|
55
|
-
|
|
56
|
-
Once run, it prints the egress inventory. Today it reports exactly one REQUIRED point:
|
|
51
|
+
`bin/loki` routes the flag straight to the bash audit regardless of route, so
|
|
52
|
+
no `LOKI_LEGACY_BASH=1` prefix is required. It prints the egress inventory. On
|
|
53
|
+
a default install it reports exactly one REQUIRED point:
|
|
57
54
|
|
|
58
55
|
```
|
|
59
56
|
REQUIRED model inference -> https://api.anthropic.com
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
optional telemetry [off] disable: loki telemetry off (default off)
|
|
57
|
+
optional telemetry [on] -> https://us.i.posthog.com
|
|
58
|
+
optional update check [on] disable: export LOKI_NO_UPDATE_CHECK=1
|
|
63
59
|
```
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
It judges the provider `loki start` would use (the project's saved
|
|
62
|
+
`loki provider set` choice, else `LOKI_PROVIDER`), and runs on both the default
|
|
63
|
+
route and `LOKI_LEGACY_BASH=1`. `--json` gives the same inventory for scripts.
|
|
64
|
+
It covers the egress the engine configures, and names what it does not audit
|
|
65
|
+
because it depends on the project: package installs (the app runner's npm, pip
|
|
66
|
+
and docker steps for the built app, the dashboard venv from PyPI, and
|
|
67
|
+
quality-gate tools fetched by `npx` when not installed locally; mirror or
|
|
68
|
+
pre-install them, since a failed `npx` fetch fails that gate) and the delegate
|
|
69
|
+
PR (`git push` and `gh pr create` after a successful run when `gh` is
|
|
70
|
+
installed; `LOKI_DELEGATE_PR=0` turns it off).
|
|
71
|
+
|
|
72
|
+
With a non-claude provider selected, `loki start` on the default (bash) route
|
|
73
|
+
and `loki quickstart` never prompt the `claude` CLI, even when one is
|
|
74
|
+
installed: PRD enrichment, done recognition, the council voters, the USAGE.md
|
|
75
|
+
refresh and the quickstart intent check fall back to their deterministic
|
|
76
|
+
paths. The opt-in Bun loop (`LOKI_SDK_LOOP=1`) is not yet held to this.
|
|
77
|
+
`LOKI_ALLOW_CLAUDE_SIDECALLS=1` restores those claude calls, and the audit then lists them as REQUIRED egress, so it cannot read air-gap ready.
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
**We cannot run a build with no model at all.** Nobody can. What we can do is
|
|
80
|
+
let you point at a model you host. Only a local-weights provider clears the
|
|
81
|
+
required line: opencode, cline or aider with an `ollama/` or `lmstudio/` model
|
|
82
|
+
id in that provider's own variable (for example
|
|
83
|
+
`LOKI_OPENCODE_MODEL=ollama/qwen2.5-coder`). An Ollama cloud model (a `:cloud`
|
|
84
|
+
or `-cloud` tag) runs on ollama.com and is still reported as required. The
|
|
85
|
+
verdict comes from the model id; the audit does not read where your Ollama or
|
|
86
|
+
LM Studio endpoint listens, so confirm that yourself. An in-network gateway for
|
|
87
|
+
claude or codex (`ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`) keeps traffic inside
|
|
88
|
+
your network, but the audit still counts it as required egress.
|
|
89
|
+
|
|
90
|
+
Telemetry is ON by default for an individual interactive install and auto-off
|
|
91
|
+
in CI, non-interactive, `LOKI_ENTERPRISE=true` and `LOKI_AIRGAP=true`
|
|
92
|
+
contexts. The audit shows its real state. Every opt-out wins (`DO_NOT_TRACK=1`,
|
|
93
|
+
`LOKI_TELEMETRY=off`, `loki telemetry off`). The adoption instrumentation added
|
|
94
|
+
in v8.6.0 requires a second explicit opt-in on top of that -- see
|
|
73
95
|
[PRIVACY.md](./PRIVACY.md).
|
|
74
96
|
|
|
75
97
|
## Why `unknown` is the right answer offline
|
|
@@ -334,11 +334,11 @@ All Loki Mode data is stored locally on the machine running the system:
|
|
|
334
334
|
| Policy files | `.loki/policies.yaml` or `.loki/policies.json` |
|
|
335
335
|
| Configuration | `.loki/config.yaml` or `.loki/config.json` |
|
|
336
336
|
|
|
337
|
-
|
|
337
|
+
Beyond model inference (required unless you run a local-weights provider), anonymous telemetry (on by default for individual interactive installs, auto-off in CI, non-interactive and `LOKI_ENTERPRISE=true` / `LOKI_AIRGAP=true` contexts) and a daily update check, no data is transmitted to external services unless explicitly configured (OTEL endpoint, integration webhooks, syslog forwarding). `loki doctor --airgap` prints the egress inventory for the current configuration.
|
|
338
338
|
|
|
339
339
|
### Provider Restrictions
|
|
340
340
|
|
|
341
|
-
The data residency controller
|
|
341
|
+
The data residency controller (`src/audit/residency.js`, configured in `.loki/residency.json`) answers whether a provider is allowed by a region or air-gapped policy. It is a library check: `loki start` does not consult it today, so setting `air_gapped: true` does not by itself stop a run from calling a hosted provider. Use `loki doctor --airgap` and network policy to enforce that.
|
|
342
342
|
|
|
343
343
|
```javascript
|
|
344
344
|
const audit = require('./src/audit');
|
|
@@ -351,17 +351,14 @@ const allowed = audit.checkProvider('anthropic', 'us'); // true/false
|
|
|
351
351
|
const airGapped = audit.isAirGapped();
|
|
352
352
|
```
|
|
353
353
|
|
|
354
|
-
Configure via `.loki/
|
|
355
|
-
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
allowed_providers:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
- us
|
|
363
|
-
- eu
|
|
364
|
-
air_gapped: false
|
|
354
|
+
Configure via `.loki/residency.json` (the file `residency.js` reads):
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
{
|
|
358
|
+
"allowed_providers": ["anthropic", "openai"],
|
|
359
|
+
"allowed_regions": ["us", "eu"],
|
|
360
|
+
"air_gapped": false
|
|
361
|
+
}
|
|
365
362
|
```
|
|
366
363
|
|
|
367
364
|
## Policy Engine Security
|
|
@@ -42,6 +42,7 @@ interact and why hitting one is a failure rather than a success.
|
|
|
42
42
|
| `LOKI_MAX_TIER` | unlimited | Caps model tier, so a run cannot escalate past what you are willing to pay for. |
|
|
43
43
|
| `LOKI_MODEL_OVERRIDE` | unset | Overrides the resolved model outright. |
|
|
44
44
|
| `LOKI_TIER` | `oss` | **Not a model setting.** The open-core licensing seam. Leave it unset. |
|
|
45
|
+
| `LOKI_ALLOW_CLAUDE_SIDECALLS` | unset | `1` (exactly) lets PRD enrichment, done recognition, the council voters, the USAGE.md refresh and the quickstart intent check prompt the `claude` CLI under a non-claude provider; `loki doctor --airgap` then reports that as required egress to Anthropic. |
|
|
45
46
|
|
|
46
47
|
### Picking a model without naming one
|
|
47
48
|
|
package/docs/exit-codes.md
CHANGED
|
@@ -75,8 +75,8 @@ codes on the Bun route and the bash route (`LOKI_LEGACY_BASH=1`).
|
|
|
75
75
|
| Code | Meaning |
|
|
76
76
|
|---|---|
|
|
77
77
|
| 0 | Clean: the integrity hash matches and the recorded diff still matches the repo (and, with `--jwks`, the attestation is VERIFIED) |
|
|
78
|
-
| 1 | Tampered or drifted; or, with `--jwks`, the attestation FAILED, or is ABSENT (the receipt is unsigned while a key set was supplied) |
|
|
79
|
-
| 2 | Could not check: the receipt is present but unusable (malformed JSON), the verifier itself is missing, or, with `--jwks`, the attestation is NOT CHECKED (key set unreadable, or a verifier dependency missing) |
|
|
78
|
+
| 1 | Tampered, or genuinely drifted (the working tree really changed since the recorded base); or, with `--jwks`, the attestation FAILED, or is ABSENT (the receipt is unsigned while a key set was supplied) |
|
|
79
|
+
| 2 | Could not check: the receipt is present but unusable (malformed JSON), the verifier itself is missing, drift could not be re-derived (verified outside a git tree, or the recorded base ref no longer resolves, with every other check passing) rather than genuinely found, or, with `--jwks`, the attestation is NOT CHECKED (key set unreadable, empty, or a verifier dependency missing) |
|
|
80
80
|
| 64 | Usage error: no proof id, more than one proof id, an unknown option (a mistyped `--jwk` must never skip the check), `--jwks` with no value or an empty one, or `-h`/`--help` (verify never exits 0 without a verdict; full help is `loki proof help`). `--` ends options, so `loki proof verify -- <id>` checks an id that begins with `-` |
|
|
81
81
|
| 66 | Input missing: no `.loki/proofs/<id>/proof.json` for that id |
|
|
82
82
|
|
|
@@ -94,6 +94,80 @@ purpose: it rolls up many stages, and an operator who fixes the named FAILED
|
|
|
94
94
|
stage and re-runs would see green while still blind on the stage that never
|
|
95
95
|
ran (`tools/verify-chain.py`, rule 4).
|
|
96
96
|
|
|
97
|
+
## `loki start --remote` receipt check (not a verify command)
|
|
98
|
+
|
|
99
|
+
This is a documented DIVERGENCE from `proof verify`, not a bug. `loki start
|
|
100
|
+
--remote <url>` submits a build to a deployed cluster (`loki_remote_submit`,
|
|
101
|
+
`autonomy/loki`), and after the job finishes it fetches the Evidence Receipt
|
|
102
|
+
and runs an internal check (`loki_remote_verify_receipt`) that prints one of
|
|
103
|
+
several verdict words also used elsewhere in `loki proof`: VERIFIED, UNSIGNED,
|
|
104
|
+
NOT CHECKED, TAMPERED, plus its own NOT VERIFIED when the verifier itself is
|
|
105
|
+
unavailable (a fifth, distinct string -- see the last row below). Despite the
|
|
106
|
+
shared vocabulary, this is not the `proof verify` contract and does not return
|
|
107
|
+
its codes.
|
|
108
|
+
|
|
109
|
+
`loki_remote_verify_receipt` is an internal helper, never a standalone
|
|
110
|
+
command. Its return value feeds `_receipt_rc` inside `loki_remote_submit` via
|
|
111
|
+
`loki_remote_fetch_receipt` (`autonomy/loki`), whose own last statement is the
|
|
112
|
+
verify call with no `||` to reset the status -- so the helper's return code
|
|
113
|
+
propagates out unchanged. `loki_remote_submit` then checks `_receipt_rc`
|
|
114
|
+
BEFORE ever looking at job status:
|
|
115
|
+
|
|
116
|
+
| Receipt verdict | Helper return | Process exit |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| VERIFIED (gpg or attestation) | 0 | Governed by the job status below |
|
|
119
|
+
| UNSIGNED | 0 | Governed by the job status below |
|
|
120
|
+
| NOT CHECKED -- gpg unavailable on this machine, or an attestation present but unreachable/uncheckable (JWKS unreachable, crypto missing) | 0 | Governed by the job status below |
|
|
121
|
+
| NOT CHECKED -- signed, but the signing key is not in the verifier's keyring (gpg `nopubkey`), and no valid attestation covers it | 1 | 1, unconditionally |
|
|
122
|
+
| NOT VERIFIED -- the local verifier itself is unavailable (no python3, or `lib/proof-verify.py` missing) | 0 | Governed by the job status below |
|
|
123
|
+
| TAMPERED (bad hash, bad gpg signature, bad attestation) | 1 | 1, unconditionally |
|
|
124
|
+
|
|
125
|
+
The exit code `loki start --remote` actually returns is gated on the job's own
|
|
126
|
+
terminal status (`passed` / `failed` / `unknown`) in most cases, but there are
|
|
127
|
+
TWO deliberate overrides that fail the whole submit regardless of job status,
|
|
128
|
+
not one:
|
|
129
|
+
|
|
130
|
+
- **TAMPERED** (bad hash, bad gpg signature, bad attestation): an accusation
|
|
131
|
+
that the receipt itself was altered.
|
|
132
|
+
- **NOT CHECKED / nopubkey** (signed, but the verifying machine does not hold
|
|
133
|
+
the signing public key, and no attestation proves it another way): per the
|
|
134
|
+
code's own comment at this branch (`autonomy/loki:1081-1084`), this is
|
|
135
|
+
"Non-zero: a caller who asked for a signed receipt did not get proof, and in
|
|
136
|
+
CI that must not pass silently." It is explicitly NOT an accusation of
|
|
137
|
+
tampering -- the message says plainly that nothing is wrong with the
|
|
138
|
+
contents -- but it still fails the submit, because a signed remote build
|
|
139
|
+
whose signature could never be checked is exactly the case this override
|
|
140
|
+
exists to catch. Concretely: a build can be genuinely signed, actually pass,
|
|
141
|
+
and still make `loki start --remote` exit 1, if the verifying machine simply
|
|
142
|
+
never imported the publisher's key and no JWKS attestation is available to
|
|
143
|
+
cover the gap.
|
|
144
|
+
|
|
145
|
+
Every other outcome -- VERIFIED by either gpg or attestation, UNSIGNED, the
|
|
146
|
+
gpg-unavailable/attestation-uncheckable NOT CHECKED case, and the
|
|
147
|
+
verifier-unavailable NOT VERIFIED case -- returns 0 from the helper and
|
|
148
|
+
defers entirely to job status, because none of those contradicts the job's
|
|
149
|
+
reported outcome.
|
|
150
|
+
|
|
151
|
+
This is why remote NOT CHECKED can exit 0 in some branches and 1 in others,
|
|
152
|
+
while `loki proof verify`'s NOT CHECKED always exits 2: they answer different
|
|
153
|
+
questions. `proof verify` IS the verdict -- its only output is "was this
|
|
154
|
+
receipt proven", so an unproven result can never exit 0. The remote helper's
|
|
155
|
+
verdict is a secondary integrity check bolted onto a job-status command whose
|
|
156
|
+
primary question is "did the build pass"; most unprovable outcomes do not
|
|
157
|
+
change that answer, but the nopubkey case is carved out by the code itself as
|
|
158
|
+
a hard failure, and TAMPERED always is. (`_deploy_receipt_verdict`'s own
|
|
159
|
+
header comment, elsewhere in `autonomy/loki`, makes the same point about this
|
|
160
|
+
helper for the UNSIGNED case specifically -- "that function returns 0 for
|
|
161
|
+
UNSIGNED, which is right for its caller (submit) and wrong here (integrity is
|
|
162
|
+
not provenance)" -- it does not speak to NOT CHECKED, whose nopubkey behavior
|
|
163
|
+
is documented at its own branch as cited above.)
|
|
164
|
+
|
|
165
|
+
To get the `proof verify` exit-code contract (0/1/2/64/66) against a receipt
|
|
166
|
+
fetched from a remote build, run `loki proof verify <job-id>` afterward
|
|
167
|
+
against the written `.loki/proofs/<job-id>/proof.json` -- that is a genuine
|
|
168
|
+
verify call and returns the codes documented above, not the ones in this
|
|
169
|
+
section.
|
|
170
|
+
|
|
97
171
|
## `loki proof chain [workspace]`
|
|
98
172
|
|
|
99
173
|
Runs the whole verification chain (`tools/verify-chain.py`) and passes its exit
|
package/docs/v10/BACKLOG.md
CHANGED
|
@@ -170,6 +170,29 @@ Status values: todo, in progress, shipped (version), parked (reason).
|
|
|
170
170
|
123. **More verdict-class leftovers from the 113 sweep:** the `audit.py verify` CLI and `compute_chain_tip_in_dir` still report valid, exit 0, when nothing was checked (the cross-chain verifier reads them, so changing them needs its own review); the cockpit, overview, checklist, gate-card, standalone, proofs.html and cost.html verdict relabels have no test of their own (only the receipt and audit surfaces are pinned by the moat case); the standalone receipt list shows the oldest receipts first (see 118).
|
|
171
171
|
124. **Dashboard innerHTML audit:** the standalone receipt and learning panels interpolated run-written text into innerHTML unescaped (fixed in cycle 4, `f6050ef1`, with hostile-input tests). No systematic audit of the other `innerHTML` sites in `dashboard-ui/components` and `build-standalone.js` has been done; add a guard that flags a template literal or string concatenation of a response field into innerHTML without an escape.
|
|
172
172
|
|
|
173
|
+
125. **P7 scanner (`tests/moat/p7-no-fabricated-data.sh`) bypass shapes found by PF-2 round 5's review but not fixed there (pre-existing before and after `fa5de15f`, not a regression -- each is a candidate for its own small slice):**
|
|
174
|
+
- **B-1 reassignment fallback:** `let rows = data; if (!rows.length) rows = [rows]; setRows(rows);` -- `DECL_ARR` only matches the `const/let/var x = [` declaration, never a later bare `x = [` assignment.
|
|
175
|
+
- **B-2 destructuring / parameter defaults:** `const { rows = [rows] } = data;` or `function load(rows = [rows]) {}` -- neither is a declaration of the form `name = [`.
|
|
176
|
+
- **B-3 object-property value on `this.x` outside a setter:** `this._state = { phases: [rows] };` -- `THIS_ARR` needs `this.x = [` directly, misses a literal nested inside an object RHS.
|
|
177
|
+
- **B-4 paren-wrapped `||`/`??` outside a setter:** `this._rows = d.rows || ([rows]);` -- `FALLBACK_ARR` requires `[` immediately after the operator; a wrapping `(` defeats it (the setter-argument span arm already catches this shape inside a setter call).
|
|
178
|
+
- **B-5 module-level named fallback table:** `const FALLBACK_ROWS = [rows];` at column 0, used later as `data.length ? data : FALLBACK_ROWS` -- the column-0 exemption plus the identifier-branch ternary both let it through; rule 7's name-based catch only fires if the constant name contains sample/mock/demo/fake/placeholder.
|
|
179
|
+
- **B-6 ternary against `null`/`undefined`:** `const r = loading ? null : [rows];` -- `is_literal(null)` is true, so the both-branches-literal exemption suppresses it; should only treat an array/object literal (never null/undefined/empty-string) as the "literal other branch."
|
|
180
|
+
- **B-7 fabricated data returned from a helper:** `function getRows(d) { if (!d) return [rows]; return d; }` then `setRows(getRows(d))` -- no arm follows a function's return value.
|
|
181
|
+
- **B-8 `Array.from` generator producing invented rows:** `Array.from({ length: 3 }, (_, i) => ({ id: i, action: 'Deployed', user: 'Admin' }))` -- the `Array.from` arm only checks a literal first argument, not a generator callback whose body invents field values.
|
|
182
|
+
- Two more from the same review, not yet given IDs: a positive-control risk (a real typed options list chosen against live data, e.g. `isAdmin ? adminOpts : [{value:'view',...}]`, could false-positive under the ternary/setter-span arms -- fix with an allow-annotation if it ever fires on real code, don't weaken the arm); and re-running `tests/moat/p6-*.sh` in isolation to confirm its 300s timeout during the PF-2 review was purely a load artifact (load average ~30 from concurrent session agents), not a real regression the merge introduced.
|
|
183
|
+
|
|
184
|
+
126. **(CORRECTED and CONFIRMED as a real CI-breaking bug, not just a local artifact -- see the retraction below.) `tests/test-airgap-ollama-host.sh` does not isolate `.loki/state/`, so a stale `.loki/state/provider` file written by an EARLIER test IN THE SAME CI SHARD's shared checkout silently overrides the test's own `LOKI_PROVIDER=opencode` env var** (the CLI's own precedence rule: saved project state wins over the env var, by design). This actually broke `Tests` (shard 3/4) in CI on `ba6610dc` -- root-caused by reproducing the exact CI shard-3 sequence locally: `tests/test-iteration-grace.sh` sources `autonomy/run.sh` from the shared repo checkout's CWD (not its own isolated `$SCRATCH` dir) at two call sites (`probe()`, ~line 42 and ~109), and `autonomy/run.sh:1741-1744`'s provider auto-detection writes `.loki/state/provider` as an unconditional side effect of sourcing. Every later test in the same shard's shared checkout inherits that leftover file. Removing it reproduces the airgap test passing again; the CI failure is not a local-checkout coincidence. **Retraction**: an earlier draft of this item concluded the opposite (called it "not a product bug", attributed it purely to local manual-testing residue, and proposed fixing only `test-airgap-ollama-host.sh`'s own isolation) -- that conclusion was WRONG. It was drawn from a local reproduction where I had ALSO independently left a stray `.loki/state/provider` in my own manual testing, which coincidentally matched the CI symptom and stopped the investigation short of finding the real cross-test contamination source. Lesson: a reproduced symptom is not the same as a verified mechanism -- confirm the EXACT trigger, not just that removing the artifact makes the symptom go away. Two fixes landed and merged: `test-iteration-grace.sh` (`b3d7cdda`) and `test-exit-code-contract.sh` (`e05e09fb`), both isolating CWD before sourcing `run.sh`. Verifying the shard is now clean surfaced a THIRD contaminator: `tests/test-code-review-self-copy.sh` sources a self-copy of `run.sh` with NO `cd` at all. Fix dispatched. This bug class (source `run.sh`, or a copy of it, before `cd`ing into an isolated scratch dir) has now appeared in at least 3 registered-in-CI files; `test-review-diff-moat.sh` also reproduces it standalone but is not registered in any CI path today (orphaned test, latent bug, not a current shard contributor). A systematic repo-wide sweep for this exact pattern (`source .*run\.sh` or a self-copy of it, with no preceding isolating `cd`) is now clearly warranted as its own slice, rather than continuing to discover instances one CI failure at a time -- each new fix's own verification pass keeps finding the next one. **FIXED (2026-09-27, merge 41c2f604):** the repo-wide sweep closed 22 more files (19 originally scoped plus 3 new instances found mid-sweep: `test-doc-scope-generator.sh`, `test-p0-gate-behavior.sh`, `tests/council/test_managed_review_flag.sh`, the last of which sources the bug via a real `bash run.sh --help` subprocess, not just `source`). Verified: all 22 run clean in sequence with no `.loki/state/provider` left behind, the victim test then passes 4/4, moat suite matches main exactly (3 of 9, no rule failed), shellcheck clean. Also independently isolated at the victim side: `test-airgap-ollama-host.sh` now sets its own `LOKI_DIR` (`0f056336`), so it is immune regardless of any remaining, still-undiscovered contaminator elsewhere in the suite. **Retroactive quorum (2026-09-27):** this MEDIUM-tier merge shipped without its 2-reviewer quorum first (a real process gap, self-caught and disclosed in PROGRESS.md). Reviewer 2/2 (Sonnet) returned APPROVE: independently re-derived the root cause (found it is actually UNCONDITIONAL, firing even when `LOKI_PROVIDER` was explicitly set, not just on auto-detect -- a stronger bug than originally described), redid the 23rd-instance search after an advisor-caught blind spot in the first pass (~35 additional call sites found repo-wide, all already isolated or targeting unrelated scripts, none a missed instance), and added a RED-check not in the original ask: removed one file's `cd` fix in an isolated worktree, reproduced the exact original bug (`.loki/state/provider` written into the worktree root), then confirmed reverting the fix restored green. No blocking findings. Reviewer 1/2 (Opus) independently also returned APPROVE: byte-diffed pre/post output across all 22 files (differs only by the 2 new isolation lines), confirmed the sleep-plus-`-nt` mutation harness is genuinely load-bearing (found `test-exit-code-contract.sh` passes VACUOUSLY on real bash 3.2 without the sleep, even while contaminated -- so the sleep is not cosmetic there), ran all 22 through a real 4-shard parallel `tests/run-all-tests.sh` invocation matching local-ci's own sharding with no leaks, and confirmed the moat suite's 2-of-9 (vs. 3-of-9 on a fully-provisioned checkout) is a fresh-worktree `node_modules`-absence artifact, not a regression. **Retroactive quorum: 2/2 APPROVE, no blocking findings from either reviewer -- gap closed.** Both reviewers flagged the same non-blocking point independently: `test-airgap-ollama-host.sh` no longer actually detects this bug class (it was isolated separately via `LOKI_DIR` in `0f056336`), so the real regression detectors going forward are each of the 22 files' own new `-nt` checks, not the original victim test.
|
|
185
|
+
|
|
186
|
+
127. **(CORRECTED 2026-09-26: an earlier draft of this item claimed `56eb866f` was already merged and fixed. It was reviewed and unanimously approved same-session as S-22 but never actually merged -- a real gap on main until now. Now genuinely fixed and merged: `56eb866f` merged via a dedicated merge commit, pushed. `council_should_stop`'s shadow-write block is now skipped entirely when `PROJECT_DIR` is unset, plus -E on the python3 call; real code-execution RED/GREEN confirmed via a live marker-file demonstration, polled across the backgrounded+disowned call.) Sibling of BACKLOG 63 (S-22): `council_should_stop` (~line 4635 in `autonomy/completion-council.sh`) had the identical unguarded `cd "${PROJECT_DIR:-$(pwd)}"` pattern feeding `python3 -m memory.managed_memory.shadow_write` -- more severe than S-22's own retrieve-path bug since this one EXECUTES a module, not just imports it for reading.** Two more copies of this pattern remain open: `autonomy/run.sh:19861` has the identical unguarded `cd "$PROJECT_DIR" && python3 -m memory.managed_memory.shadow_write` (found during the `56eb866f` fix, not fixed there, out of that slice's file set -- same fix shape needed); `autonomy/completion-council.sh:4204` (`council_managed_should_stop`) has `project_dir="${PROJECT_DIR:-$(pwd)}"` but only feeds a `git diff` read, not a python module execution, gated behind the narrower `LOKI_EXPERIMENTAL_MANAGED_AGENTS` flag -- lower severity, still worth the same guard for consistency.
|
|
187
|
+
|
|
188
|
+
128. **Stale documentation asserts a `{"runner":"none","pass":true}` writer shape that doesn't exist.** Found while fixing S-07's rework #2 (BACKLOG 33): `references/core-workflow.md:81` and `docs/VERIFIED-COMPLETION-PLAN.md:90,315,316` assert the same false shape corrected in D20 (the real writer, `enforce_test_coverage` at `run.sh:12691-12692`, writes `pass:"inconclusive"`, a string, never boolean `true`). Not behaviorally load-bearing (nothing parses these docs at runtime) but misleading to a future reader. Also: `tests/test-council-devils-advocate.sh` Case 2, `tests/test-da-veto.sh`, `tests/test-council-convergence-floor.sh:92` use synthetic `pass:true` fixtures instead of the real writer shape -- harmless today since `runner=='none'` is unconditional at every site that reads them, but should be replaced with the real shape for honesty.
|
|
189
|
+
|
|
190
|
+
129. **FIXED.** S-24's tamper-detection seal (BACKLOG 70) only hardened the DIGEST computation, not the snapshot WRITER/READER paths. Found by the adversarial reviewer of the awk-bypass fix (c315cbe9): `_loki_untracked_merge` (writes `preexisting-untracked.z`), `_loki_covered_paths` (reads it after verify), and `_loki_untracked_status`'s `git` invocation were all still bare PATH lookups (`python3 -E`, not `-I` + absolute path; `git` unresolved). An agent that plants a malicious `.pth` file or a fake `python3`/`git` earlier on PATH during one session could forge the CONTENT of the snapshot file itself before the (correctly hardened) seal ever computed its digest over that already-forged content -- the seal then faithfully approved the forgery. Reproduced both classes empirically before fixing: (a) a `.pth` in user site-packages fired arbitrary code during `_loki_untracked_merge`'s and `_loki_covered_paths`'s `python3 -E` calls and could rewrite their declared output after the legitimate write; (b) a fake `git` shadowing the real one on PATH caused `_loki_untracked_status` to omit a real untracked path from its output, which then propagated all the way into the sealed snapshot as if that path never existed. Fixed by adding `_loki_snapshot_py_tool` and `_loki_snapshot_git_tool` (same fixed-root-path-first pattern as `_loki_snapshot_hash_tool`, each candidate probed with a real run, not just `-x`) and routing every python3/git call in the writer chain through them: `_loki_untracked_merge`, `_loki_covered_paths` (both now `-I -S`, not just `-E`/`-I` -- `-S` is required to skip `import site` entirely: `-I` implies `-s` and already skips USER site-packages, but not the resolved interpreter's OWN site-packages directory, which on a Homebrew-installed python3 is itself user-writable; `/usr/bin/python3`'s own site-packages is root-owned, so `-I` alone would suffice there specifically, but `-S` is the correct universal fix since the resolver can fall back to a PATH interpreter), `_loki_untracked_status`, `_loki_tracked_by_user_since_anchor` (feeds the writer's "tracked" exclusion set), `_loki_snapshot_preexisting`'s own `rev-parse --show-toplevel`, and the `hash-snapshot` invocation that writes `.sha.z` (the sibling sealed file). Post-fix, both reproduced bypasses were re-run and confirmed closed (the `.pth`/fake-git have zero effect on the writer/reader's actual output). **Explicitly out of scope, left as BACKLOG 131**: `_loki_untrack_agent_committed_user_files`'s five bare `git` calls (feed the READER, `_loki_covered_paths`, with the paths it checks) and the bare `git reset`/`git commit`/`git update-index` calls in the commit path itself -- a fake git already defeats the commit gate directly at commit time regardless of what the snapshot says, so resolving only the reader's git feed would not change that attacker's achievable outcome; a real fix needs a process-wide resolved-git convention, tracked separately rather than folded into this change.
|
|
191
|
+
130. **S-24 follow-up, non-blocking, from the same review round:** (a) `_loki_snapshot_verify`'s `SNAPSHOT_TAMPERED` audit log doesn't record the live vs. sealed digest values, so an operator can't distinguish a genuine content change from a flaky hash-tool failure (both produce the identical message) -- add both digests to the audit line; (b) a verify-then-read race exists between `_loki_untrack_agent_committed_user_files`/`commit_session_changes`'s verify call and their later file read, where a background process could swap the file in between (compounded by BACKLOG 129's reader-side exposure, now narrowed by the BACKLOG 129 fix but not closed for the git-fed paths -- see BACKLOG 131); (c) `.loki/state/session-created.z` is not covered by the seal at all -- an agent could add a user's pre-existing files to it, causing a later resume to leave those files out of the snapshot and commit them as session work.
|
|
192
|
+
131. **Process-wide resolved-git convention needed (split out of BACKLOG 129's fix scope); reviewed 4 times total across two rounds (2/2 then a 2/2 top-up, all four APPROVE), with corrections from both rounds folded in below.** `_loki_untrack_agent_committed_user_files` (autonomy/run.sh:9969-10022; CORRECTED 2026-09-27: 9 bare `git` calls in this one function, not "5" as an earlier draft said -- `git rev-parse --verify` (9982), `git merge-base` x2 (9984, 9988), `git cat-file` (9989), `git rev-parse --show-toplevel` (9999), `git -C diff` (10000), `git reset` (10013), `git -C update-index` (10014), `git commit` (10015); the reset/update-index/commit trio is INSIDE this same function, not in `commit_session_changes` as an earlier draft misattributed -- `commit_session_changes` starts at line 10024 and has its own separate `git reset -q` at 10127) still use unresolved `git`, along with roughly 290+ other bare `git` invocations across `autonomy/run.sh`. (a) **Understated, per the first round's reviewers**: framed as "low-value to fix since a fake git already defeats the commit gate directly" -- true, but that argument only excuses NOT fixing the commit-time calls; the reader's own git feed still means BACKLOG 129's git-side hardening protects the snapshot's stored CONTENT, not what the commit outcome actually is, until this lands -- say that plainly rather than "closed" or "already covered elsewhere." A fake `git` shadowing the real one already defeats the commit gate directly (it can no-op `git commit`, or lie about what `git reset` did) independent of anything the snapshot says, so this is a materially larger fix than BACKLOG 129's writer/reader scope and was deliberately left out of it. (b) `_loki_snapshot_digest`'s python3 fallback (used only when neither sha256sum nor shasum resolves anywhere) is not yet routed through `_loki_snapshot_py_tool`/`-S`; a top-up reviewer independently reproduced that Homebrew python3 `-I` alone (without `-S`) still fires a `.pth` planted in its own site-packages, confirming this gap is real, not theoretical. (c) **Understated, per the first round's reproduction attempt, then widened by the top-up round**: `.git/config`'s agent-writable `core.fsmonitor`/`core.untrackedCache` settings don't just "shape" what a correctly-resolved `git status` reports -- `core.fsmonitor` makes git run a command the agent CHOSE on every status call and trust what it reports, which could be a full bypass of the git-side hardening entirely (an attempted untracked-cache/directory-mtime PoC did not succeed, so the full bypass is not yet proven either way -- treat as open, not dismissed). A top-up reviewer separately confirmed `core.worktree` is not yet named here and is a related agent-writable vector (it can redirect `rev-parse --show-toplevel` and `status` to point at a different tree entirely) -- add it to this enumeration; the same reviewer tried setting it as an attack and found the writer currently fails closed (`rev-parse --show-prefix` fails outside the worktree) rather than being fooled, so this specific vector is not yet a proven bypass, only a named one. Mitigate `core.fsmonitor`/`core.untrackedCache` with `-c core.fsmonitor=false -c core.untrackedCache=false` on the status call, or investigate and disclose definitively; mitigate or investigate `core.worktree` separately.
|
|
193
|
+
|
|
194
|
+
132. **Mutant-detection test relies on bash-version-specific `cd ""` behavior, found during the BACKLOG 127/S-22 shadow-write fix's quorum top-up.** `tests/test-council-shadow-write-project-dir.sh`'s mutant check (strip only the `-n PROJECT_DIR` guard, leave the `cd "$PROJECT_DIR"` rewrite in place) stays GREEN on bash 5.3 (Homebrew) because bash 5.3's `cd ""` hard-fails with `cd: null directory` and short-circuits the `&&` chain -- but the identical mutant reproduces RED on real bash 3.2.57 (macOS system `/bin/bash`, what `#!/usr/bin/env bash` resolves to on a plain macOS machine), because bash 3.2's `cd ""` is a silent success that leaves cwd unchanged. The merged fix itself is correct on both interpreters (verified directly by two independent reviewers); only the test's mutant discriminator is interpreter-luck under bash 5. Fix: run the GREEN/mutant comparison under `/bin/bash` explicitly, not `#!/usr/bin/env bash`, so a future edit that dropped only the `-n PROJECT_DIR` guard cannot look safe locally while shipping a live hole on the macOS default interpreter.
|
|
195
|
+
|
|
173
196
|
## Later milestones
|
|
174
197
|
|
|
175
198
|
M1 one command, M2 Seal and Wall, M3 assign like a teammate, M4 system map, M5 the line, M6 ship and operate, M7 one screen, M8 legacy lane, M9 enterprise readiness, M10 simplify and ship v10.0.0. Items get broken out here when their milestone comes up.
|