witnora 0.20.3 → 0.20.4

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.js CHANGED
@@ -56,7 +56,7 @@ export async function initializeCustomerGateway(options) {
56
56
  projectId: authorization.projectId,
57
57
  server: authorization.server,
58
58
  connectionName: authorization.connectionName,
59
- collectorId: `${slug}-collector`,
59
+ collectorId: collectorIdForRepository(repository, options.projectId),
60
60
  host: process.env.WITNORA_GATEWAY_HOST?.trim() || "127.0.0.1",
61
61
  port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, 8787),
62
62
  storageDirectory: "data",
@@ -223,8 +223,18 @@ export async function ensureCustomerGatewayPortAvailable(options) {
223
223
  const httpActionRaw = await readFile(httpActionPath, "utf8").catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
224
224
  const httpAction = httpActionRaw ? parseCustomerHttpActionConfig(httpActionRaw) : undefined;
225
225
  const httpActionGuideRaw = httpAction ? await readFile(httpActionGuidePath, "utf8") : undefined;
226
- const health = await localCollector(current.host, current.port, options.fetch ?? fetch);
227
- if (health === current.collectorId || (!health && await canListen(current.host, current.port))) {
226
+ const requestFetch = options.fetch ?? fetch;
227
+ let health = await localCollector(current.host, current.port, requestFetch);
228
+ if (!health && await canListen(current.host, current.port)) {
229
+ // A supervised Gateway can leave a short window between process exit and
230
+ // recovery. Recheck before claiming the port so another repository's
231
+ // supervisor cannot reclaim it between this probe and service startup.
232
+ await (options.sleep ?? wait)(600);
233
+ health = await localCollector(current.host, current.port, requestFetch);
234
+ if (!health && await canListen(current.host, current.port))
235
+ return { changed: false, port: current.port };
236
+ }
237
+ if (health === current.collectorId) {
228
238
  return { changed: false, port: current.port };
229
239
  }
230
240
  if (clientRaw !== gatewayClient(current) || readmeRaw !== gatewayReadme(current)
@@ -649,6 +659,11 @@ async function configManagedWorkflowHarnessImport() {
649
659
  function configBusinessTaskEvaluatorImport() {
650
660
  return import(new URL("./vendor/onegent-runtime/business-task-evaluator.js", import.meta.url).href);
651
661
  }
662
+ export function workflowHarnessTickDidWork(result) {
663
+ return (result.evaluationsCompleted ?? 0) > 0
664
+ || (result.evaluationsFailed ?? 0) > 0
665
+ || (result.runtimeWatchObservationsUploaded ?? 0) > 0;
666
+ }
652
667
  export async function createConfiguredWorkflowHarness(input) {
653
668
  const requestFetch = input.fetch ?? fetch;
654
669
  let managedEvaluator;
@@ -717,6 +732,8 @@ export async function createConfiguredWorkflowHarness(input) {
717
732
  && input.config.realPathActivations[0].actionPathIds.length === 1
718
733
  ? input.config.realPathActivations[0]
719
734
  : undefined;
735
+ const basePollIntervalMs = input.config.pollIntervalMs ?? 5_000;
736
+ const maxIdlePollIntervalMs = Math.max(basePollIntervalMs, 15 * 60_000);
720
737
  const runtimeWatch = runtimeWatchConfiguration
721
738
  && input.managed.ManagedFailureRuntimeWatch
722
739
  && input.managed.FileFailureRuntimeWatchCheckpointStore
@@ -730,7 +747,7 @@ export async function createConfiguredWorkflowHarness(input) {
730
747
  fetch: requestFetch,
731
748
  }),
732
749
  checkpoints: new input.managed.FileFailureRuntimeWatchCheckpointStore(join(input.directory, "data", "failure-runtime-watch")),
733
- intervalMs: input.config.pollIntervalMs ?? 5_000,
750
+ intervalMs: maxIdlePollIntervalMs,
734
751
  ...(activation ? { bindings: [{ taskContractId: activation.taskContractId, actionPathId: activation.actionPathIds[0], environment: "sandbox" }] } : {}),
735
752
  readOutcome: async ({ resourceId, actions, receipts }) => input.managed.readLocalSandboxRuntimeWatchOutcome({
736
753
  path: join(input.directory, "data", "runtime-sandbox", "fixture", "audit.jsonl"),
@@ -744,6 +761,7 @@ export async function createConfiguredWorkflowHarness(input) {
744
761
  : undefined;
745
762
  let timer;
746
763
  let closing = false;
764
+ let idleTicks = 0;
747
765
  let lastTickAt;
748
766
  let lastError;
749
767
  let lastProviderHealthAt = 0;
@@ -778,14 +796,33 @@ export async function createConfiguredWorkflowHarness(input) {
778
796
  start() {
779
797
  if (timer || closing)
780
798
  return;
781
- void tick().catch((error) => process.stderr.write(`Managed Workflow Harness tick failed: ${error instanceof Error ? error.message : "unknown error"}\n`));
782
- timer = setInterval(() => void tick().catch((error) => process.stderr.write(`Managed Workflow Harness tick failed: ${error instanceof Error ? error.message : "unknown error"}\n`)), input.config.pollIntervalMs ?? 5_000);
783
- timer.unref?.();
799
+ const schedule = (delayMs) => {
800
+ if (closing)
801
+ return;
802
+ timer = setTimeout(() => void run(), delayMs);
803
+ timer.unref?.();
804
+ };
805
+ const run = async () => {
806
+ let didWork = false;
807
+ try {
808
+ const result = await tick();
809
+ didWork = workflowHarnessTickDidWork(result);
810
+ }
811
+ catch (error) {
812
+ process.stderr.write(`Managed Workflow Harness tick failed: ${error instanceof Error ? error.message : "unknown error"}\n`);
813
+ }
814
+ if (closing)
815
+ return;
816
+ idleTicks = didWork ? 0 : idleTicks + 1;
817
+ const multiplier = 2 ** Math.max(0, idleTicks - 1);
818
+ schedule(Math.min(maxIdlePollIntervalMs, basePollIntervalMs * multiplier));
819
+ };
820
+ schedule(0);
784
821
  },
785
822
  async close() {
786
823
  closing = true;
787
824
  if (timer)
788
- clearInterval(timer);
825
+ clearTimeout(timer);
789
826
  timer = undefined;
790
827
  await managedEvaluator?.close();
791
828
  },
@@ -1317,12 +1354,15 @@ export async function statusManagedCustomerGateway(options = {}) {
1317
1354
  pid: runtime?.pid,
1318
1355
  logPath: runtime?.logPath,
1319
1356
  };
1320
- if (health && health.collectorId !== config.collectorId) {
1321
- return { ...base, state: "CONFLICT", healthy: false, managed: false, detail: `Port ${config.port} is occupied by collector ${health.collectorId}, not ${config.collectorId}.` };
1322
- }
1323
1357
  if (runtime && !runtimeMatches(runtime, config)) {
1358
+ if (!pidRunning(runtime.pid)) {
1359
+ return { ...base, state: "STALE", healthy: false, managed: false, detail: "Managed Gateway runtime metadata belongs to an older generated identity and its recorded process is no longer running." };
1360
+ }
1324
1361
  return { ...base, state: "CONFLICT", healthy: false, managed: false, detail: "Managed Gateway runtime metadata belongs to a different project or collector." };
1325
1362
  }
1363
+ if (health && health.collectorId !== config.collectorId) {
1364
+ return { ...base, state: "CONFLICT", healthy: false, managed: false, detail: `Port ${config.port} is occupied by collector ${health.collectorId}, not ${config.collectorId}.` };
1365
+ }
1326
1366
  if (health) {
1327
1367
  const managed = Boolean(runtime && pidRunning(runtime.pid));
1328
1368
  return {
@@ -2181,6 +2221,11 @@ function wait(milliseconds) {
2181
2221
  function safeSlug(value) {
2182
2222
  return (value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent-repository").slice(0, 48);
2183
2223
  }
2224
+ function collectorIdForRepository(repository, projectId) {
2225
+ const readable = safeSlug(basename(repository)).slice(0, 36);
2226
+ const identity = createHash("sha256").update(`${resolve(repository)}\n${projectId}`).digest("hex").slice(0, 10);
2227
+ return `${readable}-${identity}-collector`;
2228
+ }
2184
2229
  function message(error) {
2185
2230
  return error instanceof Error ? error.message : String(error);
2186
2231
  }
package/dist/onboard.js CHANGED
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { DEFAULT_WITNORA_SERVER, saveConnection } from "./credentials.js";
5
+ import { credentialsPath, DEFAULT_WITNORA_SERVER, loadConnection, saveConnection } from "./credentials.js";
6
6
  import { authorizeProjectConnection } from "./device-authorization.js";
7
7
  import { verifyControlPlaneConnection } from "./control-plane.js";
8
8
  import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
@@ -26,7 +26,15 @@ export async function runOnboard(options) {
26
26
  framework: frameworkForTemplate(repository.template),
27
27
  };
28
28
  const connectionName = options.name ?? repository.slug;
29
- const token = await authorizeProjectConnection({
29
+ const reusableConnection = await loadMatchingOnboardConnection({
30
+ repository: repositoryPath,
31
+ projectId: options.projectId,
32
+ server,
33
+ connectionName,
34
+ configHome: options.configHome,
35
+ fetch: requestFetch,
36
+ });
37
+ const token = reusableConnection ?? await authorizeProjectConnection({
30
38
  projectId: options.projectId,
31
39
  connectionName,
32
40
  credentialProfile: "autopilot",
@@ -38,6 +46,8 @@ export async function runOnboard(options) {
38
46
  output,
39
47
  configHome: options.configHome,
40
48
  });
49
+ if (reusableConnection)
50
+ output("Reusing the previously approved project-scoped setup connection; no new browser approval is required.\n");
41
51
  const credentialsPath = token.credentialsPath;
42
52
  await verifyControlPlaneConnection({ baseUrl: server, projectId: token.projectId, apiKey: token.apiKey, fetch: requestFetch });
43
53
  const planSnapshot = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/setup-plans`, {
@@ -211,7 +221,7 @@ export async function runOnboard(options) {
211
221
  if (!(error instanceof RuntimeSetupNotReadyError))
212
222
  throw error;
213
223
  runtimeLimitation = `Local sandbox Runtime references were found but did not establish readiness: ${error.message}`;
214
- output("\nExisting customer-owned Gateway remains RECORDED_ONLY because its Runtime references did not pass readiness checks.\n");
224
+ output(`\nExisting customer-owned Gateway remains RECORDED_ONLY because its Runtime references did not pass readiness checks: ${error.message}\n`);
215
225
  }
216
226
  }
217
227
  else {
@@ -336,7 +346,7 @@ export async function runOnboard(options) {
336
346
  output(`Action transport: ${actionTransportTest.transport.toUpperCase()} is waiting for one exact sandbox Task/action path. ${actionTransportTest.limitation}\n`);
337
347
  }
338
348
  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`
349
+ ? `Base Gateway connected, but ${actionTransportTest.transport.toUpperCase()} Action is not verified yet. Run the Agent's normal sandbox workflow from this repository; there is no Run button in Overview. Once Overview receives the activity, confirm one sandbox Business Task and choose its exact discovered Agent action, then rerun this same command.\n`
340
350
  : "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");
341
351
  return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
342
352
  repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
@@ -368,6 +378,35 @@ export async function runOnboard(options) {
368
378
  throw new Error(`Witnora Setup Autopilot rolled back this install attempt: ${diagnosis}`);
369
379
  }
370
380
  }
381
+ const AUTOPILOT_CONNECTION_SCOPES = [
382
+ "runs:read", "runs:write", "events:write", "evidence:write", "collector:manage",
383
+ "actions:read", "actions:propose", "actions:execute",
384
+ ];
385
+ async function loadMatchingOnboardConnection(options) {
386
+ const gateway = await inspectCustomerGatewayFiles({ repository: options.repository });
387
+ try {
388
+ let candidateName = options.connectionName;
389
+ if (gateway.status === "complete") {
390
+ const config = JSON.parse(await readFile(join(gateway.directory, "gateway.json"), "utf8"));
391
+ if (config.projectId !== options.projectId || config.server !== options.server || typeof config.connectionName !== "string")
392
+ return undefined;
393
+ candidateName = config.connectionName;
394
+ }
395
+ const stored = await loadConnection(candidateName, { configHome: options.configHome });
396
+ if (!stored || stored.projectId !== options.projectId || stored.server !== options.server)
397
+ return undefined;
398
+ await verifyControlPlaneConnection({ baseUrl: stored.server, projectId: stored.projectId, apiKey: stored.apiKey, fetch: options.fetch });
399
+ return {
400
+ ...stored,
401
+ connectionName: candidateName,
402
+ credentialsPath: credentialsPath({ configHome: options.configHome }),
403
+ scopes: [...AUTOPILOT_CONNECTION_SCOPES],
404
+ };
405
+ }
406
+ catch {
407
+ return undefined;
408
+ }
409
+ }
371
410
  async function runActionTransportTest(options) {
372
411
  if (!options.httpActionReady)
373
412
  return {
@@ -87,8 +87,8 @@ export class ManagedFailureRuntimeWatch {
87
87
  this.#now = options.now ?? (() => new Date());
88
88
  this.#readOutcome = options.readOutcome;
89
89
  this.#bindings = options.bindings ? new Set(options.bindings.map(bindingKey)) : undefined;
90
- if (!Number.isSafeInteger(this.#intervalMs) || this.#intervalMs < 1_000 || this.#intervalMs > 60_000) {
91
- throw new Error("Failure Runtime Watch intervalMs must be between 1000 and 60000.");
90
+ if (!Number.isSafeInteger(this.#intervalMs) || this.#intervalMs < 1_000 || this.#intervalMs > 15 * 60_000) {
91
+ throw new Error("Failure Runtime Watch intervalMs must be between 1000 and 900000.");
92
92
  }
93
93
  }
94
94
  tick() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.20.3",
3
+ "version": "0.20.4",
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",