Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
7645547
refactor(cli): add typed OpenShell sandbox observer
rsliter Aug 24, 2026
233c318
fix(cli): preserve OpenShell observation parity
rsliter Aug 24, 2026
a0ef184
Merge branch 'main' into codex/openshell-adapter-inventory
cv Aug 24, 2026
cd705b1
refactor(cli): tighten OpenShell observation boundary
rsliter Aug 24, 2026
eb9f690
docs(cli): clarify gateway identity drift
rsliter Aug 24, 2026
7418fa2
refactor(cli): add typed OpenShell provider adapter
rsliter Aug 24, 2026
101246c
test(cli): exercise doctor observation from source
rsliter Aug 24, 2026
159095c
merge(cli): refresh OpenShell adapter dependency
rsliter Aug 24, 2026
0e412df
fix(cli): fail closed on sandbox observation errors
rsliter Aug 24, 2026
bbe5830
merge(cli): refresh OpenShell adapter dependency
rsliter Aug 24, 2026
5dbab5d
test(cli): align rebuild recovery expectations
rsliter Aug 24, 2026
9bbf111
merge(cli): refresh OpenShell adapter dependency
rsliter Aug 24, 2026
5a091ce
merge(cli): refresh provider adapter onto main
rsliter Aug 27, 2026
0e7ea66
fix(cli): address provider adapter review findings
rsliter Aug 27, 2026
87d3c75
fix(cli): close provider adapter trust gaps
rsliter Aug 27, 2026
b234e73
merge(cli): refresh provider adapter onto main
rsliter Aug 27, 2026
a1743bc
test(cli): remove stale provider adapter import
rsliter Aug 27, 2026
4dfbd0d
Merge remote-tracking branch 'origin/main' into codex/openshell-provi…
rsliter Aug 28, 2026
ce5afa4
Merge remote-tracking branch 'origin/main' into codex/openshell-provi…
rsliter Aug 28, 2026
10b4999
fix(cli): address provider adapter review findings
rsliter Aug 28, 2026
abda0e7
test(cli): preserve provider command partial mock
rsliter Aug 28, 2026
140a2c6
test(cli): cover provider detach parity
rsliter Aug 28, 2026
1de41cb
fix(cli): close provider recovery review gaps
rsliter Aug 28, 2026
dfebbd7
test(cli): cover empty provider inventory
rsliter Aug 28, 2026
187c592
merge: resolve conflicts with main
github-actions[bot] Aug 28, 2026
ce0009c
merge: resolve conflicts with main
github-actions[bot] Aug 29, 2026
581e0f1
merge: resolve conflicts with main
github-actions[bot] Aug 30, 2026
ff3b4a2
chore(cli): lower source architecture ratchets
rsliter Aug 31, 2026
78cbe7f
fix(cli): validate reset provider names
rsliter Aug 31, 2026
c01f776
fix(cli): settle concurrent provider deletion
rsliter Aug 31, 2026
971b438
merge: refresh from main
prekshivyas Sep 1, 2026
ad94685
refactor(cli): reuse provider command timeout
prekshivyas Sep 1, 2026
bbbd4a8
merge: refresh from main
prekshivyas Sep 1, 2026
4d02964
merge: resolve conflicts with main
github-actions[bot] Sep 1, 2026
c8b89fe
test(e2e): cover provider credential lifecycle
prekshivyas Sep 1, 2026
8f534a0
merge: incorporate remote PR head
prekshivyas Sep 1, 2026
a1f6c6a
chore(cli): lower source architecture ratchet
prekshivyas Sep 1, 2026
8f26e4f
Merge remote-tracking branch 'origin/main' into codex/openshell-provi…
prekshivyas Sep 1, 2026
230c4a3
test(e2e): isolate provider credential lifecycle
prekshivyas Sep 1, 2026
9d9e328
fix(cli): validate bundled provider profiles
prekshivyas Sep 1, 2026
b8bff80
Merge remote-tracking branch 'origin/main' into codex/openshell-provi…
prekshivyas Sep 1, 2026
921d95d
fix(cli): verify provider trust boundaries
prekshivyas Sep 1, 2026
7fe13b5
fix(cli): report partial provider reset recovery
prekshivyas Sep 1, 2026
2ad8d5d
refactor(cli): reduce provider adapter surface
prekshivyas Sep 1, 2026
bdbca36
Merge remote-tracking branch 'origin/main' into codex/openshell-provi…
prekshivyas Sep 1, 2026
20ed35c
refactor(cli): unify provider profile imports
prekshivyas Sep 1, 2026
9cee258
fix(cli): redact provider diagnostic URLs
prekshivyas Sep 1, 2026
ac8c159
test(cli): verify profiles after import
prekshivyas Sep 1, 2026
abd869e
fix(cli): reject truncated attachment names
prekshivyas Sep 1, 2026
aa94531
fix(cli): harden provider recovery output
prekshivyas Sep 1, 2026
72c4645
fix(cli): verify inspected provider identity
prekshivyas Sep 1, 2026
d8fd4ac
docs(cli): correct managed-image fallback guidance
prekshivyas Sep 1, 2026
ce51776
fix(cli): preserve provider recovery guidance
prekshivyas Sep 1, 2026
3417f46
fix(cli): distinguish missing provider detach
prekshivyas Sep 1, 2026
167cd61
fix(cli): bind provider mutations to gateway
prekshivyas Sep 1, 2026
a23190d
fix(cli): reconcile provider creation outcomes
prekshivyas Sep 1, 2026
75e8d7b
fix(cli): bind credential inventory to gateway
prekshivyas Sep 1, 2026
1b6147a
fix(cli): close provider recovery boundaries
prekshivyas Sep 1, 2026
c6de468
fix(cli): reject ambient provider gateway overrides
prekshivyas Sep 1, 2026
1ba1afc
fix(cli): scope provider failure diagnostics
prekshivyas Sep 1, 2026
c56698c
fix(cli): preflight provider base URLs
prekshivyas Sep 1, 2026
df493a3
fix(cli): close provider DNS rebinding
prekshivyas Sep 1, 2026
8ebc723
fix(cli): preserve provider recovery guidance
rsliter Sep 1, 2026
7202ebd
test(cli): close provider adapter review findings
rsliter Sep 1, 2026
c425a23
Merge remote-tracking branch 'origin/main' into codex/openshell-provi…
prekshivyas Sep 1, 2026
88de9a8
Merge remote-tracking branch 'origin/codex/openshell-provider-adapter…
prekshivyas Sep 1, 2026
9450a44
fix(cli): clarify provider rebuild scope
rsliter Sep 1, 2026
f8edf6c
docs(cli): restore managed image fallback guidance
prekshivyas Sep 1, 2026
3937ec8
Merge remote-tracking branch 'origin/main' into codex/openshell-provi…
prekshivyas Sep 1, 2026
0cc8048
refactor(cli): reuse provider profile validator
prekshivyas Sep 1, 2026
bcf2503
docs(cli): remove stale provider detach workflow
prekshivyas Sep 1, 2026
91c1b59
fix(cli): validate messaging provider contracts
prekshivyas Sep 1, 2026
28436b0
test(e2e): verify credential sandbox boundary
prekshivyas Sep 1, 2026
05693f8
refactor(cli): restore credential-only PR scope
prekshivyas Sep 1, 2026
68bf26b
merge: reconcile credential adapter with current main
prekshivyas Sep 1, 2026
2b55927
refactor(cli): defer inference provider migration
prekshivyas Sep 1, 2026
c5d5ec2
test(cli): tag provider lifecycle cases
rsliter Sep 1, 2026
641a5b9
fix(cli): close credential review findings
prekshivyas Sep 1, 2026
5c98b47
Merge branch 'main' into codex/openshell-provider-adapter
prekshivyas Sep 1, 2026
9e43b0d
fix(cli): close provider lifecycle review gaps
prekshivyas Sep 1, 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
2 changes: 1 addition & 1 deletion ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"src/lib/messaging/channels/index.ts": 25,
"src/lib/onboard/gateway-binding.ts": 54,
"src/lib/runner.ts": 85,
"src/lib/security/redact.ts": 55,
"src/lib/security/redact.ts": 54,
"src/lib/state/mcp-lifecycle-lock.ts": 21,
"src/lib/state/onboard-session.ts": 35,
"src/lib/state/registry.ts": 97,
Expand Down
2 changes: 1 addition & 1 deletion docs/get-started/quickstart-langchain-deepagents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ Remove the target sandbox's managed-Python opt-in when it is no longer needed.
nemo-deepagents <sandbox-name> policy remove tavily --yes
```

This does not unregister the gateway-wide `tavily-search` provider; its credential and Node/curl routes remain available to sandboxes that attach it. When no sandbox needs the provider, destroy those sandboxes or detach it from each one with `openshell sandbox provider detach <sandbox-name> tavily-search`, then remove it globally with `nemo-deepagents credentials reset tavily-search --yes`. OpenShell rejects provider deletion while any sandbox still has it attached.
This does not unregister the gateway-wide `tavily-search` provider; its credential and Node/curl routes remain available to sandboxes that attach it. When no sandbox needs the provider, remove it globally with `nemo-deepagents credentials reset tavily-search --yes`. The command detaches current sandbox attachments, removes the gateway provider, and reports every detached sandbox that you must rebuild after registering a replacement.

</Accordion>

Expand Down
2 changes: 1 addition & 1 deletion docs/get-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ Use these details when your first-run path needs more control.
Logs: nemoclaw my-gpt-claw logs --follow
Model: nemoclaw inference set --model <model> --provider <provider> --sandbox my-gpt-claw
Policies: nemoclaw my-gpt-claw policy add
Credentials: nemoclaw credentials reset <KEY> && nemoclaw onboard
Credentials: nemoclaw credentials reset <PROVIDER> && nemoclaw onboard
──────────────────────────────────────────────────
```

Expand Down
8 changes: 4 additions & 4 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3207,9 +3207,9 @@ $$nemoclaw credentials list

### `$$nemoclaw credentials add <PROVIDER>`

Register a provider credential with the OpenShell gateway by name and type. Each `--credential` takes the env variable name whose value the gateway should read; export the value first so it is not placed in argv. Pass either repeatable `--credential <ENV_NAME>` or `--from-existing`, but do not combine them. `--from-existing` is available only when no managed MCP server reserves credential keys. The command fails before gateway work when a reservation exists because `--from-existing` does not expose credential keys before provider creation. Rerun with explicit `--credential <ENV_NAME>` input, or remove every managed MCP server that reserves credential keys before retrying. After the gateway accepts the provider, rebuild the target sandbox so the new provider is attached.
Register a provider credential with the OpenShell gateway by name and type. Each `--credential` takes the env variable name whose value the gateway should read; export the value first so it is not placed in argv. Pass either repeatable `--credential <ENV_NAME>` or `--from-existing`, but do not combine them. With `--from-existing`, NemoClaw inspects the provider profile and rejects registration when one of its credential keys is reserved by a managed MCP server. After the gateway accepts the provider, rebuild each sandbox that should use it.

Registered providers attach to every sandbox you build or rebuild after the call (the gateway is one process serving all sandboxes). If you want a provider available to only some sandboxes, scope it with `nemoclaw credentials reset <PROVIDER>` once those sandboxes finish using it.
Registered providers are gateway-wide and attach to every sandbox you build or rebuild after the call. `nemoclaw credentials reset <PROVIDER>` removes the provider from the gateway and detaches it from every sandbox currently using it; it cannot limit the provider to selected sandboxes. To replace a credential, reset the provider, register the replacement, and rebuild each detached sandbox.

```bash
$$nemoclaw credentials add tavily-search --type tavily --credential TAVILY_API_KEY
Expand All @@ -3219,8 +3219,8 @@ $$nemoclaw credentials add tavily-search --type tavily --credential TAVILY_API_K
| --- | --- |
| `--type <TYPE>` | Provider type (e.g. `tavily`, `nvidia`, `openai`, `anthropic`, `generic`) |
| `--credential <ENV_NAME>` | Env variable name whose value holds the credential. Repeatable |
| `--config <K=V>` | Provider configuration pair. Repeatable |
| `--from-existing` | Load credentials and config from existing local state when no managed MCP server reserves credential keys |
| `--config <K=V>` | Typed non-secret provider configuration. Supported: `OPENAI_BASE_URL=<http(s)://public-IP/path>` with `--type openai`. DNS hostnames are rejected because this gateway-wide credential path cannot enforce admission-time address pins. URLs with credentials, query parameters, fragments, loopback, link-local, private, internal, or reserved IP destinations are also rejected. Configure hostname-based and trusted private inference endpoints through onboarding so NemoClaw can preserve their trust and address pins. Repeatable |
| `--from-existing` | Load credentials and config from existing local state after checking the provider profile for credential keys reserved by managed MCP servers |

### `$$nemoclaw credentials reset <PROVIDER>`

Expand Down
190 changes: 136 additions & 54 deletions src/commands/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,22 @@ vi.mock("../lib/actions/global", () => ({
forgetExtraProvider: mocks.forgetExtraProvider,
listManagedMcpCredentialReservations: mocks.listManagedMcpCredentialReservations,
}));
vi.mock("../lib/adapters/openshell/provider-command", () => ({
OPENSHELL_OPERATION_TIMEOUT_MS: 30_000,
runOpenshellProviderCommand: mocks.runOpenshellProviderCommand,
}));
vi.mock("../lib/adapters/openshell/provider-command", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../lib/adapters/openshell/provider-command")>();
return {
...actual,
OPENSHELL_OPERATION_TIMEOUT_MS: 30_000,
runOpenshellProviderCommand: mocks.runOpenshellProviderCommand,
};
});
vi.mock("../lib/onboard/gateway-teardown-authority", () => ({
resolveGatewayCredentialMutationAuthority: mocks.resolveGatewayCredentialMutationAuthority,
}));

import { runCredentialsAddAction } from "../lib/actions/credentials-add";
import { runCredentialsListAction } from "../lib/actions/credentials/list";
import { runCredentialsResetAction } from "../lib/actions/credentials/reset";
import CredentialsCommand from "./credentials";
import CredentialsListCommand from "./credentials/list";
import CredentialsResetCommand from "./credentials/reset";
Expand Down Expand Up @@ -77,9 +84,12 @@ describe("credentials oclif adapter source coverage", () => {
await CredentialsListCommand.run([], rootDir);

expect(mocks.recoverNamedGatewayRuntime).toHaveBeenCalledWith();
expect(mocks.resolveGatewayCredentialMutationAuthority).not.toHaveBeenCalled();
expect(mocks.resolveGatewayCredentialMutationAuthority).toHaveBeenCalledWith({
gatewayName: "nemoclaw",
gatewayPort: 8080,
});
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledWith(
["provider", "list", "--names"],
["provider", "list", "-g", "nemoclaw", "--names"],
{
ignoreError: true,
stdio: ["ignore", "pipe", "pipe"],
Expand All @@ -101,7 +111,7 @@ describe("credentials oclif adapter source coverage", () => {

expect(mocks.prompt).not.toHaveBeenCalled();
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledWith(
["provider", "delete", "nvidia-prod"],
["provider", "delete", "-g", "nemoclaw", "nvidia-prod"],
{
ignoreError: true,
stdio: ["ignore", "pipe", "pipe"],
Expand Down Expand Up @@ -141,6 +151,33 @@ describe("credentials oclif adapter source coverage", () => {
);
});

it("rejects an ambient gateway endpoint before credential provider operations (#9806)", async () => {
const credentialValue = "host-only-secret";
vi.stubEnv("CUSTOM_TOKEN", credentialValue);
vi.stubEnv("OPENSHELL_GATEWAY_ENDPOINT", "https://untrusted.example.test");

const add = await runCredentialsAddAction({
provider: "custom-provider",
type: "generic",
credentials: ["CUSTOM_TOKEN"],
configPairs: [],
fromExisting: false,
});
const list = await runCredentialsListAction("nemoclaw");
const reset = await runCredentialsResetAction({
provider: "custom-provider",
confirmed: true,
});

expect(add.exitCode).toBe(1);
expect(list.exitCode).toBe(1);
expect(reset.exitCode).toBe(1);
const diagnostics = JSON.stringify([add, list, reset]);
expect(diagnostics.match(/OPENSHELL_GATEWAY_ENDPOINT is set/gu)).toHaveLength(3);
expect(diagnostics).not.toContain(credentialValue);
expect(mocks.runOpenshellProviderCommand).not.toHaveBeenCalled();
});

it("rejects a provider credential reserved by managed MCP before gateway mutation (#9388)", async () => {
vi.stubEnv("MAAS_GLEAN_TOKEN", "qa-secret-value");
mocks.listManagedMcpCredentialReservations.mockReturnValue([
Expand Down Expand Up @@ -169,31 +206,44 @@ describe("credentials oclif adapter source coverage", () => {
expect(mocks.recordExtraProvider).not.toHaveBeenCalled();
});

it("rejects --from-existing before gateway work when managed MCP reserves credentials (#9388)", async () => {
it("allows --from-existing after inspecting disjoint managed MCP credential keys (#9388)", async () => {
mocks.listManagedMcpCredentialReservations.mockReturnValue([
{
sandboxName: "hermes",
server: "maas-glean",
credentialKeys: ["MAAS_GLEAN_TOKEN"],
},
]);
mocks.runOpenshellProviderCommand
.mockReturnValueOnce({
status: 0,
stdout: JSON.stringify({
id: "generic",
credentials: [{ env_vars: ["CUSTOM_TOKEN"] }],
}),
})
.mockReturnValueOnce({ status: 0, stdout: "", stderr: "" });

const result = await runCredentialsAddAction({
provider: "maas-glean",
provider: "custom-provider",
type: "generic",
credentials: [],
configPairs: [],
fromExisting: true,
});

expect(result.exitCode).toBe(1);
expect(result.failureLines.join("\n")).toContain(
"Cannot compare imported provider credentials with keys reserved by managed MCP servers.",
expect(result.exitCode).toBe(0);
expect(mocks.runOpenshellProviderCommand).toHaveBeenNthCalledWith(
1,
["provider", "profile", "-g", "nemoclaw", "export", "generic", "--output", "json"],
expect.any(Object),
);
expect(mocks.runOpenshellProviderCommand).not.toHaveBeenCalled();
expect(mocks.recoverNamedGatewayRuntime).not.toHaveBeenCalled();
expect(mocks.resolveGatewayCredentialMutationAuthority).not.toHaveBeenCalled();
expect(mocks.recordExtraProvider).not.toHaveBeenCalled();
expect(mocks.runOpenshellProviderCommand).toHaveBeenNthCalledWith(
2,
expect.arrayContaining(["provider", "create", "custom-provider", "--from-existing"]),
expect.any(Object),
);
expect(mocks.recordExtraProvider).toHaveBeenCalledWith("custom-provider");
});

it("releases a provider reservation when credential registration fails (#9388)", async () => {
Expand Down Expand Up @@ -223,17 +273,19 @@ describe("credentials oclif adapter source coverage", () => {

it("rejects an incompatible OpenAI profile before provider creation", async () => {
vi.stubEnv("OPENAI_API_KEY", "host-only-secret");
mocks.runOpenshellProviderCommand.mockReturnValueOnce({
status: 0,
stdout: JSON.stringify({
id: "openai",
credentials: [],
endpoints: [{ name: "untrusted", url: "https://example.invalid" }],
binaries: [],
inference_capable: true,
}),
stderr: "",
});
mocks.runOpenshellProviderCommand
.mockReturnValueOnce({ status: 0, stdout: "", stderr: "" })
.mockReturnValueOnce({
status: 0,
stdout: JSON.stringify({
id: "openai",
credentials: [],
endpoints: [{ name: "untrusted", url: "https://example.invalid" }],
binaries: [],
inference_capable: true,
}),
stderr: "",
});

const result = await runCredentialsAddAction({
provider: "openai-prod",
Expand All @@ -245,25 +297,27 @@ describe("credentials oclif adapter source coverage", () => {

expect(result.exitCode).toBe(1);
expect(result.failureLines.join("\n")).toContain(
"does not match NemoClaw's endpointless inference contract",
"does not match NemoClaw's checked-in credential boundary",
);
expect(result.failureLines.join("\n")).toContain("then retry this command");
expect(result.failureLines.join("\n")).not.toContain("onboarding");
expect(result.failureLines.join("\n")).not.toContain("host-only-secret");
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledTimes(1);
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledWith(
["provider", "profile", "export", "openai", "--output", "json"],
{
ignoreError: true,
suppressOutput: true,
stdio: ["ignore", "pipe", "pipe"],
timeout: 30_000,
},
);
expect(mocks.runOpenshellProviderCommand.mock.calls.map(([args]) => args)).toEqual([
[
"provider",
"profile",
"-g",
"nemoclaw",
"import",
"--file",
expect.stringMatching(/provider-profiles\/openai\.yaml$/u),
],
["provider", "profile", "-g", "nemoclaw", "export", "openai", "--output", "json"],
]);
expect(mocks.recordExtraProvider).not.toHaveBeenCalled();
});

it("stops before provider creation when OpenAI profile inspection times out", async () => {
it("stops before provider creation when OpenAI profile import times out (#9806)", async () => {
vi.stubEnv("OPENAI_API_KEY", "host-only-secret");
mocks.runOpenshellProviderCommand.mockReturnValueOnce({
status: null,
Expand All @@ -280,27 +334,46 @@ describe("credentials oclif adapter source coverage", () => {
});

expect(result.exitCode).toBe(1);
expect(result.failureLines.join("\n")).toContain("could not be read for validation");
expect(result.failureLines.join("\n")).toContain("then retry this command");
expect(result.failureLines.join("\n")).toContain(
"Could not import bundled provider profile 'openai'",
);
expect(result.failureLines.join("\n")).toContain("operation timed out");
expect(result.failureLines.join("\n")).not.toContain("onboarding");
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledOnce();
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledWith(
["provider", "profile", "export", "openai", "--output", "json"],
[
"provider",
"profile",
"-g",
"nemoclaw",
"import",
"--file",
expect.stringMatching(/provider-profiles\/openai\.yaml$/u),
],
{
ignoreError: true,
suppressOutput: true,
stdio: ["ignore", "pipe", "pipe"],
timeout: 30_000,
},
);
expect(mocks.recordExtraProvider).not.toHaveBeenCalled();
});

it("imports a missing OpenAI profile before provider creation", async () => {
it("imports and verifies the OpenAI profile before provider creation (#9806)", async () => {
vi.stubEnv("OPENAI_API_KEY", "host-only-secret");
mocks.runOpenshellProviderCommand
.mockReturnValueOnce({ status: 1, stdout: "", stderr: "provider profile not found" })
.mockReturnValueOnce({ status: 0, stdout: "", stderr: "" })
.mockReturnValueOnce({
status: 0,
stdout: JSON.stringify({
id: "openai",
credentials: [],
endpoints: [],
binaries: [],
inference_capable: true,
}),
stderr: "",
})
.mockReturnValueOnce({ status: 0, stdout: "", stderr: "" });

const result = await runCredentialsAddAction({
Expand All @@ -313,17 +386,21 @@ describe("credentials oclif adapter source coverage", () => {

expect(result.exitCode).toBe(0);
expect(mocks.runOpenshellProviderCommand.mock.calls.map(([args]) => args)).toEqual([
["provider", "profile", "export", "openai", "--output", "json"],
[
"provider",
"profile",
"-g",
"nemoclaw",
"import",
"--file",
expect.stringMatching(/provider-profiles\/openai\.yaml$/u),
],
["provider", "profile", "-g", "nemoclaw", "export", "openai", "--output", "json"],
[
"provider",
"create",
"-g",
"nemoclaw",
"--name",
"openai-prod",
"--type",
Expand All @@ -337,7 +414,6 @@ describe("credentials oclif adapter source coverage", () => {
).toEqual([
{
ignoreError: true,
suppressOutput: true,
stdio: ["ignore", "pipe", "pipe"],
timeout: 30_000,
},
Expand All @@ -350,11 +426,13 @@ describe("credentials oclif adapter source coverage", () => {
]);
});

it("reports caller-neutral guidance when OpenAI profile import fails", async () => {
it("reports profile recovery guidance when OpenAI profile import fails (#9806)", async () => {
vi.stubEnv("OPENAI_API_KEY", "host-only-secret");
mocks.runOpenshellProviderCommand
.mockReturnValueOnce({ status: 1, stdout: "", stderr: "provider profile not found" })
.mockReturnValueOnce({ status: 1, stdout: "", stderr: "import failed" });
mocks.runOpenshellProviderCommand.mockReturnValueOnce({
status: 1,
stdout: "",
stderr: "import failed",
});

const result = await runCredentialsAddAction({
provider: "openai-prod",
Expand All @@ -365,10 +443,14 @@ describe("credentials oclif adapter source coverage", () => {
});

expect(result.exitCode).toBe(1);
expect(result.failureLines.join("\n")).toContain("could not import the checked-in");
expect(result.failureLines.join("\n")).toContain("then retry this command");
expect(result.failureLines.join("\n")).toContain(
"Could not import bundled provider profile 'openai'",
);
expect(result.failureLines.join("\n")).toContain(
"Fix the reported OpenShell provider-profile error, then retry",
);
expect(result.failureLines.join("\n")).not.toContain("onboarding");
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledTimes(2);
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledOnce();
expect(mocks.recordExtraProvider).not.toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion src/commands/credentials/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export default class CredentialsAddCommand extends NemoClawCommand {
multiple: true,
}),
config: Flags.string({
description: "Provider configuration pair (KEY=VALUE). Repeatable.",
description:
"Typed non-secret provider configuration. Supported: OPENAI_BASE_URL=<http(s)://public-IP/path> with --type openai. Use onboarding for hostname-based endpoints. Repeatable.",
multiple: true,
}),
"from-existing": Flags.boolean({
Expand Down
Loading
Loading