Skip to content
Merged
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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,13 @@ jobs:
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- run: npm ci
- run: npm run build
- name: Linked worktree lane containment
- name: Linked worktree lane and root Git metadata containment
env:
LIBRECHAT_CODE_LIVE_SRT_TESTS: '1'
run: node --test dist/linked-worktrees-live.test.js
run: >-
node --test
dist/linked-worktrees-live.test.js
dist/root-git-denies-live.test.js

lambda-microvm-provisioning:
name: Lambda MicroVM Provisioning
Expand Down
28 changes: 28 additions & 0 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,40 @@ SRT with:
worker's private scratch directory;
- read access denied to the worker's home directory except for that workspace;
- paired identity and mutation-quarantine files explicitly denied;
- the registered workspace's own Git metadata denied writes — `.git/hooks`
and `.git/config` always, plus `.git/config.worktree`, `.git/commondir`,
and the equivalent files under `.git/modules/*` and `.git/worktrees/*`
where they already exist — so a sandboxed command cannot plant a hook or a
`filter`/`fsmonitor`/`diff` config entry that would run unsandboxed the
next time Git runs in the checkout;
- `LIBRECHAT_CODE_*` and nonessential inherited environment variables removed;
- network egress denied by default, local binding denied, and Unix sockets
denied; and
- bounded time and aggregate output, with best-effort process-group termination
on cancellation, timeout, and completion.

The Git-metadata denies are applied to the registered root directly rather
than relying on SRT's Linux mandatory denies, which are derived from the
worker process's own current directory — the worker home, not the workspace —
and so never covered the registered root; macOS already enforced the
equivalent through global Seatbelt patterns, and this keeps the guarantee
identical on both platforms regardless of the worker's cwd. The set is
recomputed before every command, so a repository, submodule, or linked
worktree created on the host after the worker starts is covered from the next
command, and metadata the worker cannot inspect fails the command closed.
`.git/commondir` and `.git/config.worktree` are denied only where they already
exist, because Git reads them strictly and SRT would otherwise mask an absent
one with a stub Git cannot open, failing every Git command. Because the whole
workspace stays writable, a sandboxed command can still stage Git
configuration Git will honor later by other means — for example replacing
the entire `.git` directory, writing a new `commondir` that redirects the
common directory, creating a missing `config.worktree` in a repository that
already enables the `worktreeConfig` extension, or initializing a fresh
nested repository. Denying those safely would require
making the workspace's Git storage structurally read-only, which the
personal-machine SRT trust model does not; use the Docker/NsJail backend or a
dedicated VM boundary when a workspace command must be treated as adversarial.

SRT restrictions remain inherited by descendants. Windows additionally uses a
kill-on-close Job Object. Native macOS does not provide an equivalent hard
process-lifetime boundary: a deliberately daemonized descendant can outlive
Expand Down
122 changes: 122 additions & 0 deletions packages/code/src/native-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1751,3 +1751,125 @@ test('linked worktree Git guard refuses Windows rather than admitting an unguard
);
await sandbox.close();
});

test('non-lane roots deny their own executable Git metadata at initialization', async t => {
const root = await realpath(await mkdtemp(join(tmpdir(), 'librechat-code-gitmeta-')));
t.after(() => rm(root, { recursive: true, force: true }));
const gitDir = join(root, '.git');
await mkdir(join(gitDir, 'hooks'), { recursive: true });
await writeFile(join(gitDir, 'config'), '[core]\n');
await writeFile(join(gitDir, 'config.worktree'), '[core]\n');
// A submodule Git directory (identified by its HEAD file) and a nested one.
// This submodule has no config.worktree, so it is not denied one.
await mkdir(join(gitDir, 'modules', 'sub', 'modules', 'inner'), { recursive: true });
await writeFile(join(gitDir, 'modules', 'sub', 'HEAD'), 'ref: refs/heads/main\n');
await writeFile(join(gitDir, 'modules', 'sub', 'config'), '[core]\n');
await writeFile(
join(gitDir, 'modules', 'sub', 'modules', 'inner', 'HEAD'),
'ref: refs/heads/main\n',
);
// A linked worktree's per-worktree metadata: an existing commondir and
// config.worktree are both denied.
await mkdir(join(gitDir, 'worktrees', 'wt'), { recursive: true });
await writeFile(join(gitDir, 'worktrees', 'wt', 'commondir'), '../..\n');
await writeFile(join(gitDir, 'worktrees', 'wt', 'config.worktree'), '[core]\n');

const fake = fakeManager();
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
environment: { PATH: '/usr/bin', LANG: 'C.UTF-8' },
manager: fake.manager,
});
t.after(() => sandbox.close());
await sandbox.prepare();

const denyWrite = fake.config?.filesystem.denyWrite ?? [];
for (const relativePath of [
'.git/hooks',
'.git/config',
'.git/config.worktree',
'.git/modules/sub/hooks',
'.git/modules/sub/config',
'.git/modules/sub/modules/inner/hooks',
'.git/modules/sub/modules/inner/config',
'.git/worktrees/wt/config.worktree',
'.git/worktrees/wt/commondir',
]) {
assert.ok(denyWrite.includes(join(root, relativePath)), relativePath);
}
// Deny-if-exists paths are skipped when absent: masking them with an empty
// bind would make Git read a broken redirect or config from every command.
assert.ok(!denyWrite.includes(join(gitDir, 'commondir')));
assert.ok(!denyWrite.includes(join(gitDir, 'modules', 'sub', 'config.worktree')));
});

test('ordinary root commands deny Git metadata created after initialization', async t => {
const root = await realpath(await mkdtemp(join(tmpdir(), 'librechat-code-gitmeta-late-')));
t.after(() => rm(root, { recursive: true, force: true }));
const fake = fakeManager();
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
environment: { PATH: '/usr/bin', LANG: 'C.UTF-8' },
manager: fake.manager,
});
t.after(() => sandbox.close());
await sandbox.prepare();
const session = fake.config!.filesystem;
const gitDir = join(root, '.git');
assert.ok(!session.denyWrite.some(path => path.startsWith(gitDir)));

// The host initializes the repository and adds a submodule and a linked
// worktree while the worker keeps running.
await mkdir(join(gitDir, 'hooks'), { recursive: true });
await writeFile(join(gitDir, 'config'), '[core]\n');
await mkdir(join(gitDir, 'modules', 'late'), { recursive: true });
await writeFile(join(gitDir, 'modules', 'late', 'HEAD'), 'ref: refs/heads/main\n');
await mkdir(join(gitDir, 'worktrees', 'late'), { recursive: true });
await writeFile(join(gitDir, 'worktrees', 'late', 'commondir'), '../..\n');

const result = await sandbox.execute(request);
assert.equal(result.exitCode, 0);
const filesystem = fake.customConfigSeenDuringWrap?.filesystem;
for (const relativePath of [
'.git/hooks',
'.git/config',
'.git/modules/late/hooks',
'.git/modules/late/config',
'.git/worktrees/late/commondir',
]) {
assert.ok(filesystem?.denyWrite.includes(join(root, relativePath)), relativePath);
}
// Only denyWrite differs from the session filesystem policy.
assert.deepEqual({ ...filesystem, denyWrite: session.denyWrite }, session);
});

test('a root command fails closed when its Git metadata cannot be inspected', async t => {
if (process.getuid?.() === 0) {
t.skip('directory permissions do not restrict root');
return;
}
const root = await realpath(await mkdtemp(join(tmpdir(), 'librechat-code-gitmeta-denied-')));
t.after(() => rm(root, { recursive: true, force: true }));
const modules = join(root, '.git', 'modules');
await mkdir(modules, { recursive: true });
await writeFile(join(root, '.git', 'config'), '[core]\n');
const fake = fakeManager();
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
environment: { PATH: '/usr/bin', LANG: 'C.UTF-8' },
manager: fake.manager,
});
t.after(() => sandbox.close());
await sandbox.prepare();

await chmod(modules, 0o000);
try {
await assert.rejects(
sandbox.execute(request),
(error: unknown) =>
error instanceof WorkspaceToolError && error.code === 'COMMAND_UNAVAILABLE',
);
} finally {
await chmod(modules, 0o700);
}
});
Loading
Loading