@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.
Files changed (64) hide show
  1. package/README.md +1 -0
  2. package/dist/src/generated-version.d.ts +1 -1
  3. package/dist/src/generated-version.js +1 -1
  4. package/dist/src/inspection/lab-inspection.d.ts +16293 -0
  5. package/dist/src/inspection/lab-inspection.d.ts.map +1 -0
  6. package/dist/src/inspection/lab-inspection.js +964 -0
  7. package/dist/src/inspection/lab-inspection.js.map +1 -0
  8. package/dist/src/inspection/read-provenance.d.ts +44 -0
  9. package/dist/src/inspection/read-provenance.d.ts.map +1 -0
  10. package/dist/src/inspection/read-provenance.js +28 -0
  11. package/dist/src/inspection/read-provenance.js.map +1 -0
  12. package/dist/src/inspection/runtime-artifact.d.ts +16 -0
  13. package/dist/src/inspection/runtime-artifact.d.ts.map +1 -0
  14. package/dist/src/inspection/runtime-artifact.js +99 -0
  15. package/dist/src/inspection/runtime-artifact.js.map +1 -0
  16. package/dist/src/lab-exec/claim-resolve.d.ts +97 -0
  17. package/dist/src/lab-exec/claim-resolve.d.ts.map +1 -1
  18. package/dist/src/lab-exec/claim-resolve.js +5 -0
  19. package/dist/src/lab-exec/claim-resolve.js.map +1 -1
  20. package/dist/src/lab-exec/executor.d.ts +3 -0
  21. package/dist/src/lab-exec/executor.d.ts.map +1 -1
  22. package/dist/src/lab-exec/executor.js +4 -1
  23. package/dist/src/lab-exec/executor.js.map +1 -1
  24. package/dist/src/lab-exec/remote.d.ts +3 -0
  25. package/dist/src/lab-exec/remote.d.ts.map +1 -1
  26. package/dist/src/lab-exec/remote.js +5 -0
  27. package/dist/src/lab-exec/remote.js.map +1 -1
  28. package/dist/src/lab-exec/server.d.ts.map +1 -1
  29. package/dist/src/lab-exec/server.js +3 -2
  30. package/dist/src/lab-exec/server.js.map +1 -1
  31. package/dist/src/tools/appliance-candidate-retire.d.ts +6 -6
  32. package/dist/src/tools/appliance-candidate-update.d.ts +33 -6
  33. package/dist/src/tools/appliance-candidate-update.d.ts.map +1 -1
  34. package/dist/src/tools/appliance-candidate-update.js +99 -4
  35. package/dist/src/tools/appliance-candidate-update.js.map +1 -1
  36. package/dist/src/tools/appliance-claim-tools.d.ts +62 -3
  37. package/dist/src/tools/appliance-claim-tools.d.ts.map +1 -1
  38. package/dist/src/tools/appliance-claim-tools.js +810 -16
  39. package/dist/src/tools/appliance-claim-tools.js.map +1 -1
  40. package/dist/src/tools/claim-lifecycle.d.ts +4 -4
  41. package/dist/src/tools/claimed-import-ova.d.ts +9 -9
  42. package/dist/src/tools/claimed-import-qcow.d.ts +2 -2
  43. package/dist/src/tools/cube-canonical.d.ts +2 -2
  44. package/dist/src/tools/host-migrate-vm.d.ts +4 -4
  45. package/dist/src/tools/import-qcow.d.ts +6 -6
  46. package/dist/src/tools/index.d.ts.map +1 -1
  47. package/dist/src/tools/index.js +8 -3
  48. package/dist/src/tools/index.js.map +1 -1
  49. package/dist/src/tools/lab-dev.d.ts +38 -38
  50. package/dist/src/tools/list-hosts.d.ts.map +1 -1
  51. package/dist/src/tools/list-hosts.js +16 -4
  52. package/dist/src/tools/list-hosts.js.map +1 -1
  53. package/dist/src/tools/list-inventory.d.ts.map +1 -1
  54. package/dist/src/tools/list-inventory.js +13 -3
  55. package/dist/src/tools/list-inventory.js.map +1 -1
  56. package/dist/src/tools/provision-tools.d.ts +20 -20
  57. package/dist/src/tools/quick-provision.d.ts +2 -2
  58. package/dist/src/tools/sandbox-tools.d.ts +4 -4
  59. package/dist/src/tools/slot-tools.d.ts +8 -8
  60. package/dist/src/tools/storage-tools.d.ts +8 -8
  61. package/dist/src/tools/uat-access.d.ts +2 -2
  62. package/dist/src/tools/vm-action.d.ts +8 -8
  63. package/dist/src/tools/vm-repair.d.ts +6 -6
  64. 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 queryKubernetesApi(path, options = {}) {
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
- return res.json();
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, behind the executor's kill switch,
262
- * create/renew/delete/pause. Forward only when the local backend is unconfigured, so
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
- return forwarded;
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 apiRes = await queryKubernetesApi(apiPath);
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
- return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, `ApplianceClaim '${name}' not found in namespace '${namespace}': ${err.message}`);
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
  /**