witnora 0.18.16 → 0.19.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/README.md CHANGED
@@ -66,6 +66,30 @@ npx witnora@latest gateway restart
66
66
  npx witnora@latest gateway stop
67
67
  ```
68
68
 
69
+ When the approved setup contains exactly one sandbox Task/action path, the same
70
+ onboarding command also writes `.witnora/gateway/HTTP_ACTION.md`. The Agent can
71
+ then call one ordinary HTTP endpoint instead of importing the generated client:
72
+
73
+ ```http
74
+ POST http://127.0.0.1:8787/v1/http-actions/cancel-order
75
+ Authorization: Bearer <action-scoped token>
76
+ Idempotency-Key: <stable external request id>
77
+ Content-Type: application/json
78
+
79
+ {
80
+ "resourceId": "order-42",
81
+ "status": "CANCELLED"
82
+ }
83
+ ```
84
+
85
+ The response is a Hosted Action in `PENDING_APPROVAL`, not a premature success
86
+ claim. The existing approval, idempotent Runtime worker, separate read-only
87
+ Probe, Receipt, and Runtime Watch continue on the same path. The action token is
88
+ separate from the full Gateway token and stays in ignored customer-owned data.
89
+ For a cloud Agent, the customer exposes only this path through its HTTPS ingress
90
+ or private network. This generated endpoint is limited to the current sandbox
91
+ Task/action path; it does not create a production Provider or broader authority.
92
+
69
93
  Initialization writes a reusable `.witnora/gateway/client.mjs`. Import its
70
94
  `witnoraGateway.start`, `event`, and `complete` methods at one existing sandbox
71
95
  workflow boundary. The generated README contains the exact code and privacy
package/dist/gateway.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID } from "node:crypto";
1
+ import { createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual } 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, realpath, rename, rm, writeFile } from "node:fs/promises";
@@ -214,15 +214,21 @@ export async function ensureCustomerGatewayPortAvailable(options) {
214
214
  const configPath = join(directory, "gateway.json");
215
215
  const clientPath = join(directory, "client.mjs");
216
216
  const readmePath = join(directory, "README.md");
217
+ const httpActionPath = join(directory, "http-action.json");
218
+ const httpActionGuidePath = join(directory, "HTTP_ACTION.md");
217
219
  const [configRaw, clientRaw, readmeRaw] = await Promise.all([
218
220
  readFile(configPath, "utf8"), readFile(clientPath, "utf8"), readFile(readmePath, "utf8"),
219
221
  ]);
220
222
  const current = parseConfig(configRaw);
223
+ const httpActionRaw = await readFile(httpActionPath, "utf8").catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
224
+ const httpAction = httpActionRaw ? parseCustomerHttpActionConfig(httpActionRaw) : undefined;
225
+ const httpActionGuideRaw = httpAction ? await readFile(httpActionGuidePath, "utf8") : undefined;
221
226
  const health = await localCollector(current.host, current.port, options.fetch ?? fetch);
222
227
  if (health === current.collectorId || (!health && await canListen(current.host, current.port))) {
223
228
  return { changed: false, port: current.port };
224
229
  }
225
- if (clientRaw !== gatewayClient(current) || readmeRaw !== gatewayReadme(current)) {
230
+ if (clientRaw !== gatewayClient(current) || readmeRaw !== gatewayReadme(current)
231
+ || (httpAction && httpActionGuideRaw !== customerHttpActionGuide(current, httpAction))) {
226
232
  throw new Error(`Gateway port ${current.port} is occupied, and generated Gateway files were modified; refusing to rewrite customer files.`);
227
233
  }
228
234
  const excluded = new Set([current.port]);
@@ -238,10 +244,13 @@ export async function ensureCustomerGatewayPortAvailable(options) {
238
244
  await atomicWrite(configPath, `${JSON.stringify(next, null, 2)}\n`, 0o644);
239
245
  await atomicWrite(clientPath, gatewayClient(next), 0o644);
240
246
  await atomicWrite(readmePath, gatewayReadme(next), 0o644);
247
+ if (httpAction)
248
+ await atomicWrite(httpActionGuidePath, customerHttpActionGuide(next, httpAction), 0o644);
241
249
  }
242
250
  catch (error) {
243
251
  await Promise.all([
244
252
  atomicWrite(configPath, configRaw, 0o644), atomicWrite(clientPath, clientRaw, 0o644), atomicWrite(readmePath, readmeRaw, 0o644),
253
+ ...(httpActionGuideRaw ? [atomicWrite(httpActionGuidePath, httpActionGuideRaw, 0o644)] : []),
245
254
  ]).catch(() => undefined);
246
255
  throw error;
247
256
  }
@@ -477,6 +486,20 @@ export async function doctorCustomerGateway(options) {
477
486
  checks.push({ id: "runtime_configuration", status: "FAIL", message: message(error) });
478
487
  }
479
488
  }
489
+ if (await exists(join(directory, "http-action.json"))) {
490
+ try {
491
+ const configured = await loadConfiguredCustomerHttpAction(directory, config);
492
+ if (!configured)
493
+ throw new Error("HTTP Action configuration disappeared during validation.");
494
+ checks.push({ id: "http_action", status: "PASS", message: `POST ${configured.action.path} is bound to the exact sandbox Task/action path with a separate action-scoped token.` });
495
+ }
496
+ catch (error) {
497
+ checks.push({ id: "http_action", status: "FAIL", message: message(error) });
498
+ }
499
+ }
500
+ else {
501
+ checks.push({ id: "http_action", status: "WARN", message: "No exact single sandbox Task/action path is available for the simple HTTP Action endpoint." });
502
+ }
480
503
  try {
481
504
  const health = await gatewayHealth(`http://${config.host}:${config.port}`, options.fetch ?? fetch, config.runtimeWorker);
482
505
  if (!health)
@@ -516,6 +539,7 @@ export async function runCustomerGateway(options) {
516
539
  const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
517
540
  const config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
518
541
  const secrets = parseSecrets(await readFile(join(directory, "secrets.json"), "utf8"));
542
+ const httpAction = await loadConfiguredCustomerHttpAction(directory, config);
519
543
  const connection = await loadConnection(config.connectionName, { configHome: options.configHome });
520
544
  if (!connection || connection.projectId !== config.projectId || connection.server !== config.server) {
521
545
  throw new Error("The saved Gateway credential does not match gateway.json. Run gateway init again.");
@@ -595,6 +619,7 @@ export async function runCustomerGateway(options) {
595
619
  } } : {}),
596
620
  } } : {}),
597
621
  assuranceHarness: { status: () => assuranceController.status() },
622
+ ...(httpAction ? { httpActionIngress: createCustomerHttpActionIngress(config, httpAction.action, httpAction.token) } : {}),
598
623
  });
599
624
  assuranceController.start();
600
625
  process.stdout.write(`Witnora customer-owned Gateway listening on ${gateway.baseUrl}\n`);
@@ -603,6 +628,8 @@ export async function runCustomerGateway(options) {
603
628
  : "Evidence ceiling: RECORDED. No exact target adapter and separate outcome probe are configured, so runtime writes remain fail-closed.\n");
604
629
  if (await assuranceController.status())
605
630
  process.stdout.write("Managed Workflow Harness: ACTIVE. Approved task changes are detected and revalidated automatically.\n");
631
+ if (httpAction)
632
+ process.stdout.write(`HTTP Action: READY at ${gateway.baseUrl}${httpAction.action.path} (sandbox Task/action path only).\n`);
606
633
  for (const signal of ["SIGINT", "SIGTERM"]) {
607
634
  process.once(signal, () => void Promise.allSettled([gateway.close(), assuranceController.close(), ...(sandboxFixture ? [sandboxFixture.close()] : [])]).finally(() => process.exit(0)));
608
635
  }
@@ -1411,6 +1438,202 @@ export function renderGatewayDoctor(result) {
1411
1438
  export function isGatewayDoctorReady(result) {
1412
1439
  return result.overall === "READY_TO_RECORD" || result.overall === "READY_FOR_RUNTIME";
1413
1440
  }
1441
+ async function loadConfiguredCustomerHttpAction(directory, gateway) {
1442
+ const configPath = join(directory, "http-action.json");
1443
+ let action;
1444
+ try {
1445
+ action = parseCustomerHttpActionConfig(await readFile(configPath, "utf8"));
1446
+ }
1447
+ catch (error) {
1448
+ if (error.code === "ENOENT")
1449
+ return undefined;
1450
+ throw error;
1451
+ }
1452
+ if (gateway.runtimeWorker?.adapterId !== LOCAL_SANDBOX_ADAPTER_ID)
1453
+ throw new Error("HTTP Action is configured without the generated localhost sandbox Runtime.");
1454
+ const harness = parseManagedWorkflowHarnessConfig(await readFile(join(directory, "workflow-harness.json"), "utf8"));
1455
+ const expected = harness.schemaVersion === MANAGED_WORKFLOW_HARNESS_SCHEMA
1456
+ ? createCustomerHttpActionConfig(harness.realPathActivations ?? [])
1457
+ : undefined;
1458
+ if (!expected || JSON.stringify(expected) !== JSON.stringify(action)) {
1459
+ throw new Error("HTTP Action binding does not match the active exact sandbox Task/action path.");
1460
+ }
1461
+ const token = (await readFile(customerHttpActionTokenPath(directory, action.id), "utf8")).trim();
1462
+ if (token.length < 32)
1463
+ throw new Error("Customer HTTP Action token is missing or invalid.");
1464
+ return { action, token };
1465
+ }
1466
+ export function createCustomerHttpActionConfig(activations) {
1467
+ if (activations.length !== 1)
1468
+ return undefined;
1469
+ const activation = activations[0];
1470
+ if (activation.environment !== "sandbox" || activation.actionPathIds.length !== 1)
1471
+ return undefined;
1472
+ const actionPathId = activation.actionPathIds[0];
1473
+ const config = {
1474
+ schemaVersion: "witnora.customer_http_action.v0.1",
1475
+ id: actionPathId,
1476
+ method: "POST",
1477
+ path: `/v1/http-actions/${actionPathId}`,
1478
+ environment: "sandbox",
1479
+ integrationId: activation.integrationId,
1480
+ integrationDigestSha256: activation.integrationDigestSha256,
1481
+ taskContractId: activation.taskContractId,
1482
+ taskContractDigestSha256: activation.taskContractDigestSha256,
1483
+ actionPathId,
1484
+ agentId: activation.agentId,
1485
+ agentVersion: activation.agentVersion,
1486
+ defaultStatus: "SUBMITTED",
1487
+ };
1488
+ validateCustomerHttpActionConfig(config);
1489
+ return config;
1490
+ }
1491
+ export async function configureCustomerHttpAction(options) {
1492
+ const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
1493
+ const gateway = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
1494
+ const action = createCustomerHttpActionConfig(options.activations);
1495
+ if (!action || gateway.runtimeWorker?.adapterId !== LOCAL_SANDBOX_ADAPTER_ID) {
1496
+ const configPath = join(directory, "http-action.json");
1497
+ if (!await exists(configPath))
1498
+ return { state: "NOT_CONFIGURED", changed: false, generatedFiles: [], rollback: async () => undefined };
1499
+ const currentRaw = await readFile(configPath, "utf8");
1500
+ const current = parseCustomerHttpActionConfig(currentRaw);
1501
+ const guidePath = join(directory, "HTTP_ACTION.md");
1502
+ const guideRaw = await readFile(guidePath, "utf8");
1503
+ if (guideRaw !== customerHttpActionGuide(gateway, current)) {
1504
+ throw new Error("Existing HTTP Action files are customer-modified; refusing to disable them automatically.");
1505
+ }
1506
+ await Promise.all([rm(configPath, { force: true }), rm(guidePath, { force: true })]);
1507
+ return {
1508
+ state: "NOT_CONFIGURED",
1509
+ changed: true,
1510
+ generatedFiles: [],
1511
+ rollback: async () => {
1512
+ await Promise.all([
1513
+ writeFile(configPath, currentRaw, { encoding: "utf8", mode: 0o644 }),
1514
+ writeFile(guidePath, guideRaw, { encoding: "utf8", mode: 0o644 }),
1515
+ ]);
1516
+ },
1517
+ };
1518
+ }
1519
+ const configPath = join(directory, "http-action.json");
1520
+ const guidePath = join(directory, "HTTP_ACTION.md");
1521
+ const tokenPath = customerHttpActionTokenPath(directory, action.id);
1522
+ const serialized = `${JSON.stringify(action, null, 2)}\n`;
1523
+ const guide = customerHttpActionGuide(gateway, action);
1524
+ const created = [];
1525
+ const rollback = async () => {
1526
+ for (const path of [...created].reverse())
1527
+ await rm(path, { force: true }).catch(() => undefined);
1528
+ };
1529
+ try {
1530
+ if (await exists(configPath)) {
1531
+ const current = await readFile(configPath, "utf8");
1532
+ parseCustomerHttpActionConfig(current);
1533
+ if (current !== serialized)
1534
+ throw new Error("Existing http-action.json differs from the exact generated sandbox Task/action binding; refusing to overwrite it.");
1535
+ }
1536
+ else {
1537
+ await writeExclusive(configPath, serialized, false, 0o644);
1538
+ created.push(configPath);
1539
+ }
1540
+ if (await exists(guidePath)) {
1541
+ if (await readFile(guidePath, "utf8") !== guide)
1542
+ throw new Error("Existing HTTP_ACTION.md differs from the generated integration guide; refusing to overwrite it.");
1543
+ }
1544
+ else {
1545
+ await writeExclusive(guidePath, guide, false, 0o644);
1546
+ created.push(guidePath);
1547
+ }
1548
+ if (!await exists(tokenPath)) {
1549
+ await writeExclusive(tokenPath, `${randomBytes(32).toString("base64url")}\n`, false, 0o600);
1550
+ created.push(tokenPath);
1551
+ }
1552
+ else {
1553
+ const token = (await readFile(tokenPath, "utf8")).trim();
1554
+ if (token.length < 32)
1555
+ throw new Error("Existing customer HTTP Action token is invalid.");
1556
+ }
1557
+ }
1558
+ catch (error) {
1559
+ await rollback();
1560
+ throw error;
1561
+ }
1562
+ return { state: "READY", changed: created.length > 0, action, generatedFiles: created, rollback };
1563
+ }
1564
+ export function parseCustomerHttpActionConfig(raw) {
1565
+ const value = JSON.parse(raw);
1566
+ validateCustomerHttpActionConfig(value);
1567
+ return value;
1568
+ }
1569
+ export function createCustomerHttpActionIngress(config, action, token) {
1570
+ const runtime = config.runtimeWorker;
1571
+ if (!action || runtime?.adapterId !== LOCAL_SANDBOX_ADAPTER_ID || !runtime.mandateId || !runtime.sandboxPrincipalId || !runtime.sandboxOrigin) {
1572
+ throw new Error("Customer HTTP Action requires one generated localhost sandbox Runtime binding.");
1573
+ }
1574
+ if (typeof token !== "string" || token.length < 32)
1575
+ throw new Error("Customer HTTP Action token must contain at least 32 characters.");
1576
+ const agentBuildId = `witnora-http-action:${action.id}@1.0.0`;
1577
+ const agentBuildDigest = createHash("sha256").update(agentBuildId).digest("hex");
1578
+ return {
1579
+ async resolve({ actionId, authorization, idempotencyKey, body, reject }) {
1580
+ const suppliedToken = authorization?.replace(/^Bearer\s+/i, "") ?? "";
1581
+ if (!secretEquals(suppliedToken, token))
1582
+ reject(401, "HTTP Action authentication failed.");
1583
+ if (actionId !== action.id)
1584
+ reject(404, "HTTP Action was not found.");
1585
+ const externalId = typeof idempotencyKey === "string" && /^[A-Za-z0-9._:-]{1,160}$/.test(idempotencyKey)
1586
+ ? idempotencyKey
1587
+ : reject(400, "Idempotency-Key is required and must use URL-safe identifier characters.");
1588
+ const unexpected = Object.keys(body).filter((field) => field !== "resourceId" && field !== "status");
1589
+ if (unexpected.length > 0)
1590
+ reject(400, `HTTP Action body contains unsupported field(s): ${unexpected.join(", ")}.`);
1591
+ const resourceId = typeof body.resourceId === "string" && /^[A-Za-z0-9._:-]{1,160}$/.test(body.resourceId)
1592
+ ? body.resourceId
1593
+ : reject(400, "resourceId is required and must use URL-safe identifier characters.");
1594
+ const status = body.status === undefined
1595
+ ? action.defaultStatus
1596
+ : typeof body.status === "string" && /^[A-Za-z0-9._:-]{1,100}$/.test(body.status)
1597
+ ? body.status
1598
+ : reject(400, "status must use URL-safe identifier characters.");
1599
+ const approvedParameters = { resourceId, status };
1600
+ return {
1601
+ idempotencyKey: externalId,
1602
+ proposal: {
1603
+ externalId,
1604
+ agentId: action.agentId,
1605
+ principal: { id: runtime.sandboxPrincipalId, version: "sandbox-v1" },
1606
+ actionType: "UPDATE",
1607
+ targetSystem: "WitnoraLocalSandbox",
1608
+ requestedPermissions: [],
1609
+ sensitive: true,
1610
+ expectedState: approvedParameters,
1611
+ businessTaskBinding: {
1612
+ taskContractId: action.taskContractId,
1613
+ taskContractDigestSha256: action.taskContractDigestSha256,
1614
+ actionPathId: action.actionPathId,
1615
+ environment: "sandbox",
1616
+ realPathIntegrationId: action.integrationId,
1617
+ realPathIntegrationDigestSha256: action.integrationDigestSha256,
1618
+ },
1619
+ mandateId: runtime.mandateId,
1620
+ requireMandate: true,
1621
+ executionIntent: {
1622
+ adapterId: LOCAL_SANDBOX_ADAPTER_ID,
1623
+ adapterVersionConstraint: `^${LOCAL_SANDBOX_ADAPTER_VERSION}`,
1624
+ allowedOrigins: [runtime.sandboxOrigin],
1625
+ approvedParameters,
1626
+ outcomePredicate: { type: "state_subset", expected: approvedParameters },
1627
+ agentBuildId,
1628
+ agentBuildDigest,
1629
+ allowedOperation: "UPDATE",
1630
+ allowedResource: `mock-state/${resourceId}`,
1631
+ },
1632
+ },
1633
+ };
1634
+ },
1635
+ };
1636
+ }
1414
1637
  function parseConfig(raw) {
1415
1638
  const value = JSON.parse(raw);
1416
1639
  if (value.schemaVersion !== CONFIG_SCHEMA || !value.projectId || !value.server || !value.connectionName || !value.collectorId) {
@@ -1430,6 +1653,16 @@ function parseConfig(raw) {
1430
1653
  }
1431
1654
  return config;
1432
1655
  }
1656
+ function validateCustomerHttpActionConfig(value) {
1657
+ if (value.schemaVersion !== "witnora.customer_http_action.v0.1" || value.method !== "POST" || value.environment !== "sandbox"
1658
+ || !/^[A-Za-z0-9._:-]{1,160}$/.test(value.id) || value.path !== `/v1/http-actions/${value.id}`
1659
+ || !/^[A-Za-z0-9._:-]{1,200}$/.test(value.integrationId) || !validDigest(value.integrationDigestSha256)
1660
+ || !/^[A-Za-z0-9._:-]{1,200}$/.test(value.taskContractId) || !validDigest(value.taskContractDigestSha256)
1661
+ || value.actionPathId !== value.id || !/^[A-Za-z0-9._:-]{1,200}$/.test(value.agentId)
1662
+ || typeof value.agentVersion !== "string" || !value.agentVersion || !/^[A-Za-z0-9._:-]{1,100}$/.test(value.defaultStatus)) {
1663
+ throw new Error("http-action.json must bind one exact sandbox Task/action path.");
1664
+ }
1665
+ }
1433
1666
  function validateGatewayAgentIdentity(value) {
1434
1667
  for (const [key, item] of Object.entries(value)) {
1435
1668
  if (typeof item !== "string" || item.trim().length === 0 || item.length > 200) {
@@ -1652,6 +1885,42 @@ function gatewayReadme(config) {
1652
1885
  : "This 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.";
1653
1886
  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\n${assuranceBoundary}\n`;
1654
1887
  }
1888
+ function customerHttpActionTokenPath(directory, actionId) {
1889
+ return join(directory, "data", "http-actions", `${actionId}.token`);
1890
+ }
1891
+ function customerHttpActionGuide(gateway, action) {
1892
+ const endpoint = `http://${gateway.host}:${gateway.port}${action.path}`;
1893
+ return [
1894
+ "# Witnora HTTP Action — sandbox",
1895
+ "",
1896
+ `This is the smallest integration surface for the exact \`${action.actionPathId}\` sandbox Task/action path. The Agent sends one ordinary HTTP request; Witnora holds it for authorization, executes it idempotently inside the customer-owned Gateway, verifies the result through the separate read-only Probe, and preserves the Receipt.`,
1897
+ "",
1898
+ "## Request",
1899
+ "",
1900
+ "```http",
1901
+ `POST ${endpoint}`,
1902
+ "Authorization: Bearer <action-scoped token>",
1903
+ "Idempotency-Key: <stable external request id>",
1904
+ "Content-Type: application/json",
1905
+ "",
1906
+ "{",
1907
+ ' "resourceId": "order-42",',
1908
+ ' "status": "CANCELLED"',
1909
+ "}",
1910
+ "```",
1911
+ "",
1912
+ `The action-scoped token is stored locally at \`data/http-actions/${action.id}.token\`. Put that value in the calling Agent platform's secret store; never put the Gateway token in the Agent platform.`,
1913
+ "",
1914
+ "## Expected response",
1915
+ "",
1916
+ "The first response is the Hosted Action in `PENDING_APPROVAL`. It is not a claim that the business outcome already happened. After the exact action is approved, the existing Runtime worker performs at most one authorized write, the independent Probe reads the resulting sandbox state, and Witnora creates the Receipt. Reusing the same `Idempotency-Key` with the same body returns the same Action; changing the body under that key fails closed.",
1917
+ "",
1918
+ "Only `resourceId` and `status` are accepted. This endpoint covers the current sandbox Task/action path only. It does not add a production Provider, broader permissions, navigation, or a higher evidence grade.",
1919
+ "",
1920
+ "For a cloud-hosted Agent, expose only this action path through the customer's authenticated HTTPS ingress or private network. Do not expose the full Gateway API or `gatewayToken`.",
1921
+ "",
1922
+ ].join("\n");
1923
+ }
1655
1924
  function gatewayConfigDigest(config) {
1656
1925
  const runtimeWorker = config.runtimeWorker ? { ...config.runtimeWorker, configDigestSha256: undefined } : undefined;
1657
1926
  return createHash("sha256").update(JSON.stringify(canonical({ ...config, runtimeWorker }))).digest("hex");
@@ -1672,6 +1941,11 @@ function activeWindow(validFrom, expiresAt, now) {
1672
1941
  return Number.isFinite(starts) && Number.isFinite(expires) && starts <= now && expires > now;
1673
1942
  }
1674
1943
  function validDigest(value) { return /^[a-f0-9]{64}$/.test(value); }
1944
+ function secretEquals(leftValue, rightValue) {
1945
+ const left = Buffer.from(leftValue);
1946
+ const right = Buffer.from(rightValue);
1947
+ return left.length === right.length && timingSafeEqual(left, right);
1948
+ }
1675
1949
  function localSandboxOrigin(value) {
1676
1950
  try {
1677
1951
  if (!value)
@@ -20,6 +20,18 @@ export interface RemoteCollectorTransport {
20
20
  getAction(actionId: string): Promise<Record<string, unknown>>;
21
21
  issueExecutionGrant(actionId: string, grant: Record<string, unknown>, idempotencyKey: string): Promise<Record<string, unknown>>;
22
22
  }
23
+ export interface CustomerHttpActionIngress {
24
+ resolve(input: {
25
+ actionId: string;
26
+ authorization?: string;
27
+ idempotencyKey?: string;
28
+ body: Record<string, unknown>;
29
+ reject(status: number, message: string): never;
30
+ }): Promise<{
31
+ proposal: Record<string, unknown>;
32
+ idempotencyKey: string;
33
+ }>;
34
+ }
23
35
  export interface CustomerOwnedCollectorGatewayOptions {
24
36
  client: RemoteCollectorTransport;
25
37
  keyRing: CustomerSourceKeyRing;
@@ -55,6 +67,7 @@ export interface CustomerOwnedCollectorGatewayOptions {
55
67
  requestId: string;
56
68
  action: "RESTART_AND_VERIFY";
57
69
  }) => void | Promise<void>;
70
+ httpActionIngress?: CustomerHttpActionIngress;
58
71
  }
59
72
  export interface CustomerOwnedCollectorGateway {
60
73
  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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"}
@@ -100,38 +100,58 @@ export async function startCustomerOwnedCollectorGateway(options) {
100
100
  };
101
101
  };
102
102
  let sendHeartbeat = async () => { };
103
+ const proposeHostedAction = async (proposal, idempotencyKey) => {
104
+ const proposalEnvironment = typeof proposal.environment === "string" ? proposal.environment : undefined;
105
+ if (monitoringControlFor(proposalEnvironment)?.state === "PAUSED") {
106
+ throw new GatewayRequestError(409, "Monitoring is paused for this Agent. Reconnect it in Witnora before proposing a new action.");
107
+ }
108
+ const hostedProposal = runtimeBinding ? { ...proposal, localRuntimeBinding: runtimeBinding } : proposal;
109
+ const propose = () => options.client.proposeAction(hostedProposal, idempotencyKey);
110
+ let action;
111
+ try {
112
+ action = await propose();
113
+ }
114
+ catch (error) {
115
+ if (!(error instanceof RemoteCollectorApiError) || error.code !== "local_sandbox_binding_stale" || !runtimeBinding)
116
+ throw error;
117
+ await sendHeartbeat();
118
+ action = await propose();
119
+ }
120
+ if (options.actionWorker && action.externalId === proposal.externalId) {
121
+ await options.actionWorker.track({ actionId: identifier(action.id, "actionId"), proposal });
122
+ }
123
+ return action;
124
+ };
103
125
  const server = createServer(async (request, response) => {
104
126
  try {
105
127
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
106
128
  if (request.method === "GET" && url.pathname === "/healthz")
107
129
  return json(response, 200, await status());
130
+ const httpActionRoute = url.pathname.match(/^\/v1\/http-actions\/([A-Za-z0-9._:-]+)$/);
131
+ if (request.method === "POST" && httpActionRoute) {
132
+ if (!options.httpActionIngress)
133
+ return json(response, 404, { error: "not found" });
134
+ const idempotencyHeader = request.headers["idempotency-key"];
135
+ const resolved = await options.httpActionIngress.resolve({
136
+ actionId: identifier(httpActionRoute[1], "actionId"),
137
+ authorization: request.headers.authorization,
138
+ idempotencyKey: typeof idempotencyHeader === "string" ? idempotencyHeader : undefined,
139
+ body: await readJson(request, options.maxBodyBytes ?? 1_048_576),
140
+ reject(status, messageText) {
141
+ const safeStatus = Number.isInteger(status) && status >= 400 && status <= 499 ? status : 500;
142
+ throw new GatewayRequestError(safeStatus, safeStatus === status ? messageText : "HTTP Action ingress rejected the request with an invalid status.");
143
+ },
144
+ });
145
+ return json(response, 202, await proposeHostedAction(object(resolved.proposal), identifier(resolved.idempotencyKey, "idempotencyKey")));
146
+ }
108
147
  authenticate(request, options.gatewayToken);
109
148
  if (request.method === "POST" && url.pathname === "/v1/flush")
110
149
  return json(response, 200, await flush());
111
150
  if (request.method === "POST" && url.pathname === "/v1/actions") {
112
151
  const body = await readJson(request, options.maxBodyBytes ?? 1_048_576);
113
152
  const proposal = object(body.proposal);
114
- const proposalEnvironment = typeof proposal.environment === "string" ? proposal.environment : undefined;
115
- if (monitoringControlFor(proposalEnvironment)?.state === "PAUSED") {
116
- throw new GatewayRequestError(409, "Monitoring is paused for this Agent. Reconnect it in Witnora before proposing a new action.");
117
- }
118
- const hostedProposal = runtimeBinding ? { ...proposal, localRuntimeBinding: runtimeBinding } : proposal;
119
153
  const idempotencyKey = identifier(body.idempotencyKey, "idempotencyKey");
120
- const propose = () => options.client.proposeAction(hostedProposal, idempotencyKey);
121
- let action;
122
- try {
123
- action = await propose();
124
- }
125
- catch (error) {
126
- if (!(error instanceof RemoteCollectorApiError) || error.code !== "local_sandbox_binding_stale" || !runtimeBinding)
127
- throw error;
128
- await sendHeartbeat();
129
- action = await propose();
130
- }
131
- if (options.actionWorker && action.externalId === proposal.externalId) {
132
- await options.actionWorker.track({ actionId: identifier(action.id, "actionId"), proposal });
133
- }
134
- return json(response, 202, action);
154
+ return json(response, 202, await proposeHostedAction(proposal, idempotencyKey));
135
155
  }
136
156
  const actionRoute = url.pathname.match(/^\/v1\/actions\/([A-Za-z0-9._:-]+)$/);
137
157
  if (request.method === "GET" && actionRoute) {
package/dist/onboard.js CHANGED
@@ -8,7 +8,7 @@ import { verifyControlPlaneConnection } from "./control-plane.js";
8
8
  import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
9
9
  import { parseAgentTemplate, starterAdapter, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
10
10
  import { writeTryEvidence } from "./try.js";
11
- import { doctorCustomerGateway, activateManagedWorkflowHarness, ensureCustomerGatewayPortAvailable, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
11
+ import { doctorCustomerGateway, activateManagedWorkflowHarness, configureCustomerHttpAction, ensureCustomerGatewayPortAvailable, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
12
12
  import { discoverRuntimeReferences, planRuntimeSandboxModules } from "./runtime-sandbox-kit.js";
13
13
  import { automaticRuntimeReferencesUsable, bootstrapLocalRuntime, verifyAutomaticRuntimeProbe } from "./runtime-bootstrap.js";
14
14
  import { activateRealPathIntegrations } from "./real-path-activation.js";
@@ -113,6 +113,7 @@ export async function runOnboard(options) {
113
113
  limitation: "No approved customer Harness module is bound to this Agent repository.",
114
114
  };
115
115
  let assuranceHarnessChanged = false;
116
+ let httpActionSetup;
116
117
  let continuousService;
117
118
  const prepareRuntime = async (operation) => {
118
119
  const delays = [250, 500, 1_000, 2_000, 4_000, 8_000];
@@ -251,7 +252,11 @@ export async function runOnboard(options) {
251
252
  throw error;
252
253
  assuranceHarnessReadiness = { state: "WAITING_FOR_CUSTOMER_HARNESS", limitation: detail };
253
254
  }
254
- if (assuranceHarnessChanged && gatewayState.status !== "absent") {
255
+ if (assuranceHarnessReadiness.state === "ACTIVE") {
256
+ httpActionSetup = await configureCustomerHttpAction({ repository: repositoryPath, activations: realPathActivation.activations });
257
+ generatedFiles.push(...httpActionSetup.generatedFiles);
258
+ }
259
+ if ((assuranceHarnessChanged || httpActionSetup?.changed) && gatewayState.status !== "absent") {
255
260
  const stopped = await stopGatewayOwners();
256
261
  if (stopped.state !== "STOPPED" && stopped.state !== "STALE")
257
262
  throw new Error("The prior managed Gateway process did not stop before Assurance Harness activation.");
@@ -308,17 +313,28 @@ export async function runOnboard(options) {
308
313
  output(assuranceHarnessReadiness.state === "ACTIVE"
309
314
  ? `Assurance Harness: ACTIVE. Nora can dispatch authority-ready Replay and no-write Shadow evaluations without manual Workflow IDs, evaluator origins, credentials, or contract digests.${realPathActivation.activations.length ? ` ${realPathActivation.activations.length} exact Stripe test-mode real-path binding(s) passed read-only preflight.` : ""}\n`
310
315
  : `Assurance Harness: WAITING_FOR_CUSTOMER_HARNESS. ${assuranceHarnessReadiness.limitation}\n`);
316
+ if (httpActionSetup?.state === "READY" && httpActionSetup.action) {
317
+ output(`HTTP Action: READY. POST ${managedGateway.baseUrl}${httpActionSetup.action.path}; copy the action-scoped token and request example from .witnora/gateway/HTTP_ACTION.md.\n`);
318
+ }
311
319
  output("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");
312
320
  return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
313
321
  repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
314
322
  gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory,
315
- gateway: managedGateway, runtimeReadiness, assuranceHarnessReadiness, continuousService };
323
+ gateway: managedGateway, runtimeReadiness, assuranceHarnessReadiness, continuousService,
324
+ ...(httpActionSetup?.state === "READY" && httpActionSetup.action ? { httpAction: {
325
+ state: "READY",
326
+ method: "POST",
327
+ path: httpActionSetup.action.path,
328
+ guidePath: join(repositoryPath, ".witnora", "gateway", "HTTP_ACTION.md"),
329
+ } } : {}),
330
+ };
316
331
  }
317
332
  catch (error) {
318
333
  const diagnosis = error instanceof Error ? error.message : String(error);
319
334
  if (managedGateway?.started) {
320
335
  await stopGatewayOwners().catch(() => undefined);
321
336
  }
337
+ await httpActionSetup?.rollback().catch(() => undefined);
322
338
  await realPathActivation?.rollback().catch(() => undefined);
323
339
  await rollbackGeneratedFiles(generatedFiles);
324
340
  await runtimeUpgrade?.rollback().catch(() => undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.18.16",
3
+ "version": "0.19.0",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",