Skip to content

feat(bindings): host env and mounts applied after construction survive reset - #2292

Merged
chaliy merged 3 commits into
mainfrom
claude/bashkit-issue-2291-5oimsh
Aug 13, 2026
Merged

feat(bindings): host env and mounts applied after construction survive reset#2292
chaliy merged 3 commits into
mainfrom
claude/bashkit-issue-2291-5oimsh

Conversation

@chaliy

@chaliy chaliy commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What changed

A host can now apply environment to a running interpreter, and host setup applied after construction survives a rebuild.

  • Core: Bash::set_env(key, value) — the environment counterpart to live mount(). Preserves shell state; no rebuild.
  • NAPI / Python: setEnv / set_env, plus a recorded replay of host set_env and runtime mount calls. reset() now keeps them the way it already kept custom builtins and constructor files. unmount() retracts its record, so a rebuild cannot resurrect a removed mount. Env a script exported stays transient — reset() still discards it.

The practical effect: a reusable mount + env + builtin bundle installs onto an existing instance in one step, instead of splitting into a config-fragment the host merges by hand plus a post-construction apply function where only half the setup survives a reset.

Bash::set_env writes both the exported entry and the shell variable, matching BashBuilder::env. The exported entry alone is shadowed by a same-named shell variable during expansion, so a host value applied later would silently lose to a builder-configured one. This was caught by a failing binding test, not by inspection.

Why

Issue #2291 asks for a publishable extension object with mount/env/command hooks. Investigating it surfaced a smaller, more concrete gap underneath: mount was runtime-callable but env was construction-only, so no single-phase install was expressible; and of the three parts of such a bundle, only builtins survived reset().

Fixing that makes the extension shape ordinary userland code, and fixes a lifecycle inconsistency that stood on its own regardless of the issue.

Scope: this is the prerequisite, not the whole issue. Still open from #2291: the declarative extension object itself (extensions: [...] / installExtension), schema-backed ctx.command() on Bash (today addBuiltin gives raw argv; help/discover/--dry-run live only in ScriptedTool), and onBeforeExecute.

Before / After

The issue's example, run against the shipped API (node, NAPI binding):

Before — two phases, and the mount is gone after a reset:

$ node skill-today.mjs
# my-skill (args: hello)
SKILL.md

after reset: exit 1 — cat: /skills/my-skill/SKILL.md: io error: file not found
re-mounted: # my-skill        # only after calling mountSkill() again by hand

After — one phase, intact across reset():

const installSkill = (bash) => {
  bash.mount(SKILL_PATH, skillFs);
  bash.setEnv("SKILL_PATH", SKILL_PATH);
  bash.addBuiltin("my-skill", (ctx) => ctx.fs.readFile(`${SKILL_PATH}/SKILL.md`));
};
$ node smoke.mjs
fresh:               "# my-skill\n/skills/my-skill\n"
after reset:         "# my-skill\n# my-skill\n"
after unmount+reset: 1

Python is byte-identical on the same script:

fresh:               '# my-skill\n/skills/my-skill\n'
after reset:         '# my-skill\n# my-skill\n'
after unmount+reset: 1

Risk

  • Low
  • New API surface only; no existing call path changes behavior. The one semantic shift is that a binding reset() now keeps host setEnv/runtime mounts where it previously dropped them — the direction TM-ISO-025 already requires for constructor config, and the reason unmount() had to become retracting rather than live-only.
  • Nothing lands on the exec hot path: the replay logs are touched only by setEnv/mount/unmount and read once per rebuild.
  • The env log holds one entry per key, so a host calling setEnv per request does not accumulate entries.

Checklist

  • Tests added or updated — 7 core (runtime_env_tests.rs), 15 NAPI (runtime-extension.spec.ts), 14 Python (test_runtime_extension.py), covering live application, export visibility, precedence against builder/constructor env, reset replay, unmount retraction, and the full bundle end to end
  • Backward compatibility considered — additive; internal project, no external API contract

Knowledge updated: runtime_env row in the capability parity contract (with recorded reasons for the five surfaces that intentionally lack it), TM-ISO-025 evidence, the live-mount guide, the Python package doc, and the knowledge log.

Part of #2291

chaliy added 2 commits August 12, 2026 22:45
…e reset

Adds `Bash::set_env()` to the core as the environment counterpart to live
`mount()`, and surfaces it as `setEnv` (NAPI) and `set_env` (Python).

Both bindings now record host mutations applied after construction —
`set_env` and the runtime `mount*` APIs — and replay them on every rebuild,
so `reset()` preserves them the way it already preserved custom builtins and
constructor files (TM-ISO-025). `unmount()` retracts its record, so a rebuild
cannot resurrect a removed mount. Env a *script* exported stays transient.

This makes a reusable mount + env + builtin bundle installable on a live
instance instead of requiring a two-phase construct-then-apply split where
only half the setup survives a reset (issue #2291).

`Bash::set_env()` writes both the exported entry and the shell variable,
matching `BashBuilder::env`: the exported entry alone is shadowed by a
same-named shell variable during expansion, so a host value applied later
would silently lose to a builder-configured one.

Adds the `runtime_env` capability row to the parity contract, with reasons
for the surfaces that intentionally lack it (Rust BashTool/ScriptedTool
build a fresh shell per execution; CLI, browser wasm, and C ABI expose no
post-construction host API).
Review follow-ups on the runtime mutation logs:

- One entry per key in the env log. A host calling `setEnv()` per request
  accumulated an entry per call; replacing the key's entry keeps last-write-wins
  semantics and bounds the log by distinct key count.
- The Python log helpers return `PyResult` instead of silently skipping a record
  on a poisoned lock, matching how the rest of the binding treats lock failures.
  Swallowing it would leave a caller believing a value survives `reset()`.
- Document live `set_env` alongside live mounts in the mount guide, including
  the binding-level replay contract.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bashkit 9e37c43 Commit Preview URL

Branch Preview URL
Aug 13 2026, 01:09 AM

@chaliy
chaliy merged commit 53b0d8e into main Aug 13, 2026
48 checks passed
@chaliy
chaliy deleted the claude/bashkit-issue-2291-5oimsh branch August 13, 2026 01:25
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