From e6b7a82a3261d0d3d30f5548338cd454b3b7362e Mon Sep 17 00:00:00 2001 From: chtnnh <59027776+chtnnh@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:05:56 +0400 Subject: [PATCH] fix: use signature validity for receipt sealed report and commit /push allowed Know-Code-Verified: 574a346e9d10ba631a9e6531f1d53c2a63d3b38f732019e676d10b56430e4083 --- packages/cli/src/cli-surface.test.ts | 107 ++++++++++++++++++++++++++- packages/cli/src/commands/status.ts | 36 +++++++-- 2 files changed, 134 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/cli-surface.test.ts b/packages/cli/src/cli-surface.test.ts index 0d23573..e4ebd50 100644 --- a/packages/cli/src/cli-surface.test.ts +++ b/packages/cli/src/cli-surface.test.ts @@ -10,8 +10,23 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { writeConfig } from "./config.js"; +import { + answersDigest, + writeAnswers, + writeGrade, + writeTaught, +} from "./attest.js"; +import { readConfig, writeConfig } from "./config.js"; +import { cmdStatus } from "./commands/status.js"; +import { materializedTreeOid, readGate, writeGate } from "./gate.js"; +import { computeDiffContext } from "./hash.js"; import { readRangeSession, writeRangeSeal } from "./range.js"; +import { + initAttestKey, + readAttestMeta, + signPayload, + verifyPayload, +} from "./seal.js"; import { commitAll, git, @@ -173,6 +188,96 @@ describe("cli surface (spawned)", () => { } }); + it("status separates receipt signature validity from a missing pending trailer", () => { + const { root, cleanup } = withTempRepo("kc-cli-status-receipt-"); + const attestHome = mkdtempSync(join(tmpdir(), "kc-attest-")); + const priorAttestHome = process.env.KNOW_CODE_ATTEST_HOME; + try { + setupRepo(root, liteConfig({ requireTrailer: true })); + const repoRoot = git(root, ["rev-parse", "--show-toplevel"]); + process.env.KNOW_CODE_ATTEST_HOME = attestHome; + initAttestKey(repoRoot, "test-passphrase"); + const hash = computeDiffContext(repoRoot, readConfig(repoRoot)).diffHash; + const gate = { + version: 1 as const, + diffHash: hash, + level: "lite" as const, + passedAt: new Date().toISOString(), + commitRange: "x", + baseRef: "y", + headRef: git(repoRoot, ["rev-parse", "HEAD"]), + gatedTreeOid: materializedTreeOid(repoRoot), + }; + const signed = signPayload(repoRoot, "test-passphrase", gate); + writeGate(repoRoot, { ...gate, ...signed }); + const pub = readAttestMeta(repoRoot)?.pubKey; + assert.ok(pub, "expected test attest public key"); + assert.equal( + verifyPayload( + pub, + readGate(repoRoot) as unknown as Record & { + sig?: string; + keyId?: string; + }, + ), + true, + ); + writeTaught(repoRoot, { + version: 1, + diffHash: hash, + taughtAt: new Date().toISOString(), + skipped: false, + }); + writeFileSync(join(repoRoot, ".know-code", "quiz.json"), "{}\n"); + const answers = { + diffHash: hash, + answers: [{ id: "q1", answer: "understood" }], + submittedAt: new Date().toISOString(), + }; + writeAnswers(repoRoot, answers); + writeGrade(repoRoot, { + version: 1, + diffHash: hash, + score: 1, + passed: true, + gradedAt: new Date().toISOString(), + answersDigest: answersDigest(answers), + }); + + const previousCwd = process.cwd(); + const log = console.log; + let output = ""; + console.log = (line: string) => { + output += line; + }; + process.chdir(root); + let payload: Record; + try { + cmdStatus({ json: true }); + payload = JSON.parse(output); + } finally { + process.chdir(previousCwd); + console.log = log; + } + assert.equal(payload.allowed, false); + assert.equal(payload.attestReady, true); + assert.equal(payload.receiptSealed, true); + assert.deepEqual(payload.blockers, [ + { + step: "check", + message: "requireTrailer: HEAD missing Know-Code-Verified trailer", + command: 'know-code commit -m "…"', + }, + ]); + assert.equal(payload.nextStep, 'know-code commit -m "…"'); + } finally { + if (priorAttestHome === undefined) delete process.env.KNOW_CODE_ATTEST_HOME; + else process.env.KNOW_CODE_ATTEST_HOME = priorAttestHome; + rmSync(attestHome, { recursive: true, force: true }); + cleanup(); + } + }); + it("range continue --yes starts the next session at current HEAD", () => { const { root, cleanup } = withTempRepo("kc-cli-rangecont-"); try { diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts index d39c6c5..059d5e8 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/cli/src/commands/status.ts @@ -53,7 +53,8 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void { ); const session = readRangeSession(repoRoot); const receipt = readGateSafe(repoRoot); - const allowed = runCheck(repoRoot).allowed; + const check = runCheck(repoRoot); + const allowed = check.allowed; const from = mergeBase(repoRoot, ctx.baseRef, ctx.headRef); const stat = diffStat(repoRoot, from, ctx.headRef); const log = logOneline(repoRoot, from, ctx.headRef); @@ -67,6 +68,16 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void { const proposal = proposalR.value; const meta = readAttestMeta(repoRoot); const pub = meta?.pubKey; + const receiptSealed = + !!receipt && + !!pub && + verifyPayload( + pub, + receipt as unknown as Record & { + sig?: string; + keyId?: string; + }, + ); const taughtOk = !!taught && taught.diffHash === effectiveHash && @@ -79,6 +90,13 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void { verifyPayload(pub, grade as unknown as Record & { sig?: string; keyId?: string }); const pipeline = evaluatePipeline(repoRoot); + const blockers = + !allowed && pipeline.blockers.length === 0 && check.reason + ? [ + ...pipeline.blockers, + { step: "check", message: check.reason, command: check.next }, + ] + : pipeline.blockers; const unstaged = hasUnstagedTrackedChanges(repoRoot); const taughtStaleDetail = taughtR.corrupt ? "corrupt" @@ -95,8 +113,8 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void { const payload = { allowed, - nextStep: pipeline.nextStep, - blockers: pipeline.blockers, + nextStep: blockers[0]?.command ?? pipeline.nextStep, + blockers, level: config.level, baseBranch: config.baseBranch, attestKeyId: meta?.keyId || null, @@ -112,6 +130,7 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void { headRef: ctx.headRef, commitRange: ctx.commitRange, receipt, + receiptSealed, taught: taughtR.corrupt ? "corrupt" : taught?.diffHash === effectiveHash @@ -147,12 +166,13 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void { console.log(`know-code status`); console.log(` commit/push allowed: ${allowed ? "yes" : "no"}`); - if (pipeline.nextStep) { - console.log(` next: ${pipeline.nextStep}`); + const nextStep = blockers[0]?.command ?? pipeline.nextStep; + if (nextStep) { + console.log(` next: ${nextStep}`); } - if (opts.next !== false && pipeline.blockers.length) { + if (opts.next !== false && blockers.length) { console.log(` blockers:`); - for (const b of pipeline.blockers) { + for (const b of blockers) { console.log(` - ${b.step}: ${b.message}`); } } @@ -171,7 +191,7 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void { console.log(` base: ${ctx.baseRef}`); if (receipt) { console.log( - ` receipt: ${receipt.level} @ ${receipt.passedAt} (${receipt.diffHash.slice(0, 12)}…) sealed=${allowed ? "yes" : "no"}`, + ` receipt: ${receipt.level} @ ${receipt.passedAt} (${receipt.diffHash.slice(0, 12)}…) sealed=${receiptSealed ? "yes" : "no"}`, ); } else { console.log(` receipt: (none)`);