witnora 0.20.1 → 0.20.3

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/dist/cli.js CHANGED
@@ -41,7 +41,7 @@ import { inspectRepository } from "./onboard.js";
41
41
  import { renderReleaseEvaluation, runReleaseEvaluation } from "./release-evaluation.js";
42
42
  import { runPrivateCapabilityDiscovery } from "./private-discovery.js";
43
43
  import { configureManagedWorkflowHarness, doctorCustomerGateway, initializeCustomerGateway, isGatewayDoctorReady, readManagedCustomerGatewayLogs, renderGatewayDoctor, renderManagedGatewayStatus, restartManagedCustomerGateway, runCustomerGateway, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, superviseManagedCustomerGateway, } from "./gateway.js";
44
- import { installCurrentGatewayService, restartCurrentGatewayService, uninstallCurrentGatewayService } from "./gateway-service.js";
44
+ import { installCurrentGatewayService, uninstallCurrentGatewayService } from "./gateway-service.js";
45
45
  import { verifyEvidencePacketV02 } from "./evidence-v02.js";
46
46
  process.on("uncaughtException", reportFatalError);
47
47
  process.on("unhandledRejection", reportFatalError);
@@ -238,7 +238,6 @@ else if (command === "gateway") {
238
238
  const repository = readFlag("--repo") ?? process.cwd();
239
239
  const dir = readFlag("--dir");
240
240
  const service = await installCurrentGatewayService({ repository, gatewayDirectory: dir, cliEntry: fileURLToPath(import.meta.url) });
241
- await restartCurrentGatewayService({ repository, gatewayDirectory: dir, cliEntry: fileURLToPath(import.meta.url) });
242
241
  const result = await startManagedCustomerGateway({ repository, dir });
243
242
  const doctor = await doctorCustomerGateway({ repository, dir });
244
243
  if (!isGatewayDoctorReady(doctor))
@@ -263,7 +262,7 @@ else if (command === "gateway") {
263
262
  throw new Error("Use witnora gateway service install|uninstall.");
264
263
  }
265
264
  else if (action === "supervise") {
266
- await superviseManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir"), configHome: readFlag("--config-home"), output: (message) => process.stdout.write(message) });
265
+ await superviseManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir"), configHome: readFlag("--config-home"), serviceLeasePath: readFlag("--service-lease"), serviceGeneration: readFlag("--service-generation"), output: (message) => process.stdout.write(message) });
267
266
  }
268
267
  else if (action === "stop") {
269
268
  const result = await stopManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
@@ -1,17 +1,22 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { execFile } from "node:child_process";
3
3
  import { mkdir, rm, writeFile } from "node:fs/promises";
4
4
  import { homedir, userInfo } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  const execFileAsync = promisify(execFile);
8
+ const WINDOWS_PARENT_EXIT_GRACE_MS = 500;
8
9
  export async function installGatewayService(input) {
9
10
  const plan = createGatewayServicePlan(input);
10
11
  // A prior task can still be supervising an older generated Gateway. End it
11
12
  // before replacing the definition so onboarding never leaves two owners
12
13
  // racing for the same localhost port.
13
- if (plan.kind === "WINDOWS_TASK")
14
+ if (plan.kind === "WINDOWS_TASK") {
14
15
  await input.run(plan.stop.command, plan.stop.args).catch(() => undefined);
16
+ await (input.removeFile ?? removeFile)(plan.serviceLease.path);
17
+ await (input.sleep ?? wait)(WINDOWS_PARENT_EXIT_GRACE_MS);
18
+ await input.writeDefinition(plan.serviceLease.path, `${plan.serviceLease.generation}\n`);
19
+ }
15
20
  if (plan.launcher)
16
21
  await input.writeDefinition(plan.launcher.path, plan.launcher.definition);
17
22
  await input.writeDefinition(plan.definitionPath, plan.definition);
@@ -23,31 +28,42 @@ export async function installGatewayService(input) {
23
28
  return { installed: true, plan };
24
29
  }
25
30
  export async function installCurrentGatewayService(options) {
26
- const planInput = currentPlanInput(options);
31
+ const planInput = currentPlanInput(options, randomUUID());
27
32
  return installGatewayService({ ...planInput,
28
33
  writeDefinition: async (path, value) => {
29
34
  await mkdir(dirname(path), { recursive: true });
30
- const contents = planInput.platform === "win32"
35
+ const contents = planInput.platform === "win32" && !path.endsWith(".lease")
31
36
  ? Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(value, "utf16le")])
32
37
  : Buffer.from(value, "utf8");
33
38
  await writeFile(path, contents, { mode: 0o600 });
34
39
  },
35
40
  run: options.run ?? runCommand,
41
+ sleep: options.sleep,
42
+ removeFile: options.removeFile,
36
43
  });
37
44
  }
38
45
  export async function uninstallCurrentGatewayService(options) {
39
46
  const plan = createGatewayServicePlan(currentPlanInput(options));
40
47
  const run = options.run ?? runCommand;
48
+ if (plan.kind === "WINDOWS_TASK") {
49
+ await run(plan.stop.command, plan.stop.args).catch(() => undefined);
50
+ await (options.removeFile ?? removeFile)(plan.serviceLease.path);
51
+ await (options.sleep ?? wait)(WINDOWS_PARENT_EXIT_GRACE_MS);
52
+ }
41
53
  await run(plan.uninstall.command, plan.uninstall.args);
42
54
  if (plan.kind === "SYSTEMD_USER")
43
55
  await run("systemctl", ["--user", "daemon-reload"]);
44
56
  await rm(plan.definitionPath, { force: true });
45
57
  if (plan.launcher)
46
58
  await rm(plan.launcher.path, { force: true });
59
+ if (plan.serviceLease)
60
+ await rm(plan.serviceLease.path, { force: true });
47
61
  return { uninstalled: true, plan };
48
62
  }
49
63
  export async function restartCurrentGatewayService(options) {
50
64
  const plan = createGatewayServicePlan(currentPlanInput(options));
65
+ if (plan.kind === "WINDOWS_TASK")
66
+ return (await installCurrentGatewayService(options)).plan;
51
67
  const run = options.run ?? runCommand;
52
68
  await run(plan.stop.command, plan.stop.args).catch(() => undefined);
53
69
  await run(plan.start.command, plan.start.args);
@@ -56,6 +72,10 @@ export async function restartCurrentGatewayService(options) {
56
72
  export async function stopCurrentGatewayService(options) {
57
73
  const plan = createGatewayServicePlan(currentPlanInput(options));
58
74
  await (options.run ?? runCommand)(plan.stop.command, plan.stop.args).catch(() => undefined);
75
+ if (plan.kind === "WINDOWS_TASK") {
76
+ await (options.removeFile ?? removeFile)(plan.serviceLease.path);
77
+ await (options.sleep ?? wait)(WINDOWS_PARENT_EXIT_GRACE_MS);
78
+ }
59
79
  return plan;
60
80
  }
61
81
  export function createGatewayServicePlan(input) {
@@ -64,8 +84,12 @@ export function createGatewayServicePlan(input) {
64
84
  const superviseArgs = [input.cliEntry, "gateway", "supervise", "--repo", input.repository, "--dir", input.gatewayDirectory];
65
85
  if (input.configHome)
66
86
  superviseArgs.push("--config-home", input.configHome);
67
- if (input.platform === "win32")
68
- return windowsPlan(input, id, superviseArgs);
87
+ if (input.platform === "win32") {
88
+ const leasePath = `${input.serviceHome}\\${id}.lease`;
89
+ const generation = input.serviceGeneration ?? "manual-service-generation";
90
+ superviseArgs.push("--service-lease", leasePath, "--service-generation", generation);
91
+ return { ...windowsPlan(input, id, superviseArgs), serviceLease: { path: leasePath, generation } };
92
+ }
69
93
  if (input.platform === "darwin")
70
94
  return launchdPlan(input, id, superviseArgs);
71
95
  if (input.platform === "linux")
@@ -96,7 +120,7 @@ function windowsPlan(input, id, args) {
96
120
  stop: { command: "schtasks.exe", args: ["/End", "/TN", id] },
97
121
  uninstall: { command: "schtasks.exe", args: ["/Delete", "/TN", id, "/F"] } };
98
122
  }
99
- function currentPlanInput(options) {
123
+ function currentPlanInput(options, serviceGeneration) {
100
124
  const platform = options.platform ?? process.platform;
101
125
  if (platform !== "win32" && platform !== "linux" && platform !== "darwin")
102
126
  throw new Error(`Gateway service installation is not supported on ${platform}.`);
@@ -104,9 +128,15 @@ function currentPlanInput(options) {
104
128
  const env = options.env ?? process.env;
105
129
  const serviceHome = platform === "win32" ? join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "Witnora", "services")
106
130
  : platform === "darwin" ? join(homedir(), "Library", "LaunchAgents") : join(homedir(), ".config", "systemd", "user");
107
- return { platform, repository, gatewayDirectory: resolve(repository, options.gatewayDirectory ?? ".witnora/gateway"), cliEntry: resolve(options.cliEntry), nodeExecutable: options.nodeExecutable ?? process.execPath, serviceHome, userId: platform === "win32" ? `${env.USERDOMAIN ? `${env.USERDOMAIN}\\` : ""}${env.USERNAME ?? userInfo().username}` : userInfo().username, configHome: options.configHome ?? env.WITNORA_CONFIG_HOME };
131
+ return { platform, repository, gatewayDirectory: resolve(repository, options.gatewayDirectory ?? ".witnora/gateway"), cliEntry: resolve(options.cliEntry), nodeExecutable: options.nodeExecutable ?? process.execPath, serviceHome, userId: platform === "win32" ? `${env.USERDOMAIN ? `${env.USERDOMAIN}\\` : ""}${env.USERNAME ?? userInfo().username}` : userInfo().username, configHome: options.configHome ?? env.WITNORA_CONFIG_HOME, serviceGeneration };
108
132
  }
109
133
  async function runCommand(command, args) { await execFileAsync(command, args, { windowsHide: true }); }
134
+ function wait(milliseconds) {
135
+ return new Promise((resolveWait) => setTimeout(resolveWait, milliseconds));
136
+ }
137
+ async function removeFile(path) {
138
+ await rm(path, { force: true });
139
+ }
110
140
  function systemdPlan(input, id, args) {
111
141
  const unit = `${id}.service`;
112
142
  const definitionPath = `${input.serviceHome}/${unit}`;
package/dist/gateway.js CHANGED
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { closeSync, openSync } from "node:fs";
4
4
  import { access, chmod, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
5
5
  import { createServer as createNetServer } from "node:net";
6
- import { basename, dirname, join, relative, resolve } from "node:path";
6
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import { loadConnection } from "./credentials.js";
9
9
  import { authorizeProjectConnection } from "./device-authorization.js";
@@ -1014,9 +1014,7 @@ async function startManagedLocalEvaluator(directory, config, evaluatorKit) {
1014
1014
  if (!repositoryRelative || repositoryRelative.startsWith("..") || resolve(repository, repositoryRelative) !== modulePath) {
1015
1015
  throw new Error("Managed evaluator module escaped the Agent repository.");
1016
1016
  }
1017
- const [realRepository, realModulePath] = await Promise.all([realpath(repository), realpath(modulePath)]);
1018
- const realRelative = relative(realRepository, realModulePath);
1019
- if (!realRelative || realRelative.startsWith("..") || resolve(realRepository, realRelative) !== realModulePath) {
1017
+ if (!await isRealPathContained(repository, modulePath)) {
1020
1018
  throw new Error("Managed evaluator module symlink escaped the Agent repository.");
1021
1019
  }
1022
1020
  const source = await readFile(modulePath);
@@ -1392,20 +1390,47 @@ export async function restartManagedCustomerGateway(options = {}) {
1392
1390
  export async function superviseManagedCustomerGateway(options = {}) {
1393
1391
  const inspect = options.inspect ?? (() => statusManagedCustomerGateway({ repository: options.repository, dir: options.dir }));
1394
1392
  const start = options.start ?? (() => startManagedCustomerGateway({ repository: options.repository, dir: options.dir, configHome: options.configHome }));
1393
+ const stop = options.stop ?? (() => stopManagedCustomerGateway({ repository: options.repository, dir: options.dir, configHome: options.configHome }));
1395
1394
  const sleep = options.sleep ?? wait;
1396
1395
  const maxCycles = options.maxCycles ?? Number.POSITIVE_INFINITY;
1396
+ if (Boolean(options.serviceLeasePath) !== Boolean(options.serviceGeneration)) {
1397
+ throw new Error("A service-owned Gateway supervisor requires both its lease path and generation.");
1398
+ }
1399
+ const readServiceLease = options.readServiceLease ?? ((path) => readFile(path, "utf8"));
1400
+ const leaseRevoked = async () => {
1401
+ if (!options.serviceLeasePath || !options.serviceGeneration)
1402
+ return false;
1403
+ try {
1404
+ return (await readServiceLease(options.serviceLeasePath)).trim() !== options.serviceGeneration;
1405
+ }
1406
+ catch {
1407
+ return true;
1408
+ }
1409
+ };
1410
+ const stopForRevokedLease = async () => {
1411
+ if (!await leaseRevoked())
1412
+ return false;
1413
+ await stop();
1414
+ return true;
1415
+ };
1397
1416
  let recoveries = 0;
1398
1417
  for (let cycle = 0; cycle < maxCycles; cycle += 1) {
1418
+ if (await stopForRevokedLease())
1419
+ return;
1399
1420
  const status = await inspect();
1421
+ if (await stopForRevokedLease())
1422
+ return;
1400
1423
  if (status.state === "CONFLICT")
1401
1424
  throw new Error(status.detail);
1402
1425
  if (status.state === "STOPPED" || status.state === "STALE") {
1403
1426
  await start();
1404
1427
  recoveries += 1;
1405
1428
  options.output?.(`Recovered customer-owned Gateway (${recoveries}).\n`);
1429
+ if (await stopForRevokedLease())
1430
+ return;
1406
1431
  }
1407
1432
  if (cycle + 1 < maxCycles)
1408
- await sleep(options.intervalMs ?? 5_000);
1433
+ await sleep(options.intervalMs ?? (options.serviceLeasePath ? 250 : 5_000));
1409
1434
  }
1410
1435
  }
1411
1436
  export async function readManagedCustomerGatewayLogs(options = {}) {
@@ -1744,9 +1769,7 @@ export async function activateManagedWorkflowHarness(options) {
1744
1769
  if (!moduleRelativeToRepository || moduleRelativeToRepository.startsWith("..") || resolve(repository, moduleRelativeToRepository) !== absoluteModulePath) {
1745
1770
  throw new Error("The customer Assurance Harness module must remain inside the Agent repository.");
1746
1771
  }
1747
- const [realRepository, realModulePath] = await Promise.all([realpath(repository), realpath(absoluteModulePath).catch(() => absoluteModulePath)]);
1748
- const realRelative = relative(realRepository, realModulePath);
1749
- if (realRelative.startsWith("..") || resolve(realRepository, realRelative) !== realModulePath) {
1772
+ if (!await isRealPathContained(repository, absoluteModulePath)) {
1750
1773
  throw new Error("The customer Assurance Harness module symlink must remain inside the Agent repository.");
1751
1774
  }
1752
1775
  const source = await readFile(absoluteModulePath, "utf8").catch((error) => {
@@ -1795,6 +1818,25 @@ export async function activateManagedWorkflowHarness(options) {
1795
1818
  }
1796
1819
  return { state: "READY_TO_START", path, modulePath: evaluatorModulePath, config, created, changed };
1797
1820
  }
1821
+ export async function isRealPathContained(repository, candidate, resolveRealPath = realpath) {
1822
+ const canonicalExistingPath = async (path) => {
1823
+ const resolvedPath = await resolveRealPath(path);
1824
+ const resolvedParent = await resolveRealPath(dirname(resolvedPath));
1825
+ return join(resolvedParent, basename(resolvedPath));
1826
+ };
1827
+ const realRepository = await canonicalExistingPath(repository);
1828
+ let realCandidate;
1829
+ try {
1830
+ realCandidate = await canonicalExistingPath(candidate);
1831
+ }
1832
+ catch (error) {
1833
+ if (error.code !== "ENOENT")
1834
+ throw error;
1835
+ realCandidate = join(await canonicalExistingPath(dirname(candidate)), basename(candidate));
1836
+ }
1837
+ const realRelative = relative(realRepository, realCandidate);
1838
+ return realRelative === "" || (realRelative !== ".." && !realRelative.startsWith(`..${sep}`) && !isAbsolute(realRelative));
1839
+ }
1798
1840
  function isExactGeneratedHarnessUpgrade(current, next, previousGeneratedModuleSha256) {
1799
1841
  if (!previousGeneratedModuleSha256 || current.evaluatorModuleSha256 !== previousGeneratedModuleSha256 || current.evaluatorContractSha256 !== previousGeneratedModuleSha256)
1800
1842
  return false;
package/dist/onboard.js CHANGED
@@ -335,7 +335,9 @@ export async function runOnboard(options) {
335
335
  else if (actionTransportTest) {
336
336
  output(`Action transport: ${actionTransportTest.transport.toUpperCase()} is waiting for one exact sandbox Task/action path. ${actionTransportTest.limitation}\n`);
337
337
  }
338
- output("Connected. Nora is monitoring this Agent. Run it normally whenever it is ready; the first source-signed activity will appear automatically without blocking setup.\n");
338
+ output(actionTransportTest?.state === "WAITING_FOR_ACTION_PATH"
339
+ ? `Base Gateway connected, but ${actionTransportTest.transport.toUpperCase()} Action is not verified yet. Go to Overview, run the Agent once, confirm one exact sandbox Business Task/action path, then rerun this same command.\n`
340
+ : "Connected. Nora is monitoring this Agent. Run it normally whenever it is ready; the first source-signed activity will appear automatically without blocking setup.\n");
339
341
  return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
340
342
  repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
341
343
  gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory,
@@ -376,13 +378,13 @@ async function runActionTransportTest(options) {
376
378
  const binding = await loadCustomerMcpActionBinding({ repository: options.repository });
377
379
  const checkedAt = new Date().toISOString();
378
380
  const passed = options.transport === "http"
379
- ? {
381
+ ? await verifyHttpActionConnection(binding, options.fetch).then((endpoint) => ({
380
382
  state: "PASSED",
381
383
  transport: "http",
382
384
  checkedAt,
383
385
  actionId: binding.action.id,
384
- endpoint: `${binding.baseUrl}${binding.action.path}`,
385
- }
386
+ endpoint,
387
+ }))
386
388
  : await import("./mcp.js").then(async ({ testWitnoraMcpConnection }) => {
387
389
  const result = await testWitnoraMcpConnection({ repository: options.repository, fetch: options.fetch });
388
390
  return {
@@ -418,6 +420,24 @@ async function runActionTransportTest(options) {
418
420
  options.generatedFiles.push(receiptPath);
419
421
  return { ...passed, receiptPath };
420
422
  }
423
+ async function verifyHttpActionConnection(binding, requestFetch) {
424
+ const endpoint = `${binding.baseUrl}${binding.action.path}`;
425
+ const response = await requestFetch(endpoint, {
426
+ method: "POST",
427
+ headers: {
428
+ authorization: `Bearer ${binding.actionToken}`,
429
+ "content-type": "application/json",
430
+ "idempotency-key": `witnora-transport-test-${binding.action.id}`,
431
+ },
432
+ body: "{}",
433
+ signal: AbortSignal.timeout(5_000),
434
+ });
435
+ const body = await response.json().catch(() => ({}));
436
+ if (response.status !== 400 || body.error !== "resourceId is required and must use URL-safe identifier characters.") {
437
+ throw new Error(`HTTP Action connection test failed closed (${response.status}). The exact customer-owned endpoint, Action binding, or action-scoped token is unavailable.`);
438
+ }
439
+ return endpoint;
440
+ }
421
441
  async function waitForInstalledGatewayService(options) {
422
442
  const pause = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
423
443
  const deadline = Date.now() + (options.timeoutMs ?? 12_000);
@@ -84,9 +84,7 @@ export async function activateRealPathIntegrations(options) {
84
84
  }
85
85
  async function providerPreflight(request, env, plan, now, postgresClientFactory) {
86
86
  if (plan.generated.providerPackId === "STRIPE_REFUND") {
87
- const secret = env.STRIPE_SECRET_KEY;
88
- if (!secret?.startsWith("sk_test_") || secret.length < 12)
89
- throw new Error("Stripe test-mode activation requires STRIPE_SECRET_KEY to reference an sk_test_ credential in the customer environment.");
87
+ const secret = stripeTestCredential(env);
90
88
  return stripePreflight(request, secret, plan, now);
91
89
  }
92
90
  if (plan.generated.providerPackId === "SHOPIFY_DISPUTE")
@@ -252,7 +250,7 @@ import {join} from "node:path";
252
250
  const contracts=${JSON.stringify(contracts)};
253
251
  const sha=(value)=>createHash("sha256").update(typeof value==="string"?value:JSON.stringify(value)).digest("hex");
254
252
  const request=async(url,key)=>{const response=await fetch(url,{method:"GET",headers:{accept:"application/json",authorization:"Bearer "+key},redirect:"error",signal:AbortSignal.timeout(5000)});if(!response.ok)throw new Error("Provider read-only observation failed ("+response.status+").");return response.json();};
255
- const observe=async(contract,resourceId,repository)=>{if(contract.sandboxObservation)return structuredClone(contract.sandboxObservation);const key=(await readFile(join(repository,contract.credentialHandle),"utf8")).trim();if(!key)throw new Error("The local read-only Provider credential is unavailable.");if(contract.providerPackId==="STRIPE_REFUND"){if(!key.startsWith("sk_test_"))throw new Error("Stripe test-mode credential is unavailable.");return request("https://api.stripe.com/v1/refunds/"+encodeURIComponent(resourceId),key);}if(contract.providerPackId==="SHOPIFY_DISPUTE"){const response=await fetch(contract.providerConfiguration.origin+"/admin/api/2025-07/shopify_payments/disputes/"+encodeURIComponent(resourceId)+".json",{method:"GET",headers:{accept:"application/json","x-shopify-access-token":key},redirect:"error",signal:AbortSignal.timeout(5000)});if(!response.ok)throw new Error("Provider read-only observation failed ("+response.status+").");return (await response.json()).dispute;}if(contract.providerPackId==="ZENDESK_TICKET")return (await request(contract.providerConfiguration.origin+"/api/v2/tickets/"+encodeURIComponent(resourceId)+".json",key)).ticket;if(contract.providerPackId==="SALESFORCE_RECORD")return request(contract.providerConfiguration.origin+"/services/data/v61.0/sobjects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId),key);if(contract.providerPackId==="HUBSPOT_CRM_RECORD")return (await request("https://api.hubapi.com/crm/v3/objects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId)+"?properties="+encodeURIComponent(contract.criterion.field),key)).properties;if(contract.providerPackId==="POSTGRES_RECORD"){for(const name of [contract.providerConfiguration.viewName,contract.providerConfiguration.idColumn,contract.criterion.field])if(!/^[A-Za-z][A-Za-z0-9_]{0,99}$/.test(name))throw new Error("PostgreSQL identifier is invalid.");const {Client}=await import("pg");const client=new Client({connectionString:key,application_name:"witnora-read-only-probe"});await client.connect();try{await client.query("BEGIN READ ONLY");const result=await client.query({name:"witnora-provider-observe",text:'SELECT "'+contract.criterion.field+'" FROM "'+contract.providerConfiguration.viewName+'" WHERE "'+contract.providerConfiguration.idColumn+'" = $1 LIMIT 1',values:[resourceId]});await client.query("ROLLBACK");return result.rows[0]??{};}catch(error){await client.query("ROLLBACK").catch(()=>{});throw error;}finally{await client.end();}}throw new Error("Provider Harness contract is unsupported.");};
253
+ const observe=async(contract,resourceId,repository)=>{if(contract.sandboxObservation)return structuredClone(contract.sandboxObservation);const key=(await readFile(join(repository,contract.credentialHandle),"utf8")).trim();if(!key)throw new Error("The local read-only Provider credential is unavailable.");if(contract.providerPackId==="STRIPE_REFUND"){if(!key.startsWith("rk_test_")&&!key.startsWith("sk_test_"))throw new Error("Stripe test-mode credential is unavailable.");return request("https://api.stripe.com/v1/refunds/"+encodeURIComponent(resourceId),key);}if(contract.providerPackId==="SHOPIFY_DISPUTE"){const response=await fetch(contract.providerConfiguration.origin+"/admin/api/2025-07/shopify_payments/disputes/"+encodeURIComponent(resourceId)+".json",{method:"GET",headers:{accept:"application/json","x-shopify-access-token":key},redirect:"error",signal:AbortSignal.timeout(5000)});if(!response.ok)throw new Error("Provider read-only observation failed ("+response.status+").");return (await response.json()).dispute;}if(contract.providerPackId==="ZENDESK_TICKET")return (await request(contract.providerConfiguration.origin+"/api/v2/tickets/"+encodeURIComponent(resourceId)+".json",key)).ticket;if(contract.providerPackId==="SALESFORCE_RECORD")return request(contract.providerConfiguration.origin+"/services/data/v61.0/sobjects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId),key);if(contract.providerPackId==="HUBSPOT_CRM_RECORD")return (await request("https://api.hubapi.com/crm/v3/objects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId)+"?properties="+encodeURIComponent(contract.criterion.field),key)).properties;if(contract.providerPackId==="POSTGRES_RECORD"){for(const name of [contract.providerConfiguration.viewName,contract.providerConfiguration.idColumn,contract.criterion.field])if(!/^[A-Za-z][A-Za-z0-9_]{0,99}$/.test(name))throw new Error("PostgreSQL identifier is invalid.");const {Client}=await import("pg");const client=new Client({connectionString:key,application_name:"witnora-read-only-probe"});await client.connect();try{await client.query("BEGIN READ ONLY");const result=await client.query({name:"witnora-provider-observe",text:'SELECT "'+contract.criterion.field+'" FROM "'+contract.providerConfiguration.viewName+'" WHERE "'+contract.providerConfiguration.idColumn+'" = $1 LIMIT 1',values:[resourceId]});await client.query("ROLLBACK");return result.rows[0]??{};}catch(error){await client.query("ROLLBACK").catch(()=>{});throw error;}finally{await client.end();}}throw new Error("Provider Harness contract is unsupported.");};
256
254
  export function createWitnoraBusinessTaskEvaluatorOptions(context){return {
257
255
  loadShadowObservations:async(task)=>{const contract=contracts.find((item)=>item.taskContractId===task.id);if(!contract)throw new Error("No exact real-path contract.");const path=join(context.repository,".witnora","scenarios",task.id+".json");const value=JSON.parse(await readFile(path,"utf8"));if(!Array.isArray(value)||value.length>100)throw new Error("Local scenario file is invalid.");return value.map((item)=>({...item,source:"LIVE_SHADOW"}));},
258
256
  evaluateShadowCandidate:async(candidate,task)=>{const contract=contracts.find((item)=>item.taskContractId===task.id);if(!contract)throw new Error("No exact real-path contract.");const resourceId=String(candidate.input?.resourceId??"");if(!/^[A-Za-z0-9._:-]{1,200}$/.test(resourceId))throw new Error("Provider resourceId is invalid.");for(const pathId of contract.actionPathIds)candidate.propose({pathId,parametersDigestSha256:sha(resourceId)});const raw=await observe(contract,resourceId,context.repository);const actual=raw?.[contract.criterion.field];const observed=actual===undefined?{}:{[contract.criterion.field]:actual};return {resultDigestSha256:sha(observed),criteria:[{id:contract.criterion.id,passed:actual===contract.criterion.expected}]};}
@@ -265,11 +263,14 @@ async function persistProviderCredential(repository, plan, environment) {
265
263
  return;
266
264
  if (plan.generated.providerPackId === "QUEUE_JOB" && plan.environment === "sandbox")
267
265
  return;
268
- const name = providerCredentialEnvironmentName(plan.generated.providerPackId);
269
- const value = environment[name];
270
266
  const directory = join(repository, ".witnora", "provider-credentials");
271
267
  const target = join(directory, `${plan.id}.secret`);
272
268
  const current = await readFile(target, "utf8").catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
269
+ const name = providerCredentialEnvironmentName(plan.generated.providerPackId);
270
+ const stripeCredentialSupplied = Boolean(environment.STRIPE_RESTRICTED_TEST_KEY?.trim() || environment.STRIPE_SECRET_KEY?.trim());
271
+ const value = plan.generated.providerPackId === "STRIPE_REFUND"
272
+ ? stripeCredentialSupplied ? stripeTestCredential(environment) : undefined
273
+ : environment[name];
273
274
  if (!value) {
274
275
  if (current !== undefined)
275
276
  return;
@@ -294,7 +295,7 @@ function sameActivationPlans(plans, activations) {
294
295
  }
295
296
  function providerCredentialEnvironmentName(packId) {
296
297
  if (packId === "STRIPE_REFUND")
297
- return "STRIPE_SECRET_KEY";
298
+ return "STRIPE_RESTRICTED_TEST_KEY";
298
299
  if (packId === "SHOPIFY_DISPUTE")
299
300
  return "SHOPIFY_READ_ACCESS_TOKEN";
300
301
  if (packId === "ZENDESK_TICKET")
@@ -307,6 +308,18 @@ function providerCredentialEnvironmentName(packId) {
307
308
  return "WITNORA_POSTGRES_READ_URL";
308
309
  throw new Error(`Provider credential mapping is not supported for ${packId}.`);
309
310
  }
311
+ function stripeTestCredential(environment) {
312
+ const restricted = environment.STRIPE_RESTRICTED_TEST_KEY?.trim();
313
+ if (restricted) {
314
+ if (!restricted.startsWith("rk_test_") || restricted.length < 12)
315
+ throw new Error("Stripe sandbox activation requires STRIPE_RESTRICTED_TEST_KEY to contain an rk_test_ restricted test key.");
316
+ return restricted;
317
+ }
318
+ const legacy = environment.STRIPE_SECRET_KEY?.trim();
319
+ if (!legacy?.startsWith("sk_test_") || legacy.length < 12)
320
+ throw new Error("Stripe sandbox activation requires a read-only rk_test_ key in STRIPE_RESTRICTED_TEST_KEY. STRIPE_SECRET_KEY remains a legacy sk_test_ fallback only.");
321
+ return legacy;
322
+ }
310
323
  function parsePlan(value) { if (!value || typeof value !== "object" || Array.isArray(value))
311
324
  throw new Error("Real-path integration response is invalid."); const plan = value; if (plan.schemaVersion !== "witnora.real_path_integration.v0.1" || !plan.id || !plan.taskContractId || !plan.subject?.agentId || !plan.subject.agentVersion || !plan.generated || plan.generated.boundaries?.rawPayloadUpload !== false || plan.generated.boundaries.rawCredentialUpload !== false || plan.generated.boundaries.evaluatorWrites !== false || plan.generated.boundaries.firstAcceptanceProductionWrites !== 0 || !digest(plan.digestSha256) || !digest(plan.taskContractDigestSha256) || !digest(plan.generated.providerContractDigestSha256))
312
325
  throw new Error("Real-path integration response failed its safety contract."); return plan; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.20.1",
3
+ "version": "0.20.3",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",