mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Merge commit '1a2b5b0fec32c2dddbfec76e2f0479de9cfc31c4' into torch/project-kernel
# Conflicts: # docs/TEST_INTEGRITY.md
This commit is contained in:
commit
dd62c77877
6 files changed
+294
-38
No files matched your search
@@ -2,6 +2,39 @@
|
||||
|
||||
Repository state outranks conversation memory. Query ownership before crossing a domain boundary.
|
||||
|
||||
## Fresh owner authorization: guarded integration and continued development (2026-10-01)
|
||||
|
||||
The owner explicitly answered the arbiter's two held decisions: "yes do whatever
|
||||
you need to do. And you can push commits to." This supersedes the earlier
|
||||
no-landing/no-push restrictions ONLY for continued TORCH development on
|
||||
rewrite/portable-agent-fleet and task-owned specialist branches. It is fresh
|
||||
human consent, not an automatic goal continuation.
|
||||
|
||||
Session Manager may resolve owner approval 2b2dfb35-46ac-4adb-ac14-5680f7a85e80,
|
||||
authorize and land exact request cce22c51-1fa8-4324-8e28-bf8cbfc56165 through
|
||||
TORCH's guarded serialized integration path after checking clean canonical state,
|
||||
exact candidate, ancestry and required PASS receipts. Subsequent QA-accepted
|
||||
development candidates may use that same checked path; never waive gates.
|
||||
The arbiter may push verified commits to origin/rewrite/portable-agent-fleet
|
||||
without force, after checking the remote boundary and test evidence.
|
||||
|
||||
QA may retry interrupted decision 406ff30e-b317-4733-b39e-f63c9a57506d. QA must
|
||||
independently recheck the exact five PNG paths, candidate HEAD, current/archive
|
||||
hashes and clean-before-run provenance. Only Project Kernel may apply an approved
|
||||
five-path restoration; keep the archive and old unqualified receipts. The earlier
|
||||
c2a1702b rejection stays rejected. This is not arbitrary cleanup authority.
|
||||
|
||||
Continue resolving the existing development backlog under domain ownership,
|
||||
assignment continuity, peer consent and independent QA. Finish the reliability
|
||||
dependencies first. Manager coordinates and records actual outcomes; the arbiter
|
||||
still controls provider starts and installation changes. Automatic wakes remain
|
||||
paused until executor qualification and an explicit safe activation decision.
|
||||
No default-main/legacy changes, deployment, package publication, spending/model
|
||||
changes, destructive cleanup, arbitrary new identities or hierarchy activation
|
||||
are granted. Preserve existing unique commits and user changes. Read this current
|
||||
canonical brief before acting; old conversation restrictions cannot conceal this
|
||||
new consent, and this consent cannot conceal failed checks.
|
||||
|
||||
## Routine work stays inside the fleet
|
||||
|
||||
The owner authorizes routine coordination within existing task/role authority:
|
||||
|
||||
@@ -42,6 +42,37 @@ test_integrity_note:
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
## 2026-09-30 — Generated exact-check artifacts
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: new_tests
|
||||
scenarios:
|
||||
- id: SCN-generated-check-artifacts
|
||||
given: A clean managed worktree and an exact check that emits evidence
|
||||
when: The same check runs twice through TORCH
|
||||
then: Each pass retains separate external bytes and hashes bound to candidate commit, check definition, and run ID while the tested tree stays clean
|
||||
- id: SCN-generated-check-mutation
|
||||
given: A clean managed worktree and an exact check that emits external evidence
|
||||
when: The command exits zero after mutating a tracked source file
|
||||
then: The receipt remains incomplete and cannot qualify despite retained output
|
||||
observable_outcomes:
|
||||
- Recoverable output paths, byte counts, SHA-256 hashes, candidate/check/run provenance, and exact-pass qualification
|
||||
- Git worktree status and incomplete receipt for a zero-exit source mutation
|
||||
determinism_controls:
|
||||
- Disposable local Git fixtures, deterministic run IDs, fixed evidence bytes, and no network or provider calls
|
||||
- No retries, sleeps, baseline updates, stashes, restores, or source cleanup
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- Treating exit zero as an exact-check pass after a tracked expectation changes
|
||||
- Losing or overwriting generated evidence between sequential check runs
|
||||
- Claiming provenance without retained bytes and a content hash
|
||||
- Hiding source mutations through automatic restore or cleanup
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
## 2026-09-30 — Exact observed-file ownership evidence
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -2,8 +2,14 @@ import assert from 'node:assert/strict';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { once } from 'node:events';
|
||||
import { chromium } from '@playwright/test';
|
||||
import { join } from 'node:path';
|
||||
import { createConsoleServer } from '../src/console/server.mjs';
|
||||
|
||||
const screenshotDirectory = process.env.TORCH_CHECK_ARTIFACT_DIR || 'reports/design-system';
|
||||
function screenshotPath(name) {
|
||||
return join(screenshotDirectory, name);
|
||||
}
|
||||
|
||||
const server = createConsoleServer();
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
@@ -35,13 +41,13 @@ try {
|
||||
assert.equal(await page.locator('#operation-outcomes button').count(), 0);
|
||||
await page.locator('#operation-outcomes .check-evidence').last().locator('summary').click();
|
||||
assert.match(await page.locator('#operation-outcomes').innerText(), /transient failure.*effects unknown/s);
|
||||
mkdirSync('reports/design-system', { recursive: true });
|
||||
await page.locator('#operation-outcomes').screenshot({ path: 'reports/design-system/torch-dashboard-operation-outcomes.png' });
|
||||
mkdirSync(screenshotDirectory, { recursive: true });
|
||||
await page.locator('#operation-outcomes').screenshot({ path: screenshotPath('torch-dashboard-operation-outcomes.png') });
|
||||
await page.locator('#activity-review > summary').click();
|
||||
assert.match(await page.locator('#activity-review').innerText(), /4 days without a named commit/);
|
||||
assert.match(await page.locator('#activity-review').innerText(), /waiting may be expected/);
|
||||
mkdirSync('reports/design-system', { recursive: true });
|
||||
await page.locator('#activity-review').screenshot({ path: 'reports/design-system/torch-dashboard-activity-review.png' });
|
||||
mkdirSync(screenshotDirectory, { recursive: true });
|
||||
await page.locator('#activity-review').screenshot({ path: screenshotPath('torch-dashboard-activity-review.png') });
|
||||
assert.match(await page.locator('#check-list').innerText(), /Running — completion unconfirmed/);
|
||||
await page.locator('#check-list .check-evidence').filter({ hasText: 'interrupted-browser' }).locator('summary').click();
|
||||
assert.match(await page.locator('#check-list').innerText(), /Abandoned — not qualified/);
|
||||
@@ -51,14 +57,14 @@ try {
|
||||
await page.locator('#check-list .check-evidence').filter({ hasText: 'recovery-scenario' }).locator('summary').click();
|
||||
assert.match(await page.locator('#check-list').innerText(), /clockMoving.*true/s);
|
||||
assert.match(await page.locator('#check-list').innerText(), /not independent sensor truth/);
|
||||
mkdirSync('reports/design-system', { recursive: true });
|
||||
await page.locator('#check-list').screenshot({ path: 'reports/design-system/torch-dashboard-check-evidence.png' });
|
||||
mkdirSync(screenshotDirectory, { recursive: true });
|
||||
await page.locator('#check-list').screenshot({ path: screenshotPath('torch-dashboard-check-evidence.png') });
|
||||
assert.match(await page.locator('#owner-briefing').innerText(), /Choose the project home layout/);
|
||||
assert.match(await page.locator('#owner-briefing').innerText(), /not independent live verification/);
|
||||
await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click();
|
||||
assert.match(await page.locator('#owner-briefing').innerText(), /Landed — not necessarily deployed/);
|
||||
mkdirSync('reports/design-system', { recursive: true });
|
||||
await page.locator('#owner-briefing').screenshot({ path: 'reports/design-system/torch-dashboard-owner-briefing-desktop.png' });
|
||||
mkdirSync(screenshotDirectory, { recursive: true });
|
||||
await page.locator('#owner-briefing').screenshot({ path: screenshotPath('torch-dashboard-owner-briefing-desktop.png') });
|
||||
assert.match(await page.locator('#attention-list').innerText(), /manager launch needs inspection/);
|
||||
await page.locator('#attention-list a[href="#manager-wakes"]').click();
|
||||
assert.equal(await page.locator('#manager-wakes').isVisible(), true);
|
||||
@@ -93,8 +99,8 @@ try {
|
||||
await conclusion.locator('[data-review-confirm]').waitFor();
|
||||
await conclusion.getByText('Exact proposal and evidence in this decision', { exact: true }).click();
|
||||
assert.match(await conclusion.locator('[data-review-scope]').innerText(), /schedulesRetained/);
|
||||
mkdirSync('reports/design-system', { recursive: true });
|
||||
await conclusion.screenshot({ path: 'reports/design-system/torch-dashboard-pilot-conclusion.png' });
|
||||
mkdirSync(screenshotDirectory, { recursive: true });
|
||||
await conclusion.screenshot({ path: screenshotPath('torch-dashboard-pilot-conclusion.png') });
|
||||
await conclusion.locator('[data-review-confirm]').click();
|
||||
await page.locator('#organization-action-status').filter({ hasText: 'recorded as reversed' }).waitFor();
|
||||
assert.equal(await page.locator('.agent-row').count(), 6);
|
||||
@@ -149,12 +155,12 @@ try {
|
||||
await page.locator('[data-review-confirm]').click();
|
||||
await page.locator('#organization-action-status').filter({ hasText: 'recorded as approved' }).waitFor();
|
||||
|
||||
mkdirSync('reports/design-system', { recursive: true });
|
||||
mkdirSync(screenshotDirectory, { recursive: true });
|
||||
await pilotPanel.locator(':scope > summary').click();
|
||||
await pilotPanel.screenshot({ path: 'reports/design-system/torch-dashboard-pilot-review.png' });
|
||||
await page.locator('#manager-wakes').screenshot({ path: 'reports/design-system/torch-dashboard-manager-wakes.png' });
|
||||
await pilotPanel.screenshot({ path: screenshotPath('torch-dashboard-pilot-review.png') });
|
||||
await page.locator('#manager-wakes').screenshot({ path: screenshotPath('torch-dashboard-manager-wakes.png') });
|
||||
await page.evaluate(() => scrollTo(0, 0));
|
||||
await page.screenshot({ path: 'reports/design-system/torch-dashboard-demo-desktop.png' });
|
||||
await page.screenshot({ path: screenshotPath('torch-dashboard-demo-desktop.png') });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
// Let responsive styles and layout finish before measuring the new viewport.
|
||||
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||
@@ -165,13 +171,13 @@ try {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
return { tag: element.tagName, id: element.id, className: element.className, right: bounds.right, width: bounds.width };
|
||||
}).filter((element) => element.right > innerWidth + 1).slice(-30) }))));
|
||||
await page.screenshot({ path: 'reports/design-system/torch-dashboard-demo-mobile.png' });
|
||||
await page.screenshot({ path: screenshotPath('torch-dashboard-demo-mobile.png') });
|
||||
await page.locator('.console-rail a[href="#owner-briefing"]').click();
|
||||
const briefingHeading = await page.locator('#owner-briefing-title').boundingBox();
|
||||
const stickyRail = await page.locator('.console-rail').boundingBox();
|
||||
assert.ok(briefingHeading.y >= stickyRail.y + stickyRail.height, 'briefing anchor must clear sticky mobile navigation');
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
|
||||
await page.locator('#owner-briefing').screenshot({ path: 'reports/design-system/torch-dashboard-owner-briefing-mobile.png' });
|
||||
await page.locator('#owner-briefing').screenshot({ path: screenshotPath('torch-dashboard-owner-briefing-mobile.png') });
|
||||
await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click();
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, 'expanded briefing evidence must fit mobile width');
|
||||
await page.locator('#reset-demo').click();
|
||||
@@ -236,7 +242,7 @@ try {
|
||||
assert.equal(await livePage.locator('#task-create-form [name="title"]').inputValue(), 'My unfinished task');
|
||||
await livePage.locator('#refresh-console').click();
|
||||
assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), 'owner-draft-model');
|
||||
await livePage.screenshot({ path: 'reports/design-system/torch-dashboard-live-refresh.png' });
|
||||
await livePage.screenshot({ path: screenshotPath('torch-dashboard-live-refresh.png') });
|
||||
await livePage.close();
|
||||
assert.deepEqual(errors, []);
|
||||
assert.deepEqual(apiRequests, [], 'demo must never contact live project APIs');
|
||||
|
||||
@@ -18,7 +18,7 @@ function inspectReport(policy, report, subject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function probe({ definition, subject, cwd, executor, clock }) {
|
||||
function probe({ definition, subject, cwd, executor, clock, environment = {} }) {
|
||||
const policy = definition.conditions;
|
||||
const startedAt = clock().toISOString();
|
||||
let result;
|
||||
@@ -26,7 +26,8 @@ function probe({ definition, subject, cwd, executor, clock }) {
|
||||
result = executor(policy.command, policy.args ?? [], {
|
||||
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: (policy.timeout_seconds ?? 30) * 1000, maxBuffer: MAX_REPORT_BYTES,
|
||||
env: { ...process.env, TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' },
|
||||
env: { ...process.env, ...environment,
|
||||
TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' },
|
||||
});
|
||||
} catch (error) {
|
||||
return { valid: false, reason: 'condition-probe-executor-failed', startedAt,
|
||||
@@ -60,7 +61,7 @@ export function validConditionEvidence({ definition, conditions, commit }) {
|
||||
&& (before.runtimeId === undefined || after.runtimeId === before.runtimeId);
|
||||
}
|
||||
|
||||
export function executeConditionedCheck({ definition, subject, cwd, executor, clock }) {
|
||||
export function executeConditionedCheck({ definition, subject, cwd, executor, clock, environment = {} }) {
|
||||
const conditions = definition.conditions ? {
|
||||
provenance: 'project-reported', subject,
|
||||
required: definition.conditions.required, before: null, after: null,
|
||||
@@ -68,7 +69,7 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl
|
||||
let result;
|
||||
let invalidReason = null;
|
||||
if (conditions) {
|
||||
conditions.before = probe({ definition, subject, cwd, executor, clock });
|
||||
conditions.before = probe({ definition, subject, cwd, executor, clock, environment });
|
||||
if (!conditions.before.valid) return {
|
||||
result: { status: null, stdout: `TORCH_CHECK_CONDITIONS ${JSON.stringify(conditions.before)}\n`,
|
||||
stderr: conditions.before.reason },
|
||||
@@ -78,7 +79,8 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl
|
||||
try {
|
||||
result = executor(definition.command, definition.args ?? [], {
|
||||
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' },
|
||||
env: { ...process.env, ...environment,
|
||||
TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' },
|
||||
});
|
||||
if (result.error || result.signal) invalidReason = 'executor-failed';
|
||||
} catch (error) {
|
||||
@@ -86,7 +88,7 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl
|
||||
invalidReason = 'executor-failed';
|
||||
}
|
||||
if (conditions) {
|
||||
conditions.after = probe({ definition, subject, cwd, executor, clock });
|
||||
conditions.after = probe({ definition, subject, cwd, executor, clock, environment });
|
||||
if (conditions.after.valid && conditions.before.report.runtimeId !== undefined
|
||||
&& conditions.after.report.runtimeId !== conditions.before.report.runtimeId) {
|
||||
conditions.after = { ...conditions.after, valid: false, reason: 'condition-runtime-changed' };
|
||||
|
||||
+86
-15
@@ -1,7 +1,7 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join, relative, resolve } from 'node:path';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
import { readInstallManifest } from '../kernel/install.mjs';
|
||||
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
|
||||
@@ -35,6 +35,55 @@ function definitionHash(definition) {
|
||||
return createHash('sha256').update(JSON.stringify(definition)).digest('hex');
|
||||
}
|
||||
|
||||
function createCheckArtifactDirectory(stateRoot, runId) {
|
||||
const root = realpathSync(stateRoot);
|
||||
const parent = join(root, 'checks');
|
||||
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
||||
const ownedParent = realpathSync(parent);
|
||||
if (relative(root, ownedParent).startsWith('..') || lstatSync(ownedParent).isSymbolicLink()
|
||||
|| !lstatSync(ownedParent).isDirectory()) {
|
||||
throw new TorchError('Check artifact parent is not an owned state directory', { code: 'CHECK_ARTIFACT_INVALID' });
|
||||
}
|
||||
const directory = join(ownedParent, runId);
|
||||
if (resolve(directory) !== directory || relative(ownedParent, directory).startsWith('..')) {
|
||||
throw new TorchError('Check artifact directory escapes local state', { code: 'CHECK_ARTIFACT_INVALID' });
|
||||
}
|
||||
mkdirSync(directory, { mode: 0o700 });
|
||||
const generatedDirectory = join(directory, 'generated');
|
||||
mkdirSync(generatedDirectory, { mode: 0o700 });
|
||||
return { directory, generatedDirectory };
|
||||
}
|
||||
|
||||
function captureGeneratedArtifacts({ generatedDirectory, commit, checkId, definition, runId }) {
|
||||
const files = [];
|
||||
function visit(current, path = '') {
|
||||
for (const name of readdirSync(current).sort()) {
|
||||
const absolute = join(current, name);
|
||||
const entryPath = path ? `${path}/${name}` : name;
|
||||
const stat = lstatSync(absolute);
|
||||
if (stat.isSymbolicLink()) throw new TorchError('Generated check artifact cannot be a symlink', {
|
||||
code: 'CHECK_ARTIFACT_INVALID', details: { path: entryPath },
|
||||
});
|
||||
if (stat.isDirectory()) visit(absolute, entryPath);
|
||||
else if (stat.isFile()) {
|
||||
const bytes = readFileSync(absolute);
|
||||
files.push({ path: entryPath, bytes: bytes.length,
|
||||
sha256: createHash('sha256').update(bytes).digest('hex') });
|
||||
} else throw new TorchError('Generated check artifact must be a regular file', {
|
||||
code: 'CHECK_ARTIFACT_INVALID', details: { path: entryPath },
|
||||
});
|
||||
}
|
||||
}
|
||||
visit(generatedDirectory);
|
||||
return {
|
||||
directory: generatedDirectory,
|
||||
candidate: { commit },
|
||||
check: { id: checkId, definitionHash: definitionHash(definition) },
|
||||
run: { id: runId },
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
export class CheckService {
|
||||
constructor({
|
||||
repositoryRoot, controlPlane, resourceService = null,
|
||||
@@ -145,6 +194,7 @@ export class CheckService {
|
||||
}
|
||||
const receiptId = this.idFactory();
|
||||
const startedAt = this.clock().toISOString();
|
||||
const artifacts = createCheckArtifactDirectory(this.stateRoot, receiptId);
|
||||
beginWorktreeGuard(this.controlPlane, {
|
||||
areaId: plan.areaId, type: 'check', reason: `Running ${checkId}`,
|
||||
clock: this.clock, idFactory: this.idFactory,
|
||||
@@ -155,6 +205,7 @@ export class CheckService {
|
||||
execution = executeConditionedCheck({
|
||||
definition: plan.definition, subject: { commit: plan.commit }, cwd: plan.worktree,
|
||||
executor: this.executor, clock: this.clock,
|
||||
environment: { TORCH_CHECK_ARTIFACT_DIR: artifacts.generatedDirectory },
|
||||
});
|
||||
result = execution.result;
|
||||
} finally {
|
||||
@@ -174,19 +225,27 @@ export class CheckService {
|
||||
status = 'incomplete';
|
||||
invalidReason = 'worktree-changed-during-check';
|
||||
}
|
||||
const artifactDirectory = join(this.stateRoot, 'checks', receiptId);
|
||||
mkdirSync(artifactDirectory, { recursive: false });
|
||||
const artifactPath = join(artifactDirectory, 'output.json');
|
||||
let generatedArtifacts;
|
||||
try {
|
||||
generatedArtifacts = captureGeneratedArtifacts({ generatedDirectory: artifacts.generatedDirectory,
|
||||
commit: plan.commit, checkId, definition: plan.definition, runId: receiptId });
|
||||
} catch (error) {
|
||||
status = 'incomplete';
|
||||
invalidReason = error.code ?? 'check-artifact-invalid';
|
||||
generatedArtifacts = { directory: artifacts.generatedDirectory, error: invalidReason };
|
||||
}
|
||||
const artifactPath = join(artifacts.directory, 'output.json');
|
||||
writeFileSync(artifactPath, `${JSON.stringify({
|
||||
stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr),
|
||||
conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted,
|
||||
generatedArtifacts,
|
||||
}, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
||||
const receipt = {
|
||||
id: receiptId, projectId: this.controlPlane.projectId, checkId, areaId: plan.areaId,
|
||||
definition: plan.definition, worktree: plan.worktree, commit: plan.commit,
|
||||
startedAt, finishedAt, exitStatus: result.status ?? null, result: status,
|
||||
artifactPath, invalidReason,
|
||||
conditions: execution.conditions,
|
||||
conditions: execution.conditions, generatedArtifacts,
|
||||
};
|
||||
this.controlPlane.database.prepare(`
|
||||
INSERT INTO check_receipts (
|
||||
@@ -278,11 +337,14 @@ export class CheckService {
|
||||
if (claimed.changes !== 1) throw new TorchError('Prepared check was claimed concurrently', {
|
||||
code: 'PREPARED_CHECK_UNAVAILABLE',
|
||||
});
|
||||
const receiptId = this.idFactory();
|
||||
const artifacts = createCheckArtifactDirectory(this.stateRoot, receiptId);
|
||||
const startedAt = this.clock().toISOString();
|
||||
const execution = executeConditionedCheck({
|
||||
definition: item.definition,
|
||||
subject: { commit: item.snapshot.commit, inputDigest: item.snapshot.digest },
|
||||
cwd: item.snapshot.inputRoot, executor: this.executor, clock: this.clock,
|
||||
environment: { TORCH_CHECK_ARTIFACT_DIR: artifacts.generatedDirectory },
|
||||
});
|
||||
const result = execution.result;
|
||||
let invalidReason = execution.invalidReason;
|
||||
@@ -296,11 +358,19 @@ export class CheckService {
|
||||
invalidReason = 'resource-lease-lost-during-check';
|
||||
}
|
||||
const finishedAt = this.clock().toISOString();
|
||||
const receiptId = this.idFactory();
|
||||
const status = invalidReason ? 'incomplete' : result.status === 0 ? 'pass' : 'fail';
|
||||
const artifactDirectory = join(this.stateRoot, 'checks', receiptId);
|
||||
mkdirSync(artifactDirectory, { recursive: false });
|
||||
const artifactPath = join(artifactDirectory, 'output.json');
|
||||
let receiptStatus = status;
|
||||
let receiptInvalidReason = invalidReason;
|
||||
let generatedArtifacts;
|
||||
try {
|
||||
generatedArtifacts = captureGeneratedArtifacts({ generatedDirectory: artifacts.generatedDirectory,
|
||||
commit: item.snapshot.commit, checkId: item.checkId, definition: item.definition, runId: receiptId });
|
||||
} catch (error) {
|
||||
receiptStatus = 'incomplete';
|
||||
receiptInvalidReason = error.code ?? 'check-artifact-invalid';
|
||||
generatedArtifacts = { directory: artifacts.generatedDirectory, error: receiptInvalidReason };
|
||||
}
|
||||
const artifactPath = join(artifacts.directory, 'output.json');
|
||||
const snapshotEvidence = {
|
||||
preparedId: item.id, inputDigest: item.snapshot.digest, commit: item.snapshot.commit,
|
||||
files: item.snapshot.files, provenance: item.snapshot.provenance,
|
||||
@@ -309,13 +379,14 @@ export class CheckService {
|
||||
writeFileSync(artifactPath, `${JSON.stringify({
|
||||
stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr), snapshot: snapshotEvidence,
|
||||
conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted,
|
||||
generatedArtifacts,
|
||||
}, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
||||
const receipt = {
|
||||
id: receiptId, projectId: this.controlPlane.projectId, checkId: item.checkId, areaId: area,
|
||||
definition: item.definition, worktree: item.snapshot.sourceWorktree, commit: item.snapshot.commit,
|
||||
startedAt, finishedAt, exitStatus: result.status ?? null, result: status,
|
||||
artifactPath, invalidReason, snapshot: snapshotEvidence,
|
||||
conditions: execution.conditions,
|
||||
startedAt, finishedAt, exitStatus: result.status ?? null, result: receiptStatus,
|
||||
artifactPath, invalidReason: receiptInvalidReason, snapshot: snapshotEvidence,
|
||||
conditions: execution.conditions, generatedArtifacts,
|
||||
};
|
||||
this.controlPlane.database.prepare(`INSERT INTO check_receipts
|
||||
(id, project_id, check_id, definition, definition_hash, area_id, worktree, commit_sha,
|
||||
@@ -323,13 +394,13 @@ export class CheckService {
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(receipt.id, receipt.projectId, receipt.checkId, JSON.stringify(receipt.definition),
|
||||
definitionHash(receipt.definition), area, receipt.worktree, receipt.commit,
|
||||
startedAt, finishedAt, receipt.exitStatus, status, artifactPath, invalidReason,
|
||||
startedAt, finishedAt, receipt.exitStatus, receiptStatus, artifactPath, receiptInvalidReason,
|
||||
execution.conditions ? JSON.stringify(execution.conditions) : null);
|
||||
this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'finished', receipt_id = ? WHERE id = ?`)
|
||||
.run(receiptId, item.id);
|
||||
this.controlPlane.audit({
|
||||
actorId: area, operation: 'check.run-prepared', entityType: 'check-receipt', entityId: receiptId,
|
||||
details: { preparedId: item.id, checkId: item.checkId, commit: receipt.commit, result: status },
|
||||
details: { preparedId: item.id, checkId: item.checkId, commit: receipt.commit, result: receiptStatus },
|
||||
});
|
||||
disposeCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot });
|
||||
return receipt;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { basename, join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { CheckService } from '../../src/checks/service.mjs';
|
||||
import { openControlPlane } from '../../src/control-plane/service.mjs';
|
||||
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
|
||||
import { proposeDomains } from '../../src/kernel/domains.mjs';
|
||||
import { inspectRepository } from '../../src/kernel/git.mjs';
|
||||
import { installProject } from '../../src/kernel/install.mjs';
|
||||
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'torch-generated-artifacts-'));
|
||||
execFileSync('git', ['init', '-b', 'main', root]);
|
||||
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
|
||||
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
|
||||
writeFileSync(join(root, 'package.json'), `${JSON.stringify({
|
||||
name: 'generated-artifacts-fixture', scripts: { 'verify:pass': 'node -e "process.exit(0)"' },
|
||||
}, null, 2)}\n`);
|
||||
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
|
||||
mkdirSync(join(root, 'test'));
|
||||
writeFileSync(join(root, 'test', 'scenario.js'), 'export const scenario = true;\n');
|
||||
execFileSync('git', ['-C', root, 'add', '.']);
|
||||
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
|
||||
let repository = inspectRepository(root);
|
||||
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
|
||||
proposal.review = { status: 'approved', reviewedAt: '2026-09-30T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
|
||||
const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) };
|
||||
installProject({ repository, proposal, env, projectId: 'generated-artifacts-fixture' });
|
||||
execFileSync('git', ['-C', root, 'add', '.torch']);
|
||||
execFileSync('git', ['-C', root, 'commit', '-m', 'install torch']);
|
||||
repository = inspectRepository(root);
|
||||
createWorktrees({ repository, parentOverride: join(tmpdir(), `${basename(root)}-worktrees`) });
|
||||
return { root, env, worker: proposal.domains.find((domain) => domain.id !== 'qa').id };
|
||||
}
|
||||
|
||||
function addCheck(context, definition) {
|
||||
const path = join(context.root, '.torch', 'torch.yaml');
|
||||
const config = JSON.parse(readFileSync(path, 'utf8'));
|
||||
config.checks.push(definition);
|
||||
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function treeStatus(path) {
|
||||
return execFileSync('git', ['-C', path, 'status', '--porcelain'], { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
test('SCN-generated-check-artifacts: sequential exact checks retain external hashed evidence without changing tracked inputs', () => {
|
||||
const context = fixture();
|
||||
addCheck(context, {
|
||||
id: 'emit-proof', command: process.execPath,
|
||||
args: ['-e', `const fs=require('node:fs');const path=require('node:path');
|
||||
const output=process.env.TORCH_CHECK_ARTIFACT_DIR;if(!output)process.exit(44);
|
||||
fs.mkdirSync(path.join(output,'screenshots'),{recursive:true});
|
||||
fs.writeFileSync(path.join(output,'screenshots','proof.txt'),'captured boundary evidence');`],
|
||||
});
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
||||
let id = 0;
|
||||
const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control,
|
||||
idFactory: () => `artifact-run-${++id}` });
|
||||
const expectedHash = createHash('sha256').update('captured boundary evidence').digest('hex');
|
||||
try {
|
||||
const receipts = [
|
||||
checks.run({ checkId: 'emit-proof', areaId: context.worker }),
|
||||
checks.run({ checkId: 'emit-proof', areaId: context.worker }),
|
||||
];
|
||||
for (const receipt of receipts) {
|
||||
assert.equal(receipt.result, 'pass');
|
||||
assert.equal(treeStatus(receipt.worktree), '', 'a passing check must leave its tested tree unchanged');
|
||||
const retained = JSON.parse(readFileSync(receipt.artifactPath, 'utf8')).generatedArtifacts;
|
||||
assert.deepEqual(retained.candidate, { commit: receipt.commit });
|
||||
assert.deepEqual(retained.check, { id: 'emit-proof',
|
||||
definitionHash: createHash('sha256').update(JSON.stringify(receipt.definition)).digest('hex') });
|
||||
assert.deepEqual(retained.run, { id: receipt.id });
|
||||
assert.deepEqual(retained.files, [{ path: 'screenshots/proof.txt', bytes: 26, sha256: expectedHash }]);
|
||||
assert.equal(readFileSync(join(retained.directory, 'screenshots', 'proof.txt'), 'utf8'), 'captured boundary evidence');
|
||||
}
|
||||
assert.notEqual(receipts[0].generatedArtifacts.directory, receipts[1].generatedArtifacts.directory,
|
||||
'each run retains its own recoverable evidence directory');
|
||||
assert.equal(checks.exactPasses({ commit: receipts[0].commit, requiredChecks: ['emit-proof'] }), true);
|
||||
} finally {
|
||||
control.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('SCN-generated-check-mutation: exit zero plus a tested-tree mutation is incomplete even when external evidence was retained', () => {
|
||||
const context = fixture();
|
||||
addCheck(context, {
|
||||
id: 'emit-and-mutate', command: process.execPath,
|
||||
args: ['-e', `const fs=require('node:fs');const path=require('node:path');
|
||||
fs.writeFileSync(path.join(process.env.TORCH_CHECK_ARTIFACT_DIR,'proof.txt'),'retained despite mutation');
|
||||
fs.writeFileSync('app.js','source mutation');`],
|
||||
});
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
||||
const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control,
|
||||
idFactory: () => 'mutation-run' });
|
||||
try {
|
||||
const receipt = checks.run({ checkId: 'emit-and-mutate', areaId: context.worker });
|
||||
assert.equal(receipt.exitStatus, 0);
|
||||
assert.equal(receipt.result, 'incomplete');
|
||||
assert.equal(receipt.invalidReason, 'worktree-changed-during-check');
|
||||
const retained = JSON.parse(readFileSync(receipt.artifactPath, 'utf8')).generatedArtifacts;
|
||||
assert.equal(existsSync(join(retained.directory, 'proof.txt')), true);
|
||||
assert.notEqual(treeStatus(receipt.worktree), '');
|
||||
assert.equal(checks.exactPasses({ commit: receipt.commit, requiredChecks: ['emit-and-mutate'] }), false);
|
||||
} finally {
|
||||
control.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user