witnora 0.18.10 → 0.18.12
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/dist/gateway.js +240 -26
- package/dist/internal/control-client/collector-gateway.d.ts +1 -1
- package/dist/internal/control-client/collector-gateway.d.ts.map +1 -1
- package/dist/internal/control-client/collector-gateway.js +2 -1
- package/dist/onboard.js +32 -6
- package/dist/real-path-activation.js +34 -13
- package/package.json +1 -1
package/dist/gateway.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash, createPrivateKey, createPublicKey, randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID } 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";
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
8
8
|
import { loadConnection } from "./credentials.js";
|
|
9
9
|
import { authorizeProjectConnection } from "./device-authorization.js";
|
|
10
10
|
import { verifyHostedProbeCredential } from "./runtime-bootstrap.js";
|
|
11
|
+
import { activateRealPathIntegrations } from "./real-path-activation.js";
|
|
11
12
|
import { inspectIsolatedOutcomeProbe, observeInIsolatedOutcomeProbe } from "./probe-process.js";
|
|
12
13
|
import { findAvailableRuntimeSandboxOrigin, LOCAL_SANDBOX_FIXTURE_CONTRACT_SHA256, startRuntimeSandboxFixture, } from "./runtime-sandbox-fixture.js";
|
|
13
14
|
import { generateRuntimeSandboxKit, LOCAL_SANDBOX_ADAPTER_ID, LOCAL_SANDBOX_ADAPTER_VERSION, LOCAL_SANDBOX_PROBE_ID, } from "./runtime-sandbox-kit.js";
|
|
@@ -60,6 +61,7 @@ export async function initializeCustomerGateway(options) {
|
|
|
60
61
|
port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, 8787),
|
|
61
62
|
storageDirectory: "data",
|
|
62
63
|
privacyMode: "metadata_only",
|
|
64
|
+
...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}),
|
|
63
65
|
coverage: runtimeKit
|
|
64
66
|
? { recorded: "configured", enforced: "configured", outcomeVerified: "configured" }
|
|
65
67
|
: { recorded: "configured", enforced: "not_configured", outcomeVerified: "not_configured" },
|
|
@@ -135,14 +137,20 @@ export async function upgradeCustomerGatewayRuntime(options) {
|
|
|
135
137
|
if (current.projectId !== options.authorization.projectId || current.server !== options.authorization.server)
|
|
136
138
|
throw new Error("Existing Gateway project/server binding does not match this authorized Runtime upgrade.");
|
|
137
139
|
const generatedClient = clientRaw === gatewayClient(current)
|
|
138
|
-
|| (!current.runtimeWorker && clientRaw === recordedGatewayClient(current)
|
|
140
|
+
|| (!current.runtimeWorker && (clientRaw === recordedGatewayClient(current)
|
|
141
|
+
|| clientRaw === recordedGatewayClient({ ...current, agentIdentity: undefined })));
|
|
139
142
|
if (!generatedClient || readmeRaw !== gatewayReadme(current))
|
|
140
143
|
throw new Error("Existing Gateway files differ from the Witnora-generated version; refusing to overwrite them.");
|
|
141
144
|
if (current.runtimeWorker && current.runtimeWorker.adapterId !== LOCAL_SANDBOX_ADAPTER_ID) {
|
|
142
145
|
throw new Error("Existing Gateway already has a different Runtime worker; preserve it and reconfigure in Advanced mode.");
|
|
143
146
|
}
|
|
144
147
|
const runtimeKit = await prepareRuntimeSandboxKit(options.runtimeReferences, options.authorization, options.configHome, options.fetch ?? fetch, directory);
|
|
145
|
-
const next = {
|
|
148
|
+
const next = {
|
|
149
|
+
...current,
|
|
150
|
+
...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}),
|
|
151
|
+
coverage: { recorded: "configured", enforced: "configured", outcomeVerified: "configured" },
|
|
152
|
+
runtimeWorker: runtimeKit.config,
|
|
153
|
+
};
|
|
146
154
|
next.runtimeWorker.configDigestSha256 = gatewayConfigDigest(next);
|
|
147
155
|
const replacingGeneratedRuntime = current.runtimeWorker?.adapterId === LOCAL_SANDBOX_ADAPTER_ID;
|
|
148
156
|
if (replacingGeneratedRuntime) {
|
|
@@ -154,7 +162,8 @@ export async function upgradeCustomerGatewayRuntime(options) {
|
|
|
154
162
|
|| createHash("sha256").update(probeBytes).digest("hex") !== current.runtimeWorker.probeModuleSha256) {
|
|
155
163
|
throw new Error("Existing generated Runtime modules were modified; refusing to overwrite them.");
|
|
156
164
|
}
|
|
157
|
-
if (sameRuntimeGeneration(current.runtimeWorker, next.runtimeWorker)
|
|
165
|
+
if (sameRuntimeGeneration(current.runtimeWorker, next.runtimeWorker)
|
|
166
|
+
&& JSON.stringify(current.agentIdentity) === JSON.stringify(next.agentIdentity)) {
|
|
158
167
|
return { config: current, generatedFiles: [], changed: false, rollback: async () => undefined };
|
|
159
168
|
}
|
|
160
169
|
}
|
|
@@ -509,10 +518,6 @@ export async function runCustomerGateway(options) {
|
|
|
509
518
|
throw new Error("The saved Gateway credential does not match gateway.json. Run gateway init again.");
|
|
510
519
|
}
|
|
511
520
|
const dataDirectory = resolve(directory, config.storageDirectory);
|
|
512
|
-
const workflowHarnessConfigPath = join(directory, "workflow-harness.json");
|
|
513
|
-
const workflowHarnessConfig = await exists(workflowHarnessConfigPath)
|
|
514
|
-
? parseManagedWorkflowHarnessConfig(await readFile(workflowHarnessConfigPath, "utf8"))
|
|
515
|
-
: undefined;
|
|
516
521
|
const keyRingPath = join(dataDirectory, "source-keys.json");
|
|
517
522
|
const keyRing = await (await exists(keyRingPath)
|
|
518
523
|
? CustomerSourceKeyRing.open(keyRingPath)
|
|
@@ -534,16 +539,10 @@ export async function runCustomerGateway(options) {
|
|
|
534
539
|
FileActionCheckpointStore: (await durableWorker).FileActionCheckpointStore,
|
|
535
540
|
})
|
|
536
541
|
: undefined;
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
server: config.server,
|
|
542
|
-
apiKey: connection.apiKey,
|
|
543
|
-
config: workflowHarnessConfig,
|
|
544
|
-
managed: await configManagedWorkflowHarnessImport(),
|
|
545
|
-
})
|
|
546
|
-
: undefined;
|
|
542
|
+
const assuranceController = await createContinuousAssuranceController({
|
|
543
|
+
repository: resolve(directory, "..", ".."), directory, projectId: config.projectId, server: config.server,
|
|
544
|
+
apiKey: connection.apiKey, config, sourceSigner: () => keyRing.activeSigner(),
|
|
545
|
+
});
|
|
547
546
|
actionWorker?.start();
|
|
548
547
|
const gateway = await startCustomerOwnedCollectorGateway({
|
|
549
548
|
client: new RemoteCollectorClient({ baseUrl: connection.server, projectId: connection.projectId, apiKey: connection.apiKey }),
|
|
@@ -592,19 +591,17 @@ export async function runCustomerGateway(options) {
|
|
|
592
591
|
fixtureReady: false,
|
|
593
592
|
} } : {}),
|
|
594
593
|
} } : {}),
|
|
595
|
-
|
|
596
|
-
? { assuranceHarness: { status: () => workflowHarness.status() } }
|
|
597
|
-
: {}),
|
|
594
|
+
assuranceHarness: { status: () => assuranceController.status() },
|
|
598
595
|
});
|
|
599
|
-
|
|
596
|
+
assuranceController.start();
|
|
600
597
|
process.stdout.write(`Witnora customer-owned Gateway listening on ${gateway.baseUrl}\n`);
|
|
601
598
|
process.stdout.write(config.runtimeWorker
|
|
602
599
|
? "Runtime worker: READY. Approved exact configured actions execute automatically, then use the separate read-only probe and Hosted signed receipt.\n"
|
|
603
600
|
: "Evidence ceiling: RECORDED. No exact target adapter and separate outcome probe are configured, so runtime writes remain fail-closed.\n");
|
|
604
|
-
if (
|
|
605
|
-
process.stdout.write("Managed Workflow Harness:
|
|
601
|
+
if (await assuranceController.status())
|
|
602
|
+
process.stdout.write("Managed Workflow Harness: ACTIVE. Approved task changes are detected and revalidated automatically.\n");
|
|
606
603
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
607
|
-
process.once(signal, () => void Promise.allSettled([gateway.close(),
|
|
604
|
+
process.once(signal, () => void Promise.allSettled([gateway.close(), assuranceController.close(), ...(sandboxFixture ? [sandboxFixture.close()] : [])]).finally(() => process.exit(0)));
|
|
608
605
|
}
|
|
609
606
|
}
|
|
610
607
|
catch (error) {
|
|
@@ -727,6 +724,210 @@ export async function createConfiguredWorkflowHarness(input) {
|
|
|
727
724
|
tick,
|
|
728
725
|
};
|
|
729
726
|
}
|
|
727
|
+
function configSignedChangeManifestImport() {
|
|
728
|
+
return import(new URL("./vendor/onegent-runtime/signed-change-manifest.js", import.meta.url).href);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Reconciles the existing approved-task, Real Path, Manifest, and Autopilot
|
|
732
|
+
* contracts from inside the already-running Gateway. No new service or durable
|
|
733
|
+
* orchestration state is introduced; local config remains the recoverable cache.
|
|
734
|
+
*/
|
|
735
|
+
export async function createContinuousAssuranceController(input) {
|
|
736
|
+
const request = input.fetch ?? fetch;
|
|
737
|
+
const managed = input.managed ?? await configManagedWorkflowHarnessImport();
|
|
738
|
+
let harness;
|
|
739
|
+
let harnessFingerprint;
|
|
740
|
+
let timer;
|
|
741
|
+
let reconciling = false;
|
|
742
|
+
let closing = false;
|
|
743
|
+
let lastReportedError;
|
|
744
|
+
const replaceHarness = async (config) => {
|
|
745
|
+
const fingerprint = createHash("sha256").update(JSON.stringify(config)).digest("hex");
|
|
746
|
+
if (harness && fingerprint === harnessFingerprint)
|
|
747
|
+
return;
|
|
748
|
+
const next = await createConfiguredWorkflowHarness({
|
|
749
|
+
directory: input.directory,
|
|
750
|
+
projectId: input.projectId,
|
|
751
|
+
server: input.server,
|
|
752
|
+
apiKey: input.apiKey,
|
|
753
|
+
config,
|
|
754
|
+
managed,
|
|
755
|
+
evaluatorKit: input.evaluatorKit,
|
|
756
|
+
fetch: request,
|
|
757
|
+
});
|
|
758
|
+
next.start();
|
|
759
|
+
const previous = harness;
|
|
760
|
+
harness = next;
|
|
761
|
+
harnessFingerprint = fingerprint;
|
|
762
|
+
await previous?.close();
|
|
763
|
+
};
|
|
764
|
+
const workflowHarnessPath = join(input.directory, "workflow-harness.json");
|
|
765
|
+
if (await exists(workflowHarnessPath)) {
|
|
766
|
+
try {
|
|
767
|
+
await replaceHarness(parseManagedWorkflowHarnessConfig(await readFile(workflowHarnessPath, "utf8")));
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
lastReportedError = message(error);
|
|
771
|
+
process.stderr.write(`Existing Assurance Harness will be repaired in the background: ${lastReportedError}\n`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const reconcile = async () => {
|
|
775
|
+
if (closing || reconciling)
|
|
776
|
+
return;
|
|
777
|
+
reconciling = true;
|
|
778
|
+
try {
|
|
779
|
+
const activation = await activateRealPathIntegrations({
|
|
780
|
+
repository: input.repository,
|
|
781
|
+
server: input.server,
|
|
782
|
+
projectId: input.projectId,
|
|
783
|
+
apiKey: input.apiKey,
|
|
784
|
+
fetch: request,
|
|
785
|
+
});
|
|
786
|
+
if (activation.state === "READY_TO_START") {
|
|
787
|
+
const configured = await activateManagedWorkflowHarness({
|
|
788
|
+
repository: input.repository,
|
|
789
|
+
modulePath: activation.modulePath,
|
|
790
|
+
realPathActivations: activation.activations,
|
|
791
|
+
previousGeneratedModuleSha256: activation.previousGeneratedModuleSha256,
|
|
792
|
+
});
|
|
793
|
+
await replaceHarness(configured.config);
|
|
794
|
+
}
|
|
795
|
+
await publishCurrentChangeManifests({ ...input, fetch: request }, harness ? await harness.status() : undefined);
|
|
796
|
+
lastReportedError = undefined;
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
const current = message(error).slice(0, 500);
|
|
800
|
+
if (current !== lastReportedError)
|
|
801
|
+
process.stderr.write(`Continuous Assurance reconciliation is waiting safely: ${current}\n`);
|
|
802
|
+
lastReportedError = current;
|
|
803
|
+
}
|
|
804
|
+
finally {
|
|
805
|
+
reconciling = false;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
return {
|
|
809
|
+
start() {
|
|
810
|
+
if (timer || closing)
|
|
811
|
+
return;
|
|
812
|
+
void reconcile();
|
|
813
|
+
timer = setInterval(() => void reconcile(), input.intervalMs ?? 15_000);
|
|
814
|
+
timer.unref?.();
|
|
815
|
+
},
|
|
816
|
+
reconcile,
|
|
817
|
+
status: () => harness?.status() ?? Promise.resolve(undefined),
|
|
818
|
+
async close() {
|
|
819
|
+
closing = true;
|
|
820
|
+
if (timer)
|
|
821
|
+
clearInterval(timer);
|
|
822
|
+
timer = undefined;
|
|
823
|
+
await harness?.close();
|
|
824
|
+
harness = undefined;
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
async function publishCurrentChangeManifests(input, heartbeat) {
|
|
829
|
+
const base = input.server.replace(/\/$/, "");
|
|
830
|
+
const [activationResponse, manifestResponse] = await Promise.all([
|
|
831
|
+
input.fetch(`${base}/v1/projects/${encodeURIComponent(input.projectId)}/nora/activation-plan`, { headers: { authorization: `Bearer ${input.apiKey}` }, redirect: "error", signal: AbortSignal.timeout(10_000) }),
|
|
832
|
+
input.fetch(`${base}/v1/projects/${encodeURIComponent(input.projectId)}/change-manifests`, { headers: { authorization: `Bearer ${input.apiKey}` }, redirect: "error", signal: AbortSignal.timeout(10_000) }),
|
|
833
|
+
]);
|
|
834
|
+
if (!activationResponse.ok)
|
|
835
|
+
throw new Error(`Assurance activation plan returned HTTP ${activationResponse.status}.`);
|
|
836
|
+
if (!manifestResponse.ok)
|
|
837
|
+
throw new Error(`Signed Change Manifest collection returned HTTP ${manifestResponse.status}.`);
|
|
838
|
+
const activation = await boundedResponseJson(activationResponse, 512 * 1024);
|
|
839
|
+
const collection = await boundedResponseJson(manifestResponse, 512 * 1024);
|
|
840
|
+
const targets = Array.isArray(activation.targets) ? activation.targets.filter(validActivationTarget) : [];
|
|
841
|
+
const manifests = Array.isArray(collection.manifests) ? collection.manifests.filter(validChangeManifestRecord) : [];
|
|
842
|
+
if (!targets.length)
|
|
843
|
+
return;
|
|
844
|
+
const packageDigest = await optionalFileDigest(join(input.repository, "package.json"));
|
|
845
|
+
const discoveryDigest = await optionalFileDigest(join(input.repository, "witnora.discovery.json"));
|
|
846
|
+
const policyRevision = createHash("sha256").update(JSON.stringify({ privacyMode: input.config.privacyMode, coverage: input.config.coverage })).digest("hex");
|
|
847
|
+
const explicitModelDigest = process.env.WITNORA_MODEL_DIGEST?.trim();
|
|
848
|
+
const explicitPromptWorkflowDigest = process.env.WITNORA_PROMPT_WORKFLOW_DIGEST?.trim();
|
|
849
|
+
const runtime = input.config.runtimeWorker;
|
|
850
|
+
const components = {
|
|
851
|
+
...(explicitModelDigest && /^[a-f0-9]{64}$/.test(explicitModelDigest) ? { modelDigest: explicitModelDigest } : {}),
|
|
852
|
+
...(validSha256(explicitPromptWorkflowDigest) ? { promptWorkflowDigest: explicitPromptWorkflowDigest } : packageDigest ? { promptWorkflowDigest: packageDigest } : {}),
|
|
853
|
+
...(discoveryDigest ? { toolManifestDigest: discoveryDigest } : {}),
|
|
854
|
+
policyRevision,
|
|
855
|
+
...(heartbeat ? { evaluatorIdentity: { id: "witnora-managed-evaluator", version: "1", digestSha256: heartbeat.evaluatorContractSha256 } } : {}),
|
|
856
|
+
...(runtime ? { adapterIdentity: { id: runtime.adapterId, version: runtime.adapterVersion, digestSha256: runtime.adapterModuleSha256 } } : {}),
|
|
857
|
+
};
|
|
858
|
+
const signed = input.signedChangeManifest ?? await configSignedChangeManifestImport();
|
|
859
|
+
const unique = new Map();
|
|
860
|
+
const exactBindings = heartbeat?.realPathActivations ?? [];
|
|
861
|
+
for (const target of targets) {
|
|
862
|
+
for (const environment of target.environments) {
|
|
863
|
+
if (!exactBindings.some((binding) => binding.agentId === target.agentId && binding.environment === environment))
|
|
864
|
+
continue;
|
|
865
|
+
unique.set(`${target.agentId}:${environment}`, { agentId: target.agentId, agentVersion: target.currentAgentVersion, environment });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
for (const target of unique.values()) {
|
|
869
|
+
const previous = manifests.find((manifest) => manifest.agentId === target.agentId && manifest.environment === target.environment);
|
|
870
|
+
if (previous && previous.agentVersion === target.agentVersion && sameManifestComponents(previous, components))
|
|
871
|
+
continue;
|
|
872
|
+
const observedAt = new Date().toISOString();
|
|
873
|
+
const payload = {
|
|
874
|
+
schemaVersion: "witnora.signed_change_manifest.v0.1",
|
|
875
|
+
manifestId: `gateway-${randomUUID()}`,
|
|
876
|
+
projectId: input.projectId,
|
|
877
|
+
agentId: target.agentId,
|
|
878
|
+
agentVersion: target.agentVersion,
|
|
879
|
+
environment: target.environment,
|
|
880
|
+
observedAt,
|
|
881
|
+
...components,
|
|
882
|
+
...(previous?.manifestDigestSha256 ? { previousManifestDigest: previous.manifestDigestSha256 } : {}),
|
|
883
|
+
};
|
|
884
|
+
const envelope = signed.createSignedChangeManifestEnvelope(payload, input.sourceSigner());
|
|
885
|
+
await signed.uploadSignedChangeManifest({ baseUrl: base, projectId: input.projectId, apiKey: input.apiKey, envelope, fetch: input.fetch });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function validActivationTarget(value) {
|
|
889
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
890
|
+
return false;
|
|
891
|
+
const target = value;
|
|
892
|
+
return typeof target.agentId === "string" && typeof target.currentAgentVersion === "string"
|
|
893
|
+
&& Array.isArray(target.environments) && target.environments.every((environment) => environment === "sandbox" || environment === "staging" || environment === "production");
|
|
894
|
+
}
|
|
895
|
+
function validChangeManifestRecord(value) {
|
|
896
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
897
|
+
return false;
|
|
898
|
+
const manifest = value;
|
|
899
|
+
return typeof manifest.agentId === "string" && typeof manifest.agentVersion === "string"
|
|
900
|
+
&& (manifest.environment === "sandbox" || manifest.environment === "staging" || manifest.environment === "production")
|
|
901
|
+
&& typeof manifest.manifestDigestSha256 === "string";
|
|
902
|
+
}
|
|
903
|
+
function sameManifestComponents(previous, next) {
|
|
904
|
+
return previous.modelDigest === next.modelDigest && previous.promptWorkflowDigest === next.promptWorkflowDigest
|
|
905
|
+
&& previous.toolManifestDigest === next.toolManifestDigest && previous.policyRevision === next.policyRevision
|
|
906
|
+
&& JSON.stringify(previous.evaluatorIdentity) === JSON.stringify(next.evaluatorIdentity)
|
|
907
|
+
&& JSON.stringify(previous.adapterIdentity) === JSON.stringify(next.adapterIdentity);
|
|
908
|
+
}
|
|
909
|
+
async function optionalFileDigest(path) {
|
|
910
|
+
try {
|
|
911
|
+
return createHash("sha256").update(await readFile(path)).digest("hex");
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
if (error.code === "ENOENT")
|
|
915
|
+
return undefined;
|
|
916
|
+
throw error;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function validSha256(value) {
|
|
920
|
+
return Boolean(value && /^[a-f0-9]{64}$/.test(value));
|
|
921
|
+
}
|
|
922
|
+
async function boundedResponseJson(response, maxBytes) {
|
|
923
|
+
const text = await response.text();
|
|
924
|
+
if (Buffer.byteLength(text) > maxBytes)
|
|
925
|
+
throw new Error("Hosted Assurance response exceeded its size limit.");
|
|
926
|
+
const value = JSON.parse(text);
|
|
927
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
928
|
+
throw new Error("Hosted Assurance response is invalid.");
|
|
929
|
+
return value;
|
|
930
|
+
}
|
|
730
931
|
async function startManagedLocalEvaluator(directory, config, evaluatorKit) {
|
|
731
932
|
const repository = resolve(directory, "..", "..");
|
|
732
933
|
const modulePath = resolve(repository, config.evaluatorModulePath);
|
|
@@ -1165,6 +1366,8 @@ function parseConfig(raw) {
|
|
|
1165
1366
|
}
|
|
1166
1367
|
if (value.privacyMode !== "metadata_only")
|
|
1167
1368
|
throw new Error("Gateway privacyMode must remain metadata_only.");
|
|
1369
|
+
if (value.agentIdentity)
|
|
1370
|
+
validateGatewayAgentIdentity(value.agentIdentity);
|
|
1168
1371
|
if (!value.host || !Number.isSafeInteger(value.port) || Number(value.port) < 1 || Number(value.port) > 65_535)
|
|
1169
1372
|
throw new Error("Gateway host and port are invalid.");
|
|
1170
1373
|
if (value.runtimeWorker)
|
|
@@ -1175,6 +1378,13 @@ function parseConfig(raw) {
|
|
|
1175
1378
|
}
|
|
1176
1379
|
return config;
|
|
1177
1380
|
}
|
|
1381
|
+
function validateGatewayAgentIdentity(value) {
|
|
1382
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1383
|
+
if (typeof item !== "string" || item.trim().length === 0 || item.length > 200) {
|
|
1384
|
+
throw new Error(`Gateway agentIdentity.${key} must be a non-empty string no longer than 200 characters.`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1178
1388
|
export function parseManagedWorkflowHarnessConfig(raw) {
|
|
1179
1389
|
const value = JSON.parse(raw);
|
|
1180
1390
|
const workflowIds = value.workflowIds;
|
|
@@ -1459,7 +1669,11 @@ async function ed25519PublicKeyPem(handle) {
|
|
|
1459
1669
|
return createPublicKey(key).export({ type: "spki", format: "pem" }).toString();
|
|
1460
1670
|
}
|
|
1461
1671
|
function recordedGatewayClient(config) {
|
|
1462
|
-
|
|
1672
|
+
const defaultAgent = config.agentIdentity ? JSON.stringify(config.agentIdentity) : undefined;
|
|
1673
|
+
const startPayload = defaultAgent
|
|
1674
|
+
? `const payload = { ...metadata, agent: metadata.agent ?? ${defaultAgent} };\n return post(runId, "start", { payload, idempotencyKey: "run-start" });`
|
|
1675
|
+
: `return post(runId, "start", { payload: metadata, idempotencyKey: "run-start" });`;
|
|
1676
|
+
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 ${startPayload}\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`;
|
|
1463
1677
|
}
|
|
1464
1678
|
async function writeExclusive(path, content, force, mode) {
|
|
1465
1679
|
if (!force && await exists(path))
|
|
@@ -48,7 +48,7 @@ export interface CustomerOwnedCollectorGatewayOptions {
|
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
50
|
assuranceHarness?: {
|
|
51
|
-
status(): Promise<AssuranceHarnessHeartbeat>;
|
|
51
|
+
status(): Promise<AssuranceHarnessHeartbeat | undefined>;
|
|
52
52
|
};
|
|
53
53
|
runtimeBinding?: RuntimeReadinessBinding;
|
|
54
54
|
onRepairRequested?: (request: {
|
|
@@ -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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"}
|
|
@@ -76,6 +76,7 @@ export async function startCustomerOwnedCollectorGateway(options) {
|
|
|
76
76
|
return { delivered, reconciled, pending };
|
|
77
77
|
};
|
|
78
78
|
const status = async () => {
|
|
79
|
+
const assuranceHarness = options.assuranceHarness ? await options.assuranceHarness.status() : undefined;
|
|
79
80
|
let pendingRecordCount = 0;
|
|
80
81
|
let lastAckSequence;
|
|
81
82
|
for (const journal of journals.values()) {
|
|
@@ -94,7 +95,7 @@ export async function startCustomerOwnedCollectorGateway(options) {
|
|
|
94
95
|
lastRemoteSuccessAt,
|
|
95
96
|
lastRemoteError,
|
|
96
97
|
...(options.actionWorker ? { actionWorker: await options.actionWorker.status() } : {}),
|
|
97
|
-
...(
|
|
98
|
+
...(assuranceHarness ? { assuranceHarness } : {}),
|
|
98
99
|
...(monitoringControlFor() ? { monitoring: monitoringControlFor() } : {}),
|
|
99
100
|
};
|
|
100
101
|
};
|
package/dist/onboard.js
CHANGED
|
@@ -19,6 +19,12 @@ export async function runOnboard(options) {
|
|
|
19
19
|
const server = normalizeServer(options.server ?? DEFAULT_WITNORA_SERVER);
|
|
20
20
|
const repositoryPath = resolve(options.repository ?? process.cwd());
|
|
21
21
|
const repository = await inspectRepository(repositoryPath, options.template);
|
|
22
|
+
const agentIdentity = {
|
|
23
|
+
externalId: repository.name,
|
|
24
|
+
name: humanizeAgentName(repository.name),
|
|
25
|
+
version: repository.version,
|
|
26
|
+
framework: frameworkForTemplate(repository.template),
|
|
27
|
+
};
|
|
22
28
|
const connectionName = options.name ?? repository.slug;
|
|
23
29
|
const token = await authorizeProjectConnection({
|
|
24
30
|
projectId: options.projectId,
|
|
@@ -95,6 +101,24 @@ export async function runOnboard(options) {
|
|
|
95
101
|
};
|
|
96
102
|
let assuranceHarnessChanged = false;
|
|
97
103
|
let continuousService;
|
|
104
|
+
const prepareRuntime = async (operation) => {
|
|
105
|
+
const delays = [250, 500, 1_000, 2_000, 4_000, 8_000];
|
|
106
|
+
let lastError;
|
|
107
|
+
for (let attempt = 0; attempt <= delays.length; attempt += 1) {
|
|
108
|
+
try {
|
|
109
|
+
return await operation();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!(error instanceof RuntimeSetupNotReadyError))
|
|
113
|
+
throw error;
|
|
114
|
+
lastError = error;
|
|
115
|
+
if (attempt === delays.length)
|
|
116
|
+
break;
|
|
117
|
+
await (options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))))(delays[attempt]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw lastError;
|
|
121
|
+
};
|
|
98
122
|
await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
|
|
99
123
|
try {
|
|
100
124
|
generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
|
|
@@ -137,11 +161,12 @@ export async function runOnboard(options) {
|
|
|
137
161
|
if (gatewayState.status === "absent") {
|
|
138
162
|
let gateway;
|
|
139
163
|
try {
|
|
140
|
-
gateway = await initializeCustomerGateway({
|
|
164
|
+
gateway = await prepareRuntime(() => initializeCustomerGateway({
|
|
141
165
|
projectId: token.projectId, server, repository: repositoryPath, authorization: token,
|
|
142
166
|
runtimeReferences: localRuntime.references,
|
|
167
|
+
agentIdentity,
|
|
143
168
|
fetch: requestFetch, configHome: options.configHome, output,
|
|
144
|
-
});
|
|
169
|
+
}));
|
|
145
170
|
}
|
|
146
171
|
catch (error) {
|
|
147
172
|
if (!(error instanceof RuntimeSetupNotReadyError) || !localRuntime.references)
|
|
@@ -157,7 +182,7 @@ export async function runOnboard(options) {
|
|
|
157
182
|
await saveConnection(binding.connectionName, { server, projectId: token.projectId, apiKey: token.apiKey }, { configHome: options.configHome });
|
|
158
183
|
if (localRuntime.references) {
|
|
159
184
|
try {
|
|
160
|
-
runtimeUpgrade = await upgradeCustomerGatewayRuntime({ repository: repositoryPath, authorization: token, runtimeReferences: localRuntime.references, configHome: options.configHome, fetch: requestFetch });
|
|
185
|
+
runtimeUpgrade = await prepareRuntime(() => upgradeCustomerGatewayRuntime({ repository: repositoryPath, authorization: token, runtimeReferences: localRuntime.references, agentIdentity, configHome: options.configHome, fetch: requestFetch }));
|
|
161
186
|
if (runtimeUpgrade.changed) {
|
|
162
187
|
const stopped = await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
|
|
163
188
|
if (stopped.state !== "STOPPED" || stopped.healthy)
|
|
@@ -187,11 +212,12 @@ export async function runOnboard(options) {
|
|
|
187
212
|
await assertGatewayStopped(requestFetch, binding.host, binding.port, binding.projectId, token.projectId);
|
|
188
213
|
gatewayMigration = await archiveGateway(gatewayState.directory, repositoryPath, binding.projectId);
|
|
189
214
|
output(`\nExisting Gateway belongs to project ${binding.projectId}; archived it at ${gatewayMigration.archiveDirectory}.\n`);
|
|
190
|
-
const gateway = await initializeCustomerGateway({
|
|
215
|
+
const gateway = await prepareRuntime(() => initializeCustomerGateway({
|
|
191
216
|
projectId: token.projectId, server, repository: repositoryPath, authorization: token,
|
|
192
217
|
runtimeReferences: localRuntime.references,
|
|
218
|
+
agentIdentity,
|
|
193
219
|
fetch: requestFetch, configHome: options.configHome, output,
|
|
194
|
-
});
|
|
220
|
+
}));
|
|
195
221
|
generatedFiles.push(...gateway.generatedFiles);
|
|
196
222
|
}
|
|
197
223
|
}
|
|
@@ -252,7 +278,7 @@ export async function runOnboard(options) {
|
|
|
252
278
|
: { state: "RECORDED_ONLY", checkedAt: new Date().toISOString(), limitations: [runtimeLimitation ?? "The local sandbox Runtime worker has not established exact adapter, probe, source-key, identity, mandate, and readiness bindings."] };
|
|
253
279
|
await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
|
|
254
280
|
status: "verified", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles), runtimeReadiness,
|
|
255
|
-
connectedAgent:
|
|
281
|
+
connectedAgent: agentIdentity,
|
|
256
282
|
...(runtimeBinding ? { runtimeBinding } : {}),
|
|
257
283
|
});
|
|
258
284
|
output(`\nWitnora Setup Autopilot completed for ${repository.name}.\n`);
|
|
@@ -11,11 +11,25 @@ export async function activateRealPathIntegrations(options) {
|
|
|
11
11
|
if (!response.ok)
|
|
12
12
|
throw new Error(`Could not load approved real-path integrations (${response.status}).`);
|
|
13
13
|
const body = await boundedJson(response);
|
|
14
|
-
const approved = Array.isArray(body.integrations) ? body.integrations.map(parsePlan).filter((plan) => plan.status === "READY_TO_ACTIVATE") : [];
|
|
14
|
+
const approved = Array.isArray(body.integrations) ? body.integrations.map(parsePlan).filter((plan) => plan.status === "READY_TO_ACTIVATE" || plan.status === "HARNESS_ACTIVE") : [];
|
|
15
15
|
const plans = approved.filter((plan) => ["STRIPE_REFUND", "SHOPIFY_DISPUTE", "ZENDESK_TICKET", "SALESFORCE_RECORD", "HUBSPOT_CRM_RECORD", "POSTGRES_RECORD"].includes(plan.generated.providerPackId) && plan.environment === "sandbox" && plan.customerSummary.evaluationMode === "SHADOW");
|
|
16
16
|
if (!plans.length)
|
|
17
17
|
return { state: "NOT_CONFIGURED", created: false, activations: [], generatedFiles: [], rollback: async () => undefined };
|
|
18
18
|
const environment = options.env ?? process.env;
|
|
19
|
+
const modulePath = join(repository, "witnora.assurance-harness.mjs");
|
|
20
|
+
const manifestPath = join(repository, ".witnora", "gateway", "real-path-activations.json");
|
|
21
|
+
const source = generatedProviderHarness(plans);
|
|
22
|
+
const moduleDigestSha256 = sha(source);
|
|
23
|
+
const current = await readFile(modulePath, "utf8").catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
|
|
24
|
+
const priorSource = await readFile(manifestPath, "utf8").catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
|
|
25
|
+
const prior = priorSource === undefined ? undefined : JSON.parse(priorSource);
|
|
26
|
+
const cachedActivations = prior?.projectId === options.projectId && prior.moduleDigestSha256 === moduleDigestSha256 && current === source
|
|
27
|
+
&& Array.isArray(prior.activations) && sameActivationPlans(plans, prior.activations) ? structuredClone(prior.activations) : undefined;
|
|
28
|
+
if (cachedActivations) {
|
|
29
|
+
for (const plan of plans)
|
|
30
|
+
await persistProviderCredential(repository, plan, environment);
|
|
31
|
+
return { state: "READY_TO_START", created: false, modulePath: "witnora.assurance-harness.mjs", activations: cachedActivations, generatedFiles: [], previousGeneratedModuleSha256: moduleDigestSha256, rollback: async () => undefined };
|
|
32
|
+
}
|
|
19
33
|
const activations = [];
|
|
20
34
|
const scenarios = [];
|
|
21
35
|
for (const plan of plans) {
|
|
@@ -25,15 +39,8 @@ export async function activateRealPathIntegrations(options) {
|
|
|
25
39
|
activations.push({ integrationId: plan.id, integrationDigestSha256: plan.digestSha256, taskContractId: plan.taskContractId, taskContractDigestSha256: plan.taskContractDigestSha256, agentId: plan.subject.agentId, agentVersion: plan.subject.agentVersion, environment: plan.environment, providerPackId: plan.generated.providerPackId, providerContractDigestSha256: plan.generated.providerContractDigestSha256, actionPathIds: plan.generated.actionPathBindings.map((item) => item.actionPathId), acceptance });
|
|
26
40
|
scenarios.push({ plan, value: [preflight.scenario] });
|
|
27
41
|
}
|
|
28
|
-
const modulePath = join(repository, "witnora.assurance-harness.mjs");
|
|
29
|
-
const manifestPath = join(repository, ".witnora", "gateway", "real-path-activations.json");
|
|
30
|
-
const source = generatedProviderHarness(plans);
|
|
31
|
-
const moduleDigestSha256 = sha(source);
|
|
32
42
|
let created = false;
|
|
33
43
|
const generatedFiles = [];
|
|
34
|
-
const current = await readFile(modulePath, "utf8").catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
|
|
35
|
-
const priorSource = await readFile(manifestPath, "utf8").catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
|
|
36
|
-
const prior = priorSource === undefined ? undefined : JSON.parse(priorSource);
|
|
37
44
|
const previousGeneratedModuleSha256 = current !== undefined && sha(current) === prior?.moduleDigestSha256 ? prior.moduleDigestSha256 : undefined;
|
|
38
45
|
if (current === undefined) {
|
|
39
46
|
await writeFile(modulePath, source, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
@@ -253,16 +260,30 @@ async function persistProviderCredential(repository, plan, environment) {
|
|
|
253
260
|
return;
|
|
254
261
|
const name = providerCredentialEnvironmentName(plan.generated.providerPackId);
|
|
255
262
|
const value = environment[name];
|
|
256
|
-
if (!value)
|
|
257
|
-
throw new Error(`${plan.generated.providerPackId} activation requires its read-only credential in the customer environment.`);
|
|
258
263
|
const directory = join(repository, ".witnora", "provider-credentials");
|
|
259
264
|
const target = join(directory, `${plan.id}.secret`);
|
|
260
|
-
const
|
|
265
|
+
const current = await readFile(target, "utf8").catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
|
|
266
|
+
if (!value) {
|
|
267
|
+
if (current !== undefined)
|
|
268
|
+
return;
|
|
269
|
+
throw new Error(`${plan.generated.providerPackId} activation requires its read-only credential in the customer environment.`);
|
|
270
|
+
}
|
|
271
|
+
if (current === value)
|
|
272
|
+
return;
|
|
261
273
|
await mkdir(directory, { recursive: true });
|
|
262
274
|
await writeFile(join(directory, ".gitignore"), "*\n!.gitignore\n", { encoding: "utf8", mode: 0o600, flag: "wx" }).catch((error) => { if (error.code !== "EEXIST")
|
|
263
275
|
throw error; });
|
|
264
|
-
await writeFile(
|
|
265
|
-
|
|
276
|
+
await writeFile(target, value, { encoding: "utf8", mode: 0o600 });
|
|
277
|
+
}
|
|
278
|
+
function sameActivationPlans(plans, activations) {
|
|
279
|
+
if (plans.length !== activations.length)
|
|
280
|
+
return false;
|
|
281
|
+
return plans.every((plan) => activations.some((activation) => activation.integrationId === plan.id && activation.integrationDigestSha256 === plan.digestSha256
|
|
282
|
+
&& activation.taskContractId === plan.taskContractId && activation.taskContractDigestSha256 === plan.taskContractDigestSha256
|
|
283
|
+
&& activation.agentId === plan.subject.agentId && activation.agentVersion === plan.subject.agentVersion && activation.environment === plan.environment
|
|
284
|
+
&& activation.providerPackId === plan.generated.providerPackId && activation.providerContractDigestSha256 === plan.generated.providerContractDigestSha256
|
|
285
|
+
&& canonical([...activation.actionPathIds].sort()) === canonical(plan.generated.actionPathBindings.map((item) => item.actionPathId).sort())
|
|
286
|
+
&& activation.acceptance?.kind === "READ_ONLY_PROVIDER_PREFLIGHT" && activation.acceptance.productionWrites === 0 && digest(activation.acceptance.observationDigestSha256)));
|
|
266
287
|
}
|
|
267
288
|
function providerCredentialEnvironmentName(packId) {
|
|
268
289
|
if (packId === "STRIPE_REFUND")
|