Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ flowchart LR
The direct OpenClaw blueprint runner can opt in to a provider-neutral runtime identity component.

The shipped blueprint and normal `nemoclaw onboard` do not enable or configure this experimental capability.
Refer to [Configure Experimental Runtime Identity](configure-runtime-identity) for the trust boundary, Okta and Microsoft Entra reference profiles, apply lifecycle, inspection, and rollback behavior.
Refer to [Configure Experimental Runtime Identity](configure-runtime-identity) for the trust boundary, Okta refresh, Okta OBO, Microsoft Entra, inspection, and rollback behavior.
</AgentOnly>
<AgentOnly variant="hermes,deepagents">
Runtime identity is an experimental capability of the direct OpenClaw blueprint runner.
Expand Down
84 changes: 70 additions & 14 deletions docs/reference/configure-runtime-identity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
title: "Configure Experimental Runtime Identity"
sidebar-title: "Configure Runtime Identity"
description: "Configure the experimental provider-neutral runtime identity component for a direct OpenClaw blueprint."
description-agent: "Configures the direct OpenClaw blueprint runner's experimental runtime identity component, including its Okta and Microsoft Entra profiles, trust boundary, lifecycle, inspection, and rollback. Use when operating components.identity outside normal onboarding."
keywords: ["nemoclaw runtime identity", "openclaw runtime identity", "okta runtime identity", "microsoft entra runtime identity"]
description-agent: "Configures the direct OpenClaw blueprint runner's experimental runtime identity component, including its Okta refresh, Okta on-behalf-of, and Microsoft Entra profiles. Use when operating components.identity outside normal onboarding."
keywords: ["nemoclaw runtime identity", "openclaw runtime identity", "okta runtime identity", "okta obo", "microsoft entra runtime identity"]
agent-variants: ["openclaw"]
content:
type: "how_to"
Expand All @@ -14,7 +14,7 @@ content:
The direct OpenClaw blueprint runner can attach a provider-neutral runtime identity to a sandbox.
The shipped blueprint and normal `nemoclaw onboard` do not enable or configure this experimental capability.

The bundled Okta and Microsoft Entra profiles are data-only implementations of the component.
The bundled Okta, Okta on-behalf-of (OBO), and Microsoft Entra profiles are data-only implementations of the component.
The blueprint schema describes the OpenShell provider binding and OAuth refresh inputs without using the identity provider as a schema discriminator.

## Review the Trust Boundary
Expand All @@ -26,18 +26,19 @@ The host process that supplies the named OAuth bootstrap variables and the authe
The blueprint, copied profile, same-name gateway resources, sandbox workload, subprocess output, persisted state, and CI artifacts are untrusted or observable surfaces.

The runner validates the complete data-only profile before import.
It fails closed on ambiguous resource inspection, scopes secret material to one refresh-configuration subprocess, and persists only non-secret ownership receipts.
OpenShell owns credential custody, refresh, admitted-request enforcement, and bearer substitution.
It fails closed on ambiguous resource inspection, scopes credential material to one required subprocess, and persists only non-secret ownership receipts.
OpenShell owns credential custody, admitted-request enforcement, and bearer substitution.
It also owns refresh for profiles that configure gateway-managed refresh.
NemoClaw never exposes the minted bearer to the sandbox.

<Warning>
Runtime identity profiles may use DNS-backed HTTPS only within a provider type's repository-reviewed hostname suffixes.
Token endpoints may use DNS-backed HTTPS only within a provider type's repository-reviewed hostname suffixes.
NemoClaw resolves every destination before import and rejects private or internal addresses.
OpenShell performs connect-time server-side request forgery (SSRF) and L7 enforcement before it injects the provider credential.

OpenShell 0.0.85 verifies the original hostname's certificate but does not pin the address that NemoClaw resolved.
Only identity-platform-controlled DNS namespaces such as `okta.com`, `login.microsoftonline.com`, and `graph.microsoft.com` belong in this trust table.
An attacker-controlled or customer-controlled suffix requires a pinning-capable upstream refresh boundary and new conformance evidence.
The OBO profile permits a configured public HTTPS resource host after NemoClaw SSRF validation.
The OpenShell proxy resolves that host again and applies its SSRF policy before the upstream connection.
The runner still restricts the OBO token endpoint to `okta.com` or `oktapreview.com`.
</Warning>

### Review the Conformance Evidence
Expand Down Expand Up @@ -122,7 +123,7 @@ Private, loopback, link-local, and unresolved destinations are rejected.

DNS-backed destinations require a reviewed policy that marks their namespace as identity-platform-controlled.
Other profile policies reject them before import.
The current `okta-runtime-v1` policy accepts `okta.com` and its subdomains.
The current `okta-runtime-v1` policy accepts `okta.com`, `oktapreview.com`, and their subdomains.
Custom Okta domains are not supported.

The reviewed policy fixes the non-secret client ID source to `OKTA_CLIENT_ID`.
Expand All @@ -140,6 +141,56 @@ After apply succeeds, run `unset OKTA_REFRESH_TOKEN OKTA_CLIENT_SECRET` in the h
OpenShell retains the refresh material in the gateway credential store until rollback or provider deletion removes the owned provider.
</Warning>

## Configure the Okta On-Behalf-Of Reference Profile

The `okta-obo-v1` profile lets a sandbox call an API with a short-lived token delegated from a signed-in Okta user.
NemoClaw performs one OAuth 2.0 token exchange on the host.
It then creates a standard OpenShell provider with the returned token and attaches that provider to the sandbox.
This flow uses the current OpenShell provider API and does not require an OpenShell token-exchange strategy.

Copy `nemoclaw-blueprint/provider-profiles/okta-obo-v1.yaml` into your blueprint as `provider-profiles/acme-okta-obo.yaml`.
Replace `api.example.com` with the public HTTPS API host that accepts the delegated token.
Keep the endpoint rule as narrow as the API allows.

Configure the reference under `components`.

```yaml
identity:
profile_path: provider-profiles/acme-okta-obo.yaml
provider_type: okta-obo-v1
provider_name: acme-okta-obo
credential_key: OKTA_OBO_ACCESS_TOKEN
client_id_env: OKTA_CLIENT_ID
client_secret_env: OKTA_CLIENT_SECRET
subject_token_env: OKTA_SUBJECT_TOKEN
token_url: https://example.okta.com/oauth2/default/v1/token
audience: api://orders
scopes:
- orders.read
```

`OKTA_SUBJECT_TOKEN` must contain a current access token for the signed-in user.
The client ID and client secret identify the confidential Okta client authorized to perform the exchange.
The runner sends an RFC 8693 token-exchange request to the configured Okta token endpoint.
It rejects redirects, URL credentials, non-HTTPS endpoints, private addresses, and token hosts outside `okta.com` or `oktapreview.com`.
Comment on lines +172 to +175

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

State that client_secret_env is required for the OBO profile.

isRuntimeIdentityConfig requires client_secret_env to be a string for the token-exchange flow. See nemoclaw/src/blueprint/runtime-identity.ts Line 711. Line 96 of this page states that client_secret_env is optional, and that statement belongs to the refresh profile. A reader who applies that statement to the OBO profile and omits the field receives only Runtime identity configuration is invalid.

Add the requirement to this section.

📝 Proposed addition
 `OKTA_SUBJECT_TOKEN` must contain a current access token for the signed-in user.
 The client ID and client secret identify the confidential Okta client authorized to perform the exchange.
+`client_secret_env` is required for this profile, unlike the refresh profile.
 The runner sends an RFC 8693 token-exchange request to the configured Okta token endpoint.
As per coding guidelines: "Verify commands, defaults, flags, API names, and technical claims against checked-in source, tests, scripts, or another accepted source of truth."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`OKTA_SUBJECT_TOKEN` must contain a current access token for the signed-in user.
The client ID and client secret identify the confidential Okta client authorized to perform the exchange.
The runner sends an RFC 8693 token-exchange request to the configured Okta token endpoint.
It rejects redirects, URL credentials, non-HTTPS endpoints, private addresses, and token hosts outside `okta.com` or `oktapreview.com`.
`OKTA_SUBJECT_TOKEN` must contain a current access token for the signed-in user.
The client ID and client secret identify the confidential Okta client authorized to perform the exchange.
`client_secret_env` is required for this profile, unlike the refresh profile.
The runner sends an RFC 8693 token-exchange request to the configured Okta token endpoint.
It rejects redirects, URL credentials, non-HTTPS endpoints, private addresses, and token hosts outside `okta.com` or `oktapreview.com`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/configure-runtime-identity.mdx` around lines 172 - 175, Update
the OBO profile documentation in the section describing the Okta token exchange
to state that client_secret_env is required and must name the environment
variable containing the confidential client secret; keep the existing
optionality guidance scoped to the refresh profile so readers do not apply it to
OBO.

Source: Coding guidelines


The runner does not put the subject token, client secret, or delegated token in command arguments, plans, or NemoClaw state.
It passes the delegated token only through the environment of one `openshell provider create` subprocess.
OpenShell stores the delegated token and gives the sandbox an opaque `OKTA_OBO_ACCESS_TOKEN` placeholder.
The OpenShell proxy rewrites that placeholder only for an admitted request to the configured resource endpoint.

<Warning>
The host process can read `OKTA_SUBJECT_TOKEN` and `OKTA_CLIENT_SECRET` until you unset them or the process exits.
After apply succeeds, run `unset OKTA_SUBJECT_TOKEN OKTA_CLIENT_SECRET` in the host shell.
OpenShell retains the delegated token until rollback or provider deletion removes the provider.
The protected API rejects the token after its Okta expiration time.
</Warning>

This reference performs one exchange during each apply.
It does not renew the delegated token or select a token for each user at request time.
When the token expires, roll back the owned provider and apply again with a current subject token.
A long-running or multi-user deployment requires a separate credential broker or a future OpenShell dynamic credential source.

## Configure the Microsoft Entra Reference Profile

Copy `nemoclaw-blueprint/provider-profiles/entra-runtime-v1.yaml` into your blueprint as `provider-profiles/acme-entra-runtime.yaml`.
Expand Down Expand Up @@ -223,14 +274,15 @@ It never reuses that provider because OpenShell does not expose a secret-safe sn

Only an absent provider proceeds to profile import.
When the profile is already registered, the runner first requires an exact export match.
It then creates the provider, configures gateway-managed OAuth refresh, and performs the initial token rotation.
For a refresh profile, the runner creates the provider, configures gateway-managed OAuth refresh, and performs the initial token rotation.
For the OBO profile, the host exchanges the subject token and creates a static provider with the returned token.

Before attachment, the runner creates or reuses the configured inference provider.
For a reused sandbox and provider, it preserves the live route only when the provider, model, and requested timeout match the blueprint.
An absent or different route must pass `openshell inference set`.
The runner attaches the runtime identity and applies policy additions only after OpenShell confirms the route.

Each successful apply creates and owns the runtime identity provider whose refresh state it mutates.
Each successful apply creates and owns the runtime identity provider.
The persisted plan records ownership of the runtime identity provider, sandbox attachment, inference provider, and sandbox.
The runner writes the receipt as it acquires each resource.
The receipt preserves a recovery path when automatic compensation fails.
Expand All @@ -243,7 +295,10 @@ It passes them to `openshell provider refresh configure` through a scoped subpro
OpenShell stores the credential material in the gateway credential store.
Other runner subprocesses receive the allowlisted environment without identity material.

Each sandbox child launch receives an opaque provider-specific placeholder such as `OKTA_ACCESS_TOKEN` or `ENTRA_ACCESS_TOKEN`.
The OBO path gives only the delegated token to its scoped `openshell provider create` subprocess.
It does not give that subprocess the subject token or Okta client secret.

Each sandbox child launch receives an opaque provider-specific placeholder such as `OKTA_ACCESS_TOKEN`, `OKTA_OBO_ACCESS_TOKEN`, or `ENTRA_ACCESS_TOKEN`.
The OpenShell L7 proxy substitutes the corresponding access token only for admitted HTTPS requests.
After rotation, launch a new child process to receive the new revision-scoped placeholder.
An earlier child process does not adopt the rotation.
Expand All @@ -254,6 +309,7 @@ Check gateway-side state without printing credential values.

```bash
openshell provider refresh status acme-okta-runtime --credential-key OKTA_ACCESS_TOKEN
openshell provider get acme-okta-obo
openshell sandbox provider list <sandbox-name>
```

Expand All @@ -278,7 +334,7 @@ Rollback does not mutate a same-name provider when its binding no longer matches

Runtime identity does not add a generic blueprint middleware surface.
Configure deployment-specific pre-credential policy through separately supported OpenShell tooling.
This reference does not package an OAuth bootstrap application, an on-behalf-of exchange, or a production identity middleware service.
These references do not package an OAuth bootstrap application, automatic OBO renewal, or a production identity middleware service.

## Related Topics

Expand Down
32 changes: 32 additions & 0 deletions nemoclaw-blueprint/provider-profiles/okta-obo-v1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Data-only profile for the experimental host-managed Okta OBO reference.
# NemoClaw exchanges the user subject token before this provider is created.
# OpenShell stores the resulting short-lived token and injects it only for an
# admitted request; the sandbox receives an opaque placeholder instead.
id: okta-obo-v1
display_name: Okta OBO Runtime Credentials v1
description: Short-lived Okta delegated access token for an attached sandbox
category: agent
credentials:
- name: OKTA_OBO_ACCESS_TOKEN
description: Short-lived Okta delegated access token
env_vars:
- OKTA_OBO_ACCESS_TOKEN
required: true
auth_style: bearer
header_name: authorization
endpoints:
- host: api.example.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
binaries:
- /usr/local/bin/node
- /usr/bin/node
- /usr/local/bin/curl
- /usr/bin/curl
inference_capable: false
29 changes: 18 additions & 11 deletions nemoclaw/src/blueprint/runner-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ describe("blueprint identity wrapper", () => {
],
],
[
"sandbox create --from openclaw --name test-sandbox --forward 18789",
"sandbox create --from openclaw --name test-sandbox --forward 18789 --no-tty -- /bin/true",
[{ exitCode: 1, stdout: "", stderr: "sandbox already exists" }],
],
]);
Expand Down Expand Up @@ -744,7 +744,7 @@ describe("blueprint identity wrapper", () => {
);
expect(mockExeca).toHaveBeenCalledWith(
"openshell",
["sandbox", "remove", "test-sandbox"],
["sandbox", "delete", "test-sandbox"],
expect.objectContaining({ reject: false }),
);
const planEntry = [...store.entries()].find(([path]) => path.endsWith("/plan.json"))?.[1];
Expand Down Expand Up @@ -795,7 +795,7 @@ describe("blueprint identity wrapper", () => {
const commands = mockExeca.mock.calls.map(([, args]) => (args ?? []).join(" "));
expect(commands).toContain("sandbox provider detach test-sandbox acme-okta-runtime");
expect(commands).toContain("provider delete acme-okta-runtime");
expect(commands).toContain("sandbox remove test-sandbox");
expect(commands).toContain("sandbox delete test-sandbox");
expect(commands).toContain("provider delete test-provider");
const planEntry = [...store.entries()].find(([path]) => path.endsWith("/plan.json"))?.[1];
expect(JSON.parse(planEntry!.content!)).toMatchObject({
Expand Down Expand Up @@ -837,7 +837,7 @@ describe("blueprint identity wrapper", () => {

expect(mockExeca).toHaveBeenCalledWith(
"openshell",
["sandbox", "remove", "test-sandbox"],
["sandbox", "delete", "test-sandbox"],
expect.objectContaining({ reject: false }),
);
});
Expand Down Expand Up @@ -872,7 +872,7 @@ describe("blueprint identity wrapper", () => {

const applyCommands = mockExeca.mock.calls.map(([, args]) => (args ?? []).join(" "));
expect(applyCommands).not.toContain(
"sandbox create --from openclaw --name test-sandbox --forward 18789",
"sandbox create --from openclaw --name test-sandbox --forward 18789 --no-tty -- /bin/true",
);
expect(applyCommands).toContain("provider get test-provider");
expect(applyCommands).toContain("inference get");
Expand Down Expand Up @@ -902,7 +902,7 @@ describe("blueprint identity wrapper", () => {

const rollbackCommands = mockExeca.mock.calls.map(([, args]) => (args ?? []).join(" "));
expect(rollbackCommands).not.toContain("sandbox stop test-sandbox");
expect(rollbackCommands).not.toContain("sandbox remove test-sandbox");
expect(rollbackCommands).not.toContain("sandbox delete test-sandbox");

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'sandbox (delete|remove)|rollbackCommands|responseQueue' \
  nemoclaw/src/blueprint/runner-identity.test.ts

Repository: NVIDIA/NemoClaw

Length of output: 13998


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test helper and setup ---'
sed -n '1,150p' nemoclaw/src/blueprint/runner-identity.test.ts

printf '%s\n' '--- rollback-related tests ---'
sed -n '845,1085p' nemoclaw/src/blueprint/runner-identity.test.ts

printf '%s\n' '--- production command construction ---'
rg -n -C 5 'sandbox (delete|remove)|actionRollback|Failed to remove owned sandbox' nemoclaw/src

Repository: NVIDIA/NemoClaw

Length of output: 37956


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- actionRollback implementation ---'
sed -n '1365,1435p' nemoclaw/src/blueprint/runner.ts

printf '%s\n' '--- complete compensation fixture and assertions ---'
sed -n '1046,1102p' nemoclaw/src/blueprint/runner-identity.test.ts

printf '%s\n' '--- all sandbox removal command references ---'
rg -n -C 3 'sandbox (delete|remove|stop)' nemoclaw/src/blueprint

Repository: NVIDIA/NemoClaw

Length of output: 11681


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- targeted diff for the command migration ---'
git diff --unified=8 -- nemoclaw/src/blueprint/runner.ts nemoclaw/src/blueprint/runner-identity.test.ts \
  | rg -n -C 8 'sandbox (delete|remove)|responseQueue|rollbackCommands|actionRollback' || true

printf '%s\n' '--- deterministic fallback probe ---'
python3 - <<'PY'
responses = {
    "sandbox delete test-sandbox": [
        {"exitCode": 1, "stderr": "sandbox delete denied"},
        {"exitCode": 0, "stderr": ""},
    ],
}
fallbacks = {
    "sandbox get test-sandbox": {"exitCode": 0, "stderr": ""},
    "settings get --global --json": {"exitCode": 0, "stderr": ""},
}
def response(command):
    queued = responses.get(command)
    if queued:
        return queued.pop(0)
    return fallbacks.get(command, {"exitCode": 0, "stderr": ""})

print("sandbox remove test-sandbox ->", response("sandbox remove test-sandbox"))
print("sandbox delete test-sandbox ->", response("sandbox delete test-sandbox"))
PY

Repository: NVIDIA/NemoClaw

Length of output: 381


Reject the legacy sandbox remove command in migration tests.

Add negative assertions for sandbox remove at lines 905, 922, and 980. Make responseQueue fail on unexpected commands, or assert the exact command sequence in the fixture at lines 1069–1075.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoclaw/src/blueprint/runner-identity.test.ts` at line 905, Update the
migration tests around the rollback command assertions at lines 905, 922, and
980 to also verify that rollbackCommands does not contain the legacy “sandbox
remove” command. Strengthen the responseQueue fixture or the exact
command-sequence assertion near lines 1069–1075 so unexpected commands cause the
test to fail.

Source: Path instructions

expect(rollbackCommands).toContain("sandbox provider detach test-sandbox acme-okta-runtime");
expect(rollbackCommands).toContain("provider delete acme-okta-runtime");
});
Expand All @@ -919,7 +919,7 @@ describe("blueprint identity wrapper", () => {

const rollbackCommands = mockExeca.mock.calls.map(([, args]) => (args ?? []).join(" "));
expect(rollbackCommands).not.toContain("sandbox stop pre-existing-sandbox");
expect(rollbackCommands).not.toContain("sandbox remove pre-existing-sandbox");
expect(rollbackCommands).not.toContain("sandbox delete pre-existing-sandbox");
expect(store.get(`${stateDir}/rolled_back`)?.content).toBeDefined();
});

Expand Down Expand Up @@ -977,7 +977,7 @@ describe("blueprint identity wrapper", () => {
await actionRollback(JSON.parse(planEntry!.content!).run_id);
const rollbackCommands = mockExeca.mock.calls.map(([, args]) => (args ?? []).join(" "));
expect(rollbackCommands).toContain("provider delete test-provider");
expect(rollbackCommands).not.toContain("sandbox remove test-sandbox");
expect(rollbackCommands).not.toContain("sandbox delete test-sandbox");
});

it("keeps an owned sandbox receipt retryable when removal fails", async () => {
Expand All @@ -991,11 +991,11 @@ describe("blueprint identity wrapper", () => {
}),
});
responseQueue([
["sandbox remove owned-sandbox", [{ exitCode: 1, stdout: "", stderr: "remove denied" }]],
["sandbox delete owned-sandbox", [{ exitCode: 1, stdout: "", stderr: "delete denied" }]],
]);

await expect(actionRollback("failed-sandbox-removal")).rejects.toThrow(
/Failed to remove owned sandbox 'owned-sandbox': remove denied/,
/Failed to remove owned sandbox 'owned-sandbox': delete denied/,
);
expect(store.get(`${stateDir}/rolled_back`)).toBeUndefined();
});
Expand Down Expand Up @@ -1066,6 +1066,13 @@ describe("blueprint identity wrapper", () => {
{ exitCode: 0, stdout: "", stderr: "" },
],
],
[
"sandbox delete test-sandbox",
[
{ exitCode: 1, stdout: "", stderr: "sandbox delete denied" },
{ exitCode: 0, stdout: "", stderr: "" },
],
],
[
"inference set --provider test-provider --model test-model",
[{ exitCode: 1, stdout: "", stderr: "route failed" }],
Expand Down Expand Up @@ -1194,7 +1201,7 @@ describe("blueprint identity wrapper", () => {
);
expect(mockExeca).toHaveBeenCalledWith(
"openshell",
["sandbox", "remove", "test-sandbox"],
["sandbox", "delete", "test-sandbox"],
expect.objectContaining({ reject: false }),
);
expect(mockExeca).toHaveBeenCalledWith(
Expand Down
2 changes: 1 addition & 1 deletion nemoclaw/src/blueprint/runner-name-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ describe("blueprint name validation (fail-closed integration)", () => {
it("rollback rejects a plan whose sandbox_name is not an RFC 1035 label", async () => {
const runDir = `${RUNS_DIR}/nc-run-1`;
addDir(runDir);
// "--rm" would be consumed as a flag by `openshell sandbox stop/remove`.
// "--rm" would be consumed as a flag by `openshell sandbox stop/delete`.
addFile(`${runDir}/plan.json`, JSON.stringify({ sandbox_name: "--rm" }));

await expect(actionRollback("nc-run-1")).rejects.toThrow(/Invalid sandbox name/);
Expand Down
4 changes: 3 additions & 1 deletion nemoclaw/src/blueprint/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,9 @@ describe("runner", () => {

expect(mockExeca).toHaveBeenCalledWith(
"openshell",
["sandbox", "create", "--from", "openclaw", "--name", "test-sandbox", "--forward", "18789"],
"sandbox create --from openclaw --name test-sandbox --forward 18789 --no-tty -- /bin/true".split(
" ",
),
expect.objectContaining({ reject: false }),
);
});
Expand Down
Loading
Loading