-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(identity): add host-managed Okta OBO reference #8155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
197782c
1b65042
6d6913e
52161fd
82fd2ca
a93b641
0c43dd3
eed88b1
48a38ff
106084d
12ea51b
fc58704
2430fb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" }], | ||
| ], | ||
| ]); | ||
|
|
@@ -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]; | ||
|
|
@@ -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({ | ||
|
|
@@ -837,7 +837,7 @@ describe("blueprint identity wrapper", () => { | |
|
|
||
| expect(mockExeca).toHaveBeenCalledWith( | ||
| "openshell", | ||
| ["sandbox", "remove", "test-sandbox"], | ||
| ["sandbox", "delete", "test-sandbox"], | ||
| expect.objectContaining({ reject: false }), | ||
| ); | ||
| }); | ||
|
|
@@ -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"); | ||
|
|
@@ -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"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.tsRepository: 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/srcRepository: 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/blueprintRepository: 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"))
PYRepository: NVIDIA/NemoClaw Length of output: 381 Reject the legacy Add negative assertions for 🤖 Prompt for AI AgentsSource: Path instructions |
||
| expect(rollbackCommands).toContain("sandbox provider detach test-sandbox acme-okta-runtime"); | ||
| expect(rollbackCommands).toContain("provider delete acme-okta-runtime"); | ||
| }); | ||
|
|
@@ -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(); | ||
| }); | ||
|
|
||
|
|
@@ -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 () => { | ||
|
|
@@ -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(); | ||
| }); | ||
|
|
@@ -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" }], | ||
|
|
@@ -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( | ||
|
|
||
There was a problem hiding this comment.
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_envis required for the OBO profile.isRuntimeIdentityConfigrequiresclient_secret_envto be a string for the token-exchange flow. Seenemoclaw/src/blueprint/runtime-identity.tsLine 711. Line 96 of this page states thatclient_secret_envis optional, and that statement belongs to the refresh profile. A reader who applies that statement to the OBO profile and omits the field receives onlyRuntime 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.📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines