Repository navigation
Description
Context
The OpenShellSpawner has a working E2E flow (SDK migrated, parallel-safe, network policy auto-derived) but silently ignores several production features that other spawners implement. These features are passed by temporal_activities.py in every workflow and are critical for production use.
Cross-platform requirement: All features must work on both Podman and K8s deployments. No feature should silently fail on either target.
Gap Analysis
| Feature | Current State | Proper Solution |
|---|---|---|
skills_image |
Silently ignored | exec_stream() to copy skills into sandbox post-creation |
credential_secret_name |
Silently ignored | CreateProvider + AttachSandboxProvider raw gRPC RPCs |
read_only |
Silently ignored | SandboxPolicy.filesystem.read_only path list |
mcp_secret_mounts |
Silently ignored | _do_write_file() to inject secret files post-creation |
tls_certs |
Silently ignored | Not needed — gateway provides transport security; log info + skip |
service_account |
Silently ignored | No OpenShell equivalent; log info + skip |
| JWT workaround | Stop/copy/restart hack | Upstream fix needed — document as known limitation |
Plan
1. Skills image support (cross-platform)
Add _load_skills() method called inside _do_spawn() after sandbox creation.
Step 1 — Extract skills from image (platform-specific):
- Podman mode (
podman_cliset):podman run --rm -v /tmp/skills-<name>:/out <skills_image> cp -r <paths> /out/ - K8s mode (
podman_cliis None): Usecrictlor spawn a short-lived K8s Job to extract skills to a temp dir
Step 2 — Stream into sandbox (platform-independent):
- Primary:
exec_stream(sandbox_id, ["tar", "xf", "-", "-C", "/app/skills"], stdin=tar_bytes) - Fallback (if
exec_streamstdin not supported by SDK):_do_write_file()per-file with recursive directory walk
Pre-implementation: Verify exec_stream supports stdin bytes in the ExecSandboxRequest proto. If not, use _do_write_file() fallback from the start.
Test: Verify skills_paths subdirectory selection is preserved (matching K8s/Podman spawner contract).
2. Credential injection via OpenShell Provider API
Add _inject_credentials() using raw gRPC (same pattern as _expose_service()):
- Derive credentials map from
envdict:{credential_secret_name: env[credential_secret_name]} CreateProvider(type="cloud-agents", credentials={...})AttachSandboxProvider(sandbox=sandbox_name, provider=provider_id)- Track
provider_idinself._provider_ids: dict[str, str]for cleanup _do_destroy()callsDetachSandboxProviderand cleans up idempotently
Fallback chain (if Provider RPCs unavailable in proto):
- Write credential files to
/var/run/secrets/llm-credentials/via_do_write_file() - Set matching env vars (
GOOGLE_APPLICATION_CREDENTIALS=/var/run/secrets/llm-credentials/..., etc.) - For simple API key providers: set env var directly
- Fail closed for providers that require file-backed credentials if file write fails — no silent degradation
Pre-implementation: Verify CreateProvider/AttachSandboxProvider exist in openshell._proto.openshell_pb2.
3. Read-only filesystem via SandboxPolicy
When read_only=True in _do_spawn():
- Set
spec.policy.filesystem.read_only = ["/"] - Set
spec.policy.filesystem.read_write = ["/tmp", "/home/agent", "/var/log", "/app/skills/", "/var/secrets/mcp/", "/var/run/secrets/llm-credentials/"] - Set
spec.policy.filesystem.include_workdir = True - Uses the same
spec.policyobject where_build_network_policy()already populates network rules
The read_write list includes all post-create injection targets so that skills loading, MCP secret injection, and credential writes succeed in advisory mode.
4. MCP secret mounts via file injection
Add _inject_mcp_secrets() method:
- For each
(secret_name, key, mount_path)tuple, resolve the secret value from runner env - Create parent dirs:
exec_stream(sandbox_id, ["mkdir", "-p", mount_path]) - Write to
f"{mount_path}{key}"(mount_path is the directory, not the file) - Matches the existing contract where
LIGHTSPEED_MCP_SERVERSheaders referencefile:/var/secrets/mcp/<server>/<key>
5. TLS certs — skip with info log
When tls_certs is not None, log info: "TLS certs not needed for OpenShell — gateway provides transport security." Use port 8080 (not 8443). Per spike doc analysis, the gateway terminates TLS at the gRPC boundary.
6. Service account — skip with info log
Log info that OpenShell manages identity. No K8s SA equivalent needed.
7. JWT workaround — document as known limitation
Keep _inject_podman_token() workaround. Add docstring noting upstream fix path: OpenShell Podman driver should use secret_env instead of file-based secret mounts to work around the Podman 5.8.x REST API bug. Not fixable in our code.
8. Tests
TestSkillsLoading— verify extraction + streaming, both Podman and K8s paths,skills_pathssubdirectory selection preservedTestCredentialInjection— verify gRPC CreateProvider + AttachSandboxProvider called, provider_id tracked for cleanup; test file-based fallback for VertexTestReadOnlyFilesystem— verify SandboxPolicy.filesystem populated with full read_write allowlist including injection targetsTestMCPSecretInjection— verify mkdir + write tof"{mount_path}{key}", not mount_pathTestTLSSkipped— verify info log, port remains 8080TestServiceAccountSkipped— verify info log
Verification
- Unit tests pass on all 8 items
- E2E on EC2 with OpenShell gateway: workflow with skills_image + credentials
- Read-only mode sets filesystem policy correctly (visible in gateway logs)
- MCP secret files written to correct paths and readable by sandbox agent
- Vertex-style file-backed credentials work end-to-end
Activity
beesarmy commentedon Jul 11, 2026
Triage
Summary: Close production gaps in OpenShellSpawner — 7 features currently silently ignored need proper handling (skills, credentials, filesystem policy, MCP secrets, TLS, service account, JWT workaround).
Affected components:
src/cloud_agents/spawner/openshell_spawner.py— all 7 gaps are in_do_spawn()signature (lines 484-490) and_do_write_file()(line 734)src/cloud_agents/workflow/temporal_activities.py— passes all features (lines 426-433)tests/unit/spawner/test_openshell_spawner.py— new test classes neededRelated issues/PRs:
Suggested labels:
enhancement,area/spawnerPlan review:
_do_spawnlines 484-490) and activity layer (lines 426-433)_do_write_file()already exists at line 734 — reusable for MCP secret injection_expose_service()approachspec.policyobject as_build_network_policy()— clean extensionexec_stream+taris the most complex item. The spawner would need to pull the skills image locally first (via Podman or container runtime), then stream contents into the sandbox. Worth specifying whether this uses the host's Podman CLI (like_inject_podman_token) or the OpenShell SDK.CreateProvider/AttachSandboxProvider) — confirm these RPCs exist in the OpenShell proto package. The issue states "no Python SDK wrapper" but doesn't confirm the proto messages are available atopenshell._proto.openshell_pb2.Non-blocking⚠️ only. Plan approved.
🤖 Triaged by beesarmy via Claude Code
jameswnl commentedon Jul 11, 2026
One thing I'd tighten in the plan:
mcp_secret_mountscurrently carries(secret_name, key, mount_path)wheremount_pathis the directory/var/secrets/mcp/<server>/, whileLIGHTSPEED_MCP_SERVERSencodes the header file ref as/var/secrets/mcp/<server>/<key>. So_inject_mcp_secrets()cannot just call_do_write_file()onmount_path; it needs to write tof"{mount_path}{key}"(or change the tuple contract), and the helper should create parent dirs first because those directories do not exist today.Without that, the plan matches the high-level intent but misses the current activity/spawner contract details.
jameswnl commentedon Jul 11, 2026
Related advisory-mode gotcha: if we implement post-create injection for skills and MCP secrets, the
SandboxPolicy.filesystem.read_writeallowlist probably needs to include the destinations we write after sandbox creation, not just/tmp,/home/agent, and/var/log.Today the planned writes would target at least
/app/skillsand/var/secrets/mcp/...; if credential files end up under/var/run/secrets/llm-credentials/, that path likely belongs there too. Otherwise the copy/injection helpers may work in normal mode but fail specifically whenread_only=True.jameswnl commentedon Jul 11, 2026
On credential providers, I think the plan needs one more contract decision before implementation.
temporal_activities.pypassescredential_secret_name=_to_k8s_secret_name(provider.get("credentials_secret")), but the actual secret value is only present inenvunder the original key (for exampleOPENAI_API_KEY). For Provider RPCs we likely need a concrete credentials map and maybe multiple keys, not just the normalized secret-name string.I'd either (a) derive the provider payload explicitly from
envin the spawner and document which keys are forwarded, or (b) change the spawn contract to pass a credential payload object instead of onlycredential_secret_name. Also worth tracking attachedprovider_ids separately so spawn-failure cleanup and_do_destroy()can detach/delete them idempotently.beesarmy commentedon Jul 11, 2026
Re-triage (author update)
Three good refinements that tighten the plan:
MCP secret mount path — correct.
mount_pathis the directory, file needsf"{mount_path}{key}". Parent dir creation needed. Will note for implementation.Read-write allowlist for advisory mode — valid catch. When
read_only=True, thefilesystem.read_writelist needs/app/skills/,/var/secrets/mcp/, and credential paths in addition to/tmp,/home/agent,/var/log. Otherwise post-create injection fails specifically in advisory mode.Credential contract — the most architectural point.
credential_secret_nameis just the K8s secret name, but the Provider API needs actual key-value pairs. Two options:envin spawner — spawner knows which env vars are credentialsOption (a) is more pragmatic (no ABC change), option (b) is cleaner long-term. Either way,
provider_idtracking for cleanup is essential.Plan remains approved with these refinements baked in before coding.
🤖 Triaged by beesarmy via Claude Code
jameswnl commentedon Jul 11, 2026
Plan update — incorporating feedback
Refinement 1: MCP secret mount path (jameswnl)
mount_pathis the directory (/var/secrets/mcp/<server>/), not the file. The write target isf"{mount_path}{key}"._inject_mcp_secrets()will:exec_stream(sandbox_id, ["mkdir", "-p", mount_path])f"{mount_path}{key}"(notmount_path)Refinement 2: Read-write allowlist for advisory mode (jameswnl)
When
read_only=True,filesystem.read_writemust include all post-create injection targets:/tmp,/home/agent,/var/log(agent workspace)/app/skills/(skills injection)/var/secrets/mcp/(MCP secret injection)/var/run/secrets/llm-credentials/(credential files, if applicable)Otherwise post-create writes fail specifically in advisory mode.
Refinement 3: Credential contract (jameswnl + beesarmy)
Going with option (a) — derive from
envin spawner:_do_spawnalready receives the fullenvdict which contains the actual credential values (e.g.,OPENAI_API_KEY=sk-...)credential_secret_nametells us which env key holds the credential{credential_secret_name: env[credential_secret_name]}provider_idinself._provider_ids: dict[str, str]for cleanup_do_destroy()callsDetachSandboxProvider+ optionally deletes the providerNo ABC change needed — keeps the existing spawn contract.
beesarmy⚠️ : Skills loading host runtime
Skills image extraction will use the host's Podman CLI (same as
_inject_podman_token):podman run --rm -v /tmp/skills-<name>:/out <skills_image> cp -r <paths> /out/exec_stream(sandbox_id, ["tar", "xf", "-", "-C", "/app/skills"], stdin=tar_bytes)podman_cliis None (K8s mode), fall back to_do_write_file()per-filebeesarmy⚠️ : Proto confirmation
Will verify
CreateProvider/AttachSandboxProviderexist inopenshell._proto.openshell_pb2before implementation. If missing, fall back to env var injection with a logged warning.🤖 Updated by Claude Code
jameswnl commentedon Jul 11, 2026
Plan update — cross-platform requirement
Skills loading must work on both Podman and K8s deployments. Updated approach:
Cross-platform skills loading:
podman_cliset):podman run --rm -v /tmp/skills-<name>:/out <image> cp -r <paths> /out/podman_cliis None): Usecrictlor spawn a short-lived K8s Job to extract, or bundle skills as a tar in the runner imageexec_stream(sandbox_id, ["tar", "xf", "-", "-C", "/app/skills"], stdin=tar_bytes)Step 3 is identical regardless of deployment — only the extraction method (step 1) varies by platform. This is the same pattern as the existing spawner ABC where
_do_spawnimplementations differ but the contract is the same.This cross-platform principle applies to all features in this issue — no feature should silently fail on either deployment target.
🤖 Updated by Claude Code
beesarmy commentedon Jul 11, 2026
Re-review — plan-approved removed
Removing
plan-approvedper user request. The plan is missing K8s deployment considerations.K8s deployment gaps
The entire plan assumes the OpenShell gateway runs with the Podman driver (DooD pattern on RHEL). But OpenShell also supports a K8s driver (line 77 of spike doc: "K8s driver requires
agents.x-k8s.ioCRD installed in the cluster"). The plan needs to address how each feature works when OpenShell runs on K8s:Skills image — The plan proposes extracting skills via Podman CLI +
exec_streamtar. On K8s, this Podman CLI approach won't work. The K8s spawner uses init containers (skills_imageas aninitContaineratkubernetes_spawner.py:167-185). OpenShell's K8s driver may support init containers or volume mounts natively — this needs investigation.Credential injection — The K8s spawner mounts credentials via K8s Secrets projected into pods (
kubernetes_spawner.py:222-241). The plan proposes raw gRPCCreateProvider/AttachSandboxProviderRPCs. Does the K8s driver handle Provider credentials differently than the Podman driver? Are secrets mounted as volumes or injected as env vars?MCP secret mounts — The K8s spawner uses K8s Secret volume mounts. The plan proposes
_do_write_file()post-creation injection. On K8s, secrets might be better handled via projected volumes (same pattern ascredential_secret_name).JWT workaround — The
_inject_podman_token()workaround is correctly noted as "No-op on K8s path" (spike doc line 151). But the plan should explicitly state what happens on K8s: does the K8s driver handle JWT injection via projected SA tokens automatically?Network policy — The
_build_network_policy()usesSandboxPolicy. Does the K8s driver enforce network policy differently (e.g., via K8s NetworkPolicy resources instead of the sandbox policy object)?Service account — The plan says "No K8s SA equivalent needed" but the K8s spawner passes
service_accountto pod specs (kubernetes_spawner.py:46). If OpenShell's K8s driver creates pods, it may need SA configuration.What the plan should add
The plan is solid for Podman-only deployments but needs K8s driver coverage before approval.
🤖 Triaged by beesarmy via Claude Code
beesarmy commentedon Jul 11, 2026
Re-review — K8s gaps assessment
The author's plan updates (04:25 and 04:28) were posted before my K8s re-review (04:29) — they already address most gaps. Evaluating:
podman_cliset, K8s fallback when not. Step 3 (exec_streamtar) is universal regardless of driver.envdict. Driver-agnostic sinceenvis always available. No K8s-specific projected volumes needed.exec_streamfor mkdir +_do_write_file. Driver-agnostic — OpenShell's exec works identically on both drivers.SandboxPolicyis an OpenShell API object applied by the gateway — works identically regardless of K8s or Podman driver.The cross-platform principle ("no feature should silently fail on either deployment target") is the right framing.
Remaining minor gap: K8s skills extraction path is vague ("use
crictlor spawn a short-lived K8s Job"). This can be refined during implementation — the architecture (extract → tar →exec_stream) is sound and the Podman path is well-specified.Plan approved.
🤖 Triaged by beesarmy via Claude Code
jameswnl commentedon Jul 11, 2026
Another plan-risk to pin down before implementation: the updated skills flow now depends on
exec_stream(sandbox_id, ["tar", "xf", "-", "-C", "/app/skills"], stdin=tar_bytes), but this repo never usesexec_streamwith stdin today. All currentOpenShellSpawnercall sites only doexec_stream(sandbox_id, cmd, env=...), and the spike docs only demonstrate output-oriented exec paths.So I’d verify the Python SDK + raw proto layer actually supports streaming stdin in the sync-iterator API before committing to the tar approach. If not, the plan needs a different transport helper for archive upload. I’d also add a test that proves
skills_pathssurvives the new path, since preserving subdirectory selection is part of the existingKubernetesSpawner/PodmanSpawnercontract.jameswnl commentedon Jul 11, 2026
I’d also avoid the current fallback of "if Provider RPCs are missing, fall back to env var injection with a logged warning". That is probably acceptable for direct API-key providers, but it breaks the file-based credential contract used by Vertex in
lightspeed-agentic-sandbox:GOOGLE_APPLICATION_CREDENTIALSis expected to point at/var/run/secrets/llm-credentials/GOOGLE_APPLICATION_CREDENTIALS, and the sandbox spec/docs call out that file mount as required.So if Provider RPCs are unavailable, I think the safer fallback is either (a) write the credential files into
/var/run/secrets/llm-credentials/yourself and set the matching env vars, or (b) fail closed for providers that require file-backed credentials. A warning-only env fallback would violate the updated "no feature should silently fail on either deployment target" rule.jameswnl commentedon Jul 11, 2026
Plan update — addressing exec_stream stdin + credential fallback
1. exec_stream stdin verification
Valid concern. The current code only uses
exec_stream(sandbox_id, cmd, env=...)for output-only paths. Before implementing the tar approach:exec_streamsupportsstdinbytes in the SDK proto layer (ExecSandboxRequestmessage)_do_write_file()per-file with recursive directory walk. Slower but uses the proven code path already at line 734skills_pathssubdirectory selection is preserved (matching K8s/Podman spawner contract)2. Credential fallback — file-based, not env-only
Agreed. Env-only fallback breaks Vertex (
GOOGLE_APPLICATION_CREDENTIALSrequires a file at/var/run/secrets/llm-credentials/). Updated fallback chain:If Provider RPCs are available:
CreateProvider+AttachSandboxProvider(gateway-managed)If Provider RPCs are unavailable:
/var/run/secrets/llm-credentials/via_do_write_file()GOOGLE_APPLICATION_CREDENTIALS=/var/run/secrets/llm-credentials/..., etc.)This satisfies the "no feature should silently fail" cross-platform rule.
🤖 Updated by Claude Code
beesarmy commentedon Jul 11, 2026
Re-triage (latest updates)
Two good risk mitigations added:
exec_stream stdin — valid risk. Pre-implementation spike to verify
ExecSandboxRequestsupports stdin bytes. Fallback to_do_write_file()per-file is the right safety net (proven path at line 734). Skills path subdirectory test preserves K8s/Podman contract parity. ✅Credential fallback — file-based, not env-only — correct call. Env-only breaks Vertex (
GOOGLE_APPLICATION_CREDENTIALSrequires file mount). The updated fallback chain (Provider RPCs → file write + env → fail closed for file-required providers) properly satisfies the cross-platform rule. ✅Plan remains approved with these refinements.
🤖 Triaged by beesarmy via Claude Code
RSPEED-0000: Close OpenShell spawner production gaps — skills, creden…
RSPEED-0000: Skills image via Podman driver_config + fragility analys…