witnora 0.20.2 → 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 +21 -3
- 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
|
@@ -378,13 +378,13 @@ async function runActionTransportTest(options) {
|
|
|
378
378
|
const binding = await loadCustomerMcpActionBinding({ repository: options.repository });
|
|
379
379
|
const checkedAt = new Date().toISOString();
|
|
380
380
|
const passed = options.transport === "http"
|
|
381
|
-
? {
|
|
381
|
+
? await verifyHttpActionConnection(binding, options.fetch).then((endpoint) => ({
|
|
382
382
|
state: "PASSED",
|
|
383
383
|
transport: "http",
|
|
384
384
|
checkedAt,
|
|
385
385
|
actionId: binding.action.id,
|
|
386
|
-
endpoint
|
|
387
|
-
}
|
|
386
|
+
endpoint,
|
|
387
|
+
}))
|
|
388
388
|
: await import("./mcp.js").then(async ({ testWitnoraMcpConnection }) => {
|
|
389
389
|
const result = await testWitnoraMcpConnection({ repository: options.repository, fetch: options.fetch });
|
|
390
390
|
return {
|
|
@@ -420,6 +420,24 @@ async function runActionTransportTest(options) {
|
|
|
420
420
|
options.generatedFiles.push(receiptPath);
|
|
421
421
|
return { ...passed, receiptPath };
|
|
422
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
|
+
}
|
|
423
441
|
async function waitForInstalledGatewayService(options) {
|
|
424
442
|
const pause = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
425
443
|
const deadline = Date.now() + (options.timeoutMs ?? 12_000);
|