From 683bdb135781739659e96d0fb2c2c489114b3b9b Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Sat, 18 Jul 2026 13:30:51 -0700 Subject: [PATCH 1/3] fix(checks): harden trusted policy scanners Co-authored-by: Carlos Villela Signed-off-by: Apurv Kumaria --- .github/actions/ci-static-checks/action.yaml | 4 -- .../checks/openshell-policy-mutation-read.mts | 50 ++++++++++++++++--- test/policy-mutation-read-discovery.test.ts | 22 ++++++++ test/pr-workflow-contract.test.ts | 3 ++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/.github/actions/ci-static-checks/action.yaml b/.github/actions/ci-static-checks/action.yaml index dbcd4a4c06d..78e4b45044b 100644 --- a/.github/actions/ci-static-checks/action.yaml +++ b/.github/actions/ci-static-checks/action.yaml @@ -41,10 +41,6 @@ runs: shell: bash run: npm install --ignore-scripts - - name: Enforce base-trusted createRequire allowlist ratchet - shell: bash - run: npx tsx "$GITHUB_ACTION_PATH/create-require-ratchet.mts" - - name: Validate config schemas shell: bash run: npm run validate:configs diff --git a/scripts/checks/openshell-policy-mutation-read.mts b/scripts/checks/openshell-policy-mutation-read.mts index c0677c8f704..c8c24886d5a 100644 --- a/scripts/checks/openshell-policy-mutation-read.mts +++ b/scripts/checks/openshell-policy-mutation-read.mts @@ -121,13 +121,17 @@ function isPolicyBuilderModule( ); } -function requireModuleSpecifier(expression: ts.Expression | undefined): string | null { +function requireModuleSpecifier( + expression: ts.Expression | undefined, + checker: ts.TypeChecker, +): string | null { if ( !expression || !ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.expression.text !== "require" || - expression.arguments.length !== 1 + expression.arguments.length !== 1 || + checker.getSymbolAtLocation(expression.expression) ) { return null; } @@ -143,7 +147,7 @@ function collectRequiredPolicyBindings( identifiers: Set, namespaces: Set, ): void { - const moduleSpecifier = requireModuleSpecifier(declaration.initializer); + const moduleSpecifier = requireModuleSpecifier(declaration.initializer, checker); if (!moduleSpecifier || !isPolicyBuilderModule(fileName, moduleSpecifier, repoRoot)) return; if (ts.isIdentifier(declaration.name)) { const symbol = checker.getSymbolAtLocation(declaration.name); @@ -258,7 +262,38 @@ function literalText(expression: ts.Expression): string | null { return ts.isStringLiteralLike(expression) ? expression.text : null; } -function isDirectPolicyRead(expression: ts.ArrayLiteralExpression): boolean { +function isCanonicalOpenshellResolverCall( + expression: ts.Expression, + fileName: string, + repoRoot: string, + checker: ts.TypeChecker, +): boolean { + if ( + !ts.isCallExpression(expression) || + !ts.isIdentifier(expression.expression) || + expression.expression.text !== "resolveOpenshellBinary" || + expression.arguments.length !== 0 || + path.resolve(fileName) !== path.resolve(repoRoot, "src/lib/policy/commands.ts") + ) { + return false; + } + const symbol = checker.getSymbolAtLocation(expression.expression); + return ( + symbol?.declarations?.some( + (declaration) => + ts.isFunctionDeclaration(declaration) && + declaration.name?.text === "resolveOpenshellBinary" && + path.resolve(declaration.getSourceFile().fileName) === path.resolve(fileName), + ) === true + ); +} + +function isDirectPolicyRead( + expression: ts.ArrayLiteralExpression, + fileName: string, + repoRoot: string, + checker: ts.TypeChecker, +): boolean { const first = expression.elements[0]; if (!first || !ts.isExpression(first)) return false; const firstText = literalText(first); @@ -266,7 +301,7 @@ function isDirectPolicyRead(expression: ts.ArrayLiteralExpression): boolean { firstText === "policy" ? 0 : firstText === "openshell" || - (ts.isCallExpression(first) && calledName(first.expression) === "resolveOpenshellBinary") + isCanonicalOpenshellResolverCall(first, fileName, repoRoot, checker) ? 1 : -1; if (offset < 0) return false; @@ -295,7 +330,10 @@ export function countPolicyReadCalls( isPolicyBuilderCall(node.expression, builderBindings, checker) ) { readCalls += 1; - } else if (ts.isArrayLiteralExpression(node) && isDirectPolicyRead(node)) { + } else if ( + ts.isArrayLiteralExpression(node) && + isDirectPolicyRead(node, fileName, repoRoot, checker) + ) { readCalls += 1; } ts.forEachChild(node, visit); diff --git a/test/policy-mutation-read-discovery.test.ts b/test/policy-mutation-read-discovery.test.ts index e9c00e34747..402dae2f0fc 100644 --- a/test/policy-mutation-read-discovery.test.ts +++ b/test/policy-mutation-read-discovery.test.ts @@ -52,6 +52,28 @@ describe("OpenShell policy mutation read discovery (#6921)", () => { expect(countPolicyReadCalls(source, "/repo/src/lib/fixture.ts", "/repo")).toBe(0); }); + it("ignores locally shadowed CommonJS require and OpenShell resolver decoys", () => { + const source = [ + "const require = () => ({ buildPolicyGetCommand: () => [] });", + 'const { buildPolicyGetCommand } = require("./policy");', + "function resolveOpenshellBinary() { return 'openshell'; }", + "buildPolicyGetCommand(sandboxName);", + '[resolveOpenshellBinary(), "policy", "get", "--base", sandboxName];', + ].join("\n"); + + expect(countPolicyReadCalls(source, "/repo/src/lib/fixture.ts", "/repo")).toBe(0); + }); + + it("counts the canonical policy command resolver arrays", () => { + const source = [ + "function resolveOpenshellBinary() { return 'openshell'; }", + 'const base = [resolveOpenshellBinary(), "policy", "get", "--base", sandboxName];', + 'const full = [resolveOpenshellBinary(), "policy", "get", "--full", sandboxName];', + ].join("\n"); + + expect(countPolicyReadCalls(source, "/repo/src/lib/policy/commands.ts", "/repo")).toBe(2); + }); + it("ignores a named policy builder import when a nested binding shadows its alias", () => { const source = [ 'import { buildPolicyGetCommand as buildBase } from "./policy/commands";', diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts index f798daea7be..13b851eabba 100644 --- a/test/pr-workflow-contract.test.ts +++ b/test/pr-workflow-contract.test.ts @@ -1074,6 +1074,9 @@ describe("pull request and main workflow contracts", () => { expect(trustedRatchet.run).toBe( 'node --experimental-strip-types "$GITHUB_ACTION_PATH/create-require-ratchet.mts"', ); + expect(stepRuns(sharedActions.staticChecks)).not.toContain( + 'npx tsx "$GITHUB_ACTION_PATH/create-require-ratchet.mts"', + ); expect( requiredStepIndex( sharedActions.staticChecks, From 0e1b1e2ddba432ae9c0dbd4000382f1f28dd9868 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Sat, 18 Jul 2026 14:00:10 -0700 Subject: [PATCH 2/3] test(checks): cover resolver shadow origin Co-authored-by: Carlos Villela Signed-off-by: Apurv Kumaria --- test/policy-mutation-read-discovery.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/policy-mutation-read-discovery.test.ts b/test/policy-mutation-read-discovery.test.ts index 402dae2f0fc..04ce4c35627 100644 --- a/test/policy-mutation-read-discovery.test.ts +++ b/test/policy-mutation-read-discovery.test.ts @@ -64,6 +64,17 @@ describe("OpenShell policy mutation read discovery (#6921)", () => { expect(countPolicyReadCalls(source, "/repo/src/lib/fixture.ts", "/repo")).toBe(0); }); + it("ignores a nested resolver shadow in the canonical policy command module", () => { + const source = [ + "function resolveOpenshellBinary() { return 'openshell'; }", + "function inspect(resolveOpenshellBinary: () => string) {", + ' return [resolveOpenshellBinary(), "policy", "get", "--base", sandboxName];', + "}", + ].join("\n"); + + expect(countPolicyReadCalls(source, "/repo/src/lib/policy/commands.ts", "/repo")).toBe(0); + }); + it("counts the canonical policy command resolver arrays", () => { const source = [ "function resolveOpenshellBinary() { return 'openshell'; }", From 3793c92447129292a5197733944b571eb2758c71 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Sun, 19 Jul 2026 06:21:41 -0700 Subject: [PATCH 3/3] fix(checks): reject nested resolver declarations Co-authored-by: Carlos Villela Signed-off-by: Apurv Kumaria --- scripts/checks/openshell-policy-mutation-read.mts | 1 + test/policy-mutation-read-discovery.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/scripts/checks/openshell-policy-mutation-read.mts b/scripts/checks/openshell-policy-mutation-read.mts index c8c24886d5a..57b9a9dbb4f 100644 --- a/scripts/checks/openshell-policy-mutation-read.mts +++ b/scripts/checks/openshell-policy-mutation-read.mts @@ -282,6 +282,7 @@ function isCanonicalOpenshellResolverCall( symbol?.declarations?.some( (declaration) => ts.isFunctionDeclaration(declaration) && + ts.isSourceFile(declaration.parent) && declaration.name?.text === "resolveOpenshellBinary" && path.resolve(declaration.getSourceFile().fileName) === path.resolve(fileName), ) === true diff --git a/test/policy-mutation-read-discovery.test.ts b/test/policy-mutation-read-discovery.test.ts index 04ce4c35627..c6e2ac102e5 100644 --- a/test/policy-mutation-read-discovery.test.ts +++ b/test/policy-mutation-read-discovery.test.ts @@ -75,6 +75,18 @@ describe("OpenShell policy mutation read discovery (#6921)", () => { expect(countPolicyReadCalls(source, "/repo/src/lib/policy/commands.ts", "/repo")).toBe(0); }); + it("ignores a nested resolver function in the canonical policy command module", () => { + const source = [ + "function resolveOpenshellBinary() { return 'openshell'; }", + "function inspect() {", + " function resolveOpenshellBinary() { return 'decoy'; }", + ' return [resolveOpenshellBinary(), "policy", "get", "--base", sandboxName];', + "}", + ].join("\n"); + + expect(countPolicyReadCalls(source, "/repo/src/lib/policy/commands.ts", "/repo")).toBe(0); + }); + it("counts the canonical policy command resolver arrays", () => { const source = [ "function resolveOpenshellBinary() { return 'openshell'; }",