witnora 0.20.3 → 0.20.5
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:
|
|
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
|
|
227
|
-
|
|
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)
|
|
@@ -566,6 +576,10 @@ export async function runCustomerGateway(options) {
|
|
|
566
576
|
FileActionCheckpointStore: (await durableWorker).FileActionCheckpointStore,
|
|
567
577
|
})
|
|
568
578
|
: undefined;
|
|
579
|
+
const probeCredentialReady = createBoundedRuntimeProbeReadiness({
|
|
580
|
+
check: () => runtimeProbeCredentialReady(config, options.configHome),
|
|
581
|
+
initialReady: true,
|
|
582
|
+
});
|
|
569
583
|
const assuranceController = await createContinuousAssuranceController({
|
|
570
584
|
repository: resolve(directory, "..", ".."), directory, projectId: config.projectId, server: config.server,
|
|
571
585
|
apiKey: connection.apiKey, config, sourceSigner: () => keyRing.activeSigner(),
|
|
@@ -588,7 +602,7 @@ export async function runCustomerGateway(options) {
|
|
|
588
602
|
track: (input) => isConfiguredRuntimeProposal(input.proposal, config.runtimeWorker) ? actionWorker.track(input) : Promise.resolve(undefined),
|
|
589
603
|
status: async () => {
|
|
590
604
|
const status = await actionWorker.status();
|
|
591
|
-
const probeReady = await
|
|
605
|
+
const probeReady = await probeCredentialReady();
|
|
592
606
|
return {
|
|
593
607
|
...status,
|
|
594
608
|
ready: status.ready === true && probeReady,
|
|
@@ -649,6 +663,11 @@ async function configManagedWorkflowHarnessImport() {
|
|
|
649
663
|
function configBusinessTaskEvaluatorImport() {
|
|
650
664
|
return import(new URL("./vendor/onegent-runtime/business-task-evaluator.js", import.meta.url).href);
|
|
651
665
|
}
|
|
666
|
+
export function workflowHarnessTickDidWork(result) {
|
|
667
|
+
return (result.evaluationsCompleted ?? 0) > 0
|
|
668
|
+
|| (result.evaluationsFailed ?? 0) > 0
|
|
669
|
+
|| (result.runtimeWatchObservationsUploaded ?? 0) > 0;
|
|
670
|
+
}
|
|
652
671
|
export async function createConfiguredWorkflowHarness(input) {
|
|
653
672
|
const requestFetch = input.fetch ?? fetch;
|
|
654
673
|
let managedEvaluator;
|
|
@@ -717,6 +736,8 @@ export async function createConfiguredWorkflowHarness(input) {
|
|
|
717
736
|
&& input.config.realPathActivations[0].actionPathIds.length === 1
|
|
718
737
|
? input.config.realPathActivations[0]
|
|
719
738
|
: undefined;
|
|
739
|
+
const basePollIntervalMs = input.config.pollIntervalMs ?? 5_000;
|
|
740
|
+
const maxIdlePollIntervalMs = Math.max(basePollIntervalMs, 15 * 60_000);
|
|
720
741
|
const runtimeWatch = runtimeWatchConfiguration
|
|
721
742
|
&& input.managed.ManagedFailureRuntimeWatch
|
|
722
743
|
&& input.managed.FileFailureRuntimeWatchCheckpointStore
|
|
@@ -730,7 +751,7 @@ export async function createConfiguredWorkflowHarness(input) {
|
|
|
730
751
|
fetch: requestFetch,
|
|
731
752
|
}),
|
|
732
753
|
checkpoints: new input.managed.FileFailureRuntimeWatchCheckpointStore(join(input.directory, "data", "failure-runtime-watch")),
|
|
733
|
-
intervalMs:
|
|
754
|
+
intervalMs: maxIdlePollIntervalMs,
|
|
734
755
|
...(activation ? { bindings: [{ taskContractId: activation.taskContractId, actionPathId: activation.actionPathIds[0], environment: "sandbox" }] } : {}),
|
|
735
756
|
readOutcome: async ({ resourceId, actions, receipts }) => input.managed.readLocalSandboxRuntimeWatchOutcome({
|
|
736
757
|
path: join(input.directory, "data", "runtime-sandbox", "fixture", "audit.jsonl"),
|
|
@@ -744,6 +765,7 @@ export async function createConfiguredWorkflowHarness(input) {
|
|
|
744
765
|
: undefined;
|
|
745
766
|
let timer;
|
|
746
767
|
let closing = false;
|
|
768
|
+
let idleTicks = 0;
|
|
747
769
|
let lastTickAt;
|
|
748
770
|
let lastError;
|
|
749
771
|
let lastProviderHealthAt = 0;
|
|
@@ -778,14 +800,33 @@ export async function createConfiguredWorkflowHarness(input) {
|
|
|
778
800
|
start() {
|
|
779
801
|
if (timer || closing)
|
|
780
802
|
return;
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
803
|
+
const schedule = (delayMs) => {
|
|
804
|
+
if (closing)
|
|
805
|
+
return;
|
|
806
|
+
timer = setTimeout(() => void run(), delayMs);
|
|
807
|
+
timer.unref?.();
|
|
808
|
+
};
|
|
809
|
+
const run = async () => {
|
|
810
|
+
let didWork = false;
|
|
811
|
+
try {
|
|
812
|
+
const result = await tick();
|
|
813
|
+
didWork = workflowHarnessTickDidWork(result);
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
process.stderr.write(`Managed Workflow Harness tick failed: ${error instanceof Error ? error.message : "unknown error"}\n`);
|
|
817
|
+
}
|
|
818
|
+
if (closing)
|
|
819
|
+
return;
|
|
820
|
+
idleTicks = didWork ? 0 : idleTicks + 1;
|
|
821
|
+
const multiplier = 2 ** Math.max(0, idleTicks - 1);
|
|
822
|
+
schedule(Math.min(maxIdlePollIntervalMs, basePollIntervalMs * multiplier));
|
|
823
|
+
};
|
|
824
|
+
schedule(0);
|
|
784
825
|
},
|
|
785
826
|
async close() {
|
|
786
827
|
closing = true;
|
|
787
828
|
if (timer)
|
|
788
|
-
|
|
829
|
+
clearTimeout(timer);
|
|
789
830
|
timer = undefined;
|
|
790
831
|
await managedEvaluator?.close();
|
|
791
832
|
},
|
|
@@ -1317,12 +1358,15 @@ export async function statusManagedCustomerGateway(options = {}) {
|
|
|
1317
1358
|
pid: runtime?.pid,
|
|
1318
1359
|
logPath: runtime?.logPath,
|
|
1319
1360
|
};
|
|
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
1361
|
if (runtime && !runtimeMatches(runtime, config)) {
|
|
1362
|
+
if (!pidRunning(runtime.pid)) {
|
|
1363
|
+
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." };
|
|
1364
|
+
}
|
|
1324
1365
|
return { ...base, state: "CONFLICT", healthy: false, managed: false, detail: "Managed Gateway runtime metadata belongs to a different project or collector." };
|
|
1325
1366
|
}
|
|
1367
|
+
if (health && health.collectorId !== config.collectorId) {
|
|
1368
|
+
return { ...base, state: "CONFLICT", healthy: false, managed: false, detail: `Port ${config.port} is occupied by collector ${health.collectorId}, not ${config.collectorId}.` };
|
|
1369
|
+
}
|
|
1326
1370
|
if (health) {
|
|
1327
1371
|
const managed = Boolean(runtime && pidRunning(runtime.pid));
|
|
1328
1372
|
return {
|
|
@@ -2181,6 +2225,39 @@ function wait(milliseconds) {
|
|
|
2181
2225
|
function safeSlug(value) {
|
|
2182
2226
|
return (value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent-repository").slice(0, 48);
|
|
2183
2227
|
}
|
|
2228
|
+
export function createBoundedRuntimeProbeReadiness(input) {
|
|
2229
|
+
const now = input.now ?? Date.now;
|
|
2230
|
+
const successTtlMs = input.successTtlMs ?? 300_000;
|
|
2231
|
+
const failureTtlMs = input.failureTtlMs ?? 15_000;
|
|
2232
|
+
let ready = input.initialReady ?? false;
|
|
2233
|
+
let checkedAt = input.initialReady === undefined ? Number.NEGATIVE_INFINITY : now();
|
|
2234
|
+
let pending;
|
|
2235
|
+
return async () => {
|
|
2236
|
+
const ttl = ready ? successTtlMs : failureTtlMs;
|
|
2237
|
+
if (now() - checkedAt < ttl)
|
|
2238
|
+
return ready;
|
|
2239
|
+
if (pending)
|
|
2240
|
+
return pending;
|
|
2241
|
+
pending = input.check()
|
|
2242
|
+
.then((nextReady) => {
|
|
2243
|
+
ready = nextReady;
|
|
2244
|
+
checkedAt = now();
|
|
2245
|
+
return ready;
|
|
2246
|
+
})
|
|
2247
|
+
.catch(() => {
|
|
2248
|
+
ready = false;
|
|
2249
|
+
checkedAt = now();
|
|
2250
|
+
return false;
|
|
2251
|
+
})
|
|
2252
|
+
.finally(() => { pending = undefined; });
|
|
2253
|
+
return pending;
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
function collectorIdForRepository(repository, projectId) {
|
|
2257
|
+
const readable = safeSlug(basename(repository)).slice(0, 36);
|
|
2258
|
+
const identity = createHash("sha256").update(`${resolve(repository)}\n${projectId}`).digest("hex").slice(0, 10);
|
|
2259
|
+
return `${readable}-${identity}-collector`;
|
|
2260
|
+
}
|
|
2184
2261
|
function message(error) {
|
|
2185
2262
|
return error instanceof Error ? error.message : String(error);
|
|
2186
2263
|
}
|
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
|
|
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(
|
|
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.
|
|
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
|
|
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() {
|