v0.10.0 — Security hardening + opt-in cloud models#3
Merged
Conversation
Reader executables (cat/head/tail/grep/rg/wc/file/stat/du) classified LOW auto-ran under the balanced profile even when a path argument resolved outside the workspace, a parallel ungated file-read channel that bypassed the boundary read_file/search enforce. Reuse the (renamed) _path_arg_outside_workspace boundary check for these reader execs so an out-of-workspace read escalates to HIGH (never AUTO). Marker-secret reads remain HIGH via the existing _touches_secret_path check; in-workspace reads are unchanged.
The prior test compared the full Live-streamed export_text() against a single static Markdown print; those can never be equal because rich Live records intermediate streaming frames (the leading partial token leaks into the recorded output). Rewrite it to assert on the markup handed to Markdown via a recording monkeypatch (the same pattern the ANSI-sanitization test uses): the final flush must hand the complete, un-corrupted fenced markdown to Markdown, and the rendered output shows the code block and bold text. Sanitization is a no-op on control-free markdown, so this proves B1 does not corrupt legitimate multi-line content.
The append-only JSONL audit log and session transcripts were created with open(path, "a"), yielding 0644 files and 0755 parent dirs under the default umask, while memory.json/overrides.json already use mkstemp (0600). These two files hold the highest-value at-rest data (full prompts/replies/tool args and output; every command/path/arg). Create them via os.open(..., O_WRONLY|O_CREAT |O_APPEND, 0o600) with parent dirs 0o700, matching the json_store reference. New files only; append semantics and redaction-before-write unchanged.
Network egress (tools.web, model.base_url) and the security posture (runtime.security_profile) could be set via the program-managed overrides.json and /config set, and base_url/profile via env vars, despite the invariant that such changes must be explicit config.toml acts (audit F7). Unify the per-key special-cases into one CONFIG_FILE_ONLY_KEYS set enforced at both the overrides loop and validate_override (messages byte-identical for model.options/skills.enabled), drop SHELLPILOT_OLLAMA_BASE_URL and SHELLPILOT_PROFILE from ENV_MAP, and mark runtime.security_profile boot-only. Also stop resolve_base_url() reading SHELLPILOT_OLLAMA_BASE_URL so clients built without an explicit base_url (e.g. doctor) cannot have their endpoint redirected by an ambient env var. DESIGN 17.4/17.5 updated.
Group B sanitized streamed text and command/tool output, but plan.goal and step.title (straight from the model's propose_plan/update_plan args) still rendered as bare Text in plan_panel/plan_step_line — so a hostile model could emit terminal control codes (e.g. \x1b[2J) to clear or spoof the plan and approval UI. Route both through the existing _sanitize_line helper. No new deps; mirrors the Group B sanitization.
_check_url validated only IP literals; a DNS name returned early WITHOUT resolution, so a public-looking host resolving to a private/loopback/metadata IP (DNS rebinding) bypassed the guard whose purpose is to block those (audit F11). Now resolve every name via getaddrinfo and run each resolved address through a single not-is_global predicate (also applied to IP literals; this additionally catches CGNAT 100.64.0.0/10, e.g. the Alibaba/Oracle metadata endpoint). Redirect hops already re-check, so they inherit validation. The connection is deliberately not IP-pinned (would break TLS SNI/cert for arbitrary HTTPS); the narrow resolve->reconnect rebind window is an accepted, documented residual on a single-user, approval-gated fetch.
Project (workspace) AGENTS.md was injected as authoritative "## Project instructions" with the same authority as ShellPilot's own system prompt, default-on, every turn, no consent — so cloning/running in an untrusted repo silently loaded attacker-authored standing instructions (audit F4). Gate the PROJECT AGENTS.md behind trust-on-first-use: its SHA-256 digest is recorded in .shellpilot/state.json; an unrecognized or changed file prompts the user (default No) before it is loaded, a non-TTY session fails closed (not loaded). The GLOBAL config-dir AGENTS.md stays trusted. workspace_state now read-merge-writes so the trust digest and last_model coexist. DESIGN 16.2.1.
With a remote model endpoint the whole prompt egresses to a provider, yet redaction ran only on disk sinks (F3), nothing recorded that a request was sent or to where (F10/F12), and the web tools' query/url egress was unaudited. Add an egress-locality signal to the runtime (non-loopback base_url; a hook is left for Part 2's cloud-model names) and, on egressing turns only: send a best-effort redacted COPY of the messages (never mutating history; images and novel-format secrets still egress, disclosed) and write a body-free model_request audit event (host/model/counts). A loopback turn is byte- identical. The executor writes a redacted web_egress event for NETWORK tools after all gates pass. Best-effort DiD, not a guarantee. DESIGN 15.1/22.
…gate A cloud/remote model sends the entire prompt off-device, so this gates that egress on explicit, fail-closed consent. Add is_cloud_model + a master [model] allow_cloud switch (default off, config-file-only + boot-only, never env/override/set-reachable). A model is egressing when it is a -cloud name OR the base_url is non-loopback (shared is_loopback_url, fail-closed on unparseable/no-host URLs, used by both the consent gate and the Group-E redaction trigger so they cannot drift). Before ANY model load/chat, an egressing session must clear allow_cloud and a per-session y/N (default No) consent prompt that honestly states the whole prompt egresses possibly unredacted; a non-TTY session is refused. Both boot and /model use are gated (no preload/switch on refusal); the local availability gate is skipped for cloud names. cloud_consent_granted is audited. The base system prompt drops its false 'entirely on this machine / no network access' claim when egressing (local prompt byte-identical; PROMPT_VERSION 4->5). Default localhost users are completely unaffected. DESIGN 15.2/17/22.
v0.10.0 makes cloud models possible, so the README's unconditional 'nothing leaves your machine' framing is now 'local-first by default': nothing leaves the box unless the user opts into cloud via config-file-only [model] allow_cloud and clears a per-session consent prompt. Adds a 'Cloud models (opt-in)' section (off by default, the consent flow, the honest whole-prompt-egress disclosure with best-effort-not-guaranteed redaction, the cloud_consent_granted/model_request audit events, the /model use gate) and moves opt-in cloud from later-candidates into the v0.10.0 milestone.
…rkflow skills Four builtin workflow skills that consume the v0.9.0 progressive-disclosure mechanism: each has a lean ENABLED-triggered SKILL.md body (~175-185 tokens) that routes by name, via skill_read, to deeper on-demand references/ docs (not injected until read). Opt-in via [skills] enabled, so a default session is byte-identical. git-workflow's risk claims match the command classifier (reset/clean/branch-delete/force-push HIGH, plain push MEDIUM). Adds a guard test against the fabricated-tool-name class (the search tool is search_text). DESIGN 23.4.
Swap the boot-time banner at run_interactive for the new panel renderer (banner.py:render_banner), passing the boot-time egressing_session so the model name reads green for a local session and amber for a cloud/remote one. The old render.banner had no other callers, so it and its test are deleted as dead code (the profile it showed remains in the context line and /status).
First boot in a workspace still shows the full numbered menu; every boot after shows a compact 'Last flight: <model>' confirm where Enter (or EOF / Ctrl-C) flies the last model and any other key opens the full menu. Adds the pure confirm_last_model(console, last_model) helper with the console injected for testability. The chosen model flows unchanged into the availability and cloud-egress consent gates.
Extract the cloud/remote egress predicate into is_egressing(model, base_url) in config/model.py (one source of truth) and reuse it at the boot consent gate, the runtime egress chokepoint, and /model use. Add three unspoofable indicators derived from the LIVE egress signal: the boot banner already colors the model name, a persistent amber 'CLOUD MODEL ACTIVE' header bar above the prompt each REPL iteration (so mid-session /model use flips it), and a /status 'Locality:' line (REMOTE host vs local). A default local session is visually unchanged.
…ionals Expand the v0.10.0 roadmap bullet to cover all shipped features: security hardening pass (policy, terminal sanitization, DNS guard, file modes, config-key hardening, AGENTS.md TOFU), opt-in cloud with consent gate and active-cloud indicator, four workflow skills, boot banner, streamlined picker, and approval diff scrolling reveal. Remove "richer workflow skills" from the Later candidates line — they shipped in v0.10.0. Add the four opt-in workflow skills (debugging, verification, code-review, git-workflow) to the Skills row in the Capabilities table.
…ational requests The chokepoint claim that every model request passes through the conversation.py tool loop was inaccurate: /memory compact (SlashDispatcher._memory_compact) calls the Ollama client directly, bypassing the model_request audit event and the outbound-redaction pass. Scope the claim to every conversational model request and add an accepted-residual note: per-session consent already covers the call and the content is stored preferences only (never history/files/output); the open gap is audit-completeness, with routing _memory_compact through the chokepoint a planned hardening follow-up. Note /compact is unaffected (no model call).
- Header status/date -> through v0.10.0, 2026-06-21. - 1 Purpose / 2 Original Repo Baseline: bump trailing "through v0.8.0" current-state references to v0.10.0; qualify "local-only through Ollama" as local-first with opt-in cloud (15.2). - 5.1 Local First: qualify "all model calls through local Ollama" as by-default; cloud/remote is opt-in, off by default, gated by [model] allow_cloud + per-session consent (15.2). - 15 privacy preamble: qualify "No cloud model calls" to by-default, with consent + indicator + egress audit as the boundary. - 23.4 workflow-skills label v0.9.x -> v0.10.0 (shipped this release). - New section 36 Security Hardening: documents the red-team-audited pass at category level (no exploit mechanics) under the threat-model framing of machine-from-model (existing safety) vs user-data-from- provider (cloud egress): classifier read-path boundary, terminal output sanitization, config-file-only egress invariant, AGENTS.md TOFU, egress chokepoint + audit, web_fetch SSRF resolve-and-validate, 0600 file-permission parity, and the cloud consent triad. Each claim verified against the committed implementation. Section 31.4 already carries the diff-reveal description (be26e96) and is unchanged.
is_cloud_model used name.endswith("-cloud"), which matched sized cloud
variants (gemma4:31b-cloud) but missed the un-sized ':cloud' tag form
(nemotron-3-super:cloud). On a loopback Ollama endpoint is_cloud_model is
the only egress signal, so a ':cloud' model was classed local: no consent
gate, no active-cloud indicator, no model_request audit, no outbound
redaction -- a silent egress, exactly the hole v0.10.0 closes.
Match the cloud tag on the segment after the final ':' (== "cloud" or
ending "-cloud"), covering both Ollama forms and failing toward cloud.
Update DESIGN 15.1/15.2/36.5 and the docstrings to describe both tag forms.
Found during v0.10.0 live-acceptance prep against the installed cloud models.
… symmetry Replace the command-only two-column banner with a bounded-width panel split by a full-height vertical divider: left column centers the welcome line, jet, model name, and a "<profile> · <local|cloud>" sub-line; right column stacks Commands / Tips / Workflow skills / Recent sessions, separated by dim rules. The Workflow skills section shows enabled skills or, when none, the available builtins plus a "/skills to enable" hint; Recent sessions is omitted when empty. Fix the live-render jet asymmetry: the old per-line justify="center" skewed narrow rows because Rich strips trailing whitespace. The jet is now one fixed-width (28-cell) block centered as a unit via Align.center. A regression test pins per-row symmetry (fails against the old implementation). Wire real data via render_banner(model, *, is_cloud, profile, skills, recent_sessions): profile from settings.runtime.security_profile, skills from settings.skills.enabled, recent sessions from a new SessionStore.recent() (label = truncated first user message, falling back to the model name) mapped to relative ages by _relative_age(). Recent list is captured before the current session's meta is written so it never lists itself. DESIGN §31.10 updated.
'!<cmd>' runs a single command through the existing audited manual-shell path (raw shell=True, same as /shell); a bare '!' opens the shell loop. The banner already advertised '! <cmd>' but no handler existed, so '!' lines fell through to the model as prompts. This is a human-only escape (model output never reaches the prompt reader), carrying the same trust as /shell. Test: a '!' line routes to run_manual_command and is never sent to the model.
The Recent sessions label is a snippet of a past session's first user message — untrusted, possibly-pasted input persisted verbatim (secret- redacted only, not control-stripped). It was appended to the banner Text unsanitized, so a stored ANSI/control sequence (e.g. clear-screen) could repaint the terminal on next boot. Route the label through _sanitize_line at the render sink (Group B). Test: an ESC in a recent-session label is stripped before render.
…ep env blocked Split the locked config set: CONFIG_FILE_ONLY_KEYS now holds only the two structural keys (model.options, skills.enabled), and a new HIGH_STAKES_KEYS holds the four egress/safety keys (tools.web, model.base_url, runtime.security_profile, model.allow_cloud). The four egress keys are now settable at runtime via /config set, gated by an amber warning + explicit confirm (per-key risk, boot-only "next session" note, "persists until /config unset" note; runtime.security_profile also points at /profile use for a session-only change). A decline saves nothing. They flow through the overrides layer normally (source "set"); validate_override no longer rejects them. The two structural keys still reject in both chokepoints. The env invariant is preserved and load-bearing: all four keys stay absent from ENV_MAP — no ambient env var can enable egress or downgrade the profile. The per-session cloud-consent gate is unchanged and remains the egress barrier. A boot notice (high_stakes_override_notice, a pure testable helper) lists any active high-stakes override on every launch, so a set-and-forget egress override can't quietly stay on. DESIGN updated in-commit: rewrote the two-tier setter invariant (§17.3, §36.3), the /config set high-stakes doc (§17.3), the allow_cloud bullet (§15.2), the consent-triad control 1 (§36.8), §5.1, the env-section and metadata-probe notes. The env-absence claim stays true everywhere.
The relax-config change warned that every high-stakes /config set "takes
effect next session," but runtime.security_profile is read per turn
(conversation.py) and update_settings propagates it — so the safety downgrade
applies immediately, the same live effect as /profile use. The warning (and the
boot-only note) falsely deferred a live safety change to next session.
Remove runtime.security_profile from BOOT_ONLY_KEYS (it was misplaced — the set
means "construction-time only," which it is not), key the /config set when-note
off BOOT_ONLY_KEYS membership ("this session (next turn)" vs "next session"),
reword the /profile hint to "unsaved, session-only," and stop the boot-only note
double-printing for high-stakes keys. The three egress keys (tools.web,
model.base_url, model.allow_cloud) stay boot-only and the env-absence invariant
is unchanged for all four.
DESIGN updated in-commit (§17.3 setter notes, §36.3, the high-stakes key list).
Found by the post-relax security review.
One exit command instead of two — /quit is no longer recognized (falls through to the unknown-command hint pointing at /help). Updated HELP_ROWS, the command table in README/DESIGN, and the completion-word + dispatch tests. Also hoist the three function-local `from shellpilot.config.model import ...` re-imports (is_cloud_model/is_egressing/is_tested_model/TESTED_FAMILIES) to a single module-level import — config.model is already in slash.py's import graph via runtime.conversation, so there's no cycle. Net deletion, and it clears the duplicate-import that Pyright flagged as a Final reassignment.
… alias /prefs had converged with /memory: /prefs edit only printed the store paths (/memory show already prints them) and /prefs show was a preferences-only slice of /memory show (which already lists prefs and facts with their ids). Retire /prefs and fold its one extra detail — the per-preference (scope, source) tag — into /memory show via a keyword-only render(meta=True). The injected memory prompt format is unchanged (meta defaults False), so the model-facing prompt is byte-identical. Also demote /config reset <key>: it merely aliased /config unset <key>. /config reset <key> now falls to the usage hint; /config unset <key> is the one way to remove a key, and /config reset (no key) still clears all. HELP_ROWS, README, and the DESIGN command tables updated to match. Tests updated red-first (render meta display-vs-injected, /memory show carries the tag + /prefs gone, /config reset <key> no longer unsets).
The hardening changed behavior but README/DESIGN still described the old model:
- pytest is now MEDIUM/ASK (Group A dropped it from LOW_EXECUTABLES + removed the
-m pytest carve-out), but README L135 + a transcript + DESIGN §11.1/§14.3 still
showed it auto-running under balanced. Fixed to genuinely-LOW examples
(ls, git status, in-workspace cat) and a MEDIUM approval prompt in the transcript.
- SHELLPILOT_* env vars are now only model/color/glyphs (Group C dropped the
Ollama-URL + profile env overrides); README L207 list corrected.
- tools.web / model.base_url / runtime.security_profile / model.allow_cloud are
HIGH_STAKES — settable via config.toml OR a confirm-gated /config set (persisting
through overrides.json), never env. README L234/L246/L262 said they were
config-file-only and not /config-set-able; corrected, and the two tiers
(config-file-only {model.options, skills.enabled} vs high-stakes) now described
accurately. README now agrees with the in-commit DESIGN §17.3/§36 updates.
Docs-only; verified against command_policy.py / loader.py / slash.py.
A changed line emitted a separate removal row and addition row (correct), but
_diff_row colored only "{marker} {content}" — so a short removal's red bar
stopped at its text while the longer addition's green bar ran further, and with
both sharing a gutter number the pair didn't read as two distinct unified-diff
lines (violating DESIGN §31.4's "full-line backgrounds").
Compute pad_width (widest changed-line content via rich.cells.cell_len) and
right-pad each backgrounded row's content INSIDE its style so removals and
additions render as uniform full-width bars: old text on its own red row, new
text on the next green row. Fix lives in the shared _diff_rows, so render_diff
and the streaming DiffReveal stay consistent. Headers (---/+++), context,
"\ No newline", and the truncation markers stay un-padded; the row-counting cap
is unaffected.
TDD red-first (test_render_diff_changed_lines_are_separate_full_width_bars,
fails 17==26 before, passes after). DESIGN §31.4 clarified in-commit.
Previously /config edit printed a path to a config.toml that doesn't exist by
default (the config dir only holds overrides.json/memory/audit), which is
confusing and useless. Now, when the user config is absent, /config edit writes
a commented starter template at that path (then prints it + boot-only vs
/config-set guidance); when it exists, it only prints the path — never rewrites.
Safety: the create path is doubly guarded — an exists() check AND os.O_EXCL — so
an existing config.toml is never touched (the project never rewrites config.toml).
The template is entirely commented out, so it loads to pure defaults with zero
warnings (config.toml errors are fatal, so a valid-empty template is required);
every commented example value is valid if uncommented. Written 0600. Comments
honestly describe the egress/safety tiers (config-file-only {model.options,
skills.enabled} vs high-stakes {tools.web, model.base_url, runtime.security_profile,
model.allow_cloud} — config.toml or confirm-gated /config set, never env).
TDD red-first (creates-starter, 0600 perms, does-not-overwrite). DESIGN §17.2
documents the behavior.
…del arg The action path was always bounded by resolve_in_workspace, but the path shown to the user (tool-call line, approval head + audit, write/patch diff title) was the RAW model argument — so a model could make the display read as one file while a different in-workspace file is actually touched, spoofing the approval. Add workspace_display(workspace, raw_path) in tools/base.py next to resolve_in_workspace; it resolves via the SAME resolver and returns the workspace-relative path (or "<outside workspace>" on a boundary escape). Route the three approval-time displays through it (executor _display_for/_display_value, terminal show_tool_call, patch.unified_diff title via its callers) so display == action by construction. The boundary resolution itself is byte-for-byte unchanged — this is display-only. Escapes render the honest marker, never a fabricated path; all sinks remain Group-B sanitized. TDD red-first (spoof arg -> resolved display; escape -> marker), incl. a subdirectory-target preview test so resolved-relative != basename genuinely guards the header. DESIGN §14.5 + new §36.9.
…provals Approval prompts are now [y]es / [e]dit / [n]o. Choosing [e] asks "Tell the model what to do instead:", drops the proposed action WITHOUT running it, and feeds the guidance back as the tool result so the model proposes a corrected action — which re-enters the normal classify->decide->approve gate like any tool call. This is reject-and-steer (akin to /plan revise), not inline-edit-and-run: the user never edits a command that then executes under the badge it was approved under. Seam: ApprovalAsker now returns ApprovalReply(approved, steer_text) instead of bool (new frozen type + APPROVE/DECLINE singletons in policy/approvals.py). The sole consumer (executor) checks reply.approved explicitly — no object-truthiness path, so nothing runs on decline or steer (proven red-first: a steered rm -rf deletes the decoy against the old bool code, survives now). High-risk commands still require typing the literal "run" (a plain y is rejected; [e] steers without running). Empty guidance after [e] = plain decline. Steered approvals audit as decision="steered". TDD red-first (8 tests across executor/terminal/conversation). DESIGN §14.6 + §31.5. Verified with a full truthiness-trap sweep.
…ntext % Add a one-line status bar pinned at the input via prompt_toolkit's bottom_toolbar: `~/Projects · gemma4:e4b · balanced · ● local 12% ctx`. Model + locality go green when local, amber (`☁ CLOUD`, bold, with an amber chevron + separator wash) when egressing; context % is color-coded green<50 / amber 50-79 / red>=80. This folds the active-cloud indicator into one always-on place: the separate amber "CLOUD MODEL ACTIVE" header bar is removed, and context utilization moves off the per-response line (turn_finished is now a no-op; the runtime still emits TurnStats for the protocol) into the bar. The indicator stays unspoofable and harness-rendered — is_cloud is is_egressing(live model, base_url) recomputed each prompt; dir/model/profile/ctx come from runtime.status(), never model output; the user-controlled workspace path is sanitized before render. New pure builder shellpilot/cli/status_bar.py (mirrors banner.py) with ctx_percent() reusing the runtime's exact formula. Non-TTY PlainInput unchanged (and a non-TTY session can't egress — the consent gate refuses it). TDD red-first (test_status_bar.py: local-no-amber, cloud-amber+CLOUD, ctx thresholds, separators, home-abbrev, control-char sanitization); removed the turn_stats render test for the deleted per-turn line. DESIGN §31.11 (new) + §31.2/§31.8/§15.2/§36.8. Queued input / type-ahead (prototype State C) is deferred to its own pass.
Correct README/DESIGN drift from the v0.10.0 work: - README session transcript: panel-banner note, [y]es/[e]dit/[n]o approval prompt, drop the removed per-turn stats line, status bar shows locality + ctx%; document the ! one-shot shell escape; qualify project vs global AGENTS.md trust; refresh the v0.10.0 changelog + add a v0.10.1 roadmap note. - DESIGN: §20.1 Commands (+ /config set/unset/reset rows), §21 ! escape note, §33.4 rewrite the DNS-rebind limitation to the real residual (resolve-and- validate shipped; no IP pin — §36.6), §36.3 note SHELLPILOT_PROFILE removal. - Normalize a few internal comment markers and UI-analogy phrasing. Docs/comments only; phase gate green.
5fe27df to
d16bf70
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v0.10.0 — Security hardening + opt-in cloud models
A combined release: a red-team-audited security hardening pass, opt-in cloud models built on the hardened base, four opt-in workflow skills, and a UI/UX overhaul.
Opt-in cloud models (off by default)
Local-first stays the default and the only full-privacy posture. Enabling a cloud/remote model is a deliberate, security-gated act:
[model] allow_cloud(defaultfalse). It has no env override — an ambient env var can never enable egress. It is settable only by editingconfig.tomlor via a confirm-gated/config set(amber warning + explicit confirm, persisted tooverrides.json); the per-session consent gate below is the real egress boundary regardless of how it was set.--model,/model use); a non-TTY session refuses with no network call./statuslocality — all derived from the live model, never from model output. Cloud detection matches the Ollama cloud tag in bothname:cloudandname-cloudforms.cloud_consent_grantedandmodel_requestegress audit events (host/model/counts only — never message bodies).Security hardening pass
run_commandreader path now honours the workspace boundary and sensitive-read gate.model.options,skills.enabled) are config-file-only; egress/safety keys (tools.web,model.base_url,runtime.security_profile,model.allow_cloud) are high-stakes — settable viaconfig.tomlor a confirm-gated/config set, never via an env var. A boot notice lists any active high-stakes override.resolve_in_workspace), so a model can't spoof an approval with a misleading raw argument.AGENTS.mdgated behind trust-on-first-use (re-prompts on content change).web_fetch(resolve-and-validate every address, every redirect hop).Approvals & control
[y]es / [e]dit / [n]o.[e]drops the proposed action without running it and feeds your instruction back to the model, which re-proposes a corrected action that re-enters the full classify→decide→approve gate. High-risk commands still require typing the literalrun.Workflow skills
Four opt-in builtins via progressive disclosure (
skill_read):debugging,verification,code-review,git-workflow.UI / UX
… (+N more)footer.!runs a one-off shell command (or!alone enters Manual Shell);/config editwrites a commented starterconfig.tomlwhen none exists (never overwrites an existing one; loads to defaults)./exit);/prefsfolded into/memory show(which now shows each preference's scope/source); the/config reset <key>alias removed.Docs
README and DESIGN corrected so user-facing claims match shipped behavior (
pytestnow asks underbalanced; theSHELLPILOT_*env-var list; the egress-key tiers).Validation
ruff+ruff format+mypy --strict+ 1157 tests. CI green on Python 3.11 and 3.14./status local, a turn) and gemma4:31b-cloud cloud path (consent-before-egress, indicator,/status REMOTE, a response, full audit trail);allow_cloud=falseand non-TTY refusals verified fail-closed.Design of record:
docs/DESIGN.md§14 (approvals), §15 (cloud/egress consent), §17 (config), §31 (visual design), §32 (model picker), §36 (security hardening).