Repository navigation
A materialize that names no mount passes over a writable mount whose config leaves out mode, and answers that it materialized nothing #718
Description
A workspace mount whose config leaves out mode is writable: write stores to it, a commit push treats it as writable (pushesOnCommit: mode !== 'read-only'), and the direct-mount refusal says of a mode left out "it's 'read-write'". A materialize call that names no mount, though, takes only mounts whose mode is exactly 'read-write' (at 8e0bb04ea1, src/modules/workspace/index.ts line 5113). So it passes over such a mount, writes none of its files, and answers { materialized: [], count: 0 } with success. Nothing in the answer says a mount was passed over, so an agent that wrote to that mount takes the call to have put its writes on disk.
A run
At 8e0bb04ea1, on Linux with Node 24.22.0: a store-backed mount, a write of work/a.txt, then materialize with no mount, then status, then materialize naming the mount. The probe is below.
The mount's mode |
materialize with no mount |
On disk after it | status before the named call |
On disk after materialize naming the mount |
|---|---|---|---|---|
'read-write' |
{ materialized: [{ mount: 'work', path: 'a.txt' }], count: 1 } |
yes | pendingChanges: 0 |
yes |
| left out | { materialized: [], count: 0 } |
no | pendingChanges: 1, no mode |
yes |
Who meets it
A host whose config gives a writable mount without mode, which MountConfig's type requires but a config read from JSON can leave out, and whose agent calls materialize without naming a mount, as its description allows (mount: "Specific mount (optional)"). The files stay in the store, and the answer gives no hint of it.
Not run
Hosts other than this probe's; whether any deployed config leaves mode out.
Our internal review of #719, which makes direct mounts writable, found this.
The probe
// A bare materialize and a mount whose config names no mode, on main.
import test, { type TestContext } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { JsStore } from '@animalabs/chronicle';
import { WorkspaceModule } from '../src/modules/workspace/index.js';
import type { MountConfig } from '../src/modules/workspace/types.js';
import type { ModuleContext } from '../src/types/module.js';
const ctx = { isRestart: false, getState: () => null, setState: () => {}, pushEvent: () => {} } as unknown as ModuleContext;
async function probe(t: TestContext, mode: MountConfig['mode'] | undefined) {
const root = mkdtempSync(join(tmpdir(), 'af-probe-'));
const dir = join(root, 'work');
mkdirSync(dir);
const store = JsStore.openOrCreate({ path: join(root, 'ws.chronicle') });
const config = { name: 'work', path: dir, watch: 'never', ...(mode ? { mode } : {}) } as MountConfig;
const m = new WorkspaceModule({ mounts: [config] });
m.initStore(store);
await m.start(ctx);
t.after(async () => { await m.stop(); store.close(); rmSync(root, { recursive: true, force: true }); });
const write = await m.handleToolCall({ id: 'w', name: 'write', input: { path: 'work/a.txt', content: 'one\n' } });
const bare = await m.handleToolCall({ id: 'm1', name: 'materialize', input: {} });
const afterBare = existsSync(join(dir, 'a.txt'));
const status = (await m.handleToolCall({ id: 's', name: 'status', input: {} })).data as Record<string, { mode?: string; pendingChanges: number }>;
const named = await m.handleToolCall({ id: 'm2', name: 'materialize', input: { mount: 'work' } });
const afterNamed = existsSync(join(dir, 'a.txt'));
console.log(JSON.stringify({
mode: mode ?? '(left out)',
write: write.success,
bare: { success: bare.success, data: bare.data },
onDiskAfterBare: afterBare,
statusBeforeNamed: { mode: status.work?.mode, pendingChanges: status.work?.pendingChanges },
named: { success: named.success, error: named.error },
onDiskAfterNamed: afterNamed,
}));
return { afterBare, afterNamed };
}
test("mode 'read-write': a bare materialize writes the file", async (t) => {
const r = await probe(t, 'read-write');
assert.equal(r.afterBare, true);
});
test('mode left out: a bare materialize passes over the mount', async (t) => {
const r = await probe(t, undefined);
assert.equal(r.afterBare, false);
assert.equal(r.afterNamed, true);
});Its output:
{"mode":"read-write","write":true,"bare":{"success":true,"data":{"materialized":[{"mount":"work","path":"a.txt"}],"count":1}},"onDiskAfterBare":true,"statusBeforeNamed":{"mode":"read-write","pendingChanges":0},"named":{"success":true},"onDiskAfterNamed":true}
✔ mode 'read-write': a bare materialize writes the file (135.075116ms)
{"mode":"(left out)","write":true,"bare":{"success":true,"data":{"materialized":[],"count":0}},"onDiskAfterBare":false,"statusBeforeNamed":{"pendingChanges":1},"named":{"success":true},"onDiskAfterNamed":true}
✔ mode left out: a bare materialize passes over the mount (89.071373ms)
Written by Beth-2709 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.
Activity