Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1fee71b
feat(blueprint): add Okta runtime identity wrapper
afourniernv Jul 20, 2026
85292df
feat(blueprint): add Entra identity reference
afourniernv Jul 20, 2026
2a7ef35
fix(blueprint): keep runtime identity Okta-only
afourniernv Jul 20, 2026
590e385
fix(blueprint): harden Okta identity validation
afourniernv Jul 21, 2026
cb6d516
Merge branch 'main' into 6871-agent-identity-references/af
ericksoa Jul 23, 2026
b990376
Merge remote-tracking branch 'origin/main' into pr-7265
ericksoa Jul 27, 2026
3518f36
fix(blueprint): harden runtime identity lifecycle
ericksoa Jul 27, 2026
d35d4c6
fix(blueprint): close identity review gaps
ericksoa Jul 27, 2026
537c216
fix(blueprint): address identity review findings
ericksoa Jul 27, 2026
bdcd239
Merge remote-tracking branch 'origin/main' into pr-7265
ericksoa Jul 27, 2026
9a484a7
test(blueprint): keep identity fixtures linear
ericksoa Jul 27, 2026
182dc26
fix(blueprint): fail closed on identity DNS rebinding
ericksoa Jul 27, 2026
549106e
fix(blueprint): require owned identity providers
ericksoa Jul 27, 2026
a78525e
fix(blueprint): constrain runtime identity policy
ericksoa Jul 27, 2026
71e4496
Merge remote-tracking branch 'origin/main' into pr-7265
ericksoa Jul 27, 2026
4b32c5e
fix(blueprint): import validated identity profile bytes
ericksoa Jul 27, 2026
2a5c17c
test(blueprint): keep identity lifecycle linear
ericksoa Jul 27, 2026
0242437
fix(identity): prove runtime OAuth flow end to end
ericksoa Jul 27, 2026
7b79ce8
Merge remote-tracking branch 'origin/main' into pr-7265
ericksoa Jul 27, 2026
f7df500
test(identity): keep rotation proof linear
ericksoa Jul 27, 2026
0bc4646
fix(identity): tighten ownership and DNS trust
ericksoa Jul 27, 2026
5497274
fix(identity): bind client ID source to policy
ericksoa Jul 27, 2026
72e1193
fix(identity): keep failed rollback retryable
ericksoa Jul 27, 2026
b07fca5
Merge remote-tracking branch 'origin/main' into pr-7265
ericksoa Jul 27, 2026
7a85de9
fix(identity): reuse existing sandbox before create
ericksoa Jul 27, 2026
f2cbbf0
Merge remote-tracking branch 'origin/main' into pr-7265
ericksoa Jul 27, 2026
ff871ea
fix(identity): fail closed on sandbox inspection
ericksoa Jul 27, 2026
258f034
docs(identity): document sandbox preflight order
ericksoa Jul 27, 2026
d717806
test(identity): keep sandbox fixture linear
ericksoa Jul 27, 2026
0718158
fix(identity): reuse configured inference provider
ericksoa Jul 27, 2026
e0b5470
Merge remote-tracking branch 'origin/main' into pr-7265
ericksoa Jul 27, 2026
616c896
docs(identity): clarify provider preflight order
ericksoa Jul 27, 2026
7a35fc5
fix(identity): track inference provider ownership
ericksoa Jul 27, 2026
fa7a376
fix(identity): revalidate provider create races
ericksoa Jul 27, 2026
8fc7df8
fix(blueprint): verify inference before identity attach
ericksoa Jul 27, 2026
8ea8850
fix(blueprint): preserve active inference route
ericksoa Jul 27, 2026
20c64fb
docs(architecture): clarify route timeout reuse
ericksoa Jul 27, 2026
58dcf6e
test(blueprint): keep route timeout case linear
ericksoa Jul 27, 2026
1734e67
fix(blueprint): revalidate raced sandbox
ericksoa Jul 27, 2026
ba3b1ec
fix(blueprint): recheck sandbox before identity attach
ericksoa Jul 27, 2026
3d71424
fix(schema): align runtime client id names
ericksoa Jul 28, 2026
0fcf798
fix(identity): prove provider policy auth path
ericksoa Jul 28, 2026
c5b1821
fix(identity): bound inference route parsing
ericksoa Jul 28, 2026
efb00ba
test(e2e): verify preserved sandbox with supported command
ericksoa Jul 28, 2026
7ea7370
test(e2e): align runtime identity phase evidence
ericksoa Jul 28, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,9 @@ jobs:
# localhost-compatible routing, namespace-aware HTTPS pinning, DNS
# rebinding resistance, private-target redirect rejection, and
# credential-bearing URL state rejection without live provider quota.
# It also proves runtime identity through a deterministic public OAuth
# refresh, real OpenShell provider attachment/injection, rotation, and
# rollback without using a third-party tenant.
# Provider smokes live in inference-routing-provider-smoke.test.ts; any
# future secret-bearing lane must run that file from trusted main.
run: |
Expand Down
115 changes: 115 additions & 0 deletions docs/reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,121 @@ flowchart LR
4. Apply. The runner executes the plan by calling `openshell` CLI commands.
5. Status. The runner reports current state.

### Experimental Runtime Identity

The direct OpenClaw blueprint runner can opt in to a provider-neutral runtime identity component.
The Hermes manifest onboarding path does not consume this component.
This experimental reference capability is not enabled by the shipped blueprint, and normal `$$nemoclaw onboard` does not collect or provision its inputs.
The bundled Okta profile is the first data-only implementation of the component.
The blueprint schema describes the OpenShell provider binding and OAuth refresh inputs without using the identity provider as a schema discriminator.

<Warning>
Runtime identity profiles 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 SSRF and L7 enforcement for sandbox requests before it injects the provider credential.
OpenShell 0.0.85's gateway-side OAuth refresh client enforces HTTPS and verifies the original hostname's certificate but does not pin the address NemoClaw resolved, so only identity-platform-controlled DNS suffixes such as `okta.com` belong in this trust table.
Adding an attacker-controlled or customer-controlled DNS suffix requires a pinning-capable upstream refresh boundary and new conformance evidence.
</Warning>

#### Threat Model and Conformance Evidence

Runtime identity is opt-in only when a direct-runner blueprint includes `components.identity`; there is no implicit activation path.
The host process that supplies the named OAuth bootstrap variables and the authenticated OpenShell gateway are trusted.
The blueprint, copied profile, same-name gateway resources, sandbox workload, subprocess output, persisted state, and CI artifacts are treated as untrusted or observable surfaces.
The runner therefore validates the complete data-only profile before import, fails closed on ambiguous resource inspection, scopes secret material to the single refresh-configuration subprocess, and persists only non-secret ownership receipts.
OpenShell remains responsible for credential custody, refresh, admitted-request enforcement, and bearer substitution; NemoClaw never exposes the minted bearer to the sandbox.

The deterministic `TC-INF-12` protected E2E scenario is the conformance gate for this boundary.
It runs the real blueprint runner against a real OpenShell gateway and sandbox, enables provider-derived policy for the test and restores its prior setting, exchanges a refresh token through a public HTTPS OAuth endpoint, proves the sandbox sees only opaque placeholders, proves a child launched after rotation receives a different revision-scoped placeholder, proves the protected resource receives the first and rotated bearers, checks secret-free plan, status, state, logs, and request ledgers, and verifies ownership-aware rollback.
Focused runner and runtime-identity tests cover malformed profiles, endpoint and DNS rejection, subprocess scoping, same-name resource handling, partial-apply compensation, and retryable rollback receipts.
The gateway refresh client's connect-time DNS-pinning limitation described above is the accepted residual boundary; expanding the trusted hostname policy requires upstream pinning and new conformance evidence.

Enable provider-derived policy on the target gateway before applying a blueprint that attaches this component:

```bash
openshell settings set --global --key providers_v2_enabled --value true --yes
```

Apply reads the gateway-global setting before identity mutation and again immediately before attachment, and stops unless its JSON value is exactly `true`.
This prevents a successful-looking attachment whose provider-derived network policy and credential injection are inactive.

Copy `nemoclaw-blueprint/provider-profiles/okta-runtime-v1.yaml` into the blueprint you operate as `provider-profiles/acme-okta-runtime.yaml`.
You can choose another tenant-specific filename, but set `profile_path` below to that exact relative path.
Set its `token_url` to the tenant's authorization-server token endpoint.
Replace `api.example.okta.com` with the approved upstream API host.
Keep the token endpoint and upstream host in the profile so refresh material and bearer-token presentation cannot be redirected by blueprint input.

Configure the reference under `components`:

```yaml
identity:
profile_path: provider-profiles/acme-okta-runtime.yaml
provider_type: okta-runtime-v1
provider_name: acme-okta-runtime
credential_key: OKTA_ACCESS_TOKEN
client_id_env: OKTA_CLIENT_ID
refresh_token_env: OKTA_REFRESH_TOKEN
client_secret_env: OKTA_CLIENT_SECRET
```

`client_secret_env` is optional, but when it is configured its environment variable must be present.
Secret-material environment names must equal one of `API_KEY`, `TOKEN`, `SECRET`, `PASSWORD`, or `CREDENTIAL`, or end in one of those terms preceded by an underscore.
Subprocess-control names such as `NODE_OPTIONS` and names forwarded by the general subprocess allowlist, such as the `OPENSHELL_`, `GRPC_`, and `XDG_` prefixes, are rejected.
The profile path must name an existing regular file whose resolved path stays inside the blueprint directory; absolute paths, outward traversal, and outward symlinks are rejected.
The profile `id` must match `provider_type`, and the profile must declare exactly one credential whose name matches `credential_key`.
Before import, the runner validates the complete credential-delivery policy and rejects unknown fields.
The Okta reference requires bearer presentation through the `authorization` header, the reviewed OAuth refresh-material shape, enforced REST `GET /**` endpoint rules, and exactly the bundled Node.js and curl executable allowlist.
The runner imports a private temporary snapshot of the exact profile bytes that passed validation, so replacing the original file or its symlink cannot change the policy OpenShell receives.
Before importing the profile, the runner requires HTTPS refresh and endpoint destinations, restricts them to the provider type's trusted host policy, and sends each through NemoClaw's DNS-aware SSRF boundary; private, loopback, link-local, and unresolved destinations are rejected.
DNS-backed destinations require an explicit reviewed policy marking 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; custom Okta domains are not supported in this slice.
The same reviewed policy fixes the non-secret client ID source to `OKTA_CLIENT_ID`; a blueprint cannot select another host variable, and command failures redact the client ID value along with secret refresh material.
Before applying the blueprint, export the named environment variables in the host process that runs the direct blueprint runner.
Obtain the refresh token through an authorized OAuth bootstrap flow for the same Okta client and authorization server as the profile.
`openshell gateway login` authenticates a CLI user to the gateway and is not part of this runtime-credential flow.

During plan, the runner reports only the non-secret provider type, provider name, and credential key.
During apply, the runner first inspects the target sandbox and stops before identity mutation unless it confirms the exact name and `Ready` phase or an explicit sandbox-not-found result.
If sandbox creation races with another creator, the runner repeats the same exact-name and `Ready` inspection before continuing.
It performs that inspection again immediately before attaching the runtime identity, after inference routing is confirmed, and compensates without attachment if the sandbox changed.
The runner always inspects the configured inference provider before identity mutation, whether it will create or reuse the sandbox.
It reuses that inference provider only when its reported name, type, and required non-secret key shape match the blueprint, and permits creation only after an explicit provider-not-found result.
If creation then reports that another actor created the provider concurrently, the runner repeats the same binding inspection before treating it as unowned reusable state.
Any other inference-provider inspection failure stops apply.
The runner then inspects the requested runtime identity provider name and stops if any same-name provider already exists.
It validates an existing provider's non-secret binding to produce a precise error, but never reuses it for this mutable refresh flow because OpenShell does not expose a secret-safe snapshot that rollback could restore.
Only an absent provider proceeds to profile import, or exact export comparison when that profile is already registered, followed by provider creation, gateway-managed OAuth refresh configuration, and initial token rotation.
Before attaching that runtime identity to the sandbox, the runner creates or reuses the configured inference provider.
For a reused sandbox and provider, it reads the live gateway route and preserves it only when the provider, model, and any requested timeout match the blueprint; an absent or different route must pass `openshell inference set`.
This keeps the new credential attachment out of the sandbox until OpenShell has either reported the exact active route or accepted the requested route; only then does the runner attach the runtime identity and apply any policy additions.
Each successful apply therefore creates and owns the provider whose refresh state it mutates.
The persisted run plan records whether that apply created the runtime identity provider, its sandbox attachment, the inference provider, and the sandbox itself.
The runner writes that ownership receipt as each identity resource is acquired, so `status` and a later `rollback` retain a recovery path if automatic compensation fails.
If a later apply step fails, the runner detaches the runtime identity provider when that apply attached it, deletes each provider when that apply created it, and removes a sandbox created by that apply.
The runner never places the refresh token or optional client secret in command arguments or persisted plans.
It passes those values to `openshell provider refresh configure` through a scoped subprocess environment, and OpenShell stores the resulting credential material in the gateway credential store.
All other runner subprocesses receive the allowlisted environment without the identity material.
Each sandbox child launch receives an opaque `OKTA_ACCESS_TOKEN` placeholder, and the OpenShell L7 proxy substitutes its corresponding access token only for admitted HTTPS requests.
After a credential rotation, launch a new child process to receive the new revision-scoped placeholder instead of expecting an earlier child launch to adopt the rotation.

Check the gateway-side state without printing credential values:

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

The direct runner `status` action reports the non-secret identity ownership receipt.
The `rollback` action verifies the current provider binding again, detaches it only when that apply created the sandbox attachment, and deletes it only when that apply created the provider.
Rollback stops and removes the sandbox only when the persisted plan proves that apply created it; reused sandboxes and legacy plans with unknown sandbox ownership are preserved.
After an owned sandbox is removed, or immediately when the sandbox was reused, rollback deletes the inference provider only when the persisted receipt proves that apply created it.
If removal of an apply-owned sandbox fails, rollback returns the bounded error without writing its completion marker, leaving the ownership receipt available for retry.
Rollback stops without mutating a same-name provider when the binding no longer matches the receipt.

Runtime identity does not add a generic blueprint middleware surface in this slice.
Configure any 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.

## Sandbox Environment

<AgentOnly variant="openclaw,hermes">
Expand Down
49 changes: 49 additions & 0 deletions nemoclaw-blueprint/provider-profiles/okta-runtime-v1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Data-only reference for the experimental runtime identity contract.
# The direct runner validates this complete credential-delivery shape before
# import, including the Okta-controlled destination suffix, exact executable
# allowlist, and enforced endpoint rule. OpenShell owns refresh-token custody,
# access-token rotation, sandbox placeholders, and post-policy injection.
# Copy this file, replace the example authorization-server URL and API host,
# and review the endpoint rules before use.
id: okta-runtime-v1
display_name: Okta Runtime Credentials v1
description: Gateway-managed Okta access-token refresh for an attached sandbox
category: agent
credentials:
- name: OKTA_ACCESS_TOKEN
description: Short-lived Okta API access token
env_vars:
- OKTA_ACCESS_TOKEN
required: true
auth_style: bearer
header_name: authorization
refresh:
strategy: oauth2_refresh_token
token_url: https://example.okta.com/oauth2/default/v1/token
refresh_before_seconds: 300
max_lifetime_seconds: 3600
material:
- name: client_id
required: true
- name: refresh_token
required: true
secret: true
- name: client_secret
required: false
secret: true
endpoints:
- host: api.example.okta.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
Loading
Loading