Skip to content

feat(sdk): file-injection spawn API + bump contracts to file-injection on main#57

Merged
joshuajbouw merged 2 commits into
mainfrom
feat/process-file-injection
Jun 16, 2026
Merged

feat(sdk): file-injection spawn API + bump contracts to file-injection on main#57
joshuajbouw merged 2 commits into
mainfrom
feat/process-file-injection

Conversation

@joshuajbouw

Copy link
Copy Markdown
Contributor

Bumps the contracts submodule 9742f800a50cfc (canonical wit main: #14 persistent-stdin un-stub + #15 process file-injection) and adds the Rust builder surface for read-only file injection.

Why

core #890 landed read-only file injection on the host + astrid:[email protected], and the wit side is now merged (#15). The SDK was pinned at wit #13, so a process-tier capsule built against it carried an older [email protected] than the host exposes — the linker mismatch the submodule lockstep exists to prevent. Bumping the pin (and build.rs re-staging wit-staging from contracts/host) restores lockstep.

API

  • InjectionPlacement { EnvPointer(String), FixedPath(String) }
  • Command::inject_env_file(env_var, content) — OS-agnostic (host-owned path + env redirect)
  • Command::inject_file_at(path, content) — Linux-only ro-bind (rejected on macOS)
  • Command::inject_file(content, placement) — explicit placement
  • into_wit maps to wit_process::FileInjection. Honored by all spawn modes.

Injected bytes are read-only and unmodifiable by the child, its subprocesses, or the spawning principal's fs surface.

Verification

  • Full workspace build + tests green; clippy clean.
  • scripts/sync-contracts-wit.sh --check passes (events-bundle mirror in sync — astrid-contracts.wit is unchanged since file-injection is a host/ contract, not interfaces/).
  • New tests: inject_builders_accumulate_in_order, no_injection_by_default.

Lockstep with core #920 (wit submodule bump to the same 0a50cfc).

…ss file-injection)

Bumps the contracts submodule 9742f80 -> 0a50cfc (wit #14 + #15), bringing
astrid:[email protected]'s read-only file-injection into the SDK's WIT (build.rs
re-stages wit-staging from contracts/host). This closes the lockstep gap with
core #890: process-tier capsules built against the SDK now carry the same
[email protected] the host exposes, instead of an older shape that fails to link.

Adds the Rust builder surface so capsules can actually use it:
- InjectionPlacement { EnvPointer(name), FixedPath(path) }
- Command::inject_env_file / inject_file_at / inject_file
- into_wit maps them onto wit_process::FileInjection.

The injected bytes are exposed read-only and unmodifiable by the child or the
spawning principal's fs surface; env-pointer is OS-agnostic, fixed-path is
Linux-only (rejected on macOS). astrid-contracts.wit (events bundle) is
unchanged — file-injection is a host/ change, not an interfaces/ one.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces read-only file injection capabilities to the spawn builder (Command), allowing host-verified, read-only bytes to be exposed to spawned child processes via environment variables or fixed paths. The changes include updating the WIT contract, adding the InjectionPlacement enum and builder methods to the SDK, and adding corresponding unit tests. The review feedback suggests deriving PartialEq and Eq on InjectionPlacement and implementing From for WIT conversion, which simplifies both the conversion logic in into_wit and the assertions in the unit tests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread astrid-sdk/src/process.rs
Comment on lines +119 to +135
#[derive(Debug, Clone)]
pub enum InjectionPlacement {
/// The host materializes the bytes at a host-owned path (outside every
/// VFS mount) and sets the named environment variable on the child to
/// that path. The host owns the path — there is no caller-chosen target.
/// Works on **Linux and macOS** (the OS-agnostic mode); use it for
/// programs whose enforced config tier is reachable via an env-redirected
/// file. The `String` is the env-var name.
EnvPointer(String),
/// The host ro-binds the bytes at this absolute in-sandbox path (the
/// mount point is created, so `path` need not pre-exist). **Linux only**
/// — rejected on macOS with `invalid-input`, since Seatbelt has no mount
/// namespace and materializing at a caller-named host path would be an
/// arbitrary host write. Use it for programs whose enforced tier is a
/// fixed path with no env redirect.
FixedPath(String),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Deriving PartialEq and Eq on InjectionPlacement makes it much easier for library consumers to compare placement options and write assertions in tests. Additionally, implementing From<InjectionPlacement> for wit_process::InjectionPlacement encapsulates the WIT conversion logic, keeping the builder's into_wit method clean and idiomatic.

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InjectionPlacement {
    /// The host materializes the bytes at a host-owned path (outside every
    /// VFS mount) and sets the named environment variable on the child to
    /// that path. The host owns the path — there is no caller-chosen target.
    /// Works on **Linux and macOS** (the OS-agnostic mode); use it for
    /// programs whose enforced config tier is reachable via an env-redirected
    /// file. The `String` is the env-var name.
    EnvPointer(String),
    /// The host ro-binds the bytes at this absolute in-sandbox path (the
    /// mount point is created, so `path` need not pre-exist). **Linux only**
    /// — rejected on macOS with `invalid-input`, since Seatbelt has no mount
    /// namespace and materializing at a caller-named host path would be an
    /// arbitrary host write. Use it for programs whose enforced tier is a
    /// fixed path with no env redirect.
    FixedPath(String),
}

impl From<InjectionPlacement> for wit_process::InjectionPlacement {
    fn from(placement: InjectionPlacement) -> Self {
        match placement {
            InjectionPlacement::EnvPointer(v) => Self::EnvPointer(v),
            InjectionPlacement::FixedPath(p) => Self::FixedPath(p),
        }
    }
}
References
  1. To improve maintainability and reduce duplication, refactor repeated blocks of code into a single helper function.

Comment thread astrid-sdk/src/process.rs
Comment on lines +329 to +343
file_injections: self
.file_injections
.into_iter()
.map(|(content, placement)| wit_process::FileInjection {
content,
placement: match placement {
InjectionPlacement::EnvPointer(v) => {
wit_process::InjectionPlacement::EnvPointer(v)
},
InjectionPlacement::FixedPath(p) => {
wit_process::InjectionPlacement::FixedPath(p)
},
},
})
.collect(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

With the From implementation in place, we can simplify the file_injections mapping in into_wit by calling placement.into(), which improves readability and maintainability.

            file_injections: self
                .file_injections
                .into_iter
                .map(|(content, placement)| wit_process::FileInjection {
                    content,
                    placement: placement.into(),
                })
                .collect(),
References
  1. To improve maintainability and reduce duplication, refactor repeated blocks of code into a single helper function.

Comment thread astrid-sdk/src/process.rs
Comment on lines +880 to +892
assert!(matches!(
cmd.file_injections[0].1,
InjectionPlacement::EnvPointer(ref v) if v == "CLAUDE_CODE_MANAGED_SETTINGS_PATH"
));
assert_eq!(cmd.file_injections[0].0, b"{}");
assert!(matches!(
cmd.file_injections[1].1,
InjectionPlacement::FixedPath(ref p) if p == "/etc/codex/requirements.toml"
));
assert!(matches!(
cmd.file_injections[2].1,
InjectionPlacement::EnvPointer(ref v) if v == "GEMINI"
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since InjectionPlacement now implements PartialEq and Eq, we can replace the verbose matches! assertions with cleaner and more readable assert_eq! calls.

Suggested change
assert!(matches!(
cmd.file_injections[0].1,
InjectionPlacement::EnvPointer(ref v) if v == "CLAUDE_CODE_MANAGED_SETTINGS_PATH"
));
assert_eq!(cmd.file_injections[0].0, b"{}");
assert!(matches!(
cmd.file_injections[1].1,
InjectionPlacement::FixedPath(ref p) if p == "/etc/codex/requirements.toml"
));
assert!(matches!(
cmd.file_injections[2].1,
InjectionPlacement::EnvPointer(ref v) if v == "GEMINI"
));
assert_eq!(
cmd.file_injections[0].1,
InjectionPlacement::EnvPointer("CLAUDE_CODE_MANAGED_SETTINGS_PATH".to_string())
);
assert_eq!(cmd.file_injections[0].0, b"{}");
assert_eq!(
cmd.file_injections[1].1,
InjectionPlacement::FixedPath("/etc/codex/requirements.toml".to_string())
);
assert_eq!(
cmd.file_injections[2].1,
InjectionPlacement::EnvPointer("GEMINI".to_string())
);

@joshuajbouw joshuajbouw merged commit 1573243 into main Jun 16, 2026
8 checks passed
@joshuajbouw joshuajbouw deleted the feat/process-file-injection branch June 16, 2026 01:55
joshuajbouw added a commit to astrid-runtime/astrid that referenced this pull request Jun 16, 2026
## Linked Issue

Closes #919

## Summary

Repins the `wit` submodule from `b8fdb6f` (the tip of the now-merged wit
PR #15 branch `feat/process-file-injection`) onto the canonical wit main
commit `0a50cfc` (#15 squash-merged). The tree is identical to
`b8fdb6f`, so there is no WIT content change — this only moves the pin
off an unmerged-PR commit onto main.

## Changes

- Bump `wit` submodule `b8fdb6f` → `0a50cfc` (tree-identical; pure
pointer move).

## Verification

- `git diff b8fdb6f 0a50cfc` in the wit repo is empty (identical tree),
so there is no host/guest WIT behaviour change — the staged diff is a
single gitlink change.
- Lockstep with `astrid-runtime/sdk-rust#57`, which bumps the contracts
submodule to the same `0a50cfc`.

## Checklist

- [x] Linked to an issue
- [x] CHANGELOG.md updated (not applicable — submodule pointer move,
tree-identical, no code or behaviour change)
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