diff --git a/apps/dispatcher/src/grant-catalog.ts b/apps/dispatcher/src/grant-catalog.ts index 5a093f37..82707cf9 100644 --- a/apps/dispatcher/src/grant-catalog.ts +++ b/apps/dispatcher/src/grant-catalog.ts @@ -183,6 +183,19 @@ export const RUN_GRANTS: Readonly> = { // capability, not from the container — so `cf-api` is deliberately absent. "finops-audit": { profiles: ["public-repo-read"], rollout: "legacy" }, "contextful-measures": { profiles: ["public-repo-read", "rust-install"], rollout: "legacy" }, + "contextful-protocol": { profiles: ["public-repo-read", "rust-install"], rollout: "legacy", facadeGaps: ["container-artifact"] }, + // The parent uses Worker-side GitHub and child-run capabilities only. + "contextful-release": { profiles: [], rollout: "legacy" }, + // Cells and the formula clone the public tree, build with Cargo, and upload + // container artifacts; the substrate facade does not serve that upload yet. + "contextful-release-cell": { + profiles: ["public-repo-read", "rust-install"], rollout: "legacy", + facadeGaps: ["container-artifact"], + }, + "contextful-release-formula": { + profiles: ["public-repo-read", "rust-install"], rollout: "legacy", + facadeGaps: ["container-artifact"], + }, // --- deploy -------------------------------------------------------------- // `cf-api` is where `wrangler deploy` sends its bytes; `wrangler` itself diff --git a/apps/dispatcher/src/index.test.ts b/apps/dispatcher/src/index.test.ts index 031b715f..4d072e79 100644 --- a/apps/dispatcher/src/index.test.ts +++ b/apps/dispatcher/src/index.test.ts @@ -110,6 +110,7 @@ describe("GET /health", () => { "ci-triage-pr", "contextful-gate", "contextful-measures", + "contextful-protocol", "contextful-release", "contextful-release-cell", "contextful-release-formula", diff --git a/apps/dispatcher/src/registry.ts b/apps/dispatcher/src/registry.ts index 230c4ce3..9637f37f 100644 --- a/apps/dispatcher/src/registry.ts +++ b/apps/dispatcher/src/registry.ts @@ -16,6 +16,7 @@ import { ciTriagePr, contextfulGate, contextfulMeasures, + contextfulProtocol, demoReel, deploySmoke, emailOtpLogin, @@ -78,6 +79,7 @@ const RUN_REGISTRY: Record> = { [emailOtpLogin.name]: emailOtpLogin as Run, [finopsAudit.name]: finopsAudit as Run, [contextfulMeasures.name]: contextfulMeasures as Run, + [contextfulProtocol.name]: contextfulProtocol as Run, [workerDeploy.name]: workerDeploy as Run, // Presentation stage for a captured demo: consumes a product-demo // execution's demo-bundle/v1 and renders a deck (+ MP4 when the image diff --git a/runs/contextful-protocol.test.ts b/runs/contextful-protocol.test.ts new file mode 100644 index 00000000..70c45f04 --- /dev/null +++ b/runs/contextful-protocol.test.ts @@ -0,0 +1,155 @@ +import { it } from "@effect/vitest"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Effect, Exit } from "effect"; +import { describe, expect } from "vitest"; +import { makeCFRuntimeTest } from "@fractalboxdev/flare-dispatch-core/testing"; +import { contextfulProtocol, protocolCommand } from "./contextful-protocol"; + +const sha = "a".repeat(40); +const input = { repo: "fractalboxdev/contextful", ref: "refs/heads/main", firedAt: 1791253020000 }; + +describe("contextful-protocol", () => { + it("restores the installed Lean environment in a fresh command process", () => { + const dir = mkdtempSync(join(tmpdir(), "protocol-command-")); + try { + const elanHome = join(dir, "elan"); + mkdirSync(join(elanHome, "bin"), { recursive: true }); + mkdirSync(join(dir, "formal")); + writeFileSync(join(dir, "formal/lean-toolchain"), "leanprover/lean4:v4.29.1\n"); + writeFileSync( + join(elanHome, "bin/cargo"), + `#!${process.execPath}\nconsole.log(JSON.stringify({toolchain:process.env.ELAN_TOOLCHAIN,requireLean:process.env.CONTEXTFUL_REQUIRE_LEAN,path:process.env.PATH,args:process.argv.slice(2)}));\n`, + { mode: 0o755 }, + ); + const result = spawnSync("sh", ["-c", protocolCommand("123")], { + cwd: dir, + env: { + ...process.env, + ELAN_HOME: elanHome, + ELAN_TOOLCHAIN: "unrelated", + CONTEXTFUL_REQUIRE_LEAN: "0", + }, + encoding: "utf8", + }); + expect(result.status).toBe(0); + const observed = JSON.parse(result.stdout); + expect(observed.toolchain).toBe("leanprover/lean4:v4.29.1"); + expect(observed.requireLean).toBe("1"); + expect(observed.path.split(":")[0]).toBe(join(elanHome, "bin")); + expect(observed.args.slice(-6)).toEqual([ + "formal", + "protocol-differential", + "--seed", + "123", + "--cases", + "256", + ]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, 30000); + it("routes the weekly schedule with one identity per tick", () => { + const schedule = contextfulProtocol.schedules?.[0]; + expect(schedule?.cron).toBe("17 3 * * 0"); + expect(schedule?.inputs({ cron: "17 3 * * 0", firedAt: input.firedAt })).toEqual(input); + expect(schedule?.idempotencyKey({ cron: "17 3 * * 0", firedAt: input.firedAt })).toBe( + `contextful-protocol:${input.firedAt}`, + ); + }); + + it.effect("checks the pinned model before exploring a checkpointed fresh u64 seed", () => { + const { layer, handles } = makeCFRuntimeTest({ + github: { branchHeads: { "fractalboxdev/contextful:main": sha } }, + sandboxProgram: { + "gate --predecessors --stage toolchain": { exitCode: 0 }, + randomBytes: { exitCode: 0, stdout: "18446744073709551615\n" }, + "protocol-differential": { exitCode: 0 }, + "formal check": { exitCode: 0 }, + }, + }); + return Effect.gen(function* () { + const out = yield* contextfulProtocol.run(input); + expect(out.commit).toBe(sha); + expect(out.seed).toBe("18446744073709551615"); + expect(handles.sandbox.clones).toEqual([{ repo: input.repo, sha }]); + const commands = handles.sandbox.execs.map((exec) => exec.command); + expect(commands[0]).toContain("gate --predecessors --stage toolchain"); + expect(commands[1]).toContain("formal check"); + expect(commands[3]).toBe(protocolCommand(out.seed)); + expect(commands[3]).toContain('export PATH="${ELAN_HOME:-$HOME/.elan}/bin:$PATH"'); + expect(commands[3]).toContain('export ELAN_TOOLCHAIN="$(cat formal/lean-toolchain)"'); + expect(commands[3]).toContain("--no-default-features"); + expect(commands[3]).toContain("--cases 256"); + expect(handles.artifact.uploads).toHaveLength(3); + }).pipe(Effect.provide(layer)); + }); + + it.effect( + "preserves the failing seed, log and reduced regressions before reporting drift", + () => { + const { layer, handles } = makeCFRuntimeTest({ + github: { branchHeads: { "fractalboxdev/contextful:main": sha } }, + sandboxProgram: { + "gate --predecessors --stage toolchain": { exitCode: 0 }, + randomBytes: { exitCode: 0, stdout: "123\n" }, + "protocol-differential": { exitCode: 3 }, + "formal check": { exitCode: 0 }, + }, + sandboxFiles: { + "/workspace/contextful/formal/protocol/regressions.jsonl": '{"seed":123}\n', + }, + }); + return Effect.gen(function* () { + const out = yield* Effect.exit(contextfulProtocol.run(input)); + expect(Exit.isFailure(out)).toBe(true); + expect(handles.artifact.uploads.map((upload) => upload.name)).toContain( + "protocol-regressions.jsonl", + ); + expect(handles.sandbox.execs[3]?.command).toContain("--seed 123"); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect("rejects a different repository before allocating a sandbox", () => { + const { layer, handles } = makeCFRuntimeTest({}); + return Effect.gen(function* () { + const out = yield* Effect.exit(contextfulProtocol.run({ ...input, repo: "other/repo" })); + expect(Exit.isFailure(out)).toBe(true); + expect(handles.sandbox.clones).toHaveLength(0); + }).pipe(Effect.provide(layer)); + }); + + it.effect("keeps malformed seed output out of the shell command", () => { + const { layer, handles } = makeCFRuntimeTest({ + github: { branchHeads: { "fractalboxdev/contextful:main": sha } }, + sandboxProgram: { + "gate --predecessors --stage toolchain": { exitCode: 0 }, + randomBytes: { exitCode: 0, stdout: "123; touch injected\n" }, + "formal check": { exitCode: 0 }, + }, + }); + return Effect.gen(function* () { + const out = yield* Effect.exit(contextfulProtocol.run(input)); + expect(Exit.isFailure(out)).toBe(true); + expect(handles.sandbox.execs).toHaveLength(3); + }).pipe(Effect.provide(layer)); + }); + + it.effect("retains the toolchain failure log and starts no model check", () => { + const { layer, handles } = makeCFRuntimeTest({ + github: { branchHeads: { "fractalboxdev/contextful:main": sha } }, + sandboxProgram: { "gate --predecessors --stage toolchain": { exitCode: 7 } }, + }); + return Effect.gen(function* () { + const out = yield* Effect.exit(contextfulProtocol.run(input)); + expect(Exit.isFailure(out)).toBe(true); + expect(handles.sandbox.execs).toHaveLength(1); + expect(handles.artifact.uploads.map((upload) => upload.name)).toEqual([ + "protocol-toolchain.log", + ]); + }).pipe(Effect.provide(layer)); + }); +}); diff --git a/runs/contextful-protocol.ts b/runs/contextful-protocol.ts new file mode 100644 index 00000000..635c309c --- /dev/null +++ b/runs/contextful-protocol.ts @@ -0,0 +1,175 @@ +import { Effect, Schema } from "effect"; +import { + AcceptanceFailed, + artifact, + defineRun, + github, + sandbox, + StepFailed, + step, +} from "@fractalboxdev/flare-dispatch-core"; +import { workspace } from "@fractalboxdev/flare-dispatch-core/primitives"; + +const REPO = "fractalboxdev/contextful"; +const REF = "refs/heads/main"; +const TOOLCHAIN_COMMAND = + "cargo run --locked -q -p contextful-ci -- gate --predecessors --stage toolchain"; +const PINNED_ENVIRONMENT = + 'export PATH="${ELAN_HOME:-$HOME/.elan}/bin:$PATH"; export ELAN_TOOLCHAIN="$(cat formal/lean-toolchain)"; export CONTEXTFUL_REQUIRE_LEAN=1; '; +const FORMAL_COMMAND = `${PINNED_ENVIRONMENT}cargo run --locked -q -p contextful-cli --no-default-features --bin contextful -- formal check`; +const SEED_COMMAND = `node -e 'console.log(require("node:crypto").randomBytes(8).readBigUInt64LE().toString())'`; +const REGRESSIONS = "formal/protocol/regressions.jsonl"; +const COMMAND_TIMEOUT_SEC = 1800; +const Input = Schema.Struct({ repo: Schema.String, ref: Schema.String, firedAt: Schema.Number }); +const Output = Schema.Struct({ commit: Schema.String, seed: Schema.String, logUrl: Schema.String }); + +/** The gate installs elan; the separate command restores its pinned executable environment. */ +export const protocolCommand = (seed: string): string => + `${PINNED_ENVIRONMENT}cargo run --locked -q -p contextful-cli --no-default-features --bin contextful -- formal protocol-differential --seed ${seed} --cases 256`; + +export const contextfulProtocol = defineRun({ + name: "contextful-protocol", + version: "1.0.0", + schedules: [ + { + cron: "17 3 * * 0", + idempotencyKey: ({ firedAt }) => `contextful-protocol:${firedAt}`, + inputs: ({ firedAt }): typeof Input.Type => ({ repo: REPO, ref: REF, firedAt }), + }, + ], + inputs: Input, + outputs: Output, + limits: { maxDurationSec: 7200, admissionMaxQueueAgeSec: 21600 }, + run: (input) => + Effect.gen(function* () { + if (input.repo !== REPO || input.ref !== REF) { + return yield* Effect.fail( + new StepFailed({ + step: "scope", + cause: "protocol exploration targets the Contextful default branch", + }), + ); + } + const commit = yield* step("resolve-head", () => + github.branchHead({ repo: REPO, branch: "main" }), + ); + const ws = yield* step("checkout", () => workspace({ repo: REPO, sha: commit })); + const provisioned = yield* step( + "toolchain", + () => + sandbox.exec({ + container: ws.container, + cwd: ws.dir, + command: TOOLCHAIN_COMMAND, + timeoutSec: COMMAND_TIMEOUT_SEC, + }), + { timeoutSec: COMMAND_TIMEOUT_SEC + 120, retries: 0 }, + ); + const toolchainLog = yield* step("toolchain-log", () => + artifact.upload({ + name: "protocol-toolchain.log", + path: provisioned.logPath, + signedUrlTTL: "30 days", + }), + ); + if (provisioned.exitCode !== 0) { + return yield* Effect.fail( + new AcceptanceFailed({ + exitCode: provisioned.exitCode, + summaryMd: `The pinned toolchain fails on ${commit}. [Log](${toolchainLog})`, + }), + ); + } + const formal = yield* step( + "formal-gate", + () => + sandbox.exec({ + container: ws.container, + cwd: ws.dir, + command: FORMAL_COMMAND, + timeoutSec: COMMAND_TIMEOUT_SEC, + }), + { timeoutSec: COMMAND_TIMEOUT_SEC + 120, retries: 0 }, + ); + const formalLog = yield* step("formal-log", () => + artifact.upload({ + name: "protocol-formal.log", + path: formal.logPath, + signedUrlTTL: "30 days", + }), + ); + if (formal.exitCode !== 0) { + return yield* Effect.fail( + new AcceptanceFailed({ + exitCode: formal.exitCode, + summaryMd: `The pinned formal gate fails on ${commit}. [Log](${formalLog})`, + }), + ); + } + const generated = yield* step("fresh-seed", () => + sandbox.exec({ + container: ws.container, + cwd: ws.dir, + command: SEED_COMMAND, + timeoutSec: 60, + }), + ); + const seed = generated.stdout.trim(); + if ( + generated.exitCode !== 0 || + !/^(0|[1-9][0-9]{0,19})$/.test(seed) || + BigInt(seed) > 18446744073709551615n + ) { + return yield* Effect.fail( + new StepFailed({ + step: "fresh-seed", + cause: "seed generation returns no unsigned 64-bit integer", + }), + ); + } + const compared = yield* step( + "protocol-exploration", + () => + sandbox.exec({ + container: ws.container, + cwd: ws.dir, + command: protocolCommand(seed), + timeoutSec: COMMAND_TIMEOUT_SEC, + }), + { timeoutSec: COMMAND_TIMEOUT_SEC + 120, retries: 0 }, + ); + const logUrl = yield* step("protocol-log", () => + artifact.upload({ + name: "protocol-exploration.log", + path: compared.logPath, + signedUrlTTL: "30 days", + }), + ); + if (compared.exitCode !== 0) { + const saved = yield* step("saved-regressions", () => + sandbox + .readFile({ container: ws.container, path: `${ws.dir}/${REGRESSIONS}` }) + .pipe(Effect.catchTag("ReadFileFailed", () => Effect.succeed(undefined))), + ); + const regressionsUrl = + saved === undefined + ? undefined + : yield* step("regressions-artifact", () => + artifact.upload({ + name: "protocol-regressions.jsonl", + path: `${ws.dir}/${REGRESSIONS}`, + container: ws.container, + contentType: "application/x-ndjson", + signedUrlTTL: "30 days", + }), + ); + return yield* Effect.fail( + new AcceptanceFailed({ + exitCode: compared.exitCode, + summaryMd: `Protocol exploration fails on ${commit} with seed ${seed}. [Log](${logUrl})${regressionsUrl === undefined ? "" : ` [Regressions](${regressionsUrl})`}`, + }), + ); + } + return { commit, seed, logUrl }; + }), +}); diff --git a/runs/index.ts b/runs/index.ts index d3e2cf6e..96424796 100644 --- a/runs/index.ts +++ b/runs/index.ts @@ -34,4 +34,5 @@ export { contextfulGate } from "./contextful-gate"; export { emailOtpLogin } from "./email-otp-login"; export { finopsAudit } from "./finops-audit"; export { contextfulMeasures } from "./contextful-measures"; +export { contextfulProtocol } from "./contextful-protocol"; export { demoReel } from "./demo-reel"; diff --git a/runs/sandbox-image.test.ts b/runs/sandbox-image.test.ts index 4ffad82a..e7b79a34 100644 --- a/runs/sandbox-image.test.ts +++ b/runs/sandbox-image.test.ts @@ -58,7 +58,8 @@ describe("sandboxImage catalog", () => { r.sandboxImage !== undefined && r.sandboxImage !== "lean" && r.sandboxImage !== "browser" && - r.sandboxImage !== "agent", + r.sandboxImage !== "agent" && + r.sandboxImage !== "release", ) .map((r) => r.name); expect(unknown).toEqual([]); diff --git a/wrangler.jsonc b/wrangler.jsonc index dc45b903..c16523c6 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -29,6 +29,7 @@ "0 6 * * *", // ci-triage-pr — daily "0 7 * * 1", // finops-audit — Mondays "17 2 * * *", // contextful-measures — daily + "17 3 * * 0", // contextful-protocol — weekly ], },