witnora 0.13.2 → 0.13.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/README.md +1 -1
- package/dist/conformance.js +1 -1
- package/dist/control-plane.js +4 -4
- package/dist/gateway.js +221 -15
- package/dist/internal/control-client/canonical.d.ts +2 -0
- package/dist/internal/control-client/canonical.d.ts.map +1 -0
- package/dist/internal/control-client/canonical.js +19 -0
- package/dist/internal/control-client/collector-gateway.d.ts +64 -0
- package/dist/internal/control-client/collector-gateway.d.ts.map +1 -0
- package/dist/internal/control-client/collector-gateway.js +392 -0
- package/dist/internal/control-client/durable-action-worker.d.ts +194 -0
- package/dist/internal/control-client/durable-action-worker.d.ts.map +1 -0
- package/dist/internal/control-client/durable-action-worker.js +359 -0
- package/dist/internal/control-client/remote-collector.d.ts +132 -0
- package/dist/internal/control-client/remote-collector.d.ts.map +1 -0
- package/dist/internal/control-client/remote-collector.js +318 -0
- package/dist/onboarding-templates.js +2 -2
- package/dist/probe-process.js +135 -0
- package/package.json +3 -6
- /package/dist/{vendor/agentcert-sdk → internal/control-client}/deployment-enforcement.d.ts +0 -0
- /package/dist/{vendor/agentcert-sdk → internal/control-client}/deployment-enforcement.d.ts.map +0 -0
- /package/dist/{vendor/agentcert-sdk → internal/control-client}/deployment-enforcement.js +0 -0
package/README.md
CHANGED
|
@@ -353,7 +353,7 @@ Control semantics and attestation format:
|
|
|
353
353
|
[release gate checklist](https://github.com/Kakarottoooo/agentcert/blob/main/docs/release-gate-checklist.md).
|
|
354
354
|
|
|
355
355
|
CI users can run Tripwire and Witnora together with
|
|
356
|
-
`Kakarottoooo/
|
|
356
|
+
`Kakarottoooo/witnora/actions/tripwire@v0`.
|
|
357
357
|
|
|
358
358
|
The public Real Agent Robustness Lab compares browser-use, Stagehand, and
|
|
359
359
|
Playwright-based agents over the same fault suite:
|
package/dist/conformance.js
CHANGED
|
@@ -10,7 +10,7 @@ const SUPPORTED_TOP_LEVEL_FIELDS = new Set([
|
|
|
10
10
|
export async function runEvidenceConformance(input, options) {
|
|
11
11
|
const checks = [];
|
|
12
12
|
const schema = validateAgentCertSchema("evidence-bundle", input);
|
|
13
|
-
checks.push(check("schema", "
|
|
13
|
+
checks.push(check("schema", "Witnora evidence satisfies the stable agentcert v0.1 protocol contract.", schema.errors));
|
|
14
14
|
const bundle = object(input);
|
|
15
15
|
const compatibilityErrors = bundle
|
|
16
16
|
? Object.keys(bundle).filter((key) => !SUPPORTED_TOP_LEVEL_FIELDS.has(key)).map((key) => `Unsupported top-level field: ${key}.`)
|
package/dist/control-plane.js
CHANGED
|
@@ -397,7 +397,7 @@ async function requestJson(request, url, init) {
|
|
|
397
397
|
}
|
|
398
398
|
catch (error) {
|
|
399
399
|
const message = error instanceof Error ? error.message : String(error);
|
|
400
|
-
throw new ControlPlaneRequestError(`
|
|
400
|
+
throw new ControlPlaneRequestError(`Witnora control plane request failed: ${message}`);
|
|
401
401
|
}
|
|
402
402
|
const text = await response.text();
|
|
403
403
|
let value = {};
|
|
@@ -407,12 +407,12 @@ async function requestJson(request, url, init) {
|
|
|
407
407
|
}
|
|
408
408
|
catch {
|
|
409
409
|
if (!response.ok)
|
|
410
|
-
throw new ControlPlaneRequestError(`
|
|
411
|
-
throw new ControlPlaneRequestError("
|
|
410
|
+
throw new ControlPlaneRequestError(`Witnora control plane returned HTTP ${response.status}.`, response.status);
|
|
411
|
+
throw new ControlPlaneRequestError("Witnora control plane returned invalid JSON.", response.status);
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
if (!response.ok) {
|
|
415
|
-
throw new ControlPlaneRequestError([typeof value.error === "string" ? value.error : `
|
|
415
|
+
throw new ControlPlaneRequestError([typeof value.error === "string" ? value.error : `Witnora control plane returned HTTP ${response.status}.`,
|
|
416
416
|
typeof value.recovery === "string" ? value.recovery : undefined,
|
|
417
417
|
typeof value.requestId === "string" ? `Request ID: ${value.requestId}.` : undefined].filter(Boolean).join(" "), response.status, typeof value.code === "string" ? value.code : undefined, typeof value.requestId === "string" ? value.requestId : response.headers.get("x-request-id") ?? undefined, typeof value.recovery === "string" ? value.recovery : undefined);
|
|
418
418
|
}
|
package/dist/gateway.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { closeSync, openSync } from "node:fs";
|
|
4
4
|
import { access, chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { loadConnection } from "./credentials.js";
|
|
8
8
|
import { authorizeProjectConnection } from "./device-authorization.js";
|
|
9
|
+
import { inspectIsolatedOutcomeProbe, observeInIsolatedOutcomeProbe } from "./probe-process.js";
|
|
9
10
|
const CONFIG_SCHEMA = "witnora.customer_gateway_setup.v0.1";
|
|
10
11
|
const SECRETS_SCHEMA = "witnora.customer_gateway_local_secrets.v0.1";
|
|
11
12
|
const RUNTIME_SCHEMA = "witnora.managed_gateway_runtime.v0.1";
|
|
@@ -36,7 +37,7 @@ export async function initializeCustomerGateway(options) {
|
|
|
36
37
|
output,
|
|
37
38
|
configHome: options.configHome,
|
|
38
39
|
});
|
|
39
|
-
const expectedScopes = ["runs:read", "events:write", "collector:manage"];
|
|
40
|
+
const expectedScopes = ["runs:read", "events:write", "collector:manage", "actions:read", "actions:propose", "actions:execute"];
|
|
40
41
|
const missingScopes = expectedScopes.filter((scope) => !authorization.scopes.includes(scope));
|
|
41
42
|
if (missingScopes.length > 0)
|
|
42
43
|
throw new Error(`Gateway authorization is missing required scope(s): ${missingScopes.join(", ")}.`);
|
|
@@ -132,31 +133,67 @@ export async function doctorCustomerGateway(options) {
|
|
|
132
133
|
catch (error) {
|
|
133
134
|
checks.push({ id: "hosted_credential", status: "FAIL", message: message(error) });
|
|
134
135
|
}
|
|
136
|
+
if (config.runtimeWorker) {
|
|
137
|
+
try {
|
|
138
|
+
const adapterModulePath = resolve(directory, config.runtimeWorker.adapterModulePath);
|
|
139
|
+
const probeModulePath = resolve(directory, config.runtimeWorker.probeModulePath);
|
|
140
|
+
if (adapterModulePath === probeModulePath)
|
|
141
|
+
throw new Error("Adapter and outcome probe must use separate modules.");
|
|
142
|
+
const [adapterDigest, probeDigest] = await Promise.all([
|
|
143
|
+
readFile(adapterModulePath).then((bytes) => createHash("sha256").update(bytes).digest("hex")),
|
|
144
|
+
readFile(probeModulePath).then((bytes) => createHash("sha256").update(bytes).digest("hex")),
|
|
145
|
+
]);
|
|
146
|
+
if (adapterDigest !== config.runtimeWorker.adapterModuleSha256 || probeDigest !== config.runtimeWorker.probeModuleSha256)
|
|
147
|
+
throw new Error("Runtime adapter or probe module digest does not match gateway.json.");
|
|
148
|
+
const [primary, probe] = await Promise.all([
|
|
149
|
+
loadConnection(config.connectionName, { configHome: options.configHome }),
|
|
150
|
+
loadConnection(config.runtimeWorker.probeConnectionName, { configHome: options.configHome }),
|
|
151
|
+
]);
|
|
152
|
+
if (!primary || !probe || probe.projectId !== config.projectId || probe.server !== config.server || probe.apiKey === primary.apiKey) {
|
|
153
|
+
throw new Error("A separate outcome-probe credential bound to this project is required.");
|
|
154
|
+
}
|
|
155
|
+
checks.push({ id: "runtime_configuration", status: "PASS", message: "The digest-pinned adapter module and separate outcome-probe credential are present." });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
checks.push({ id: "runtime_configuration", status: "FAIL", message: message(error) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
135
161
|
try {
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
138
|
-
throw new Error(
|
|
162
|
+
const health = await gatewayHealth(`http://${config.host}:${config.port}`, options.fetch ?? fetch, Boolean(config.runtimeWorker));
|
|
163
|
+
if (!health)
|
|
164
|
+
throw new Error("Gateway health or runtime-worker readiness was not established.");
|
|
139
165
|
checks.push({ id: "process", status: "PASS", message: `Gateway is listening at http://${config.host}:${config.port}.` });
|
|
140
166
|
}
|
|
141
167
|
catch {
|
|
142
168
|
checks.push({ id: "process", status: "WARN", message: "Gateway is not running yet. Start managed mode with `witnora gateway start`." });
|
|
143
169
|
}
|
|
144
170
|
}
|
|
171
|
+
const runtimeConfigurationReady = checks.some((check) => check.id === "runtime_configuration" && check.status === "PASS")
|
|
172
|
+
&& checks.some((check) => check.id === "process" && check.status === "PASS");
|
|
145
173
|
if (config && secrets) {
|
|
146
|
-
checks.push(
|
|
147
|
-
|
|
174
|
+
checks.push(config.runtimeWorker && runtimeConfigurationReady
|
|
175
|
+
? { id: "enforcement", status: "PASS", message: `Exact adapter ${config.runtimeWorker.adapterId}@${config.runtimeWorker.adapterVersion} is configured behind the durable worker.` }
|
|
176
|
+
: { id: "enforcement", status: "WARN", message: "Exact write-credential mediation is not proven ready; current evidence ceiling remains RECORDED." });
|
|
177
|
+
checks.push(config.runtimeWorker && runtimeConfigurationReady
|
|
178
|
+
? { id: "outcome_probe", status: "PASS", message: `Separate read-only probe ${config.runtimeWorker.probeId} is configured.` }
|
|
179
|
+
: { id: "outcome_probe", status: "WARN", message: "An independent digest-pinned probe and read-only target credential are not proven ready." });
|
|
148
180
|
}
|
|
149
181
|
const failed = checks.some((check) => check.status === "FAIL");
|
|
150
182
|
return {
|
|
151
183
|
schemaVersion: "witnora.customer_gateway_doctor.v0.1",
|
|
152
|
-
overall: failed ? "SETUP_INCOMPLETE" : "READY_TO_RECORD",
|
|
184
|
+
overall: failed ? "SETUP_INCOMPLETE" : runtimeConfigurationReady ? "READY_FOR_RUNTIME" : "READY_TO_RECORD",
|
|
153
185
|
checks,
|
|
154
|
-
evidenceCeiling: "recorded",
|
|
155
|
-
nextAction: failed ? "Run
|
|
186
|
+
evidenceCeiling: runtimeConfigurationReady ? "outcome_verified" : "recorded",
|
|
187
|
+
nextAction: failed ? "Resolve the failed Gateway checks before sending another action." : runtimeConfigurationReady ? "Run the Agent normally; approved configured actions continue automatically after the human decision." : "Ensure the managed Gateway is healthy, then send one sandbox run through its local event API.",
|
|
156
188
|
};
|
|
157
189
|
}
|
|
158
190
|
export async function runCustomerGateway(options) {
|
|
159
|
-
const
|
|
191
|
+
const remoteCollector = await import(new URL("./internal/control-client/remote-collector.js", import.meta.url).href);
|
|
192
|
+
const collectorGateway = await import(new URL("./internal/control-client/collector-gateway.js", import.meta.url).href);
|
|
193
|
+
const CustomerSourceKeyRing = remoteCollector.CustomerSourceKeyRing;
|
|
194
|
+
const RemoteCollectorClient = remoteCollector.RemoteCollectorClient;
|
|
195
|
+
const startCustomerOwnedCollectorGateway = collectorGateway.startCustomerOwnedCollectorGateway;
|
|
196
|
+
const durableWorker = configRuntimeWorkerImport();
|
|
160
197
|
const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
|
|
161
198
|
const config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
|
|
162
199
|
const secrets = parseSecrets(await readFile(join(directory, "secrets.json"), "utf8"));
|
|
@@ -169,6 +206,14 @@ export async function runCustomerGateway(options) {
|
|
|
169
206
|
const keyRing = await (await exists(keyRingPath)
|
|
170
207
|
? CustomerSourceKeyRing.open(keyRingPath)
|
|
171
208
|
: CustomerSourceKeyRing.create(keyRingPath, config.collectorId));
|
|
209
|
+
const actionWorker = config.runtimeWorker
|
|
210
|
+
? await createConfiguredRuntimeActionWorker({
|
|
211
|
+
directory, config, connection, configHome: options.configHome,
|
|
212
|
+
DurableApprovedActionWorker: (await durableWorker).DurableApprovedActionWorker,
|
|
213
|
+
FileActionCheckpointStore: (await durableWorker).FileActionCheckpointStore,
|
|
214
|
+
})
|
|
215
|
+
: undefined;
|
|
216
|
+
actionWorker?.start();
|
|
172
217
|
const gateway = await startCustomerOwnedCollectorGateway({
|
|
173
218
|
client: new RemoteCollectorClient({ baseUrl: connection.server, projectId: connection.projectId, apiKey: connection.apiKey }),
|
|
174
219
|
keyRing,
|
|
@@ -177,13 +222,143 @@ export async function runCustomerGateway(options) {
|
|
|
177
222
|
host: process.env.WITNORA_GATEWAY_HOST?.trim() || config.host,
|
|
178
223
|
port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, config.port),
|
|
179
224
|
environment: "customer-owned",
|
|
225
|
+
...(actionWorker && config.runtimeWorker ? { actionWorker: {
|
|
226
|
+
track: (input) => isConfiguredRuntimeProposal(input.proposal, config.runtimeWorker) ? actionWorker.track(input) : Promise.resolve(undefined),
|
|
227
|
+
status: () => actionWorker.status(), close: () => actionWorker.stop(),
|
|
228
|
+
} } : {}),
|
|
180
229
|
});
|
|
181
230
|
process.stdout.write(`Witnora customer-owned Gateway listening on ${gateway.baseUrl}\n`);
|
|
182
|
-
process.stdout.write(
|
|
231
|
+
process.stdout.write(config.runtimeWorker
|
|
232
|
+
? "Runtime worker: READY. Approved exact configured actions execute automatically, then use the separate read-only probe and Hosted signed receipt.\n"
|
|
233
|
+
: "Evidence ceiling: RECORDED. No exact target adapter and separate outcome probe are configured, so runtime writes remain fail-closed.\n");
|
|
183
234
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
184
235
|
process.once(signal, () => void gateway.close().finally(() => process.exit(0)));
|
|
185
236
|
}
|
|
186
237
|
}
|
|
238
|
+
export async function createConfiguredRuntimeActionWorker(input) {
|
|
239
|
+
const workerConfig = input.config.runtimeWorker;
|
|
240
|
+
if (!workerConfig?.enabled)
|
|
241
|
+
throw new Error("Runtime worker is not enabled.");
|
|
242
|
+
const adapterModulePath = resolve(input.directory, workerConfig.adapterModulePath);
|
|
243
|
+
const probeModulePath = resolve(input.directory, workerConfig.probeModulePath);
|
|
244
|
+
if (adapterModulePath === probeModulePath)
|
|
245
|
+
throw new Error("Adapter and outcome probe must use separate modules.");
|
|
246
|
+
const [adapterBytes, probeBytes] = await Promise.all([readFile(adapterModulePath), readFile(probeModulePath)]);
|
|
247
|
+
const adapterModuleDigest = createHash("sha256").update(adapterBytes).digest("hex");
|
|
248
|
+
const probeModuleDigest = createHash("sha256").update(probeBytes).digest("hex");
|
|
249
|
+
if (adapterModuleDigest !== workerConfig.adapterModuleSha256 || probeModuleDigest !== workerConfig.probeModuleSha256)
|
|
250
|
+
throw new Error("Runtime adapter or probe module digest does not match gateway.json; refusing to load it.");
|
|
251
|
+
const adapterModule = await import(`${pathToFileURL(adapterModulePath).href}?sha256=${adapterModuleDigest}`);
|
|
252
|
+
if (typeof adapterModule.createWitnoraRuntimeAdapter !== "function")
|
|
253
|
+
throw new Error("Runtime adapter module must export its factory.");
|
|
254
|
+
const probeConnection = await loadConnection(workerConfig.probeConnectionName, { configHome: input.configHome });
|
|
255
|
+
if (!probeConnection || probeConnection.projectId !== input.config.projectId || probeConnection.server !== input.config.server) {
|
|
256
|
+
throw new Error("The separate outcome-probe credential does not match this Gateway project and server.");
|
|
257
|
+
}
|
|
258
|
+
if (probeConnection.apiKey === input.connection.apiKey)
|
|
259
|
+
throw new Error("The execution Gateway and outcome probe must use separate Hosted credentials.");
|
|
260
|
+
const adapterContext = Object.freeze({
|
|
261
|
+
adapterId: workerConfig.adapterId, adapterVersion: workerConfig.adapterVersion,
|
|
262
|
+
projectId: input.config.projectId,
|
|
263
|
+
hosted: runtimeHostedTransport(input.connection, input.fetch ?? fetch),
|
|
264
|
+
storageDirectory: resolve(input.directory, "data", "runtime-actions"),
|
|
265
|
+
});
|
|
266
|
+
const isolatedProbeConfig = {
|
|
267
|
+
modulePath: probeModulePath,
|
|
268
|
+
moduleSha256: probeModuleDigest,
|
|
269
|
+
probeId: workerConfig.probeId,
|
|
270
|
+
projectId: input.config.projectId,
|
|
271
|
+
credentialHandle: workerConfig.probeTargetCredentialHandle,
|
|
272
|
+
};
|
|
273
|
+
const runtime = await adapterModule.createWitnoraRuntimeAdapter(adapterContext);
|
|
274
|
+
await inspectIsolatedOutcomeProbe(isolatedProbeConfig);
|
|
275
|
+
if (runtime.id !== workerConfig.adapterId || runtime.version !== workerConfig.adapterVersion || runtime.reconcileReadOnly !== true
|
|
276
|
+
|| typeof runtime.prepareClaim !== "function" || typeof runtime.execute !== "function" || typeof runtime.reconcile !== "function") {
|
|
277
|
+
throw new Error("Runtime adapter does not match the exact configured id/version or read-only reconciliation contract.");
|
|
278
|
+
}
|
|
279
|
+
const requestFetch = input.fetch ?? fetch;
|
|
280
|
+
const primary = projectTransport(input.connection, requestFetch);
|
|
281
|
+
const verifier = projectTransport(probeConnection, requestFetch);
|
|
282
|
+
return new input.DurableApprovedActionWorker({
|
|
283
|
+
config: {
|
|
284
|
+
adapterId: workerConfig.adapterId, adapterVersion: workerConfig.adapterVersion, probeId: workerConfig.probeId,
|
|
285
|
+
probeCredentialHandle: workerConfig.probeTargetCredentialHandle,
|
|
286
|
+
runtimeIdentityId: workerConfig.runtimeIdentityId, grantTtlSeconds: workerConfig.grantTtlSeconds, pollIntervalMs: workerConfig.pollIntervalMs,
|
|
287
|
+
},
|
|
288
|
+
store: new input.FileActionCheckpointStore(resolve(input.directory, "data", "runtime-actions", "checkpoints")),
|
|
289
|
+
hosted: {
|
|
290
|
+
getAction: (actionId) => primary(`actions/${encodeURIComponent(actionId)}`),
|
|
291
|
+
issueExecutionGrant: (actionId, body, idempotencyKey) => primary(`actions/${encodeURIComponent(actionId)}/execution-grant`, { method: "POST", body, idempotencyKey }),
|
|
292
|
+
verifyAction: (actionId, body, idempotencyKey) => verifier(`actions/${encodeURIComponent(actionId)}/verify`, { method: "POST", body, idempotencyKey }),
|
|
293
|
+
listActionReceipts: async (actionId) => (await primary(`actions/${encodeURIComponent(actionId)}/receipts`)).receipts ?? [],
|
|
294
|
+
claimExecutionGrant: (executionGrantId, claim, idempotencyKey) => claimHostedExecutionGrant(input.connection, requestFetch, executionGrantId, claim, idempotencyKey),
|
|
295
|
+
},
|
|
296
|
+
runtime: { reconcileReadOnly: true, prepareClaim: runtime.prepareClaim, execute: runtime.execute, reconcile: runtime.reconcile },
|
|
297
|
+
probe: {
|
|
298
|
+
id: workerConfig.probeId,
|
|
299
|
+
credentialHandle: workerConfig.probeTargetCredentialHandle,
|
|
300
|
+
readOnly: true,
|
|
301
|
+
observe: (request) => observeInIsolatedOutcomeProbe(isolatedProbeConfig, request),
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function projectTransport(connection, requestFetch) {
|
|
306
|
+
return async (suffix, options = {}) => {
|
|
307
|
+
const response = await requestFetch(`${connection.server}/v1/projects/${encodeURIComponent(connection.projectId)}/${suffix}`, {
|
|
308
|
+
method: options.method ?? "GET",
|
|
309
|
+
headers: { authorization: `Bearer ${connection.apiKey}`, ...(options.body ? { "content-type": "application/json" } : {}), ...(options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}) },
|
|
310
|
+
...(options.body ? { body: JSON.stringify(options.body) } : {}),
|
|
311
|
+
});
|
|
312
|
+
const body = await response.json().catch(() => ({}));
|
|
313
|
+
if (!response.ok)
|
|
314
|
+
throw new Error(String(body.error ?? `Witnora Hosted API returned HTTP ${response.status}.`));
|
|
315
|
+
return body;
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function runtimeHostedTransport(connection, requestFetch) {
|
|
319
|
+
return {
|
|
320
|
+
baseUrl: connection.server,
|
|
321
|
+
projectId: connection.projectId,
|
|
322
|
+
async request(suffix, options = {}) {
|
|
323
|
+
if (!/^(execution-grants\/[A-Za-z0-9._:-]+\/consume|execution-attempts\/[A-Za-z0-9._:-]+\/phase|execution-sessions\/[A-Za-z0-9._:-]+\/evidence)$/.test(suffix)) {
|
|
324
|
+
throw new Error("Runtime adapter attempted to access a Hosted path outside its execution boundary.");
|
|
325
|
+
}
|
|
326
|
+
if ((options.method ?? "GET") !== "POST")
|
|
327
|
+
throw new Error("Runtime Hosted execution paths require POST.");
|
|
328
|
+
const response = await requestFetch(`${connection.server}/v1/runtime/projects/${encodeURIComponent(connection.projectId)}/${suffix}`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { authorization: `Bearer ${connection.apiKey}`, ...(options.body ? { "content-type": "application/json" } : {}), ...(options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}) },
|
|
331
|
+
...(options.body ? { body: JSON.stringify(options.body) } : {}),
|
|
332
|
+
});
|
|
333
|
+
const body = await response.json().catch(() => ({}));
|
|
334
|
+
if (!response.ok)
|
|
335
|
+
throw new Error(String(body.error ?? `Witnora Hosted runtime API returned HTTP ${response.status}.`));
|
|
336
|
+
return body;
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
async function claimHostedExecutionGrant(connection, requestFetch, executionGrantId, claim, idempotencyKey) {
|
|
341
|
+
const response = await requestFetch(`${connection.server}/v1/runtime/projects/${encodeURIComponent(connection.projectId)}/execution-grants/${encodeURIComponent(executionGrantId)}/claim`, {
|
|
342
|
+
method: "POST",
|
|
343
|
+
headers: { authorization: `Bearer ${connection.apiKey}`, "content-type": "application/json", "idempotency-key": idempotencyKey },
|
|
344
|
+
body: JSON.stringify(claim),
|
|
345
|
+
});
|
|
346
|
+
if (response.status === 409)
|
|
347
|
+
return { acquired: false };
|
|
348
|
+
const body = await response.json().catch(() => ({}));
|
|
349
|
+
if (!response.ok)
|
|
350
|
+
throw new Error(String(body.error ?? `Witnora Hosted grant claim returned HTTP ${response.status}.`));
|
|
351
|
+
return { acquired: body.status === "CLAIMED" };
|
|
352
|
+
}
|
|
353
|
+
function configRuntimeWorkerImport() {
|
|
354
|
+
return import(new URL("./internal/control-client/durable-action-worker.js", import.meta.url).href);
|
|
355
|
+
}
|
|
356
|
+
function isConfiguredRuntimeProposal(proposal, config) {
|
|
357
|
+
const intent = proposal.executionIntent;
|
|
358
|
+
return Boolean(intent && typeof intent === "object" && !Array.isArray(intent)
|
|
359
|
+
&& intent.adapterId === config.adapterId
|
|
360
|
+
&& typeof intent.adapterVersionConstraint === "string");
|
|
361
|
+
}
|
|
187
362
|
export async function startManagedCustomerGateway(options = {}) {
|
|
188
363
|
const repository = resolve(options.repository ?? process.cwd());
|
|
189
364
|
const directory = resolve(repository, options.dir ?? ".witnora/gateway");
|
|
@@ -268,7 +443,7 @@ export async function statusManagedCustomerGateway(options = {}) {
|
|
|
268
443
|
const config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
|
|
269
444
|
const baseUrl = `http://${config.host}:${config.port}`;
|
|
270
445
|
const runtime = await readRuntime(directory);
|
|
271
|
-
const health = await gatewayHealth(baseUrl, options.fetch ?? fetch);
|
|
446
|
+
const health = await gatewayHealth(baseUrl, options.fetch ?? fetch, Boolean(config.runtimeWorker));
|
|
272
447
|
const base = {
|
|
273
448
|
schemaVersion: "witnora.managed_gateway_status.v0.1",
|
|
274
449
|
baseUrl,
|
|
@@ -383,8 +558,30 @@ function parseConfig(raw) {
|
|
|
383
558
|
throw new Error("Gateway privacyMode must remain metadata_only.");
|
|
384
559
|
if (!value.host || !Number.isSafeInteger(value.port) || Number(value.port) < 1 || Number(value.port) > 65_535)
|
|
385
560
|
throw new Error("Gateway host and port are invalid.");
|
|
561
|
+
if (value.runtimeWorker)
|
|
562
|
+
validateRuntimeWorkerConfig(value.runtimeWorker);
|
|
386
563
|
return value;
|
|
387
564
|
}
|
|
565
|
+
function validateRuntimeWorkerConfig(value) {
|
|
566
|
+
if (value.enabled !== true || !value.adapterModulePath || !/^[a-f0-9]{64}$/.test(value.adapterModuleSha256)
|
|
567
|
+
|| !value.probeModulePath || !/^[a-f0-9]{64}$/.test(value.probeModuleSha256)
|
|
568
|
+
|| !value.adapterId || !/^v?\d+\.\d+\.\d+$/.test(value.adapterVersion) || !value.probeId
|
|
569
|
+
|| !value.probeConnectionName || !credentialHandle(value.probeTargetCredentialHandle) || !value.runtimeIdentityId) {
|
|
570
|
+
throw new Error("gateway.json runtimeWorker requires separate exact adapter/probe module digests, adapter id/version, probe credentials, and runtime identity.");
|
|
571
|
+
}
|
|
572
|
+
if (value.adapterModulePath === value.probeModulePath || value.adapterModuleSha256 === value.probeModuleSha256)
|
|
573
|
+
throw new Error("runtimeWorker adapter and outcome probe modules must be independently pinned.");
|
|
574
|
+
if (value.adapterId === value.probeId)
|
|
575
|
+
throw new Error("runtimeWorker adapter and outcome probe must be separate.");
|
|
576
|
+
if (value.grantTtlSeconds !== undefined && (!Number.isInteger(value.grantTtlSeconds) || value.grantTtlSeconds < 15 || value.grantTtlSeconds > 300))
|
|
577
|
+
throw new Error("runtimeWorker grantTtlSeconds must be between 15 and 300.");
|
|
578
|
+
if (value.pollIntervalMs !== undefined && (!Number.isInteger(value.pollIntervalMs) || value.pollIntervalMs < 250 || value.pollIntervalMs > 30_000))
|
|
579
|
+
throw new Error("runtimeWorker pollIntervalMs must be between 250 and 30000.");
|
|
580
|
+
}
|
|
581
|
+
function credentialHandle(value) {
|
|
582
|
+
return typeof value === "string" && value.length <= 256 && /^[a-z][a-z0-9+.-]*:\/\/[^\s]+$/i.test(value)
|
|
583
|
+
&& !/(?:ac_live_|bearer\s|private.?key|password|token=)/i.test(value);
|
|
584
|
+
}
|
|
388
585
|
function parseSecrets(raw) {
|
|
389
586
|
const value = JSON.parse(raw);
|
|
390
587
|
if (value.schemaVersion !== SECRETS_SCHEMA || typeof value.gatewayToken !== "string" || value.gatewayToken.length < 32) {
|
|
@@ -405,6 +602,13 @@ function gatewayReadme(config) {
|
|
|
405
602
|
return `# Witnora customer-owned Gateway\n\nThis directory configures a metadata-only Gateway for project \`${config.projectId}\`. The Setup Autopilot starts it in the background after browser authorization.\n\n## Operations\n\n\`\`\`bash\nnpx witnora@latest gateway status\nnpx witnora@latest gateway logs\nnpx witnora@latest gateway restart\nnpx witnora@latest gateway stop\n\`\`\`\n\n\`gateway run\` remains available as a foreground debugging command. In the Agent repository, import the generated client at one meaningful sandbox workflow boundary:\n\n\`\`\`js\nimport { randomUUID } from "node:crypto";\nimport { witnoraGateway } from "./.witnora/gateway/client.mjs";\n\nconst runId = randomUUID();\nawait witnoraGateway.start(runId, { workflow: "sandbox-workflow" });\ntry {\n // Run the existing customer workflow here. Do not add raw inputs or outputs.\n await witnoraGateway.event(runId, "workflow.step.completed", { step: "meaningful-boundary" });\n await witnoraGateway.complete(runId, { status: "completed" });\n} catch (error) {\n await witnoraGateway.event(runId, "workflow.failed", { errorType: error?.name ?? "Error" });\n await witnoraGateway.complete(runId, { status: "failed" });\n throw error;\n}\n\`\`\`\n\nThe generated client reads only the local ignored Gateway token and sends metadata to \`http://${config.host}:${config.port}\`. The Hosted API key and source-signing key stay in the Gateway process. Do not commit \`secrets.json\`, \`data/\`, or \`runtime/\`.\n\nThis reference process creates source-signed, durable **RECORDED** evidence. It does not claim complete mediation. **ENFORCED** requires the target write credential to be removed from the Agent and placed behind a controlled execution adapter. **OUTCOME VERIFIED** requires a separate read-only credential and independent probe.\n`;
|
|
406
603
|
}
|
|
407
604
|
function gatewayClient(config) {
|
|
605
|
+
const requestHelper = `async function actionRequest(path, init = {}) {\n const headers = new Headers(init.headers);\n headers.set("authorization", \`Bearer \${await token()}\`);\n if (init.body) headers.set("content-type", "application/json");\n const response = await fetch(\`\${baseUrl}\${path}\`, { ...init, headers });\n const result = await response.json().catch(() => ({}));\n if (!response.ok) throw new Error(result.error ?? \`Witnora Gateway returned HTTP \${response.status}.\`);\n return result;\n}\n`;
|
|
606
|
+
const actionMethods = ` proposeAction(proposal, idempotencyKey = proposal?.externalId) {\n if (typeof idempotencyKey !== "string" || !idempotencyKey) throw new Error("Witnora action proposal requires an idempotency key or externalId.");\n return actionRequest("/v1/actions", { method: "POST", body: JSON.stringify({ proposal, idempotencyKey }) });\n },\n getAction(actionId) {\n if (!/^[A-Za-z0-9._:-]+$/.test(actionId)) throw new Error("Witnora actionId contains unsupported characters.");\n return actionRequest(\`/v1/actions/\${encodeURIComponent(actionId)}\`);\n },\n issueExecutionGrant(actionId, grant, idempotencyKey = \`grant:\${actionId}\`) {\n if (!/^[A-Za-z0-9._:-]+$/.test(actionId)) throw new Error("Witnora actionId contains unsupported characters.");\n return actionRequest(\`/v1/actions/\${encodeURIComponent(actionId)}/execution-grant\`, { method: "POST", body: JSON.stringify({ grant, idempotencyKey }) });\n },\n`;
|
|
607
|
+
return recordedGatewayClient(config)
|
|
608
|
+
.replace("export const witnoraGateway = {", `${requestHelper}\nexport const witnoraGateway = {`)
|
|
609
|
+
.replace(/\n};\n$/, `\n${actionMethods}};\n`);
|
|
610
|
+
}
|
|
611
|
+
function recordedGatewayClient(config) {
|
|
408
612
|
return `import { readFile } from "node:fs/promises";\n\nconst baseUrl = "http://${config.host}:${config.port}";\nlet gatewayToken;\n\nasync function token() {\n if (gatewayToken) return gatewayToken;\n const secrets = JSON.parse(await readFile(new URL("./secrets.json", import.meta.url), "utf8"));\n if (typeof secrets.gatewayToken !== "string" || secrets.gatewayToken.length < 32) {\n throw new Error("Witnora local Gateway token is missing or invalid.");\n }\n gatewayToken = secrets.gatewayToken;\n return gatewayToken;\n}\n\nasync function post(runId, operation, body) {\n if (!/^[A-Za-z0-9._:-]+$/.test(runId)) throw new Error("Witnora runId contains unsupported characters.");\n const response = await fetch(\`\${baseUrl}/v1/runs/\${encodeURIComponent(runId)}/\${operation}\`, {\n method: "POST",\n headers: { authorization: \`Bearer \${await token()}\`, "content-type": "application/json" },\n body: JSON.stringify(body),\n });\n const result = await response.json().catch(() => ({}));\n if (!response.ok) throw new Error(result.error ?? \`Witnora Gateway returned HTTP \${response.status}.\`);\n return result;\n}\n\nexport const witnoraGateway = {\n start(runId, metadata = {}) {\n return post(runId, "start", { payload: metadata, idempotencyKey: "run-start" });\n },\n event(runId, type, metadata = {}, idempotencyKey = \`\${type}-\${crypto.randomUUID()}\`) {\n return post(runId, "events", { type, payload: metadata, idempotencyKey });\n },\n complete(runId, metadata = {}) {\n return post(runId, "complete", {\n payload: metadata,\n evidenceStrength: {\n schemaVersion: "agentcert.evidence_strength.v0.1",\n level: "recorded",\n claims: [],\n limitations: ["No write-credential mediation or independent outcome probe is configured."],\n },\n idempotencyKey: "run-complete",\n });\n },\n};\n`;
|
|
409
613
|
}
|
|
410
614
|
async function writeExclusive(path, content, force, mode) {
|
|
@@ -432,12 +636,14 @@ function gatewayPaths(directory) {
|
|
|
432
636
|
join(directory, "README.md"),
|
|
433
637
|
];
|
|
434
638
|
}
|
|
435
|
-
async function gatewayHealth(baseUrl, requestFetch) {
|
|
639
|
+
async function gatewayHealth(baseUrl, requestFetch, requireRuntimeWorker = false) {
|
|
436
640
|
try {
|
|
437
641
|
const response = await requestFetch(`${baseUrl}/healthz`, { signal: AbortSignal.timeout(800) });
|
|
438
642
|
if (!response.ok)
|
|
439
643
|
return undefined;
|
|
440
644
|
const value = await response.json();
|
|
645
|
+
if (requireRuntimeWorker && value.actionWorker?.ready !== true)
|
|
646
|
+
return undefined;
|
|
441
647
|
return typeof value.collectorId === "string" && value.collectorId ? { collectorId: value.collectorId } : undefined;
|
|
442
648
|
}
|
|
443
649
|
catch {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/canonical.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function canonicalJson(value) {
|
|
2
|
+
return JSON.stringify(canonicalValue(value));
|
|
3
|
+
}
|
|
4
|
+
function canonicalValue(value) {
|
|
5
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
6
|
+
return value;
|
|
7
|
+
if (typeof value === "number") {
|
|
8
|
+
if (!Number.isFinite(value))
|
|
9
|
+
throw new Error("Canonical JSON does not support non-finite numbers.");
|
|
10
|
+
return Object.is(value, -0) ? 0 : value;
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return value.map(canonicalValue);
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
const record = value;
|
|
16
|
+
return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== undefined).map((key) => [key, canonicalValue(record[key])]));
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Canonical JSON does not support ${typeof value}.`);
|
|
19
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CustomerSourceKeyRing, type CustomerSourceSigner, type RemoteCollectorAck, type RemoteTrustedSourceRecord } from "./remote-collector.js";
|
|
2
|
+
export interface RemoteCollectorTransport {
|
|
3
|
+
registerSourceKey(input: {
|
|
4
|
+
collectorId: string;
|
|
5
|
+
keyId: string;
|
|
6
|
+
publicKeyPem: string;
|
|
7
|
+
previousKeyId?: string;
|
|
8
|
+
}): Promise<Record<string, unknown>>;
|
|
9
|
+
append(runId: string, records: RemoteTrustedSourceRecord[], idempotencyKey?: string): Promise<RemoteCollectorAck>;
|
|
10
|
+
heartbeat(input: {
|
|
11
|
+
collectorId: string;
|
|
12
|
+
signer: CustomerSourceSigner;
|
|
13
|
+
pendingRecordCount: number;
|
|
14
|
+
lastAckSequence?: number;
|
|
15
|
+
}): Promise<Record<string, unknown>>;
|
|
16
|
+
reconcile(runId: string, receipt: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
17
|
+
proposeAction(proposal: Record<string, unknown>, idempotencyKey: string): Promise<Record<string, unknown>>;
|
|
18
|
+
getAction(actionId: string): Promise<Record<string, unknown>>;
|
|
19
|
+
issueExecutionGrant(actionId: string, grant: Record<string, unknown>, idempotencyKey: string): Promise<Record<string, unknown>>;
|
|
20
|
+
}
|
|
21
|
+
export interface CustomerOwnedCollectorGatewayOptions {
|
|
22
|
+
client: RemoteCollectorTransport;
|
|
23
|
+
keyRing: CustomerSourceKeyRing;
|
|
24
|
+
gatewayToken: string;
|
|
25
|
+
storageDirectory: string;
|
|
26
|
+
collectorVersion?: string;
|
|
27
|
+
environment?: string;
|
|
28
|
+
host?: string;
|
|
29
|
+
port?: number;
|
|
30
|
+
flushIntervalMs?: number;
|
|
31
|
+
heartbeatIntervalMs?: number;
|
|
32
|
+
maxBodyBytes?: number;
|
|
33
|
+
actionWorker?: {
|
|
34
|
+
track(input: {
|
|
35
|
+
actionId: string;
|
|
36
|
+
proposal: Record<string, unknown>;
|
|
37
|
+
}): Promise<unknown>;
|
|
38
|
+
status(): Promise<Record<string, unknown>>;
|
|
39
|
+
close?(): void | Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface CustomerOwnedCollectorGateway {
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
flush(): Promise<{
|
|
46
|
+
delivered: number;
|
|
47
|
+
reconciled: number;
|
|
48
|
+
pending: number;
|
|
49
|
+
}>;
|
|
50
|
+
status(): Promise<CollectorGatewayStatus>;
|
|
51
|
+
}
|
|
52
|
+
export interface CollectorGatewayStatus {
|
|
53
|
+
schemaVersion: "agentcert.customer_collector_gateway_status.v0.2";
|
|
54
|
+
collectorId: string;
|
|
55
|
+
sourceKeyId: string;
|
|
56
|
+
runCount: number;
|
|
57
|
+
pendingRecordCount: number;
|
|
58
|
+
lastAckSequence?: number;
|
|
59
|
+
lastRemoteSuccessAt?: string;
|
|
60
|
+
lastRemoteError?: string;
|
|
61
|
+
actionWorker?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
export declare function startCustomerOwnedCollectorGateway(options: CustomerOwnedCollectorGatewayOptions): Promise<CustomerOwnedCollectorGateway>;
|
|
64
|
+
//# sourceMappingURL=collector-gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector-gateway.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/collector-gateway.ts"],"names":[],"mappings":"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"}
|