witnora 0.15.1 → 0.17.0
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 +35 -2
- package/dist/command-help.js +6 -2
- package/dist/gateway-service.js +104 -0
- package/dist/gateway.js +23 -0
- package/dist/internal/control-client/collector-gateway.d.ts +4 -0
- package/dist/internal/control-client/collector-gateway.d.ts.map +1 -1
- package/dist/internal/control-client/collector-gateway.js +8 -1
- package/dist/onboard.js +14 -1
- package/dist/real-path-activation.js +145 -9
- package/dist/runtime-bootstrap.js +5 -2
- package/dist/vendor/onegent-runtime/provider-integration-packs.d.ts +1 -0
- package/dist/vendor/onegent-runtime/provider-integration-packs.d.ts.map +1 -1
- package/dist/vendor/onegent-runtime/provider-integration-packs.js +38 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { validateEvidenceArtifacts } from "./artifact-validation.js";
|
|
6
7
|
import { renderAgentCertBadge } from "./badge.js";
|
|
7
8
|
import { buildEvidenceBundle } from "./bundle.js";
|
|
@@ -39,7 +40,8 @@ import { runOnboard } from "./onboard.js";
|
|
|
39
40
|
import { inspectRepository } from "./onboard.js";
|
|
40
41
|
import { renderReleaseEvaluation, runReleaseEvaluation } from "./release-evaluation.js";
|
|
41
42
|
import { runPrivateCapabilityDiscovery } from "./private-discovery.js";
|
|
42
|
-
import { configureManagedWorkflowHarness, doctorCustomerGateway, initializeCustomerGateway, isGatewayDoctorReady, readManagedCustomerGatewayLogs, renderGatewayDoctor, renderManagedGatewayStatus, restartManagedCustomerGateway, runCustomerGateway, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, } from "./gateway.js";
|
|
43
|
+
import { configureManagedWorkflowHarness, doctorCustomerGateway, initializeCustomerGateway, isGatewayDoctorReady, readManagedCustomerGatewayLogs, renderGatewayDoctor, renderManagedGatewayStatus, restartManagedCustomerGateway, runCustomerGateway, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, superviseManagedCustomerGateway, } from "./gateway.js";
|
|
44
|
+
import { installCurrentGatewayService, restartCurrentGatewayService, uninstallCurrentGatewayService } from "./gateway-service.js";
|
|
43
45
|
import { verifyEvidencePacketV02 } from "./evidence-v02.js";
|
|
44
46
|
process.on("uncaughtException", reportFatalError);
|
|
45
47
|
process.on("unhandledRejection", reportFatalError);
|
|
@@ -224,6 +226,37 @@ else if (command === "gateway") {
|
|
|
224
226
|
const result = await restartManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
|
|
225
227
|
process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderManagedGatewayStatus(result));
|
|
226
228
|
}
|
|
229
|
+
else if (action === "repair") {
|
|
230
|
+
const repository = readFlag("--repo") ?? process.cwd();
|
|
231
|
+
const dir = readFlag("--dir");
|
|
232
|
+
const service = await installCurrentGatewayService({ repository, gatewayDirectory: dir, cliEntry: fileURLToPath(import.meta.url) });
|
|
233
|
+
await restartCurrentGatewayService({ repository, gatewayDirectory: dir, cliEntry: fileURLToPath(import.meta.url) });
|
|
234
|
+
const result = await startManagedCustomerGateway({ repository, dir });
|
|
235
|
+
const doctor = await doctorCustomerGateway({ repository, dir });
|
|
236
|
+
if (!isGatewayDoctorReady(doctor))
|
|
237
|
+
throw new Error(`Connection repair did not restore a ready Gateway. ${doctor.nextAction}`);
|
|
238
|
+
process.stdout.write(`Connection repaired. ${service.plan.kind} ${service.plan.id} is installed and the Gateway is healthy.\n`);
|
|
239
|
+
process.stdout.write(renderManagedGatewayStatus(result));
|
|
240
|
+
}
|
|
241
|
+
else if (action === "service") {
|
|
242
|
+
const serviceAction = process.argv[4] ?? "install";
|
|
243
|
+
const repository = readFlag("--repo") ?? process.cwd();
|
|
244
|
+
const gatewayDirectory = readFlag("--dir");
|
|
245
|
+
const cliEntry = fileURLToPath(import.meta.url);
|
|
246
|
+
if (serviceAction === "install") {
|
|
247
|
+
const result = await installCurrentGatewayService({ repository, gatewayDirectory, cliEntry });
|
|
248
|
+
process.stdout.write(`Installed ${result.plan.kind} service ${result.plan.id}.\n`);
|
|
249
|
+
}
|
|
250
|
+
else if (serviceAction === "uninstall") {
|
|
251
|
+
const result = await uninstallCurrentGatewayService({ repository, gatewayDirectory, cliEntry });
|
|
252
|
+
process.stdout.write(`Removed Gateway service ${result.plan.id}.\n`);
|
|
253
|
+
}
|
|
254
|
+
else
|
|
255
|
+
throw new Error("Use witnora gateway service install|uninstall.");
|
|
256
|
+
}
|
|
257
|
+
else if (action === "supervise") {
|
|
258
|
+
await superviseManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir"), output: (message) => process.stdout.write(message) });
|
|
259
|
+
}
|
|
227
260
|
else if (action === "stop") {
|
|
228
261
|
const result = await stopManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
|
|
229
262
|
process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderManagedGatewayStatus(result));
|
|
@@ -249,7 +282,7 @@ else if (command === "gateway") {
|
|
|
249
282
|
process.stdout.write(`Configured the Managed Workflow Harness for ${result.config.workflowIds?.length ?? "all active"} Workflow Contract(s).\nConfig: ${result.path}\nRestart the managed Gateway to activate it.\n`);
|
|
250
283
|
}
|
|
251
284
|
else {
|
|
252
|
-
throw new Error("Use witnora gateway init|doctor|start|status|logs|restart|stop|run|workflow-harness.");
|
|
285
|
+
throw new Error("Use witnora gateway init|doctor|start|status|logs|restart|repair|service|stop|run|workflow-harness.");
|
|
253
286
|
}
|
|
254
287
|
}
|
|
255
288
|
else if (command === "discover") {
|
package/dist/command-help.js
CHANGED
|
@@ -63,6 +63,8 @@ Options:
|
|
|
63
63
|
witnora gateway status
|
|
64
64
|
witnora gateway logs [--lines 100]
|
|
65
65
|
witnora gateway restart
|
|
66
|
+
witnora gateway repair
|
|
67
|
+
witnora gateway service install|uninstall
|
|
66
68
|
witnora gateway stop
|
|
67
69
|
witnora gateway run
|
|
68
70
|
witnora gateway workflow-harness --workflow <id> --evaluator-origin http://127.0.0.1:<port>/ \\
|
|
@@ -71,8 +73,10 @@ Options:
|
|
|
71
73
|
Initializes and manages a customer-owned, metadata-only collector beside the Agent.
|
|
72
74
|
The browser approval issues a collector-scoped credential; no API key is copied into
|
|
73
75
|
the repository or exposed to the Agent. The local queue and source signing key remain
|
|
74
|
-
under customer control.
|
|
75
|
-
the
|
|
76
|
+
under customer control. The service command installs an OS-native auto-start and restart
|
|
77
|
+
boundary. Repair safely reinstalls that boundary, restarts the existing generated Gateway,
|
|
78
|
+
and reruns digest, credential, Probe, and health checks. The run command is the foreground
|
|
79
|
+
debugging path.
|
|
76
80
|
|
|
77
81
|
The reference Gateway establishes RECORDED evidence only. ENFORCED requires target
|
|
78
82
|
write credentials behind a controlled execution adapter. OUTCOME VERIFIED requires a
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir, userInfo } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export async function installGatewayService(input) {
|
|
9
|
+
const plan = createGatewayServicePlan(input);
|
|
10
|
+
await input.writeDefinition(plan.definitionPath, plan.definition);
|
|
11
|
+
if (plan.kind === "SYSTEMD_USER")
|
|
12
|
+
await input.run("systemctl", ["--user", "daemon-reload"]);
|
|
13
|
+
await input.run(plan.install.command, plan.install.args);
|
|
14
|
+
if (plan.kind !== "SYSTEMD_USER")
|
|
15
|
+
await input.run(plan.start.command, plan.start.args);
|
|
16
|
+
return { installed: true, plan };
|
|
17
|
+
}
|
|
18
|
+
export async function installCurrentGatewayService(options) {
|
|
19
|
+
const planInput = currentPlanInput(options);
|
|
20
|
+
return installGatewayService({ ...planInput,
|
|
21
|
+
writeDefinition: async (path, value) => { await mkdir(dirname(path), { recursive: true }); await writeFile(path, value, { encoding: "utf8", mode: 0o600 }); },
|
|
22
|
+
run: options.run ?? runCommand,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function uninstallCurrentGatewayService(options) {
|
|
26
|
+
const plan = createGatewayServicePlan(currentPlanInput(options));
|
|
27
|
+
const run = options.run ?? runCommand;
|
|
28
|
+
await run(plan.uninstall.command, plan.uninstall.args);
|
|
29
|
+
if (plan.kind === "SYSTEMD_USER")
|
|
30
|
+
await run("systemctl", ["--user", "daemon-reload"]);
|
|
31
|
+
await rm(plan.definitionPath, { force: true });
|
|
32
|
+
return { uninstalled: true, plan };
|
|
33
|
+
}
|
|
34
|
+
export async function restartCurrentGatewayService(options) {
|
|
35
|
+
const plan = createGatewayServicePlan(currentPlanInput(options));
|
|
36
|
+
const run = options.run ?? runCommand;
|
|
37
|
+
await run(plan.stop.command, plan.stop.args).catch(() => undefined);
|
|
38
|
+
await run(plan.start.command, plan.start.args);
|
|
39
|
+
return plan;
|
|
40
|
+
}
|
|
41
|
+
export function createGatewayServicePlan(input) {
|
|
42
|
+
const suffix = createHash("sha256").update(`${input.repository}\n${input.gatewayDirectory}`).digest("hex").slice(0, 12);
|
|
43
|
+
const id = `witnora-gateway-${suffix}`;
|
|
44
|
+
const superviseArgs = [input.cliEntry, "gateway", "supervise", "--repo", input.repository, "--dir", input.gatewayDirectory];
|
|
45
|
+
if (input.platform === "win32")
|
|
46
|
+
return windowsPlan(input, id, superviseArgs);
|
|
47
|
+
if (input.platform === "darwin")
|
|
48
|
+
return launchdPlan(input, id, superviseArgs);
|
|
49
|
+
if (input.platform === "linux")
|
|
50
|
+
return systemdPlan(input, id, superviseArgs);
|
|
51
|
+
throw new Error(`Gateway service installation is not supported on ${input.platform}.`);
|
|
52
|
+
}
|
|
53
|
+
function windowsPlan(input, id, args) {
|
|
54
|
+
const definitionPath = `${input.serviceHome}\\${id}.xml`;
|
|
55
|
+
const argumentsValue = args.map(windowsArgument).join(" ");
|
|
56
|
+
const definition = `<?xml version="1.0" encoding="UTF-8"?>
|
|
57
|
+
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
58
|
+
<Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml(input.userId)}</UserId></LogonTrigger></Triggers>
|
|
59
|
+
<Principals><Principal id="Author"><UserId>${xml(input.userId)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
60
|
+
<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure></Settings>
|
|
61
|
+
<Actions Context="Author"><Exec><Command>${xml(input.nodeExecutable)}</Command><Arguments>${xml(argumentsValue)}</Arguments><WorkingDirectory>${xml(input.repository)}</WorkingDirectory></Exec></Actions>
|
|
62
|
+
</Task>`;
|
|
63
|
+
return { id, kind: "WINDOWS_TASK", definitionPath, definition,
|
|
64
|
+
install: { command: "schtasks.exe", args: ["/Create", "/TN", id, "/XML", definitionPath, "/F"] },
|
|
65
|
+
start: { command: "schtasks.exe", args: ["/Run", "/TN", id] },
|
|
66
|
+
stop: { command: "schtasks.exe", args: ["/End", "/TN", id] },
|
|
67
|
+
uninstall: { command: "schtasks.exe", args: ["/Delete", "/TN", id, "/F"] } };
|
|
68
|
+
}
|
|
69
|
+
function currentPlanInput(options) {
|
|
70
|
+
const platform = options.platform ?? process.platform;
|
|
71
|
+
if (platform !== "win32" && platform !== "linux" && platform !== "darwin")
|
|
72
|
+
throw new Error(`Gateway service installation is not supported on ${platform}.`);
|
|
73
|
+
const repository = resolve(options.repository ?? process.cwd());
|
|
74
|
+
const env = options.env ?? process.env;
|
|
75
|
+
const serviceHome = platform === "win32" ? join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "Witnora", "services")
|
|
76
|
+
: platform === "darwin" ? join(homedir(), "Library", "LaunchAgents") : join(homedir(), ".config", "systemd", "user");
|
|
77
|
+
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 };
|
|
78
|
+
}
|
|
79
|
+
async function runCommand(command, args) { await execFileAsync(command, args, { windowsHide: true }); }
|
|
80
|
+
function systemdPlan(input, id, args) {
|
|
81
|
+
const unit = `${id}.service`;
|
|
82
|
+
const definitionPath = `${input.serviceHome}/${unit}`;
|
|
83
|
+
const definition = `[Unit]\nDescription=Witnora customer-owned Gateway\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nWorkingDirectory=${systemd(input.repository)}\nExecStart=${[input.nodeExecutable, ...args].map(systemd).join(" ")}\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`;
|
|
84
|
+
return { id, kind: "SYSTEMD_USER", definitionPath, definition,
|
|
85
|
+
install: { command: "systemctl", args: ["--user", "enable", "--now", unit] },
|
|
86
|
+
start: { command: "systemctl", args: ["--user", "start", unit] },
|
|
87
|
+
stop: { command: "systemctl", args: ["--user", "stop", unit] },
|
|
88
|
+
uninstall: { command: "systemctl", args: ["--user", "disable", "--now", unit] } };
|
|
89
|
+
}
|
|
90
|
+
function launchdPlan(input, id, args) {
|
|
91
|
+
const label = `com.witnora.${id}`;
|
|
92
|
+
const definitionPath = `${input.serviceHome}/${label}.plist`;
|
|
93
|
+
const programArguments = [input.nodeExecutable, ...args].map((value) => `<string>${xml(value)}</string>`).join("");
|
|
94
|
+
const definition = `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array>${programArguments}</array><key>WorkingDirectory</key><string>${xml(input.repository)}</string><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>5</integer></dict></plist>`;
|
|
95
|
+
const domain = `gui/${process.getuid?.() ?? input.userId}`;
|
|
96
|
+
return { id, kind: "LAUNCHD_AGENT", definitionPath, definition,
|
|
97
|
+
install: { command: "launchctl", args: ["bootstrap", domain, definitionPath] },
|
|
98
|
+
start: { command: "launchctl", args: ["kickstart", `${domain}/${label}`] },
|
|
99
|
+
stop: { command: "launchctl", args: ["kill", "SIGTERM", `${domain}/${label}`] },
|
|
100
|
+
uninstall: { command: "launchctl", args: ["bootout", domain, definitionPath] } };
|
|
101
|
+
}
|
|
102
|
+
function windowsArgument(value) { return `"${value.replaceAll('"', '\\"')}"`; }
|
|
103
|
+
function systemd(value) { return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; }
|
|
104
|
+
function xml(value) { return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); }
|
package/dist/gateway.js
CHANGED
|
@@ -514,6 +514,10 @@ export async function runCustomerGateway(options) {
|
|
|
514
514
|
host: process.env.WITNORA_GATEWAY_HOST?.trim() || config.host,
|
|
515
515
|
port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, config.port),
|
|
516
516
|
environment: "customer-owned",
|
|
517
|
+
onRepairRequested: async ({ requestId }) => {
|
|
518
|
+
process.stderr.write(`Hosted connection repair ${requestId} requested a verified Gateway restart.\n`);
|
|
519
|
+
setTimeout(() => process.exit(75), 25).unref();
|
|
520
|
+
},
|
|
517
521
|
...(actionWorker && config.runtimeWorker ? { actionWorker: {
|
|
518
522
|
track: (input) => isConfiguredRuntimeProposal(input.proposal, config.runtimeWorker) ? actionWorker.track(input) : Promise.resolve(undefined),
|
|
519
523
|
status: async () => {
|
|
@@ -1056,6 +1060,25 @@ export async function restartManagedCustomerGateway(options = {}) {
|
|
|
1056
1060
|
await stopManagedCustomerGateway(options);
|
|
1057
1061
|
return startManagedCustomerGateway(options);
|
|
1058
1062
|
}
|
|
1063
|
+
export async function superviseManagedCustomerGateway(options = {}) {
|
|
1064
|
+
const inspect = options.inspect ?? (() => statusManagedCustomerGateway({ repository: options.repository, dir: options.dir }));
|
|
1065
|
+
const start = options.start ?? (() => startManagedCustomerGateway({ repository: options.repository, dir: options.dir }));
|
|
1066
|
+
const sleep = options.sleep ?? wait;
|
|
1067
|
+
const maxCycles = options.maxCycles ?? Number.POSITIVE_INFINITY;
|
|
1068
|
+
let recoveries = 0;
|
|
1069
|
+
for (let cycle = 0; cycle < maxCycles; cycle += 1) {
|
|
1070
|
+
const status = await inspect();
|
|
1071
|
+
if (status.state === "CONFLICT")
|
|
1072
|
+
throw new Error(status.detail);
|
|
1073
|
+
if (status.state === "STOPPED" || status.state === "STALE") {
|
|
1074
|
+
await start();
|
|
1075
|
+
recoveries += 1;
|
|
1076
|
+
options.output?.(`Recovered customer-owned Gateway (${recoveries}).\n`);
|
|
1077
|
+
}
|
|
1078
|
+
if (cycle + 1 < maxCycles)
|
|
1079
|
+
await sleep(options.intervalMs ?? 5_000);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1059
1082
|
export async function readManagedCustomerGatewayLogs(options = {}) {
|
|
1060
1083
|
const repository = resolve(options.repository ?? process.cwd());
|
|
1061
1084
|
const directory = resolve(repository, options.dir ?? ".witnora/gateway");
|
|
@@ -50,6 +50,10 @@ export interface CustomerOwnedCollectorGatewayOptions {
|
|
|
50
50
|
status(): Promise<AssuranceHarnessHeartbeat>;
|
|
51
51
|
};
|
|
52
52
|
runtimeBinding?: RuntimeReadinessBinding;
|
|
53
|
+
onRepairRequested?: (request: {
|
|
54
|
+
requestId: string;
|
|
55
|
+
action: "RESTART_AND_VERIFY";
|
|
56
|
+
}) => void | Promise<void>;
|
|
53
57
|
}
|
|
54
58
|
export interface CustomerOwnedCollectorGateway {
|
|
55
59
|
baseUrl: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collector-gateway.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/collector-gateway.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"collector-gateway.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/collector-gateway.ts"],"names":[],"mappings":"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"}
|
|
@@ -176,7 +176,7 @@ export async function startCustomerOwnedCollectorGateway(options) {
|
|
|
176
176
|
try {
|
|
177
177
|
await ensureSourceKeyRegistered();
|
|
178
178
|
const workerStatus = options.actionWorker ? await options.actionWorker.status() : undefined;
|
|
179
|
-
await options.client.heartbeat({
|
|
179
|
+
const response = await options.client.heartbeat({
|
|
180
180
|
collectorId: activeCollector.id,
|
|
181
181
|
signer,
|
|
182
182
|
pendingRecordCount: current.pendingRecordCount,
|
|
@@ -184,6 +184,13 @@ export async function startCustomerOwnedCollectorGateway(options) {
|
|
|
184
184
|
runtime: runtimeBinding && workerStatus ? runtimeHeartbeat(runtimeBinding, workerStatus, options.actionWorker?.runtimeBinding?.fixtureReady) : undefined,
|
|
185
185
|
assuranceHarness: options.assuranceHarness ? await options.assuranceHarness.status() : undefined,
|
|
186
186
|
});
|
|
187
|
+
const repair = response.repair;
|
|
188
|
+
if (repair && typeof repair === "object" && !Array.isArray(repair)) {
|
|
189
|
+
const requestId = repair.requestId;
|
|
190
|
+
const action = repair.action;
|
|
191
|
+
if (typeof requestId === "string" && action === "RESTART_AND_VERIFY")
|
|
192
|
+
await options.onRepairRequested?.({ requestId, action });
|
|
193
|
+
}
|
|
187
194
|
lastRemoteSuccessAt = new Date().toISOString();
|
|
188
195
|
lastRemoteError = undefined;
|
|
189
196
|
}
|
package/dist/onboard.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { DEFAULT_WITNORA_SERVER, saveConnection } from "./credentials.js";
|
|
5
6
|
import { authorizeProjectConnection } from "./device-authorization.js";
|
|
6
7
|
import { verifyControlPlaneConnection } from "./control-plane.js";
|
|
@@ -11,6 +12,7 @@ import { doctorCustomerGateway, activateManagedWorkflowHarness, initializeCustom
|
|
|
11
12
|
import { discoverRuntimeReferences, planRuntimeSandboxModules } from "./runtime-sandbox-kit.js";
|
|
12
13
|
import { automaticRuntimeReferencesUsable, bootstrapLocalRuntime, verifyAutomaticRuntimeProbe } from "./runtime-bootstrap.js";
|
|
13
14
|
import { activateRealPathIntegrations } from "./real-path-activation.js";
|
|
15
|
+
import { installCurrentGatewayService } from "./gateway-service.js";
|
|
14
16
|
export async function runOnboard(options) {
|
|
15
17
|
const requestFetch = options.fetch ?? fetch;
|
|
16
18
|
const output = options.output ?? ((message) => process.stdout.write(message));
|
|
@@ -91,6 +93,7 @@ export async function runOnboard(options) {
|
|
|
91
93
|
limitation: "No approved customer Harness module is bound to this Agent repository.",
|
|
92
94
|
};
|
|
93
95
|
let assuranceHarnessChanged = false;
|
|
96
|
+
let continuousService;
|
|
94
97
|
await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
|
|
95
98
|
try {
|
|
96
99
|
generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
|
|
@@ -176,6 +179,10 @@ export async function runOnboard(options) {
|
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
181
|
else {
|
|
182
|
+
const stopped = await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
|
|
183
|
+
if (stopped.state === "STOPPED" || stopped.state === "STALE") {
|
|
184
|
+
output(`\nStopped the managed Gateway for project ${binding.projectId} before rebinding this repository.\n`);
|
|
185
|
+
}
|
|
179
186
|
await assertGatewayStopped(requestFetch, binding.host, binding.port, binding.projectId, token.projectId);
|
|
180
187
|
gatewayMigration = await archiveGateway(gatewayState.directory, repositoryPath, binding.projectId);
|
|
181
188
|
output(`\nExisting Gateway belongs to project ${binding.projectId}; archived it at ${gatewayMigration.archiveDirectory}.\n`);
|
|
@@ -209,6 +216,10 @@ export async function runOnboard(options) {
|
|
|
209
216
|
if (stopped.state !== "STOPPED" && stopped.state !== "STALE")
|
|
210
217
|
throw new Error("The prior managed Gateway process did not stop before Assurance Harness activation.");
|
|
211
218
|
}
|
|
219
|
+
if (!options.gatewayLifecycle) {
|
|
220
|
+
const installed = await installCurrentGatewayService({ repository: repositoryPath, cliEntry: fileURLToPath(new URL("./cli.js", import.meta.url)) });
|
|
221
|
+
continuousService = { state: "INSTALLED", kind: installed.plan.kind, id: installed.plan.id };
|
|
222
|
+
}
|
|
212
223
|
managedGateway = await gatewayLifecycle.start({
|
|
213
224
|
repository: repositoryPath,
|
|
214
225
|
configHome: options.configHome,
|
|
@@ -233,6 +244,8 @@ export async function runOnboard(options) {
|
|
|
233
244
|
output(`Private discovery: ${discovery.capabilityCount} capability group(s); ${discovery.unknownCapabilityCount} pending confirmation.\n`);
|
|
234
245
|
output("Installed: customer-owned Gateway, default-deny policy, independent-probe contract, review contract, and PR/release/nightly CI.\n");
|
|
235
246
|
output(`Gateway: ${managedGateway.state} at ${managedGateway.baseUrl}${managedGateway.pid ? ` (process ${managedGateway.pid})` : ""}.\n`);
|
|
247
|
+
if (continuousService)
|
|
248
|
+
output(`Continuous operation: ${continuousService.kind} ${continuousService.id} will start at login and recover the Gateway after process failure.\n`);
|
|
236
249
|
output(runtimeReadiness.state === "LOCAL_SANDBOX_READY"
|
|
237
250
|
? "Runtime: LOCAL_SANDBOX_READY. This proves only the generated localhost sandbox loop is ready; it does not establish coverage, CURRENT, or a verified customer outcome.\n"
|
|
238
251
|
: `Runtime: RECORDED_ONLY. ${runtimeReadiness.limitations[0]}\n`);
|
|
@@ -243,7 +256,7 @@ export async function runOnboard(options) {
|
|
|
243
256
|
return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
|
|
244
257
|
repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
|
|
245
258
|
gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory,
|
|
246
|
-
gateway: managedGateway, runtimeReadiness, assuranceHarnessReadiness };
|
|
259
|
+
gateway: managedGateway, runtimeReadiness, assuranceHarnessReadiness, continuousService };
|
|
247
260
|
}
|
|
248
261
|
catch (error) {
|
|
249
262
|
const diagnosis = error instanceof Error ? error.message : String(error);
|
|
@@ -12,23 +12,21 @@ export async function activateRealPathIntegrations(options) {
|
|
|
12
12
|
throw new Error(`Could not load approved real-path integrations (${response.status}).`);
|
|
13
13
|
const body = await boundedJson(response);
|
|
14
14
|
const approved = Array.isArray(body.integrations) ? body.integrations.map(parsePlan).filter((plan) => plan.status === "READY_TO_ACTIVATE") : [];
|
|
15
|
-
const plans = approved.filter((plan) => plan.generated.providerPackId
|
|
15
|
+
const plans = approved.filter((plan) => ["STRIPE_REFUND", "ZENDESK_TICKET", "SALESFORCE_RECORD", "HUBSPOT_CRM_RECORD", "POSTGRES_RECORD"].includes(plan.generated.providerPackId) && plan.environment === "sandbox" && plan.customerSummary.evaluationMode === "SHADOW");
|
|
16
16
|
if (!plans.length)
|
|
17
17
|
return { state: "NOT_CONFIGURED", created: false, activations: [], generatedFiles: [] };
|
|
18
|
-
const
|
|
19
|
-
if (!secret?.startsWith("sk_test_") || secret.length < 12)
|
|
20
|
-
throw new Error("Stripe test-mode activation requires STRIPE_SECRET_KEY to reference an sk_test_ credential in the customer environment.");
|
|
18
|
+
const environment = options.env ?? process.env;
|
|
21
19
|
const activations = [];
|
|
22
20
|
const scenarios = [];
|
|
23
21
|
for (const plan of plans) {
|
|
24
|
-
const preflight = await
|
|
22
|
+
const preflight = await providerPreflight(request, environment, plan, options.now?.() ?? new Date(), options.postgresClientFactory);
|
|
25
23
|
const acceptance = preflight.acceptance;
|
|
26
24
|
activations.push({ integrationId: plan.id, integrationDigestSha256: plan.digestSha256, taskContractId: plan.taskContractId, taskContractDigestSha256: plan.taskContractDigestSha256, agentId: plan.subject.agentId, agentVersion: plan.subject.agentVersion, environment: plan.environment, providerPackId: plan.generated.providerPackId, providerContractDigestSha256: plan.generated.providerContractDigestSha256, acceptance });
|
|
27
25
|
scenarios.push({ plan, value: [preflight.scenario] });
|
|
28
26
|
}
|
|
29
27
|
const modulePath = join(repository, "witnora.assurance-harness.mjs");
|
|
30
28
|
const manifestPath = join(repository, ".witnora", "gateway", "real-path-activations.json");
|
|
31
|
-
const source =
|
|
29
|
+
const source = generatedProviderHarness(plans);
|
|
32
30
|
const moduleDigestSha256 = sha(source);
|
|
33
31
|
let created = false;
|
|
34
32
|
const generatedFiles = [];
|
|
@@ -65,6 +63,23 @@ export async function activateRealPathIntegrations(options) {
|
|
|
65
63
|
}
|
|
66
64
|
return { state: "READY_TO_START", created, modulePath: "witnora.assurance-harness.mjs", activations, generatedFiles };
|
|
67
65
|
}
|
|
66
|
+
async function providerPreflight(request, env, plan, now, postgresClientFactory) {
|
|
67
|
+
if (plan.generated.providerPackId === "STRIPE_REFUND") {
|
|
68
|
+
const secret = env.STRIPE_SECRET_KEY;
|
|
69
|
+
if (!secret?.startsWith("sk_test_") || secret.length < 12)
|
|
70
|
+
throw new Error("Stripe test-mode activation requires STRIPE_SECRET_KEY to reference an sk_test_ credential in the customer environment.");
|
|
71
|
+
return stripePreflight(request, secret, plan, now);
|
|
72
|
+
}
|
|
73
|
+
if (plan.generated.providerPackId === "ZENDESK_TICKET")
|
|
74
|
+
return zendeskPreflight(request, env, plan, now);
|
|
75
|
+
if (plan.generated.providerPackId === "SALESFORCE_RECORD")
|
|
76
|
+
return salesforcePreflight(request, env, plan, now);
|
|
77
|
+
if (plan.generated.providerPackId === "HUBSPOT_CRM_RECORD")
|
|
78
|
+
return hubspotPreflight(request, env, plan, now);
|
|
79
|
+
if (plan.generated.providerPackId === "POSTGRES_RECORD")
|
|
80
|
+
return postgresPreflight(env, plan, now, postgresClientFactory);
|
|
81
|
+
throw new Error(`${plan.generated.providerPackId} activation is not implemented by this CLI version.`);
|
|
82
|
+
}
|
|
68
83
|
async function stripePreflight(request, secret, plan, now) {
|
|
69
84
|
const response = await request("https://api.stripe.com/v1/refunds?limit=1", { headers: { authorization: `Bearer ${secret}` }, redirect: "error", signal: AbortSignal.timeout(5_000) });
|
|
70
85
|
if (!response.ok)
|
|
@@ -82,9 +97,113 @@ async function stripePreflight(request, secret, plan, now) {
|
|
|
82
97
|
const resultDigest = sha(canonical(observation));
|
|
83
98
|
return { acceptance: { kind: "READ_ONLY_PROVIDER_PREFLIGHT", passedAt: now.toISOString(), productionWrites: 0, observationDigestSha256: resultDigest }, scenario: { id: `stripe:${sha(first.id).slice(0, 16)}`, source: "LIVE_SHADOW", sanitized: true, input: { resourceId: first.id }, inputDigestSha256: sha(canonical({ resourceId: first.id })), baseline: { resultDigestSha256: resultDigest, actionIntents: plan.generated.actionPathBindings.map((item) => ({ pathId: item.actionPathId, parametersDigestSha256: parameterDigest })) } } };
|
|
84
99
|
}
|
|
85
|
-
function
|
|
86
|
-
const
|
|
87
|
-
|
|
100
|
+
async function zendeskPreflight(request, env, plan, now) {
|
|
101
|
+
const token = env.ZENDESK_API_TOKEN;
|
|
102
|
+
if (!token || token.length < 8)
|
|
103
|
+
throw new Error("Zendesk activation requires the read-only ZENDESK_API_TOKEN in the customer environment.");
|
|
104
|
+
const origin = providerOrigin(plan, "Zendesk", ".zendesk.com");
|
|
105
|
+
const response = await request(`${origin}/api/v2/tickets.json?per_page=1&sort_by=updated_at&sort_order=desc`, { method: "GET", headers: { accept: "application/json", authorization: `Bearer ${token}` }, redirect: "error", signal: AbortSignal.timeout(5_000) });
|
|
106
|
+
if (!response.ok)
|
|
107
|
+
throw new Error(`Zendesk read-only preflight failed (${response.status}).`);
|
|
108
|
+
const body = await boundedJson(response);
|
|
109
|
+
const tickets = Array.isArray(body.tickets) ? body.tickets : [];
|
|
110
|
+
const first = record(tickets[0], "Zendesk ticket");
|
|
111
|
+
const id = first.id;
|
|
112
|
+
if (typeof id !== "number" && typeof id !== "string")
|
|
113
|
+
throw new Error("Zendesk preflight requires one existing sandbox ticket.");
|
|
114
|
+
const observation = pick(first, ["status", "priority", "type", "via.channel"]);
|
|
115
|
+
assertCriterion(plan, observation);
|
|
116
|
+
return preflightResult(plan, now, "zendesk", id, observation);
|
|
117
|
+
}
|
|
118
|
+
async function salesforcePreflight(request, env, plan, now) {
|
|
119
|
+
const token = env.SALESFORCE_ACCESS_TOKEN;
|
|
120
|
+
if (!token || token.length < 8)
|
|
121
|
+
throw new Error("Salesforce activation requires the read-only SALESFORCE_ACCESS_TOKEN in the customer environment.");
|
|
122
|
+
const origin = providerOrigin(plan, "Salesforce", ".my.salesforce.com");
|
|
123
|
+
const resourceType = providerIdentifier(plan.generated.providerConfiguration?.resourceType, "Salesforce record type");
|
|
124
|
+
const field = providerIdentifier(plan.generated.criterion.field, "Salesforce criterion field");
|
|
125
|
+
const query = `SELECT Id,${field} FROM ${resourceType} ORDER BY LastModifiedDate DESC LIMIT 1`;
|
|
126
|
+
const response = await request(`${origin}/services/data/v61.0/query?${new URLSearchParams({ q: query })}`, { method: "GET", headers: { accept: "application/json", authorization: `Bearer ${token}` }, redirect: "error", signal: AbortSignal.timeout(5_000) });
|
|
127
|
+
if (!response.ok)
|
|
128
|
+
throw new Error(`Salesforce read-only preflight failed (${response.status}).`);
|
|
129
|
+
const body = await boundedJson(response);
|
|
130
|
+
const records = Array.isArray(body.records) ? body.records : [];
|
|
131
|
+
const first = record(records[0], "Salesforce record");
|
|
132
|
+
const id = first.Id;
|
|
133
|
+
if (typeof id !== "string")
|
|
134
|
+
throw new Error("Salesforce preflight requires one existing sandbox record.");
|
|
135
|
+
const observation = pick(first, [field]);
|
|
136
|
+
assertCriterion(plan, observation);
|
|
137
|
+
return preflightResult(plan, now, "salesforce", id, observation);
|
|
138
|
+
}
|
|
139
|
+
async function hubspotPreflight(request, env, plan, now) {
|
|
140
|
+
const token = env.HUBSPOT_ACCESS_TOKEN;
|
|
141
|
+
if (!token || token.length < 8)
|
|
142
|
+
throw new Error("HubSpot activation requires the read-only HUBSPOT_ACCESS_TOKEN in the customer environment.");
|
|
143
|
+
const resourceType = providerIdentifier(plan.generated.providerConfiguration?.resourceType, "HubSpot record type");
|
|
144
|
+
const field = providerIdentifier(plan.generated.criterion.field, "HubSpot criterion field");
|
|
145
|
+
const url = `https://api.hubapi.com/crm/v3/objects/${encodeURIComponent(resourceType)}?${new URLSearchParams({ limit: "1", properties: field })}`;
|
|
146
|
+
const response = await request(url, { method: "GET", headers: { accept: "application/json", authorization: `Bearer ${token}` }, redirect: "error", signal: AbortSignal.timeout(5_000) });
|
|
147
|
+
if (!response.ok)
|
|
148
|
+
throw new Error(`HubSpot read-only preflight failed (${response.status}).`);
|
|
149
|
+
const body = await boundedJson(response);
|
|
150
|
+
const results = Array.isArray(body.results) ? body.results : [];
|
|
151
|
+
const first = record(results[0], "HubSpot record");
|
|
152
|
+
const id = first.id;
|
|
153
|
+
if (typeof id !== "string")
|
|
154
|
+
throw new Error("HubSpot preflight requires one existing sandbox record.");
|
|
155
|
+
const properties = record(first.properties, "HubSpot properties");
|
|
156
|
+
const observation = pick(properties, [field]);
|
|
157
|
+
assertCriterion(plan, observation);
|
|
158
|
+
return preflightResult(plan, now, "hubspot", id, observation);
|
|
159
|
+
}
|
|
160
|
+
async function postgresPreflight(env, plan, now, clientFactory) {
|
|
161
|
+
const connectionString = env.WITNORA_POSTGRES_READ_URL;
|
|
162
|
+
if (!connectionString)
|
|
163
|
+
throw new Error("PostgreSQL activation requires WITNORA_POSTGRES_READ_URL in the customer environment.");
|
|
164
|
+
const view = providerIdentifier(plan.generated.providerConfiguration?.viewName, "PostgreSQL approved view");
|
|
165
|
+
const idColumn = providerIdentifier(plan.generated.providerConfiguration?.idColumn, "PostgreSQL record ID column");
|
|
166
|
+
const field = providerIdentifier(plan.generated.criterion.field, "PostgreSQL criterion field");
|
|
167
|
+
const client = clientFactory ? clientFactory(connectionString) : await defaultPostgresClient(connectionString);
|
|
168
|
+
await client.connect();
|
|
169
|
+
let first;
|
|
170
|
+
try {
|
|
171
|
+
await client.query("BEGIN READ ONLY");
|
|
172
|
+
const result = await client.query({ text: `SELECT "${idColumn}", "${field}" FROM "${view}" ORDER BY "${idColumn}" DESC LIMIT 1` });
|
|
173
|
+
first = result.rows[0];
|
|
174
|
+
await client.query("ROLLBACK");
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
await client.query("ROLLBACK").catch(() => undefined);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await client.end();
|
|
182
|
+
}
|
|
183
|
+
if (!first)
|
|
184
|
+
throw new Error("PostgreSQL preflight requires one existing record in the approved read-only view.");
|
|
185
|
+
const id = first[idColumn];
|
|
186
|
+
if (typeof id !== "string" && typeof id !== "number")
|
|
187
|
+
throw new Error("PostgreSQL preflight returned an invalid record ID.");
|
|
188
|
+
const observation = pick(first, [field]);
|
|
189
|
+
assertCriterion(plan, observation);
|
|
190
|
+
return preflightResult(plan, now, "postgres", id, observation);
|
|
191
|
+
}
|
|
192
|
+
async function defaultPostgresClient(connectionString) { const imported = await import("pg"); return new imported.Client({ connectionString, application_name: "witnora-read-only-preflight" }); }
|
|
193
|
+
function generatedProviderHarness(plans) {
|
|
194
|
+
const contracts = plans.map((plan) => ({ taskContractId: plan.taskContractId, providerPackId: plan.generated.providerPackId, providerConfiguration: plan.generated.providerConfiguration ?? {}, criterion: plan.generated.criterion, actionPathIds: plan.generated.actionPathBindings.map((item) => item.actionPathId) }));
|
|
195
|
+
return `import {createHash} from "node:crypto";
|
|
196
|
+
import {readFile} from "node:fs/promises";
|
|
197
|
+
import {join} from "node:path";
|
|
198
|
+
const contracts=${JSON.stringify(contracts)};
|
|
199
|
+
const sha=(value)=>createHash("sha256").update(typeof value==="string"?value:JSON.stringify(value)).digest("hex");
|
|
200
|
+
const request=async(url,key)=>{const response=await fetch(url,{method:"GET",headers:{accept:"application/json",authorization:"Bearer "+key},redirect:"error",signal:AbortSignal.timeout(5000)});if(!response.ok)throw new Error("Provider read-only observation failed ("+response.status+").");return response.json();};
|
|
201
|
+
const observe=async(contract,resourceId)=>{if(contract.providerPackId==="STRIPE_REFUND"){const key=process.env.STRIPE_SECRET_KEY;if(!key?.startsWith("sk_test_"))throw new Error("Stripe test-mode credential is unavailable.");return request("https://api.stripe.com/v1/refunds/"+encodeURIComponent(resourceId),key);}if(contract.providerPackId==="ZENDESK_TICKET"){const key=process.env.ZENDESK_API_TOKEN;if(!key)throw new Error("Zendesk read-only credential is unavailable.");return (await request(contract.providerConfiguration.origin+"/api/v2/tickets/"+encodeURIComponent(resourceId)+".json",key)).ticket;}if(contract.providerPackId==="SALESFORCE_RECORD"){const key=process.env.SALESFORCE_ACCESS_TOKEN;if(!key)throw new Error("Salesforce read-only credential is unavailable.");return request(contract.providerConfiguration.origin+"/services/data/v61.0/sobjects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId),key);}if(contract.providerPackId==="HUBSPOT_CRM_RECORD"){const key=process.env.HUBSPOT_ACCESS_TOKEN;if(!key)throw new Error("HubSpot read-only credential is unavailable.");return (await request("https://api.hubapi.com/crm/v3/objects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId)+"?properties="+encodeURIComponent(contract.criterion.field),key)).properties;}if(contract.providerPackId==="POSTGRES_RECORD"){const url=process.env.WITNORA_POSTGRES_READ_URL;if(!url)throw new Error("PostgreSQL read-only credential is unavailable.");for(const name of [contract.providerConfiguration.viewName,contract.providerConfiguration.idColumn,contract.criterion.field])if(!/^[A-Za-z][A-Za-z0-9_]{0,99}$/.test(name))throw new Error("PostgreSQL identifier is invalid.");const {Client}=await import("pg");const client=new Client({connectionString:url,application_name:"witnora-read-only-probe"});await client.connect();try{await client.query("BEGIN READ ONLY");const result=await client.query({name:"witnora-provider-observe",text:'SELECT "'+contract.criterion.field+'" FROM "'+contract.providerConfiguration.viewName+'" WHERE "'+contract.providerConfiguration.idColumn+'" = $1 LIMIT 1',values:[resourceId]});await client.query("ROLLBACK");return result.rows[0]??{};}catch(error){await client.query("ROLLBACK").catch(()=>{});throw error;}finally{await client.end();}}throw new Error("Provider Harness contract is unsupported.");};
|
|
202
|
+
export function createWitnoraBusinessTaskEvaluatorOptions(context){return {
|
|
203
|
+
loadShadowObservations:async(task)=>{const contract=contracts.find((item)=>item.taskContractId===task.id);if(!contract)throw new Error("No exact real-path contract.");const path=join(context.repository,".witnora","scenarios",task.id+".json");const value=JSON.parse(await readFile(path,"utf8"));if(!Array.isArray(value)||value.length>100)throw new Error("Local scenario file is invalid.");return value;},
|
|
204
|
+
evaluateShadowCandidate:async(candidate,task)=>{const contract=contracts.find((item)=>item.taskContractId===task.id);if(!contract)throw new Error("No exact real-path contract.");const resourceId=String(candidate.input?.resourceId??"");if(!/^[A-Za-z0-9._:-]{1,200}$/.test(resourceId))throw new Error("Provider resourceId is invalid.");for(const pathId of contract.actionPathIds)candidate.propose({pathId,parametersDigestSha256:sha(resourceId)});const raw=await observe(contract,resourceId);const actual=raw?.[contract.criterion.field];const observed=actual===undefined?{}:{[contract.criterion.field]:actual};return {resultDigestSha256:sha(observed),criteria:[{id:contract.criterion.id,passed:actual===contract.criterion.expected}]};}
|
|
205
|
+
};}
|
|
206
|
+
`;
|
|
88
207
|
}
|
|
89
208
|
function parsePlan(value) { if (!value || typeof value !== "object" || Array.isArray(value))
|
|
90
209
|
throw new Error("Real-path integration response is invalid."); const plan = value; if (plan.schemaVersion !== "witnora.real_path_integration.v0.1" || !plan.id || !plan.taskContractId || !plan.subject?.agentId || !plan.subject.agentVersion || !plan.generated || plan.generated.boundaries?.rawPayloadUpload !== false || plan.generated.boundaries.rawCredentialUpload !== false || plan.generated.boundaries.evaluatorWrites !== false || plan.generated.boundaries.firstAcceptanceProductionWrites !== 0 || !digest(plan.digestSha256) || !digest(plan.taskContractDigestSha256) || !digest(plan.generated.providerContractDigestSha256))
|
|
@@ -93,6 +212,23 @@ async function boundedJson(response) { const text = await response.text(); if (t
|
|
|
93
212
|
throw new Error("Real-path response exceeded the size limit."); const value = JSON.parse(text); if (!value || typeof value !== "object" || Array.isArray(value))
|
|
94
213
|
throw new Error("Real-path response is invalid."); return value; }
|
|
95
214
|
function scalar(value) { return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean"; }
|
|
215
|
+
function providerOrigin(plan, provider, suffix) { const value = plan.generated.providerConfiguration?.origin; try {
|
|
216
|
+
const url = new URL(value ?? "");
|
|
217
|
+
if (url.protocol !== "https:" || !url.hostname.endsWith(suffix) || url.origin !== (value ?? "").replace(/\/$/, ""))
|
|
218
|
+
throw new Error();
|
|
219
|
+
return url.origin;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
throw new Error(`${provider} integration has an invalid trusted origin.`);
|
|
223
|
+
} }
|
|
224
|
+
function providerIdentifier(value, field) { if (!value || !/^[A-Za-z][A-Za-z0-9_]{0,99}$/.test(value))
|
|
225
|
+
throw new Error(`${field} is invalid.`); return value; }
|
|
226
|
+
function record(value, field) { if (!value || typeof value !== "object" || Array.isArray(value))
|
|
227
|
+
throw new Error(`${field} response is invalid.`); return value; }
|
|
228
|
+
function pick(value, fields) { return Object.fromEntries(fields.flatMap((field) => { const selected = field.split(".").reduce((current, key) => current && typeof current === "object" ? current[key] : undefined, value); return scalar(selected) ? [[field, selected]] : []; })); }
|
|
229
|
+
function assertCriterion(plan, observation) { if (observation[plan.generated.criterion.field] !== plan.generated.criterion.expected)
|
|
230
|
+
throw new Error("The provider sandbox record does not satisfy the approved business success definition."); }
|
|
231
|
+
function preflightResult(plan, now, provider, resourceId, observation) { const resultDigest = sha(canonical(observation)); return { acceptance: { kind: "READ_ONLY_PROVIDER_PREFLIGHT", passedAt: now.toISOString(), productionWrites: 0, observationDigestSha256: resultDigest }, scenario: { id: `${provider}:${sha(String(resourceId)).slice(0, 16)}`, source: "LIVE_SHADOW", sanitized: true, input: { resourceId }, inputDigestSha256: sha(canonical({ resourceId })), baseline: { resultDigestSha256: resultDigest, actionIntents: plan.generated.actionPathBindings.map((item) => ({ pathId: item.actionPathId, parametersDigestSha256: sha(String(resourceId)) })) } } }; }
|
|
96
232
|
function digest(value) { return typeof value === "string" && /^[a-f0-9]{64}$/.test(value); }
|
|
97
233
|
function canonical(value) { if (value === null || typeof value !== "object")
|
|
98
234
|
return JSON.stringify(value); if (Array.isArray(value))
|
|
@@ -49,8 +49,11 @@ export async function bootstrapLocalRuntime(input) {
|
|
|
49
49
|
body: JSON.stringify({ ...contract, contractFingerprintSha256 }),
|
|
50
50
|
});
|
|
51
51
|
const body = await response.json().catch(() => ({}));
|
|
52
|
-
if (!response.ok)
|
|
53
|
-
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const detail = typeof body.error === "string" ? body.error : typeof body.message === "string" ? body.message : undefined;
|
|
54
|
+
const code = typeof body.code === "string" ? body.code : undefined;
|
|
55
|
+
throw new Error(`Hosted Runtime bootstrap did not complete (HTTP ${response.status})${code ? ` [${code}]` : ""}${detail ? `: ${detail.slice(0, 300)}` : "."}`);
|
|
56
|
+
}
|
|
54
57
|
const issued = validateResponse(body, contractFingerprintSha256, attempt.deliveryPublicKeyPem);
|
|
55
58
|
const probeCredentialPath = join(directory, "outcome-probe-api-key.txt");
|
|
56
59
|
const probeCredentialMetadataPath = join(directory, "outcome-probe-api-key.json");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-integration-packs.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/provider-integration-packs.ts"],"names":[],"mappings":"AAEA,OAAO,EASL,KAAK,sCAAsC,EAC3C,KAAK,gCAAgC,EACrC,KAAK,8BAA8B,EACnC,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,EACjC,MAAM,+BAA+B,CAAC;AAGvC,eAAO,MAAM,wCAAwC,EAAG,wCAAiD,CAAC;AAE1G,MAAM,MAAM,yBAAyB,GACjC,eAAe,GAAG,mBAAmB,GAAG,oBAAoB,GAAG,gBAAgB,GAAG,iBAAiB,GACnG,iBAAiB,GAAG,cAAc,GAAG,kBAAkB,GAAG,WAAW,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;AAElH,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,OAAO,wCAAwC,CAAC;IAC/D,EAAE,EAAE,yBAAyB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,2BAA2B,CAAC;IACtC,YAAY,EAAE,KAAK,CAAC,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC,CAAC;IAC1D,UAAU,EAAE;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,cAAc,GAAG,iBAAiB,GAAG,eAAe,CAAA;KAAE,CAAC;IAClJ,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC1H,QAAQ,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,OAAO,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IACpF,UAAU,EAAE;QAAE,MAAM,EAAE,KAAK,CAAC;QAAC,SAAS,EAAE,KAAK,CAAC;QAAC,gBAAgB,EAAE,KAAK,CAAC;QAAC,mBAAmB,EAAE,KAAK,CAAA;KAAE,CAAC;IACrG,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,OAAO,CAAC,yBAAyB,EAAE,iBAAiB,GAAG,cAAc,GAAG,kBAAkB,CAAC,CAAC;IACpG,WAAW,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,8BAA8B,CAAC,mBAAmB,CAAC,CAAC;IACpJ,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"provider-integration-packs.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/provider-integration-packs.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,7 +26,8 @@ export function createProviderPackEvaluator(options) {
|
|
|
26
26
|
const spec = getProviderIntegrationPack(options.packId);
|
|
27
27
|
if (!spec.environments.includes(options.environment))
|
|
28
28
|
throw new Error("Provider pack does not support this environment.");
|
|
29
|
-
const
|
|
29
|
+
const selectFields = selector(spec.fieldAllowlist);
|
|
30
|
+
const select = (value) => selectFields(providerPayload(options.packId, value));
|
|
30
31
|
const common = { provider: spec.provider, environment: options.environment, credentialHandle: options.credentialHandle, resolveCredential: options.resolveCredential, select, timeoutMs: options.timeoutMs, now: options.now };
|
|
31
32
|
if (options.packId === "POSTGRES_RECORD" || options.packId === "MYSQL_RECORD")
|
|
32
33
|
return createDatabaseReadOnlyEvaluator({ ...common, statementId: options.statementId, executePrepared: options.executePrepared });
|
|
@@ -36,7 +37,8 @@ export function createProviderPackEvaluator(options) {
|
|
|
36
37
|
const origin = http.allowedOrigin ?? spec.endpoint.origin;
|
|
37
38
|
if (!origin)
|
|
38
39
|
throw new Error("Provider pack requires the customer-specific HTTPS origin discovered during local setup.");
|
|
39
|
-
|
|
40
|
+
validateProviderOrigin(options.packId, origin);
|
|
41
|
+
const resourcePath = resourcePathBuilder(options.packId, spec.endpoint.resourcePattern, providerResourceType(options.packId, http.resourceType), spec.fieldAllowlist);
|
|
40
42
|
const input = { ...common, allowedOrigin: origin, resourcePath, fetch: http.fetch };
|
|
41
43
|
switch (spec.template) {
|
|
42
44
|
case "CRM_RECORD_STATE": return createCrmRecordStateEvaluator(input);
|
|
@@ -71,6 +73,38 @@ function criterion(id, question, field, suggestedValues) { return { id, question
|
|
|
71
73
|
function selector(fields) {
|
|
72
74
|
return (value) => Object.fromEntries(fields.flatMap((field) => { const selected = field.split(".").reduce((current, key) => current && typeof current === "object" ? current[key] : undefined, value); return selected === undefined ? [] : [[field, selected]]; }));
|
|
73
75
|
}
|
|
74
|
-
function resourcePathBuilder(pattern) {
|
|
75
|
-
return (resourceId) =>
|
|
76
|
+
function resourcePathBuilder(packId, pattern, resourceType, fields) {
|
|
77
|
+
return (resourceId) => {
|
|
78
|
+
const path = pattern.replace("{resourceId}", encodeURIComponent(resourceId)).replace("{object}", encodeURIComponent(resourceType));
|
|
79
|
+
return packId === "HUBSPOT_CRM_RECORD" ? `${path}?${new URLSearchParams({ properties: fields.join(",") })}` : path;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function providerPayload(packId, value) {
|
|
83
|
+
if (packId === "ZENDESK_TICKET")
|
|
84
|
+
return nestedRecord(value, "ticket", "Zendesk ticket response");
|
|
85
|
+
if (packId === "HUBSPOT_CRM_RECORD")
|
|
86
|
+
return nestedRecord(value, "properties", "HubSpot record response");
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function validateProviderOrigin(packId, value) {
|
|
90
|
+
const url = new URL(value);
|
|
91
|
+
if (packId === "ZENDESK_TICKET" && (url.protocol !== "https:" || !url.hostname.endsWith(".zendesk.com")))
|
|
92
|
+
throw new Error("Zendesk origin must be an HTTPS customer subdomain of zendesk.com.");
|
|
93
|
+
if (packId === "SALESFORCE_RECORD" && (url.protocol !== "https:" || !url.hostname.endsWith(".my.salesforce.com")))
|
|
94
|
+
throw new Error("Salesforce origin must be the customer's HTTPS my.salesforce.com domain.");
|
|
95
|
+
if (packId === "HUBSPOT_CRM_RECORD" && (url.protocol !== "https:" || url.hostname !== "api.hubapi.com"))
|
|
96
|
+
throw new Error("HubSpot origin must be the fixed HTTPS api.hubapi.com endpoint.");
|
|
97
|
+
}
|
|
98
|
+
function providerResourceType(packId, value) {
|
|
99
|
+
if (packId !== "SALESFORCE_RECORD" && packId !== "HUBSPOT_CRM_RECORD")
|
|
100
|
+
return "records";
|
|
101
|
+
if (!value || !/^[A-Za-z][A-Za-z0-9_]{0,99}$/.test(value))
|
|
102
|
+
throw new Error(`${packId === "SALESFORCE_RECORD" ? "Salesforce" : "HubSpot"} resource type is invalid.`);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
function nestedRecord(value, key, field) {
|
|
106
|
+
const nested = value[key];
|
|
107
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested))
|
|
108
|
+
throw new Error(`${field} is missing.`);
|
|
109
|
+
return nested;
|
|
76
110
|
}
|