@rallycry/conveyor-k3 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ var __export = (target2, all) => {
10
10
  };
11
11
 
12
12
  // src/lib/paths.ts
13
- var ETC_CONVEYOR, ENV_FILE, TUNNEL_ENV_FILE, STATE_DIR, STATE_FILE, LOCAL_REGISTRY_DATA_DIR, LOCAL_REGISTRY_MIRROR_RESTART_FILE, RANCHER_DIR, K3S_OWNER_CONFIG, K3S_CONFIG_DROPIN_DIR, K3S_CONFIG_DROPIN, REGISTRIES_FILE, K3S_CERTS_D, K3S_SERVER_CA, K3S_CONTAINERD_DIR, JOURNALD_DROPIN, CLOUDFLARED_DIR, CA_POOL_PATH, SYSTEMD_DIR, DAEMON_UNIT, TUNNEL_UNIT, UPDATE_UNIT, UPDATE_TIMER, ALL_UNITS, OWNED_FILES, RUNNERS_SUDOERS_FILE;
13
+ var ETC_CONVEYOR, ENV_FILE, TUNNEL_ENV_FILE, STATE_DIR, STATE_FILE, LOCAL_REGISTRY_DATA_DIR, LOCAL_REGISTRY_MIRROR_RESTART_FILE, LOCAL_REGISTRY_CREDENTIALS_FILE, LOCAL_REGISTRY_AUTH_STATE_FILE, RANCHER_DIR, K3S_OWNER_CONFIG, K3S_CONFIG_DROPIN_DIR, K3S_CONFIG_DROPIN, REGISTRIES_FILE, K3S_CERTS_D, K3S_SERVER_CA, K3S_CONTAINERD_DIR, JOURNALD_DROPIN, CLOUDFLARED_DIR, CA_POOL_PATH, SYSTEMD_DIR, DAEMON_UNIT, TUNNEL_UNIT, UPDATE_UNIT, UPDATE_TIMER, ALL_UNITS, OWNED_FILES, RUNNERS_SUDOERS_FILE;
14
14
  var init_paths = __esm({
15
15
  "src/lib/paths.ts"() {
16
16
  "use strict";
@@ -21,6 +21,8 @@ var init_paths = __esm({
21
21
  STATE_FILE = `${STATE_DIR}/state.json`;
22
22
  LOCAL_REGISTRY_DATA_DIR = `${STATE_DIR}/registry`;
23
23
  LOCAL_REGISTRY_MIRROR_RESTART_FILE = `${STATE_DIR}/registry-mirror-restart`;
24
+ LOCAL_REGISTRY_CREDENTIALS_FILE = `${STATE_DIR}/registry-credentials.json`;
25
+ LOCAL_REGISTRY_AUTH_STATE_FILE = `${STATE_DIR}/registry-auth.json`;
24
26
  RANCHER_DIR = "/etc/rancher/k3s";
25
27
  K3S_OWNER_CONFIG = `${RANCHER_DIR}/config.yaml`;
26
28
  K3S_CONFIG_DROPIN_DIR = `${RANCHER_DIR}/config.yaml.d`;
@@ -216,17 +218,44 @@ var init_safe_arg = __esm({
216
218
  });
217
219
 
218
220
  // src/lib/env.ts
219
- function readCliEnv(env = process.env) {
220
- const apiUrl = env.CONVEYOR_API_URL ?? "";
221
- const clusterToken = env.CONVEYOR_CLUSTER_TOKEN ?? "";
222
- const clusterId = env.CONVEYOR_CLUSTER_ID ?? "";
221
+ import { readFileSync } from "fs";
222
+ function parseEnvFile(content) {
223
+ const values = {};
224
+ for (const raw of content.split("\n")) {
225
+ const line = raw.trim();
226
+ if (!line || line.startsWith("#")) continue;
227
+ const eq = line.indexOf("=");
228
+ if (eq <= 0) continue;
229
+ const key = line.slice(0, eq).trim();
230
+ let value = line.slice(eq + 1).trim();
231
+ if (value.length >= 2 && (value[0] === '"' || value[0] === "'") && value.endsWith(value[0])) {
232
+ value = value.slice(1, -1);
233
+ }
234
+ values[key] = value;
235
+ }
236
+ return values;
237
+ }
238
+ function readCliEnv(env = process.env, readEnvFile = readEnvFileFromDisk) {
239
+ let source = env;
240
+ if (!env.CONVEYOR_API_URL || !env.CONVEYOR_CLUSTER_TOKEN) {
241
+ const content = readEnvFile?.() ?? null;
242
+ if (content !== null) source = { ...parseEnvFile(content), ...definedOnly(env) };
243
+ }
244
+ const apiUrl = source.CONVEYOR_API_URL ?? "";
245
+ const clusterToken = source.CONVEYOR_CLUSTER_TOKEN ?? "";
246
+ const clusterId = source.CONVEYOR_CLUSTER_ID ?? "";
223
247
  if (!apiUrl || !clusterToken) {
224
248
  throw new Error(
225
- "This machine is not enrolled. /etc/conveyor/k3.env must set CONVEYOR_API_URL and CONVEYOR_CLUSTER_TOKEN \u2014 re-run the install command from your Conveyor profile."
249
+ `This machine is not enrolled. ${ENV_FILE} must set CONVEYOR_API_URL and CONVEYOR_CLUSTER_TOKEN \u2014 re-run the install command from your Conveyor profile.`
226
250
  );
227
251
  }
228
252
  return { apiUrl, clusterToken, clusterId };
229
253
  }
254
+ function definedOnly(env) {
255
+ return Object.fromEntries(
256
+ Object.entries(env).filter((entry) => Boolean(entry[1]))
257
+ );
258
+ }
230
259
  async function preflight(exec) {
231
260
  const problems = [];
232
261
  if (process.platform !== "linux") {
@@ -250,11 +279,20 @@ async function preflight(exec) {
250
279
  function checkedHostname(hostname2) {
251
280
  return assertHostname(hostname2, "tunnel hostname");
252
281
  }
282
+ var readEnvFileFromDisk;
253
283
  var init_env = __esm({
254
284
  "src/lib/env.ts"() {
255
285
  "use strict";
256
286
  init_safe_arg();
257
287
  init_exec();
288
+ init_paths();
289
+ readEnvFileFromDisk = () => {
290
+ try {
291
+ return readFileSync(ENV_FILE, "utf8");
292
+ } catch {
293
+ return null;
294
+ }
295
+ };
258
296
  }
259
297
  });
260
298
 
@@ -4374,7 +4412,7 @@ var init_zod = __esm({
4374
4412
  }
4375
4413
  });
4376
4414
 
4377
- // ../shared/dist/chunk-VSUBG7GR.js
4415
+ // ../shared/dist/chunk-SFSQ7NKP.js
4378
4416
  function formatModelId(provider, model) {
4379
4417
  return `${provider}/${model}`;
4380
4418
  }
@@ -4392,8 +4430,8 @@ function uniqueSortedArray(item, minimum = 0) {
4392
4430
  }).transform((values) => [...values].sort());
4393
4431
  }
4394
4432
  var DEFAULT_SONNET_MODEL, DEFAULT_OPUS_MODEL, DEFAULT_HAIKU_MODEL, FABLE_MODEL, PREVIOUS_SONNET_MODEL, PREVIOUS_OPUS_MODEL, JEV_MODEL, JEV_MODEL_ID, JEV_ZEN_MODEL, JEV_ZEN_MODEL_ID, JEV_INPUT_PRICE_PER_TOKEN, JEV_STATE_MAX_CHARS, PTY_STREAM_PORT_BASE, PTY_STREAM_PORT_ATTEMPTS, PREVIEW_PORT_DENY_LIST, checkpointPathSchema, secretNameSchema, checkpointKeySchema, checkpointDigestRefSchema, ACTIONS_PREBAKE_REGISTRY_PATTERN, actionsPrebakeRegistrySchema, projectCheckpointSettingsSchema, CARD_DESCRIPTION_MAX, CARD_DESCRIPTION_LIMIT_MESSAGE, CARD_DESCRIPTION_FIELD_HINT, MAX_MANUAL_TESTS_PER_TASK, MAX_MANUAL_TEST_TITLE_LENGTH, ManualTestItemsSchema, MAX_CI_WAIT_TIMEOUT_MINUTES;
4395
- var init_chunk_VSUBG7GR = __esm({
4396
- "../shared/dist/chunk-VSUBG7GR.js"() {
4433
+ var init_chunk_SFSQ7NKP = __esm({
4434
+ "../shared/dist/chunk-SFSQ7NKP.js"() {
4397
4435
  "use strict";
4398
4436
  init_zod();
4399
4437
  init_zod();
@@ -4519,11 +4557,56 @@ function localBakeSudoersCommands(k3sPath) {
4519
4557
  return `${k3sPath} ctr ${command.args.join(" ")}${suffix}`;
4520
4558
  });
4521
4559
  }
4522
- var ACHIEVEMENT_RARITIES, RISK_LEVELS, riskLevelSchema, DEFAULT_RISK_LEVELS, LEVEL_BY_VALUE, ACTIVE_WORK_STATUSES, IDENTIFIED_WORK_STATUSES, DEFAULT_TASK_STATUS_COLOR, DEFAULT_TASK_STATUS_COLOR_INT, MAX_FILE_SIZE_BYTES, MAX_FILE_TAGS, MAX_FILE_TAG_LENGTH, ALLOWED_FILE_MIME_TYPES, ALLOWED_FILE_ACCEPT, EMBED_THRESHOLD_IMAGES, EMBED_THRESHOLD_TEXT, IDLE_HEARTBEAT_MS, TurnEndToolCallSchema, AgentTurnUsageSchema, KnownAgentEventSchema, AgentEventSchema, PtyChatAtSchema, PtyChatTurnUsageSchema, PtyChatTurnFileSchema, PTY_CHAT_TURN_FILES_MAX, PtyChatEventPayloadSchema, PtyChatEventRequestSchema, PtyChatAttachRequestSchema, PTY_EFFORT_LEVELS, SetPtyModelEffortRequestSchema, cardDescription, AgentHeartbeatSchema, CreatePRInputSchema, PostToChatInputSchema, GetTaskContextRequestSchema, GetChatMessagesRequestSchema, GetTaskFilesRequestSchema, GetTaskFileRequestSchema, DeleteTaskFileRequestSchema, SetTaskFileShowcaseRequestSchema, GetTaskRequestSchema, GetCliHistoryRequestSchema, ListSubtasksRequestSchema, GetDependenciesRequestSchema, GetSuggestionsRequestSchema, ListManualTestsRequestSchema, QueryManualTestsRequestSchema, CreatePullRequestRequestSchema, RequestFileUploadRequestSchema, ConfirmFileUploadRequestSchema, UpdateTaskStatusRequestSchema, StoreSessionIdRequestSchema, SetManualTestsRequestSchema, EditManualTestRequestSchema, RemoveManualTestRequestSchema, ApproveManualTestRequestSchema, RejectManualTestRequestSchema, SessionStartRequestSchema, SessionStopRequestSchema, EndReviewSessionRequestSchema, ConnectAgentRequestSchema, ReportAgentStatusRequestSchema, NotifyAgentVersionRequestSchema, DiscoveredPortSchema, ReportDiscoveredPortsRequestSchema, ReportBootMilestoneRequestSchema, CreateSubtaskRequestSchema, UpdateSubtaskRequestSchema, DeleteSubtaskRequestSchema, SetSubtaskParentRequestSchema, GetTaskPropertiesRequestSchema, UpdateTaskFieldsRequestSchema, UpdateTaskPropertiesRequestSchema, ListIconsRequestSchema, GenerateTaskIconRequestSchema, SearchFaIconsRequestSchema, PickFaIconRequestSchema, CreateFollowUpTaskRequestSchema, AddDependencyRequestSchema, RemoveDependencyRequestSchema, CreateSuggestionRequestSchema, VoteSuggestionRequestSchema, TriggerIdentificationRequestSchema, HandoffToImplementerRequestSchema, SubmitCodeReviewResultRequestSchema, CycleCodingAgentKeyRequestSchema, PostChildChatMessageRequestSchema, UpdateChildStatusRequestSchema, GetAgentStatusRequestSchema, GetUiCliHistoryRequestSchema, GetActivePtySessionRequestSchema, ListActivePtySessionsRequestSchema, SendSoftStopRequestSchema, StopTaskSessionRequestSchema, FlushTaskQueueRequestSchema, CancelTaskQueuedMessageRequestSchema, FlushSingleQueuedMessageRequestSchema, AnswerAgentQuestionRequestSchema, ClearAgentTodosRequestSchema, AgentQuestionOptionSchema, AgentQuestionSchema, AskUserQuestionRequestSchema, PostAgentMessageRequestSchema, EmitAgentEventRequestSchema, RefreshGithubTokenRequestSchema, ReportCredentialFailureRequestSchema, ReportReviewSpawnFailureRequestSchema, ReportBuilderSpawnFailureRequestSchema, SpawnTaskSessionRequestSchema, StartCodeReviewRequestSchema, StopCodeReviewRequestSchema, ReportSessionSpawnFailureRequestSchema, RefreshGithubTokenResponseSchema, PTY_FRAME_MAX_CHARS, PTY_MAX_DIMENSION, PtyOutputRequestSchema, PtyEndedRequestSchema, PtyInputRequestSchema, PtyResizeRequestSchema, PtyAttachRequestSchema, ReportPtyStreamRequestSchema, ReportPtyInputRequestSchema, GetPtyStreamEndpointRequestSchema, CreatePRResponseSchema, PostToChatResponseSchema, UpdateTaskStatusResponseSchema, StoreSessionIdResponseSchema, HeartbeatResponseSchema, SessionStartResponseSchema, SessionStopResponseSchema, DeleteSubtaskResponseSchema, ParkOnCheckResultRequestSchema, GIT_BRANCH_NAME_MAX, GIT_BRANCH_NAME_MESSAGE, ALLOWED_REF, cardDescription2, ListAccessibleProjectsRequestSchema, ListProjectTasksRequestSchema, GetProjectTaskRequestSchema, SearchProjectTasksRequestSchema, ListProjectTagsRequestSchema, GetProjectTagRequestSchema, ListProjectTagAttachmentsRequestSchema, SetProjectFileTagsRequestSchema, GetProjectSummaryRequestSchema, GetProjectOnboardingStatusRequestSchema, GetProjectOnboardingStepRequestSchema, GetProjectConnectUrlsRequestSchema, conveyorCapabilitySchema, GetConnectionContextRequestSchema, VerifyConnectionRequestSchema, ListAccessibleSubprojectsRequestSchema, CreateProjectTaskRequestSchema, SetProjectTaskParentRequestSchema, UpdateProjectTaskRequestSchema, TransitionProjectTaskStatusRequestSchema, MoveProjectCardRequestSchema, PostToProjectTaskChatRequestSchema, GetProjectTaskCliRequestSchema, GetProjectTaskSessionsRequestSchema, QueryProjectGcpLogsRequestSchema, QueryProjectGrafanaLogsRequestSchema, driveFileNameSchema, DRIVE_MAX_CONTENT_CHARS, ListProjectDriveFilesRequestSchema, ReadProjectDriveFileRequestSchema, CreateProjectDriveFileRequestSchema, UpdateProjectDriveFileRequestSchema, DeleteProjectDriveFileRequestSchema, CreateProjectDriveFolderRequestSchema, StartProjectBuildRequestSchema, StopProjectBuildRequestSchema, StartProjectWorkspaceRequestSchema, StopProjectWorkspaceRequestSchema, ListMyLiveSessionsRequestSchema, ListProjectSessionGroupsRequestSchema, ListMyLiveSessionsAcrossProjectsRequestSchema, ListSessionGroupsAcrossProjectsRequestSchema, GetProjectAvailableTuisRequestSchema, StartAdhocSessionRequestSchema, StopAdhocSessionRequestSchema, ResumeAdhocSessionRequestSchema, RefreshCodingAgentKeyUsageRequestSchema, ListKeysToProbeRequestSchema, CreateProjectReleaseRequestSchema, AddTasksToProjectReleaseRequestSchema, ApproveProjectMergePRRequestSchema, ListProjectSubtasksRequestSchema, CreateProjectSubtaskRequestSchema, UpdateProjectSubtaskRequestSchema, DeleteProjectSubtaskRequestSchema, GetProjectTaskChatRequestSchema, AddProjectTaskDependencyRequestSchema, RemoveProjectTaskDependencyRequestSchema, VoteProjectSuggestionRequestSchema, GetProjectTaskDependenciesRequestSchema, CreateProjectPullRequestRequestSchema, ListProjectMembersRequestSchema, AddProjectTaskReviewerRequestSchema, RemoveProjectTaskReviewerRequestSchema, ListProjectManualTestsRequestSchema, QueryProjectManualTestsRequestSchema, SetProjectManualTestsRequestSchema, EditProjectManualTestRequestSchema, RemoveProjectManualTestRequestSchema, ApproveProjectManualTestRequestSchema, RejectProjectManualTestRequestSchema, CreateProjectSuggestionRequestSchema, ListProjectChannelsRequestSchema, READ_CHANNEL_MESSAGES_MAX_LIMIT, ReadChannelMessagesRequestSchema, POST_CHANNEL_MESSAGE_MAX_CHARS, PostChannelMessageRequestSchema, GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS, GetProjectAnalyticsSummaryRequestSchema, ListProjectTaskFilesRequestSchema, GetProjectAttachmentRequestSchema, DeleteProjectTaskFileRequestSchema, SetProjectTaskFileShowcaseRequestSchema, RequestProjectFileUploadRequestSchema, ConfirmProjectFileUploadRequestSchema, RequestWorkspaceRecycleRequestSchema, ReportApiOutageRequestSchema, CONTEXT_LINK_LOCATOR_MAX, TAG_DESCRIPTION_MAX, TAG_OVERVIEW_MAX, TAG_REASON_MAX, ProjectTagContextPathSchema, hexColor, overviewPathSchema, CreateProjectTagRequestSchema, UpdateProjectTagRequestSchema, PostToProjectChatRequestSchema, StartTagAuditRequestSchema, StartTaskAuditRequestSchema, GetActiveAuditSessionsRequestSchema, ReportTaskAuditResultRequestSchema, GetTaskAuditsRequestSchema, GetTaskAuditRequestSchema, GetTaskAuditAggregatesRequestSchema, DeleteTaskAuditRequestSchema, MarkInitialPromptSubmittedRequestSchema, PERSONAL_CLUSTER_EVENTS, PERSONAL_CLUSTER_HEARTBEAT_INTERVAL_MS, DECISION_STATUS_LABELS, DECISION_OPTION_KINDS, DECISION_VOTE_SOURCES, DECISION_OPTIONS_MIN, DECISION_OPTIONS_MAX, DECISION_OPTION_LABEL_MAX, DECISION_QUESTION_MAX, DECISION_RESOLUTION_MAX, DECISION_VOTE_REASON_MAX, DECISION_DEADLINE_MIN_HOURS, DECISION_DEADLINE_MAX_DAYS, DECISION_DEADLINE_MAX_HOURS, DECISION_LIST_MAX_LIMIT, decisionIdSchema, optionInputSchema, optionsArraySchema, CreateDecisionRequestSchema, VoteDecisionRequestSchema, RemoveDecisionVoteRequestSchema, ResolveDecisionRequestSchema, CancelDecisionRequestSchema, BulkDecisionIdsRequestSchema, ReopenDecisionRequestSchema, UpdateDecisionRequestSchema, GetDecisionRequestSchema, ListDecisionsRequestSchema, optionInput, optionsInput, deadlineHours, CreateProjectDecisionRequestSchema, ListProjectDecisionsRequestSchema, GetProjectDecisionRequestSchema, VoteProjectDecisionRequestSchema, ResolveProjectDecisionRequestSchema, UpdateProjectDecisionRequestSchema, EVALUATE_MAX_QUESTIONS, EVALUATE_MAX_ITEMS, EVALUATE_ITEM_MAX_CHARS, EVALUATE_REQUEST_MAX_CHARS, TEXT_MAX, QUESTION_KEY, EvaluateQuestionSchema, EvaluateQuestionsRequestSchema, MEETING_CHECKLIST_TITLE_MAX, MEETING_TRANSCRIPT_MAX_CHARS, MEETING_TITLE_MAX, MEETING_OCCURRED_AT_MIN_YEAR, MEETING_OCCURRED_AT_MAX_FUTURE_MS, OCCURRED_AT_RANGE_MESSAGE, MeetingOccurredAtSchema, CreateMeetingFromTranscriptRequestSchema, GetMeetingRequestSchema, UpdateMeetingRequestSchema, RegenerateMeetingSummaryRequestSchema, DeleteMeetingRequestSchema, checklistTitle, ListMeetingChecklistRequestSchema, AddMeetingChecklistItemsRequestSchema, UpdateMeetingChecklistItemRequestSchema, DeleteMeetingChecklistItemRequestSchema, SetMeetingChecklistItemCheckedRequestSchema, ListMeetingsRequestSchema, ReadMeetingTranscriptRequestSchema, MEETING_SUMMARY_MAX_CHARS, AddProjectMeetingChecklistItemsRequestSchema, CheckProjectMeetingChecklistItemRequestSchema, EditProjectMeetingChecklistItemRequestSchema, RemoveProjectMeetingChecklistItemRequestSchema, CreateProjectMeetingRequestSchema, UpdateProjectMeetingRequestSchema, TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT, AGENT_CHAT_HISTORY_FETCH_LIMIT, ANTHROPIC_CATALOG, CLAUDESPACE_WORKLOAD_LABEL, CLAUDESPACE_WORKLOAD_VALUE, CONVEYOR_POD_SELECTOR, CONTAINERD_K8S_NAMESPACE, CONTAINERD_PIN_LABEL, CONVEYOR_CONTENT_HASH_LABEL, VM_IMPORT_DISCOVERY_DIR, LOCAL_BAKE_ROOT_COMMANDS, PERSONAL_LOCAL_REGISTRY_NAMESPACE, PERSONAL_LOCAL_REGISTRY_NAME, PERSONAL_LOCAL_REGISTRY_PORT, PERSONAL_LOCAL_REGISTRY_HOST, PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT, BAKE_COMPONENT_LABEL, BAKE_COMPONENT_PREBAKE, BAKE_COMPONENT_STATE_BAKE, BAKE_COMPONENT_VALUES, PERSONAL_BAKE_NAMESPACE, MS_PER_DAY, POSTGRES_ENV, FIREBASE_EMULATOR_COMMAND, FIREBASE_EMULATOR_ENV, CATALOG, SERVICE_DEFINITIONS, SUPPORTED_CLAUDESPACE_DEPS, STATEFUL_BAKE_DEPS, MIRRORABLE_SIDECARS, LEVELS_PER_BAND, PRESTIGE_TIERS, TOP_PRESTIGE_BAND, CARD_TYPE_SURFACE, surfaceTypes, BOARD_CARD_TYPES, REPORT_CARD_TYPES, DECISION_CARD_TYPES;
4560
+ function relayAllowanceGiB(relaySlots = BASE_RELAY_SLOTS) {
4561
+ return Math.max(0, relaySlots - BASE_RELAY_SLOTS) * RELAY_RSS_ALLOWANCE_MIB / 1024;
4562
+ }
4563
+ function vmHostMemoryGiB(guestGiB, relaySlots = BASE_RELAY_SLOTS) {
4564
+ return guestGiB + Math.max(1, guestGiB / 4) + relayAllowanceGiB(relaySlots);
4565
+ }
4566
+ function maximumGuestMemoryGiBWithin(budgetGiB, relaySlots = BASE_RELAY_SLOTS) {
4567
+ const guestBudget = budgetGiB - relayAllowanceGiB(relaySlots);
4568
+ if (!Number.isFinite(guestBudget) || guestBudget <= 0) return 0;
4569
+ return Math.max(0, Math.floor(Math.min(guestBudget - 1, guestBudget / 1.25)));
4570
+ }
4571
+ function vmMemoryBudgetGiB(hostMemoryGiB) {
4572
+ return hostMemoryGiB * VM_HOST_MEMORY_SHARE;
4573
+ }
4574
+ function vmCpuBudget(hostCpus) {
4575
+ return Math.max(1, hostCpus - 1);
4576
+ }
4577
+ function recommendedVmSize(hostMemoryGiB, hostCpus) {
4578
+ return {
4579
+ cpus: defaultCpus(hostCpus),
4580
+ memoryGiB: Math.min(
4581
+ VM_DEFAULT_MEMORY_CAP_GIB,
4582
+ maximumGuestMemoryGiBWithin(vmMemoryBudgetGiB(hostMemoryGiB))
4583
+ )
4584
+ };
4585
+ }
4586
+ function defaultCpus(hostCpus) {
4587
+ if (!Number.isFinite(hostCpus) || hostCpus < 1) return 0;
4588
+ const cores = Math.floor(hostCpus);
4589
+ return Math.min(VM_DEFAULT_CPU_CAP, vmCpuBudget(cores), Math.max(2, cores - 2));
4590
+ }
4591
+ function podsThatFit(size) {
4592
+ const memory = size.memoryGiB - GUEST_SYSTEM_RESERVE_GIB;
4593
+ const cpu = size.cpus - GUEST_SYSTEM_RESERVE_CPU;
4594
+ const fit = (preset) => Math.max(0, Math.floor(Math.min(memory / preset.memory, cpu / preset.cpu) + 1e-9));
4595
+ return { medium: fit(POD_SIZE_PRESETS.medium), small: fit(POD_SIZE_PRESETS.small) };
4596
+ }
4597
+ function smallestVmFor(pod) {
4598
+ const preset = POD_SIZE_PRESETS[pod];
4599
+ return {
4600
+ cpus: Math.ceil(preset.cpu + GUEST_SYSTEM_RESERVE_CPU),
4601
+ memoryGiB: Math.ceil(preset.memory + GUEST_SYSTEM_RESERVE_GIB)
4602
+ };
4603
+ }
4604
+ var ACHIEVEMENT_RARITIES, RISK_LEVELS, riskLevelSchema, DEFAULT_RISK_LEVELS, LEVEL_BY_VALUE, ACTIVE_WORK_STATUSES, IDENTIFIED_WORK_STATUSES, DEFAULT_TASK_STATUS_COLOR, DEFAULT_TASK_STATUS_COLOR_INT, MAX_FILE_SIZE_BYTES, MAX_FILE_TAGS, MAX_FILE_TAG_LENGTH, ALLOWED_FILE_MIME_TYPES, ALLOWED_FILE_ACCEPT, EMBED_THRESHOLD_IMAGES, EMBED_THRESHOLD_TEXT, IDLE_HEARTBEAT_MS, siteAssetExtension, RequestSiteAssetUploadRequestSchema, ListSiteAssetTargetsRequestSchema, TurnEndToolCallSchema, AgentTurnUsageSchema, KnownAgentEventSchema, AgentEventSchema, PtyChatAtSchema, PtyChatTurnUsageSchema, PtyChatTurnFileSchema, PTY_CHAT_TURN_FILES_MAX, PtyChatEventPayloadSchema, PtyChatEventRequestSchema, PtyChatAttachRequestSchema, PTY_EFFORT_LEVELS, SetPtyModelEffortRequestSchema, cardDescription, AgentHeartbeatSchema, CreatePRInputSchema, PostToChatInputSchema, GetTaskContextRequestSchema, GetChatMessagesRequestSchema, GetTaskFilesRequestSchema, GetTaskFileRequestSchema, DeleteTaskFileRequestSchema, SetTaskFileShowcaseRequestSchema, GetTaskRequestSchema, GetCliHistoryRequestSchema, ListSubtasksRequestSchema, GetDependenciesRequestSchema, GetSuggestionsRequestSchema, ListManualTestsRequestSchema, QueryManualTestsRequestSchema, CreatePullRequestRequestSchema, RequestFileUploadRequestSchema, ConfirmFileUploadRequestSchema, UpdateTaskStatusRequestSchema, StoreSessionIdRequestSchema, SetManualTestsRequestSchema, EditManualTestRequestSchema, RemoveManualTestRequestSchema, ApproveManualTestRequestSchema, RejectManualTestRequestSchema, SessionStartRequestSchema, SessionStopRequestSchema, EndReviewSessionRequestSchema, ConnectAgentRequestSchema, ReportAgentStatusRequestSchema, NotifyAgentVersionRequestSchema, DiscoveredPortSchema, ReportDiscoveredPortsRequestSchema, CreateSubtaskRequestSchema, UpdateSubtaskRequestSchema, DeleteSubtaskRequestSchema, SetSubtaskParentRequestSchema, GetTaskPropertiesRequestSchema, UpdateTaskFieldsRequestSchema, UpdateTaskPropertiesRequestSchema, ListIconsRequestSchema, GenerateTaskIconRequestSchema, SearchFaIconsRequestSchema, PickFaIconRequestSchema, CreateFollowUpTaskRequestSchema, AddDependencyRequestSchema, RemoveDependencyRequestSchema, CreateSuggestionRequestSchema, VoteSuggestionRequestSchema, TriggerIdentificationRequestSchema, HandoffToImplementerRequestSchema, SubmitCodeReviewResultRequestSchema, CycleCodingAgentKeyRequestSchema, PostChildChatMessageRequestSchema, UpdateChildStatusRequestSchema, GetAgentStatusRequestSchema, GetUiCliHistoryRequestSchema, GetActivePtySessionRequestSchema, ListActivePtySessionsRequestSchema, SendSoftStopRequestSchema, StopTaskSessionRequestSchema, FlushTaskQueueRequestSchema, CancelTaskQueuedMessageRequestSchema, FlushSingleQueuedMessageRequestSchema, AnswerAgentQuestionRequestSchema, ClearAgentTodosRequestSchema, AgentQuestionOptionSchema, AgentQuestionSchema, AskUserQuestionRequestSchema, PostAgentMessageRequestSchema, EmitAgentEventRequestSchema, RefreshGithubTokenRequestSchema, ReportCredentialFailureRequestSchema, ReportReviewSpawnFailureRequestSchema, ReportBuilderSpawnFailureRequestSchema, SpawnTaskSessionRequestSchema, StartCodeReviewRequestSchema, StopCodeReviewRequestSchema, ReportSessionSpawnFailureRequestSchema, RefreshGithubTokenResponseSchema, PTY_FRAME_MAX_CHARS, PTY_MAX_DIMENSION, PtyOutputRequestSchema, PtyEndedRequestSchema, PtyInputRequestSchema, PtyResizeRequestSchema, PtyAttachRequestSchema, ReportPtyStreamRequestSchema, ReportPtyInputRequestSchema, GetPtyStreamEndpointRequestSchema, CreatePRResponseSchema, PostToChatResponseSchema, UpdateTaskStatusResponseSchema, StoreSessionIdResponseSchema, HeartbeatResponseSchema, SessionStartResponseSchema, SessionStopResponseSchema, DeleteSubtaskResponseSchema, ParkOnCheckResultRequestSchema, GIT_BRANCH_NAME_MAX, GIT_BRANCH_NAME_MESSAGE, ALLOWED_REF, cardDescription2, ListAccessibleProjectsRequestSchema, ListProjectTasksRequestSchema, GetProjectTaskRequestSchema, SearchProjectTasksRequestSchema, ListProjectTagsRequestSchema, GetProjectTagRequestSchema, ListProjectTagAttachmentsRequestSchema, SetProjectFileTagsRequestSchema, GetProjectSummaryRequestSchema, GetProjectOnboardingStatusRequestSchema, GetProjectOnboardingStepRequestSchema, GetProjectConnectUrlsRequestSchema, conveyorCapabilitySchema, GetConnectionContextRequestSchema, VerifyConnectionRequestSchema, ListAccessibleSubprojectsRequestSchema, CreateProjectTaskRequestSchema, SetProjectTaskParentRequestSchema, UpdateProjectTaskRequestSchema, TransitionProjectTaskStatusRequestSchema, MoveProjectCardRequestSchema, PostToProjectTaskChatRequestSchema, GetProjectTaskCliRequestSchema, GetProjectTaskSessionsRequestSchema, QueryProjectGcpLogsRequestSchema, QueryProjectGrafanaLogsRequestSchema, driveFileNameSchema, DRIVE_MAX_CONTENT_CHARS, ListProjectDriveFilesRequestSchema, ReadProjectDriveFileRequestSchema, CreateProjectDriveFileRequestSchema, UpdateProjectDriveFileRequestSchema, DeleteProjectDriveFileRequestSchema, CreateProjectDriveFolderRequestSchema, StartProjectBuildRequestSchema, StopProjectBuildRequestSchema, StartProjectWorkspaceRequestSchema, StopProjectWorkspaceRequestSchema, ListMyLiveSessionsRequestSchema, ListProjectSessionGroupsRequestSchema, ListMyLiveSessionsAcrossProjectsRequestSchema, ListSessionGroupsAcrossProjectsRequestSchema, GetProjectAvailableTuisRequestSchema, StartAdhocSessionRequestSchema, StopAdhocSessionRequestSchema, ResumeAdhocSessionRequestSchema, RefreshCodingAgentKeyUsageRequestSchema, ListKeysToProbeRequestSchema, CreateProjectReleaseRequestSchema, AddTasksToProjectReleaseRequestSchema, ApproveProjectMergePRRequestSchema, ListProjectSubtasksRequestSchema, CreateProjectSubtaskRequestSchema, UpdateProjectSubtaskRequestSchema, DeleteProjectSubtaskRequestSchema, GetProjectTaskChatRequestSchema, AddProjectTaskDependencyRequestSchema, RemoveProjectTaskDependencyRequestSchema, VoteProjectSuggestionRequestSchema, GetProjectTaskDependenciesRequestSchema, CreateProjectPullRequestRequestSchema, ListProjectMembersRequestSchema, AddProjectTaskReviewerRequestSchema, RemoveProjectTaskReviewerRequestSchema, ListProjectManualTestsRequestSchema, QueryProjectManualTestsRequestSchema, SetProjectManualTestsRequestSchema, EditProjectManualTestRequestSchema, RemoveProjectManualTestRequestSchema, ApproveProjectManualTestRequestSchema, RejectProjectManualTestRequestSchema, CreateProjectSuggestionRequestSchema, ListProjectChannelsRequestSchema, READ_CHANNEL_MESSAGES_MAX_LIMIT, ReadChannelMessagesRequestSchema, POST_CHANNEL_MESSAGE_MAX_CHARS, PostChannelMessageRequestSchema, GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS, GetProjectAnalyticsSummaryRequestSchema, ListProjectTaskFilesRequestSchema, GetProjectAttachmentRequestSchema, DeleteProjectTaskFileRequestSchema, SetProjectTaskFileShowcaseRequestSchema, RequestProjectFileUploadRequestSchema, ConfirmProjectFileUploadRequestSchema, RequestWorkspaceRecycleRequestSchema, ReportApiOutageRequestSchema, CONTEXT_LINK_LOCATOR_MAX, TAG_DESCRIPTION_MAX, TAG_OVERVIEW_MAX, TAG_REASON_MAX, ProjectTagContextPathSchema, hexColor, overviewPathSchema, CreateProjectTagRequestSchema, UpdateProjectTagRequestSchema, PostToProjectChatRequestSchema, StartTagAuditRequestSchema, StartTaskAuditRequestSchema, GetActiveAuditSessionsRequestSchema, ReportTaskAuditResultRequestSchema, GetTaskAuditsRequestSchema, GetTaskAuditRequestSchema, GetTaskAuditAggregatesRequestSchema, DeleteTaskAuditRequestSchema, MarkInitialPromptSubmittedRequestSchema, PERSONAL_CLUSTER_EVENTS, PERSONAL_CLUSTER_HEARTBEAT_INTERVAL_MS, DECISION_STATUS_LABELS, DECISION_OPTION_KINDS, DECISION_VOTE_SOURCES, DECISION_OPTIONS_MIN, DECISION_OPTIONS_MAX, DECISION_OPTION_LABEL_MAX, DECISION_QUESTION_MAX, DECISION_RESOLUTION_MAX, DECISION_VOTE_REASON_MAX, DECISION_DEADLINE_MIN_HOURS, DECISION_DEADLINE_MAX_DAYS, DECISION_DEADLINE_MAX_HOURS, DECISION_LIST_MAX_LIMIT, decisionIdSchema, optionInputSchema, optionsArraySchema, CreateDecisionRequestSchema, VoteDecisionRequestSchema, RemoveDecisionVoteRequestSchema, ResolveDecisionRequestSchema, CancelDecisionRequestSchema, BulkDecisionIdsRequestSchema, ReopenDecisionRequestSchema, UpdateDecisionRequestSchema, GetDecisionRequestSchema, ListDecisionsRequestSchema, optionInput, optionsInput, deadlineHours, CreateProjectDecisionRequestSchema, ListProjectDecisionsRequestSchema, GetProjectDecisionRequestSchema, VoteProjectDecisionRequestSchema, ResolveProjectDecisionRequestSchema, UpdateProjectDecisionRequestSchema, EVALUATE_MAX_QUESTIONS, EVALUATE_MAX_ITEMS, EVALUATE_ITEM_MAX_CHARS, EVALUATE_REQUEST_MAX_CHARS, TEXT_MAX, QUESTION_KEY, EvaluateQuestionSchema, EvaluateQuestionsRequestSchema, MEETING_CHECKLIST_TITLE_MAX, MEETING_TRANSCRIPT_MAX_CHARS, MEETING_TITLE_MAX, MEETING_OCCURRED_AT_MIN_YEAR, MEETING_OCCURRED_AT_MAX_FUTURE_MS, OCCURRED_AT_RANGE_MESSAGE, MeetingOccurredAtSchema, CreateMeetingFromTranscriptRequestSchema, GetMeetingRequestSchema, UpdateMeetingRequestSchema, RegenerateMeetingSummaryRequestSchema, DeleteMeetingRequestSchema, checklistTitle, ListMeetingChecklistRequestSchema, AddMeetingChecklistItemsRequestSchema, UpdateMeetingChecklistItemRequestSchema, DeleteMeetingChecklistItemRequestSchema, SetMeetingChecklistItemCheckedRequestSchema, ListMeetingsRequestSchema, ReadMeetingTranscriptRequestSchema, MEETING_SUMMARY_MAX_CHARS, AddProjectMeetingChecklistItemsRequestSchema, CheckProjectMeetingChecklistItemRequestSchema, EditProjectMeetingChecklistItemRequestSchema, RemoveProjectMeetingChecklistItemRequestSchema, CreateProjectMeetingRequestSchema, UpdateProjectMeetingRequestSchema, TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT, AGENT_CHAT_HISTORY_FETCH_LIMIT, ANTHROPIC_CATALOG, PROJECT_ARCHIVE_RETENTION_DAYS, RETENTION_MS, CLAUDESPACE_WORKLOAD_LABEL, CLAUDESPACE_WORKLOAD_VALUE, CONVEYOR_POD_SELECTOR, CONTAINERD_K8S_NAMESPACE, CONTAINERD_PIN_LABEL, CONVEYOR_CONTENT_HASH_LABEL, VM_IMPORT_DISCOVERY_DIR, LOCAL_BAKE_ROOT_COMMANDS, PERSONAL_LOCAL_REGISTRY_NAMESPACE, PERSONAL_LOCAL_REGISTRY_NAME, PERSONAL_LOCAL_REGISTRY_PORT, PERSONAL_LOCAL_REGISTRY_HOST, PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT, PERSONAL_LOCAL_REGISTRY_PUSH_SECRET, PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY, BAKE_COMPONENT_LABEL, BAKE_COMPONENT_PREBAKE, BAKE_COMPONENT_STATE_BAKE, BAKE_COMPONENT_VALUES, PERSONAL_BAKE_NAMESPACE, MS_PER_DAY, POSTGRES_ENV, FIREBASE_EMULATOR_COMMAND, FIREBASE_EMULATOR_ENV, CATALOG, SERVICE_DEFINITIONS, SUPPORTED_CLAUDESPACE_DEPS, STATEFUL_BAKE_DEPS, MIRRORABLE_SIDECARS, LEVELS_PER_BAND, PRESTIGE_TIERS, TOP_PRESTIGE_BAND, POD_SIZE_PRESETS, VM_HOST_MEMORY_SHARE, RELAY_RSS_ALLOWANCE_MIB, BASE_RELAY_SLOTS, VM_DEFAULT_MEMORY_CAP_GIB, VM_DEFAULT_CPU_CAP, GUEST_SYSTEM_RESERVE_GIB, GUEST_SYSTEM_RESERVE_CPU, CARD_TYPE_SURFACE, surfaceTypes, BOARD_CARD_TYPES, REPORT_CARD_TYPES, DECISION_CARD_TYPES;
4523
4605
  var init_dist = __esm({
4524
4606
  "../shared/dist/index.js"() {
4525
4607
  "use strict";
4526
- init_chunk_VSUBG7GR();
4608
+ init_chunk_SFSQ7NKP();
4609
+ init_zod();
4527
4610
  init_zod();
4528
4611
  init_zod();
4529
4612
  init_zod();
@@ -4645,6 +4728,15 @@ var init_dist = __esm({
4645
4728
  EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
4646
4729
  EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
4647
4730
  IDLE_HEARTBEAT_MS = 90 * 1e3;
4731
+ siteAssetExtension = external_exports.string().regex(/^[a-z0-9]{1,10}$/);
4732
+ RequestSiteAssetUploadRequestSchema = external_exports.object({
4733
+ projectId: external_exports.string(),
4734
+ siteId: external_exports.string().min(1).max(100),
4735
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
4736
+ ext: siteAssetExtension,
4737
+ sizeBytes: external_exports.number().int().positive()
4738
+ });
4739
+ ListSiteAssetTargetsRequestSchema = external_exports.object({ projectId: external_exports.string() });
4648
4740
  TurnEndToolCallSchema = external_exports.object({
4649
4741
  tool: external_exports.string(),
4650
4742
  input: external_exports.string().optional(),
@@ -4936,7 +5028,9 @@ var init_dist = __esm({
4936
5028
  GetTaskFileRequestSchema = external_exports.object({
4937
5029
  sessionId: external_exports.string(),
4938
5030
  fileId: external_exports.string(),
4939
- taskId: external_exports.string().optional()
5031
+ taskId: external_exports.string().optional(),
5032
+ /** Ask for a short-lived `agentDownloadUrl` when the bytes are not inlined. */
5033
+ agentDownload: external_exports.boolean().optional()
4940
5034
  });
4941
5035
  DeleteTaskFileRequestSchema = external_exports.object({
4942
5036
  sessionId: external_exports.string(),
@@ -5076,10 +5170,6 @@ var init_dist = __esm({
5076
5170
  sessionId: external_exports.string(),
5077
5171
  ports: external_exports.array(DiscoveredPortSchema).max(64)
5078
5172
  });
5079
- ReportBootMilestoneRequestSchema = external_exports.object({
5080
- sessionId: external_exports.string(),
5081
- key: external_exports.string().max(64)
5082
- });
5083
5173
  CreateSubtaskRequestSchema = external_exports.object({
5084
5174
  sessionId: external_exports.string(),
5085
5175
  title: external_exports.string().min(1),
@@ -6532,6 +6622,8 @@ var init_dist = __esm({
6532
6622
  anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
6533
6623
  anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
6534
6624
  ];
6625
+ PROJECT_ARCHIVE_RETENTION_DAYS = 30;
6626
+ RETENTION_MS = PROJECT_ARCHIVE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
6535
6627
  CLAUDESPACE_WORKLOAD_LABEL = "rc-workload";
6536
6628
  CLAUDESPACE_WORKLOAD_VALUE = "claudespace";
6537
6629
  CONVEYOR_POD_SELECTOR = `${CLAUDESPACE_WORKLOAD_LABEL}=${CLAUDESPACE_WORKLOAD_VALUE}`;
@@ -6556,6 +6648,8 @@ var init_dist = __esm({
6556
6648
  PERSONAL_LOCAL_REGISTRY_PORT = 5e3;
6557
6649
  PERSONAL_LOCAL_REGISTRY_HOST = `${PERSONAL_LOCAL_REGISTRY_NAME}.${PERSONAL_LOCAL_REGISTRY_NAMESPACE}.svc.cluster.local:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6558
6650
  PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT = `http://127.0.0.1:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6651
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET = "conveyor-registry-push";
6652
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY = "config.json";
6559
6653
  BAKE_COMPONENT_LABEL = "claudespace.rallycry.com/component";
6560
6654
  BAKE_COMPONENT_PREBAKE = "prebake";
6561
6655
  BAKE_COMPONENT_STATE_BAKE = "state-bake";
@@ -6862,6 +6956,18 @@ exec firebase emulators:start --only=auth --project=rally-cry-dev`
6862
6956
  minLevel: (index + 1) * LEVELS_PER_BAND
6863
6957
  }));
6864
6958
  TOP_PRESTIGE_BAND = PRESTIGE_TIERS.length;
6959
+ POD_SIZE_PRESETS = {
6960
+ small: { cpu: 1.25, memory: 7, cpuLimit: 3 },
6961
+ medium: { cpu: 2.25, memory: 14, cpuLimit: 7 },
6962
+ large: { cpu: 2.4, memory: 15.5, cpuLimit: 9.5 }
6963
+ };
6964
+ VM_HOST_MEMORY_SHARE = 0.75;
6965
+ RELAY_RSS_ALLOWANCE_MIB = 128;
6966
+ BASE_RELAY_SLOTS = 16;
6967
+ VM_DEFAULT_MEMORY_CAP_GIB = 32;
6968
+ VM_DEFAULT_CPU_CAP = 8;
6969
+ GUEST_SYSTEM_RESERVE_GIB = 1.5;
6970
+ GUEST_SYSTEM_RESERVE_CPU = 0.25;
6865
6971
  CARD_TYPE_SURFACE = {
6866
6972
  task: "board",
6867
6973
  chat: "board",
@@ -7519,23 +7625,10 @@ var init_host_state = __esm({
7519
7625
  });
7520
7626
 
7521
7627
  // src/vm/memory-budget.ts
7522
- function relayAllowanceGiB(relaySlots = BASE_RELAY_SLOTS) {
7523
- return Math.max(0, relaySlots - BASE_RELAY_SLOTS) * RELAY_RSS_ALLOWANCE_MIB / 1024;
7524
- }
7525
- function vmHostMemoryGiB(guestGiB, relaySlots = BASE_RELAY_SLOTS) {
7526
- return guestGiB + Math.max(1, guestGiB / 4) + relayAllowanceGiB(relaySlots);
7527
- }
7528
- function maximumGuestMemoryGiBWithin(budgetGiB, relaySlots = BASE_RELAY_SLOTS) {
7529
- const guestBudget = budgetGiB - relayAllowanceGiB(relaySlots);
7530
- if (!Number.isFinite(guestBudget) || guestBudget <= 0) return 0;
7531
- return Math.max(0, Math.floor(Math.min(guestBudget - 1, guestBudget / 1.25)));
7532
- }
7533
- var RELAY_RSS_ALLOWANCE_MIB, BASE_RELAY_SLOTS;
7534
7628
  var init_memory_budget = __esm({
7535
7629
  "src/vm/memory-budget.ts"() {
7536
7630
  "use strict";
7537
- RELAY_RSS_ALLOWANCE_MIB = 128;
7538
- BASE_RELAY_SLOTS = 16;
7631
+ init_dist();
7539
7632
  }
7540
7633
  });
7541
7634
 
@@ -7543,10 +7636,10 @@ var init_memory_budget = __esm({
7543
7636
  import { cpus, freemem, totalmem } from "os";
7544
7637
  import { stringify as stringify2 } from "yaml";
7545
7638
  function cpuBudget(hostCpus) {
7546
- return Math.max(1, hostCpus - 1);
7639
+ return vmCpuBudget(hostCpus);
7547
7640
  }
7548
7641
  function memoryBudgetGiB(hostBytes) {
7549
- return hostBytes / GIB2 * 0.75;
7642
+ return vmMemoryBudgetGiB(hostBytes / GIB2);
7550
7643
  }
7551
7644
  function reservedHostMemoryGiB(allocations2) {
7552
7645
  return allocations2.reduce(
@@ -7574,7 +7667,7 @@ function validateResources(resources, host = { cpus: cpus().length, memory: tota
7574
7667
  );
7575
7668
  }
7576
7669
  const hostGiB = vmHostMemoryGiB(memoryGiB, relaySlots) + reservedHostMemoryGiB(others);
7577
- if (!Number.isInteger(memoryGiB) || memoryGiB < 2 || hostGiB * GIB2 > host.memory * 0.75) {
7670
+ if (!Number.isInteger(memoryGiB) || memoryGiB < 2 || hostGiB > memoryBudgetGiB(host.memory)) {
7578
7671
  throw new Error(
7579
7672
  `VM memory must be at least 2 GiB and leave 25% of host memory available, including QEMU and relay overhead.${sharedTotals(others, `${hostGiB.toFixed(1)} of ${memoryBudgetGiB(host.memory).toFixed(1)} GiB of host memory`)}${relayNote(relaySlots)}`
7580
7673
  );
@@ -7616,20 +7709,6 @@ function renderVmTemplate(arch, resources, dnsScript, firmware) {
7616
7709
  provision: [{ mode: "system", script: dnsScript }]
7617
7710
  });
7618
7711
  }
7619
- function parseInstallUrl(value) {
7620
- const url = new URL(value.trim());
7621
- if (url.protocol !== "https:" || url.username || url.password || url.hash || url.port) {
7622
- throw new Error(
7623
- "Enrollment requires an HTTPS install URL on port 443 without userinfo or fragments."
7624
- );
7625
- }
7626
- if (!/^\/api\/personal-clusters\/[a-z0-9]+\/install\.sh$/.test(url.pathname)) {
7627
- throw new Error("Use the install URL from Personal Compute settings.");
7628
- }
7629
- if (!url.searchParams.get("token"))
7630
- throw new Error("Install URL is missing its enrollment token.");
7631
- return url;
7632
- }
7633
7712
  function validateStartupMemory(memoryGiB, hostBytes = totalmem(), others = [], relaySlots) {
7634
7713
  const budgetGiB = memoryBudgetGiB(hostBytes) - reservedHostMemoryGiB(others);
7635
7714
  if (memoryGiB > maximumGuestMemoryGiBWithin(budgetGiB, relaySlots)) {
@@ -7642,6 +7721,7 @@ var GIB2, IMAGES;
7642
7721
  var init_config = __esm({
7643
7722
  "src/vm/config.ts"() {
7644
7723
  "use strict";
7724
+ init_dist();
7645
7725
  init_memory_budget();
7646
7726
  init_instance_name();
7647
7727
  GIB2 = 1024 ** 3;
@@ -7754,7 +7834,7 @@ var init_host_env = __esm({
7754
7834
  });
7755
7835
 
7756
7836
  // src/vm/endpoints.ts
7757
- var CONTROL_PLANE_HOSTS, DEFAULT_ENDPOINTS, GUEST_ENROLL, GUEST_CLI, TUNNEL_TCP;
7837
+ var CONTROL_PLANE_HOSTS, DEFAULT_ENDPOINTS, GUEST_CLI, TUNNEL_TCP;
7758
7838
  var init_endpoints = __esm({
7759
7839
  "src/vm/endpoints.ts"() {
7760
7840
  "use strict";
@@ -7815,15 +7895,6 @@ var init_endpoints = __esm({
7815
7895
  host,
7816
7896
  ports: host.endsWith(".ubuntu.com") || host === "deb.debian.org" ? [80, 443] : [443]
7817
7897
  }));
7818
- GUEST_ENROLL = `set -euo pipefail
7819
- IFS= read -r install_url
7820
- umask 077
7821
- installer=$(mktemp /run/conveyor-installer.XXXXXX)
7822
- trap 'rm -f "$installer"' EXIT
7823
- curl --proto '=https' --tlsv1.2 --fail --silent --show-error --max-time 120 "$install_url" -o "$installer"
7824
- unset install_url
7825
- bash "$installer"
7826
- `;
7827
7898
  GUEST_CLI = `set -euo pipefail
7828
7899
  set -a
7829
7900
  . /etc/conveyor/k3.env
@@ -8041,7 +8112,7 @@ var init_public_web = __esm({
8041
8112
  // src/vm/relay.ts
8042
8113
  import { kill } from "process";
8043
8114
  import { execFileSync } from "child_process";
8044
- import { lstatSync as lstatSync2, mkdirSync, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
8115
+ import { lstatSync as lstatSync2, mkdirSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
8045
8116
  import { join as join3 } from "path";
8046
8117
  import { Socket } from "net";
8047
8118
  function validPid2(value) {
@@ -8082,7 +8153,7 @@ function stoppedSlotRecord(root, slot) {
8082
8153
  }
8083
8154
  try {
8084
8155
  const record2 = JSON.parse(
8085
- readFileSync3(join3(path, "owner.json"), "utf8")
8156
+ readFileSync4(join3(path, "owner.json"), "utf8")
8086
8157
  );
8087
8158
  if (!validPid2(record2.pid)) throw new Error("missing process ID");
8088
8159
  return record2;
@@ -8153,7 +8224,7 @@ var init_host_packages = __esm({
8153
8224
  "firmware-x64": "ovmf",
8154
8225
  "firmware-arm64": "qemu-efi-aarch64"
8155
8226
  };
8156
- LIMA_INSTALL_HINT = "Lima 2.2+ (2.x) is not in apt: download lima-<version>-Linux-<x86_64|aarch64>.tar.gz from https://github.com/lima-vm/lima/releases and run sudo tar -C /usr/local -xzf <file> (or tar -C ~/.local -xzf <file>). The CLI searches ~/.local/bin, /usr/local/bin, and /usr/bin, not /home/linuxbrew.";
8227
+ LIMA_INSTALL_HINT = "Lima 2.2+ (2.x) is not in apt: download lima-<version>-Linux-<x86_64|aarch64>.tar.gz from https://github.com/lima-vm/lima/releases and run tar -C ~/.local -xzf <file> (preferred) or sudo tar --no-same-owner --no-same-permissions -C /usr/local -xzf <file>; a plain sudo tar hands /usr/local/bin to the uid that built the archive. The CLI searches ~/.local/bin, /usr/local/bin, and /usr/bin, not /home/linuxbrew.";
8157
8228
  }
8158
8229
  });
8159
8230
 
@@ -8248,13 +8319,25 @@ async function findFirmware(qemu, arch, host, os) {
8248
8319
  const hint = packageHint(arch === "arm64" ? "firmware-arm64" : "firmware-x64", os);
8249
8320
  throw new Error(`Install QEMU UEFI firmware before creating the VM${hint ? `: ${hint}` : "."}`);
8250
8321
  }
8251
- async function preflightHost(exec, paths, resources, host = realHost()) {
8322
+ async function warnOnForeignPrefixOwner(exec, log2) {
8323
+ const result = await exec("/usr/bin/stat", ["-c", "%u %U %n", ...ROOT_OWNED_PREFIX]).catch(
8324
+ () => void 0
8325
+ );
8326
+ const foreign = (result?.stdout ?? "").split("\n").map((line) => /^(\d+) (\S+) (\/\S*)$/.exec(line.trim())).filter((match) => !!match && match[1] !== "0").map(([, uid, name2, path]) => `${path} (uid ${uid}${name2 === "UNKNOWN" ? "" : `, ${name2}`})`);
8327
+ if (foreign.length === 0) return;
8328
+ log2(
8329
+ `Warning: ${foreign.join(", ")} is not owned by root, so that account can replace programs root runs from /usr/local/bin. A sudo tar extraction without --no-same-owner causes this. Fix it with: sudo chown root:root ${ROOT_OWNED_PREFIX.join(" ")}`
8330
+ );
8331
+ }
8332
+ async function preflightHost(exec, paths, resources, host = realHost(), log2 = () => {
8333
+ }) {
8252
8334
  if (!["darwin", "linux"].includes(host.platform) || host.uid === 0) {
8253
8335
  throw new Error("Run the VM launcher as an ordinary user on macOS or Linux, without sudo.");
8254
8336
  }
8255
8337
  const arch = vmArch(host.arch);
8256
8338
  const os = host.platform === "linux" ? await host.osRelease() : void 0;
8257
8339
  if (host.platform === "linux") await assertKvm(host);
8340
+ if (host.platform === "linux") await warnOnForeignPrefixOwner(exec, log2);
8258
8341
  const { lima, qemu, nc } = await locateTools(exec, paths, arch, host, os);
8259
8342
  await assertLimaVersion(exec, paths, lima, host);
8260
8343
  const acceleration = await runOrThrow(exec, qemu, ["-accel", "help"]);
@@ -8268,6 +8351,7 @@ async function preflightHost(exec, paths, resources, host = realHost()) {
8268
8351
  const firmware = await findFirmware(qemu, arch, host, os);
8269
8352
  return { arch, lima, qemu, nc, firmware };
8270
8353
  }
8354
+ var ROOT_OWNED_PREFIX;
8271
8355
  var init_preflight = __esm({
8272
8356
  "src/vm/preflight.ts"() {
8273
8357
  "use strict";
@@ -8275,16 +8359,17 @@ var init_preflight = __esm({
8275
8359
  init_config();
8276
8360
  init_host_env();
8277
8361
  init_host_packages();
8362
+ ROOT_OWNED_PREFIX = ["/usr/local", "/usr/local/bin", "/usr/local/share"];
8278
8363
  }
8279
8364
  });
8280
8365
 
8281
8366
  // src/vm/host-runtime.ts
8282
8367
  import { readFile as readFile5 } from "fs/promises";
8283
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
8368
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
8284
8369
  import { join as join5 } from "path";
8285
8370
  import { fileURLToPath } from "url";
8286
8371
  function hashText(value) {
8287
- return createHash2("sha256").update(value).digest("hex");
8372
+ return createHash4("sha256").update(value).digest("hex");
8288
8373
  }
8289
8374
  function newVmRunId() {
8290
8375
  return randomUUID2();
@@ -8824,7 +8909,6 @@ __export(manager_exports, {
8824
8909
  configureVm: () => configureVm,
8825
8910
  createVm: () => createVm,
8826
8911
  denyEndpoint: () => denyEndpoint,
8827
- enrollVm: () => enrollVm,
8828
8912
  enrollVmBootstrap: () => enrollVmBootstrap,
8829
8913
  ensureVmDevicePublicKey: () => ensureVmDevicePublicKey,
8830
8914
  internetMode: () => internetMode,
@@ -8850,16 +8934,15 @@ async function saveState(paths, state) {
8850
8934
  await atomicPrivateWrite(paths.state, `${JSON.stringify(state, null, 2)}
8851
8935
  `);
8852
8936
  }
8853
- async function createVm(ports, resources, installUrl) {
8937
+ async function createVm(ports, resources) {
8854
8938
  validateResources(resources, void 0, await ports.otherAllocations?.() ?? []);
8855
8939
  const { paths, exec, log: log2 } = ports;
8856
- const runtime = await preflightHost(exec, paths, resources);
8940
+ const runtime = await preflightHost(exec, paths, resources, void 0, log2);
8857
8941
  const endpoints = [
8858
8942
  ...DEFAULT_ENDPOINTS,
8859
8943
  { host: "region1.v2.argotunnel.com", ports: [7844] },
8860
8944
  { host: "region2.v2.argotunnel.com", ports: [7844] }
8861
8945
  ];
8862
- if (installUrl) endpoints.push({ host: parseInstallUrl(installUrl).hostname, ports: [443] });
8863
8946
  const network = withHostSettings(await resolveNetwork(endpoints), {});
8864
8947
  const template = renderVmTemplate(
8865
8948
  runtime.arch,
@@ -8978,29 +9061,6 @@ async function beginEnrollment(ports, state) {
8978
9061
  delete state.linkedDisplayName;
8979
9062
  await saveState(ports.paths, state);
8980
9063
  }
8981
- async function enrollVm(ports, state, input) {
8982
- const url = parseInstallUrl(input);
8983
- if (!state.endpoints.some(
8984
- (endpoint) => endpoint.host === url.hostname && endpoint.ports.includes(443)
8985
- )) {
8986
- throw new Error(
8987
- `Approve ${url.hostname}:443 with vm allow while the VM is stopped, then retry.`
8988
- );
8989
- }
8990
- await beginEnrollment(ports, state);
8991
- ports.log("Preparing guest prerequisites and enrolling inside the VM.");
8992
- await guestCommand(
8993
- ports.exec,
8994
- state,
8995
- ports.paths,
8996
- "set -e; export DEBIAN_FRONTEND=noninteractive; apt-get update -qq; apt-get install -y -qq curl unzip ca-certificates"
8997
- );
8998
- await guestCommand(ports.exec, state, ports.paths, GUEST_ENROLL, `${url.href}
8999
- `);
9000
- state.enrolled = true;
9001
- await saveState(ports.paths, state);
9002
- ports.log("Guest enrollment completed. Check Personal Compute for cluster health.");
9003
- }
9004
9064
  async function enrollVmBootstrap(ports, state, bootstrapScript) {
9005
9065
  if (bootstrapScript.length === 0 || bootstrapScript.length > 1e6)
9006
9066
  throw new Error("Pairing bootstrap payload is invalid.");
@@ -9556,7 +9616,7 @@ done
9556
9616
  });
9557
9617
 
9558
9618
  // src/vm/dev-artifact.ts
9559
- import { createHash as createHash3 } from "crypto";
9619
+ import { createHash as createHash5 } from "crypto";
9560
9620
  import { readFile as readFile7, stat } from "fs/promises";
9561
9621
  async function readDevArtifact() {
9562
9622
  const file = process.env.CONVEYOR_DEV_ARTIFACT;
@@ -9570,7 +9630,7 @@ async function readDevArtifact() {
9570
9630
  if (!info.isFile() || info.size > 32 * 1024 * 1024)
9571
9631
  throw new Error("Development artifact must be a file of at most 32 MiB.");
9572
9632
  const bytes = await readFile7(file);
9573
- if (bytes.length > 32 * 1024 * 1024 || createHash3("sha256").update(bytes).digest("hex") !== digest)
9633
+ if (bytes.length > 32 * 1024 * 1024 || createHash5("sha256").update(bytes).digest("hex") !== digest)
9574
9634
  throw new Error("Development artifact checksum mismatch.");
9575
9635
  return { bytes, sha256: digest };
9576
9636
  }
@@ -10665,14 +10725,6 @@ function commandPorts(exec, instance, log2, layout) {
10665
10725
  otherAllocations: async () => (await readOtherAllocations(paths, layout)).map((other) => other.resources)
10666
10726
  };
10667
10727
  }
10668
- async function readInstallInput() {
10669
- let input = "";
10670
- for await (const chunk of process.stdin) {
10671
- input += String(chunk);
10672
- if (input.length > 16384) throw new Error("Install URL exceeds the input limit.");
10673
- }
10674
- return input.trim();
10675
- }
10676
10728
  function resourceFlag(flags, key, fallback) {
10677
10729
  if (flags[key] === void 0) return fallback;
10678
10730
  if (typeof flags[key] !== "string" || !/^\d+$/.test(flags[key]))
@@ -10693,25 +10745,47 @@ function setupDefaults(others, host = hostCapacity()) {
10693
10745
  `Your other VMs already reserve this computer's CPU or memory budget: ${reservations}. Stop one and lower it with vm configure --instance=<name> --cpus=<n> --memory=<GiB> or --profile=default, then retry.`
10694
10746
  );
10695
10747
  }
10748
+ const wanted = recommendedVmSize(host.memory / GIB2, host.cpus);
10696
10749
  return {
10697
- cpus: Math.min(4, room.cpus),
10698
- memoryGiB: Math.min(8, Math.floor(host.memory / GIB2 / 2), room.memoryGiB),
10750
+ cpus: Math.min(wanted.cpus, room.cpus),
10751
+ memoryGiB: Math.min(wanted.memoryGiB, room.memoryGiB),
10699
10752
  diskGiB: 64
10700
10753
  };
10701
10754
  }
10702
- async function createInstance(ports, parsed, url) {
10755
+ function setupResources(flags, defaults) {
10756
+ return {
10757
+ cpus: resourceFlag(flags, "cpus", defaults.cpus),
10758
+ memoryGiB: resourceFlag(flags, "memory", defaults.memoryGiB),
10759
+ diskGiB: resourceFlag(flags, "disk", defaults.diskGiB)
10760
+ };
10761
+ }
10762
+ function setupSizeNotice(resources, sizedToHost) {
10763
+ const fit = podsThatFit(resources);
10764
+ const pods = `${fit.medium} medium or ${fit.small} small pod${fit.small === 1 ? "" : "s"}`;
10765
+ const vm = `${resources.memoryGiB} GiB, ${resources.cpus}-CPU VM`;
10766
+ const lines2 = [
10767
+ sizedToHost ? `Creating a ${vm} sized to this computer. It fits ${pods} at a time. Pass --memory and --cpus to choose another size.` : `Creating a ${vm}. It fits ${pods} at a time.`
10768
+ ];
10769
+ if (fit.small === 0) {
10770
+ const smallest = smallestVmFor("small");
10771
+ lines2.push(
10772
+ `No Conveyor pod fits in this VM, so builds skip it and use project compute. A small pod needs a VM with at least ${smallest.memoryGiB} GiB and ${smallest.cpus} CPUs.`
10773
+ );
10774
+ } else if (fit.medium === 0) {
10775
+ lines2.push(
10776
+ "Only small pods fit in this VM. Builds that need a medium pod use project compute."
10777
+ );
10778
+ }
10779
+ return lines2;
10780
+ }
10781
+ async function createInstance(ports, parsed) {
10703
10782
  if (ports.paths.instance !== DEFAULT_INSTANCE) assertInstancePathFits(ports.paths);
10704
10783
  return await withAdmissionLock(async (release) => {
10705
10784
  const defaults = setupDefaults(await readOtherAllocations(ports.paths, ports.layout));
10706
- return await createVm(
10707
- { ...ports, reserved: release },
10708
- {
10709
- cpus: resourceFlag(parsed.flags, "cpus", defaults.cpus),
10710
- memoryGiB: resourceFlag(parsed.flags, "memory", defaults.memoryGiB),
10711
- diskGiB: resourceFlag(parsed.flags, "disk", defaults.diskGiB)
10712
- },
10713
- url
10714
- );
10785
+ const resources = setupResources(parsed.flags, defaults);
10786
+ const sizedToHost = parsed.flags.cpus === void 0 && parsed.flags.memory === void 0;
10787
+ for (const line of setupSizeNotice(resources, sizedToHost)) ports.log(line);
10788
+ return await createVm({ ...ports, reserved: release }, resources);
10715
10789
  }, ports.layout.base);
10716
10790
  }
10717
10791
  async function configureWithAdmission(ports, state, parsed) {
@@ -10745,17 +10819,12 @@ function relinkNotice(state, relink) {
10745
10819
  );
10746
10820
  }
10747
10821
  async function setupVm(ports, parsed, state) {
10748
- const url = parsed.flags["install-url-stdin"] === true ? await readInstallInput() : void 0;
10749
- if (url !== void 0) parseInstallUrl(url);
10750
10822
  if (state && RESOURCE_FLAGS.some((key) => parsed.flags[key] !== void 0)) {
10751
10823
  throw new Error("This VM already exists; setup resource flags only apply at creation.");
10752
10824
  }
10753
- const notice = state && url ? relinkNotice(state, parsed.flags.relink === true) : void 0;
10754
- if (notice) ports.log(notice);
10755
- if (!state) state = await createInstance(ports, parsed, url);
10825
+ if (!state) state = await createInstance(ports, parsed);
10756
10826
  await startVm(ports, state);
10757
- if (url) await enrollVm(ports, state, url);
10758
- else ports.log(setupNextStep(state, ports.paths.instance));
10827
+ ports.log(setupNextStep(state, ports.paths.instance));
10759
10828
  }
10760
10829
  function setupNextStep(state, instance) {
10761
10830
  const link = instance === DEFAULT_INSTANCE ? "conveyor-k3 vm link" : `conveyor-k3 vm link --instance=${instance}`;
@@ -10859,6 +10928,7 @@ var RESOURCE_FLAGS;
10859
10928
  var init_command_setup = __esm({
10860
10929
  "src/vm/command-setup.ts"() {
10861
10930
  "use strict";
10931
+ init_dist();
10862
10932
  init_config();
10863
10933
  init_dev_artifact();
10864
10934
  init_device_link();
@@ -10932,7 +11002,7 @@ var init_service_unit = __esm({
10932
11002
  });
10933
11003
 
10934
11004
  // src/vm/service-control.ts
10935
- import { constants as constants2, readFileSync as readFileSync4 } from "fs";
11005
+ import { constants as constants2, readFileSync as readFileSync5 } from "fs";
10936
11006
  import { access as access5, lstat as lstat3, mkdir as mkdir6, readFile as readFile8, readdir as readdir2, rename as rename3, rm as rm7, writeFile as writeFile6 } from "fs/promises";
10937
11007
  import { tmpdir as tmpdir2, userInfo as userInfo3 } from "os";
10938
11008
  import { dirname as dirname3, join as join12 } from "path";
@@ -10966,7 +11036,7 @@ function realServiceHost(exec, log2) {
10966
11036
  function cliVersion() {
10967
11037
  try {
10968
11038
  const url = new URL("../package.json", import.meta.url);
10969
- return JSON.parse(readFileSync4(url, "utf8")).version ?? "<version>";
11039
+ return JSON.parse(readFileSync5(url, "utf8")).version ?? "<version>";
10970
11040
  } catch {
10971
11041
  return "<version>";
10972
11042
  }
@@ -11616,7 +11686,7 @@ function validateFlags(parsed) {
11616
11686
  const action = actionOf(parsed);
11617
11687
  const allowed = {
11618
11688
  ...BAKE_VERBS,
11619
- setup: ["install-url-stdin", "cpus", "memory", "disk", "relink"],
11689
+ setup: ["cpus", "memory", "disk"],
11620
11690
  start: [],
11621
11691
  configure: [...RESOURCE_FLAGS2, ...CAPACITY_FLAGS],
11622
11692
  stop: ["force"],
@@ -11640,7 +11710,7 @@ function validateFlags(parsed) {
11640
11710
  for (const flag of Object.keys(parsed.flags)) {
11641
11711
  if (!accepted.includes(flag)) throw new Error(`Unknown flag for vm ${action}: --${flag}`);
11642
11712
  }
11643
- const booleanFlags = /* @__PURE__ */ new Set(["install-url-stdin", "force", "json", "purge", "yes", "relink"]);
11713
+ const booleanFlags = /* @__PURE__ */ new Set(["force", "json", "purge", "yes", "relink"]);
11644
11714
  for (const [flag, value] of Object.entries(parsed.flags)) {
11645
11715
  if (booleanFlags.has(flag) && value !== true) throw new Error(`Use --${flag} without a value.`);
11646
11716
  }
@@ -11761,7 +11831,8 @@ var init_command = __esm({
11761
11831
  Usage: conveyor-k3 vm <setup|start|stop|status|list|link|internet|logout|pods|pod-logs|disk-usage|configure|update|allow|deny|refresh|uninstall|image import|runners grant>
11762
11832
  Every command but list acts on one VM: --instance=<name> selects a named VM,
11763
11833
  and omitting it selects this user's default VM.
11764
- setup [--install-url-stdin] [--relink] [--cpus=4] [--memory=8] [--disk=64]
11834
+ setup [--cpus=<n>] [--memory=<GiB>] [--disk=64] Without --cpus and --memory, sizes the VM
11835
+ to this computer: the largest the budget allows, up to 32 GiB and 8 CPUs.
11765
11836
  configure --cpus=4 --memory=8 [--disk=64] Changes allocation while stopped.
11766
11837
  configure --profile=default|server [--relay-slots=96] [--relay-max-gib=8]
11767
11838
  [--disk-mbps=1000] [--disk-iops=20000] Changes network and disk capacity while stopped.
@@ -11795,6 +11866,7 @@ Run without sudo. Requires Lima 2.2+ and QEMU with HVF (macOS) or KVM (Linux).
11795
11866
  var command_exports2 = {};
11796
11867
  __export(command_exports2, {
11797
11868
  HOST_HELP: () => HOST_HELP,
11869
+ hostNextStep: () => hostNextStep,
11798
11870
  runHostCommand: () => runHostCommand
11799
11871
  });
11800
11872
  import { hostname } from "os";
@@ -11848,6 +11920,11 @@ async function prepareHostLink(parsed, ports, log2) {
11848
11920
  }
11849
11921
  return { api, name: name2 };
11850
11922
  }
11923
+ function hostNextStep(state) {
11924
+ if (state.k3sInstalledByConveyor === true)
11925
+ return "Next: in Conveyor open this instance, set Keep for this machine (CPU and memory), max concurrent and projects, and Save. Conveyor restarts k3s once to apply them, while no build runs.";
11926
+ return "Next: in Conveyor open this instance, set Keep for this machine (CPU and memory), max concurrent and projects, Save, then run `sudo systemctl restart k3s` once while no build runs. Conveyor never restarts a k3s it did not install.";
11927
+ }
11851
11928
  async function completeHostLink(api, pairing, log2, ports) {
11852
11929
  const poll = () => pollPairing(api, pairing.deviceCode);
11853
11930
  const retry = () => log2("Securing connection\u2026 retrying pairing status.");
@@ -11880,6 +11957,7 @@ async function completeHostLink(api, pairing, log2, ports) {
11880
11957
  log2(
11881
11958
  `Linked to ${completed.accountLabel ?? "Conveyor"}${completed.displayName ? ` as ${completed.displayName}` : ""}.`
11882
11959
  );
11960
+ log2(hostNextStep(await ports.readState()));
11883
11961
  }
11884
11962
  async function hostLink(parsed, log2, ports) {
11885
11963
  const { api, name: name2 } = await prepareHostLink(parsed, ports, log2);
@@ -11944,8 +12022,10 @@ Usage: conveyor-k3 host <setup|link|status|uninstall>
11944
12022
  uninstall [--purge-k3s --yes] Remove Conveyor's units, state, and device key. k3s,
11945
12023
  your config, and your workloads stay unless you purge.
11946
12024
  Run with sudo on a dedicated Linux server. Builds run on this server's own k3s with
11947
- no VM boundary; Pod Security and an egress policy stand in for one. See the guide's
11948
- "Link a dedicated server (host)" section.
12025
+ no VM boundary; Pod Security and an egress policy stand in for one. After link, on a
12026
+ k3s you installed yourself: set Keep for this machine in Conveyor and Save, then run
12027
+ sudo systemctl restart k3s once. Conveyor never restarts a k3s it did not install.
12028
+ See the guide's "Link a dedicated server (host)" section.
11949
12029
  `;
11950
12030
  ALLOWED_FLAGS = /* @__PURE__ */ new Map([
11951
12031
  ["setup", []],
@@ -12026,7 +12106,7 @@ async function writeWorkloads(data) {
12026
12106
 
12027
12107
  // src/cli.ts
12028
12108
  init_exec();
12029
- import { readFileSync as readFileSync5 } from "fs";
12109
+ import { readFileSync as readFileSync6 } from "fs";
12030
12110
 
12031
12111
  // src/lib/fs-port.ts
12032
12112
  import { chmod, mkdir as mkdir2, readFile, rm as rm2, writeFile as writeFile2 } from "fs/promises";
@@ -12040,7 +12120,7 @@ var nodeFileSystem = {
12040
12120
  },
12041
12121
  async writeFile(path, content, mode) {
12042
12122
  await mkdir2(path.slice(0, path.lastIndexOf("/")), { recursive: true });
12043
- await writeFile2(path, content, "utf8");
12123
+ await writeFile2(path, content, { encoding: "utf8", ...mode === void 0 ? {} : { mode } });
12044
12124
  if (mode !== void 0) await chmod(path, mode);
12045
12125
  },
12046
12126
  async removeFile(path) {
@@ -12091,11 +12171,235 @@ init_dist();
12091
12171
  // src/lib/local-registry.ts
12092
12172
  init_dist();
12093
12173
  init_paths();
12174
+ import { createHash as createHash2 } from "crypto";
12175
+
12176
+ // src/lib/registry-credentials.ts
12177
+ import { createHash, randomBytes } from "crypto";
12178
+
12179
+ // src/lib/bcrypt.ts
12180
+ var P_WORDS = 18;
12181
+ var S_WORDS = 1024;
12182
+ var MAGIC = "OrpheanBeholderScryDoubt";
12183
+ var ALPHABET = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
12184
+ var MAX_KEY_BYTES = 72;
12185
+ var BCRYPT_SALT_BYTES = 16;
12186
+ function scaledAtanInverse(x, one) {
12187
+ const xSquared = x * x;
12188
+ let power = one / x;
12189
+ let sum = 0n;
12190
+ for (let n = 0n; power !== 0n; n++) {
12191
+ const term = power / (2n * n + 1n);
12192
+ sum = n % 2n === 0n ? sum + term : sum - term;
12193
+ power /= xSquared;
12194
+ }
12195
+ return sum;
12196
+ }
12197
+ function piFractionWords(count) {
12198
+ const guard = 64n;
12199
+ const bits = BigInt(count * 32) + guard;
12200
+ const one = 1n << bits;
12201
+ const pi = 16n * scaledAtanInverse(5n, one) - 4n * scaledAtanInverse(239n, one);
12202
+ const fraction = pi - 3n * one >> guard;
12203
+ const words = new Uint32Array(count);
12204
+ for (let index = 0; index < count; index++) {
12205
+ words[index] = Number(fraction >> BigInt(32 * (count - 1 - index)) & 0xffffffffn);
12206
+ }
12207
+ return words;
12208
+ }
12209
+ var initialState = null;
12210
+ function blowfishInitialState() {
12211
+ if (!initialState) {
12212
+ const words = piFractionWords(P_WORDS + S_WORDS);
12213
+ initialState = { p: words.slice(0, P_WORDS), s: words.slice(P_WORDS) };
12214
+ }
12215
+ return initialState;
12216
+ }
12217
+ function word(array, index) {
12218
+ return array[index] ?? 0;
12219
+ }
12220
+ function round(s, x) {
12221
+ const mixed = word(s, x >>> 24) + word(s, 256 + (x >>> 16 & 255)) ^ word(s, 512 + (x >>> 8 & 255));
12222
+ return mixed + word(s, 768 + (x & 255)) | 0;
12223
+ }
12224
+ function encipher(state, data, offset) {
12225
+ const { p, s } = state;
12226
+ let left = word(data, offset) ^ word(p, 0);
12227
+ let right = word(data, offset + 1);
12228
+ for (let index = 1; index <= 16; index += 2) {
12229
+ right ^= round(s, left) ^ word(p, index);
12230
+ left ^= round(s, right) ^ word(p, index + 1);
12231
+ }
12232
+ data[offset] = right ^ word(p, 17);
12233
+ data[offset + 1] = left;
12234
+ }
12235
+ function wordStream(bytes) {
12236
+ let position = 0;
12237
+ return () => {
12238
+ let value = 0;
12239
+ for (let index = 0; index < 4; index++) {
12240
+ value = value << 8 | (bytes[position] ?? 0);
12241
+ position = (position + 1) % bytes.length;
12242
+ }
12243
+ return value >>> 0;
12244
+ };
12245
+ }
12246
+ function expandKey(state, key, salt) {
12247
+ const nextKeyWord = wordStream(key);
12248
+ for (let index = 0; index < P_WORDS; index++) {
12249
+ state.p[index] = word(state.p, index) ^ nextKeyWord();
12250
+ }
12251
+ const nextSaltWord = salt ? wordStream(salt) : () => 0;
12252
+ const block = new Uint32Array(2);
12253
+ const fill = (target2, length) => {
12254
+ for (let index = 0; index < length; index += 2) {
12255
+ block[0] = word(block, 0) ^ nextSaltWord();
12256
+ block[1] = word(block, 1) ^ nextSaltWord();
12257
+ encipher(state, block, 0);
12258
+ target2[index] = word(block, 0);
12259
+ target2[index + 1] = word(block, 1);
12260
+ }
12261
+ };
12262
+ fill(state.p, P_WORDS);
12263
+ fill(state.s, S_WORDS);
12264
+ }
12265
+ function encodeBase64(bytes, length) {
12266
+ let out = "";
12267
+ let offset = 0;
12268
+ while (offset < length) {
12269
+ let first = bytes[offset++] ?? 0;
12270
+ out += ALPHABET[first >> 2];
12271
+ first = (first & 3) << 4;
12272
+ if (offset >= length) {
12273
+ out += ALPHABET[first];
12274
+ break;
12275
+ }
12276
+ let second = bytes[offset++] ?? 0;
12277
+ out += ALPHABET[first | second >> 4];
12278
+ first = (second & 15) << 2;
12279
+ if (offset >= length) {
12280
+ out += ALPHABET[first];
12281
+ break;
12282
+ }
12283
+ second = bytes[offset++] ?? 0;
12284
+ out += ALPHABET[first | second >> 6];
12285
+ out += ALPHABET[second & 63];
12286
+ }
12287
+ return out;
12288
+ }
12289
+ function bcryptHash(password, cost, salt) {
12290
+ if (!Number.isInteger(cost) || cost < 4 || cost > 31) {
12291
+ throw new Error(`bcrypt cost must be an integer from 4 to 31, not ${cost}`);
12292
+ }
12293
+ if (salt.length !== BCRYPT_SALT_BYTES) {
12294
+ throw new Error(`bcrypt needs a ${BCRYPT_SALT_BYTES}-byte salt`);
12295
+ }
12296
+ const passwordBytes = new TextEncoder().encode(password).slice(0, MAX_KEY_BYTES);
12297
+ const key = new Uint8Array(passwordBytes.length + 1);
12298
+ key.set(passwordBytes);
12299
+ const base = blowfishInitialState();
12300
+ const state = { p: base.p.slice(), s: base.s.slice() };
12301
+ expandKey(state, key, salt);
12302
+ for (let rounds = 2 ** cost; rounds > 0; rounds--) {
12303
+ expandKey(state, key, null);
12304
+ expandKey(state, salt, null);
12305
+ }
12306
+ const magic = new TextEncoder().encode(MAGIC);
12307
+ const nextMagicWord = wordStream(magic);
12308
+ const block = Uint32Array.from({ length: 6 }, () => nextMagicWord());
12309
+ for (let pass = 0; pass < 64; pass++) {
12310
+ for (let offset = 0; offset < 6; offset += 2) encipher(state, block, offset);
12311
+ }
12312
+ const digest = new Uint8Array(24);
12313
+ block.forEach((value, index) => {
12314
+ digest[index * 4] = value >>> 24;
12315
+ digest[index * 4 + 1] = value >>> 16 & 255;
12316
+ digest[index * 4 + 2] = value >>> 8 & 255;
12317
+ digest[index * 4 + 3] = value & 255;
12318
+ });
12319
+ const costText = String(cost).padStart(2, "0");
12320
+ return `$2y$${costText}$${encodeBase64(salt, BCRYPT_SALT_BYTES)}${encodeBase64(digest, 23)}`;
12321
+ }
12322
+
12323
+ // src/lib/registry-credentials.ts
12324
+ init_paths();
12325
+ var LOCAL_REGISTRY_USERNAME = "conveyor";
12326
+ var BCRYPT_COST = 5;
12327
+ var USERNAME = /^[a-z][a-z0-9-]{0,31}$/;
12328
+ var PASSWORD = /^[A-Za-z0-9_-]{32,128}$/;
12329
+ function generateRegistryCredentials(random = randomBytes) {
12330
+ const username = LOCAL_REGISTRY_USERNAME;
12331
+ const password = Buffer.from(random(32)).toString("base64url");
12332
+ const hash = bcryptHash(password, BCRYPT_COST, random(BCRYPT_SALT_BYTES));
12333
+ return { username, password, htpasswd: `${username}:${hash}` };
12334
+ }
12335
+ function parseRegistryCredentials(raw) {
12336
+ if (!raw) return null;
12337
+ try {
12338
+ const value = JSON.parse(raw);
12339
+ const { username, password, htpasswd } = value ?? {};
12340
+ if (typeof username !== "string" || !USERNAME.test(username)) return null;
12341
+ if (typeof password !== "string" || !PASSWORD.test(password)) return null;
12342
+ if (typeof htpasswd !== "string" || !htpasswd.startsWith(`${username}:$2`)) return null;
12343
+ if (htpasswd.includes("\n")) return null;
12344
+ return { username, password, htpasswd };
12345
+ } catch {
12346
+ return null;
12347
+ }
12348
+ }
12349
+ async function loadOrCreateRegistryCredentials(fs2, random) {
12350
+ const stored = parseRegistryCredentials(await fs2.readFile(LOCAL_REGISTRY_CREDENTIALS_FILE));
12351
+ if (stored) return stored;
12352
+ const created = generateRegistryCredentials(random);
12353
+ await fs2.writeFile(LOCAL_REGISTRY_CREDENTIALS_FILE, `${JSON.stringify(created)}
12354
+ `, 384);
12355
+ return created;
12356
+ }
12357
+ function credentialFingerprint(credentials) {
12358
+ return createHash("sha256").update(`${credentials.username}:${credentials.password}`).digest("hex").slice(0, 16);
12359
+ }
12360
+ function basicAuthorization(credentials) {
12361
+ return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64")}`;
12362
+ }
12363
+ function registryDockerConfig(credentials, host) {
12364
+ const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64");
12365
+ return JSON.stringify({ auths: { [host]: { auth } } });
12366
+ }
12367
+ function parseRegistryAuthState(raw) {
12368
+ if (!raw) return null;
12369
+ try {
12370
+ const value = JSON.parse(raw);
12371
+ if (typeof value?.credential !== "string") return null;
12372
+ const writtenAt = typeof value.writtenAt === "string" && !Number.isNaN(Date.parse(value.writtenAt)) ? value.writtenAt : null;
12373
+ return { credential: value.credential, writtenAt, enforced: value.enforced === true };
12374
+ } catch {
12375
+ return null;
12376
+ }
12377
+ }
12378
+ async function registryAuthEnforced(readFile9) {
12379
+ return parseRegistryAuthState(await readFile9(LOCAL_REGISTRY_AUTH_STATE_FILE))?.enforced === true;
12380
+ }
12381
+ function withRegistryCredentials(fetchImpl, readFile9) {
12382
+ let credentials = null;
12383
+ return async (url, init) => {
12384
+ credentials ??= readFile9(LOCAL_REGISTRY_CREDENTIALS_FILE).then(parseRegistryCredentials);
12385
+ const stored = await credentials;
12386
+ if (!stored) return fetchImpl(url, init);
12387
+ const headers = new Headers(init?.headers);
12388
+ headers.set("Authorization", basicAuthorization(stored));
12389
+ return fetchImpl(url, { ...init, headers });
12390
+ };
12391
+ }
12392
+
12393
+ // src/lib/local-registry.ts
12094
12394
  var LOCAL_REGISTRY_IMAGE = "docker.io/library/registry:3.1.2@sha256:c87f33837722a100572e95d7dc4bf539fc42cf68202b13c3bc03c0ff54c3a649";
12095
12395
  var LOCAL_REGISTRY_CONFIG = "/etc/distribution/config.yml";
12096
12396
  var LOCAL_REGISTRY_GC_JOB = "conveyor-registry-gc";
12397
+ var LOCAL_REGISTRY_HTPASSWD_SECRET = "conveyor-registry-htpasswd";
12398
+ var HTPASSWD_KEY = "htpasswd";
12399
+ var AUTH_MOUNT = "/etc/distribution/auth";
12097
12400
  var LOCAL_REGISTRY_SELECTOR = { "app.kubernetes.io/name": PERSONAL_LOCAL_REGISTRY_NAME };
12098
12401
  var MANAGED_BY = { "app.kubernetes.io/managed-by": "conveyor-k3" };
12402
+ var AUTH_CHECKSUM_ANNOTATION = "conveyor.rallycry.com/registry-auth";
12099
12403
  var DATA_MOUNT = "/var/lib/registry";
12100
12404
  var REGISTRY_ENV = [
12101
12405
  { name: "REGISTRY_STORAGE_DELETE_ENABLED", value: "true" },
@@ -12110,6 +12414,23 @@ var VOLUME_MOUNTS = [
12110
12414
  { name: "data", mountPath: DATA_MOUNT },
12111
12415
  { name: "tmp", mountPath: "/tmp" }
12112
12416
  ];
12417
+ var AUTH_ENV = [
12418
+ { name: "REGISTRY_AUTH", value: "htpasswd" },
12419
+ { name: "REGISTRY_AUTH_HTPASSWD_REALM", value: "conveyor" },
12420
+ { name: "REGISTRY_AUTH_HTPASSWD_PATH", value: `${AUTH_MOUNT}/${HTPASSWD_KEY}` }
12421
+ ];
12422
+ var AUTH_VOLUME = {
12423
+ name: "auth",
12424
+ secret: {
12425
+ secretName: LOCAL_REGISTRY_HTPASSWD_SECRET,
12426
+ items: [{ key: HTPASSWD_KEY, path: HTPASSWD_KEY }],
12427
+ defaultMode: 256
12428
+ }
12429
+ };
12430
+ var AUTH_VOLUME_MOUNT = { name: "auth", mountPath: AUTH_MOUNT, readOnly: true };
12431
+ function htpasswdChecksum(htpasswd) {
12432
+ return createHash2("sha256").update(htpasswd).digest("hex").slice(0, 16);
12433
+ }
12113
12434
  var VOLUMES = [
12114
12435
  { name: "data", hostPath: { path: LOCAL_REGISTRY_DATA_DIR, type: "Directory" } },
12115
12436
  { name: "tmp", emptyDir: { sizeLimit: "64Mi" } }
@@ -12146,7 +12467,34 @@ function registryNetworkPolicy(metadata) {
12146
12467
  }
12147
12468
  };
12148
12469
  }
12149
- function renderLocalRegistryManifests() {
12470
+ function renderLocalRegistrySecrets(credentials) {
12471
+ const encode = (value) => Buffer.from(value).toString("base64");
12472
+ const secret = (namespace, name2, key, value) => ({
12473
+ apiVersion: "v1",
12474
+ kind: "Secret",
12475
+ metadata: { name: name2, namespace, labels: { ...MANAGED_BY } },
12476
+ type: "Opaque",
12477
+ data: { [key]: encode(value) }
12478
+ });
12479
+ const items = [
12480
+ secret(
12481
+ PERSONAL_LOCAL_REGISTRY_NAMESPACE,
12482
+ LOCAL_REGISTRY_HTPASSWD_SECRET,
12483
+ HTPASSWD_KEY,
12484
+ `${credentials.htpasswd}
12485
+ `
12486
+ ),
12487
+ secret(
12488
+ PERSONAL_BAKE_NAMESPACE,
12489
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET,
12490
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY,
12491
+ registryDockerConfig(credentials, PERSONAL_LOCAL_REGISTRY_HOST)
12492
+ )
12493
+ ];
12494
+ return `${JSON.stringify({ apiVersion: "v1", kind: "List", items }, null, 2)}
12495
+ `;
12496
+ }
12497
+ function renderLocalRegistryManifests(auth = null) {
12150
12498
  const metadata = {
12151
12499
  name: PERSONAL_LOCAL_REGISTRY_NAME,
12152
12500
  namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE,
@@ -12162,15 +12510,19 @@ function renderLocalRegistryManifests() {
12162
12510
  strategy: { type: "Recreate" },
12163
12511
  selector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
12164
12512
  template: {
12165
- metadata: { labels: metadata.labels },
12513
+ metadata: {
12514
+ labels: metadata.labels,
12515
+ ...auth ? { annotations: { [AUTH_CHECKSUM_ANNOTATION]: htpasswdChecksum(auth.htpasswd) } } : {}
12516
+ },
12166
12517
  spec: {
12167
12518
  ...POD_BASE,
12519
+ ...auth ? { volumes: [...VOLUMES, AUTH_VOLUME] } : {},
12168
12520
  containers: [
12169
12521
  {
12170
12522
  name: "registry",
12171
12523
  image: LOCAL_REGISTRY_IMAGE,
12172
12524
  imagePullPolicy: "IfNotPresent",
12173
- env: REGISTRY_ENV,
12525
+ env: auth ? [...REGISTRY_ENV, ...AUTH_ENV] : REGISTRY_ENV,
12174
12526
  ports: [
12175
12527
  {
12176
12528
  name: "registry",
@@ -12182,8 +12534,10 @@ function renderLocalRegistryManifests() {
12182
12534
  protocol: "TCP"
12183
12535
  }
12184
12536
  ],
12537
+ // A TCP check: once the credential is enforced, `/v2/` answers
12538
+ // 401 to a probe that does not carry it.
12185
12539
  readinessProbe: {
12186
- httpGet: { path: "/v2/", port: PERSONAL_LOCAL_REGISTRY_PORT },
12540
+ tcpSocket: { port: PERSONAL_LOCAL_REGISTRY_PORT },
12187
12541
  periodSeconds: 10
12188
12542
  },
12189
12543
  resources: {
@@ -12191,7 +12545,7 @@ function renderLocalRegistryManifests() {
12191
12545
  limits: { memory: "512Mi" }
12192
12546
  },
12193
12547
  securityContext: CONTAINER_SECURITY,
12194
- volumeMounts: VOLUME_MOUNTS
12548
+ volumeMounts: auth ? [...VOLUME_MOUNTS, AUTH_VOLUME_MOUNT] : VOLUME_MOUNTS
12195
12549
  }
12196
12550
  ]
12197
12551
  }
@@ -12282,11 +12636,18 @@ async function registryAnswers(fetchImpl = fetch) {
12282
12636
  }
12283
12637
  async function localRegistryReady(ports) {
12284
12638
  const [answers, mirrored] = await Promise.all([
12285
- registryAnswers(ports.fetch),
12639
+ registryAnswers(withRegistryCredentials(ports.fetch ?? fetch, ports.readFile)),
12286
12640
  mirrorLoaded(ports.readFile)
12287
12641
  ]);
12288
12642
  return answers && mirrored;
12289
12643
  }
12644
+ async function localRegistryStatus(ports) {
12645
+ const [ready, authEnforced] = await Promise.all([
12646
+ localRegistryReady(ports),
12647
+ registryAuthEnforced(ports.readFile)
12648
+ ]);
12649
+ return { ready, authEnforced };
12650
+ }
12290
12651
 
12291
12652
  // src/daemon/registry-gc.ts
12292
12653
  init_paths();
@@ -12683,7 +13044,10 @@ async function collectRegistryGarbage(ports, options) {
12683
13044
  blobsCollected: false
12684
13045
  });
12685
13046
  const now = ports.now ?? Date.now;
12686
- const fetchImpl = ports.fetch ?? fetch;
13047
+ const fetchImpl = withRegistryCredentials(
13048
+ ports.fetch ?? fetch,
13049
+ ports.readFile ?? ((path) => nodeFileSystem.readFile(path))
13050
+ );
12687
13051
  const retain = await readRetainList(ports);
12688
13052
  if (typeof retain === "string") return skip(retain);
12689
13053
  const blocked = await registryBlocker(ports, now());
@@ -12750,6 +13114,16 @@ function upsertRegistryAuth(file, host, auth) {
12750
13114
  next.configs = configs;
12751
13115
  return next;
12752
13116
  }
13117
+ function removeRegistryAuth(file, host) {
13118
+ const existing = file.configs?.[host];
13119
+ if (!existing?.auth) return file;
13120
+ const { auth: _auth, ...rest } = existing;
13121
+ const { [host]: _removed, ...others } = file.configs ?? {};
13122
+ const configs = Object.keys(rest).length > 0 ? { ...others, [host]: rest } : others;
13123
+ const next = { ...file, configs };
13124
+ if (Object.keys(configs).length === 0) delete next.configs;
13125
+ return next;
13126
+ }
12753
13127
  function mirrorEndpoints(file, host) {
12754
13128
  const entry = file.mirrors?.[host];
12755
13129
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
@@ -12966,6 +13340,126 @@ var GRANTED_COMMAND_NAMES = LOCAL_BAKE_ROOT_COMMANDS.map((command) => command.na
12966
13340
  // src/commands/uninstall.ts
12967
13341
  init_dist();
12968
13342
 
13343
+ // src/daemon/pod-cpu-quota.ts
13344
+ init_safe_arg();
13345
+ var POD_SLICE = "kubepods.slice";
13346
+ function cpuMillicores(quantity) {
13347
+ const checked = assertQuantity(quantity, "reserveCpu");
13348
+ const match = /^(\d+(?:\.\d+)?)(m?)$/.exec(checked);
13349
+ if (!match?.[1])
13350
+ throw new Error(`Invalid reserveCpu: ${JSON.stringify(checked)} is not a CPU amount`);
13351
+ const amount = Number.parseFloat(match[1]);
13352
+ return Math.round(match[2] === "m" ? amount : amount * 1e3);
13353
+ }
13354
+ function podCpuQuotaPercent(cores, reservedMillicores) {
13355
+ const left = cores * 1e3 - reservedMillicores;
13356
+ if (left < 10) return null;
13357
+ return Math.floor(left / 10);
13358
+ }
13359
+ function setQuota(exec, value) {
13360
+ return exec("systemctl", ["set-property", POD_SLICE, `CPUQuota=${value}`]);
13361
+ }
13362
+ async function liftPodCpuQuota(exec) {
13363
+ const shown = await exec("systemctl", ["show", POD_SLICE, "-p", "CPUQuotaPerSecUSec", "--value"]);
13364
+ if (shown.exitCode !== 0 || ["", "infinity"].includes(shown.stdout.trim())) return null;
13365
+ const cleared = await setQuota(exec, "");
13366
+ return cleared.exitCode === 0 ? null : cleared.stderr.trim().slice(0, 200);
13367
+ }
13368
+ async function reconcilePodCpuQuota(exec, desired, warnings) {
13369
+ const reserve = desired.kubelet.systemReserved?.cpu;
13370
+ if (!reserve) {
13371
+ const failure = await liftPodCpuQuota(exec);
13372
+ if (failure) warnings.push(`Could not lift the CPU cap on ${POD_SLICE}: ${failure}`);
13373
+ return void 0;
13374
+ }
13375
+ const reserved = cpuMillicores(reserve);
13376
+ const state = await exec("systemctl", ["show", POD_SLICE, "-p", "ActiveState", "--value"]);
13377
+ if (state.exitCode !== 0 || state.stdout.trim() !== "active") {
13378
+ warnings.push(
13379
+ `Pods do not run under ${POD_SLICE} (k3s is not using the systemd cgroup driver), so the CPU you keep for this machine is weighted, not capped.`
13380
+ );
13381
+ return false;
13382
+ }
13383
+ const nproc = await exec("nproc", []);
13384
+ const cores = Number.parseInt(nproc.stdout.trim(), 10);
13385
+ if (nproc.exitCode !== 0 || !Number.isInteger(cores) || cores <= 0) {
13386
+ warnings.push(
13387
+ "Could not count this machine's CPUs, so the CPU you keep is weighted, not capped."
13388
+ );
13389
+ return false;
13390
+ }
13391
+ const percent = podCpuQuotaPercent(cores, reserved);
13392
+ if (percent === null) {
13393
+ warnings.push(
13394
+ `The CPU you keep (${reserve}) leaves no CPU for builds on this ${cores}-CPU machine. Lower it in your Conveyor profile.`
13395
+ );
13396
+ return false;
13397
+ }
13398
+ const set = await setQuota(exec, `${percent}%`);
13399
+ if (set.exitCode !== 0) {
13400
+ warnings.push(
13401
+ `Could not cap pod CPU at ${percent / 100} CPUs: ${set.stderr.trim().slice(0, 200)}`
13402
+ );
13403
+ return false;
13404
+ }
13405
+ return true;
13406
+ }
13407
+
13408
+ // src/daemon/registry-auth.ts
13409
+ init_dist();
13410
+ init_paths();
13411
+ var LOCAL_REGISTRY_AUTH_HOSTS = [
13412
+ PERSONAL_LOCAL_REGISTRY_HOST,
13413
+ new URL(PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT).host
13414
+ ];
13415
+ function registryAuthConfigured(file, credentials) {
13416
+ return LOCAL_REGISTRY_AUTH_HOSTS.every((host) => {
13417
+ const auth = file.configs?.[host]?.auth;
13418
+ return auth?.username === credentials.username && auth.password === credentials.password && auth.auth === void 0 && auth.identitytoken === void 0;
13419
+ });
13420
+ }
13421
+ function withLocalRegistryAuth(file, credentials) {
13422
+ return LOCAL_REGISTRY_AUTH_HOSTS.reduce(
13423
+ (next, host) => upsertRegistryAuth(next, host, {
13424
+ username: credentials.username,
13425
+ password: credentials.password
13426
+ }),
13427
+ file
13428
+ );
13429
+ }
13430
+ function monotonicNowUs() {
13431
+ return Number(process.hrtime.bigint() / 1000n);
13432
+ }
13433
+ async function k3sStartedAtMs(exec, nowMs, monotonicNow = monotonicNowUs) {
13434
+ const result = await exec(
13435
+ "systemctl",
13436
+ ["show", "k3s", "--property=ActiveEnterTimestampMonotonic", "--value"],
13437
+ { timeoutMs: 1e4 }
13438
+ ).catch(() => null);
13439
+ if (!result || result.exitCode !== 0) return null;
13440
+ const startedUs = Number.parseInt(result.stdout.trim(), 10);
13441
+ if (!Number.isFinite(startedUs) || startedUs <= 0) return null;
13442
+ return nowMs - (monotonicNow() - startedUs) / 1e3;
13443
+ }
13444
+ async function settleRegistryAuth(ctx, credentials, written) {
13445
+ const now = (ctx.now ?? (() => /* @__PURE__ */ new Date()))();
13446
+ const raw = await ctx.fs.readFile(LOCAL_REGISTRY_AUTH_STATE_FILE);
13447
+ const credential = credentialFingerprint(credentials);
13448
+ const stored = parseRegistryAuthState(raw);
13449
+ const state = stored?.credential === credential ? stored : { credential, writtenAt: null, enforced: false };
13450
+ if (!state.enforced && (written || state.writtenAt === null)) {
13451
+ state.writtenAt = now.toISOString();
13452
+ }
13453
+ if (!state.enforced && state.writtenAt !== null) {
13454
+ const startedAt = await k3sStartedAtMs(ctx.exec, now.getTime(), ctx.monotonicNowUs);
13455
+ if (startedAt !== null && startedAt > Date.parse(state.writtenAt)) state.enforced = true;
13456
+ }
13457
+ const next = `${JSON.stringify(state)}
13458
+ `;
13459
+ if (next !== raw) await ctx.fs.writeFile(LOCAL_REGISTRY_AUTH_STATE_FILE, next, 384);
13460
+ return state.enforced;
13461
+ }
13462
+
12969
13463
  // src/lib/isolation.ts
12970
13464
  init_dist();
12971
13465
 
@@ -13234,17 +13728,36 @@ async function removeLocalRegistry(ports) {
13234
13728
  `Could not remove the local image registry from the cluster: ${removed.stderr.trim().slice(0, 200)}`
13235
13729
  );
13236
13730
  }
13731
+ for (const [namespace, name2] of [
13732
+ [PERSONAL_LOCAL_REGISTRY_NAMESPACE, LOCAL_REGISTRY_HTPASSWD_SECRET],
13733
+ [PERSONAL_BAKE_NAMESPACE, PERSONAL_LOCAL_REGISTRY_PUSH_SECRET]
13734
+ ]) {
13735
+ const secret = await ports.exec(
13736
+ "k3s",
13737
+ ["kubectl", "-n", namespace, "delete", "secret", name2, "--ignore-not-found"],
13738
+ { timeoutMs: 6e4 }
13739
+ );
13740
+ if (secret.exitCode !== 0) {
13741
+ ports.log(
13742
+ `Could not remove the local image registry's Secret ${namespace}/${name2}: ${secret.stderr.trim().slice(0, 200)}`
13743
+ );
13744
+ }
13745
+ }
13237
13746
  const raw = await ports.fs.readFile(REGISTRIES_FILE);
13238
13747
  const parsed = parseRegistries(raw);
13239
- if (mirrorEndpoints(parsed, PERSONAL_LOCAL_REGISTRY_HOST).length === 0) return;
13748
+ const next = LOCAL_REGISTRY_AUTH_HOSTS.reduce(
13749
+ removeRegistryAuth,
13750
+ removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST)
13751
+ );
13752
+ if (serializeRegistries(next) === serializeRegistries(parsed)) return;
13240
13753
  const backup = await writeRegistriesWithBackup(
13241
13754
  ports.fs,
13242
13755
  raw,
13243
- removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST),
13756
+ next,
13244
13757
  (ports.now ?? (() => /* @__PURE__ */ new Date()))()
13245
13758
  );
13246
13759
  ports.log(
13247
- `Removed the local image registry's mirror from ${REGISTRIES_FILE} (backup: ${backup}).`
13760
+ `Removed the local image registry's mirror and credential from ${REGISTRIES_FILE} (backup: ${backup}).`
13248
13761
  );
13249
13762
  }
13250
13763
  async function removeIsolation(ports) {
@@ -13301,6 +13814,10 @@ async function uninstall(ports, options = {}) {
13301
13814
  }
13302
13815
  await removeLocalRegistry(ports);
13303
13816
  await removeIsolation(ports);
13817
+ const lifted = await liftPodCpuQuota(ports.exec).catch(
13818
+ (error) => error instanceof Error ? error.message : String(error)
13819
+ );
13820
+ if (lifted) ports.log(`Could not lift Conveyor's CPU cap on kubepods.slice: ${lifted}`);
13304
13821
  for (const path of OWNED_FILES) {
13305
13822
  await ports.fs.removeFile(path);
13306
13823
  result.removedFiles.push(path);
@@ -13353,6 +13870,74 @@ async function uninstall(ports, options = {}) {
13353
13870
  return result;
13354
13871
  }
13355
13872
 
13873
+ // src/lib/bins.ts
13874
+ var INSTALLER_PATHS = {
13875
+ bun: "/root/.bun/bin/bun",
13876
+ cli: "/root/.bun/bin/conveyor-k3",
13877
+ cloudflared: "/usr/local/bin/cloudflared"
13878
+ };
13879
+ async function whichOrNull(exec, bin) {
13880
+ const result = await exec("which", [bin]).catch(() => null);
13881
+ if (!result || result.exitCode !== 0) return null;
13882
+ const path = result.stdout.trim().split("\n")[0]?.trim();
13883
+ return path && path.startsWith("/") ? path : null;
13884
+ }
13885
+ async function resolveBinaries(exec, state, selfPath) {
13886
+ const recorded = state.bins;
13887
+ const cliSelf = selfPath?.startsWith("/") ? selfPath : null;
13888
+ return {
13889
+ bun: recorded?.bun ?? await whichOrNull(exec, "bun") ?? INSTALLER_PATHS.bun,
13890
+ // `cli` prefers the running process's own path: if this code is executing,
13891
+ // that path is by definition correct.
13892
+ cli: cliSelf ?? recorded?.cli ?? await whichOrNull(exec, "conveyor-k3") ?? INSTALLER_PATHS.cli,
13893
+ cloudflared: recorded?.cloudflared ?? await whichOrNull(exec, "cloudflared") ?? INSTALLER_PATHS.cloudflared
13894
+ };
13895
+ }
13896
+ var CLI_PATH_LINK = "/usr/local/bin/conveyor-k3";
13897
+ async function linkCliOnPath(exec, target2 = INSTALLER_PATHS.cli) {
13898
+ if (target2 === CLI_PATH_LINK) return { linked: false };
13899
+ const present = await exec("test", ["-x", target2]).catch(() => null);
13900
+ if (present?.exitCode !== 0) return { linked: false };
13901
+ const link = await exec("ln", ["-sfn", target2, CLI_PATH_LINK]).catch(() => null);
13902
+ if (link?.exitCode === 0) return { linked: true };
13903
+ return {
13904
+ linked: false,
13905
+ problem: `Could not link ${CLI_PATH_LINK} to ${target2}: ${link?.stderr.trim().slice(0, 200) || "ln failed"}`
13906
+ };
13907
+ }
13908
+ var NPM_GLOBAL_PACKAGE_DIRS = [
13909
+ "/usr/local/lib/node_modules/@rallycry/conveyor-k3",
13910
+ "/usr/lib/node_modules/@rallycry/conveyor-k3"
13911
+ ];
13912
+ async function npmCopyHint(readFile9) {
13913
+ for (const dir of NPM_GLOBAL_PACKAGE_DIRS) {
13914
+ if (await readFile9(`${dir}/package.json`) !== null) {
13915
+ return `An npm copy of conveyor-k3 is installed at ${dir}. Conveyor runs and updates its own copy (${INSTALLER_PATHS.cli}, linked from ${CLI_PATH_LINK}), so you can remove it: sudo npm uninstall -g @rallycry/conveyor-k3`;
13916
+ }
13917
+ }
13918
+ return null;
13919
+ }
13920
+
13921
+ // src/lib/cloudflared.ts
13922
+ var PACKAGE_MANAGER_REFUSAL = "installed by a package manager";
13923
+ async function resolveRealPath(exec, bin) {
13924
+ const result = await exec("readlink", ["-f", bin]).catch(() => null);
13925
+ const resolved = result?.exitCode === 0 ? result.stdout.trim() : "";
13926
+ return resolved.startsWith("/") ? resolved : bin;
13927
+ }
13928
+ async function cloudflaredUpdater(exec, bin) {
13929
+ const real = await resolveRealPath(exec, bin);
13930
+ const owner = await exec("dpkg", ["-S", real]).catch(() => null);
13931
+ return owner?.exitCode === 0 ? "package-manager" : "conveyor";
13932
+ }
13933
+ function isPackageManagerRefusal(output) {
13934
+ return `${output.stdout}
13935
+ ${output.stderr}`.toLowerCase().includes(PACKAGE_MANAGER_REFUSAL);
13936
+ }
13937
+ function describeCloudflaredUpdater(updater) {
13938
+ return updater === "package-manager" ? "cloudflared is updated by your package manager, not by Conveyor" : "cloudflared is updated by Conveyor's daily update";
13939
+ }
13940
+
13356
13941
  // src/commands/update.ts
13357
13942
  init_paths();
13358
13943
  var CLOUDFLARED_UPDATED_EXIT = 11;
@@ -13441,7 +14026,22 @@ async function updatePackage(ports, result, targetVersion) {
13441
14026
  await ports.exec("systemctl", ["restart", DAEMON_UNIT]);
13442
14027
  }
13443
14028
  async function updateCloudflared(ports, result) {
14029
+ const packaged = () => {
14030
+ result.cloudflaredUpdater = "package-manager";
14031
+ result.notes.push(
14032
+ `cloudflared update skipped: ${describeCloudflaredUpdater("package-manager")}`
14033
+ );
14034
+ };
14035
+ if (await cloudflaredUpdater(ports.exec, ports.bins.cloudflared) === "package-manager") {
14036
+ packaged();
14037
+ return;
14038
+ }
13444
14039
  const update = await ports.exec(ports.bins.cloudflared, ["update"], { timeoutMs: 5 * 6e4 });
14040
+ if (isPackageManagerRefusal(update)) {
14041
+ packaged();
14042
+ return;
14043
+ }
14044
+ result.cloudflaredUpdater = "conveyor";
13445
14045
  if (update.exitCode !== CLOUDFLARED_UPDATED_EXIT) return;
13446
14046
  result.cloudflaredUpdated = true;
13447
14047
  ports.log("cloudflared was updated; restarting the tunnel.");
@@ -13451,10 +14051,13 @@ async function runUpdate(ports, decision, channel, targetVersion) {
13451
14051
  const result = {
13452
14052
  packageUpdated: false,
13453
14053
  cloudflaredUpdated: false,
14054
+ cloudflaredUpdater: null,
13454
14055
  k3sUpgraded: false,
13455
14056
  notes: []
13456
14057
  };
13457
14058
  await updatePackage(ports, result, targetVersion);
14059
+ const linked = await linkCliOnPath(ports.exec);
14060
+ if (linked.problem) result.notes.push(linked.problem);
13458
14061
  await updateCloudflared(ports, result);
13459
14062
  if (!decision.upgrade) {
13460
14063
  result.notes.push(`k3s upgrade skipped: ${decision.reason}`);
@@ -13656,7 +14259,7 @@ async function ensureCaPool(ports) {
13656
14259
  const existing = await ports.fs.readFile(CA_POOL_PATH);
13657
14260
  if (existing !== ca) await ports.fs.writeFile(CA_POOL_PATH, ca, 420);
13658
14261
  }
13659
- async function mintServiceAccountToken(ports) {
14262
+ async function ensureServiceAccount(ports) {
13660
14263
  await runOrThrow(ports.exec, "k3s", [
13661
14264
  "kubectl",
13662
14265
  "create",
@@ -13685,6 +14288,9 @@ async function mintServiceAccountToken(ports) {
13685
14288
  stdin: result.stdout
13686
14289
  });
13687
14290
  });
14291
+ }
14292
+ async function mintServiceAccountToken(ports) {
14293
+ await ensureServiceAccount(ports);
13688
14294
  const token = await runOrThrow(ports.exec, "k3s", [
13689
14295
  "kubectl",
13690
14296
  "-n",
@@ -13752,13 +14358,13 @@ function buildConveyorSocketOptions(auth) {
13752
14358
 
13753
14359
  // src/daemon/control-endpoint.ts
13754
14360
  import { lookup as dnsLookup } from "dns";
13755
- import { existsSync, readFileSync } from "fs";
14361
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
13756
14362
  import { Agent } from "https";
13757
14363
  var CONTROL_FILE = "/etc/conveyor-vm/control.json";
13758
14364
  var CONTROL_ALIAS_IP = /^192\.168\.5\.24[0-7]$/;
13759
14365
  function readControlFile(path) {
13760
14366
  try {
13761
- return readFileSync(path, "utf8");
14367
+ return readFileSync2(path, "utf8");
13762
14368
  } catch {
13763
14369
  return null;
13764
14370
  }
@@ -13804,8 +14410,8 @@ function controlPlaneRoute(apiUrl, readFile9 = readControlFile, fileExists = exi
13804
14410
  }
13805
14411
 
13806
14412
  // src/daemon/device-proof.ts
13807
- import { lstatSync, readFileSync as readFileSync2 } from "fs";
13808
- import { createPrivateKey, randomBytes, sign } from "crypto";
14413
+ import { lstatSync, readFileSync as readFileSync3 } from "fs";
14414
+ import { createPrivateKey, randomBytes as randomBytes2, sign } from "crypto";
13809
14415
  var DEVICE_KEY_PATH = "/etc/conveyor/device-key.pem";
13810
14416
  function privateDeviceKey(path, ownerUid) {
13811
14417
  let stat2;
@@ -13819,7 +14425,7 @@ function privateDeviceKey(path, ownerUid) {
13819
14425
  throw new Error("Conveyor device key must be a regular file.");
13820
14426
  if ((stat2.mode & 511) !== 384 || stat2.uid !== ownerUid)
13821
14427
  throw new Error("Conveyor device key must be owned by root with mode 0600.");
13822
- const pem = readFileSync2(path, "utf8");
14428
+ const pem = readFileSync3(path, "utf8");
13823
14429
  if (Buffer.byteLength(pem) > 16 * 1024) throw new Error("Conveyor device key is too large.");
13824
14430
  const key = createPrivateKey(pem);
13825
14431
  if (key.asymmetricKeyType !== "ed25519")
@@ -13829,7 +14435,7 @@ function privateDeviceKey(path, ownerUid) {
13829
14435
  function createClusterProof(apiUrl, clusterToken, key) {
13830
14436
  const origin = new URL(apiUrl).origin;
13831
14437
  const timestamp = Date.now();
13832
- const nonce = randomBytes(32).toString("base64url");
14438
+ const nonce = randomBytes2(32).toString("base64url");
13833
14439
  const message3 = `conveyor-device-v1
13834
14440
  ${origin}
13835
14441
  ${clusterToken}
@@ -14003,7 +14609,7 @@ var ClusterConnection = class {
14003
14609
  };
14004
14610
 
14005
14611
  // src/lib/manifest-guard.ts
14006
- import { isMap, isScalar, parseAllDocuments } from "yaml";
14612
+ import { isAlias, isMap, isScalar, parseAllDocuments } from "yaml";
14007
14613
  var ALLOWED_MANIFEST_KINDS = [
14008
14614
  "Namespace",
14009
14615
  "Role",
@@ -14015,6 +14621,14 @@ var ALLOWED_MANIFEST_KINDS = [
14015
14621
  var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
14016
14622
  var MISSING_KIND = "(missing kind)";
14017
14623
  var UNPARSEABLE = "(unparseable)";
14624
+ var LIST_SHAPED = "(list-shaped document)";
14625
+ function isListShaped(map) {
14626
+ return map.items.some(({ key }) => {
14627
+ if (isAlias(key)) return true;
14628
+ const name2 = isScalar(key) ? key.value : key;
14629
+ return typeof name2 === "string" && (name2 === "<<" || name2.toLowerCase() === "items");
14630
+ });
14631
+ }
14018
14632
  function findDisallowedKinds(manifest) {
14019
14633
  let documents;
14020
14634
  try {
@@ -14037,6 +14651,10 @@ function findDisallowedKinds(manifest) {
14037
14651
  record2(MISSING_KIND);
14038
14652
  continue;
14039
14653
  }
14654
+ if (isListShaped(doc.contents)) {
14655
+ record2(LIST_SHAPED);
14656
+ continue;
14657
+ }
14040
14658
  const kind = doc.get("kind");
14041
14659
  if (typeof kind !== "string" || kind.length === 0) {
14042
14660
  record2(MISSING_KIND);
@@ -14046,14 +14664,64 @@ function findDisallowedKinds(manifest) {
14046
14664
  }
14047
14665
  return disallowed;
14048
14666
  }
14667
+ function withoutKind(manifest, kind) {
14668
+ return parseAllDocuments(manifest).filter((doc) => !(isMap(doc.contents) && doc.get("kind") === kind)).map((doc) => doc.toString()).join("---\n");
14669
+ }
14670
+ function declaresPriorityClass(manifest, name2, value) {
14671
+ return parseAllDocuments(manifest).some(
14672
+ (doc) => isMap(doc.contents) && doc.get("kind") === "PriorityClass" && doc.getIn(["metadata", "name"]) === name2 && doc.get("value") === value
14673
+ );
14674
+ }
14049
14675
 
14050
14676
  // src/daemon/reconcile.ts
14051
14677
  init_safe_arg();
14678
+
14679
+ // src/daemon/kubelet-upkeep.ts
14052
14680
  init_paths();
14681
+ function ownerRestartWarning(change) {
14682
+ return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
14683
+ }
14684
+ async function reconcileKubelet(ctx, desired, result, managed) {
14685
+ const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
14686
+ const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
14687
+ result.skippedKubeletFlags = rendered.skipped;
14688
+ result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
14689
+ for (const flag of rendered.skipped) {
14690
+ result.warnings.push(
14691
+ `Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
14692
+ );
14693
+ }
14694
+ const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
14695
+ if (rendered.content === null) {
14696
+ if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
14697
+ return;
14698
+ }
14699
+ if (current === rendered.content) return;
14700
+ await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
14701
+ await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
14702
+ if (!managed) {
14703
+ result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
14704
+ return;
14705
+ }
14706
+ if (!mayDisrupt(ctx.runningConveyorPods)) {
14707
+ result.warnings.push(
14708
+ `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
14709
+ );
14710
+ return;
14711
+ }
14712
+ const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
14713
+ if (restart.exitCode === 0) {
14714
+ result.restartedK3s = true;
14715
+ } else {
14716
+ result.warnings.push(
14717
+ `Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
14718
+ );
14719
+ }
14720
+ }
14053
14721
 
14054
14722
  // src/daemon/local-registry-upkeep.ts
14055
14723
  init_dist();
14056
- import { createHash } from "crypto";
14724
+ import { createHash as createHash3 } from "crypto";
14057
14725
  init_paths();
14058
14726
  function nowDate() {
14059
14727
  return /* @__PURE__ */ new Date();
@@ -14062,33 +14730,47 @@ function mirrorConfigured(raw) {
14062
14730
  const endpoints = mirrorEndpoints(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST);
14063
14731
  return endpoints.length === 1 && endpoints[0] === PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT;
14064
14732
  }
14065
- async function applyUnlessCollecting(ctx) {
14733
+ async function applyUnlessCollecting(ctx, auth) {
14066
14734
  const deployment = await readRegistryDeployment(ctx.exec);
14067
14735
  if (!deployment) throw new Error("the registry's Deployment could not be read");
14068
14736
  if (gcMarkIsLive(deployment.mark, (ctx.now ?? nowDate)().getTime(), ctx.isAlive)) return false;
14069
14737
  if (deployment.mark !== null && !await deleteSweepJob(ctx.exec)) {
14070
14738
  throw new Error("a stopped cleanup's Job could not be deleted, so the registry stays stopped");
14071
14739
  }
14072
- await kubectlApply(ctx.exec, renderLocalRegistryManifests());
14740
+ await kubectlApply(ctx.exec, renderLocalRegistryManifests(auth));
14073
14741
  if (deployment.mark !== null) await clearGcMark(ctx.exec);
14074
14742
  return true;
14075
14743
  }
14076
14744
  async function deployLocalRegistry(ctx) {
14077
14745
  await ctx.fs.ensureDir(LOCAL_REGISTRY_DATA_DIR, 448);
14078
- const applied = await applyUnlessCollecting(ctx);
14746
+ const credentials = await loadOrCreateRegistryCredentials(ctx.fs, ctx.random);
14079
14747
  const raw = await ctx.fs.readFile(REGISTRIES_FILE);
14080
- if (mirrorConfigured(raw)) return { applied, mirrorWritten: false, backupPath: null };
14081
- const next = upsertRegistryMirror(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST, [
14082
- PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
14083
- ]);
14084
- const backupPath = await writeRegistriesWithBackup(ctx.fs, raw, next, (ctx.now ?? nowDate)());
14085
- return { applied, mirrorWritten: true, backupPath };
14748
+ let file = parseRegistries(raw);
14749
+ const mirrorWritten = !mirrorConfigured(raw);
14750
+ const authWritten = !registryAuthConfigured(file, credentials);
14751
+ let backupPath = null;
14752
+ if (mirrorWritten) {
14753
+ file = upsertRegistryMirror(file, PERSONAL_LOCAL_REGISTRY_HOST, [
14754
+ PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
14755
+ ]);
14756
+ }
14757
+ if (authWritten) file = withLocalRegistryAuth(file, credentials);
14758
+ if (mirrorWritten || authWritten) {
14759
+ backupPath = await writeRegistriesWithBackup(ctx.fs, raw, file, (ctx.now ?? nowDate)());
14760
+ }
14761
+ const authEnforced = await settleRegistryAuth(ctx, credentials, authWritten);
14762
+ await kubectlApply(ctx.exec, renderLocalRegistrySecrets(credentials));
14763
+ const applied = await applyUnlessCollecting(ctx, authEnforced ? credentials : null);
14764
+ return { applied, mirrorWritten, authWritten, backupPath, authEnforced };
14086
14765
  }
14087
14766
  async function readMirrorState(fs2) {
14088
14767
  const raw = await fs2.readFile(REGISTRIES_FILE);
14089
- const fingerprint = createHash("sha256").update(raw ?? "").digest("hex");
14768
+ const authPending = !await registryAuthEnforced((path) => fs2.readFile(path));
14769
+ const fingerprint = createHash3("sha256").update(raw ?? "").digest("hex");
14090
14770
  if (!mirrorConfigured(raw)) return { state: "absent", fingerprint };
14091
- if (await mirrorLoaded((path) => fs2.readFile(path))) return { state: "loaded", fingerprint };
14771
+ if (!authPending && await mirrorLoaded((path) => fs2.readFile(path))) {
14772
+ return { state: "loaded", fingerprint };
14773
+ }
14092
14774
  const tried = (await fs2.readFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE))?.trim() === fingerprint;
14093
14775
  return { state: tried ? "tried" : "owed", fingerprint };
14094
14776
  }
@@ -14100,14 +14782,14 @@ async function loadRegistryMirror(ctx) {
14100
14782
  if (state === "tried") {
14101
14783
  return {
14102
14784
  restarted: false,
14103
- warning: `k3s restarted but did not load the local image registry's mirror, so pods cannot pull from that registry. Conveyor restarts k3s for it again only after /etc/rancher/k3s/registries.yaml changes. Check that k3s reads that file and keeps its data in the default directory: the mirror should appear under ${K3S_CERTS_D}.`
14785
+ warning: `k3s restarted but did not load the local image registry's mirror or credential, so pods cannot pull from that registry or the registry stays open to every account on this machine. Conveyor restarts k3s for it again only after /etc/rancher/k3s/registries.yaml changes. Check that k3s reads that file and keeps its data in the default directory: the mirror should appear under ${K3S_CERTS_D}.`
14104
14786
  };
14105
14787
  }
14106
14788
  if (state !== "owed") return { restarted: false, warning: null };
14107
14789
  if (!mayDisrupt(ctx.runningConveyorPods)) {
14108
14790
  return {
14109
14791
  restarted: false,
14110
- warning: `The local image registry needs a k3s restart to load its mirror, but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
14792
+ warning: `The local image registry needs a k3s restart to load its mirror or credential, but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
14111
14793
  };
14112
14794
  }
14113
14795
  const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
@@ -14138,7 +14820,18 @@ async function stdoutOf(exec, bin, args) {
14138
14820
  const run = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
14139
14821
  return run && run.exitCode === 0 ? run.stdout : null;
14140
14822
  }
14823
+ async function dropInDirExists(exec) {
14824
+ const run = await exec("test", ["-d", K3S_CONFIG_DROPIN_DIR], { timeoutMs: 1e4 }).catch(
14825
+ () => null
14826
+ );
14827
+ if (run === null) return null;
14828
+ if (run.exitCode === 0) return true;
14829
+ return run.exitCode === 1 && run.stderr.trim() === "" ? false : null;
14830
+ }
14141
14831
  async function readK3sConfigs(ctx) {
14832
+ const dirExists = await dropInDirExists(ctx.exec);
14833
+ if (dirExists === null) return null;
14834
+ if (!dirExists) return [await ctx.fs.readFile(K3S_OWNER_CONFIG)];
14142
14835
  const listed = await stdoutOf(ctx.exec, "find", [
14143
14836
  "-L",
14144
14837
  K3S_CONFIG_DROPIN_DIR,
@@ -14432,7 +15125,7 @@ async function reportLocalImages(exec, inventory, wantedImages, cache) {
14432
15125
  if (!inventory) return void 0;
14433
15126
  const wanted = new Set(wantedImages);
14434
15127
  const held = inventory.images.flatMap(
14435
- (image) => (image.repoTags ?? []).filter((ref) => ref.startsWith("localhost/") && isImageRef(ref)).map((ref) => ({ ref, imageId: image.id }))
15128
+ (image) => (image.repoTags ?? []).filter((ref) => (ref.startsWith("localhost/") || wanted.has(ref)) && isImageRef(ref)).map((ref) => ({ ref, imageId: image.id }))
14436
15129
  );
14437
15130
  held.sort(
14438
15131
  (a, b) => Number(wanted.has(b.ref)) - Number(wanted.has(a.ref)) || (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)
@@ -14504,14 +15197,22 @@ async function readImageInventory(exec) {
14504
15197
  }
14505
15198
  return { images, referencedIds };
14506
15199
  }
15200
+ var CRICTL_RMI_TIMEOUT = "120s";
15201
+ var RMI_EXEC_TIMEOUT_MS = 15e4;
15202
+ var DEADLINE_EXCEEDED = "context deadline exceeded";
14507
15203
  async function pruneStrandedImages(exec, inventory, wantedImages) {
14508
15204
  const candidates = selectStrandedImages(inventory, wantedImages);
14509
- const result = { removed: 0, bytesReclaimed: 0, failures: [] };
15205
+ const result = { removed: 0, bytesReclaimed: 0, failures: [], pending: [] };
14510
15206
  for (const candidate of candidates) {
14511
- const removal = await exec("crictl", ["rmi", candidate.id]);
15207
+ const removal = await exec("crictl", ["--timeout", CRICTL_RMI_TIMEOUT, "rmi", candidate.id], {
15208
+ timeoutMs: RMI_EXEC_TIMEOUT_MS
15209
+ });
14512
15210
  if (removal.exitCode === 0) {
14513
15211
  result.removed += 1;
14514
15212
  result.bytesReclaimed += candidate.sizeBytes;
15213
+ } else if (`${removal.stderr}
15214
+ ${removal.stdout}`.includes(DEADLINE_EXCEEDED)) {
15215
+ result.pending.push(candidate.id);
14515
15216
  } else {
14516
15217
  result.failures.push(`${candidate.id}: ${removal.stderr.trim().slice(0, 120)}`);
14517
15218
  }
@@ -14613,17 +15314,26 @@ function countPinnedWanted(inventory, wantedImages) {
14613
15314
  // src/daemon/telemetry.ts
14614
15315
  init_paths();
14615
15316
  var LOCAL_REGISTRY_WARN_AFTER_MS = 10 * 6e4;
15317
+ var LOCAL_REGISTRY_OPEN_WARNING = "The local image registry is open to every account on this machine: any of them can read and replace the images your workspaces start from. It requires its credential once k3s restarts and loads it.";
14616
15318
  async function reportLocalRegistry(ports, watch2, now = Date.now()) {
14617
15319
  if (!watch2) return { fields: {}, warnings: [] };
14618
- const ready = await localRegistryReady({ readFile: ports.readFile, fetch: ports.fetch });
15320
+ const { ready, authEnforced } = await localRegistryStatus({
15321
+ readFile: ports.readFile,
15322
+ fetch: ports.fetch
15323
+ });
14619
15324
  const paused = (watch2.pausedUntil ?? 0) > now;
14620
15325
  watch2.notReadySince = ready || paused ? null : watch2.notReadySince ?? now;
14621
15326
  const stuckMs = watch2.notReadySince === null ? 0 : now - watch2.notReadySince;
14622
- return {
14623
- fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready } },
14624
- warnings: stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS ? [
15327
+ const warnings = [];
15328
+ if (stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS) {
15329
+ warnings.push(
14625
15330
  `The local image registry has not been ready for ${Math.floor(stuckMs / 6e4)} minutes, so shadow bakes cannot use it yet. Check its pod with \`k3s kubectl -n conveyor-system get pods -l app.kubernetes.io/name=conveyor-registry\`. If the pod is running, k3s has not loaded the registry's mirror yet: it restarts k3s for that only while no Conveyor pod runs.`
14626
- ] : []
15331
+ );
15332
+ }
15333
+ if (!authEnforced) warnings.push(LOCAL_REGISTRY_OPEN_WARNING);
15334
+ return {
15335
+ fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready, authEnforced } },
15336
+ warnings
14627
15337
  };
14628
15338
  }
14629
15339
  var VM_CAPACITY_FILE = "/etc/conveyor-vm/capacity.json";
@@ -14746,6 +15456,7 @@ async function collectHeartbeat(ports, state, inventory) {
14746
15456
  ...localImages ? { localImages } : {},
14747
15457
  ...registry.fields,
14748
15458
  ...state.isolation ? { isolation: state.isolation } : {},
15459
+ ...state.cpuQuotaApplied === void 0 ? {} : { cpuQuotaApplied: state.cpuQuotaApplied },
14749
15460
  localWarnings: withinHeartbeatBounds([
14750
15461
  ...state.localWarnings,
14751
15462
  ...gcWarning ? [gcWarning] : [],
@@ -14796,7 +15507,15 @@ async function collect(ports, now) {
14796
15507
  if (watch2) watch2.pausedUntil = now.getTime() + REGISTRY_GC_MARK_TTL_MS;
14797
15508
  try {
14798
15509
  const outcome = await collectRegistryGarbage(
14799
- { exec, log: log2, requestDesiredState, fetch: fetch2, sleep: sleep4, isAlive },
15510
+ {
15511
+ exec,
15512
+ log: log2,
15513
+ requestDesiredState,
15514
+ fetch: fetch2,
15515
+ readFile: (path) => ports.fs.readFile(path),
15516
+ sleep: sleep4,
15517
+ isAlive
15518
+ },
14800
15519
  { dryRun: false }
14801
15520
  );
14802
15521
  if (outcome.skipped) return outcome.skipped;
@@ -14842,48 +15561,7 @@ async function runRegistryUpkeep(ports, lastGcAt, maintenanceHour) {
14842
15561
  }
14843
15562
 
14844
15563
  // src/daemon/reconcile.ts
14845
- function ownerRestartWarning(change) {
14846
- return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
14847
- }
14848
- async function reconcileKubelet(ctx, desired, result, managed) {
14849
- const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
14850
- const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
14851
- result.skippedKubeletFlags = rendered.skipped;
14852
- result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
14853
- for (const flag of rendered.skipped) {
14854
- result.warnings.push(
14855
- `Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
14856
- );
14857
- }
14858
- const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
14859
- if (rendered.content === null) {
14860
- if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
14861
- return;
14862
- }
14863
- if (current === rendered.content) return;
14864
- await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
14865
- await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
14866
- if (!managed) {
14867
- result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
14868
- return;
14869
- }
14870
- if (!mayDisrupt(ctx.runningConveyorPods)) {
14871
- result.warnings.push(
14872
- `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
14873
- );
14874
- return;
14875
- }
14876
- const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
14877
- if (restart.exitCode === 0) {
14878
- result.restartedK3s = true;
14879
- } else {
14880
- result.warnings.push(
14881
- `Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
14882
- );
14883
- }
14884
- }
14885
- async function reconcilePriorityClass(ctx, desired, result) {
14886
- const name2 = assertK8sName(desired.priorityClass.name, "priorityClass.name");
15564
+ async function livePriorityClassValue(ctx, name2) {
14887
15565
  const read = await ctx.exec("k3s", [
14888
15566
  "kubectl",
14889
15567
  "get",
@@ -14892,30 +15570,59 @@ async function reconcilePriorityClass(ctx, desired, result) {
14892
15570
  "-o",
14893
15571
  "jsonpath={.value}"
14894
15572
  ]);
14895
- if (read.exitCode !== 0) return;
14896
- const existingValue = Number.parseInt(read.stdout.trim(), 10);
14897
- if (existingValue === desired.priorityClass.value) return;
15573
+ const value = Number.parseInt(read.stdout.trim(), 10);
15574
+ return read.exitCode === 0 && Number.isFinite(value) ? value : null;
15575
+ }
15576
+ async function reconcilePriorityClass(ctx, desired, result) {
15577
+ const name2 = assertK8sName(desired.priorityClass.name, "priorityClass.name");
15578
+ const wanted = desired.priorityClass.value;
15579
+ const existingValue = await livePriorityClassValue(ctx, name2);
15580
+ if (existingValue === null || existingValue === wanted) return "ready";
15581
+ if (!declaresPriorityClass(desired.rbacManifest, name2, wanted)) {
15582
+ result.warnings.push(
15583
+ `The \`${name2}\` PriorityClass has value ${existingValue}, but Conveyor's manifest does not declare it at ${wanted}, so it is left as it is. This should never happen; report it.`
15584
+ );
15585
+ return "kept";
15586
+ }
14898
15587
  if (!mayDisrupt(ctx.runningConveyorPods)) {
14899
15588
  result.warnings.push(
14900
- `The \`${name2}\` PriorityClass has value ${existingValue}, but Conveyor needs ${desired.priorityClass.value}. The value is immutable, so it must be recreated \u2014 deferred because ${busyReason(ctx.runningConveyorPods)}.`
15589
+ `The \`${name2}\` PriorityClass has value ${existingValue}, but Conveyor needs ${wanted}. The value is immutable, so it must be recreated \u2014 deferred because ${busyReason(ctx.runningConveyorPods)}.`
14901
15590
  );
14902
- return;
15591
+ return "kept";
14903
15592
  }
14904
- const removed = await ctx.exec("k3s", ["kubectl", "delete", "priorityclass", name2]);
15593
+ const removed = await ctx.exec("k3s", [
15594
+ "kubectl",
15595
+ "delete",
15596
+ "priorityclass",
15597
+ name2,
15598
+ "--ignore-not-found"
15599
+ ]);
14905
15600
  if (removed.exitCode !== 0) {
14906
15601
  result.warnings.push(
14907
- `Could not replace the \`${name2}\` PriorityClass (value ${existingValue}, needs ${desired.priorityClass.value}): ${removed.stderr.trim().slice(0, 160)}`
15602
+ `Could not replace the \`${name2}\` PriorityClass (value ${existingValue}, needs ${wanted}): ${removed.stderr.trim().slice(0, 160)}`
14908
15603
  );
14909
- return;
15604
+ return "kept";
14910
15605
  }
15606
+ return "removed";
15607
+ }
15608
+ async function applyRbac(ctx, desired, result) {
15609
+ const outcome = await reconcilePriorityClass(ctx, desired, result);
15610
+ const manifest = outcome === "kept" ? withoutKind(desired.rbacManifest, "PriorityClass") : desired.rbacManifest;
14911
15611
  try {
14912
- await kubectlApply(ctx.exec, desired.rbacManifest);
14913
- result.recreatedPriorityClass = true;
15612
+ await kubectlApply(ctx.exec, manifest);
14914
15613
  } catch (error) {
15614
+ if (outcome !== "removed") throw error;
15615
+ const value = await livePriorityClassValue(ctx, desired.priorityClass.name).catch(() => null);
15616
+ if (value === desired.priorityClass.value) {
15617
+ result.recreatedPriorityClass = true;
15618
+ throw error;
15619
+ }
14915
15620
  result.warnings.push(
14916
- `The \`${name2}\` PriorityClass was removed to change its value but could not be recreated: ${error instanceof Error ? error.message : String(error)}. No build can start on this machine until it exists \u2014 run \`conveyor-k3 sync\`.`
15621
+ `The \`${desired.priorityClass.name}\` PriorityClass was removed to change its value but could not be recreated: ${error instanceof Error ? error.message : String(error)}. No build can start on this machine until it exists \u2014 run \`conveyor-k3 sync\`.`
14917
15622
  );
15623
+ return;
14918
15624
  }
15625
+ if (outcome === "removed") result.recreatedPriorityClass = true;
14919
15626
  }
14920
15627
  async function step(result, label2, run) {
14921
15628
  try {
@@ -14941,12 +15648,7 @@ async function reconcile(ctx, desired) {
14941
15648
  `Conveyor sent a cluster manifest containing disallowed kind(s) ${disallowedKinds.join(", ")} \u2014 refused. RBAC and PriorityClass steps skipped; this should never happen, report it.`
14942
15649
  );
14943
15650
  } else {
14944
- await step(result, "Could not apply RBAC", () => kubectlApply(ctx.exec, desired.rbacManifest));
14945
- await step(
14946
- result,
14947
- "Could not reconcile the PriorityClass",
14948
- () => reconcilePriorityClass(ctx, desired, result)
14949
- );
15651
+ await step(result, "Could not apply RBAC", () => applyRbac(ctx, desired, result));
14950
15652
  }
14951
15653
  await step(
14952
15654
  result,
@@ -14978,11 +15680,23 @@ async function reconcile(ctx, desired) {
14978
15680
  "Could not apply kubelet settings",
14979
15681
  () => reconcileKubelet(ctx, desired, result, managed)
14980
15682
  );
15683
+ if (desired.isolation !== void 0) {
15684
+ await step(result, "Could not cap pod CPU", async () => {
15685
+ result.cpuQuotaApplied = await reconcilePodCpuQuota(ctx.exec, desired, result.warnings);
15686
+ });
15687
+ if (desired.kubelet.systemReserved?.cpu && result.cpuQuotaApplied === void 0) {
15688
+ result.cpuQuotaApplied = false;
15689
+ }
15690
+ }
14981
15691
  if (registry && !result.restartedK3s) {
14982
15692
  await step(result, "Could not load the local image registry mirror", async () => {
14983
15693
  if (!managed) {
14984
15694
  if (await registryMirrorPending(ctx.fs))
14985
- result.warnings.push(ownerRestartWarning("The local image registry's mirror is new."));
15695
+ result.warnings.push(
15696
+ ownerRestartWarning(
15697
+ "The local image registry's mirror or credential is new, and the registry stays open to every account on this machine until k3s loads it."
15698
+ )
15699
+ );
14986
15700
  return;
14987
15701
  }
14988
15702
  const loaded = await loadRegistryMirror(ctx);
@@ -15039,6 +15753,7 @@ async function enrollMachine(ports, env, cloudflaredBin, recorded = {}) {
15039
15753
  const enrollment = await connection.enroll();
15040
15754
  log2(`Enrolled as ${enrollment.hostname}.`);
15041
15755
  await installUnits(ports, enrollment, cloudflaredBin, enrollment.desiredState.maintenance.hour);
15756
+ await ensureServiceAccount(ports);
15042
15757
  const runningConveyorPods = await countRunningConveyorPods(exec);
15043
15758
  const result = await reconcile({ exec, fs: fs2, runningConveyorPods }, enrollment.desiredState);
15044
15759
  for (const warning of result.warnings) log2(warning);
@@ -15060,30 +15775,6 @@ async function enrollMachine(ports, env, cloudflaredBin, recorded = {}) {
15060
15775
  for (const line of renderRegistryAuthSummary(missing)) log2(line);
15061
15776
  }
15062
15777
 
15063
- // src/lib/bins.ts
15064
- var INSTALLER_PATHS = {
15065
- bun: "/root/.bun/bin/bun",
15066
- cli: "/root/.bun/bin/conveyor-k3",
15067
- cloudflared: "/usr/local/bin/cloudflared"
15068
- };
15069
- async function whichOrNull(exec, bin) {
15070
- const result = await exec("which", [bin]).catch(() => null);
15071
- if (!result || result.exitCode !== 0) return null;
15072
- const path = result.stdout.trim().split("\n")[0]?.trim();
15073
- return path && path.startsWith("/") ? path : null;
15074
- }
15075
- async function resolveBinaries(exec, state, selfPath) {
15076
- const recorded = state.bins;
15077
- const cliSelf = selfPath?.startsWith("/") ? selfPath : null;
15078
- return {
15079
- bun: recorded?.bun ?? await whichOrNull(exec, "bun") ?? INSTALLER_PATHS.bun,
15080
- // `cli` prefers the running process's own path: if this code is executing,
15081
- // that path is by definition correct.
15082
- cli: cliSelf ?? recorded?.cli ?? await whichOrNull(exec, "conveyor-k3") ?? INSTALLER_PATHS.cli,
15083
- cloudflared: recorded?.cloudflared ?? await whichOrNull(exec, "cloudflared") ?? INSTALLER_PATHS.cloudflared
15084
- };
15085
- }
15086
-
15087
15778
  // src/lib/guest-vm.ts
15088
15779
  async function requireGuestVm(exec) {
15089
15780
  try {
@@ -15171,6 +15862,8 @@ var ClusterDaemon = class {
15171
15862
  * against k3s's own settings before reporting it.
15172
15863
  */
15173
15864
  isolation = void 0;
15865
+ /** Whether the pod slice's CPU cap is in force, from the last reconcile. */
15866
+ cpuQuotaApplied = void 0;
15174
15867
  timers = [];
15175
15868
  /** When the last store walk started, so one that failed is not retried on every reconcile. */
15176
15869
  storeWalkStartedAt = 0;
@@ -15253,6 +15946,7 @@ var ClusterDaemon = class {
15253
15946
  );
15254
15947
  this.warnings = result.warnings;
15255
15948
  this.isolation = result.isolation;
15949
+ this.cpuQuotaApplied = result.cpuQuotaApplied;
15256
15950
  this.scheduleIsolationRetry();
15257
15951
  for (const warning of result.warnings) this.ports.log(warning);
15258
15952
  if (result.prunedNamespaces.length > 0) {
@@ -15409,6 +16103,11 @@ var ClusterDaemon = class {
15409
16103
  );
15410
16104
  }
15411
16105
  for (const failure of result.failures) this.ports.log(`Image prune failed \u2014 ${failure}`);
16106
+ if (result.pending.length > 0) {
16107
+ this.ports.log(
16108
+ `Image removal still in progress for ${result.pending.length} image(s); the next prune re-checks them.`
16109
+ );
16110
+ }
15412
16111
  } catch (error) {
15413
16112
  this.ports.log(
15414
16113
  `Skipping image prune: ${error instanceof Error ? error.message : String(error)}`
@@ -15447,6 +16146,7 @@ var ClusterDaemon = class {
15447
16146
  localImageHashes: this.localImageHashes,
15448
16147
  ...registryOn ? { localRegistry: this.registryWatch } : {},
15449
16148
  ...isolation.isolation ? { isolation: isolation.isolation } : {},
16149
+ cpuQuotaApplied: this.cpuQuotaApplied,
15450
16150
  k3sStoreBytes: this.measuredStoreBytes
15451
16151
  },
15452
16152
  inventory
@@ -15643,7 +16343,7 @@ var log = (line) => {
15643
16343
  function resolveVersion() {
15644
16344
  try {
15645
16345
  const url = new URL("../package.json", import.meta.url);
15646
- const pkg = JSON.parse(readFileSync5(url, "utf8"));
16346
+ const pkg = JSON.parse(readFileSync6(url, "utf8"));
15647
16347
  return pkg.version ?? "0.0.0";
15648
16348
  } catch {
15649
16349
  return "0.0.0";
@@ -15662,7 +16362,7 @@ function setupPorts(exec) {
15662
16362
  async function runSetup(exec) {
15663
16363
  await requireRoot(exec);
15664
16364
  await requireGuestVm(exec);
15665
- const env = readCliEnv();
16365
+ const env = readCliEnv(process.env, null);
15666
16366
  const ports = setupPorts(exec);
15667
16367
  const { cloudflaredBin } = await prepareMachine(ports);
15668
16368
  await enrollMachine(ports, env, cloudflaredBin);
@@ -15672,15 +16372,23 @@ async function runHostSetup(exec) {
15672
16372
  const { cloudflaredBin } = await prepareMachine(ports);
15673
16373
  let env;
15674
16374
  try {
15675
- env = readCliEnv();
16375
+ env = readCliEnv(process.env, null);
15676
16376
  } catch {
15677
16377
  const state = await readState(nodeFileSystem);
15678
16378
  log(
15679
16379
  state.enrolled ? `Prepared. This server is already linked as ${state.hostname}; its daemon keeps it current. To move it to another account, run: sudo conveyor-k3 host link --relink` : "Prepared. This server is not linked yet \u2014 run: sudo conveyor-k3 host link --name=<name>"
15680
16380
  );
16381
+ await reportCliInstall(exec);
15681
16382
  return;
15682
16383
  }
15683
16384
  await enrollMachine(ports, env, cloudflaredBin, { runtime: "host" });
16385
+ await reportCliInstall(exec);
16386
+ }
16387
+ async function reportCliInstall(exec) {
16388
+ const linked = await linkCliOnPath(exec);
16389
+ if (linked.problem) log(linked.problem);
16390
+ const hint = await npmCopyHint((path) => nodeFileSystem.readFile(path));
16391
+ if (hint) log(hint);
15684
16392
  }
15685
16393
  function hostPorts(exec) {
15686
16394
  return {
@@ -15755,6 +16463,8 @@ async function runStatus(exec, asJson, notEnrolledHint = "conveyor-k3 setup") {
15755
16463
  }
15756
16464
  const k3s = await exec("k3s", ["--version"]).catch(() => null);
15757
16465
  const version = k3s ? parseK3sVersion(k3s.stdout || k3s.stderr) : null;
16466
+ const bins = await resolveBinaries(exec, state, process.argv[1]);
16467
+ const cloudflaredUpdates = await cloudflaredUpdater(exec, bins.cloudflared);
15758
16468
  const report = {
15759
16469
  cliVersion: resolveVersion(),
15760
16470
  hostname: state.hostname,
@@ -15765,6 +16475,7 @@ async function runStatus(exec, asJson, notEnrolledHint = "conveyor-k3 setup") {
15765
16475
  lastWarmAt: state.lastWarmAt,
15766
16476
  lastPruneAt: state.lastPruneAt,
15767
16477
  runningConveyorPods: await countRunningConveyorPods(exec),
16478
+ cloudflaredUpdates,
15768
16479
  ...state.runtime ? { runtime: state.runtime } : {}
15769
16480
  };
15770
16481
  if (asJson) {
@@ -15778,6 +16489,7 @@ async function runStatus(exec, asJson, notEnrolledHint = "conveyor-k3 setup") {
15778
16489
  if (state.runtime) log(`runtime: ${state.runtime}`);
15779
16490
  log(`k3s: ${report.k3sVersion ?? "not detected"}`);
15780
16491
  for (const [unit, active] of Object.entries(units)) log(`${unit}: ${active}`);
16492
+ log(describeCloudflaredUpdater(cloudflaredUpdates));
15781
16493
  log(`Settings version applied: ${report.appliedSettingsVersion}`);
15782
16494
  log(`Conveyor pods running: ${report.runningConveyorPods}`);
15783
16495
  }