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
46 changes: 41 additions & 5 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1102,11 +1102,47 @@ This does not attach another checkout's `node_modules`, provision linked lanes o
command admission, or recheck existing instances on every command. It does not
deduplicate installed dependencies between worktrees or enforce disk quotas.

The next resource-store slice must explicitly grant shared cache paths under SRT,
keep monorepo links and mutable outputs checkout-local, and bound retention. Do not
work around that missing grant by broadening the sandbox root or symlinking another
branch's full installation. Shared download caches alone do not reduce installed
`node_modules` copies.
Shared tool cache grants below do not attach another checkout's installed dependency
tree. Keep monorepo links and mutable outputs checkout-local. Do not broaden the
sandbox root or symlink another branch's full installation. Shared download caches
alone do not reduce installed `node_modules` copies.

## Shared tool and download resources

Explicit operator-managed stores can live outside the checkouts:

```yaml
resources:
- kind: npm-cache
path: /srv/lia-resources/npm
access: read-write
- kind: uv-cache
path: /srv/lia-resources/uv
access: read-write
- kind: playwright-browsers
path: /srv/lia-resources/playwright
access: read-only
```

Create each directory as the worker service account with mode `0700`, then populate
browser binaries using the matching Playwright version and `PLAYWRIGHT_BROWSERS_PATH`
outside the coding session. Roots must exist and may not be symlinks or overlap any
registered workspace or worker control state. Linux mount-alias checks cover both
directions. Keep the mount namespace stable while the worker runs.

The worker grants only these paths and injects `npm_config_cache`, `UV_CACHE_DIR`
and `PLAYWRIGHT_BROWSERS_PATH` from the loaded definition, including in linked lanes
and fresh conversation worktrees. An undeclared process environment variable never
grants filesystem access. Each store root is inode-bound and revalidated before
commands. Read-only stores stay read-only, and speculative programmatic probes
cannot write any shared store.

Sharing is explicit within one worker's trust domain. Do not share mutable caches
between unrelated principals, store credentials in them, or treat their contents
as trusted worker code. Separate package caches from browsers and mutable browser
profiles, Redis data, build output and test state. Stores currently have no automatic
eviction; the storage-lifecycle slice adds that separately. This shares downloads
and browser binaries, not installed `node_modules` trees.
No setup output is sent to the model.

Named actions are fixed commands without model-supplied substitution. The bridge
Expand Down
31 changes: 24 additions & 7 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
EnvironmentWorkspaceTools,
} from './environment.js';
import { prepareCodeEnvironment } from './environment-preparation.js';
import { assertEnvironmentResourceIsolation } from './environment-resources.js';
import { startFileRelay } from './relay.js';
import { DockerFileRelaySupervisor } from './relay-runtime.js';
import {
Expand Down Expand Up @@ -837,6 +838,16 @@ async function run(
definition: { name: 'preparation-state', root: preparationDirectory }, fingerprint: '',
}], roots);
}
// Cache contents may be shared explicitly; their grants must never authorize source or control state.
const resourceRoots = environments.flatMap(environment => (environment.resources ?? []).map(resource => ({
path: resource.path, sourceParents: resource.controlPaths,
definition: { name: 'shared-resource', root: resource.path }, fingerprint: '',
})));
await assertEnvironmentResourceIsolation(environments.flatMap(environment => environment.resources ?? []), roots.map(root => root.root), [
identityPath, preparationDirectory, github.privateKeyPath,
...environments.map(environment => environment.path), ...rootQuarantinePaths.values(),
].filter((path): path is string => path != null));
await assertEnvironmentDefinitionsOutsideRoots(resourceRoots, roots);
const preparationReceipt = (root: string) => join(preparationDirectory,
`${createHash('sha256').update(JSON.stringify([codeApiUrl, workerId, root])).digest('hex')}.json`);
// Keep an admission boundary even when trusted-VM checkout routing uses a
Expand Down Expand Up @@ -1110,19 +1121,23 @@ async function run(
}
: {}),
};
const nativeOptionsForWorkspace = (workspaceId: string): NativeProcessSandboxOptions => ({
...nativeOptions,
resources: environments.find(environment => environment.definition.name === workspaceId)?.resources,
});
const nativeCommandSandbox =
allowWorkspaceCommands && commandSandboxMode === 'native-srt'
? roots.length > 1 || workspaceLeaseSlots > 1 || conversationWorktreeRoot || linkedWorktreeLanes
? new NativeWorkspaceCommandPool(
new Map(
roots.map(root => [
root.id,
{ ...nativeOptions, workspaceRoot: root.root, workspaceIdentity: root.identity },
{ ...nativeOptionsForWorkspace(root.id), workspaceRoot: root.root, workspaceIdentity: root.identity },
]),
),
workspaceLeaseSlots,
)
: new NativeProcessWorkspaceCommandSandbox(nativeOptions)
: new NativeProcessWorkspaceCommandSandbox(nativeOptionsForWorkspace(roots[0].id))
: undefined;
if (allowWorkspaceCommands && workspaceTools) {
workspaceTools = new SandboxWorkspaceTools({
Expand Down Expand Up @@ -1161,14 +1176,15 @@ async function run(
}
const id = internalWorkspaceId(instance.sourceWorkspaceId, instance.id);
await nativeCommandSandbox.registerRoot(id, {
...nativeOptions,
...nativeOptionsForWorkspace(instance.sourceWorkspaceId),
workspaceIdentity: instance.identity,
workspaceRoot: instance.root,
});
await prepareCodeEnvironment({
root: instance.root, identity: instance.identity, setup,
receiptPath: preparationReceipt(instance.root),
context: JSON.stringify([serializeNativeSrtCommandPolicy(commandPolicy), commandAllowedDomains, github.policyIdentity]),
context: JSON.stringify([serializeNativeSrtCommandPolicy(commandPolicy), commandAllowedDomains, github.policyIdentity,
nativeOptionsForWorkspace(instance.sourceWorkspaceId).resources]),
signal,
execute: (command, timeoutMs) => nativeCommandSandbox.execute({
protocolVersion: 1,
Expand Down Expand Up @@ -1225,7 +1241,7 @@ async function run(
root.id,
{
command: {
...nativeOptions,
...nativeOptionsForWorkspace(root.id),
workspaceIdentity: root.identity,
workspaceRoot: root.root,
},
Expand Down Expand Up @@ -1263,7 +1279,7 @@ async function run(
{
root: root.root,
identity: root.identity,
command: nativeOptions,
command: nativeOptionsForWorkspace(root.id),
repositoryInstructions: args.includes('--repository-instructions'),
writable: root.writable ?? false,
},
Expand Down Expand Up @@ -1340,7 +1356,8 @@ async function run(
root: environment.definition.root,
identity: roots.find(root => root.id === id)!.identity!,
setup, receiptPath: preparationReceipt(environment.definition.root),
context: JSON.stringify([serializeNativeSrtCommandPolicy(commandPolicy), commandAllowedDomains, github.policyIdentity]),
context: JSON.stringify([serializeNativeSrtCommandPolicy(commandPolicy), commandAllowedDomains, github.policyIdentity,
nativeOptionsForWorkspace(id).resources]),
signal: controller.signal,
execute: async (command, timeoutMs) => {
if (!armed) {
Expand Down
24 changes: 11 additions & 13 deletions packages/code/src/environment-preparation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function fixture(t: test.TestContext) {
t.after(() => rm(directory, { recursive: true, force: true }));
const root = join(directory, 'checkout');
const state = join(directory, 'private');
await mkdir(root);
await mkdir(root, { mode: 0o700 });
await prepareEnvironmentPreparationDirectory(state);
await writeFile(join(root, 'package-lock.json'), 'lock-v1');
const commands: string[] = [];
Expand Down Expand Up @@ -316,9 +316,9 @@ test(
});
t.after(() => sandbox.close());
await sandbox.prepare();
const execute: EnvironmentPreparationOptions['execute'] = (
command,
timeoutMs,
const execute = (
command: string,
timeoutMs: number,
) =>
sandbox.execute({
protocolVersion: 1,
Expand All @@ -344,15 +344,13 @@ test(
};
assert.equal(await prepareCodeEnvironment(configured), 'prepared');
assert.equal(await prepareCodeEnvironment(configured), 'reused');
assert.notEqual(
(await execute(`cat '${options.receiptPath}'`, 5000)).exitCode,
0,
);
assert.notEqual(
(await execute(`printf forged > '${options.receiptPath}'`, 5000))
.exitCode,
0,
);
const receipt = await readFile(options.receiptPath, 'utf8');
const protectedRead = await execute(`cat '${options.receiptPath}'`, 5000);
// Linux SRT may mask denied reads with an empty file. Exit status alone
// is not evidence that the protected receipt was exposed.
assert.ok(!protectedRead.stdout?.includes(receipt));
await execute(`printf forged > '${options.receiptPath}'`, 5000);
assert.equal(await readFile(options.receiptPath, 'utf8'), receipt);
assert.equal(
await readFile(join(options.root, 'installs.log'), 'utf8'),
'install\n',
Expand Down
Loading
Loading