blob: a4a7c731533b6a9c038d4cb1a57c78107f0a3abb [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { isAbsolute, join } from 'node:path';
import { pathToFileURL } from 'node:url';
const SESSION_NAME = 'Released State Root qualification';
const MESSAGE_ID = 'released-state-root-message';
const TASK_TITLE = 'Released State Root durable task';
const ACCESS_PRINCIPAL_ID = 'released-state-root-qualification-client';
const FUTURE_FIRE_DELAY_MS = 24 * 60 * 60 * 1_000;
const input = parseFixtureArgs(process.argv.slice(2));
const storageRootAuthority = await loadInstalled(
input.packageRoot,
'node_modules/@maka/storage/dist/root-authority.js',
);
const capability = await storageRootAuthority.resolveStorageRoot({
path: input.rootPath,
kind: 'interactive',
});
const owner = await storageRootAuthority.tryAcquireInteractiveRootOwner(capability);
if (!owner) {
writeResult({ kind: 'writer_busy' });
process.exit(0);
}
try {
if (input.action === 'seed') {
writeResult(await seedFixture(input.packageRoot, input.rootPath, owner, capability.rootId));
} else if (input.action === 'inspect') {
writeResult(await inspectFixture(input.packageRoot, input.rootPath, owner, capability.rootId));
} else {
writeResult(await probeWriterFence(input.targetPackageRoot, input.rootPath, capability.rootId));
}
} finally {
await owner.close();
}
async function seedFixture(packageRoot, rootPath, rootOwner, rootId) {
const sessionsModule = await loadInstalled(
packageRoot,
'node_modules/@maka/storage/dist/session-store.js',
);
const scheduledTasksModule = await loadInstalled(
packageRoot,
'node_modules/@maka/storage/dist/scheduled-task-store.js',
);
const sessions = sessionsModule.createSessionStore(rootPath);
const scheduledTasks = await scheduledTasksModule.openInteractiveScheduledTaskStoreForWrite(
rootOwner.lease,
);
try {
const session = await sessions.create({
cwd: rootPath,
backend: 'ai-sdk',
llmConnectionSlug: 'released-state-root-qualification',
model: 'qualification-model',
permissionMode: 'ask',
name: SESSION_NAME,
labels: ['release-qualification'],
});
await sessions.appendMessage(session.id, {
type: 'user',
id: MESSAGE_ID,
turnId: 'released-state-root-turn',
ts: Date.now(),
text: 'Preserve this released State Root fact.',
});
const now = Date.now();
const task = await scheduledTasks.create(
{
title: TASK_TITLE,
intentBody: '',
schedule: { kind: 'once', runAt: now + FUTURE_FIRE_DELAY_MS },
effect: { kind: 'notify', channel: 'local' },
createdBy: { kind: 'user' },
},
now,
);
return {
kind: 'facts',
rootId,
session: {
id: session.id,
name: session.name,
message: {
id: MESSAGE_ID,
type: 'user',
text: 'Preserve this released State Root fact.',
},
},
scheduledTask: {
id: task.id,
title: task.title,
status: task.status,
schedule: task.schedule,
effect: task.effect,
},
access: await seedAccessCredential(packageRoot, rootOwner.controlDirectory),
};
} finally {
scheduledTasks.close();
await sessions.close?.();
}
}
// The access file lives in the account-local control namespace rather than the
// State Root, and the Host opens it before the Kernel starts. A credential
// issued by the released build is therefore the one durable record that decides
// whether the current build can start at all.
async function seedAccessCredential(packageRoot, controlDirectory) {
const accessAuthority = await loadInstalled(
packageRoot,
'node_modules/@maka/runtime-host/dist/server/access-authority.js',
);
const protocol = await loadInstalled(
packageRoot,
'node_modules/@maka/runtime-host/dist/protocol/index.js',
);
const authority = await accessAuthority.openRuntimeHostAccessAuthority(controlDirectory);
try {
// Ask the released build what it is able to grant instead of naming
// operations here. A fixture that hard-codes today's keys stops covering
// the next rename the moment that rename lands.
const issued = await accessAuthority.issueAccessCredential(authority, {
principalKind: 'remote_owner',
principalId: ACCESS_PRINCIPAL_ID,
operationGrants: [...protocol.REMOTE_OWNER_OPERATION_GRANTS],
canPublishClientCapabilities: false,
canUseHostPaths: false,
});
if (!issued.ok) {
throw new Error(
`The released access credential could not be issued: ${issued.error.message}`,
);
}
return { credentialId: issued.result.credentialId, principalId: issued.result.principalId };
} finally {
await authority.close();
}
}
// Grants are deliberately absent from the facts: a rename is supposed to move a
// stored grant to its successor, so comparing the grant list across builds
// would report a correct migration as a lost fact. The credential identity is
// what must survive, and the Host refusing to open the file is what fails.
async function inspectAccessCredential(packageRoot, controlDirectory) {
const store = await loadInstalled(
packageRoot,
'node_modules/@maka/runtime-host/dist/server/access-credential-store.js',
);
const file = await store.readAccessCredentialFile(join(controlDirectory, store.ACCESS_FILE_NAME));
const credential = file.credentials.find(
(candidate) => candidate.principalId === ACCESS_PRINCIPAL_ID,
);
if (!credential) throw new Error('The released access credential fact is missing');
// This fixture runs on both builds, which do not share a field name here: the
// record is `grants` on builds that separate it from the derived authority and
// `operationGrants` on those that did not. The published JSON key never moved.
const grants = credential.grants ?? credential.operationGrants;
if (!grants.includes('host.status')) {
throw new Error('The released access credential lost its liveness grant');
}
// Asked of whichever build is reading, so it is deliberately not a shared
// fact: a grant the reader can neither serve nor account for means a rename
// shipped without its migration entry. Builds predating the check skip it.
if (typeof store.unresolvedPersistedGrants === 'function') {
const unresolved = store.unresolvedPersistedGrants(file);
if (unresolved.length > 0) {
throw new Error(
`The released access credential carries grants this build cannot account for: ${unresolved.join(', ')}`,
);
}
}
return { credentialId: credential.credentialId, principalId: credential.principalId };
}
async function inspectFixture(packageRoot, rootPath, rootOwner, rootId) {
const sessionsModule = await loadInstalled(
packageRoot,
'node_modules/@maka/storage/dist/session-store.js',
);
const scheduledTasksModule = await loadInstalled(
packageRoot,
'node_modules/@maka/storage/dist/scheduled-task-store.js',
);
const sessions = sessionsModule.createSessionStore(rootPath);
const scheduledTasks = await scheduledTasksModule.openInteractiveScheduledTaskStoreForWrite(
rootOwner.lease,
);
try {
const session = (await sessions.listHeaders()).find(
(candidate) => candidate.name === SESSION_NAME,
);
if (!session) throw new Error('The released Session fact is missing');
const messages = await sessions.readMessages(session.id);
const message = messages.find((candidate) => candidate.id === MESSAGE_ID);
if (!message) throw new Error('The released Session message is missing');
const task = (await scheduledTasks.list()).find((candidate) => candidate.title === TASK_TITLE);
if (!task) throw new Error('The released Scheduled Task fact is missing');
return {
kind: 'facts',
rootId,
session: {
id: session.id,
name: session.name,
message: { id: message.id, type: message.type, text: message.text },
},
scheduledTask: {
id: task.id,
title: task.title,
status: task.status,
schedule: task.schedule,
effect: task.effect,
},
access: await inspectAccessCredential(packageRoot, rootOwner.controlDirectory),
};
} finally {
scheduledTasks.close();
await sessions.close?.();
}
}
function parseFixtureArgs(argv) {
const values = new Map();
for (let index = 0; index < argv.length; index += 2) {
const name = argv[index];
const value = argv[index + 1];
if (!name?.startsWith('--') || value === undefined) {
throw new Error('Fixture arguments must be --name value pairs');
}
if (values.has(name)) throw new Error(`Duplicate fixture argument: ${name}`);
values.set(name, value);
}
const action = values.get('--action');
const packageRoot = values.get('--package-root');
const rootPath = values.get('--root');
if (!['seed', 'inspect', 'fence'].includes(action)) {
throw new Error('Fixture action must be seed, inspect, or fence');
}
if (!packageRoot || !isAbsolute(packageRoot)) {
throw new Error('Fixture package root must be absolute');
}
if (!rootPath || !isAbsolute(rootPath)) {
throw new Error('Fixture State Root must be absolute');
}
const targetPackageRoot = values.get('--target-package-root');
if (action === 'fence' && (!targetPackageRoot || !isAbsolute(targetPackageRoot))) {
throw new Error('Fence fixture target package root must be absolute');
}
const expectedNames = action === 'fence' ? 4 : 3;
if (values.size !== expectedNames) throw new Error('Unknown fixture argument');
return { action, packageRoot, rootPath, targetPackageRoot };
}
async function loadInstalled(packageRoot, relativePath) {
return import(pathToFileURL(join(packageRoot, relativePath)).href);
}
function writeResult(result) {
process.stdout.write(`${JSON.stringify(result)}\n`);
}
async function probeWriterFence(targetPackageRoot, rootPath, rootId) {
const targetAuthority = await loadInstalled(
targetPackageRoot,
'node_modules/@maka/storage/dist/root-authority.js',
);
const targetCapability = await targetAuthority.resolveStorageRoot({
path: rootPath,
kind: 'interactive',
});
const targetOwner = await targetAuthority.tryAcquireInteractiveRootOwner(targetCapability);
if (targetOwner) {
await targetOwner.close();
return { kind: 'writer_acquired', rootId };
}
return { kind: 'writer_fenced', rootId };
}