witnora 0.10.4 → 0.12.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 +45 -12
- package/dist/cli.js +30 -0
- package/dist/command-help.js +25 -0
- package/dist/device-authorization.js +69 -0
- package/dist/gateway.js +256 -0
- package/dist/onboard.js +131 -65
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -13,18 +13,21 @@ outcome. It writes portable reports and accumulates a local failure corpus.
|
|
|
13
13
|
|
|
14
14
|
## 5-minute hosted path
|
|
15
15
|
|
|
16
|
-
Create a Hosted project
|
|
17
|
-
|
|
16
|
+
Create a Hosted project. The Setup Wizard pre-fills a metadata-only plan and
|
|
17
|
+
asks for two decisions: **Confirm plan** and **Authorize install**. Then open a
|
|
18
|
+
terminal in the agent repository and run the one command shown by Witnora:
|
|
18
19
|
|
|
19
20
|
```bash
|
|
20
21
|
npx witnora@latest onboard --project your-project-id
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
The browser approval creates
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
The browser approval creates one restricted project credential, then the CLI
|
|
25
|
+
saves it outside the repository, detects capability groups, and installs only
|
|
26
|
+
missing Gateway, outcome-probe, CI, policy, review, configuration, and boundary
|
|
27
|
+
files. It verifies each component and reports progress to Hosted. A failed
|
|
28
|
+
attempt removes only files created by that attempt. The onboarding receipt is
|
|
29
|
+
synthetic: it cannot create a run, evidence object, release decision, or
|
|
30
|
+
`CURRENT` assurance.
|
|
28
31
|
|
|
29
32
|
Onboarding also performs the default private capability discovery locally. It
|
|
30
33
|
does not read source-file contents or upload source code, prompt text,
|
|
@@ -40,12 +43,42 @@ Customers that do not permit repository metadata inspection can choose
|
|
|
40
43
|
runtime-only discovery in Hosted. Source-assisted analysis is a separate,
|
|
41
44
|
explicit GitHub authorization and is never enabled implicitly.
|
|
42
45
|
|
|
46
|
+
### Customer-owned Gateway
|
|
47
|
+
|
|
48
|
+
The default Setup Wizard installs a durable customer-owned Gateway beside the
|
|
49
|
+
Agent automatically. Start it, then run the Agent's normal sandbox workflow:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx witnora@latest gateway run
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Initialization writes a reusable `.witnora/gateway/client.mjs`. Import its
|
|
56
|
+
`witnoraGateway.start`, `event`, and `complete` methods at one existing sandbox
|
|
57
|
+
workflow boundary. The generated README contains the exact code and privacy
|
|
58
|
+
boundary; no Hosted key or source-signing key is written into the repository.
|
|
59
|
+
|
|
60
|
+
The browser approval issues one bounded credential for setup, evidence, the
|
|
61
|
+
customer-owned recorder, and covered runtime actions. It has no team,
|
|
62
|
+
retention, administrative, target-system, or customer-data permissions. The
|
|
63
|
+
Hosted key is stored in the user's Witnora credential directory; the repository
|
|
64
|
+
contains only metadata-only config plus ignored local secrets and queue data.
|
|
65
|
+
This reference path establishes **RECORDED** evidence.
|
|
66
|
+
It does not claim **ENFORCED** until target write credentials are placed behind a
|
|
67
|
+
controlled execution adapter, or **OUTCOME VERIFIED** until an independent
|
|
68
|
+
read-only probe checks the target state.
|
|
69
|
+
|
|
70
|
+
Enterprises that require self-hosted or manual component control can use the
|
|
71
|
+
individual `gateway init`, `gateway doctor`, policy, probe, CI, and review
|
|
72
|
+
commands under Hosted **Advanced**. They are not part of the default customer
|
|
73
|
+
journey.
|
|
74
|
+
|
|
43
75
|
After the connection self-test, Hosted shows one template-specific next step.
|
|
44
|
-
Place the generated
|
|
45
|
-
normally. The
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
Place the generated Gateway client at one meaningful sandbox boundary and run
|
|
77
|
+
it normally. The Gateway reuses the saved restricted connection; CI and
|
|
78
|
+
production should use the same project-scoped values from a secret manager.
|
|
79
|
+
Only a source-signed Gateway run whose journal is reconciled by Hosted completes
|
|
80
|
+
onboarding. A direct run is still visible, but cannot verify the Gateway or
|
|
81
|
+
establish a release decision or `CURRENT` assurance.
|
|
49
82
|
|
|
50
83
|
Without an agent repository, run `npx witnora@latest try --template workflow`
|
|
51
84
|
for an offline-only sample.
|
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ import { runDesignPartnerCommand } from "./design-partner-v02.js";
|
|
|
38
38
|
import { runOnboard } from "./onboard.js";
|
|
39
39
|
import { inspectRepository } from "./onboard.js";
|
|
40
40
|
import { runPrivateCapabilityDiscovery } from "./private-discovery.js";
|
|
41
|
+
import { doctorCustomerGateway, initializeCustomerGateway, renderGatewayDoctor, runCustomerGateway } from "./gateway.js";
|
|
41
42
|
import { verifyEvidencePacketV02 } from "./evidence-v02.js";
|
|
42
43
|
process.on("uncaughtException", reportFatalError);
|
|
43
44
|
process.on("unhandledRejection", reportFatalError);
|
|
@@ -157,6 +158,35 @@ else if (command === "onboard") {
|
|
|
157
158
|
openBrowser: readBoolFlag("--no-browser") ? async () => { } : undefined,
|
|
158
159
|
});
|
|
159
160
|
}
|
|
161
|
+
else if (command === "gateway") {
|
|
162
|
+
const action = process.argv[3] ?? "help";
|
|
163
|
+
if (action === "init") {
|
|
164
|
+
const projectId = readFlag("--project") ?? brandedEnvironment("PROJECT_ID");
|
|
165
|
+
if (!projectId)
|
|
166
|
+
throw new Error("--project <project-id> is required.");
|
|
167
|
+
await initializeCustomerGateway({
|
|
168
|
+
projectId,
|
|
169
|
+
server: readFlag("--server") ?? brandedEnvironment("BASE_URL") ?? DEFAULT_WITNORA_SERVER,
|
|
170
|
+
repository: readFlag("--repo") ?? process.cwd(),
|
|
171
|
+
outDir: readFlag("--dir"),
|
|
172
|
+
connectionName: readFlag("--name"),
|
|
173
|
+
force: readBoolFlag("--force"),
|
|
174
|
+
openBrowser: readBoolFlag("--no-browser") ? async () => { } : undefined,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
else if (action === "doctor") {
|
|
178
|
+
const result = await doctorCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
|
|
179
|
+
process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderGatewayDoctor(result));
|
|
180
|
+
if (result.overall !== "READY_TO_RECORD")
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
}
|
|
183
|
+
else if (action === "run") {
|
|
184
|
+
await runCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
throw new Error("Use witnora gateway init|doctor|run.");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
160
190
|
else if (command === "discover") {
|
|
161
191
|
const connectionName = readFlag("--connection");
|
|
162
192
|
const connection = await resolveConnection({
|
package/dist/command-help.js
CHANGED
|
@@ -32,6 +32,31 @@ Options:
|
|
|
32
32
|
--repo <directory> Repository to configure (default: current directory)
|
|
33
33
|
--template <type> Override automatic repository detection
|
|
34
34
|
--no-browser Print the approval URL without opening it
|
|
35
|
+
`;
|
|
36
|
+
if (command === "gateway")
|
|
37
|
+
return `Usage:
|
|
38
|
+
witnora gateway init --project <project-id>
|
|
39
|
+
witnora gateway doctor
|
|
40
|
+
witnora gateway run
|
|
41
|
+
|
|
42
|
+
Initializes and runs a customer-owned, metadata-only collector beside the Agent.
|
|
43
|
+
The browser approval issues a collector-scoped credential; no API key is copied into
|
|
44
|
+
the repository or exposed to the Agent. The local queue and source signing key remain
|
|
45
|
+
under customer control.
|
|
46
|
+
|
|
47
|
+
The reference Gateway establishes RECORDED evidence only. ENFORCED requires target
|
|
48
|
+
write credentials behind a controlled execution adapter. OUTCOME VERIFIED requires a
|
|
49
|
+
separate read-only probe.
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--server <url> Hosted server (default: https://witnora.com)
|
|
53
|
+
--project <id> Project to authorize (required for init)
|
|
54
|
+
--repo <directory> Agent repository (default: current directory)
|
|
55
|
+
--dir <directory> Gateway directory (default: .witnora/gateway)
|
|
56
|
+
--name <name> Saved collector connection name
|
|
57
|
+
--no-browser Print the approval URL without opening it
|
|
58
|
+
--force Replace an existing reviewed local setup
|
|
59
|
+
--json JSON doctor output
|
|
35
60
|
`;
|
|
36
61
|
if (command === "design-partner")
|
|
37
62
|
return `Usage:
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { DEFAULT_WITNORA_SERVER, saveConnection } from "./credentials.js";
|
|
4
|
+
export async function authorizeProjectConnection(options) {
|
|
5
|
+
const requestFetch = options.fetch ?? fetch;
|
|
6
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds)));
|
|
7
|
+
const output = options.output ?? ((message) => process.stdout.write(message));
|
|
8
|
+
const server = normalizeServer(options.server ?? DEFAULT_WITNORA_SERVER);
|
|
9
|
+
const codeVerifier = randomBytes(48).toString("base64url");
|
|
10
|
+
const codeChallengeSha256 = createHash("sha256").update(codeVerifier).digest("hex");
|
|
11
|
+
const device = await jsonRequest(requestFetch, `${server}/v1/onboarding/device-authorizations`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "content-type": "application/json" },
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
projectId: options.projectId,
|
|
16
|
+
connectionName: options.connectionName,
|
|
17
|
+
credentialProfile: options.credentialProfile ?? "repository",
|
|
18
|
+
codeChallengeSha256,
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
output(`Authorize Witnora in your browser:\n ${device.verificationUriComplete}\nCode: ${device.userCode}\n`);
|
|
22
|
+
await (options.openBrowser ?? openSystemBrowser)(device.verificationUriComplete);
|
|
23
|
+
const token = await waitForToken(requestFetch, server, device, codeVerifier, sleep, options.timeoutMs ?? device.expiresIn * 1_000);
|
|
24
|
+
const credentialsPath = await saveConnection(token.connectionName, {
|
|
25
|
+
server,
|
|
26
|
+
projectId: token.projectId,
|
|
27
|
+
apiKey: token.apiKey,
|
|
28
|
+
}, { configHome: options.configHome });
|
|
29
|
+
return { ...token, server, credentialsPath };
|
|
30
|
+
}
|
|
31
|
+
async function waitForToken(requestFetch, server, device, codeVerifier, sleep, timeoutMs) {
|
|
32
|
+
const deadline = Date.now() + timeoutMs;
|
|
33
|
+
while (Date.now() < deadline) {
|
|
34
|
+
const response = await requestFetch(`${server}/v1/onboarding/device-authorizations/token`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ deviceCode: device.deviceCode, codeVerifier }),
|
|
38
|
+
});
|
|
39
|
+
const body = await response.json().catch(() => ({}));
|
|
40
|
+
if (response.ok)
|
|
41
|
+
return body;
|
|
42
|
+
if (response.status !== 428 || body.code !== "authorization_pending") {
|
|
43
|
+
throw new Error(String(body.error ?? `Device authorization failed with HTTP ${response.status}.`));
|
|
44
|
+
}
|
|
45
|
+
await sleep(Math.max(1, device.interval) * 1_000);
|
|
46
|
+
}
|
|
47
|
+
throw new Error("Device authorization timed out. Run the command again to create a new one-time request.");
|
|
48
|
+
}
|
|
49
|
+
async function jsonRequest(requestFetch, url, init) {
|
|
50
|
+
const response = await requestFetch(url, init);
|
|
51
|
+
const body = await response.json().catch(() => ({}));
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
throw new Error(String(body.error ?? `${init.method ?? "GET"} ${url} failed with HTTP ${response.status}.`));
|
|
54
|
+
return body;
|
|
55
|
+
}
|
|
56
|
+
async function openSystemBrowser(url) {
|
|
57
|
+
const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
58
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
59
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
60
|
+
child.unref();
|
|
61
|
+
}
|
|
62
|
+
function normalizeServer(value) {
|
|
63
|
+
const url = new URL(value);
|
|
64
|
+
const loopback = new Set(["localhost", "127.0.0.1", "[::1]"]).has(url.hostname);
|
|
65
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
66
|
+
throw new Error("Witnora server must use HTTPS. Plain HTTP is allowed only for localhost development.");
|
|
67
|
+
}
|
|
68
|
+
return url.toString().replace(/\/$/, "");
|
|
69
|
+
}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { access, chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { loadConnection } from "./credentials.js";
|
|
5
|
+
import { authorizeProjectConnection } from "./device-authorization.js";
|
|
6
|
+
const CONFIG_SCHEMA = "witnora.customer_gateway_setup.v0.1";
|
|
7
|
+
const SECRETS_SCHEMA = "witnora.customer_gateway_local_secrets.v0.1";
|
|
8
|
+
export async function initializeCustomerGateway(options) {
|
|
9
|
+
const repository = resolve(options.repository ?? process.cwd());
|
|
10
|
+
const outDir = resolve(repository, options.outDir ?? ".witnora/gateway");
|
|
11
|
+
const output = options.output ?? ((message) => process.stdout.write(message));
|
|
12
|
+
const slug = safeSlug(basename(repository));
|
|
13
|
+
const connectionName = options.connectionName ?? `${slug}-gateway`;
|
|
14
|
+
const [configPath, secretsPath, gitignorePath, clientPath, readmePath] = gatewayPaths(outDir);
|
|
15
|
+
if (!(options.force ?? false)) {
|
|
16
|
+
const { existing } = await inspectCustomerGatewayFiles({ repository, outDir });
|
|
17
|
+
if (existing.length > 0) {
|
|
18
|
+
throw new Error(`Gateway setup already exists at ${existing.join(", ")}. Use --force only after reviewing the existing local setup.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const authorization = options.authorization
|
|
22
|
+
? options.authorization
|
|
23
|
+
: await authorizeProjectConnection({
|
|
24
|
+
projectId: options.projectId,
|
|
25
|
+
connectionName,
|
|
26
|
+
credentialProfile: "customer_gateway",
|
|
27
|
+
server: options.server,
|
|
28
|
+
openBrowser: options.openBrowser,
|
|
29
|
+
fetch: options.fetch,
|
|
30
|
+
sleep: options.sleep,
|
|
31
|
+
timeoutMs: options.timeoutMs,
|
|
32
|
+
output,
|
|
33
|
+
configHome: options.configHome,
|
|
34
|
+
});
|
|
35
|
+
const expectedScopes = ["runs:read", "events:write", "collector:manage"];
|
|
36
|
+
const missingScopes = expectedScopes.filter((scope) => !authorization.scopes.includes(scope));
|
|
37
|
+
if (missingScopes.length > 0)
|
|
38
|
+
throw new Error(`Gateway authorization is missing required scope(s): ${missingScopes.join(", ")}.`);
|
|
39
|
+
const config = {
|
|
40
|
+
schemaVersion: CONFIG_SCHEMA,
|
|
41
|
+
projectId: authorization.projectId,
|
|
42
|
+
server: authorization.server,
|
|
43
|
+
connectionName: authorization.connectionName,
|
|
44
|
+
collectorId: `${slug}-collector`,
|
|
45
|
+
host: process.env.WITNORA_GATEWAY_HOST?.trim() || "127.0.0.1",
|
|
46
|
+
port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, 8787),
|
|
47
|
+
storageDirectory: "data",
|
|
48
|
+
privacyMode: "metadata_only",
|
|
49
|
+
coverage: { recorded: "configured", enforced: "not_configured", outcomeVerified: "not_configured" },
|
|
50
|
+
};
|
|
51
|
+
const secrets = {
|
|
52
|
+
schemaVersion: SECRETS_SCHEMA,
|
|
53
|
+
gatewayToken: randomBytes(32).toString("base64url"),
|
|
54
|
+
};
|
|
55
|
+
await mkdir(outDir, { recursive: true });
|
|
56
|
+
const generatedFiles = [];
|
|
57
|
+
const force = options.force ?? false;
|
|
58
|
+
try {
|
|
59
|
+
for (const [path, content, mode] of [
|
|
60
|
+
[configPath, `${JSON.stringify(config, null, 2)}\n`, 0o644],
|
|
61
|
+
[secretsPath, `${JSON.stringify(secrets, null, 2)}\n`, 0o600],
|
|
62
|
+
[gitignorePath, "secrets.json\ndata/\n", 0o644],
|
|
63
|
+
[clientPath, gatewayClient(config), 0o644],
|
|
64
|
+
[readmePath, gatewayReadme(config), 0o644],
|
|
65
|
+
]) {
|
|
66
|
+
await writeExclusive(path, content, force, mode);
|
|
67
|
+
if (!force)
|
|
68
|
+
generatedFiles.push(path);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!force) {
|
|
73
|
+
for (const path of [...generatedFiles].reverse())
|
|
74
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
output("\nCustomer-owned Gateway initialized.\n");
|
|
79
|
+
output(`Configuration: ${configPath}\nLocal secret: ${secretsPath} (never commit or upload)\n`);
|
|
80
|
+
output("Next:\n");
|
|
81
|
+
output(" 1. Run: npx witnora@latest gateway doctor\n");
|
|
82
|
+
output(" 2. Run: npx witnora@latest gateway run\n");
|
|
83
|
+
output(" 3. Import .witnora/gateway/client.mjs at one sandbox workflow boundary.\n");
|
|
84
|
+
output(" 4. Run that workflow normally; start, event, and complete are recorded locally.\n");
|
|
85
|
+
output("This reference path establishes RECORDED evidence only. ENFORCED requires target write credentials behind a controlled execution adapter; OUTCOME VERIFIED requires a separate read-only probe.\n");
|
|
86
|
+
return { configPath, secretsPath, config, generatedFiles };
|
|
87
|
+
}
|
|
88
|
+
export async function inspectCustomerGatewayFiles(options) {
|
|
89
|
+
const repository = resolve(options.repository ?? process.cwd());
|
|
90
|
+
const directory = resolve(repository, options.outDir ?? ".witnora/gateway");
|
|
91
|
+
const paths = gatewayPaths(directory);
|
|
92
|
+
const states = await Promise.all(paths.map(async (path) => ({ path, exists: await exists(path) })));
|
|
93
|
+
const existing = states.filter((item) => item.exists).map((item) => item.path);
|
|
94
|
+
const missing = states.filter((item) => !item.exists).map((item) => item.path);
|
|
95
|
+
return {
|
|
96
|
+
status: existing.length === 0 ? "absent" : missing.length === 0 ? "complete" : "partial",
|
|
97
|
+
directory,
|
|
98
|
+
paths,
|
|
99
|
+
existing,
|
|
100
|
+
missing,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export async function doctorCustomerGateway(options) {
|
|
104
|
+
const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
|
|
105
|
+
const checks = [];
|
|
106
|
+
let config;
|
|
107
|
+
let secrets;
|
|
108
|
+
try {
|
|
109
|
+
config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
|
|
110
|
+
checks.push({ id: "configuration", status: "PASS", message: "Gateway configuration is valid and metadata-only." });
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
checks.push({ id: "configuration", status: "FAIL", message: message(error) });
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
secrets = parseSecrets(await readFile(join(directory, "secrets.json"), "utf8"));
|
|
117
|
+
checks.push({ id: "local_auth", status: "PASS", message: "A local Gateway token is present in the ignored customer-owned secret file." });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
checks.push({ id: "local_auth", status: "FAIL", message: message(error) });
|
|
121
|
+
}
|
|
122
|
+
if (config) {
|
|
123
|
+
try {
|
|
124
|
+
const connection = await loadConnection(config.connectionName, { configHome: options.configHome });
|
|
125
|
+
if (!connection || connection.projectId !== config.projectId || connection.server !== config.server)
|
|
126
|
+
throw new Error("The saved Gateway credential does not match gateway.json.");
|
|
127
|
+
checks.push({ id: "hosted_credential", status: "PASS", message: "The collector-scoped Hosted credential is stored outside the repository." });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
checks.push({ id: "hosted_credential", status: "FAIL", message: message(error) });
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const response = await (options.fetch ?? fetch)(`http://${config.host}:${config.port}/healthz`, { signal: AbortSignal.timeout(800) });
|
|
134
|
+
if (!response.ok)
|
|
135
|
+
throw new Error(`Gateway health returned HTTP ${response.status}.`);
|
|
136
|
+
checks.push({ id: "process", status: "PASS", message: `Gateway is listening at http://${config.host}:${config.port}.` });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
checks.push({ id: "process", status: "WARN", message: "Gateway is not running yet. Start it with `witnora gateway run`." });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (config && secrets) {
|
|
143
|
+
checks.push({ id: "enforcement", status: "WARN", message: "Write-credential mediation is not configured; current evidence ceiling is RECORDED." });
|
|
144
|
+
checks.push({ id: "outcome_probe", status: "WARN", message: "An independent read-only outcome probe is not configured." });
|
|
145
|
+
}
|
|
146
|
+
const failed = checks.some((check) => check.status === "FAIL");
|
|
147
|
+
return {
|
|
148
|
+
schemaVersion: "witnora.customer_gateway_doctor.v0.1",
|
|
149
|
+
overall: failed ? "SETUP_INCOMPLETE" : "READY_TO_RECORD",
|
|
150
|
+
checks,
|
|
151
|
+
evidenceCeiling: "recorded",
|
|
152
|
+
nextAction: failed ? "Run `witnora gateway init --project <project-id>` again after resolving failed checks." : "Start the Gateway, then send one sandbox run through its local event API.",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export async function runCustomerGateway(options) {
|
|
156
|
+
const { CustomerSourceKeyRing, RemoteCollectorClient, startCustomerOwnedCollectorGateway, } = await import("agentcert-sdk");
|
|
157
|
+
const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
|
|
158
|
+
const config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
|
|
159
|
+
const secrets = parseSecrets(await readFile(join(directory, "secrets.json"), "utf8"));
|
|
160
|
+
const connection = await loadConnection(config.connectionName, { configHome: options.configHome });
|
|
161
|
+
if (!connection || connection.projectId !== config.projectId || connection.server !== config.server) {
|
|
162
|
+
throw new Error("The saved Gateway credential does not match gateway.json. Run gateway init again.");
|
|
163
|
+
}
|
|
164
|
+
const dataDirectory = resolve(directory, config.storageDirectory);
|
|
165
|
+
const keyRingPath = join(dataDirectory, "source-keys.json");
|
|
166
|
+
const keyRing = await (await exists(keyRingPath)
|
|
167
|
+
? CustomerSourceKeyRing.open(keyRingPath)
|
|
168
|
+
: CustomerSourceKeyRing.create(keyRingPath, config.collectorId));
|
|
169
|
+
const gateway = await startCustomerOwnedCollectorGateway({
|
|
170
|
+
client: new RemoteCollectorClient({ baseUrl: connection.server, projectId: connection.projectId, apiKey: connection.apiKey }),
|
|
171
|
+
keyRing,
|
|
172
|
+
gatewayToken: secrets.gatewayToken,
|
|
173
|
+
storageDirectory: dataDirectory,
|
|
174
|
+
host: process.env.WITNORA_GATEWAY_HOST?.trim() || config.host,
|
|
175
|
+
port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, config.port),
|
|
176
|
+
environment: "customer-owned",
|
|
177
|
+
});
|
|
178
|
+
process.stdout.write(`Witnora customer-owned Gateway listening on ${gateway.baseUrl}\n`);
|
|
179
|
+
process.stdout.write("Evidence ceiling: RECORDED. No target write credential or outcome-probe credential is loaded by this reference process.\n");
|
|
180
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
181
|
+
process.once(signal, () => void gateway.close().finally(() => process.exit(0)));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function renderGatewayDoctor(result) {
|
|
185
|
+
return [
|
|
186
|
+
`Customer-owned Gateway: ${result.overall}`,
|
|
187
|
+
...result.checks.map((check) => `${check.status.padEnd(4)} ${check.id}: ${check.message}`),
|
|
188
|
+
`Evidence ceiling: ${result.evidenceCeiling.toUpperCase()}`,
|
|
189
|
+
`Next: ${result.nextAction}`,
|
|
190
|
+
"",
|
|
191
|
+
].join("\n");
|
|
192
|
+
}
|
|
193
|
+
function parseConfig(raw) {
|
|
194
|
+
const value = JSON.parse(raw);
|
|
195
|
+
if (value.schemaVersion !== CONFIG_SCHEMA || !value.projectId || !value.server || !value.connectionName || !value.collectorId) {
|
|
196
|
+
throw new Error("gateway.json is missing required Witnora Gateway fields.");
|
|
197
|
+
}
|
|
198
|
+
if (value.privacyMode !== "metadata_only")
|
|
199
|
+
throw new Error("Gateway privacyMode must remain metadata_only.");
|
|
200
|
+
if (!value.host || !Number.isSafeInteger(value.port) || Number(value.port) < 1 || Number(value.port) > 65_535)
|
|
201
|
+
throw new Error("Gateway host and port are invalid.");
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
function parseSecrets(raw) {
|
|
205
|
+
const value = JSON.parse(raw);
|
|
206
|
+
if (value.schemaVersion !== SECRETS_SCHEMA || typeof value.gatewayToken !== "string" || value.gatewayToken.length < 32) {
|
|
207
|
+
throw new Error("secrets.json is missing a valid local Gateway token.");
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
function gatewayPort(value, fallback) {
|
|
212
|
+
if (!value?.trim())
|
|
213
|
+
return fallback;
|
|
214
|
+
const parsed = Number(value);
|
|
215
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535) {
|
|
216
|
+
throw new Error("WITNORA_GATEWAY_PORT must be an integer between 1 and 65535.");
|
|
217
|
+
}
|
|
218
|
+
return parsed;
|
|
219
|
+
}
|
|
220
|
+
function gatewayReadme(config) {
|
|
221
|
+
return `# Witnora customer-owned Gateway\n\nThis directory configures a metadata-only Gateway for project \`${config.projectId}\`.\n\n## Start the Gateway\n\n\`\`\`bash\nnpx witnora@latest gateway doctor\nnpx witnora@latest gateway run\n\`\`\`\n\nKeep \`gateway run\` open. 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\` or \`data/\`.\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`;
|
|
222
|
+
}
|
|
223
|
+
function gatewayClient(config) {
|
|
224
|
+
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`;
|
|
225
|
+
}
|
|
226
|
+
async function writeExclusive(path, content, force, mode) {
|
|
227
|
+
if (!force && await exists(path))
|
|
228
|
+
throw new Error(`${path} already exists. Use --force only after reviewing the existing local Gateway setup.`);
|
|
229
|
+
await mkdir(dirname(path), { recursive: true });
|
|
230
|
+
await writeFile(path, content, { encoding: "utf8", mode });
|
|
231
|
+
await chmod(path, mode).catch(() => undefined);
|
|
232
|
+
}
|
|
233
|
+
async function exists(path) {
|
|
234
|
+
try {
|
|
235
|
+
await access(path);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function gatewayPaths(directory) {
|
|
243
|
+
return [
|
|
244
|
+
join(directory, "gateway.json"),
|
|
245
|
+
join(directory, "secrets.json"),
|
|
246
|
+
join(directory, ".gitignore"),
|
|
247
|
+
join(directory, "client.mjs"),
|
|
248
|
+
join(directory, "README.md"),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
function safeSlug(value) {
|
|
252
|
+
return (value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent-repository").slice(0, 48);
|
|
253
|
+
}
|
|
254
|
+
function message(error) {
|
|
255
|
+
return error instanceof Error ? error.message : String(error);
|
|
256
|
+
}
|
package/dist/onboard.js
CHANGED
|
@@ -1,80 +1,152 @@
|
|
|
1
|
-
import { createHash
|
|
2
|
-
import {
|
|
3
|
-
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
-
import { DEFAULT_WITNORA_SERVER
|
|
4
|
+
import { DEFAULT_WITNORA_SERVER } from "./credentials.js";
|
|
5
|
+
import { authorizeProjectConnection } from "./device-authorization.js";
|
|
6
6
|
import { verifyControlPlaneConnection } from "./control-plane.js";
|
|
7
7
|
import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
|
|
8
|
-
import { parseAgentTemplate, starterAdapter,
|
|
8
|
+
import { parseAgentTemplate, starterAdapter, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
|
|
9
9
|
import { writeTryEvidence } from "./try.js";
|
|
10
|
+
import { doctorCustomerGateway, initializeCustomerGateway, inspectCustomerGatewayFiles } from "./gateway.js";
|
|
10
11
|
export async function runOnboard(options) {
|
|
11
12
|
const requestFetch = options.fetch ?? fetch;
|
|
12
|
-
const sleep = options.sleep ?? ((milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds)));
|
|
13
13
|
const output = options.output ?? ((message) => process.stdout.write(message));
|
|
14
14
|
const server = normalizeServer(options.server ?? DEFAULT_WITNORA_SERVER);
|
|
15
15
|
const repositoryPath = resolve(options.repository ?? process.cwd());
|
|
16
16
|
const repository = await inspectRepository(repositoryPath, options.template);
|
|
17
17
|
const connectionName = options.name ?? repository.slug;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const token = await authorizeProjectConnection({
|
|
19
|
+
projectId: options.projectId,
|
|
20
|
+
connectionName,
|
|
21
|
+
credentialProfile: "autopilot",
|
|
22
|
+
server,
|
|
23
|
+
openBrowser: options.openBrowser,
|
|
24
|
+
fetch: requestFetch,
|
|
25
|
+
sleep: options.sleep,
|
|
26
|
+
timeoutMs: options.timeoutMs,
|
|
27
|
+
output,
|
|
28
|
+
configHome: options.configHome,
|
|
23
29
|
});
|
|
24
|
-
|
|
25
|
-
await (options.openBrowser ?? openSystemBrowser)(device.verificationUriComplete);
|
|
26
|
-
const token = await waitForToken(requestFetch, server, device, codeVerifier, sleep, options.timeoutMs ?? device.expiresIn * 1_000);
|
|
27
|
-
const credentialsPath = await saveConnection(token.connectionName, { server, projectId: token.projectId, apiKey: token.apiKey }, { configHome: options.configHome });
|
|
30
|
+
const credentialsPath = token.credentialsPath;
|
|
28
31
|
await verifyControlPlaneConnection({ baseUrl: server, projectId: token.projectId, apiKey: token.apiKey, fetch: requestFetch });
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
template: repository.template,
|
|
32
|
-
subject: `witnora-onboarding-${repository.slug}`,
|
|
33
|
-
outDir: join(repositoryPath, ".witnora", "onboarding", "self-test"),
|
|
32
|
+
const planSnapshot = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/setup-plans`, {
|
|
33
|
+
method: "GET", headers: { authorization: `Bearer ${token.apiKey}` },
|
|
34
34
|
});
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const setupPlan = planSnapshot.plan;
|
|
36
|
+
if (!setupPlan || !setupPlan.authorizedOperations.some((item) => item.kind === "authorize_install")) {
|
|
37
|
+
throw new Error("Confirm the Setup Plan and authorize installation in the Witnora Workspace before running onboard.");
|
|
38
|
+
}
|
|
39
|
+
const attemptId = `install-${Date.now()}-${randomSuffix()}`;
|
|
40
|
+
const generatedFiles = [];
|
|
41
|
+
await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
|
|
42
|
+
try {
|
|
43
|
+
generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
|
|
44
|
+
const selfTestDir = join(repositoryPath, ".witnora", "onboarding", "self-test", attemptId);
|
|
45
|
+
const selfTest = await writeTryEvidence({
|
|
39
46
|
template: repository.template,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
subject: `witnora-onboarding-${repository.slug}`,
|
|
48
|
+
outDir: selfTestDir,
|
|
49
|
+
});
|
|
50
|
+
generatedFiles.push(selfTestDir);
|
|
51
|
+
const bundleBytes = Buffer.from(`${JSON.stringify(selfTest.bundle, null, 2)}\n`);
|
|
52
|
+
const receipt = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/onboarding/self-test`, {
|
|
53
|
+
method: "POST", headers: { authorization: `Bearer ${token.apiKey}`, "content-type": "application/json" },
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
template: repository.template,
|
|
56
|
+
repository: { kind: repository.kind, name: repository.name, fingerprintSha256: repository.fingerprintSha256 },
|
|
57
|
+
bundleSha256: createHash("sha256").update(bundleBytes).digest("hex"),
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
const receiptPath = join(repositoryPath, ".witnora", "onboarding", "receipts", `${attemptId}.json`);
|
|
61
|
+
await mkdir(dirname(receiptPath), { recursive: true });
|
|
62
|
+
await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
63
|
+
generatedFiles.push(receiptPath);
|
|
64
|
+
const legacyReceiptPath = join(repositoryPath, ".witnora", "onboarding", "receipt.json");
|
|
65
|
+
if (!await exists(legacyReceiptPath)) {
|
|
66
|
+
await writeFile(legacyReceiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
67
|
+
generatedFiles.push(legacyReceiptPath);
|
|
68
|
+
}
|
|
69
|
+
const discovery = await runPrivateCapabilityDiscovery({
|
|
70
|
+
connection: { server, projectId: token.projectId, apiKey: token.apiKey },
|
|
71
|
+
repository,
|
|
72
|
+
connectionName: token.connectionName,
|
|
73
|
+
configHome: options.configHome,
|
|
74
|
+
fetch: requestFetch,
|
|
75
|
+
});
|
|
76
|
+
const gatewayState = await inspectCustomerGatewayFiles({ repository: repositoryPath });
|
|
77
|
+
if (gatewayState.status === "partial") {
|
|
78
|
+
throw new Error(`Existing Gateway setup is incomplete. Preserve the existing files and repair or remove the partial setup in Advanced mode. Missing: ${gatewayState.missing.map((path) => basename(path)).join(", ")}.`);
|
|
79
|
+
}
|
|
80
|
+
if (gatewayState.status === "absent") {
|
|
81
|
+
const gateway = await initializeCustomerGateway({
|
|
82
|
+
projectId: token.projectId, server, repository: repositoryPath, authorization: token,
|
|
83
|
+
fetch: requestFetch, configHome: options.configHome, output,
|
|
84
|
+
});
|
|
85
|
+
generatedFiles.push(...gateway.generatedFiles);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
output("\nExisting complete customer-owned Gateway found; verifying and reusing it.\n");
|
|
89
|
+
}
|
|
90
|
+
generatedFiles.push(...await generateAutopilotFiles(repositoryPath, repository.name));
|
|
91
|
+
const doctor = await doctorCustomerGateway({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
|
|
92
|
+
if (doctor.overall !== "READY_TO_RECORD")
|
|
93
|
+
throw new Error(doctor.checks.filter((check) => check.status === "FAIL").map((check) => check.message).join(" "));
|
|
94
|
+
await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
|
|
95
|
+
status: "verified", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles),
|
|
68
96
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
output(`\nWitnora Setup Autopilot completed for ${repository.name}.\n`);
|
|
98
|
+
output(`Project: ${token.projectId}\nTemplate: ${repository.template} (${repository.kind})\n`);
|
|
99
|
+
output(`Credentials: ${credentialsPath}\nSelf-test receipt: ${receiptPath}\n`);
|
|
100
|
+
output(`Private discovery: ${discovery.capabilityCount} capability group(s); ${discovery.unknownCapabilityCount} pending confirmation.\n`);
|
|
101
|
+
output("Installed: customer-owned Gateway, default-deny policy, independent-probe contract, review contract, and PR/release/nightly CI.\n");
|
|
102
|
+
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
|
+
return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
|
|
104
|
+
repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
|
|
105
|
+
gatewayDirectory: join(repositoryPath, ".witnora", "gateway") };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const diagnosis = error instanceof Error ? error.message : String(error);
|
|
109
|
+
await rollbackGeneratedFiles(generatedFiles);
|
|
110
|
+
await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
|
|
111
|
+
status: "failed", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles), diagnosis, rolledBack: true,
|
|
112
|
+
}).catch(() => undefined);
|
|
113
|
+
throw new Error(`Witnora Setup Autopilot rolled back this install attempt: ${diagnosis}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function generateAutopilotFiles(repositoryPath, subject) {
|
|
117
|
+
const files = new Map([
|
|
118
|
+
[".witnora/setup/policy.json", `${JSON.stringify({ schemaVersion: "witnora.setup_policy.v0.1", subject, unknownCapabilities: "pending_confirmation", defaultDecision: "deny", environment: "sandbox" }, null, 2)}\n`],
|
|
119
|
+
[".witnora/setup/outcome-probe.json", `${JSON.stringify({ schemaVersion: "witnora.outcome_probe_contract.v0.1", mode: "independent_read_only", credentialEnv: "WITNORA_OUTCOME_PROBE_CREDENTIAL", status: "awaiting_customer_secret", uploadsCredential: false }, null, 2)}\n`],
|
|
120
|
+
[".witnora/setup/review.json", `${JSON.stringify({ schemaVersion: "witnora.review_contract.v0.1", independentReviewRequired: true, syntheticEvidenceEligible: false }, null, 2)}\n`],
|
|
121
|
+
[".github/workflows/witnora-assurance.yml", continuousAssuranceWorkflow()],
|
|
122
|
+
]);
|
|
123
|
+
const written = [];
|
|
124
|
+
for (const [relativePath, content] of files) {
|
|
125
|
+
const target = join(repositoryPath, relativePath);
|
|
126
|
+
if (await exists(target))
|
|
127
|
+
continue;
|
|
128
|
+
await mkdir(dirname(target), { recursive: true });
|
|
129
|
+
await writeFile(target, content);
|
|
130
|
+
written.push(target);
|
|
75
131
|
}
|
|
76
|
-
|
|
132
|
+
return written;
|
|
133
|
+
}
|
|
134
|
+
function continuousAssuranceWorkflow() {
|
|
135
|
+
return `name: Witnora assurance\non:\n pull_request:\n push:\n tags: [\"v*\"]\n schedule:\n - cron: \"17 3 * * *\"\n workflow_dispatch:\npermissions:\n contents: read\njobs:\n assurance:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npx witnora@latest release-gate --config witnora.config.json --strict\n`;
|
|
136
|
+
}
|
|
137
|
+
async function reportInstall(requestFetch, server, projectId, apiKey, setupPlanId, body) {
|
|
138
|
+
await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(projectId)}/setup-plans/${encodeURIComponent(setupPlanId)}/install-status`, {
|
|
139
|
+
method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, body: JSON.stringify(body),
|
|
140
|
+
});
|
|
77
141
|
}
|
|
142
|
+
async function rollbackGeneratedFiles(files) {
|
|
143
|
+
for (const path of [...files].reverse())
|
|
144
|
+
await rm(path, { recursive: true, force: true }).catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
function relativeGeneratedFiles(repositoryPath, files) {
|
|
147
|
+
return files.map((file) => file.startsWith(repositoryPath) ? file.slice(repositoryPath.length + 1).replaceAll("\\", "/") : file).filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
function randomSuffix() { return Math.random().toString(36).slice(2, 10); }
|
|
78
150
|
export async function inspectRepository(repositoryPath, explicitTemplate) {
|
|
79
151
|
const entries = await readdir(repositoryPath, { withFileTypes: true });
|
|
80
152
|
const names = entries.map((entry) => entry.name).sort();
|
|
@@ -124,12 +196,6 @@ async function jsonRequest(requestFetch, url, init) {
|
|
|
124
196
|
throw new Error(String(body.error ?? `${init.method ?? "GET"} ${url} failed with HTTP ${response.status}.`));
|
|
125
197
|
return body;
|
|
126
198
|
}
|
|
127
|
-
async function openSystemBrowser(url) {
|
|
128
|
-
const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
129
|
-
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
130
|
-
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
131
|
-
child.unref();
|
|
132
|
-
}
|
|
133
199
|
async function optionalJson(path) {
|
|
134
200
|
try {
|
|
135
201
|
return JSON.parse(await readFile(path, "utf8"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "witnora",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Independent assurance for covered agent action paths across models and frameworks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -66,5 +66,8 @@
|
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
68
|
"pg": "^8.16.3"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"agentcert-sdk": "0.5.0"
|
|
69
72
|
}
|
|
70
73
|
}
|