Skip to content

fix(sandbox): add network-restricted sandbox hints#71

Merged
BlackHole1 merged 1 commit intomainfrom
sandbox
Apr 18, 2026
Merged

fix(sandbox): add network-restricted sandbox hints#71
BlackHole1 merged 1 commit intomainfrom
sandbox

Conversation

@BlackHole1
Copy link
Copy Markdown
Member

Sandboxed network failures were surfacing as generic request errors, which made it unclear that the user should retry with elevated permissions.

Detect socket-open and connection-refused sandbox errors in the shared request path, append localized guidance, and update affected auth, connector, file, package, and skills flows with coverage.

Sandboxed network failures were surfacing as generic request errors,
which made it unclear that the user should retry with elevated
permissions.

Detect socket-open and connection-refused sandbox errors in the shared
request path, append localized guidance, and update affected auth,
connector, file, package, and skills flows with coverage.

Signed-off-by: Kevin Cui <bh@bugs.cc>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 18, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Improved error handling for network-restricted sandbox environments with actionable hints to request elevated permissions.
    • Enhanced error messages across authentication, connector operations, file transfers, and package searches to include network restriction guidance.
  • Documentation

    • Updated skill documentation to clarify outbound network access requirements and recovery steps for sandboxed environments.
  • Tests

    • Added test coverage for network failure scenarios in sandboxed environments.

Walkthrough

This PR adds i18n translator support and network-error detection throughout the CLI. It threads a translator property through execution contexts across multiple command domains (auth, connector, file, package, skills), adds helper functions for creating network-related errors in tests, introduces detection and message augmentation for sandbox-restricted network failures, and updates the i18n catalog with new translated messages for these scenarios. A skill documentation note is also added regarding outbound network access requirements. The changes affect function signatures, test cases, error handling paths, and translation keys without modifying core control flow logic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows the required format and clearly describes the main change: adding network-restricted sandbox hints to improve user guidance during sandboxed network failures.
Description check ✅ Passed The PR description is directly related to the changeset, explaining the problem being solved (sandboxed network failures appearing generic) and the solution (detecting and appending localized guidance).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/application/commands/auth/index.cli.test.ts (1)

6-17: 🛠️ Refactor suggestion | 🟠 Major

Reuse writeAuthFile for these auth-status sandbox tests and remove unnecessary async from throwing fetchers.

Both new tests duplicate the same auth TOML setup. The shared helper writeAuthFile already creates this default account (user-1 / Alice / secret-1), so using it avoids drift and makes tests focus on network behavior. The throwing fetchers can also be synchronous since they contain no await.

Proposed changes
 import {
     createCliSandbox,
     createCliSnapshot,
     createConnectionRefusedError,
     createFailedToOpenSocketError,
     defaultAuthEndpoint,
     findLoginUrl,
     readAuthLoginUrlPrefix,
     readLatestLogContent,
     runPrintedAuthLogin,
     toRequest,
+    writeAuthFile,
 } from "../../../../__tests__/helpers.ts";

In the first test (around line 376):

-            const authFilePath = join(
-                sandbox.env.XDG_CONFIG_HOME!,
-                APP_NAME,
-                "auth.toml",
-            );
-
-            await Bun.write(
-                authFilePath,
-                [
-                    "id = \"user-1\"",
-                    "",
-                    "[[auth]]",
-                    "id = \"user-1\"",
-                    "name = \"Alice\"",
-                    "api_key = \"secret-1\"",
-                    "endpoint = \"oomol.com\"",
-                    "",
-                ].join("\n"),
-            );
+            await writeAuthFile(sandbox);
 
             const result = await sandbox.run(
                 ["auth", "status"],
                 {
-                    fetcher: async () => {
+                    fetcher: () => {
                         throw createFailedToOpenSocketError("network down");
                     },
                 },

Apply the same replacement in the connection-refused test.

Per coding guidelines: "Extract repeated setup (mocks, stubs, setup objects) into local factory functions in test files" and "Never mark a function async if it contains no await".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/auth/index.cli.test.ts` around lines 6 - 17, Replace
the duplicated auth TOML setup in the auth-status sandbox tests by calling the
existing helper writeAuthFile (which creates the default account
user-1/Alice/secret-1) instead of re-writing the file in-test; locate the two
tests that currently use createCliSandbox/createCliSnapshot with inline auth
content and swap that setup to invoke writeAuthFile before creating the sandbox,
and also remove the unnecessary async keyword from the throwing fetcher
functions (they contain no await) so the throwing fetchers used in the
connection-refused and other network-behavior tests are plain synchronous
functions.
🧹 Nitpick comments (4)
src/application/commands/shared/request.test.ts (1)

182-262: Consolidate the duplicated sandbox-hint tests using a table-driven approach. Both tests repeat the same request context and error constructors with only the error factory and expected message differing. Extract this common setup and use a loop to eliminate duplication, removing the unnecessary async keywords from the fetchers since they only throw and never await.

Example refactor
-    test("adds a sandbox network hint when the fetcher cannot open a socket", async () => {
-        const logCapture = createLogCapture();
-
-        try {
-            await expect(executeCliRequest({
-                context: {
-                    fetcher: async () => {
-                        throw createFailedToOpenSocketError("network down");
-                    },
-                    logger: logCapture.logger,
-                    translator: createTranslator("zh"),
-                },
-                createRequestError: error => new CliUserError(
-                    "errors.shared.requestError",
-                    1,
-                    {
-                        message: error instanceof Error ? error.message : String(error),
-                    },
-                ),
-                createRequestFailedError: status => new CliUserError(
-                    "errors.shared.requestFailed",
-                    1,
-                    {
-                        status,
-                    },
-                ),
-                label: "Shared",
-                requestUrl: new URL("https://example.com/items/1"),
-            })).rejects.toMatchObject({
-                key: "errors.shared.requestError",
-                params: {
-                    message:
-                        "network down\n当前环境可能在网络受限的沙箱中,请尝试提权。",
-                },
-            });
-        }
-        finally {
-            logCapture.close();
-        }
-    });
-
-    test("adds a sandbox network hint when the fetcher connection is refused", async () => {
+    for (const testCase of [
+        {
+            title: "adds a sandbox network hint when the fetcher cannot open a socket",
+            createError: () => createFailedToOpenSocketError("network down"),
+            expectedMessage: "network down\n当前环境可能在网络受限的沙箱中,请尝试提权。",
+        },
+        {
+            title: "adds a sandbox network hint when the fetcher connection is refused",
+            createError: () => createConnectionRefusedError("connection refused"),
+            expectedMessage: "connection refused\n当前环境可能在网络受限的沙箱中,请尝试提权。",
+        },
+    ] as const) {
+        test(testCase.title, async () => {
         const logCapture = createLogCapture();
 
         try {
             await expect(executeCliRequest({
                 context: {
-                    fetcher: async () => {
-                        throw createConnectionRefusedError("connection refused");
+                    fetcher: () => {
+                        throw testCase.createError();
                     },
                     logger: logCapture.logger,
                     translator: createTranslator("zh"),
@@
                 key: "errors.shared.requestError",
                 params: {
-                    message:
-                        "connection refused\n当前环境可能在网络受限的沙箱中,请尝试提权。",
+                    message: testCase.expectedMessage,
                 },
             });
         }
         finally {
             logCapture.close();
         }
-    });
+        });
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/shared/request.test.ts` around lines 182 - 262,
Tests duplicate nearly identical cases for sandbox hint behavior; consolidate
them into a table-driven loop that iterates over error factories and expected
messages. Replace the two separate tests with a single test that builds a cases
array (e.g. entries pairing createFailedToOpenSocketError and "network
down\n当前环境可能在网络受限的沙箱中,请尝试提权。" and createConnectionRefusedError and "connection
refused\n当前环境可能在网络受限的沙箱中,请尝试提权。"), and for each case call executeCliRequest with
the same context (use createLogCapture(), createTranslator("zh"), same
CliUserError factories, requestUrl) but set fetcher to a non-async throwing
function created from the case's error factory; assert rejects.toMatchObject as
before and ensure logCapture.close() runs after the loop.
src/application/auth/login-flow.test.ts (1)

174-196: Hardcoded localized string couples this test to the i18n catalog.

The assertion embeds the full zh translation (当前环境可能在网络受限的沙箱中,请尝试提权。). Any wording tweak in the catalog will break this unrelated auth test. Consider asserting via the same translator instance (e.g., build the expected suffix from translator.t("…sandboxHint…")) or using expect.stringContaining("network down") plus a dedicated test in the shared request module that owns the hint text.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/auth/login-flow.test.ts` around lines 174 - 196, The test is
hardcoding the full Chinese sandbox hint which couples it to the i18n catalog;
change the assertion to derive the expected localized suffix from the same
translator instance (created via createTranslator("zh")) or use partial
assertions: either build the expected params.message by concatenating the error
message with translator.t("sandboxHint") and compare that object from
startAuthLoginSession, or replace the strict message check with
expect.stringContaining("network down") plus
expect.stringContaining(translator.t("sandboxHint")). Update references in the
failing test that call startAuthLoginSession, createTranslator, and
createFailedToOpenSocketError accordingly.
src/application/commands/connector/shared.test.ts (1)

208-229: Duplicated hardcoded hint string across tests.

The en sandbox hint ("Current environment may be running in a network-restricted sandbox. Try requesting elevated permissions.") is embedded here and the zh variant is embedded in src/application/auth/login-flow.test.ts. Each consumer of getUnexpectedRequestErrorMessage re-asserts the same copy, so a catalog wording change cascades across unrelated test files. Prefer asserting the behavior via the translator (resolve the key at test time) and keep one exhaustive text-level test in the shared request module tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/connector/shared.test.ts` around lines 208 - 229,
The test hardcodes the English sandbox hint; instead resolve the translation at
test time so wording changes don’t break unrelated tests—update the assertion in
the test using runConnectorAction and createRequestContext to call the app
translator (the same translation key used by getUnexpectedRequestErrorMessage)
and compare error.params.message to the concatenation of the original error
message and the translator-resolved sandbox hint, rather than embedding the
literal English string.
src/application/commands/connector/shared.ts (1)

97-103: Optional: consider using getUnexpectedRequestErrorMessage consistently.

The createUnexpectedError callbacks in searchConnectorActions, listAuthenticatedConnectorServices, and getConnectorActionMetadata still inline error instanceof Error ? error.message : String(error). This currently works only because performLoggedRequest pre-enhances the error with the sandbox hint. Routing all three through getUnexpectedRequestErrorMessage(error, context.translator) (as runConnectorAction does) would make the hint behavior explicit at each call site and remove a silent dependency on upstream wrapping — matching the intent called out in the coding guideline to extract shared utilities.

Also applies to: 158-164, 214-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/connector/shared.ts` around lines 97 - 103, The
createUnexpectedError lambda currently constructs the error message inline;
update the three sites (searchConnectorActions,
listAuthenticatedConnectorServices, getConnectorActionMetadata) to call
getUnexpectedRequestErrorMessage(error, context.translator) when building the
CliUserError so the sandbox hint behavior is explicit and consistent with
runConnectorAction; leave the CliUserError construction (message key and
metadata shape) intact but replace the message expression with the shared
helper, and confirm performLoggedRequest still supplies the original error to
preserve existing context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/application/commands/skills/search.cli.test.ts`:
- Around line 251-275: The fetcher passed into sandbox.run in the test "adds a
sandbox hint when the skills search request cannot open a socket" is marked
async but only performs a synchronous throw (createFailedToOpenSocketError), so
remove the unnecessary async keyword from the fetcher callback (change fetcher:
async () => { throw ... } to fetcher: () => { throw ... }) in that test to
satisfy the "no async without await" guideline while keeping its behavior
unchanged.

---

Outside diff comments:
In `@src/application/commands/auth/index.cli.test.ts`:
- Around line 6-17: Replace the duplicated auth TOML setup in the auth-status
sandbox tests by calling the existing helper writeAuthFile (which creates the
default account user-1/Alice/secret-1) instead of re-writing the file in-test;
locate the two tests that currently use createCliSandbox/createCliSnapshot with
inline auth content and swap that setup to invoke writeAuthFile before creating
the sandbox, and also remove the unnecessary async keyword from the throwing
fetcher functions (they contain no await) so the throwing fetchers used in the
connection-refused and other network-behavior tests are plain synchronous
functions.

---

Nitpick comments:
In `@src/application/auth/login-flow.test.ts`:
- Around line 174-196: The test is hardcoding the full Chinese sandbox hint
which couples it to the i18n catalog; change the assertion to derive the
expected localized suffix from the same translator instance (created via
createTranslator("zh")) or use partial assertions: either build the expected
params.message by concatenating the error message with
translator.t("sandboxHint") and compare that object from startAuthLoginSession,
or replace the strict message check with expect.stringContaining("network down")
plus expect.stringContaining(translator.t("sandboxHint")). Update references in
the failing test that call startAuthLoginSession, createTranslator, and
createFailedToOpenSocketError accordingly.

In `@src/application/commands/connector/shared.test.ts`:
- Around line 208-229: The test hardcodes the English sandbox hint; instead
resolve the translation at test time so wording changes don’t break unrelated
tests—update the assertion in the test using runConnectorAction and
createRequestContext to call the app translator (the same translation key used
by getUnexpectedRequestErrorMessage) and compare error.params.message to the
concatenation of the original error message and the translator-resolved sandbox
hint, rather than embedding the literal English string.

In `@src/application/commands/connector/shared.ts`:
- Around line 97-103: The createUnexpectedError lambda currently constructs the
error message inline; update the three sites (searchConnectorActions,
listAuthenticatedConnectorServices, getConnectorActionMetadata) to call
getUnexpectedRequestErrorMessage(error, context.translator) when building the
CliUserError so the sandbox hint behavior is explicit and consistent with
runConnectorAction; leave the CliUserError construction (message key and
metadata shape) intact but replace the message expression with the shared
helper, and confirm performLoggedRequest still supplies the original error to
preserve existing context.

In `@src/application/commands/shared/request.test.ts`:
- Around line 182-262: Tests duplicate nearly identical cases for sandbox hint
behavior; consolidate them into a table-driven loop that iterates over error
factories and expected messages. Replace the two separate tests with a single
test that builds a cases array (e.g. entries pairing
createFailedToOpenSocketError and "network down\n当前环境可能在网络受限的沙箱中,请尝试提权。" and
createConnectionRefusedError and "connection refused\n当前环境可能在网络受限的沙箱中,请尝试提权。"),
and for each case call executeCliRequest with the same context (use
createLogCapture(), createTranslator("zh"), same CliUserError factories,
requestUrl) but set fetcher to a non-async throwing function created from the
case's error factory; assert rejects.toMatchObject as before and ensure
logCapture.close() runs after the loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e690cd2-7867-4d18-9982-e4d5cffed3a8

📥 Commits

Reviewing files that changed from the base of the PR and between a184f88 and 0c84531.

📒 Files selected for processing (28)
  • __tests__/helpers.ts
  • contrib/skills/codex/oo/SKILL.md
  • src/application/auth/login-flow.test.ts
  • src/application/auth/login-flow.ts
  • src/application/commands/auth/index.cli.test.ts
  • src/application/commands/auth/login.ts
  • src/application/commands/auth/status.ts
  • src/application/commands/cloud-task/shared.ts
  • src/application/commands/connector/schema-cache.test.ts
  • src/application/commands/connector/schema-cache.ts
  • src/application/commands/connector/search-provider.ts
  • src/application/commands/connector/shared.test.ts
  • src/application/commands/connector/shared.ts
  • src/application/commands/file/download/plan.ts
  • src/application/commands/file/download/request.test.ts
  • src/application/commands/file/download/request.ts
  • src/application/commands/file/shared.test.ts
  • src/application/commands/file/shared.ts
  • src/application/commands/package/search-provider.ts
  • src/application/commands/package/shared.test.ts
  • src/application/commands/package/shared.ts
  • src/application/commands/shared/request.test.ts
  • src/application/commands/shared/request.ts
  • src/application/commands/skills/registry-skill-source.test.ts
  • src/application/commands/skills/registry-skill-source.ts
  • src/application/commands/skills/search.cli.test.ts
  • src/application/commands/skills/search.ts
  • src/i18n/catalog.ts

Comment on lines +251 to +275
test("adds a sandbox hint when the skills search request cannot open a socket", async () => {
const sandbox = await createCliSandbox();

try {
await writeAuthFile(sandbox);

const result = await sandbox.run(
["skills", "search", "text generation"],
{
fetcher: async () => {
throw createFailedToOpenSocketError("network down");
},
},
);

expect(result.exitCode).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toBe(
"The skills search request failed: network down\nCurrent environment may be running in a network-restricted sandbox. Try requesting elevated permissions.\n",
);
}
finally {
await sandbox.cleanup();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the changed skills-search sandbox test no longer contains an async callback with no await.
awk 'NR>=251 && NR<=275 { print NR ":" $0 }' src/application/commands/skills/search.cli.test.ts | sed -n '/fetcher: async/,+4p'

Repository: oomol-lab/oo-cli

Length of output: 256


Remove async from the throwing test fetcher—it has no await.

Line 260 marks the fetcher callback as async but contains only a synchronous throw with no await, violating the coding guideline that states: "Never mark a function async if it contains no await; remove async when all code paths are synchronous."

Change
-                    fetcher: async () => {
+                    fetcher: () => {
                         throw createFailedToOpenSocketError("network down");
                     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/skills/search.cli.test.ts` around lines 251 - 275,
The fetcher passed into sandbox.run in the test "adds a sandbox hint when the
skills search request cannot open a socket" is marked async but only performs a
synchronous throw (createFailedToOpenSocketError), so remove the unnecessary
async keyword from the fetcher callback (change fetcher: async () => { throw ...
} to fetcher: () => { throw ... }) in that test to satisfy the "no async without
await" guideline while keeping its behavior unchanged.

@BlackHole1 BlackHole1 merged commit 15ff080 into main Apr 18, 2026
5 checks passed
@BlackHole1 BlackHole1 deleted the sandbox branch April 18, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant