diff --git a/docs/architecture/runtime-managed-workspace-baseline-open-v1.zh-CN.md b/docs/architecture/runtime-managed-workspace-baseline-open-v1.zh-CN.md index c8a2cb7b343..1fc5d1f04e2 100644 --- a/docs/architecture/runtime-managed-workspace-baseline-open-v1.zh-CN.md +++ b/docs/architecture/runtime-managed-workspace-baseline-open-v1.zh-CN.md @@ -110,6 +110,7 @@ v1 receipt 严格包含: - durable binding 与调用 identity 完全相同; - repository record、epoch artifact、baseline ref、head ref、commit/tree 与 pinned Git capability 一致; +- reopening 已存在 binding 时重新观测 source repository identity、HEAD commit 与 tree;任一项相对已接受边界发生漂移都 fail closed,不得静默复用旧 baseline; - worktree registration 仍被 Maka ownership lock 锁定; - HEAD/tree 等于 baseline,worktree clean; - policy version/hash 等于实现实际执行的 canonical M0 policy; diff --git a/packages/runtime-host/src/__tests__/workspace-execution-composition.test.ts b/packages/runtime-host/src/__tests__/workspace-execution-composition.test.ts index a9e958f016c..28e0dfb2ab5 100644 --- a/packages/runtime-host/src/__tests__/workspace-execution-composition.test.ts +++ b/packages/runtime-host/src/__tests__/workspace-execution-composition.test.ts @@ -156,6 +156,9 @@ function fakeManagedOwner(input: { async openManagedWorkspaceBaseline() { throw new Error('not used'); }, + async openManagedWorkspaceBaselineFromExecutionStores() { + throw new Error('not used'); + }, async withManagedWorkspaceExecution(handle, operation) { assert.equal(handle, input.handle); input.calls.push('managed:admit'); diff --git a/packages/storage/src/__tests__/artifact-writer-lock.test.ts b/packages/storage/src/__tests__/artifact-writer-lock.test.ts index 3fd8da24496..7ce763b589e 100644 --- a/packages/storage/src/__tests__/artifact-writer-lock.test.ts +++ b/packages/storage/src/__tests__/artifact-writer-lock.test.ts @@ -115,6 +115,33 @@ test('public Store mutation waits for a child-held writer lock and preserves met }); }); +test('an aborted artifact writer admission stops waiting for a child-held lock', { + timeout: TEST_TIMEOUT_MS, +}, async () => { + await withTemporaryDirectory(async (root) => { + const stateRoot = join(root, 'state'); + const holder = await spawnLockHolder(stateRoot); + const abort = new AbortController(); + let entered = false; + try { + const admission = withArtifactWriterLock( + stateRoot, + async () => { + entered = true; + }, + abort.signal, + ); + await assertPending(admission, 'abortable artifact writer admission'); + abort.abort(new DOMException('Baseline admission cancelled', 'AbortError')); + + await assert.rejects(admission, { name: 'AbortError' }); + assert.equal(entered, false); + } finally { + await stopHolder(holder); + } + }); +}); + test('public Store mutations in separate processes reload and publish under one OS lock', { timeout: TEST_TIMEOUT_MS, }, async () => { diff --git a/packages/storage/src/__tests__/git-workspace-service.test.ts b/packages/storage/src/__tests__/git-workspace-service.test.ts index 831b88d97f0..b5fb8b71391 100644 --- a/packages/storage/src/__tests__/git-workspace-service.test.ts +++ b/packages/storage/src/__tests__/git-workspace-service.test.ts @@ -176,7 +176,7 @@ describe('Git workspace service', () => { assert.equal((await restarted.inspectManagedWorkspace(adopted)).state, 'ready'); }); - test('reopens an existing managed epoch after the source checkout advances', async () => { + test('rejects reopening an existing managed epoch after the source checkout advances', async () => { const root = await temporaryRoot(); const sourceRoot = await createEligibleSource(join(root, 'source')); const storageRoot = join(root, 'storage'); @@ -199,11 +199,10 @@ describe('Git workspace service', () => { ); const restarted = await serviceAt(storageRoot); - const reopened = await restarted.openManagedWorkspaceFromBinding(created); - - assert.deepEqual(reopened, created); - assert.equal((await restarted.inspectManagedWorkspace(reopened)).state, 'ready'); - assert.equal(await readFile(join(reopened.worktreePath, 'tracked.txt'), 'utf8'), 'tracked\n'); + await assert.rejects( + restarted.openManagedWorkspaceFromBinding(created), + isWorkspaceError('managed_workspace_source_drifted'), + ); }); test('creates a second epoch with a different baseline in the same managed repository', async () => { diff --git a/packages/storage/src/__tests__/managed-dependency-environment.test.ts b/packages/storage/src/__tests__/managed-dependency-environment.test.ts index d154404ecce..8fde21b886e 100644 --- a/packages/storage/src/__tests__/managed-dependency-environment.test.ts +++ b/packages/storage/src/__tests__/managed-dependency-environment.test.ts @@ -142,6 +142,37 @@ test('rejects a second authority for the same storage root in one process', asyn await first.close(); }); +test('rejects a pre-aborted acquisition before invoking the producer', async (t) => { + const storageRoot = await mkdtemp(join(tmpdir(), 'maka-dependency-pre-abort-')); + t.after(() => rm(storageRoot, { recursive: true, force: true })); + let provisionCalls = 0; + const authority = await createManagedDependencyEnvironmentAuthority({ + storageRoot, + producer: { + capability: FIXTURE_PRODUCER_CAPABILITY, + packageManagerName: 'npm', + packageManagerVersion: '11.12.1', + nodeRuntime: fixtureNodeRuntime(), + async provision() { + provisionCalls += 1; + }, + }, + }); + const source = dependencySourceForName('pre-abort'); + const abort = new AbortController(); + abort.abort(new DOMException('Cancelled before acquisition', 'AbortError')); + + await assert.rejects( + authority.acquire(computeManagedDependencyEnvironmentIdentity(source), { + ...source, + abortSignal: abort.signal, + }), + { name: 'AbortError' }, + ); + assert.equal(provisionCalls, 0); + await authority.close(); +}); + test('rejects a published environment whose dependency content was modified', async (t) => { const storageRoot = await mkdtemp(join(tmpdir(), 'maka-dependency-tamper-')); t.after(() => rm(storageRoot, { recursive: true, force: true })); @@ -595,6 +626,128 @@ test('publishes one Maka-owned artifact for concurrent equivalent acquisitions', await authority.close(); }); +test('does not cancel shared provisioning while another acquisition still needs it', async (t) => { + const storageRoot = await mkdtemp(join(tmpdir(), 'maka-dependency-shared-abort-')); + t.after(() => rm(storageRoot, { recursive: true, force: true })); + let acknowledgeProvision!: () => void; + const provisionStarted = new Promise((resolve) => { + acknowledgeProvision = resolve; + }); + let finishProvision!: () => void; + const provisionAllowed = new Promise((resolve) => { + finishProvision = resolve; + }); + let producerSignal: AbortSignal | undefined; + const authority = await createManagedDependencyEnvironmentAuthority({ + storageRoot, + producer: { + capability: FIXTURE_PRODUCER_CAPABILITY, + packageManagerName: 'npm', + packageManagerVersion: '11.12.1', + nodeRuntime: fixtureNodeRuntime(), + async provision(input) { + producerSignal = input.abortSignal; + acknowledgeProvision(); + await Promise.race([ + provisionAllowed, + new Promise((_resolve, reject) => { + const abort = () => + reject( + input.abortSignal?.reason ?? + new DOMException('Shared provision cancelled', 'AbortError'), + ); + if (input.abortSignal?.aborted) abort(); + else input.abortSignal?.addEventListener('abort', abort, { once: true }); + }), + ]); + input.abortSignal?.throwIfAborted(); + await writeFile(join(input.outputRoot, 'payload'), 'shared\n', 'utf8'); + }, + }, + }); + const source = dependencySourceForName('shared-abort'); + const identity = computeManagedDependencyEnvironmentIdentity(source); + const firstAbort = new AbortController(); + const first = authority.acquire(identity, { ...source, abortSignal: firstAbort.signal }); + await provisionStarted; + const second = authority.acquire(identity, source); + + firstAbort.abort(new DOMException('First waiter cancelled', 'AbortError')); + + await assert.rejects(first, { name: 'AbortError' }); + assert.equal(producerSignal?.aborted, false); + finishProvision(); + const lease = await second; + assert.equal(await readFile(join(lease.dependencyRoot, 'payload'), 'utf8'), 'shared\n'); + await lease.release(); + await authority.close(); +}); + +test('starts a fresh publication when a new caller arrives during aborted publication cleanup', async (t) => { + const storageRoot = await mkdtemp(join(tmpdir(), 'maka-dependency-abort-handoff-')); + t.after(() => rm(storageRoot, { recursive: true, force: true })); + let provisionCalls = 0; + let acknowledgeFirstStart!: () => void; + const firstStarted = new Promise((resolve) => { + acknowledgeFirstStart = resolve; + }); + let acknowledgeFirstAbort!: () => void; + const firstAborted = new Promise((resolve) => { + acknowledgeFirstAbort = resolve; + }); + let finishFirstCleanup!: () => void; + const firstCleanupAllowed = new Promise((resolve) => { + finishFirstCleanup = resolve; + }); + const authority = await createManagedDependencyEnvironmentAuthority({ + storageRoot, + producer: { + capability: FIXTURE_PRODUCER_CAPABILITY, + packageManagerName: 'npm', + packageManagerVersion: '11.12.1', + nodeRuntime: fixtureNodeRuntime(), + async provision(input) { + provisionCalls += 1; + if (provisionCalls === 1) { + acknowledgeFirstStart(); + await new Promise((resolve) => { + const onAbort = () => { + acknowledgeFirstAbort(); + resolve(); + }; + if (input.abortSignal?.aborted) onAbort(); + else input.abortSignal?.addEventListener('abort', onAbort, { once: true }); + }); + await firstCleanupAllowed; + input.abortSignal?.throwIfAborted(); + } + await writeFile(join(input.outputRoot, 'payload'), 'fresh\n', 'utf8'); + }, + }, + }); + t.after(() => authority.close().catch(() => undefined)); + const source = dependencySourceForName('abort-handoff'); + const identity = computeManagedDependencyEnvironmentIdentity(source); + const firstAbort = new AbortController(); + const first = authority.acquire(identity, { ...source, abortSignal: firstAbort.signal }); + await firstStarted; + + firstAbort.abort(new DOMException('First caller cancelled', 'AbortError')); + await assert.rejects(first, { name: 'AbortError' }); + await firstAborted; + + const second = authority.acquire(identity, source); + await Promise.resolve(); + assert.equal(provisionCalls, 1, 'fresh publication started before aborted cleanup settled'); + finishFirstCleanup(); + + const lease = await second; + assert.equal(provisionCalls, 2); + assert.equal(await readFile(join(lease.dependencyRoot, 'payload'), 'utf8'), 'fresh\n'); + await lease.release(); + await authority.close(); +}); + test('close drains an acquisition through lease installation before deciding its outcome', async (t) => { const storageRoot = await mkdtemp(join(tmpdir(), 'maka-dependency-close-drain-')); t.after(() => rm(storageRoot, { recursive: true, force: true })); diff --git a/packages/storage/src/__tests__/managed-workspace-baseline.test.ts b/packages/storage/src/__tests__/managed-workspace-baseline.test.ts index 8adbb57c390..f043ebc71c3 100644 --- a/packages/storage/src/__tests__/managed-workspace-baseline.test.ts +++ b/packages/storage/src/__tests__/managed-workspace-baseline.test.ts @@ -1041,6 +1041,50 @@ test('reissues execution authority after a real process crash during admission v } }); +test('rejects reopening an accepted baseline after the source HEAD advances', async () => { + const root = await temporaryRoot(); + const storageRoot = join(root, 'storage'); + const sourceRoot = await createEligibleSource(join(root, 'source')); + const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); + const rootOwner = await tryAcquireInteractiveRootOwner(capability); + assert.ok(rootOwner); + const runtimeStore = createSqliteRuntimeStore(join(storageRoot, 'runtime.sqlite')); + try { + const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutablePath, + expectedSha256: gitExecutableSha256, + }, + }); + const request = openRequest(sourceRoot); + await owner.openManagedWorkspaceBaseline(runtimeStore, request); + + await writeFile(join(sourceRoot, 'tracked.txt'), 'advanced\n', 'utf8'); + await git(sourceRoot, 'add', 'tracked.txt'); + await git( + sourceRoot, + '-c', + 'user.name=Maka Test', + '-c', + 'user.email=test@maka.invalid', + 'commit', + '--quiet', + '-m', + 'advance source', + ); + + await assert.rejects( + owner.openManagedWorkspaceBaseline(runtimeStore, request), + /source no longer matches its accepted Git boundary/u, + ); + await owner.close(); + } finally { + runtimeStore.close(); + await rootOwner.close(); + } +}); + test('rejects a source tree containing a non-UTF-8 Git path', { skip: process.platform === 'win32', }, async () => { diff --git a/packages/storage/src/__tests__/managed-workspace-owner.test.ts b/packages/storage/src/__tests__/managed-workspace-owner.test.ts index 20a2b242439..bcd50ab0511 100644 --- a/packages/storage/src/__tests__/managed-workspace-owner.test.ts +++ b/packages/storage/src/__tests__/managed-workspace-owner.test.ts @@ -21,7 +21,7 @@ import assert from 'node:assert/strict'; import { execFile } from 'node:child_process'; import { createHash } from 'node:crypto'; import { createReadStream, existsSync } from 'node:fs'; -import { mkdir, mkdtemp, realpath, rename, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { setTimeout as delay } from 'node:timers/promises'; @@ -39,6 +39,8 @@ import { } from '../managed-workspace-execution-authority-internal.js'; import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '../root-authority.js'; import { createSqliteRuntimeStore } from '../sqlite-runtime-store.js'; +import { createManagedDependencyEnvironmentProducerCapability } from '../managed-dependency-environment.js'; +import { withArtifactWriterLock } from '../artifact-writer-lock.js'; const execFileAsync = promisify(execFile); const cleanup: string[] = []; @@ -227,6 +229,329 @@ test('publishes only a revocable execution scope through its accepted handle', a } }); +test('fails closed before scope issue when dependency provisioning is unavailable', async () => { + const root = await temporaryRoot(); + const storageRoot = join(root, 'storage'); + const sourceRoot = await createEligibleSource(join(root, 'source')); + const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); + const rootOwner = await tryAcquireInteractiveRootOwner(capability); + assert.ok(rootOwner); + const runtimeStore = createSqliteRuntimeStore(join(storageRoot, 'runtime.sqlite')); + try { + const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutablePath, + expectedSha256: gitExecutableSha256, + }, + }); + const accepted = await owner.openManagedWorkspaceBaseline( + runtimeStore, + openRequest(sourceRoot), + ); + let callbackEntered = false; + + await assert.rejects( + owner.withManagedWorkspaceExecution( + accepted.executionHandle, + async () => { + callbackEntered = true; + }, + { provisioning: 'unknown' } as never, + ), + isOwnerError('managed_workspace_execution_options_invalid'), + ); + + await assert.rejects( + owner.withManagedWorkspaceExecution( + accepted.executionHandle, + async () => { + callbackEntered = true; + }, + { provisioning: 'dependency_environment_v1' }, + ), + isOwnerError('managed_dependency_producer_unavailable'), + ); + assert.equal(callbackEntered, false); + await owner.close(); + } finally { + runtimeStore.close(); + await rootOwner.close(); + } +}); + +test('binds one Maka-owned dependency environment to the managed execution scope', async () => { + const root = await temporaryRoot(); + const storageRoot = join(root, 'storage'); + const sourceRoot = await createEligibleDependencySource(join(root, 'source')); + const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); + const rootOwner = await tryAcquireInteractiveRootOwner(capability); + assert.ok(rootOwner); + const runtimeStore = createSqliteRuntimeStore(join(storageRoot, 'runtime.sqlite')); + let provisionCalls = 0; + try { + const runtimeIdentity = `sha256:${'9'.repeat(64)}` as const; + const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutablePath, + expectedSha256: gitExecutableSha256, + }, + dependencyEnvironmentProducer: { + capability: createManagedDependencyEnvironmentProducerCapability(runtimeIdentity), + packageManagerName: 'npm', + packageManagerVersion: '12.0.2', + nodeRuntime: { + version: process.versions.node, + abi: process.versions.modules ?? 'unknown', + platform: process.platform, + arch: process.arch, + }, + async provision(input) { + provisionCalls += 1; + await mkdir(join(input.outputRoot, 'fixture-package'), { recursive: true }); + await writeFile( + join(input.outputRoot, 'fixture-package', 'index.js'), + 'export const source = "maka-owned";\n', + ); + }, + }, + filesystemWorker: { + async execute(input) { + assert.equal(input.operation.kind, 'read'); + assert.equal(input.operation.path.startsWith(sourceRoot), false); + return { kind: 'read', content: await readFile(input.operation.path, 'utf8') }; + }, + }, + }); + const accepted = await owner.openManagedWorkspaceBaseline( + runtimeStore, + openRequest(sourceRoot), + ); + + for (let index = 0; index < 2; index += 1) { + const result = await owner.withManagedWorkspaceExecution( + accepted.executionHandle, + (scope) => + owner.executeReadOnlyFilesystemOperation(scope, { + kind: 'read', + path: 'node_modules/fixture-package/index.js', + }), + { provisioning: 'dependency_environment_v1' }, + ); + assert.deepEqual(result, { + kind: 'read', + content: 'export const source = "maka-owned";\n', + }); + } + assert.equal(provisionCalls, 1); + await owner.close(); + } finally { + runtimeStore.close(); + await rootOwner.close(); + } +}); + +test('cancels dependency provisioning before issuing an execution scope', async () => { + const root = await temporaryRoot(); + const storageRoot = join(root, 'storage'); + const sourceRoot = await createEligibleDependencySource(join(root, 'source')); + const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); + const rootOwner = await tryAcquireInteractiveRootOwner(capability); + assert.ok(rootOwner); + const runtimeStore = createSqliteRuntimeStore(join(storageRoot, 'runtime.sqlite')); + let acknowledgeProvision!: () => void; + const provisionStarted = new Promise((resolve) => { + acknowledgeProvision = resolve; + }); + try { + const runtimeIdentity = `sha256:${'8'.repeat(64)}` as const; + const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutablePath, + expectedSha256: gitExecutableSha256, + }, + dependencyEnvironmentProducer: { + capability: createManagedDependencyEnvironmentProducerCapability(runtimeIdentity), + packageManagerName: 'npm', + packageManagerVersion: '12.0.2', + nodeRuntime: { + version: process.versions.node, + abi: process.versions.modules ?? 'unknown', + platform: process.platform, + arch: process.arch, + }, + async provision(input) { + acknowledgeProvision(); + await new Promise((_resolve, reject) => { + const abort = () => + reject( + input.abortSignal?.reason ?? + new DOMException('Dependency provision cancelled', 'AbortError'), + ); + if (input.abortSignal?.aborted) abort(); + else input.abortSignal?.addEventListener('abort', abort, { once: true }); + }); + }, + }, + }); + const accepted = await owner.openManagedWorkspaceBaseline( + runtimeStore, + openRequest(sourceRoot), + ); + const abort = new AbortController(); + let callbackEntered = false; + const execution = owner.withManagedWorkspaceExecution( + accepted.executionHandle, + async () => { + callbackEntered = true; + }, + { provisioning: 'dependency_environment_v1', abortSignal: abort.signal }, + ); + await provisionStarted; + + abort.abort(new DOMException('User cancelled managed execution', 'AbortError')); + + await assert.rejects(execution, { name: 'AbortError' }); + assert.equal(callbackEntered, false); + await owner.close(); + } finally { + runtimeStore.close(); + await rootOwner.close(); + } +}); + +test('cancels dependency baseline reads while artifact admission is blocked', async () => { + const root = await temporaryRoot(); + const storageRoot = join(root, 'storage'); + const sourceRoot = await createEligibleDependencySource(join(root, 'source')); + const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); + const rootOwner = await tryAcquireInteractiveRootOwner(capability); + assert.ok(rootOwner); + const runtimeStore = createSqliteRuntimeStore(join(storageRoot, 'runtime.sqlite')); + let releaseLock!: () => void; + const lockMayRelease = new Promise((resolve) => { + releaseLock = resolve; + }); + let lockAcquired!: () => void; + const lockIsHeld = new Promise((resolve) => { + lockAcquired = resolve; + }); + let lockTask: Promise | undefined; + try { + const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutablePath, + expectedSha256: gitExecutableSha256, + }, + dependencyEnvironmentProducer: { + capability: createManagedDependencyEnvironmentProducerCapability( + `sha256:${'9'.repeat(64)}`, + ), + packageManagerName: 'npm', + packageManagerVersion: '12.0.2', + nodeRuntime: { + version: process.versions.node, + abi: process.versions.modules ?? 'unknown', + platform: process.platform, + arch: process.arch, + }, + async provision() { + throw new Error('Provisioning must not start after cancellation'); + }, + }, + }); + const accepted = await owner.openManagedWorkspaceBaseline( + runtimeStore, + openRequest(sourceRoot), + ); + lockTask = withArtifactWriterLock(storageRoot, async () => { + lockAcquired(); + await lockMayRelease; + }); + await lockIsHeld; + + const abort = new AbortController(); + let callbackEntered = false; + const execution = owner.withManagedWorkspaceExecution( + accepted.executionHandle, + async () => { + callbackEntered = true; + }, + { provisioning: 'dependency_environment_v1', abortSignal: abort.signal }, + ); + await delay(25); + abort.abort(new DOMException('User cancelled baseline admission', 'AbortError')); + + await assert.rejects( + Promise.race([ + execution, + delay(2_000).then(() => { + throw new Error('Cancellation did not interrupt dependency baseline admission'); + }), + ]), + { name: 'AbortError' }, + ); + assert.equal(callbackEntered, false); + releaseLock(); + await lockTask; + await owner.close(); + } finally { + releaseLock(); + await lockTask?.catch(() => undefined); + runtimeStore.close(); + await rootOwner.close(); + } +}); + +test('rechecks cancellation immediately before issuing an execution scope', async () => { + const root = await temporaryRoot(); + const storageRoot = join(root, 'storage'); + const sourceRoot = await createEligibleSource(join(root, 'source')); + const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); + const rootOwner = await tryAcquireInteractiveRootOwner(capability); + assert.ok(rootOwner); + const runtimeStore = createSqliteRuntimeStore(join(storageRoot, 'runtime.sqlite')); + const abort = new AbortController(); + let callbackEntered = false; + try { + const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutablePath, + expectedSha256: gitExecutableSha256, + }, + failpoint(point) { + if (point === 'before_execution_scope_issue') { + abort.abort(new DOMException('User cancelled before scope issue', 'AbortError')); + } + }, + }); + const accepted = await owner.openManagedWorkspaceBaseline( + runtimeStore, + openRequest(sourceRoot), + ); + + await assert.rejects( + owner.withManagedWorkspaceExecution( + accepted.executionHandle, + async () => { + callbackEntered = true; + }, + { abortSignal: abort.signal }, + ), + { name: 'AbortError' }, + ); + assert.equal(callbackEntered, false); + await owner.close(); + } finally { + runtimeStore.close(); + await rootOwner.close(); + } +}); + test('does not let caller-mutated head state or a shadowed public reader forge execution authority', async () => { const root = await temporaryRoot(); const storageRoot = join(root, 'storage'); @@ -952,6 +1277,39 @@ async function createEligibleSource(sourceRoot: string): Promise { return realpath(sourceRoot); } +async function createEligibleDependencySource(sourceRoot: string): Promise { + const canonicalSource = await createEligibleSource(sourceRoot); + await Promise.all([ + writeFile( + join(canonicalSource, 'package.json'), + '{"name":"fixture","packageManager":"npm@12.0.2"}\n', + ), + writeFile( + join(canonicalSource, 'package-lock.json'), + '{"name":"fixture","lockfileVersion":3,"packages":{"":{"name":"fixture"}}}\n', + ), + writeFile(join(canonicalSource, '.gitignore'), '.maka-workspace.json\nnode_modules/\n'), + ]); + await git(canonicalSource, 'add', 'package.json', 'package-lock.json', '.gitignore'); + await git( + canonicalSource, + '-c', + 'user.name=Maka Test', + '-c', + 'user.email=test@maka.invalid', + 'commit', + '--quiet', + '-m', + 'add dependency manifest', + ); + await mkdir(join(canonicalSource, 'node_modules', 'fixture-package'), { recursive: true }); + await writeFile( + join(canonicalSource, 'node_modules', 'fixture-package', 'index.js'), + 'export const source = "attached-checkout";\n', + ); + return canonicalSource; +} + function openRequest(sourceRoot: string) { return { repositoryId: 'repository_11111111111111111111111111111111', diff --git a/packages/storage/src/__tests__/managed-workspace-worker-bridge-internal.test.ts b/packages/storage/src/__tests__/managed-workspace-worker-bridge-internal.test.ts index 188271d2ae4..57d224e0038 100644 --- a/packages/storage/src/__tests__/managed-workspace-worker-bridge-internal.test.ts +++ b/packages/storage/src/__tests__/managed-workspace-worker-bridge-internal.test.ts @@ -18,6 +18,7 @@ */ import assert from 'node:assert/strict'; +import { join } from 'node:path'; import test from 'node:test'; import { createReadOnlyPermissionProfile } from '@maka/core/permission-profile'; import { @@ -27,6 +28,7 @@ import { import { createManagedWorkspaceWorkerBridgeInternal, ManagedWorkspaceWorkerBridgeError, + type ManagedWorkspaceReadOnlyOperation, } from '../managed-workspace-worker-bridge-internal.js'; function scopeFor(ownerToken: object) { @@ -39,6 +41,17 @@ function scopeFor(ownerToken: object) { }); } +function dependencyScopeFor(ownerToken: object, dependencyRoot: string) { + return issueManagedWorkspaceExecutionScopeInternal(ownerToken, { + provisioning: 'dependency_environment_v1', + workspaceEffect: 'none', + cwd: '/managed/worktree', + dependencyRoot, + binding: Object.freeze({}) as never, + head: Object.freeze({}) as never, + }); +} + test('injects the owner-bound cwd and read-only boundary for allowed operations', async () => { const ownerToken = {}; const calls: unknown[] = []; @@ -122,3 +135,135 @@ test('rejects foreign, expired, mutating, and unknown operations before worker d } assert.equal(dispatches, 0); }); + +test('routes logical node_modules reads through the leased dependency root', async () => { + const ownerToken = {}; + const dependencyRoot = join('/maka', 'dependencies', 'environment', 'node_modules'); + const calls: Array<{ operation: ManagedWorkspaceReadOnlyOperation; cwd: string }> = []; + const bridge = createManagedWorkspaceWorkerBridgeInternal(ownerToken, { + async execute(input) { + calls.push({ operation: input.operation, cwd: input.cwd }); + return input.operation.kind === 'glob' + ? { kind: 'glob', files: [join('fixture-package', 'index.js')] } + : { kind: 'read', content: 'maka-owned' }; + }, + }); + const scope = dependencyScopeFor(ownerToken, dependencyRoot); + + assert.deepEqual( + await bridge.execute(scope, { + kind: 'read', + path: 'node_modules/fixture-package/index.js', + }), + { kind: 'read', content: 'maka-owned' }, + ); + assert.deepEqual( + await bridge.execute(scope, { + kind: 'glob', + path: 'node_modules', + pattern: '**/*.js', + }), + { kind: 'glob', files: ['node_modules/fixture-package/index.js'] }, + ); + assert.equal(calls[0]?.cwd, '/managed/worktree'); + assert.equal( + calls[0]?.operation.kind === 'read' ? calls[0].operation.path : undefined, + join(dependencyRoot, 'fixture-package', 'index.js'), + ); + assert.equal( + calls[1]?.operation.kind === 'glob' ? calls[1].operation.path : undefined, + dependencyRoot, + ); +}); + +test('rejects non-canonical dependency paths before worker dispatch', async () => { + const ownerToken = {}; + let dispatches = 0; + const bridge = createManagedWorkspaceWorkerBridgeInternal(ownerToken, { + async execute() { + dispatches += 1; + return { kind: 'read', content: 'unexpected' }; + }, + }); + const scope = dependencyScopeFor(ownerToken, join('/maka', 'dependencies', 'node_modules')); + + for (const path of [ + 'node_modules/../outside.txt', + 'node_modules/./fixture.js', + 'foo/../node_modules/rogue.js', + 'node_modules/fixture.js:alternate-stream', + 'node_modules/fixture\0.js', + ]) { + await assert.rejects( + () => bridge.execute(scope, { kind: 'read', path }), + (error) => + error instanceof ManagedWorkspaceWorkerBridgeError && + error.code === 'managed_workspace_operation_denied', + ); + } + assert.equal(dispatches, 0); +}); + +test('routes Windows node_modules casing through the dependency lease', async (t) => { + if (process.platform !== 'win32') { + t.skip('Windows path identity is case insensitive'); + return; + } + const ownerToken = {}; + const dependencyRoot = join('C:\\maka', 'dependencies', 'node_modules'); + let dispatchedPath: string | undefined; + const bridge = createManagedWorkspaceWorkerBridgeInternal(ownerToken, { + async execute(input) { + dispatchedPath = input.operation.path; + return { kind: 'read', content: 'maka-owned' }; + }, + }); + const scope = issueManagedWorkspaceExecutionScopeInternal(ownerToken, { + provisioning: 'dependency_environment_v1', + workspaceEffect: 'none', + cwd: 'C:\\managed\\worktree', + dependencyRoot, + binding: Object.freeze({}) as never, + head: Object.freeze({}) as never, + }); + + await bridge.execute(scope, { kind: 'read', path: 'NODE_MODULES/fixture/index.js' }); + + assert.equal(dispatchedPath, join(dependencyRoot, 'fixture', 'index.js')); +}); + +test('never falls back a dependency scope without an exact leased root', async () => { + const ownerToken = {}; + let dispatches = 0; + const bridge = createManagedWorkspaceWorkerBridgeInternal(ownerToken, { + async execute() { + dispatches += 1; + return { kind: 'read', content: 'unexpected' }; + }, + }); + const missingRoot = issueManagedWorkspaceExecutionScopeInternal(ownerToken, { + provisioning: 'dependency_environment_v1', + workspaceEffect: 'none', + cwd: '/managed/worktree', + binding: Object.freeze({}) as never, + head: Object.freeze({}) as never, + }); + const unexpectedRoot = issueManagedWorkspaceExecutionScopeInternal(ownerToken, { + provisioning: 'canonical_tree_only_v1', + workspaceEffect: 'none', + cwd: '/managed/worktree', + dependencyRoot: '/maka/dependencies/node_modules', + binding: Object.freeze({}) as never, + head: Object.freeze({}) as never, + }); + + for (const scope of [missingRoot, unexpectedRoot]) { + await assert.rejects( + () => bridge.execute(scope, { kind: 'read', path: 'node_modules/a.js' }), + (error) => + error instanceof ManagedWorkspaceWorkerBridgeError && + error.code === 'managed_workspace_operation_denied', + ); + } + assert.equal(dispatches, 0); +}); diff --git a/packages/storage/src/__tests__/workspace-version-authority-persistence.test.ts b/packages/storage/src/__tests__/workspace-version-authority-persistence.test.ts index 82ceabf2a7f..cf9d7d99103 100644 --- a/packages/storage/src/__tests__/workspace-version-authority-persistence.test.ts +++ b/packages/storage/src/__tests__/workspace-version-authority-persistence.test.ts @@ -202,6 +202,33 @@ describe('workspace version persistence authority', () => { } }); + it('does not mistake mutated authority control rows for empty schema bootstrap state', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-unbound-authority-state-')); + const dbPath = join(root, 'runtime.sqlite'); + const store = createSqliteRuntimeStore(dbPath); + try { + const raw = new DatabaseSync(dbPath); + try { + raw.exec(` + CREATE TABLE automation_authority_state ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + revision INTEGER NOT NULL CHECK (revision >= 0) + ); + INSERT INTO automation_authority_state(singleton, revision) VALUES (1, 1); + `); + } finally { + raw.close(); + } + assert.throws( + () => bindWorkspaceBaselineAuthorityStoreRootInternal(store, TEST_STORAGE_ROOT_ID), + /unbound operational data require explicit storage-root adoption/iu, + ); + } finally { + store.close(); + await rm(root, { recursive: true, force: true }); + } + }); + for (const failpoint of [ 'after_workspace_epoch_event_insert', 'after_workspace_version_event_insert', diff --git a/packages/storage/src/artifact-writer-bootstrap-lock.ts b/packages/storage/src/artifact-writer-bootstrap-lock.ts index 3eec8899bcf..5551ea5c8b5 100644 --- a/packages/storage/src/artifact-writer-bootstrap-lock.ts +++ b/packages/storage/src/artifact-writer-bootstrap-lock.ts @@ -21,13 +21,15 @@ import { constants as fsConstants } from 'node:fs'; import { lstat, open, type FileHandle } from 'node:fs/promises'; -import { unlock, waitForLock } from 'fs-native-extensions'; +import { setTimeout as delay } from 'node:timers/promises'; +import { tryLock, unlock, waitForLock } from 'fs-native-extensions'; const lockGates = new Map>(); export async function withArtifactWriterBootstrapLock( lockPath: string, operation: () => Promise, + abortSignal?: AbortSignal, ): Promise { const previous = lockGates.get(lockPath); let releaseGate!: () => void; @@ -35,8 +37,9 @@ export async function withArtifactWriterBootstrapLock( releaseGate = resolve; }); lockGates.set(lockPath, current); - await previous?.catch(() => {}); try { + if (previous) await waitForGate(previous, abortSignal); + abortSignal?.throwIfAborted(); const handle = await open( lockPath, fsConstants.O_CREAT | fsConstants.O_RDWR | fsConstants.O_NOFOLLOW, @@ -46,9 +49,10 @@ export async function withArtifactWriterBootstrapLock( try { await assertStableRegularFile(handle, lockPath); if (process.platform !== 'win32') await handle.chmod(0o600); - await waitForLock(handle.fd); + await acquireLock(handle.fd, abortSignal); locked = true; await assertStableRegularFile(handle, lockPath); + abortSignal?.throwIfAborted(); return await operation(); } finally { if (locked) releaseLock(handle); @@ -60,6 +64,41 @@ export async function withArtifactWriterBootstrapLock( } } +async function acquireLock(fd: number, abortSignal: AbortSignal | undefined): Promise { + if (!abortSignal) { + await waitForLock(fd); + return; + } + abortSignal.throwIfAborted(); + while (!tryLock(fd)) { + await delay(25, undefined, { signal: abortSignal }); + } +} + +async function waitForGate( + gate: Promise, + abortSignal: AbortSignal | undefined, +): Promise { + if (!abortSignal) { + await gate.catch(() => {}); + return; + } + abortSignal.throwIfAborted(); + await Promise.race([ + gate.catch(() => {}), + new Promise((_resolve, reject) => { + const onAbort = () => + reject( + abortSignal.reason instanceof Error + ? abortSignal.reason + : new DOMException('Artifact writer admission was aborted', 'AbortError'), + ); + abortSignal.addEventListener('abort', onAbort, { once: true }); + gate.finally(() => abortSignal.removeEventListener('abort', onAbort)).catch(() => {}); + }), + ]); +} + async function assertStableRegularFile(handle: FileHandle, lockPath: string): Promise { const [handleStat, pathStat] = await Promise.all([ handle.stat({ bigint: true }), diff --git a/packages/storage/src/artifact-writer-lock.ts b/packages/storage/src/artifact-writer-lock.ts index 20869b83a3b..0b7adac6b0f 100644 --- a/packages/storage/src/artifact-writer-lock.ts +++ b/packages/storage/src/artifact-writer-lock.ts @@ -22,7 +22,8 @@ import { constants as fsConstants } from 'node:fs'; import { lstat, mkdir, open, realpath, type FileHandle } from 'node:fs/promises'; import { join } from 'node:path'; -import { unlock, waitForLock } from 'fs-native-extensions'; +import { setTimeout as delay } from 'node:timers/promises'; +import { tryLock, unlock, waitForLock } from 'fs-native-extensions'; import { ARTIFACT_WRITER_LOCK_FILE } from './artifact-storage-layout.js'; import { withArtifactWriterBootstrapLock } from './artifact-writer-bootstrap-lock.js'; import { @@ -37,21 +38,32 @@ const lockGates = new Map>(); export async function withArtifactWriterLock( workspaceRoot: string, operation: (canonicalRoot: string) => Promise, + abortSignal?: AbortSignal, ): Promise { + abortSignal?.throwIfAborted(); await mkdir(workspaceRoot, { recursive: true }); const requestedCanonicalRoot = await realpath(workspaceRoot); const bootstrap = await prepareArtifactWriterBootstrapAuthority(requestedCanonicalRoot); - return withArtifactWriterBootstrapLock(bootstrap.lockPath, async () => { - await bootstrap.assertCurrentRoot(); - const authority = await prepareArtifactWriterLockAuthorityForMarkedRoot( - bootstrap.canonicalPath, - ); - if (!authority) return operation(bootstrap.canonicalPath); - if (authority.bootstrapLockPath !== bootstrap.lockPath) { - throw new Error('Storage root identity changed while acquiring its Artifact writer lock'); - } - return withAuthorityArtifactWriterLock(authority, () => operation(bootstrap.canonicalPath)); - }); + return withArtifactWriterBootstrapLock( + bootstrap.lockPath, + async () => { + abortSignal?.throwIfAborted(); + await bootstrap.assertCurrentRoot(); + const authority = await prepareArtifactWriterLockAuthorityForMarkedRoot( + bootstrap.canonicalPath, + ); + if (!authority) return operation(bootstrap.canonicalPath); + if (authority.bootstrapLockPath !== bootstrap.lockPath) { + throw new Error('Storage root identity changed while acquiring its Artifact writer lock'); + } + return withAuthorityArtifactWriterLock( + authority, + () => operation(bootstrap.canonicalPath), + abortSignal, + ); + }, + abortSignal, + ); } export async function withLeaseBoundArtifactWriterLock( @@ -66,6 +78,7 @@ export async function withLeaseBoundArtifactWriterLock( async function withAuthorityArtifactWriterLock( authority: ArtifactWriterLockAuthority, operation: () => Promise, + abortSignal?: AbortSignal, ): Promise { return withArtifactWriterLockPath( join(authority.controlDirectory, ARTIFACT_WRITER_LOCK_FILE), @@ -73,38 +86,51 @@ async function withAuthorityArtifactWriterLock( await authority.assertCurrentRoot(); return operation(); }, + abortSignal, ); } async function withArtifactWriterLockPath( lockPath: string, operation: () => Promise, + abortSignal?: AbortSignal, ): Promise { - return runWithLockGate(lockPath, async () => { - const handle = await openArtifactWriterLock(lockPath); - let acquired = false; - try { - await assertStableRegularFile(handle, lockPath); - await waitForLock(handle.fd); - acquired = true; - await assertStableRegularFile(handle, lockPath); - return await operation(); - } finally { - if (acquired) releaseLock(handle); - await handle.close(); - } - }); + return runWithLockGate( + lockPath, + async () => { + abortSignal?.throwIfAborted(); + const handle = await openArtifactWriterLock(lockPath); + let acquired = false; + try { + await assertStableRegularFile(handle, lockPath); + await acquireLock(handle.fd, abortSignal); + acquired = true; + await assertStableRegularFile(handle, lockPath); + abortSignal?.throwIfAborted(); + return await operation(); + } finally { + if (acquired) releaseLock(handle); + await handle.close(); + } + }, + abortSignal, + ); } -async function runWithLockGate(lockPath: string, operation: () => Promise): Promise { +async function runWithLockGate( + lockPath: string, + operation: () => Promise, + abortSignal?: AbortSignal, +): Promise { const previous = lockGates.get(lockPath); let release!: () => void; const current = new Promise((resolve) => { release = resolve; }); lockGates.set(lockPath, current); - await previous?.catch(() => {}); try { + if (previous) await waitForGate(previous, abortSignal); + abortSignal?.throwIfAborted(); return await operation(); } finally { release(); @@ -112,6 +138,41 @@ async function runWithLockGate(lockPath: string, operation: () => Promise) } } +async function acquireLock(fd: number, abortSignal: AbortSignal | undefined): Promise { + if (!abortSignal) { + await waitForLock(fd); + return; + } + abortSignal.throwIfAborted(); + while (!tryLock(fd)) { + await delay(25, undefined, { signal: abortSignal }); + } +} + +async function waitForGate( + gate: Promise, + abortSignal: AbortSignal | undefined, +): Promise { + if (!abortSignal) { + await gate.catch(() => {}); + return; + } + abortSignal.throwIfAborted(); + await Promise.race([ + gate.catch(() => {}), + new Promise((_resolve, reject) => { + const onAbort = () => + reject( + abortSignal.reason instanceof Error + ? abortSignal.reason + : new DOMException('Artifact writer admission was aborted', 'AbortError'), + ); + abortSignal.addEventListener('abort', onAbort, { once: true }); + gate.finally(() => abortSignal.removeEventListener('abort', onAbort)).catch(() => {}); + }), + ]); +} + async function openArtifactWriterLock(lockPath: string): Promise { const handle = await open( lockPath, diff --git a/packages/storage/src/execution-stores-workspace-authority-internal.ts b/packages/storage/src/execution-stores-workspace-authority-internal.ts new file mode 100644 index 00000000000..68182790d0f --- /dev/null +++ b/packages/storage/src/execution-stores-workspace-authority-internal.ts @@ -0,0 +1,21 @@ +import type { RuntimeWorkspaceVersionAuthorityStore } from '@maka/core/runtime-event-store'; + +const workspaceAuthorities = new WeakMap(); + +export function registerExecutionStoresWorkspaceAuthorityInternal( + stores: object, + authority: RuntimeWorkspaceVersionAuthorityStore, +): void { + if (workspaceAuthorities.has(stores)) { + throw new Error('Execution stores workspace authority is already registered'); + } + workspaceAuthorities.set(stores, authority); +} + +export function requireExecutionStoresWorkspaceAuthorityInternal( + stores: object, +): RuntimeWorkspaceVersionAuthorityStore { + const authority = workspaceAuthorities.get(stores); + if (!authority) throw new Error('Execution stores workspace authority is unavailable'); + return authority; +} diff --git a/packages/storage/src/execution-stores.ts b/packages/storage/src/execution-stores.ts index ea8e49bcdab..7d809128eb2 100644 --- a/packages/storage/src/execution-stores.ts +++ b/packages/storage/src/execution-stores.ts @@ -76,6 +76,11 @@ import type { ToolCommitResult, ToolOperationRecord, } from './sqlite-runtime-store.js'; +import { registerExecutionStoresWorkspaceAuthorityInternal } from './execution-stores-workspace-authority-internal.js'; +import { + bindWorkspaceBaselineAuthorityStoreRootInternal, + WorkspaceStorageRootAdoptionRequiredError, +} from './workspace-version-authority-internal.js'; const executionStoresWriterBrand: unique symbol = Symbol('ExecutionStoresWriter'); const executionStoresReaderBrand: unique symbol = Symbol('ExecutionStoresReader'); @@ -258,12 +263,30 @@ export function authenticateExecutionStoresReader( export async function openInteractiveExecutionStoresForWrite( lease: StorageRootLease<'interactive', 'write'>, ): Promise> { + // Establish root identity before any other operational authority creates + // singleton control rows. Existing unbound logical data remain explicitly + // unadopted and therefore unavailable to managed workspace admission. + await initializeWorkspaceStorageRootBinding(lease); const interactionStore = await openSqliteInteractiveInteractionStoreForWrite(lease); return openExecutionStoresForWrite(lease, 'interactive', { interactionStore, }); } +async function initializeWorkspaceStorageRootBinding( + lease: StorageRootLease<'interactive', 'write'>, +): Promise { + await assertStorageRootLease(lease, 'interactive', 'write'); + const persistence = await openRuntimeEventPersistence({ workspaceRoot: lease.canonicalPath }); + try { + bindWorkspaceBaselineAuthorityStoreRootInternal(persistence.runtimeCommitStore, lease.rootId); + } catch (error) { + if (!(error instanceof WorkspaceStorageRootAdoptionRequiredError)) throw error; + } finally { + persistence.close(); + } +} + async function openExecutionStoresForWrite( lease: StorageRootLease, kind: K, @@ -312,6 +335,22 @@ async function createExecutionStoresForWrite {}); + throw error; + } + } let conversationOperationalStateStore: ConversationOperationalStateStore; try { conversationOperationalStateStore = createConversationOperationalStateStore( @@ -558,6 +597,7 @@ async function createExecutionStoresForWrite; createManagedWorkspaceFromSource( input: CreateManagedWorkspaceFromSourceInput, + options?: GitWorkspaceOperationOptions, ): Promise; openManagedWorkspaceFromBinding( input: ManagedWorkspaceIdentity, + options?: GitWorkspaceOperationOptions, ): Promise; - inspectManagedWorkspace(binding: ManagedWorkspaceBinding): Promise; + inspectManagedWorkspace( + binding: ManagedWorkspaceBinding, + options?: GitWorkspaceOperationOptions, + ): Promise; + hasManagedWorkspaceBaselinePath( + binding: ManagedWorkspaceBinding, + path: string, + options?: GitWorkspaceOperationOptions, + ): Promise; + readManagedWorkspaceBaselineFile( + binding: ManagedWorkspaceBinding, + path: string, + options?: GitWorkspaceOperationOptions, + ): Promise; quarantineManagedWorkspace( binding: ManagedWorkspaceBinding, reason: string, @@ -388,9 +409,12 @@ class GitWorkspaceServiceImpl implements GitWorkspaceService { } this.runtime = new VerifiedGitRuntime(input.gitRuntime); registerManagedBaselineReceiptAuthorityInternal(this, { - issue: (binding) => this.#createOrReuseManagedWorkspaceBaselineReceipt(binding), - require: (request) => this.#requireManagedWorkspaceBaselineReceipt(request), - verify: (receipt) => this.#verifyManagedWorkspaceBaselineReceipt(receipt), + issue: (binding, abortSignal) => + this.#createOrReuseManagedWorkspaceBaselineReceipt(binding, abortSignal), + require: (request, abortSignal) => + this.#requireManagedWorkspaceBaselineReceipt(request, abortSignal), + verify: (receipt, abortSignal) => + this.#verifyManagedWorkspaceBaselineReceipt(receipt, abortSignal), }); } @@ -401,133 +425,147 @@ class GitWorkspaceServiceImpl implements GitWorkspaceService { async createManagedWorkspaceFromSource( input: CreateManagedWorkspaceFromSourceInput, + options: GitWorkspaceOperationOptions = {}, ): Promise { - assertOpenIdentity(input); - const runtime = await this.runtime.verify(); - - return withArtifactWriterLock(this.input.storageRoot, async (canonicalStorageRoot) => { - const layout = workspaceLayout(canonicalStorageRoot, input); - await ensureOwnedDirectory(layout.managedRoot, canonicalStorageRoot); - await ensureOwnedDirectory(layout.quarantineRoot, layout.managedRoot); - await ensureOwnedDirectory(layout.homePath, layout.managedRoot); - await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); - - const quarantined = await this.resumePendingQuarantine(input, layout, runtime.digest); - if (quarantined) { - throw new GitWorkspaceServiceError( - 'managed_workspace_unavailable', - `Managed workspace instance was quarantined: ${input.workspaceInstanceId}`, - ); - } - - const existingBinding = await readBinding(layout.bindingPath); - if (existingBinding) { - assertBindingMatches(existingBinding, input, layout, runtime.digest); - return this.adoptStoredBinding(input, existingBinding, layout); - } - - const sourceRoot = await canonicalDirectory(input.sourceRoot, 'repository_ineligible'); - const source = await this.inspectSourceRepository(sourceRoot); - const repository = await this.openRepository(input, source, layout, runtime.digest); - const epoch = await this.openEpochArtifact(input, source, repository, layout); - await this.clearIncompleteInstance(input, epoch, layout); - await ensureOwnedDirectory(layout.instanceRoot, layout.managedRoot); - await this.runtime.run( - [ - '--git-dir', - repository.repositoryPath, - 'worktree', - 'add', - '--quiet', - '--detach', - layout.worktreePath, - epoch.baselineCommitOid, - ], - layout.homePath, - ); - await this.input.failpoint?.('after_worktree_materialized'); + return this.runtime.runWithAbortSignal(options.abortSignal, async () => { + assertOpenIdentity(input); + const runtime = await this.runtime.verify(); + + return withArtifactWriterLock( + this.input.storageRoot, + async (canonicalStorageRoot) => { + const layout = workspaceLayout(canonicalStorageRoot, input); + await ensureOwnedDirectory(layout.managedRoot, canonicalStorageRoot); + await ensureOwnedDirectory(layout.quarantineRoot, layout.managedRoot); + await ensureOwnedDirectory(layout.homePath, layout.managedRoot); + await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); + + const quarantined = await this.resumePendingQuarantine(input, layout, runtime.digest); + if (quarantined) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace instance was quarantined: ${input.workspaceInstanceId}`, + ); + } + + const existingBinding = await readBinding(layout.bindingPath); + if (existingBinding) { + assertBindingMatches(existingBinding, input, layout, runtime.digest); + return this.adoptStoredBinding(input, existingBinding, layout); + } + + const sourceRoot = await canonicalDirectory(input.sourceRoot, 'repository_ineligible'); + const source = await this.inspectSourceRepository(sourceRoot); + const repository = await this.openRepository(input, source, layout, runtime.digest); + const epoch = await this.openEpochArtifact(input, source, repository, layout); + await this.clearIncompleteInstance(input, epoch, layout); + await ensureOwnedDirectory(layout.instanceRoot, layout.managedRoot); + await this.runtime.run( + [ + '--git-dir', + repository.repositoryPath, + 'worktree', + 'add', + '--quiet', + '--detach', + layout.worktreePath, + epoch.baselineCommitOid, + ], + layout.homePath, + ); + await this.input.failpoint?.('after_worktree_materialized'); - await this.runtime.run( - [ - '--git-dir', - repository.repositoryPath, - 'worktree', - 'lock', - '--reason', - worktreeLockReason(input), - layout.worktreePath, - ], - layout.homePath, - ); - await this.input.failpoint?.('after_worktree_locked'); - await this.updateRefCas( - repository.repositoryPath, - epoch.headRef, - epoch.baselineCommitOid, - repository.objectFormat, - layout.homePath, + await this.runtime.run( + [ + '--git-dir', + repository.repositoryPath, + 'worktree', + 'lock', + '--reason', + worktreeLockReason(input), + layout.worktreePath, + ], + layout.homePath, + ); + await this.input.failpoint?.('after_worktree_locked'); + await this.updateRefCas( + repository.repositoryPath, + epoch.headRef, + epoch.baselineCommitOid, + repository.objectFormat, + layout.homePath, + ); + await this.input.failpoint?.('after_head_ref_updated'); + + const binding: ManagedWorkspaceBinding = { + schemaVersion: BINDING_SCHEMA_VERSION, + protocol: 'git_managed_workspace_v1', + repositoryId: input.repositoryId, + workspaceId: input.workspaceId, + workspaceEpochId: input.workspaceEpochId, + workspaceInstanceId: input.workspaceInstanceId, + sourceRoot: epoch.sourceRoot, + sourceGitCommonDir: epoch.sourceGitCommonDir, + sourceHeadCommitOid: epoch.sourceHeadCommitOid, + sourceTreeOid: epoch.sourceTreeOid, + repositoryPath: repository.repositoryPath, + worktreePath: normalize(layout.worktreePath), + hooksPath: repository.hooksPath, + baselineCommitOid: epoch.baselineCommitOid, + baselineTreeOid: epoch.baselineTreeOid, + headRef: epoch.headRef, + gitRuntimeSha256: runtime.digest, + objectFormat: repository.objectFormat, + materializationProfileDigest: epoch.materializationProfileDigest, + materializationSemantics: MATERIALIZATION_SEMANTICS, + }; + const inspection = await this.inspectBinding(binding, layout); + if (inspection.state !== 'ready') { + await this.beginQuarantine(binding, layout, 'initial_materialization_drift'); + throw new GitWorkspaceServiceError( + 'managed_workspace_drifted', + 'Git materialization did not produce a clean managed workspace', + ); + } + await atomicWriteJson(layout.bindingPath, binding); + return binding; + }, + options.abortSignal, ); - await this.input.failpoint?.('after_head_ref_updated'); - - const binding: ManagedWorkspaceBinding = { - schemaVersion: BINDING_SCHEMA_VERSION, - protocol: 'git_managed_workspace_v1', - repositoryId: input.repositoryId, - workspaceId: input.workspaceId, - workspaceEpochId: input.workspaceEpochId, - workspaceInstanceId: input.workspaceInstanceId, - sourceRoot: epoch.sourceRoot, - sourceGitCommonDir: epoch.sourceGitCommonDir, - sourceHeadCommitOid: epoch.sourceHeadCommitOid, - sourceTreeOid: epoch.sourceTreeOid, - repositoryPath: repository.repositoryPath, - worktreePath: normalize(layout.worktreePath), - hooksPath: repository.hooksPath, - baselineCommitOid: epoch.baselineCommitOid, - baselineTreeOid: epoch.baselineTreeOid, - headRef: epoch.headRef, - gitRuntimeSha256: runtime.digest, - objectFormat: repository.objectFormat, - materializationProfileDigest: epoch.materializationProfileDigest, - materializationSemantics: MATERIALIZATION_SEMANTICS, - }; - const inspection = await this.inspectBinding(binding, layout); - if (inspection.state !== 'ready') { - await this.beginQuarantine(binding, layout, 'initial_materialization_drift'); - throw new GitWorkspaceServiceError( - 'managed_workspace_drifted', - 'Git materialization did not produce a clean managed workspace', - ); - } - await atomicWriteJson(layout.bindingPath, binding); - return binding; }); } async openManagedWorkspaceFromBinding( input: ManagedWorkspaceIdentity, + options: GitWorkspaceOperationOptions = {}, ): Promise { - assertOpenIdentity(input); - const runtime = await this.runtime.verify(); - return withArtifactWriterLock(this.input.storageRoot, async (canonicalStorageRoot) => { - const layout = workspaceLayout(canonicalStorageRoot, input); - await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); - const quarantined = await this.resumePendingQuarantine(input, layout, runtime.digest); - if (quarantined) { - throw new GitWorkspaceServiceError( - 'managed_workspace_unavailable', - `Managed workspace instance was quarantined: ${input.workspaceInstanceId}`, - ); - } - const binding = await readBinding(layout.bindingPath); - if (!binding) { - throw new GitWorkspaceServiceError( - 'managed_workspace_unavailable', - `Managed workspace binding is unavailable: ${input.workspaceInstanceId}`, - ); - } - assertBindingIdentity(binding, input, layout, runtime.digest); - return this.adoptStoredBinding(input, binding, layout); + return this.runtime.runWithAbortSignal(options.abortSignal, async () => { + assertOpenIdentity(input); + const runtime = await this.runtime.verify(); + return withArtifactWriterLock( + this.input.storageRoot, + async (canonicalStorageRoot) => { + const layout = workspaceLayout(canonicalStorageRoot, input); + await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); + const quarantined = await this.resumePendingQuarantine(input, layout, runtime.digest); + if (quarantined) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace instance was quarantined: ${input.workspaceInstanceId}`, + ); + } + const binding = await readBinding(layout.bindingPath); + if (!binding) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace binding is unavailable: ${input.workspaceInstanceId}`, + ); + } + assertBindingIdentity(binding, input, layout, runtime.digest); + return this.adoptStoredBinding(input, binding, layout); + }, + options.abortSignal, + ); }); } @@ -536,6 +574,10 @@ class GitWorkspaceServiceImpl implements GitWorkspaceService { binding: ManagedWorkspaceBinding, layout: WorkspaceLayout, ): Promise { + const currentSource = await this.inspectSourceRepository( + await canonicalDirectory(binding.sourceRoot, 'repository_ineligible'), + ); + assertBindingSourceObservation(binding, currentSource); const repository = await this.requireRepository(input, layout); assertBindingRepository(binding, repository); const epoch = await this.requireEpochArtifact(input, repository, layout); @@ -552,132 +594,274 @@ class GitWorkspaceServiceImpl implements GitWorkspaceService { async inspectManagedWorkspace( binding: ManagedWorkspaceBinding, + options: GitWorkspaceOperationOptions = {}, ): Promise { + return this.runtime.runWithAbortSignal(options.abortSignal, async () => { + const runtime = await this.runtime.verify(); + assertBindingShape(binding); + assertOpenIdentity(binding); + return withArtifactWriterLock( + this.input.storageRoot, + async (canonicalStorageRoot) => { + const layout = workspaceLayout(canonicalStorageRoot, binding); + await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); + assertBindingPaths(binding, layout); + const quarantined = await this.resumePendingQuarantine(binding, layout, runtime.digest); + if (quarantined) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace instance was quarantined: ${binding.workspaceInstanceId}`, + ); + } + const stored = await readBinding(layout.bindingPath); + if (!stored || !sameBinding(stored, binding)) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace binding is unavailable: ${binding.workspaceInstanceId}`, + ); + } + const repository = await readRepositoryRecord(layout.repositoryRecordPath); + if (!repository) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed Git repository record is unavailable: ${binding.repositoryId}`, + ); + } + assertBindingRepository(binding, repository); + await this.assertRepositoryArtifact(repository); + const epoch = await this.requireEpochArtifact(binding, repository, layout); + assertBindingEpoch(binding, epoch); + return this.inspectBinding(binding, layout); + }, + options.abortSignal, + ); + }); + } + + async readManagedWorkspaceBaselineFile( + binding: ManagedWorkspaceBinding, + path: string, + options: GitWorkspaceOperationOptions = {}, + ): Promise { + options.abortSignal?.throwIfAborted(); const runtime = await this.runtime.verify(); + options.abortSignal?.throwIfAborted(); assertBindingShape(binding); assertOpenIdentity(binding); - return withArtifactWriterLock(this.input.storageRoot, async (canonicalStorageRoot) => { - const layout = workspaceLayout(canonicalStorageRoot, binding); - await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); - assertBindingPaths(binding, layout); - const quarantined = await this.resumePendingQuarantine(binding, layout, runtime.digest); - if (quarantined) { - throw new GitWorkspaceServiceError( - 'managed_workspace_unavailable', - `Managed workspace instance was quarantined: ${binding.workspaceInstanceId}`, - ); - } - const stored = await readBinding(layout.bindingPath); - if (!stored || !sameBinding(stored, binding)) { - throw new GitWorkspaceServiceError( - 'managed_workspace_unavailable', - `Managed workspace binding is unavailable: ${binding.workspaceInstanceId}`, - ); - } - const repository = await readRepositoryRecord(layout.repositoryRecordPath); - if (!repository) { - throw new GitWorkspaceServiceError( - 'managed_workspace_unavailable', - `Managed Git repository record is unavailable: ${binding.repositoryId}`, - ); - } - assertBindingRepository(binding, repository); - await this.assertRepositoryArtifact(repository); - const epoch = await this.requireEpochArtifact(binding, repository, layout); - assertBindingEpoch(binding, epoch); - return this.inspectBinding(binding, layout); - }); + const trackedPath = assertManagedTrackedPath(path); + return this.runtime.runWithAbortSignal(options.abortSignal, () => + withArtifactWriterLock( + this.input.storageRoot, + async (canonicalStorageRoot) => { + const layout = workspaceLayout(canonicalStorageRoot, binding); + await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); + assertBindingPaths(binding, layout); + const quarantined = await this.resumePendingQuarantine(binding, layout, runtime.digest); + if (quarantined) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace instance was quarantined: ${binding.workspaceInstanceId}`, + ); + } + const stored = await readBinding(layout.bindingPath); + if (!stored || !sameBinding(stored, binding)) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace binding is unavailable: ${binding.workspaceInstanceId}`, + ); + } + const repository = await this.requireRepository(binding, layout); + assertBindingRepository(binding, repository); + await this.assertRepositoryArtifact(repository); + const epoch = await this.requireEpochArtifact(binding, repository, layout); + assertBindingEpoch(binding, epoch); + options.abortSignal?.throwIfAborted(); + return await this.runtime.runBuffer( + [ + '--git-dir', + binding.repositoryPath, + 'show', + `${binding.baselineCommitOid}:${trackedPath}`, + ], + layout.homePath, + ); + }, + options.abortSignal, + ), + ); + } + + async hasManagedWorkspaceBaselinePath( + binding: ManagedWorkspaceBinding, + path: string, + options: GitWorkspaceOperationOptions = {}, + ): Promise { + options.abortSignal?.throwIfAborted(); + const runtime = await this.runtime.verify(); + options.abortSignal?.throwIfAborted(); + assertBindingShape(binding); + assertOpenIdentity(binding); + const trackedPath = assertManagedTrackedPath(path); + return this.runtime.runWithAbortSignal(options.abortSignal, () => + withArtifactWriterLock( + this.input.storageRoot, + async (canonicalStorageRoot) => { + const layout = workspaceLayout(canonicalStorageRoot, binding); + await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); + assertBindingPaths(binding, layout); + const quarantined = await this.resumePendingQuarantine(binding, layout, runtime.digest); + if (quarantined) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace instance was quarantined: ${binding.workspaceInstanceId}`, + ); + } + const stored = await readBinding(layout.bindingPath); + if (!stored || !sameBinding(stored, binding)) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + `Managed workspace binding is unavailable: ${binding.workspaceInstanceId}`, + ); + } + const repository = await this.requireRepository(binding, layout); + assertBindingRepository(binding, repository); + await this.assertRepositoryArtifact(repository); + const epoch = await this.requireEpochArtifact(binding, repository, layout); + assertBindingEpoch(binding, epoch); + options.abortSignal?.throwIfAborted(); + const output = await this.runtime.runBuffer( + [ + '--git-dir', + binding.repositoryPath, + 'ls-tree', + '-z', + binding.baselineCommitOid, + '--', + trackedPath, + ], + layout.homePath, + ); + return output.byteLength > 0; + }, + options.abortSignal, + ), + ); } async #requireManagedWorkspaceBaselineReceipt( input: CreateManagedWorkspaceFromSourceInput, + abortSignal?: AbortSignal, ): Promise { - const runtime = await this.runtime.verify(); - assertOpenIdentity(input); - return withArtifactWriterLock(this.input.storageRoot, async (canonicalStorageRoot) => { - const layout = workspaceLayout(canonicalStorageRoot, input); - await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); - const receipt = await readBaselineReceipt(layout.baselineReceiptPath); - if (!receipt) { - throw new GitWorkspaceServiceError( - 'managed_workspace_unavailable', - 'Canonical workspace baseline receipt is unavailable', - ); - } - assertBindingMatches(receipt.binding, input, layout, runtime.digest); - const summary = await this.requireVerifiedBaselineContext( - receipt.binding, - layout, - runtime.digest, + return this.runtime.runWithAbortSignal(abortSignal, async () => { + const runtime = await this.runtime.verify(); + assertOpenIdentity(input); + return withArtifactWriterLock( + this.input.storageRoot, + async (canonicalStorageRoot) => { + const layout = workspaceLayout(canonicalStorageRoot, input); + await assertOwnedManagedWorkspaceLayout(canonicalStorageRoot, layout); + const receipt = await readBaselineReceipt(layout.baselineReceiptPath); + if (!receipt) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + 'Canonical workspace baseline receipt is unavailable', + ); + } + assertBindingMatches(receipt.binding, input, layout, runtime.digest); + const summary = await this.requireVerifiedBaselineContext( + receipt.binding, + layout, + runtime.digest, + ); + assertBaselineReceiptMatches(receipt, receipt.binding, summary); + return receipt; + }, + abortSignal, ); - assertBaselineReceiptMatches(receipt, receipt.binding, summary); - return receipt; }); } async #createOrReuseManagedWorkspaceBaselineReceipt( binding: ManagedWorkspaceBinding, + abortSignal?: AbortSignal, ): Promise { - const runtime = await this.runtime.verify(); - assertBindingShape(binding); - assertOpenIdentity(binding); - return withArtifactWriterLock(this.input.storageRoot, async (canonicalStorageRoot) => { - const layout = workspaceLayout(canonicalStorageRoot, binding); - const summary = await this.requireVerifiedBaselineContext(binding, layout, runtime.digest); - const existing = await readBaselineReceipt(layout.baselineReceiptPath); - if (existing) { - assertBaselineReceiptMatches(existing, binding, summary); - return existing; - } - const identities = deriveBaselineReceiptIdentities(binding); - const receipt: ManagedWorkspaceBaselineReceiptV1 = { - schemaVersion: BASELINE_RECEIPT_SCHEMA_VERSION, - protocol: 'maka_managed_workspace_baseline_receipt_v1', - binding, - workspaceVersionId: identities.workspaceVersionId, - policyVersion: 1, - policyHash: MANAGED_BASELINE_POLICY_HASH_V1, - epochOpenedEventId: identities.epochOpenedEventId, - baselineAcceptedEventId: identities.baselineAcceptedEventId, - treeDeltaDigest: summary.treeDeltaDigest, - changedFileCount: summary.changedFileCount, - deletedFileCount: 0, - }; - await atomicWriteJson(layout.baselineReceiptPath, receipt); - await this.input.failpoint?.('after_baseline_receipt'); - const durable = await readBaselineReceipt(layout.baselineReceiptPath); - if (!durable) { - throw new GitWorkspaceServiceError( - 'managed_workspace_unavailable', - 'Managed workspace baseline receipt was not durable', - ); - } - assertBaselineReceiptMatches(durable, binding, summary); - return durable; + return this.runtime.runWithAbortSignal(abortSignal, async () => { + const runtime = await this.runtime.verify(); + assertBindingShape(binding); + assertOpenIdentity(binding); + return withArtifactWriterLock( + this.input.storageRoot, + async (canonicalStorageRoot) => { + const layout = workspaceLayout(canonicalStorageRoot, binding); + const summary = await this.requireVerifiedBaselineContext( + binding, + layout, + runtime.digest, + ); + const existing = await readBaselineReceipt(layout.baselineReceiptPath); + if (existing) { + assertBaselineReceiptMatches(existing, binding, summary); + return existing; + } + const identities = deriveBaselineReceiptIdentities(binding); + const receipt: ManagedWorkspaceBaselineReceiptV1 = { + schemaVersion: BASELINE_RECEIPT_SCHEMA_VERSION, + protocol: 'maka_managed_workspace_baseline_receipt_v1', + binding, + workspaceVersionId: identities.workspaceVersionId, + policyVersion: 1, + policyHash: MANAGED_BASELINE_POLICY_HASH_V1, + epochOpenedEventId: identities.epochOpenedEventId, + baselineAcceptedEventId: identities.baselineAcceptedEventId, + treeDeltaDigest: summary.treeDeltaDigest, + changedFileCount: summary.changedFileCount, + deletedFileCount: 0, + }; + await atomicWriteJson(layout.baselineReceiptPath, receipt); + await this.input.failpoint?.('after_baseline_receipt'); + const durable = await readBaselineReceipt(layout.baselineReceiptPath); + if (!durable) { + throw new GitWorkspaceServiceError( + 'managed_workspace_unavailable', + 'Managed workspace baseline receipt was not durable', + ); + } + assertBaselineReceiptMatches(durable, binding, summary); + return durable; + }, + abortSignal, + ); }); } async #verifyManagedWorkspaceBaselineReceipt( receipt: ManagedWorkspaceBaselineReceiptV1, + abortSignal?: AbortSignal, ): Promise { - const runtime = await this.runtime.verify(); - assertBaselineReceiptShape(receipt); - assertOpenIdentity(receipt.binding); - await withArtifactWriterLock(this.input.storageRoot, async (canonicalStorageRoot) => { - const layout = workspaceLayout(canonicalStorageRoot, receipt.binding); - const summary = await this.requireVerifiedBaselineContext( - receipt.binding, - layout, - runtime.digest, + await this.runtime.runWithAbortSignal(abortSignal, async () => { + const runtime = await this.runtime.verify(); + assertBaselineReceiptShape(receipt); + assertOpenIdentity(receipt.binding); + await withArtifactWriterLock( + this.input.storageRoot, + async (canonicalStorageRoot) => { + const layout = workspaceLayout(canonicalStorageRoot, receipt.binding); + const summary = await this.requireVerifiedBaselineContext( + receipt.binding, + layout, + runtime.digest, + ); + const durable = await readBaselineReceipt(layout.baselineReceiptPath); + if (!durable || !sameBaselineReceipt(durable, receipt)) { + throw new GitWorkspaceServiceError( + 'managed_workspace_identity_conflict', + 'Managed workspace baseline receipt does not match its durable artifact', + ); + } + assertBaselineReceiptMatches(durable, receipt.binding, summary); + }, + abortSignal, ); - const durable = await readBaselineReceipt(layout.baselineReceiptPath); - if (!durable || !sameBaselineReceipt(durable, receipt)) { - throw new GitWorkspaceServiceError( - 'managed_workspace_identity_conflict', - 'Managed workspace baseline receipt does not match its durable artifact', - ); - } - assertBaselineReceiptMatches(durable, receipt.binding, summary); }); } @@ -1628,6 +1812,8 @@ class GitWorkspaceServiceImpl implements GitWorkspaceService { } class VerifiedGitRuntime { + readonly #abortContext = new AsyncLocalStorage(); + constructor(private readonly input: VerifiedGitRuntimeInput) { if ( !isAbsolute(input.executablePath) || @@ -1661,12 +1847,27 @@ class VerifiedGitRuntime { return this.verifyOnce(); } + runWithAbortSignal( + abortSignal: AbortSignal | undefined, + operation: () => Promise, + ): Promise { + abortSignal?.throwIfAborted(); + if (!abortSignal) return operation(); + return this.#abortContext.run(abortSignal, async () => { + const result = await operation(); + abortSignal.throwIfAborted(); + return result; + }); + } + async run( args: readonly string[], homePath?: string, extraEnv?: NodeJS.ProcessEnv, acceptedExitCodes: readonly number[] = [], ): Promise { + const abortSignal = this.#abortContext.getStore(); + abortSignal?.throwIfAborted(); const runtime = await this.verify(); const hooksPath = homePath ? join(homePath, 'empty-hooks') : dirname(runtime.executablePath); if (homePath) { @@ -1686,6 +1887,7 @@ class VerifiedGitRuntime { encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER_BYTES, timeout: GIT_TIMEOUT_MS, + ...(abortSignal ? { signal: abortSignal } : {}), windowsHide: true, }, ); @@ -1701,6 +1903,8 @@ class VerifiedGitRuntime { homePath?: string, extraEnv?: NodeJS.ProcessEnv, ): Promise { + const abortSignal = this.#abortContext.getStore(); + abortSignal?.throwIfAborted(); const runtime = await this.verify(); const hooksPath = homePath ? join(homePath, 'empty-hooks') : dirname(runtime.executablePath); if (homePath) { @@ -1717,6 +1921,7 @@ class VerifiedGitRuntime { encoding: 'buffer', maxBuffer: GIT_MAX_BUFFER_BYTES, timeout: GIT_TIMEOUT_MS, + ...(abortSignal ? { signal: abortSignal } : {}), windowsHide: true, }, ); @@ -1741,6 +1946,8 @@ class VerifiedGitRuntime { repositoryPath: string, homePath: string, ): Promise { + const abortSignal = this.#abortContext.getStore(); + abortSignal?.throwIfAborted(); const packRuntime = await this.verify(); const hooksPath = join(homePath, 'empty-hooks'); await mkdir(hooksPath, { recursive: true }); @@ -1752,9 +1959,11 @@ class VerifiedGitRuntime { cwd: homePath, env: isolatedGitEnvironment(this.input, homePath), stdio: ['pipe', 'pipe', 'pipe'], + ...(abortSignal ? { signal: abortSignal } : {}), windowsHide: true, }, ); + const packExit = waitForChildExit(pack); let indexRuntime: Awaited>; try { // Each Git process is an independent trust decision. Re-check the @@ -1763,7 +1972,7 @@ class VerifiedGitRuntime { indexRuntime = await this.verify(); } catch (error) { pack.kill('SIGKILL'); - await waitForChildExit(pack).catch(() => undefined); + await packExit.catch(() => undefined); throw error; } const index = spawn( @@ -1773,12 +1982,15 @@ class VerifiedGitRuntime { cwd: homePath, env: isolatedGitEnvironment(this.input, homePath), stdio: ['pipe', 'ignore', 'pipe'], + ...(abortSignal ? { signal: abortSignal } : {}), windowsHide: true, }, ); + const indexExit = waitForChildExit(index); if (!pack.stdin || !pack.stdout || !index.stdin) { pack.kill('SIGKILL'); index.kill('SIGKILL'); + await Promise.all([packExit.catch(() => undefined), indexExit.catch(() => undefined)]); throw new Error('Git tree import did not create the required process pipes'); } const packStderr = collectBoundedStderr(pack.stderr); @@ -1793,10 +2005,7 @@ class VerifiedGitRuntime { index.kill('SIGKILL'); }, GIT_TIMEOUT_MS); try { - const [packCode, indexCode] = await Promise.all([ - waitForChildExit(pack), - waitForChildExit(index), - ]); + const [packCode, indexCode] = await Promise.all([packExit, indexExit]); if (packCode !== 0 || indexCode !== 0) { throw new Error( `Git tree import failed: pack=${packCode} index=${indexCode} ` + @@ -2097,6 +2306,24 @@ function assertSameSourceObservation( } } +function assertBindingSourceObservation( + binding: ManagedWorkspaceBinding, + current: SourceRepositoryInspection, +): void { + if ( + !samePath(binding.sourceRoot, current.sourceRoot) || + !samePath(binding.sourceGitCommonDir, current.gitCommonDir) || + binding.sourceHeadCommitOid !== current.headCommitOid || + binding.sourceTreeOid !== current.treeOid || + binding.objectFormat !== current.objectFormat + ) { + throw new GitWorkspaceServiceError( + 'managed_workspace_source_drifted', + 'Managed workspace source no longer matches its accepted Git boundary', + ); + } +} + async function readEpochArtifact(path: string): Promise { const value = await readJson(path); if (value === undefined) return undefined; @@ -2640,6 +2867,26 @@ function compactIdentity(value: string): string { return createHash('sha256').update(value).digest('hex').slice(0, 20); } +function assertManagedTrackedPath(path: string): string { + const normalized = path.replaceAll('\\', '/'); + if ( + !normalized || + normalized.includes('\0') || + normalized.includes(':') || + normalized.startsWith('/') || + normalized === '..' || + normalized.startsWith('../') || + normalized.includes('/../') || + posix.normalize(normalized) !== normalized + ) { + throw new GitWorkspaceServiceError( + 'managed_workspace_identity_conflict', + 'Managed workspace tracked file path is invalid', + ); + } + return normalized; +} + function managedHeadRef(workspaceId: string, workspaceEpochId: string): string { return `refs/maka/workspaces/${workspaceId}/epochs/${workspaceEpochId}/head`; } diff --git a/packages/storage/src/managed-baseline-receipt-authority-internal.ts b/packages/storage/src/managed-baseline-receipt-authority-internal.ts index e1c18ee4586..c35fa36a96a 100644 --- a/packages/storage/src/managed-baseline-receipt-authority-internal.ts +++ b/packages/storage/src/managed-baseline-receipt-authority-internal.ts @@ -25,9 +25,15 @@ import type { } from './git-workspace-service.js'; export interface ManagedBaselineReceiptAuthorityInternal { - issue(binding: ManagedWorkspaceBinding): Promise; - require(input: CreateManagedWorkspaceFromSourceInput): Promise; - verify(receipt: ManagedWorkspaceBaselineReceiptV1): Promise; + issue( + binding: ManagedWorkspaceBinding, + abortSignal?: AbortSignal, + ): Promise; + require( + input: CreateManagedWorkspaceFromSourceInput, + abortSignal?: AbortSignal, + ): Promise; + verify(receipt: ManagedWorkspaceBaselineReceiptV1, abortSignal?: AbortSignal): Promise; } const receiptAuthorities = new WeakMap< diff --git a/packages/storage/src/managed-dependency-environment.ts b/packages/storage/src/managed-dependency-environment.ts index 85065dde3c0..140825ee01c 100644 --- a/packages/storage/src/managed-dependency-environment.ts +++ b/packages/storage/src/managed-dependency-environment.ts @@ -208,6 +208,13 @@ interface PublishedManagedDependencyEnvironment { readonly dependencyRoot: string; } +interface InflightManagedDependencyPublication { + readonly controller: AbortController; + readonly task: Promise; + waiters: number; + settled: boolean; +} + interface DependencyReceiptAuthority { read(digest: string): ManagedDependencyEnvironmentReceiptV1 | undefined; list(): readonly ManagedDependencyEnvironmentReceiptV1[]; @@ -456,7 +463,7 @@ async function createManagedDependencyEnvironmentAuthorityForOwner( receiptAuthority.close(); throw error; } - const inflight = new Map>(); + const inflight = new Map(); const leaseCounts = new Map(); const pendingCounts = new Map(); let state: 'open' | 'draining' | 'closed' = 'open'; @@ -489,6 +496,7 @@ async function createManagedDependencyEnvironmentAuthorityForOwner( async acquire(identity, source) { const finishAcquisition = beginAcquisition(); try { + source.abortSignal?.throwIfAborted(); assertCanonicalIdentity(identity, source); if ( identity.packageManagerName !== input.producer.packageManagerName || @@ -508,23 +516,71 @@ async function createManagedDependencyEnvironmentAuthorityForOwner( pendingCounts.set(digest, (pendingCounts.get(digest) ?? 0) + 1); let artifact: PublishedManagedDependencyEnvironment; try { - let task = inflight.get(digest); - if (!task) { - task = openOrPublishEnvironment({ + let publication: InflightManagedDependencyPublication; + for (;;) { + const current = inflight.get(digest); + if (current?.controller.signal.aborted) { + // The last waiter revoked this publication, but its producer may + // still be draining. A later caller must not inherit that abort. + // Wait for owner cleanup, then re-observe the canonical slot. + await waitForPublication( + current.task.then( + () => undefined, + () => undefined, + ), + source.abortSignal, + ); + source.abortSignal?.throwIfAborted(); + continue; + } + if (current) { + publication = current; + break; + } + const controller = new AbortController(); + let pending!: InflightManagedDependencyPublication; + const task = openOrPublishEnvironment({ environmentsRoot, receiptAuthority, stagingRoot, identity, - source, + source: { ...source, abortSignal: controller.signal }, producer: input.producer, failpoint: input.failpoint, - }).finally(() => inflight.delete(digest)); - inflight.set(digest, task); + }).finally(() => { + pending.settled = true; + if (inflight.get(digest) === pending) inflight.delete(digest); + }); + pending = { + controller, + task, + waiters: 0, + settled: false, + }; + publication = pending; + inflight.set(digest, publication); + break; } - artifact = await task; + publication.waiters += 1; + try { + artifact = await waitForPublication(publication.task, source.abortSignal); + } finally { + publication.waiters -= 1; + if ( + publication.waiters === 0 && + !publication.settled && + !publication.controller.signal.aborted + ) { + publication.controller.abort( + new DOMException('Managed dependency acquisition was abandoned', 'AbortError'), + ); + } + } + source.abortSignal?.throwIfAborted(); const now = new Date(); await utimes(dirname(artifact.dependencyRoot), now, now); await input.failpoint?.('before_environment_lease'); + source.abortSignal?.throwIfAborted(); leaseCounts.set(digest, (leaseCounts.get(digest) ?? 0) + 1); } finally { decrementCount(pendingCounts, digest); @@ -564,6 +620,7 @@ async function createManagedDependencyEnvironmentAuthorityForOwner( state = 'draining'; closeTask = (async () => { await waitForAcquisitions(); + await Promise.allSettled([...inflight.values()].map(({ task }) => task)); let gcError: unknown; try { await gcTask; @@ -1229,6 +1286,33 @@ function decrementCount(counts: Map, key: string): void { else counts.delete(key); } +function waitForPublication(task: Promise, signal: AbortSignal | undefined): Promise { + if (!signal) return task; + signal.throwIfAborted(); + return new Promise((resolve, reject) => { + let settled = false; + const finish = (operation: () => void) => { + if (settled) return; + settled = true; + signal.removeEventListener('abort', onAbort); + operation(); + }; + const onAbort = () => + finish(() => + reject( + signal.reason instanceof Error + ? signal.reason + : new DOMException('Managed dependency acquisition was aborted', 'AbortError'), + ), + ); + signal.addEventListener('abort', onAbort, { once: true }); + task.then( + (value) => finish(() => resolve(value)), + (error: unknown) => finish(() => reject(error)), + ); + }); +} + async function syncDirectory(path: string): Promise { const handle = await open(path, 'r'); try { diff --git a/packages/storage/src/managed-workspace-execution-authority-internal.ts b/packages/storage/src/managed-workspace-execution-authority-internal.ts index 19676b9cde1..d0e4f3d967a 100644 --- a/packages/storage/src/managed-workspace-execution-authority-internal.ts +++ b/packages/storage/src/managed-workspace-execution-authority-internal.ts @@ -61,9 +61,10 @@ const states = new WeakMap< >(); export interface ManagedWorkspaceExecutionScopeStateInternal { - readonly provisioning: 'canonical_tree_only_v1'; + readonly provisioning: 'canonical_tree_only_v1' | 'dependency_environment_v1'; readonly workspaceEffect: 'none'; readonly cwd: string; + readonly dependencyRoot?: string; readonly binding: Readonly; readonly head: Readonly; } diff --git a/packages/storage/src/managed-workspace-owner.ts b/packages/storage/src/managed-workspace-owner.ts index 5ba63d72935..555ffa172dd 100644 --- a/packages/storage/src/managed-workspace-owner.ts +++ b/packages/storage/src/managed-workspace-owner.ts @@ -49,6 +49,14 @@ import { } from './managed-workspace-worker-bridge-internal.js'; import type { RuntimeWorkspaceVersionAuthorityStore } from '@maka/core/runtime-event-store'; import type { WorkspaceHeadRecordV1 } from '@maka/core/workspace-version-authority'; +import { + computeManagedDependencyEnvironmentIdentity, + createManagedDependencyEnvironmentAuthority, + type ManagedDependencyEnvironmentAuthority, + type ManagedDependencyEnvironmentFailpoint, + type ManagedDependencyEnvironmentProducer, + type ManagedDependencyEnvironmentProducerInput, +} from './managed-dependency-environment.js'; import { assertInteractiveRootOwner, authenticateInteractiveRootOwner, @@ -62,6 +70,8 @@ import { commitWorkspaceBaselineInternal, readWorkspaceHeadInternal, } from './workspace-version-authority-internal.js'; +import type { InteractiveExecutionStoresWriter } from './execution-stores.js'; +import { requireExecutionStoresWorkspaceAuthorityInternal } from './execution-stores-workspace-authority-internal.js'; // RuntimeEvent order is assigned by the SQLite authority spine. M0 therefore // uses a protocol-fixed logical timestamp and keeps unauthenticated wall-clock @@ -74,8 +84,12 @@ export type ManagedWorkspaceOwnerErrorCode = | 'managed_workspace_owner_closing' | 'managed_workspace_owner_reentrant_close' | 'managed_workspace_worker_unavailable' + | 'managed_dependency_producer_unavailable' + | 'managed_dependency_manifest_unsupported' + | 'managed_dependency_provision_failed' | 'managed_workspace_quarantined' - | 'managed_workspace_execution_handle_invalid'; + | 'managed_workspace_execution_handle_invalid' + | 'managed_workspace_execution_options_invalid'; export class ManagedWorkspaceOwnerError extends Error { constructor( @@ -93,14 +107,17 @@ export interface OpenManagedWorkspaceOwnerInput { readonly gitRuntime: VerifiedGitRuntimeInput; readonly failpoint?: (point: ManagedWorkspaceOwnerFailpoint) => void | Promise; readonly filesystemWorker?: ManagedWorkspaceFilesystemWorker; + readonly dependencyEnvironmentProducer?: ManagedDependencyEnvironmentProducer; } export type ManagedWorkspaceOwnerFailpoint = | GitWorkspaceServiceFailpoint + | ManagedDependencyEnvironmentFailpoint | 'after_initial_store_root_validation' | 'after_baseline_authority_commit' | 'after_post_commit_artifact_verification' - | 'after_execution_artifact_verification'; + | 'after_execution_artifact_verification' + | 'before_execution_scope_issue'; export type OpenManagedWorkspaceBaselineInput = CreateManagedWorkspaceFromSourceInput; @@ -110,15 +127,31 @@ export interface OpenManagedWorkspaceBaselineResult { readonly executionHandle: ManagedWorkspaceExecutionHandle; } +export interface ManagedWorkspaceExecutionOptions { + readonly provisioning?: 'canonical_tree_only_v1' | 'dependency_environment_v1'; + readonly abortSignal?: AbortSignal; +} + +export interface ManagedWorkspaceAdmissionOptions { + readonly abortSignal?: AbortSignal; +} + export interface ManagedWorkspaceOwner { readonly state: 'ready' | 'closing' | 'closed'; openManagedWorkspaceBaseline( store: RuntimeWorkspaceVersionAuthorityStore, input: OpenManagedWorkspaceBaselineInput, + options?: ManagedWorkspaceAdmissionOptions, + ): Promise; + openManagedWorkspaceBaselineFromExecutionStores( + stores: InteractiveExecutionStoresWriter, + input: OpenManagedWorkspaceBaselineInput, + options?: ManagedWorkspaceAdmissionOptions, ): Promise; withManagedWorkspaceExecution( handle: ManagedWorkspaceExecutionHandle, operation: (scope: ManagedWorkspaceExecutionScope) => Promise, + options?: ManagedWorkspaceExecutionOptions, ): Promise; executeReadOnlyFilesystemOperation( scope: ManagedWorkspaceExecutionScope, @@ -129,6 +162,8 @@ export interface ManagedWorkspaceOwner { } export type { + ManagedDependencyEnvironmentProducer, + ManagedDependencyEnvironmentProducerInput, ManagedWorkspaceExecutionHandle, ManagedWorkspaceExecutionScope, ManagedWorkspaceFilesystemWorker, @@ -166,12 +201,21 @@ export async function openManagedWorkspaceOwner( // flight. Revalidate after the lease-bound operation so a stale lifecycle // owner is never published as ready. await assertInteractiveRootOwner(rootOwner); + const dependencyAuthority = input.dependencyEnvironmentProducer + ? await createManagedDependencyEnvironmentAuthority({ + storageRoot: rootOwner.capability.canonicalPath, + producer: input.dependencyEnvironmentProducer, + ...(input.failpoint ? { failpoint: (point) => input.failpoint?.(point) } : {}), + }) + : undefined; const owner = new ManagedWorkspaceOwnerImpl( rootOwner, service, requireManagedBaselineReceiptAuthorityInternal(service), input.failpoint, input.filesystemWorker, + input.dependencyEnvironmentProducer, + dependencyAuthority, ); owners.set(rootOwner, owner); return owner; @@ -202,6 +246,8 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { private readonly receiptAuthority: ManagedBaselineReceiptAuthorityInternal, private readonly failpoint?: (point: ManagedWorkspaceOwnerFailpoint) => void | Promise, filesystemWorker?: ManagedWorkspaceFilesystemWorker, + private readonly dependencyProducer?: ManagedDependencyEnvironmentProducer, + private readonly dependencyAuthority?: ManagedDependencyEnvironmentAuthority, ) { // Capture the identity guard while the lease is active. Unlike a fresh // admission check, this guard remains valid for an already-admitted @@ -223,8 +269,10 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { async openManagedWorkspaceBaseline( store: RuntimeWorkspaceVersionAuthorityStore, input: OpenManagedWorkspaceBaselineInput, + options: ManagedWorkspaceAdmissionOptions = {}, ): Promise { return this.#run(async () => { + options.abortSignal?.throwIfAborted(); await assertWorkspaceBaselineAuthorityStoreRootInternal( store, this.rootOwner.capability.canonicalPath, @@ -236,11 +284,17 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { input.workspaceId, input.workspaceEpochId, ); - const receipt = existingHead ? await this.receiptAuthority.require(input) : undefined; + const receipt = existingHead + ? await this.receiptAuthority.require(input, options.abortSignal) + : undefined; const binding = receipt - ? await this.#openReadyBinding(receipt.binding) - : await this.#requireReady(await this.service.createManagedWorkspaceFromSource(input)); - const durableReceipt = receipt ?? (await this.receiptAuthority.issue(binding)); + ? await this.#openReadyBinding(receipt.binding, options.abortSignal) + : await this.#requireReady( + await this.service.createManagedWorkspaceFromSource(input, options), + options.abortSignal, + ); + const durableReceipt = + receipt ?? (await this.receiptAuthority.issue(binding, options.abortSignal)); if ( existingHead && (existingHead.workspaceVersionId !== durableReceipt.workspaceVersionId || @@ -291,7 +345,7 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { // Canonical acceptance never makes a missing Git artifact acceptable. // Reverify after the SQLite transaction so post-accept artifact loss is // reported fail-closed instead of returning a usable workspace head. - await this.receiptAuthority.verify(durableReceipt); + await this.receiptAuthority.verify(durableReceipt, options.abortSignal); await this.failpoint?.('after_post_commit_artifact_verification'); // The root marker is mutable host state and is not covered by receipt // verification. Revalidate its identity at the final return gate without @@ -313,11 +367,25 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { }); } + openManagedWorkspaceBaselineFromExecutionStores( + stores: InteractiveExecutionStoresWriter, + input: OpenManagedWorkspaceBaselineInput, + options: ManagedWorkspaceAdmissionOptions = {}, + ): Promise { + return this.openManagedWorkspaceBaseline( + requireExecutionStoresWorkspaceAuthorityInternal(stores), + input, + options, + ); + } + async withManagedWorkspaceExecution( handle: ManagedWorkspaceExecutionHandle, operation: (scope: ManagedWorkspaceExecutionScope) => Promise, + options: ManagedWorkspaceExecutionOptions = {}, ): Promise { return this.#run(async () => { + options.abortSignal?.throwIfAborted(); let accepted; try { accepted = requireManagedWorkspaceExecutionHandleInternal( @@ -336,50 +404,71 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { // can stop after a real, completed artifact verification. The ordinary // production path performs only the final proof below. if (this.failpoint) { - await this.#verifyExecutionArtifactOrQuarantine(accepted.binding, accepted.receipt); + await this.#verifyExecutionArtifactOrQuarantine( + accepted.binding, + accepted.receipt, + options.abortSignal, + ); await this.failpoint('after_execution_artifact_verification'); } - // The root write lease excludes cooperating Maka writers across this - // proof bundle. Verify the durable Git artifact first, then make the - // immutable workspace head the final durable reread before scope issue. - await this.#assertCurrentRootIdentity(); - await this.#verifyExecutionArtifactOrQuarantine(accepted.binding, accepted.receipt); - const currentHead = await readWorkspaceHeadInternal( - accepted.store, - accepted.binding.workspaceId, - accepted.binding.workspaceEpochId, - ); - // Rebind only after the head read. This catches a database pathname - // detach at the admission boundary without putting a mutable DB guard - // ahead of the durable head evidence it protects. - await assertWorkspaceBaselineAuthorityStoreRootInternal( - accepted.store, - this.rootOwner.capability.canonicalPath, - ); - bindWorkspaceBaselineAuthorityStoreRootInternal( - accepted.store, - this.rootOwner.capability.rootId, - ); - if (!currentHead || !sameWorkspaceHead(currentHead, accepted.head)) { - throw new ManagedWorkspaceOwnerError( - 'managed_workspace_owner_unavailable', - 'Managed workspace execution handle no longer matches the canonical workspace head', - ); - } - assertExecutionCrossPlaneIdentity(accepted.binding, accepted.receipt, currentHead); const binding = accepted.binding; - const scope = issueManagedWorkspaceExecutionScopeInternal(this.#executionOwnerToken, { - provisioning: 'canonical_tree_only_v1', - workspaceEffect: 'none', - cwd: binding.worktreePath, - binding: Object.freeze({ ...binding }), - head: freezeWorkspaceHead(currentHead), - }); + const provisioning = requireExecutionProvisioning(options); + const dependencyLease = + provisioning === 'dependency_environment_v1' + ? await this.#acquireDependencyEnvironment(binding, options.abortSignal) + : undefined; try { - return await this.#executionContext.run(this.#executionOwnerToken, () => operation(scope)); + options.abortSignal?.throwIfAborted(); + // Provisioning may take minutes. External editors do not cooperate + // with Maka's root lease, so the durable Git artifact and workspace + // head are revalidated only after provisioning and immediately before + // the scoped capability is issued. + await this.#assertCurrentRootIdentity(); + await this.#verifyExecutionArtifactOrQuarantine( + accepted.binding, + accepted.receipt, + options.abortSignal, + ); + const currentHead = await readWorkspaceHeadInternal( + accepted.store, + accepted.binding.workspaceId, + accepted.binding.workspaceEpochId, + ); + await assertWorkspaceBaselineAuthorityStoreRootInternal( + accepted.store, + this.rootOwner.capability.canonicalPath, + ); + bindWorkspaceBaselineAuthorityStoreRootInternal( + accepted.store, + this.rootOwner.capability.rootId, + ); + if (!currentHead || !sameWorkspaceHead(currentHead, accepted.head)) { + throw new ManagedWorkspaceOwnerError( + 'managed_workspace_owner_unavailable', + 'Managed workspace execution handle no longer matches the canonical workspace head', + ); + } + assertExecutionCrossPlaneIdentity(accepted.binding, accepted.receipt, currentHead); + await this.failpoint?.('before_execution_scope_issue'); + options.abortSignal?.throwIfAborted(); + const scope = issueManagedWorkspaceExecutionScopeInternal(this.#executionOwnerToken, { + provisioning, + workspaceEffect: 'none', + cwd: binding.worktreePath, + ...(dependencyLease ? { dependencyRoot: dependencyLease.dependencyRoot } : {}), + binding: Object.freeze({ ...binding }), + head: freezeWorkspaceHead(currentHead), + }); + try { + return await this.#executionContext.run(this.#executionOwnerToken, () => + operation(scope), + ); + } finally { + revokeManagedWorkspaceExecutionScopeInternal(this.#executionOwnerToken, scope); + } } finally { - revokeManagedWorkspaceExecutionScopeInternal(this.#executionOwnerToken, scope); + await dependencyLease?.release(); } }); } @@ -410,6 +499,7 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { this.#closeTask ??= (async () => { this.#state = 'closing'; await this.#waitForDrain(); + await this.dependencyAuthority?.close(); this.#state = 'closed'; })(); return this.#closeTask; @@ -443,8 +533,11 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { return new Promise((resolve) => this.#drainWaiters.add(resolve)); } - async #requireReady(binding: ManagedWorkspaceBinding): Promise { - const inspection = await this.service.inspectManagedWorkspace(binding); + async #requireReady( + binding: ManagedWorkspaceBinding, + abortSignal?: AbortSignal, + ): Promise { + const inspection = await this.service.inspectManagedWorkspace(binding, { abortSignal }); if (inspection.state === 'ready') return binding; const quarantine = await this.service.quarantineManagedWorkspace( binding, @@ -456,9 +549,15 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { ); } - async #openReadyBinding(binding: ManagedWorkspaceBinding): Promise { + async #openReadyBinding( + binding: ManagedWorkspaceBinding, + abortSignal?: AbortSignal, + ): Promise { try { - return await this.#requireReady(await this.service.openManagedWorkspaceFromBinding(binding)); + return await this.#requireReady( + await this.service.openManagedWorkspaceFromBinding(binding, { abortSignal }), + abortSignal, + ); } catch (error) { if ( !(error instanceof GitWorkspaceServiceError) || @@ -481,9 +580,10 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { async #verifyExecutionArtifactOrQuarantine( binding: ManagedWorkspaceBinding, receipt: ManagedWorkspaceBaselineReceiptV1, + abortSignal?: AbortSignal, ): Promise { try { - await this.receiptAuthority.verify(receipt); + await this.receiptAuthority.verify(receipt, abortSignal); } catch (error) { if ( !(error instanceof GitWorkspaceServiceError) || @@ -502,6 +602,126 @@ class ManagedWorkspaceOwnerImpl implements ManagedWorkspaceOwner { ); } } + + async #acquireDependencyEnvironment( + binding: ManagedWorkspaceBinding, + abortSignal: AbortSignal | undefined, + ) { + if (!this.dependencyProducer || !this.dependencyAuthority) { + throw new ManagedWorkspaceOwnerError( + 'managed_dependency_producer_unavailable', + 'Managed dependency provisioning requires an explicitly configured producer', + ); + } + try { + if ( + await this.service.hasManagedWorkspaceBaselinePath(binding, 'node_modules', { + ...(abortSignal ? { abortSignal } : {}), + }) + ) { + throw new ManagedWorkspaceOwnerError( + 'managed_dependency_manifest_unsupported', + 'Managed dependency provisioning requires node_modules to be absent from the canonical tree', + ); + } + const [manifestBytes, lockfileBytes] = await Promise.all([ + this.service.readManagedWorkspaceBaselineFile(binding, 'package.json', { + ...(abortSignal ? { abortSignal } : {}), + }), + this.service.readManagedWorkspaceBaselineFile(binding, 'package-lock.json', { + ...(abortSignal ? { abortSignal } : {}), + }), + ]); + const packageManager = readExactPackageManager(manifestBytes); + if ( + packageManager.name !== this.dependencyProducer.packageManagerName || + packageManager.version !== this.dependencyProducer.packageManagerVersion + ) { + throw new ManagedWorkspaceOwnerError( + 'managed_dependency_manifest_unsupported', + 'Managed dependency producer does not match the exact packageManager declaration', + ); + } + const identity = computeManagedDependencyEnvironmentIdentity({ + manifestPath: 'package.json', + manifestBytes, + lockfilePath: 'package-lock.json', + lockfileBytes, + packageManagerName: packageManager.name, + packageManagerVersion: packageManager.version, + nodeVersion: this.dependencyProducer.nodeRuntime.version, + nodeAbi: this.dependencyProducer.nodeRuntime.abi, + platform: this.dependencyProducer.nodeRuntime.platform, + arch: this.dependencyProducer.nodeRuntime.arch, + producerRuntimeIdentitySha256: this.dependencyProducer.capability.runtimeIdentitySha256, + producerPolicyIdentitySha256: this.dependencyProducer.capability.policyIdentitySha256, + policyVersion: 'managed_dependency_environment_v1', + }); + return await this.dependencyAuthority.acquire(identity, { + manifestBytes, + lockfileBytes, + ...(abortSignal ? { abortSignal } : {}), + }); + } catch (error) { + if (error instanceof ManagedWorkspaceOwnerError) throw error; + if (isAbortError(error)) throw error; + if (abortSignal?.aborted) { + throw abortSignal.reason instanceof Error + ? abortSignal.reason + : new DOMException('Managed dependency provisioning was aborted', 'AbortError'); + } + throw new ManagedWorkspaceOwnerError( + 'managed_dependency_provision_failed', + 'Unable to provision the managed dependency environment', + { cause: error }, + ); + } + } +} + +function isAbortError(error: unknown): error is Error { + return error instanceof Error && error.name === 'AbortError'; +} + +function requireExecutionProvisioning( + options: ManagedWorkspaceExecutionOptions, +): NonNullable { + const provisioning = options.provisioning ?? 'canonical_tree_only_v1'; + if (provisioning !== 'canonical_tree_only_v1' && provisioning !== 'dependency_environment_v1') { + throw new ManagedWorkspaceOwnerError( + 'managed_workspace_execution_options_invalid', + 'Managed workspace execution provisioning profile is invalid', + ); + } + return provisioning; +} + +function readExactPackageManager(bytes: Uint8Array): { + readonly name: 'npm'; + readonly version: string; +} { + let value: unknown; + try { + value = JSON.parse(Buffer.from(bytes).toString('utf8')); + } catch (error) { + throw new ManagedWorkspaceOwnerError( + 'managed_dependency_manifest_unsupported', + 'Managed dependency package.json is not valid JSON', + { cause: error }, + ); + } + const packageManager = + value && typeof value === 'object' + ? (value as { packageManager?: unknown }).packageManager + : undefined; + const match = typeof packageManager === 'string' ? /^npm@([^\s]+)$/u.exec(packageManager) : null; + if (!match?.[1]) { + throw new ManagedWorkspaceOwnerError( + 'managed_dependency_manifest_unsupported', + 'Managed dependency v1 requires an exact npm packageManager declaration', + ); + } + return Object.freeze({ name: 'npm', version: match[1] }); } function sameWorkspaceHead(left: WorkspaceHeadRecordV1, right: WorkspaceHeadRecordV1): boolean { diff --git a/packages/storage/src/managed-workspace-worker-bridge-internal.ts b/packages/storage/src/managed-workspace-worker-bridge-internal.ts index bf07b53e295..cdcd8173c19 100644 --- a/packages/storage/src/managed-workspace-worker-bridge-internal.ts +++ b/packages/storage/src/managed-workspace-worker-bridge-internal.ts @@ -22,6 +22,7 @@ import { type ExecutionBoundary, } from '@maka/core/sandbox-boundary'; import { createReadOnlyPermissionProfile } from '@maka/core/permission-profile'; +import { isAbsolute, join, normalize, parse, relative, sep } from 'node:path'; import { requireManagedWorkspaceExecutionScopeInternal, type ManagedWorkspaceExecutionScope, @@ -120,23 +121,163 @@ export function createManagedWorkspaceWorkerBridgeInternal( ); } const state = requireManagedWorkspaceExecutionScopeInternal(ownerToken, scope); - if (state.workspaceEffect !== 'none' || state.provisioning !== 'canonical_tree_only_v1') { + const hasDependencyRoot = typeof state.dependencyRoot === 'string'; + if ( + state.workspaceEffect !== 'none' || + (state.provisioning === 'dependency_environment_v1') !== hasDependencyRoot + ) { throw new ManagedWorkspaceWorkerBridgeError( 'managed_workspace_operation_denied', 'Managed workspace execution scope does not permit filesystem mutation', ); } - return await worker.execute({ - operation, + const routedOperation = routeDependencyOperation(operation, state.cwd, state.dependencyRoot); + const baseProfile = createReadOnlyPermissionProfile(); + const profile = state.dependencyRoot + ? { + ...baseProfile, + name: 'custom' as const, + fileSystem: { + ...baseProfile.fileSystem, + entries: [ + ...baseProfile.fileSystem.entries, + { + kind: 'path' as const, + access: 'read' as const, + path: state.dependencyRoot, + match: 'subtree' as const, + }, + ], + }, + } + : baseProfile; + const result = await worker.execute({ + operation: routedOperation, cwd: state.cwd, - executionBoundary: createManagedExecutionBoundary(createReadOnlyPermissionProfile(), 0), + executionBoundary: createManagedExecutionBoundary(profile, 0), ...(abortSignal ? { abortSignal } : {}), }); + return remapDependencyResult(result, state.dependencyRoot, routedOperation); }, }; return Object.freeze(bridge); } +function routeDependencyOperation( + operation: ManagedWorkspaceReadOnlyOperation, + cwd: string, + dependencyRoot: string | undefined, +): ManagedWorkspaceReadOnlyOperation { + if (!dependencyRoot) return operation; + let segments: string[]; + if (isAbsolute(operation.path)) { + assertCanonicalPathSegments(operation.path.slice(parse(operation.path).root.length)); + const logicalDependencyRoot = join(cwd, 'node_modules'); + const suffix = relative(logicalDependencyRoot, operation.path); + if (suffix === '..' || suffix.startsWith(`..${sep}`) || isAbsolute(suffix)) return operation; + segments = suffix === '' ? [] : suffix.split(/[\\/]/u); + } else { + const portableSegments = operation.path.replaceAll('\\', '/').split('/'); + assertCanonicalSegments(portableSegments); + if (!isNodeModulesSegment(portableSegments[0])) return operation; + segments = portableSegments.slice(1); + } + assertCanonicalSegments(segments, { allowEmpty: true }); + const routedPath = join(dependencyRoot, ...segments); + if (!isPathWithin(routedPath, dependencyRoot)) { + throw new ManagedWorkspaceWorkerBridgeError( + 'managed_workspace_operation_denied', + 'Managed dependency path escapes its environment', + ); + } + return Object.freeze({ ...operation, path: routedPath }); +} + +function assertCanonicalPathSegments(path: string): void { + assertCanonicalSegments(path.replaceAll('\\', '/').split('/'), { allowEmpty: true }); +} + +function assertCanonicalSegments( + segments: readonly string[], + options: { readonly allowEmpty?: boolean } = {}, +): void { + if ( + (!options.allowEmpty && segments.length === 0) || + segments.some( + (segment) => + segment === '..' || + segment === '.' || + segment === '' || + segment.includes('\0') || + segment.includes(':'), + ) + ) { + throw new ManagedWorkspaceWorkerBridgeError( + 'managed_workspace_operation_denied', + 'Managed dependency path is invalid', + ); + } +} + +function isNodeModulesSegment(segment: string | undefined): boolean { + if (segment === undefined) return false; + return process.platform === 'win32' + ? segment.toLowerCase() === 'node_modules' + : segment === 'node_modules'; +} + +function remapDependencyResult( + result: ManagedWorkspaceReadOnlyResult, + dependencyRoot: string | undefined, + routedOperation: ManagedWorkspaceReadOnlyOperation, +): ManagedWorkspaceReadOnlyResult { + if (!dependencyRoot) return result; + if (result.kind === 'grep') { + return Object.freeze({ + kind: 'grep', + matches: result.matches.map((match) => remapDependencyPath(match, dependencyRoot)), + }); + } + if (result.kind === 'glob') { + return Object.freeze({ + kind: 'glob', + files: result.files.map((path) => + remapDependencyGlobPath(path, dependencyRoot, routedOperation), + ), + }); + } + return result; +} + +function remapDependencyGlobPath( + path: string, + dependencyRoot: string, + routedOperation: ManagedWorkspaceReadOnlyOperation, +): string { + const absolute = remapDependencyPath(path, dependencyRoot); + if (absolute !== path || isAbsolute(path) || routedOperation.kind !== 'glob') return absolute; + if (!isPathWithin(routedOperation.path, dependencyRoot)) return path; + + const relativeOperationRoot = relative(dependencyRoot, routedOperation.path); + const operationSegments = + relativeOperationRoot === '' ? [] : relativeOperationRoot.split(/[\\/]/u); + const resultSegments = path.replaceAll('\\', '/').split('/'); + assertCanonicalSegments(operationSegments, { allowEmpty: true }); + assertCanonicalSegments(resultSegments); + return ['node_modules', ...operationSegments, ...resultSegments].join('/'); +} + +function remapDependencyPath(path: string, dependencyRoot: string): string { + const prefix = normalize(dependencyRoot); + if (path !== prefix && !path.startsWith(`${prefix}${sep}`)) return path; + return `node_modules${path.slice(prefix.length)}`.replaceAll('\\', '/'); +} + +function isPathWithin(candidate: string, root: string): boolean { + const path = relative(normalize(root), normalize(candidate)); + return path === '' || (!path.startsWith('..') && !isAbsolute(path)); +} + function isReadOnlyOperation(input: unknown): input is ManagedWorkspaceReadOnlyOperation { if (!input || typeof input !== 'object') return false; const kind = (input as { kind?: unknown }).kind; diff --git a/packages/storage/src/sqlite-runtime-store.ts b/packages/storage/src/sqlite-runtime-store.ts index 6a406734eeb..e20e6971232 100644 --- a/packages/storage/src/sqlite-runtime-store.ts +++ b/packages/storage/src/sqlite-runtime-store.ts @@ -90,7 +90,10 @@ import { RUNTIME_WORKSPACE_VERSION_AUTHORITY_CAPABILITY_VERSION, SQLITE_RUNTIME_SCHEMA_VERSION, } from './sqlite-runtime-schema.js'; -import { registerWorkspaceBaselineAuthorityWriterInternal } from './workspace-version-authority-internal.js'; +import { + registerWorkspaceBaselineAuthorityWriterInternal, + WorkspaceStorageRootAdoptionRequiredError, +} from './workspace-version-authority-internal.js'; import type { ConversationCopyRuntimeEventBatch, ImmutableSteeringMessageProof, @@ -1251,7 +1254,7 @@ export class SqliteRuntimeStore return; } if (this.#databaseHasLogicalStateBeforeRootBinding()) { - throw new Error('Unbound operational data require explicit storage-root adoption'); + throw new WorkspaceStorageRootAdoptionRequiredError(); } this.db .prepare(` @@ -1280,10 +1283,11 @@ export class SqliteRuntimeStore } #databaseHasLogicalStateBeforeRootBinding(): boolean { - const metadataTables = new Set([ + const schemaMetadataTables = new Set([ 'operational_schema_migrations', 'runtime_capabilities', 'runtime_storage_root_binding', + 'session_metadata_schema', ]); const tables = this.db .prepare(` @@ -1294,7 +1298,36 @@ export class SqliteRuntimeStore `) .all() as Array<{ name: string }>; for (const { name } of tables) { - if (metadataTables.has(name)) continue; + if (schemaMetadataTables.has(name)) continue; + // These authority rows are installed by schema bootstrap before the + // storage root is bound. Only their exact empty-state values are + // metadata; a non-zero revision/generation is already logical data and + // must require the explicit whole-root adoption flow. + if (name === 'automation_authority_state' || name === 'usage_pricing_authority') { + if ( + !this.db + .prepare(`SELECT 1 FROM "${name}" WHERE singleton <> 1 OR revision <> 0 LIMIT 1`) + .get() + ) { + continue; + } + return true; + } + if (name === 'session_catalog_state') { + if ( + !this.db + .prepare(` + SELECT 1 + FROM session_catalog_state + WHERE scope <> 'catalog' OR generation <> 0 OR pending_writes <> 0 + LIMIT 1 + `) + .get() + ) { + continue; + } + return true; + } const quotedName = `"${name.replaceAll('"', '""')}"`; if (this.db.prepare(`SELECT 1 FROM ${quotedName} LIMIT 1`).get()) return true; } diff --git a/packages/storage/src/workspace-version-authority-internal.ts b/packages/storage/src/workspace-version-authority-internal.ts index ed77104ee14..2d3478c2482 100644 --- a/packages/storage/src/workspace-version-authority-internal.ts +++ b/packages/storage/src/workspace-version-authority-internal.ts @@ -46,6 +46,13 @@ interface WorkspaceBaselineAuthorityRegistration { boundRootId?: string; } +export class WorkspaceStorageRootAdoptionRequiredError extends Error { + constructor() { + super('Unbound operational data require explicit storage-root adoption'); + this.name = 'WorkspaceStorageRootAdoptionRequiredError'; + } +} + const workspaceBaselineAuthorityWriters = new WeakMap< object, WorkspaceBaselineAuthorityRegistration