feat(agent-runtime): durable sandbox allocation, teardown-gated deletion, and Tensorlake integration - #431
feat(agent-runtime): durable sandbox allocation, teardown-gated deletion, and Tensorlake integration#431gabrik wants to merge 28 commits into
Conversation
… test
Add the /v1/agent-runs REST API as a thin wrapper over Temper's
existing TemperAgent IOA entity. The API exposes four endpoints:
POST /v1/agent-runs → create + configure + provision
GET /v1/agent-runs/:id → poll run status
POST /v1/agent-runs/:id/steer → inject steering message
POST /v1/agent-runs/:id/cancel → cancel the run
Handlers dispatch directly via ServerState::dispatch_tenant_action
— no self-referential HTTP round-trips.
IOA spec additions (temper_agent.ioa.toml):
- sandbox_provider, workspace_checkpoint, last_tool_batch_id,
run_budget, artifact_ref state fields for PoC tracking
Fixture repo (test-fixtures/agent-runtime-fixture/):
- Deliberately broken Python calculator (divide returns a*b)
- pytest test that fails on the bug
- README describing expected agent behavior
E2E test (os-apps/temper-agent/tests/agent_runtime_m1_e2e.sh):
1. Create run → poll until completion
2. Verify durable state via OData entity read
3. Verify fixture was fixed (divide = a/b, git diff exists)
4. Test Steer while active
5. Test Cancel and verify Cancelled terminal state
Add Tensorlake as a first-class sandbox backend alongside the existing
local and E2B providers, following the same branching pattern already
used for E2B detection.
sandbox_provisioner WASM module:
- New priority-0 branch: when sandbox_provider == "tensorlake",
calls Tensorlake REST API (POST /v2/sandboxes) to provision a
MicroVM sandbox with configurable image, resources, and timeout
- Requires tensorlake_api_key secret (stored in vault)
- Returns sandbox_url (ingress endpoint) and sandbox_id
tool_runner WASM module:
- Add is_tensorlake_sandbox() URL detection (same pattern as
is_e2b_sandbox())
- File operations (read/write/edit): Tensorlake uses /files?path=
like E2B envd, so both share the remote_files code path
- Process execution: Tensorlake uses plain HTTP /v1/processes/run
like the local sandbox (not Connect protocol like E2B)
- Fsync: uses GNU stat format (Linux) for both E2B and Tensorlake
workspace_restorer WASM module:
- Updated file write branch to use remote_files (e2b || tensorlake)
so restored files go to the correct API endpoint
IOA spec (temper_agent.ioa.toml):
- Add sandbox_provider and sandbox_image to Configure action params
- Add tensorlake_api_key and tensorlake_api_url to provision_sandbox
trigger config secrets
temper-sandbox crate:
- Add provider.rs with SandboxProvider trait, RunSpec, SandboxHandle,
ToolRequest, ToolEvent, CheckpointResult, and ProviderType
- Documents the canonical Rust contract that backends implement
/v1/agent-runs API:
- Add sandbox_provider (default: "local") and optional sandbox_image
fields to CreateRunRequest
- Forward both to Configure action params
E2E test:
- Add sandbox_provider: "local" to the create run request
… teardown
IOA spec additions (temper_agent.ioa.toml):
- New "Recovering" state in the state machine
- New actions: SandboxLost (Executing → Recovering),
SandboxRestored (Recovering → Thinking, triggers call_llm),
RestoreFailed (Recovering → Failed)
- Cancel action now triggers destroy_sandbox WASM module
- HandleToolResults params include last_tool_batch_id and
workspace_checkpoint for idempotency and recovery
- Fail/Cancel/Heartbeat/TimeoutFail from-lists include Recovering
- TurnCountNonNegative invariant includes Recovering
sandbox_destroyer WASM module (new):
- Triggered by Cancel action to tear down the sandbox
- Tensorlake: DELETE /v2/sandboxes/:id
- E2B: DELETE /sandboxes/:id
- Local: no-op (shared sandbox)
- on_failure = "Log" — teardown failure does not block Cancel
tool_runner WASM module:
- Idempotency: compute_batch_id() from sorted tool call IDs;
if batch_id matches last_tool_batch_id in entity state, return
cached result without re-executing tools
- Checkpoint: after tool batch, calls checkpoint_tensorlake()
(POST /v2/sandboxes/:id/snapshots) for Tensorlake sandboxes;
stores workspace_checkpoint ref in callback params
- Reads sandbox_id from entity state for checkpoint calls
Cedar policy (agent.cedar):
- Add SandboxLost, SandboxRestored, RestoreFailed to system callback
permits
- Add sandbox_destroyer to http_call and access_secret permits
- Add SandboxLost to supervisor/human/system permit
- New forbid: non-admin/non-supervisor/non-human principals cannot
Configure an agent with bash in tools_enabled
E2E test:
- Test 7: verify last_tool_batch_id is set on completed run
- Test 8: verify unauthorized requests (no admin principal) are
denied by policy for create and steer
build.sh:
- Add sandbox_destroyer to the WASM module build list
Wire the TemperAgent entity into Temper's existing workflow tracing
infrastructure so agent runs produce a single correlated trace tree
visible in Datadog APM.
workflow_tracing.rs:
- Add "TemperAgent" to should_use_workflow_root_span() so agent
runs get a dedicated root span (temper.workflow) with a stable
trace ID across all actions in the run lifecycle
- Update test to verify TemperAgent is in the workflow entity list
state/dispatch/mod.rs:
- record_workflow_span_attrs() now emits agent.run_id, agent.entity_type,
and agent.action tags when dispatching on TemperAgent entities
- These appear as Datadog APM tags for filtering agent run traces
agent_runtime/handlers.rs:
- Add #[tracing::instrument] spans to all four API handlers:
agent.run.create, agent.run.get, agent.run.steer, agent.run.cancel
- create_run records agent.run_id, agent.provider, agent.model
- get_run extracts the trace ID from the current span context and
returns it in the RunStatus response as trace_id
- Callers can use this trace_id to find the correlated trace in Datadog
WASM module telemetry (log_structured):
- sandbox_provisioner: emits sandbox.provision event with run_id
and sandbox_provider
- tool_runner: emits tool_batch_start event with run_id, batch_id,
and tool_count; emits tool.exec event per tool with tool_name
and tool_call_id
- sandbox_destroyer: emits sandbox.destroy event with run_id,
sandbox_id, and sandbox_provider
- These structured logs flow through the server's tracing pipeline
and appear as span events in Datadog
E2E test:
- Test 9: verify trace_id is returned in the RunStatus response
for Datadog correlation
Expected trace tree in Datadog APM:
temper.workflow (root, agent run lifecycle)
└─ agent.run.create (POST /v1/agent-runs)
└─ TemperAgent.Configure (dispatch)
└─ TemperAgent.Provision (dispatch)
└─ sandbox.provision (WASM log_structured)
└─ TemperAgent.SandboxReady (dispatch)
└─ TemperAgent.ProcessToolCalls (dispatch, turn=N)
└─ tool_batch_start (WASM log_structured)
└─ tool.exec (WASM log_structured, per tool)
└─ TemperAgent.HandleToolResults (dispatch)
└─ TemperAgent.CheckSteering → FinalizeResult (dispatch)
└─ agent.run.cancel or natural completion
Step-by-step guide for running the agent runtime PoC locally with the local sandbox (no Tensorlake API key needed). Covers: building WASM modules, starting the server, preparing the fixture repo, creating runs, polling, steering, cancelling, running the E2E test suite, and switching to Tensorlake when an API key is available.
…er_api_url Pre-existing bug: simulate_mock_hang() called temper_api_url(ctx) which doesn't exist. The function is resolve_temper_api_url(ctx, fields). Fixed to read entity fields and call the correct function.
The /v1/agent-runs handlers required Extension<AuthenticatedRequestContext> which is only set when a Bearer token is resolved against the credential registry. In local dev without TEMPER_API_KEY set, no context is created and requests get 401. Changed all four handlers to accept Option<Extension<...>> and added resolve_auth() which falls back to creating an admin context from the x-tenant-id header when no credential was resolved. This mirrors how the OData handlers use Option + require_authenticated_context.
Routes were registered at /v1/ instead of /v1/agent-runs, so all curl requests to POST /v1/agent-runs hit the fallback handler and got 401 instead of reaching the agent runtime handlers.
The global require_authenticated_request_context middleware rejects any request without an AuthenticatedRequestContext in extensions. In local dev (no TEMPER_API_KEY), no context is ever created so all /v1/agent-runs requests get 401 before reaching the handler. Added inject_local_dev_auth middleware on the /v1 sub-router that creates an admin AuthenticatedRequestContext from the x-tenant-id header if one doesn't already exist. In production with real credentials, the context is already set and this is a no-op.
The inject_local_dev_auth middleware on the sub-router doesn't work because parent router layers (require_authenticated_request_context) run before sub-router layers. The request gets 401 before our middleware ever executes. Instead, add /v1/agent-runs to is_public_kernel_request so the global auth check lets it through. The handler's resolve_auth() then creates the AuthenticatedRequestContext from x-tenant-id.
"Log" is not a declared action — the parser validates that on_failure references an existing action. This caused the entire temper-agent app bundle to fail loading, which is why TemperAgent was not registered as a governed entity type.
Without [[wasm_modules]] declarations in app.toml, the server's find_wasm_modules() returns empty and no WASM modules are uploaded. The provision_sandbox trigger then fails because the sandbox_provisioner module is not registered, causing the agent run to fail immediately at turn 0 during Provisioning.
The old model ID claude-sonnet-4-20250514 returns 404 from the Anthropic API. Updated default in both the IOA spec and the /v1/agent-runs API to claude-sonnet-4-5-20250929.
- API handler passes repo.url and repo.ref to Configure params - IOA spec adds repo_url and repo_ref state fields + Configure params - IOA spec adds github_token to provision_sandbox trigger config - sandbox_provisioner WASM: clone_repo_into_sandbox() runs git clone in the sandbox after provisioning, injecting github_token for private repos (https://x-access-token:TOKEN@github.com/...) - Works for local, Tensorlake (POST /v1/processes/run), and E2B (Connect protocol)
The server now reads GITHUB_TOKEN, TENSORLAKE_API_KEY, and
TENSORLAKE_API_URL from environment variables at startup and
caches them as platform secrets, same as ANTHROPIC_API_KEY and
EXA_API_KEY. This makes {secret:github_token} and
{secret:tensorlake_api_key} resolve in WASM integration configs
without needing the /api/secrets endpoint (which requires observe).
Secrets should be set via the API endpoint
PUT /api/tenants/{tenant}/secrets/{key_name} instead.
Same issue as /v1/agent-runs — the global auth middleware blocks /api routes without an AuthenticatedRequestContext. Added /api/tenants/ to is_public_kernel_request so secrets management works in local dev.
…aids Root cause of the persistent 401s: there are TWO auth layers. The outer one is temper-platform's bearer_auth_check (router.rs:57), which resolves Authorization: Bearer against the tenant's AgentCredential registry via IdentityResolver. It returns an EMPTY-body 401 — which is what curl -vv showed — whereas the kernel guard returns a JSON body. I had been patching the kernel guard, so the edits never affected the request. Reverted three band-aids that were wrong regardless of the 401: 1. authz/edge.rs: removed /v1/agent-runs and /api/tenants/ from is_public_kernel_request. Making these public disabled authentication on the agent-run API and on secrets management, and it silently broke PoC acceptance criterion nerdsane#6 (unauthorized request denied by policy). 2. agent_runtime/handlers.rs: removed resolve_auth(), which fabricated an admin SecurityContext from the x-tenant-id header. Reconstructing a principal from request headers is exactly what the typed-authority guard exists to prevent. Replaced with require_auth(), which requires the platform-resolved AuthenticatedRequestContext and returns 401. 3. Deleted agent_runtime/auth.rs (inject_local_dev_auth). It could never run: parent-router layers execute before nested-router layers. Also fixed a real authorization gap: the handlers dispatched with AgentContext::for_service("agent-runtime-api"), so Cedar evaluated a service principal rather than the caller. Added caller_agent_context(), mirroring the OData write path — the caller's SecurityContext is attached verbatim and identity fields are copied only for Agent/Admin principals. policies/agent.cedar: TEMPER_API_KEY bootstraps an "operator" AgentType credential (temper-platform/src/bootstrap.rs), so "operator" is now permitted for the agent-run lifecycle actions and exempted from the bash-tool forbid. The forbid still denies unprivileged agent types. Local usage is now the designed path: start with TEMPER_API_KEY set, then send Authorization: Bearer $TEMPER_API_KEY on every request.
The 403 on PUT /api/tenants/{tenant}/secrets/{key} was correct
default-deny, not a bug: api/secrets.rs:53 authorizes action
"manage_secrets" on resource type "Secret", and repo-wide the only
manage_secrets permit lived in a test fixture. No app shipped one, so no
principal could ever manage tenant secrets over HTTP.
Note the near-miss: agent.cedar:81 does mention "resource is Secret", but
for action "access_secret" — that is a WASM guest READING a secret at
integration time, which is a different action and does not grant the
management API.
Adds policies/secrets.cedar permitting manage_secrets for Admin and for
the operator/supervisor/human agent types. "operator" is the agent_type
of the credential bootstrapped from TEMPER_API_KEY. Resource shape follows
the authoritative test (crates/temper-server/tests/resource_authorization/
secret_authorization.rs): Secret::"<key_name>", with "__keys__" as the
collection id for the list endpoint.
Kept as a separate file with an explicit scope note: secrets management is
a platform/tenant-admin concern that only lives in this app because a
tenant's policy set is assembled from installed apps. It should move to a
platform-level policy seed rather than being copied into each app.
Your run failed with:
HTTP call failed: POST {secret:tensorlake_api_url}/v2/sandboxes
resolve_secret_templates leaves the literal pattern in place when a secret
is missing (secrets/template.rs: "Missing secret — leave the pattern
as-is"), so ctx.config.get(key) returns Some("{secret:NAME}") rather than
None. My M2/M3 code then called .unwrap_or_else(default), which never
fires, and the raw template reached the wire as a URL.
I had applied the repo's existing .contains("{secret:") guard to the API
keys but not to the URLs or the GitHub token. Four sites, same bug class:
sandbox_provisioner:582 tensorlake_api_url -> literal template as URL
sandbox_destroyer:121 tensorlake_api_url -> same, on Cancel teardown
tool_runner:945 tensorlake_api_url -> same, on checkpoint
sandbox_provisioner:685 github_token -> injected the literal
template into the clone URL as x-access-token:{secret:github_token}@,
which would have produced a confusing auth failure (and put the
pattern in logs) whenever github_token was unset
Also guarded e2b_api_url in sandbox_destroyer and tensorlake_image in the
provisioner, which had the same latent defect.
Adds a resolved_config() helper to each of the three modules, following the
convention already used at sandbox_provisioner:157 and llm_caller:2287:
filter out empty and {secret:-containing values so a missing secret falls
back to the documented default instead of corrupting the request.
The remaining ctx.config.get(...).unwrap_or_default() sites are API keys
that are immediately followed by an explicit .contains("{secret:") check,
so they were already correct and are left as-is.
Effect: tensorlake_api_url and tensorlake_image are now genuinely optional
(defaulting to https://api.tensorlake.ai and tensorlake/ubuntu); only
tensorlake_api_key must be stored.
Validates the real Tensorlake API shape against a live key before the WASM
integration is rewritten against it, so we stop discovering contract errors
one server round-trip at a time.
Every path and field is taken from the OpenAPI spec
(docs.tensorlake.ai/api-reference/openapi.yaml), which contradicts the M2
implementation in eight places:
control plane my M2 code actual
create POST /v2/sandboxes POST /sandboxes
resources flat cpus/memory_mb nested "resources"
network flat allow_internet_access nested "network"
destroy DELETE /v2/sandboxes/{id} DELETE /sandboxes/{id}
checkpoint POST .../snapshots POST .../snapshot (202)
readiness assumed immediate status=pending + poll
proxy files /files?path= (E2B shape) /api/v1/files?path=
proxy exec /v1/processes/run + workdir /api/v1/processes/run
parsed {stdout,stderr,exit} + working_dir, SSE stream
Polling is mandatory, not cosmetic: CreateSandboxResponse.ingress_endpoint is
declared nullable and status starts at pending, so the sandbox proxy is not
addressable until status == running and ingress is non-null. Hitting
api.tensorlake.ai with a proxy path instead returns LIFECYCLE_PATH_NOT_FOUND.
The probe covers create -> poll -> proxy exec (SSE) -> file write/read ->
snapshot -> delete, and always cleans the sandbox up via an EXIT trap.
Step 3 also answers what the spec cannot: whether the proxy accepts the same
bearer token, and whether the default managed image ships git/python3/pip3,
which the repo clone and fixture tests both require.
First probe run settled the control plane: POST /sandboxes with nested
resources/network returns 200, POST /sandboxes/{id}/snapshot returns 202
{snapshot_id,status:in_progress}, DELETE returns 200, and status was already
'running' ~1s after create.
It also surfaced a field the OpenAPI spec does not declare in
CreateSandboxResponse:
ingress_endpoint = https://sandbox.tensorlake.ai (SHARED host)
sandbox_url = https://<id>.sandbox.tensorlake.ai (per-sandbox)
Proxy paths on the shared host fail with LIFECYCLE_PATH_NOT_FOUND, so the
docs' advice to 'prefer ingress_endpoint over constructing *.sandbox
hostnames' does not hold for /api/v1/* routing. Note the original M2 code
read sandbox_url first and only fell back to ingress_endpoint, so that part
was already right; the earlier probe used ingress_endpoint and was wrong.
The rejection names two candidate routes, so steps 3-4 now try both and
report which one works:
(a) sandbox_url + no routing header
(b) ingress_endpoint + x-tensorlake-sandbox-id header
The file probe then reuses whichever base/header combination succeeded, so
one run pins down the proxy contract instead of another guess.
…ion, and Tensorlake integration
## Durable sandbox allocation before bootstrap
Split provisioning into two governed stages:
Provision → provision_sandbox → SandboxAllocated (Provisioning)
SandboxAllocated → bootstrap_sandbox → SandboxReady (Thinking)
The provider sandbox ID, URL, and actual provider are persisted
immediately after creation succeeds, before TemperFS bootstrap or
private-repository clone can fail. A failed bootstrap now retains
the provider resource identity needed for later teardown.
## Teardown-gated deletion lifecycle (ADR-0167)
Add DELETE /v1/agent-runs/{id} with a governed state-machine lifecycle:
Completed | Failed | Cancelled
→ RequestDeletion → Deleting
→ delete_sandbox (sandbox_destroyer WASM)
→ DeletionTeardownSucceeded → Deleted
DeletionFailed → RetryDeletion → Deleting
Deletion is strict: missing provider credentials or a missing sandbox ID
for a remote provider produces DeletionFailed, not Deleted. Provider 404
responses are treated as successful idempotent teardown. Cancellation
remains best-effort with an empty callback (no invalid second Cancel
transition from the terminal Cancelled state).
DELETE on an already-Deleted run Cedar-authorizes the caller via
authorize_with_context without creating an outgoing transition from
the terminal Deleted state.
A concurrent DELETE race is resolved by rereading authoritative actor
state only for known stale-dispatch errors, never masking Cedar denials.
## Tensorlake integration
- Clone private repos using x-access-token + GIT_ASKPASS (command-scoped
env injection, token never in URL or argv)
- Public repos clone without a token
- Readiness polling with bounded delay between attempts
- Provider 404 treated as already-absent during teardown
- E2B backward compatibility: historical runs with provider=local but
e2b.app URLs are still destroyed correctly
## Review fixes
- nerdsane#7: Remove invalid second Cancel callback from sandbox_destroyer
- nerdsane#8: Preserve provision_sandbox dispatch key; declare new config keys
in migration-differential test
- nerdsane#12: Remove dead provider abstraction from temper-sandbox
- nerdsane#5: Add bounded delay between readiness poll attempts
## Live E2E validation
A full Tensorlake agent run succeeded:
- Sandbox provisioned and cloned the private fixture repo
- Agent repaired the divide bug, ran pytest (4 passed), committed
- Run completed with status Completed
- Result: divide function fixed from a*b to a/b, all tests passing
## Validation
- IOA L0-L3 cascade: all passed
- Migration differential: 3 passed
- Destroyer unit tests: 3 passed
- Provisioner unit tests: 2 passed
- Handler tests: 4 passed
- All WASM modules compiled and built
- cargo check -p temper-sandbox (provider removed)
- cargo check -p temper-server
- git diff --check
CI Failure: Readability RatchetThe Fix #1 — Split
|
CI: Compile & Lint — Clippy failures (5 errors)All errors are in 1. The -> Result<(TenantId, AuthenticatedRequestContext), Box<Response>>(Update all 2. // before
return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e);
// after
return error_response(StatusCode::INTERNAL_SERVER_ERROR, e);(Two occurrences — lines 124 and 147.) 3. // before (line 126)
if let Ok(resp) = &configure_result {
if !resp.success {
let msg = resp.error.as_deref().unwrap_or("configure failed");
return error_response(StatusCode::BAD_REQUEST, msg);
}
}
// after
if let Ok(resp) = &configure_result
&& !resp.success {
let msg = resp.error.as_deref().unwrap_or("configure failed");
return error_response(StatusCode::BAD_REQUEST, msg);
}Same pattern at line 149 ( Run |
Summary
This PR implements durable sandbox allocation, teardown-gated deletion, and Tensorlake integration for the Temper agent runtime.
Durable sandbox allocation before bootstrap
Split provisioning into two governed stages:
The provider sandbox ID, URL, and actual provider are persisted immediately after creation succeeds, before TemperFS bootstrap or private-repository clone can fail. A failed bootstrap now retains the provider resource identity needed for later teardown.
Teardown-gated deletion lifecycle (ADR-0167)
Added
DELETE /v1/agent-runs/{id}with a governed state-machine lifecycle:Key properties:
DeletionFailed, notDeleted404responses are treated as successful idempotent teardownCanceltransition from terminalCancelled)DELETEon an already-Deletedrun Cedar-authorizes the caller without creating an outgoing transition from the terminal stateDELETErace is resolved by rereading authoritative actor state only for known stale-dispatch errors, never masking Cedar denialsTensorlake integration
x-access-token+GIT_ASKPASS(command-scoped env injection, token never in URL or argv)404treated as already-absent during teardownprovider=localbute2b.appURLs are still destroyed correctlyDead provider abstraction removed
Removed the unused
SandboxProvidertrait,RunSpec,SandboxHandle,ProviderType, and related types fromcrates/temper-sandbox/src/provider.rs. These had no tracked consumers; runtime behavior is implemented directly in the WASM modules.Live E2E validation
A full Tensorlake agent run succeeded against the private fixture repository
gabrik/agent-runtime-fixture:{ "status": "Completed", "error": null, "result": "## Summary\n\nI've successfully fixed the failing test! ...\n### The Fix\nChanged the `divide()` function to correctly perform division:\n- **Before**: `return a * b` (multiplication)\n- **After**: `return a / b` (division)\n### Test Results\n- **Before**: 1 failed, 3 passed\n- **After**: 4 passed ✅" }The agent:
a * b→a / b)How to test end-to-end
Prerequisites
wasm32-unknown-unknowntargetgabrik/agent-runtime-fixtureSteps
See
os-apps/temper-agent/AGENT_RUNTIME_POC_GUIDE.mdfor the full guide including local sandbox setup, steering, cancellation, and troubleshooting.Validation
cargo check -p temper-sandboxcargo check -p temper-servergit diff --checkFiles changed
docs/adrs/0167-agent-run-deletion-lifecycle.md— new ADRos-apps/temper-agent/specs/temper_agent.ioa.toml— allocation/bootstrap split, deletion lifecycle states/actionsos-apps/temper-agent/policies/agent.cedar— Cedar permits for new actionsos-apps/temper-agent/wasm/sandbox_provisioner/src/lib.rs— allocation/bootstrap split, Tensorlake clone, readiness pollingos-apps/temper-agent/wasm/sandbox_destroyer/src/lib.rs— strict deletion, fail-closed, 404 success, empty cancel callbackos-apps/temper-agent/wasm/tool_runner/src/lib.rs— Tensorlake API key injectioncrates/temper-server/src/agent_runtime/handlers.rs— DELETE route, race resolution, Cedar authorizationcrates/temper-server/src/agent_runtime/models.rs— DeleteRunResponse modelcrates/temper-spec/tests/migration_differential.rs— allowed config keys for new triggerscrates/temper-sandbox/src/provider.rs— removed (dead abstraction)crates/temper-sandbox/src/lib.rs— removed provider modulecrates/temper-sandbox/Cargo.toml— removed async-trait dependencyos-apps/temper-agent/AGENT_RUNTIME_POC_GUIDE.md— updated with bearer auth and Tensorlake instructionsGreptile Summary
This PR adds durable two-stage sandbox provisioning, teardown-gated run deletion, and Tensorlake-backed sandbox execution.
Confidence Score: 5/5
The PR appears safe to merge because no blocking failure remains.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR Created -->|Provision| Provisioning Provisioning -->|provision_sandbox| Allocated[SandboxAllocated] Allocated -->|bootstrap_sandbox| Thinking[SandboxReady / Thinking] Thinking --> Completed Thinking --> Failed Thinking --> Cancelled Completed -->|RequestDeletion| Deleting Failed -->|RequestDeletion| Deleting Cancelled -->|RequestDeletion| Deleting Deleting -->|DeletionTeardownSucceeded| Deleted Deleting -->|teardown failure| DeletionFailed DeletionFailed -->|RetryDeletion| DeletingReviews (3): Last reviewed commit: "chore: restore tracked codex spec file" | Re-trigger Greptile
Context used (4)