Skip to content
4 changes: 3 additions & 1 deletion packages/runtime-host/src/server/execution-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1631,7 +1631,9 @@ function adaptManagedWorkspaceFilesystemWorker(
): ManagedWorkspaceFilesystemWorker {
return {
async execute(input) {
const result = await worker.execute(input);
// Read-only operations never participate in CAS; the adapter says so
// explicitly (#3484) instead of relying on an absent optional field.
const result = await worker.execute({ ...input, expectedIdentity: 'unchecked' });
switch (result.kind) {
case 'read':
case 'read_image':
Expand Down
40 changes: 37 additions & 3 deletions packages/runtime/src/__tests__/filesystem-mutation-outcome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type FilesystemWorkerClient,
type FilesystemWorkerClientErrorReason,
type FilesystemWorkerExecuteInput,
type FilesystemWorkerExpectedIdentity,
} from '../filesystem-worker/client.js';
import type { FilesystemWorkerResult } from '../filesystem-worker/protocol.js';
import { createLocalWorkspaceExecutor } from '../workspace-executor.js';
Expand Down Expand Up @@ -216,7 +217,7 @@ describe('filesystem mutation T0 identity capture (queue-window closure)', () =>
releaseFirst = resolve;
});
let calls = 0;
let queuedIdentity: { dev: string; ino: string } | undefined;
let queuedIdentity: FilesystemWorkerExpectedIdentity | undefined;
const gatedWorker: {
execute: (input: FilesystemWorkerExecuteInput) => Promise<FilesystemWorkerResult>;
} = {
Expand Down Expand Up @@ -255,13 +256,46 @@ describe('filesystem mutation T0 identity capture (queue-window closure)', () =>
releaseFirst();
await Promise.all([first, second]);

assert.ok(queuedIdentity, 'the queued mutation should have dispatched to the worker');
assert.ok(
queuedIdentity && typeof queuedIdentity !== 'string',
'the queued mutation should have dispatched with the captured identity',
);
assert.equal(
queuedIdentity.ino,
(queuedIdentity as { dev: string; ino: string }).ino,
String(original.ino),
'identity must be the inode captured at lock acquisition (before the replacement); a T1 capture would sample the replacement',
);
});

test('an apply_patch mutation forwards its captured identity, not unchecked (#3484 regression)', async () => {
const cwd = await realpath(await mkdtemp(join(tmpdir(), 'maka-t0-applypatch-')));
cleanup.push(cwd);
const target = join(cwd, 'file.txt');
await writeFile(target, 'original', 'utf8');

const original = await stat(target, { bigint: true });
let dispatched: FilesystemWorkerExpectedIdentity | undefined;
const gatedWorker: {
execute: (input: FilesystemWorkerExecuteInput) => Promise<FilesystemWorkerResult>;
} = {
async execute(input) {
dispatched = input.expectedIdentity;
return { kind: 'apply_patch', ok: true, path: target };
},
};
const fs = executorWith(gatedWorker);

await fs.applyPatch({
operation: { type: 'update_file', path: target, diff: '--- a\n+++ b\n' },
cwd,
});

assert.ok(
dispatched && typeof dispatched !== 'string',
'apply_patch must dispatch with the captured identity, not unchecked',
);
assert.equal(dispatched.ino, String(original.ino));
});
});

function sleep(ms: number): Promise<void> {
Expand Down
77 changes: 70 additions & 7 deletions packages/runtime/src/__tests__/filesystem-target-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ async function temporaryDirectory(prefix: string): Promise<string> {

function requestFor(
operation: FilesystemWorkerRequest['operation'],
expectedTarget: FilesystemWorkerTarget,
expectedTarget: Omit<FilesystemWorkerTarget, 'identity'>,
identity: FilesystemWorkerTarget['identity'] = 'missing',
): FilesystemWorkerRequest {
return {
version: FILESYSTEM_WORKER_PROTOCOL_VERSION,
Expand All @@ -40,7 +41,7 @@ function requestFor(
entries: [{ path: expectedTarget.enforcementPath, access: 'write', scope: 'exact' }],
},
},
expectedTarget,
expectedTarget: { ...expectedTarget, identity },
};
}

Expand All @@ -67,7 +68,8 @@ describe('filesystem worker target identity CAS', () => {
const response = await executeFilesystemWorkerRequest(
requestFor(
{ kind: 'write', cwd, path: target, content: 'new' },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file', identity },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file' },
identity,
),
);

Expand All @@ -90,7 +92,8 @@ describe('filesystem worker target identity CAS', () => {
const response = await executeFilesystemWorkerRequest(
requestFor(
{ kind: 'write', cwd, path: target, content: 'updated' },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file', identity },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file' },
identity,
),
);

Expand All @@ -115,7 +118,8 @@ describe('filesystem worker target identity CAS', () => {
const response = await executeFilesystemWorkerRequest(
requestFor(
{ kind: 'apply_patch', cwd, path: target, action: 'delete' },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file', identity },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file' },
identity,
),
);

Expand All @@ -139,7 +143,8 @@ describe('filesystem worker target identity CAS', () => {
const response = await executeFilesystemWorkerRequest(
requestFor(
{ kind: 'edit', cwd, path: target, oldString: 'old', newString: 'new' },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file', identity },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file' },
identity,
),
);

Expand All @@ -150,6 +155,37 @@ describe('filesystem worker target identity CAS', () => {
assert.equal(await readFile(target, 'utf8'), 'replacement\nold\n');
});

test('rejects an apply_patch update when the target inode changed after authorisation', async () => {
const cwd = await temporaryDirectory('maka-identity-applypatch-update-');
const target = join(cwd, 'file.txt');
const replacement = join(cwd, 'replacement.txt');
await writeFile(target, 'line\noriginal\n', 'utf8');
await writeFile(replacement, 'line\nreplacement\n', 'utf8');

const identity = await captureIdentity(target);
await rename(replacement, target);

const response = await executeFilesystemWorkerRequest(
requestFor(
{
kind: 'apply_patch',
cwd,
path: target,
action: 'update',
diff: '--- a\n+++ b\n@@ -1,2 +1,2 @@\n line\n-original\n+updated\n',
},
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file' },
identity,
),
);

assert.equal(response.ok, false);
assert.equal(response.error?.code, 'path_changed');
// The replacement content must be untouched (the patch never applied).
const { readFile } = await import('node:fs/promises');
assert.equal(await readFile(target, 'utf8'), 'line\nreplacement\n');
});

test('creates a missing target without requiring an identity', async () => {
const cwd = await temporaryDirectory('maka-identity-missing-');
const target = join(cwd, 'brand-new.txt');
Expand All @@ -170,6 +206,32 @@ describe('filesystem worker target identity CAS', () => {
assert.equal(await readFile(target, 'utf8'), 'created');
});

test('a write to a missing target reports a creation diff (#3487 P1)', async () => {
const cwd = await temporaryDirectory('maka-identity-missing-write-');
const target = join(cwd, 'brand-new.txt');

// The truthy 'missing' identity string used to make the "approved
// missing" truthiness test fail, collapsing the diff into unknown and
// hiding new-file changes from review. The write must report the creation
// diff (`--- /dev/null`) instead.
const response = await executeFilesystemWorkerRequest(
requestFor(
{ kind: 'write', cwd, path: target, content: 'created\n' },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'missing' },
),
);

assert.equal(response.ok, true);
assert.equal(response.result.kind, 'write');
if (response.result.kind !== 'write') return;
assert.ok(
response.result.diff?.includes('--- /dev/null'),
'a created file must carry a creation diff with --- /dev/null',
);
const { readFile } = await import('node:fs/promises');
assert.equal(await readFile(target, 'utf8'), 'created\n');
});

test('reports path_changed when the target is replaced before the write (pre-write CAS)', async () => {
const cwd = await temporaryDirectory('maka-identity-prewrite-');
const target = join(cwd, 'file.txt');
Expand All @@ -186,7 +248,8 @@ describe('filesystem worker target identity CAS', () => {
const response = await executeFilesystemWorkerRequest(
requestFor(
{ kind: 'write', cwd, path: target, content: 'new' },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file', identity },
{ enforcementPath: target, access: 'write', scope: 'exact', targetType: 'file' },
identity,
),
);

Expand Down
Loading