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.
Files changed (77) hide show
  1. package/README.md +76 -9
  2. package/dist/assurance-contract.js +312 -0
  3. package/dist/assurance-loop-demo.js +245 -0
  4. package/dist/authorization-state-machine.js +35 -0
  5. package/dist/authorization-v02.js +60 -0
  6. package/dist/bundle.js +22 -0
  7. package/dist/canonical-v02.js +197 -0
  8. package/dist/canonical.js +31 -0
  9. package/dist/cli.js +132 -0
  10. package/dist/command-help.js +81 -0
  11. package/dist/control-plane.js +12 -3
  12. package/dist/credentials.js +1 -1
  13. package/dist/design-partner-pilot.js +402 -0
  14. package/dist/design-partner-v02.js +449 -0
  15. package/dist/evidence-v02.js +118 -0
  16. package/dist/generic-eval.js +160 -0
  17. package/dist/github-design-partner-v02.js +399 -0
  18. package/dist/github-live-v02.js +45 -0
  19. package/dist/guided-setup.js +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/offline-verifier.js +375 -0
  22. package/dist/onboard.js +132 -0
  23. package/dist/onboarding-templates.js +49 -6
  24. package/dist/outcome-evaluator.js +51 -0
  25. package/dist/privacy-manifest.js +273 -0
  26. package/dist/probe-protocol-v02.js +101 -0
  27. package/dist/runtime-context.js +75 -0
  28. package/dist/sandbox.js +1 -1
  29. package/dist/schema-validator.js +141 -3
  30. package/dist/trust-v02.js +111 -0
  31. package/dist/try.js +1 -1
  32. package/dist/vendor/design-partner/failure-exercises.mjs +353 -0
  33. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +8 -0
  34. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -1
  35. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +43 -1
  36. package/dist/vendor/witnora-probe/canonical.d.ts +3 -0
  37. package/dist/vendor/witnora-probe/canonical.js +39 -0
  38. package/dist/vendor/witnora-probe/cli.d.ts +2 -0
  39. package/dist/vendor/witnora-probe/cli.js +36 -0
  40. package/dist/vendor/witnora-probe/config.d.ts +2 -0
  41. package/dist/vendor/witnora-probe/config.js +63 -0
  42. package/dist/vendor/witnora-probe/credential-resolver.d.ts +6 -0
  43. package/dist/vendor/witnora-probe/credential-resolver.js +17 -0
  44. package/dist/vendor/witnora-probe/crypto.d.ts +2 -0
  45. package/dist/vendor/witnora-probe/crypto.js +14 -0
  46. package/dist/vendor/witnora-probe/emulator.d.ts +51 -0
  47. package/dist/vendor/witnora-probe/emulator.js +164 -0
  48. package/dist/vendor/witnora-probe/index.d.ts +11 -0
  49. package/dist/vendor/witnora-probe/index.js +11 -0
  50. package/dist/vendor/witnora-probe/logger.d.ts +3 -0
  51. package/dist/vendor/witnora-probe/logger.js +26 -0
  52. package/dist/vendor/witnora-probe/metrics.d.ts +12 -0
  53. package/dist/vendor/witnora-probe/metrics.js +31 -0
  54. package/dist/vendor/witnora-probe/probe.d.ts +22 -0
  55. package/dist/vendor/witnora-probe/probe.js +208 -0
  56. package/dist/vendor/witnora-probe/request-verifier.d.ts +9 -0
  57. package/dist/vendor/witnora-probe/request-verifier.js +84 -0
  58. package/dist/vendor/witnora-probe/server.d.ts +9 -0
  59. package/dist/vendor/witnora-probe/server.js +85 -0
  60. package/dist/vendor/witnora-probe/storage.d.ts +16 -0
  61. package/dist/vendor/witnora-probe/storage.js +114 -0
  62. package/dist/vendor/witnora-probe/types.d.ts +126 -0
  63. package/dist/vendor/witnora-probe/types.js +2 -0
  64. package/dist/vendor/witnora-verifier-python/README.md +43 -0
  65. package/dist/vendor/witnora-verifier-python/pyproject.toml +23 -0
  66. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/__init__.py +33 -0
  67. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/canonical.py +218 -0
  68. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/cli.py +75 -0
  69. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/conformance.py +124 -0
  70. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/crypto.py +139 -0
  71. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/errors.py +15 -0
  72. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/event_chain.py +80 -0
  73. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/evidence.py +336 -0
  74. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/probe.py +188 -0
  75. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/producer.py +127 -0
  76. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/trust.py +337 -0
  77. 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 }; }
@@ -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: ${JSON.stringify(subject)}, agentVersion: process.env.AGENT_VERSION ?? "unversioned", framework: ${JSON.stringify(framework)}, adapter: "witnora-init-v0.2" },
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: process.env.AGENT_ENVIRONMENT ?? "development" } },
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 --package agentcert@latest witnora run --tripwire .tripwire/latest/tripwire-result.json --subject ${JSON.stringify(subject)} --fail-on-verdict
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 --package agentcert@latest witnora run --mcpbench .mcpbench/latest/results.json --subject ${JSON.stringify(subject)} --fail-on-verdict
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: .agentcert/latest
197
+ agentcert-out: .witnora/latest
155
198
  fail-on-verdict: "true"
156
199
  release-gate: "true"
157
200
  `;