mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
fix(ci): integrate main and repair inherited static checks
This commit is contained in:
commit
afd4d571f6
543 files changed
+39569
-12128
No files matched your search
@@ -107,6 +107,7 @@ export const SCOPES = {
|
||||
"tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts",
|
||||
"tests/boundary/cli/nativeSignatureCli.test.ts",
|
||||
"tests/boundary/cli/nativeMachoArm64_32Cli.test.ts",
|
||||
"tests/boundary/cli/nativeMachoEncryptionCli.test.ts",
|
||||
"tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts",
|
||||
"src/process/capture/FilesystemSnapshot.test.ts",
|
||||
"tests/process-global/filesystemSnapshotFallbackCancellation.test.ts",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.NET.HostModel">
|
||||
<HintPath>$(MSBuildSDKsPath)/../Microsoft.NET.HostModel.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration><packageSources><clear /></packageSources></configuration>
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.NET.HostModel.AppHost;
|
||||
using Microsoft.NET.HostModel.Bundle;
|
||||
|
||||
// Trusted fixture generator only. Generated hosts are inspected, never executed.
|
||||
if (args.Length is < 2 or > 3)
|
||||
throw new ArgumentException("Expected apphost template, absent output directory, and optional windows/linux/osx target");
|
||||
var targetOS = (args.ElementAtOrDefault(2) ?? "windows") switch
|
||||
{
|
||||
"windows" => OSPlatform.Windows,
|
||||
"linux" => OSPlatform.Linux,
|
||||
"osx" => OSPlatform.OSX,
|
||||
_ => throw new ArgumentException("Target OS must be windows, linux or osx"),
|
||||
};
|
||||
var hostName = targetOS == OSPlatform.Windows ? "Probe.exe" : "Probe";
|
||||
var template = Path.GetFullPath(args[0]);
|
||||
var output = Path.GetFullPath(args[1]);
|
||||
if (Directory.Exists(output)) throw new IOException("Output directory must be absent");
|
||||
Directory.CreateDirectory(output);
|
||||
var assembly = typeof(Program).Assembly.Location;
|
||||
var resource = Path.Combine(output, "resource.txt");
|
||||
File.WriteAllText(resource, string.Concat(Enumerable.Repeat("owned compressed fixture content\n", 4096)));
|
||||
foreach (var (version, compress) in new[] { (new Version(3, 1), false), (new Version(5, 0), false), (new Version(8, 0), false), (new Version(8, 0), true) })
|
||||
{
|
||||
var label = $"net{version.Major}-{(compress ? "compressed" : "plain")}";
|
||||
var directory = Path.Combine(output, label);
|
||||
Directory.CreateDirectory(directory);
|
||||
var host = Path.Combine(directory, targetOS == OSPlatform.Windows ? "template.exe" : "template");
|
||||
HostWriter.CreateAppHost(template, host, "BundleFixture.dll");
|
||||
var bundler = new Bundler(hostName, directory,
|
||||
BundleOptions.BundleOtherFiles | (compress ? BundleOptions.EnableCompression : BundleOptions.None),
|
||||
targetOS, Architecture.X64, version, appAssemblyName: "BundleFixture");
|
||||
var inputs = new Dictionary<string, string>
|
||||
{
|
||||
["BundleFixture.dll"] = assembly,
|
||||
["BundleFixture.deps.json"] = Path.ChangeExtension(assembly, ".deps.json"),
|
||||
["BundleFixture.runtimeconfig.json"] = Path.ChangeExtension(assembly, ".runtimeconfig.json"),
|
||||
["data/resource.txt"] = resource,
|
||||
};
|
||||
var generated = bundler.GenerateBundle(new[] {
|
||||
new FileSpec(host, hostName),
|
||||
new FileSpec(assembly, "BundleFixture.dll"),
|
||||
new FileSpec(Path.ChangeExtension(assembly, ".deps.json"), "BundleFixture.deps.json"),
|
||||
new FileSpec(Path.ChangeExtension(assembly, ".runtimeconfig.json"), "BundleFixture.runtimeconfig.json"),
|
||||
new FileSpec(resource, "data/resource.txt"),
|
||||
});
|
||||
File.WriteAllText(Path.Combine(directory, "oracle.json"), JsonSerializer.Serialize(Observe(generated, inputs), new JsonSerializerOptions { WriteIndented = true }));
|
||||
Console.WriteLine(generated);
|
||||
}
|
||||
|
||||
// Independent positive-fixture oracle: .NET's BinaryReader owns its string/number framing.
|
||||
static object Observe(string path, Dictionary<string, string> inputs)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
var signature = Convert.FromHexString("8b1202b96a612038727b930214d7a03213f5b9e6efae3318ee3b2dce24b36aae");
|
||||
var marker = bytes.AsSpan().IndexOf(signature);
|
||||
if (marker < 8) throw new InvalidDataException("Generated host has no bundle marker");
|
||||
var header = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(marker - 8, 8));
|
||||
using var stream = new MemoryStream(bytes);
|
||||
stream.Position = header;
|
||||
using var reader = new BinaryReader(stream);
|
||||
var major = reader.ReadUInt32();
|
||||
var minor = reader.ReadUInt32();
|
||||
var count = reader.ReadInt32();
|
||||
var bundleId = reader.ReadString();
|
||||
if (major >= 2) stream.Position += 40;
|
||||
var rows = new List<object>();
|
||||
var found = new HashSet<string>();
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
var offset = reader.ReadInt64();
|
||||
var size = reader.ReadInt64();
|
||||
var compressed = major == 6 ? reader.ReadInt64() : 0;
|
||||
var kind = reader.ReadByte();
|
||||
var name = reader.ReadString();
|
||||
if (!inputs.TryGetValue(name, out var input) || !found.Add(name))
|
||||
throw new InvalidDataException("Unexpected generated member: " + name);
|
||||
using var stored = new MemoryStream(bytes, checked((int)offset), checked((int)(compressed == 0 ? size : compressed)));
|
||||
using var expanded = compressed == 0 ? null : new DeflateStream(stored, CompressionMode.Decompress);
|
||||
var digest = Convert.ToHexString(SHA256.HashData((Stream)expanded ?? stored)).ToLowerInvariant();
|
||||
var expected = File.ReadAllBytes(input);
|
||||
if (expected.LongLength != size || digest != Convert.ToHexString(SHA256.HashData(expected)).ToLowerInvariant())
|
||||
throw new InvalidDataException("Generated member bytes differ: " + name);
|
||||
rows.Add(new { name, offset, size, compressed_size = compressed, file_type = kind, sha256 = digest });
|
||||
}
|
||||
if (!found.SetEquals(inputs.Keys)) throw new InvalidDataException("Generated bundle omitted an input");
|
||||
return new { source = "Microsoft.NET.HostModel + independent BinaryReader/DeflateStream", host = path, major, minor, header_offset = header, bundle_id = bundleId, sha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), files = rows };
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#include <stdio.h>
|
||||
__attribute__((noinline)) int target_fn(int x) { return x * 5; }
|
||||
__attribute__((noinline)) int call_through(int (*fp)(int), int x) { return fp(x); }
|
||||
__attribute__((noinline)) int call_direct(int x) { return target_fn(x); }
|
||||
int main(void) { printf("%d %d\n", call_through(target_fn, 3), call_direct(4)); return 0; }
|
||||
@@ -38,6 +38,11 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
|
||||
// Test-only outputs are declared beside the test planner rather than becoming
|
||||
// runtime or typecheck prerequisites. Add a consumer here when it reads one.
|
||||
const artifactConsumers = {
|
||||
"artifacts:skills": [
|
||||
"tests/boundary/cli/guidanceCli.test.ts",
|
||||
"tests/boundary/guidanceSearch.test.ts",
|
||||
"tests/boundary/mcp/registerGuidanceTools.test.ts",
|
||||
],
|
||||
"artifacts:mcp-catalog": [
|
||||
"tests/boundary/mcp/deferredRuntime.test.ts",
|
||||
"tests/boundary/mcp/contractPresentation.test.ts",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/** One static import or re-export specifier and its source line. */
|
||||
export interface ImportSpecifierFact {
|
||||
readonly specifier: string;
|
||||
readonly line: number;
|
||||
}
|
||||
|
||||
/** Static string specifier on one import or re-export node, when it has one. */
|
||||
export function importSpecifier(node: {
|
||||
readonly type: string;
|
||||
}): string | undefined;
|
||||
|
||||
/**
|
||||
* Static import and re-export specifiers in source order.
|
||||
* Comments and string literals are not imports.
|
||||
*/
|
||||
export function readImportSpecifiers(
|
||||
source: string,
|
||||
): readonly ImportSpecifierFact[];
|
||||
@@ -0,0 +1,43 @@
|
||||
import { parse } from "@babel/parser";
|
||||
import { traverseFast } from "@babel/types";
|
||||
|
||||
const literalValue = (node) => {
|
||||
if (node?.type === "StringLiteral") return node.value;
|
||||
if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
|
||||
return node.quasis[0]?.value.cooked ?? undefined;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Static string specifier on one import or re-export node, when it has one. */
|
||||
export const importSpecifier = (node) => {
|
||||
switch (node.type) {
|
||||
case "ImportDeclaration":
|
||||
case "ExportNamedDeclaration":
|
||||
case "ExportAllDeclaration":
|
||||
case "ImportExpression":
|
||||
case "TSImportType":
|
||||
return literalValue(node.source);
|
||||
case "TSImportEqualsDeclaration":
|
||||
return node.moduleReference.type === "TSExternalModuleReference"
|
||||
? literalValue(node.moduleReference.expression)
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Static import and re-export specifiers in source order.
|
||||
* Comments and string literals are not imports. Non-string dynamic imports
|
||||
* are omitted. Callers decide which specifiers are in-repo edges.
|
||||
*/
|
||||
export const readImportSpecifiers = (source) => {
|
||||
const specifiers = [];
|
||||
const tree = parse(source, { sourceType: "module", plugins: ["typescript"] });
|
||||
traverseFast(tree, (node) => {
|
||||
const specifier = importSpecifier(node);
|
||||
if (specifier === undefined) return;
|
||||
specifiers.push({ specifier, line: node.loc?.start.line ?? 1 });
|
||||
});
|
||||
return specifiers;
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { parse } from "@babel/parser";
|
||||
import { traverseFast } from "@babel/types";
|
||||
import { isAbsolute, relative, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import { readImportSpecifiers } from "./import-specifiers.mjs";
|
||||
|
||||
const PURE_LAYERS = new Set(["domain", "contracts"]);
|
||||
const MIGRATED_PROVIDER_ROOTS = new Set([
|
||||
"sqlite",
|
||||
@@ -27,30 +27,6 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
|
||||
const repositoryPath = (root, path) =>
|
||||
relative(root, path).replaceAll("\\", "/");
|
||||
|
||||
const literalValue = (node) => {
|
||||
if (node?.type === "StringLiteral") return node.value;
|
||||
if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
|
||||
return node.quasis[0]?.value.cooked ?? undefined;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const importSpecifier = (node) => {
|
||||
switch (node.type) {
|
||||
case "ImportDeclaration":
|
||||
case "ExportNamedDeclaration":
|
||||
case "ExportAllDeclaration":
|
||||
case "ImportExpression":
|
||||
case "TSImportType":
|
||||
return literalValue(node.source);
|
||||
case "TSImportEqualsDeclaration":
|
||||
return node.moduleReference.type === "TSExternalModuleReference"
|
||||
? literalValue(node.moduleReference.expression)
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const sourceTarget = (file, specifier) => {
|
||||
if (specifier.startsWith("file:")) return fileURLToPath(specifier);
|
||||
if (isAbsolute(specifier)) return resolve(specifier);
|
||||
@@ -115,26 +91,14 @@ const failedBoundary = (file, target) => {
|
||||
export const inspectModuleBoundaries = (file, source, root) => {
|
||||
const absoluteFile = resolve(root, file);
|
||||
const importer = repositoryPath(root, absoluteFile);
|
||||
const tree = parse(source, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript"],
|
||||
});
|
||||
const violations = [];
|
||||
traverseFast(tree, (node) => {
|
||||
const specifier = importSpecifier(node);
|
||||
if (specifier === undefined) return;
|
||||
for (const { specifier, line } of readImportSpecifiers(source)) {
|
||||
const resolved = sourceTarget(absoluteFile, specifier);
|
||||
if (resolved === undefined) return;
|
||||
if (resolved === undefined) continue;
|
||||
const target = repositoryPath(root, resolved);
|
||||
const boundary = failedBoundary(importer, target);
|
||||
if (boundary !== undefined)
|
||||
violations.push({
|
||||
file: importer,
|
||||
line: node.loc?.start.line ?? 1,
|
||||
specifier,
|
||||
target,
|
||||
boundary,
|
||||
});
|
||||
});
|
||||
violations.push({ file: importer, line, specifier, target, boundary });
|
||||
}
|
||||
return violations;
|
||||
};
|
||||
+5
-5
@@ -128,7 +128,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
);
|
||||
const configurationError = cliConfigurationError(process.env);
|
||||
let dispatchFailure;
|
||||
let closeCliJavaScriptResources;
|
||||
let closeCliResources;
|
||||
// Incur can exit from the callback below before this function returns.
|
||||
// That callback owns the status, including a closed-pipe exit of 141.
|
||||
let exiting = false;
|
||||
@@ -150,7 +150,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
writeStdout(versionOutput);
|
||||
} else {
|
||||
const cliRuntime = await import("../dist/cli-runtime/cli.js");
|
||||
closeCliJavaScriptResources = cliRuntime.closeCliJavaScriptResources;
|
||||
closeCliResources = cliRuntime.closeCliResources;
|
||||
const { createCli } = cliRuntime;
|
||||
const { createCliResultOutput } =
|
||||
await import("../dist/cli-runtime/cliResultOutput.js");
|
||||
@@ -176,10 +176,10 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
void (async () => {
|
||||
let status = code;
|
||||
try {
|
||||
await closeCliJavaScriptResources();
|
||||
await closeCliResources();
|
||||
} catch (cleanup) {
|
||||
process.stderr.write(
|
||||
`Failed to close the JavaScript analysis worker: ${cleanupFailureText(cleanup)}\n`,
|
||||
`Failed to close CLI resources: ${cleanupFailureText(cleanup)}\n`,
|
||||
);
|
||||
if (status === 0) status = 1;
|
||||
}
|
||||
@@ -225,7 +225,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
// Serve and output rendering can throw after a worker is parked. Stop it
|
||||
// on that path too, and keep the dispatch failure when both fail.
|
||||
try {
|
||||
await closeCliJavaScriptResources?.();
|
||||
await closeCliResources?.();
|
||||
} catch (cleanup) {
|
||||
// The exit callback reports this same shared failure and then exits.
|
||||
if (!exiting) {
|
||||
|
||||
@@ -61,8 +61,10 @@ try {
|
||||
process.execPath,
|
||||
resolve("scripts/run-turbo.mjs"),
|
||||
"run",
|
||||
...(plan.needsBuild ? ["artifacts:skills"] : []),
|
||||
...plan.artifactTasks,
|
||||
...new Set([
|
||||
...(plan.needsBuild ? ["artifacts:skills"] : []),
|
||||
...plan.artifactTasks,
|
||||
]),
|
||||
]);
|
||||
if (prepared.code !== 0) process.exitCode = prepared.code;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
|
||||
const root = await PrivateRuntimeRoot.create({ prefix: "rea-bun-verifier-" });
|
||||
const commands = await createVerifierCommands();
|
||||
const run = (command, args, options = {}) =>
|
||||
commands.exec(command, args, {
|
||||
timeout: 120_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
...options,
|
||||
});
|
||||
// The root holds the compiled target and child temporary files. Release it
|
||||
// only after every owned command family has settled.
|
||||
let commandsClosed = false;
|
||||
await withVerifierCleanup(async () => {
|
||||
const source = join(root.path, "hello.ts");
|
||||
await writeFile(source, 'console.log("REA_BUN_VERIFICATION");\n');
|
||||
const docker = process.argv.includes("--docker");
|
||||
const binary = join(root.path, "hello");
|
||||
const compiler = docker
|
||||
? "oven/bun:1.4.0"
|
||||
: (await run("bun", ["--version"])).stdout.trim();
|
||||
if (docker) {
|
||||
await run("docker", [
|
||||
"run",
|
||||
"--rm",
|
||||
"--network=none",
|
||||
"-v",
|
||||
`${root.path}:/work`,
|
||||
"-w",
|
||||
"/work",
|
||||
"oven/bun:1.4.0",
|
||||
"build",
|
||||
"--compile",
|
||||
"hello.ts",
|
||||
"--outfile",
|
||||
"hello",
|
||||
]);
|
||||
} else {
|
||||
await run("bun", ["build", "--compile", source, "--outfile", binary]);
|
||||
}
|
||||
const cli = async (command) =>
|
||||
JSON.parse(
|
||||
(
|
||||
await run(
|
||||
process.execPath,
|
||||
["scripts/rea.mjs", command, binary, "--json"],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
TMPDIR: root.path,
|
||||
TMP: root.path,
|
||||
TEMP: root.path,
|
||||
},
|
||||
},
|
||||
)
|
||||
).stdout,
|
||||
);
|
||||
const inspected = parseEvidence(
|
||||
await cli("inspect-artifact"),
|
||||
).normalized_result;
|
||||
const graph = inspected.substeps.find(
|
||||
(step) => step.operation === "inventory_artifact",
|
||||
)?.evidence.normalized_result;
|
||||
assert.equal(graph.manifest.root_format, "bun-standalone");
|
||||
const module = graph.occurrences.find(
|
||||
(row) =>
|
||||
row.format_metadata?.kind === "bun-standalone-entry" &&
|
||||
row.format_metadata.region === "contents" &&
|
||||
row.format_metadata.entry_point,
|
||||
);
|
||||
assert.ok(module);
|
||||
const extracted = parseEvidence(
|
||||
await cli("extract-artifact"),
|
||||
).normalized_result;
|
||||
assert.equal(extracted.containment_verified, true);
|
||||
assert.match(
|
||||
await readFile(join(extracted.output_root, module.logical_path), "utf8"),
|
||||
/REA_BUN_VERIFICATION/u,
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
compiler,
|
||||
host: `${process.platform}-${process.arch}`,
|
||||
docker,
|
||||
root_format: graph.manifest.root_format,
|
||||
entry_point: module.logical_path,
|
||||
sha256: graph.nodes.find(
|
||||
(node) => node.artifact_id === module.artifact_id,
|
||||
)?.sha256,
|
||||
extraction_verified: true,
|
||||
target_executed: false,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}, [
|
||||
async () => {
|
||||
await commands.close();
|
||||
commandsClosed = true;
|
||||
},
|
||||
async () => {
|
||||
if (commandsClosed) await root.close();
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,309 @@
|
||||
// Run from the repository root after `npm run build:cached` on macOS arm64,
|
||||
// with Hopper and the Xcode command-line tools installed. No target is executed.
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { artifactMcpCall } from "./lib/artifact-e2e.mjs";
|
||||
import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs";
|
||||
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js";
|
||||
|
||||
assert.equal(process.platform, "darwin", "requires macOS");
|
||||
assert.equal(
|
||||
process.arch,
|
||||
"arm64",
|
||||
"requires macOS arm64 (fixture tail branch is ARM64)",
|
||||
);
|
||||
const root = process.cwd();
|
||||
const require = createRequire(join(root, "package.json"));
|
||||
const { Client } = await import(
|
||||
pathToFileURL(require.resolve("@modelcontextprotocol/client"))
|
||||
);
|
||||
const { StdioClientTransport } = await import(
|
||||
pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio"))
|
||||
);
|
||||
const { parseConfig } = await import(
|
||||
pathToFileURL(join(root, "dist/config/parseConfig.js"))
|
||||
);
|
||||
const config = parseConfig(process.env);
|
||||
if (!config.ok) throw config.error;
|
||||
await access(config.value.hopperLauncherPath, constants.X_OK);
|
||||
const run = promisify(execFile);
|
||||
const clang = (await run("xcrun", ["--find", "clang"])).stdout.trim();
|
||||
const sdk = (
|
||||
await run("xcrun", ["--sdk", "macosx", "--show-sdk-path"])
|
||||
).stdout.trim();
|
||||
const directory = await mkdtemp(join("/tmp", "rea-hopper-callers-verifier-"));
|
||||
const fixture = join(directory, "tail-call");
|
||||
const large = join(directory, "long-scan");
|
||||
const source = fileURLToPath(
|
||||
new URL("./fixtures/hopper-tail-call.c", import.meta.url),
|
||||
);
|
||||
const digest = async (path) =>
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
const sessionsBefore = await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
);
|
||||
const ownedProcessIds = new Set();
|
||||
const client = new Client({ name: "hopper-callers-verifier", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: ["dist/main.js"],
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
REA_ANALYSIS_PROVIDER: "hopper",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_HOPPER_CALLER_SCAN_BUDGET_MS: "5000",
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
const call = async (name, args, options = {}) => {
|
||||
process.stderr.write(`calling ${name}\n`);
|
||||
const result = await artifactMcpCall(
|
||||
client,
|
||||
{ name, arguments: args },
|
||||
{ timeout: 120000, ...options },
|
||||
);
|
||||
assert.notEqual(result.isError, true, `${name}: ${JSON.stringify(result)}`);
|
||||
return (
|
||||
result.structuredContent ??
|
||||
JSON.parse(result.content.find((x) => x.type === "text").text)
|
||||
);
|
||||
};
|
||||
const address = (result) => {
|
||||
const match = result.normalized_result.match(/^(0x[0-9a-f]+):/mu);
|
||||
assert(match, "Hopper assembly has no instruction address");
|
||||
return match[1];
|
||||
};
|
||||
const matchingRef = (ref, site, from, to) =>
|
||||
ref.source_address === site &&
|
||||
ref.source_procedure?.address === from &&
|
||||
ref.target_procedure?.address === to &&
|
||||
ref.call?.classification === "direct";
|
||||
try {
|
||||
await run(clang, [
|
||||
"-isysroot",
|
||||
sdk,
|
||||
"-arch",
|
||||
"arm64",
|
||||
"-O1",
|
||||
source,
|
||||
"-o",
|
||||
fixture,
|
||||
]);
|
||||
const lines = ["#include <stdio.h>"];
|
||||
for (let i = 0; i < 4000; i++) {
|
||||
const id = String(i).padStart(4, "0");
|
||||
lines.push(
|
||||
`__attribute__((noinline, used)) int rea_progress_${id}(void) { puts("REA_PROGRESS_${id}"); return ${i}; }`,
|
||||
);
|
||||
}
|
||||
lines.push("int main(void) { return rea_progress_0000(); }");
|
||||
const largeSource = join(directory, "long-scan.c");
|
||||
await writeFile(largeSource, lines.join("\n"));
|
||||
await run(clang, [
|
||||
"-isysroot",
|
||||
sdk,
|
||||
"-arch",
|
||||
"arm64",
|
||||
"-O1",
|
||||
largeSource,
|
||||
"-o",
|
||||
large,
|
||||
]);
|
||||
const before = await digest(fixture),
|
||||
beforeLarge = await digest(large);
|
||||
await client.connect(transport);
|
||||
if (transport.pid === null)
|
||||
throw new Error("MCP process identity unavailable");
|
||||
ownedProcessIds.add(transport.pid);
|
||||
let notifications = 0;
|
||||
const original = transport.onmessage;
|
||||
transport.onmessage = (message) => {
|
||||
if (message.method === "notifications/progress") notifications++;
|
||||
original?.(message);
|
||||
};
|
||||
await call("open_binary", { path: fixture });
|
||||
const assembly = await call("procedure_assembly", {
|
||||
procedure: "_call_direct",
|
||||
});
|
||||
const branch = assembly.normalized_result.match(
|
||||
/^(0x[0-9a-f]+):\s+b\s+_target_fn\s*$/mu,
|
||||
);
|
||||
assert(
|
||||
branch,
|
||||
`compiler did not produce expected tail branch: ${assembly.normalized_result}`,
|
||||
);
|
||||
const site = branch[1],
|
||||
from = address(assembly);
|
||||
const to = address(
|
||||
await call("procedure_assembly", { procedure: "_target_fn" }),
|
||||
);
|
||||
assert(
|
||||
(
|
||||
await call("procedure_callees", { procedure: from })
|
||||
).normalized_result.includes(to),
|
||||
);
|
||||
const callers = await call("procedure_callers", {
|
||||
procedure: to,
|
||||
detail: "complete",
|
||||
});
|
||||
assert(callers.normalized_result.includes(from), "tail caller missing");
|
||||
assert.equal(callers.relationship_coverage.status, "unknown");
|
||||
assert(
|
||||
callers.raw_result.forward_observations.callee_inventory_callers.some(
|
||||
(x) => x.address === from,
|
||||
),
|
||||
"forward caller provenance missing",
|
||||
);
|
||||
const refs = (
|
||||
await call("procedure_references", { procedure: to, direction: "incoming" })
|
||||
).normalized_result.references;
|
||||
assert(
|
||||
refs.some((x) => matchingRef(x, site, from, to)),
|
||||
"exact incoming tail reference missing",
|
||||
);
|
||||
const dossier = (
|
||||
await call("analyze_function", { procedure: to, detail: "complete" })
|
||||
).normalized_result;
|
||||
assert(
|
||||
dossier.callers.some((x) => x.address === from),
|
||||
"dossier caller missing",
|
||||
);
|
||||
assert(
|
||||
dossier.incoming_references.some((x) => matchingRef(x, site, from, to)),
|
||||
"dossier reference missing",
|
||||
);
|
||||
const graph = (
|
||||
await call("get_call_graph", {
|
||||
address: to,
|
||||
direction: "backward",
|
||||
detail: "complete",
|
||||
})
|
||||
).normalized_result.graph;
|
||||
assert(
|
||||
Object.values(graph)
|
||||
.flat()
|
||||
.some((x) => x.address === to && x.calls.includes(from)),
|
||||
"backward graph edge missing",
|
||||
);
|
||||
const path = (
|
||||
await call("trace_call_path", {
|
||||
start: to,
|
||||
goal: from,
|
||||
direction: "backward",
|
||||
})
|
||||
).normalized_result;
|
||||
assert.equal(path.goal_status, "reached");
|
||||
assert(
|
||||
path.edges.some(
|
||||
(x) => x.source_address === from && x.target_address === to,
|
||||
),
|
||||
);
|
||||
await call("close_binary", {});
|
||||
assert.equal(await digest(fixture), before);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
event: "tail_call_verified",
|
||||
from,
|
||||
site,
|
||||
to,
|
||||
sha256: before,
|
||||
}),
|
||||
);
|
||||
await call("open_binary", { path: large });
|
||||
await call("procedure_assembly", { procedure: "_rea_progress_0000" });
|
||||
const query = {
|
||||
address: "_rea_progress_0000",
|
||||
direction: "backward",
|
||||
detail: "complete",
|
||||
};
|
||||
const count = notifications;
|
||||
await call("get_call_graph", query);
|
||||
assert.equal(notifications, count, "unsolicited progress without token");
|
||||
const progress = [];
|
||||
await call("get_call_graph", query, { onprogress: (x) => progress.push(x) });
|
||||
assert(
|
||||
progress.every((x, i) => i === 0 || x.progress > progress[i - 1].progress),
|
||||
"progress not monotonic",
|
||||
);
|
||||
assert(
|
||||
progress.some((x) =>
|
||||
x.message?.includes("hopper_caller_reconciliation: Examined"),
|
||||
),
|
||||
"missing examined-count progress",
|
||||
);
|
||||
const controller = new AbortController();
|
||||
let partial = false;
|
||||
let resolved = false;
|
||||
try {
|
||||
await artifactMcpCall(
|
||||
client,
|
||||
{ name: "get_call_graph", arguments: query },
|
||||
{
|
||||
timeout: 120000,
|
||||
signal: controller.signal,
|
||||
onprogress: (x) => {
|
||||
const match = x.message?.match(
|
||||
/hopper_caller_reconciliation: Examined (\d+) of (\d+) procedures/u,
|
||||
);
|
||||
if (match && Number(match[1]) < Number(match[2])) {
|
||||
partial = true;
|
||||
controller.abort("cancel during real scan");
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
resolved = true;
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) throw error;
|
||||
}
|
||||
assert(
|
||||
partial && !resolved,
|
||||
"did not observe cancellation after partial scan progress; cancellation coverage unavailable on this host",
|
||||
);
|
||||
await client.ping();
|
||||
const retry = await call("get_call_graph", query);
|
||||
assert(retry.normalized_result.graph, "retry did not return graph");
|
||||
await call("close_binary", {});
|
||||
assert.equal(await digest(large), beforeLarge);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
event: "progress_cancel_retry_verified",
|
||||
progress,
|
||||
sha256: beforeLarge,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const leaked = [
|
||||
...(await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
ownedProcessIds,
|
||||
)),
|
||||
].filter((path) => !sessionsBefore.has(path));
|
||||
if (leaked.length === 0)
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
assert.deepEqual(
|
||||
leaked,
|
||||
[],
|
||||
`Hopper resources remain: ${leaked.join(", ")}; fixture retained at ${directory}`,
|
||||
);
|
||||
}
|
||||
@@ -10,10 +10,14 @@ import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceS
|
||||
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
|
||||
import {
|
||||
artifactCliEvidence,
|
||||
artifactMcpCall,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
import { nativeDispatchMetadataResultSchema } from "../../../dist/domain/native/objcSwiftMetadata.js";
|
||||
import { analysisErrorProjectionSchema } from "../../../dist/contracts/errorSchemas.js";
|
||||
import { cliOperationFailurePayload } from "../../../dist/cliLogging.js";
|
||||
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
@@ -344,6 +348,74 @@ try {
|
||||
commands,
|
||||
target_sha256: cliResult.target_sha256,
|
||||
});
|
||||
for (const { section, size, reason } of [
|
||||
{
|
||||
section: "__objc_classlist",
|
||||
size: 12,
|
||||
reason: "Misaligned Objective-C class-list size",
|
||||
},
|
||||
{
|
||||
section: "__swift5_types",
|
||||
size: 6,
|
||||
reason: "Misaligned Swift type section",
|
||||
},
|
||||
]) {
|
||||
const bytes = Buffer.alloc(4096);
|
||||
bytes.writeUInt32LE(0xfeedfacf);
|
||||
bytes.writeUInt32LE(0x0100000c, 4);
|
||||
bytes.writeUInt32LE(2, 12);
|
||||
bytes.writeUInt32LE(1, 16);
|
||||
bytes.writeUInt32LE(152, 20);
|
||||
bytes.writeUInt32LE(0x19, 32); // LC_SEGMENT_64
|
||||
bytes.writeUInt32LE(152, 36);
|
||||
bytes.write("__DATA", 40);
|
||||
bytes.writeBigUInt64LE(0x100000000n, 56);
|
||||
bytes.writeBigUInt64LE(4096n, 64);
|
||||
bytes.writeBigUInt64LE(4096n, 80);
|
||||
bytes.writeUInt32LE(7, 88);
|
||||
bytes.writeUInt32LE(5, 92);
|
||||
bytes.writeUInt32LE(1, 96);
|
||||
bytes.write(section, 104);
|
||||
bytes.write("__DATA", 120);
|
||||
bytes.writeBigUInt64LE(0x100000200n, 136);
|
||||
bytes.writeBigUInt64LE(BigInt(size), 144);
|
||||
bytes.writeUInt32LE(512, 152);
|
||||
const path = join(root, `malformed-${section}.macho`);
|
||||
await writeFile(path, bytes);
|
||||
const target = await parseBinaryTarget(path);
|
||||
assert.ok(
|
||||
target.ok,
|
||||
"Malformed section fixture must pass target admission",
|
||||
);
|
||||
let cliFailure;
|
||||
try {
|
||||
await artifactCliEvidence("inspect-native-dispatch-metadata", path);
|
||||
assert.fail("Malformed metadata unexpectedly succeeded");
|
||||
} catch (cause) {
|
||||
assert.equal(cause.code, 1);
|
||||
cliFailure = analysisErrorProjectionSchema.parse(
|
||||
cliOperationFailurePayload(JSON.parse(cause.stdout)),
|
||||
);
|
||||
}
|
||||
assert.equal(cliFailure.code, "unsupported_target");
|
||||
assert.equal(cliFailure.category, "unsupported_target");
|
||||
assert.deepEqual(cliFailure.details, {
|
||||
operation: "inspect_native_dispatch_metadata",
|
||||
path: target.value.path,
|
||||
reason,
|
||||
});
|
||||
await withArtifactMcp(path, async (client) => {
|
||||
const failure = requireMcpToolError(
|
||||
await artifactMcpCall(client, {
|
||||
name: "inspect_native_dispatch_metadata",
|
||||
arguments: {},
|
||||
}),
|
||||
);
|
||||
assert.equal(failure.code, cliFailure.code);
|
||||
assert.deepEqual(failure.details, cliFailure.details);
|
||||
});
|
||||
results.push({ variant: `malformed-${section}`, reason });
|
||||
}
|
||||
for (const mode of LINK_MODES) {
|
||||
const objcPath = join(root, `fixture-${mode.mode}`);
|
||||
await exec("/usr/bin/xcrun", [
|
||||
|
||||
@@ -134,6 +134,15 @@ export async function verifyNativeProvenance(app, executable) {
|
||||
]);
|
||||
|
||||
if (operation === "inspect_macho") {
|
||||
const encryption = cli.normalized_result.encryption;
|
||||
assert.equal(encryption.exhaustive, true);
|
||||
assert.ok(encryption.total > 0);
|
||||
assert.equal(encryption.items.length, encryption.total);
|
||||
for (const slice of encryption.items) {
|
||||
assert.equal(slice.status, "absent");
|
||||
assert.deepEqual(slice.commands, []);
|
||||
assert.equal(slice.range, null);
|
||||
}
|
||||
const delayed = cli.normalized_result.load_commands.items.find(
|
||||
({ fields }) => fields.name === "@rpath/libdelay.dylib (offset 28)",
|
||||
);
|
||||
|
||||
@@ -156,6 +156,9 @@ check(
|
||||
files,
|
||||
/`[^`\n]*\\0/,
|
||||
[
|
||||
// Bundle fingerprint: fixed packaged paths (no NUL/newline) and 64 hex
|
||||
// digest characters per record, never guide contents. Preserve v1 locators.
|
||||
"src/application/GuidanceAccess.ts",
|
||||
"src/application/javascript/ElectronBoundaryAnalysis.ts",
|
||||
"src/application/javascript/ElectronBoundaryGraphContext.ts",
|
||||
"src/application/javascript/JavaScriptArtifactGraphContext.ts",
|
||||
|
||||
Reference in new issue
Block a user