Skip to content

feat(api): POST /environment/reload applies the guest metadata environment live - #289

Open
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
cdrappier/devin/env-reload-from-metadata
Open

feat(api): POST /environment/reload applies the guest metadata environment live#289
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
cdrappier/devin/env-reload-from-metadata

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes ENG-4745

Summary

Companion to blaxel-ai/executionplane#448 (pull-based guest metadata). An environment update on a running sandbox now reaches the sandbox-api process without a restart:

  • vmm-manager persists the new env and notifies the guest; the initrd refetches /bl/metadata and then fires one best-effort POST http://127.0.0.1:8080/environment/reload.
  • New EnvironmentHandler.HandleReload re-reads /bl/metadata and applies its environment to this process:
for key := range h.applied { if _, ok := doc.Environment[key]; !ok { os.Unsetenv(key) } }
for key, value := range doc.Environment { os.Setenv(key, value); h.applied[key] = struct{}{} }

Because buildProcessEnv starts from os.Environ(), every process spawned afterwards (process API, terminals, restarts) inherits the updated set too. Only keys that came from the metadata document are ever unset — image ENV and boot-time variables are untouched.

Backward compatible: a VM on an older initrd has no /bl/metadata, the endpoint returns 404, and nothing else changes.

Link to Devin session: https://app.devin.ai/sessions/6ab2c5d636a2486684f6cece3d0818e4
Requested by: @drappier-charles


Note

Adds POST /environment/reload endpoint that re-reads /bl/metadata and applies its environment map to the sandbox-api process via os.Setenv/os.Unsetenv. The latest commit simplifies the removal logic to always unset (rather than restore boot-time values), since the metadata document represents the host's complete environment set.

Written by Mendral for commit 96c39e9.


Note

Medium Risk
Mutates the API process environment at runtime (including unsetting host-managed keys), which affects all subsequently spawned commands; endpoint is localhost-oriented but unauthenticated like other system routes.

Overview
Adds POST /environment/reload so sandbox-api can adopt host environment updates without restarting, paired with pull-based guest metadata from the initrd.

EnvironmentHandler re-reads /bl/metadata (or BL_METADATA_PATH, fixed at handler construction), unmarshals generation and environment, then os.Setenv for each key and os.Unsetenv only for keys previously applied from metadata that are absent in the new document. Child processes pick up changes because process spawning uses os.Environ(). Missing metadata returns 404 for older VMs.

Router wiring, OpenAPI/Swagger docs, and unit tests cover apply/update/remove and the no-metadata case.

Reviewed by Cursor Bugbot for commit 96c39e9. Bugbot is set up for automated code reviews on this repo. Configure here.

…nment live

The guest init pings this endpoint after applying a new metadata generation.
The handler re-reads /bl/metadata and applies its environment to the
sandbox-api process (os.Setenv/Unsetenv), so the API process and every process
spawned afterwards see the current values without a sandbox restart. VMs on an
older initrd simply have no /bl/metadata and get a 404.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@drappier-charles drappier-charles self-assigned this Aug 13, 2026
@drappier-charles
drappier-charles self-requested a review August 13, 2026 02:34
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@mendral-app

mendral-app Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🧪 Testing Guide

What this PR addresses

Adds a new POST /environment/reload endpoint to the sandbox-api that allows live environment variable updates without restarting the process. When the vmm-manager updates metadata, the guest initrd refetches /bl/metadata and calls this endpoint so the sandbox-api process (and any subsequently spawned processes) picks up the new environment variables. Variables removed from a newer generation are also unset.

Steps to reproduce the original issue

Previously, updating environment variables on a running sandbox required a full restart of the sandbox-api process. There was no mechanism for the guest to apply environment changes live.

How to exercise the new behavior

  1. Unit tests — Run the new handler tests:

    cd sandbox-api && go test ./src/handler/ -run TestHandleReload -v

    This validates apply, update, remove, and missing-metadata scenarios.

  2. Manual / integration test (requires a running sandbox):

    • Create a file at /bl/metadata (or set BL_METADATA_PATH to a custom path) with content:
      {"generation":1,"environment":{"MY_VAR":"hello"}}
    • Call the endpoint:
      curl -X POST http://127.0.0.1:8080/environment/reload
    • Verify the response shows applied: 1, removed: 0, generation: 1.
    • Update the metadata file to remove MY_VAR and add OTHER_VAR:
      {"generation":2,"environment":{"OTHER_VAR":"world"}}
    • Call reload again and verify applied: 1, removed: 1, generation: 2.
    • Confirm spawned processes (via process API or terminal) see OTHER_VAR=world and MY_VAR is unset.
  3. Edge cases to test:

    • Call /environment/reload when /bl/metadata does not exist → expect 404.
    • Provide malformed JSON in the metadata file → expect 500.
    • Call reload multiple times with the same generation → should be idempotent.

What to verify (expected behavior)

  • POST /environment/reload returns 200 with {generation, applied, removed} counts.
  • ✅ Environment variables from the metadata document are applied to the sandbox-api process via os.Setenv.
  • ✅ Variables present in a previous generation but absent in the current one are removed via os.Unsetenv.
  • ✅ The endpoint is thread-safe (mutex protects the applied-keys map).
  • ✅ Missing metadata returns 404; malformed metadata returns 500.
  • ✅ OpenAPI docs (docs.go, openapi.yml) are updated with the new endpoint and response schema.
  • ✅ Existing tests and endpoints are unaffected (no regression).

Note

Posted by PR Testing Guide · Tag @mendral-app with feedback.

@mendral-app

mendral-app Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🔄 Interaction Flow: Environment Reload

sequenceDiagram
    participant VMM as vmm-manager
    participant Initrd as initrd (guest)
    participant API as sandbox-api<br>/environment/reload
    participant Meta as /bl/metadata
    participant OS as os (env vars)

    VMM->>Initrd: notify env update
    Initrd->>Meta: refetch /bl/metadata
    Meta-->>Initrd: JSON {generation, environment}
    Initrd->>API: POST /environment/reload
    API->>API: acquire mutex lock
    API->>Meta: read metadata file
    Meta-->>API: MetadataDocument
    API->>API: diff applied map vs new env
    loop removed keys
        API->>OS: Unsetenv(key)
    end
    loop new/updated keys
        API->>OS: Setenv(key, value)
    end
    API->>API: update applied map & generation
    API-->>Initrd: 200 {generation, applied, removed}
Loading

Summary

This PR introduces a live environment reload mechanism for running sandboxes:

  1. vmm-manager persists updated environment and notifies the guest.
  2. initrd refetches /bl/metadata then fires a best-effort POST /environment/reload to the local sandbox-api.
  3. EnvironmentHandler reads the metadata file, diffs against previously applied keys, removes stale vars, applies new ones via os.Setenv, and returns a summary response.

Thread safety is ensured via sync.Mutex on the applied-keys map. Child processes spawned after reload inherit the updated environment.

Note

Posted by PR Sequence Diagram · Tag @mendral-app with feedback.

@mendral-app

mendral-app Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

📋 Created Linear issue ENG-4745 — status: In Progress

  • Assignee: Charles Drappier (reviewer — bot PR)
  • Labels: Feature, Sandbox, Runtime
  • Project: Sandbox MK3.1 Release
  • Estimate: M (271 additions, 5 files)
  • PR linked: ✅ Issue will auto-close when this PR merges

Auto-created because no Linear reference was found in the PR title, description, or branch name.

Note

Posted by Linear Issue Enforcer · Tag @mendral-app with feedback.

mendral-app[bot]

This comment was marked as outdated.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
mendral-app[bot]

This comment was marked as outdated.

@drappier-charles
drappier-charles marked this pull request as ready for review August 13, 2026 03:06

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +94 to +111
removed := 0
for key := range h.applied {
if _, ok := doc.Environment[key]; !ok {
if err := os.Unsetenv(key); err == nil {
removed++
}
delete(h.applied, key)
}
}
applied := 0
for key, value := range doc.Environment {
if err := os.Setenv(key, value); err != nil {
logrus.WithError(err).WithField("key", key).Warn("Failed to set environment variable")
continue
}
h.applied[key] = struct{}{}
applied++
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 Variables that also exist in the image configuration are erased instead of restored when dropped from metadata

A variable that the container image already defined is deleted outright (os.Unsetenv(key) at sandbox-api/src/handler/environment.go:97) once it disappears from the metadata, instead of falling back to the value the image originally provided, so settings silently vanish.
Impact: If an environment update temporarily overrides a built-in setting (for example a search path or a service URL) and later drops it, the sandbox and every program it launches afterwards run with that setting completely missing rather than with the original default.

No snapshot of pre-existing values before overwriting

HandleReload tracks only the set of keys it applied (h.applied), not their prior values. First reload: metadata contains KEY=override, so os.Setenv("KEY", "override") replaces the image-provided value and h.applied["KEY"] is recorded (sandbox-api/src/handler/environment.go:104-111). Next reload where metadata no longer carries KEY: the loop at sandbox-api/src/handler/environment.go:95-102 unsets it, so the original image value is lost for this process and, via buildProcessEnv starting from os.Environ() (sandbox-api/src/handler/process/process.go:141), for every process spawned afterwards. Recording the previous value (and whether it existed) when first overriding would allow restoring instead of unsetting.

Prompt for agents
In sandbox-api/src/handler/environment.go, EnvironmentHandler only remembers which keys came from the metadata document (h.applied as a set), not what the process environment held before those keys were first overridden. When a later metadata generation drops a key, the handler unsets it, which erases any value that originally came from the container image ENV or boot-time setup rather than restoring it. Consider changing h.applied to a map from key to the previous value plus a flag indicating whether the key existed before, captured only the first time the handler overrides that key; on removal, restore the previous value if it existed, otherwise unset.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6b36cc0applied now records the value (and existence) the process held before the first override; when a later generation drops a key, that original value is restored instead of the variable being erased. Covered by TestHandleReloadRestoresPreExistingValue.

Comment on lines +20 to +25
func metadataPath() string {
if path := os.Getenv("BL_METADATA_PATH"); path != "" {
return path
}
return defaultMetadataPath
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔍 Metadata can override the very variable that selects the metadata path

metadataPath() reads BL_METADATA_PATH from the process environment, and HandleReload then applies arbitrary key/values from the document via os.Setenv. If a metadata document ever carries BL_METADATA_PATH, subsequent reloads read from a different file (and if a later generation drops the key, the path silently reverts). Worth considering skipping this key when applying, or resolving the path once at handler construction.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6b36cc0 — the metadata path is now resolved once at handler construction, so a document carrying BL_METADATA_PATH can no longer redirect subsequent reloads.

…ues on removal

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
mendral-app[bot]

This comment was marked as outdated.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6b36cc0. Configure here.

Comment thread sandbox-api/src/handler/environment.go
…time values

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@mendral-app mendral-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

The simplification in 96c39e9 (unset instead of restore) is correct given the protocol design: the metadata document is the authoritative complete set, so a removed key should be fully gone. The code compiles (uses the package-level jsoniter variable from filesystem.go), tests cover the key scenarios, and the mutex serializes concurrent reloads properly.

Tag @mendral-app with feedback or questions. View session

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