Skip to content

Commit aa8790e

Browse files
committed
fix(sandbox): bound startup failures and preserve activation history
Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
1 parent 65dad0f commit aa8790e

16 files changed

Lines changed: 446 additions & 157 deletions

File tree

‎architecture/gateway.md‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -335,15 +335,15 @@ The storage schema is intentionally narrow:
335335
### Protobuf API and storage boundaries
336336

337337
Public RPC contracts and durable protobuf formats have separate ownership. The
338-
`openshell.v1.OpenShell` service currently has 74 RPCs. Their request and
338+
`openshell.v1.OpenShell` service currently has 75 RPCs. Their request and
339339
response roots, streaming flags, and transitive message closure come from the
340340
public descriptor set generated by `openshell-core`; a fingerprint test in
341341
`openshell-server` requires this inventory to be reviewed whenever it changes.
342342
Compute-driver, credential-driver, gateway-interceptor, and
343343
supervisor-middleware services are compiled contracts for internal extension
344-
boundaries, not public gateway RPCs. The current public inventory has 74
345-
methods, 278 messages, and 12 enums
346-
(`8ac68c71d93e6a5e56406b8df1882ee40c6066270969e03eb99803f0e6396fc1`).
344+
boundaries, not public gateway RPCs. The current public inventory has 75
345+
methods, 281 messages, and 13 enums
346+
(`068073560e3ae5164826a2cae4f72095cabaac5011516aedb3c72f6c78e2867b`).
347347
The removed `NetworkBinary.harness` field remains reserved by number and name,
348348
so protobuf implementations cannot reuse its wire slot or source identifier.
349349
The durable-policy compatibility decoder reads the former boolean before Prost
@@ -363,13 +363,21 @@ Go, Python, and TypeScript client generation inputs do not advertise them.
363363
| Public messages used directly as encoded storage roots | `Sandbox`, `SandboxWorkloadTemplate`, `Provider`, `Workspace`, `WorkspaceMember`, `SshSession`, `ServiceEndpoint` | The generated public type is also the persisted payload. `SshSession` is not in the current public RPC message closure. |
364364
| Embedded encoded root | `SandboxPolicy` | Stored in policy rows and inside the JSON settings envelope. |
365365

366-
The 12 encoded durable roots above have a closure of 81 messages and eight
367-
enums (`369b36511c2e38b9df9621704a00123516c7538d8ee89a499158d7de5cee1882`).
368-
Its intersection with the public RPC closure contains 71 messages and eight
369-
enums (`05add438ba041defc98d791038ae593d3f09352677cae43f2276d494205ce415`).
366+
The 12 encoded durable roots above have a closure of 82 messages and nine
367+
enums (`19ea13ee6ca6d84beb8e2b4aa3d6532fabd44b6e88c315879b23ffc4987266d3`).
368+
Its intersection with the public RPC closure contains 72 messages and nine
369+
enums (`68127e24cdb88b67433f68c4a1443c22a322f37ed1225b7547614a96f768cfac`).
370370
The descriptor-derived test owns these full inventories; the tables here record
371371
the reviewed roots and classifications.
372372

373+
Configuration admission adds `SandboxStatus.configuration_admission` at field
374+
10 and optional `configuration_activated` at field 11, extending the public and
375+
durable closures. New sandboxes explicitly store `false` until first acceptance;
376+
acceptance stores `true` permanently, including across restart. Legacy rows
377+
have neither field and conservatively retain static-policy restrictions. No
378+
database rewrite is required. A pre-admission byte fixture verifies that legacy
379+
phase and policy-version fields survive without fabricated admission or activation.
380+
373381
| Dual-purpose encoded root | Current decision |
374382
|---|---|
375383
| `Sandbox` | Defer a storage twin; govern its complete dependency closure as durable. |

‎architecture/sandbox.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,9 @@ condition, even when the container backend reports readiness. Gateway management
488488
operations remain available. Replacing the policy or repairing providers allows
489489
the same supervisor to reconcile and launch; it does not recreate the sandbox.
490490
Static policy fields can be replaced before the first accepted activation.
491+
A durable first-activation marker closes this repair window permanently, including
492+
across stop/start and later rejected configurations. Legacy records without the
493+
marker retain static-field immutability.
491494
Admission validates policy composition; image and host setup failures, such as
492495
an unresolved OCI user or unavailable isolation facilities, retain their existing
493496
startup error behavior.
@@ -497,7 +500,10 @@ provider-environment revision, and reporting supervisor instance. Startup captur
497500
the matching provider environment and constructs the runtime before reporting
498501
acceptance. Live reconciliation begins only after the main process has spawned,
499502
so it cannot replace the configuration captured for that launch. Restart resets
500-
admission and requires a fresh accepted configuration.
503+
admission and requires a fresh accepted configuration. Permanent gateway errors
504+
and exhausted transient retries terminate startup; only acknowledged configuration
505+
rejections wait indefinitely for repair. Process-sidecar discovery uses the
506+
existing sidecar readiness timeout.
501507

502508
Policy and provider refreshes are prepared before publication. Publication
503509
invalidates prior policy guards before exposing new provider material and swaps

‎crates/openshell-core/src/grpc_client.rs‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ use tonic::service::interceptor::InterceptedService;
3939
use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
4040
use tracing::{debug, info, warn};
4141

42+
/// Preserve the gRPC status as a source so callers can classify retryable errors.
43+
/// `IntoDiagnostic` alone hides the wrapped error's concrete type.
44+
pub fn grpc_status_error(status: Status) -> miette::Report {
45+
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
46+
#[error("{0}")]
47+
struct GrpcStatusError(#[source] Status);
48+
GrpcStatusError(status).into()
49+
}
50+
4251
/// Channel type after the [`AuthInterceptor`] is applied. Aliased so the
4352
/// generated client type signatures stay readable.
4453
pub type AuthedChannel = InterceptedService<Channel, AuthInterceptor>;
@@ -720,7 +729,7 @@ async fn fetch_settings_snapshot_with_client(
720729
sandbox_id: sandbox_id.to_string(),
721730
})
722731
.await
723-
.into_diagnostic()?;
732+
.map_err(grpc_status_error)?;
724733

725734
Ok(settings_poll_result(response.into_inner()))
726735
}
@@ -757,7 +766,7 @@ async fn sync_policy_with_client(
757766
..Default::default()
758767
})
759768
.await
760-
.into_diagnostic()
769+
.map_err(grpc_status_error)
761770
.wrap_err("failed to sync policy to server")?;
762771

763772
Ok(())
@@ -852,7 +861,7 @@ pub async fn report_sandbox_configuration(
852861
}),
853862
})
854863
.await
855-
.into_diagnostic()?;
864+
.map_err(grpc_status_error)?;
856865
Ok(())
857866
}
858867

@@ -875,7 +884,7 @@ pub async fn fetch_provider_environment(
875884
supports_static_credential_bindings: true,
876885
})
877886
.await
878-
.into_diagnostic()?;
887+
.map_err(grpc_status_error)?;
879888

880889
let inner = response.into_inner();
881890
Ok(ProviderEnvironmentResult {

0 commit comments

Comments
 (0)