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.
@@ -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) => { await mkdir(dirname(path), { recursive: true }); await writeFile(path, value, { encoding: "utf8", mode: 0o600 }); },
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-8"?>
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
- disposition: "pending",
70
- schemaSha256: sha256(canonicalJson({ key, observedName, transport, source: "repository_metadata" })),
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) => key !== "key" && key !== "observedName" && key !== "transport")
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 { key: capability.key, observedName: capability.observedName, transport: capability.transport };
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",