fix(ql3): require read-only legacy target mount

This commit is contained in:
whyour
2026-08-31 00:09:41 +08:00
parent 32c0143f5d
commit 4b0241f992
2 changed files with 51 additions and 6 deletions
@@ -361,6 +361,7 @@ function mappedMount(
hostPath: string,
targetPath: string,
label: string,
expectedReadWrite = true,
): DockerMount {
const matches = mounts.filter((mount) => {
const relative = path.relative(mount.source, hostPath);
@@ -370,8 +371,13 @@ function mappedMount(
path.join(mount.destination, relative) === targetPath
);
});
if (matches.length !== 1 || matches[0]?.readWrite !== true) {
configurationError(`${label} must have one read-write bind mapping`);
if (
matches.length !== 1 ||
matches[0]?.readWrite !== expectedReadWrite
) {
configurationError(
`${label} must have one ${expectedReadWrite ? 'read-write' : 'read-only'} bind mapping`,
);
}
return matches[0]!;
}
@@ -597,6 +603,7 @@ export function parseTargetContainerEvidence(
command.request.legacySourcePath,
application.targetLegacySourcePath,
'target legacy source',
false,
);
const databaseMount = mappedMount(
mounts,
@@ -88,7 +88,7 @@ function stoppedLegacyInspection(state, running = false) {
]);
}
function targetInspection(state) {
function targetInspection(state, options = {}) {
return JSON.stringify([
{
Id: state.targetContainerId,
@@ -115,10 +115,27 @@ function targetInspection(state) {
Mounts: [
{
Type: 'bind',
Source: state.managementRoot,
Destination: '/host',
Source: state.deploymentRoot,
Destination: targetPath(state, state.deploymentRoot),
RW: true,
},
{
Type: 'bind',
Source: state.legacySourcePath,
Destination: targetPath(state, state.legacySourcePath),
RW: options.writableLegacySource === true,
},
...[
state.targetDatabasePath,
state.recoveryPath,
state.manifestPath,
state.activationPath,
].map((filePath) => ({
Type: 'bind',
Source: filePath,
Destination: targetPath(state, filePath),
RW: true,
})),
],
},
]);
@@ -472,7 +489,7 @@ function harness(state, options = {}) {
);
}
if (args[0] === 'container' && args[1] === 'inspect') {
return targetInspection(state);
return targetInspection(state, options);
}
if (args[0] === 'container' && args[1] === 'start') {
if (args[2] === state.legacyContainerId) {
@@ -574,6 +591,27 @@ test('starts an exact target once and replays the active commitment without Dock
assert.equal(replay.recordDigest, active.recordDigest);
});
test('requires the target container to keep the Legacy source read-only', async (t) => {
const state = fixture(t);
const controller = harness(state, { writableLegacySource: true });
const result = await runLocalDeploymentDockerTarget(
command(state),
controller,
);
assert.equal(result.state, 'manual_required');
assert.equal(
controller.calls.filter((args) => args[1] === 'start').length,
0,
);
const request = JSON.parse(
fs.readFileSync(
path.join(state.journal, '0003-target-start-decision.json'),
'utf8',
),
);
assert.equal(request.evidence.reason, 'target_preflight_unproved');
});
test('starts an offline Trial Kit image only when its local reference and content ID both match', async (t) => {
const state = fixture(t);
state.targetImageAuthority = 'local-image-id';