Files
maka/scripts/release-cli-runtime-host-diagnostics.test.mjs
AstroHan c633ea5b25 chore: add ASF source headers and a release header audit gate (#3397)
* chore: add an ASF source-header policy and audit it in the release gate

The first Apache Maka (Incubating) source release has to apply the ASF header
policy and enforce it, and nothing in the repository decided which files the
policy covers or checked that they comply.

`scripts/asf-license-headers.mjs` is that decision. It asserts two properties
of the tree it is pointed at, and nothing else. Every entry is classified
exactly once: covered by a comment syntax and required to carry the ASF header,
or matched by one reviewed exclusion rule that records why the header does not
belong there. And in every covered file the license text occurs exactly once,
as the canonical rendering at the top. A file that is neither covered nor
excluded fails the audit, so a new file type or an unexpected path cannot reach
a release candidate until someone writes down which of the two it is.

Both properties are stated about the artifact rather than about what the writer
emits. An audit that asks "does this file start with the bytes I would write?"
can only confirm its own writer: it treats a header rendering it does not
recognize as absent, writes a second license block above it, and then accepts
the result, because the block it just wrote is at the top of the file. So
detection is deliberately loose and acceptance deliberately strict. Detection
flattens comment punctuation and line wrapping away and finds the license text
written with `//`, in a JSDoc block, with `#`, inside an HTML comment, at any
indentation, with an `https` license URL, or rewrapped into a paragraph.
Acceptance takes only the exact canonical rendering. `write` is correspondingly
dumb: it inserts a header where there is none and refuses every other state,
because reconciling someone else's rendering is a decision, not a rewrite.

Nothing is dropped from the tree under audit. An extracted candidate is walked
whole, with no directory skipped by name, because a file inside a `dist/` or
`release/` directory in a source release is not a tree to audit around — it is
unclassified, and the gate says so. An entry that is neither a directory nor a
regular file is reported rather than passed over.

What the enumeration does decide is which tree a checkout stands in for: the
files a source release would carry, which means subtracting what `export-ignore`
prunes. `.gitattributes` owns that fact, so the audit asks Git for it rather
than restating it in a rule. A copy would answer for the original in the one
case that matters — an extracted candidate containing such a path would be
excused by the very reason that says it cannot be there. Asking Git also has to
walk the ancestors, since `export-ignore` on a directory prunes the subtree
while `git check-attr` reports one path at a time and does not inherit. The two
modes now audit the same set of files.

An exclusion rule names the files it excludes or states a structural property
that holds for anything it matches. A bare directory prefix does neither: it
lends its justification to whatever is added to that directory next. The rules
cover the ASF release documents themselves, verbatim upstream license texts,
the notices Maka publishes about a bundled third-party runtime, third-party
work kept under its own license, generated files that a `check:` script
byte-compares against a fresh run, payloads republished verbatim as prompt text
or as a pull request body, byte-significant fixtures, formats with no comment
syntax, binaries, and version-control metadata.

ASF policy treats the part of a file that the copyright owner did not
contribute as third-party work, so the files Maka adapted from opencode and
from the Vercel AI SDK are excluded rather than given a whole-file header.
Whether a heavily modified file should carry a combined header is a PPMC
decision. Their attribution belongs to LICENSE and the NOTICE audit.

The audit cannot determine provenance — that is a fact about people, not a
property of a path — so it refuses to let an unreviewed claim of one pass. A
covered file carrying an SPDX identifier, an explicit copyright line, or an
adaptation notice fails unless it is recorded in `reviewedProvenance` with the
decision that put an ASF header on it anyway. The markers were chosen by
measuring them against the whole tree: those three hit three files between
them, while `derived from` hits seventy and `opencode` hits forty-five, because
Maka supports it as a provider. A net that noisy gets answered with a list of
paths added to quiet it. This is a net and not a proof: it catches a file that
states its origin, not prose that alludes to one.

`scripts/protocol-epoch-check.mjs` gains one exemption. It requires an epoch
bump whenever a file under the protocol directory changes, a conservative proxy
for "the protocol changed" that is right to be conservative: a needless bump
costs a number, a missed one ships two incompatible protocols under one. But
inserting a license header provably does not change the protocol, and bumping
for it would tell every peer the wire is incompatible over a comment. The guard
now exempts a change that is exactly `applyHeader` applied to the previous
content — the same authority that writes the headers, so the exemption covers
the canonical insertion and nothing that merely resembles it. A protocol file
that gained a header and a real edit fails that comparison and still needs an
epoch.

`Prepare ASF source candidate` runs the audit inside the extracted archive
before anything installs or builds into that tree, so the release gate reads
the exact bytes a voter downloads instead of a checkout that resembles them. CI
runs it unconditionally beside the other install-free checks, because a header
gate that only sees the affected surfaces is not a gate.

Apache RAT was the alternative. It would put a JVM and a downloaded jar into
the audit path that reviewers are expected to reproduce, and its glob
exclusions fail open: a new unexpected file inside an excluded directory is
approved silently. This audit needs no toolchain beyond the Node the archive
already requires, and an unexpected file fails it.

The headers themselves are not in this commit. Until the sweep lands, the audit
reports every existing source file as missing one.

Refs #3271

Generated-by: Claude Code

* chore: apply the ASF license header to every covered source file

`npm run write:asf-headers` produced this commit, and `npm run
check:asf-headers` now passes on the checkout and on the extracted source
archive. Every file in the header sweep is exactly its previous content with
the standard ASF header inserted at the top, below an interpreter shebang, an
HTML doctype, or Markdown front matter where one of those has to open the
file. The two files described below, `DISCLAIMER-WIP` and
`.git-blame-ignore-revs`, are the only hand edits in this commit; nothing else
was touched by hand. `.asf.yaml`, which already carried the header, is
untouched.

`DISCLAIMER-WIP` no longer discloses that source files lack Apache license
headers, because they no longer do. The remaining disclosures about `NOTICE`
and about the software grant and ICLAs are unchanged and still open.

`.git-blame-ignore-revs` gets an entry for this sweep. Following the
convention the file documents, the placeholder records the landed hash in a
follow-up right after the squash-merge, since a hash that never reaches main
makes `git blame` fail outright.

Merging this will require open pull requests to rebase. That is the cost of
doing the sweep once; splitting it per directory would multiply the rebases
instead of avoiding them.

Refs #3271

Generated-by: Claude Code
2026-08-22 21:48:21 +08:00

206 lines
7.4 KiB
JavaScript

/*
* 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 assert from 'node:assert/strict';
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import {
collectRuntimeHostFailureDiagnostic,
retireCollectedRuntimeHostStartupDiagnostic,
} from './release-cli-runtime-host-diagnostics.mjs';
const ROOT_ID = 'a'.repeat(64);
const STARTUP_ATTEMPT_ID = '00000000-0000-4000-8000-000000000001';
test('collects canonical Runtime Host evidence without invoking mutating storage authority', async () => {
const fixture = createFixture();
try {
const startupDiagnostic = {
schemaVersion: 1,
rootId: ROOT_ID,
startupAttemptId: STARTUP_ATTEMPT_ID,
candidatePid: 123,
capturedAt: '2026-08-19T00:00:00.000Z',
reason: 'local_ipc_security_failed',
errorChain: [{ message: '{"stage":"acl_apply"}' }],
logs: ['endpoint setup failed'],
};
const securityCalls = [];
const diagnostic = await collectRuntimeHostFailureDiagnostic('/installed', fixture.root, {
platform: 'win32',
architecture: 'x64',
environment: {
ImageOS: 'win25',
ImageVersion: '20260817.1',
RUNNER_NAME: 'hosted-runner',
RUNNER_ENVIRONMENT: 'github-hosted',
},
loadInstalled: async (_packageRoot, relativePath) => {
if (relativePath.endsWith('/root-authority.js')) {
return {
STORAGE_ROOT_MARKER_FILE: '.maka-storage-root.json',
discoverMarkedStorageRoot: async () => ({ rootId: ROOT_ID }),
resolveRootControlNamespace: () => fixture.controlRoot,
};
}
if (relativePath.endsWith('/registration.js')) {
return {
RUNTIME_HOST_REGISTRATION_FILE: 'registration.json',
readHostRegistration: async () => ({
schemaVersion: 1,
rootId: ROOT_ID,
hostEpoch: 'host-epoch',
state: 'recovering',
lifecycleMode: 'interactive',
pid: 123,
endpoint: '\\\\.\\pipe\\maka-runtime-host',
}),
};
}
if (relativePath.endsWith('/startup-diagnostic.js')) {
return {
RUNTIME_HOST_STARTUP_DIAGNOSTIC_FILE: 'startup-diagnostic.json',
readCandidateStartupDiagnostic: async () => startupDiagnostic,
};
}
throw new Error(`Unexpected installed module: ${relativePath}`);
},
readPathSecurityBatch: async (paths) => {
securityCalls.push(paths);
return paths.map(() => ({
state: 'present',
owner: 'runner\\user',
sddl: 'O:SYD:P',
}));
},
});
assert.deepEqual(
diagnostic.paths.map(({ role }) => role),
[
'root',
'root_marker',
'control_root',
'control_directory',
'registration',
'startup_diagnostic',
],
);
assert.deepEqual(securityCalls, [
[fixture.root, fixture.controlRoot, fixture.controlDirectory],
]);
assert.equal(diagnostic.paths[0].security.owner, 'runner\\user');
assert.equal(diagnostic.storageAuthority.state, 'valid');
assert.equal(diagnostic.registration.rootIdMatches, true);
assert.deepEqual(diagnostic.startup, { state: 'present', diagnostic: startupDiagnostic });
assert.equal(diagnostic.runner.architecture, undefined);
let retired;
assert.equal(
await retireCollectedRuntimeHostStartupDiagnostic('/installed', diagnostic, {
loadInstalled: async (_packageRoot, relativePath) => {
assert.match(relativePath, /startup-diagnostic\.js$/u);
return {
clearSelectedCandidateStartupDiagnostic: async (rootId, startupAttemptId) => {
retired = { rootId, startupAttemptId };
return true;
},
};
},
}),
true,
);
assert.deepEqual(retired, { rootId: ROOT_ID, startupAttemptId: STARTUP_ATTEMPT_ID });
} finally {
fixture.cleanup();
}
});
test('keeps read-only path evidence when root authority validation fails', async () => {
const fixture = createFixture({ createControlDirectory: false });
try {
const diagnostic = await collectRuntimeHostFailureDiagnostic('/installed', fixture.root, {
platform: 'win32',
loadInstalled: async (_packageRoot, relativePath) => {
if (!relativePath.endsWith('/root-authority.js')) {
throw new Error(`Unexpected installed module: ${relativePath}`);
}
return {
STORAGE_ROOT_MARKER_FILE: '.maka-storage-root.json',
discoverMarkedStorageRoot: async () => {
throw new Error('marker validation failed', {
cause: Object.assign(new Error('Access is denied'), { code: 'EACCES' }),
});
},
resolveRootControlNamespace: () => fixture.controlRoot,
};
},
readPathSecurityBatch: async (paths) => {
return paths.map((path) =>
path === fixture.root
? {
state: 'unavailable',
error: { name: 'UnauthorizedAccessException', nativeErrorCode: 5 },
}
: { state: 'present', owner: 'runner\\user', sddl: 'O:SYD:P' },
);
},
});
assert.equal(diagnostic.storageAuthority.state, 'invalid');
assert.equal(diagnostic.storageAuthority.error.cause.code, 'EACCES');
assert.equal(diagnostic.paths[0].security.state, 'unavailable');
assert.equal(diagnostic.paths[0].security.error.nativeErrorCode, 5);
assert.deepEqual(
diagnostic.paths.map(({ role }) => role),
['root', 'root_marker', 'control_root'],
);
assert.equal(existsSync(fixture.controlDirectory), false);
} finally {
fixture.cleanup();
}
});
function createFixture({ createControlDirectory = true } = {}) {
const base = mkdtempSync(join(tmpdir(), 'maka-runtime-host-diagnostic-test-'));
const root = join(base, 'root');
const controlRoot = join(base, 'control');
const controlDirectory = join(controlRoot, ROOT_ID);
const registrationPath = join(controlDirectory, 'registration.json');
const startupPath = join(controlDirectory, 'startup-diagnostic.json');
mkdirSync(root, { recursive: true });
mkdirSync(controlRoot, { recursive: true });
writeFileSync(join(root, '.maka-storage-root.json'), '{}\n');
if (createControlDirectory) {
mkdirSync(controlDirectory, { recursive: true });
writeFileSync(registrationPath, '{}\n');
writeFileSync(startupPath, '{}\n');
}
return {
root,
controlRoot,
controlDirectory,
registrationPath,
startupPath,
cleanup: () => rmSync(base, { recursive: true, force: true }),
};
}