witnora 0.20.15 → 0.20.17

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.
@@ -23,8 +23,9 @@ export async function saveConnection(name, input, options = {}) {
23
23
  return path;
24
24
  }
25
25
  export async function loadConnection(name, options = {}) {
26
+ const explicitConfigHome = options.configHome ?? process.env.WITNORA_CONFIG_HOME ?? process.env.AGENTCERT_CONFIG_HOME;
26
27
  const file = await readCredentialFile(credentialsPath(options))
27
- ?? (options.configHome ? undefined : await readCredentialFile(legacyCredentialsPath()));
28
+ ?? (explicitConfigHome !== undefined ? undefined : await readCredentialFile(legacyCredentialsPath()));
28
29
  if (!file)
29
30
  return undefined;
30
31
  const connectionName = name ? validateConnectionName(name) : file.defaultConnection;
@@ -90,7 +91,8 @@ function validateConnection(input) {
90
91
  throw new Error("Witnora project ID is required.");
91
92
  if (!apiKey.startsWith("ac_live_"))
92
93
  throw new Error("Witnora API key must start with ac_live_.");
93
- return { server: normalizeServer(input.server), projectId, apiKey };
94
+ const approvedSetupPlanId = input.approvedSetupPlanId?.trim();
95
+ return { server: normalizeServer(input.server), projectId, apiKey, ...(approvedSetupPlanId ? { approvedSetupPlanId } : {}) };
94
96
  }
95
97
  function normalizeServer(value) {
96
98
  let url;
@@ -25,6 +25,7 @@ export async function authorizeProjectConnection(options) {
25
25
  server,
26
26
  projectId: token.projectId,
27
27
  apiKey: token.apiKey,
28
+ approvedSetupPlanId: token.approvedSetupPlanId,
28
29
  }, { configHome: options.configHome });
29
30
  return { ...token, server, credentialsPath };
30
31
  }
package/dist/onboard.js CHANGED
@@ -48,18 +48,22 @@ export async function runOnboard(options) {
48
48
  configHome: options.configHome,
49
49
  });
50
50
  if (reusableConnection)
51
- output("Reusing the previously approved project-scoped setup connection; no new browser approval is required.\n");
51
+ output("Reusing this Agent's previously approved setup connection; verifying its exact Setup Plan.\n");
52
52
  const credentialsPath = token.credentialsPath;
53
53
  await verifyControlPlaneConnection({ baseUrl: server, projectId: token.projectId, apiKey: token.apiKey, fetch: requestFetch });
54
- const planSnapshot = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/setup-plans`, {
54
+ const planQuery = token.approvedSetupPlanId ? `?setupPlanId=${encodeURIComponent(token.approvedSetupPlanId)}` : "";
55
+ const planSnapshot = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/setup-plans${planQuery}`, {
55
56
  method: "GET", headers: { authorization: `Bearer ${token.apiKey}` },
56
57
  });
57
58
  const setupPlan = planSnapshot.plan;
58
- if (!setupPlan || !setupPlan.authorizedOperations.some((item) => item.kind === "authorize_install")) {
59
+ if (!setupPlan || (token.approvedSetupPlanId && setupPlan.id !== token.approvedSetupPlanId) || !setupPlan.authorizedOperations.some((item) => item.kind === "authorize_install")) {
59
60
  throw new Error("Confirm the Setup Plan and authorize installation in the Witnora Workspace before running onboard.");
60
61
  }
62
+ const attemptId = `install-${Date.now()}-${randomSuffix()}`;
63
+ // Resume an authorized failed install before requesting fresh Runtime authority.
64
+ await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
61
65
  let localRuntime = await discoverRuntimeReferences({
62
- repository: repositoryPath, configHome: options.configHome, projectId: token.projectId, server, env: options.env ?? process.env,
66
+ repository: repositoryPath, configHome: options.configHome, projectId: token.projectId, setupPlanId: setupPlan.id, server, env: options.env ?? process.env,
63
67
  });
64
68
  let runtimeLimitation = localRuntime.limitation;
65
69
  let runtimeBootstrapRequired = !localRuntime.references;
@@ -96,7 +100,6 @@ export async function runOnboard(options) {
96
100
  runtimeLimitation = `Hosted Runtime bootstrap did not complete: ${detail.slice(0, 500)} The existing Gateway was preserved and setup remains RECORDED_ONLY.`;
97
101
  }
98
102
  }
99
- const attemptId = `install-${Date.now()}-${randomSuffix()}`;
100
103
  const generatedFiles = [];
101
104
  const gatewayLifecycle = options.gatewayLifecycle ?? {
102
105
  start: startManagedCustomerGateway,
@@ -145,7 +148,6 @@ export async function runOnboard(options) {
145
148
  }
146
149
  throw lastError;
147
150
  };
148
- await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
149
151
  try {
150
152
  generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
151
153
  const selfTestDir = join(repositoryPath, ".witnora", "onboarding", "self-test", attemptId);
@@ -203,9 +205,13 @@ export async function runOnboard(options) {
203
205
  generatedFiles.push(...gateway.generatedFiles);
204
206
  }
205
207
  else {
206
- const binding = await inspectGatewayBinding(gatewayState.directory, token.projectId, server);
207
- if (binding.matches) {
208
- await saveConnection(binding.connectionName, { server, projectId: token.projectId, apiKey: token.apiKey }, { configHome: options.configHome });
208
+ const binding = await inspectGatewayBinding(gatewayState.directory, token.projectId, server, setupPlan.id);
209
+ if (binding.projectId === token.projectId && binding.server === server) {
210
+ const planRepairRequired = !binding.matches && Boolean(binding.runtimeSetupPlanId);
211
+ if (planRepairRequired && !localRuntime.references) {
212
+ throw new RuntimeSetupNotReadyError(`The existing Gateway belongs to a different Setup Plan and could not be restored. ${runtimeLimitation ?? "Runtime references are unavailable."} Run onboard again to retry; the existing Gateway is preserved.`);
213
+ }
214
+ await saveConnection(binding.connectionName, { server, projectId: token.projectId, apiKey: token.apiKey, approvedSetupPlanId: setupPlan.id }, { configHome: options.configHome });
209
215
  if (localRuntime.references) {
210
216
  try {
211
217
  runtimeUpgrade = await prepareRuntime(() => upgradeCustomerGatewayRuntime({ repository: repositoryPath, authorization: token, runtimeReferences: localRuntime.references, agentIdentity, configHome: options.configHome, fetch: requestFetch }));
@@ -222,6 +228,8 @@ export async function runOnboard(options) {
222
228
  catch (error) {
223
229
  if (!(error instanceof RuntimeSetupNotReadyError))
224
230
  throw error;
231
+ if (planRepairRequired)
232
+ throw error;
225
233
  runtimeLimitation = `Local sandbox Runtime references were found but did not establish readiness: ${error.message}`;
226
234
  output(`\nExisting customer-owned Gateway remains RECORDED_ONLY because its Runtime references did not pass readiness checks: ${error.message}\n`);
227
235
  }
@@ -597,7 +605,7 @@ function configuredGatewayPorts(environment) {
597
605
  const port = Number(value);
598
606
  return Number.isInteger(port) && port > 0 && port <= 65_535 ? new Set([port]) : new Set();
599
607
  }
600
- async function inspectGatewayBinding(directory, projectId, server) {
608
+ async function inspectGatewayBinding(directory, projectId, server, setupPlanId) {
601
609
  let value;
602
610
  try {
603
611
  value = JSON.parse(await readFile(join(directory, "gateway.json"), "utf8"));
@@ -611,10 +619,12 @@ async function inspectGatewayBinding(directory, projectId, server) {
611
619
  const collectorId = typeof value.collectorId === "string" ? value.collectorId.trim() : "";
612
620
  const host = typeof value.host === "string" && value.host.trim() ? value.host.trim() : "127.0.0.1";
613
621
  const port = Number(value.port);
622
+ const worker = value.runtimeWorker;
623
+ const runtimeSetupPlanId = typeof worker?.runtimeSetupPlanId === "string" ? worker.runtimeSetupPlanId : undefined;
614
624
  if (!existingProjectId || !existingServer || !connectionName || !collectorId || !Number.isInteger(port) || port < 1 || port > 65_535) {
615
625
  throw new Error("Existing Gateway configuration is missing a valid project, server, connection, collector, host, or port. Preserve it and repair the setup in Advanced mode.");
616
626
  }
617
- return { matches: existingProjectId === projectId && existingServer === server, projectId: existingProjectId, server: existingServer, connectionName, collectorId, host, port };
627
+ return { matches: existingProjectId === projectId && existingServer === server && runtimeSetupPlanId === setupPlanId, runtimeSetupPlanId, projectId: existingProjectId, server: existingServer, connectionName, collectorId, host, port };
618
628
  }
619
629
  async function assertGatewayStopped(requestFetch, host, port, oldCollectorId, oldProjectId, newProjectId) {
620
630
  try {
@@ -3,7 +3,7 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { credentialsPath } from "./credentials.js";
6
- import { saveRuntimeReferenceManifest } from "./runtime-sandbox-kit.js";
6
+ import { runtimeReferenceManifestPath, saveRuntimeReferenceManifest } from "./runtime-sandbox-kit.js";
7
7
  const SCHEMA_VERSION = "witnora.runtime_bootstrap.v0.1";
8
8
  const ENVELOPE_ALGORITHM = "X25519-HKDF-SHA256-A256GCM";
9
9
  const PROBE_SCOPES = ["outcomes:verify"];
@@ -85,6 +85,7 @@ export async function bootstrapLocalRuntime(input) {
85
85
  await acknowledge(requestFetch, endpoint, input.apiKey, issued.bootstrapId, issued.envelope.digestSha256);
86
86
  const manifestPath = join(dirname(credentialsPath({ configHome: input.configHome })), "runtime-references.json");
87
87
  await saveRuntimeReferenceManifest(manifestPath, input.projectId, server, references);
88
+ await saveRuntimeReferenceManifest(runtimeReferenceManifestPath({ ...input, server, setupPlanId: input.planId }), input.projectId, server, references);
88
89
  await rm(attemptPath, { force: true });
89
90
  return { references, bootstrapId: issued.bootstrapId };
90
91
  }
@@ -16,15 +16,26 @@ export async function discoverRuntimeReferences(input) {
16
16
  runtimeKeyId: input.env.WITNORA_RUNTIME_KEY_ID?.trim(),
17
17
  runtimeSigningKeyHandle: input.env.WITNORA_RUNTIME_SIGNING_KEY_HANDLE?.trim(),
18
18
  };
19
- const manifestPath = join(dirname(credentialsPath({ configHome: input.configHome })), "runtime-references.json");
20
- const manifest = await optionalJson(manifestPath);
19
+ const manifestPath = runtimeReferenceManifestPath(input);
20
+ const legacyManifestPath = join(dirname(credentialsPath({ configHome: input.configHome })), "runtime-references.json");
21
+ const gateway = input.setupPlanId ? await optionalJson(join(input.repository, ".witnora", "gateway", "gateway.json")) : undefined;
22
+ const worker = object(gateway?.runtimeWorker);
23
+ const gatewayReferences = gateway?.projectId === input.projectId && gateway.server === input.server && worker.runtimeSetupPlanId === input.setupPlanId
24
+ ? completeReferences({ ...worker, setupPlanId: worker.runtimeSetupPlanId, bootstrapId: worker.runtimeBootstrapId,
25
+ adapterDigestSha256: worker.adapterModuleSha256, probeDigestSha256: worker.probeModuleSha256, fixtureContractDigestSha256: worker.fixtureContractSha256 })
26
+ : undefined;
27
+ const manifest = await optionalJson(manifestPath) ?? (gatewayReferences
28
+ ? { schemaVersion: "witnora.runtime_references.v0.1", projectId: input.projectId, server: input.server, references: gatewayReferences }
29
+ : await optionalJson(legacyManifestPath));
21
30
  const manifestReferences = manifest?.schemaVersion === "witnora.runtime_references.v0.1" && manifest.projectId === input.projectId && manifest.server === input.server
22
31
  ? completeReferences(manifest.references)
23
32
  : undefined;
33
+ const matchingReferences = !input.setupPlanId || !manifestReferences?.probeApiKeyId || manifestReferences.setupPlanId === input.setupPlanId
34
+ ? manifestReferences : undefined;
24
35
  const environmentOverrides = Object.fromEntries(Object.entries(entries).filter(([, value]) => value));
25
- const configuredReferences = completeReferences({ ...manifestReferences, ...environmentOverrides });
36
+ const configuredReferences = completeReferences({ ...matchingReferences, ...environmentOverrides });
26
37
  if (configuredReferences) {
27
- if (Object.keys(environmentOverrides).length > 0)
38
+ if (input.setupPlanId || Object.keys(environmentOverrides).length > 0)
28
39
  await saveRuntimeReferenceManifest(manifestPath, input.projectId, input.server, configuredReferences);
29
40
  return { references: configuredReferences, manifestPath };
30
41
  }
@@ -48,6 +59,13 @@ export async function discoverRuntimeReferences(input) {
48
59
  ? `Local sandbox Runtime references are incomplete (${present.join(", ")} present). Existing Gateway setup remains RECORDED_ONLY.`
49
60
  : "No existing private Witnora Runtime reference manifest was found. Gateway setup remains RECORDED_ONLY until Hosted bootstrap can issue one-time Runtime references." };
50
61
  }
62
+ export function runtimeReferenceManifestPath(input) {
63
+ const root = dirname(credentialsPath({ configHome: input.configHome }));
64
+ if (!input.setupPlanId)
65
+ return join(root, "runtime-references.json");
66
+ const bindingId = createHash("sha256").update(JSON.stringify([normalizeServer(input.server), input.projectId, input.setupPlanId])).digest("hex");
67
+ return join(root, "runtime-bootstrap", bindingId, "runtime-references.json");
68
+ }
51
69
  function completeReferences(value) {
52
70
  const mandateId = text(value.mandateId);
53
71
  const runtimeIdentityId = text(value.runtimeIdentityId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.20.15",
3
+ "version": "0.20.17",
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",