feat: config trust boundary, honest command feedback, json print mode - #46
Open
mike-diff wants to merge 4 commits into
Open
feat: config trust boundary, honest command feedback, json print mode#46mike-diff wants to merge 4 commits into
mike-diff wants to merge 4 commits into
Conversation
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.
Three related changes, one per commit, each grounded in experiments run in
this repository against the real binary. The through-line: things a checked-out
repo or a raw exit code could make sesh say or send are now made honest.
1. Project config cannot steer the brain (
fix(providers))The hole, measured
A scratch repo whose
.sesh/providers.jsonnames a different default, runagainst a local listener with a clean HOME:
The whole conversation is silently routed to whatever URL the repo names. The
exit 1 is irrelevant: the prompt left on the first POST, and the attacker
needed no key. Two more paths shared the hole:
brief_providerresolvesthrough the project-overlaid config (repl.go briefWriter), so a project
tuning.jsonroutes handoff briefs, transcripts included, the same way; and afresh session adopts its brain BY NAME from the last session in the directory,
so a project file redefining a trusted global profile name poisons every future
explicit
-providerin that directory.This contradicted the repo's own rules three times over: tool mods mount
global-only because "a project-local one in a repo you just cloned would be
someone else's code"; the project
mcp.jsonoverlay may select but neverdefine; and "a project providers.json is safe to commit" is true for keys,
not for routing.
The rule
Enforced in the parser, with loud stderr notes for every refusal:
stays one flag away:
-provider company-gwworks exactly as before)defaulttuning.jsonmay not setbrief_provider/brief_model;every other dial keeps normal layering (thresholds are repo-safe)
/provider add,/provider remove, and the global file are untouched.Honest cost: a team whose members relied on a project default now passes one
flag. That is the boundary working, not a regression to paper over.
Proof
The exfiltration run, repeated against the fixed binary:
2. Honest command feedback (
feat(tools)+feat(bash))Secret masking, ordered before anything is kept
bash env,cat .env, a git remote printed with its embedded token shippedvalues to the provider verbatim; the README's standing answer was "bash
remains a hole." Masking now runs inside
shape(), FIRST: before thehead/tail split and before the spill write, so the model's shaped copy AND the
spilled file in
~/.sesh/out/both hold the masked text. Masking after thespill would persist the secret; masking only the model copy leaves the
read-back path leaky.
Pattern discipline, chosen so ordinary output survives:
(password, passwd, api_key, apikey, private_key, access_key) match as
substrings; the bare words token and secret must be a whole underscore
segment, so
GITHUB_TOKENmatches whiletokenizer_pathdoes not. Barekeyis never matched, soPATHandMONKEY_HOMEsurvive untouched.API_KEY="[redacted]", notAPI_KEY=[redacted]sk-,ghp_/gho_/ghu_/github_pat_,AKIAxox*) masked wherever they appear, with anchors that do not eatprose ("the sk-fork of the repo" survives)
true value, only to know one was there.
Scope is automatic:
shaped()wraps exactly bash, the engines, and tool mods,so masking reaches arbitrary-output tools and nothing else.
read/searchresults are untouched: file content the user pointed the agent at is a
different, deliberate act. Dial:
result_mask_off(inverted default-on, likeresult_spill_off), because a false positive that breaks a legitimateworkflow must be turnable off without a recompile.
Benign exit notes
grep -q needle fileexits 1 when nothing matched: that is the commandANSWERING, not failing. Both bash paths returned the bare
exit status 1,the model read failure, and the classic flail followed: pointless re-runs,
tool switching, breakage reports for things that never broke.
Now a small table appends what exit 1 MEANS for that program (grep, diff,
cmp, test,
[, pgrep), while the result stays an error result. The noteteaches semantics without lying about status; exit 2 and up stay bare.
3.
-p -json(feat(print))Print mode already had an exit-code contract and a clean stdout; this adds
its machine-readable twin, the "third set of hooks" AGENTS.md anticipated:
{"reply":"...","exit_code":0,"outcome":"done","provider":"local","model":"m", "session":"...","iterations":2,"tool_calls":14,"mutations":3, "usage":{"input":12000,"output":3400,"cache_read":800}}One line on stdout in every outcome, failures included: a failed run emits
{"outcome":"error","error":"..."}rather than dying to stderr, so a pipestays parseable no matter how the run ended.
-jsonwithout-pis refusedwith usage.
blockedshares exit 0 withdonein text mode, so theoutcomefield is the only way to tell them apart; that is why it exists.Progress notices stay on stderr (stdout purity is the invariant; stderr is
the human channel).
Tests
Every test was run against a surgically reverted behavior to confirm it
fails. Eleven breakers across the three changes:
Two pre-existing tests asserted the old behavior and were updated with
breaker comments rather than deleted:
TestTuningBriefDials(its projectlayer case asserted the routing hole) and
TestProvidersOverlay(its commentdescribed the project overlay; the primitive is now the global merge).
Three test-written tests caught three real bugs during development, which is
the rule working: quotes were initially swallowed by the mask;
tokenizer_pathwas initially masked by substring matching; and the firstkey-rule test could not fail because a
ghp_value is masked by the tokenshapes independently, so it was strengthened with a value no shape catches.
Verification
gofmtclean,go vet ./...clean,go test -race ./...green, fullSESH_E2E=1suite green, on this branch as assembled. E2E coverage includesthe poisoned-config scenario (the run proceeds on the user's own default,
both refusals loud, and the model that served is the mock's, not the poisoned
name's) and both JSON scenarios (success envelope naming the serving brain;
failure still parseable).
Real-binary smoke:
-jsonwithout-pexits 2 with usage; a no-provider runemits a parseable error envelope.
Non-goals
read/searchresults (deliberate scope, stated boundary)./providerflows or the global config file.