Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions apps/local-qa-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,17 @@ production policy code. It:
- always finalizes the injected session after acquisition; and
- returns a bounded, deterministic serialized result.

The fixed Worker executable walks that policy through the registered bounded
`qa.local-worker-protocol/v1` over stdin/stdout. It accepts one invocation,
performs the seven fixed typed capability exchanges, emits one terminal result,
and exits. The process acceptance harness acts as the Host-shaped peer; the
production Host does not yet invoke this Worker.

The worker never discovers or launches Chrome, opens network or filesystem
resources, creates profiles, downloads, or processes, persists Evidence, or
owns Host cleanup. Browser output acquisition, screenshot production, reference
digesting, and storage are responsibilities of the injected ports, not worker
policy. The Host does not yet invoke this worker boundary.
resources, creates profiles, downloads, or child processes, persists Evidence,
or owns Host cleanup. Browser output acquisition, screenshot production,
reference digesting, and storage are responsibilities of the protocol peer, not
worker policy.

The Rust `evidence-stager/` library owns bounded Local Evidence filesystem
effects. It accepts validated Evidence identities and bytes, derives confined
Expand All @@ -82,8 +88,7 @@ safety net do not expose caller-programmable browser behavior.

The following capabilities remain explicitly deferred:

- Host coordination of the fixed Browser adapter and a Host-worker process
protocol;
- Host executor integration of the fixed Browser adapter and Worker protocol;
- Host integration of filesystem Evidence staging and journal Evidence
references;
- execution terminal outcomes and restart-to-`lost` reconciliation;
Expand Down
20 changes: 13 additions & 7 deletions apps/local-qa-runtime/tests/scaffold-structure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,19 @@ done

node - <<'NODE'
const pkg = require('./apps/local-qa-runtime/workers/package.json');
const scripts = {
build: 'tsc -p tsconfig.build.json',
typecheck: 'tsc -p tsconfig.json',
test: 'tsc -p tsconfig.build.json && node --test test/*.test.mjs',
const dependencies = { '@chronoai/fkst-qa-contracts': 'file:../../../packages/qa-contracts' };
const devDependencies = { '@types/node': '20.19.43', typescript: '5.9.3' };
const bin = { 'fkst-local-qa-worker': 'dist/worker-main.js' };
const expectedScripts = {
build: 'npm run build --prefix ../../../packages/qa-contracts && tsc -p tsconfig.build.json',
typecheck: 'npm run build --prefix ../../../packages/qa-contracts && tsc -p tsconfig.json',
test: 'npm run build --prefix ../../../packages/qa-contracts && tsc -p tsconfig.build.json && node --test test/*.test.mjs',
};
if (pkg.dependencies || JSON.stringify(pkg.scripts) !== JSON.stringify(scripts) ||
JSON.stringify(pkg.devDependencies) !== JSON.stringify({ typescript: '5.9.3' })) {
throw new Error('workers package gained unreviewed dependencies or scripts');
if (JSON.stringify(pkg.dependencies) !== JSON.stringify(dependencies) ||
JSON.stringify(pkg.scripts) !== JSON.stringify(expectedScripts) ||
JSON.stringify(pkg.devDependencies) !== JSON.stringify(devDependencies) ||
JSON.stringify(pkg.bin) !== JSON.stringify(bin)) {
throw new Error('workers package gained unreviewed dependencies, scripts, or executables');
}
NODE

Expand All @@ -157,6 +162,7 @@ unexpected=$(find apps/local-qa-runtime -type f \
\( -perm -111 -o -name '*.sh' -o -name '*.py' -o -name '*.js' -o -name '*.mjs' \) \
! -path 'apps/local-qa-runtime/tests/scaffold-structure.sh' \
! -path 'apps/local-qa-runtime/workers/test/browser-smoke.test.mjs' \
! -path 'apps/local-qa-runtime/workers/test/protocol-worker.test.mjs' \
! -path '*/node_modules/*' ! -path '*/target/*' ! -path '*/dist/*')
[[ -z "$unexpected" ]] || { echo 'unexpected executable implementation file' >&2; exit 1; }

Expand Down
96 changes: 96 additions & 0 deletions apps/local-qa-runtime/workers/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions apps/local-qa-runtime/workers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc -p tsconfig.json",
"test": "tsc -p tsconfig.build.json && node --test test/*.test.mjs"
"build": "npm run build --prefix ../../../packages/qa-contracts && tsc -p tsconfig.build.json",
"typecheck": "npm run build --prefix ../../../packages/qa-contracts && tsc -p tsconfig.json",
"test": "npm run build --prefix ../../../packages/qa-contracts && tsc -p tsconfig.build.json && node --test test/*.test.mjs"
},
"devDependencies": {
"@types/node": "20.19.43",
"typescript": "5.9.3"
},
"bin": {
"fkst-local-qa-worker": "dist/worker-main.js"
},
"dependencies": {
"@chronoai/fkst-qa-contracts": "file:../../../packages/qa-contracts"
}
}
2 changes: 2 additions & 0 deletions apps/local-qa-runtime/workers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export {
type EvidenceStagingPort,
type WorkerErrorCode,
} from "./policy.js";

export { runProtocolWorker } from "./protocol-worker.js";
63 changes: 33 additions & 30 deletions apps/local-qa-runtime/workers/src/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export interface BrowserSessionPort {
run(request: BrowserSmokeRequest): Promise<{
finalUrl: string;
observedText: string;
sanitizedObservationRef: DigestBoundRef<"qa.sanitized-observation/v1">;
screenshotArtifactRef: DigestBoundRef<"qa.artifact-pointer/v1">;
sanitizedObservationRef: DigestBoundRef<"qa.local-evidence/v1">;
screenshotEvidenceRef: DigestBoundRef<"qa.local-evidence/v1">;
}>;
close(): Promise<void>;
}
Expand All @@ -53,12 +53,12 @@ export interface EvidenceStagingPort {
name: "runner.log";
mediaType: "text/plain; charset=utf-8";
bytes: Uint8Array;
}): Promise<DigestBoundRef<"qa.artifact-pointer/v1">>;
}): Promise<DigestBoundRef<"qa.local-evidence/v1">>;
}

export interface ClockPort {
now(): string;
monotonicMs(): number;
now(): string | Promise<string>;
monotonicMs(): number | Promise<number>;
}

export interface BrowserSmokeResult {
Expand All @@ -70,7 +70,7 @@ export interface BrowserSmokeResult {
readonly selector: typeof SELECTOR;
readonly expectedText: typeof EXPECTED_TEXT;
readonly observedText: typeof EXPECTED_TEXT;
readonly sanitizedObservationRef: DigestBoundRef<"qa.sanitized-observation/v1">;
readonly sanitizedObservationRef: DigestBoundRef<"qa.local-evidence/v1">;
};
readonly startedAt: string;
readonly finishedAt: string;
Expand All @@ -79,12 +79,12 @@ export interface BrowserSmokeResult {
{
readonly objectId: "evidence/0";
readonly role: "screenshot";
readonly artifactRef: DigestBoundRef<"qa.artifact-pointer/v1">;
readonly artifactRef: DigestBoundRef<"qa.local-evidence/v1">;
},
{
readonly objectId: "evidence/1";
readonly role: "runner-log";
readonly artifactRef: DigestBoundRef<"qa.artifact-pointer/v1">;
readonly artifactRef: DigestBoundRef<"qa.local-evidence/v1">;
},
];
}
Expand Down Expand Up @@ -148,10 +148,10 @@ export async function runBrowserSmoke(
},
): Promise<BrowserSmokeBundle> {
const request = parseBrowserSmokeRequest(source);
const startedAt = clockNow(ports.clock);
const startedMonotonicMs = monotonicNow(ports.clock);
const startedAt = await clockNow(ports.clock);
const startedMonotonicMs = await monotonicNow(ports.clock);
let validatedSession: ReturnType<typeof validateSessionResult>;
let runnerLogArtifactRef: DigestBoundRef<"qa.artifact-pointer/v1">;
let runnerLogArtifactRef: DigestBoundRef<"qa.local-evidence/v1">;

try {
let sessionResult: Awaited<ReturnType<BrowserSessionPort["run"]>>;
Expand Down Expand Up @@ -181,8 +181,8 @@ export async function runBrowserSmoke(
mediaType: "text/plain; charset=utf-8",
bytes: RUNNER_LOG.slice(),
}),
"artifact-pointer",
"qa.artifact-pointer/v1",
"local-evidence-object",
"qa.local-evidence/v1",
);
} catch (error) {
if (error instanceof BrowserSmokeWorkerError) {
Expand All @@ -198,8 +198,8 @@ export async function runBrowserSmoke(
}
}

const finishedAt = clockNow(ports.clock);
const finishedMonotonicMs = monotonicNow(ports.clock);
const finishedAt = await clockNow(ports.clock);
const finishedMonotonicMs = await monotonicNow(ports.clock);
const durationMs = finishedMonotonicMs - startedMonotonicMs;
if (!Number.isSafeInteger(durationMs) || durationMs < 0) {
throw new BrowserSmokeWorkerError("clock.invalid_value");
Expand All @@ -224,7 +224,7 @@ export async function runBrowserSmoke(
{
objectId: "evidence/0",
role: "screenshot",
artifactRef: validatedSession.screenshotArtifactRef,
artifactRef: validatedSession.screenshotEvidenceRef,
},
{
objectId: "evidence/1",
Expand Down Expand Up @@ -313,14 +313,14 @@ function isFixedFixtureUrl(value: string): boolean {
function validateSessionResult(value: unknown): {
readonly finalUrl: string;
readonly observedText: string;
readonly sanitizedObservationRef: DigestBoundRef<"qa.sanitized-observation/v1">;
readonly screenshotArtifactRef: DigestBoundRef<"qa.artifact-pointer/v1">;
readonly sanitizedObservationRef: DigestBoundRef<"qa.local-evidence/v1">;
readonly screenshotEvidenceRef: DigestBoundRef<"qa.local-evidence/v1">;
} {
if (!isExactRecord(value, [
"finalUrl",
"observedText",
"sanitizedObservationRef",
"screenshotArtifactRef",
"screenshotEvidenceRef",
])) {
throw new BrowserSmokeWorkerError("session.invalid_response");
}
Expand All @@ -332,13 +332,13 @@ function validateSessionResult(value: unknown): {
observedText: value.observedText,
sanitizedObservationRef: validateReference(
value.sanitizedObservationRef,
"sanitized-observation",
"qa.sanitized-observation/v1",
"local-sanitized-observation",
"qa.local-evidence/v1",
),
screenshotArtifactRef: validateReference(
value.screenshotArtifactRef,
"artifact-pointer",
"qa.artifact-pointer/v1",
screenshotEvidenceRef: validateReference(
value.screenshotEvidenceRef,
"local-evidence-object",
"qa.local-evidence/v1",
),
};
}
Expand All @@ -361,7 +361,10 @@ function validateReference<TSchema extends string>(
}
if (
value.kind !== expectedKind ||
!isNonEmptyString(value.id) ||
typeof value.id !== "string" ||
!(expectedKind === "local-sanitized-observation"
? /^observation\/[0-9]+$/.test(value.id)
: /^evidence\/[0-9]+$/.test(value.id)) ||
value.schema_version !== expectedSchema ||
typeof value.content_digest !== "string" ||
!/^sha256:[0-9a-f]{64}$/.test(value.content_digest) ||
Expand Down Expand Up @@ -390,10 +393,10 @@ function canonicalReference<TSchema extends string>(
};
}

function clockNow(clock: ClockPort): string {
async function clockNow(clock: ClockPort): Promise<string> {
let value: string;
try {
value = clock.now();
value = await clock.now();
} catch {
throw new BrowserSmokeWorkerError("clock.failed");
}
Expand All @@ -403,10 +406,10 @@ function clockNow(clock: ClockPort): string {
return value;
}

function monotonicNow(clock: ClockPort): number {
async function monotonicNow(clock: ClockPort): Promise<number> {
let value: number;
try {
value = clock.monotonicMs();
value = await clock.monotonicMs();
} catch {
throw new BrowserSmokeWorkerError("clock.failed");
}
Expand Down
Loading
Loading