You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/29 04:16:17 UTC

(maka) branch main updated: docs(security): align sandbox boundary claims (#4065)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new da7a3a82b docs(security): align sandbox boundary claims (#4065)
da7a3a82b is described below

commit da7a3a82b1db3999fd46059d12e28b1699541c5f
Author: ooopSok <81...@users.noreply.github.com>
AuthorDate: Sat Aug 29 12:16:13 2026 +0800

    docs(security): align sandbox boundary claims (#4065)
    
    Correct stale sandbox documentation to describe the Runtime Host enforcement boundaries across macOS, Linux, and Windows.
    
    Add product-composition regression evidence and document the Windows filesystem worker operations that intentionally fail closed.
    
    Generated-by: Codex
---
 SECURITY.md                                        |  54 ++++++--
 .../__tests__/execution-model-composition.test.ts  | 148 +++++++++++++++++++--
 packages/runtime/src/sandbox/README.md             |  22 ++-
 3 files changed, 202 insertions(+), 22 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index 22021811d..d5c1c575c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -80,7 +80,7 @@ and they are NOT equally load-bearing.
   user's OS account: filesystem, network, OS permissions
   (Keychain / Microphone / Screen recording).
 
-### 2.2 The boundary: the OS user account
+### 2.2 OS enforcement boundaries
 
 **The only enforcement boundary against an adversarial LLM is the
 operating system.** Nothing inside the agent process constitutes
@@ -94,11 +94,28 @@ because they are useful UX safety nets — they catch accidental
 output and slow down adversarial output enough for a human to
 notice — but we do not ship them as guarantees.
 
-Maka does not run tools in a separate process or container by default. The
-runtime exposes a macOS Seatbelt command transformer for restricted profiles,
-but current product compositions do not yet route command execution through
-it, so it is not a product boundary today. Externally isolated runtimes may
-supply their own boundary. See
+The OS user account is Maka's outer trust envelope. Inside that envelope,
+Runtime Host also routes selected agent tool effects through OS-enforced child
+process sandboxes when the active session has a restricted managed
+`ExecutionBoundary`. On macOS and Linux, non-PTY Bash commands and the
+filesystem worker run through Seatbelt and bubblewrap respectively. On
+Windows, the AppContainer boundary currently covers the purpose-built
+filesystem worker only; arbitrary-shell Bash is unavailable and fails closed
+when the active profile requires a command sandbox.
+
+This is not universal tool containment. Bypass boundaries and unrestricted,
+disabled, or external profiles do not add a Maka-managed local sandbox;
+external environments may supply their own isolation. Managed PTY Bash is
+refused when the active profile requires sandboxing. Client-launched
+integrated terminals are host PTYs outside the managed agent execution
+boundary, and runtime or attachment resource reads do not execute through the
+local filesystem worker. The standard `workspace-write` profile permits the
+workspace roots and the configured temporary roots.
+
+Seatbelt, bubblewrap, and AppContainer are OS enforcement mechanisms. The
+in-process checks that decide whether to request or invoke them remain
+heuristics, not containment. The exact product coverage and fail-closed
+selection rules are documented in
 [`packages/runtime/src/sandbox/README.md`](./packages/runtime/src/sandbox/README.md).
 
 ### 2.3 Boundaries we DO treat as load-bearing
@@ -106,7 +123,14 @@ supply their own boundary. See
 1. **OS user account.** Tools run with the user's privileges. The
    user is expected to run Maka as a non-admin account on systems
    where that matters.
-2. **Credential-at-rest boundaries.** The provider credential store
+2. **Restricted managed tool sandboxes.** For the product surfaces listed in
+   §2.2, the active session `ExecutionBoundary` is compiled into a per-command
+   Seatbelt, bubblewrap, or AppContainer policy. Required enforcement fails
+   closed rather than silently retrying on the host. This boundary is limited
+   to the documented tool and platform matrix; it does not include bypass,
+   unrestricted, disabled, external, managed PTY, or integrated-terminal
+   execution.
+3. **Credential-at-rest boundaries.** The provider credential store
    writes `credentials.json` as versioned plaintext JSON under the
    user's workspace directory. Its load-bearing boundary is the OS
    user account plus filesystem controls: directory mode 0o700,
@@ -119,21 +143,21 @@ supply their own boundary. See
    this boundary anymore. Pre-existing safeStorage-encrypted credential
    or token files are not imported; users with only those copies must
    re-authenticate.
-3. **Renderer process sandbox + preload IPC bridge.** The
+4. **Renderer process sandbox + preload IPC bridge.** The
    renderer cannot reach files, network, or shell directly. Every
    IPC handler in `apps/desktop/src/main/main.ts` is the trust
    boundary between renderer-controlled input and main-process
    action. Renderer code is treated as semi-trusted: it can read
    masked / sanitized data, but cleartext secrets never cross the
    boundary in the main-to-renderer direction (see §4).
-4. **Settings sensitive masking.** Tokens, API keys, and proxy
+5. **Settings sensitive masking.** Tokens, API keys, and proxy
    passwords are masked at the IPC store boundary
    (`maskAppSettings` in `apps/desktop/src/main/settings-ipc-helpers.ts`).
    Re-submitting the mask sentinel `••••••` is interpreted as
    "keep current" by the merge logic; an empty string is
    interpreted as an explicit clear. The Tavily API key follows the
    same boundary.
-5. **Network egress through user-configured proxies.** The
+6. **Network egress through user-configured proxies.** The
    `network.proxy` settings drive Electron's session proxy. Tools
    that bypass `proxiedFetch` (Tavily lives in main, uses
    standard fetch) are individually audited.
@@ -156,8 +180,10 @@ are welcome as ordinary issues, not security advisories.
    URLs out of agent tool results before they reach a renderer
    `<a href>`.
 4. **`PermissionMode.ask`** as default. Mode names are UX controls,
-   not security boundaries; even permissive modes remain subject to
-   the current policy table and OS isolation boundary.
+   not security boundaries. `ask` and `explore` establish restricted
+   managed profiles, while `bypass` explicitly does not promise a
+   Maka-managed local sandbox; the active `ExecutionBoundary` is the
+   execution authority.
 5. **Modal-lifecycle contract test.** Catches the React
    `useEffect`-before-`if (!open) return null` pattern that can
    violate React hook ordering. Static analysis only.
@@ -204,6 +230,10 @@ boundary in §2.3 was crossed. Examples:
   result envelopes, log lines).
 - A tool intended to be permission-gated bypasses the
   PermissionEngine.
+- A non-PTY Bash command or local-path filesystem operation under a
+  restricted managed boundary escapes its effective Seatbelt,
+  bubblewrap, or supported AppContainer policy, or silently falls
+  back to host execution when required enforcement is unavailable.
 
 Reports against the §2.4 heuristics are out of scope as security
 advisories. They are welcome as ordinary issues or pull requests.
diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
index 53d3260a5..2b8b24512 100644
--- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
@@ -21,8 +21,8 @@ import assert from 'node:assert/strict';
 import { execFile } from 'node:child_process';
 import { randomUUID } from 'node:crypto';
 import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
-import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
+import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
+import { homedir, tmpdir } from 'node:os';
 import { join } from 'node:path';
 import { promisify } from 'node:util';
 import { test } from 'node:test';
@@ -270,8 +270,9 @@ test('production Host executes current-boundary Bash and refreshes live sandbox
   const base = await mkdtemp(join(tmpdir(), 'maka-host-managed-bash-'));
   const root = join(base, 'interactive');
   const project = join(base, 'project');
+  let outsideRoot: string | undefined;
+  let sandboxPaths: ManagedSandboxPaths | undefined;
   const provider = await startProvider();
-  provider.configureManagedBashFlow();
   const capability = await resolveStorageRoot({ path: root, kind: 'interactive' });
   const owner = await tryAcquireInteractiveRootOwner(capability);
   assert.ok(owner);
@@ -284,6 +285,16 @@ test('production Host executes current-boundary Bash and refreshes live sandbox
   };
   let composition: Awaited<ReturnType<typeof createExecutionRuntimeHostComposition>> | undefined;
   try {
+    if (process.platform === 'darwin') {
+      outsideRoot = await mkdtemp(join(homedir(), '.maka-host-sandbox-boundary-'));
+      sandboxPaths = {
+        outsideBash: join(outsideRoot, 'bash-denied.txt'),
+        outsideWrite: join(outsideRoot, 'write-denied.txt'),
+        workspaceBash: join(project, 'bash-allowed.txt'),
+        workspaceWrite: join(project, 'write-allowed.txt'),
+      };
+    }
+    provider.configureManagedBashFlow(sandboxPaths);
     await mkdir(project);
     const policy = await openInteractiveRuntimePolicyStoresForWrite(owner.lease);
     const created = await policy.connectionCatalog.create({
@@ -450,6 +461,77 @@ test('production Host executes current-boundary Bash and refreshes live sandbox
     const refreshedRequestText = JSON.stringify(refreshedRequests[2]?.body);
     assert.match(refreshedRequestText, /<sandbox_context>/u);
     assert.match(refreshedRequestText, /Network: enabled/u);
+
+    if (sandboxPaths) {
+      const sandboxTurnId = 'hosted-managed-sandbox-turn-3';
+      const sandboxTerminal = await waitForTerminal(
+        composition,
+        session.id,
+        sandboxTurnId,
+        await startTurn(
+          composition,
+          session.id,
+          sandboxTurnId,
+          'Exercise the enforced filesystem boundary.',
+          context,
+        ),
+        context,
+      );
+      assert.equal(sandboxTerminal.status, 'completed');
+
+      const sandboxRequests = provider.requests.filter((request) => request.body.stream === true);
+      assert.equal(sandboxRequests.length, 8);
+      assert.match(latestToolResultText(sandboxRequests[4]!.body) ?? '', /macos-seatbelt/u);
+      assert.match(
+        latestToolResultText(sandboxRequests[4]!.body) ?? '',
+        /Operation not permitted/u,
+      );
+      assert.match(
+        latestToolResultText(sandboxRequests[5]!.body) ?? '',
+        /sandbox_boundary_required/u,
+      );
+      assert.equal(await fileExists(sandboxPaths.outsideBash), false);
+      assert.equal(await fileExists(sandboxPaths.outsideWrite), false);
+      assert.equal(await readFile(sandboxPaths.workspaceBash, 'utf8'), 'bash allowed');
+      assert.equal(await readFile(sandboxPaths.workspaceWrite, 'utf8'), 'write allowed');
+
+      const sandboxEvents = await execution.runtimeEventStore.readRuntimeEvents(
+        session.id,
+        sandboxTerminal.runId,
+      );
+      const sandboxResponses = sandboxEvents.filter(
+        (event) => event.content?.kind === 'function_response',
+      );
+      assert.deepEqual(
+        sandboxResponses.map((event) =>
+          event.content?.kind === 'function_response'
+            ? {
+                name: event.content.name,
+                isError: event.content.isError === true,
+              }
+            : undefined,
+        ),
+        [
+          { name: 'Bash', isError: true },
+          { name: 'Write', isError: true },
+          { name: 'Bash', isError: false },
+          { name: 'Write', isError: false },
+        ],
+      );
+      assert.equal(
+        sandboxEvents.some(
+          (event) => event.actions?.stateDelta?.sandboxBoundaryRequest !== undefined,
+        ),
+        false,
+      );
+      assert.deepEqual(
+        await execution.sessionStore.listPendingSandboxBoundaryRequests(session.id),
+        [],
+      );
+      const sandboxBoundary = await execution.sessionStore.readExecutionBoundary(session.id);
+      assert.equal(sandboxBoundary.kind, 'managed');
+      assert.equal(sandboxBoundary.revision, expanded.boundary.revision);
+    }
   } finally {
     try {
       await composition?.close();
@@ -457,8 +539,15 @@ test('production Host executes current-boundary Bash and refreshes live sandbox
       try {
         await owner.close();
       } finally {
-        await provider.close();
-        await rm(base, { recursive: true, force: true });
+        try {
+          await provider.close();
+        } finally {
+          try {
+            await rm(base, { recursive: true, force: true });
+          } finally {
+            if (outsideRoot) await rm(outsideRoot, { recursive: true, force: true });
+          }
+        }
       }
     }
   }
@@ -3872,9 +3961,19 @@ interface ProviderRequest {
   readonly body: Record<string, unknown>;
 }
 
+interface ManagedSandboxPaths {
+  readonly outsideBash: string;
+  readonly outsideWrite: string;
+  readonly workspaceBash: string;
+  readonly workspaceWrite: string;
+}
+
 type ProviderFlow =
   | { readonly kind: 'default' }
-  | { readonly kind: 'managed_bash' }
+  | {
+      readonly kind: 'managed_bash';
+      readonly sandboxPaths?: ManagedSandboxPaths;
+    }
   | {
       readonly kind: 'client_capability';
       readonly groupId: string;
@@ -3891,7 +3990,7 @@ type ProviderFlow =
 async function startProvider(): Promise<{
   readonly baseUrl: string;
   readonly requests: ProviderRequest[];
-  configureManagedBashFlow(): void;
+  configureManagedBashFlow(sandboxPaths?: ManagedSandboxPaths): void;
   configureClientCapability(input: { groupId: string; toolName: string }): void;
   configureChildAgentFlow(): void;
   configureImplementationChildAgentFlow(): void;
@@ -3911,9 +4010,12 @@ async function startProvider(): Promise<{
   return {
     baseUrl: `http://127.0.0.1:${address.port}/v1`,
     requests,
-    configureManagedBashFlow: () => {
+    configureManagedBashFlow: (sandboxPaths) => {
       if (flow.kind !== 'default') throw new Error('Provider flow is already configured');
-      flow = { kind: 'managed_bash' };
+      flow = {
+        kind: 'managed_bash',
+        ...(sandboxPaths ? { sandboxPaths } : {}),
+      };
     },
     configureClientCapability: (input) => {
       if (flow.kind !== 'default') throw new Error('Provider flow is already configured');
@@ -4008,6 +4110,34 @@ async function handleProviderRequest(
     });
     return;
   }
+  if (flow.kind === 'managed_bash' && flow.sandboxPaths && streamRequestIndex === 4) {
+    respondProviderToolCall(response, streamRequestIndex, 'Bash', {
+      command: `printf denied > ${JSON.stringify(flow.sandboxPaths.outsideBash)}`,
+      boundary_intent: 'current',
+    });
+    return;
+  }
+  if (flow.kind === 'managed_bash' && flow.sandboxPaths && streamRequestIndex === 5) {
+    respondProviderToolCall(response, streamRequestIndex, 'Write', {
+      path: flow.sandboxPaths.outsideWrite,
+      content: 'write denied',
+    });
+    return;
+  }
+  if (flow.kind === 'managed_bash' && flow.sandboxPaths && streamRequestIndex === 6) {
+    respondProviderToolCall(response, streamRequestIndex, 'Bash', {
+      command: `printf 'bash allowed' > ${JSON.stringify(flow.sandboxPaths.workspaceBash)}`,
+      boundary_intent: 'current',
+    });
+    return;
+  }
+  if (flow.kind === 'managed_bash' && flow.sandboxPaths && streamRequestIndex === 7) {
+    respondProviderToolCall(response, streamRequestIndex, 'Write', {
+      path: flow.sandboxPaths.workspaceWrite,
+      content: 'write allowed',
+    });
+    return;
+  }
   if (flow.kind === 'managed_bash') {
     respondProviderText(response, RESPONSE_TEXT);
     return;
diff --git a/packages/runtime/src/sandbox/README.md b/packages/runtime/src/sandbox/README.md
index a1e33854c..5441f5b73 100644
--- a/packages/runtime/src/sandbox/README.md
+++ b/packages/runtime/src/sandbox/README.md
@@ -22,7 +22,7 @@
 This directory owns platform sandbox selection and command transformation. It translates the profile in an active session `ExecutionBoundary` into an execution request; it does not decide whether a requested boundary expansion is approved and does not execute the request itself.
 
 Code and focused tests are the final authority. Windows enforcement work is tracked in
-[issue #2142](https://github.com/maka-agent/maka-agent/issues/2142) and specified by the
+[issue #2142](https://github.com/apache/maka/issues/2142) and specified by the
 [Windows sandbox backend RFC](../../../../docs/architecture/windows-sandbox-rfc-v1.md)
 ([中文](../../../../docs/architecture/windows-sandbox-rfc-v1.zh-CN.md)).
 
@@ -58,6 +58,22 @@ Code and focused tests are the final authority. Windows enforcement work is trac
   fails closed as unavailable. Other unsupported platforms return `unsupported_platform`.
 - A backend that receives an invalid or unsupported profile returns a typed failure; it does not silently downgrade to host execution.
 
+## Product coverage
+
+| Surface                                                                             | macOS                                                     | Linux                                                     | Windows                                                                                                     | When no Maka-managed sandbox is required                                                                                          |
+| ----------------------------------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| Agent Bash, foreground or background without a PTY                                  | Seatbelt                                                  | bubblewrap                                                | Restricted managed execution fails closed because the AppContainer broker cannot launch an arbitrary shell  | Runs through the detected host shell                                                                                              |
+| Agent Bash with a PTY                                                               | Refused when the active profile requires sandboxing       | Refused when the active profile requires sandboxing       | Refused when the active profile requires sandboxing                                                         | Runs as a host PTY                                                                                                                |
+| Local-path `Read`, `Write`, `Edit`, `FormatJson`, `Glob`, `Grep`, and `apply_patch` | Filesystem worker under Seatbelt                          | Filesystem worker under bubblewrap                        | Purpose-built filesystem worker under the AppContainer broker, subject to the fail-closed limitations below | Managed execution uses the worker without an OS sandbox; bypass uses the host-local executor; external uses the injected executor |
+| `Read` of runtime or attachment resource refs                                       | Resource service; not a local filesystem-worker operation | Resource service; not a local filesystem-worker operation | Resource service; not a local filesystem-worker operation                                                   | Same resource-service path                                                                                                        |
+| Client `runtime.resource.start` integrated terminal                                 | Host PTY outside the managed agent boundary               | Host PTY outside the managed agent boundary               | Host PTY outside the managed agent boundary                                                                 | Same host PTY path                                                                                                                |
+
+In the Windows AppContainer preview, local-path `Read`, `Glob`, existing-target `Write`, `Edit`, `FormatJson`, and `apply_patch` update operations run through the filesystem worker. `Grep` fails closed with `grep_unavailable`. Missing-target `Write` and `apply_patch` create/delete operations also fail closed because the current broker policy cannot represent exact parent-entry write authority without widening the kernel grant.
+
+`ask` starts with the managed `workspace-write` profile and `explore` starts with a managed read-only profile. Both profiles require a platform sandbox because their filesystem or network policy is restricted. `workspace-write` permits writes to the workspace roots, `:tmpdir`, and `:slash_tmp`; it is not a workspace-only profile.
+
+A bypass boundary, unrestricted managed profiles, and disabled profiles do not request a Maka-managed local sandbox. When the filesystem worker is wired, managed execution can still use that worker as its backend while `SandboxManager` selects `none`; this is process separation, not OS sandbox enforcement. A bypass boundary uses the host-local executor, while an external boundary delegates filesystem isolation to its injected workspace executor and does not stack a local platform sandbox [...]
+
 ## Boundaries
 
 - The session `ExecutionBoundary` is the authority for whether an operation is currently inside the sandbox boundary. Sandbox selection does not expand that boundary.
@@ -82,7 +98,11 @@ Code and focused tests are the final authority. Windows enforcement work is trac
 - Selection and transformation: `packages/runtime/src/__tests__/sandbox-manager.test.ts`
 - macOS policy and wrapper: `packages/runtime/src/__tests__/macos-seatbelt.test.ts`
 - macOS platform behavior: `packages/runtime/src/__tests__/macos-seatbelt-smoke.test.ts`
+- macOS filesystem-worker behavior: `packages/runtime/src/__tests__/filesystem-worker-smoke.test.ts`
 - Linux policy and wrapper: `packages/runtime/src/__tests__/linux-sandbox.test.ts`
 - Linux platform behavior: `packages/runtime/src/__tests__/linux-sandbox-smoke.test.ts`
+- Linux filesystem-worker behavior: `packages/runtime/src/__tests__/filesystem-worker-linux-smoke.test.ts`
 - Windows profile and broker transform: `windows-profile.test.ts` and `windows-sandbox.test.ts`
+- Windows filesystem-worker behavior: `packages/runtime/src/__tests__/filesystem-worker-windows-smoke.test.ts`
+- Runtime Host product composition: `packages/runtime-host/src/__tests__/execution-model-composition.test.ts`
 - Public exports and default registration: `sandbox-export.test.ts` and `default-sandbox-manager.test.ts`


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org