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 +2 -3
- package/dist/gateway-service.js +38 -8
- package/dist/gateway.js +50 -8
- package/dist/onboard.js +24 -4
- package/dist/real-path-activation.js +20 -7
- package/package.json +1 -1
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,
|
|
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") });
|
package/dist/gateway-service.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 "
|
|
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; }
|