@calltelemetry/ct-lab-mcp 0.8.69 → 0.8.71

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 (86) hide show
  1. package/README.md +8 -1
  2. package/dist/src/appliance/executor.d.ts.map +1 -1
  3. package/dist/src/appliance/executor.js +3 -172
  4. package/dist/src/appliance/executor.js.map +1 -1
  5. package/dist/src/hypervisors/esxi/client.d.ts.map +1 -1
  6. package/dist/src/hypervisors/esxi/client.js +5 -0
  7. package/dist/src/hypervisors/esxi/client.js.map +1 -1
  8. package/dist/src/proxmox/client.d.ts.map +1 -1
  9. package/dist/src/proxmox/client.js +17 -0
  10. package/dist/src/proxmox/client.js.map +1 -1
  11. package/dist/src/proxmox/mock.d.ts.map +1 -1
  12. package/dist/src/proxmox/mock.js +28 -0
  13. package/dist/src/proxmox/mock.js.map +1 -1
  14. package/dist/src/server.d.ts.map +1 -1
  15. package/dist/src/server.js +1 -0
  16. package/dist/src/server.js.map +1 -1
  17. package/dist/src/slot/purge-controller.d.ts +1 -0
  18. package/dist/src/slot/purge-controller.d.ts.map +1 -1
  19. package/dist/src/slot/purge-controller.js +4 -33
  20. package/dist/src/slot/purge-controller.js.map +1 -1
  21. package/dist/src/tools/appliance-claim-tools.d.ts +11 -7
  22. package/dist/src/tools/appliance-claim-tools.d.ts.map +1 -1
  23. package/dist/src/tools/appliance-claim-tools.js +28 -81
  24. package/dist/src/tools/appliance-claim-tools.js.map +1 -1
  25. package/dist/src/tools/appliance-exec.d.ts +4 -4
  26. package/dist/src/tools/claim-creation-projection.d.ts +7 -0
  27. package/dist/src/tools/claim-creation-projection.d.ts.map +1 -0
  28. package/dist/src/tools/claim-creation-projection.js +112 -0
  29. package/dist/src/tools/claim-creation-projection.js.map +1 -0
  30. package/dist/src/tools/claim-first.d.ts.map +1 -1
  31. package/dist/src/tools/claim-first.js +19 -15
  32. package/dist/src/tools/claim-first.js.map +1 -1
  33. package/dist/src/tools/claimed-import-qcow.d.ts +3 -3
  34. package/dist/src/tools/claimed-import-qcow.d.ts.map +1 -1
  35. package/dist/src/tools/claimed-import-qcow.js +11 -1
  36. package/dist/src/tools/claimed-import-qcow.js.map +1 -1
  37. package/dist/src/tools/cube-provision.d.ts +11 -0
  38. package/dist/src/tools/cube-provision.d.ts.map +1 -1
  39. package/dist/src/tools/cube-provision.js +12 -3
  40. package/dist/src/tools/cube-provision.js.map +1 -1
  41. package/dist/src/tools/host-migrate-vm.d.ts.map +1 -1
  42. package/dist/src/tools/host-migrate-vm.js +10 -1
  43. package/dist/src/tools/host-migrate-vm.js.map +1 -1
  44. package/dist/src/tools/import-qcow.d.ts +7 -7
  45. package/dist/src/tools/import-qcow.d.ts.map +1 -1
  46. package/dist/src/tools/import-qcow.js +13 -3
  47. package/dist/src/tools/import-qcow.js.map +1 -1
  48. package/dist/src/tools/index.d.ts.map +1 -1
  49. package/dist/src/tools/index.js +9 -5
  50. package/dist/src/tools/index.js.map +1 -1
  51. package/dist/src/tools/legacy-guest-guard.d.ts +4 -0
  52. package/dist/src/tools/legacy-guest-guard.d.ts.map +1 -0
  53. package/dist/src/tools/legacy-guest-guard.js +26 -0
  54. package/dist/src/tools/legacy-guest-guard.js.map +1 -0
  55. package/dist/src/tools/license-tools.d.ts +2 -2
  56. package/dist/src/tools/physical-slot-claim-spec-schema.v1.json +1 -0
  57. package/dist/src/tools/physical-slot.d.ts +205 -0
  58. package/dist/src/tools/physical-slot.d.ts.map +1 -0
  59. package/dist/src/tools/physical-slot.js +295 -0
  60. package/dist/src/tools/physical-slot.js.map +1 -0
  61. package/dist/src/tools/provision-tools.d.ts +16 -16
  62. package/dist/src/tools/provision-tools.d.ts.map +1 -1
  63. package/dist/src/tools/provision-tools.js +36 -2
  64. package/dist/src/tools/provision-tools.js.map +1 -1
  65. package/dist/src/tools/quick-provision.d.ts +2 -0
  66. package/dist/src/tools/quick-provision.d.ts.map +1 -1
  67. package/dist/src/tools/quick-provision.js +15 -1
  68. package/dist/src/tools/quick-provision.js.map +1 -1
  69. package/dist/src/tools/sandbox-tools.d.ts +2 -2
  70. package/dist/src/tools/sandbox-tools.d.ts.map +1 -1
  71. package/dist/src/tools/sandbox-tools.js +11 -1
  72. package/dist/src/tools/sandbox-tools.js.map +1 -1
  73. package/dist/src/tools/slot-kubernetes.d.ts +13 -0
  74. package/dist/src/tools/slot-kubernetes.d.ts.map +1 -0
  75. package/dist/src/tools/slot-kubernetes.js +108 -0
  76. package/dist/src/tools/slot-kubernetes.js.map +1 -0
  77. package/dist/src/tools/storage-tools.d.ts +19 -0
  78. package/dist/src/tools/storage-tools.d.ts.map +1 -1
  79. package/dist/src/tools/storage-tools.js +15 -0
  80. package/dist/src/tools/storage-tools.js.map +1 -1
  81. package/dist/src/tools/thin-storage-policy.d.ts +99 -0
  82. package/dist/src/tools/thin-storage-policy.d.ts.map +1 -0
  83. package/dist/src/tools/thin-storage-policy.js +236 -0
  84. package/dist/src/tools/thin-storage-policy.js.map +1 -0
  85. package/dist/src/tools/vm-action.d.ts +8 -8
  86. package/package.json +2 -2
@@ -0,0 +1,26 @@
1
+ /** Legacy SDK effects have no canonical physical custody; flags cannot grant it. */
2
+ export const LEGACY_GUEST_AUTHORITY_ERROR = "CANONICAL_PHYSICAL_SLOT_AUTHORITY_REQUIRED";
3
+ const guestHandlers = new Set([
4
+ "appliance_exec", "factory_reset_appliance", "mount_iso", "unmount_iso", "create_snapshot",
5
+ "rollback_snapshot", "update_appliance_cli", "get_appliance_diagnostics", "manage_vm_network",
6
+ "get_guest_agent_status", "claim_cube", "release_cube", "provision_cube", "setup_cucm_emulator",
7
+ "clean_storage", "proxmox_quick_provision", "pin_pool_slots",
8
+ ]);
9
+ const bridgedEffects = new Set([
10
+ "provision_vm", "auto_provision_vm", "claim_slot", "release_slot", "purge_slots", "purge_and_redeploy",
11
+ "cleanup_expired_vms", "prune_untracked_vms",
12
+ ]);
13
+ export function legacyGuestHandlerBlocked(name, args, canonical) {
14
+ if (guestHandlers.has(name))
15
+ return true; // No qualified typed bridge for these legacy handlers.
16
+ if (canonical)
17
+ return false; // Existing typed CLI routes independently own admission.
18
+ if (bridgedEffects.has(name))
19
+ return true;
20
+ if (name === "vm_action")
21
+ return args?.action !== "status";
22
+ if (name === "find_slot")
23
+ return args?.autoClaim === true || args?.purgeIfFull === true;
24
+ return false;
25
+ }
26
+ //# sourceMappingURL=legacy-guest-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacy-guest-guard.js","sourceRoot":"","sources":["../../../src/tools/legacy-guest-guard.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,CAAC,MAAM,4BAA4B,GAAG,4CAA4C,CAAC;AACzF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,gBAAgB,EAAE,yBAAyB,EAAE,WAAW,EAAE,aAAa,EAAE,iBAAiB;IAC1F,mBAAmB,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,mBAAmB;IAC7F,wBAAwB,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,qBAAqB;IAC/F,eAAe,EAAE,yBAAyB,EAAE,gBAAgB;CAC7D,CAAC,CAAC;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,cAAc,EAAE,mBAAmB,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB;IACtG,qBAAqB,EAAE,qBAAqB;CAC7C,CAAC,CAAC;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY,EAAE,IAAS,EAAE,SAAkB;IACnF,IAAI,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,uDAAuD;IACjG,IAAI,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,yDAAyD;IACtF,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,IAAI,EAAE,MAAM,KAAK,QAAQ,CAAC;IAC3D,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,IAAI,EAAE,SAAS,KAAK,IAAI,IAAI,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IACxF,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -62,10 +62,10 @@ export declare const ApplyLicenseInputSchema: z.ZodObject<{
62
62
  tier: "Demo" | "Community" | "Essentials" | "Advanced" | "Premium" | "Beta";
63
63
  port: number;
64
64
  targetPath: string;
65
+ mode: "auto" | "legacy" | "sideload";
65
66
  timeoutMs: number;
66
67
  orgId: number;
67
68
  days: number;
68
- mode: "auto" | "legacy" | "sideload";
69
69
  token?: string | undefined;
70
70
  email?: string | undefined;
71
71
  host?: string | undefined;
@@ -90,12 +90,12 @@ export declare const ApplyLicenseInputSchema: z.ZodObject<{
90
90
  port?: number | undefined;
91
91
  purpose?: string | undefined;
92
92
  targetPath?: string | undefined;
93
+ mode?: "auto" | "legacy" | "sideload" | undefined;
93
94
  timeoutMs?: number | undefined;
94
95
  orgId?: number | undefined;
95
96
  days?: number | undefined;
96
97
  dashboardUrl?: string | undefined;
97
98
  dashboardApiKey?: string | undefined;
98
- mode?: "auto" | "legacy" | "sideload" | undefined;
99
99
  applianceId?: string | undefined;
100
100
  }>;
101
101
  export type ApplyLicenseInput = z.infer<typeof ApplyLicenseInputSchema>;
@@ -0,0 +1 @@
1
+ {"properties":{"adminEmail":{"default":"demo@calltelemetry.com","type":"string"},"adminPassword":{"type":"string"},"adminPasswordSecretRef":{"properties":{"key":{"default":"password","type":"string"},"name":{"type":"string"}},"type":"object"},"applianceId":{"type":"string"},"claimType":{"default":"dynamic","enum":["dynamic","fixture"],"type":"string"},"closed":{"type":"boolean"},"containerOverrides":{"properties":{"jtapi":{"type":"string"},"media":{"type":"string"},"syslog":{"type":"string"},"vue":{"type":"string"},"web":{"type":"string"}},"type":"object"},"cubes":{"items":{"properties":{"dialpeers":{"items":{"properties":{"codec":{"default":"g711ulaw","enum":["g711ulaw","g711alaw","g729","g729r8"],"type":"string"},"destinationPattern":{"type":"string"},"dtmfRelay":{"default":"rtp-nte","enum":["rtp-nte","cisco-rtp","sip-notify","sip-kpml"],"type":"string"},"incomingCalledNumber":{"type":"string"},"preference":{"default":0,"maximum":10,"minimum":0,"type":"integer"},"sessionTarget":{"type":"string"},"tag":{"maximum":2147483647,"minimum":1,"type":"integer"},"type":{"default":"voip","enum":["voip","pots"],"type":"string"}},"required":["tag"],"type":"object"},"type":"array"},"gatewayIp":{"pattern":"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$","type":"string"},"ip":{"pattern":"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$","type":"string"},"mode":{"default":"dynamic_vm","enum":["dynamic_vm","dynamic_mock","claim_fixture","in_guest_emulator"],"type":"string"},"name":{"type":"string"},"node":{"default":"proxmox","type":"string"},"role":{"enum":["cucm-edge","att-pstn","internal","external","standalone"],"type":"string"},"templateVmid":{"default":200,"type":"integer"},"vlan":{"default":125,"maximum":4094,"minimum":1,"type":"integer"},"vmid":{"maximum":299,"minimum":200,"type":"integer"},"xcc":{"properties":{"applianceUrl":{"type":"string"},"enabled":{"default":true,"type":"boolean"},"heartbeatIntervalSeconds":{"default":15,"maximum":300,"minimum":1,"type":"integer"},"remoteUrl":{"type":"string"}},"type":"object"}},"required":["name","role","mode","ip"],"type":"object"},"type":"array"},"cucmEmulator":{"properties":{"axlVersion":{"default":"14.0","enum":["11.5","12.0","12.5","14.0","15.0"],"type":"string"},"cdrPublisher":{"properties":{"enabled":{"default":true,"type":"boolean"},"intervalSeconds":{"default":60,"maximum":3600,"minimum":5,"type":"integer"},"sftpDestination":{"properties":{"host":{"type":"string"},"passwordSecretRef":{"properties":{"key":{"default":"password","type":"string"},"name":{"type":"string"}},"type":"object"},"path":{"default":"/var/spool/calltelemetry/cdr","type":"string"},"port":{"default":2222,"maximum":65535,"minimum":1,"type":"integer"},"username":{"default":"calltelemetry","type":"string"}},"type":"object"}},"type":"object"},"enabled":{"default":false,"type":"boolean"},"mockPhonesCount":{"default":10,"maximum":1000,"minimum":0,"type":"integer"},"mode":{"default":"dynamic_pod","enum":["dynamic_pod","in_guest","external"],"type":"string"},"profile":{"default":"standard-lab","type":"string"},"scenario":{"type":"string"}},"type":"object"},"enableCallManagers":{"default":true,"type":"boolean"},"enableCubes":{"default":true,"type":"boolean"},"licenseDays":{"default":90,"maximum":3650,"minimum":1,"type":"integer"},"licenseTier":{"default":"Demo","enum":["Demo","Community","Standard","Enterprise"],"type":"string"},"loadTesting":{"properties":{"calledNumberPattern":{"default":"+1555200XXXX","type":"string"},"callerIdPattern":{"default":"+1555100XXXX","type":"string"},"chaosErrorRate":{"default":0,"maximum":100,"minimum":0,"type":"number"},"codec":{"default":"g711ulaw","enum":["g711ulaw","g711alaw","g729"],"type":"string"},"concurrency":{"default":5,"maximum":500,"minimum":1,"type":"integer"},"cps":{"default":1,"maximum":500,"minimum":0.1,"type":"number"},"durationSeconds":{"default":60,"maximum":86400,"minimum":1,"type":"integer"},"enabled":{"default":false,"type":"boolean"},"target":{"default":"external_cube","enum":["external_cube","internal_cube","appliance"],"type":"string"}},"type":"object"},"mockPstn":{"properties":{"autoAnswer":{"default":true,"type":"boolean"},"callRateCps":{"default":2,"maximum":500,"minimum":0.1,"type":"number"},"destinationPrefix":{"default":"1...","type":"string"},"enabled":{"default":false,"type":"boolean"},"ip":{"pattern":"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$","type":"string"},"sipPort":{"default":5060,"maximum":65535,"minimum":1,"type":"integer"},"vlan":{"default":123,"maximum":4094,"minimum":1,"type":"integer"}},"type":"object"},"node":{"default":"minis","type":"string"},"orgName":{"default":"CallTelemetry Demo","type":"string"},"paused":{"default":false,"type":"boolean"},"pausedReason":{"maxLength":500,"type":"string"},"phoneEmulator":{"properties":{"autoAnswer":{"default":true,"type":"boolean"},"bibAudioStreaming":{"default":true,"type":"boolean"},"bibStreaming":{"default":true,"type":"boolean"},"controlPlanePort":{"default":8080,"maximum":65535,"minimum":1024,"type":"integer"},"count":{"default":2,"maximum":500,"minimum":1,"type":"integer"},"cucmHost":{"type":"string"},"devicePrefix":{"default":"SEP","type":"string"},"enabled":{"default":false,"type":"boolean"},"mode":{"default":"dynamic_pod","enum":["dynamic_pod","in_guest"],"type":"string"},"model":{"default":"CP-7965G","type":"string"},"phoneCount":{"maximum":500,"minimum":1,"type":"integer"},"phoneWebXmlAccess":{"default":true,"type":"boolean"},"protocol":{"default":"SCCP","enum":["SCCP","SIP","sccp","sip"],"type":"string"},"webXmlAccess":{"default":true,"type":"boolean"}},"type":"object"},"preset":{"default":"comprehensive_demo","enum":["comprehensive_demo","standard_demo","minimal_demo","empty"],"type":"string"},"profile":{"default":"demo","enum":["demo","bare"],"type":"string"},"role":{"enum":["runner","gate","cucm","gateway","infra","harness"],"type":"string"},"sftp":{"properties":{"enabled":{"default":false,"type":"boolean"},"mode":{"default":"appliance_builtin","enum":["appliance_builtin","dynamic_pod"],"type":"string"},"passwordSecretRef":{"properties":{"key":{"default":"password","type":"string"},"name":{"type":"string"}},"type":"object"},"port":{"default":2222,"maximum":65535,"minimum":1,"type":"integer"},"stagingPath":{"default":"/opt/calltelemetry/cdr_staging","type":"string"},"user":{"default":"calltelemetry","type":"string"}},"type":"object"},"sideloadLicense":{"type":"string"},"sshUser":{"default":"calltelemetry","type":"string"},"state":{"enum":["Active","Closed"],"type":"string"},"targetIp":{"pattern":"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$","type":"string"},"targetPort":{"default":2222,"maximum":65535,"minimum":1,"type":"integer"},"train":{"default":"stable","enum":["stable","beta","rc","candidate","dev","edge"],"type":"string"},"ttlHours":{"default":24,"maximum":8760,"minimum":0,"type":"integer"},"version":{"default":"0.8.6.27","type":"string"},"vmName":{"type":"string"},"vmid":{"maximum":8999,"minimum":100,"type":"integer"}},"type":"object"}
@@ -0,0 +1,205 @@
1
+ import { z } from "zod";
2
+ import { type SlotApi } from "./slot-kubernetes.js";
3
+ export declare const SLOT_ANNOTATION = "calltelemetry.com/physical-slot";
4
+ export declare const SLOT_NONCE = "calltelemetry.com/physical-slot-operation-nonce";
5
+ export declare const SLOT_REQUEST = "calltelemetry.com/physical-slot-request-sha256";
6
+ declare const SlotSchema: z.ZodObject<{
7
+ hostAssetId: z.ZodString;
8
+ node: z.ZodString;
9
+ vmid: z.ZodNumber;
10
+ }, "strict", z.ZodTypeAny, {
11
+ node: string;
12
+ vmid: number;
13
+ hostAssetId: string;
14
+ }, {
15
+ node: string;
16
+ vmid: number;
17
+ hostAssetId: string;
18
+ }>;
19
+ export type PhysicalSlot = z.infer<typeof SlotSchema>;
20
+ export declare const ReservationSchema: z.ZodObject<{
21
+ schema: z.ZodLiteral<"physical-slot-reservation/v1">;
22
+ slot: z.ZodObject<{
23
+ hostAssetId: z.ZodString;
24
+ node: z.ZodString;
25
+ vmid: z.ZodNumber;
26
+ }, "strict", z.ZodTypeAny, {
27
+ node: string;
28
+ vmid: number;
29
+ hostAssetId: string;
30
+ }, {
31
+ node: string;
32
+ vmid: number;
33
+ hostAssetId: string;
34
+ }>;
35
+ phase: z.ZodEnum<["provisional", "bound", "provider-bound", "retiring", "released"]>;
36
+ operationNonce: z.ZodEffects<z.ZodString, string, string>;
37
+ requestSha256: z.ZodString;
38
+ intendedClaim: z.ZodObject<{
39
+ namespace: z.ZodLiteral<"ct-lab">;
40
+ name: z.ZodEffects<z.ZodString, string, string>;
41
+ }, "strict", z.ZodTypeAny, {
42
+ name: string;
43
+ namespace: "ct-lab";
44
+ }, {
45
+ name: string;
46
+ namespace: "ct-lab";
47
+ }>;
48
+ context: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
49
+ kind: z.ZodLiteral<"mcp">;
50
+ issueKey: z.ZodOptional<z.ZodString>;
51
+ }, "strict", z.ZodTypeAny, {
52
+ kind: "mcp";
53
+ issueKey?: string | undefined;
54
+ }, {
55
+ kind: "mcp";
56
+ issueKey?: string | undefined;
57
+ }>, z.ZodObject<{
58
+ kind: z.ZodLiteral<"operator">;
59
+ issueKey: z.ZodOptional<z.ZodString>;
60
+ }, "strict", z.ZodTypeAny, {
61
+ kind: "operator";
62
+ issueKey?: string | undefined;
63
+ }, {
64
+ kind: "operator";
65
+ issueKey?: string | undefined;
66
+ }>, z.ZodObject<{
67
+ kind: z.ZodLiteral<"ct-meta">;
68
+ logicalAssetId: z.ZodString;
69
+ issueKey: z.ZodString;
70
+ importOperationId: z.ZodString;
71
+ }, "strict", z.ZodTypeAny, {
72
+ kind: "ct-meta";
73
+ issueKey: string;
74
+ logicalAssetId: string;
75
+ importOperationId: string;
76
+ }, {
77
+ kind: "ct-meta";
78
+ issueKey: string;
79
+ logicalAssetId: string;
80
+ importOperationId: string;
81
+ }>]>;
82
+ claim: z.ZodNullable<z.ZodObject<{
83
+ namespace: z.ZodLiteral<"ct-lab">;
84
+ name: z.ZodEffects<z.ZodString, string, string>;
85
+ uid: z.ZodEffects<z.ZodString, string, string>;
86
+ }, "strict", z.ZodTypeAny, {
87
+ name: string;
88
+ namespace: "ct-lab";
89
+ uid: string;
90
+ }, {
91
+ name: string;
92
+ namespace: "ct-lab";
93
+ uid: string;
94
+ }>>;
95
+ provider: z.ZodNullable<z.ZodObject<{
96
+ uuid: z.ZodEffects<z.ZodString, string, string>;
97
+ vmName: z.ZodString;
98
+ managedOwnershipSha256: z.ZodString;
99
+ volumes: z.ZodArray<z.ZodString, "many">;
100
+ }, "strict", z.ZodTypeAny, {
101
+ uuid: string;
102
+ vmName: string;
103
+ managedOwnershipSha256: string;
104
+ volumes: string[];
105
+ }, {
106
+ uuid: string;
107
+ vmName: string;
108
+ managedOwnershipSha256: string;
109
+ volumes: string[];
110
+ }>>;
111
+ }, "strict", z.ZodTypeAny, {
112
+ claim: {
113
+ name: string;
114
+ namespace: "ct-lab";
115
+ uid: string;
116
+ } | null;
117
+ slot: {
118
+ node: string;
119
+ vmid: number;
120
+ hostAssetId: string;
121
+ };
122
+ schema: "physical-slot-reservation/v1";
123
+ phase: "released" | "provisional" | "bound" | "provider-bound" | "retiring";
124
+ operationNonce: string;
125
+ requestSha256: string;
126
+ intendedClaim: {
127
+ name: string;
128
+ namespace: "ct-lab";
129
+ };
130
+ context: {
131
+ kind: "mcp";
132
+ issueKey?: string | undefined;
133
+ } | {
134
+ kind: "operator";
135
+ issueKey?: string | undefined;
136
+ } | {
137
+ kind: "ct-meta";
138
+ issueKey: string;
139
+ logicalAssetId: string;
140
+ importOperationId: string;
141
+ };
142
+ provider: {
143
+ uuid: string;
144
+ vmName: string;
145
+ managedOwnershipSha256: string;
146
+ volumes: string[];
147
+ } | null;
148
+ }, {
149
+ claim: {
150
+ name: string;
151
+ namespace: "ct-lab";
152
+ uid: string;
153
+ } | null;
154
+ slot: {
155
+ node: string;
156
+ vmid: number;
157
+ hostAssetId: string;
158
+ };
159
+ schema: "physical-slot-reservation/v1";
160
+ phase: "released" | "provisional" | "bound" | "provider-bound" | "retiring";
161
+ operationNonce: string;
162
+ requestSha256: string;
163
+ intendedClaim: {
164
+ name: string;
165
+ namespace: "ct-lab";
166
+ };
167
+ context: {
168
+ kind: "mcp";
169
+ issueKey?: string | undefined;
170
+ } | {
171
+ kind: "operator";
172
+ issueKey?: string | undefined;
173
+ } | {
174
+ kind: "ct-meta";
175
+ issueKey: string;
176
+ logicalAssetId: string;
177
+ importOperationId: string;
178
+ };
179
+ provider: {
180
+ uuid: string;
181
+ vmName: string;
182
+ managedOwnershipSha256: string;
183
+ volumes: string[];
184
+ } | null;
185
+ }>;
186
+ export type Reservation = z.infer<typeof ReservationSchema>;
187
+ export declare function canonicalJson(value: any): string;
188
+ export declare function physicalSlot(hostAssetId: string, node: string, vmid: number): PhysicalSlot;
189
+ export declare function slotLeaseName(role: "mutex" | "reservation", slot: PhysicalSlot): string;
190
+ export declare function creationOwnerSha(record: Reservation): string;
191
+ export declare function claimPhysicalSlot(claim: any): PhysicalSlot;
192
+ /** Internal dependency seam; callers cannot supply transport, clocks, nonce or permits. */
193
+ export interface SlotDeps {
194
+ api: SlotApi;
195
+ now: () => number;
196
+ nonce: () => string;
197
+ }
198
+ export declare function createPhysicalClaim(manifest: any, deps?: SlotDeps): Promise<{
199
+ claim: any;
200
+ reservation: any;
201
+ reused: boolean;
202
+ }>;
203
+ export declare function deletePhysicalClaim(name: string, expectedUid: string, deps?: SlotDeps): Promise<any>;
204
+ export {};
205
+ //# sourceMappingURL=physical-slot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physical-slot.d.ts","sourceRoot":"","sources":["../../../src/tools/physical-slot.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEnE,eAAO,MAAM,eAAe,oCAAoC,CAAC;AACjE,eAAO,MAAM,UAAU,oDAAoD,CAAC;AAC5E,eAAO,MAAM,YAAY,mDAAmD,CAAC;AAO7E,QAAA,MAAM,UAAU;;;;;;;;;;;;EAA2G,CAAC;AAC5H,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAOtD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAOnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAK5D,wBAAgB,aAAa,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,CAIhD;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAE1F;AACD,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAIvF;AACD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAG5D;AACD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,GAAG,GAAG,YAAY,CAK1D;AAiDD,2FAA2F;AAC3F,MAAM,WAAW,QAAQ;IAAG,GAAG,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,MAAM,CAAC;CAAE;AAyFnF,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,WAAa,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,GAAG,CAAC;IAAC,WAAW,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CAmDtI;AACD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,WAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAsB5G"}
@@ -0,0 +1,295 @@
1
+ /** Shared physical-slot authority (REL-1564). Durable records never expire or auto-clear. */
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { z } from "zod";
4
+ import { load as yamlLoad, JSON_SCHEMA } from "js-yaml";
5
+ import { actualClaimIssue, claimCreationSha256, effectiveClaimSpec } from "./claim-creation-projection.js";
6
+ import { createSlotApi } from "./slot-kubernetes.js";
7
+ export const SLOT_ANNOTATION = "calltelemetry.com/physical-slot";
8
+ export const SLOT_NONCE = "calltelemetry.com/physical-slot-operation-nonce";
9
+ export const SLOT_REQUEST = "calltelemetry.com/physical-slot-request-sha256";
10
+ const LEASES = "/apis/coordination.k8s.io/v1/namespaces/ct-lab/leases";
11
+ const CLAIMS = "/apis/calltelemetry.com/v1alpha1/namespaces/ct-lab/applianceclaims";
12
+ const printable = z.string().min(1).max(256).regex(/^[\x20-\x7e]+$/);
13
+ const uuid = z.string().uuid().refine(value => value === value.toLowerCase(), "UUID must be canonical lowercase");
14
+ const sha = z.string().regex(/^[0-9a-f]{64}$/);
15
+ const objectName = z.string().min(1).max(253).refine(name => name.split(".").every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)), "DNS subdomain labels must be nonempty and at most63 characters");
16
+ const SlotSchema = z.object({ hostAssetId: printable, node: printable, vmid: z.number().safe().int().positive() }).strict();
17
+ const ClaimIdentitySchema = z.object({ namespace: z.literal("ct-lab"), name: objectName, uid: uuid }).strict();
18
+ const ContextSchema = z.discriminatedUnion("kind", [
19
+ z.object({ kind: z.literal("mcp"), issueKey: printable.optional() }).strict(),
20
+ z.object({ kind: z.literal("operator"), issueKey: printable.optional() }).strict(),
21
+ z.object({ kind: z.literal("ct-meta"), logicalAssetId: printable, issueKey: printable, importOperationId: printable }).strict(),
22
+ ]);
23
+ export const ReservationSchema = z.object({
24
+ schema: z.literal("physical-slot-reservation/v1"), slot: SlotSchema,
25
+ phase: z.enum(["provisional", "bound", "provider-bound", "retiring", "released"]),
26
+ operationNonce: uuid, requestSha256: sha,
27
+ intendedClaim: z.object({ namespace: z.literal("ct-lab"), name: objectName }).strict(), context: ContextSchema,
28
+ claim: ClaimIdentitySchema.nullable(),
29
+ provider: z.object({ uuid, vmName: printable, managedOwnershipSha256: sha, volumes: z.array(z.string().min(1).max(512).regex(/^[\x20-\x7e]+$/)).max(256) }).strict().nullable(),
30
+ }).strict();
31
+ const MutexSchema = z.object({ schema: z.literal("physical-slot-mutex/v1"), slot: SlotSchema, ownerNonce: uuid, token: uuid }).strict();
32
+ function requireSlot(ok, message) {
33
+ if (!ok)
34
+ throw new Error(`physical slot ${message}; retain reservation`);
35
+ }
36
+ export function canonicalJson(value) {
37
+ if (Array.isArray(value))
38
+ return `[${value.map(canonicalJson).join(",")}]`;
39
+ if (value !== null && typeof value === "object")
40
+ return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(",")}}`;
41
+ return JSON.stringify(value);
42
+ }
43
+ const digest = (value) => createHash("sha256").update(canonicalJson(value), "utf8").digest("hex");
44
+ export function physicalSlot(hostAssetId, node, vmid) {
45
+ return SlotSchema.parse({ hostAssetId, node, vmid });
46
+ }
47
+ export function slotLeaseName(role, slot) {
48
+ SlotSchema.parse(slot);
49
+ const hash = digest([slot.hostAssetId, slot.node, slot.vmid]);
50
+ return `ct-physical-slot-${role}.${hash.slice(0, 32)}.${hash.slice(32)}`;
51
+ }
52
+ export function creationOwnerSha(record) {
53
+ const { schema, slot, operationNonce, requestSha256, intendedClaim, context } = ReservationSchema.parse(record);
54
+ return digest({ schema, slot, operationNonce, requestSha256, intendedClaim, context });
55
+ }
56
+ export function claimPhysicalSlot(claim) {
57
+ const node = claim?.spec?.node;
58
+ requireSlot(node === "minis" || node === "proxmox", "requires a canonical Proxmox node");
59
+ requireSlot(Number.isSafeInteger(claim.spec.vmid) && claim.spec.vmid >= 100 && claim.spec.vmid <= 8999, "requires an explicit provider VMID");
60
+ return physicalSlot(node === "minis" ? "proxmox-minis-lab" : "proxmox-lab", node, claim.spec.vmid);
61
+ }
62
+ function leaseEnvelope(lease, name) {
63
+ requireSlot(lease?.apiVersion === "coordination.k8s.io/v1" && lease.kind === "Lease"
64
+ && lease.metadata?.namespace === "ct-lab" && lease.metadata.name === name
65
+ && uuid.safeParse(lease.metadata.uid).success && typeof lease.metadata.resourceVersion === "string"
66
+ && lease.metadata.resourceVersion.length > 0 && !lease.metadata.deletionTimestamp, "Lease identity or CAS version differs");
67
+ }
68
+ function annotationJson(text) {
69
+ requireSlot(typeof text === "string" && Buffer.byteLength(text) <= 65_536, "annotation JSON unavailable or oversized");
70
+ const value = JSON.parse(text); // Accept only JSON syntax, not YAML extensions.
71
+ yamlLoad(text, { schema: JSON_SCHEMA, json: false }); // Reject duplicate/escaped duplicate keys.
72
+ return value;
73
+ }
74
+ function reservation(lease, slot) {
75
+ leaseEnvelope(lease, slotLeaseName("reservation", slot));
76
+ const record = ReservationSchema.parse(annotationJson(lease.metadata.annotations?.[SLOT_ANNOTATION]));
77
+ requireSlot(canonicalJson(record.slot) === canonicalJson(slot), "stored full tuple differs");
78
+ requireSlot(lease.spec && Object.keys(lease.spec).length === 0, "durable reservation has expiring lease semantics");
79
+ requireSlot(record.phase === "provisional" ? record.claim === null && record.provider === null : record.claim !== null, "phase and claim binding differ");
80
+ if (record.claim)
81
+ requireSlot(record.claim.namespace === record.intendedClaim.namespace && record.claim.name === record.intendedClaim.name, "bound claim differs from original intent");
82
+ if (record.provider)
83
+ requireSlot(canonicalJson(record.provider.volumes) === canonicalJson([...new Set(record.provider.volumes)].sort()), "provider volumes are not sorted and unique");
84
+ requireSlot(record.phase !== "provider-bound" || record.provider !== null, "provider phase lacks immutable provider binding");
85
+ return record;
86
+ }
87
+ function verifyClaim(live, record, expectedUid) {
88
+ requireSlot(live?.apiVersion === "calltelemetry.com/v1alpha1" && live.kind === "ApplianceClaim"
89
+ && live.metadata?.namespace === record.intendedClaim.namespace && live.metadata.name === record.intendedClaim.name
90
+ && uuid.safeParse(live.metadata.uid).success && (!expectedUid || live.metadata.uid === expectedUid)
91
+ && typeof live.metadata.resourceVersion === "string" && live.metadata.resourceVersion.length > 0
92
+ && canonicalJson(claimPhysicalSlot(live)) === canonicalJson(record.slot)
93
+ && live.metadata.annotations?.[SLOT_NONCE] === record.operationNonce
94
+ && live.metadata.annotations?.[SLOT_REQUEST] === record.requestSha256
95
+ && (actualClaimIssue(live) ?? null) === (record.context.issueKey ?? null)
96
+ && claimCreationSha256(live) === record.requestSha256, "independent claim UID, tuple or source lineage differs");
97
+ }
98
+ const MUTEX_FIELDS = ["holderIdentity", "leaseDurationSeconds", "acquireTime", "renewTime", "leaseTransitions"].sort();
99
+ function wireTime(milliseconds) { return new Date(milliseconds).toISOString().replace(/Z$/, "000Z"); }
100
+ function timeValue(value) {
101
+ requireSlot(typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/.test(value), "mutex timestamp is not canonical UTC microseconds");
102
+ const parsed = Date.parse(value);
103
+ requireSlot(Number.isFinite(parsed) && new Date(parsed).toISOString() === value.slice(0, 23) + "Z", "mutex timestamp is invalid");
104
+ return parsed;
105
+ }
106
+ function leaseBody(name, record, existing, spec = {}) {
107
+ return { apiVersion: "coordination.k8s.io/v1", kind: "Lease", metadata: {
108
+ ...(existing?.metadata || {}), name, namespace: "ct-lab",
109
+ annotations: { ...(existing?.metadata?.annotations || {}), [SLOT_ANNOTATION]: canonicalJson(record) },
110
+ }, spec };
111
+ }
112
+ const defaults = () => ({ api: createSlotApi(), now: Date.now, nonce: randomUUID });
113
+ class Mutex {
114
+ deps;
115
+ slot;
116
+ lease;
117
+ owner;
118
+ timer;
119
+ tail = Promise.resolve();
120
+ failure;
121
+ constructor(deps, slot) {
122
+ this.deps = deps;
123
+ this.slot = slot;
124
+ this.owner = MutexSchema.parse({ schema: "physical-slot-mutex/v1", slot, ownerNonce: deps.nonce(), token: deps.nonce() });
125
+ }
126
+ name() { return slotLeaseName("mutex", this.slot); }
127
+ queue(fn) {
128
+ const run = this.tail.then(fn);
129
+ this.tail = run.catch(() => { });
130
+ return run;
131
+ }
132
+ validate(value) {
133
+ leaseEnvelope(value, this.name());
134
+ const wire = MutexSchema.parse(annotationJson(value.metadata.annotations?.[SLOT_ANNOTATION]));
135
+ requireSlot(canonicalJson(wire.slot) === canonicalJson(this.slot) && value.spec?.holderIdentity === wire.token
136
+ && value.spec.leaseDurationSeconds === 120 && Number.isSafeInteger(value.spec.leaseTransitions) && value.spec.leaseTransitions >= 0
137
+ && canonicalJson(Object.keys(value.spec).sort()) === canonicalJson(MUTEX_FIELDS)
138
+ && Number.isFinite(timeValue(value.spec.acquireTime)) && Number.isFinite(timeValue(value.spec.renewTime)), "mutex tuple, holder or duration differs");
139
+ return wire;
140
+ }
141
+ async enter() {
142
+ const path = `${LEASES}/${this.name()}`;
143
+ const previous = await this.deps.api.request(path, "GET");
144
+ if (previous) {
145
+ this.validate(previous);
146
+ requireSlot(Date.parse(previous.spec.renewTime) + 120_000 <= this.deps.now(), "mutex is occupied");
147
+ }
148
+ const transitions = previous ? previous.spec.leaseTransitions + 1 : 0;
149
+ requireSlot(Number.isSafeInteger(transitions), "mutex transition counter overflow");
150
+ const spec = { holderIdentity: this.owner.token, leaseDurationSeconds: 120, acquireTime: wireTime(this.deps.now()), renewTime: wireTime(this.deps.now()), leaseTransitions: transitions };
151
+ this.lease = await this.deps.api.request(previous ? path : LEASES, previous ? "PUT" : "POST", leaseBody(this.name(), this.owner, previous, spec));
152
+ this.validate(this.lease);
153
+ requireSlot(canonicalJson(this.validate(this.lease)) === canonicalJson(this.owner), "mutex creation returned another holder");
154
+ this.timer = setInterval(() => { void this.touch().catch(err => { this.failure = err; }); }, 20_000);
155
+ this.timer.unref();
156
+ }
157
+ async touch() {
158
+ return this.queue(async () => {
159
+ if (this.failure)
160
+ throw this.failure;
161
+ const path = `${LEASES}/${this.name()}`;
162
+ const live = await this.deps.api.request(path, "GET");
163
+ const wire = this.validate(live);
164
+ requireSlot(live.metadata.uid === this.lease.metadata.uid && canonicalJson(wire) === canonicalJson(this.owner)
165
+ && Date.parse(live.spec.renewTime) + 120_000 > this.deps.now(), "mutex ownership expired or changed");
166
+ this.lease = await this.deps.api.request(path, "PUT", leaseBody(this.name(), this.owner, live, { ...live.spec, renewTime: wireTime(this.deps.now()) }));
167
+ this.validate(this.lease);
168
+ requireSlot(this.lease.metadata.uid === live.metadata.uid && canonicalJson(this.validate(this.lease)) === canonicalJson(this.owner) && Date.parse(this.lease.spec.renewTime) + 120_000 > this.deps.now(), "mutex renewal did not preserve ownership");
169
+ });
170
+ }
171
+ async exit() {
172
+ if (this.timer)
173
+ clearInterval(this.timer);
174
+ await this.tail;
175
+ if (!this.lease || this.failure)
176
+ return;
177
+ try {
178
+ await this.queue(async () => {
179
+ const path = `${LEASES}/${this.name()}`;
180
+ const live = await this.deps.api.request(path, "GET");
181
+ requireSlot(live?.metadata?.uid === this.lease.metadata.uid && canonicalJson(this.validate(live)) === canonicalJson(this.owner), "mutex release owner differs");
182
+ await this.deps.api.request(path, "PUT", leaseBody(this.name(), this.owner, live, { ...live.spec, renewTime: "1970-01-01T00:00:00.000000Z" }));
183
+ });
184
+ }
185
+ catch { /* Unknown release never removes durable reservation; mutex expires naturally. */ }
186
+ }
187
+ }
188
+ async function withMutex(deps, slot, action) {
189
+ const mutex = new Mutex(deps, slot);
190
+ try {
191
+ await mutex.enter();
192
+ return await action(mutex);
193
+ }
194
+ finally {
195
+ await mutex.exit();
196
+ }
197
+ }
198
+ async function noOtherClaim(deps, slot, ownUid) {
199
+ let next = "";
200
+ for (let page = 0; page < 10; page++) {
201
+ const result = await deps.api.request(`${CLAIMS}?limit=500${next ? `&continue=${encodeURIComponent(next)}` : ""}`, "GET");
202
+ requireSlot(result?.kind === "ApplianceClaimList" && result.apiVersion === "calltelemetry.com/v1alpha1" && Array.isArray(result.items), "claim occupancy list unavailable");
203
+ for (const claim of result.items) {
204
+ if (claim.metadata?.uid === ownUid)
205
+ continue;
206
+ const id = claim.spec?.vmid;
207
+ const node = claim.spec?.node;
208
+ const alias = node === "proxmox-lab" ? "proxmox" : ["proxmox-mini", "proxmox-minis-lab"].includes(node) ? "minis" : node;
209
+ if (String(id) === String(slot.vmid) && (alias === slot.node || !["minis", "proxmox", "esxi"].includes(alias)))
210
+ throw new Error("physical slot has another existing ApplianceClaim; retain reservation");
211
+ }
212
+ next = result.metadata?.continue || "";
213
+ requireSlot(typeof next === "string", "claim occupancy pagination malformed");
214
+ if (!next)
215
+ return;
216
+ }
217
+ throw new Error("physical slot claim occupancy scan incomplete; retain reservation");
218
+ }
219
+ export async function createPhysicalClaim(manifest, deps = defaults()) {
220
+ requireSlot(manifest.metadata?.namespace === "ct-lab", "namespace must be ct-lab");
221
+ objectName.parse(manifest.metadata.name);
222
+ manifest = { ...manifest, spec: effectiveClaimSpec(manifest.spec) };
223
+ const slot = claimPhysicalSlot(manifest);
224
+ requireSlot(typeof manifest.spec.vmName === "string" && manifest.spec.vmName.length > 0, "constructor must provide the actual VM name before claim POST");
225
+ requireSlot(Number.isInteger(manifest.spec?.ttlHours) && manifest.spec.ttlHours > 0 && manifest.spec.ttlHours <= 8760, "dynamic claim requires finite TTL");
226
+ const context = ContextSchema.parse({ kind: "mcp", ...(actualClaimIssue(manifest) ? { issueKey: actualClaimIssue(manifest) } : {}) });
227
+ const requestSha256 = claimCreationSha256(manifest);
228
+ return withMutex(deps, slot, async (mutex) => {
229
+ const path = `${LEASES}/${slotLeaseName("reservation", slot)}`;
230
+ let existing = await deps.api.request(path, "GET");
231
+ if (existing) {
232
+ const held = reservation(existing, slot);
233
+ requireSlot(["bound", "provider-bound"].includes(held.phase) && held.requestSha256 === requestSha256
234
+ && canonicalJson(held.context) === canonicalJson(context) && held.intendedClaim.name === manifest.metadata.name, "durable reservation is occupied");
235
+ const live = await deps.api.request(`${CLAIMS}/${manifest.metadata.name}`, "GET");
236
+ verifyClaim(live, held, held.claim.uid);
237
+ requireSlot(!live.metadata.deletionTimestamp && live.spec?.closed !== true && live.spec?.state !== "Closed" && live.status?.phase !== "Closed", "bound claim is retiring");
238
+ requireSlot(Number.isInteger(live.spec?.ttlHours) && live.spec.ttlHours >= manifest.spec.ttlHours && live.spec.ttlHours <= 8760 && live.spec.claimType !== "fixture", "bound claim finite TTL differs");
239
+ if (live.status?.phase === "Ready" || live.status?.completionTime) {
240
+ const baseline = Date.parse(live.status?.completionTime || "");
241
+ requireSlot(Number.isFinite(baseline) && baseline + live.spec.ttlHours * 3_600_000 > deps.now(), "bound claim expired or Ready TTL baseline unavailable");
242
+ }
243
+ await noOtherClaim(deps, slot, live.metadata.uid);
244
+ await mutex.touch();
245
+ const current = await deps.api.request(path, "GET");
246
+ requireSlot(current?.metadata?.uid === existing.metadata.uid && canonicalJson(reservation(current, slot)) === canonicalJson(held), "bound reservation changed during retry");
247
+ return { claim: live, reservation: { uid: existing.metadata.uid, creationOwnerSha256: creationOwnerSha(held), record: held }, reused: true };
248
+ }
249
+ await noOtherClaim(deps, slot);
250
+ const record = ReservationSchema.parse({ schema: "physical-slot-reservation/v1", slot, phase: "provisional", operationNonce: deps.nonce(), requestSha256,
251
+ intendedClaim: { namespace: "ct-lab", name: manifest.metadata.name }, context, claim: null, provider: null });
252
+ await mutex.touch();
253
+ existing = await deps.api.request(LEASES, "POST", leaseBody(slotLeaseName("reservation", slot), record));
254
+ requireSlot(canonicalJson(reservation(existing, slot)) === canonicalJson(record), "reservation creation returned another owner");
255
+ const protectedManifest = { ...manifest, metadata: { ...manifest.metadata, annotations: { ...(manifest.metadata.annotations || {}), [SLOT_NONCE]: record.operationNonce, [SLOT_REQUEST]: requestSha256 } } };
256
+ await mutex.touch();
257
+ const response = await deps.api.request(CLAIMS, "POST", protectedManifest);
258
+ requireSlot(uuid.safeParse(response?.metadata?.uid).success, "claim creation response lacks actual UID");
259
+ const live = await deps.api.request(`${CLAIMS}/${manifest.metadata.name}`, "GET");
260
+ verifyClaim(live, record, response.metadata.uid);
261
+ requireSlot(!live.metadata.deletionTimestamp, "new claim is already deleting");
262
+ const bound = { ...record, phase: "bound", claim: { ...record.intendedClaim, uid: live.metadata.uid } };
263
+ await mutex.touch();
264
+ const current = await deps.api.request(path, "GET");
265
+ requireSlot(current?.metadata?.uid === existing.metadata.uid && canonicalJson(reservation(current, slot)) === canonicalJson(record), "provisional reservation changed before UID binding");
266
+ existing = await deps.api.request(path, "PUT", leaseBody(slotLeaseName("reservation", slot), bound, current));
267
+ requireSlot(existing?.metadata?.uid === current.metadata.uid && canonicalJson(reservation(existing, slot)) === canonicalJson(bound), "UID binding not independently confirmed");
268
+ return { claim: live, reservation: { uid: existing.metadata.uid, creationOwnerSha256: creationOwnerSha(bound), record: bound }, reused: false };
269
+ });
270
+ }
271
+ export async function deletePhysicalClaim(name, expectedUid, deps = defaults()) {
272
+ objectName.parse(name);
273
+ uuid.parse(expectedUid);
274
+ const live = await deps.api.request(`${CLAIMS}/${name}`, "GET");
275
+ requireSlot(live?.metadata?.uid === expectedUid, "original selected claim UID differs or is absent");
276
+ const slot = claimPhysicalSlot(live);
277
+ return withMutex(deps, slot, async (mutex) => {
278
+ const path = `${LEASES}/${slotLeaseName("reservation", slot)}`;
279
+ let lease = await deps.api.request(path, "GET");
280
+ const held = reservation(lease, slot);
281
+ requireSlot(["bound", "provider-bound", "retiring"].includes(held.phase) && held.claim?.uid === expectedUid && held.intendedClaim.name === name, "claim does not own the bound reservation");
282
+ let selected = await deps.api.request(`${CLAIMS}/${name}`, "GET");
283
+ verifyClaim(selected, held, expectedUid);
284
+ const retiring = { ...held, phase: "retiring" };
285
+ await mutex.touch();
286
+ lease = await deps.api.request(path, "PUT", leaseBody(slotLeaseName("reservation", slot), retiring, lease));
287
+ requireSlot(canonicalJson(reservation(lease, slot)) === canonicalJson(retiring), "retiring reservation write failed");
288
+ await mutex.touch();
289
+ selected = await deps.api.request(`${CLAIMS}/${name}`, "GET");
290
+ verifyClaim(selected, retiring, expectedUid);
291
+ await deps.api.request(`${CLAIMS}/${name}`, "DELETE", { apiVersion: "v1", kind: "DeleteOptions", preconditions: { uid: expectedUid, resourceVersion: selected.metadata.resourceVersion } });
292
+ return { requested: true, expectedUid, reservationRetained: true, phase: "retiring", next: "The governed operator/controller must prove original claim, provider and owned-volume absence before releasing physical-slot custody." };
293
+ });
294
+ }
295
+ //# sourceMappingURL=physical-slot.js.map