witnora 0.18.2 → 0.18.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/gateway-service.js +8 -2
- package/dist/gateway.js +66 -0
- package/dist/onboard.js +4 -1
- package/dist/private-discovery.js +17 -8
- package/package.json +1 -1
package/dist/gateway-service.js
CHANGED
|
@@ -18,7 +18,13 @@ export async function installGatewayService(input) {
|
|
|
18
18
|
export async function installCurrentGatewayService(options) {
|
|
19
19
|
const planInput = currentPlanInput(options);
|
|
20
20
|
return installGatewayService({ ...planInput,
|
|
21
|
-
writeDefinition: async (path, value) => {
|
|
21
|
+
writeDefinition: async (path, value) => {
|
|
22
|
+
await mkdir(dirname(path), { recursive: true });
|
|
23
|
+
const contents = planInput.platform === "win32"
|
|
24
|
+
? Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(value, "utf16le")])
|
|
25
|
+
: Buffer.from(value, "utf8");
|
|
26
|
+
await writeFile(path, contents, { mode: 0o600 });
|
|
27
|
+
},
|
|
22
28
|
run: options.run ?? runCommand,
|
|
23
29
|
});
|
|
24
30
|
}
|
|
@@ -53,7 +59,7 @@ export function createGatewayServicePlan(input) {
|
|
|
53
59
|
function windowsPlan(input, id, args) {
|
|
54
60
|
const definitionPath = `${input.serviceHome}\\${id}.xml`;
|
|
55
61
|
const argumentsValue = args.map(windowsArgument).join(" ");
|
|
56
|
-
const definition = `<?xml version="1.0" encoding="UTF-
|
|
62
|
+
const definition = `<?xml version="1.0" encoding="UTF-16"?>
|
|
57
63
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
58
64
|
<Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml(input.userId)}</UserId></LogonTrigger></Triggers>
|
|
59
65
|
<Principals><Principal id="Author"><UserId>${xml(input.userId)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
package/dist/gateway.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, createPrivateKey, createPublicKey, randomBytes } from "node
|
|
|
2
2
|
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
|
+
import { createServer as createNetServer } from "node:net";
|
|
5
6
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
import { loadConnection } from "./credentials.js";
|
|
@@ -196,6 +197,44 @@ export async function upgradeCustomerGatewayRuntime(options) {
|
|
|
196
197
|
}
|
|
197
198
|
return { config: next, generatedFiles: [configPath, clientPath, readmePath, ...created, ...(replacingGeneratedRuntime ? [join(directory, runtimeKit.config.adapterModulePath), join(directory, runtimeKit.config.probeModulePath)] : [])], changed: true, rollback: restore };
|
|
198
199
|
}
|
|
200
|
+
export async function ensureCustomerGatewayPortAvailable(options) {
|
|
201
|
+
const directory = resolve(options.repository, options.dir ?? ".witnora/gateway");
|
|
202
|
+
const configPath = join(directory, "gateway.json");
|
|
203
|
+
const clientPath = join(directory, "client.mjs");
|
|
204
|
+
const readmePath = join(directory, "README.md");
|
|
205
|
+
const [configRaw, clientRaw, readmeRaw] = await Promise.all([
|
|
206
|
+
readFile(configPath, "utf8"), readFile(clientPath, "utf8"), readFile(readmePath, "utf8"),
|
|
207
|
+
]);
|
|
208
|
+
const current = parseConfig(configRaw);
|
|
209
|
+
const health = await localCollector(current.host, current.port, options.fetch ?? fetch);
|
|
210
|
+
if (health === current.collectorId || (!health && await canListen(current.host, current.port))) {
|
|
211
|
+
return { changed: false, port: current.port };
|
|
212
|
+
}
|
|
213
|
+
if (clientRaw !== gatewayClient(current) || readmeRaw !== gatewayReadme(current)) {
|
|
214
|
+
throw new Error(`Gateway port ${current.port} is occupied, and generated Gateway files were modified; refusing to rewrite customer files.`);
|
|
215
|
+
}
|
|
216
|
+
const excluded = new Set([current.port]);
|
|
217
|
+
if (current.runtimeWorker?.sandboxOrigin)
|
|
218
|
+
excluded.add(Number(new URL(current.runtimeWorker.sandboxOrigin).port));
|
|
219
|
+
const port = await nextAvailablePort(current.host, current.port, excluded);
|
|
220
|
+
const next = { ...current, port };
|
|
221
|
+
if (next.runtimeWorker)
|
|
222
|
+
next.runtimeWorker = { ...next.runtimeWorker, configDigestSha256: undefined };
|
|
223
|
+
if (next.runtimeWorker)
|
|
224
|
+
next.runtimeWorker.configDigestSha256 = gatewayConfigDigest(next);
|
|
225
|
+
try {
|
|
226
|
+
await atomicWrite(configPath, `${JSON.stringify(next, null, 2)}\n`, 0o644);
|
|
227
|
+
await atomicWrite(clientPath, gatewayClient(next), 0o644);
|
|
228
|
+
await atomicWrite(readmePath, gatewayReadme(next), 0o644);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
await Promise.all([
|
|
232
|
+
atomicWrite(configPath, configRaw, 0o644), atomicWrite(clientPath, clientRaw, 0o644), atomicWrite(readmePath, readmeRaw, 0o644),
|
|
233
|
+
]).catch(() => undefined);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
return { changed: true, port };
|
|
237
|
+
}
|
|
199
238
|
function sameRuntimeGeneration(left, right) {
|
|
200
239
|
return JSON.stringify({ ...left, configDigestSha256: undefined }) === JSON.stringify({ ...right, configDigestSha256: undefined });
|
|
201
240
|
}
|
|
@@ -1412,6 +1451,33 @@ async function atomicWrite(path, content, mode) {
|
|
|
1412
1451
|
await rename(temporary, path);
|
|
1413
1452
|
await chmod(path, mode).catch(() => undefined);
|
|
1414
1453
|
}
|
|
1454
|
+
async function localCollector(host, port, requestFetch) {
|
|
1455
|
+
try {
|
|
1456
|
+
const response = await requestFetch(`http://${host}:${port}/healthz`, { signal: AbortSignal.timeout(750) });
|
|
1457
|
+
if (!response.ok)
|
|
1458
|
+
return "occupied";
|
|
1459
|
+
const value = await response.json();
|
|
1460
|
+
return typeof value.collectorId === "string" ? value.collectorId : "occupied";
|
|
1461
|
+
}
|
|
1462
|
+
catch {
|
|
1463
|
+
return undefined;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async function canListen(host, port) {
|
|
1467
|
+
const server = createNetServer();
|
|
1468
|
+
return new Promise((resolvePromise) => {
|
|
1469
|
+
server.once("error", () => resolvePromise(false));
|
|
1470
|
+
server.listen(port, host, () => server.close(() => resolvePromise(true)));
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
async function nextAvailablePort(host, preferred, excluded) {
|
|
1474
|
+
for (let offset = 1; offset <= 1_000; offset += 1) {
|
|
1475
|
+
const candidate = 1_024 + ((preferred - 1_024 + offset) % (65_535 - 1_024));
|
|
1476
|
+
if (!excluded.has(candidate) && await canListen(host, candidate))
|
|
1477
|
+
return candidate;
|
|
1478
|
+
}
|
|
1479
|
+
throw new Error("No available localhost port was found for the customer-owned Gateway.");
|
|
1480
|
+
}
|
|
1415
1481
|
async function exists(path) {
|
|
1416
1482
|
try {
|
|
1417
1483
|
await access(path);
|
package/dist/onboard.js
CHANGED
|
@@ -8,7 +8,7 @@ import { verifyControlPlaneConnection } from "./control-plane.js";
|
|
|
8
8
|
import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
|
|
9
9
|
import { parseAgentTemplate, starterAdapter, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
|
|
10
10
|
import { writeTryEvidence } from "./try.js";
|
|
11
|
-
import { doctorCustomerGateway, activateManagedWorkflowHarness, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, startManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
|
|
11
|
+
import { doctorCustomerGateway, activateManagedWorkflowHarness, ensureCustomerGatewayPortAvailable, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, startManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
|
|
12
12
|
import { discoverRuntimeReferences, planRuntimeSandboxModules } from "./runtime-sandbox-kit.js";
|
|
13
13
|
import { automaticRuntimeReferencesUsable, bootstrapLocalRuntime, verifyAutomaticRuntimeProbe } from "./runtime-bootstrap.js";
|
|
14
14
|
import { activateRealPathIntegrations } from "./real-path-activation.js";
|
|
@@ -217,6 +217,9 @@ export async function runOnboard(options) {
|
|
|
217
217
|
throw new Error("The prior managed Gateway process did not stop before Assurance Harness activation.");
|
|
218
218
|
}
|
|
219
219
|
if (!options.gatewayLifecycle) {
|
|
220
|
+
const port = await ensureCustomerGatewayPortAvailable({ repository: repositoryPath });
|
|
221
|
+
if (port.changed)
|
|
222
|
+
output(`Rebound this repository's generated Gateway to available localhost port ${port.port}; another project remains untouched.\n`);
|
|
220
223
|
const installed = await installCurrentGatewayService({ repository: repositoryPath, cliEntry: fileURLToPath(new URL("./cli.js", import.meta.url)) });
|
|
221
224
|
continuousService = { state: "INSTALLED", kind: installed.plan.kind, id: installed.plan.id };
|
|
222
225
|
}
|
|
@@ -31,7 +31,7 @@ export async function runPrivateCapabilityDiscovery(input) {
|
|
|
31
31
|
localRedactionApplied: true,
|
|
32
32
|
},
|
|
33
33
|
capabilities: input.repository.capabilities,
|
|
34
|
-
unknownCapabilityCount: input.repository.capabilities.length,
|
|
34
|
+
unknownCapabilityCount: input.repository.capabilities.filter((item) => item.disposition === "pending").length,
|
|
35
35
|
};
|
|
36
36
|
const payloadSha256 = sha256(canonicalJson(payload));
|
|
37
37
|
const privateKey = createPrivateKey(identity.privateKeyPem);
|
|
@@ -62,12 +62,13 @@ export async function runPrivateCapabilityDiscovery(input) {
|
|
|
62
62
|
export function inferPrivateCapabilities(input) {
|
|
63
63
|
const searchable = [...input.dependencyNames, ...input.topLevelNames].join(" ").toLowerCase();
|
|
64
64
|
const detected = new Map();
|
|
65
|
-
const add = (key, observedName, transport) => detected.set(key, {
|
|
65
|
+
const add = (key, observedName, transport, capabilityId, disposition = "pending") => detected.set(key, {
|
|
66
66
|
key,
|
|
67
67
|
observedName,
|
|
68
68
|
transport,
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
...(capabilityId ? { capabilityId } : {}),
|
|
70
|
+
disposition,
|
|
71
|
+
schemaSha256: sha256(canonicalJson({ key, observedName, transport, capabilityId, disposition, source: "repository_metadata" })),
|
|
71
72
|
source: "repository_metadata",
|
|
72
73
|
});
|
|
73
74
|
add("workflow:orchestration", "Workflow orchestration", "workflow");
|
|
@@ -84,7 +85,7 @@ export function inferPrivateCapabilities(input) {
|
|
|
84
85
|
if (/slack|discord|twilio|resend|sendgrid|postmark|nodemailer|email/.test(searchable))
|
|
85
86
|
add("messaging:external", "External messaging", "messaging");
|
|
86
87
|
for (const capability of repositoryManifestCapabilities(input.manifest))
|
|
87
|
-
add(capability.key, capability.observedName, capability.transport);
|
|
88
|
+
add(capability.key, capability.observedName, capability.transport, capability.capabilityId, capability.disposition);
|
|
88
89
|
return [...detected.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
89
90
|
}
|
|
90
91
|
function repositoryManifestCapabilities(value) {
|
|
@@ -101,13 +102,21 @@ function repositoryManifestCapabilities(value) {
|
|
|
101
102
|
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
102
103
|
throw new Error(`Repository discovery manifest capability ${index} is invalid.`);
|
|
103
104
|
const capability = item;
|
|
104
|
-
if (Object.keys(capability).some((key) =>
|
|
105
|
+
if (Object.keys(capability).some((key) => !["key", "observedName", "transport", "capabilityId", "disposition"].includes(key))
|
|
105
106
|
|| typeof capability.key !== "string" || !/^[A-Za-z0-9._:-]{1,120}$/.test(capability.key)
|
|
106
107
|
|| typeof capability.observedName !== "string" || !/^[A-Za-z0-9._:-]{1,120}$/.test(capability.observedName)
|
|
107
|
-
|| !transports.has(capability.transport)
|
|
108
|
+
|| !transports.has(capability.transport)
|
|
109
|
+
|| capability.capabilityId !== undefined && (typeof capability.capabilityId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(capability.capabilityId))
|
|
110
|
+
|| capability.disposition !== undefined && !["allowed", "approval_required", "denied", "pending"].includes(String(capability.disposition))) {
|
|
108
111
|
throw new Error(`Repository discovery manifest capability ${index} failed its bounded contract.`);
|
|
109
112
|
}
|
|
110
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
key: capability.key,
|
|
115
|
+
observedName: capability.observedName,
|
|
116
|
+
transport: capability.transport,
|
|
117
|
+
...(capability.capabilityId ? { capabilityId: capability.capabilityId } : {}),
|
|
118
|
+
disposition: capability.disposition ?? "pending",
|
|
119
|
+
};
|
|
111
120
|
});
|
|
112
121
|
}
|
|
113
122
|
function discoveryIdentityPath(projectId, configHome) {
|