Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 42 additions & 12 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -94,19 +94,43 @@ 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

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,
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -249,8 +249,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);
Expand All @@ -263,6 +264,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({
Expand Down Expand Up @@ -428,15 +439,93 @@ 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();
} finally {
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 });
}
}
}
}
}
Expand Down Expand Up @@ -3851,9 +3940,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;
Expand All @@ -3870,7 +3969,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;
Expand All @@ -3890,9 +3989,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');
Expand Down Expand Up @@ -3987,6 +4089,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;
Expand Down
Loading