Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ jobs:
NEMOCLAW_CI_TARGET_ROOT="$GITHUB_WORKSPACE" \
node --experimental-strip-types "$trusted_inspector"
)"
required="$(jq -er '.required | select(type == "boolean")' <<<"$decision")"
required="$(
jq -er '.required | if type == "boolean" then tostring else error("invalid required flag") end' \
<<<"$decision"
)"
if [ "$required" != "true" ]; then
echo "required=false" >> "$GITHUB_OUTPUT"
exit 0
Expand Down
5 changes: 5 additions & 0 deletions ci/source-shape-test-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@
"test": "restore helper re-asserts 2770/660 after the tree is tightened to 700/600",
"category": "security"
},
{
"file": "test/install/installer-homebrew-formula-reuse-trust.test.ts",
"test": "accepts the prospective dev MUSL sandbox template with its trusted digest",
"category": "security"
},
{
"file": "test/mcp/mcp-tool-discovery-image-contract.test.ts",
"test": "pins the reviewed image runtime artifacts exactly",
Expand Down
11 changes: 7 additions & 4 deletions scripts/checks/extract-installer-pins.mts
Original file line number Diff line number Diff line change
Expand Up @@ -336,16 +336,19 @@ const TRUSTED_OPENSHELL_RELEASES: readonly OpenShellReleaseTrust[] = [
// The first template came from the downstream 0.0.106 pin. The second authorizes its
// fail-before-download strings preflight. The third authorizes repair when an existing formula
// has an invalid checksum or its release formula is unavailable. The fourth preserves that
// template after installer tests moved under test/install. Homebrew owns the formula source
// state, so NemoClaw cannot correct it there; the installer verifies the trusted release
// formula before reuse. installer-homebrew-formula-reuse-trust.test.ts and
// installer-hash-check.test.ts lock the template and trust transitions. Remove the repair
// template after installer tests moved under test/install. Per the intentional two-step trust
// rollout in check-installer-hash.sh, the fifth authorizes the exact tested follow-up template
// before that installer can select the MUSL development sandbox while preserving stable GNU.
// Homebrew owns the formula source state, so NemoClaw cannot correct it there; the installer
// verifies the trusted release formula before reuse. installer-homebrew-formula-reuse-trust.test.ts
// and installer-hash-check.test.ts lock the template and trust transitions. Remove the repair
// digests when supported Homebrew installs no longer need this repair path.
installerTemplateSha256: [
"5d4cdb2db60df7539193b486ac15bb9be96ec1d40fc0f739a94d4d2f0bf597a0",
"e850e927aab619d52c5de72967137569d65dd7fa669920c7c5b558f0770140d1",
"e7d51536442b217e3d5e77c4ba3b7c25e6a74898bf22523f7fb58627d34329cb",
"18175cf47a0fece8ce75e5d523185062c7a7c913a3f4ceafbba4a7ca4df7c69b",
"293f45ea1d54e1531c3a070123c04b47f972f29504bd8902a44ab71acdfe6cca",
],
manifests: [
{
Expand Down
9 changes: 8 additions & 1 deletion test/automation/pull-requests/pr-workflow-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ function runWorkflowShellStep(
type SdkPackageLocatorFixture = Readonly<{
artifactFailureRunId?: number;
artifactsByRunId?: Readonly<Record<string, unknown>>;
required?: boolean;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
runs: readonly unknown[];
step: WorkflowStep;
workflowRunFailure?: boolean;
Expand All @@ -180,7 +181,9 @@ function runSdkPackageLocator(fixture: SdkPackageLocatorFixture): Readonly<{
mkdirSync(fakeBin);
writeFileSync(
join(inspectorDirectory, "prepare-ci-npm-install.mts"),
'process.stdout.write(JSON.stringify({ required: true, artifactName: "reviewed-sdk.tgz" }));\n',
`process.stdout.write(JSON.stringify({ required: ${String(
fixture.required ?? true,
)}, artifactName: "reviewed-sdk.tgz" }));\n`,
);
writeFileSync(join(workflowDirectory, "openshell-sdk-package-pr.yaml"), "name: test\n");
writeFileSync(join(fakeBin, "seq"), "#!/bin/sh\nprintf '1\\n'\n", { mode: 0o755 });
Expand Down Expand Up @@ -558,6 +561,10 @@ describe("pull request and main workflow contracts", () => {
expect(locate.run).toContain("available only to same-repository pull requests");
expect(locate.run).not.toContain("@nvidia/openshell-sdk@0.0.106");
expect(locate.run).not.toContain("nvidia-openshell-sdk-0.0.106.tgz");

const optionalPackage = runSdkPackageLocator({ required: false, runs: [], step: locate });
expect(optionalPackage.result).toMatchObject({ status: 0, stderr: "" });
expect(optionalPackage.githubOutput).toBe("required=false\n");
});

// The one-time bootstrap may proceed only while both lockfiles use the public registry.
Expand Down
68 changes: 56 additions & 12 deletions test/install/installer-homebrew-formula-reuse-trust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const TRUSTED_V00106_TEMPLATE_DIGESTS = [
"e850e927aab619d52c5de72967137569d65dd7fa669920c7c5b558f0770140d1",
"e7d51536442b217e3d5e77c4ba3b7c25e6a74898bf22523f7fb58627d34329cb",
"18175cf47a0fece8ce75e5d523185062c7a7c913a3f4ceafbba4a7ca4df7c69b",
"293f45ea1d54e1531c3a070123c04b47f972f29504bd8902a44ab71acdfe6cca",
] as const;
const tempDirs: string[] = [];

Expand Down Expand Up @@ -67,6 +68,36 @@ function restoreFlatInstallTestPaths(source: string): string {
);
}

function selectDevMuslSandboxTemplate(source: string): string {
const grouped = ` case "$ARCH_LABEL" in
x86_64)
ASSETS+=("openshell-gateway-x86_64-unknown-linux-gnu.tar.gz")
ASSETS+=("openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz")
;;
aarch64)
ASSETS+=("openshell-gateway-aarch64-unknown-linux-gnu.tar.gz")
ASSETS+=("openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz")
;;
esac`;
const devMusl = ` SANDBOX_LIBC="gnu"
if [ "$RESOLVED_CHANNEL" = "dev" ]; then
SANDBOX_LIBC="musl"
fi
case "$ARCH_LABEL" in
x86_64)
ASSETS+=("openshell-gateway-x86_64-unknown-linux-gnu.tar.gz")
ASSETS+=("openshell-sandbox-x86_64-unknown-linux-\${SANDBOX_LIBC}.tar.gz")
;;
aarch64)
ASSETS+=("openshell-gateway-aarch64-unknown-linux-gnu.tar.gz")
ASSETS+=("openshell-sandbox-aarch64-unknown-linux-\${SANDBOX_LIBC}.tar.gz")
;;
esac`;
const selected = source.replace(grouped, devMusl);
expect(selected, "grouped GNU sandbox asset selection").not.toBe(source);
return selected;
}

function runTrustCheck(source: string) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-homebrew-reuse-trust-"));
const installer = path.join(tempDir, "install-openshell.sh");
Expand All @@ -92,6 +123,22 @@ function runTrustCheck(source: string) {
);
}

function expectTrustedTemplate(source: string, digest: string): void {
const result = runTrustCheck(source);

expect(result.status, result.stderr).toBe(0);
const records = JSON.parse(result.stdout) as Array<{
operationalTemplateSha256: string;
source: string;
}>;
const installerTemplateDigests = new Set(
records
.filter((record) => record.source === "installer")
.map((record) => record.operationalTemplateSha256),
);
expect(installerTemplateDigests).toEqual(new Set([digest]));
}

describe("installer Homebrew formula reuse trust", () => {
const previousTemplate = restoreFlatInstallTestPaths(INSTALLER_SOURCE);
const baseTemplate = removeHomebrewFormulaReuseRepair(previousTemplate);
Expand All @@ -103,19 +150,16 @@ describe("installer Homebrew formula reuse trust", () => {
] as const;

it.each(templates)("accepts the %s template with digest %s", (_label, digest, source) => {
const result = runTrustCheck(source);

expect(result.status, result.stderr).toBe(0);
const records = JSON.parse(result.stdout) as Array<{
operationalTemplateSha256: string;
source: string;
}>;
const installerTemplateDigests = new Set(
records
.filter((record) => record.source === "installer")
.map((record) => record.operationalTemplateSha256),
expectTrustedTemplate(source, digest);
});

// source-shape-contract: security -- Exact prospective installer bytes must be base-authorized before trusted CI can admit the dependent runtime change
it("accepts the prospective dev MUSL sandbox template with its trusted digest", () => {
const prospectiveTemplate = selectDevMuslSandboxTemplate(INSTALLER_SOURCE);
expect(prospectiveTemplate).toContain(
'if [ "$RESOLVED_CHANNEL" = "dev" ]; then\n SANDBOX_LIBC="musl"',
);
expect(installerTemplateDigests).toEqual(new Set([digest]));
expectTrustedTemplate(prospectiveTemplate, TRUSTED_V00106_TEMPLATE_DIGESTS[4]);
});

it("rejects an unlisted template", () => {
Expand Down