Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7485a8d
feat(inference): add route-scoped HTTPS pinning transport
apurvvkumaria Jul 19, 2026
20d7d80
test(inference): keep HTTPS pin regressions linear
apurvvkumaria Jul 19, 2026
67b6a25
fix(inference): address HTTPS pin review findings
apurvvkumaria Jul 19, 2026
68a4b63
ci(e2e): provision pinned routing tunnel
apurvvkumaria Jul 19, 2026
5625f90
test(e2e): cover routing tunnel digest pin
apurvvkumaria Jul 19, 2026
6832280
fix(e2e): bootstrap pinned routing tunnel binary
apurvvkumaria Jul 19, 2026
1b137c5
fix(e2e): expose HTTPS pin placeholder to sandbox
apurvvkumaria Jul 19, 2026
0eda93d
test(e2e): pin HTTPS route placeholder port
apurvvkumaria Jul 19, 2026
e4b93da
test(e2e): wait for pinned route refresh
apurvvkumaria Jul 19, 2026
ac47c21
test(e2e): keep route polling linear
apurvvkumaria Jul 19, 2026
0f65fb8
docs(inference): define orphan recovery exit
apurvvkumaria Jul 19, 2026
2059f0b
merge(main): reconcile route cleanup updates
apurvvkumaria Jul 19, 2026
10b9f60
fix(inference): reject stale pin adapters
apurvvkumaria Jul 19, 2026
ba42c5b
fix(inference): translate pinned route API paths
apurvvkumaria Jul 19, 2026
26a70da
merge: sync scoped HTTPS routing with main
apurvvkumaria Jul 25, 2026
918e1d2
merge(main): sync PR #7188 with current main
apurvvkumaria Jul 25, 2026
b9c66f1
docs(inference): document HTTPS pin port conflicts
apurvvkumaria Jul 25, 2026
1deb665
fix(inference): defer provider binding updates
apurvvkumaria Jul 25, 2026
9eba53f
test(e2e): verify cloudflared prerequisite
apurvvkumaria Jul 25, 2026
6eba0ac
fix(inference): restrict adapter bridge sources
apurvvkumaria Jul 25, 2026
62ad5b5
docs(inference): document adapter source boundary
apurvvkumaria Jul 25, 2026
4b2f944
fix(inference): bound pinned upstream lifecycle
apurvvkumaria Jul 25, 2026
7e6703e
fix(inference): preserve bare-origin gateway paths
apurvvkumaria Jul 25, 2026
3f431c5
fix(inference): recognize persisted adapter ports
apurvvkumaria Jul 25, 2026
a3cc3d7
fix(inference): restrict adapter health metadata
apurvvkumaria Jul 25, 2026
da41cc6
fix(inference): keep route secrets off child env
apurvvkumaria Jul 25, 2026
fbbacd8
docs(inference): record adapter lifecycle guards
apurvvkumaria Jul 25, 2026
2d40456
test(e2e): prove credential URL state rejection
apurvvkumaria Jul 25, 2026
54954d4
ci(e2e): describe HTTPS pin coverage
apurvvkumaria Jul 25, 2026
0fc5cd9
Merge branch 'main' into codex/6141-route-scoped-credentials
senthilr-nv Jul 25, 2026
821969b
fix(inference): restore selection after provider failure
apurvvkumaria Jul 25, 2026
5d63951
Merge remote-tracking branch 'origin/main' into codex/pr7188-current-…
apurvvkumaria Jul 25, 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
16 changes: 16 additions & 0 deletions ci/env-var-doc-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@
"name": "NEMOCLAW_BEDROCK_RUNTIME_REGION",
"reason": "Internal child-process setting used only to pass the resolved Bedrock Runtime region to the hidden local adapter. Users should rely on the endpoint URL or standard AWS region environment variables."
},
{
"name": "NEMOCLAW_HTTPS_PIN_RUNTIME_ADAPTER_PORT",
"reason": "Internal child-process setting used only when launching the hidden HTTPS Pin Runtime adapter. The port is a fixed internal constant, not a public user-facing configuration knob."
},
{
"name": "NEMOCLAW_HTTPS_PIN_RUNTIME_ADAPTER_CONTROL_TOKEN",
"reason": "Internal host-only child-process secret used to authenticate HTTPS Pin Runtime adapter control-plane calls. It is generated by NemoClaw, stored in a private local state file, and never registered with OpenShell or supplied by users."
},
{
"name": "NEMOCLAW_HTTPS_PIN_RUNTIME_ADAPTER_BOOTSTRAP_ROUTE",
"reason": "Internal child-process setting carrying a JSON-encoded route (including a credential value) used only to seed the hidden HTTPS Pin Runtime adapter at startup. Never user-set."
},
{
"name": "NEMOCLAW_HTTPS_PIN_RUNTIME_ADAPTER_ORPHANED_ROUTES",
"reason": "Internal child-process setting carrying a JSON-encoded map of opaque route ids to provider types and non-secret token generations (no URLs or credentials) that a fresh HTTPS Pin Runtime adapter respawn could not recover, so it can authenticate and respond to them distinctly from a route that never existed. Never user-set."
},
{
"name": "NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE",
"reason": "Internal installer sentinel exported only during OpenShell gateway replacement so onboard restores the pre-upgrade sandbox backup. Not user-facing."
Expand Down
25 changes: 21 additions & 4 deletions docs/inference/custom-endpoint-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ keywords: ["nemoclaw endpoint security", "inference endpoint ssrf", "custom endp
content:
type: "concept"
---

NemoClaw keeps provider credentials on the host and validates explicit custom endpoint URLs before saving them through security-sensitive configuration paths.

## Protect Provider Credentials
Expand Down Expand Up @@ -40,9 +39,27 @@ This allowlist does not relax direct blueprint, `config set`, or unrelated persi

For a public HTTP URL, NemoClaw stores the validated IP address so the downstream runtime cannot resolve the hostname again and reach another address.

NemoClaw rejects DNS-backed HTTPS URLs in these paths because it cannot pin the downstream peer address while preserving TLS Server Name Indication and host validation across the OpenShell runtime boundary.
Use an HTTPS IP-literal endpoint with a certificate valid for that address.
Use a public HTTP endpoint only when your deployment permits unencrypted traffic.
NemoClaw rejects DNS-backed HTTPS URLs for Hermes Provider setup, host-side `config set`, and a direct blueprint run because it cannot pin the downstream peer address while preserving TLS Server Name Indication and host validation across the OpenShell runtime boundary.
For those paths, use an HTTPS IP-literal endpoint with a certificate valid for that address, or use a public HTTP endpoint if your deployment permits unencrypted traffic.

<AgentOnly variant="openclaw,hermes">

## Use a DNS-Backed HTTPS Endpoint with `inference set`

`$$nemoclaw inference set --endpoint-url <url>` on an already-onboarded sandbox supports a DNS-backed HTTPS custom endpoint through the HTTPS Pin Runtime adapter.
After SSRF validation passes, NemoClaw starts a local reverse-proxy adapter on the host that terminates a pinned, SNI-correct outbound TLS connection to the real upstream hostname, re-validating that the resolved peer IP is still public.
The sandbox, its OpenShell provider configuration and network policy, and the persisted sandbox registry only ever see the opaque local base `http://host.openshell.internal:<port>/route/<id>`.
The real upstream hostname and path never reach the sandbox or the persisted registry; host recovery state stores only the opaque route ID, provider type, a non-secret token generation value, and timestamps.
Endpoint URLs containing userinfo, a query string, or a fragment are rejected rather than stripped or persisted.

Each opaque route has its own sandbox-facing adapter credential, distinct from both the real upstream credential and the host-only control credential; a credential issued for one route cannot authorize another route.
After an adapter restart, routes other than the one that triggered recovery return a recovery-needed response until their original `inference set --endpoint-url` command is rerun.
Switching away from a route or destroying its last sandbox reference revokes it; a scoped uninstall that leaves sibling gateways in place preserves the shared adapter and its remaining routes.

This support is specific to `inference set` on an already-onboarded sandbox.
Hermes Provider setup, host-side `config set`, and a direct blueprint run still reject DNS-backed HTTPS URLs as described above.

</AgentOnly>

## Use the Sandbox Host Alias

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2937,7 +2937,8 @@ When switching to `compatible-endpoint` or `compatible-anthropic-endpoint` from
For a Hermes `compatible-anthropic-endpoint` target, `--inference-api` may be omitted because NemoClaw deterministically selects `openai-completions`; an explicit different API family is rejected.
NemoClaw rejects loopback, link-local, private, and internal endpoint addresses, including public hostnames that resolve to a private address.
For public HTTP URLs, NemoClaw stores the validated IP address to prevent DNS rebinding.
DNS-backed HTTPS URLs are rejected because NemoClaw cannot pin the downstream peer address while preserving TLS SNI and host validation across the OpenShell runtime boundary; HTTPS IP-literal URLs remain supported.
For a DNS-backed HTTPS URL, NemoClaw routes the endpoint through a local HTTPS Pin Runtime adapter that terminates a pinned, SNI-correct outbound connection to the real upstream hostname; the sandbox and the persisted registry only ever see a local `host.openshell.internal` route, never the real hostname.
HTTPS IP-literal URLs remain supported and do not need the adapter.
NemoClaw accepts `http://host.openshell.internal:<port>` only with an explicit port from `1024` through `65535`; this narrow exception supports NemoClaw's sandbox-to-host inference routes and is not a general private-endpoint bypass.
`--credential-env` may also be supplied for compatible provider metadata; supported `--inference-api` values are `openai-completions`, `anthropic-messages`, and `openai-responses`.

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1922,7 +1922,8 @@ After the upgrade, recreate the sandbox with `$$nemoclaw onboard`.
NemoClaw rejects an explicit custom endpoint when it resolves a public HTTPS hostname but cannot pin the same peer address across the downstream OpenShell runtime boundary while preserving TLS SNI and host validation.
This can appear during a direct blueprint run, custom-endpoint onboarding, or a host-side `config set` write.
<AgentOnly variant="openclaw,hermes">
It can also appear during a runtime `$$nemoclaw inference set` switch.
It does not appear during a runtime `$$nemoclaw inference set` switch on an already-onboarded sandbox; that command routes a DNS-backed HTTPS endpoint through a local HTTPS Pin Runtime adapter instead of rejecting it.
Refer to [Commands](commands) for details.
</AgentOnly>

Use an HTTPS IP-literal endpoint whose certificate is valid for that address.
Expand Down
11 changes: 6 additions & 5 deletions nemoclaw/src/blueprint/ssrf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,12 @@ export async function validateEndpointUrl(url: string): Promise<ValidatedEndpoin
export function safeEndpointUrlForDownstream(validated: ValidatedEndpoint): string {
if (validated.protocol === "https:" && validated.dnsResolved) {
throw new Error(
`DNS-backed HTTPS endpoint '${validated.hostname}' is not supported yet because ` +
"NemoClaw cannot guarantee the downstream provider connects to the same IP " +
"that passed SSRF validation across the OpenShell runtime boundary. " +
"Use an HTTPS IP-literal endpoint, an HTTP endpoint that can be DNS-pinned, " +
"or wait for the runtime-aware HTTPS pinning transport.",
`DNS-backed HTTPS endpoint '${validated.hostname}' is not supported at onboarding ` +
"time because this path cannot guarantee the downstream provider connects to the " +
"same IP that passed SSRF validation across the OpenShell runtime boundary. " +
"Use an HTTPS IP-literal endpoint or an HTTP endpoint that can be DNS-pinned to " +
"onboard. After onboarding, `inference set --endpoint-url` supports a DNS-backed " +
"HTTPS endpoint through the HTTPS Pin Runtime adapter.",
);
}

Expand Down
35 changes: 33 additions & 2 deletions src/lib/actions/inference-set-compatible-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";
import { ensureHttpsPinRuntimeAdapter as realEnsureHttpsPinRuntimeAdapter } from "../inference/https-pin-runtime-adapter";
import { HTTPS_PIN_RUNTIME_ADAPTER_PROVIDER_CREDENTIAL_ENV } from "../inference/https-pin-runtime";
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
import type { ConfigObject } from "../security/credential-filter";
import { runInferenceSet } from "./inference-set";
import { baseSession, createDeps } from "./inference-set.test-support";
Expand Down Expand Up @@ -177,6 +179,22 @@
});

it("preserves explicit inference API through the final registry and session sync", async () => {
let providerVersion = 1;
const captureOpenshell = vi.fn((args: string[]) => {
if (args[0] === "provider" && args[1] === "get") {
const output = [
"Name: compatible-endpoint",
"Id: 11111111-2222-4333-8444-555555555555",
"Type: openai",
`Resource version: ${providerVersion}`,
"Credential keys: COMPATIBLE_API_KEY",
"Config keys: OPENAI_BASE_URL",
].join("\n");
return { status: 0, output, stdout: output, stderr: "" };
}
if (args[0] === "provider" && args[1] === "update") providerVersion += 1;
return { status: 0, output: "", stdout: "", stderr: "" };
});
const config: ConfigObject = {
agents: { defaults: { model: { primary: "inference/nvidia/model-a" } } },
models: { providers: { inference: { api: "openai-completions", models: [] } } },
Expand All @@ -196,6 +214,7 @@
credentialEnv: "NVIDIA_INFERENCE_API_KEY",
preferredInferenceApi: "openai-completions",
}),
captureOpenshell,
});

await runInferenceSet(
Expand All @@ -218,20 +237,26 @@
},
},
});
// The DNS-backed HTTPS endpoint is pinned via the HTTPS-pin runtime
// adapter, so the persisted endpointUrl is the adapter's local route base
// URL, not the raw operator-supplied hostname — mirroring the existing
// HTTP precedent of persisting the validated/pinned address. The
// The canonical provider key stays stable while its invocation-local
// value is replaced by the route-scoped adapter token.
expect(deps.calls.updateSandbox.mock.calls.at(-1)).toEqual([
"alpha",
expect.objectContaining({
provider: "compatible-endpoint",
model: "mock-responses-model",
endpointUrl: "https://compatible.example/v1",
endpointUrl: "http://host.openshell.internal:11438/route/test-route",
credentialEnv: "COMPATIBLE_API_KEY",
preferredInferenceApi: "openai-responses",
}),
]);
expect(deps.getSession()).toMatchObject({
provider: "compatible-endpoint",
model: "mock-responses-model",
endpointUrl: "https://compatible.example/v1",
endpointUrl: "http://host.openshell.internal:11438/route/test-route",
credentialEnv: "COMPATIBLE_API_KEY",
preferredInferenceApi: "openai-responses",
});
Expand Down Expand Up @@ -317,6 +342,12 @@
},
rewriteConfigUrlsWithDnsPinning: (value) =>
actualConfig.rewriteConfigUrlsWithDnsPinning(value, lookup),
// DNS-backed HTTPS endpoints (the "DNS-private" case below) route
// through the HTTPS-pin runtime adapter instead of
// rewriteConfigUrlsWithDnsPinning, so its real SSRF preflight is
// exercised here too, with the same injected DNS lookup.
ensureHttpsPinRuntimeAdapter: (adapterOptions) =>
realEnsureHttpsPinRuntimeAdapter({ ...adapterOptions, lookup }),
});

await expect(
Expand Down
35 changes: 35 additions & 0 deletions src/lib/actions/inference-set-endpoint-security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { describe, expect, it, vi } from "vitest";
import { rewriteConfigUrlsWithDnsPinning } from "../sandbox/config";
import type { ConfigValue } from "../security/credential-filter";
import { normalizeCustomEndpointUrl } from "./inference-set";

describe("custom inference endpoint DNS pinning", () => {
Expand Down Expand Up @@ -48,6 +49,21 @@ describe("custom inference endpoint DNS pinning", () => {
expect(lookup).toHaveBeenCalledWith("public-endpoint.example", { all: true });
});

it.each([
["userinfo", "https://user:secret@public-endpoint.example/v1"],
["query", "https://public-endpoint.example/v1?api_key=secret"],
["fragment", "https://public-endpoint.example/v1#secret"],
])("rejects a source endpoint with %s instead of silently stripping it", async (_kind, endpointUrl) => {
const rewriteUrl = vi.fn(async (value: ConfigValue) => value);
const ensureAdapter = vi.fn(async () => "http://host.openshell.internal:11438/route/test");

await expect(
normalizeCustomEndpointUrl(endpointUrl, rewriteUrl, ensureAdapter),
).rejects.toThrow("without userinfo, query, or fragment components");
expect(rewriteUrl).not.toHaveBeenCalled();
expect(ensureAdapter).not.toHaveBeenCalled();
});

it("fails closed for DNS-backed HTTPS endpoints until runtime-aware pinning exists", async () => {
const lookup = vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]);

Expand All @@ -57,4 +73,23 @@ describe("custom inference endpoint DNS pinning", () => {
),
).rejects.toThrow(/DNS-backed HTTPS URLs are not supported/);
});

it("adds the HTTPS Pin Runtime adapter hint only at the inference-set call site, not in the generic config validator's own message (#6141)", async () => {
const lookup = vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]);

// The generic validator (also used by plain `config set` for arbitrary
// fields) must not mention inference set or the adapter -- it has no way
// to know the field it's validating is an inference endpoint.
await expect(
rewriteConfigUrlsWithDnsPinning("https://public-endpoint.example/v1/", lookup),
).rejects.toThrow(/^(?!.*(?:inference set|HTTPS Pin Runtime adapter)).*$/is);

// normalizeCustomEndpointUrl is only ever called for `inference set
// --endpoint-url`, so it appends the adapter-specific hint itself.
await expect(
normalizeCustomEndpointUrl("https://public-endpoint.example/v1/", (value) =>
rewriteConfigUrlsWithDnsPinning(value, lookup),
),
).rejects.toThrow(/HTTPS Pin Runtime adapter/);
});
});
Loading
Loading