Skip to content

feat(bin): add guarded settings inspection tool - #2

Merged
Profreshor merged 17 commits into
mainfrom
fm/fm-secret-guard
Sep 25, 2026
Merged

Profreshor merged 17 commits into
mainfrom
fm/fm-secret-guard

Conversation

@Profreshor

@Profreshor Profreshor commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Intent

That's two secrets exposed in under 24 hours, Firstmate. This is becoming a problem. How do we make this stop happening? Is this an AGENTS.md addition or what? I can't have this keep happening...

Asked whether to build a settings tool that becomes the only way workers touch settings (list names, check presence, run a command with only named settings injected and every secret value scrubbed from its output): "Yes"

Context: two real leaks on 2026-09-24. (1) TypeSafe API keys were printed while a worker read the running warner-api/warner-scheduler process environment with a redaction predicate that was too narrow. (2) The ATS production database URL (postgres://user:password@host form) was printed while a worker dumped ~/warner-analytics/.env through a sed redaction filter that missed a value line with leading whitespace. Workers and those services all run as the same OS user, so agents can read both .env files and /proc//environ. A written no-secrets rule in the standing worker instructions already existed and did not stop either leak.

What Changed

  • Add fm-secrets.sh and its Python implementation for listing setting names, checking file or systemd service setting presence, and running commands with named settings injected and output scrubbed.
  • Add the guarded settings-tool rule to worker briefs, forbidding direct settings-file, process-environment, and systemd environment reads.
  • Document the new tool and cover its parsing, isolation, service-inspection, and redaction behavior with shell tests.

Risk Assessment

✅ Low: The final wildcard-order repair is minimal and the broader settings boundary has no additional source-verifiable defect in the reviewed call paths.

Testing

Live CLI checks demonstrated names/booleans only, selected-environment isolation, URL and short-secret redaction, exit-status preservation, cross-stream redaction, terminal isolation, conservative real-service presence, and emitted worker guidance. Focused behavior coverage passed for the stopped-service fallback; a controlled real stopped-systemd-unit proof was not permitted. This is CLI/Markdown behavior, so rendered UI evidence does not apply.

  • Live validation: ✅ go - 5 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Inspect settings without exposing values ✅ pass live Live fm-secrets CLI transcript
Run with only selected settings and redact output ✅ pass live Live fm-secrets CLI transcript
Block terminal and cross-stream secret-output bypasses ✅ pass live Live terminal-isolation and service transcript
Check a real service without exposing environment values ✅ pass live Live terminal-isolation and service transcript
Generated worker briefs require the settings boundary ✅ pass live Generated worker settings rule
Stopped-service EnvironmentFile precedence and fail-closed behavior ⏸️ untested no A real stopped unit with controlled EnvironmentFile and UnsetEnvironment directives would require creating or managing systemd state outside this worktree. Provide an isolated disposable systemd lab o…
Evidence: Live fm-secrets CLI transcript

Source: Live fm-secrets CLI transcript

fm-secrets live CLI transcript (synthetic values; output is scrubbed)
$ fm-secrets.sh names settings.env
DATABASE_URL
TYPESAFE_API_KEY
OTP
PUBLIC_VALUE
$ fm-secrets.sh has settings.env DATABASE_URL OTP MISSING
DATABASE_URL=yes
OTP=yes
MISSING=no
$ AMBIENT_SECRET=... fm-secrets.sh run settings.env --only DATABASE_URL,TYPESAFE_API_KEY,OTP -- sh -c ...
db=<redacted:DATABASE_URL> api=<redacted:TYPESAFE_API_KEY> otp=<redacted:OTP> ambient=unset public=unset
decoded-password=<redacted:DATABASE_URL>
child exit status=37
$ fm-secrets.sh run settings.env --only TYPESAFE_API_KEY -- sh -c split streams
<redacted:TYPESAFE_API_KEY>
Evidence: Live terminal-isolation and service transcript

Source: Live terminal-isolation and service transcript

fm-secrets live isolation/service transcript (synthetic value; output is scrubbed)
$ fm-secrets.sh run … -- sh -c "printf %s \"$TOKEN\" > /dev/tty; printf %s \"$TOKEN\" >&0" (inside a pty)
sh: 1: cannot create /dev/tty: No such device or address
terminal-isolation exit status=0
$ fm-secrets.sh has --service dbus.service PATH DOES_NOT_EXIST
PATH=unknown
DOES_NOT_EXIST=unknown
Evidence: Generated worker settings rule

Source: Generated worker settings rule

7. Use `'~/.no-mistakes/worktrees/aa96bb817995/01M3CQDG0EGPW464G4FNT1VMJT/bin/fm-secrets.sh'` for every settings file or service environment; its `--help` owns the safe operations and limits.
   Never use `cat`, `sed`, `nl`, or `grep` to read settings values, never read `/proc/*/environ`, and never run `systemctl show Environment` directly.
Evidence: Generated scout brief

Source: Generated scout brief

You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
## Captain's intent
{TASK}

## Firstmate spec
{FIRSTMATE_SPEC}

# Herdr lifecycle declaration - NOT ENABLED
**HARD SAFETY GATE:** this scaffold cannot inspect the task text filled in above.
If the task will start, stop, delete, restart, profile, or otherwise drive Herdr lifecycle behavior, stop and regenerate the brief with `--herdr-lab` before dispatch.
Do not add Herdr lifecycle commands to this unguarded brief by hand.

# Setup
You are in a disposable git worktree of alpha, at a detached HEAD on a clean default branch.
This is a SCOUT task: the deliverable is a written report, not a PR.
The worktree is your laboratory - install, run, edit, and make scratch commits freely; all of it is discarded at teardown.
The report is the only thing that survives, so anything worth keeping must be in it.

# Rules
1. Never push to any remote and never open a PR.
2. Stay inside this worktree; the only files you may write outside it are the report and the status file below.
3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
4. Report status by appending one line:
   `echo "{state} [at=<epoch>]: {one short line}" >> '~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/state/live-settings-worker.status' && { [ ! -e '~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/config/fleet-ledger' ] || '~/.no-mistakes/worktrees/aa96bb817995/01M3CQDG0EGPW464G4FNT1VMJT/bin/fm-fleet-ledger.sh' appended '~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/config' '~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/state/live-settings-worker.status' >/dev/null 2>&1 || true; }`
   States: working, needs-decision, blocked, paused, done, failed.
   Substitute `<epoch>` with the current Unix time in seconds - run `date +%s` and write the number it printed; a stamp that is not plain digits records no time at all.
   Each append wakes firstmate, so report sparingly: only phase changes a supervisor
   would act on and the needs-decision/blocked/paused/done/failed states. No step-by-step
   FYI progress lines; firstmate reads your pane for that.
   Whenever you mention a PR anywhere - a status line, your terminal, a summary - write its full
   https:// URL exactly as the forge printed it, never a bare number such as "PR 108"; firstmate
   copies that URL from your line rather than assembling one.
   Use `paused: {why}` - distinct from `blocked:` - ONLY when you are deliberately idling on a
   known external wait you expect to clear on its own (an upstream release, a rate-limit reset, a scheduled window, or your own validation round):
   firstmate then leaves your idle pane alone and rechecks it on a long cadence instead of
   treating it as a possible wedge. When you know when the wait clears, say so in the line with
   `until <YYYY-MM-DDTHH:MMZ>` (UTC) and firstmate rechecks at that time instead.
   Use `blocked:` when you are stuck and need help.
5. If you hit the same obstacle twice, append `blocked [at=<epoch>]: {why}` and stop; firstmate will help.
6. If a decision belongs to a human (product choices, destructive actions),
   append `needs-decision [at=<epoch>]: {summary of options}` and stop. Firstmate will reply with the decision.
   A decision or blocker you opened stays open until a `resolved` line carrying its exact key lands; a later `done:` or `working:` line never closes it, even when the answer is what started that work.
   Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append `resolved [at=<epoch>]: {how it cleared}` yourself (same `[key=<slug>]` if you opened it with one) as you resume.
7. Use `'~/.no-mistakes/worktrees/aa96bb817995/01M3CQDG0EGPW464G4FNT1VMJT/bin/fm-secrets.sh'` for every settings file or service environment; its `--help` owns the safe operations and limits.
   Never use `cat`, `sed`, `nl`, or `grep` to read settings values, never read `/proc/*/environ`, and never run `systemctl show Environment` directly.
8. Never administer infrastructure that every lane shares. Two things are shared:
   - The `no-mistakes` daemon - one instance serving every lane/home, so stopping, restarting, or
     updating it kills other lanes' in-flight pipeline runs; only firstmate manages the daemon.
     Before you append `blocked:` about the pipeline, run `no-mistakes daemon status` and
     `no-mistakes axi status`. If the daemon socket refuses connections or is missing, append
     `blocked [at=<epoch>]: {the daemon error}` and stop even when the local run record still says running or
     fixing, because that record can be stale after the daemon exits. A run record failed with a
     daemon error is also a real block.
     Only after ruling out socket refusal, if the run is still running or fixing, reattach and keep
     going. A drive-call error, timeout, slow read, or generic unreachability is NOT a daemon error:
     the daemon accepts `respond` immediately and runs the round in the background, so a killed or
     timed-out call was only waiting for a read while the run kept working.
   - The worktree pool your own worktree came from, and the repository every lane's worktree
     shares. Never create, remove, return, prune, move, or reassign a worktree or pool slot, and
     never write into a sibling slot's directory. Rule 2 does not cover this: removing a worktree
     is administration rather than an edit outside your directory, and it lands on lanes that are
     running right now. The act is the rule and commands are only examples of it - `treehouse`
     get/return/remove/prune, the equivalent operations on any other worktree provider or runtime
     backend, and `git worktree add|remove|move|prune`. A slot that looks unused is not evidence
     that it is free, and returning your own worktree is firstmate's job at cleanup, not yours.
   If you genuinely need a second checkout, another slot, or the daemon touched, append
   `blocked [at=<epoch>]: {what you need}` and stop; firstmate arranges it.

# Firstmate instruction inbox
Firstmate steers you through durable message files in '~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/state/live-settings-worker.inbox'.
When a terminal message says an instruction is waiting there - and at any natural checkpoint when you are unsure - list '~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/state/live-settings-worker.inbox'/*.msg, read and act on each message in numeric order, then acknowledge each handled message by moving it: `mv '~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/state/live-settings-worker.inbox'/NNN.msg '~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/state/live-settings-worker.inbox'/handled/`.
The move IS the acknowledgement: without it firstmate rings again and eventually treats you as stuck. An empty or absent inbox needs no action.

# Definition of done
Write your findings to `~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home/data/live-settings-worker/report.md`.
The report must stand alone: what you did, what you found, the evidence (commands run, output, file:line references), and what you recommend.
If your deliverable is a visual artifact the captain will review and iterate on, use the lavish-axi rule: arm your board with bin/fm-procevent-lavish.sh arm <artifact.html> --for <task-id>; never run lavish-axi poll yourself. Re-arm with the reply after each nonterminal round to acknowledge it, route the board feedback through your steering inbox, write needs-decision [key=board-review] with the live board URL when the captain owes a decision, and stop at session_ended or an empty End without re-arming - acknowledge that final round with bin/fm-procevent.sh handled <source-id> <sequence> to conclude and retire your board.
Before reporting done, read and follow `~/.no-mistakes/worktrees/aa96bb817995/01M3CQDG0EGPW464G4FNT1VMJT/.agents/skills/captain-hold-lifecycle/SKILL.md` and pass its shared completion gate for the report and any visual review.
When the report is complete, append `done [at=<epoch>]: {one-line conclusion}` to the status file and stop.
If your findings reveal work that should ship (e.g. you reproduced a bug and the fix is clear), say so in the report; firstmate may promote this task in place, and you would then receive mode-specific ship instructions as a follow-up message.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-secrets.py:367 - Wildcard EnvironmentFiles= entries use unsorted glob.glob() results, while later assignments determine the final value. With 10.env: TOKEN=keep, 20.env: TOKEN=remove, and UnsetEnvironment=TOKEN=remove, a 20,10 expansion returns TOKEN=yes although systemd's ordered expansion leaves it absent. Sort each wildcard expansion while preserving directive order.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Inspect settings without exposing values ✅ pass live Live fm-secrets CLI transcript
Run with only selected settings and redact output ✅ pass live Live fm-secrets CLI transcript
Block terminal and cross-stream secret-output bypasses ✅ pass live Live terminal-isolation and service transcript
Check a real service without exposing environment values ✅ pass live Live terminal-isolation and service transcript
Generated worker briefs require the settings boundary ✅ pass live Generated worker settings rule
Stopped-service EnvironmentFile precedence and fail-closed behavior ⏸️ untested no A real stopped unit with controlled EnvironmentFile and UnsetEnvironment directives would require creating or managing systemd state outside this worktree. Provide an isolated disposable systemd lab o…
  • bin/fm-secrets.sh names, has, and run against process-substitution settings fixtures; captured redacted live transcript
  • bin/fm-secrets.sh run from a real pty, attempting /dev/tty output
  • bin/fm-secrets.sh has --service dbus.service PATH DOES_NOT_EXIST
  • FM_HOME=~/.no-mistakes/evidence/01M3CQDG0EGPW464G4FNT1VMJT/fm-brief-live-home bin/fm-brief.sh live-settings-worker alpha --scout
  • bash tests/fm-secrets.test.sh
  • git diff 5b83e6aa3aec7b019218adcc0cfa5f9adf586e98 b66a85caa693b47cca0ad3e88ee7d406c82c2706 | jev find "a change unrelated to the worker settings inspection, selected-environment command runner, secret output redaction, systemd presence lookup, or generated worker settings rule"
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T16:31:22.352766Z 18963c7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18963c7afa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bin/fm-secrets.py
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward termination signals to the detached child

When the wrapper receives SIGTERM or SIGHUP while the command is running—for example during a worker timeout or teardown—start_new_session=True prevents the child from receiving that signal, and the wrapper has no handler that terminates the new process group. I reproduced this with a child sleep: terminating fm-secrets.sh left the child alive with the selected secret still in its environment. Track the Popen instance and forward termination to its process group before exiting so commands cannot outlive the guarded runner.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b66a85c. The wrapper now retains the Popen handle, forwards SIGHUP, SIGINT, and SIGTERM to the detached child process group with os.killpg(), waits for the child, scrubs captured output, and returns the conventional 128+signal status. A behavioral regression test sends SIGTERM around a sleeping child and verifies status 143 and that the child group is gone.

@Profreshor Profreshor changed the title feat: add guarded settings inspection tool feat(bin): add guarded settings inspection tool Sep 25, 2026
@Profreshor
Profreshor merged commit 4c727ac into main Sep 25, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant