witnora 0.9.3 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -9
- package/dist/assurance-contract.js +312 -0
- package/dist/assurance-loop-demo.js +245 -0
- package/dist/authorization-state-machine.js +35 -0
- package/dist/authorization-v02.js +60 -0
- package/dist/bundle.js +22 -0
- package/dist/canonical-v02.js +197 -0
- package/dist/canonical.js +31 -0
- package/dist/cli.js +132 -0
- package/dist/command-help.js +81 -0
- package/dist/control-plane.js +12 -3
- package/dist/credentials.js +1 -1
- package/dist/design-partner-pilot.js +402 -0
- package/dist/design-partner-v02.js +449 -0
- package/dist/evidence-v02.js +118 -0
- package/dist/generic-eval.js +160 -0
- package/dist/github-design-partner-v02.js +399 -0
- package/dist/github-live-v02.js +45 -0
- package/dist/guided-setup.js +1 -1
- package/dist/index.js +1 -0
- package/dist/offline-verifier.js +375 -0
- package/dist/onboard.js +132 -0
- package/dist/onboarding-templates.js +49 -6
- package/dist/outcome-evaluator.js +51 -0
- package/dist/privacy-manifest.js +273 -0
- package/dist/probe-protocol-v02.js +101 -0
- package/dist/runtime-context.js +75 -0
- package/dist/sandbox.js +1 -1
- package/dist/schema-validator.js +141 -3
- package/dist/trust-v02.js +111 -0
- package/dist/try.js +1 -1
- package/dist/vendor/design-partner/failure-exercises.mjs +353 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +8 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -1
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +43 -1
- package/dist/vendor/witnora-probe/canonical.d.ts +3 -0
- package/dist/vendor/witnora-probe/canonical.js +39 -0
- package/dist/vendor/witnora-probe/cli.d.ts +2 -0
- package/dist/vendor/witnora-probe/cli.js +36 -0
- package/dist/vendor/witnora-probe/config.d.ts +2 -0
- package/dist/vendor/witnora-probe/config.js +63 -0
- package/dist/vendor/witnora-probe/credential-resolver.d.ts +6 -0
- package/dist/vendor/witnora-probe/credential-resolver.js +17 -0
- package/dist/vendor/witnora-probe/crypto.d.ts +2 -0
- package/dist/vendor/witnora-probe/crypto.js +14 -0
- package/dist/vendor/witnora-probe/emulator.d.ts +51 -0
- package/dist/vendor/witnora-probe/emulator.js +164 -0
- package/dist/vendor/witnora-probe/index.d.ts +11 -0
- package/dist/vendor/witnora-probe/index.js +11 -0
- package/dist/vendor/witnora-probe/logger.d.ts +3 -0
- package/dist/vendor/witnora-probe/logger.js +26 -0
- package/dist/vendor/witnora-probe/metrics.d.ts +12 -0
- package/dist/vendor/witnora-probe/metrics.js +31 -0
- package/dist/vendor/witnora-probe/probe.d.ts +22 -0
- package/dist/vendor/witnora-probe/probe.js +208 -0
- package/dist/vendor/witnora-probe/request-verifier.d.ts +9 -0
- package/dist/vendor/witnora-probe/request-verifier.js +84 -0
- package/dist/vendor/witnora-probe/server.d.ts +9 -0
- package/dist/vendor/witnora-probe/server.js +85 -0
- package/dist/vendor/witnora-probe/storage.d.ts +16 -0
- package/dist/vendor/witnora-probe/storage.js +114 -0
- package/dist/vendor/witnora-probe/types.d.ts +126 -0
- package/dist/vendor/witnora-probe/types.js +2 -0
- package/dist/vendor/witnora-verifier-python/README.md +43 -0
- package/dist/vendor/witnora-verifier-python/pyproject.toml +23 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/__init__.py +33 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/canonical.py +218 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/cli.py +75 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/conformance.py +124 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/crypto.py +139 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/errors.py +15 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/event_chain.py +80 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/evidence.py +336 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/probe.py +188 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/producer.py +127 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/trust.py +337 -0
- package/package.json +5 -5
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { sha256Canonical } from "./canonical.js";
|
|
5
|
+
export const GENERIC_EVAL_SCHEMA_VERSION = "witnora.generic_eval.v0.1";
|
|
6
|
+
export const GENERIC_EVAL_REPORT_SCHEMA_VERSION = "witnora.generic_eval_report.v0.1";
|
|
7
|
+
const OBSERVATION_STATUSES = ["SATISFIED", "NOT_SATISFIED", "NOT_OBSERVED", "INDETERMINATE", "EXCLUDED"];
|
|
8
|
+
const TOP_LEVEL_FIELDS = new Set(["schemaVersion", "suite", "agent", "environment", "scopeFingerprint", "cases"]);
|
|
9
|
+
const AGENT_FIELDS = new Set(["name", "version", "framework"]);
|
|
10
|
+
const CASE_FIELDS = new Set(["id", "capability", "initialState", "task", "faultInjection", "prohibitedActions", "reportedSuccess", "reportedResult", "observedResult", "observedOutcome", "outcomePredicate", "evaluator", "limitations", "artifacts", "timestamps", "resultStatus", "costUsd", "latencyMs", "tokenUsage"]);
|
|
11
|
+
export function validateGenericEval(input) {
|
|
12
|
+
const value = requireRecord(input, "generic eval");
|
|
13
|
+
rejectUnknown(value, TOP_LEVEL_FIELDS, "generic eval");
|
|
14
|
+
if (value.schemaVersion !== GENERIC_EVAL_SCHEMA_VERSION)
|
|
15
|
+
throw new Error(`schemaVersion must be ${GENERIC_EVAL_SCHEMA_VERSION}`);
|
|
16
|
+
const agent = requireRecord(value.agent, "agent");
|
|
17
|
+
rejectUnknown(agent, AGENT_FIELDS, "agent");
|
|
18
|
+
requireString(agent.name, "agent.name");
|
|
19
|
+
requireString(agent.version, "agent.version");
|
|
20
|
+
if (agent.framework !== undefined)
|
|
21
|
+
requireString(agent.framework, "agent.framework");
|
|
22
|
+
requireString(value.suite, "suite");
|
|
23
|
+
requireString(value.scopeFingerprint, "scopeFingerprint");
|
|
24
|
+
if (!["sandbox", "staging", "production"].includes(String(value.environment)))
|
|
25
|
+
throw new Error("environment must be sandbox, staging, or production");
|
|
26
|
+
if (!Array.isArray(value.cases) || value.cases.length === 0)
|
|
27
|
+
throw new Error("cases must be a non-empty array");
|
|
28
|
+
const ids = new Set();
|
|
29
|
+
for (const [index, candidate] of value.cases.entries()) {
|
|
30
|
+
const item = requireRecord(candidate, `cases[${index}]`);
|
|
31
|
+
rejectUnknown(item, CASE_FIELDS, `cases[${index}]`);
|
|
32
|
+
requireString(item.id, `cases[${index}].id`);
|
|
33
|
+
if (ids.has(String(item.id)))
|
|
34
|
+
throw new Error(`duplicate case id: ${item.id}`);
|
|
35
|
+
ids.add(String(item.id));
|
|
36
|
+
if (typeof item.reportedSuccess !== "boolean")
|
|
37
|
+
throw new Error(`cases[${index}].reportedSuccess must be boolean`);
|
|
38
|
+
for (const text of ["capability", "task", "faultInjection", "resultStatus"])
|
|
39
|
+
if (item[text] !== undefined)
|
|
40
|
+
requireString(item[text], `cases[${index}].${text}`);
|
|
41
|
+
if (item.prohibitedActions !== undefined && (!Array.isArray(item.prohibitedActions) || item.prohibitedActions.some((entry) => typeof entry !== "string")))
|
|
42
|
+
throw new Error(`cases[${index}].prohibitedActions must be a string array`);
|
|
43
|
+
if (!OBSERVATION_STATUSES.includes(item.observedOutcome))
|
|
44
|
+
throw new Error(`cases[${index}].observedOutcome is invalid`);
|
|
45
|
+
requireString(item.outcomePredicate, `cases[${index}].outcomePredicate`);
|
|
46
|
+
requireString(item.evaluator, `cases[${index}].evaluator`);
|
|
47
|
+
if (!Array.isArray(item.limitations) || item.limitations.some((entry) => typeof entry !== "string"))
|
|
48
|
+
throw new Error(`cases[${index}].limitations must be a string array`);
|
|
49
|
+
if (item.timestamps !== undefined) {
|
|
50
|
+
const timestamps = requireRecord(item.timestamps, `cases[${index}].timestamps`);
|
|
51
|
+
rejectUnknown(timestamps, new Set(["startedAt", "completedAt"]), `cases[${index}].timestamps`);
|
|
52
|
+
requireIsoTime(timestamps.startedAt, `cases[${index}].timestamps.startedAt`);
|
|
53
|
+
requireIsoTime(timestamps.completedAt, `cases[${index}].timestamps.completedAt`);
|
|
54
|
+
}
|
|
55
|
+
if (item.artifacts !== undefined) {
|
|
56
|
+
if (!Array.isArray(item.artifacts))
|
|
57
|
+
throw new Error(`cases[${index}].artifacts must be an array`);
|
|
58
|
+
item.artifacts.forEach((artifact, artifactIndex) => {
|
|
59
|
+
const value = requireRecord(artifact, `cases[${index}].artifacts[${artifactIndex}]`);
|
|
60
|
+
rejectUnknown(value, new Set(["kind", "digestSha256", "size"]), `cases[${index}].artifacts[${artifactIndex}]`);
|
|
61
|
+
requireString(value.kind, `cases[${index}].artifacts[${artifactIndex}].kind`);
|
|
62
|
+
if (typeof value.digestSha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.digestSha256))
|
|
63
|
+
throw new Error(`cases[${index}].artifacts[${artifactIndex}].digestSha256 must be lowercase SHA-256`);
|
|
64
|
+
if (!Number.isSafeInteger(value.size) || Number(value.size) < 0)
|
|
65
|
+
throw new Error(`cases[${index}].artifacts[${artifactIndex}].size must be a non-negative safe integer`);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (const metric of ["costUsd", "latencyMs", "tokenUsage"]) {
|
|
69
|
+
if (item[metric] !== undefined && (typeof item[metric] !== "number" || !Number.isFinite(item[metric]) || item[metric] < 0))
|
|
70
|
+
throw new Error(`cases[${index}].${metric} must be a non-negative finite number`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
export function summarizeGenericEval(input) {
|
|
76
|
+
const value = validateGenericEval(input);
|
|
77
|
+
const total = value.cases.length;
|
|
78
|
+
const reportedSuccess = value.cases.filter((item) => item.reportedSuccess).length;
|
|
79
|
+
const independentlyVerifiable = value.cases.filter((item) => item.observedOutcome === "SATISFIED" || item.observedOutcome === "NOT_SATISFIED");
|
|
80
|
+
const independentlyVerifiedSuccess = independentlyVerifiable.filter((item) => item.observedOutcome === "SATISFIED").length;
|
|
81
|
+
const apparentTaskSuccess = reportedSuccess / total;
|
|
82
|
+
const verifiedRate = independentlyVerifiable.length > 0 ? independentlyVerifiedSuccess / independentlyVerifiable.length : null;
|
|
83
|
+
const metric = (field) => {
|
|
84
|
+
const values = value.cases.map((item) => item[field]);
|
|
85
|
+
return values.every((entry) => entry !== undefined) ? values.reduce((sum, entry) => sum + (entry ?? 0), 0) : null;
|
|
86
|
+
};
|
|
87
|
+
const warnings = [];
|
|
88
|
+
if (total < 20)
|
|
89
|
+
warnings.push("LOW_SAMPLE_SIZE: fewer than 20 cases; rates are descriptive and not a reliability claim.");
|
|
90
|
+
if (independentlyVerifiable.length !== total)
|
|
91
|
+
warnings.push("PARTIAL_OUTCOME_COVERAGE: the independently verified rate excludes NOT_OBSERVED, INDETERMINATE, and EXCLUDED cases.");
|
|
92
|
+
if (independentlyVerifiable.length === 0)
|
|
93
|
+
warnings.push("NO_INDEPENDENT_DENOMINATOR: Assurance Gap cannot be calculated.");
|
|
94
|
+
return {
|
|
95
|
+
schemaVersion: GENERIC_EVAL_REPORT_SCHEMA_VERSION,
|
|
96
|
+
sourceDigest: sha256Canonical(value),
|
|
97
|
+
suite: value.suite,
|
|
98
|
+
agent: value.agent,
|
|
99
|
+
environment: value.environment,
|
|
100
|
+
scopeFingerprint: value.scopeFingerprint,
|
|
101
|
+
counts: {
|
|
102
|
+
total,
|
|
103
|
+
reportedSuccess,
|
|
104
|
+
independentlyVerifiable: independentlyVerifiable.length,
|
|
105
|
+
independentlyVerifiedSuccess,
|
|
106
|
+
notObserved: value.cases.filter((item) => item.observedOutcome === "NOT_OBSERVED").length,
|
|
107
|
+
indeterminate: value.cases.filter((item) => item.observedOutcome === "INDETERMINATE").length,
|
|
108
|
+
excluded: value.cases.filter((item) => item.observedOutcome === "EXCLUDED").length,
|
|
109
|
+
},
|
|
110
|
+
rates: {
|
|
111
|
+
apparentTaskSuccess,
|
|
112
|
+
independentlyVerifiedSuccess: verifiedRate,
|
|
113
|
+
assuranceGap: verifiedRate === null ? null : apparentTaskSuccess - verifiedRate,
|
|
114
|
+
},
|
|
115
|
+
cost: { totalUsd: metric("costUsd"), totalTokens: metric("tokenUsage"), totalLatencyMs: metric("latencyMs") },
|
|
116
|
+
warnings,
|
|
117
|
+
authority: "IMPORTED_UNREVIEWED",
|
|
118
|
+
establishesCurrentAssurance: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export async function importGenericEval(path, out) {
|
|
122
|
+
const report = summarizeGenericEval(JSON.parse(await readFile(resolve(path), "utf8")));
|
|
123
|
+
if (out) {
|
|
124
|
+
const output = resolve(out);
|
|
125
|
+
await mkdir(dirname(output), { recursive: true });
|
|
126
|
+
await writeFile(output, `${JSON.stringify(report, null, 2)}\n`);
|
|
127
|
+
}
|
|
128
|
+
return report;
|
|
129
|
+
}
|
|
130
|
+
export function renderGenericEvalReport(report) {
|
|
131
|
+
const percent = (value) => value === null ? "not calculable" : `${(value * 100).toFixed(1)}%`;
|
|
132
|
+
return [
|
|
133
|
+
`Generic eval: ${report.suite}`,
|
|
134
|
+
`Agent: ${report.agent.name}@${report.agent.version}`,
|
|
135
|
+
`Apparent task success: ${percent(report.rates.apparentTaskSuccess)} (${report.counts.reportedSuccess}/${report.counts.total})`,
|
|
136
|
+
`Independently verified success: ${percent(report.rates.independentlyVerifiedSuccess)} (${report.counts.independentlyVerifiedSuccess}/${report.counts.independentlyVerifiable})`,
|
|
137
|
+
`Assurance Gap: ${percent(report.rates.assuranceGap)}`,
|
|
138
|
+
`Authority: ${report.authority}; CURRENT assurance: no`,
|
|
139
|
+
...report.warnings.map((warning) => `Warning: ${warning}`),
|
|
140
|
+
"",
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
|
143
|
+
function requireRecord(value, path) {
|
|
144
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
145
|
+
throw new Error(`${path} must be an object`);
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
function requireString(value, path) {
|
|
149
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
150
|
+
throw new Error(`${path} must be a non-empty string`);
|
|
151
|
+
}
|
|
152
|
+
function requireIsoTime(value, path) {
|
|
153
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value)))
|
|
154
|
+
throw new Error(`${path} must be an ISO timestamp`);
|
|
155
|
+
}
|
|
156
|
+
function rejectUnknown(value, allowed, path) {
|
|
157
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
158
|
+
if (unknown.length > 0)
|
|
159
|
+
throw new Error(`${path}.${unknown.join(",")} is not allowed`);
|
|
160
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { createPrivateKey, generateKeyPairSync, randomBytes, randomUUID, sign } from "node:crypto";
|
|
3
|
+
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { canonicalBytesV02, sha256CanonicalV02 } from "./canonical-v02.js";
|
|
6
|
+
import { approvalDigestV02, signApprovalV02, signExecutionGrantV02, verifyAuthorizationV02 } from "./authorization-v02.js";
|
|
7
|
+
import { verifyEvidencePacketV02 } from "./evidence-v02.js";
|
|
8
|
+
import { signProbeRequest, verifyProbeAttestation } from "./probe-protocol-v02.js";
|
|
9
|
+
import { appendRevocation, createTrustRoot, issueKeyCertificate } from "./trust-v02.js";
|
|
10
|
+
export function githubActionDigest(identity) {
|
|
11
|
+
return sha256CanonicalV02(identity);
|
|
12
|
+
}
|
|
13
|
+
export function createGitHubAcceptanceIdentity(state, targetUrl) {
|
|
14
|
+
const contractDigest = sha256CanonicalV02({ capability: "github.pull_request.merge", target: `${state.owner}/${state.repository}`, version: "v0.2" });
|
|
15
|
+
const resourceCommitment = sha256CanonicalV02({ type: "github.pull_request.read.v0.2", repository: `${state.owner}/${state.repository}`, pullRequestNumber: state.pullRequestNumber });
|
|
16
|
+
return {
|
|
17
|
+
protocolVersion: "witnora.github_pull_request_merge.v0.2", owner: state.owner, repository: state.repository,
|
|
18
|
+
pullRequestNumber: state.pullRequestNumber, baseBranch: state.baseBranch, approvedBaseSha: state.baseSha,
|
|
19
|
+
headBranch: state.headBranch, approvedHeadSha: state.headSha, mergeMethod: "squash", requiredChecks: Object.keys(state.checks).sort(),
|
|
20
|
+
agentRef: "agent:design-partner", agentBuildDigest: sha256CanonicalV02("design-partner-agent-v0.2"), contractDigest,
|
|
21
|
+
policyVersion: "witnora.github.sandbox.v0.2", mandateVersion: "mandate:v1", resourceCommitment,
|
|
22
|
+
parameterCommitment: sha256CanonicalV02({ mergeMethod: "squash", baseSha: state.baseSha, headSha: state.headSha }),
|
|
23
|
+
executionBoundaryDigest: executionBoundaryDigest(targetUrl),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function executionBoundaryDigest(targetUrl) {
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = new URL(targetUrl);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error("Gateway target URL must be an absolute URL.");
|
|
33
|
+
}
|
|
34
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
35
|
+
throw new Error("Gateway target URL cannot contain credentials, query parameters, or fragments.");
|
|
36
|
+
const isLoopback = ["localhost", "127.0.0.1", "::1", "target-sandbox"].includes(parsed.hostname);
|
|
37
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLoopback))
|
|
38
|
+
throw new Error("Gateway target URL must use HTTPS outside the local sandbox.");
|
|
39
|
+
const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
40
|
+
return sha256CanonicalV02({ protocolVersion: "witnora.http_execution_boundary.v0.2", origin: parsed.origin.toLowerCase(), pathname });
|
|
41
|
+
}
|
|
42
|
+
export function validateSandboxIdentity(identity, allowMain = false) {
|
|
43
|
+
if (!/sandbox|test/iu.test(identity.repository))
|
|
44
|
+
throw new Error("GitHub acceptance repository must be explicitly named as sandbox or test.");
|
|
45
|
+
if (!allowMain && ["main", "master"].includes(identity.baseBranch))
|
|
46
|
+
throw new Error("Refusing main/master without an explicit sandbox override.");
|
|
47
|
+
if (!identity.owner || !identity.repository || !Number.isSafeInteger(identity.pullRequestNumber) || identity.pullRequestNumber < 1)
|
|
48
|
+
throw new Error("Invalid GitHub target identity.");
|
|
49
|
+
if (!/^[a-f0-9]{64}$/.test(identity.executionBoundaryDigest))
|
|
50
|
+
throw new Error("A canonical execution-boundary commitment is required.");
|
|
51
|
+
if (identity.requiredChecks.length === 0)
|
|
52
|
+
throw new Error("At least one required check is needed for acceptance.");
|
|
53
|
+
}
|
|
54
|
+
export async function startGitHubTargetEmulator(input) {
|
|
55
|
+
const statePath = resolve(input.statePath);
|
|
56
|
+
let mutationQueue = Promise.resolve();
|
|
57
|
+
const withMutationLock = async (operation) => {
|
|
58
|
+
const previous = mutationQueue;
|
|
59
|
+
let release;
|
|
60
|
+
mutationQueue = new Promise((resolveQueue) => { release = resolveQueue; });
|
|
61
|
+
await previous;
|
|
62
|
+
try {
|
|
63
|
+
return await withFileLock(`${statePath}.lock`, operation);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
release();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const server = createServer(async (request, response) => {
|
|
70
|
+
try {
|
|
71
|
+
const state = JSON.parse(await readFile(statePath, "utf8"));
|
|
72
|
+
if (request.method === "GET" && request.url === "/health")
|
|
73
|
+
return json(response, 200, { status: "ok", targetVersion: state.targetVersion });
|
|
74
|
+
if (request.url !== "/v0.2/github/pull-request")
|
|
75
|
+
return json(response, 404, { error: "not_found" });
|
|
76
|
+
if (request.method === "GET") {
|
|
77
|
+
requireBearer(request, input.readCredential);
|
|
78
|
+
return json(response, 200, state);
|
|
79
|
+
}
|
|
80
|
+
if (request.method === "POST") {
|
|
81
|
+
requireBearer(request, input.writeCredential);
|
|
82
|
+
const body = await readJsonBody(request);
|
|
83
|
+
const result = await withMutationLock(async () => {
|
|
84
|
+
const current = JSON.parse(await readFile(statePath, "utf8"));
|
|
85
|
+
if (!body.idempotencyKey)
|
|
86
|
+
throw new HttpError(422, "idempotency_key_required");
|
|
87
|
+
if (body.identity.executionBoundaryDigest !== input.expectedExecutionBoundaryDigest)
|
|
88
|
+
throw new HttpError(403, "execution_boundary_mismatch");
|
|
89
|
+
const actionDigest = githubActionDigest(body.identity);
|
|
90
|
+
try {
|
|
91
|
+
verifyAuthorizationV02({ approval: body.approval, grant: body.grant, trust: { root: input.authorizationTrust.root, approvalCertificate: input.authorizationTrust.approvalCertificate, grantCertificate: input.authorizationTrust.grantCertificate, revocationJournal: input.authorizationTrust.journal }, expected: { tenantRef: body.grant.tenantRef, projectRef: body.grant.projectRef, environment: body.grant.environment, actionDigest } });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new HttpError(403, "signed_authorization_invalid");
|
|
95
|
+
}
|
|
96
|
+
if (body.idempotencyKey !== body.grant.grantId)
|
|
97
|
+
throw new HttpError(409, "grant_idempotency_binding_mismatch");
|
|
98
|
+
if (current.consumedGrantIds?.includes(body.grant.grantId))
|
|
99
|
+
throw new HttpError(409, "grant_replay_rejected");
|
|
100
|
+
assertIdentityMatchesState(body.identity, current);
|
|
101
|
+
if (current.merged)
|
|
102
|
+
throw new HttpError(409, "target_already_changed_without_this_grant");
|
|
103
|
+
if (!current.open || !current.mergeable || body.identity.requiredChecks.some((check) => current.checks[check] !== "success"))
|
|
104
|
+
throw new HttpError(409, "preconditions_not_satisfied");
|
|
105
|
+
if (current.simulateFalseSuccess)
|
|
106
|
+
return current;
|
|
107
|
+
const next = {
|
|
108
|
+
...current, open: false, merged: true, mergedAt: new Date().toISOString(),
|
|
109
|
+
mergeCommitSha: sha256CanonicalV02({ base: current.baseSha, head: current.headSha, method: body.identity.mergeMethod }),
|
|
110
|
+
baseSha: sha256CanonicalV02({ base: current.baseSha, head: current.headSha }), targetVersion: current.targetVersion + 1,
|
|
111
|
+
consumedGrantIds: [...(current.consumedGrantIds ?? []), body.grant.grantId],
|
|
112
|
+
};
|
|
113
|
+
await atomicWriteJson(statePath, next);
|
|
114
|
+
return next;
|
|
115
|
+
});
|
|
116
|
+
return json(response, 200, { accepted: true, alreadyApplied: false, state: result });
|
|
117
|
+
}
|
|
118
|
+
return json(response, 405, { error: "method_not_allowed" });
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error instanceof HttpError)
|
|
122
|
+
return json(response, error.status, { error: error.message });
|
|
123
|
+
return json(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const host = input.host ?? "0.0.0.0";
|
|
127
|
+
await new Promise((resolvePromise, reject) => server.once("error", reject).listen(input.port, host, resolvePromise));
|
|
128
|
+
process.stdout.write(`Witnora GitHub sandbox emulator ready on ${host}:${input.port}\n`);
|
|
129
|
+
const shutdown = () => server.close(() => process.exit(0));
|
|
130
|
+
process.on("SIGTERM", shutdown);
|
|
131
|
+
process.on("SIGINT", shutdown);
|
|
132
|
+
return server;
|
|
133
|
+
}
|
|
134
|
+
async function withFileLock(lockPath, operation) {
|
|
135
|
+
const token = `${process.pid}:${randomUUID()}`;
|
|
136
|
+
const deadline = Date.now() + 5_000;
|
|
137
|
+
while (true) {
|
|
138
|
+
try {
|
|
139
|
+
const handle = await open(lockPath, "wx", 0o600);
|
|
140
|
+
try {
|
|
141
|
+
await handle.writeFile(`${token}\n${new Date().toISOString()}\n`);
|
|
142
|
+
await handle.sync();
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
await handle.close();
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error.code !== "EEXIST")
|
|
151
|
+
throw error;
|
|
152
|
+
const lockStat = await stat(lockPath).catch((statError) => {
|
|
153
|
+
if (statError.code === "ENOENT")
|
|
154
|
+
return undefined;
|
|
155
|
+
throw statError;
|
|
156
|
+
});
|
|
157
|
+
if (!lockStat)
|
|
158
|
+
continue;
|
|
159
|
+
const ageMs = Date.now() - lockStat.mtimeMs;
|
|
160
|
+
if (ageMs > 30_000)
|
|
161
|
+
await unlink(lockPath).catch(() => undefined);
|
|
162
|
+
else if (Date.now() >= deadline)
|
|
163
|
+
throw new Error("Timed out acquiring the cross-process target mutation lock.");
|
|
164
|
+
else
|
|
165
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return await operation();
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
const owner = await readFile(lockPath, "utf8").catch(() => "");
|
|
173
|
+
if (owner.startsWith(`${token}\n`))
|
|
174
|
+
await unlink(lockPath).catch(() => undefined);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function runDesignPartnerAcceptance(options) {
|
|
178
|
+
const started = Date.now();
|
|
179
|
+
const outDir = resolve(options.outDir);
|
|
180
|
+
await mkdir(outDir, { recursive: true });
|
|
181
|
+
const state = structuredClone(options.declaredTarget);
|
|
182
|
+
if (state.merged)
|
|
183
|
+
throw new Error("The declared acceptance target must start unmerged.");
|
|
184
|
+
const tenantRef = options.tenantRef ?? "tenant:design-partner";
|
|
185
|
+
const projectRef = options.projectRef ?? "project:github-sandbox";
|
|
186
|
+
const environment = options.environment ?? "sandbox";
|
|
187
|
+
const identity = createGitHubAcceptanceIdentity(state, options.targetUrl);
|
|
188
|
+
const contractDigest = identity.contractDigest;
|
|
189
|
+
const observationQuery = { type: "github.pull_request.read.v0.2", repository: `${state.owner}/${state.repository}`, pullRequestNumber: state.pullRequestNumber };
|
|
190
|
+
const resourceCommitment = sha256CanonicalV02(observationQuery);
|
|
191
|
+
validateSandboxIdentity(identity);
|
|
192
|
+
const actionDigest = githubActionDigest(identity);
|
|
193
|
+
const keys = options.trustMaterial ?? createAcceptanceTrustMaterial({ tenantRef, projectRef, environment });
|
|
194
|
+
verifyAuthorizationV02({ approval: options.authorization.approval, grant: options.authorization.grant, trust: { root: keys.root, approvalCertificate: keys.approvalCertificate, grantCertificate: keys.grantCertificate, revocationJournal: keys.journal }, expected: { tenantRef, projectRef, environment, actionDigest } });
|
|
195
|
+
const preconditionExpected = { type: "github.pull_request.outcome.v0.2", merged: false, headSha: identity.approvedHeadSha, baseBranch: identity.baseBranch };
|
|
196
|
+
const targetGrantEchoRequired = options.requireTargetGrantEcho !== false;
|
|
197
|
+
const outcomeExpected = {
|
|
198
|
+
...preconditionExpected,
|
|
199
|
+
merged: true,
|
|
200
|
+
...(targetGrantEchoRequired ? { consumedGrantId: options.authorization.grant.grantId } : {}),
|
|
201
|
+
};
|
|
202
|
+
const predicateDigest = sha256CanonicalV02(outcomeExpected);
|
|
203
|
+
const attemptPath = resolve(options.attemptStatePath ?? resolve(outDir, "gateway-attempt-v0.2.json"));
|
|
204
|
+
const claimDirectory = resolve(options.grantClaimDirectory ?? resolve(dirname(attemptPath), "grant-claims"));
|
|
205
|
+
const claimPath = resolve(claimDirectory, `${options.authorization.grant.grantId}.json`);
|
|
206
|
+
const firstObservationAt = Date.now();
|
|
207
|
+
let attempt;
|
|
208
|
+
if (options.recoverOnly) {
|
|
209
|
+
attempt = JSON.parse(await readFile(attemptPath, "utf8"));
|
|
210
|
+
if (attempt.protocolVersion !== "witnora.gateway_execution_attempt.v0.2" || attempt.identity.protocolVersion !== identity.protocolVersion || githubActionDigest(attempt.identity) !== actionDigest || attempt.grant.grantId !== options.authorization.grant.grantId)
|
|
211
|
+
throw new Error("Durable recovery state does not match the exact authorized action.");
|
|
212
|
+
if (!["DISPATCH_STARTED", "DISPATCH_UNKNOWN", "DISPATCH_ACKNOWLEDGED", "RECONCILIATION_PENDING"].includes(attempt.state) || attempt.dispatchCount !== 1)
|
|
213
|
+
throw new Error("Only a claimed single-dispatch attempt can enter reconciliation recovery.");
|
|
214
|
+
await assertGrantClaim(claimPath, attempt.attemptId, attempt.grant.grantId);
|
|
215
|
+
attempt.state = "RECONCILIATION_PENDING";
|
|
216
|
+
attempt.recoveryHistory.push({ state: "RECONCILIATION_PENDING", occurredAt: new Date().toISOString(), reason: "A new Gateway process resumed Probe-only reconciliation without redispatching." });
|
|
217
|
+
await atomicWriteJson(attemptPath, attempt);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const request = signProbeRequest({
|
|
221
|
+
nonce: randomBytes(24).toString("hex"), tenantRef, projectRef, environment, actionDigest, contractDigest,
|
|
222
|
+
resourceCommitment, predicateDigest: sha256CanonicalV02(preconditionExpected), probeId: "customer-github-probe", notBefore: new Date(Date.now() - 5_000).toISOString(),
|
|
223
|
+
expiresAt: new Date(Date.now() + 120_000).toISOString(), requestedAt: new Date().toISOString(), requestSignerKeyId: keys.requestSignerCertificate.subjectKeyId,
|
|
224
|
+
}, keys.requestSignerPrivateKeyPem);
|
|
225
|
+
const precondition = await requestProbe(options.probeUrl, request, { expected: preconditionExpected, query: observationQuery });
|
|
226
|
+
verifyAcceptanceObservation(precondition, request, keys, { tenantRef, projectRef, environment, actionDigest, contractDigest, resourceCommitment, predicateDigest: request.predicateDigest });
|
|
227
|
+
if (precondition.outcomeStatus !== "VERIFIED")
|
|
228
|
+
throw new Error("Independent Probe did not verify GitHub preconditions.");
|
|
229
|
+
attempt = { protocolVersion: "witnora.gateway_execution_attempt.v0.2", attemptId: randomUUID(), identity, approval: options.authorization.approval, grant: options.authorization.grant, state: "RESERVED", dispatchCount: 0, recoveryHistory: [{ state: "RESERVED", occurredAt: new Date().toISOString(), reason: "Signed single-use Grant reserved in durable local state." }] };
|
|
230
|
+
await claimGrantV02(claimPath, attempt.attemptId, attempt.grant.grantId);
|
|
231
|
+
await atomicWriteJson(attemptPath, attempt);
|
|
232
|
+
attempt.state = "DISPATCH_STARTED";
|
|
233
|
+
attempt.dispatchCount = 1;
|
|
234
|
+
attempt.recoveryHistory.push({ state: "DISPATCH_STARTED", occurredAt: new Date().toISOString(), reason: "Durable dispatch intent recorded before the exact action left the Gateway." });
|
|
235
|
+
await atomicWriteJson(attemptPath, attempt);
|
|
236
|
+
if (options.dispatchTarget) {
|
|
237
|
+
await options.dispatchTarget({ identity, approval: options.authorization.approval, grant: options.authorization.grant });
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
await targetMerge(options.targetUrl, options.writeCredential, identity, options.authorization.approval, options.authorization.grant);
|
|
241
|
+
}
|
|
242
|
+
if (options.crashAfterTargetSuccess) {
|
|
243
|
+
attempt.state = "DISPATCH_UNKNOWN";
|
|
244
|
+
attempt.recoveryHistory.push({ state: "DISPATCH_UNKNOWN", occurredAt: new Date().toISOString(), reason: "Target accepted the request but the Gateway crashed before recording an acknowledgment; redispatch is forbidden." });
|
|
245
|
+
await atomicWriteJson(attemptPath, attempt);
|
|
246
|
+
throw new InjectedGatewayCrashError(attemptPath);
|
|
247
|
+
}
|
|
248
|
+
attempt.state = "DISPATCH_ACKNOWLEDGED";
|
|
249
|
+
attempt.recoveryHistory.push({ state: "DISPATCH_ACKNOWLEDGED", occurredAt: new Date().toISOString(), reason: "Target response recorded after the durable dispatch marker." });
|
|
250
|
+
await atomicWriteJson(attemptPath, attempt);
|
|
251
|
+
if (options.crashAfterTargetAcknowledged)
|
|
252
|
+
throw new InjectedGatewayCrashError(attemptPath);
|
|
253
|
+
}
|
|
254
|
+
const outcomeRequest = signProbeRequest({
|
|
255
|
+
nonce: randomBytes(24).toString("hex"), tenantRef, projectRef, environment, actionDigest, contractDigest,
|
|
256
|
+
resourceCommitment, predicateDigest, probeId: "customer-github-probe", notBefore: new Date(Date.now() - 5_000).toISOString(),
|
|
257
|
+
expiresAt: new Date(Date.now() + 120_000).toISOString(), requestedAt: new Date().toISOString(), requestSignerKeyId: keys.requestSignerCertificate.subjectKeyId,
|
|
258
|
+
}, keys.requestSignerPrivateKeyPem);
|
|
259
|
+
const outcome = await requestProbe(options.probeUrl, outcomeRequest, { expected: outcomeExpected, query: observationQuery });
|
|
260
|
+
verifyAcceptanceObservation(outcome, outcomeRequest, keys, { tenantRef, projectRef, environment, actionDigest, contractDigest, resourceCommitment, predicateDigest });
|
|
261
|
+
const reconciled = outcome.outcomeStatus === "VERIFIED";
|
|
262
|
+
attempt.state = reconciled ? "RECONCILED_SUCCESS" : "RECONCILED_FAILURE";
|
|
263
|
+
attempt.recoveryHistory.push({ state: attempt.state, occurredAt: new Date().toISOString(), reason: "Customer-operated Probe reconciled target state; no recovery path redispatched the action." });
|
|
264
|
+
await atomicWriteJson(attemptPath, attempt);
|
|
265
|
+
const unsigned = {
|
|
266
|
+
protocolVersion: "witnora.verifiable_evidence_packet.v0.2", packetId: randomUUID(), issuedAt: new Date().toISOString(),
|
|
267
|
+
tenantRef, projectRef, environment, actionDigest, contractDigest, resourceCommitment, predicateDigest, approval: options.authorization.approval,
|
|
268
|
+
grant: options.authorization.grant, executionAttempt: { attemptId: attempt.attemptId, state: attempt.state, dispatchCount: attempt.dispatchCount, recoveryHistory: attempt.recoveryHistory },
|
|
269
|
+
reconciliation: { status: reconciled ? "TARGET_STATE_RECONCILED" : "UNKNOWN", authorizationBindingSource: targetGrantEchoRequired ? "TARGET_GRANT_ECHO" : "GATEWAY_DISPATCH_RECORD", targetVersion: outcome.targetVersion, ...(reconciled ? { reconciledAt: outcome.observedAt } : {}) },
|
|
270
|
+
probeRequest: outcomeRequest, probeAttestation: outcome,
|
|
271
|
+
trust: { rootId: keys.root.rootId, requestSignerCertificate: keys.requestSignerCertificate, gatewayCertificate: keys.gatewayCertificate, probeCertificate: keys.probeCertificate, approvalCertificate: keys.approvalCertificate, grantCertificate: keys.grantCertificate, revocationJournal: keys.journal },
|
|
272
|
+
separation: { probeDeploymentMode: options.probeDeploymentMode ?? "separate_process", networkPolicyDigest: sha256CanonicalV02({ gateway: "write-only", probe: "read-only" }), readCredentialClass: "github-fine-grained-read-only", writeCredentialIsolationClaim: "Gateway-only secret reference; not disclosed in evidence." },
|
|
273
|
+
privacy: { manifestDigest: sha256CanonicalV02({ default: "deny", credentials: "local_only", rawObservation: "local_only" }), egressReportDigest: sha256CanonicalV02({ rawCanaryMatchesOutsideLocal: 0 }), rawCanaryMatchesOutsideLocal: 0 },
|
|
274
|
+
eventChainDigest: sha256CanonicalV02(attempt.recoveryHistory), artifactManifestDigest: sha256CanonicalV02([]),
|
|
275
|
+
evidenceStrength: { coverage: "COMPLETE_MEDIATION_DECLARED", sourceIntegrity: "CUSTOMER_ROOT_CERTIFIED", authorizationBinding: targetGrantEchoRequired ? "EXACT_ACTION_GRANT_BOUND" : "GATEWAY_DISPATCH_BOUND", executionBinding: reconciled ? "TARGET_STATE_RECONCILED" : "DISPATCH_UNKNOWN", outcomeObservation: "CUSTOMER_OPERATED_READ_PATH", operatorSeparation: options.probeDeploymentMode === "customer_network" ? "CUSTOMER_NETWORK_SEPARATED" : options.probeDeploymentMode === "separate_container" ? "CONTAINER_SEPARATED" : "PROCESS_SEPARATED", completeness: "EVENT_AND_RECOVERY_CHAIN_CHECKED", review: "INTERNAL" },
|
|
276
|
+
limitations: ["Complete mediation is declared for this configured sandbox path, not universally established.", "GitHub read-state reconciliation does not establish complete target audit-log coverage.", ...(targetGrantEchoRequired ? [] : ["The live GitHub target does not echo the Grant ID; authorization is bound to the signed Gateway dispatch record, not to a target-side Grant receipt."]), "No organizationally independent review was performed."],
|
|
277
|
+
};
|
|
278
|
+
const packet = { ...unsigned, gatewaySignature: { keyId: keys.gatewayCertificate.subjectKeyId, signature: sign(null, canonicalBytesV02(unsigned), createPrivateKey(keys.gatewayPrivateKeyPem)).toString("base64") } };
|
|
279
|
+
const report = verifyEvidencePacketV02(packet, keys.root, new Date(), keys.journal);
|
|
280
|
+
const paths = {
|
|
281
|
+
packet: resolve(outDir, "witnora-evidence-packet-v0.2.json"), trustRoot: resolve(outDir, "pinned-trust-root-v0.2.json"),
|
|
282
|
+
revocationJournal: resolve(outDir, "revocation-journal-v0.2.json"), report: resolve(outDir, "offline-verification-report-v0.2.json"),
|
|
283
|
+
};
|
|
284
|
+
await Promise.all([
|
|
285
|
+
writeFile(paths.packet, `${JSON.stringify(packet, null, 2)}\n`), writeFile(paths.trustRoot, `${JSON.stringify(keys.root, null, 2)}\n`),
|
|
286
|
+
writeFile(paths.revocationJournal, `${JSON.stringify(keys.journal, null, 2)}\n`), writeFile(paths.report, `${JSON.stringify(report, null, 2)}\n`),
|
|
287
|
+
]);
|
|
288
|
+
return { packet, report, paths, metrics: { timeToFirstValidContractMs: firstObservationAt - started, timeToFirstSignedObservationMs: Date.now() - firstObservationAt, timeToFirstEvidencePacketMs: Date.now() - started, manualInterventions: 1, failedSetupSteps: 0, outcomeVerificationLatencyMs: Date.now() - firstObservationAt } };
|
|
289
|
+
}
|
|
290
|
+
export async function writeGitHubEmulatorState(path) {
|
|
291
|
+
const state = { owner: "witnora-design-partner", repository: "assurance-sandbox", pullRequestNumber: 17, baseBranch: "acceptance", baseSha: "base-a", headBranch: "agent-change", headSha: "head-a", open: true, merged: false, checks: { build: "success", policy: "success" }, mergeable: true, targetVersion: 1 };
|
|
292
|
+
await mkdir(dirname(resolve(path)), { recursive: true });
|
|
293
|
+
await writeFile(resolve(path), `${JSON.stringify(state, null, 2)}\n`);
|
|
294
|
+
}
|
|
295
|
+
export function createAcceptanceTrustMaterial(scope) {
|
|
296
|
+
const certificateScope = { tenantRef: scope.tenantRef, projectRef: scope.projectRef, environment: scope.environment };
|
|
297
|
+
const rootKeys = generateKeyPairSync("ed25519");
|
|
298
|
+
const requestSignerKeys = generateKeyPairSync("ed25519");
|
|
299
|
+
const gatewayKeys = generateKeyPairSync("ed25519");
|
|
300
|
+
const probeKeys = generateKeyPairSync("ed25519");
|
|
301
|
+
const approvalKeys = generateKeyPairSync("ed25519");
|
|
302
|
+
const grantKeys = generateKeyPairSync("ed25519");
|
|
303
|
+
const pem = (key, kind) => key.export({ format: "pem", type: kind === "private" ? "pkcs8" : "spki" }).toString();
|
|
304
|
+
const now = Date.now();
|
|
305
|
+
const root = createTrustRoot({ rootId: "root:customer-assurance", publicKeyPem: pem(rootKeys.publicKey, "public"), algorithm: "Ed25519", ownerType: "CUSTOMER_ASSURANCE_ROOT", allowedRoles: ["server_attestor", "gateway_runtime", "outcome_probe", "reviewer", "resource_scope_evaluator"], organizationRef: scope.tenantRef, createdAt: new Date(now).toISOString(), notBefore: new Date(now - 60_000).toISOString(), notAfter: new Date(now + 86_400_000).toISOString(), policyVersion: "v0.2" });
|
|
306
|
+
const issue = (role, keyId, publicKeyPem, operatorIdentity, serialNumber) => issueKeyCertificate({ certificateId: `cert:${keyId}`, serialNumber, subjectKeyId: keyId, publicKeyPem, issuerRootId: root.rootId, role, ...certificateScope, operatorIdentity, notBefore: new Date(now - 60_000).toISOString(), notAfter: new Date(now + 3_600_000).toISOString() }, pem(rootKeys.privateKey, "private"));
|
|
307
|
+
const requestSignerCertificate = issue("server_attestor", "key:request-signer", pem(requestSignerKeys.publicKey, "public"), "witnora-request-signer", 1);
|
|
308
|
+
const gatewayCertificate = issue("gateway_runtime", "key:gateway", pem(gatewayKeys.publicKey, "public"), "customer-gateway-process", 2);
|
|
309
|
+
const probeCertificate = issue("outcome_probe", "key:probe", pem(probeKeys.publicKey, "public"), "customer-github-probe", 3);
|
|
310
|
+
const approvalCertificate = issue("reviewer", "key:approval-authority", pem(approvalKeys.publicKey, "public"), "customer-approval-authority", 4);
|
|
311
|
+
const grantCertificate = issue("resource_scope_evaluator", "key:grant-issuer", pem(grantKeys.publicKey, "public"), "customer-grant-issuer", 5);
|
|
312
|
+
const journal = appendRevocation(undefined, { revokedId: "none", effectiveAt: new Date(now + 86_400_000).toISOString(), retroactive: false, reason: "Initial signed empty-equivalent checkpoint.", issuerRootId: root.rootId }, pem(rootKeys.privateKey, "private"));
|
|
313
|
+
return { root, requestSignerCertificate, gatewayCertificate, probeCertificate, approvalCertificate, grantCertificate, journal, rootPrivateKeyPem: pem(rootKeys.privateKey, "private"), requestSignerPrivateKeyPem: pem(requestSignerKeys.privateKey, "private"), gatewayPrivateKeyPem: pem(gatewayKeys.privateKey, "private"), probePrivateKeyPem: pem(probeKeys.privateKey, "private"), approvalPrivateKeyPem: pem(approvalKeys.privateKey, "private"), grantPrivateKeyPem: pem(grantKeys.privateKey, "private") };
|
|
314
|
+
}
|
|
315
|
+
export function issueAcceptanceAuthorization(material, input) {
|
|
316
|
+
const now = input.now ?? new Date();
|
|
317
|
+
const expiresAt = new Date(now.getTime() + 15 * 60_000).toISOString();
|
|
318
|
+
const approval = signApprovalV02({ tenantRef: input.tenantRef, projectRef: input.projectRef, environment: input.environment, actionDigest: input.actionDigest, approverRef: "reviewer:design-partner", approvedAt: now.toISOString(), expiresAt, signerKeyId: material.approvalCertificate.subjectKeyId }, material.approvalPrivateKeyPem);
|
|
319
|
+
const grant = signExecutionGrantV02({ approvalDigest: approvalDigestV02(approval), tenantRef: input.tenantRef, projectRef: input.projectRef, environment: input.environment, actionDigest: input.actionDigest, maxUses: 1, issuedAt: now.toISOString(), notBefore: new Date(now.getTime() - 5_000).toISOString(), expiresAt, issuerKeyId: material.grantCertificate.subjectKeyId }, material.grantPrivateKeyPem);
|
|
320
|
+
return { approval, grant };
|
|
321
|
+
}
|
|
322
|
+
export function revokeProbeCertificate(material, input) {
|
|
323
|
+
return { ...material, journal: appendRevocation(material.journal, { revokedId: material.probeCertificate.certificateId, effectiveAt: input.effectiveAt, retroactive: input.retroactive, reason: input.reason, issuerRootId: material.root.rootId }, material.rootPrivateKeyPem) };
|
|
324
|
+
}
|
|
325
|
+
export function rotateProbeCertificate(material, now = new Date()) {
|
|
326
|
+
const keys = generateKeyPairSync("ed25519");
|
|
327
|
+
const publicKeyPem = keys.publicKey.export({ format: "pem", type: "spki" }).toString();
|
|
328
|
+
const privateKeyPem = keys.privateKey.export({ format: "pem", type: "pkcs8" }).toString();
|
|
329
|
+
const serialNumber = Math.max(material.requestSignerCertificate.serialNumber, material.gatewayCertificate.serialNumber, material.probeCertificate.serialNumber) + 1;
|
|
330
|
+
const certificate = issueKeyCertificate({ certificateId: `cert:key:probe:${serialNumber}`, serialNumber, subjectKeyId: `key:probe:${serialNumber}`, publicKeyPem, issuerRootId: material.root.rootId, role: "outcome_probe", tenantRef: material.probeCertificate.tenantRef, projectRef: material.probeCertificate.projectRef, environment: material.probeCertificate.environment, operatorIdentity: material.probeCertificate.operatorIdentity, notBefore: new Date(now.getTime() - 60_000).toISOString(), notAfter: new Date(now.getTime() + 3_600_000).toISOString() }, material.rootPrivateKeyPem);
|
|
331
|
+
return { ...material, probeCertificate: certificate, probePrivateKeyPem: privateKeyPem };
|
|
332
|
+
}
|
|
333
|
+
function verifyAcceptanceObservation(attestation, request, keys, binding) {
|
|
334
|
+
verifyProbeAttestation(attestation, request, { root: keys.root, certificate: keys.probeCertificate, revocationJournal: keys.journal }, { ...binding, probeId: "customer-github-probe", now: new Date(attestation.observedAt), maxFreshnessMs: 300_000 });
|
|
335
|
+
}
|
|
336
|
+
export async function readGitHubTargetState(url, credential) {
|
|
337
|
+
const response = await fetch(`${url}/v0.2/github/pull-request`, { headers: { authorization: `Bearer ${credential}` } });
|
|
338
|
+
if (!response.ok)
|
|
339
|
+
throw new Error(`Target read failed with ${response.status}.`);
|
|
340
|
+
return response.json();
|
|
341
|
+
}
|
|
342
|
+
async function targetMerge(url, credential, identity, approval, grant) {
|
|
343
|
+
const response = await fetch(`${url}/v0.2/github/pull-request`, { method: "POST", headers: { authorization: `Bearer ${credential}`, "content-type": "application/json" }, body: JSON.stringify({ identity, approval, grant, idempotencyKey: grant.grantId }) });
|
|
344
|
+
if (!response.ok)
|
|
345
|
+
throw new Error(`Gateway target dispatch failed with ${response.status}: ${await response.text()}`);
|
|
346
|
+
}
|
|
347
|
+
async function requestProbe(url, request, detail) {
|
|
348
|
+
const response = await fetch(`${url}/v0.2/observe`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ request, ...detail }) });
|
|
349
|
+
if (!response.ok)
|
|
350
|
+
throw new Error(`Probe observation failed with ${response.status}: ${await response.text()}`);
|
|
351
|
+
return response.json();
|
|
352
|
+
}
|
|
353
|
+
function assertIdentityMatchesState(identity, state) {
|
|
354
|
+
validateSandboxIdentity(identity);
|
|
355
|
+
if (identity.owner !== state.owner || identity.repository !== state.repository || identity.pullRequestNumber !== state.pullRequestNumber || identity.baseBranch !== state.baseBranch || identity.approvedBaseSha !== state.baseSha || identity.headBranch !== state.headBranch || identity.approvedHeadSha !== state.headSha)
|
|
356
|
+
throw new HttpError(409, "approved_action_binding_mismatch");
|
|
357
|
+
}
|
|
358
|
+
function requireBearer(request, expected) { if (request.headers.authorization !== `Bearer ${expected}`)
|
|
359
|
+
throw new HttpError(401, "credential_invalid"); }
|
|
360
|
+
async function readJsonBody(request) { const chunks = []; for await (const chunk of request)
|
|
361
|
+
chunks.push(Buffer.from(chunk)); return JSON.parse(Buffer.concat(chunks).toString("utf8")); }
|
|
362
|
+
function json(response, status, value) { response.writeHead(status, { "content-type": "application/json" }); response.end(JSON.stringify(value)); }
|
|
363
|
+
class HttpError extends Error {
|
|
364
|
+
status;
|
|
365
|
+
constructor(status, message) {
|
|
366
|
+
super(message);
|
|
367
|
+
this.status = status;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export class InjectedGatewayCrashError extends Error {
|
|
371
|
+
attemptPath;
|
|
372
|
+
constructor(attemptPath) {
|
|
373
|
+
super(`Injected Gateway process crash after target acceptance. Durable attempt: ${attemptPath}`);
|
|
374
|
+
this.attemptPath = attemptPath;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function atomicWriteJson(path, value) { const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`; await mkdir(dirname(path), { recursive: true }); await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`); await rename(temporary, path); }
|
|
378
|
+
export async function claimGrantV02(path, attemptId, grantId) {
|
|
379
|
+
await mkdir(dirname(path), { recursive: true });
|
|
380
|
+
let handle;
|
|
381
|
+
try {
|
|
382
|
+
handle = await open(path, "wx", 0o600);
|
|
383
|
+
await handle.writeFile(`${JSON.stringify({ protocolVersion: "witnora.grant_claim.v0.2", grantId, attemptId, claimedAt: new Date().toISOString() })}\n`);
|
|
384
|
+
await handle.sync();
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (error.code === "EEXIST")
|
|
388
|
+
throw new Error("Execution Grant was already claimed by another Gateway attempt.");
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
await handle?.close();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function assertGrantClaim(path, attemptId, grantId) {
|
|
396
|
+
const claim = JSON.parse(await readFile(path, "utf8"));
|
|
397
|
+
if (claim.protocolVersion !== "witnora.grant_claim.v0.2" || claim.grantId !== grantId || claim.attemptId !== attemptId)
|
|
398
|
+
throw new Error("Durable Grant claim does not belong to this recovery attempt.");
|
|
399
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function assertLiveGitHubSafety(target, baseBranch, allowlistedRepository) {
|
|
2
|
+
const repository = `${target.owner}/${target.repository}`;
|
|
3
|
+
if (process.env.WITNORA_LIVE_GITHUB_PREFLIGHT !== "1")
|
|
4
|
+
throw new Error("Live GitHub read-only preflight is disabled. Set WITNORA_LIVE_GITHUB_PREFLIGHT=1 only in a protected sandbox environment.");
|
|
5
|
+
if (repository !== allowlistedRepository)
|
|
6
|
+
throw new Error("Live GitHub repository is not the explicit sandbox allowlist target.");
|
|
7
|
+
if (!/(?:sandbox|test)/iu.test(repository))
|
|
8
|
+
throw new Error("Live GitHub target name must contain sandbox or test.");
|
|
9
|
+
if (["main", "master"].includes(baseBranch.toLowerCase()))
|
|
10
|
+
throw new Error("Live GitHub acceptance refuses main/master.");
|
|
11
|
+
}
|
|
12
|
+
export async function readLiveGitHubPullRequest(input) {
|
|
13
|
+
const apiBase = (input.apiBase ?? "https://api.github.com").replace(/\/$/u, "");
|
|
14
|
+
const repositoryPath = `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repository)}`;
|
|
15
|
+
const pull = await githubJson(`${apiBase}${repositoryPath}/pulls/${input.pullRequestNumber}`, input.credential);
|
|
16
|
+
const head = pull.head;
|
|
17
|
+
const base = pull.base;
|
|
18
|
+
const status = await githubJson(`${apiBase}${repositoryPath}/commits/${String(head.sha)}/status`, input.credential);
|
|
19
|
+
const contexts = new Map((status.statuses ?? []).map((entry) => [entry.context, entry.state]));
|
|
20
|
+
const checks = Object.fromEntries(input.requiredChecks.map((name) => [name, normalizeCheck(contexts.get(name))]));
|
|
21
|
+
return {
|
|
22
|
+
owner: input.owner, repository: input.repository, pullRequestNumber: input.pullRequestNumber,
|
|
23
|
+
baseBranch: String(base.ref), baseSha: String(base.sha), headBranch: String(head.ref), headSha: String(head.sha),
|
|
24
|
+
open: pull.state === "open", merged: pull.merged === true, checks, mergeable: pull.mergeable === true,
|
|
25
|
+
targetVersion: Number(pull.updated_at ? Date.parse(String(pull.updated_at)) : 0),
|
|
26
|
+
...(pull.merged_at ? { mergedAt: String(pull.merged_at) } : {}),
|
|
27
|
+
...(pull.merge_commit_sha ? { mergeCommitSha: String(pull.merge_commit_sha) } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function githubJson(url, credential) {
|
|
31
|
+
const response = await fetch(url, { headers: githubHeaders(credential) });
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
throw new Error(`Live GitHub read failed with ${response.status}.`);
|
|
34
|
+
return response.json();
|
|
35
|
+
}
|
|
36
|
+
function githubHeaders(credential) {
|
|
37
|
+
return { authorization: `Bearer ${credential}`, accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28", "content-type": "application/json" };
|
|
38
|
+
}
|
|
39
|
+
function normalizeCheck(value) {
|
|
40
|
+
if (value === "success")
|
|
41
|
+
return "success";
|
|
42
|
+
if (value === "failure" || value === "error")
|
|
43
|
+
return "failure";
|
|
44
|
+
return "pending";
|
|
45
|
+
}
|
package/dist/guided-setup.js
CHANGED
|
@@ -86,7 +86,7 @@ function adapterWorkflow() {
|
|
|
86
86
|
return `name: Witnora Observation\n\non:\n pull_request:\n workflow_dispatch:\n\npermissions:\n contents: read\n\njobs:\n observe:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v7\n - uses: actions/setup-node@v6\n with:\n node-version: "20"\n - name: Record one bounded agent event\n run: node witnora.adapter.mjs\n env:\n WITNORA_BASE_URL: https://witnora.com\n WITNORA_PROJECT_ID: \${{ vars.WITNORA_PROJECT_ID }}\n WITNORA_API_KEY: \${{ secrets.WITNORA_API_KEY }}\n\n# This workflow proves event ingestion only. It does not establish CURRENT assurance.\n# Add a deterministic task assertion and evidence generation before using it as a release gate.\n`;
|
|
87
87
|
}
|
|
88
88
|
function mcpWorkflow(subject) {
|
|
89
|
-
return `name: Witnora MCP Assurance\n\non:\n workflow_dispatch:\n schedule:\n - cron: "17 3 * * *"\n\npermissions:\n contents: read\n\njobs:\n assurance:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v7\n - uses: actions/setup-node@v6\n with:\n node-version: "20"\n - name: Require an MCPBench result\n shell: bash\n run: test -f .mcpbench/latest/results.json || { echo "Run MCPBench before Witnora; expected .mcpbench/latest/results.json"; exit 2; }\n - run: npx
|
|
89
|
+
return `name: Witnora MCP Assurance\n\non:\n workflow_dispatch:\n schedule:\n - cron: "17 3 * * *"\n\npermissions:\n contents: read\n\njobs:\n assurance:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v7\n - uses: actions/setup-node@v6\n with:\n node-version: "20"\n - name: Require an MCPBench result\n shell: bash\n run: test -f .mcpbench/latest/results.json || { echo "Run MCPBench before Witnora; expected .mcpbench/latest/results.json"; exit 2; }\n - run: npx witnora@latest run --mcpbench .mcpbench/latest/results.json --subject ${JSON.stringify(subject)} --push\n env:\n WITNORA_BASE_URL: https://witnora.com\n WITNORA_PROJECT_ID: \${{ vars.WITNORA_PROJECT_ID }}\n WITNORA_API_KEY: \${{ secrets.WITNORA_API_KEY }}\n`;
|
|
90
90
|
}
|
|
91
91
|
async function exists(path) { try {
|
|
92
92
|
await access(path);
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./failure-review.js";
|
|
|
9
9
|
export * from "./evidence-signing.js";
|
|
10
10
|
export * from "./local-server.js";
|
|
11
11
|
export * from "./monitor.js";
|
|
12
|
+
export * from "./onboard.js";
|
|
12
13
|
export * from "./normalizers.js";
|
|
13
14
|
export * from "./report.js";
|
|
14
15
|
export * from "./release-gate.js";
|