witnora 0.12.0 → 0.12.1

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.
Files changed (2) hide show
  1. package/dist/onboard.js +72 -4
  2. package/package.json +1 -1
package/dist/onboard.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
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 { DEFAULT_WITNORA_SERVER } from "./credentials.js";
4
+ import { DEFAULT_WITNORA_SERVER, saveConnection } from "./credentials.js";
5
5
  import { authorizeProjectConnection } from "./device-authorization.js";
6
6
  import { verifyControlPlaneConnection } from "./control-plane.js";
7
7
  import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
@@ -38,6 +38,7 @@ export async function runOnboard(options) {
38
38
  }
39
39
  const attemptId = `install-${Date.now()}-${randomSuffix()}`;
40
40
  const generatedFiles = [];
41
+ let gatewayMigration;
41
42
  await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
42
43
  try {
43
44
  generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
@@ -85,7 +86,21 @@ export async function runOnboard(options) {
85
86
  generatedFiles.push(...gateway.generatedFiles);
86
87
  }
87
88
  else {
88
- output("\nExisting complete customer-owned Gateway found; verifying and reusing it.\n");
89
+ const binding = await inspectGatewayBinding(gatewayState.directory, token.projectId, server);
90
+ if (binding.matches) {
91
+ await saveConnection(binding.connectionName, { server, projectId: token.projectId, apiKey: token.apiKey }, { configHome: options.configHome });
92
+ output("\nExisting customer-owned Gateway matches this project; verifying and reusing it.\n");
93
+ }
94
+ else {
95
+ await assertGatewayStopped(requestFetch, binding.host, binding.port, binding.projectId, token.projectId);
96
+ gatewayMigration = await archiveGateway(gatewayState.directory, repositoryPath, binding.projectId);
97
+ output(`\nExisting Gateway belongs to project ${binding.projectId}; archived it at ${gatewayMigration.archiveDirectory}.\n`);
98
+ const gateway = await initializeCustomerGateway({
99
+ projectId: token.projectId, server, repository: repositoryPath, authorization: token,
100
+ fetch: requestFetch, configHome: options.configHome, output,
101
+ });
102
+ generatedFiles.push(...gateway.generatedFiles);
103
+ }
89
104
  }
90
105
  generatedFiles.push(...await generateAutopilotFiles(repositoryPath, repository.name));
91
106
  const doctor = await doctorCustomerGateway({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
@@ -102,17 +117,70 @@ export async function runOnboard(options) {
102
117
  output("The self-test remains isolated. Start the generated Gateway and run the agent normally; the first source-signed, server-reconciled Gateway run completes onboarding.\n");
103
118
  return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
104
119
  repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
105
- gatewayDirectory: join(repositoryPath, ".witnora", "gateway") };
120
+ gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory };
106
121
  }
107
122
  catch (error) {
108
123
  const diagnosis = error instanceof Error ? error.message : String(error);
109
124
  await rollbackGeneratedFiles(generatedFiles);
125
+ if (gatewayMigration)
126
+ await restoreGateway(gatewayMigration);
110
127
  await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
111
128
  status: "failed", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles), diagnosis, rolledBack: true,
112
129
  }).catch(() => undefined);
113
130
  throw new Error(`Witnora Setup Autopilot rolled back this install attempt: ${diagnosis}`);
114
131
  }
115
132
  }
133
+ async function inspectGatewayBinding(directory, projectId, server) {
134
+ let value;
135
+ try {
136
+ value = JSON.parse(await readFile(join(directory, "gateway.json"), "utf8"));
137
+ }
138
+ catch {
139
+ throw new Error("Existing Gateway configuration is not valid JSON. Preserve it and repair the setup in Advanced mode.");
140
+ }
141
+ const existingProjectId = typeof value.projectId === "string" ? value.projectId.trim() : "";
142
+ const existingServer = typeof value.server === "string" ? normalizeServer(value.server) : "";
143
+ const connectionName = typeof value.connectionName === "string" ? value.connectionName.trim() : "";
144
+ const host = typeof value.host === "string" && value.host.trim() ? value.host.trim() : "127.0.0.1";
145
+ const port = Number(value.port);
146
+ if (!existingProjectId || !existingServer || !connectionName || !Number.isInteger(port) || port < 1 || port > 65_535) {
147
+ throw new Error("Existing Gateway configuration is missing a valid project, server, host, or port. Preserve it and repair the setup in Advanced mode.");
148
+ }
149
+ return { matches: existingProjectId === projectId && existingServer === server, projectId: existingProjectId, server: existingServer, connectionName, host, port };
150
+ }
151
+ async function assertGatewayStopped(requestFetch, host, port, oldProjectId, newProjectId) {
152
+ try {
153
+ const response = await requestFetch(`http://${host}:${port}/healthz`, { signal: AbortSignal.timeout(800) });
154
+ if (!response.ok)
155
+ return;
156
+ }
157
+ catch {
158
+ return;
159
+ }
160
+ throw new Error(`The running Gateway belongs to project ${oldProjectId}, not ${newProjectId}. Stop it with Ctrl+C, then run onboard again so Witnora can preserve the old evidence and bind this repository to the new project.`);
161
+ }
162
+ async function archiveGateway(gatewayDirectory, repositoryPath, projectId) {
163
+ const archiveRoot = join(repositoryPath, ".witnora", "gateway-archive");
164
+ const archiveDirectory = join(archiveRoot, `${Date.now()}-${safePathSegment(projectId)}-${randomSuffix()}`);
165
+ await mkdir(archiveRoot, { recursive: true });
166
+ const archiveIgnore = join(archiveRoot, ".gitignore");
167
+ if (!await exists(archiveIgnore))
168
+ await writeFile(archiveIgnore, "*\n!.gitignore\n");
169
+ try {
170
+ await rename(gatewayDirectory, archiveDirectory);
171
+ }
172
+ catch (error) {
173
+ throw new Error(`Could not archive the Gateway for project ${projectId}. Stop any running Gateway with Ctrl+C and retry. ${error instanceof Error ? error.message : String(error)}`);
174
+ }
175
+ return { gatewayDirectory, archiveDirectory };
176
+ }
177
+ async function restoreGateway(migration) {
178
+ await rm(migration.gatewayDirectory, { recursive: true, force: true });
179
+ await rename(migration.archiveDirectory, migration.gatewayDirectory);
180
+ }
181
+ function safePathSegment(value) {
182
+ return (value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-|-$/g, "") || "unknown-project").slice(0, 80);
183
+ }
116
184
  async function generateAutopilotFiles(repositoryPath, subject) {
117
185
  const files = new Map([
118
186
  [".witnora/setup/policy.json", `${JSON.stringify({ schemaVersion: "witnora.setup_policy.v0.1", subject, unknownCapabilities: "pending_confirmation", defaultDecision: "deny", environment: "sandbox" }, null, 2)}\n`],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",