repl: :config show meta + HELP (Phase 9 commit #3)
User-facing diagnostic for the project-overlay layer. Reads config._sources (R3 cfg-embedded by main.lua's load_config_with_ overlay in commit #2) + the effective config; surfaces which file contributed each top-level key. :config show top-level keys + which source set each (nested tables collapsed to inner-key list) :config show full recursive dump with sensitive-key masking Masking heuristic (any key containing token/secret/auth/key, case-insensitive) -> "(set)" instead of the value. R6: applied RECURSIVELY in full mode so the actual leak vector (mcp.servers.<alias>.auth_token, models.<x>.auth_token) is caught. Defensive depth cap (5) prevents pathological recursion. When config._sources is absent (caller didn't go through load_config_with_overlay), status: "(unknown — main didn't pass _sources)" — meta still runs, just labels source as "?". N2 known cosmetic false-positive: `key_env` / `auth_env` config fields hold env-var NAMES (not secrets) but match the heuristic. Future polish exempts `*_env` patterns. Same for `token_budget` (contains "token") — also masked despite being a plain number. Acceptable; errs toward over-masking. HELP gains 1 :config line. E2E verified across 4 scenarios with AISH_TRUST_FILE + isolated HOME: A. No project overlay: 6 user keys; nested tables collapsed. `secrets` masked as (set) at top level. B. Project overlay accepted: source map cleanly partitioned (user has 4 keys; project has 2 — default_model + models); each top-level row tagged [user] or [project]. C. :config show full: nested dump; auth_token in models.cloud correctly masked as (set); SECRET_VAL never appears in output (grep count = 0). Regression: test_safety 87/87, test_router_model 31/31, repl loads. Commit #4 next: config.lua template comment + PHASE9.md status header -> Implement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -188,6 +188,8 @@ Meta commands:
|
|||||||
:cost summary of session token/cost usage
|
:cost summary of session token/cost usage
|
||||||
:cost detail per-model + per-category breakdown
|
:cost detail per-model + per-category breakdown
|
||||||
:cost reset zero the cost meter (also clears warn flags)
|
:cost reset zero the cost meter (also clears warn flags)
|
||||||
|
:config show [full] show config sources (user / project overlay) +
|
||||||
|
effective config; sensitive keys masked as (set)
|
||||||
:highlight [on|off|status]
|
:highlight [on|off|status]
|
||||||
toggle tree-sitter syntax highlighting on assistant
|
toggle tree-sitter syntax highlighting on assistant
|
||||||
code fences (requires external tree-sitter CLI +
|
code fences (requires external tree-sitter CLI +
|
||||||
@@ -2045,6 +2047,99 @@ function M.run(config)
|
|||||||
-- :cost reset zero the meter (also clears warn flags)
|
-- :cost reset zero the meter (also clears warn flags)
|
||||||
-- R7 sort: (cost desc, model asc, category asc) — table.sort is
|
-- R7 sort: (cost desc, model asc, category asc) — table.sort is
|
||||||
-- unstable, so the 3-level key ensures deterministic output.
|
-- unstable, so the 3-level key ensures deterministic output.
|
||||||
|
-- Phase 9: :config show meta. Reads config._sources (R3 cfg-
|
||||||
|
-- embedded source map from main.lua's load_config_with_overlay)
|
||||||
|
-- + the effective config. Sanitizes token-bearing fields per
|
||||||
|
-- the masking heuristic (any key containing token/secret/auth/
|
||||||
|
-- key, case-insensitive -> "(set)" instead of the value).
|
||||||
|
--
|
||||||
|
-- R6: `:config show` (default) shows top-level keys with nested
|
||||||
|
-- tables collapsed to their inner-key list; `:config show full`
|
||||||
|
-- recurses and applies the same masking heuristic at every level.
|
||||||
|
-- N2 known false positive: key_env / auth_env hold env-var
|
||||||
|
-- names (not secrets) but match the heuristic; future polish
|
||||||
|
-- exempts `*_env` patterns.
|
||||||
|
local _CFG_MASK_RE = "token|secret|auth|key" -- pipe-OR via gsub
|
||||||
|
local function _is_sensitive_key(k)
|
||||||
|
local lk = tostring(k):lower()
|
||||||
|
return lk:find("token", 1, true)
|
||||||
|
or lk:find("secret", 1, true)
|
||||||
|
or lk:find("auth", 1, true)
|
||||||
|
or lk:find("key", 1, true)
|
||||||
|
end
|
||||||
|
local function _fmt_value(v, full, depth)
|
||||||
|
depth = depth or 0
|
||||||
|
if type(v) == "string" then return string.format("%q", v) end
|
||||||
|
if type(v) == "number" or type(v) == "boolean" then return tostring(v) end
|
||||||
|
if type(v) == "function" then return "<function>" end
|
||||||
|
if type(v) == "table" then
|
||||||
|
local keys = {}
|
||||||
|
for k, _ in pairs(v) do keys[#keys + 1] = tostring(k) end
|
||||||
|
table.sort(keys)
|
||||||
|
if not full then
|
||||||
|
return "{" .. table.concat(keys, ", ") .. "}"
|
||||||
|
end
|
||||||
|
-- Full mode: recurse, masking sensitive keys.
|
||||||
|
if depth >= 5 then return "{...}" end -- defensive depth cap
|
||||||
|
local parts = {}
|
||||||
|
for _, k in ipairs(keys) do
|
||||||
|
local val
|
||||||
|
if _is_sensitive_key(k) then
|
||||||
|
val = "(set)"
|
||||||
|
else
|
||||||
|
val = _fmt_value(v[k], true, depth + 1)
|
||||||
|
end
|
||||||
|
parts[#parts + 1] = ("%s = %s"):format(k, val)
|
||||||
|
end
|
||||||
|
return "{" .. table.concat(parts, ", ") .. "}"
|
||||||
|
end
|
||||||
|
return "<" .. type(v) .. ">"
|
||||||
|
end
|
||||||
|
meta.config = function(args)
|
||||||
|
local sub = ((args or ""):match("^%s*(%S*)") or ""):lower()
|
||||||
|
if sub ~= "" and sub ~= "show" then
|
||||||
|
renderer.status("usage: :config show [full]")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local full = (args or ""):lower():match("show%s+full") ~= nil
|
||||||
|
local sources = config._sources
|
||||||
|
if sources then
|
||||||
|
-- Group by source for the "config sources" listing.
|
||||||
|
local user_keys, project_keys = {}, {}
|
||||||
|
for k, src in pairs(sources) do
|
||||||
|
if src == "project" then project_keys[#project_keys + 1] = k
|
||||||
|
else user_keys[#user_keys + 1] = k end
|
||||||
|
end
|
||||||
|
table.sort(user_keys); table.sort(project_keys)
|
||||||
|
renderer.status("config sources:")
|
||||||
|
io.write((" user: %s keys: %s\n"):format(
|
||||||
|
#user_keys, table.concat(user_keys, ", ")))
|
||||||
|
if #project_keys > 0 then
|
||||||
|
io.write((" project: %s keys: %s\n"):format(
|
||||||
|
#project_keys, table.concat(project_keys, ", ")))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
renderer.status("config sources: (unknown — main didn't pass _sources)")
|
||||||
|
end
|
||||||
|
renderer.status(full and "effective config (full, sensitive masked):"
|
||||||
|
or "effective config (top-level; ':config show full' for deep):")
|
||||||
|
local top_keys = {}
|
||||||
|
for k, _ in pairs(config) do
|
||||||
|
if k ~= "_sources" then top_keys[#top_keys + 1] = k end
|
||||||
|
end
|
||||||
|
table.sort(top_keys)
|
||||||
|
for _, k in ipairs(top_keys) do
|
||||||
|
local source_tag = sources and sources[k] or "?"
|
||||||
|
local val
|
||||||
|
if _is_sensitive_key(k) then
|
||||||
|
val = "(set)"
|
||||||
|
else
|
||||||
|
val = _fmt_value(config[k], full)
|
||||||
|
end
|
||||||
|
io.write((" %-20s = %s [%s]\n"):format(k, val, source_tag))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- R10: $%.6f for sub-cent precision (cloud costs can be ~3e-05).
|
-- R10: $%.6f for sub-cent precision (cloud costs can be ~3e-05).
|
||||||
-- R6: annotation uses the per-slot is_local sticky flag rather
|
-- R6: annotation uses the per-slot is_local sticky flag rather
|
||||||
-- than a fragile cost==0 heuristic.
|
-- than a fragile cost==0 heuristic.
|
||||||
|
|||||||
Reference in New Issue
Block a user