Skip to content

fix(launch): snapshot agent config to stop session-URL leak; rename agents uninstall → remove#14

Merged
joch merged 9 commits intomainfrom
snapshot-config
May 8, 2026
Merged

fix(launch): snapshot agent config to stop session-URL leak; rename agents uninstall → remove#14
joch merged 9 commits intomainfrom
snapshot-config

Conversation

@joch
Copy link
Copy Markdown
Member

@joch joch commented May 8, 2026

Summary

  • Closes the known limitation in feat: opper launch URL-sessions for usage attribution #11: direct invocations of codex / pi / openclaw / opencode (without opper launch) used to inherit the previous launch's /v3/session/<sid>/<tags> URL because we were rewriting persistent config files at spawn and never putting them back.
  • Adds withConfigSnapshot(path, fn) (captures bytes + mode or absence, restores in finally, even on non-zero exits and thrown errors) and wraps each affected adapter's rewrite + spawnSync in it. Hermes and Claude Code already weren't leaking — Hermes runs against an isolated HERMES_HOME, Claude Code routes via env vars.
  • Renames opper agents uninstall <name>opper agents remove <name> (the menu option too). The integration command never uninstalled the agent binary — "remove" matches what it actually does and pairs with the implicit "add" that happens on first launch / configure. Skills uninstall is unrelated and stays.

Special cases

  • OpenClaw default gateway start skips the snapshot: the daemon outlives spawnSync and owns models.json from then on, so restoring would either break a running gateway or be cosmetic. Pass-through args (e.g. agent --local) still get the snapshot. Documented in-code.
  • OpenCode --project uses a narrower revert — baseURL only, captured after configureOpenCode runs — so a checked-in project config keeps its opper provider across launches, and a hand-edited self-hosted baseURL survives unchanged.
  • Mode preservation: writeFile's mode option is silently ignored when the file already exists, so the helper now follows the write with an explicit chmod — and the test uses chmodSync mid-run instead of writeFileSync(... mode) so it actually exercises the code path.

Mental model

opper launch <agent> is now ephemeral — it never alters persistent state. If there's no opper block, none gets left behind; if there's a custom one (self-hosted Opper, mixed-provider config, etc.), it survives. The persistent-setup surface lives entirely in opper agents (menu + CLI) — users own whatever's in their agent's config file.

A user-visible side effect: anyone who was relying on opper launch leaving an opper block behind now has to set it up explicitly via the menu or opper agents. Worth a release-note line.

Test plan

  • `npm test` — 340/340 pass. New helper tests (6) and per-adapter regression tests (success path, no-prior-config cleanup, non-zero exit, mode preservation, project-scope provider persistence + custom-baseURL preservation, openclaw daemon-no-snapshot).
  • `npm run build` clean.
  • Manual smoke: `opper launch pi -p hello` against a polluted `~/.pi/agent/models.json` — the post-launch URL now matches the pre-launch URL byte-for-byte (vs. the new session URL on `main`), confirming snapshot/restore is firing. The "uninstall + launch leaves no opper block" cycle works for the cleanup path too.
  • Manual: `opper launch opencode --project` in a fresh dir — opencode.json keeps the opper provider, baseURL ends at compat URL.
  • Manual: `opper launch openclaw` — daemon path keeps the session URL in models.json (intentional), `gateway stop` cleans up.

Copy link
Copy Markdown

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

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: fe06b486cb

ℹ️ 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 src/util/config-snapshot.ts Outdated
joch added 2 commits May 8, 2026 10:48
…session URL

opper launch was rewriting persistent agent config files (codex,
pi, openclaw, opencode) with a per-session URL and never putting
them back, so subsequent direct invocations of the agent inherited
the previous session's attribution.

Adds withConfigSnapshot(path, fn) — captures the file (bytes + mode,
or absence) before the spawn and restores it in finally, even on
non-zero exits and thrown errors. Each affected adapter wraps its
existing rewrite + spawnSync in this helper.

Special cases:

- openclaw default `gateway start` skips the snapshot: the daemon
  outlives spawnSync and owns models.json from then on, so restoring
  would either break a running gateway or be cosmetic. Pass-through
  args (e.g. `agent --local`) still get the snapshot.
- opencode --project uses a narrower revert (baseURL only, captured
  after configureOpenCode) so a checked-in project config keeps its
  opper provider — and any custom self-hosted baseURL — across
  launches.

Hermes and Claude Code already weren't leaking (Hermes runs against
an isolated HERMES_HOME; Claude Code routes via env vars), so they
need no changes.
The integration command never uninstalled the agent binary itself —
it only removed Opper-owned config. "remove" matches the pairing
with the implicit "add" that happens on first launch / configure,
and avoids the suggestion that the agent gets uninstalled.

Renames both the CLI subcommand and the menu option, plus the
internal command function (agentsUninstallCommand →
agentsRemoveCommand). README updated. Skills `uninstall` is left
alone since that one really does remove files from disk.
@joch joch force-pushed the snapshot-config branch from fe06b48 to 06191d0 Compare May 8, 2026 08:48
Codex review flagged that the previous full-file snapshot/restore
clobbered any edits the launched agent (or a concurrent user edit)
made to other parts of the config during the session — themes,
default model, MCP servers in opencode; sibling providers in
pi/openclaw; user [settings] in codex.

Replace `withConfigSnapshot(path, fn)` with `withJsonKey(path,
keyPath, fn)` for the JSON adapters (pi, openclaw, opencode):
captures only the value at the keyPath, runs fn, then re-reads
the (possibly mutated) post-fn file and replaces only that key.
Sibling keys edited mid-spawn are preserved.

For codex (TOML sentinel-delimited) inline an equivalent narrow
restore: capture the sentinel block contents, run, then strip
the current block and reinsert the captured one — content
outside the sentinels is preserved.

Adds four regression tests (one per affected adapter) asserting
that sibling edits made during spawnSync survive the restore.
@joch
Copy link
Copy Markdown
Member Author

joch commented May 8, 2026

@codex review

Copy link
Copy Markdown

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

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: eb29302dc3

ℹ️ 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 src/agents/openclaw.ts Outdated
`opper launch openclaw -- gateway start` (or `daemon start`) routes
into a long-lived service the same way the default no-args path does
— both detach a daemon that outlives spawnSync. The previous logic
gated the daemon-safe behavior on `args.length === 0`, so an explicit
`gateway start` fell into the snapshot/restore branch and clobbered
or desynchronized the daemon's routing.

Match `gateway|daemon start` directly instead. Adds tests pinning
both explicit forms.
@joch
Copy link
Copy Markdown
Member Author

joch commented May 8, 2026

@codex review

Copy link
Copy Markdown

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

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: 78fab57642

ℹ️ 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 src/agents/opencode.ts Outdated
Comment thread src/agents/opencode.ts Outdated
… order

Two issues codex flagged on the previous narrow-snapshot pass:

1. The opencode template writes BOTH `provider.opper` AND a top-level
   `model: "opper/..."`. Snapshotting only `provider.opper` left an
   orphaned `model` pointing at a removed provider after a fresh
   first launch — direct `opencode` runs after that would fail.
2. Project scope captured `restoreUrl` AFTER `configureOpenCode({
   overwrite: true })` ran. With overwrite enabled, configureOpenCode
   replaces provider.opper with template values BEFORE the capture,
   so a hand-edited self-hosted baseURL was wiped before we could
   read it.

Generalise the helper from `withJsonKey(path, keyPath, fn)` to
`withJsonKeys(path, keyPaths[], fn)` and pass both Opper-owned
keypaths from opencode user-scope. Move the readBaseUrl capture to
before configureOpenCode in project-scope.

Strengthens the project-scope custom-baseURL test to actually
simulate `configureOpenCode`'s overwrite behavior, plus adds a
test pinning that an orphaned `model: "opper/..."` is removed
when no opper provider existed before.
@joch
Copy link
Copy Markdown
Member Author

joch commented May 8, 2026

@codex review

Copy link
Copy Markdown

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

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: 3660f7574b

ℹ️ 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 src/agents/codex.ts Outdated
The codex spawn was reading config.toml outside any try/catch, so a
permission error or transient I/O failure on a pre-existing-but-
unreadable file would abort the launch — a regression vs. the prior
behavior, where writeOpperBlock tolerated read failures and
proceeded with an empty baseline.

Wrap the readFileSync (capture of the opper sentinel block) in
try/catch and fall back to a null block on failure. Same defensive
guard around statSync in withJsonKeys (mode capture for
pi/openclaw/opencode).

Restore paths were already wrapped — only the capture side was
exposed.
@joch
Copy link
Copy Markdown
Member Author

joch commented May 8, 2026

@codex review

Copy link
Copy Markdown

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

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: 415f1d00d6

ℹ️ 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 src/agents/openclaw.ts Outdated
OpenClaw allows global flags before the subcommand
(`openclaw [--dev] [--profile <name>] <command>`), so daemon
launches like `opper launch openclaw -- --profile dev gateway
start` arrived with `args[0] === "--profile"`. The previous
positional check missed those, falling into the snapshot/restore
path — `spawnSync` returns when the daemon detaches and restore
immediately desyncs models.json from the live gateway.

Scan for the adjacent `gateway|daemon start` pair anywhere in
args. Test pinning the leading-flags case.
@joch
Copy link
Copy Markdown
Member Author

joch commented May 8, 2026

@codex review

Copy link
Copy Markdown

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

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: 74a9b033dc

ℹ️ 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 src/agents/openclaw.ts
`restart` shares the daemon-lifecycle surface with `start` — both
end with a long-lived service detached from spawnSync. The previous
detection only matched `start`, so `opper launch openclaw -- gateway
restart` fell into the snapshot path and reverted the just-written
session URL while the restarted daemon was coming up.

`stop` / `status` / `logs` are all synchronous and stay on the
snapshot path.
@joch
Copy link
Copy Markdown
Member Author

joch commented May 8, 2026

@codex review

Copy link
Copy Markdown

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

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: 1d3067c3ed

ℹ️ 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 src/agents/opencode.ts Outdated
Comment thread src/agents/codex.ts Outdated
Two issues codex flagged on the previous pass:

1. Codex sentinel restore used indexOf-FIRST to strip the block.
   When the pre-launch config had a stale unclosed `# >>> opper-cli >>>`
   marker (e.g. a partial manual edit), the strip would cross-match
   that stale opener with our newly-appended closer and delete every
   line in between — silently losing user data. Add lastIndexOf-based
   `extractLastOpperBlock` and `stripLastOpperBlock`, used in the
   snapshot capture and restore paths. The legacy first-match helpers
   stay for unconfigure (separate concern).
2. The opencode template writes `$schema` alongside `provider.opper`
   and `model`. The narrow snapshot only covered the latter two, so
   a fresh first launch left a `{"$schema": "..."}` file behind.
   Add `["$schema"]` to the keypaths so the launch is truly ephemeral.

Adds regression tests for both: codex pre-existing stale opener
keeps unrelated user content intact; fresh opencode launch with
template's three top-level keys leaves no file behind.
@joch
Copy link
Copy Markdown
Member Author

joch commented May 8, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ 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".

@joch joch merged commit 38e80de into main May 8, 2026
4 checks passed
@joch joch deleted the snapshot-config branch May 8, 2026 10:57
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