@calltelemetry/ct-lab-mcp 0.8.149 → 0.8.151
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 +1 -0
- package/dist/src/generated-version.d.ts +1 -1
- package/dist/src/generated-version.js +1 -1
- package/dist/src/inspection/lab-inspection.d.ts +16293 -0
- package/dist/src/inspection/lab-inspection.d.ts.map +1 -0
- package/dist/src/inspection/lab-inspection.js +964 -0
- package/dist/src/inspection/lab-inspection.js.map +1 -0
- package/dist/src/inspection/read-provenance.d.ts +44 -0
- package/dist/src/inspection/read-provenance.d.ts.map +1 -0
- package/dist/src/inspection/read-provenance.js +28 -0
- package/dist/src/inspection/read-provenance.js.map +1 -0
- package/dist/src/inspection/runtime-artifact.d.ts +16 -0
- package/dist/src/inspection/runtime-artifact.d.ts.map +1 -0
- package/dist/src/inspection/runtime-artifact.js +99 -0
- package/dist/src/inspection/runtime-artifact.js.map +1 -0
- package/dist/src/lab-exec/claim-resolve.d.ts +97 -0
- package/dist/src/lab-exec/claim-resolve.d.ts.map +1 -1
- package/dist/src/lab-exec/claim-resolve.js +5 -0
- package/dist/src/lab-exec/claim-resolve.js.map +1 -1
- package/dist/src/lab-exec/executor.d.ts +3 -0
- package/dist/src/lab-exec/executor.d.ts.map +1 -1
- package/dist/src/lab-exec/executor.js +4 -1
- package/dist/src/lab-exec/executor.js.map +1 -1
- package/dist/src/lab-exec/remote.d.ts +3 -0
- package/dist/src/lab-exec/remote.d.ts.map +1 -1
- package/dist/src/lab-exec/remote.js +5 -0
- package/dist/src/lab-exec/remote.js.map +1 -1
- package/dist/src/lab-exec/server.d.ts.map +1 -1
- package/dist/src/lab-exec/server.js +3 -2
- package/dist/src/lab-exec/server.js.map +1 -1
- package/dist/src/tools/appliance-candidate-retire.d.ts +6 -6
- package/dist/src/tools/appliance-candidate-update.d.ts +33 -6
- package/dist/src/tools/appliance-candidate-update.d.ts.map +1 -1
- package/dist/src/tools/appliance-candidate-update.js +99 -4
- package/dist/src/tools/appliance-candidate-update.js.map +1 -1
- package/dist/src/tools/appliance-claim-tools.d.ts +62 -3
- package/dist/src/tools/appliance-claim-tools.d.ts.map +1 -1
- package/dist/src/tools/appliance-claim-tools.js +810 -16
- package/dist/src/tools/appliance-claim-tools.js.map +1 -1
- package/dist/src/tools/claim-lifecycle.d.ts +4 -4
- package/dist/src/tools/claimed-import-ova.d.ts +9 -9
- package/dist/src/tools/claimed-import-qcow.d.ts +2 -2
- package/dist/src/tools/cube-canonical.d.ts +2 -2
- package/dist/src/tools/host-migrate-vm.d.ts +4 -4
- package/dist/src/tools/import-qcow.d.ts +6 -6
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +8 -3
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/lab-dev.d.ts +38 -38
- package/dist/src/tools/list-hosts.d.ts.map +1 -1
- package/dist/src/tools/list-hosts.js +16 -4
- package/dist/src/tools/list-hosts.js.map +1 -1
- package/dist/src/tools/list-inventory.d.ts.map +1 -1
- package/dist/src/tools/list-inventory.js +13 -3
- package/dist/src/tools/list-inventory.js.map +1 -1
- package/dist/src/tools/provision-tools.d.ts +20 -20
- package/dist/src/tools/quick-provision.d.ts +2 -2
- package/dist/src/tools/sandbox-tools.d.ts +4 -4
- package/dist/src/tools/slot-tools.d.ts +8 -8
- package/dist/src/tools/storage-tools.d.ts +8 -8
- package/dist/src/tools/uat-access.d.ts +2 -2
- package/dist/src/tools/vm-action.d.ts +8 -8
- package/dist/src/tools/vm-repair.d.ts +6 -6
- package/package.json +1 -1
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { fetch, Agent } from "undici";
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { isIP } from "node:net";
|
|
13
15
|
import { ProxmoxErrorCode, createToolErrorResult } from "../types/mcp.js";
|
|
14
16
|
import { logger } from "../utils/logger.js";
|
|
15
17
|
import { validateStigDescriptor } from "../stig/model.js";
|
|
16
|
-
import { createClaimWithCustody, deleteClaimWithCustody, NoClaimCustodyError } from "./claim-custody.js";
|
|
18
|
+
import { CUSTODY_ANNOTATION, claimCustody, createClaimWithCustody, deleteClaimWithCustody, NoClaimCustodyError, verifyCustodyClaim } from "./claim-custody.js";
|
|
19
|
+
import { buildProvenance } from "../provenance.js";
|
|
20
|
+
import { buildLabInspectionEnvelope, InspectionConflictError, projectAccessObserved, projectCurrentNetworkReadiness, projectLifecycleDiagnostic, projectNetworkRevalidation, projectOperatorWorkflow, NETWORK_REVALIDATION_REQUEST_TTL_MS, unknown as unknownEvidence, verified as verifiedEvidence, } from "../inspection/lab-inspection.js";
|
|
21
|
+
import { readInstalledRuntimeArtifact } from "../inspection/runtime-artifact.js";
|
|
17
22
|
import { LAB_CLAIM_BACKEND_REMEDIATION, LabClaimBackendConfigError, diagnoseLabKubernetesBackend, resolveLabKubernetesBackend, runLabKubectl, } from "./kubernetes-backend.js";
|
|
18
23
|
import { AUTO_NODE, PLACEMENT_ANNOTATION, fleetFromManager, isAutoNode, pinnedPlacementAnnotation, placeOnFleet, placementAnnotation, } from "./placement.js";
|
|
19
|
-
import { proxmoxClaimNode, PROXMOX_HOST_CLAIM_NODES, } from "./proxmox-host-policy.js";
|
|
24
|
+
import { proxmoxClaimNode, CLAIM_NODE_TO_PROXMOX_HOST, PROXMOX_HOST_CLAIM_NODES, } from "./proxmox-host-policy.js";
|
|
20
25
|
export const RECOGNIZED_CHANNELS = ["stable", "beta", "rc", "candidate", "dev", "edge"];
|
|
21
26
|
export function resolveAndValidateChannel(version, trainOrChannel) {
|
|
22
27
|
const isPrerelease = version.includes("-rc") || version.includes("-dev") || version.includes("-beta") || version.includes("-candidate");
|
|
@@ -59,9 +64,22 @@ export const ListApplianceClaimsInputShape = {
|
|
|
59
64
|
export const ListApplianceClaimsInputSchema = z.object(ListApplianceClaimsInputShape);
|
|
60
65
|
export const GetApplianceClaimInputShape = {
|
|
61
66
|
name: z.string().min(1).describe("Name of the ApplianceClaim resource (metadata.name)."),
|
|
62
|
-
namespace: z.string().optional().default("ct-lab").describe("Kubernetes namespace (default: 'ct-lab').")
|
|
67
|
+
namespace: z.string().optional().default("ct-lab").describe("Kubernetes namespace (default: 'ct-lab')."),
|
|
68
|
+
expectedUid: z.string().uuid().optional().describe("Optional expected current claim UID. A mismatch is rejected; this value is never treated as observed evidence."),
|
|
69
|
+
expectedGeneration: z.number().int().nonnegative().optional().describe("Optional expected current claim generation. A mismatch is rejected; this value is never treated as observed evidence."),
|
|
70
|
+
expectedRequestId: z.string().uuid().optional().describe("Optional exact read-only network-revalidation request ID whose persisted result should be projected."),
|
|
63
71
|
};
|
|
64
72
|
export const GetApplianceClaimInputSchema = z.object(GetApplianceClaimInputShape);
|
|
73
|
+
export const RevalidateApplianceClaimNetworkInputShape = {
|
|
74
|
+
name: z.string().min(1).describe("Current ApplianceClaim name."),
|
|
75
|
+
namespace: z.string().optional().default("ct-lab").describe("Kubernetes namespace (default: 'ct-lab')."),
|
|
76
|
+
expectedUid: z.string().uuid().describe("Exact live claim UID to bind the read-only revalidation request."),
|
|
77
|
+
expectedGeneration: z.number().int().positive().describe("Exact live claim generation to bind the read-only revalidation request."),
|
|
78
|
+
operationId: z.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/).describe("Current durable W2 CUBE workflow operationId."),
|
|
79
|
+
intentDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/).describe("Current durable W2 secret-free immutable intent digest."),
|
|
80
|
+
requestId: z.string().uuid().describe("Unique idempotency identity for this read-only probe attempt."),
|
|
81
|
+
};
|
|
82
|
+
export const RevalidateApplianceClaimNetworkInputSchema = z.object(RevalidateApplianceClaimNetworkInputShape).strict();
|
|
65
83
|
export const CreateApplianceClaimInputShape = {
|
|
66
84
|
name: z.string().optional().describe("Name of the ApplianceClaim resource (metadata.name). Auto-generated if omitted."),
|
|
67
85
|
namespace: z.string().optional().default("ct-lab").describe("Kubernetes namespace (default: 'ct-lab')."),
|
|
@@ -134,7 +152,7 @@ export function normalizeClaimNode(node) {
|
|
|
134
152
|
return "esxi";
|
|
135
153
|
return proxmoxClaimNode(raw);
|
|
136
154
|
}
|
|
137
|
-
export async function
|
|
155
|
+
export async function queryKubernetesApiWithObservation(path, options = {}) {
|
|
138
156
|
const backend = resolveLabKubernetesBackend();
|
|
139
157
|
if (backend.mode === "in-cluster") {
|
|
140
158
|
const token = fs.readFileSync(backend.tokenPath, "utf-8").trim();
|
|
@@ -162,7 +180,13 @@ export async function queryKubernetesApi(path, options = {}) {
|
|
|
162
180
|
});
|
|
163
181
|
if (!res.ok)
|
|
164
182
|
throw new Error(`Kubernetes API request failed (${res.status})`);
|
|
165
|
-
|
|
183
|
+
const body = await res.json();
|
|
184
|
+
const dateHeader = res.headers?.get?.("date");
|
|
185
|
+
const dateMs = typeof dateHeader === "string" ? Date.parse(dateHeader) : NaN;
|
|
186
|
+
return {
|
|
187
|
+
data: body,
|
|
188
|
+
...(Number.isFinite(dateMs) ? { observedAt: new Date(dateMs).toISOString() } : {}),
|
|
189
|
+
};
|
|
166
190
|
}
|
|
167
191
|
finally {
|
|
168
192
|
await agent.close();
|
|
@@ -171,12 +195,100 @@ export async function queryKubernetesApi(path, options = {}) {
|
|
|
171
195
|
// The caller must use runLabKubectl, which validates the same explicit binding.
|
|
172
196
|
return null;
|
|
173
197
|
}
|
|
198
|
+
export async function queryKubernetesApi(path, options = {}) {
|
|
199
|
+
const observation = await queryKubernetesApiWithObservation(path, options);
|
|
200
|
+
return observation?.data ?? null;
|
|
201
|
+
}
|
|
202
|
+
function claimHasFinalRevalidation(claim, requestId, uid, generation) {
|
|
203
|
+
if (claim?.metadata?.uid !== uid || claim?.metadata?.generation !== generation)
|
|
204
|
+
return false;
|
|
205
|
+
const workflow = claim?.status?.cubeWorkflow;
|
|
206
|
+
const receipt = workflow?.networkRevalidation ?? workflow?.summary?.networkRevalidation;
|
|
207
|
+
return receipt?.requestId === requestId && ["verified", "unknown"].includes(receipt?.state);
|
|
208
|
+
}
|
|
209
|
+
function parseWatchClaimEvents(stdout, requestId, uid, generation) {
|
|
210
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
211
|
+
if (!line.trim())
|
|
212
|
+
continue;
|
|
213
|
+
let event;
|
|
214
|
+
try {
|
|
215
|
+
event = JSON.parse(line);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const object = event?.object ?? event;
|
|
221
|
+
if (event?.type === "DELETED" && object?.metadata?.uid === uid && object?.metadata?.generation === generation) {
|
|
222
|
+
return { claim: object, deleted: true };
|
|
223
|
+
}
|
|
224
|
+
if (!["ADDED", "MODIFIED"].includes(event?.type)
|
|
225
|
+
|| !claimHasFinalRevalidation(object, requestId, uid, generation))
|
|
226
|
+
continue;
|
|
227
|
+
return { claim: object, deleted: false };
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
/** One Kubernetes watch for the exact request; no GET polling and no guest effect. */
|
|
232
|
+
async function watchClaimRevalidation(namespace, name, resourceVersion, requestId, uid, generation, timeoutMs = 20_000) {
|
|
233
|
+
const backend = resolveLabKubernetesBackend();
|
|
234
|
+
const apiPath = `/apis/calltelemetry.com/v1alpha1/namespaces/${encodeURIComponent(namespace)}/applianceclaims?watch=true&allowWatchBookmarks=true&fieldSelector=${encodeURIComponent(`metadata.name=${name}`)}&resourceVersion=${encodeURIComponent(resourceVersion)}&timeoutSeconds=${Math.ceil(timeoutMs / 1000)}`;
|
|
235
|
+
if (backend.mode === "kubeconfig") {
|
|
236
|
+
try {
|
|
237
|
+
const { stdout } = await runLabKubectl([
|
|
238
|
+
"get", "applianceclaim", name, "-n", namespace,
|
|
239
|
+
"--watch-only", "--output-watch-events", "-o", "json", `--request-timeout=${Math.ceil(timeoutMs / 1000)}s`,
|
|
240
|
+
], { timeoutMs: timeoutMs + 1_000, maxBufferBytes: 1_048_576 });
|
|
241
|
+
return parseWatchClaimEvents(stdout, requestId, uid, generation);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const token = fs.readFileSync(backend.tokenPath, "utf-8").trim();
|
|
248
|
+
if (!token)
|
|
249
|
+
return undefined;
|
|
250
|
+
const ca = fs.readFileSync(backend.caPath);
|
|
251
|
+
const agent = new Agent({ connect: { ca, rejectUnauthorized: true } });
|
|
252
|
+
try {
|
|
253
|
+
const response = await fetch(`${backend.server}${apiPath}`, {
|
|
254
|
+
method: "GET",
|
|
255
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
256
|
+
dispatcher: agent,
|
|
257
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
258
|
+
});
|
|
259
|
+
if (!response.ok || !response.body)
|
|
260
|
+
return undefined;
|
|
261
|
+
const dateHeader = response.headers?.get?.("date");
|
|
262
|
+
const dateMs = typeof dateHeader === "string" ? Date.parse(dateHeader) : NaN;
|
|
263
|
+
const observedAt = Number.isFinite(dateMs) ? new Date(dateMs).toISOString() : undefined;
|
|
264
|
+
const decoder = new TextDecoder();
|
|
265
|
+
let pending = "";
|
|
266
|
+
for await (const chunk of response.body) {
|
|
267
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
268
|
+
const lines = pending.split(/\r?\n/u);
|
|
269
|
+
pending = lines.pop() ?? "";
|
|
270
|
+
const found = parseWatchClaimEvents(`${lines.join("\n")}\n`, requestId, uid, generation);
|
|
271
|
+
if (found)
|
|
272
|
+
return { ...found, ...(observedAt ? { observedAt } : {}) };
|
|
273
|
+
}
|
|
274
|
+
const final = parseWatchClaimEvents(pending, requestId, uid, generation);
|
|
275
|
+
return final ? { ...final, ...(observedAt ? { observedAt } : {}) } : undefined;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await agent.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
174
284
|
export function independentClaimVerb(operation, args) {
|
|
175
285
|
const input = args;
|
|
176
286
|
if (operation === "list_appliance_claims")
|
|
177
287
|
return "list";
|
|
178
288
|
if (operation === "get_appliance_claim")
|
|
179
289
|
return "get";
|
|
290
|
+
if (operation === "revalidate_appliance_claim_network")
|
|
291
|
+
return "patch";
|
|
180
292
|
if (operation === "create_appliance_claim")
|
|
181
293
|
return input?.dryRun === true ? null : "create";
|
|
182
294
|
if (operation === "delete_appliance_claim")
|
|
@@ -249,6 +361,7 @@ export function claimBackendPreflightError(operation) {
|
|
|
249
361
|
const FORWARD_METHOD = {
|
|
250
362
|
list_appliance_claims: "claimsList",
|
|
251
363
|
get_appliance_claim: "claimsGet",
|
|
364
|
+
revalidate_appliance_claim_network: "claimsNetworkRevalidate",
|
|
252
365
|
list_held_claims: "claimsHeld",
|
|
253
366
|
lab_capacity: "claimsCapacity",
|
|
254
367
|
create_appliance_claim: "claimsCreate",
|
|
@@ -258,8 +371,8 @@ const FORWARD_METHOD = {
|
|
|
258
371
|
};
|
|
259
372
|
/**
|
|
260
373
|
* REL-1961/REL-1964: a Worker has no k3s API route but holds a RemoteLabExecutor whose
|
|
261
|
-
* in-cluster claim plane serves claim list/get and
|
|
262
|
-
*
|
|
374
|
+
* in-cluster claim plane serves claim list/get and request-bound CUBE revalidation and,
|
|
375
|
+
* behind the executor's kill switch, claim mutations. Forward only when the local backend is unconfigured, so
|
|
263
376
|
* gateways and the executor itself keep running locally.
|
|
264
377
|
*/
|
|
265
378
|
export async function forwardClaim(operation, input, context) {
|
|
@@ -344,6 +457,473 @@ export async function handleListApplianceClaims(args, context) {
|
|
|
344
457
|
return createToolErrorResult(ProxmoxErrorCode.UPSTREAM_API_ERROR, `Failed to list appliance claims in namespace '${namespace}': ${err.message}`);
|
|
345
458
|
}
|
|
346
459
|
}
|
|
460
|
+
const CUBE_REVALIDATION_REQUEST_ANNOTATION = "calltelemetry.com/cube-revalidation-request";
|
|
461
|
+
const CUBE_REVALIDATION_REQUEST_SCHEMA = "ct.appliance.cube-revalidation-request/v1";
|
|
462
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
463
|
+
const MAC_PATTERN = /^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i;
|
|
464
|
+
function inspectionDigest(value) {
|
|
465
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
466
|
+
}
|
|
467
|
+
function safeRecord(value) {
|
|
468
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
469
|
+
? value
|
|
470
|
+
: undefined;
|
|
471
|
+
}
|
|
472
|
+
function validDate(value) {
|
|
473
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value)))
|
|
474
|
+
return undefined;
|
|
475
|
+
try {
|
|
476
|
+
return new Date(Date.parse(value)).toISOString();
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function observedFact(value, observedAt) {
|
|
483
|
+
return observedAt
|
|
484
|
+
? verifiedEvidence({ value, observedAt }, "operator_status_readback")
|
|
485
|
+
: unknownEvidence("source_observation_time_missing");
|
|
486
|
+
}
|
|
487
|
+
function desiredHostTuple(claim, custodyGuest) {
|
|
488
|
+
const node = normalizeClaimNode(claim.spec.node);
|
|
489
|
+
const vmid = claim.spec.vmid;
|
|
490
|
+
if (!node || node === "esxi" || !Number.isSafeInteger(vmid) || vmid < 100)
|
|
491
|
+
return undefined;
|
|
492
|
+
const hostAssetId = CLAIM_NODE_TO_PROXMOX_HOST[node];
|
|
493
|
+
const tuple = { hostAssetId, node, vmid: vmid };
|
|
494
|
+
if (custodyGuest && JSON.stringify(tuple) !== JSON.stringify(custodyGuest)) {
|
|
495
|
+
throw new InspectionConflictError("INSPECTION_CONFLICT");
|
|
496
|
+
}
|
|
497
|
+
return tuple;
|
|
498
|
+
}
|
|
499
|
+
function claimExpiry(claim) {
|
|
500
|
+
const status = safeRecord(claim.status);
|
|
501
|
+
const explicit = validDate(status?.expiresAt);
|
|
502
|
+
if (explicit)
|
|
503
|
+
return verifiedEvidence(explicit, "operator_claim_expiry");
|
|
504
|
+
const ttlHours = claim.spec.ttlHours ?? 24;
|
|
505
|
+
if (ttlHours === 0)
|
|
506
|
+
return verifiedEvidence(null, "claim_spec_no_expiry");
|
|
507
|
+
if (!Number.isSafeInteger(ttlHours) || ttlHours < 0 || ttlHours > 8760)
|
|
508
|
+
return unknownEvidence("claim_expiry_invalid");
|
|
509
|
+
const startsAt = validDate(status?.completionTime) ?? validDate(claim.metadata.creationTimestamp);
|
|
510
|
+
if (!startsAt)
|
|
511
|
+
return unknownEvidence("claim_expiry_source_missing");
|
|
512
|
+
return verifiedEvidence(new Date(Date.parse(startsAt) + ttlHours * 3_600_000).toISOString(), "claim_ttl_contract");
|
|
513
|
+
}
|
|
514
|
+
function claimCustodyProjection(claim) {
|
|
515
|
+
try {
|
|
516
|
+
const record = claimCustody(claim);
|
|
517
|
+
if (!record)
|
|
518
|
+
return { evidence: verifiedEvidence({ present: false, phase: "missing" }, "claim_metadata_annotations") };
|
|
519
|
+
verifyCustodyClaim(claim, record, claim.metadata.uid);
|
|
520
|
+
const guest = record.guest;
|
|
521
|
+
return {
|
|
522
|
+
evidence: verifiedEvidence({ present: true, phase: record.phase, guest }, "claim_custody_annotation"),
|
|
523
|
+
record,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
throw new InspectionConflictError("INSPECTION_CONFLICT");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function deriveCubeOperationId(uid, generation, intentDigest) {
|
|
531
|
+
if (!UUID_PATTERN.test(uid) || !Number.isSafeInteger(generation) || generation < 1
|
|
532
|
+
|| !/^sha256:[a-f0-9]{64}$/.test(intentDigest))
|
|
533
|
+
return undefined;
|
|
534
|
+
const input = `${uid.toLowerCase()}\0${generation}\0${intentDigest}`;
|
|
535
|
+
return `cube-${createHash("sha256").update(input, "utf8").digest("hex").slice(0, 32)}`;
|
|
536
|
+
}
|
|
537
|
+
function workflowBindsClaimGeneration(rawWorkflow, uid, generation) {
|
|
538
|
+
if (!uid || generation === undefined)
|
|
539
|
+
return false;
|
|
540
|
+
const summary = safeRecord(safeRecord(rawWorkflow)?.summary);
|
|
541
|
+
if (typeof summary?.intentDigest !== "string" || typeof summary.operationId !== "string")
|
|
542
|
+
return false;
|
|
543
|
+
return summary.operationId === deriveCubeOperationId(uid, generation, summary.intentDigest);
|
|
544
|
+
}
|
|
545
|
+
function activeCubeClaimRefusal(claim, input, now = Date.now()) {
|
|
546
|
+
const metadata = safeRecord(claim.metadata) ?? {};
|
|
547
|
+
const status = safeRecord(claim.status) ?? {};
|
|
548
|
+
const spec = safeRecord(claim.spec) ?? {};
|
|
549
|
+
const workflow = safeRecord(status.cubeWorkflow);
|
|
550
|
+
const summary = safeRecord(workflow?.summary);
|
|
551
|
+
if (metadata.uid !== input.expectedUid || metadata.generation !== input.expectedGeneration)
|
|
552
|
+
return "claim_identity_conflict";
|
|
553
|
+
if (metadata.deletionTimestamp)
|
|
554
|
+
return "claim_deleting";
|
|
555
|
+
if (spec.state === "Closed" || spec.closed === true || spec.paused === true)
|
|
556
|
+
return "claim_inactive";
|
|
557
|
+
if (status.phase !== "Ready" || status.observedGeneration !== input.expectedGeneration
|
|
558
|
+
|| summary?.phase !== "ready" || summary?.operationId !== input.operationId || summary?.intentDigest !== input.intentDigest
|
|
559
|
+
|| !workflowBindsClaimGeneration(workflow, input.expectedUid, input.expectedGeneration)) {
|
|
560
|
+
return "claim_not_ready_or_workflow_changed";
|
|
561
|
+
}
|
|
562
|
+
if (spec.profile !== "cube" || !safeRecord(spec.cube))
|
|
563
|
+
return "cube_claim_required";
|
|
564
|
+
const expiry = claimExpiry(claim);
|
|
565
|
+
if (expiry.state !== "verified" || !expiry.value || Date.parse(expiry.value) <= now)
|
|
566
|
+
return "claim_inactive";
|
|
567
|
+
try {
|
|
568
|
+
const custody = claimCustody(claim);
|
|
569
|
+
if (!custody || custody.phase !== "provider-bound")
|
|
570
|
+
return "active_provider_custody_required";
|
|
571
|
+
verifyCustodyClaim(claim, custody, input.expectedUid);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return "claim_custody_conflict";
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
function phaseTime(workflow, phase) {
|
|
579
|
+
if (workflow.state !== "verified")
|
|
580
|
+
return undefined;
|
|
581
|
+
const result = workflow.value.phases[phase]?.result;
|
|
582
|
+
return validDate(result?.observedAt);
|
|
583
|
+
}
|
|
584
|
+
function phaseReadiness(workflow, phase) {
|
|
585
|
+
if (workflow.state !== "verified")
|
|
586
|
+
return unknownEvidence("workflow_status_unavailable");
|
|
587
|
+
const result = workflow.value.phases[phase]?.result;
|
|
588
|
+
const observedAt = validDate(result?.observedAt);
|
|
589
|
+
if (!result || !observedAt)
|
|
590
|
+
return unknownEvidence("phase_result_unavailable");
|
|
591
|
+
if (result.effectCertainty === "confirmed")
|
|
592
|
+
return verifiedEvidence({ value: { state: "ready" }, observedAt }, "operator_workflow_status");
|
|
593
|
+
if (result.effectCertainty === "none")
|
|
594
|
+
return verifiedEvidence({ value: { state: "not_ready" }, observedAt }, "operator_workflow_status");
|
|
595
|
+
return verifiedEvidence({ value: { state: "pending" }, observedAt }, "operator_workflow_status");
|
|
596
|
+
}
|
|
597
|
+
function readinessDimension(observed, desired = "ready") {
|
|
598
|
+
return { desired: verifiedEvidence(desired, "claim_profile"), observed };
|
|
599
|
+
}
|
|
600
|
+
function projectBootstrapAddress(statusValue) {
|
|
601
|
+
const status = safeRecord(statusValue);
|
|
602
|
+
const bootstrap = safeRecord(status?.networkBootstrap);
|
|
603
|
+
const observedAt = validDate(bootstrap?.leaseObservedAt);
|
|
604
|
+
if (!bootstrap || !observedAt || bootstrap.leaseSource !== "ios-dhcp-interface") {
|
|
605
|
+
return {
|
|
606
|
+
address: unknownEvidence("historical_network_observation_unavailable"),
|
|
607
|
+
uuid: unknownEvidence("provider_identity_not_recorded"),
|
|
608
|
+
mac: unknownEvidence("provider_identity_not_recorded"),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const address = typeof bootstrap.address === "string" && isIP(bootstrap.address) === 4
|
|
612
|
+
? verifiedEvidence({ value: bootstrap.address, source: "ios_dhcp_interface", observedAt }, "operator_network_bootstrap")
|
|
613
|
+
: unknownEvidence("historical_address_invalid");
|
|
614
|
+
const uuid = typeof bootstrap.guestUuid === "string" && UUID_PATTERN.test(bootstrap.guestUuid)
|
|
615
|
+
? verifiedEvidence({ value: bootstrap.guestUuid.toLowerCase(), observedAt }, "ios_dhcp_interface")
|
|
616
|
+
: unknownEvidence("provider_identity_not_recorded");
|
|
617
|
+
const mac = typeof bootstrap.macAddress === "string" && MAC_PATTERN.test(bootstrap.macAddress)
|
|
618
|
+
? verifiedEvidence({ value: bootstrap.macAddress.toLowerCase(), observedAt }, "ios_dhcp_interface")
|
|
619
|
+
: unknownEvidence("provider_identity_not_recorded");
|
|
620
|
+
return { address, uuid, mac };
|
|
621
|
+
}
|
|
622
|
+
function projectRequestAnnotation(value) {
|
|
623
|
+
if (typeof value !== "string" || value.length > 2048)
|
|
624
|
+
return undefined;
|
|
625
|
+
try {
|
|
626
|
+
const request = JSON.parse(value);
|
|
627
|
+
if (!request || request.schema !== CUBE_REVALIDATION_REQUEST_SCHEMA
|
|
628
|
+
|| typeof request.requestId !== "string" || !UUID_PATTERN.test(request.requestId)
|
|
629
|
+
|| typeof request.claimUid !== "string" || !UUID_PATTERN.test(request.claimUid)
|
|
630
|
+
|| !Number.isSafeInteger(request.generation) || request.generation < 1
|
|
631
|
+
|| typeof request.operationId !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(request.operationId)
|
|
632
|
+
|| typeof request.intentDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(request.intentDigest)
|
|
633
|
+
|| typeof request.requestedAt !== "string" || !z.string().datetime({ offset: false }).safeParse(request.requestedAt).success
|
|
634
|
+
|| typeof request.requestExpiresAt !== "string" || !z.string().datetime({ offset: false }).safeParse(request.requestExpiresAt).success
|
|
635
|
+
|| Object.keys(request).some((key) => !["schema", "requestId", "claimUid", "generation", "operationId", "intentDigest", "requestedAt", "requestExpiresAt"].includes(key))) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
return request;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function projectInspectionNetworkResult(claim, workflow, custody, expiry, historical, expectedRequestId, trustedNow) {
|
|
645
|
+
const status = safeRecord(claim.status);
|
|
646
|
+
const rawWorkflow = safeRecord(status?.cubeWorkflow);
|
|
647
|
+
const rawStatus = safeRecord(rawWorkflow?.networkRevalidation)
|
|
648
|
+
?? safeRecord(safeRecord(rawWorkflow?.summary)?.networkRevalidation);
|
|
649
|
+
const workflowValue = workflow.state === "verified" ? workflow.value : undefined;
|
|
650
|
+
const operationId = workflowValue?.operationId;
|
|
651
|
+
const intentDigest = workflowValue?.intentDigest;
|
|
652
|
+
const uid = claim.metadata.uid;
|
|
653
|
+
const generation = claim.metadata.generation;
|
|
654
|
+
if (!expectedRequestId && !rawStatus)
|
|
655
|
+
return undefined;
|
|
656
|
+
if (!uid || !UUID_PATTERN.test(uid) || !Number.isSafeInteger(generation) || !operationId || !intentDigest) {
|
|
657
|
+
if (rawStatus)
|
|
658
|
+
throw new InspectionConflictError("INSPECTION_CONFLICT");
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
const requestId = expectedRequestId ?? (typeof rawStatus?.requestId === "string" ? rawStatus.requestId : undefined);
|
|
662
|
+
if (!requestId)
|
|
663
|
+
return undefined;
|
|
664
|
+
const requestBase = {
|
|
665
|
+
schema: "ct.appliance.cube-network-revalidation/v1",
|
|
666
|
+
requestId,
|
|
667
|
+
claimUid: uid,
|
|
668
|
+
generation,
|
|
669
|
+
operationId,
|
|
670
|
+
intentDigest,
|
|
671
|
+
};
|
|
672
|
+
if (rawStatus && rawStatus.requestId === requestId) {
|
|
673
|
+
const annotations = claim.metadata.annotations ?? {};
|
|
674
|
+
const requestAnnotation = projectRequestAnnotation(annotations[CUBE_REVALIDATION_REQUEST_ANNOTATION]);
|
|
675
|
+
if (requestAnnotation?.requestId === requestId
|
|
676
|
+
&& (requestAnnotation.requestedAt !== rawStatus.requestedAt
|
|
677
|
+
|| requestAnnotation.requestExpiresAt !== rawStatus.requestExpiresAt)) {
|
|
678
|
+
throw new InspectionConflictError("INSPECTION_CONFLICT");
|
|
679
|
+
}
|
|
680
|
+
const host = custody.evidence.state === "verified" ? custody.evidence.value.guest : undefined;
|
|
681
|
+
const expectedGuestUuid = custody.record?.provider?.uuid
|
|
682
|
+
?? (historical.uuid.state === "verified" ? historical.uuid.value.value : undefined);
|
|
683
|
+
const expectedMac = historical.mac.state === "verified" ? historical.mac.value.value : undefined;
|
|
684
|
+
const expectedExpiry = expiry.state === "verified" && expiry.value ? expiry.value : undefined;
|
|
685
|
+
const receipt = projectNetworkRevalidation(rawStatus, {
|
|
686
|
+
...requestBase,
|
|
687
|
+
...(expectedExpiry ? { claimExpiresAt: expectedExpiry } : {}),
|
|
688
|
+
...(host ? { host } : {}),
|
|
689
|
+
...(expectedGuestUuid ? { guestUuid: expectedGuestUuid } : {}),
|
|
690
|
+
...(expectedMac ? { mac: expectedMac } : {}),
|
|
691
|
+
}, trustedNow);
|
|
692
|
+
if (receipt.status === "verified" && expiry.state !== "verified") {
|
|
693
|
+
return { ...requestBase, status: "unknown", reasonCode: "claim_expiry_unavailable" };
|
|
694
|
+
}
|
|
695
|
+
return receipt;
|
|
696
|
+
}
|
|
697
|
+
const annotations = claim.metadata.annotations ?? {};
|
|
698
|
+
const requestAnnotation = projectRequestAnnotation(annotations[CUBE_REVALIDATION_REQUEST_ANNOTATION]);
|
|
699
|
+
if (requestAnnotation?.requestId === requestId
|
|
700
|
+
&& requestAnnotation.claimUid === uid
|
|
701
|
+
&& requestAnnotation.generation === generation
|
|
702
|
+
&& requestAnnotation.operationId === operationId
|
|
703
|
+
&& requestAnnotation.intentDigest === intentDigest) {
|
|
704
|
+
const expectedExpiry = expiry.state === "verified" && expiry.value ? expiry.value : undefined;
|
|
705
|
+
if (!expectedExpiry) {
|
|
706
|
+
return {
|
|
707
|
+
...requestBase,
|
|
708
|
+
status: "unknown",
|
|
709
|
+
requestedAt: requestAnnotation.requestedAt,
|
|
710
|
+
requestExpiresAt: requestAnnotation.requestExpiresAt,
|
|
711
|
+
reasonCode: "claim_expiry_unavailable",
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
return projectNetworkRevalidation({
|
|
715
|
+
...requestAnnotation,
|
|
716
|
+
schema: "ct.appliance.cube-network-revalidation/v1",
|
|
717
|
+
state: "pending",
|
|
718
|
+
}, {
|
|
719
|
+
...requestBase,
|
|
720
|
+
claimExpiresAt: expectedExpiry,
|
|
721
|
+
}, trustedNow);
|
|
722
|
+
}
|
|
723
|
+
return { ...requestBase, status: "unknown", reasonCode: "revalidation_result_not_available" };
|
|
724
|
+
}
|
|
725
|
+
function buildClaimInspection(claim, input, retrievedAt, backendObservedAt, context, operation = "get_appliance_claim") {
|
|
726
|
+
const metadata = safeRecord(claim.metadata) ?? {};
|
|
727
|
+
const spec = safeRecord(claim.spec) ?? {};
|
|
728
|
+
const status = safeRecord(claim.status) ?? {};
|
|
729
|
+
const uid = typeof metadata.uid === "string" && UUID_PATTERN.test(metadata.uid) ? metadata.uid : undefined;
|
|
730
|
+
const generation = Number.isSafeInteger(metadata.generation) && metadata.generation >= 0 ? metadata.generation : undefined;
|
|
731
|
+
if (input.expectedUid && uid !== input.expectedUid)
|
|
732
|
+
throw new InspectionConflictError("INSPECTION_CONFLICT");
|
|
733
|
+
if (input.expectedGeneration !== undefined && generation !== input.expectedGeneration)
|
|
734
|
+
throw new InspectionConflictError("INSPECTION_CONFLICT");
|
|
735
|
+
if (metadata.name !== input.name || metadata.namespace !== input.namespace)
|
|
736
|
+
throw new InspectionConflictError("INSPECTION_CONFLICT");
|
|
737
|
+
const custody = claimCustodyProjection(claim);
|
|
738
|
+
const desiredHost = desiredHostTuple(claim, custody.record?.guest);
|
|
739
|
+
const expiry = claimExpiry(claim);
|
|
740
|
+
const rawWorkflow = status.cubeWorkflow;
|
|
741
|
+
const statusGenerationCurrent = generation !== undefined
|
|
742
|
+
&& Number.isSafeInteger(status.observedGeneration)
|
|
743
|
+
&& status.observedGeneration === generation;
|
|
744
|
+
const workflow = workflowBindsClaimGeneration(rawWorkflow, uid, generation)
|
|
745
|
+
? projectOperatorWorkflow(rawWorkflow, statusGenerationCurrent)
|
|
746
|
+
: unknownEvidence("workflow_generation_unverified");
|
|
747
|
+
const statusFactsCurrent = statusGenerationCurrent && workflow.state === "verified";
|
|
748
|
+
const staleStatusReason = statusGenerationCurrent ? "workflow_generation_unverified" : "claim_status_generation_stale";
|
|
749
|
+
const diagnostic = statusGenerationCurrent
|
|
750
|
+
? projectLifecycleDiagnostic(status.diagnostic, {
|
|
751
|
+
uid: uid ?? "00000000-0000-4000-8000-000000000000",
|
|
752
|
+
generation: generation ?? -1,
|
|
753
|
+
})
|
|
754
|
+
: unknownEvidence("claim_status_generation_stale");
|
|
755
|
+
if (workflow.state === "verified" && diagnostic.state === "verified") {
|
|
756
|
+
const current = workflow.value;
|
|
757
|
+
const report = diagnostic.value;
|
|
758
|
+
if (current.phase !== report.phase || current.retryClass !== report.retryClass
|
|
759
|
+
|| current.effectCertainty !== report.effectCertainty || current.nextAction !== report.nextAction
|
|
760
|
+
|| current.resumable !== report.resumable || current.retryAt !== report.retryAt) {
|
|
761
|
+
throw new InspectionConflictError("INSPECTION_CONFLICT");
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const historical = projectBootstrapAddress(statusGenerationCurrent ? status : undefined);
|
|
765
|
+
const rawRevalidation = safeRecord(safeRecord(rawWorkflow)?.networkRevalidation)
|
|
766
|
+
?? safeRecord(safeRecord(safeRecord(rawWorkflow)?.summary)?.networkRevalidation);
|
|
767
|
+
const selectedRequestId = input.expectedRequestId
|
|
768
|
+
?? (typeof rawRevalidation?.requestId === "string" ? rawRevalidation.requestId : undefined);
|
|
769
|
+
const validationAt = new Date().toISOString();
|
|
770
|
+
const networkRevalidation = statusFactsCurrent
|
|
771
|
+
? projectInspectionNetworkResult(claim, workflow, custody, expiry, historical, selectedRequestId, validationAt)
|
|
772
|
+
: selectedRequestId && workflow.state === "verified" && uid && generation !== undefined
|
|
773
|
+
? {
|
|
774
|
+
schema: "ct.appliance.cube-network-revalidation/v1",
|
|
775
|
+
status: "unknown",
|
|
776
|
+
requestId: selectedRequestId,
|
|
777
|
+
claimUid: uid,
|
|
778
|
+
generation,
|
|
779
|
+
operationId: workflow.value.operationId,
|
|
780
|
+
intentDigest: workflow.value.intentDigest,
|
|
781
|
+
reasonCode: staleStatusReason,
|
|
782
|
+
}
|
|
783
|
+
: undefined;
|
|
784
|
+
const networkReadiness = !statusGenerationCurrent
|
|
785
|
+
? unknownEvidence("claim_status_generation_stale")
|
|
786
|
+
: workflow.state !== "verified" && safeRecord(rawWorkflow)
|
|
787
|
+
? unknownEvidence("workflow_generation_unverified")
|
|
788
|
+
: projectCurrentNetworkReadiness(networkRevalidation, validationAt);
|
|
789
|
+
const currentAddress = networkRevalidation?.status === "verified"
|
|
790
|
+
? verifiedEvidence({ value: networkRevalidation.address, source: networkRevalidation.source, observedAt: networkRevalidation.observedAt, validUntil: networkRevalidation.validUntil }, "operator_cube_revalidation")
|
|
791
|
+
: historical.address;
|
|
792
|
+
const currentUuid = networkRevalidation?.status === "verified"
|
|
793
|
+
? verifiedEvidence({ value: networkRevalidation.guestUuid, observedAt: networkRevalidation.observedAt, validUntil: networkRevalidation.validUntil }, "operator_cube_revalidation")
|
|
794
|
+
: historical.uuid;
|
|
795
|
+
const currentMac = networkRevalidation?.status === "verified"
|
|
796
|
+
? verifiedEvidence({ value: networkRevalidation.mac.toLowerCase(), observedAt: networkRevalidation.observedAt, validUntil: networkRevalidation.validUntil }, "operator_cube_revalidation")
|
|
797
|
+
: historical.mac;
|
|
798
|
+
const accessPhaseTime = phaseTime(workflow, "ios_access");
|
|
799
|
+
const accessRaw = statusFactsCurrent
|
|
800
|
+
? safeRecord(rawWorkflow)?.summary?.accessObserved ?? safeRecord(rawWorkflow)?.accessObserved
|
|
801
|
+
: undefined;
|
|
802
|
+
const accessObserved = projectAccessObserved(accessRaw, accessPhaseTime);
|
|
803
|
+
const cube = safeRecord(spec.cube);
|
|
804
|
+
const desiredAccess = cube
|
|
805
|
+
? verifiedEvidence({
|
|
806
|
+
credentialSchema: "ct-appliance-cube-credentials/v1",
|
|
807
|
+
...(cube.role === "admin" || cube.role === "full_admin" ? { role: cube.role } : {}),
|
|
808
|
+
capabilities: ["resolved", "authenticated", "privileged"],
|
|
809
|
+
}, "claim_spec")
|
|
810
|
+
: unknownEvidence("access_requirement_not_projected");
|
|
811
|
+
const claimStatusPhase = typeof status.phase === "string" ? status.phase : undefined;
|
|
812
|
+
const readyResultTime = phaseTime(workflow, "ready");
|
|
813
|
+
const claimReadiness = statusFactsCurrent && readyResultTime && claimStatusPhase === "Ready"
|
|
814
|
+
? verifiedEvidence({ value: { state: "ready" }, observedAt: readyResultTime }, "operator_workflow_status")
|
|
815
|
+
: unknownEvidence(statusFactsCurrent ? "claim_readiness_source_unavailable" : staleStatusReason);
|
|
816
|
+
const providerReadiness = statusFactsCurrent
|
|
817
|
+
? phaseReadiness(workflow, "clone_bind")
|
|
818
|
+
: unknownEvidence(staleStatusReason);
|
|
819
|
+
const guestReadiness = statusFactsCurrent && currentUuid.state === "verified" && currentMac.state === "verified"
|
|
820
|
+
? verifiedEvidence({ value: { state: "ready" }, observedAt: currentUuid.value.observedAt, ...(currentUuid.value.validUntil ? { validUntil: currentUuid.value.validUntil } : {}) }, currentUuid.source)
|
|
821
|
+
: unknownEvidence(statusFactsCurrent ? "guest_identity_unverified" : staleStatusReason);
|
|
822
|
+
const sshReadiness = statusFactsCurrent && networkRevalidation?.status === "verified" && networkRevalidation.ssh.state === "verified"
|
|
823
|
+
? verifiedEvidence({ value: { state: "ready" }, observedAt: networkRevalidation.ssh.observedAt, validUntil: networkRevalidation.ssh.validUntil }, "same_guest_ssh")
|
|
824
|
+
: unknownEvidence(statusFactsCurrent ? "read_only_revalidation_required" : staleStatusReason);
|
|
825
|
+
const configTimes = [phaseTime(workflow, "running_config"), phaseTime(workflow, "startup_config")].filter((value) => Boolean(value));
|
|
826
|
+
const configurationReadiness = statusFactsCurrent && configTimes.length === 2
|
|
827
|
+
? verifiedEvidence({ value: { state: "ready" }, observedAt: configTimes.sort().at(-1) }, "operator_workflow_status")
|
|
828
|
+
: unknownEvidence(statusFactsCurrent ? "configuration_readback_unavailable" : staleStatusReason);
|
|
829
|
+
const applicationNotRequired = spec.profile === "cube";
|
|
830
|
+
const applicationReadiness = applicationNotRequired
|
|
831
|
+
? verifiedEvidence({ value: { state: "not_required" }, observedAt: readyResultTime ?? validDate(status.diagnostic?.observedAt) ?? retrievedAt }, "claim_profile")
|
|
832
|
+
: unknownEvidence("application_readiness_not_projected");
|
|
833
|
+
const backend = (() => {
|
|
834
|
+
try {
|
|
835
|
+
const selected = resolveLabKubernetesBackend();
|
|
836
|
+
const provenance = buildProvenance({
|
|
837
|
+
plane: context?.plane,
|
|
838
|
+
credentials: context?.credentials,
|
|
839
|
+
mockPermitted: context?.mockPermitted,
|
|
840
|
+
});
|
|
841
|
+
return verifiedEvidence({
|
|
842
|
+
kind: "kubernetes_claims",
|
|
843
|
+
mode: selected.mode,
|
|
844
|
+
contextFingerprint: inspectionDigest(selected.context),
|
|
845
|
+
serverFingerprint: inspectionDigest(selected.server),
|
|
846
|
+
plane: provenance.plane,
|
|
847
|
+
runtimeVersion: provenance.runtimeVersion,
|
|
848
|
+
mock: provenance.mock,
|
|
849
|
+
}, "configured_claim_backend");
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
return unknownEvidence("backend_identity_unavailable");
|
|
853
|
+
}
|
|
854
|
+
})();
|
|
855
|
+
const diagnosticEvidence = diagnostic;
|
|
856
|
+
const reason = diagnostic.state === "verified"
|
|
857
|
+
? verifiedEvidence({ code: diagnostic.value.code, classification: diagnostic.value.classification, evidenceRefs: diagnostic.value.evidenceRefs }, "operator_lifecycle_diagnostic")
|
|
858
|
+
: unknownEvidence("diagnostic_not_recorded");
|
|
859
|
+
const phaseValue = workflow.state === "verified" ? workflow.value : undefined;
|
|
860
|
+
const effectCertainty = phaseValue
|
|
861
|
+
? verifiedEvidence(phaseValue.effectCertainty, "operator_workflow_status")
|
|
862
|
+
: diagnostic.state === "verified" ? verifiedEvidence(diagnostic.value.effectCertainty, "operator_lifecycle_diagnostic") : verifiedEvidence("none", "read_only_inspection");
|
|
863
|
+
const retryClass = phaseValue
|
|
864
|
+
? verifiedEvidence(phaseValue.retryClass, "operator_workflow_status")
|
|
865
|
+
: diagnostic.state === "verified" ? verifiedEvidence(diagnostic.value.retryClass, "operator_lifecycle_diagnostic") : verifiedEvidence("never", "read_only_inspection");
|
|
866
|
+
const nextAction = phaseValue
|
|
867
|
+
? verifiedEvidence(phaseValue.nextAction, "operator_workflow_status")
|
|
868
|
+
: diagnostic.state === "verified" ? verifiedEvidence(diagnostic.value.nextAction, "operator_lifecycle_diagnostic") : verifiedEvidence("none", "read_only_inspection");
|
|
869
|
+
const hostObserved = networkRevalidation?.status === "verified"
|
|
870
|
+
? verifiedEvidence({ value: networkRevalidation.host, observedAt: networkRevalidation.observedAt, validUntil: networkRevalidation.validUntil }, "operator_cube_revalidation")
|
|
871
|
+
: unknownEvidence("provider_host_readback_unavailable");
|
|
872
|
+
const guestUuidDesired = custody.record?.provider?.uuid && UUID_PATTERN.test(custody.record.provider.uuid)
|
|
873
|
+
? verifiedEvidence(custody.record.provider.uuid, "claim_custody_provider_binding")
|
|
874
|
+
: unknownEvidence("provider_identity_not_recorded");
|
|
875
|
+
return buildLabInspectionEnvelope({
|
|
876
|
+
schema: "ct-lab-inspection/v1",
|
|
877
|
+
operation: { name: operation, intent: operation === "get_appliance_claim" ? "inspect_claim" : "revalidate_network" },
|
|
878
|
+
backend: {
|
|
879
|
+
identity: backend,
|
|
880
|
+
observedAt: backendObservedAt ? verifiedEvidence(backendObservedAt, "kubernetes_http_date") : unknownEvidence("source_observation_time_unavailable"),
|
|
881
|
+
retrievedAt: verifiedEvidence(retrievedAt, "adapter_read_boundary"),
|
|
882
|
+
},
|
|
883
|
+
claim: {
|
|
884
|
+
name: typeof metadata.name === "string" ? verifiedEvidence(metadata.name, "claim_metadata") : unknownEvidence("claim_name_unavailable"),
|
|
885
|
+
assetId: typeof spec.vmName === "string" && spec.vmName.length > 0 ? verifiedEvidence(spec.vmName, "claim_spec") : unknownEvidence("claim_asset_id_unavailable"),
|
|
886
|
+
uid: uid ? verifiedEvidence(uid, "claim_metadata") : unknownEvidence("claim_uid_unavailable"),
|
|
887
|
+
generation: generation !== undefined ? verifiedEvidence(generation, "claim_metadata") : unknownEvidence("claim_generation_unavailable"),
|
|
888
|
+
observedGeneration: Number.isSafeInteger(status.observedGeneration) ? verifiedEvidence(status.observedGeneration, "operator_claim_status") : unknownEvidence("claim_observed_generation_unavailable"),
|
|
889
|
+
resourceVersion: typeof metadata.resourceVersion === "string" && metadata.resourceVersion.length > 0 ? verifiedEvidence(metadata.resourceVersion, "claim_metadata") : unknownEvidence("claim_resource_version_unavailable"),
|
|
890
|
+
expiresAt: expiry,
|
|
891
|
+
custody: custody.evidence,
|
|
892
|
+
},
|
|
893
|
+
host: { canonical: {
|
|
894
|
+
desired: desiredHost ? verifiedEvidence(desiredHost, "claim_spec_and_custody") : unknownEvidence("canonical_host_not_bound"),
|
|
895
|
+
observed: hostObserved,
|
|
896
|
+
} },
|
|
897
|
+
guest: {
|
|
898
|
+
uuid: { desired: guestUuidDesired, observed: currentUuid },
|
|
899
|
+
mac: { desired: unknownEvidence("guest_mac_not_in_claim_spec"), observed: currentMac },
|
|
900
|
+
},
|
|
901
|
+
address: {
|
|
902
|
+
desired: typeof spec.targetIp === "string" && isIP(spec.targetIp) === 4
|
|
903
|
+
? verifiedEvidence({ value: spec.targetIp, source: "claim_spec" }, "claim_spec")
|
|
904
|
+
: unknownEvidence("dynamic_address_not_known_before_boot"),
|
|
905
|
+
observed: currentAddress,
|
|
906
|
+
},
|
|
907
|
+
access: { desired: desiredAccess, observed: accessObserved },
|
|
908
|
+
readiness: {
|
|
909
|
+
claim: readinessDimension(claimReadiness),
|
|
910
|
+
provider: readinessDimension(providerReadiness),
|
|
911
|
+
guestIdentity: readinessDimension(guestReadiness),
|
|
912
|
+
network: readinessDimension(networkReadiness),
|
|
913
|
+
ssh: readinessDimension(sshReadiness),
|
|
914
|
+
configuration: readinessDimension(configurationReadiness),
|
|
915
|
+
application: readinessDimension(applicationReadiness, applicationNotRequired ? "not_required" : "ready"),
|
|
916
|
+
},
|
|
917
|
+
...(networkRevalidation ? { networkRevalidation } : {}),
|
|
918
|
+
runtimeArtifact: readInstalledRuntimeArtifact(),
|
|
919
|
+
workflow,
|
|
920
|
+
diagnostic: diagnosticEvidence,
|
|
921
|
+
reason,
|
|
922
|
+
effectCertainty,
|
|
923
|
+
retryClass,
|
|
924
|
+
nextAction,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
347
927
|
export async function handleGetApplianceClaim(args, context) {
|
|
348
928
|
const parsed = GetApplianceClaimInputSchema.safeParse(args);
|
|
349
929
|
if (!parsed.success) {
|
|
@@ -351,25 +931,52 @@ export async function handleGetApplianceClaim(args, context) {
|
|
|
351
931
|
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, issues);
|
|
352
932
|
}
|
|
353
933
|
const forwarded = await forwardClaim("get_appliance_claim", parsed.data, context);
|
|
354
|
-
if (forwarded)
|
|
355
|
-
|
|
934
|
+
if (forwarded) {
|
|
935
|
+
if (forwarded.isError)
|
|
936
|
+
return forwarded;
|
|
937
|
+
const structured = safeRecord(forwarded.structuredContent);
|
|
938
|
+
const claim = safeRecord(structured?.claim);
|
|
939
|
+
if (!claim?.metadata)
|
|
940
|
+
return forwarded;
|
|
941
|
+
try {
|
|
942
|
+
const inspection = buildClaimInspection(claim, parsed.data, new Date().toISOString(), undefined, context);
|
|
943
|
+
const body = { ...(structured ?? {}), inspection };
|
|
944
|
+
return { ...forwarded, content: [{ type: "text", text: JSON.stringify(body, null, 2) }], structuredContent: body };
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
if (error instanceof InspectionConflictError) {
|
|
948
|
+
return createToolErrorResult("INSPECTION_CONFLICT", "Forwarded live claim identity or lifecycle sources disagree; inspect the current claim before continuing.", {
|
|
949
|
+
operation: "get_appliance_claim",
|
|
950
|
+
name: parsed.data.name,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
return createToolErrorResult("INSPECTION_CONTRACT_INVALID", "The forwarded claim did not meet the redacted inspection contract.", {
|
|
954
|
+
operation: "get_appliance_claim",
|
|
955
|
+
name: parsed.data.name,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
356
959
|
const { name, namespace } = parsed.data;
|
|
357
960
|
try {
|
|
358
961
|
const apiPath = `/apis/calltelemetry.com/v1alpha1/namespaces/${namespace}/applianceclaims/${name}`;
|
|
359
|
-
const
|
|
962
|
+
const apiObservation = await queryKubernetesApiWithObservation(apiPath);
|
|
963
|
+
const apiRes = apiObservation?.data;
|
|
360
964
|
const claim = apiRes?.metadata
|
|
361
965
|
? apiRes
|
|
362
966
|
: JSON.parse((await runLabKubectl(["get", "applianceclaim", name, "-n", namespace, "-o", "json"])).stdout);
|
|
967
|
+
const retrievedAt = new Date().toISOString();
|
|
968
|
+
if (!claim?.metadata)
|
|
969
|
+
throw new Error("claim read did not return the requested resource");
|
|
970
|
+
const inspection = buildClaimInspection(claim, parsed.data, retrievedAt, apiObservation?.observedAt, context);
|
|
971
|
+
const body = { success: true, claim, inspection };
|
|
363
972
|
return {
|
|
364
973
|
content: [
|
|
365
974
|
{
|
|
366
975
|
type: "text",
|
|
367
|
-
text: JSON.stringify(
|
|
368
|
-
success: true,
|
|
369
|
-
claim
|
|
370
|
-
}, null, 2)
|
|
976
|
+
text: JSON.stringify(body, null, 2)
|
|
371
977
|
}
|
|
372
|
-
]
|
|
978
|
+
],
|
|
979
|
+
structuredContent: body,
|
|
373
980
|
};
|
|
374
981
|
}
|
|
375
982
|
catch (err) {
|
|
@@ -378,7 +985,194 @@ export async function handleGetApplianceClaim(args, context) {
|
|
|
378
985
|
if (cfg)
|
|
379
986
|
return cfg;
|
|
380
987
|
}
|
|
381
|
-
|
|
988
|
+
if (err instanceof InspectionConflictError) {
|
|
989
|
+
return createToolErrorResult("INSPECTION_CONFLICT", "Live claim identity or lifecycle sources disagree; inspect the current claim before continuing.", {
|
|
990
|
+
operation: "get_appliance_claim",
|
|
991
|
+
name,
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, `ApplianceClaim '${name}' not found in namespace '${namespace}'.`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function rejectedRevalidationResult(input, live, reasonCode) {
|
|
998
|
+
const workflow = safeRecord(live?.status?.cubeWorkflow);
|
|
999
|
+
const summary = safeRecord(workflow?.summary);
|
|
1000
|
+
const actualOperationId = typeof summary?.operationId === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(summary.operationId)
|
|
1001
|
+
? summary.operationId : input.operationId;
|
|
1002
|
+
const actualIntentDigest = typeof summary?.intentDigest === "string" && /^sha256:[a-f0-9]{64}$/.test(summary.intentDigest)
|
|
1003
|
+
? summary.intentDigest : input.intentDigest;
|
|
1004
|
+
const identity = {
|
|
1005
|
+
schema: "ct.appliance.cube-network-revalidation/v1",
|
|
1006
|
+
status: "rejected",
|
|
1007
|
+
requestId: input.requestId,
|
|
1008
|
+
claimUid: typeof live?.metadata?.uid === "string" && UUID_PATTERN.test(live.metadata.uid)
|
|
1009
|
+
? live.metadata.uid : input.expectedUid,
|
|
1010
|
+
generation: Number.isSafeInteger(live?.metadata?.generation) && live.metadata.generation > 0
|
|
1011
|
+
? live.metadata.generation : input.expectedGeneration,
|
|
1012
|
+
operationId: actualOperationId,
|
|
1013
|
+
intentDigest: actualIntentDigest,
|
|
1014
|
+
reasonCode,
|
|
1015
|
+
};
|
|
1016
|
+
const result = createToolErrorResult("INSPECTION_CONFLICT", "The read-only network revalidation request was refused by the live claim preconditions.", {
|
|
1017
|
+
operation: "revalidate_appliance_claim_network",
|
|
1018
|
+
reasonCode,
|
|
1019
|
+
});
|
|
1020
|
+
const structured = result.structuredContent;
|
|
1021
|
+
return {
|
|
1022
|
+
...result,
|
|
1023
|
+
structuredContent: { ...(structured ?? {}), networkRevalidation: identity },
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
async function readLiveClaim(name, namespace) {
|
|
1027
|
+
const path = `/apis/calltelemetry.com/v1alpha1/namespaces/${encodeURIComponent(namespace)}/applianceclaims/${encodeURIComponent(name)}`;
|
|
1028
|
+
const apiObservation = await queryKubernetesApiWithObservation(path);
|
|
1029
|
+
if (safeRecord(apiObservation?.data)?.metadata)
|
|
1030
|
+
return { claim: apiObservation.data, observedAt: apiObservation?.observedAt };
|
|
1031
|
+
const { stdout } = await runLabKubectl(["get", "applianceclaim", name, "-n", namespace, "-o", "json"]);
|
|
1032
|
+
const claim = JSON.parse(stdout);
|
|
1033
|
+
if (!safeRecord(claim)?.metadata)
|
|
1034
|
+
throw new Error("claim read did not return the requested resource");
|
|
1035
|
+
return { claim };
|
|
1036
|
+
}
|
|
1037
|
+
export async function handleRevalidateApplianceClaimNetwork(args, context) {
|
|
1038
|
+
const parsed = RevalidateApplianceClaimNetworkInputSchema.safeParse(args);
|
|
1039
|
+
if (!parsed.success) {
|
|
1040
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
1041
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, issues);
|
|
1042
|
+
}
|
|
1043
|
+
const input = parsed.data;
|
|
1044
|
+
const forwarded = await forwardClaim("revalidate_appliance_claim_network", input, context);
|
|
1045
|
+
if (forwarded)
|
|
1046
|
+
return forwarded;
|
|
1047
|
+
let lastLiveClaim;
|
|
1048
|
+
try {
|
|
1049
|
+
let { claim, observedAt } = await readLiveClaim(input.name, input.namespace);
|
|
1050
|
+
lastLiveClaim = claim;
|
|
1051
|
+
const meta = safeRecord(claim.metadata) ?? {};
|
|
1052
|
+
const status = safeRecord(claim.status) ?? {};
|
|
1053
|
+
const workflow = safeRecord(status.cubeWorkflow);
|
|
1054
|
+
const summary = safeRecord(workflow?.summary);
|
|
1055
|
+
const initialRefusal = activeCubeClaimRefusal(claim, input);
|
|
1056
|
+
if (initialRefusal)
|
|
1057
|
+
return rejectedRevalidationResult(input, claim, initialRefusal);
|
|
1058
|
+
const expiry = claimExpiry(claim);
|
|
1059
|
+
if (expiry.state !== "verified" || !expiry.value)
|
|
1060
|
+
return rejectedRevalidationResult(input, claim, "claim_inactive");
|
|
1061
|
+
const annotations = safeRecord(meta.annotations) ?? {};
|
|
1062
|
+
const existingStatus = safeRecord(workflow?.networkRevalidation)
|
|
1063
|
+
?? safeRecord(summary?.networkRevalidation);
|
|
1064
|
+
const existingRequest = projectRequestAnnotation(annotations[CUBE_REVALIDATION_REQUEST_ANNOTATION]);
|
|
1065
|
+
const existingRequestExpired = existingRequest !== undefined
|
|
1066
|
+
&& Date.parse(String(existingRequest.requestExpiresAt)) <= Date.now();
|
|
1067
|
+
const identityMatches = (record) => record
|
|
1068
|
+
&& record.requestId === input.requestId
|
|
1069
|
+
&& record.claimUid === input.expectedUid
|
|
1070
|
+
&& record.generation === input.expectedGeneration
|
|
1071
|
+
&& record.operationId === input.operationId
|
|
1072
|
+
&& record.intentDigest === input.intentDigest;
|
|
1073
|
+
if (existingStatus?.requestId === input.requestId && !identityMatches(existingStatus)) {
|
|
1074
|
+
return rejectedRevalidationResult(input, claim, "request_id_identity_conflict");
|
|
1075
|
+
}
|
|
1076
|
+
if (existingRequest && existingRequest.requestId !== input.requestId && !existingRequestExpired) {
|
|
1077
|
+
return rejectedRevalidationResult(input, claim, "another_revalidation_request_is_active");
|
|
1078
|
+
}
|
|
1079
|
+
if (existingRequest?.requestId === input.requestId && !identityMatches(existingRequest)) {
|
|
1080
|
+
return rejectedRevalidationResult(input, claim, "request_id_identity_conflict");
|
|
1081
|
+
}
|
|
1082
|
+
let selectedClaim = claim;
|
|
1083
|
+
if ((!existingStatus || existingStatus.requestId !== input.requestId)
|
|
1084
|
+
&& (!existingRequest || (existingRequest.requestId !== input.requestId && existingRequestExpired))) {
|
|
1085
|
+
const requestedAtMs = Date.now();
|
|
1086
|
+
const requestExpiresAtMs = Math.min(requestedAtMs + NETWORK_REVALIDATION_REQUEST_TTL_MS, Date.parse(expiry.value));
|
|
1087
|
+
if (requestExpiresAtMs <= requestedAtMs)
|
|
1088
|
+
return rejectedRevalidationResult(input, claim, "claim_inactive");
|
|
1089
|
+
const requestValue = {
|
|
1090
|
+
schema: CUBE_REVALIDATION_REQUEST_SCHEMA,
|
|
1091
|
+
requestId: input.requestId,
|
|
1092
|
+
claimUid: input.expectedUid,
|
|
1093
|
+
generation: input.expectedGeneration,
|
|
1094
|
+
operationId: input.operationId,
|
|
1095
|
+
intentDigest: input.intentDigest,
|
|
1096
|
+
requestedAt: new Date(requestedAtMs).toISOString(),
|
|
1097
|
+
requestExpiresAt: new Date(requestExpiresAtMs).toISOString(),
|
|
1098
|
+
};
|
|
1099
|
+
const annotationPath = `/metadata/annotations/${CUBE_REVALIDATION_REQUEST_ANNOTATION.replace(/~/g, "~0").replace(/\//g, "~1")}`;
|
|
1100
|
+
const patch = [
|
|
1101
|
+
{ op: "test", path: "/metadata/uid", value: input.expectedUid },
|
|
1102
|
+
{ op: "test", path: "/metadata/generation", value: input.expectedGeneration },
|
|
1103
|
+
{ op: "test", path: "/metadata/resourceVersion", value: meta.resourceVersion },
|
|
1104
|
+
{ op: "test", path: `/metadata/annotations/${CUSTODY_ANNOTATION.replace(/~/g, "~0").replace(/\//g, "~1")}`, value: annotations[CUSTODY_ANNOTATION] },
|
|
1105
|
+
{ op: "add", path: annotationPath, value: JSON.stringify(requestValue) },
|
|
1106
|
+
];
|
|
1107
|
+
const apiPath = `/apis/calltelemetry.com/v1alpha1/namespaces/${encodeURIComponent(input.namespace)}/applianceclaims/${encodeURIComponent(input.name)}`;
|
|
1108
|
+
const patchObservation = await queryKubernetesApiWithObservation(apiPath, {
|
|
1109
|
+
method: "PATCH",
|
|
1110
|
+
body: patch,
|
|
1111
|
+
contentType: "application/json-patch+json",
|
|
1112
|
+
timeoutMs: 10_000,
|
|
1113
|
+
});
|
|
1114
|
+
if (safeRecord(patchObservation?.data)?.metadata) {
|
|
1115
|
+
selectedClaim = patchObservation.data;
|
|
1116
|
+
lastLiveClaim = selectedClaim;
|
|
1117
|
+
observedAt = patchObservation?.observedAt ?? observedAt;
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
const { stdout } = await runLabKubectl([
|
|
1121
|
+
"patch", "applianceclaim", input.name, "-n", input.namespace,
|
|
1122
|
+
"--type=json", "--patch", JSON.stringify(patch), "-o", "json",
|
|
1123
|
+
], { timeoutMs: 10_000 });
|
|
1124
|
+
selectedClaim = JSON.parse(stdout);
|
|
1125
|
+
lastLiveClaim = selectedClaim;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const selectedMeta = safeRecord(selectedClaim.metadata) ?? {};
|
|
1129
|
+
if (selectedMeta.uid !== input.expectedUid || selectedMeta.generation !== input.expectedGeneration) {
|
|
1130
|
+
return rejectedRevalidationResult(input, selectedClaim, "claim_identity_conflict");
|
|
1131
|
+
}
|
|
1132
|
+
const selectedReceipt = safeRecord(selectedClaim.status?.cubeWorkflow?.networkRevalidation)
|
|
1133
|
+
?? safeRecord(selectedClaim.status?.cubeWorkflow?.summary?.networkRevalidation);
|
|
1134
|
+
const selectedRequest = projectRequestAnnotation(safeRecord(selectedMeta.annotations)?.[CUBE_REVALIDATION_REQUEST_ANNOTATION]);
|
|
1135
|
+
if (!identityMatches(selectedReceipt) && !identityMatches(selectedRequest)) {
|
|
1136
|
+
return rejectedRevalidationResult(input, selectedClaim, "request_annotation_not_persisted");
|
|
1137
|
+
}
|
|
1138
|
+
if (!claimHasFinalRevalidation(selectedClaim, input.requestId, input.expectedUid, input.expectedGeneration)) {
|
|
1139
|
+
const watched = await watchClaimRevalidation(input.namespace, input.name, typeof selectedMeta.resourceVersion === "string" ? selectedMeta.resourceVersion : String(meta.resourceVersion ?? ""), input.requestId, input.expectedUid, input.expectedGeneration);
|
|
1140
|
+
if (watched) {
|
|
1141
|
+
if (watched.deleted)
|
|
1142
|
+
return rejectedRevalidationResult(input, watched.claim, "claim_deleted");
|
|
1143
|
+
selectedClaim = watched.claim;
|
|
1144
|
+
lastLiveClaim = selectedClaim;
|
|
1145
|
+
observedAt = watched.observedAt ?? observedAt;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
else if (selectedReceipt?.requestId !== input.requestId) {
|
|
1149
|
+
return rejectedRevalidationResult(input, selectedClaim, "request_id_identity_conflict");
|
|
1150
|
+
}
|
|
1151
|
+
const finalRefusal = activeCubeClaimRefusal(selectedClaim, input);
|
|
1152
|
+
if (finalRefusal)
|
|
1153
|
+
return rejectedRevalidationResult(input, selectedClaim, finalRefusal);
|
|
1154
|
+
const retrievedAt = new Date().toISOString();
|
|
1155
|
+
const inspection = buildClaimInspection(selectedClaim, { name: input.name, namespace: input.namespace, expectedUid: input.expectedUid, expectedGeneration: input.expectedGeneration, expectedRequestId: input.requestId }, retrievedAt, observedAt, context, "revalidate_appliance_claim_network");
|
|
1156
|
+
const body = { success: true, claim: selectedClaim, inspection };
|
|
1157
|
+
return {
|
|
1158
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
1159
|
+
structuredContent: body,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
const config = claimBackendConfigErrorResult(error, "revalidate_appliance_claim_network");
|
|
1164
|
+
if (config)
|
|
1165
|
+
return config;
|
|
1166
|
+
if (error instanceof InspectionConflictError) {
|
|
1167
|
+
return rejectedRevalidationResult(input, lastLiveClaim, "inspection_conflict");
|
|
1168
|
+
}
|
|
1169
|
+
if (error instanceof Error && /Kubernetes API request failed \(409\)/u.test(error.message)) {
|
|
1170
|
+
return rejectedRevalidationResult(input, lastLiveClaim, "claim_resource_version_conflict");
|
|
1171
|
+
}
|
|
1172
|
+
return createToolErrorResult("NETWORK_REVALIDATION_REQUEST_FAILED", "The read-only network revalidation request could not be recorded.", {
|
|
1173
|
+
operation: "revalidate_appliance_claim_network",
|
|
1174
|
+
requestId: input.requestId,
|
|
1175
|
+
});
|
|
382
1176
|
}
|
|
383
1177
|
}
|
|
384
1178
|
/**
|