witnora 0.13.4 → 0.13.6

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.
@@ -0,0 +1,295 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { credentialsPath } from "./credentials.js";
6
+ import { findAvailableRuntimeSandboxOrigin, LOCAL_SANDBOX_FIXTURE_CONTRACT_SHA256 } from "./runtime-sandbox-fixture.js";
7
+ export const LOCAL_SANDBOX_ADAPTER_ID = "witnora.local-sandbox.v1";
8
+ export const LOCAL_SANDBOX_ADAPTER_VERSION = "1.0.0";
9
+ export const LOCAL_SANDBOX_PROBE_ID = "witnora.local-sandbox.read-only-probe.v1";
10
+ export async function discoverRuntimeReferences(input) {
11
+ const entries = {
12
+ mandateId: input.env.WITNORA_RUNTIME_MANDATE_ID?.trim(),
13
+ probeConnectionName: input.env.WITNORA_RUNTIME_PROBE_CONNECTION?.trim(),
14
+ probeHostedCredentialHandle: input.env.WITNORA_RUNTIME_PROBE_HOSTED_CREDENTIAL_HANDLE?.trim(),
15
+ runtimeIdentityId: input.env.WITNORA_RUNTIME_IDENTITY_ID?.trim(),
16
+ runtimeKeyId: input.env.WITNORA_RUNTIME_KEY_ID?.trim(),
17
+ runtimeSigningKeyHandle: input.env.WITNORA_RUNTIME_SIGNING_KEY_HANDLE?.trim(),
18
+ };
19
+ const manifestPath = join(dirname(credentialsPath({ configHome: input.configHome })), "runtime-references.json");
20
+ const manifest = await optionalJson(manifestPath);
21
+ const manifestReferences = manifest?.schemaVersion === "witnora.runtime_references.v0.1" && manifest.projectId === input.projectId && manifest.server === input.server
22
+ ? completeReferences(manifest.references)
23
+ : undefined;
24
+ const environmentOverrides = Object.fromEntries(Object.entries(entries).filter(([, value]) => value));
25
+ const configuredReferences = completeReferences({ ...manifestReferences, ...environmentOverrides });
26
+ if (configuredReferences) {
27
+ if (Object.keys(environmentOverrides).length > 0)
28
+ await saveRuntimeReferenceManifest(manifestPath, input.projectId, input.server, configuredReferences);
29
+ return { references: configuredReferences, manifestPath };
30
+ }
31
+ for (const candidate of [
32
+ join(input.repository, "runtime-production-acceptance.config.json"),
33
+ join(input.repository, ".witnora", "runtime-production-acceptance.config.json"),
34
+ join(input.repository, ".witnora", "runtime-acceptance", "config.json"),
35
+ ]) {
36
+ const acceptance = await optionalJson(candidate);
37
+ const migrated = acceptanceReferences(acceptance, input.projectId, input.server);
38
+ if (!migrated)
39
+ continue;
40
+ const migratedWithOverrides = completeReferences({ ...migrated, ...environmentOverrides });
41
+ if (!migratedWithOverrides)
42
+ continue;
43
+ await saveRuntimeReferenceManifest(manifestPath, input.projectId, input.server, migratedWithOverrides);
44
+ return { references: migratedWithOverrides, manifestPath };
45
+ }
46
+ const present = Object.entries(entries).filter(([, value]) => value).map(([key]) => key);
47
+ return { limitation: present.length
48
+ ? `Local sandbox Runtime references are incomplete (${present.join(", ")} present). Existing Gateway setup remains RECORDED_ONLY.`
49
+ : "No existing private Witnora Runtime reference manifest was found. Gateway setup remains RECORDED_ONLY until Hosted bootstrap can issue one-time Runtime references." };
50
+ }
51
+ function completeReferences(value) {
52
+ const mandateId = text(value.mandateId);
53
+ const runtimeIdentityId = text(value.runtimeIdentityId);
54
+ const runtimeKeyId = text(value.runtimeKeyId);
55
+ const runtimeSigningKeyHandle = text(value.runtimeSigningKeyHandle);
56
+ const probeConnectionName = text(value.probeConnectionName);
57
+ const probeHostedCredentialHandle = text(value.probeHostedCredentialHandle);
58
+ if (!mandateId || !runtimeIdentityId || !runtimeKeyId || !runtimeSigningKeyHandle || (!probeConnectionName && !probeHostedCredentialHandle))
59
+ return undefined;
60
+ return {
61
+ mandateId,
62
+ runtimeIdentityId,
63
+ runtimeKeyId,
64
+ runtimeSigningKeyHandle,
65
+ ...(probeConnectionName ? { probeConnectionName } : { probeHostedCredentialHandle }),
66
+ ...optionalBootstrapFields(value),
67
+ };
68
+ }
69
+ function acceptanceReferences(value, projectId, server) {
70
+ if (!value || value.projectId !== projectId || normalizeServer(text(value.hostedBaseUrl)) !== normalizeServer(server))
71
+ return undefined;
72
+ const runtime = object(value.runtimeIdentity);
73
+ const probe = object(value.probe);
74
+ const privateKeyPath = text(runtime.privateKeyPemPath);
75
+ const probeApiKeyPath = text(probe.apiKeyPath);
76
+ if (!privateKeyPath || !probeApiKeyPath)
77
+ return undefined;
78
+ return completeReferences({
79
+ mandateId: value.mandateId,
80
+ runtimeIdentityId: runtime.runtimeIdentityId,
81
+ runtimeKeyId: runtime.keyId,
82
+ runtimeSigningKeyHandle: pathToFileURL(resolve(privateKeyPath)).href,
83
+ probeHostedCredentialHandle: pathToFileURL(resolve(probeApiKeyPath)).href,
84
+ });
85
+ }
86
+ export async function saveRuntimeReferenceManifest(path, projectId, server, references) {
87
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
88
+ const temporary = `${path}.${process.pid}.tmp`;
89
+ await writeFile(temporary, `${JSON.stringify({ schemaVersion: "witnora.runtime_references.v0.1", projectId, server, references }, null, 2)}\n`, { mode: 0o600 });
90
+ await rename(temporary, path);
91
+ await chmod(path, 0o600).catch(() => undefined);
92
+ }
93
+ export async function planRuntimeSandboxModules() {
94
+ const sandboxOrigin = await findAvailableRuntimeSandboxOrigin();
95
+ const generated = generateRuntimeSandboxKit(sandboxOrigin);
96
+ return {
97
+ sandboxOrigin,
98
+ adapterDigestSha256: generated.adapterSha256,
99
+ probeDigestSha256: generated.probeSha256,
100
+ fixtureContractDigestSha256: LOCAL_SANDBOX_FIXTURE_CONTRACT_SHA256,
101
+ };
102
+ }
103
+ function optionalBootstrapFields(value) {
104
+ const bootstrapId = text(value.bootstrapId);
105
+ const probeApiKeyId = text(value.probeApiKeyId);
106
+ const setupPlanId = text(value.setupPlanId);
107
+ const sandboxOrigin = text(value.sandboxOrigin);
108
+ const adapterDigestSha256 = digest(value.adapterDigestSha256);
109
+ const probeDigestSha256 = digest(value.probeDigestSha256);
110
+ const fixtureContractDigestSha256 = digest(value.fixtureContractDigestSha256);
111
+ return {
112
+ ...(bootstrapId ? { bootstrapId } : {}),
113
+ ...(probeApiKeyId ? { probeApiKeyId } : {}),
114
+ ...(setupPlanId ? { setupPlanId } : {}),
115
+ ...(sandboxOrigin ? { sandboxOrigin } : {}),
116
+ ...(adapterDigestSha256 ? { adapterDigestSha256 } : {}),
117
+ ...(probeDigestSha256 ? { probeDigestSha256 } : {}),
118
+ ...(fixtureContractDigestSha256 ? { fixtureContractDigestSha256 } : {}),
119
+ };
120
+ }
121
+ async function optionalJson(path) {
122
+ try {
123
+ return JSON.parse(await readFile(path, "utf8"));
124
+ }
125
+ catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ function text(value) { return typeof value === "string" && value.trim() ? value.trim() : undefined; }
130
+ function digest(value) { const item = text(value); return item && /^[a-f0-9]{64}$/.test(item) ? item : undefined; }
131
+ function object(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; }
132
+ function normalizeServer(value) { try {
133
+ return value ? new URL(value).toString().replace(/\/$/, "") : undefined;
134
+ }
135
+ catch {
136
+ return undefined;
137
+ } }
138
+ export function generateRuntimeSandboxKit(sandboxOrigin) {
139
+ const origin = new URL(sandboxOrigin);
140
+ if (origin.protocol !== "http:" || origin.hostname !== "127.0.0.1" || !origin.port || origin.pathname !== "/" || origin.search || origin.hash)
141
+ throw new Error("Generated Runtime sandbox origin must be a bare 127.0.0.1 HTTP origin.");
142
+ const adapterSource = localSandboxAdapterSource(origin.origin);
143
+ const probeSource = localSandboxProbeSource(origin.origin);
144
+ return {
145
+ adapterSource,
146
+ adapterSha256: createHash("sha256").update(adapterSource).digest("hex"),
147
+ probeSource,
148
+ probeSha256: createHash("sha256").update(probeSource).digest("hex"),
149
+ };
150
+ }
151
+ function localSandboxAdapterSource(sandboxOrigin) {
152
+ return `// Witnora localhost-only controlled sandbox adapter. Generated; do not add production origins.
153
+ import { createHash, createPrivateKey, randomBytes, randomUUID, sign } from "node:crypto";
154
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
155
+ import { dirname, join } from "node:path";
156
+ import { fileURLToPath } from "node:url";
157
+
158
+ export function createWitnoraRuntimeAdapter(context) {
159
+ const runtime = exactObject(context.runtimeIdentity, "runtimeIdentity");
160
+ const signingKey = exactHandle(context.runtimeSigningKey, "runtimeSigningKey");
161
+ const writeCredential = exactHandle(context.targetWriteCredential, "targetWriteCredential");
162
+ const reconcileCredential = exactHandle(context.reconcileAuditCredential, "reconcileAuditCredential");
163
+ if (context.adapterId !== "${LOCAL_SANDBOX_ADAPTER_ID}" || context.adapterVersion !== "${LOCAL_SANDBOX_ADAPTER_VERSION}" || !context.hosted?.request || !context.storageDirectory) throw new Error("runtime_adapter_context_invalid");
164
+ return {
165
+ id: context.adapterId,
166
+ version: context.adapterVersion,
167
+ reconcileReadOnly: true,
168
+ capabilities: Object.freeze({ targetSystem: "WitnoraLocalSandbox", operations: ["UPDATE"], origins: ["${sandboxOrigin}"], resources: ["mock-state/*"] }),
169
+ async prepareClaim({ action, grant }) {
170
+ const grantPayload = exactObject(grant?.grant?.payload, "grant.payload");
171
+ const executionSessionId = randomUUID();
172
+ const payload = {
173
+ protocolVersion: "agentcert.browser_enforcement.v0.2",
174
+ objectType: "RuntimeClaim",
175
+ signatureContext: "onegent.runtime-claim.v0.2",
176
+ runtimeIdentityId: runtime.id,
177
+ executionGrantId: grant.id,
178
+ executionGrantDigest: grant.grant.payloadSha256,
179
+ actionId: action.id,
180
+ ...(grantPayload.principalIdentityAssertionId ? { principalIdentityAssertionId: grantPayload.principalIdentityAssertionId } : {}),
181
+ ...(grantPayload.principalIdentityDigest ? { principalIdentityDigest: grantPayload.principalIdentityDigest } : {}),
182
+ ...(grantPayload.principalIdentityProvider ? { principalIdentityProvider: grantPayload.principalIdentityProvider } : {}),
183
+ executionSessionId,
184
+ claimNonce: randomBytes(24).toString("base64url"),
185
+ claimedAt: new Date().toISOString(),
186
+ runtimeKeyId: runtime.keyId,
187
+ idempotencyKey: "claim:" + grantPayload.jti,
188
+ };
189
+ const payloadSha256 = sha256(canonicalJson(payload));
190
+ const privateKeyPem = await readSecretHandle(signingKey.handle);
191
+ const signature = sign(null, Buffer.from(payloadSha256, "hex"), createPrivateKey(privateKeyPem)).toString("base64url");
192
+ await persistClaim(context.storageDirectory, action.id, { executionSessionId });
193
+ return { executionSessionId, claim: { payload, payloadSha256, signature: { algorithm: "Ed25519", keyId: runtime.keyId, signature } } };
194
+ },
195
+ async execute({ action, proposal, grant, hostedReservation }) {
196
+ const target = localhostTarget(proposal);
197
+ const executionSessionId = hostedReservation.executionSessionId;
198
+ await context.hosted.request("execution-attempts/" + encodeURIComponent(executionSessionId) + "/phase", { method: "POST", body: { executionSessionId, runtimeClaim: hostedReservation.claim, phase: "DISPATCH_STARTED" } });
199
+ const credential = await readSecretHandle(writeCredential.handle);
200
+ const response = await fetch(target.url, { method: "POST", redirect: "error", headers: { "content-type": "application/json", "x-sandbox-write-credential": credential }, body: JSON.stringify({ ...proposal.executionIntent.approvedParameters, actionId: action.id, executionSessionId }) });
201
+ const observedState = await response.json().catch(() => ({}));
202
+ if (!response.ok) throw new Error("sandbox_write_failed_" + response.status);
203
+ await context.hosted.request("execution-attempts/" + encodeURIComponent(executionSessionId) + "/phase", { method: "POST", body: { executionSessionId, runtimeClaim: hostedReservation.claim, phase: "DISPATCH_ACKNOWLEDGED", writeResponseCommitment: sha256(canonicalJson(observedState)), targetOperationId: executionSessionId } });
204
+ await context.hosted.request("execution-grants/" + encodeURIComponent(grant.id) + "/consume", { method: "POST", body: { executionSessionId, runtimeClaim: hostedReservation.claim } });
205
+ const execution = { executionSessionId, observedState, targetOperationId: executionSessionId };
206
+ await persistExecution(context.storageDirectory, action.id, execution);
207
+ return execution;
208
+ },
209
+ async reconcile({ action }) {
210
+ try { return JSON.parse(await readFile(join(context.storageDirectory, "executions", safeId(action.id) + ".json"), "utf8")); }
211
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
212
+ let pending;
213
+ try { pending = JSON.parse(await readFile(join(context.storageDirectory, "claims", safeId(action.id) + ".json"), "utf8")); }
214
+ catch (error) { if (error?.code === "ENOENT") return undefined; throw error; }
215
+ const credential = await readSecretHandle(reconcileCredential.handle);
216
+ const response = await fetch("${sandboxOrigin}/audit/actions/" + encodeURIComponent(action.id) + "/sessions/" + encodeURIComponent(pending.executionSessionId), { redirect: "error", headers: { "x-sandbox-audit-credential": credential } });
217
+ if (response.status === 404) return undefined;
218
+ const audit = await response.json().catch(() => ({}));
219
+ if (!response.ok || audit.phase !== "COMMITTED" || audit.actionId !== action.id || audit.executionSessionId !== pending.executionSessionId) throw new Error("sandbox_reconcile_audit_invalid");
220
+ const execution = { executionSessionId: pending.executionSessionId, observedState: audit.observedState, targetOperationId: pending.executionSessionId, reconciledFromAudit: true };
221
+ await persistExecution(context.storageDirectory, action.id, execution);
222
+ return execution;
223
+ },
224
+ };
225
+ }
226
+
227
+ function localhostTarget(proposal) {
228
+ if (proposal?.targetSystem !== "WitnoraLocalSandbox") throw new Error("sandbox_target_system_invalid");
229
+ const origins = proposal?.executionIntent?.allowedOrigins;
230
+ if (!Array.isArray(origins) || origins.length !== 1) throw new Error("localhost_origin_required");
231
+ const origin = new URL(origins[0]);
232
+ if (origin.protocol !== "http:" || !new Set(["127.0.0.1", "localhost", "[::1]"]).has(origin.hostname) || origin.username || origin.password || origin.search || origin.hash || (origin.pathname && origin.pathname !== "/")) throw new Error("localhost_origin_required");
233
+ if (origin.origin !== "${sandboxOrigin}") throw new Error("sandbox_origin_binding_invalid");
234
+ const resource = String(proposal.executionIntent.allowedResource ?? "");
235
+ if (!/^mock-state\\/[A-Za-z0-9._:-]+$/.test(resource) || proposal.executionIntent.allowedOperation !== "UPDATE") throw new Error("sandbox_resource_invalid");
236
+ const resourceId = resource.slice("mock-state/".length);
237
+ if (proposal.executionIntent.approvedParameters?.resourceId !== resourceId) throw new Error("sandbox_resource_binding_invalid");
238
+ return { url: origin.origin + "/mock-state/" + encodeURIComponent(resourceId) };
239
+ }
240
+ async function persistExecution(directory, actionId, execution) {
241
+ const path = join(directory, "executions", safeId(actionId) + ".json");
242
+ await mkdir(dirname(path), { recursive: true });
243
+ const temporary = path + "." + randomUUID() + ".tmp";
244
+ await writeFile(temporary, JSON.stringify(execution) + "\\n", { mode: 0o600 });
245
+ await rename(temporary, path);
246
+ }
247
+ async function persistClaim(directory, actionId, claim) {
248
+ const path = join(directory, "claims", safeId(actionId) + ".json");
249
+ await mkdir(dirname(path), { recursive: true });
250
+ const temporary = path + "." + randomUUID() + ".tmp";
251
+ await writeFile(temporary, JSON.stringify(claim) + "\\n", { mode: 0o600 });
252
+ await rename(temporary, path);
253
+ }
254
+ function exactObject(value, label) { if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(label + "_invalid"); return value; }
255
+ function exactHandle(value, label) { const item = exactObject(value, label); if (typeof item.handle !== "string" || item.access !== "READ_ONLY" && item.access !== "WRITE_ONLY") throw new Error(label + "_invalid"); return item; }
256
+ function safeId(value) { if (!/^[A-Za-z0-9._:-]+$/.test(value)) throw new Error("action_id_invalid"); return value; }
257
+ async function readSecretHandle(handle) { const url = new URL(handle); if (url.protocol !== "file:") throw new Error("only_file_secret_provider_supported"); const value = (await readFile(fileURLToPath(url), "utf8")).trim(); if (!value) throw new Error("credential_reference_empty"); return value; }
258
+ function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
259
+ function canonicalJson(value) { return JSON.stringify(canonical(value)); }
260
+ function canonical(value) { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number" && Number.isFinite(value)) return Object.is(value, -0) ? 0 : value; if (Array.isArray(value)) return value.map(canonical); if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().filter((key) => value[key] !== undefined).map((key) => [key, canonical(value[key])])); throw new Error("canonical_value_invalid"); }
261
+ `;
262
+ }
263
+ function localSandboxProbeSource(sandboxOrigin) {
264
+ return `// Witnora independent READ_ONLY localhost sandbox probe. Generated and executed in an isolated child process.
265
+ import { readFile } from "node:fs/promises";
266
+ import { fileURLToPath } from "node:url";
267
+
268
+ export function createWitnoraOutcomeProbe(context) {
269
+ if (context.targetReadCredential?.access !== "READ_ONLY") throw new Error("read_only_probe_required");
270
+ return {
271
+ id: context.probeId,
272
+ credentialHandle: context.targetReadCredential.handle,
273
+ readOnly: true,
274
+ async observe({ proposal }) {
275
+ const origins = proposal?.executionIntent?.allowedOrigins;
276
+ if (!Array.isArray(origins) || origins.length !== 1) throw new Error("localhost_origin_required");
277
+ const origin = new URL(origins[0]);
278
+ if (origin.protocol !== "http:" || !new Set(["127.0.0.1", "localhost", "[::1]"]).has(origin.hostname) || origin.username || origin.password || origin.search || origin.hash || (origin.pathname && origin.pathname !== "/")) throw new Error("localhost_origin_required");
279
+ if (origin.origin !== "${sandboxOrigin}") throw new Error("sandbox_origin_binding_invalid");
280
+ const resource = String(proposal.executionIntent.allowedResource ?? "");
281
+ if (!/^mock-state\\/[A-Za-z0-9._:-]+$/.test(resource)) throw new Error("sandbox_resource_invalid");
282
+ const credentialUrl = new URL(context.targetReadCredential.handle);
283
+ if (credentialUrl.protocol !== "file:") throw new Error("only_file_secret_provider_supported");
284
+ const credential = (await readFile(fileURLToPath(credentialUrl), "utf8")).trim();
285
+ if (!credential) throw new Error("credential_reference_empty");
286
+ const resourceId = resource.slice("mock-state/".length);
287
+ const response = await fetch(origin.origin + "/mock-state/" + encodeURIComponent(resourceId), { redirect: "error", headers: { "x-sandbox-read-credential": credential } });
288
+ const observedState = await response.json().catch(() => ({}));
289
+ if (!response.ok) throw new Error("sandbox_probe_failed_" + response.status);
290
+ return { observedState, observationMethod: "TARGET_API", observationSource: context.probeId, confidence: 1 };
291
+ },
292
+ };
293
+ }
294
+ `;
295
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",