loki-mode 7.78.0 → 7.80.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.
@@ -0,0 +1,459 @@
1
+ # Unified Config-File Plan (FEAT-CONFIG, task #691)
2
+
3
+ Status: design for implementation. Build target: POST-v7.73.0 main (the v7.73.0
4
+ branch-default + `loki deploy` + secret-scan work is present in the working tree;
5
+ all line anchors below are verified against that state). NO version bump, NO
6
+ commit, NO implementation code in this doc. Devil's-advocate corrections to the
7
+ approved plan (`~/.claude/plans/polished-waddling-stardust.md`) are folded in and
8
+ marked CORRECTION where the approved text was factually wrong against source.
9
+
10
+ Goal: `loki start --config <path>` (aliases `--vars`, `--env-file`) loads a single
11
+ `.env` / YAML / JSON file so Docker / compose / k8s / Vault operators inject one
12
+ mounted file instead of a wall of `LOKI_*` env vars or CLI flags. v1 makes ALL
13
+ ~250 flags configurable via flat `.env`; nested friendly YAML/JSON full-coverage
14
+ is v2. Secrets are never inlined -- referenced via `${VAR}`.
15
+
16
+ --------------------------------------------------------------------------------
17
+ ## 0. Locked precedence ladder (CONTRACT -- cannot be phased)
18
+
19
+ ```
20
+ CLI flags (explicit this run) highest
21
+ > --config file (explicit this run) <- NEW layer
22
+ > ambient env (ConfigMap / Secret / .env_file / exported LOKI_*)
23
+ > auto .loki/config.yaml + ~/.config/loki-mode/config.yaml (ambient project file)
24
+ > settings.json (.loki/config/settings.json)
25
+ > built-in defaults lowest
26
+ ```
27
+
28
+ `--config` MUST beat ambient env: the Helm chart injects every non-secret `LOKI_*`
29
+ via `envFrom: configMapRef` and docker-compose auto-loads `.env`, so ambient
30
+ `LOKI_*` is ALWAYS present in the exact deployments this feature targets. If env
31
+ beat `--config`, a mounted file would override nothing. Auto-discovered
32
+ `.loki/config.yaml` stays env-LOSES (unchanged contract).
33
+
34
+ --------------------------------------------------------------------------------
35
+ ## 1. Pre-pass placement in main() (autonomy/loki)
36
+
37
+ Verified anchors: `main()` at loki:15285; `command="$1"; shift` at 15301-15302;
38
+ `loki_telemetry` at 15307; `case "$command"` dispatch at 15309 (`start)` ->
39
+ cmd_start at 15314, `run)`/`quick)` adjacent). cmd_start at loki:1012, its arg
40
+ loop at 1042. Exec handoff `_loki_new_session_exec "$RUN_SH" ...` at loki:2097.
41
+ CLI flags export inline in the cmd_start loop (e.g. `export LOKI_COMPLEXITY=simple`
42
+ under `--simple`; `export LOKI_GITHUB_IMPORT=true` under `--github`), confirmed in
43
+ the 1042-1560 range.
44
+
45
+ ### 1a. Where the pre-pass runs
46
+ Insert the pre-pass AFTER `command="$1"; shift` (15302) and AFTER the
47
+ `loki_telemetry` call (15307), and BEFORE the `case "$command"` dispatch (15309).
48
+ Gate it to SESSION commands only so `loki status` / `config` / `stop` etc. never
49
+ trigger a config load:
50
+
51
+ ```
52
+ case "$command" in
53
+ start|run|quick)
54
+ loki_maybe_apply_config_file "$@" # NEW pre-pass; scans, does NOT consume
55
+ ;;
56
+ esac
57
+ # ... existing case "$command" dispatch unchanged ...
58
+ ```
59
+
60
+ `loki_maybe_apply_config_file` is a NEW helper sourced from the new lib (SS2). It:
61
+ 1. Honors `LOKI_CONFIG_FILE` env var first (for ENTRYPOINT / in-container direct
62
+ `loki start` where flags are awkward). An explicit flag overrides the env var.
63
+ 2. Pre-SCANS `"$@"` for `--config[=]`, `--vars[=]`, `--env-file[=]` to extract the
64
+ path, WITHOUT shifting/consuming (the per-command loops still see every arg).
65
+ Scan reads both `--config <path>` and `--config=<path>` forms.
66
+ 3. If a path is found, calls `loki_apply_config_file "$path"` (SS2), which detects
67
+ format, expands `${VAR}` refs, validates, and `export LOKI_*` for each key.
68
+
69
+ ### 1b. Why pre-pass (not a run.sh flag)
70
+ By the time run.sh runs, a config-set `LOKI_COMPLEXITY=simple` is byte-identical to
71
+ an ambient env var; run.sh cannot tell explicit-config from ambient env. Only a
72
+ pre-pass running BEFORE the cmd_start arg loop can:
73
+ - (a) be OVERWRITTEN by the subsequent CLI loop -> CLI > config, no extra logic
74
+ (the loop's own `export LOKI_*=...` arms run after and win); and
75
+ - (b) have its exports SURVIVE `exec run.sh` (loki:2097), where auto-config /
76
+ settings.json / defaults all env-LOSE to what is already set.
77
+
78
+ The pre-pass export OVERRIDES ambient env -- the ONE intentional difference from
79
+ the existing env-wins loaders, correct per the Helm/compose reasoning. DO NOT add
80
+ a "force-override from explicit config" path to run.sh's env-wins guard
81
+ (run.sh:395/490): it would clobber CLI flags and break CLI > config.
82
+
83
+ ### 1c. REQUIRED cmd_start edit (do not let this hide in "scan without consume")
84
+ Because the pre-pass does NOT consume args, cmd_start's loop (1042+) still SEES
85
+ `--config <path>`. With no case arm it would be misread as the PRD positional or
86
+ forwarded to run.sh. ADD consume-and-ignore arms in the cmd_start arg loop (and in
87
+ cmd_run / cmd_quick loops if they accept these) for all three aliases, both `=`
88
+ and space forms:
89
+
90
+ ```
91
+ --config|--vars|--env-file) shift 2; continue ;; # consumed by pre-pass
92
+ --config=*|--vars=*|--env-file=*) shift; continue ;;
93
+ ```
94
+
95
+ These arms intentionally do nothing else: the pre-pass already applied the file.
96
+
97
+ --------------------------------------------------------------------------------
98
+ ## 2. NEW autonomy/lib/config-map.sh (single-source mapping; fixes the 3-way drift)
99
+
100
+ This lib is the canonical mapping array PLUS the config-file loader helpers, and
101
+ it is the SINGLE HOME of the parse-and-export logic. It is sourced by BOTH the loki
102
+ pre-pass and run.sh's parsers, collapsing 3 tables to 1.
103
+
104
+ DESIGN CONSTRAINT (for SDET, SS7): the lib MUST be SIDE-EFFECT-FREE ON SOURCE --
105
+ sourcing it only defines the array + functions, exports nothing, runs nothing. The
106
+ loki pre-pass and run.sh each call the functions explicitly. This lets unit tests
107
+ source the lib and call individual functions (parser, expander) in isolation.
108
+
109
+ ### 2a-0. The override-mode loader (the load-bearing precedence mechanism)
110
+ The keystone (config beats ambient env) lives HERE, as an `override` PARAMETER on
111
+ the shared per-key export -- NOT in run.sh, NOT duplicated per format. All three
112
+ format parsers (.env / YAML / JSON) call ONE export helper:
113
+
114
+ ```
115
+ loki_config_export_key <env_var> <value> <override>
116
+ # if [ "$override" != 1 ] && [ -n "${!env_var:-}" ]; then return 0; fi # env-wins guard
117
+ # value -> ${VAR}-expand -> validate_yaml_value -> export
118
+ ```
119
+
120
+ - loki pre-pass (SS1) calls every key with override=1 -> config BEATS ambient env,
121
+ UNIFORMLY across .env / YAML / JSON (this is what makes case 3 AND case 7 pass).
122
+ - run.sh's two parsers (SS3) delegate with override=0 -> env-wins guard PRESERVED,
123
+ the existing auto-discovery contract is byte-unchanged.
124
+
125
+ CORRECTION (defect in the first draft): do NOT have the pre-pass "reuse run.sh
126
+ engines" by sourcing run.sh -- run.sh fires on-source side effects
127
+ (load_config_file at run.sh:507, _load_json_settings at 567, all the `:-` defaults).
128
+ The pre-pass sources config-map.sh ONLY and calls its loader; run.sh ALSO sources
129
+ config-map.sh and calls the same loader with override=0. Without the override
130
+ parameter, a reused env-wins parser would SKIP a config key whenever ambient env is
131
+ present -- exactly the Helm/compose case -- so the keystone would silently fail for
132
+ YAML/JSON while .env (no guard) overrode correctly, diverging the formats.
133
+
134
+ ### 2a. Canonical array
135
+
136
+ ```
137
+ LOKI_CONFIG_MAP=(
138
+ "nested.path:LOKI_ENV_VAR"
139
+ ...
140
+ )
141
+ ```
142
+
143
+ ### 2b. VERIFIED drift enumeration (CORRECTIONS to the approved plan)
144
+
145
+ Three tables exist today:
146
+ - T1 `parse_simple_yaml` (run.sh:262-350) -- 64 `set_from_yaml` calls.
147
+ - T2 `parse_yaml_with_yq` (run.sh:419-504) -- 61 `path:LOKI_*` entries.
148
+ - T3 `config.example.yaml` (docs the user-facing keys).
149
+
150
+ Verified deltas (read against source, not the approved plan's claims):
151
+
152
+ 1. `model.planning`, `model.development`, `model.fast` -> in T1 (run.sh:320-322)
153
+ ONLY; ABSENT from T2 and from T3. (Approved plan correct.) Target env vars
154
+ LOKI_MODEL_PLANNING / _DEVELOPMENT / _FAST are CONFIRMED via settings.json
155
+ mapping (run.sh:546-548). T1 minus T2 = exactly these 3 (64 vs 61).
156
+
157
+ 2. `model.compaction_interval` -> in T3 (config.example.yaml:120) ONLY; NOT in T1,
158
+ NOT in T2. CORRECTION/IMPORTANT: it has ZERO runtime consumer anywhere
159
+ (`grep compaction_interval|LOKI_COMPACTION` -> nothing). It is a DEAD documented
160
+ key. Do NOT invent a LOKI_* var for it in v1. Either (a) wire a real consumer
161
+ first, or (b) drop it from the generated example. Recommended v1: leave it OUT
162
+ of LOKI_CONFIG_MAP and remove it from the generated example (note in CHANGELOG),
163
+ defer a real consumer to v2.
164
+
165
+ 3. `model.autonomy_mode` -> CORRECTION: the approved plan says "neither table
166
+ carries" it. FALSE. It is in T1 (run.sh:319), T2 (run.sh:463) AND T3
167
+ (config.example.yaml:118). It is fully consistent; do not treat it as drift.
168
+
169
+ 4. `completion.council.*` (6 keys: enabled/size/threshold/check_interval/
170
+ min_iterations/stagnation_limit) and `completion.uncertainty.*` (4 keys:
171
+ escalation/rounds/nochange_min/split_rounds) -> in BOTH T1 and T2, but ABSENT
172
+ as live keys from T3 (council not present; uncertainty only as commented prose
173
+ at example.yaml:92-107). So T3 (config.example.yaml) is the MOST out-of-sync of
174
+ the three. The generated `config example` (SS6) closes this by emitting these
175
+ from LOKI_CONFIG_MAP so example can never lag the parsers again.
176
+
177
+ ### 2c. Reconciled canonical key set (LOCK)
178
+
179
+ LOKI_CONFIG_MAP = the 64 keys of T1 (the superset; it equals T2's 61 PLUS the 3
180
+ model.* keys). `model.compaction_interval` is EXCLUDED (no consumer). Final v1
181
+ count: 64 mappings. They are (grouped):
182
+
183
+ - core: max_retries, base_wait, max_wait, skip_prereqs
184
+ - dashboard: enabled, port
185
+ - resources: check_interval, cpu_threshold, mem_threshold
186
+ - security: staged_autonomy, audit_log, max_parallel_agents, sandbox_mode,
187
+ allowed_paths, blocked_commands
188
+ - phases: unit_tests, api_tests, e2e_tests, security, integration, code_review,
189
+ web_research, performance, accessibility, regression, uat
190
+ - completion: promise, max_iterations, perpetual_mode
191
+ - completion.council: enabled, size, threshold, check_interval, min_iterations,
192
+ stagnation_limit
193
+ - completion.uncertainty: escalation, rounds, nochange_min, split_rounds
194
+ - model: prompt_repetition, confidence_routing, autonomy_mode, planning,
195
+ development, fast
196
+ - parallel: enabled, max_worktrees, max_sessions, testing, docs, blog, auto_merge
197
+ - complexity: tier
198
+ - github: import, pr, sync, repo, labels, milestone, assignee, limit, pr_label
199
+ - notifications: enabled, sound
200
+
201
+ The exact `nested.path:LOKI_ENV_VAR` pairs are copied verbatim from T1's
202
+ `set_from_yaml` calls (run.sh:266-349) so env-var names are guaranteed correct.
203
+
204
+ ### 2d. settings.json mapping stays SEPARATE (do not merge)
205
+ `_load_json_settings` (run.sh:544-558) carries a DIFFERENT schema -- maxTier,
206
+ provider, issue.provider, notify.slack/discord, blind_validation,
207
+ adversarial_testing, spawn_timeout, spawn_retries, budget -- none of which any YAML
208
+ parser maps. Only model.planning/development/fast overlap. LOKI_CONFIG_MAP is the
209
+ YAML/config-file surface ONLY; settings.json keeps its own map. Merging them would
210
+ corrupt both surfaces. (v2 may optionally unify, additively.)
211
+
212
+ --------------------------------------------------------------------------------
213
+ ## 3. Refactor run.sh's two parsers to iterate the shared array
214
+
215
+ After config-map.sh exists, both parsers source it and iterate LOKI_CONFIG_MAP,
216
+ delegating the per-key export to the shared loader with override=0 (env-wins
217
+ PRESERVED -- the auto-discovery contract is unchanged):
218
+
219
+ - `parse_yaml_with_yq` (run.sh:419-504): delete the inline `mappings=(...)` array
220
+ (421-483); source config-map.sh; loop over `LOKI_CONFIG_MAP`; for each key read
221
+ the value via `yq eval ".$path"` (as today, 496) and pass it to
222
+ `loki_config_export_key "$env_var" "$value" 0`. The shared helper now owns the
223
+ env-wins guard, validate, and export (formerly run.sh:490-501).
224
+ - `parse_simple_yaml` (run.sh:262-350): replace the 64 hand-written
225
+ `set_from_yaml` calls with a loop over LOKI_CONFIG_MAP. `set_from_yaml`
226
+ (run.sh:389) is refactored to extract via grep/sed (410) then delegate to
227
+ `loki_config_export_key "$env" "$value" 0` (so its guard/validate/export at
228
+ 395/413/414 also route through the single shared helper).
229
+
230
+ `load_config_file` (run.sh:228-259) and its yq-present/absent routing are
231
+ unchanged. Net effect: 3 tables -> 1 AND one export helper -> env-wins (override=0)
232
+ and config-override (override=1) share identical parse/validate logic, so .env /
233
+ YAML / JSON can never diverge. run.sh keeps override=0 everywhere, so its shipped
234
+ behavior is byte-identical.
235
+
236
+ --------------------------------------------------------------------------------
237
+ ## 4. Format detection + parsing (reuse existing engines)
238
+
239
+ `loki_apply_config_file <path>` (in config-map.sh):
240
+
241
+ 1. Validate path: file exists, readable, not a symlink for project-local paths
242
+ (mirror load_config_file's symlink guard at run.sh:234). Missing/unreadable ->
243
+ honest non-zero exit + message; NO silent default fallback.
244
+ 2. Detect format:
245
+ - extension `.env` / no-ext-named-".env" -> ENV
246
+ - `.yaml` / `.yml` -> YAML
247
+ - `.json` -> JSON
248
+ - unknown/no extension -> content sniff: first non-blank, non-`#` line
249
+ starts with `{` -> JSON; matches `^[A-Z_][A-Z0-9_]*=` -> ENV; matches
250
+ `^[a-zA-Z0-9_.-]+:` -> YAML.
251
+ 3. Route (ALL three call `loki_config_export_key ... 1` -- override=1, in
252
+ config-map.sh; the pre-pass sources config-map.sh ONLY, never run.sh):
253
+ - ENV -> flat parser (SS4a).
254
+ - YAML -> yq if `command -v yq` else the simple grep/sed fallback, iterating
255
+ LOKI_CONFIG_MAP over the arbitrary path. Same logic as run.sh's parsers but
256
+ invoked with override=1.
257
+ - JSON -> yq reads JSON natively; else the audited python3 path modeled on
258
+ `_load_json_settings` (run.sh:525-565): json.load, isinstance(str) guard,
259
+ shlex.quote, fixed export template. Reuse that exact safe pattern, then feed
260
+ each resolved value to `loki_config_export_key ... 1`.
261
+
262
+ ### 4a. The flat `.env` parser (FULL ~250-flag coverage, day one)
263
+
264
+ For each line: skip blank and `#`-comment lines; split on the FIRST `=` only
265
+ (`key="${line%%=*}"`, `val="${line#*=}"`); strip surrounding quotes; trim. Key
266
+ allowlist: accept `^LOKI_[A-Z0-9_]+$`. Also accept a SHORT documented allowlist of
267
+ non-LOKI build vars actually consumed (verify each against source before locking;
268
+ candidates seen in run.sh defaults: e.g. provider/budget are already LOKI_*).
269
+ Default: reject any key not matching the allowlist with a visible warning (never a
270
+ silent skip). Each accepted value goes through `${VAR}` expansion (SS5) THEN
271
+ `validate_yaml_value` (run.sh:353) THEN `export`. This is what makes "all flags
272
+ configurable" honest on day one with near-zero code -- `.env` is the flat
273
+ full-surface form; nested friendly YAML/JSON full-coverage is v2.
274
+
275
+ Every value (ENV/YAML/JSON) passes `validate_yaml_value` (run.sh:353) before
276
+ export: rejects shell metachars `[$\`|;&><(){}[]\\]`, newlines, over-length
277
+ (>1000). NOTE (document this): because validate runs AFTER expansion, a resolved
278
+ secret whose VALUE contains a shell metachar would be rejected. This is
279
+ conservative and acceptable -- such values must be delivered via the env directly,
280
+ not through the validated config path.
281
+
282
+ --------------------------------------------------------------------------------
283
+ ## 5. ${VAR} env-ref expansion + raw-secret warning
284
+
285
+ ### 5a. Expansion (NEVER eval)
286
+ - Match a full-value ref `^\$\{[A-Za-z_][A-Za-z0-9_]*\}$` and embedded refs.
287
+ - Resolve via bash indirect expansion: `name="${ref:2:-1}"; value="${!name}"`.
288
+ NEVER `eval`.
289
+ - Order is EXPAND-THEN-VALIDATE: validate_yaml_value rejects `$`, so an
290
+ unexpanded `${VAR}` would always fail. Expansion is precisely what makes a ref
291
+ usable while every other literal `$` stays rejected.
292
+ - Unset ref -> SKIP that key + emit a warning (do not export empty, do not abort
293
+ the whole load). `config validate` reports unresolved refs (SS6).
294
+
295
+ ### 5b. Raw-secret warning (reuse the shipped scanner patterns)
296
+ Reuse the verified v7.73.0 commit-time scanner ideas:
297
+ `autonomy/run.sh:_commit_scan_secret_file` (6331-6382) and
298
+ `_commit_path_looks_secret` (6384+), which mirror `autonomy/verify.sh`'s
299
+ `verify_secret_scan_file`. Apply its TIER-1 format patterns to a config-file
300
+ VALUE that is a literal (not a `${VAR}` ref):
301
+
302
+ ```
303
+ AKIA[0-9A-Z]{16} | ASIA[0-9A-Z]{16}
304
+ -----BEGIN [A-Z0-9 ]*PRIVATE KEY-----
305
+ gh[pousr]_[A-Za-z0-9]{36,} | github_pat_[A-Za-z0-9_]{60,}
306
+ xox[baprs]-[A-Za-z0-9-]{10,}
307
+ sk-[A-Za-z0-9]{20,} (covers sk-ant-)
308
+ AIza[0-9A-Za-z_-]{35} | glpat-[A-Za-z0-9_-]{20,}
309
+ ```
310
+
311
+ Plus TIER-2 generic-assignment + bearer + URI-embedded-credential
312
+ (`scheme://user:pass@host`) patterns, run through the existing deny filter so
313
+ `${VAR}`-ref values are correctly IGNORED (the deny regex already excludes
314
+ `\$\{`/`\$[A-Za-z_]`/`process.env`/placeholders). On load: WARN ("use ${VAR} +
315
+ env/Vault"). In `config validate`: ERROR (non-zero exit).
316
+
317
+ --------------------------------------------------------------------------------
318
+ ## 6. `loki config example|schema|validate` (reuse loki:8177-8244 validators)
319
+
320
+ ### 6a. CRITICAL prerequisite: extract the validators
321
+ The per-key validators at loki:8177-8244 are INLINE in `cmd_config_set` (8141),
322
+ not a reusable function. CORRECTION to "reuse loki:8177-8244 validators": they
323
+ cannot be reused as-is. EXTRACT them into a new `validate_config_key <key>
324
+ <value>` helper (returns non-zero + message on invalid), and have BOTH
325
+ cmd_config_set AND `config validate` call it. Otherwise we duplicate the
326
+ validator logic -- reintroducing the exact drift this feature exists to fix.
327
+
328
+ ### 6b. New subcommands (add arms to cmd_config dispatch, loki:8093-8137)
329
+ - `config example` -> emit the full annotated nested YAML GENERATED from
330
+ LOKI_CONFIG_MAP (so it can never drift from the parsers), carrying
331
+ config.example.yaml's prose as comments. Drop the dead `compaction_interval`.
332
+ - `config schema` -> machine-readable `key -> LOKI_ENV_VAR -> type` table,
333
+ generated from LOKI_CONFIG_MAP.
334
+ - `config validate <file>` -> detect format (SS4), dry-expand refs and report
335
+ unresolved (SS5a), raw-secret check as ERROR (SS5b), run `validate_config_key`
336
+ per key where a validator exists, run `validate_yaml_value` on every value.
337
+ Non-zero exit on ANY failure. Update the cmd_config usage block (8113-8136) and
338
+ the `*)` default help to list the three new subcommands.
339
+
340
+ --------------------------------------------------------------------------------
341
+ ## 7. SDET test plan (mutation-proof; the 12 cases)
342
+
343
+ OBSERVABILITY DESIGN (this is the hard part -- named, not hand-waved):
344
+ - config-map.sh is side-effect-free on source (SS2), so the .env parser, the
345
+ `${VAR}` expander, format-detect, and the raw-secret matcher are UNIT-tested by
346
+ sourcing the lib and calling each function directly with crafted input.
347
+ - For the integration cases (esp. the keystone) the test drives the REAL binary
348
+ as a subprocess (same rationale as test-deploy.sh's header: autonomy/loki runs
349
+ main() when sourced, so extract+source is unsafe). The test needs an OBSERVATION
350
+ HOOK to read the RESOLVED `LOKI_*` without a full build. Mechanism: stub the
351
+ exec target -- put a fake `run.sh` (or a fake provider CLI) earlier on PATH /
352
+ via a test-only `RUN_SH` override that DUMPS the environment (`env | grep ^LOKI_`)
353
+ to a sentinel file and exits 0. The test asserts on the dumped values. (A small
354
+ documented `LOKI_CONFIG_DUMP=1` dry mode that prints resolved LOKI_* and exits
355
+ is an acceptable alternative observation hook; pick one and document it.)
356
+ - The test MUST NOT set `LOKI_AUTO_FIX=true`: run.sh:625-627 would clobber
357
+ MAX_ITERATIONS to 5 and corrupt the MAX_ITERATIONS assertions.
358
+ - Non-vacuity: every "value reaches runtime == X" assertion is paired with a flip
359
+ to a second value to prove it is not a default coincidence.
360
+
361
+ Cases:
362
+ 1. Config-only key reaches runtime: `LOKI_MAX_ITERATIONS=4242` via config file
363
+ only -> dumped == 4242; flip to 1337 to prove non-default.
364
+ 2. CLI overrides config: config `complexity.tier: complex` + `--simple` -> simple.
365
+ 3. KEYSTONE -- `--config` overrides ambient env: `export LOKI_MAX_ITERATIONS=10`,
366
+ config sets 4242 -> dumped == 4242. Proves the whole point.
367
+ 4. auto `.loki/config.yaml` LOSES to `--config`; AND ambient env still BEATS auto
368
+ `.loki/config.yaml` (unchanged-contract regression).
369
+ 5. `${VAR}` expands from env; `${UNSET}` -> key skipped + warning emitted.
370
+ 6. Raw-secret literal -> warning on load; non-zero on `config validate`; a
371
+ `${VAR}`-ref value -> no warning (deny-filter path).
372
+ 7. Format parity (WITH ambient env present -- mandatory): export a conflicting
373
+ ambient `LOKI_*`, then load the same logical config as `.env`, `.yaml`, AND
374
+ `.json` -> all three produce IDENTICAL dumped exports == the config value (NOT
375
+ the ambient value). Setting ambient env is required: the override defect only
376
+ surfaces when env is present (with env absent all three pass vacuously), so a
377
+ parity test without ambient env would not catch a YAML/JSON env-wins regression.
378
+ 8. Injection: value with `$(...)` / backticks / `;` rejected by
379
+ validate_yaml_value, never executed (sentinel-absent + value-not-exported).
380
+ 9. Bad/missing/symlink file -> honest non-zero exit, no silent default fallback.
381
+ 10. Drift test: every var in LOKI_CONFIG_MAP is consumed in run.sh (grep each
382
+ LOKI_* target has a `${LOKI_...:-` reader); report any unmapped LOKI_* so
383
+ coverage growth is measurable. Asserts T1==T2 (both now iterate the array).
384
+ 11. cmd_start no-op-arm test: `loki start --config f.env ./prd.md` -> prd_file is
385
+ ./prd.md (the path is NOT misread as the PRD positional), and `--config` is
386
+ NOT forwarded to the exec target.
387
+ 12. Bash/Bun parity harness (task #630) green; `bash scripts/local-ci.sh` green;
388
+ `bash tests/run-shellcheck.sh` clean on the new lib. Register a new
389
+ `test-config-file.sh` in tests/run-all-tests.sh (alongside the
390
+ `test-deploy.sh` registration at run-all-tests.sh:218).
391
+
392
+ Full SDLC fleet (Architect -> PO -> dev -> SDET -> 3/3 council) -> ship as own
393
+ MINOR release.
394
+
395
+ --------------------------------------------------------------------------------
396
+ ## 8. Docs to update + version bump
397
+
398
+ Docs (content updates, NOT version-gated):
399
+ - README.md, docs/INSTALLATION.md, DOCKER_README.md, wiki: `loki start --config
400
+ <path>` (+ `--vars` / `--env-file`), the precedence ladder, `${VAR}` syntax, the
401
+ secret rule, `config example|schema|validate`.
402
+ - docker-compose.yml: show mounting a config file + that config > env; secrets stay
403
+ in `.env` / mounted OAuth. `.env.example`: note `.env` is the flat full-surface
404
+ form.
405
+ - deploy/helm/autonomi/values.yaml + deployment-controlplane.yaml +
406
+ deployment-worker.yaml: document mounting config as a ConfigMap volume +
407
+ `--config /etc/loki/config.yaml`; secrets stay in the existing
408
+ Secret/`existingSecret` (Vault-ready) path, referenced via `${VAR}`.
409
+ - autonomy/config.example.yaml: regenerate (or note it is now generated by
410
+ `config example`); drop the dead `compaction_interval`.
411
+
412
+ Version bump -- INTEGRATOR ONLY, single release commit (per CONTRIBUTING.md:120,
413
+ devs must NOT bump versions; merge-conflict avoidance). The canonical 14 locations
414
+ (references/deployment.md:610): VERSION, package.json, SKILL.md (header + footer),
415
+ Dockerfile, Dockerfile.sandbox, vscode-extension/package.json, CLAUDE.md,
416
+ dashboard/__init__.py, mcp/__init__.py, CHANGELOG.md, docs/INSTALLATION.md,
417
+ wiki/Home.md, wiki/_Sidebar.md, wiki/API-Reference.md. Plus 4-channel validation.
418
+ NOTE for the integrator: the documented 14-list and the OBSERVED v7.73.0 bump set
419
+ diverge -- v7.73.0 also touched plugins/loki-mode/.claude-plugin/plugin.json and
420
+ loki-ts/dist/loki.js (not in the documented 14), while the documented list includes
421
+ vscode-extension/package.json and wiki/API-Reference.md (which v7.73.0 did not
422
+ touch). Reconcile against the actual repo at release time rather than trusting
423
+ either list blindly.
424
+
425
+ --------------------------------------------------------------------------------
426
+ ## 9. Phasing
427
+
428
+ - v1 (this plan, one release): `--config`/`--vars`/`--env-file` pre-pass; `.env` +
429
+ YAML + JSON; locked precedence; shared config-map.sh (pays down drift debt: 3
430
+ tables -> 1); `${VAR}` expansion + raw-secret warning; extracted
431
+ `validate_config_key`; `config example|schema|validate`; drift + mutation tests.
432
+ Satisfies "all flags configurable" via `.env` immediately.
433
+ - v2 (next release, ADDITIVE only -- never re-touch precedence): grow nested
434
+ friendly-key schema to the full ~250-flag surface; wire a real consumer for
435
+ `model.compaction_interval`; optionally unify settings.json map with
436
+ LOKI_CONFIG_MAP; optionally unify init/edit(YAML) vs set/get(settings.json).
437
+
438
+ --------------------------------------------------------------------------------
439
+ ## 10. Critical files (verified line anchors, post-v7.73.0 working tree)
440
+
441
+ - autonomy/loki -- main() pre-pass insert after 15302/before 15309; session-gate
442
+ on the start/run/quick arms (15314+); cmd_start arg loop 1042 (add no-op
443
+ --config/--vars/--env-file arms); exec handoff 2097; cmd_config dispatch
444
+ 8089-8137 (+3 subcommands); EXTRACT validators 8177-8244 -> validate_config_key.
445
+ - autonomy/run.sh -- load_config_file 228-259 (unchanged routing);
446
+ parse_simple_yaml 262-350 and parse_yaml_with_yq 419-504 (refactor to iterate
447
+ LOKI_CONFIG_MAP); set_from_yaml 389-416; validate_yaml_value 353-379;
448
+ _load_json_settings 522-566 (JSON safe-pattern reuse); secret patterns
449
+ _commit_scan_secret_file 6331-6382 (reuse for raw-secret check); AUTO_FIX clobber
450
+ 625-627 (test caveat).
451
+ - autonomy/lib/config-map.sh -- NEW: canonical LOKI_CONFIG_MAP (64 keys) +
452
+ loki_maybe_apply_config_file + loki_apply_config_file + .env parser + ${VAR}
453
+ expander + format detect; side-effect-free on source.
454
+ - autonomy/config.example.yaml -- basis for generated `config example`; drop dead
455
+ compaction_interval.
456
+ - deploy/helm/autonomi/values.yaml + deployment-controlplane.yaml +
457
+ deployment-worker.yaml -- the envFrom injection that makes --config > env
458
+ necessary; doc/mount updates.
459
+ - tests/test-config-file.sh (NEW) + tests/run-all-tests.sh (register near :218).
@@ -0,0 +1,206 @@
1
+ # Enterprise Identity Roadmap
2
+
3
+ This document scopes Loki Mode's enterprise identity surface honestly. It
4
+ separates what is shipped today from what is roadmap. Nothing in the
5
+ "Roadmap" section ships today. Where a capability is not built, it is labeled
6
+ as such, with a real effort estimate and the identity-provider (IdP) or test
7
+ infrastructure it would require.
8
+
9
+ The intent is to give a precise, non-aspirational picture so that marketing,
10
+ sales, and docs never overstate the enterprise identity story.
11
+
12
+ ## 1. Current state (shipped, verified)
13
+
14
+ Everything in this section is backed by code in `dashboard/auth.py` and
15
+ `dashboard/server.py`. Function names are cited so claims can be checked.
16
+
17
+ ### Token authentication
18
+
19
+ - Opt-in via `LOKI_ENTERPRISE_AUTH=true` (off by default).
20
+ See `ENTERPRISE_AUTH_ENABLED` and `is_enterprise_mode()` in
21
+ `dashboard/auth.py`.
22
+ - API tokens are minted, hashed (per-token random salt, SHA-256), revoked,
23
+ deleted, and listed:
24
+ `generate_token()`, `revoke_token()`, `delete_token()`, `list_tokens()`,
25
+ `validate_token()`. Tokens are stored at `~/.loki/dashboard/tokens.json`
26
+ with enforced `0600` permissions (`_save_tokens()`).
27
+ - Token validation iterates all entries with a constant-time compare to avoid
28
+ leaking token count via timing (`validate_token()` plus
29
+ `_constant_time_compare()`).
30
+
31
+ ### OIDC bearer-token validation (the closest foundation for SSO)
32
+
33
+ - Opt-in via `LOKI_OIDC_ISSUER` + `LOKI_OIDC_CLIENT_ID`.
34
+ See `OIDC_ENABLED` and `is_oidc_mode()` in `dashboard/auth.py`.
35
+ - Inbound JWTs are validated by `validate_oidc_token()`. When PyJWT +
36
+ cryptography are installed, signatures are cryptographically verified
37
+ (RS256/RS384/RS512) against the provider's JWKS endpoint, with issuer and
38
+ audience checks (`_get_oidc_config()`, `_get_jwks()`).
39
+ - Without PyJWT, tokens are rejected unless `LOKI_OIDC_SKIP_SIGNATURE_VERIFY`
40
+ is explicitly set (insecure, local-testing only, loudly logged as critical).
41
+ - Role/group claims are mapped to Loki roles by `_scopes_from_claims()` /
42
+ `_collect_role_claims()`, supporting generic `roles`/`groups`, Keycloak
43
+ `realm_access.roles`, AWS Cognito `cognito:groups`, and a configurable
44
+ claim (`LOKI_OIDC_ROLES_CLAIM`). Unrecognized claims fall back to the
45
+ least-privileged default role (`_default_oidc_role()`, default `viewer`),
46
+ never admin.
47
+
48
+ IMPORTANT distinction: this is server-side validation of a bearer JWT that
49
+ some other system obtained. Loki does NOT implement a browser login flow,
50
+ an authorization-code exchange, or an end-user SSO redirect. A separate
51
+ component must perform the user-facing sign-in and present the resulting
52
+ JWT to Loki.
53
+
54
+ ### Scopes and predefined roles (read/control style authorization)
55
+
56
+ - Four predefined roles in `ROLES`: `admin`, `operator`, `viewer`,
57
+ `auditor`. Scope hierarchy (`_SCOPE_HIERARCHY`, `has_scope()`):
58
+ `*` -> `control` -> `write` -> `read`, plus `audit`/`admin`.
59
+ - Endpoint enforcement via the `require_scope()` dependency factory and the
60
+ `get_current_token()` FastAPI dependency. When neither auth mode is enabled,
61
+ access is anonymous (local-first default).
62
+
63
+ ### Dashboard transport gating (added in Release A)
64
+
65
+ - WebSocket auth gating: the dashboard validates a bearer token (header or
66
+ `?token=`/`?access_token=` query for browser clients) at the mount
67
+ boundary before delegating, mirroring the HTTP `get_current_token` order
68
+ (OIDC first, then loki token). See `_MountAuthGuard._validate_ws_token()`
69
+ and `_ws_token_from_scope()` in `dashboard/server.py`.
70
+ - REST scope consistency: `/api/memory` and `/api/collab` endpoints were
71
+ brought in line with the read/control scope model so they are not reachable
72
+ unauthenticated when enterprise auth is on.
73
+ - Webhook/trigger HMAC: the external trigger surface requires a shared secret
74
+ and rejects unsigned/mismatched requests (constant-time compare via
75
+ `hmac.compare_digest`, surfaced through `_constant_time_compare()`).
76
+
77
+ ### Tenant isolation (data-plane, present but minimal)
78
+
79
+ - A `Tenant` model exists (`dashboard/models.py`: `class Tenant`, with
80
+ `Project.tenant_id` foreign keys) and the v2 API enforces a tenant boundary
81
+ derived from a trusted, server-validated `tenant:<id>` scope on the token
82
+ (`dashboard/api_v2.py`: `TENANT_SCOPE_PREFIX`). A non-admin token is pinned
83
+ to one tenant; cross-tenant requests are denied with 403; an un-scoped
84
+ token reaches no tenant-scoped resource.
85
+ - This is project-level data isolation keyed off a token scope. It is NOT a
86
+ full tenant RBAC system (no roles per tenant, no per-tenant policy
87
+ administration, no tenant lifecycle management UI). See the roadmap below.
88
+
89
+ ### What the infrastructure OIDC is (and is NOT)
90
+
91
+ The OIDC referenced in the Terraform and Helm assets is workload identity
92
+ (IRSA on AWS / Workload Identity on GCP) used so the running pods can assume
93
+ cloud roles. That is machine-to-cloud authentication, not end-user SSO.
94
+
95
+ Likewise, Kubernetes RBAC and NetworkPolicy in the Helm chart govern what the
96
+ cluster service accounts and pods may do. They are cluster-plane controls and
97
+ are unrelated to product-level or per-tenant authorization for Loki users.
98
+ Do not conflate cluster RBAC with application RBAC.
99
+
100
+ ## 2. Roadmap (NOT built)
101
+
102
+ None of the items below are implemented. Each lists honest scope, a rough
103
+ effort estimate, and the IdP or test infrastructure required.
104
+
105
+ ### SSO / SAML (browser sign-in flow)
106
+
107
+ - Scope: a user-facing single sign-on flow. Two sub-paths:
108
+ - OIDC authorization-code login (closer to today's code): a browser
109
+ redirect to the IdP, code exchange, session/cookie issuance, and CSRF/
110
+ state handling. The existing `validate_oidc_token()` already validates the
111
+ resulting JWT, so this is the smaller of the two.
112
+ - SAML 2.0 (still common in large enterprises): SP metadata, ACS endpoint,
113
+ XML signature validation, NameID/attribute mapping, and IdP-initiated and
114
+ SP-initiated flows.
115
+ - Effort: OIDC login flow on top of the existing validator is roughly a few
116
+ weeks (1 engineer) including session management and tests. Full SAML 2.0 is
117
+ larger, on the order of 1 to 2 months, because XML signature handling and
118
+ multi-IdP quirks are involved; using a vetted SAML library is mandatory
119
+ rather than hand-rolling.
120
+ - Needs: a real IdP to test against (Okta, Azure AD/Entra ID, or Auth0; a
121
+ free developer tenant works for early dev). Integration tests must run
122
+ against that IdP, not mocks alone.
123
+
124
+ ### SCIM user/group provisioning
125
+
126
+ - Scope: a SCIM 2.0 server so an IdP can create/update/deactivate users and
127
+ push group membership automatically, instead of users being created on
128
+ first login. Requires `/scim/v2/Users` and `/scim/v2/Groups` endpoints,
129
+ filtering, pagination, PATCH semantics, and mapping SCIM groups onto Loki
130
+ roles/tenants.
131
+ - Effort: roughly 1 to 1.5 months (1 engineer) for a conformant subset plus a
132
+ persistence model for provisioned identities (today there is no durable
133
+ user store; OIDC users are derived per-request from claims).
134
+ - Needs: a SCIM-capable IdP to drive the provisioning (Okta or Azure AD), plus
135
+ a SCIM conformance test harness. This depends on a real user-store model
136
+ landing first.
137
+
138
+ ### App-level / tenant RBAC
139
+
140
+ - Scope: roles and policy that go beyond the current four global roles and
141
+ the `read`/`control` scope hierarchy. Concretely: roles scoped per tenant,
142
+ per-tenant policy administration, custom roles, resource-level permissions,
143
+ and an admin surface to manage them.
144
+ - Current state to build on: `Tenant`/`Project` models exist and a
145
+ `tenant:<id>` scope pins a token to one tenant (data isolation). What is
146
+ missing: per-tenant role assignment, a policy model richer than the global
147
+ scope hierarchy, custom/role-definition management, and any UI for it.
148
+ - Effort: roughly 1 to 2 months (1 to 2 engineers) depending on how much
149
+ policy flexibility is committed to (a fixed per-tenant role set is the
150
+ smaller end; arbitrary custom roles and resource-level rules is the larger
151
+ end).
152
+ - Needs: no external IdP, but it pairs naturally with SCIM (group-to-role
153
+ mapping) and benefits from the durable user store noted above.
154
+
155
+ ### SOC2
156
+
157
+ - Scope: SOC2 is a compliance PROGRAM, not a code feature. It spans durable,
158
+ tamper-evident audit logging with retention and SIEM export, access reviews,
159
+ change-management process, vendor management, security policies, and a Type
160
+ II observation window (commonly 6 to 12 months of evidence collected under
161
+ an auditor).
162
+ - What exists today that helps but does not constitute SOC2: hash-chained
163
+ audit logging and syslog/SIEM forwarding (see the audit-logging docs). These
164
+ are inputs to an audit, not the certification.
165
+ - Effort: multi-quarter organizational work, typically 6 to 12 months elapsed,
166
+ involving engineering, security, and an external auditor. It is not a sprint
167
+ and should never be represented as a shippable feature.
168
+ - Needs: an auditor engagement, an evidence/observation window, and
169
+ organizational process, in parallel with (not blocking) the code items above.
170
+
171
+ ## 3. Sequencing recommendation
172
+
173
+ 1. OIDC authorization-code login flow first. It reuses the existing
174
+ `validate_oidc_token()` validator, is the smallest increment, and unlocks
175
+ the most common "we need SSO" enterprise requirement with the least new
176
+ surface. This is the natural next code step after today's OIDC bearer
177
+ validation.
178
+ 2. App-level / tenant RBAC next, building on the existing `Tenant` model and
179
+ `tenant:<id>` scope. Most mid-market and enterprise deals ask for "roles
180
+ and tenant isolation" once SSO is in place.
181
+ 3. SCIM after a durable user store exists (it depends on one). SCIM is a
182
+ larger-enterprise ask and is most valuable once SSO and RBAC are stable.
183
+ 4. SAML in parallel with or after the OIDC login flow, only when a target
184
+ account specifically requires SAML rather than OIDC. Many enterprises
185
+ accept OIDC, so do not build SAML speculatively.
186
+ 5. SOC2 runs as parallel organizational work, started early because of the
187
+ observation window, but tracked separately from the code roadmap. The
188
+ existing audit-log foundation is a head start, not a finish line.
189
+
190
+ Rationale: OIDC login -> tenant RBAC -> SCIM is the path that unlocks the most
191
+ enterprise deals soonest while reusing the most existing code. SAML is
192
+ demand-gated. SOC2 is long-lead org work that should begin in parallel rather
193
+ than block feature delivery.
194
+
195
+ ## 4. What we do NOT claim
196
+
197
+ Loki Mode does NOT today ship: a browser SSO login flow, SAML support, SCIM
198
+ provisioning, app-level or per-tenant RBAC beyond the global four-role / read-
199
+ control scope model and the `tenant:<id>` data-isolation scope, or SOC2
200
+ certification. The shipped identity surface is: opt-in token auth, opt-in OIDC
201
+ bearer-token validation, a read/control scope model with four predefined
202
+ roles, dashboard WebSocket and REST auth gating, webhook HMAC verification,
203
+ and tenant data isolation via a token scope. Workload-identity OIDC (IRSA /
204
+ Workload Identity) and Kubernetes RBAC/NetworkPolicy are infrastructure
205
+ controls and are not end-user SSO or product RBAC. Any statement beyond this
206
+ is roadmap, not a current capability.