Skip to content

feat(build): build and release a sandbox base image like every other compute type - #700

Merged
ItamarZand88 merged 7 commits into
mainfrom
itamar/alien-941-make-the-sandbox-image-follow-the-worker-build-and-release
Sep 24, 2026
Merged

ItamarZand88 merged 7 commits into
mainfrom
itamar/alien-941-make-the-sandbox-image-follow-the-worker-build-and-release

Conversation

@ItamarZand88

Copy link
Copy Markdown
Contributor

Summary

A sandbox was the only compute resource alien build ignored, so a developer who
wanted one on their own image built and pushed it by hand while the CLI built
everything else. It now goes through the same two commands as a Worker.

When a developer runs the flow, three things happen:

  1. alien build sees a sandbox declared from source and builds its base image
    from a Dockerfile, writing the same kind of artifact directory a Worker gets.
  2. alien release pushes that directory and stores a location-free reference —
    the heart of this change, and the step that means nobody types a registry path.
  3. The bundle layers the sandbox agent on afterwards, and AWS builds the MicroVM
    from it inside the customer's account, exactly as before.

This turns a hand-run docker push into alien build and alien release.

What was broken

Declaring a sandbox from source was refused at plan time on every platform, so
the base image had to exist before the stack did. Three places in the release
path knew about a worker, a container and a daemon but not a sandbox, so an
unchanged sandbox was pushed again on every release and --prebuilt carried a
path that no longer existed.

What I did

A sandbox base image is a root filesystem rather than a binary laid on a base
Alien picks, so it is built from a Dockerfile and the other toolchains are
refused by name. Source is now legal only where something can build it: on AWS
it is a declaration, and elsewhere the refusal names the platform, because there
the image is pulled as written and an unbuilt source would schedule a workload
with no image.

Two things a reviewer might look for and not find. The planner needs no sandbox
arm, because it asks which resources produce a host binary and a Dockerfile
build never does; that reasoning is now a comment where the missing branch would
be. And the preflight that refuses a resource linking a Live sandbox now names
the two ways out instead of stopping at the diagnosis.

Files touched

  • crates/alien-build/src/lib.rs — the sandbox build branch, and the Docker-only
    toolchain policy.
  • crates/alien-build/src/{merge,plan,toolchain/mod}.rs — a sandbox survives the
    partitioned CI merge; the new workload kind.
  • crates/alien-cli/src/commands/release.rs — the three push-cache and rebase
    sites.
  • crates/alien-core/src/resources/sandbox.rs — the per-platform source gate.
  • crates/alien-preflights/ — the refusal message, and a test that drives the
    gate through the runner rather than calling the method.
  • crates/alien-cloudformation/tests/ — the Live remote grant, now snapshotted.

How I tested

Ran the whole flow against real AWS rather than only in tests.

  • alien build --platforms aws on a stack declaring a sandbox from source:
    produced .alien/build/aws/alien-sandbox-b492f56d/linux-aarch64.oci.tar.
  • alien release --platforms aws: pushed it and stored the proxy reference. A
    second release did not push again, which is the cache fix working.
  • Confirmed the generated bundle's Dockerfile reads
    FROM <account>.dkr.ecr.<region>.amazonaws.com/<project repository>:<tag>,
    so the image the developer built is the one the MicroVM is built on, and that
    the bundle landed in every regional store.
  • cargo test across the touched crates: 1054 pass. Two pre-existing docker
    pull/export failures reproduce on a clean main.

This PR touches a grant and a permission set, so I walked the boundary:

  • A sandbox reaching another project's repository: the manager derives the owning
    project from the deployment record rather than the stack and refuses anything
    else, pinned by a_refused_sandbox_base_image_leaves_the_grant_incomplete.
  • The Live grant widening past its own image: the new snapshot pins an exact
    microvm-image:${AWS::StackName}-agents ARN with no trailing wildcard, so a
    sibling sandbox in the same stack is not matched. Its policy block is
    byte-identical to the Frozen one.
  • The grant landing on a wider identity: asserted it attaches to the Remote
    Bindings role alone.
  • Nothing turned up.

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the previously reported Frozen source-sandbox failure is fixed and no new actionable defects were identified.

Summary

This PR adds AWS support for building and releasing Dockerfile-backed sandbox base images through the standard build pipeline.

  • Builds sandbox source into OCI artifacts and rewrites the sandbox to reference the result.
  • Adds sandbox support to merged build outputs, prebuilt-path rebasing, push caching, and pushed-image application.
  • Allows source-backed sandboxes only on AWS and rejects them with the Frozen lifecycle before build work begins.
  • Extends tests around platform gating, artifact merging, release caching, and remote sandbox permissions.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Sandbox Source declaration] --> B[Compile-time preflights]
  B -->|AWS + Live + Docker| C[Build OCI base image]
  B -->|Frozen or unsupported platform| X[Reject configuration]
  C --> D[Write sandbox artifact directory]
  D --> E[Release push/cache handling]
  E --> F[Private registry reference]
  F --> G[Sandbox bundle layers agent]
  G --> H[AWS builds MicroVM]
Loading

Reviews (3) · Last reviewed commit: "fix(preflight): refuse a sandbox built f..."

Comment thread crates/alien-core/src/resources/sandbox.rs
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile review

…te type

A sandbox whose image is a local artifact directory is pushed, but never
recorded in the push cache, never matched against it, and never rebased under
`--prebuilt`. So an unchanged sandbox is re-pushed on every release, and a
prebuilt release from another checkout carries a path that no longer exists.

The three sites that make a push cheap enumerated the worker, the container and
the daemon. The sandbox now joins them, reading the same local-directory test
the push path already applies. A bundle URI and a catalog name reach these sites
too and name no artifact, so they pass through untouched.

The note that tells the next person where to add a compute type pointed at two
places and left these three out, which is how the sandbox came to be handled on
one side of the release and not the other.
A sandbox was the only compute resource `alien build` ignored. Declaring one
from source meant a refusal at plan time, so the base image had to be built and
pushed by hand while every other resource was built by the CLI.

`alien build` now collects a sandbox declared from source and produces an OCI
artifact directory for it, and the release pushes that directory the way it
already pushed a hand-built one. The bundle still layers the agent on top, and
AWS still builds the MicroVM from it inside the customer's account.

Source is refused only where nothing can build it. On AWS it is a declaration;
elsewhere the image is pulled as written, so an unbuilt source would schedule a
workload with no image and the refusal names the platform.

The base image is a root filesystem rather than a binary on a base Alien picks,
so it is built from a Dockerfile and the other toolchains are refused by name.
…e means

A partitioned CI build writes one artifact directory per runner group and merges
them afterwards. The merge read and wrote the image of a worker, a container and
a daemon, so a sandbox built on one runner was dropped on the way out.

The planner needs no arm. It asks which resources produce a host binary, and a
sandbox base image is built from a Dockerfile, so it never does. That is now
written where someone would otherwise go looking for the missing branch.

The declaration's own documentation said source was built at deploy time, which
was never true and is now wrong in a second way.
Four tests render a Live sandbox and three render a remote one, and none render
both, so the grant a remotely published Live sandbox depends on was asserted
nowhere.
The refusal said a Live sandbox's image is built after setup, so setup cannot
bind to it. That is the diagnosis and it stopped there, leaving a developer who
hit it with nothing to try.

It now names the two ways through: add the sandbox with `remoteAccess` and reach
it through a remote binding, or give it a base image that needs no build so it
can stay Frozen and be linked. A sandbox built from source is always the first
case, because its image is private and a private base image is Live only.
…rough the runner

The Live grant test asserted with `contains`, which is monotone: it passes just
as happily on a widened action list or a wildcarded image ARN, and it checked
four of the seven actions. It now carries the snapshot its Frozen sibling
already had, asserts the policy lands on the Remote Bindings identity, and names
every action. The snapshot's grant is byte-identical to the Frozen one, which is
what the test claims and could not previously show.

The per-platform source gate was covered by calling `validate_for_platform`
directly. That check once sat unregistered and method-level tests missed it,
which is why this file exists at all, and the Kubernetes runtime's empty-image
fallback rests on the gate being registered rather than on the method refusing.
The new case runs the registry for both Kubernetes and AWS.

A comment in that fallback justified itself with a guarantee this branch removes,
that Source is refused on every platform. The permission set's header made a
claim a Live sandbox on AWS breaks, since its image lands after the stack.
A source build is pushed to the project's own repository, so it is private by
the time anything reads the field that says so. The refusal keyed on that field,
which meant a Frozen source sandbox passed the build-time gate and failed later,
when setup tried to pull an image the registry had not opened to an account the
deployment had not yet reported.

It is refused on the declaration instead, where the developer is.
@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-941-make-the-sandbox-image-follow-the-worker-build-and-release branch from 14e96b0 to c430261 Compare September 24, 2026 17:45
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptileai review

@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile review

@ItamarZand88
ItamarZand88 merged commit 7356a49 into main Sep 24, 2026
22 checks passed
@ItamarZand88
ItamarZand88 deleted the itamar/alien-941-make-the-sandbox-image-follow-the-worker-build-and-release branch September 24, 2026 18:39
ItamarZand88 added a commit that referenced this pull request Sep 25, 2026
## Summary

AWS sandboxes can now be deployed with `alien-deploy`, the direct path
with no Terraform or CloudFormation. Setup creates everything the
runtime identity is never allowed to create itself, and the runtime
builds only what it is granted.

Step-by-step flow when `alien-deploy deploy` sets up a stack with a
sandbox:

1. **Setup, using the deployer's credentials, creates the sandbox's
build role. For `egress: deny` it also creates an operator role, a
loopback-only security group and a network connector. It records each
object and passes them to the sandbox's controller.** ← the heart
2. A Frozen sandbox's image is built during setup and rolled once when
setup runs again with a new bundle. A Live sandbox's image is built
later by the runtime, which passes the setup-made role.
3. Destroy removes the images, then everything setup created, in
dependency order.

This PR changes AWS sandboxes from template-only to deployable directly,
with the same objects the templates create.

## What was broken

`alien-deploy` could not deploy an AWS sandbox. The build role, the deny
egress objects and the remote-access grant exist only in the Terraform
and CloudFormation setups. The runtime identity is deliberately never
given `iam:CreateRole`, so it cannot make them.

## What I did

- **Setup scaffolding.** A setup step creates the build role, and for
deny egress an operator role, a loopback-only security group and a
network connector (through Cloud Control). It also writes a published
sandbox's remote-access grant onto the Remote Bindings role. Each object
is tagged, recorded in the deployment's runtime metadata and verified
before it is adopted.
- **Frozen sandboxes on the direct path.** Setup builds the image. A
setup re-run with a new bundle re-grants the role and rolls the image
once. The runtime never builds or deletes a Frozen image.
- **Guards.** Two Deny statements on the management identity block it
from writing to setup's sandbox roles and to its own role. The drift
check lets an update add either guard, since a Deny only narrows the
identity, but not remove one. An earlier version must accept stacks
carrying them, and a separate PR (#718) adds that tolerance on main. It
has to land first.
- **Updates that need setup are refused.** A runtime update that would
change scaffolding (a new sandbox, a different egress, network or build
policy, or publishing and unpublishing) is refused with the reason,
until setup runs again.
- **Teardown.** It deletes the image, then the connector, the security
group (after the connector's network interfaces are released), the roles
and the grant. Anything already gone is skipped.
- **Destroy after a failed setup.** When setup fails before the
management identity exists, and nothing runtime cleanup acts on has come
up, the manager skips runtime cleanup. The destroy goes straight to
setup teardown. Before this, it ended in `delete-failed`, and the build
role stayed in the account.

## Files touched

- `crates/alien-infra/src/setup_scaffolding/`: the scaffolding step, AWS
sandbox roles and egress, seeding and teardown.
- `crates/alien-infra/src/sandbox/aws.rs`: Frozen build, roll and
ownership rules on the direct path.
- `crates/alien-deployment/src/`: runs the scaffolding in initial setup,
refuses updates that need setup, and tears scaffolding down.
- `crates/alien-core/src/`: the runtime-metadata record and the build
role naming.
- `crates/alien-permissions/permission-sets/`: the two management
guards.
- `crates/alien-preflights/src/`: the build role name budget and the
guard exemption.
- `crates/alien-aws-clients/src/`: the Cloud Control, EC2 and IAM calls
the scaffolding needs.
- CloudFormation and Terraform generator snapshots, updated for the
guards.

## How I tested

- **Manually (us-west-2):** did a direct deployment with the local
control plane, using a Live and a Frozen sandbox with open egress.
- Setup created both build roles and built the Frozen image under setup
tags.
- A remote-access session on the Live sandbox started in 2.4s and ran a
probe.
- Setup re-run with a new Frozen bundle rolled that image exactly once.
- Added Live and Frozen `deny` sandboxes. Setup created their operator
roles, loopback-only groups and ACTIVE connectors. Sessions started and
TCP out was blocked.
- `alien-deploy destroy` removed every object setup and the runtime
created. The account inventory matched its pre-run baseline.
- With a standalone manager, `alien deploy` failed in setup after
creating the build role. `alien destroy` then reached `deleted` and
removed the role.
- **Unit tests:** `alien-infra` 741, `alien-deployment` 141,
`alien-manager` 263, plus preflights and core. Tests use strict mocks
for every IAM, EC2, Cloud Control and MicroVM call, and the behavioural
fixes are mutation-tested.
- **Anything I couldn't test:**
- Deny sessions were started with admin credentials; the compute grant
comes from `sandbox/management` and was checked by configuration only.
- Under `egress: deny`, DNS still resolves, because the security group
doesn't cover the VPC resolver. That gap also exists in the template
setups and is tracked separately.

I also ran a security review on the diff. What it checked:
- **The runtime minting a role:** `sandbox/provision` grants
`iam:PassRole` on the setup-made role only and never `iam:CreateRole`.
The management guards deny writes to setup's sandbox roles and to the
management role (`aws_management_role_guards` snapshot).
- **Adopting a role someone else made:** a same-named role is adopted
only if it carries setup's tags and its trust, inline and attached
policies match exactly. Otherwise setup refuses (`aws_sandbox.rs`
adoption tests).
- **Leaving a remote grant behind:** unpublishing requires setup, and
setup revokes the grant even when the stack no longer declares the
Remote Bindings role.
- **A destroy that skips cleanup it owes:** the rule returns nothing
once a Live resource, a synced secrets vault or a Frozen type with
runtime cleanup has come up. It also returns nothing on any resource
without a lifecycle (`deleting.rs` tests).
- **Deny egress holes:** the group has no ingress and egress only to
`127.0.0.1/32`, and a tagged group that allows more is repaired or
refused (`aws_sandbox_egress.rs` tests).
Nothing turned up.

Depends on #700 (base), #715 (image tagging) and the #712 → #713 → #714
chain (image read, build inputs, teardown). This branch merges #714 and
#715 and will rebase once they land.
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