docs/PHASE9: analyze + baseline + plan (single bundled commit)
Bundled the three doc steps since the surface is small (4-commit
impl, no major redesigns from formulate).
Analyze findings (12, A1-A12):
A1-A2 — main.lua surface clean; no new FFI needed
A3 — Q-P2 RESOLVED via baseline: sha256sum (GNU coreutils)
A4 — Q-P1: trust prompt AFTER user-config status line
A5 — Q-P3: don't log walk-up by default; :config show on demand
A6 — Q-P5: :cfg show top-level by default; `full` for deep
A7 — Q-P6: project may set secrets.vault (covered by trust prompt)
A8 — Q-P4 DEFERRED: rl.readline early-startup smoke at impl time
A9 — walk-up perf <1ms even pessimistic
A10 — trust-file race: JSONL append-only handles concurrent writes
A11 — sandboxed dofile out of scope (trust prompt IS the gate)
A12 — bootstrap order is correct: user→project→secrets_session
Baseline:
B1 — sha256sum + openssl agree byte-for-byte on noether;
sha256sum chosen (universal + simpler parse).
§10 Open Qs table now shows resolutions inline (5/6 done; Q-P4
deferred to implement-time smoke).
§13 Implementation Plan added — 4 commits:
1. history.lua: trust file helpers (read/add/is_trusted + _sha256_file)
2. main.lua: walk-up + load_config_with_overlay + trust prompt
3. repl.lua: :config show meta + startup status line
4. config.lua header note + status -> Implement
Per-commit risk index covers sha256sum-missing case, JSONL partial
write, A8 rl.readline early-startup, symlink-loop walk-up,
:config show token leakage via conservative masking heuristic.
Open at plan-time (resolve at impl):
- A8 rl.readline behavior; fall back to io.read if broken
- $AISH_TRUST_FILE env override for CI isolation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+170
-7
@@ -2,9 +2,85 @@
|
||||
|
||||
**Project:** aish — AI-augmented conversational shell
|
||||
**Document:** Phase 9 Requirements, Architecture & Design Decisions
|
||||
**Status:** Formulate (pre-analyze)
|
||||
**Status:** Plan (formulate + analyze + baseline complete; tree at `4f5c3ae`)
|
||||
**Date:** 2026-05-16
|
||||
|
||||
**Analyze + baseline findings (2026-05-16) — 5/6 open Qs resolved
|
||||
in-place; Q-P4 deferred to implement-time verify:**
|
||||
|
||||
A1. **main.lua load_config surface clean.** `load_config(opts)` at
|
||||
`main.lua:53` returns `(cfg, path)` for the user config. Adding
|
||||
a project-overlay wrapper that calls it then walks for `.aish.lua`
|
||||
is additive — no refactor of the existing 4-tier resolution.
|
||||
|
||||
A2. **No new FFI needed for walk-up.** `io.open(candidate, "rb")` is
|
||||
sufficient for existence check; `libc.getcwd()` from Phase 6
|
||||
provides the starting point. No new C bindings.
|
||||
|
||||
A3. **Q-P2 RESOLVED via probe (B1 below): use `sha256sum`** — GNU
|
||||
coreutils ships it everywhere aish targets. Single-shell-out
|
||||
pattern; output: `<digest> <path>` → `cut -d' ' -f1` for the
|
||||
hex digest. No new module dependency.
|
||||
|
||||
A4. **Q-P1 RESOLVED: trust prompt AFTER `aish: loaded config`
|
||||
status.** The user sees what user-config is in play first, then
|
||||
decides about the overlay. Natural ordering.
|
||||
|
||||
A5. **Q-P3 RESOLVED: don't log walk-up path by default.** Too noisy
|
||||
on every startup. If debugging "why isn't my project file
|
||||
found?", `:config show` after startup will reveal the walk
|
||||
result (declined-or-not-found is visible). Verbose-mode walk
|
||||
log is v2 polish.
|
||||
|
||||
A6. **Q-P5 RESOLVED: `:config show` shows top-level only by default.**
|
||||
Nested tables collapsed to `{key1, key2, ...}` (just the inner
|
||||
table's keys for orientation). `:config show full` for the
|
||||
deep dump. Keeps the diagnostic surface tractable.
|
||||
|
||||
A7. **Q-P6 RESOLVED: project layer CAN set `secrets.vault`** — it's
|
||||
part of the trust prompt's scope. User accepting the prompt
|
||||
accepts that the project file may redirect secrets. The
|
||||
in-memory secrets session is built AFTER config resolution, so
|
||||
a project-set `secrets.vault` IS honored.
|
||||
|
||||
A8. **rl.readline at startup (Q-P4 — deferred).** Phase 4's
|
||||
`:memory summarize` candidate-prompt path also calls
|
||||
`rl.readline` early (in metas; not pre-loop). The trust prompt
|
||||
fires BEFORE the main loop opens — earlier than any existing
|
||||
rl.readline call site. **Implement-time check**: smoke-test
|
||||
that rl.readline behaves correctly when called from
|
||||
`load_config_with_overlay` before `M.run` ever fires. If it
|
||||
misbehaves, fall back to a `printf "..." + read` shell-out for
|
||||
the trust prompt.
|
||||
|
||||
A9. **Walk-up performance is fine** — at most ~10 levels from a
|
||||
typical cwd to $HOME, each `io.open` is ~10us. Total walk
|
||||
cost < 1ms even on slow filesystems.
|
||||
|
||||
A10. **Trust file race**: two aish instances starting concurrently
|
||||
could double-write to `~/.aish/trusted-projects`. JSONL append
|
||||
semantics handle this OK (each writes one complete line); a
|
||||
duplicate trust entry is harmless. No flock needed (unlike
|
||||
memory.jsonl per Phase 4 where the writer SOR was important).
|
||||
|
||||
A11. **Sandboxed env for dofile?** Out of scope per §8. The trust
|
||||
prompt IS the gate; we accept full Lua execution post-trust.
|
||||
|
||||
A12. **Bootstrap chicken-egg**: project's `.aish.lua` could set
|
||||
`secrets.vault` which would change WHICH secrets are loaded.
|
||||
A12 paths through cleanly: user config loaded → project
|
||||
overlay merged → effective config passed to M.run → M.run
|
||||
reads `config.secrets.vault` (now possibly the project's) →
|
||||
secrets_session built. Order is correct; no chicken-egg.
|
||||
|
||||
**Baseline finding:**
|
||||
|
||||
B1. `sha256sum` (GNU coreutils 9.7) and `openssl dgst -sha256` agree
|
||||
bit-for-bit on the same input file. Both present on noether.
|
||||
sha256sum chosen for simpler output parsing (digest in first
|
||||
whitespace-separated field; openssl needs `awk '{print $NF}'`).
|
||||
Per A3 resolution; documented in Q-P2.
|
||||
|
||||
PHASE0 is the locked substrate; PHASE1-8 are layered on top. This manifest
|
||||
specifies what Phase 9 adds — **project-local config overlay (`.aish.lua`)**:
|
||||
a per-project config file in or above cwd that merges onto the user's
|
||||
@@ -316,12 +392,12 @@ No new config keys in v1 (the project overlay IS the new mechanism; it doesn't n
|
||||
|
||||
| # | Question | Impact | Resolution target |
|
||||
|---|---|---|---|
|
||||
| Q-P1 | Should the trust prompt happen BEFORE or AFTER `aish: loaded config from <path>` startup status? | Startup readability | Analyze (probably AFTER — user sees what config is in play, then makes trust decision about overlay) |
|
||||
| Q-P2 | `sha256_file` via `sha256sum` vs `openssl dgst -sha256`. Both are POSIX-common. Which is more universally present on the fleet? | Hash backend choice | Baseline (probe both on fleet hosts) |
|
||||
| Q-P3 | Should `_find_project_config` log the walk-up path it searched at startup (for debugging)? | Debug visibility | Analyze (probably only when no file found AND verbose mode enabled — too noisy by default) |
|
||||
| Q-P4 | Trust prompt is at startup BEFORE the readline prompt is fully set up — is `rl.readline` safe to call this early? | Interactive prompt sequencing | Analyze (probably yes — Phase 4 :memory candidate-prompt also calls rl.readline at startup; same pattern) |
|
||||
| Q-P5 | Should `:config show` display the FULL effective config (potentially 100s of lines if mcp servers etc. are deeply nested) or just top-level keys? | UX | Analyze (just top-level with "..." for nested; full dump via `:config show full` if needed) |
|
||||
| Q-P6 | Should the project file be allowed to set `secrets.vault` (Phase 5/13)? It's marked 0600-sensitive — letting an untrusted project file point at a different vault is a leak vector. | Security | Analyze (resolution: project layer CAN set secrets.vault but it's part of the trust prompt; the user accepts everything when they trust) |
|
||||
| Q-P1 | Trust prompt before/after `aish: loaded config` status | A4 — **AFTER**; user sees user-config first, then decides about overlay. |
|
||||
| Q-P2 | sha256 backend choice | B1 RESOLVED — `sha256sum` (GNU coreutils; universal on Linux); simpler output parsing than openssl. |
|
||||
| Q-P3 | Log walk-up path | A5 — **no by default**; `:config show` reveals walk result on demand. Verbose-mode walk log is v2 polish. |
|
||||
| Q-P4 | rl.readline safe at startup | A8 — DEFERRED to implement-time smoke (Phase 4 metas call rl.readline early too; new wrinkle is firing BEFORE main loop opens). If issue, fall back to printf+read shell-out. |
|
||||
| Q-P5 | `:config show` full vs top-level | A6 — **top-level by default** (nested collapsed to inner keys); `:config show full` for deep dump. |
|
||||
| Q-P6 | Project layer setting `secrets.vault` security | A7 — **allowed**; part of the trust prompt's scope. Bootstrap order (A12) ensures project's vault is honored if set. |
|
||||
|
||||
---
|
||||
|
||||
@@ -339,3 +415,90 @@ Candidate follow-ups (non-binding):
|
||||
|
||||
Phase 9 itself is self-contained — depends on no specific prior phase
|
||||
beyond the existing config loader.
|
||||
|
||||
---
|
||||
|
||||
## 13. Implementation Plan (commit-by-commit)
|
||||
|
||||
4 commits, bottom-up:
|
||||
|
||||
1. **`history.lua` — trust file helpers.**
|
||||
- `M.read_trusted(path)` -> list of `{path, sha256, ts}`
|
||||
entries; mode-check the file at 0600, refuse to load (warn)
|
||||
if wider. Missing file → empty list.
|
||||
- `M.add_trusted(trust_path, project_path, sha256)` appends a
|
||||
JSONL line; mkdir -p the parent if needed; chmod 0600.
|
||||
- `M.is_trusted(trust_path, project_path, sha256)` reads + checks
|
||||
for matching entry.
|
||||
- Internal `_sha256_file(path)` shells out to `sha256sum` and
|
||||
parses the first whitespace-separated field.
|
||||
- Smoke: 5 inline unit cases (read empty, add+read-back, mode
|
||||
check, sha mismatch returns false, missing file).
|
||||
|
||||
2. **`main.lua` — walk-up + load_with_project_overlay.**
|
||||
- `_find_project_config()` walks from libc.getcwd() up to $HOME,
|
||||
returning first `.aish.lua` or nil.
|
||||
- `_prompt_trust(project_path, sha)` calls `rl.readline` with
|
||||
the trust prompt; on accept, calls `history.add_trusted`.
|
||||
A8: smoke-test rl.readline behavior at this early call site.
|
||||
- `load_config_with_overlay(opts)` wraps existing `load_config`;
|
||||
finds project, checks trust, prompts if needed, dofiles +
|
||||
merges shallow over user config. Returns `(cfg, sources, paths)`
|
||||
triple where sources maps top-level keys to "user"/"project"
|
||||
for `:config show`.
|
||||
- `main()` calls `load_config_with_overlay`, stashes sources
|
||||
into a global so repl.lua can read it (or passes via cfg).
|
||||
- Smoke: tree-resolution test from a nested cwd; trust prompt
|
||||
accept/decline paths.
|
||||
|
||||
3. **`repl.lua` — `:config show` meta + startup status line.**
|
||||
- `:config show` / `:config show full` meta reads the sources
|
||||
map + the effective config; sanitizes token-bearing values
|
||||
(any key matching `_token`, `_TOKEN`, `auth_token` → display
|
||||
as `(set)`); prints sources + key-by-key effective.
|
||||
- Startup status line per A4: AFTER the existing `aish: loaded
|
||||
config from <path>`, if project layer fired, emit
|
||||
`[aish] project config: <path> (overlaid on <user>)`.
|
||||
- HELP gains 2 `:config` lines.
|
||||
- Smoke: with a test project file, run `:config show` and
|
||||
verify keys + sources line up.
|
||||
|
||||
4. **`config.lua` template note + status bump.**
|
||||
- Add a header comment to `config.lua` (the in-tree example)
|
||||
noting Phase 9 project-overlay availability (no other config
|
||||
change — overlay is a separate file).
|
||||
- PHASE9.md status header -> **Implement**.
|
||||
|
||||
### Risk index per commit
|
||||
|
||||
| Commit | Risk | Mitigation |
|
||||
|---|---|---|
|
||||
| 1 (history) | sha256sum not installed (some minimal images) | Detect at startup; if missing, warn + decline all trust prompts (project layer disabled). Documented. |
|
||||
| 1 (history) | Trust file partial write (interrupted append) corrupts later parse | JSONL one-line-per-entry; partial line at EOF is skipped on read (each line is a single json.decode). |
|
||||
| 2 (main) | A8 — rl.readline at startup (before main loop) untested in earlier phases | Smoke-test at commit-time; if broken, fall back to `io.read("*l")` from stdin (no readline frills like ^C-handling but functional). |
|
||||
| 2 (main) | Walk-up symlink loops | `realpath`/`stat` defenses out of scope for v1; walk is bounded by $HOME stop. Pathological symlinks could waste cycles but not infinite-loop (every iteration strips a path component). |
|
||||
| 3 (repl) | :config show might leak token values if a config key isn't matched by the masking heuristic | Conservative mask: any key containing "token", "secret", "auth", "key" (case-insensitive) → display `(set)`. Errs toward over-masking. |
|
||||
| 4 (config + status) | None | |
|
||||
|
||||
### Tests + smoke per commit
|
||||
|
||||
Each commit:
|
||||
- Pass `luajit test_safety.lua` (87/87) and `luajit test_router_model.lua` (31/31)
|
||||
- Load cleanly via `luajit -e 'package.path=...; require("repl"); print("ok")'`
|
||||
- Pass a per-feature smoke (described per row above)
|
||||
|
||||
### Things deliberately NOT split
|
||||
|
||||
- Separate `project.lua` module — small enough; history.lua already
|
||||
handles file-with-mode-check (memory.jsonl); same shape.
|
||||
- :trust / :untrust runtime metas — manual ~/.aish/trusted-projects
|
||||
editing is fine for v1.
|
||||
- Walk-up logging on first startup — easy to add later if needed.
|
||||
|
||||
### Open at plan-time (resolve at implement)
|
||||
|
||||
- A8: rl.readline early-startup behavior. If broken, swap to
|
||||
io.read("*l") for the trust prompt only.
|
||||
- Whether to make the trust file path itself overridable via
|
||||
`$AISH_TRUST_FILE` env. Useful for CI / test isolation. Default
|
||||
to ~/.aish/trusted-projects; env override is one line.
|
||||
|
||||
Reference in New Issue
Block a user