Skip to content

feat(workspace): add workspace resource model with scoping, membershi…#2243

Open
derekwaynecarr wants to merge 13 commits into
NVIDIA:mainfrom
derekwaynecarr:decarr/workspace-model
Open

feat(workspace): add workspace resource model with scoping, membershi…#2243
derekwaynecarr wants to merge 13 commits into
NVIDIA:mainfrom
derekwaynecarr:decarr/workspace-model

Conversation

@derekwaynecarr

@derekwaynecarr derekwaynecarr commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements Phase 1 of RFC 0011 — the workspace and membership model that provides hard isolation boundaries for multi-player OpenShell deployments.

  • Workspace resource: CRUD RPCs (CreateWorkspace, GetWorkspace, ListWorkspaces, DeleteWorkspace) with a default workspace created on gateway startup for backwards compatibility. Graceful deletion via Kubernetes-style Terminating phase — DeleteWorkspace atomically marks the workspace Terminating via CAS (sets deletion_timestamp_ms on ObjectMeta and phase on WorkspaceStatus) before scanning for blockers, closing the TOCTOU race between blocker scan and resource creation. Once Terminating, create-path operations are rejected with FailedPrecondition. CAS conflict on concurrent deletes is handled by re-fetching and proceeding if already Terminating. No UpdateWorkspace RPC — workspaces are immutable after creation. Workspace labels persisted in selector index column for label_selector queries.
  • Workspace-scoped resources: Workspace field on ObjectMeta — sandboxes, providers, service endpoints, SSH sessions, policies, settings, provider refresh state, and inference routes all inherit workspace from context. In-memory SandboxIndex keyed by (workspace, name) to match persistence layer scoping.
  • Membership model: AddWorkspaceMember, RemoveWorkspaceMember, ListWorkspaceMembers RPCs with (workspace, principal_subject) → role records; membership records cleaned up on workspace deletion.
  • Persistence: Name uniqueness shifts from (object_type, name) to (object_type, workspace, name) via migration 006; existing resources backfilled to default workspace at the query index level (protobuf payloads are not re-encoded — see Breaking changes); cross-workspace list_by_type store method for infrastructure operations (reconciler, resume, provider refresh). New delete_by_scope store method for sandbox-owned record cleanup. update_message_cas enforces name and workspace immutability — mutation closures that attempt to change either field are rejected with an Encode error. Cross-workspace query ordering includes workspace and id tie-breakers for deterministic pagination. list_by_scope SELECT in both SQLite and Postgres includes resource_version column — previously omitted, causing deserialized records to report the default version instead of the authoritative DB value.
  • Provider profiles: Two-tier scoping — platform-scoped profiles (workspace="") are shared across all workspaces; workspace-scoped profiles are isolated per workspace. Independent scope listing with no merged cross-scope view; provider list-profiles --global lists platform-scoped profiles. resolve_profile_workspace preserves empty string for platform scope while resolve_workspace normalizes to "default". Platform-profile attachment checks scan all workspaces to detect consumers; dependency diagnostics qualify sandbox names with workspace/name for platform-scope scans to prevent dedup collisions. Scope-mismatched providers contribute their real token-grant bindings for ambiguity detection instead of being skipped or matched against the wrong profile scope. The EffectiveProviderProfileCatalog (snapshot_catalog) takes a workspace parameter so both read and write paths enforce workspace boundaries — UserProviderProfileSource loads platform-scoped profiles (workspace "") plus target workspace profiles, preventing cross-workspace duplicate profile ID collisions. Builtins and interceptor-provided profiles remain workspace-agnostic.
  • Service routing: Endpoint hostnames use {workspace}--{sandbox}--{service}.{domain} format for all workspaces including default. Workspace, sandbox, and service names capped at 19 characters to guarantee the combined DNS label fits within the 63-character RFC 1035 limit. Auto-generated sandbox names (petname) truncated to the routable limit with trailing hyphen cleanup.
  • Inference routes: Rename Cluster* RPCs and messages to Route* (SetInferenceRoute, GetInferenceRoute, InferenceRouteConfig); workspace-scoped route storage and lookup; derive workspace from sandbox principal for bundle resolution; thread --workspace through CLI inference commands.
  • Label serialization fix: All put_if call sites now serialize object_labels() into the persistence index column. Previously, most call sites passed None for labels, causing list_with_selector queries to miss resources even when the protobuf payload contained labels. Fixed across sandbox create, provider create, inference route upsert, SSH session create/revoke, workspace member create, stored provider profile import, and default workspace creation.
  • Atomic policy revision workspace fix: put_policy_revision_atomic INSERT statements (both SQLite and Postgres) now include the workspace column and bind. The AtomicPolicyRevisionWrite struct gains a workspace: String field threaded through both production callers (handle_update_config_inner, apply_merge_operations_with_retry). Previously the atomic path — introduced in commit 83003e8 for interceptors (feat(interceptors): initial gateway interceptor implementation and reference example #2005) — omitted workspace, causing atomic policy writes to persist with an empty workspace regardless of the sandbox's actual workspace. The non-atomic put_policy_revision was already correct. Regression test policy_atomic_write_persists_workspace verifies the workspace round-trips through the atomic path.
  • Proxy ancestor identity short-circuit: collect_ancestor_identities returns an empty vec immediately when start_pid == stop_pid (socket owner is the entrypoint). Previously it walked and hashed the full process ancestry above the entrypoint — desktop infrastructure binaries (cargo, IDE, shell) outside the sandbox trust boundary. No security regression: the connecting binary itself is still hashed by resolve_owner_identity, and ancestor verification only applies to intermediate processes between socket owner and entrypoint (there are none when they are the same PID). Fixes two proxy tests that consistently timed out at 30s due to hashing ~500MB+ of ancestor binaries in development environments.
  • ObjectWorkspace trait: requires_workspace() method with debug_assert! validation in store write helpers to catch empty-workspace bugs in debug builds.
  • Sandbox lifecycle: Sandbox deletion cleans up all owned records (SSH sessions, service endpoints, settings, policy revisions, draft chunks) and cleanup errors are fatal — sandbox deletion aborts if owned records cannot be removed, preventing orphan records from blocking workspace deletion.
  • Sandbox runtime: Workspace propagated from gRPC client via workspace_tx watch channel in steady-state poll loop; SSH session revocation writes back session workspace. Workspace watch channel shared with PolicyLocalContext so policy.local API clients (SubmitPolicyAnalysis, GetDraftPolicy) send the correct workspace for non-default workspace sandboxes. submit_proposal and open_lookup_session gate on non-empty workspace — return 503 with workspace_unavailable if the workspace watch channel has not yet been populated, preventing requests from being processed with an empty workspace before supervisor discovery completes. Denial and activity aggregators use a ready_gate closure checked before drain() — events are buffered until workspace discovery completes, with a warning emitted on shutdown if events must be dropped because workspace was never learned.
  • VM driver: sandbox_snapshot propagates workspace so watch events round-trip the correct workspace back to the server.
  • K8s driver: sandbox_from_object gates on managed-by=openshell label — CRs written by other actors in the namespace are silently skipped. Managed CRs missing required fields (orphans) log a warning and are skipped rather than hard-erroring. Label selector added to list, watch, get, delete, and exists ListParams so unmanaged CRs are filtered at the K8s API level. is_openshell_managed helper centralizes the ownership check.
  • Podman driver: list_containers API changed to accept &[&str] label filters. All ID-based lookups (find, exists, get) include the managed filter alongside the sandbox ID filter to prevent matching unmanaged containers with colliding labels. Workspace label lookup in driver_sandbox_from_inspect, driver_sandbox_from_list_entry, and error event construction changed from .unwrap_or_default() to .cloned()? — containers missing the workspace label are skipped instead of defaulting to empty string, preventing pre-upgrade containers from being reconciled with incorrect workspace attribution.
  • CLI: settings_to_json_sandbox includes workspace field in JSON output so sandbox settings correctly reflect the workspace context. --workspace and --all-workspaces flags on all resource commands; --all-workspaces intentionally takes precedence over --workspace (rather than erroring) because --workspace can be set via OPENSHELL_WORKSPACE env var. WORKSPACE column as first column in list outputs when --all-workspaces (matching kubectl --all-namespaces convention); -o json/-o yaml output always includes workspace field; --names output uses workspace/name format under --all-workspaces (matching kubectl -o name --all-namespaces). Workspace CRUD and membership subcommands; STATUS column in workspace list (Active/Terminating); inference help text updated from "gateway-level" to "workspace-level". provider create --global-profile uses profile_workspace consistently across all profile lookup paths (discovery, runtime credentials, from-existing). Last-used sandbox fallback keyed by (gateway, workspace) — saves workspace alongside sandbox name and only matches the requested workspace, preventing cross-workspace mis-targeting.
  • TUI: Workspace state with [w] key cycling, WORKSPACE column in sandbox/provider tables, workspace indicator in title bar, workspace-scoped gRPC requests. Initial workspace set from CLI --workspace / OPENSHELL_WORKSPACE (no longer hardcoded to "default"). Selection indices reset on workspace cycle. Post-create exec uses the creation workspace (carried in CreateResult) instead of the selected row's workspace. cycle_workspace resets current_workspace to "default" when entering all-workspaces mode to prevent silent cross-workspace creates. Sandbox and provider create operations blocked in all-workspaces mode with a status message — resources require a specific workspace target. reset_sandbox_state clears sandbox_workspaces and provider_workspaces vectors to prevent stale workspace data after state transitions. Provider profile refresh fetches profiles per distinct profile_workspace and caches by (workspace, type) — provider detail joins by unique ID instead of name to prevent cross-workspace misattribution.
  • Python SDK: Workspace parameter on all SandboxClient methods (create, get, list, delete, wait_deleted, exec, forward_tcp, create_ssh_session); InferenceRouteClient with workspace-scoped get/set/delete; Sandbox context manager accepts workspace and syncs to the session's resolved workspace after creation (so wait_ready/wait_deleted use the correct workspace when attaching via SandboxRef). _workspace access guarded with getattr fallback for session objects that lack the attribute (e.g. test doubles). Workspace-scoped listing via list(workspace=...) / list_ids(workspace=...) and cross-workspace listing via list_for_all_workspaces() / list_ids_for_all_workspaces() (follows Kubernetes two-method pattern — no "required but ignored" parameters). New WorkspaceClient with CRUD operations (create, get, list, delete) and WorkspaceRef dataclass; membership management deferred to Phase 2.
  • Rust SDK: WorkspaceScopedClient modeled after kube::Api::namespaced — captures workspace once and injects it into every sandbox request (create, get, list, delete, wait_ready, wait_deleted, exec). Constructed via OpenShellClient::workspace("name"). Workspace CRUD methods on OpenShellClient (create_workspace, get_workspace, list_workspaces, delete_workspace) and list_sandboxes_all_workspaces for cross-workspace listing. SandboxRef extended with workspace field populated from ObjectMeta.workspace. New WorkspaceRef type with name, phase, and labels. Proto re-exports for workspace request/response types.
  • Examples: Sandbox names shortened across all example scripts to fit within the 19-character routable name limit. SSH host alias format updated from openshell-{name} to openshell-{name}.{workspace} in vscode-remote-sandbox documentation.
  • CLI upload fix: The standalone sandbox upload command reimplemented upload logic inline with bugs (symlink handling, git-aware filtering). Replaced with delegation to the existing sandbox_upload() function.

Breaking changes

  • Service endpoint hostnames now include workspace prefix for all workspaces: {workspace}--{sandbox}.{domain} (previously {sandbox}.{domain}). The default workspace does not preserve the old format.
  • Workspace, sandbox, and service names limited to 19 characters (previously 253 for sandbox, 28 for service in routing context).
  • Kubernetes sandbox CRs now require sandbox-name and sandbox-workspace labels. Pre-existing managed CRs lacking these labels are silently skipped by the driver — the reconciler will treat them as disappeared and prune their database records. Operators must drain all running sandboxes before upgrading the gateway.
  • Podman container labels add openshell.ai/sandbox-workspace label for workspace tracking. This is intentional — pre-existing containers from before the upgrade will not be visible to the watcher and require a clean shutdown before upgrade.
  • SSH config host aliases use format openshell-{name}.{workspace} (previously openshell-{name}). Old entries are not automatically cleaned up.
  • Python SDK methods now require workspace keyword argument with no default — this is intentional to make workspace selection explicit. The CLI defaults to "default" via --workspace, but the SDK requires callers to be explicit about which workspace they operate in.
  • Inference RPCs renamed: SetClusterInferenceConfigSetInferenceRoute, GetClusterInferenceConfigGetInferenceRoute, ClusterInferenceConfigInferenceRouteConfig. This is a wire-breaking change — the inference route API is pre-GA with no deployed consumers of the old RPC names.
  • Last-used sandbox file format changed from single-line sandbox name to two-line workspace\nname. Old single-line files are ignored on load (user re-selects on first use after upgrade).
  • Pre-existing resources require re-creation after upgrade. Migration 006 backfills the indexed workspace column to "default" so existing records are queryable, but the protobuf payload is not re-encoded — decoded objects carry workspace="". CAS updates on migrated records will be rejected until the resource is deleted and re-created. This is intentional: the protobuf payload is authoritative for workspace identity at creation time, and re-encoding every blob during migration adds complexity for a pre-GA system with no production deployments to migrate.

Related Issue

#1977

Changes

  • New files: workspace.rs (gRPC handlers + tests), sandbox_index.rs updates, migration 006 (sqlite + postgres), workspace_lifecycle.rs (Rust e2e), test_workspace_api.py (Python e2e), RFC 0011
  • Modified: 4 example files (demo scripts and vscode-remote doc) for name-length and SSH alias format; 5 Rust SDK files (client, types, lib, raw, mock tests) for WorkspaceScopedClient and workspace CRUD
  • Proto: Workspace, WorkspaceMember messages; workspace CRUD and membership RPCs; workspace field on ObjectMeta; all_workspaces on list requests; inference route rename (Cluster* → Route*) with workspace fields on set/get request/response messages; WorkspacePhase enum, WorkspaceStatus message, deletion_timestamp_ms on ObjectMeta for graceful deletion

Testing

  • 1037 server unit tests passing (55 workspace-specific + 1 atomic policy workspace regression, including profile scope isolation, cross-workspace profile catalog isolation, inference route workspace isolation, graceful deletion, workspace labels, name/workspace immutability, workspace CRUD handler coverage, persistence isolation, list_by_scope resource_version hydration, policy_atomic_write_persists_workspace)
  • 964 supervisor-network tests passing (34 policy_local tests including workspace receiver plumbing and ready gates)
  • 106 podman driver tests passing (workspace label handling)
  • 72 bootstrap tests passing (13 last_sandbox tests including workspace-scoped isolation)
  • 39 SDK tests passing (16 unit + 23 mock integration, including WorkspaceScopedClient, workspace CRUD, SandboxRef.workspace field, list_sandboxes_all_workspaces)
  • 28 TUI tests passing
  • 85 Python e2e tests passing — includes workspace lifecycle, profile platform-vs-workspace scope isolation, cross-workspace profile ID collision test, inference route renames, sandbox CRUD with workspace args
  • 93 Rust e2e tests passing — workspace lifecycle (CRUD, Terminating phase rejection), bind mount with SELinux label, policy lifecycle, sandbox labels, all sandbox names within 19-char routable limit
  • 5 MCP conformance tests passing

Known remaining work (Phase 2)

  • Authorization enforcement: all_workspaces list authz, workspace membership checks on resource access, data-plane workspace verification on watch/exec/forward/SSH. Current isolation is name-based — any authenticated user who knows a workspace name can operate in it, matching the pre-workspace security posture where all authenticated users see everything.
  • Expanded role model: Platform Admin / Workspace Admin / User role definitions and enforcement
  • TODO(phase2) markers in code at each gap

Non-blocking follow-ups noted in review (no action needed for this PR)

  • Workspace records keyed by name with no UUID tie (orphan rebind on recreate — worth revisiting before Phase 2 makes membership records authorization data)
  • TUI workspace unit tests re-implement logic on local arrays (can't catch regressions in actual methods)
  • CLI --workspace defaults to "default" client-side; consider removing the default and relying on server-side resolve_workspace normalization, with client-side operations (last-sandbox, completers) resolving workspace from server responses
  • OCSF audit events do not include workspace — service routing events (endpoint config, HTTP rejection, relay failure) have workspace available but don't emit it. Add .unmapped("workspace", ...) as part of a broader workspace attribution pass across all OCSF event sites
  • Inference routes reference providers by name with no delete guard — deleting and recreating a provider silently rebinds inference traffic (pre-existing, not introduced by workspace changes)
  • Provider deletion doesn't acquire sandbox_sync_guard — attach racing delete can leave a dangling by-name attachment (pre-existing TOCTOU)
  • TUI profile refresh is sequential per workspace — with many workspaces, the awaited tick handler blocks the UI event loop for N_workspaces * timeout. Needs bounded concurrent fetches or a background task. Low practical impact pre-GA.
  • Per-sandbox service endpoint and SSH session cleanup is not paginated — current flat 1,000-record page could miss records in large workspaces (TODO added). Extremely low practical likelihood (requires >1,000 endpoints or sessions per sandbox).
  • Extract shared serialize_labels helper — the label serialization block is repeated across ~12 call sites; a shared helper in persistence/mod.rs would reduce duplication and prevent divergence
  • CLI confirmation prompts for destructive operations with workspace-wide blast radius (inference route delete, provider delete, workspace delete) — currently only global policy/settings operations have confirmation
  • Python SDK WorkspaceClient membership management (AddWorkspaceMember, RemoveWorkspaceMember, ListWorkspaceMembers) deferred to Phase 2
  • workspace member subcommands define their own --workspace arg which shadows the global --workspace flag — semantically correct (you're targeting a workspace to manage its members) but confusing when both appear on the same command line. Consider renaming the member-level arg to --name or having member commands consume the global --workspace instead.
  • Parent-child TOCTOU on resource creation — an in-flight create can pass its parent-exists/Active check, then the parent is deleted before the child is inserted, leaving an orphan. This is a pre-existing structural issue affecting all parent-child relationships (workspace→sandbox, workspace→provider, sandbox→SSH session, etc.), not specific to workspace changes. The ABA variant (delayed delete removes a recreated same-name parent) is fixed by versioned delete. The orphan-creation variant requires a persistence-layer conditional insert (e.g. put_if with a requires_parent subquery) to close generically; per-call-site double-checks are fragile. The Terminating phase narrows the window significantly for workspaces. Tracked for a dedicated persistence-layer follow-up.

Checklist

  • Code compiles without warnings (clippy clean)
  • All 1037 server unit tests pass (0 failures)
  • All 78 Python e2e tests pass
  • New tests added for workspace isolation (Rust handler + Python e2e)
  • E2e tests updated for workspace and inference route API renames
  • All e2e sandbox names shortened to fit 19-char routable limit
  • Bind mount e2e test fixed with SELinux private label for enforcing hosts
  • Workspace lifecycle e2e uses generic provider (not github --runtime-credentials)
  • Sandbox cleanup is fatal — orphan records cannot block workspace deletion
  • Platform-profile attachment checks scan all workspaces
  • Provider-profile scope filters symmetric for both platform and workspace scope
  • DNS label overflow prevented by 19-char name cap
  • SandboxIndex keyed by (workspace, name) — no cross-workspace collisions
  • Default workspace auto-created on gateway startup (handles post-migration path)
  • Policy.local API sends correct workspace for non-default workspace sandboxes
  • Flush tasks gated on non-empty workspace (no silent data loss during poll race)
  • TUI post-create exec uses creation workspace, not selected row
  • CLI last-used sandbox keyed by (gateway, workspace)
  • RFC 0011 documents design
  • Migration tested with SQLite and PostgreSQL schemas
  • K8s driver filters unmanaged sandbox CRs via managed-by label selector and in-code gate
  • VM driver round-trips workspace in sandbox snapshots
  • Service endpoints cleaned up on sandbox delete
  • TUI respects --workspace / OPENSHELL_WORKSPACE
  • Python SDK Sandbox context manager syncs workspace from session
  • Graceful workspace deletion with Terminating phase (CAS before blocker scan)
  • Workspace labels persisted in selector index column
  • Generated sandbox names truncated to routable limit
  • K8s and Podman driver ID lookups include managed-by label
  • K8s delete uses UID/resourceVersion preconditions (prevents TOCTOU on same-name replacement)
  • Podman stop/delete uses immutable container ID instead of mutable container name
  • Name and workspace immutability enforced in update_message_cas
  • provider list-profiles --global flag for platform-scoped profiles
  • Platform-profile dependency diagnostics qualified with workspace
  • E2e test for Terminating phase rejection of creates
  • Cross-workspace pagination ordering deterministic (workspace + id tie-breakers)
  • TUI profile joins scoped by workspace in all-workspaces view
  • CLI provider create uses profile_workspace consistently across all lookup paths
  • Label serialization fixed across all put_if call sites (sandbox, provider, inference route, SSH session, workspace member, provider profile import, default workspace)
  • Python SDK list / list_ids uses two-method pattern: workspace-scoped and _for_all_workspaces (K8s convention)
  • Python SDK WorkspaceClient with CRUD (create, get, list, delete) and e2e tests
  • E2e test for workspace-scoped vs cross-workspace sandbox listing
  • Cascade-delete comment documenting safety of pre-CAS route/member cleanup
  • Rust SDK WorkspaceScopedClient with sandbox CRUD, wait, and exec
  • Rust SDK workspace CRUD (create, get, list, delete) and list_sandboxes_all_workspaces
  • Rust SDK SandboxRef.workspace field and WorkspaceRef type
  • Rust SDK mock tests for all workspace-scoped operations (11 new tests)
  • Example sandbox names shortened to fit 19-char routable limit
  • Example SSH host aliases updated to openshell-{name}.{workspace} format
  • CLI sandbox upload delegated to existing upload function (bug fix)
  • Provider profile catalog (snapshot_catalog) workspace-aware — prevents cross-workspace duplicate ID collisions
  • list_by_scope includes resource_version in SELECT (SQLite + Postgres)
  • Podman watcher skips containers missing workspace label (no empty-string default)
  • TUI blocks sandbox/provider create in all-workspaces mode
  • Policy local workspace ready gate (503 before workspace discovered)
  • CLI settings JSON includes workspace field
  • E2e test for cross-workspace profile ID collision
  • put_policy_revision_atomic includes workspace column (SQLite + Postgres) with regression test
  • Proxy collect_ancestor_identities short-circuits when socket owner is entrypoint (fixes flaky 30s timeout tests)

@derekwaynecarr
derekwaynecarr requested review from a team, maxamillion and mrunalp as code owners July 13, 2026 12:23
@copy-pr-bot

copy-pr-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@derekwaynecarr
derekwaynecarr marked this pull request as draft July 13, 2026 12:26
@derekwaynecarr
derekwaynecarr force-pushed the decarr/workspace-model branch 3 times, most recently from 77ef1fc to 7bba756 Compare July 14, 2026 18:18
Comment thread crates/openshell-server/src/grpc/workspace.rs
Comment thread crates/openshell-server/src/grpc/mod.rs
derekwaynecarr added a commit to derekwaynecarr/OpenShell that referenced this pull request Jul 15, 2026
Fix bugs found during PR NVIDIA#2243 review: correct migration type strings,
resolve platform-scoped profile workspace handling, fix sandbox
workspace_tx single-fire, add missing workspace deletion blockers,
reset TUI row indices on workspace switch, fix TUI provider update
workspace, and update e2e Python tests for workspace and inference
route API changes. Add Phase 2 TODO comments for authorization gaps.
Fix pre-existing clippy errors (too_many_arguments, cast_sign_loss,
iter_on_single_items).

Signed-off-by: Derek Carr <decarr@redhat.com>
rhuss pushed a commit to rhuss/OpenShell that referenced this pull request Jul 16, 2026
Fix bugs found during PR NVIDIA#2243 review: correct migration type strings,
resolve platform-scoped profile workspace handling, fix sandbox
workspace_tx single-fire, add missing workspace deletion blockers,
reset TUI row indices on workspace switch, fix TUI provider update
workspace, and update e2e Python tests for workspace and inference
route API changes. Add Phase 2 TODO comments for authorization gaps.
Fix pre-existing clippy errors (too_many_arguments, cast_sign_loss,
iter_on_single_items).

Signed-off-by: Derek Carr <decarr@redhat.com>
@jhjaggars

Copy link
Copy Markdown
Contributor

Follow-up to my earlier review comment. That pass reviewed the diff file-by-file; this pass audited the PR along four cross-cutting axes instead: (1) workspace provenance at every store call site, (2) workspace provenance at every client request-construction site, (3) the full cross-object reference graph under delete/recreate, and (4) a cross-layer behavior matrix (server/CLI/TUI/SDK/three drivers). Same scope as before: fresh install assumed, upgrade paths out of scope, missing authz deferred to Phase 2. The new findings are mostly relational — they live between components rather than inside any one file, which is why the first pass missed them.

Additional must-fix candidates

1. Watch-adoption path persists driver-supplied workspaces verbatim — one ghost record halts gateway-wide reconciliation

The create branch of apply_sandbox_update_locked (crates/openshell-server/src/compute/mod.rs:1163-1196) clones incoming.workspace from the driver event and writes via raw put_if, bypassing the requires_workspace() guard that every typed write path enforces (persistence/mod.rs:431, 618, 744). Three feeders can deliver an empty workspace on a fresh install:

  • The VM driver drops workspace entirely when building watch/status snapshots (crates/openshell-driver-vm/src/driver.rs:4980-4994, ..Default::default()), so any delete/watch race re-adopts a sandbox with workspace: "".
  • Podman forwards "" for a container carrying openshell.managed=true + a sandbox-id label but no workspace label (driver-podman/src/watcher.rs:320-326) — reachable by manually labeling a container.
  • Docker does the same via unwrap_or_default() (driver-docker/src/lib.rs:2781-2784).

Consequences of the resulting ""-workspace record: it is unreachable by every workspace-scoped RPC ("" normalizes to "default" on lookup, so get/delete return NOT_FOUND), undeletable via the API, fails the requires_workspace guard on every subsequent update_message_cas — and because reconcile_store_with_backend propagates that error with ? before its prune loop (compute/mod.rs:1057-1086), one ghost record stops orphan pruning for the entire gateway. It surfaces only as a blank WORKSPACE cell under --all-workspaces. This also contradicts RFC 0011's stated fallback ("the sandbox is assigned to the default workspace").

Fix shape: normalize/validate the workspace at the adoption boundary (empty → "default", or refuse adoption) and fix the VM driver to round-trip workspace in snapshots.

2. Inference routes reference providers by name with no delete guard and no cleanup — silent traffic rebinding

delete_provider_record (crates/openshell-server/src/grpc/provider.rs:312-345) checks only sandbox attachments; inference routes neither block provider deletion nor get cleaned up, and resolve_route_by_name (inference.rs:984-996) re-resolves the provider by name on every bundle fetch. Sequence: inference set --provider foo → delete provider foo (succeeds) → recreate foo pointing at a different backend/credentials → every sandbox in the workspace transparently starts sending inference traffic through (and receiving the api_key of) the new provider, with no route update and no audit event.

Secondary defect on the same edge: while the route dangles, the ? in resolve_inference_bundle (inference.rs:913-917) turns one broken route into a failure of the entire bundle — a dangling route also takes down delivery of the other routes for the workspace.

The codebase's own invariant elsewhere (the sandbox-attachment guard) is that name references must not dangle, so this is a gap, not name-as-identity by design. Fix shape: include inference routes in the provider delete guard (or cascade-delete them), and make resolve_inference_bundle skip-and-log a dangling route instead of failing the bundle.

3. Provider deletion doesn't synchronize with attach — dangling attachment adopts a recreated provider's credentials

handle_delete_provider (provider.rs:2484-2493) never acquires sandbox_sync_guard, while handle_attach_sandbox_provider validates provider existence before taking the guard (grpc/sandbox.rs:356-369). An attach racing a delete leaves a by-name attachment in spec.providers pointing at nothing; recreating the provider under the same name silently injects the new provider's credentials into that sandbox. Same TOCTOU shape as the workspace-member race noted in the earlier comment, but on the edge that carries credentials. Fix shape: take sandbox_sync_guard in the delete path (or re-validate provider existence inside the guarded section of attach).

4. Service endpoints are never cleaned up on sandbox delete

cleanup_sandbox_owned_records (compute/mod.rs:1384-1405) removes SSH sessions, settings, policy revisions, and draft chunks — service_endpoint is absent from the list, and no other sandbox-delete path touches it. Orphaned endpoints then (a) permanently block workspace deletion via the "service" blocker in the DeleteWorkspace guard, with no way to remove them except DeleteService against a sandbox that no longer exists, and (b) after recreating a same-name sandbox, Get/ListServices present the stale endpoint (old target_port) as belonging to the new sandbox while routing 404s (the stored sandbox UUID fails closed — the one place the dual name+UUID pattern pays off). Fix shape: add service endpoints to the cleanup list.

Worth fixing, borderline blocking

  • TUI ignores --workspace / OPENSHELL_WORKSPACE entirely: openshell term --workspace team-ml parses successfully and is dropped — main.rs:3471 never passes cli.workspace and the TUI hardcodes "default" (openshell-tui/src/app.rs:891).
  • TUI provider create has the same wrong-workspace targeting as sandbox create (openshell-tui/src/lib.rs:1616): a provider — including its credentials — is silently created in the last-cycled workspace while the header shows "all". If the fix for the sandbox-create finding is generalized to cover both create paths, this comes for free.
  • Python SDK Sandbox context manager can rebind a SandboxRef across workspaces (python/openshell/sandbox.py:810-836): attaching with a ref from workspace A while the constructor says workspace="default" silently rebinds the session to a same-named default-workspace sandbox; exec/delete then target the wrong sandbox. Prefer ref.workspace when set, or reject a mismatch.
  • Sandbox settings can rebind across delete/recreate (race): settings are keyed by (workspace, name) with no sandbox back-pointer, and UpdateConfig (settings_mutex) and sandbox deletion (compute sync_lock) share no lock. A settings write landing after cleanup leaves a record that silently applies to the next same-named sandbox — since settings carry the agentic approval mode, a stale "auto-approve" attaching to a fresh sandbox is a policy-bypass shape.
  • Workspace-not-found and resource-not-found are indistinguishable by code, and two clients act on the wrong meaning: SDK wait_deleted("box", workspace="typo") treats any NOT_FOUND as successful deletion (sandbox.py:527-542); the CLI service-forward loop reports "sandbox no longer exists" on a workspace typo (run.rs:2983-2986).
  • OCSF audit events drop workspace even where it is already in hand: service_routing.rs:597 parses the workspace out of the Host header and discards it (_workspace) before emitting the HTTP-rejection event, and the endpoint ConfigStateChange events omit it despite endpoint.metadata.workspace being on the struct — denied requests against same-named sandboxes in different workspaces produce identical audit rows. Full attribution is Phase 5, but these sites already hold the value.
  • Podman discovery is not gateway-scoped: it filters on openshell.managed=true alone (driver-podman/src/container.rs:44-46) where Docker also filters on namespace. Two gateways sharing a Podman host ingest each other's containers — which compounds with item 1, since adoption skips workspace validation.

Non-blocking notes

Docs/help claim workspace names are DNS-1123/63 chars while the server enforces 19 (proto/openshell.proto:2023, main.rs:2013 vs grpc/workspace.rs:51-58); the SSH alias format implemented (openshell-{name}.{workspace}) differs from the format documented in RFC 0011; sandbox get detail output and provider JSON omit the workspace; cleanup_sandbox_ssh_sessions reads a single page of 1000 with no pagination loop (compute/mod.rs:1407-1430), unlike its sibling scans; race-orphaned UUID-scoped records (policy revisions, draft chunks, refresh state) are unreachable by any API yet block workspace deletion forever via the guard's safety net; the SDK accepts workspace="" without validation.

Phase 2 readiness (informational — no action needed in this PR)

Classified all 73 RPCs plus the data-plane paths for "can a membership check bolt on here." Good news: the resolve_workspace choke point makes ~40 request-scoped endpoints genuine bolt-ons (one mechanical change: thread the principal into it), and all nine TODO(phase2) markers map to real gaps — none stale. Three surfaces will need rework rather than a bolt-on: GetSandboxLogs tails by client-supplied sandbox_id with no resource fetch (already marked); the browser-facing HTTP service-routing proxy runs before auth with no principal at all (unmarked — worth a marker now); and the platform-scope operations (profile "" scope, global:true config/policy, GetGatewayConfig) have no carrier for a platform-admin role check. Bolt-on-ready but unmarked: ExecSandboxInteractive, RevokeSshSession, the bearer-user path of GetSandboxConfig, Set/GetInferenceRoute, and membership filtering on ListWorkspaces/GetWorkspace. Streams verify at establish time only, and SSH revocation does not sever live ForwardTcp tunnels.

What the audit confirmed clean

For completeness, the coverage side: all 118 production store call sites classified (72 via resolve_workspace/resolve_profile_workspace, 24 via trusted ID-based lookups, 13 justified platform-scope "", zero hardcoded "default" — the adoption path in item 1 was the single unjustified site); all 112 client request-construction sites across CLI/TUI/SDK/sandbox-runtime classified with no unjustified empty-workspace sends beyond the findings above; the full cross-object reference graph over all 14 record types tabled — the UUID-revalidated paths (service proxy, SSH forward) are fully delete/recreate-safe, and the DNS label math, hostname parse round-trip, and driver name-length backstops (docker 200, podman 255, k8s 63) all hold under the 19-char caps.

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

Automated workspace-scoping review of RFC 0011 Phase 1, focused on cross-workspace read/write confusion. Core control-plane read/write path scoped correctly; three actionable items below (one latent driver inconsistency + two hardening/upgrade notes).

Comment thread crates/openshell-driver-docker/src/lib.rs
Comment thread crates/openshell-server/src/persistence/mod.rs
Comment thread crates/openshell-server/src/persistence/mod.rs
@NVIDIA NVIDIA deleted a comment from russellb Jul 16, 2026
@NVIDIA NVIDIA deleted a comment from russellb Jul 16, 2026
@NVIDIA NVIDIA deleted a comment from jhjaggars Jul 16, 2026
@NVIDIA NVIDIA deleted a comment from jhjaggars Jul 16, 2026
@NVIDIA NVIDIA deleted a comment from rhuss Jul 16, 2026
@NVIDIA NVIDIA deleted a comment from jhjaggars Jul 16, 2026
@drew

drew commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Feedback from my agent review

[P1] Do not discard startup activity before workspace discovery

Locations:

  • crates/openshell-sandbox/src/lib.rs:364
  • crates/openshell-sandbox/src/lib.rs:409

The denial and activity aggregators drain a batch before invoking their flush callbacks. When the workspace is not yet known, these callbacks return successfully, so the drained events are permanently lost. This can drop policy proposals and activity telemetry during normal sandbox startup.

Buffer the batch until workspace discovery completes, wait on the workspace receiver before starting the aggregators, or return a retryable error so the aggregator retains the batch.

[P1] Preserve global profile scope throughout provider creation

Location: crates/openshell-cli/src/run.rs:4910

provider create --global-profile initially validates a custom profile using profile_workspace, but the --from-existing and --runtime-credentials paths subsequently fetch the profile using the provider's workspace. A valid global custom profile therefore fails unless a same-named workspace profile exists. If one does exist, discovery and credential validation can use that different profile while the persisted provider remains bound to the global profile.

Pass profile_workspace through every profile lookup involved in discovery and runtime-credential validation.

[P1] Prevent resource creation from racing workspace deletion

Location: crates/openshell-server/src/grpc/workspace.rs:223

DeleteWorkspace scans for blockers and deletes the workspace in separate operations. A concurrent resource creation can resolve the workspace and commit after the blocker scan, leaving a resource whose workspace no longer exists. Normal APIs will then refuse to resolve that workspace, making the resource effectively orphaned.

Use a persisted deleting state checked by resource creation, transactional exclusion, or another mechanism that closes the check/delete race.

[P2] Paginate sandbox-owned service cleanup

Location: crates/openshell-server/src/compute/mod.rs:1440

The new service-endpoint cleanup lists only the first 1,000 endpoints in the workspace and then deletes the sandbox. A sandbox whose endpoints fall after that page is deleted while its endpoints remain. Those orphaned records then block workspace deletion and can be presented as belonging to a later same-named sandbox.

Query endpoints by their persisted sandbox label, paginate until exhaustion, or store them under a sandbox-owned scope that supports delete_by_scope.

[P2] Keep Python workspace synchronization test-clean

Locations:

  • python/openshell/sandbox.py:813
  • python/openshell/sandbox_test.py:1555

The new unconditional self._session._workspace access breaks test_high_level_creation_forwards_name_and_labels: the high-level client double returns the public session shape used by this test but has no private _workspace field. The focused SDK run now fails with 74 passing tests and this one AttributeError.

Only resynchronize the workspace on the SandboxRef attachment path that needs it, or expose workspace through a stable session property and update the test double. The current head should not regress the existing Python unit suite.

[P2] Scope TUI profile joins by workspace

Locations:

  • crates/openshell-tui/src/lib.rs:1975
  • crates/openshell-tui/src/app.rs:2603

The all-workspaces provider view loads profiles only from current_workspace and applies them to every provider by provider.type. Provider-detail rendering then rejoins the fetched provider to cached entries using provider name alone. Same profile IDs or provider names in separate workspaces can therefore display another workspace's credentials, endpoints, discovery rules, and policy metadata.

Resolve profiles by (profile_workspace, profile_id) and identify providers by ID or (workspace, name).

[P2] Give all-workspace pagination a total ordering

Locations:

  • crates/openshell-server/src/persistence/postgres.rs:440
  • crates/openshell-server/src/persistence/sqlite.rs:476

Cross-workspace queries order results only by created_at_ms, name. Now that identical names may exist in separate workspaces, records created in the same millisecond can tie. Offset pagination over a non-total order can duplicate or omit resources, including during internal paginated scans.

Add workspace and id as deterministic tie-breakers to cross-workspace query ordering.

[P2] Persist workspace labels in the selector index

Location: crates/openshell-server/src/grpc/workspace.rs:118

CreateWorkspace places request labels in ObjectMeta but passes None for the persistence label column. ListWorkspaces.label_selector queries that persistence column, so it cannot find workspaces created with labels.

Serialize the already-validated workspace labels into the persistence label column before calling put_if.

[P2] Apply the new workspace-routing limit to generated sandbox names

Locations:

  • crates/openshell-server/src/grpc/validation.rs:138
  • crates/openshell-server/src/grpc/sandbox.rs:174

This PR lowers the explicit sandbox-name limit from 253 to 19 characters so workspace--sandbox--service fits in one DNS label. Sandbox-name validation still runs before petname generates a name for an unnamed request, so the server can persist a generated name that violates the new workspace-routing contract. Subsequent service operations reject that sandbox name even though the server generated it.

Validate the generated name and regenerate or fall back until it satisfies the new routable-name constraint.

[P2] Require managed-resource labels in driver ID lookups

Locations:

  • crates/openshell-driver-kubernetes/src/driver.rs:451
  • crates/openshell-driver-kubernetes/src/driver.rs:663
  • crates/openshell-driver-podman/src/driver.rs:601

Several get, existence, and deletion paths filter only by the OpenShell sandbox ID. They can select an unmanaged platform object carrying the same label, and deletion may act on that object.

Include the OpenShell managed-by label in every lookup, not only list/watch paths.

Lower-priority feedback

[P2] Enforce persistence workspace invariants

Location: crates/openshell-server/src/persistence/mod.rs:240

The new raw put_if interface accepts a workspace separately from an encoded typed payload without verifying that it matches ObjectMeta.workspace. A caller error can produce a record whose database identity and decoded resource identity disagree. Derive or verify workspace at the typed persistence boundary; do not duplicate the existing GitHub thread about requiring a non-empty workspace on raw writes.

[P3] Complete the global-profile CLI surface

provider profile export, import, update, lint, and delete expose --global, while provider list-profiles always lists the selected workspace. Add an explicit global listing option.

[P3] Qualify platform-profile dependency diagnostics

Global-profile validation and deletion diagnostics identify dependent sandboxes only by name and then deduplicate those names. For example, dependencies on both alpha/work and beta/work are reported only as work. Return workspace/name so the operator can locate every blocker.

@drew

drew commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

PR #2243 follow-up review

Principal-engineer verdict: request changes.

Reviewed the nine commits after the previous review, through head 0b1a19d5.

Prior findings

Disposition Findings
Addressed #1, #2, #5, #6, #8, #9, #10, #12, #13
Partially addressed #3 workspace deletion race, #7 total ordering
Not addressed #4 endpoint cleanup pagination, #11 raw persistence workspace invariant

Critical findings

  1. [P1] Workspace lifecycle remains TOCTOU/ABA-prone (CWE-362, CWE-367). Sandbox creation checks that the workspace is Active, performs several awaits, and only later inserts the sandbox. Deletion can mark, scan, and delete the workspace during that window, after which the creator inserts an orphan. The new test only starts creation after Terminating; it does not exercise an already in-flight creator. Concurrent deletions have a second ABA problem: their final membership cleanup and workspace delete use the name, so a delayed delete can remove a newly created same-name workspace. (active check, child insert, name-based deletion)

    Fix this at the persistence layer: child insertion and parent Active validation must share a transaction/row lock, while workspace deletion must validate and delete the original workspace UUID/version atomically. Add barrier-controlled race tests.

  2. [P1] Migration 006 does not actually backfill stored resources. The SQL updates only the indexed workspace column, while decode_record explicitly leaves the protobuf payload's workspace empty. Migrated objects therefore query under default but return and update as workspace=""; typed CAS writes reject that empty workspace. This contradicts the PR's "existing resources backfilled to default" compatibility claim and is a concrete manifestation of prior finding Example: Run common tools from CLI tracker #11: raw put_if still accepts independent workspace and payload values. (migration, explicit breaking behavior, typed rejection)

    Re-encode legacy payloads during migration or hydrate and validate protobuf metadata from the authoritative database row. Test upgrading an actual v005 database through read, update, reconciliation, and deletion.

  3. [P1] Existing Kubernetes sandboxes can be treated as missing and pruned from the gateway. Pre-PR managed CRs had the sandbox ID and managed-by labels but not the new sandbox-name/workspace fields. The new parser requires both; list silently drops failures and get converts them to None. Reconciliation can consequently delete the database record after the grace period while the Kubernetes workload remains running. (new parser/list behavior, required fields, pruning)

    Support legacy managed CRs using metadata.name and default, then patch the new fields, or provide an explicit upgrade migration.

  4. [P1] An inference route makes a workspace permanently undeletable. Workspace deletion considers InferenceRoute a blocker, but the public service and CLI expose only Set/Get/Update—there is no delete or unset operation. The first deletion marks the workspace Terminating, after which Set is rejected, leaving no supported recovery path. (blocker, Set/Get-only API)

    Add Delete/UnsetInferenceRoute that remains usable during termination, or cascade-delete route configuration.

  5. [P1] The 19-character limit still breaks first-party launchers. Gator's default is gator-YYYYMMDDHHMMSS, which is 20 characters and is passed directly to sandbox create. Several demos and published examples are longer still. The latest commit shortened tests but not these production paths. (Gator naming, prefix, validation)

Warnings

  • [P2] PostgreSQL filtered all-workspace pagination is still nondeterministic. The unfiltered query gained workspace/id tie-breakers, but list_all_with_selector still orders only by timestamp and name. (query)

  • [P2] Service cleanup still scans only the first 1,000 records. The follow-up adds a TODO, not a fix. A later endpoint survives and continues blocking workspace deletion. Adjacent: SSH-session cleanup has the identical first-page problem. (service cleanup, SSH cleanup)

  • [P2] The corrected TUI profile join can freeze the UI. It performs one sequential five-second profile-list request per distinct workspace from inside the awaited tick handler—up to roughly 500 seconds for 100 providers—and truncates each workspace at 100 profiles. Use bounded concurrent exact-profile fetches or a batch API. (refresh loop, awaited tick)

  • [P2] Python's default list semantics silently broadened. Existing client.list() callers now enumerate all workspaces instead of the default workspace. Use an explicit all_workspaces option and retain default as the compatibility default. The published label example also omits the newly required workspace argument. (SDK behavior, broken example)

Adjacent findings

  • [Adjacent P2] SSH-session creation reads a Ready sandbox and inserts the session independently of sandbox cleanup, allowing an orphan session to be inserted after its parent is deleted. This is another CWE-362 parent/child race. (creation)

  • [Adjacent P2] Kubernetes and Podman now locate a sandbox by immutable ID/labels but discard that identity and perform the destructive action by mutable name. A same-name replacement between lookup and delete can be affected instead (CWE-367). Retain Kubernetes UID/resourceVersion delete preconditions and use the Podman container ID. (Kubernetes, Podman)

Verification

  • Python: 83 tests passed.
  • Focused workspace server tests: 18 passed.
  • Focused aggregator tests: 4 passed.
  • TUI: 28 tests passed.
  • None of these exercises the critical race, upgrade, legacy-CR, inference-route deletion, or multi-workspace TUI latency paths.
  • The PR remains draft and conflicting; Branch Checks and Helm Lint were pending at the time of review.

Implements workspace and membership model providing hard isolation
boundaries for multi-player OpenShell deployments.

Workspace CRUD with Kubernetes-style Terminating phase for graceful
deletion. All resources scoped by workspace via ObjectMeta. Membership
RPCs for workspace access control. Persistence migration shifts name
uniqueness to (object_type, workspace, name). Provider profiles support
platform and workspace scoping. Service routing uses workspace-prefixed
DNS labels. Inference routes renamed and workspace-scoped with
DeleteInferenceRoute RPC. Python SDK with WorkspaceClient, two-method
list pattern (workspace-scoped and for_all_workspaces), and workspace
parameter on all methods. CLI workspace flags, TUI workspace cycling.
K8s driver filters unmanaged CRs and uses delete preconditions. Podman
driver uses immutable container IDs. Label serialization fixed across
all put_if call sites.

Signed-off-by: Derek Carr <decarr@redhat.com>
The standalone `sandbox upload` command reimplemented upload logic
inline with two bugs: it used `Path::exists()` which follows symlinks
(rejecting dangling symlinks), and it ran git-aware filtering on
symlink sources. The `run::sandbox_upload()` function already handles
both cases correctly via `sandbox_upload_plan()`. Replace the inline
logic with a call to the existing function.

Signed-off-by: Derek Carr <decarr@redhat.com>
Shorten the sandbox name in initial_sparse_policy_is_acknowledged_as_loaded
from 'e2e-2159-sparse-enrich' (22 chars) to 'e2e-sparse-enrich' (17 chars)
to comply with MAX_ROUTABLE_NAME_LEN (19 chars).

Also capture stderr in create_keep_with_args so future sandbox creation
failures include the actual CLI error instead of reporting empty output.

Signed-off-by: Derek Carr <decarr@redhat.com>
… isolation

Add unit tests for workspace create happy path, get round-trip, get
not-found, get empty-name rejection, already-exists error, and
resolve_workspace not-found. Add persistence test proving cross-workspace
name uniqueness (same name in different workspaces produces separate
records). Add workspace name max-length boundary tests. Fix e2e harness
to include stderr in name-parse-failure error path. Align Python e2e
test_workspace_crud with try/finally pattern. Document provider profile
catalog workspace scoping gap in RFC 0011.

Signed-off-by: Derek Carr <decarr@redhat.com>
Shorten sandbox names in demo scripts to fit the 19-character
MAX_ROUTABLE_NAME_LEN limit: policy-demo prefix to pd-, multi-agent
notepad derives a short SANDBOX_TAG from the run ID, governance
interceptor uses gs-PID-RANDOM. Update vscode-remote-sandbox.md SSH
host aliases from openshell-{name} to openshell-{name}.{workspace}
format.

Signed-off-by: Derek Carr <decarr@redhat.com>
Add WorkspaceScopedClient modeled after kube::Api::namespaced — captures
workspace once and injects it into every sandbox request. Add workspace
CRUD methods (create, get, list, delete) and list_sandboxes_all_workspaces
on OpenShellClient. Extend SandboxRef with workspace field and add
WorkspaceRef type. Include mock tests for all new operations.

Signed-off-by: Derek Carr <decarr@redhat.com>
Signed-off-by: Derek Carr <decarr@redhat.com>
@derekwaynecarr
derekwaynecarr force-pushed the decarr/workspace-model branch from 2410c7d to a0985f7 Compare July 19, 2026 04:19
@derekwaynecarr
derekwaynecarr marked this pull request as ready for review July 19, 2026 04:20
@derekwaynecarr

Copy link
Copy Markdown
Collaborator Author

/ok to test

@copy-pr-bot

copy-pr-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

/ok to test

@derekwaynecarr, there was an error processing your request: E1

See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/1/

@derekwaynecarr

Copy link
Copy Markdown
Collaborator Author

/ok to test a0985f7

Signed-off-by: Derek Carr <decarr@redhat.com>
Auto-format with cargo fmt and fix clippy warnings exposed by the
reformat: unnecessary qualifications, map_unwrap_or, identical match
arms, unused variable prefix, dead code annotations, and let-unit-value
in e2e harness.

Signed-off-by: Derek Carr <decarr@redhat.com>
@derekwaynecarr

Copy link
Copy Markdown
Collaborator Author

/ok to test 30136f7

- Add workspace field to settings JSON output (CLI)
- Skip Podman containers missing workspace label instead of defaulting
  to empty string, matching K8s driver behavior
- Add resource_version to list_by_scope SELECT in both SQLite and
  Postgres backends, with regression test
- Gate PolicyLocalContext proposal/lookup routes on workspace readiness,
  returning 503 when workspace is not yet discovered
- Block sandbox and provider creation in TUI all-workspaces mode
- Clear workspace vectors in TUI reset_sandbox_state

Signed-off-by: Derek Carr <decarr@redhat.com>
Thread workspace through snapshot_catalog so the
EffectiveProviderProfileCatalog enforces workspace boundaries on both
read and write paths. UserProviderProfileSource now loads platform-scoped
profiles (workspace "") plus the target workspace's profiles, preventing
cross-workspace duplicate profile ID collisions that previously caused
global catalog failures.

Update RFC 0011 to reflect catalog scoping is implemented in Phase 1
rather than deferred to future work.

Signed-off-by: Derek Carr <decarr@redhat.com>
…INSERT

put_policy_revision_atomic omitted the workspace column from the INSERT
into the objects table in both SQLite and Postgres backends, causing
atomically-written policy revisions to lose their workspace association.
Add workspace field to AtomicPolicyRevisionWrite and thread it through
both backend INSERT statements, matching the non-atomic put_policy_revision
path which already included it.

Signed-off-by: Derek Carr <decarr@redhat.com>
collect_ancestor_identities walked the entire process tree above the
entrypoint when the connecting process was the entrypoint itself,
SHA256-hashing every ancestor binary (IDE, shell, container runtime).
On dev machines with large binaries in the ancestor chain this exceeded
the 30-second test timeout. When start_pid == stop_pid there are no
intermediate ancestors to verify, so return an empty list immediately.

Signed-off-by: Derek Carr <decarr@redhat.com>
@derekwaynecarr

Copy link
Copy Markdown
Collaborator Author

/ok to test 1682948

@derekwaynecarr derekwaynecarr added the test:e2e Requires end-to-end coverage label Jul 19, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied for 1682948. Open the existing run and click Re-run all jobs to execute with the label set. The run will execute the standard E2E suite after building the required gateway and supervisor images once. The matching required CI gate status on this PR will flip green automatically once the run finishes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants