@calltelemetry/ct-lab-mcp 0.8.90 → 0.8.92
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 +4 -4
- package/dist/src/tools/appliance-claim-tools.d.ts.map +1 -1
- package/dist/src/tools/appliance-claim-tools.js +12 -10
- package/dist/src/tools/appliance-claim-tools.js.map +1 -1
- package/dist/src/tools/claim-creation-projection.js +2 -2
- package/dist/src/tools/claim-creation-projection.js.map +1 -1
- package/dist/src/tools/claim-custody.d.ts +175 -0
- package/dist/src/tools/claim-custody.d.ts.map +1 -0
- package/dist/src/tools/claim-custody.js +196 -0
- package/dist/src/tools/claim-custody.js.map +1 -0
- package/dist/src/tools/claim-kubernetes.d.ts +14 -0
- package/dist/src/tools/claim-kubernetes.d.ts.map +1 -0
- package/dist/src/tools/{slot-kubernetes.js → claim-kubernetes.js} +30 -26
- package/dist/src/tools/claim-kubernetes.js.map +1 -0
- package/dist/src/tools/claim-reconcile-gate.d.ts +56 -0
- package/dist/src/tools/claim-reconcile-gate.d.ts.map +1 -0
- package/dist/src/tools/claim-reconcile-gate.js +110 -0
- package/dist/src/tools/claim-reconcile-gate.js.map +1 -0
- package/dist/src/tools/claimed-import-qcow.d.ts +3 -0
- package/dist/src/tools/claimed-import-qcow.d.ts.map +1 -1
- package/dist/src/tools/claimed-import-qcow.js +17 -0
- package/dist/src/tools/claimed-import-qcow.js.map +1 -1
- package/dist/src/tools/index.js +1 -1
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/kubernetes-backend.js +1 -1
- package/dist/src/tools/kubernetes-backend.js.map +1 -1
- package/dist/src/tools/{physical-slot.d.ts → legacy-reservation.d.ts} +79 -40
- package/dist/src/tools/legacy-reservation.d.ts.map +1 -0
- package/dist/src/tools/legacy-reservation.js +111 -0
- package/dist/src/tools/legacy-reservation.js.map +1 -0
- package/dist/src/tools/renewal-claim.d.ts +2 -1
- package/dist/src/tools/renewal-claim.d.ts.map +1 -1
- package/dist/src/tools/renewal-claim.js +19 -8
- package/dist/src/tools/renewal-claim.js.map +1 -1
- package/package.json +2 -2
- package/dist/src/tools/physical-slot.d.ts.map +0 -1
- package/dist/src/tools/physical-slot.js +0 -319
- package/dist/src/tools/physical-slot.js.map +0 -1
- package/dist/src/tools/slot-kubernetes.d.ts +0 -14
- package/dist/src/tools/slot-kubernetes.d.ts.map +0 -1
- package/dist/src/tools/slot-kubernetes.js.map +0 -1
- /package/dist/src/tools/{physical-slot-claim-spec-schema.v1.json → appliance-claim-spec-schema.v1.json} +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApplianceClaim custody (REL-1746, supersedes physical-slot reservation Leases).
|
|
3
|
+
*
|
|
4
|
+
* The original-owner record lives ON the claim as annotation `calltelemetry.com/claim-custody`
|
|
5
|
+
* (canonical JSON, <= 64 KiB, strict parse), written in the SAME create call as the claim. No Lease is
|
|
6
|
+
* ever created, renewed or required: per-guest serialization is in-process keyed by (node, vmid) and
|
|
7
|
+
* cross-actor safety is the claim UID/resourceVersion plus the claim-occupancy scan. Deletion only
|
|
8
|
+
* requests claim deletion with UID + resourceVersion preconditions; the operator owns teardown.
|
|
9
|
+
* Claims that still hold a legacy reservation Lease are verified READ-ONLY (the operator migrates them).
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { actualClaimIssue, claimCreationSha256, effectiveClaimSpec } from "./claim-creation-projection.js";
|
|
14
|
+
import { createClaimApi } from "./claim-kubernetes.js";
|
|
15
|
+
import { ContextSchema, GuestSchema, ProviderSchema, annotationJson, canonicalJson, claimGuest, digest, objectName, requireCustody, readLegacyReservation, sha, uuid, verifyLegacyClaim, legacyReservationLeaseName, } from "./legacy-reservation.js";
|
|
16
|
+
export { canonicalJson, claimGuest, objectName };
|
|
17
|
+
export const CUSTODY_ANNOTATION = "calltelemetry.com/claim-custody";
|
|
18
|
+
const CLAIMS = "/apis/calltelemetry.com/v1alpha1/namespaces/ct-lab/applianceclaims";
|
|
19
|
+
export const CustodySchema = z.object({
|
|
20
|
+
schema: z.literal("claim-custody/v1"), guest: GuestSchema,
|
|
21
|
+
phase: z.enum(["bound", "provider-bound", "retiring"]),
|
|
22
|
+
operationNonce: uuid, requestSha256: sha, context: ContextSchema,
|
|
23
|
+
provider: ProviderSchema.nullable(),
|
|
24
|
+
}).strict();
|
|
25
|
+
/** Strict custody parse: closed field set, duplicate keys rejected, provider/phase coherence, sorted volumes. */
|
|
26
|
+
export function parseCustody(text) {
|
|
27
|
+
const record = CustodySchema.parse(annotationJson(text));
|
|
28
|
+
if (record.provider)
|
|
29
|
+
requireCustody(canonicalJson(record.provider.volumes) === canonicalJson([...new Set(record.provider.volumes)].sort()), "provider volumes are not sorted and unique");
|
|
30
|
+
requireCustody(record.phase !== "provider-bound" || record.provider !== null, "provider phase lacks immutable provider binding");
|
|
31
|
+
requireCustody(record.phase !== "bound" || record.provider === null, "bound phase carries a provider binding");
|
|
32
|
+
return record;
|
|
33
|
+
}
|
|
34
|
+
/** The custody record of a claim, or null when the annotation is absent. Malformed records throw. */
|
|
35
|
+
export function claimCustody(claim) {
|
|
36
|
+
const text = claim?.metadata?.annotations?.[CUSTODY_ANNOTATION];
|
|
37
|
+
return text === undefined ? null : parseCustody(text);
|
|
38
|
+
}
|
|
39
|
+
export function creationOwnerSha(record) {
|
|
40
|
+
const { schema, guest, operationNonce, requestSha256, context } = CustodySchema.parse(record);
|
|
41
|
+
return digest({ schema, guest, operationNonce, requestSha256, context });
|
|
42
|
+
}
|
|
43
|
+
/** Verifies a live claim against its own custody record. */
|
|
44
|
+
export function verifyCustodyClaim(live, record, expectedUid) {
|
|
45
|
+
requireCustody(live?.apiVersion === "calltelemetry.com/v1alpha1" && live.kind === "ApplianceClaim"
|
|
46
|
+
&& live.metadata?.namespace === "ct-lab" && objectName.safeParse(live.metadata.name).success
|
|
47
|
+
&& uuid.safeParse(live.metadata.uid).success && (!expectedUid || live.metadata.uid === expectedUid)
|
|
48
|
+
&& typeof live.metadata.resourceVersion === "string" && live.metadata.resourceVersion.length > 0
|
|
49
|
+
&& canonicalJson(claimGuest(live)) === canonicalJson(record.guest)
|
|
50
|
+
&& (actualClaimIssue(live) ?? null) === (record.context.issueKey ?? null)
|
|
51
|
+
&& claimCreationSha256(live) === record.requestSha256, "claim UID, guest tuple or source lineage differs");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A claim with neither a custody annotation nor a legacy reservation Lease was created outside the guarded
|
|
55
|
+
* path: there is no original-owner record to retire it against. Distinct from a record that exists but
|
|
56
|
+
* fails identity checks.
|
|
57
|
+
*/
|
|
58
|
+
export class NoClaimCustodyError extends Error {
|
|
59
|
+
claimName;
|
|
60
|
+
guest;
|
|
61
|
+
constructor(claimName, guest) {
|
|
62
|
+
super(`ApplianceClaim '${claimName}' has no claim-custody annotation (and no legacy reservation Lease) for ${guest.node}/${guest.vmid}: it was created outside the guarded create path, so the guard has no original-owner record to retire it against; nothing was modified`);
|
|
63
|
+
this.claimName = claimName;
|
|
64
|
+
this.guest = guest;
|
|
65
|
+
this.name = "NoClaimCustodyError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const defaults = () => ({ api: createClaimApi(), now: Date.now, nonce: randomUUID });
|
|
69
|
+
const tails = new Map();
|
|
70
|
+
/** In-process serialization keyed by (node, vmid). Single MCP replica; cross-actor safety is claim UID/resourceVersion. */
|
|
71
|
+
function serialized(guest, action) {
|
|
72
|
+
const key = `${guest.node}/${guest.vmid}`;
|
|
73
|
+
const run = (tails.get(key) ?? Promise.resolve()).catch(() => { }).then(action);
|
|
74
|
+
const tail = run.catch(() => { });
|
|
75
|
+
tails.set(key, tail);
|
|
76
|
+
void tail.then(() => { if (tails.get(key) === tail)
|
|
77
|
+
tails.delete(key); });
|
|
78
|
+
return run;
|
|
79
|
+
}
|
|
80
|
+
const nodeAlias = (node) => node === "proxmox-lab" ? "proxmox" : ["proxmox-mini", "proxmox-minis-lab"].includes(node) ? "minis" : node === "proxmox-ibm64" ? "ibm64" : node;
|
|
81
|
+
async function noOtherClaim(deps, guest, ownUid) {
|
|
82
|
+
let next = "";
|
|
83
|
+
for (let page = 0; page < 10; page++) {
|
|
84
|
+
const result = await deps.api.request(`${CLAIMS}?limit=500${next ? `&continue=${encodeURIComponent(next)}` : ""}`, "GET");
|
|
85
|
+
requireCustody(result?.kind === "ApplianceClaimList" && result.apiVersion === "calltelemetry.com/v1alpha1" && Array.isArray(result.items), "claim occupancy list unavailable");
|
|
86
|
+
for (const claim of result.items) {
|
|
87
|
+
if (claim.metadata?.uid === ownUid)
|
|
88
|
+
continue;
|
|
89
|
+
const alias = nodeAlias(claim.spec?.node);
|
|
90
|
+
if (String(claim.spec?.vmid) === String(guest.vmid) && (alias === guest.node || !["minis", "proxmox", "ibm64", "esxi"].includes(alias)))
|
|
91
|
+
throw new Error("claim custody guest has another existing ApplianceClaim; custody unchanged");
|
|
92
|
+
}
|
|
93
|
+
next = result.metadata?.continue || "";
|
|
94
|
+
requireCustody(typeof next === "string", "claim occupancy pagination malformed");
|
|
95
|
+
if (!next)
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
throw new Error("claim custody occupancy scan incomplete; custody unchanged");
|
|
99
|
+
}
|
|
100
|
+
function requireResumable(live, manifest, deps) {
|
|
101
|
+
requireCustody(!live.metadata.deletionTimestamp && live.spec?.closed !== true && live.spec?.state !== "Closed" && live.status?.phase !== "Closed", "bound claim is retiring");
|
|
102
|
+
requireCustody(Number.isInteger(live.spec?.ttlHours) && live.spec.ttlHours >= manifest.spec.ttlHours && live.spec.ttlHours <= 8760 && live.spec.claimType !== "fixture", "bound claim finite TTL differs");
|
|
103
|
+
if (live.status?.phase === "Ready" || live.status?.completionTime) {
|
|
104
|
+
const baseline = Date.parse(live.status?.completionTime || "");
|
|
105
|
+
requireCustody(Number.isFinite(baseline) && baseline + live.spec.ttlHours * 3_600_000 > deps.now(), "bound claim expired or Ready TTL baseline unavailable");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function createClaimWithCustody(manifest, deps = defaults()) {
|
|
109
|
+
requireCustody(manifest.metadata?.namespace === "ct-lab", "namespace must be ct-lab");
|
|
110
|
+
objectName.parse(manifest.metadata.name);
|
|
111
|
+
manifest = { ...manifest, spec: effectiveClaimSpec(manifest.spec) };
|
|
112
|
+
const guest = claimGuest(manifest);
|
|
113
|
+
requireCustody(typeof manifest.spec.vmName === "string" && manifest.spec.vmName.length > 0, "constructor must provide the actual VM name before claim POST");
|
|
114
|
+
requireCustody(Number.isInteger(manifest.spec?.ttlHours) && manifest.spec.ttlHours > 0 && manifest.spec.ttlHours <= 8760, "dynamic claim requires finite TTL");
|
|
115
|
+
const context = ContextSchema.parse({ kind: "mcp", ...(actualClaimIssue(manifest) ? { issueKey: actualClaimIssue(manifest) } : {}) });
|
|
116
|
+
const requestSha256 = claimCreationSha256(manifest);
|
|
117
|
+
const path = `${CLAIMS}/${manifest.metadata.name}`;
|
|
118
|
+
return serialized(guest, async () => {
|
|
119
|
+
const existing = await deps.api.request(path, "GET");
|
|
120
|
+
if (existing)
|
|
121
|
+
return resume(existing, manifest, guest, context, requestSha256, deps);
|
|
122
|
+
await noOtherClaim(deps, guest);
|
|
123
|
+
// A stale legacy reservation for a different (now absent) claim blocks reuse of the guest until the
|
|
124
|
+
// operator's legacy sweep removes it; we never delete it ourselves.
|
|
125
|
+
const stale = await readLegacyReservation(deps.api, guest);
|
|
126
|
+
requireCustody(!stale, `guest ${guest.node}/${guest.vmid} still has a legacy reservation Lease (${legacyReservationLeaseName(guest)}) for another claim; the operator migrates or sweeps it`);
|
|
127
|
+
const record = CustodySchema.parse({ schema: "claim-custody/v1", guest, phase: "bound", operationNonce: deps.nonce(), requestSha256, context, provider: null });
|
|
128
|
+
const body = { ...manifest, metadata: { ...manifest.metadata, annotations: { ...(manifest.metadata.annotations || {}), [CUSTODY_ANNOTATION]: canonicalJson(record) } } };
|
|
129
|
+
const response = await deps.api.request(CLAIMS, "POST", body);
|
|
130
|
+
requireCustody(uuid.safeParse(response?.metadata?.uid).success, "claim creation response lacks actual UID");
|
|
131
|
+
const live = await deps.api.request(path, "GET");
|
|
132
|
+
const held = claimCustody(live);
|
|
133
|
+
requireCustody(held && canonicalJson(held) === canonicalJson(record), "created custody annotation not independently confirmed");
|
|
134
|
+
verifyCustodyClaim(live, held, response.metadata.uid);
|
|
135
|
+
requireCustody(!live.metadata.deletionTimestamp, "new claim is already deleting");
|
|
136
|
+
await noOtherClaim(deps, guest, live.metadata.uid);
|
|
137
|
+
return { claim: live, custody: { source: "annotation", record: held, creationOwnerSha256: creationOwnerSha(held) }, reused: false };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async function resume(live, manifest, guest, context, requestSha256, deps) {
|
|
141
|
+
const held = claimCustody(live);
|
|
142
|
+
if (held) {
|
|
143
|
+
requireCustody(["bound", "provider-bound"].includes(held.phase) && held.requestSha256 === requestSha256
|
|
144
|
+
&& canonicalJson(held.context) === canonicalJson(context) && canonicalJson(held.guest) === canonicalJson(guest), "existing claim custody is occupied by a different request");
|
|
145
|
+
verifyCustodyClaim(live, held);
|
|
146
|
+
requireResumable(live, manifest, deps);
|
|
147
|
+
await noOtherClaim(deps, guest, live.metadata.uid);
|
|
148
|
+
return { claim: live, custody: { source: "annotation", record: held, creationOwnerSha256: creationOwnerSha(held) }, reused: true };
|
|
149
|
+
}
|
|
150
|
+
const legacy = await readLegacyReservation(deps.api, guest);
|
|
151
|
+
if (!legacy)
|
|
152
|
+
throw new NoClaimCustodyError(manifest.metadata.name, guest);
|
|
153
|
+
const { record, lease } = legacy;
|
|
154
|
+
requireCustody(["bound", "provider-bound"].includes(record.phase) && record.requestSha256 === requestSha256
|
|
155
|
+
&& canonicalJson(record.context) === canonicalJson(context) && record.intendedClaim.name === manifest.metadata.name, "legacy reservation is occupied by a different request");
|
|
156
|
+
verifyLegacyClaim(live, record, record.claim.uid);
|
|
157
|
+
requireResumable(live, manifest, deps);
|
|
158
|
+
await noOtherClaim(deps, guest, live.metadata.uid);
|
|
159
|
+
return { claim: live, custody: { source: "legacy-lease", record, legacyLease: { name: lease.metadata.name, uid: lease.metadata.uid }, migration: "operator-pending" }, reused: true };
|
|
160
|
+
}
|
|
161
|
+
export async function deleteClaimWithCustody(name, expectedUid, deps = defaults()) {
|
|
162
|
+
objectName.parse(name);
|
|
163
|
+
uuid.parse(expectedUid);
|
|
164
|
+
const live = await deps.api.request(`${CLAIMS}/${name}`, "GET");
|
|
165
|
+
requireCustody(live?.metadata?.uid === expectedUid, "original selected claim UID differs or is absent");
|
|
166
|
+
const guest = claimGuest(live);
|
|
167
|
+
return serialized(guest, async () => {
|
|
168
|
+
const selected = await deps.api.request(`${CLAIMS}/${name}`, "GET");
|
|
169
|
+
requireCustody(selected?.metadata?.uid === expectedUid, "original selected claim UID differs or is absent");
|
|
170
|
+
let source;
|
|
171
|
+
let phase;
|
|
172
|
+
const held = claimCustody(selected);
|
|
173
|
+
if (held) {
|
|
174
|
+
requireCustody(selected.metadata.name === name && canonicalJson(held.guest) === canonicalJson(guest), "claim does not own its custody record");
|
|
175
|
+
verifyCustodyClaim(selected, held, expectedUid);
|
|
176
|
+
source = "annotation";
|
|
177
|
+
phase = held.phase;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const legacy = await readLegacyReservation(deps.api, guest);
|
|
181
|
+
if (!legacy)
|
|
182
|
+
throw new NoClaimCustodyError(name, guest);
|
|
183
|
+
requireCustody(["bound", "provider-bound", "retiring"].includes(legacy.record.phase) && legacy.record.claim?.uid === expectedUid && legacy.record.intendedClaim.name === name, "claim does not own the bound legacy reservation");
|
|
184
|
+
verifyLegacyClaim(selected, legacy.record, expectedUid);
|
|
185
|
+
source = "legacy-lease";
|
|
186
|
+
phase = legacy.record.phase;
|
|
187
|
+
}
|
|
188
|
+
await deps.api.request(`${CLAIMS}/${name}`, "DELETE", { apiVersion: "v1", kind: "DeleteOptions", preconditions: { uid: expectedUid, resourceVersion: selected.metadata.resourceVersion } });
|
|
189
|
+
return {
|
|
190
|
+
requested: true, expectedUid, custodySource: source, custodyPhase: phase, custodyRetained: true,
|
|
191
|
+
...(source === "legacy-lease" ? { migration: "operator-pending" } : {}),
|
|
192
|
+
next: "The governed operator/controller owns teardown: it proves original claim, provider and owned-volume absence before releasing custody."
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=claim-custody.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-custody.js","sourceRoot":"","sources":["../../../src/tools/claim-custody.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ClaimMethod = "GET" | "POST" | "DELETE";
|
|
2
|
+
export interface ClaimApi {
|
|
3
|
+
request(path: string, method: ClaimMethod, body?: unknown): Promise<any | null>;
|
|
4
|
+
}
|
|
5
|
+
export declare class ClaimApiError extends Error {
|
|
6
|
+
readonly status: number | null;
|
|
7
|
+
constructor(status: number | null, message: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function assertClaimPath(path: string, method: ClaimMethod): void;
|
|
10
|
+
/** Names the denied operation from the request itself. Remote error text is never echoed. */
|
|
11
|
+
export declare function describeClaimRequest(path: string, method: ClaimMethod): string;
|
|
12
|
+
export declare function claimFailureMessage(status: number | null, path: string, method: ClaimMethod): string;
|
|
13
|
+
export declare function createClaimApi(): ClaimApi;
|
|
14
|
+
//# sourceMappingURL=claim-kubernetes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-kubernetes.d.ts","sourceRoot":"","sources":["../../../src/tools/claim-kubernetes.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;AACpD,MAAM,WAAW,QAAQ;IAAG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;CAAE;AAC9G,qBAAa,aAAc,SAAQ,KAAK;aACV,MAAM,EAAE,MAAM,GAAG,IAAI;gBAArB,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM;CACnE;AAGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAcvE;AACD,6FAA6F;AAC7F,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAO9E;AACD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAIpG;AAOD,wBAAgB,cAAc,IAAI,QAAQ,CAgCzC"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/** Shared configured Kubernetes transport for
|
|
1
|
+
/** Shared configured Kubernetes transport for ApplianceClaim custody (REL-1746). Never retry an unknown write. Leases are read-only legacy evidence. */
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { fetch, Agent } from "undici";
|
|
4
4
|
import { KubernetesCommandError, resolveLabKubernetesBackend, runLabKubectl } from "./kubernetes-backend.js";
|
|
5
|
-
export class
|
|
5
|
+
export class ClaimApiError extends Error {
|
|
6
6
|
status;
|
|
7
7
|
constructor(status, message) {
|
|
8
8
|
super(message);
|
|
@@ -11,68 +11,72 @@ export class SlotApiError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
const MAX_BYTES = 1_048_576;
|
|
13
13
|
const COLLECTIONS = ["/apis/coordination.k8s.io/v1/namespaces/ct-lab/leases", "/apis/calltelemetry.com/v1alpha1/namespaces/ct-lab/applianceclaims"];
|
|
14
|
-
export function
|
|
14
|
+
export function assertClaimPath(path, method) {
|
|
15
15
|
const cleanPath = path.split("?")[0];
|
|
16
16
|
if (path !== cleanPath && !/^\?limit=500(?:&continue=[A-Za-z0-9%._~-]+)?$/.test(path.slice(cleanPath.length)))
|
|
17
|
-
throw new Error("
|
|
17
|
+
throw new Error("claim custody query refused");
|
|
18
18
|
const collection = COLLECTIONS.find(p => cleanPath === p || cleanPath.startsWith(p + "/"));
|
|
19
19
|
if (!collection)
|
|
20
|
-
throw new Error("
|
|
20
|
+
throw new Error("claim custody API path refused");
|
|
21
21
|
const name = cleanPath.slice(collection.length + 1);
|
|
22
22
|
if (cleanPath !== collection && (name.length > 253 || !name.split(".").every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))))
|
|
23
|
-
throw new Error("
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
throw new Error("claim custody object name refused");
|
|
24
|
+
if (collection === COLLECTIONS[0]) {
|
|
25
|
+
// Legacy reservation Leases are read-only evidence: the operator owns migration and deletion.
|
|
26
|
+
if (method !== "GET" || cleanPath === collection || !name.startsWith("ct-physical-slot-reservation."))
|
|
27
|
+
throw new Error("MCP never creates, renews or releases a Lease; only legacy reservation reads are allowed");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!["GET", "POST", "DELETE"].includes(method))
|
|
31
|
+
throw new Error("claim custody method refused");
|
|
32
|
+
if (method === "POST" && cleanPath !== collection || method !== "POST" && cleanPath === collection && method !== "GET")
|
|
33
|
+
throw new Error("claim custody method refused");
|
|
30
34
|
}
|
|
31
35
|
/** Names the denied operation from the request itself. Remote error text is never echoed. */
|
|
32
|
-
export function
|
|
36
|
+
export function describeClaimRequest(path, method) {
|
|
33
37
|
const cleanPath = path.split("?")[0];
|
|
34
38
|
const match = /^\/apis\/([^/]+)\/[^/]+\/namespaces\/([^/]+)\/([^/]+)(\/[^/]+)?$/.exec(cleanPath);
|
|
35
39
|
if (!match)
|
|
36
40
|
return "an unrecognised resource";
|
|
37
41
|
const [, group, namespace, resource, named] = match;
|
|
38
|
-
const verb = method === "GET" ? (named ? "get" : "list") : method === "POST" ? "create" :
|
|
42
|
+
const verb = method === "GET" ? (named ? "get" : "list") : method === "POST" ? "create" : "delete";
|
|
39
43
|
return `${verb} ${resource}.${group} in namespace ${namespace}`;
|
|
40
44
|
}
|
|
41
|
-
export function
|
|
42
|
-
const detail =
|
|
45
|
+
export function claimFailureMessage(status, path, method) {
|
|
46
|
+
const detail = describeClaimRequest(path, method);
|
|
43
47
|
const hint = status === 403 ? `; the service account needs RBAC to ${detail}` : "";
|
|
44
|
-
return `
|
|
48
|
+
return `claim custody Kubernetes request failed (${status ?? "unknown"}) on ${detail}${hint}; custody unchanged`;
|
|
45
49
|
}
|
|
46
50
|
function parse(text) {
|
|
47
51
|
if (Buffer.byteLength(text) > MAX_BYTES)
|
|
48
|
-
throw new Error("
|
|
52
|
+
throw new Error("claim custody response exceeds bound");
|
|
49
53
|
const value = JSON.parse(text);
|
|
50
54
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
51
|
-
throw new Error("
|
|
55
|
+
throw new Error("claim custody response is not an object");
|
|
52
56
|
return value;
|
|
53
57
|
}
|
|
54
|
-
export function
|
|
58
|
+
export function createClaimApi() {
|
|
55
59
|
return { async request(path, method, body) {
|
|
56
|
-
|
|
60
|
+
assertClaimPath(path, method);
|
|
57
61
|
const backend = resolveLabKubernetesBackend();
|
|
58
62
|
if (backend.mode === "in-cluster") {
|
|
59
63
|
const token = fs.readFileSync(backend.tokenPath, "utf8").trim();
|
|
60
64
|
if (!token)
|
|
61
|
-
throw new Error("
|
|
65
|
+
throw new Error("claim custody Kubernetes service identity refused");
|
|
62
66
|
const agent = new Agent({ connect: { ca: fs.readFileSync(backend.caPath), rejectUnauthorized: true } });
|
|
63
67
|
try {
|
|
64
68
|
const response = await fetch(`${backend.server}${path}`, { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), dispatcher: agent, signal: AbortSignal.timeout(10_000) });
|
|
65
69
|
if (response.status === 404 && method === "GET")
|
|
66
70
|
return null;
|
|
67
71
|
if (!response.ok)
|
|
68
|
-
throw new
|
|
72
|
+
throw new ClaimApiError(response.status, claimFailureMessage(response.status, path, method));
|
|
69
73
|
return parse(await response.text());
|
|
70
74
|
}
|
|
71
75
|
finally {
|
|
72
76
|
await agent.close();
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
|
-
const command = method === "GET" ? "get" : method === "POST" ? "create" :
|
|
79
|
+
const command = method === "GET" ? "get" : method === "POST" ? "create" : "delete";
|
|
76
80
|
try {
|
|
77
81
|
const result = await runLabKubectl([command, "--raw", path, ...(body === undefined ? [] : ["-f", "-"])], {
|
|
78
82
|
...(body === undefined ? {} : { input: JSON.stringify(body) }),
|
|
@@ -86,10 +90,10 @@ export function createSlotApi() {
|
|
|
86
90
|
if (method === "GET" && status === 404)
|
|
87
91
|
return null;
|
|
88
92
|
if (error instanceof KubernetesCommandError) {
|
|
89
|
-
throw new
|
|
93
|
+
throw new ClaimApiError(status, claimFailureMessage(status, path, method));
|
|
90
94
|
}
|
|
91
95
|
throw error;
|
|
92
96
|
}
|
|
93
97
|
} };
|
|
94
98
|
}
|
|
95
|
-
//# sourceMappingURL=
|
|
99
|
+
//# sourceMappingURL=claim-kubernetes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-kubernetes.js","sourceRoot":"","sources":["../../../src/tools/claim-kubernetes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim reconcile gate (UAT-1785).
|
|
3
|
+
*
|
|
4
|
+
* A claim is only a guest's protection if an ApplianceClaim operator is actually
|
|
5
|
+
* watching the cluster the claim was written to. When the claim plane and the
|
|
6
|
+
* operator's cluster differ (for example the operator moved to another cluster
|
|
7
|
+
* and the tooling still writes to the old one), a new claim is never reconciled:
|
|
8
|
+
* its status stays empty, and the operator that is running elsewhere sees the
|
|
9
|
+
* imported guest as unclaimed and destroys it once its grace window elapses,
|
|
10
|
+
* minutes into a UAT run while the claim and lease still look live.
|
|
11
|
+
*
|
|
12
|
+
* A profile=bare claim reaches status.phase=Ready within seconds of an operator
|
|
13
|
+
* seeing it, before any guest exists. Creating the guest only after that
|
|
14
|
+
* observation turns the silent mid-run loss into an immediate, actionable
|
|
15
|
+
* refusal that happens before a guest exists.
|
|
16
|
+
*/
|
|
17
|
+
import { type CallToolResult } from "../types/mcp.js";
|
|
18
|
+
export declare const CLAIM_RECONCILE_TIMEOUT_ENV = "CT_LAB_CLAIM_RECONCILE_TIMEOUT_MS";
|
|
19
|
+
export declare const CLAIM_RECONCILE_DEFAULT_TIMEOUT_MS = 120000;
|
|
20
|
+
export declare const CLAIM_RECONCILE_POLL_MS = 3000;
|
|
21
|
+
export interface ClaimReconcileDeps {
|
|
22
|
+
getClaim: (args: {
|
|
23
|
+
name: string;
|
|
24
|
+
namespace: string;
|
|
25
|
+
}) => Promise<CallToolResult>;
|
|
26
|
+
sleep: (ms: number) => Promise<void>;
|
|
27
|
+
now: () => number;
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
pollMs: number;
|
|
30
|
+
}
|
|
31
|
+
export type ClaimReconcileOutcome = {
|
|
32
|
+
reconciled: true;
|
|
33
|
+
phase: "Ready";
|
|
34
|
+
} | {
|
|
35
|
+
reconciled: false;
|
|
36
|
+
reason: "not_reconciled" | "failed" | "closed";
|
|
37
|
+
phase: string | null;
|
|
38
|
+
detail: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function claimReconcileTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
41
|
+
/**
|
|
42
|
+
* Poll the exact claim until an operator has set it Ready. A Failed or Closed
|
|
43
|
+
* phase is final; an empty phase is retried until the timeout and then reported
|
|
44
|
+
* as "not reconciled" (no operator saw the claim).
|
|
45
|
+
*/
|
|
46
|
+
export declare function awaitClaimReconciled(name: string, namespace: string, overrides?: Partial<ClaimReconcileDeps>): Promise<ClaimReconcileOutcome>;
|
|
47
|
+
/** Tool error for a claim no operator reconciled. The claim stays; nothing was imported. */
|
|
48
|
+
export declare function claimNotReconciledResult(outcome: Extract<ClaimReconcileOutcome, {
|
|
49
|
+
reconciled: false;
|
|
50
|
+
}>, context: {
|
|
51
|
+
operation: string;
|
|
52
|
+
claimName: string;
|
|
53
|
+
namespace: string;
|
|
54
|
+
timeoutMs: number;
|
|
55
|
+
}): CallToolResult;
|
|
56
|
+
//# sourceMappingURL=claim-reconcile-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-reconcile-gate.d.ts","sourceRoot":"","sources":["../../../src/tools/claim-reconcile-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG7E,eAAO,MAAM,2BAA2B,sCAAsC,CAAC;AAC/E,eAAO,MAAM,kCAAkC,SAAU,CAAC;AAC1D,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAG7C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACjF,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACpC;IAAE,UAAU,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhH,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAQpF;AAsBD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,GAAE,OAAO,CAAC,kBAAkB,CAAM,GAC1C,OAAO,CAAC,qBAAqB,CAAC,CAiChC;AAED,4FAA4F;AAC5F,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,CAAC,qBAAqB,EAAE;IAAE,UAAU,EAAE,KAAK,CAAA;CAAE,CAAC,EAC9D,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtF,cAAc,CAgBhB"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim reconcile gate (UAT-1785).
|
|
3
|
+
*
|
|
4
|
+
* A claim is only a guest's protection if an ApplianceClaim operator is actually
|
|
5
|
+
* watching the cluster the claim was written to. When the claim plane and the
|
|
6
|
+
* operator's cluster differ (for example the operator moved to another cluster
|
|
7
|
+
* and the tooling still writes to the old one), a new claim is never reconciled:
|
|
8
|
+
* its status stays empty, and the operator that is running elsewhere sees the
|
|
9
|
+
* imported guest as unclaimed and destroys it once its grace window elapses,
|
|
10
|
+
* minutes into a UAT run while the claim and lease still look live.
|
|
11
|
+
*
|
|
12
|
+
* A profile=bare claim reaches status.phase=Ready within seconds of an operator
|
|
13
|
+
* seeing it, before any guest exists. Creating the guest only after that
|
|
14
|
+
* observation turns the silent mid-run loss into an immediate, actionable
|
|
15
|
+
* refusal that happens before a guest exists.
|
|
16
|
+
*/
|
|
17
|
+
import { createToolErrorResult } from "../types/mcp.js";
|
|
18
|
+
import { handleGetApplianceClaim } from "./appliance-claim-tools.js";
|
|
19
|
+
export const CLAIM_RECONCILE_TIMEOUT_ENV = "CT_LAB_CLAIM_RECONCILE_TIMEOUT_MS";
|
|
20
|
+
export const CLAIM_RECONCILE_DEFAULT_TIMEOUT_MS = 120_000;
|
|
21
|
+
export const CLAIM_RECONCILE_POLL_MS = 3_000;
|
|
22
|
+
const CLAIM_RECONCILE_MAX_TIMEOUT_MS = 900_000;
|
|
23
|
+
export function claimReconcileTimeoutMs(env = process.env) {
|
|
24
|
+
const raw = env[CLAIM_RECONCILE_TIMEOUT_ENV]?.trim();
|
|
25
|
+
if (!raw)
|
|
26
|
+
return CLAIM_RECONCILE_DEFAULT_TIMEOUT_MS;
|
|
27
|
+
const parsed = Number(raw);
|
|
28
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > CLAIM_RECONCILE_MAX_TIMEOUT_MS) {
|
|
29
|
+
return CLAIM_RECONCILE_DEFAULT_TIMEOUT_MS;
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
function defaultDeps() {
|
|
34
|
+
return {
|
|
35
|
+
getClaim: (args) => handleGetApplianceClaim(args),
|
|
36
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
37
|
+
now: () => Date.now(),
|
|
38
|
+
timeoutMs: claimReconcileTimeoutMs(),
|
|
39
|
+
pollMs: CLAIM_RECONCILE_POLL_MS,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function claimOf(result) {
|
|
43
|
+
const block = result.content?.[0];
|
|
44
|
+
if (!block || block.type !== "text")
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(block.text)?.claim ?? null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Poll the exact claim until an operator has set it Ready. A Failed or Closed
|
|
55
|
+
* phase is final; an empty phase is retried until the timeout and then reported
|
|
56
|
+
* as "not reconciled" (no operator saw the claim).
|
|
57
|
+
*/
|
|
58
|
+
export async function awaitClaimReconciled(name, namespace, overrides = {}) {
|
|
59
|
+
const deps = { ...defaultDeps(), ...overrides };
|
|
60
|
+
const deadline = deps.now() + deps.timeoutMs;
|
|
61
|
+
let phase = null;
|
|
62
|
+
let detail = "the claim has no status; no operator has reconciled it";
|
|
63
|
+
for (;;) {
|
|
64
|
+
let result = null;
|
|
65
|
+
try {
|
|
66
|
+
result = await deps.getClaim({ name, namespace });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
detail = `claim read failed: ${String(error?.message ?? error).slice(0, 160)}`;
|
|
70
|
+
}
|
|
71
|
+
if (result && !result.isError) {
|
|
72
|
+
const claim = claimOf(result);
|
|
73
|
+
const live = typeof claim?.status?.phase === "string" && claim.status.phase ? claim.status.phase : null;
|
|
74
|
+
phase = live;
|
|
75
|
+
if (live === "Ready")
|
|
76
|
+
return { reconciled: true, phase: "Ready" };
|
|
77
|
+
if (live === "Failed" || live === "Closed") {
|
|
78
|
+
return {
|
|
79
|
+
reconciled: false,
|
|
80
|
+
reason: live === "Failed" ? "failed" : "closed",
|
|
81
|
+
phase: live,
|
|
82
|
+
detail: String(claim?.status?.message ?? `claim phase is ${live}`).slice(0, 240),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
detail = live ? `claim phase is '${live}', not Ready` : "the claim has no status; no operator has reconciled it";
|
|
86
|
+
}
|
|
87
|
+
else if (result) {
|
|
88
|
+
detail = "claim read returned an error";
|
|
89
|
+
}
|
|
90
|
+
if (deps.now() + deps.pollMs > deadline)
|
|
91
|
+
break;
|
|
92
|
+
await deps.sleep(deps.pollMs);
|
|
93
|
+
}
|
|
94
|
+
return { reconciled: false, reason: "not_reconciled", phase, detail };
|
|
95
|
+
}
|
|
96
|
+
/** Tool error for a claim no operator reconciled. The claim stays; nothing was imported. */
|
|
97
|
+
export function claimNotReconciledResult(outcome, context) {
|
|
98
|
+
const cause = outcome.reason === "not_reconciled"
|
|
99
|
+
? "No ApplianceClaim operator reconciled it. The cluster this tool writes claims to (LAB_CLAIM_K8S_CONTEXT / LAB_CLAIM_K8S_SERVER) is not the cluster the operator watches, or the operator is not running there. An operator on another cluster would destroy an imported guest as unclaimed."
|
|
100
|
+
: `The operator set the claim to ${outcome.phase}.`;
|
|
101
|
+
return createToolErrorResult("CLAIM_NOT_RECONCILED", `${context.operation}: claim '${context.claimName}' did not reach Ready within ${Math.round(context.timeoutMs / 1000)}s (${outcome.detail}). ${cause} The image was NOT imported and no guest exists. The finite claim is retained; check the claim plane binding and the operator, then retry.`, {
|
|
102
|
+
operation: context.operation,
|
|
103
|
+
claimName: context.claimName,
|
|
104
|
+
namespace: context.namespace,
|
|
105
|
+
phase: outcome.phase,
|
|
106
|
+
guestCreated: false,
|
|
107
|
+
claimProtection: { disposition: "retained", guestAbsentVerified: true },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=claim-reconcile-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-reconcile-gate.js","sourceRoot":"","sources":["../../../src/tools/claim-reconcile-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { z } from "zod";
|
|
27
27
|
import { CallToolResult } from "../types/mcp.js";
|
|
28
28
|
import { ToolContext } from "./list-hosts.js";
|
|
29
|
+
import { type ClaimReconcileOutcome } from "./claim-reconcile-gate.js";
|
|
29
30
|
export declare const ClaimedImportQcowInputObjectSchema: z.ZodObject<{
|
|
30
31
|
readonly assetId: z.ZodString;
|
|
31
32
|
readonly hostAssetId: z.ZodString;
|
|
@@ -120,6 +121,8 @@ export interface ClaimedImportDeps {
|
|
|
120
121
|
createClaim: (args: unknown) => Promise<CallToolResult>;
|
|
121
122
|
deleteClaim: (args: unknown) => Promise<CallToolResult>;
|
|
122
123
|
importQcow: (args: unknown, context: ToolContext) => Promise<CallToolResult>;
|
|
124
|
+
/** Observes the created claim reaching Ready (an operator is watching this plane) before any guest exists. */
|
|
125
|
+
awaitClaimReconciled?: (name: string, namespace: string) => Promise<ClaimReconcileOutcome>;
|
|
123
126
|
}
|
|
124
127
|
export declare function handleClaimedImportQcow(rawArgs: ClaimedImportQcowRawInput | undefined, context: ToolContext, deps?: ClaimedImportDeps): Promise<CallToolResult>;
|
|
125
128
|
export declare const CLAIMED_IMPORT_QCOW_TOOL_DESCRIPTION = "Import a published or private-candidate appliance QCOW2 as a Proxmox lab guest AND claim it, in one sanctioned step. The strict appliance operator destroys every guest no ApplianceClaim names within ~5 minutes, so this tool creates a profile=bare ApplianceClaim (no seed, no license, no bootstrap; names node + VMID + VM name; finite TTL) FIRST, then runs the canonical lab-inventory vm provision --source published-qcow apply (digest-bound, leased, journaled, receipted; the guest then awaits vm_repair, finalize and uat_access), and retains finite claim protection if the import fails, throws or has an uncertain outcome. Reconcile the exact canonical operation before explicit governed teardown. Dry-run is the default: it runs the canonical import plan and previews the claim. Requires vmid, vmPrefix and node (minis|proxmox|ibm64, matching hostAssetId). Renew with renew_appliance_claim; release with delete_appliance_claim. The guest disk is thin-only: thick or undeterminable storage is refused with THICK_STORAGE_REFUSED before the claim is created.";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claimed-import-qcow.d.ts","sourceRoot":"","sources":["../../../src/tools/claimed-import-qcow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAA2C,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"claimed-import-qcow.d.ts","sourceRoot":"","sources":["../../../src/tools/claimed-import-qcow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAA2C,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,OAAO,EAA2E,KAAK,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AA+BhJ,eAAO,MAAM,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuBpC,CAAC;AAEZ,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAE3F,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7E,8GAA8G;IAC9G,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC5F;AAqBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,yBAAyB,GAAG,SAAS,EAC9C,OAAO,EAAE,WAAW,EACpB,IAAI,GAAE,iBAA+B,GACpC,OAAO,CAAC,cAAc,CAAC,CAqLzB;AAED,eAAO,MAAM,oCAAoC,siCACo/B,CAAC"}
|
|
@@ -27,6 +27,7 @@ import { z } from "zod";
|
|
|
27
27
|
import { ProxmoxErrorCode, createToolErrorResult } from "../types/mcp.js";
|
|
28
28
|
import { canonicalOperationAvailable } from "./canonical-guard.js";
|
|
29
29
|
import { guardCanonicalThinStorage } from "./thin-storage-policy.js";
|
|
30
|
+
import { awaitClaimReconciled, claimNotReconciledResult, claimReconcileTimeoutMs } from "./claim-reconcile-gate.js";
|
|
30
31
|
import { reservedIdentityError } from "./sha-channel.js";
|
|
31
32
|
import { ImportQcowInputObjectSchema, ImportQcowInputSchema, handleProxmoxImportQcow, importQcowVersionFromUrl, } from "./import-qcow.js";
|
|
32
33
|
import { buildApplianceClaimManifest, handleCreateApplianceClaim, handleDeleteApplianceClaim, normalizeClaimNode, CreateApplianceClaimInputSchema, } from "./appliance-claim-tools.js";
|
|
@@ -65,6 +66,7 @@ const defaultDeps = {
|
|
|
65
66
|
createClaim: (args) => handleCreateApplianceClaim(args),
|
|
66
67
|
deleteClaim: (args) => handleDeleteApplianceClaim(args),
|
|
67
68
|
importQcow: (args, context) => handleProxmoxImportQcow(args, context),
|
|
69
|
+
awaitClaimReconciled: (name, namespace) => awaitClaimReconciled(name, namespace),
|
|
68
70
|
};
|
|
69
71
|
function textOf(result) {
|
|
70
72
|
const block = result.content?.[0];
|
|
@@ -165,6 +167,21 @@ export async function handleClaimedImportQcow(rawArgs, context, deps = defaultDe
|
|
|
165
167
|
if (created.isError) {
|
|
166
168
|
return createToolErrorResult(ProxmoxErrorCode.UPSTREAM_API_ERROR, `claim creation failed; the image was NOT imported: ${textOf(created)}`, { operation: "claimed_import_qcow", claimName: built.claimName });
|
|
167
169
|
}
|
|
170
|
+
// UAT-1785: the claim only protects the guest if an operator watches the plane
|
|
171
|
+
// it was written to. Require the operator to reconcile (Ready) it BEFORE the
|
|
172
|
+
// guest exists; otherwise refuse here instead of losing the guest mid-run.
|
|
173
|
+
const claimNamespace = built.manifest.metadata.namespace ?? "ct-lab";
|
|
174
|
+
if (deps.awaitClaimReconciled) {
|
|
175
|
+
const reconciled = await deps.awaitClaimReconciled(built.claimName, claimNamespace);
|
|
176
|
+
if (!reconciled.reconciled) {
|
|
177
|
+
return claimNotReconciledResult(reconciled, {
|
|
178
|
+
operation: "claimed_import_qcow",
|
|
179
|
+
claimName: built.claimName,
|
|
180
|
+
namespace: claimNamespace,
|
|
181
|
+
timeoutMs: claimReconcileTimeoutMs(),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
168
185
|
let imported;
|
|
169
186
|
try {
|
|
170
187
|
imported = await deps.importQcow(importArgs, context);
|