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,375 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify } from "node:crypto";
|
|
2
|
+
import { compileAssuranceContract, parseContractDurationMs, validateAssuranceContract } from "./assurance-contract.js";
|
|
3
|
+
import { canonicalJson, sha256Canonical } from "./canonical.js";
|
|
4
|
+
import { validatePrivacyManifest, validatePrivacyTransformAgainstManifest } from "./privacy-manifest.js";
|
|
5
|
+
import { evaluateOutcome, predicateMatches } from "./outcome-evaluator.js";
|
|
6
|
+
export const VERIFIABLE_EVIDENCE_PACKET_VERSION = "witnora.verifiable_evidence_packet.v0.1";
|
|
7
|
+
export function sourceSignedValue(packet) {
|
|
8
|
+
return { packetId: packet.packetId, privacy: packet.privacy.transformed, action: packet.action, precondition: packet.precondition, resourceScopeProof: packet.resourceScopeProof, policy: packet.policy, approval: packet.approval, events: packet.events, outcome: packet.outcome };
|
|
9
|
+
}
|
|
10
|
+
export function serverAttestedValue(packet) {
|
|
11
|
+
return {
|
|
12
|
+
packetId: packet.packetId,
|
|
13
|
+
contractDigest: packet.compiledContract.sourceContractDigest,
|
|
14
|
+
privacyManifestDigest: packet.privacy.transformed.privacyManifestDigest,
|
|
15
|
+
privacyTransformDigest: sha256Canonical(packet.privacy.transformed),
|
|
16
|
+
actionDigest: packet.action.digest,
|
|
17
|
+
preconditionDigest: sha256Canonical(packet.precondition),
|
|
18
|
+
resourceScopeProofDigest: sha256Canonical(packet.resourceScopeProof),
|
|
19
|
+
grant: packet.grant,
|
|
20
|
+
sourceDigest: sha256Canonical(sourceSignedValue(packet)),
|
|
21
|
+
outcomeDigest: sha256Canonical(packet.outcome),
|
|
22
|
+
artifactManifestDigest: sha256Canonical(packet.artifacts.map(({ contentBase64: _content, ...entry }) => entry)),
|
|
23
|
+
evidenceStrength: packet.evidenceStrength,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function resourceScopeProofSignedValue(packet) {
|
|
27
|
+
const { signature: _signature, ...proof } = packet.resourceScopeProof;
|
|
28
|
+
return { packetId: packet.packetId, actionDigest: packet.action.digest, ...proof };
|
|
29
|
+
}
|
|
30
|
+
export function outcomeProbeSignedValue(packet) {
|
|
31
|
+
const { probeSignature: _signature, ...outcome } = packet.outcome;
|
|
32
|
+
return { packetId: packet.packetId, actionDigest: packet.action.digest, outcome };
|
|
33
|
+
}
|
|
34
|
+
export function preconditionProbeSignedValue(packet) {
|
|
35
|
+
const { probeSignature: _signature, ...precondition } = packet.precondition;
|
|
36
|
+
return { packetId: packet.packetId, actionDigest: packet.action.digest, precondition };
|
|
37
|
+
}
|
|
38
|
+
export function verifyEvidencePacket(input, now = new Date(), trustBundle) {
|
|
39
|
+
const steps = [];
|
|
40
|
+
const shapeError = validatePacketShape(input);
|
|
41
|
+
if (shapeError)
|
|
42
|
+
return report(undefined, [fail("PACKET_INVALID", shapeError)], now);
|
|
43
|
+
const packet = input;
|
|
44
|
+
const trustRoleReuse = trustBundle ? duplicateTrustRoleKey(trustBundle) : undefined;
|
|
45
|
+
if (trustRoleReuse)
|
|
46
|
+
steps.push(fail("TRUST_KEY_ROLE_REUSE", trustRoleReuse));
|
|
47
|
+
if (packet.schemaVersion !== VERIFIABLE_EVIDENCE_PACKET_VERSION)
|
|
48
|
+
steps.push(fail("SCHEMA_UNSUPPORTED", `Expected ${VERIFIABLE_EVIDENCE_PACKET_VERSION}.`));
|
|
49
|
+
else
|
|
50
|
+
steps.push(pass("SCHEMA_VALID", "Evidence packet schema version is supported."));
|
|
51
|
+
let contractValid = false;
|
|
52
|
+
try {
|
|
53
|
+
const contract = validateAssuranceContract(packet.contract);
|
|
54
|
+
const expected = compileAssuranceContract(contract);
|
|
55
|
+
contractValid = expected.sourceContractDigest === packet.compiledContract?.sourceContractDigest
|
|
56
|
+
&& sha256Canonical(expected) === sha256Canonical(packet.compiledContract);
|
|
57
|
+
steps.push(contractValid ? pass("CONTRACT_COMPILED_MATCH", "Contract and deterministic compiler output match.") : fail("CONTRACT_DIGEST_MISMATCH", "Compiled contract does not match the source contract."));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
steps.push(fail("CONTRACT_INVALID", error instanceof Error ? error.message : String(error)));
|
|
61
|
+
}
|
|
62
|
+
let privacyValid = false;
|
|
63
|
+
try {
|
|
64
|
+
const manifest = validatePrivacyManifest(packet.privacy?.manifest);
|
|
65
|
+
const transformErrors = validatePrivacyTransformAgainstManifest(manifest, packet.privacy?.transformed);
|
|
66
|
+
privacyValid = transformErrors.length === 0;
|
|
67
|
+
steps.push(privacyValid
|
|
68
|
+
? pass("PRIVACY_MANIFEST_MATCH", "Every disclosed, committed, and omitted path matches the signed deny-by-default Privacy Manifest.")
|
|
69
|
+
: fail("PRIVACY_MANIFEST_MISMATCH", transformErrors.join(" ")));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
steps.push(fail("PRIVACY_MANIFEST_INVALID", error instanceof Error ? error.message : String(error)));
|
|
73
|
+
}
|
|
74
|
+
const actionDigest = sha256Canonical(packet.action?.canonical);
|
|
75
|
+
const bindings = [packet.action?.digest, packet.policy?.actionDigest, packet.approval?.actionDigest, packet.grant?.actionDigest];
|
|
76
|
+
steps.push(bindings.every((value) => value === actionDigest)
|
|
77
|
+
? pass("ACTION_BINDING_VALID", "Policy, approval, and grant bind the exact canonical action digest.")
|
|
78
|
+
: fail("ACTION_BINDING_MISMATCH", "Policy, approval, or grant binds a different action digest."));
|
|
79
|
+
const privacyBindingsValid = privacyValid
|
|
80
|
+
&& packet.privacy.transformed.commitments.resourceId?.digest === packet.action.canonical.resourceCommitment
|
|
81
|
+
&& packet.privacy.transformed.commitments.actionParameters?.digest === packet.action.canonical.parameterCommitment;
|
|
82
|
+
steps.push(privacyBindingsValid
|
|
83
|
+
? pass("PRIVACY_COMMITMENT_BINDING_VALID", "Canonical action commitments match the tenant-keyed privacy transform.")
|
|
84
|
+
: fail("PRIVACY_COMMITMENT_BINDING_MISMATCH", "Canonical action resource or parameters do not match the privacy commitments."));
|
|
85
|
+
const canonicalAction = packet.action?.canonical ?? {};
|
|
86
|
+
const actionScopeValid = canonicalAction.agentRef === `agent:${packet.contract?.subject?.agentId}`
|
|
87
|
+
&& canonicalAction.environment === packet.contract?.subject?.environment
|
|
88
|
+
&& canonicalAction.capability === packet.contract?.capability?.id
|
|
89
|
+
&& canonicalAction.action === packet.contract?.capability?.action
|
|
90
|
+
&& canonicalAction.contractDigest === packet.compiledContract?.sourceContractDigest
|
|
91
|
+
&& canonicalAction.policyRef === packet.contract?.authority?.policyRef
|
|
92
|
+
&& canonicalAction.mandateRef === packet.contract?.authority?.mandateRef;
|
|
93
|
+
steps.push(actionScopeValid ? pass("ACTION_SCOPE_VALID", "Canonical action matches the declared Agent, environment, capability, Contract, Policy, and Mandate.") : fail("ACTION_SCOPE_MISMATCH", "Canonical action is outside the declared Contract scope."));
|
|
94
|
+
const requiredDecision = packet.contract.authority.approval.required ? "REQUIRE_APPROVAL" : "ALLOW";
|
|
95
|
+
const policyValid = packet.policy.decision === requiredDecision && packet.policy.policyRef === packet.contract.authority.policyRef;
|
|
96
|
+
steps.push(policyValid
|
|
97
|
+
? pass("POLICY_DECISION_VALID", `Policy decision ${requiredDecision} matches the Contract authority requirement.`)
|
|
98
|
+
: fail("POLICY_DECISION_MISMATCH", `Expected policy decision ${requiredDecision} for the declared Contract.`));
|
|
99
|
+
const executionEvent = packet.events.find((event) => event.type === "witnora.action.executed");
|
|
100
|
+
const dispatchAt = executionEvent ? Date.parse(executionEvent.occurredAt) : Number.NaN;
|
|
101
|
+
const scopeProofBound = packet.resourceScopeProof.decision === "ALLOW"
|
|
102
|
+
&& packet.resourceScopeProof.contractScopeDigest === sha256Canonical(packet.contract.authority.resourceScope)
|
|
103
|
+
&& packet.resourceScopeProof.resourceCommitment === canonicalAction.resourceCommitment
|
|
104
|
+
&& Date.parse(packet.resourceScopeProof.evaluatedAt) <= dispatchAt;
|
|
105
|
+
steps.push(scopeProofBound
|
|
106
|
+
? pass("RESOURCE_SCOPE_PROOF_BOUND", "A policy evaluator allowed the committed resource under the exact Contract scope.")
|
|
107
|
+
: fail("RESOURCE_SCOPE_PROOF_INVALID", "Resource scope proof is denied, malformed, or bound to a different resource or Contract scope."));
|
|
108
|
+
const approvalContextValid = packet.approval?.approved
|
|
109
|
+
&& packet.approval.tenantRef === canonicalAction.tenantRef
|
|
110
|
+
&& packet.approval.agentRef === canonicalAction.agentRef
|
|
111
|
+
&& packet.approval.environment === canonicalAction.environment
|
|
112
|
+
&& packet.approval.contractDigest === canonicalAction.contractDigest
|
|
113
|
+
&& packet.approval.policyRef === canonicalAction.policyRef
|
|
114
|
+
&& packet.approval.resourceCommitment === canonicalAction.resourceCommitment
|
|
115
|
+
&& Number.isFinite(dispatchAt)
|
|
116
|
+
&& dispatchAt <= Date.parse(packet.approval.expiresAt);
|
|
117
|
+
steps.push(approvalContextValid ? pass("APPROVAL_CONTEXT_VALID", "Approval binds the exact tenant, Agent, environment, Contract, Policy, resource, and expiry.") : fail("APPROVAL_CONTEXT_MISMATCH", "Approval context is incomplete, expired, or differs from the canonical action."));
|
|
118
|
+
const grantContextValid = packet.grant?.tenantRef === canonicalAction.tenantRef
|
|
119
|
+
&& packet.grant?.agentRef === canonicalAction.agentRef
|
|
120
|
+
&& packet.grant?.environment === canonicalAction.environment
|
|
121
|
+
&& packet.grant?.policyRef === canonicalAction.policyRef
|
|
122
|
+
&& packet.grant?.mandateRef === canonicalAction.mandateRef
|
|
123
|
+
&& packet.grant?.resourceCommitment === canonicalAction.resourceCommitment;
|
|
124
|
+
steps.push(grantContextValid ? pass("GRANT_CONTEXT_VALID", "Execution Grant binds the exact authorization context.") : fail("GRANT_CONTEXT_MISMATCH", "Execution Grant context differs from the approved canonical action."));
|
|
125
|
+
const grantIssuedAt = Date.parse(packet.grant.issuedAt);
|
|
126
|
+
const grantExpiresAt = Date.parse(packet.grant.expiresAt);
|
|
127
|
+
const grantRevokedAt = packet.grant.revokedAt ? Date.parse(packet.grant.revokedAt) : undefined;
|
|
128
|
+
const grantWindow = Number.isFinite(dispatchAt)
|
|
129
|
+
&& Number.isFinite(grantIssuedAt)
|
|
130
|
+
&& Number.isFinite(grantExpiresAt)
|
|
131
|
+
&& grantIssuedAt <= dispatchAt
|
|
132
|
+
&& dispatchAt <= grantExpiresAt
|
|
133
|
+
&& (grantRevokedAt === undefined || (Number.isFinite(grantRevokedAt) && dispatchAt < grantRevokedAt));
|
|
134
|
+
const singleUse = packet.grant?.maxUses === 1 && packet.grant?.consumedCount === 1;
|
|
135
|
+
steps.push(grantWindow && singleUse
|
|
136
|
+
? pass("GRANT_SINGLE_USE_VALID", "Grant is unrevoked, unexpired, and consumed exactly once.")
|
|
137
|
+
: fail("GRANT_INVALID", "Grant is expired, revoked, malformed, unconsumed, or reused."));
|
|
138
|
+
if (contractValid && packet.grant?.contractDigest !== packet.compiledContract.sourceContractDigest)
|
|
139
|
+
steps.push(fail("GRANT_CONTRACT_MISMATCH", "Grant binds a different contract digest."));
|
|
140
|
+
else
|
|
141
|
+
steps.push(pass("GRANT_CONTRACT_MATCH", "Grant binds the compiled contract digest."));
|
|
142
|
+
const eventStep = verifyEventChain(packet.events);
|
|
143
|
+
steps.push(eventStep);
|
|
144
|
+
const artifactStep = verifyArtifacts(packet.artifacts);
|
|
145
|
+
steps.push(artifactStep);
|
|
146
|
+
const trustContext = {
|
|
147
|
+
tenantRef: String(canonicalAction.tenantRef), projectRef: String(canonicalAction.projectRef),
|
|
148
|
+
environment: String(canonicalAction.environment), at: dispatchAt,
|
|
149
|
+
};
|
|
150
|
+
const sourceStep = verifySignedValue(sourceSignedValue(packet), packet.sourceSignature, trustBundle, "SOURCE_SIGNATURE", "source", trustContext);
|
|
151
|
+
steps.push(sourceStep);
|
|
152
|
+
const serverStep = verifySignedValue(serverAttestedValue(packet), packet.serverAttestation, trustBundle, "SERVER_ATTESTATION", "server_attestor", trustContext);
|
|
153
|
+
steps.push(serverStep);
|
|
154
|
+
const preconditionProbeStep = verifySignedValue(preconditionProbeSignedValue(packet), packet.precondition?.probeSignature, trustBundle, "PRECONDITION_PROBE_SIGNATURE", "outcome_probe", trustContext);
|
|
155
|
+
steps.push(preconditionProbeStep);
|
|
156
|
+
const probeStep = verifySignedValue(outcomeProbeSignedValue(packet), packet.outcome?.probeSignature, trustBundle, "OUTCOME_PROBE_SIGNATURE", "outcome_probe", trustContext);
|
|
157
|
+
steps.push(probeStep);
|
|
158
|
+
const scopeStep = verifySignedValue(resourceScopeProofSignedValue(packet), packet.resourceScopeProof.signature, trustBundle, "RESOURCE_SCOPE_SIGNATURE", "resource_scope_evaluator", trustContext);
|
|
159
|
+
steps.push(scopeStep);
|
|
160
|
+
const eventualMs = contractValid ? parseContractDurationMs(packet.contract.effect.eventuallyWithin) : undefined;
|
|
161
|
+
const preconditionEvent = packet.events.find((event) => event.type === "witnora.precondition.observed");
|
|
162
|
+
const preconditionEventAt = preconditionEvent ? Date.parse(preconditionEvent.occurredAt) : Number.NaN;
|
|
163
|
+
const preconditionObservedAt = Date.parse(packet.precondition?.observedAt);
|
|
164
|
+
const preconditionFreshness = packet.precondition?.dataFreshnessMs;
|
|
165
|
+
const preconditionTimingValid = eventualMs !== undefined
|
|
166
|
+
&& Number.isFinite(preconditionObservedAt)
|
|
167
|
+
&& preconditionObservedAt === preconditionEventAt
|
|
168
|
+
&& preconditionObservedAt <= dispatchAt
|
|
169
|
+
&& dispatchAt - preconditionObservedAt <= eventualMs
|
|
170
|
+
&& Number.isFinite(preconditionFreshness)
|
|
171
|
+
&& preconditionFreshness >= 0
|
|
172
|
+
&& preconditionFreshness <= eventualMs;
|
|
173
|
+
const preconditionsSatisfied = contractValid
|
|
174
|
+
&& packet.precondition?.status === "SATISFIED"
|
|
175
|
+
&& packet.precondition?.probeRef === packet.contract.observation.probeRef
|
|
176
|
+
&& packet.precondition?.independentReadPath === true
|
|
177
|
+
&& preconditionTimingValid
|
|
178
|
+
&& packet.contract.preconditions.every((predicate) => predicateMatches(packet.precondition.normalizedObservation, predicate));
|
|
179
|
+
const preconditionStep = preconditionsSatisfied
|
|
180
|
+
? pass("PRECONDITIONS_INDEPENDENTLY_VERIFIED", "A separately signed, fresh pre-execution read satisfies every declared Contract precondition.")
|
|
181
|
+
: fail("PRECONDITIONS_NOT_VERIFIED", "The signed pre-execution observation is missing, stale, out of order, or does not satisfy the declared preconditions.");
|
|
182
|
+
steps.push(preconditionStep);
|
|
183
|
+
const outcomeObservedAt = Date.parse(packet.outcome?.observedAt ?? "");
|
|
184
|
+
const outcomeEvent = packet.events.find((event) => event.type === "witnora.outcome.observed");
|
|
185
|
+
const outcomeEventAt = outcomeEvent ? Date.parse(outcomeEvent.occurredAt) : Number.NaN;
|
|
186
|
+
const runCompletedAt = Date.parse(packet.events.at(-1)?.occurredAt ?? "");
|
|
187
|
+
const firstSatisfiedAt = packet.outcome?.firstSatisfiedAt ? Date.parse(packet.outcome.firstSatisfiedAt) : outcomeObservedAt;
|
|
188
|
+
const stableForMs = contractValid && packet.contract.effect.stableFor ? parseContractDurationMs(packet.contract.effect.stableFor) : 0;
|
|
189
|
+
const outcomeTimingValid = eventualMs !== undefined
|
|
190
|
+
&& Number.isFinite(outcomeObservedAt)
|
|
191
|
+
&& outcomeObservedAt === outcomeEventAt
|
|
192
|
+
&& outcomeObservedAt <= runCompletedAt
|
|
193
|
+
&& Number.isFinite(firstSatisfiedAt)
|
|
194
|
+
&& outcomeObservedAt >= dispatchAt
|
|
195
|
+
&& firstSatisfiedAt >= dispatchAt
|
|
196
|
+
&& firstSatisfiedAt - dispatchAt <= eventualMs
|
|
197
|
+
&& outcomeObservedAt - firstSatisfiedAt >= (stableForMs ?? Number.POSITIVE_INFINITY)
|
|
198
|
+
&& Number.isFinite(packet.outcome?.dataFreshnessMs)
|
|
199
|
+
&& Number(packet.outcome.dataFreshnessMs) >= 0
|
|
200
|
+
&& Number(packet.outcome.dataFreshnessMs) <= eventualMs;
|
|
201
|
+
const outcomeTimingStep = outcomeTimingValid
|
|
202
|
+
? pass("OUTCOME_TIMING_VALID", "The signed observation meets chronology, eventual, stability, and data-freshness requirements.")
|
|
203
|
+
: fail("OUTCOME_TIMING_INVALID", "The signed observation is stale, out of order, outside eventuallyWithin, or shorter than stableFor.");
|
|
204
|
+
steps.push(outcomeTimingStep);
|
|
205
|
+
const outcomeEvaluation = contractValid ? evaluateOutcome(packet.contract.effect, packet.outcome?.normalizedObservation, {
|
|
206
|
+
observationFailed: packet.outcome?.status === "OBSERVATION_FAILED",
|
|
207
|
+
timedOut: packet.outcome?.status === "TIMED_OUT",
|
|
208
|
+
stale: packet.outcome?.status === "STALE_OBSERVATION" || !outcomeTimingValid,
|
|
209
|
+
targetAuditReconciled: packet.outcome?.targetAuditReconciled,
|
|
210
|
+
targetAuditConflict: packet.outcome?.targetAuditConflict,
|
|
211
|
+
}) : undefined;
|
|
212
|
+
const outcomeBound = packet.outcome?.expectedDigest === sha256Canonical(packet.contract?.effect)
|
|
213
|
+
&& packet.outcome?.probeRef === packet.contract?.observation?.probeRef
|
|
214
|
+
&& packet.outcome?.status === outcomeEvaluation?.status;
|
|
215
|
+
const outcomeStep = outcomeBound && outcomeEvaluation?.status === "VERIFIED" && packet.outcome.independentReadPath
|
|
216
|
+
? pass("OUTCOME_INDEPENDENTLY_VERIFIED", "A separately signed read path satisfies the declared must and must_not predicates and reconciles target audit state.")
|
|
217
|
+
: outcomeEvaluation && ["INDETERMINATE", "OBSERVATION_FAILED", "TIMED_OUT", "STALE_OBSERVATION"].includes(outcomeEvaluation.status)
|
|
218
|
+
? { code: "OUTCOME_UNVERIFIABLE", status: "UNVERIFIABLE", message: `Independent outcome is ${outcomeEvaluation.status}; success is not inferred.` }
|
|
219
|
+
: fail("OUTCOME_NOT_VERIFIED", "The signed observation, declared predicate, target audit, or claimed outcome status does not agree.");
|
|
220
|
+
steps.push(outcomeStep);
|
|
221
|
+
const claimed = packet.evidenceStrength;
|
|
222
|
+
const recordedPrerequisites = [eventStep, sourceStep];
|
|
223
|
+
const enforcedPrerequisites = [eventStep, sourceStep, serverStep, scopeStep, preconditionProbeStep, preconditionStep];
|
|
224
|
+
const outcomePrerequisites = [eventStep, sourceStep, serverStep, scopeStep, preconditionProbeStep, preconditionStep, probeStep, outcomeTimingStep, outcomeStep];
|
|
225
|
+
const unavailable = [...recordedPrerequisites, ...enforcedPrerequisites, ...outcomePrerequisites].some((step) => step.status === "UNVERIFIABLE");
|
|
226
|
+
const recordedProven = recordedPrerequisites.every((step) => step.status === "PASS");
|
|
227
|
+
const enforcedProven = enforcedPrerequisites.every((step) => step.status === "PASS")
|
|
228
|
+
&& steps.filter((step) => ["PRIVACY_MANIFEST_MATCH", "ACTION_BINDING_VALID", "PRIVACY_COMMITMENT_BINDING_VALID", "ACTION_SCOPE_VALID", "POLICY_DECISION_VALID", "RESOURCE_SCOPE_PROOF_BOUND", "APPROVAL_CONTEXT_VALID", "GRANT_CONTEXT_VALID", "GRANT_SINGLE_USE_VALID", "GRANT_CONTRACT_MATCH", "PRECONDITIONS_INDEPENDENTLY_VERIFIED"].includes(step.code)).every((step) => step.status === "PASS");
|
|
229
|
+
const outcomeProven = enforcedProven && outcomePrerequisites.every((step) => step.status === "PASS");
|
|
230
|
+
const overclaimed = (claimed.recorded && !recordedProven)
|
|
231
|
+
|| (claimed.enforced && (!claimed.recorded || !enforcedProven))
|
|
232
|
+
|| (claimed.outcomeVerified && (!claimed.enforced || !outcomeProven))
|
|
233
|
+
|| claimed.independentlyReviewed;
|
|
234
|
+
steps.push(overclaimed
|
|
235
|
+
? unavailable && !claimed.independentlyReviewed
|
|
236
|
+
? { code: "EVIDENCE_STRENGTH_UNVERIFIABLE", status: "UNVERIFIABLE", message: "Caller trust is insufficient to establish the claimed evidence strength." }
|
|
237
|
+
: fail("EVIDENCE_STRENGTH_OVERCLAIM", "Evidence strength claims a level not established by the packet, or claims independent review without a reviewer attestation.")
|
|
238
|
+
: pass("EVIDENCE_STRENGTH_CONSISTENT", "Evidence strength does not exceed verified facts."));
|
|
239
|
+
return report(packet.packetId, steps, now);
|
|
240
|
+
}
|
|
241
|
+
export function renderOfflineVerification(report) {
|
|
242
|
+
return [
|
|
243
|
+
`Witnora offline verification: ${report.overall}`,
|
|
244
|
+
`Packet: ${report.packetId ?? "unknown"}`,
|
|
245
|
+
"Network used: no",
|
|
246
|
+
...report.steps.map((step) => `${step.status.padEnd(12)} ${step.code}: ${step.message}`),
|
|
247
|
+
].join("\n") + "\n";
|
|
248
|
+
}
|
|
249
|
+
function verifyEventChain(events) {
|
|
250
|
+
if (!Array.isArray(events) || events.length < 2)
|
|
251
|
+
return fail("EVENT_CHAIN_INCOMPLETE", "At least run start and run end events are required.");
|
|
252
|
+
let previousHash = null;
|
|
253
|
+
let previousTime = -Infinity;
|
|
254
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
255
|
+
const event = events[index];
|
|
256
|
+
if (event.sequence !== index + 1)
|
|
257
|
+
return fail("EVENT_SEQUENCE_GAP", `Expected sequence ${index + 1}, received ${event.sequence}.`);
|
|
258
|
+
if (event.previousHash !== previousHash)
|
|
259
|
+
return fail("EVENT_PREVIOUS_HASH_MISMATCH", `Event ${event.sequence} does not reference the previous event.`);
|
|
260
|
+
const occurredAt = Date.parse(event.occurredAt);
|
|
261
|
+
if (!Number.isFinite(occurredAt) || occurredAt < previousTime)
|
|
262
|
+
return fail("EVENT_TIME_INVALID", `Event ${event.sequence} has an invalid or non-monotonic timestamp.`);
|
|
263
|
+
const eventHash = sha256Canonical({ sequence: event.sequence, type: event.type, occurredAt: event.occurredAt, previousHash: event.previousHash, payloadDigest: event.payloadDigest });
|
|
264
|
+
if (eventHash !== event.eventHash)
|
|
265
|
+
return fail("EVENT_HASH_MISMATCH", `Event ${event.sequence} hash is invalid.`);
|
|
266
|
+
previousHash = event.eventHash;
|
|
267
|
+
previousTime = occurredAt;
|
|
268
|
+
}
|
|
269
|
+
if (events[0].type !== "witnora.run.started" || events.at(-1)?.type !== "witnora.run.completed")
|
|
270
|
+
return fail("EVENT_SEMANTIC_ORDER_INVALID", "Event chain must begin with run.started and end with run.completed.");
|
|
271
|
+
const positions = ["witnora.precondition.observed", "witnora.policy.evaluated", "witnora.approval.recorded", "witnora.grant.consumed", "witnora.action.executed", "witnora.outcome.observed"].map((type) => events.findIndex((event) => event.type === type));
|
|
272
|
+
if (positions.some((position) => position < 0) || positions.some((position, index) => index > 0 && position <= positions[index - 1]))
|
|
273
|
+
return fail("EVENT_SEMANTIC_ORDER_INVALID", "Precondition, policy, approval, grant, execution, and outcome events are missing or out of order.");
|
|
274
|
+
return pass("EVENT_CHAIN_VALID", `${events.length} ordered events form a complete hash chain.`);
|
|
275
|
+
}
|
|
276
|
+
function verifyArtifacts(artifacts) {
|
|
277
|
+
if (!Array.isArray(artifacts) || !artifacts.length)
|
|
278
|
+
return { code: "ARTIFACTS_ABSENT", status: "PARTIAL", message: "No artifact bytes are embedded for offline verification." };
|
|
279
|
+
for (const artifact of artifacts) {
|
|
280
|
+
const bytes = Buffer.from(artifact.contentBase64, "base64");
|
|
281
|
+
if (bytes.byteLength !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
|
|
282
|
+
return fail("ARTIFACT_MISMATCH", `Artifact ${artifact.path} does not match its size or raw-byte SHA-256 digest.`);
|
|
283
|
+
}
|
|
284
|
+
return pass("ARTIFACTS_VALID", `${artifacts.length} artifact entries match embedded bytes.`);
|
|
285
|
+
}
|
|
286
|
+
function verifySignedValue(value, signature, trustBundle, prefix, usage, context) {
|
|
287
|
+
if (!signature || signature.algorithm !== "Ed25519")
|
|
288
|
+
return fail(`${prefix}_MISSING`, "Required Ed25519 signature is missing.");
|
|
289
|
+
const digest = sha256Canonical(value);
|
|
290
|
+
if (digest !== signature.valueDigest)
|
|
291
|
+
return fail(`${prefix}_DIGEST_MISMATCH`, "Signed value digest does not match the packet.");
|
|
292
|
+
const trustedKey = trustBundle?.schemaVersion === "witnora.verification_trust_bundle.v0.1" ? trustBundle.keys[signature.keyId] : undefined;
|
|
293
|
+
if (!trustedKey)
|
|
294
|
+
return { code: `${prefix}_UNTRUSTED_KEY`, status: "UNVERIFIABLE", message: `${signature.keyId} is not present in the caller-supplied trust bundle.` };
|
|
295
|
+
if (trustedKey.usage !== usage)
|
|
296
|
+
return fail(`${prefix}_KEY_USAGE_MISMATCH`, `Trusted key usage ${trustedKey.usage} cannot satisfy ${usage}.`);
|
|
297
|
+
if ((trustedKey.tenantRef && trustedKey.tenantRef !== context.tenantRef)
|
|
298
|
+
|| (trustedKey.projectRef && trustedKey.projectRef !== context.projectRef)
|
|
299
|
+
|| (trustedKey.environment && trustedKey.environment !== context.environment))
|
|
300
|
+
return fail(`${prefix}_TRUST_DOMAIN_MISMATCH`, "Trusted key is outside the packet tenant, project, or environment trust domain.");
|
|
301
|
+
const notBefore = trustedKey.notBefore ? Date.parse(trustedKey.notBefore) : -Infinity;
|
|
302
|
+
const notAfter = trustedKey.notAfter ? Date.parse(trustedKey.notAfter) : Infinity;
|
|
303
|
+
const revokedAt = trustedKey.revokedAt ? Date.parse(trustedKey.revokedAt) : Infinity;
|
|
304
|
+
if (!Number.isFinite(context.at) || Number.isNaN(notBefore) || Number.isNaN(notAfter) || Number.isNaN(revokedAt)
|
|
305
|
+
|| context.at < notBefore || context.at >= notAfter || context.at >= revokedAt)
|
|
306
|
+
return fail(`${prefix}_TRUST_KEY_INACTIVE`, "Trusted key is not active at the execution time.");
|
|
307
|
+
if (signature.publicKeyPem && signature.publicKeyPem !== trustedKey.publicKeyPem)
|
|
308
|
+
return fail(`${prefix}_EMBEDDED_KEY_MISMATCH`, "Embedded key differs from the trusted key.");
|
|
309
|
+
try {
|
|
310
|
+
const valid = verify(null, Buffer.from(canonicalJson(value)), createPublicKey(trustedKey.publicKeyPem), Buffer.from(signature.signature, "base64"));
|
|
311
|
+
return valid ? pass(`${prefix}_VALID`, `${signature.keyId} signature is valid.`) : fail(`${prefix}_INVALID`, `${signature.keyId} signature is invalid.`);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
return fail(`${prefix}_INVALID_KEY`, error instanceof Error ? error.message : String(error));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function report(packetId, steps, now) {
|
|
318
|
+
const overall = steps.some((step) => step.status === "FAIL") ? "FAIL"
|
|
319
|
+
: steps.some((step) => step.status === "UNVERIFIABLE") ? "UNVERIFIABLE"
|
|
320
|
+
: steps.some((step) => step.status === "PARTIAL") ? "PARTIAL" : "PASS";
|
|
321
|
+
return { schemaVersion: "witnora.offline_verification_report.v0.1", overall, packetId, verifiedAt: now.toISOString(), networkUsed: false, steps };
|
|
322
|
+
}
|
|
323
|
+
function validatePacketShape(input) {
|
|
324
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
325
|
+
return "Packet must be an object.";
|
|
326
|
+
const packet = input;
|
|
327
|
+
const required = ["schemaVersion", "packetId", "issuedAt", "contract", "compiledContract", "privacy", "action", "precondition", "resourceScopeProof", "policy", "approval", "grant", "events", "outcome", "artifacts", "evidenceStrength", "sourceSignature", "serverAttestation"];
|
|
328
|
+
for (const key of required)
|
|
329
|
+
if (!(key in packet))
|
|
330
|
+
return `Packet is missing required field ${key}.`;
|
|
331
|
+
const objects = ["contract", "compiledContract", "privacy", "action", "precondition", "resourceScopeProof", "policy", "approval", "grant", "outcome", "evidenceStrength", "sourceSignature", "serverAttestation"];
|
|
332
|
+
for (const key of objects)
|
|
333
|
+
if (!isRecord(packet[key]))
|
|
334
|
+
return `Packet field ${key} must be an object.`;
|
|
335
|
+
if (!Array.isArray(packet.events))
|
|
336
|
+
return "Packet field events must be an array.";
|
|
337
|
+
if (!Array.isArray(packet.artifacts))
|
|
338
|
+
return "Packet field artifacts must be an array.";
|
|
339
|
+
const privacy = packet.privacy;
|
|
340
|
+
const action = packet.action;
|
|
341
|
+
const precondition = packet.precondition;
|
|
342
|
+
const outcome = packet.outcome;
|
|
343
|
+
if (!isRecord(privacy.manifest) || !isRecord(privacy.transformed))
|
|
344
|
+
return "Packet privacy manifest and transform must be objects.";
|
|
345
|
+
if (!isRecord(privacy.transformed.commitments))
|
|
346
|
+
return "Packet privacy commitments must be an object.";
|
|
347
|
+
if (!isRecord(action.canonical))
|
|
348
|
+
return "Packet canonical action must be an object.";
|
|
349
|
+
if (!isRecord(precondition.normalizedObservation) || !isRecord(precondition.probeSignature))
|
|
350
|
+
return "Packet precondition observation and probe signature must be objects.";
|
|
351
|
+
if (!isRecord(packet.resourceScopeProof.signature))
|
|
352
|
+
return "Packet resource scope proof signature must be an object.";
|
|
353
|
+
if (!isRecord(outcome.normalizedObservation) || !isRecord(outcome.probeSignature))
|
|
354
|
+
return "Packet outcome observation and probe signature must be objects.";
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
function isRecord(value) {
|
|
358
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
359
|
+
}
|
|
360
|
+
function duplicateTrustRoleKey(bundle) {
|
|
361
|
+
if (bundle.schemaVersion !== "witnora.verification_trust_bundle.v0.1")
|
|
362
|
+
return "Trust bundle schema version is unsupported.";
|
|
363
|
+
const usageByKey = new Map();
|
|
364
|
+
for (const [keyId, entry] of Object.entries(bundle.keys)) {
|
|
365
|
+
if (!entry || typeof entry.publicKeyPem !== "string")
|
|
366
|
+
return `Trust entry ${keyId} is malformed.`;
|
|
367
|
+
const prior = usageByKey.get(entry.publicKeyPem);
|
|
368
|
+
if (prior && prior !== entry.usage)
|
|
369
|
+
return `The same public key is registered for both ${prior} and ${entry.usage}; evidence roles require distinct key material.`;
|
|
370
|
+
usageByKey.set(entry.publicKeyPem, entry.usage);
|
|
371
|
+
}
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
function pass(code, message) { return { code, status: "PASS", message }; }
|
|
375
|
+
function fail(code, message) { return { code, status: "FAIL", message }; }
|
package/dist/onboard.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { DEFAULT_WITNORA_SERVER, saveConnection } from "./credentials.js";
|
|
6
|
+
import { verifyControlPlaneConnection } from "./control-plane.js";
|
|
7
|
+
import { parseAgentTemplate, starterAdapter, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
|
|
8
|
+
import { writeTryEvidence } from "./try.js";
|
|
9
|
+
export async function runOnboard(options) {
|
|
10
|
+
const requestFetch = options.fetch ?? fetch;
|
|
11
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds)));
|
|
12
|
+
const output = options.output ?? ((message) => process.stdout.write(message));
|
|
13
|
+
const server = normalizeServer(options.server ?? DEFAULT_WITNORA_SERVER);
|
|
14
|
+
const repositoryPath = resolve(options.repository ?? process.cwd());
|
|
15
|
+
const repository = await inspectRepository(repositoryPath, options.template);
|
|
16
|
+
const connectionName = options.name ?? repository.slug;
|
|
17
|
+
const codeVerifier = randomBytes(48).toString("base64url");
|
|
18
|
+
const codeChallengeSha256 = createHash("sha256").update(codeVerifier).digest("hex");
|
|
19
|
+
const device = await jsonRequest(requestFetch, `${server}/v1/onboarding/device-authorizations`, {
|
|
20
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ projectId: options.projectId, connectionName, codeChallengeSha256 }),
|
|
22
|
+
});
|
|
23
|
+
output(`Authorize Witnora in your browser:\n ${device.verificationUriComplete}\nCode: ${device.userCode}\n`);
|
|
24
|
+
await (options.openBrowser ?? openSystemBrowser)(device.verificationUriComplete);
|
|
25
|
+
const token = await waitForToken(requestFetch, server, device, codeVerifier, sleep, options.timeoutMs ?? device.expiresIn * 1_000);
|
|
26
|
+
const credentialsPath = await saveConnection(token.connectionName, { server, projectId: token.projectId, apiKey: token.apiKey }, { configHome: options.configHome });
|
|
27
|
+
await verifyControlPlaneConnection({ baseUrl: server, projectId: token.projectId, apiKey: token.apiKey, fetch: requestFetch });
|
|
28
|
+
const generatedFiles = await generateRepositoryConfig(repositoryPath, repository.template, repository.name);
|
|
29
|
+
const selfTest = await writeTryEvidence({
|
|
30
|
+
template: repository.template,
|
|
31
|
+
subject: `witnora-onboarding-${repository.slug}`,
|
|
32
|
+
outDir: join(repositoryPath, ".witnora", "onboarding", "self-test"),
|
|
33
|
+
});
|
|
34
|
+
const bundleBytes = Buffer.from(`${JSON.stringify(selfTest.bundle, null, 2)}\n`);
|
|
35
|
+
const receipt = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/onboarding/self-test`, {
|
|
36
|
+
method: "POST", headers: { authorization: `Bearer ${token.apiKey}`, "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
template: repository.template,
|
|
39
|
+
repository: { kind: repository.kind, name: repository.name, fingerprintSha256: repository.fingerprintSha256 },
|
|
40
|
+
bundleSha256: createHash("sha256").update(bundleBytes).digest("hex"),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
const receiptPath = join(repositoryPath, ".witnora", "onboarding", "receipt.json");
|
|
44
|
+
await mkdir(dirname(receiptPath), { recursive: true });
|
|
45
|
+
await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
46
|
+
output(`\nConnected ${repository.name} to Witnora.\n`);
|
|
47
|
+
output(`Project: ${token.projectId}\nTemplate: ${repository.template} (${repository.kind})\n`);
|
|
48
|
+
output(`Credentials: ${credentialsPath}\nSelf-test receipt: ${receiptPath}\n`);
|
|
49
|
+
output("The synthetic self-test verified the local evidence path. It did not create a run, evidence object, release decision, or CURRENT assurance.\n");
|
|
50
|
+
return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
|
|
51
|
+
repositoryKind: repository.kind, generatedFiles, receiptPath };
|
|
52
|
+
}
|
|
53
|
+
async function waitForToken(requestFetch, server, device, codeVerifier, sleep, timeoutMs) {
|
|
54
|
+
const deadline = Date.now() + timeoutMs;
|
|
55
|
+
while (Date.now() < deadline) {
|
|
56
|
+
const response = await requestFetch(`${server}/v1/onboarding/device-authorizations/token`, {
|
|
57
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ deviceCode: device.deviceCode, codeVerifier }),
|
|
58
|
+
});
|
|
59
|
+
const body = await response.json().catch(() => ({}));
|
|
60
|
+
if (response.ok)
|
|
61
|
+
return body;
|
|
62
|
+
if (response.status !== 428 || body.code !== "authorization_pending")
|
|
63
|
+
throw new Error(String(body.error ?? `Device authorization failed with HTTP ${response.status}.`));
|
|
64
|
+
await sleep(Math.max(1, device.interval) * 1_000);
|
|
65
|
+
}
|
|
66
|
+
throw new Error("Device authorization timed out. Run witnora onboard again to create a new one-time request.");
|
|
67
|
+
}
|
|
68
|
+
async function inspectRepository(repositoryPath, explicitTemplate) {
|
|
69
|
+
const entries = await readdir(repositoryPath, { withFileTypes: true });
|
|
70
|
+
const names = entries.map((entry) => entry.name).sort();
|
|
71
|
+
const packageJson = await optionalJson(join(repositoryPath, "package.json"));
|
|
72
|
+
const packageText = JSON.stringify(packageJson ?? {}).toLowerCase();
|
|
73
|
+
const filesText = names.join(" ").toLowerCase();
|
|
74
|
+
const kind = packageJson ? "node" : names.includes("pyproject.toml") || names.includes("requirements.txt") ? "python" : names.includes(".git") ? "git" : "generic";
|
|
75
|
+
const detected = explicitTemplate ?? (/browser-use|playwright|stagehand|puppeteer/.test(packageText) ? "browser"
|
|
76
|
+
: /modelcontextprotocol|mcp/.test(packageText + filesText) ? "mcp"
|
|
77
|
+
: /sql|pandas|duckdb|prisma/.test(packageText) ? "data"
|
|
78
|
+
: /codex|aider|coding-agent/.test(packageText + filesText) ? "coding" : "workflow");
|
|
79
|
+
const template = parseAgentTemplate(detected);
|
|
80
|
+
const rawName = typeof packageJson?.name === "string" ? packageJson.name : basename(repositoryPath);
|
|
81
|
+
const name = rawName.replace(/^@[^/]+\//, "") || "agent-repository";
|
|
82
|
+
const slug = (name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent-repository").slice(0, 64);
|
|
83
|
+
const fingerprintSha256 = createHash("sha256").update(JSON.stringify({ kind, name, files: names.filter((value) => !value.startsWith(".")).slice(0, 200) })).digest("hex");
|
|
84
|
+
return { kind, name, slug, template, fingerprintSha256 };
|
|
85
|
+
}
|
|
86
|
+
async function generateRepositoryConfig(repositoryPath, template, subject) {
|
|
87
|
+
const files = new Map();
|
|
88
|
+
files.set("witnora.config.json", `${JSON.stringify(starterProfile(template, subject), null, 2)}\n`);
|
|
89
|
+
if (template === "browser")
|
|
90
|
+
files.set("tripwire.yml", starterTripwireConfig(subject));
|
|
91
|
+
if (template === "coding" || template === "workflow" || template === "data")
|
|
92
|
+
files.set("witnora.adapter.mjs", starterAdapter(template, subject));
|
|
93
|
+
const written = [];
|
|
94
|
+
for (const [relativePath, content] of files) {
|
|
95
|
+
const target = join(repositoryPath, relativePath);
|
|
96
|
+
if (await exists(target))
|
|
97
|
+
continue;
|
|
98
|
+
await mkdir(dirname(target), { recursive: true });
|
|
99
|
+
await writeFile(target, content);
|
|
100
|
+
written.push(target);
|
|
101
|
+
}
|
|
102
|
+
return written;
|
|
103
|
+
}
|
|
104
|
+
async function jsonRequest(requestFetch, url, init) {
|
|
105
|
+
const response = await requestFetch(url, init);
|
|
106
|
+
const body = await response.json().catch(() => ({}));
|
|
107
|
+
if (!response.ok)
|
|
108
|
+
throw new Error(String(body.error ?? `${init.method ?? "GET"} ${url} failed with HTTP ${response.status}.`));
|
|
109
|
+
return body;
|
|
110
|
+
}
|
|
111
|
+
async function openSystemBrowser(url) {
|
|
112
|
+
const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
113
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
114
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
115
|
+
child.unref();
|
|
116
|
+
}
|
|
117
|
+
async function optionalJson(path) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function exists(path) { try {
|
|
126
|
+
await access(path);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
} }
|
|
132
|
+
function normalizeServer(value) { return new URL(value).toString().replace(/\/$/, ""); }
|
|
@@ -32,10 +32,15 @@ export function starterAdapter(template, subject) {
|
|
|
32
32
|
const eventType = template === "coding" ? "coding.change.proposed" : template === "workflow" ? "workflow.step.completed" : "data.query.completed";
|
|
33
33
|
return `#!/usr/bin/env node
|
|
34
34
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
35
|
+
import { readFile } from "node:fs/promises";
|
|
35
36
|
|
|
36
37
|
const baseUrl = brandedRequired("BASE_URL").replace(/\\\/$/, "");
|
|
37
38
|
const projectId = brandedRequired("PROJECT_ID");
|
|
38
39
|
const apiKey = brandedRequired("API_KEY");
|
|
40
|
+
const agentId = branded("AGENT_ID") ?? ${JSON.stringify(subject)};
|
|
41
|
+
const agentName = branded("AGENT_NAME") ?? humanize(agentId);
|
|
42
|
+
const agentVersion = await discoverAgentVersion();
|
|
43
|
+
const environment = detectEnvironment();
|
|
39
44
|
const now = new Date().toISOString();
|
|
40
45
|
const traceId = randomBytes(16).toString("hex");
|
|
41
46
|
const spanId = randomBytes(8).toString("hex");
|
|
@@ -44,10 +49,10 @@ const envelope = {
|
|
|
44
49
|
envelopeId: randomUUID(),
|
|
45
50
|
kind: "event",
|
|
46
51
|
occurredAt: now,
|
|
47
|
-
source: { agentId
|
|
48
|
-
run: { externalId: process.env.AGENT_RUN_ID ?? randomUUID(), kind: "custom" },
|
|
52
|
+
source: { agentId, agentName, agentVersion, framework: branded("AGENT_FRAMEWORK") ?? ${JSON.stringify(framework)}, adapter: "witnora-init-v0.3" },
|
|
53
|
+
run: { externalId: process.env.WITNORA_RUN_ID ?? process.env.AGENT_RUN_ID ?? randomUUID(), kind: "custom", environment: environment },
|
|
49
54
|
trace: { traceId, spanId },
|
|
50
|
-
event: { type: ${JSON.stringify(eventType)}, actor: "agent", sequence: 0, attributes: { environment
|
|
55
|
+
event: { type: ${JSON.stringify(eventType)}, actor: "agent", sequence: 0, attributes: { environment } },
|
|
51
56
|
};
|
|
52
57
|
const response = await fetch(\`\${baseUrl}/v1/projects/\${encodeURIComponent(projectId)}/envelopes\`, {
|
|
53
58
|
method: "POST",
|
|
@@ -66,6 +71,44 @@ function brandedRequired(suffix) {
|
|
|
66
71
|
if (!value) throw new Error(\`\${name} is required. Connect the Witnora CLI or set the hosted project variables. Legacy AGENTCERT_* names remain supported.\`);
|
|
67
72
|
return value;
|
|
68
73
|
}
|
|
74
|
+
|
|
75
|
+
function branded(suffix) {
|
|
76
|
+
return process.env[\`WITNORA_\${suffix}\`] ?? process.env[\`AGENTCERT_\${suffix}\`];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function humanize(value) {
|
|
80
|
+
const unscoped = value.includes("/") ? value.slice(value.lastIndexOf("/") + 1) : value;
|
|
81
|
+
return unscoped.replace(/[-_.]+/g, " ").replace(/\\b\\w/g, (character) => character.toUpperCase()).trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function discoverAgentVersion() {
|
|
85
|
+
const explicit = branded("AGENT_VERSION");
|
|
86
|
+
if (explicit) return explicit;
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(await readFile(new URL("./package.json", import.meta.url), "utf8"));
|
|
89
|
+
if (typeof pkg.version === "string" && pkg.version.trim()) return pkg.version.trim();
|
|
90
|
+
} catch {}
|
|
91
|
+
const commit = process.env.GITHUB_SHA ?? process.env.RENDER_GIT_COMMIT ?? process.env.VERCEL_GIT_COMMIT_SHA;
|
|
92
|
+
return commit ? \`git-\${commit.slice(0, 12)}\` : "local";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function detectEnvironment() {
|
|
96
|
+
const explicit = branded("ENVIRONMENT") ?? process.env.VERCEL_ENV;
|
|
97
|
+
if (explicit) return normalizeEnvironment(explicit);
|
|
98
|
+
if (["pull_request", "pull_request_target"].includes(process.env.GITHUB_EVENT_NAME ?? "")) return "staging";
|
|
99
|
+
if (process.env.GITHUB_REF_TYPE === "tag") return "production";
|
|
100
|
+
if (process.env.RENDER_SERVICE_ID) return process.env.IS_PULL_REQUEST === "true" ? "staging" : "production";
|
|
101
|
+
if (process.env.NODE_ENV === "production") return "production";
|
|
102
|
+
return "sandbox";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeEnvironment(value) {
|
|
106
|
+
const normalized = value.trim().toLowerCase();
|
|
107
|
+
if (["sandbox", "development", "dev", "test", "local"].includes(normalized)) return "sandbox";
|
|
108
|
+
if (["staging", "stage", "preview", "pull_request", "pr"].includes(normalized)) return "staging";
|
|
109
|
+
if (["production", "prod", "live"].includes(normalized)) return "production";
|
|
110
|
+
throw new Error(\`Unsupported Witnora environment \${JSON.stringify(value)}. Use sandbox, staging, or production.\`);
|
|
111
|
+
}
|
|
69
112
|
`;
|
|
70
113
|
}
|
|
71
114
|
export function starterInstructions(template, subject) {
|
|
@@ -74,13 +117,13 @@ export function starterInstructions(template, subject) {
|
|
|
74
117
|
Next:
|
|
75
118
|
1. Edit tripwire.yml so startUrl and agent.command/agent.args match your app and browser agent.
|
|
76
119
|
2. Run in CI with Kakarottoooo/agentcert/actions/tripwire@v0, or re-run init with --github-action.
|
|
77
|
-
3. Run: npx
|
|
120
|
+
3. Run: npx witnora@latest run --tripwire .tripwire/latest/tripwire-result.json --subject ${JSON.stringify(subject)} --fail-on-verdict
|
|
78
121
|
`;
|
|
79
122
|
if (template === "mcp")
|
|
80
123
|
return `
|
|
81
124
|
Next:
|
|
82
125
|
1. Run MCPBench and write its JSON result to .mcpbench/latest/results.json.
|
|
83
|
-
2. Run: npx
|
|
126
|
+
2. Run: npx witnora@latest run --mcpbench .mcpbench/latest/results.json --subject ${JSON.stringify(subject)} --fail-on-verdict
|
|
84
127
|
`;
|
|
85
128
|
return `
|
|
86
129
|
Next:
|
|
@@ -151,7 +194,7 @@ jobs:
|
|
|
151
194
|
out: .tripwire/latest
|
|
152
195
|
fail-under: "0.8"
|
|
153
196
|
subject: ${JSON.stringify(subject)}
|
|
154
|
-
agentcert-out: .
|
|
197
|
+
agentcert-out: .witnora/latest
|
|
155
198
|
fail-on-verdict: "true"
|
|
156
199
|
release-gate: "true"
|
|
157
200
|
`;
|