@rallycry/conveyor-k3 0.1.13 → 0.1.14

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
 
@@ -4519,7 +4557,7 @@ 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
+ 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, 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, 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, CARD_TYPE_SURFACE, surfaceTypes, BOARD_CARD_TYPES, REPORT_CARD_TYPES, DECISION_CARD_TYPES;
4523
4561
  var init_dist = __esm({
4524
4562
  "../shared/dist/index.js"() {
4525
4563
  "use strict";
@@ -4538,6 +4576,7 @@ var init_dist = __esm({
4538
4576
  init_zod();
4539
4577
  init_zod();
4540
4578
  init_zod();
4579
+ init_zod();
4541
4580
  init_shared();
4542
4581
  ACHIEVEMENT_RARITIES = [
4543
4582
  {
@@ -4645,6 +4684,15 @@ var init_dist = __esm({
4645
4684
  EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
4646
4685
  EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
4647
4686
  IDLE_HEARTBEAT_MS = 90 * 1e3;
4687
+ siteAssetExtension = external_exports.string().regex(/^[a-z0-9]{1,10}$/);
4688
+ RequestSiteAssetUploadRequestSchema = external_exports.object({
4689
+ projectId: external_exports.string(),
4690
+ siteId: external_exports.string().min(1).max(100),
4691
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
4692
+ ext: siteAssetExtension,
4693
+ sizeBytes: external_exports.number().int().positive()
4694
+ });
4695
+ ListSiteAssetTargetsRequestSchema = external_exports.object({ projectId: external_exports.string() });
4648
4696
  TurnEndToolCallSchema = external_exports.object({
4649
4697
  tool: external_exports.string(),
4650
4698
  input: external_exports.string().optional(),
@@ -6532,6 +6580,8 @@ var init_dist = __esm({
6532
6580
  anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
6533
6581
  anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
6534
6582
  ];
6583
+ PROJECT_ARCHIVE_RETENTION_DAYS = 30;
6584
+ RETENTION_MS = PROJECT_ARCHIVE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
6535
6585
  CLAUDESPACE_WORKLOAD_LABEL = "rc-workload";
6536
6586
  CLAUDESPACE_WORKLOAD_VALUE = "claudespace";
6537
6587
  CONVEYOR_POD_SELECTOR = `${CLAUDESPACE_WORKLOAD_LABEL}=${CLAUDESPACE_WORKLOAD_VALUE}`;
@@ -6556,6 +6606,8 @@ var init_dist = __esm({
6556
6606
  PERSONAL_LOCAL_REGISTRY_PORT = 5e3;
6557
6607
  PERSONAL_LOCAL_REGISTRY_HOST = `${PERSONAL_LOCAL_REGISTRY_NAME}.${PERSONAL_LOCAL_REGISTRY_NAMESPACE}.svc.cluster.local:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6558
6608
  PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT = `http://127.0.0.1:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6609
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET = "conveyor-registry-push";
6610
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY = "config.json";
6559
6611
  BAKE_COMPONENT_LABEL = "claudespace.rallycry.com/component";
6560
6612
  BAKE_COMPONENT_PREBAKE = "prebake";
6561
6613
  BAKE_COMPONENT_STATE_BAKE = "state-bake";
@@ -7616,20 +7668,6 @@ function renderVmTemplate(arch, resources, dnsScript, firmware) {
7616
7668
  provision: [{ mode: "system", script: dnsScript }]
7617
7669
  });
7618
7670
  }
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
7671
  function validateStartupMemory(memoryGiB, hostBytes = totalmem(), others = [], relaySlots) {
7634
7672
  const budgetGiB = memoryBudgetGiB(hostBytes) - reservedHostMemoryGiB(others);
7635
7673
  if (memoryGiB > maximumGuestMemoryGiBWithin(budgetGiB, relaySlots)) {
@@ -7754,7 +7792,7 @@ var init_host_env = __esm({
7754
7792
  });
7755
7793
 
7756
7794
  // src/vm/endpoints.ts
7757
- var CONTROL_PLANE_HOSTS, DEFAULT_ENDPOINTS, GUEST_ENROLL, GUEST_CLI, TUNNEL_TCP;
7795
+ var CONTROL_PLANE_HOSTS, DEFAULT_ENDPOINTS, GUEST_CLI, TUNNEL_TCP;
7758
7796
  var init_endpoints = __esm({
7759
7797
  "src/vm/endpoints.ts"() {
7760
7798
  "use strict";
@@ -7815,15 +7853,6 @@ var init_endpoints = __esm({
7815
7853
  host,
7816
7854
  ports: host.endsWith(".ubuntu.com") || host === "deb.debian.org" ? [80, 443] : [443]
7817
7855
  }));
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
7856
  GUEST_CLI = `set -euo pipefail
7828
7857
  set -a
7829
7858
  . /etc/conveyor/k3.env
@@ -8041,7 +8070,7 @@ var init_public_web = __esm({
8041
8070
  // src/vm/relay.ts
8042
8071
  import { kill } from "process";
8043
8072
  import { execFileSync } from "child_process";
8044
- import { lstatSync as lstatSync2, mkdirSync, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
8073
+ import { lstatSync as lstatSync2, mkdirSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
8045
8074
  import { join as join3 } from "path";
8046
8075
  import { Socket } from "net";
8047
8076
  function validPid2(value) {
@@ -8082,7 +8111,7 @@ function stoppedSlotRecord(root, slot) {
8082
8111
  }
8083
8112
  try {
8084
8113
  const record2 = JSON.parse(
8085
- readFileSync3(join3(path, "owner.json"), "utf8")
8114
+ readFileSync4(join3(path, "owner.json"), "utf8")
8086
8115
  );
8087
8116
  if (!validPid2(record2.pid)) throw new Error("missing process ID");
8088
8117
  return record2;
@@ -8153,7 +8182,7 @@ var init_host_packages = __esm({
8153
8182
  "firmware-x64": "ovmf",
8154
8183
  "firmware-arm64": "qemu-efi-aarch64"
8155
8184
  };
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.";
8185
+ 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
8186
  }
8158
8187
  });
8159
8188
 
@@ -8248,13 +8277,25 @@ async function findFirmware(qemu, arch, host, os) {
8248
8277
  const hint = packageHint(arch === "arm64" ? "firmware-arm64" : "firmware-x64", os);
8249
8278
  throw new Error(`Install QEMU UEFI firmware before creating the VM${hint ? `: ${hint}` : "."}`);
8250
8279
  }
8251
- async function preflightHost(exec, paths, resources, host = realHost()) {
8280
+ async function warnOnForeignPrefixOwner(exec, log2) {
8281
+ const result = await exec("/usr/bin/stat", ["-c", "%u %U %n", ...ROOT_OWNED_PREFIX]).catch(
8282
+ () => void 0
8283
+ );
8284
+ 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}`})`);
8285
+ if (foreign.length === 0) return;
8286
+ log2(
8287
+ `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(" ")}`
8288
+ );
8289
+ }
8290
+ async function preflightHost(exec, paths, resources, host = realHost(), log2 = () => {
8291
+ }) {
8252
8292
  if (!["darwin", "linux"].includes(host.platform) || host.uid === 0) {
8253
8293
  throw new Error("Run the VM launcher as an ordinary user on macOS or Linux, without sudo.");
8254
8294
  }
8255
8295
  const arch = vmArch(host.arch);
8256
8296
  const os = host.platform === "linux" ? await host.osRelease() : void 0;
8257
8297
  if (host.platform === "linux") await assertKvm(host);
8298
+ if (host.platform === "linux") await warnOnForeignPrefixOwner(exec, log2);
8258
8299
  const { lima, qemu, nc } = await locateTools(exec, paths, arch, host, os);
8259
8300
  await assertLimaVersion(exec, paths, lima, host);
8260
8301
  const acceleration = await runOrThrow(exec, qemu, ["-accel", "help"]);
@@ -8268,6 +8309,7 @@ async function preflightHost(exec, paths, resources, host = realHost()) {
8268
8309
  const firmware = await findFirmware(qemu, arch, host, os);
8269
8310
  return { arch, lima, qemu, nc, firmware };
8270
8311
  }
8312
+ var ROOT_OWNED_PREFIX;
8271
8313
  var init_preflight = __esm({
8272
8314
  "src/vm/preflight.ts"() {
8273
8315
  "use strict";
@@ -8275,16 +8317,17 @@ var init_preflight = __esm({
8275
8317
  init_config();
8276
8318
  init_host_env();
8277
8319
  init_host_packages();
8320
+ ROOT_OWNED_PREFIX = ["/usr/local", "/usr/local/bin", "/usr/local/share"];
8278
8321
  }
8279
8322
  });
8280
8323
 
8281
8324
  // src/vm/host-runtime.ts
8282
8325
  import { readFile as readFile5 } from "fs/promises";
8283
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
8326
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
8284
8327
  import { join as join5 } from "path";
8285
8328
  import { fileURLToPath } from "url";
8286
8329
  function hashText(value) {
8287
- return createHash2("sha256").update(value).digest("hex");
8330
+ return createHash4("sha256").update(value).digest("hex");
8288
8331
  }
8289
8332
  function newVmRunId() {
8290
8333
  return randomUUID2();
@@ -8824,7 +8867,6 @@ __export(manager_exports, {
8824
8867
  configureVm: () => configureVm,
8825
8868
  createVm: () => createVm,
8826
8869
  denyEndpoint: () => denyEndpoint,
8827
- enrollVm: () => enrollVm,
8828
8870
  enrollVmBootstrap: () => enrollVmBootstrap,
8829
8871
  ensureVmDevicePublicKey: () => ensureVmDevicePublicKey,
8830
8872
  internetMode: () => internetMode,
@@ -8850,16 +8892,15 @@ async function saveState(paths, state) {
8850
8892
  await atomicPrivateWrite(paths.state, `${JSON.stringify(state, null, 2)}
8851
8893
  `);
8852
8894
  }
8853
- async function createVm(ports, resources, installUrl) {
8895
+ async function createVm(ports, resources) {
8854
8896
  validateResources(resources, void 0, await ports.otherAllocations?.() ?? []);
8855
8897
  const { paths, exec, log: log2 } = ports;
8856
- const runtime = await preflightHost(exec, paths, resources);
8898
+ const runtime = await preflightHost(exec, paths, resources, void 0, log2);
8857
8899
  const endpoints = [
8858
8900
  ...DEFAULT_ENDPOINTS,
8859
8901
  { host: "region1.v2.argotunnel.com", ports: [7844] },
8860
8902
  { host: "region2.v2.argotunnel.com", ports: [7844] }
8861
8903
  ];
8862
- if (installUrl) endpoints.push({ host: parseInstallUrl(installUrl).hostname, ports: [443] });
8863
8904
  const network = withHostSettings(await resolveNetwork(endpoints), {});
8864
8905
  const template = renderVmTemplate(
8865
8906
  runtime.arch,
@@ -8978,29 +9019,6 @@ async function beginEnrollment(ports, state) {
8978
9019
  delete state.linkedDisplayName;
8979
9020
  await saveState(ports.paths, state);
8980
9021
  }
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
9022
  async function enrollVmBootstrap(ports, state, bootstrapScript) {
9005
9023
  if (bootstrapScript.length === 0 || bootstrapScript.length > 1e6)
9006
9024
  throw new Error("Pairing bootstrap payload is invalid.");
@@ -9556,7 +9574,7 @@ done
9556
9574
  });
9557
9575
 
9558
9576
  // src/vm/dev-artifact.ts
9559
- import { createHash as createHash3 } from "crypto";
9577
+ import { createHash as createHash5 } from "crypto";
9560
9578
  import { readFile as readFile7, stat } from "fs/promises";
9561
9579
  async function readDevArtifact() {
9562
9580
  const file = process.env.CONVEYOR_DEV_ARTIFACT;
@@ -9570,7 +9588,7 @@ async function readDevArtifact() {
9570
9588
  if (!info.isFile() || info.size > 32 * 1024 * 1024)
9571
9589
  throw new Error("Development artifact must be a file of at most 32 MiB.");
9572
9590
  const bytes = await readFile7(file);
9573
- if (bytes.length > 32 * 1024 * 1024 || createHash3("sha256").update(bytes).digest("hex") !== digest)
9591
+ if (bytes.length > 32 * 1024 * 1024 || createHash5("sha256").update(bytes).digest("hex") !== digest)
9574
9592
  throw new Error("Development artifact checksum mismatch.");
9575
9593
  return { bytes, sha256: digest };
9576
9594
  }
@@ -10665,14 +10683,6 @@ function commandPorts(exec, instance, log2, layout) {
10665
10683
  otherAllocations: async () => (await readOtherAllocations(paths, layout)).map((other) => other.resources)
10666
10684
  };
10667
10685
  }
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
10686
  function resourceFlag(flags, key, fallback) {
10677
10687
  if (flags[key] === void 0) return fallback;
10678
10688
  if (typeof flags[key] !== "string" || !/^\d+$/.test(flags[key]))
@@ -10699,7 +10709,7 @@ function setupDefaults(others, host = hostCapacity()) {
10699
10709
  diskGiB: 64
10700
10710
  };
10701
10711
  }
10702
- async function createInstance(ports, parsed, url) {
10712
+ async function createInstance(ports, parsed) {
10703
10713
  if (ports.paths.instance !== DEFAULT_INSTANCE) assertInstancePathFits(ports.paths);
10704
10714
  return await withAdmissionLock(async (release) => {
10705
10715
  const defaults = setupDefaults(await readOtherAllocations(ports.paths, ports.layout));
@@ -10709,8 +10719,7 @@ async function createInstance(ports, parsed, url) {
10709
10719
  cpus: resourceFlag(parsed.flags, "cpus", defaults.cpus),
10710
10720
  memoryGiB: resourceFlag(parsed.flags, "memory", defaults.memoryGiB),
10711
10721
  diskGiB: resourceFlag(parsed.flags, "disk", defaults.diskGiB)
10712
- },
10713
- url
10722
+ }
10714
10723
  );
10715
10724
  }, ports.layout.base);
10716
10725
  }
@@ -10745,17 +10754,12 @@ function relinkNotice(state, relink) {
10745
10754
  );
10746
10755
  }
10747
10756
  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
10757
  if (state && RESOURCE_FLAGS.some((key) => parsed.flags[key] !== void 0)) {
10751
10758
  throw new Error("This VM already exists; setup resource flags only apply at creation.");
10752
10759
  }
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);
10760
+ if (!state) state = await createInstance(ports, parsed);
10756
10761
  await startVm(ports, state);
10757
- if (url) await enrollVm(ports, state, url);
10758
- else ports.log(setupNextStep(state, ports.paths.instance));
10762
+ ports.log(setupNextStep(state, ports.paths.instance));
10759
10763
  }
10760
10764
  function setupNextStep(state, instance) {
10761
10765
  const link = instance === DEFAULT_INSTANCE ? "conveyor-k3 vm link" : `conveyor-k3 vm link --instance=${instance}`;
@@ -10932,7 +10936,7 @@ var init_service_unit = __esm({
10932
10936
  });
10933
10937
 
10934
10938
  // src/vm/service-control.ts
10935
- import { constants as constants2, readFileSync as readFileSync4 } from "fs";
10939
+ import { constants as constants2, readFileSync as readFileSync5 } from "fs";
10936
10940
  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
10941
  import { tmpdir as tmpdir2, userInfo as userInfo3 } from "os";
10938
10942
  import { dirname as dirname3, join as join12 } from "path";
@@ -10966,7 +10970,7 @@ function realServiceHost(exec, log2) {
10966
10970
  function cliVersion() {
10967
10971
  try {
10968
10972
  const url = new URL("../package.json", import.meta.url);
10969
- return JSON.parse(readFileSync4(url, "utf8")).version ?? "<version>";
10973
+ return JSON.parse(readFileSync5(url, "utf8")).version ?? "<version>";
10970
10974
  } catch {
10971
10975
  return "<version>";
10972
10976
  }
@@ -11616,7 +11620,7 @@ function validateFlags(parsed) {
11616
11620
  const action = actionOf(parsed);
11617
11621
  const allowed = {
11618
11622
  ...BAKE_VERBS,
11619
- setup: ["install-url-stdin", "cpus", "memory", "disk", "relink"],
11623
+ setup: ["cpus", "memory", "disk"],
11620
11624
  start: [],
11621
11625
  configure: [...RESOURCE_FLAGS2, ...CAPACITY_FLAGS],
11622
11626
  stop: ["force"],
@@ -11640,7 +11644,7 @@ function validateFlags(parsed) {
11640
11644
  for (const flag of Object.keys(parsed.flags)) {
11641
11645
  if (!accepted.includes(flag)) throw new Error(`Unknown flag for vm ${action}: --${flag}`);
11642
11646
  }
11643
- const booleanFlags = /* @__PURE__ */ new Set(["install-url-stdin", "force", "json", "purge", "yes", "relink"]);
11647
+ const booleanFlags = /* @__PURE__ */ new Set(["force", "json", "purge", "yes", "relink"]);
11644
11648
  for (const [flag, value] of Object.entries(parsed.flags)) {
11645
11649
  if (booleanFlags.has(flag) && value !== true) throw new Error(`Use --${flag} without a value.`);
11646
11650
  }
@@ -11761,7 +11765,7 @@ var init_command = __esm({
11761
11765
  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
11766
  Every command but list acts on one VM: --instance=<name> selects a named VM,
11763
11767
  and omitting it selects this user's default VM.
11764
- setup [--install-url-stdin] [--relink] [--cpus=4] [--memory=8] [--disk=64]
11768
+ setup [--cpus=4] [--memory=8] [--disk=64]
11765
11769
  configure --cpus=4 --memory=8 [--disk=64] Changes allocation while stopped.
11766
11770
  configure --profile=default|server [--relay-slots=96] [--relay-max-gib=8]
11767
11771
  [--disk-mbps=1000] [--disk-iops=20000] Changes network and disk capacity while stopped.
@@ -11795,6 +11799,7 @@ Run without sudo. Requires Lima 2.2+ and QEMU with HVF (macOS) or KVM (Linux).
11795
11799
  var command_exports2 = {};
11796
11800
  __export(command_exports2, {
11797
11801
  HOST_HELP: () => HOST_HELP,
11802
+ hostNextStep: () => hostNextStep,
11798
11803
  runHostCommand: () => runHostCommand
11799
11804
  });
11800
11805
  import { hostname } from "os";
@@ -11848,6 +11853,11 @@ async function prepareHostLink(parsed, ports, log2) {
11848
11853
  }
11849
11854
  return { api, name: name2 };
11850
11855
  }
11856
+ function hostNextStep(state) {
11857
+ if (state.k3sInstalledByConveyor === true)
11858
+ 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.";
11859
+ 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.";
11860
+ }
11851
11861
  async function completeHostLink(api, pairing, log2, ports) {
11852
11862
  const poll = () => pollPairing(api, pairing.deviceCode);
11853
11863
  const retry = () => log2("Securing connection\u2026 retrying pairing status.");
@@ -11880,6 +11890,7 @@ async function completeHostLink(api, pairing, log2, ports) {
11880
11890
  log2(
11881
11891
  `Linked to ${completed.accountLabel ?? "Conveyor"}${completed.displayName ? ` as ${completed.displayName}` : ""}.`
11882
11892
  );
11893
+ log2(hostNextStep(await ports.readState()));
11883
11894
  }
11884
11895
  async function hostLink(parsed, log2, ports) {
11885
11896
  const { api, name: name2 } = await prepareHostLink(parsed, ports, log2);
@@ -11944,8 +11955,10 @@ Usage: conveyor-k3 host <setup|link|status|uninstall>
11944
11955
  uninstall [--purge-k3s --yes] Remove Conveyor's units, state, and device key. k3s,
11945
11956
  your config, and your workloads stay unless you purge.
11946
11957
  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.
11958
+ no VM boundary; Pod Security and an egress policy stand in for one. After link, on a
11959
+ k3s you installed yourself: set Keep for this machine in Conveyor and Save, then run
11960
+ sudo systemctl restart k3s once. Conveyor never restarts a k3s it did not install.
11961
+ See the guide's "Link a dedicated server (host)" section.
11949
11962
  `;
11950
11963
  ALLOWED_FLAGS = /* @__PURE__ */ new Map([
11951
11964
  ["setup", []],
@@ -12026,7 +12039,7 @@ async function writeWorkloads(data) {
12026
12039
 
12027
12040
  // src/cli.ts
12028
12041
  init_exec();
12029
- import { readFileSync as readFileSync5 } from "fs";
12042
+ import { readFileSync as readFileSync6 } from "fs";
12030
12043
 
12031
12044
  // src/lib/fs-port.ts
12032
12045
  import { chmod, mkdir as mkdir2, readFile, rm as rm2, writeFile as writeFile2 } from "fs/promises";
@@ -12040,7 +12053,7 @@ var nodeFileSystem = {
12040
12053
  },
12041
12054
  async writeFile(path, content, mode) {
12042
12055
  await mkdir2(path.slice(0, path.lastIndexOf("/")), { recursive: true });
12043
- await writeFile2(path, content, "utf8");
12056
+ await writeFile2(path, content, { encoding: "utf8", ...mode === void 0 ? {} : { mode } });
12044
12057
  if (mode !== void 0) await chmod(path, mode);
12045
12058
  },
12046
12059
  async removeFile(path) {
@@ -12091,11 +12104,235 @@ init_dist();
12091
12104
  // src/lib/local-registry.ts
12092
12105
  init_dist();
12093
12106
  init_paths();
12107
+ import { createHash as createHash2 } from "crypto";
12108
+
12109
+ // src/lib/registry-credentials.ts
12110
+ import { createHash, randomBytes } from "crypto";
12111
+
12112
+ // src/lib/bcrypt.ts
12113
+ var P_WORDS = 18;
12114
+ var S_WORDS = 1024;
12115
+ var MAGIC = "OrpheanBeholderScryDoubt";
12116
+ var ALPHABET = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
12117
+ var MAX_KEY_BYTES = 72;
12118
+ var BCRYPT_SALT_BYTES = 16;
12119
+ function scaledAtanInverse(x, one) {
12120
+ const xSquared = x * x;
12121
+ let power = one / x;
12122
+ let sum = 0n;
12123
+ for (let n = 0n; power !== 0n; n++) {
12124
+ const term = power / (2n * n + 1n);
12125
+ sum = n % 2n === 0n ? sum + term : sum - term;
12126
+ power /= xSquared;
12127
+ }
12128
+ return sum;
12129
+ }
12130
+ function piFractionWords(count) {
12131
+ const guard = 64n;
12132
+ const bits = BigInt(count * 32) + guard;
12133
+ const one = 1n << bits;
12134
+ const pi = 16n * scaledAtanInverse(5n, one) - 4n * scaledAtanInverse(239n, one);
12135
+ const fraction = pi - 3n * one >> guard;
12136
+ const words = new Uint32Array(count);
12137
+ for (let index = 0; index < count; index++) {
12138
+ words[index] = Number(fraction >> BigInt(32 * (count - 1 - index)) & 0xffffffffn);
12139
+ }
12140
+ return words;
12141
+ }
12142
+ var initialState = null;
12143
+ function blowfishInitialState() {
12144
+ if (!initialState) {
12145
+ const words = piFractionWords(P_WORDS + S_WORDS);
12146
+ initialState = { p: words.slice(0, P_WORDS), s: words.slice(P_WORDS) };
12147
+ }
12148
+ return initialState;
12149
+ }
12150
+ function word(array, index) {
12151
+ return array[index] ?? 0;
12152
+ }
12153
+ function round(s, x) {
12154
+ const mixed = word(s, x >>> 24) + word(s, 256 + (x >>> 16 & 255)) ^ word(s, 512 + (x >>> 8 & 255));
12155
+ return mixed + word(s, 768 + (x & 255)) | 0;
12156
+ }
12157
+ function encipher(state, data, offset) {
12158
+ const { p, s } = state;
12159
+ let left = word(data, offset) ^ word(p, 0);
12160
+ let right = word(data, offset + 1);
12161
+ for (let index = 1; index <= 16; index += 2) {
12162
+ right ^= round(s, left) ^ word(p, index);
12163
+ left ^= round(s, right) ^ word(p, index + 1);
12164
+ }
12165
+ data[offset] = right ^ word(p, 17);
12166
+ data[offset + 1] = left;
12167
+ }
12168
+ function wordStream(bytes) {
12169
+ let position = 0;
12170
+ return () => {
12171
+ let value = 0;
12172
+ for (let index = 0; index < 4; index++) {
12173
+ value = value << 8 | (bytes[position] ?? 0);
12174
+ position = (position + 1) % bytes.length;
12175
+ }
12176
+ return value >>> 0;
12177
+ };
12178
+ }
12179
+ function expandKey(state, key, salt) {
12180
+ const nextKeyWord = wordStream(key);
12181
+ for (let index = 0; index < P_WORDS; index++) {
12182
+ state.p[index] = word(state.p, index) ^ nextKeyWord();
12183
+ }
12184
+ const nextSaltWord = salt ? wordStream(salt) : () => 0;
12185
+ const block = new Uint32Array(2);
12186
+ const fill = (target2, length) => {
12187
+ for (let index = 0; index < length; index += 2) {
12188
+ block[0] = word(block, 0) ^ nextSaltWord();
12189
+ block[1] = word(block, 1) ^ nextSaltWord();
12190
+ encipher(state, block, 0);
12191
+ target2[index] = word(block, 0);
12192
+ target2[index + 1] = word(block, 1);
12193
+ }
12194
+ };
12195
+ fill(state.p, P_WORDS);
12196
+ fill(state.s, S_WORDS);
12197
+ }
12198
+ function encodeBase64(bytes, length) {
12199
+ let out = "";
12200
+ let offset = 0;
12201
+ while (offset < length) {
12202
+ let first = bytes[offset++] ?? 0;
12203
+ out += ALPHABET[first >> 2];
12204
+ first = (first & 3) << 4;
12205
+ if (offset >= length) {
12206
+ out += ALPHABET[first];
12207
+ break;
12208
+ }
12209
+ let second = bytes[offset++] ?? 0;
12210
+ out += ALPHABET[first | second >> 4];
12211
+ first = (second & 15) << 2;
12212
+ if (offset >= length) {
12213
+ out += ALPHABET[first];
12214
+ break;
12215
+ }
12216
+ second = bytes[offset++] ?? 0;
12217
+ out += ALPHABET[first | second >> 6];
12218
+ out += ALPHABET[second & 63];
12219
+ }
12220
+ return out;
12221
+ }
12222
+ function bcryptHash(password, cost, salt) {
12223
+ if (!Number.isInteger(cost) || cost < 4 || cost > 31) {
12224
+ throw new Error(`bcrypt cost must be an integer from 4 to 31, not ${cost}`);
12225
+ }
12226
+ if (salt.length !== BCRYPT_SALT_BYTES) {
12227
+ throw new Error(`bcrypt needs a ${BCRYPT_SALT_BYTES}-byte salt`);
12228
+ }
12229
+ const passwordBytes = new TextEncoder().encode(password).slice(0, MAX_KEY_BYTES);
12230
+ const key = new Uint8Array(passwordBytes.length + 1);
12231
+ key.set(passwordBytes);
12232
+ const base = blowfishInitialState();
12233
+ const state = { p: base.p.slice(), s: base.s.slice() };
12234
+ expandKey(state, key, salt);
12235
+ for (let rounds = 2 ** cost; rounds > 0; rounds--) {
12236
+ expandKey(state, key, null);
12237
+ expandKey(state, salt, null);
12238
+ }
12239
+ const magic = new TextEncoder().encode(MAGIC);
12240
+ const nextMagicWord = wordStream(magic);
12241
+ const block = Uint32Array.from({ length: 6 }, () => nextMagicWord());
12242
+ for (let pass = 0; pass < 64; pass++) {
12243
+ for (let offset = 0; offset < 6; offset += 2) encipher(state, block, offset);
12244
+ }
12245
+ const digest = new Uint8Array(24);
12246
+ block.forEach((value, index) => {
12247
+ digest[index * 4] = value >>> 24;
12248
+ digest[index * 4 + 1] = value >>> 16 & 255;
12249
+ digest[index * 4 + 2] = value >>> 8 & 255;
12250
+ digest[index * 4 + 3] = value & 255;
12251
+ });
12252
+ const costText = String(cost).padStart(2, "0");
12253
+ return `$2y$${costText}$${encodeBase64(salt, BCRYPT_SALT_BYTES)}${encodeBase64(digest, 23)}`;
12254
+ }
12255
+
12256
+ // src/lib/registry-credentials.ts
12257
+ init_paths();
12258
+ var LOCAL_REGISTRY_USERNAME = "conveyor";
12259
+ var BCRYPT_COST = 5;
12260
+ var USERNAME = /^[a-z][a-z0-9-]{0,31}$/;
12261
+ var PASSWORD = /^[A-Za-z0-9_-]{32,128}$/;
12262
+ function generateRegistryCredentials(random = randomBytes) {
12263
+ const username = LOCAL_REGISTRY_USERNAME;
12264
+ const password = Buffer.from(random(32)).toString("base64url");
12265
+ const hash = bcryptHash(password, BCRYPT_COST, random(BCRYPT_SALT_BYTES));
12266
+ return { username, password, htpasswd: `${username}:${hash}` };
12267
+ }
12268
+ function parseRegistryCredentials(raw) {
12269
+ if (!raw) return null;
12270
+ try {
12271
+ const value = JSON.parse(raw);
12272
+ const { username, password, htpasswd } = value ?? {};
12273
+ if (typeof username !== "string" || !USERNAME.test(username)) return null;
12274
+ if (typeof password !== "string" || !PASSWORD.test(password)) return null;
12275
+ if (typeof htpasswd !== "string" || !htpasswd.startsWith(`${username}:$2`)) return null;
12276
+ if (htpasswd.includes("\n")) return null;
12277
+ return { username, password, htpasswd };
12278
+ } catch {
12279
+ return null;
12280
+ }
12281
+ }
12282
+ async function loadOrCreateRegistryCredentials(fs2, random) {
12283
+ const stored = parseRegistryCredentials(await fs2.readFile(LOCAL_REGISTRY_CREDENTIALS_FILE));
12284
+ if (stored) return stored;
12285
+ const created = generateRegistryCredentials(random);
12286
+ await fs2.writeFile(LOCAL_REGISTRY_CREDENTIALS_FILE, `${JSON.stringify(created)}
12287
+ `, 384);
12288
+ return created;
12289
+ }
12290
+ function credentialFingerprint(credentials) {
12291
+ return createHash("sha256").update(`${credentials.username}:${credentials.password}`).digest("hex").slice(0, 16);
12292
+ }
12293
+ function basicAuthorization(credentials) {
12294
+ return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64")}`;
12295
+ }
12296
+ function registryDockerConfig(credentials, host) {
12297
+ const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64");
12298
+ return JSON.stringify({ auths: { [host]: { auth } } });
12299
+ }
12300
+ function parseRegistryAuthState(raw) {
12301
+ if (!raw) return null;
12302
+ try {
12303
+ const value = JSON.parse(raw);
12304
+ if (typeof value?.credential !== "string") return null;
12305
+ const writtenAt = typeof value.writtenAt === "string" && !Number.isNaN(Date.parse(value.writtenAt)) ? value.writtenAt : null;
12306
+ return { credential: value.credential, writtenAt, enforced: value.enforced === true };
12307
+ } catch {
12308
+ return null;
12309
+ }
12310
+ }
12311
+ async function registryAuthEnforced(readFile9) {
12312
+ return parseRegistryAuthState(await readFile9(LOCAL_REGISTRY_AUTH_STATE_FILE))?.enforced === true;
12313
+ }
12314
+ function withRegistryCredentials(fetchImpl, readFile9) {
12315
+ let credentials = null;
12316
+ return async (url, init) => {
12317
+ credentials ??= readFile9(LOCAL_REGISTRY_CREDENTIALS_FILE).then(parseRegistryCredentials);
12318
+ const stored = await credentials;
12319
+ if (!stored) return fetchImpl(url, init);
12320
+ const headers = new Headers(init?.headers);
12321
+ headers.set("Authorization", basicAuthorization(stored));
12322
+ return fetchImpl(url, { ...init, headers });
12323
+ };
12324
+ }
12325
+
12326
+ // src/lib/local-registry.ts
12094
12327
  var LOCAL_REGISTRY_IMAGE = "docker.io/library/registry:3.1.2@sha256:c87f33837722a100572e95d7dc4bf539fc42cf68202b13c3bc03c0ff54c3a649";
12095
12328
  var LOCAL_REGISTRY_CONFIG = "/etc/distribution/config.yml";
12096
12329
  var LOCAL_REGISTRY_GC_JOB = "conveyor-registry-gc";
12330
+ var LOCAL_REGISTRY_HTPASSWD_SECRET = "conveyor-registry-htpasswd";
12331
+ var HTPASSWD_KEY = "htpasswd";
12332
+ var AUTH_MOUNT = "/etc/distribution/auth";
12097
12333
  var LOCAL_REGISTRY_SELECTOR = { "app.kubernetes.io/name": PERSONAL_LOCAL_REGISTRY_NAME };
12098
12334
  var MANAGED_BY = { "app.kubernetes.io/managed-by": "conveyor-k3" };
12335
+ var AUTH_CHECKSUM_ANNOTATION = "conveyor.rallycry.com/registry-auth";
12099
12336
  var DATA_MOUNT = "/var/lib/registry";
12100
12337
  var REGISTRY_ENV = [
12101
12338
  { name: "REGISTRY_STORAGE_DELETE_ENABLED", value: "true" },
@@ -12110,6 +12347,23 @@ var VOLUME_MOUNTS = [
12110
12347
  { name: "data", mountPath: DATA_MOUNT },
12111
12348
  { name: "tmp", mountPath: "/tmp" }
12112
12349
  ];
12350
+ var AUTH_ENV = [
12351
+ { name: "REGISTRY_AUTH", value: "htpasswd" },
12352
+ { name: "REGISTRY_AUTH_HTPASSWD_REALM", value: "conveyor" },
12353
+ { name: "REGISTRY_AUTH_HTPASSWD_PATH", value: `${AUTH_MOUNT}/${HTPASSWD_KEY}` }
12354
+ ];
12355
+ var AUTH_VOLUME = {
12356
+ name: "auth",
12357
+ secret: {
12358
+ secretName: LOCAL_REGISTRY_HTPASSWD_SECRET,
12359
+ items: [{ key: HTPASSWD_KEY, path: HTPASSWD_KEY }],
12360
+ defaultMode: 256
12361
+ }
12362
+ };
12363
+ var AUTH_VOLUME_MOUNT = { name: "auth", mountPath: AUTH_MOUNT, readOnly: true };
12364
+ function htpasswdChecksum(htpasswd) {
12365
+ return createHash2("sha256").update(htpasswd).digest("hex").slice(0, 16);
12366
+ }
12113
12367
  var VOLUMES = [
12114
12368
  { name: "data", hostPath: { path: LOCAL_REGISTRY_DATA_DIR, type: "Directory" } },
12115
12369
  { name: "tmp", emptyDir: { sizeLimit: "64Mi" } }
@@ -12146,7 +12400,34 @@ function registryNetworkPolicy(metadata) {
12146
12400
  }
12147
12401
  };
12148
12402
  }
12149
- function renderLocalRegistryManifests() {
12403
+ function renderLocalRegistrySecrets(credentials) {
12404
+ const encode = (value) => Buffer.from(value).toString("base64");
12405
+ const secret = (namespace, name2, key, value) => ({
12406
+ apiVersion: "v1",
12407
+ kind: "Secret",
12408
+ metadata: { name: name2, namespace, labels: { ...MANAGED_BY } },
12409
+ type: "Opaque",
12410
+ data: { [key]: encode(value) }
12411
+ });
12412
+ const items = [
12413
+ secret(
12414
+ PERSONAL_LOCAL_REGISTRY_NAMESPACE,
12415
+ LOCAL_REGISTRY_HTPASSWD_SECRET,
12416
+ HTPASSWD_KEY,
12417
+ `${credentials.htpasswd}
12418
+ `
12419
+ ),
12420
+ secret(
12421
+ PERSONAL_BAKE_NAMESPACE,
12422
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET,
12423
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY,
12424
+ registryDockerConfig(credentials, PERSONAL_LOCAL_REGISTRY_HOST)
12425
+ )
12426
+ ];
12427
+ return `${JSON.stringify({ apiVersion: "v1", kind: "List", items }, null, 2)}
12428
+ `;
12429
+ }
12430
+ function renderLocalRegistryManifests(auth = null) {
12150
12431
  const metadata = {
12151
12432
  name: PERSONAL_LOCAL_REGISTRY_NAME,
12152
12433
  namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE,
@@ -12162,15 +12443,19 @@ function renderLocalRegistryManifests() {
12162
12443
  strategy: { type: "Recreate" },
12163
12444
  selector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
12164
12445
  template: {
12165
- metadata: { labels: metadata.labels },
12446
+ metadata: {
12447
+ labels: metadata.labels,
12448
+ ...auth ? { annotations: { [AUTH_CHECKSUM_ANNOTATION]: htpasswdChecksum(auth.htpasswd) } } : {}
12449
+ },
12166
12450
  spec: {
12167
12451
  ...POD_BASE,
12452
+ ...auth ? { volumes: [...VOLUMES, AUTH_VOLUME] } : {},
12168
12453
  containers: [
12169
12454
  {
12170
12455
  name: "registry",
12171
12456
  image: LOCAL_REGISTRY_IMAGE,
12172
12457
  imagePullPolicy: "IfNotPresent",
12173
- env: REGISTRY_ENV,
12458
+ env: auth ? [...REGISTRY_ENV, ...AUTH_ENV] : REGISTRY_ENV,
12174
12459
  ports: [
12175
12460
  {
12176
12461
  name: "registry",
@@ -12182,8 +12467,10 @@ function renderLocalRegistryManifests() {
12182
12467
  protocol: "TCP"
12183
12468
  }
12184
12469
  ],
12470
+ // A TCP check: once the credential is enforced, `/v2/` answers
12471
+ // 401 to a probe that does not carry it.
12185
12472
  readinessProbe: {
12186
- httpGet: { path: "/v2/", port: PERSONAL_LOCAL_REGISTRY_PORT },
12473
+ tcpSocket: { port: PERSONAL_LOCAL_REGISTRY_PORT },
12187
12474
  periodSeconds: 10
12188
12475
  },
12189
12476
  resources: {
@@ -12191,7 +12478,7 @@ function renderLocalRegistryManifests() {
12191
12478
  limits: { memory: "512Mi" }
12192
12479
  },
12193
12480
  securityContext: CONTAINER_SECURITY,
12194
- volumeMounts: VOLUME_MOUNTS
12481
+ volumeMounts: auth ? [...VOLUME_MOUNTS, AUTH_VOLUME_MOUNT] : VOLUME_MOUNTS
12195
12482
  }
12196
12483
  ]
12197
12484
  }
@@ -12282,11 +12569,18 @@ async function registryAnswers(fetchImpl = fetch) {
12282
12569
  }
12283
12570
  async function localRegistryReady(ports) {
12284
12571
  const [answers, mirrored] = await Promise.all([
12285
- registryAnswers(ports.fetch),
12572
+ registryAnswers(withRegistryCredentials(ports.fetch ?? fetch, ports.readFile)),
12286
12573
  mirrorLoaded(ports.readFile)
12287
12574
  ]);
12288
12575
  return answers && mirrored;
12289
12576
  }
12577
+ async function localRegistryStatus(ports) {
12578
+ const [ready, authEnforced] = await Promise.all([
12579
+ localRegistryReady(ports),
12580
+ registryAuthEnforced(ports.readFile)
12581
+ ]);
12582
+ return { ready, authEnforced };
12583
+ }
12290
12584
 
12291
12585
  // src/daemon/registry-gc.ts
12292
12586
  init_paths();
@@ -12683,7 +12977,10 @@ async function collectRegistryGarbage(ports, options) {
12683
12977
  blobsCollected: false
12684
12978
  });
12685
12979
  const now = ports.now ?? Date.now;
12686
- const fetchImpl = ports.fetch ?? fetch;
12980
+ const fetchImpl = withRegistryCredentials(
12981
+ ports.fetch ?? fetch,
12982
+ ports.readFile ?? ((path) => nodeFileSystem.readFile(path))
12983
+ );
12687
12984
  const retain = await readRetainList(ports);
12688
12985
  if (typeof retain === "string") return skip(retain);
12689
12986
  const blocked = await registryBlocker(ports, now());
@@ -12750,6 +13047,16 @@ function upsertRegistryAuth(file, host, auth) {
12750
13047
  next.configs = configs;
12751
13048
  return next;
12752
13049
  }
13050
+ function removeRegistryAuth(file, host) {
13051
+ const existing = file.configs?.[host];
13052
+ if (!existing?.auth) return file;
13053
+ const { auth: _auth, ...rest } = existing;
13054
+ const { [host]: _removed, ...others } = file.configs ?? {};
13055
+ const configs = Object.keys(rest).length > 0 ? { ...others, [host]: rest } : others;
13056
+ const next = { ...file, configs };
13057
+ if (Object.keys(configs).length === 0) delete next.configs;
13058
+ return next;
13059
+ }
12753
13060
  function mirrorEndpoints(file, host) {
12754
13061
  const entry = file.mirrors?.[host];
12755
13062
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
@@ -12966,6 +13273,126 @@ var GRANTED_COMMAND_NAMES = LOCAL_BAKE_ROOT_COMMANDS.map((command) => command.na
12966
13273
  // src/commands/uninstall.ts
12967
13274
  init_dist();
12968
13275
 
13276
+ // src/daemon/pod-cpu-quota.ts
13277
+ init_safe_arg();
13278
+ var POD_SLICE = "kubepods.slice";
13279
+ function cpuMillicores(quantity) {
13280
+ const checked = assertQuantity(quantity, "reserveCpu");
13281
+ const match = /^(\d+(?:\.\d+)?)(m?)$/.exec(checked);
13282
+ if (!match?.[1])
13283
+ throw new Error(`Invalid reserveCpu: ${JSON.stringify(checked)} is not a CPU amount`);
13284
+ const amount = Number.parseFloat(match[1]);
13285
+ return Math.round(match[2] === "m" ? amount : amount * 1e3);
13286
+ }
13287
+ function podCpuQuotaPercent(cores, reservedMillicores) {
13288
+ const left = cores * 1e3 - reservedMillicores;
13289
+ if (left < 10) return null;
13290
+ return Math.floor(left / 10);
13291
+ }
13292
+ function setQuota(exec, value) {
13293
+ return exec("systemctl", ["set-property", POD_SLICE, `CPUQuota=${value}`]);
13294
+ }
13295
+ async function liftPodCpuQuota(exec) {
13296
+ const shown = await exec("systemctl", ["show", POD_SLICE, "-p", "CPUQuotaPerSecUSec", "--value"]);
13297
+ if (shown.exitCode !== 0 || ["", "infinity"].includes(shown.stdout.trim())) return null;
13298
+ const cleared = await setQuota(exec, "");
13299
+ return cleared.exitCode === 0 ? null : cleared.stderr.trim().slice(0, 200);
13300
+ }
13301
+ async function reconcilePodCpuQuota(exec, desired, warnings) {
13302
+ const reserve = desired.kubelet.systemReserved?.cpu;
13303
+ if (!reserve) {
13304
+ const failure = await liftPodCpuQuota(exec);
13305
+ if (failure) warnings.push(`Could not lift the CPU cap on ${POD_SLICE}: ${failure}`);
13306
+ return void 0;
13307
+ }
13308
+ const reserved = cpuMillicores(reserve);
13309
+ const state = await exec("systemctl", ["show", POD_SLICE, "-p", "ActiveState", "--value"]);
13310
+ if (state.exitCode !== 0 || state.stdout.trim() !== "active") {
13311
+ warnings.push(
13312
+ `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.`
13313
+ );
13314
+ return false;
13315
+ }
13316
+ const nproc = await exec("nproc", []);
13317
+ const cores = Number.parseInt(nproc.stdout.trim(), 10);
13318
+ if (nproc.exitCode !== 0 || !Number.isInteger(cores) || cores <= 0) {
13319
+ warnings.push(
13320
+ "Could not count this machine's CPUs, so the CPU you keep is weighted, not capped."
13321
+ );
13322
+ return false;
13323
+ }
13324
+ const percent = podCpuQuotaPercent(cores, reserved);
13325
+ if (percent === null) {
13326
+ warnings.push(
13327
+ `The CPU you keep (${reserve}) leaves no CPU for builds on this ${cores}-CPU machine. Lower it in your Conveyor profile.`
13328
+ );
13329
+ return false;
13330
+ }
13331
+ const set = await setQuota(exec, `${percent}%`);
13332
+ if (set.exitCode !== 0) {
13333
+ warnings.push(
13334
+ `Could not cap pod CPU at ${percent / 100} CPUs: ${set.stderr.trim().slice(0, 200)}`
13335
+ );
13336
+ return false;
13337
+ }
13338
+ return true;
13339
+ }
13340
+
13341
+ // src/daemon/registry-auth.ts
13342
+ init_dist();
13343
+ init_paths();
13344
+ var LOCAL_REGISTRY_AUTH_HOSTS = [
13345
+ PERSONAL_LOCAL_REGISTRY_HOST,
13346
+ new URL(PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT).host
13347
+ ];
13348
+ function registryAuthConfigured(file, credentials) {
13349
+ return LOCAL_REGISTRY_AUTH_HOSTS.every((host) => {
13350
+ const auth = file.configs?.[host]?.auth;
13351
+ return auth?.username === credentials.username && auth.password === credentials.password && auth.auth === void 0 && auth.identitytoken === void 0;
13352
+ });
13353
+ }
13354
+ function withLocalRegistryAuth(file, credentials) {
13355
+ return LOCAL_REGISTRY_AUTH_HOSTS.reduce(
13356
+ (next, host) => upsertRegistryAuth(next, host, {
13357
+ username: credentials.username,
13358
+ password: credentials.password
13359
+ }),
13360
+ file
13361
+ );
13362
+ }
13363
+ function monotonicNowUs() {
13364
+ return Number(process.hrtime.bigint() / 1000n);
13365
+ }
13366
+ async function k3sStartedAtMs(exec, nowMs, monotonicNow = monotonicNowUs) {
13367
+ const result = await exec(
13368
+ "systemctl",
13369
+ ["show", "k3s", "--property=ActiveEnterTimestampMonotonic", "--value"],
13370
+ { timeoutMs: 1e4 }
13371
+ ).catch(() => null);
13372
+ if (!result || result.exitCode !== 0) return null;
13373
+ const startedUs = Number.parseInt(result.stdout.trim(), 10);
13374
+ if (!Number.isFinite(startedUs) || startedUs <= 0) return null;
13375
+ return nowMs - (monotonicNow() - startedUs) / 1e3;
13376
+ }
13377
+ async function settleRegistryAuth(ctx, credentials, written) {
13378
+ const now = (ctx.now ?? (() => /* @__PURE__ */ new Date()))();
13379
+ const raw = await ctx.fs.readFile(LOCAL_REGISTRY_AUTH_STATE_FILE);
13380
+ const credential = credentialFingerprint(credentials);
13381
+ const stored = parseRegistryAuthState(raw);
13382
+ const state = stored?.credential === credential ? stored : { credential, writtenAt: null, enforced: false };
13383
+ if (!state.enforced && (written || state.writtenAt === null)) {
13384
+ state.writtenAt = now.toISOString();
13385
+ }
13386
+ if (!state.enforced && state.writtenAt !== null) {
13387
+ const startedAt = await k3sStartedAtMs(ctx.exec, now.getTime(), ctx.monotonicNowUs);
13388
+ if (startedAt !== null && startedAt > Date.parse(state.writtenAt)) state.enforced = true;
13389
+ }
13390
+ const next = `${JSON.stringify(state)}
13391
+ `;
13392
+ if (next !== raw) await ctx.fs.writeFile(LOCAL_REGISTRY_AUTH_STATE_FILE, next, 384);
13393
+ return state.enforced;
13394
+ }
13395
+
12969
13396
  // src/lib/isolation.ts
12970
13397
  init_dist();
12971
13398
 
@@ -13234,17 +13661,36 @@ async function removeLocalRegistry(ports) {
13234
13661
  `Could not remove the local image registry from the cluster: ${removed.stderr.trim().slice(0, 200)}`
13235
13662
  );
13236
13663
  }
13664
+ for (const [namespace, name2] of [
13665
+ [PERSONAL_LOCAL_REGISTRY_NAMESPACE, LOCAL_REGISTRY_HTPASSWD_SECRET],
13666
+ [PERSONAL_BAKE_NAMESPACE, PERSONAL_LOCAL_REGISTRY_PUSH_SECRET]
13667
+ ]) {
13668
+ const secret = await ports.exec(
13669
+ "k3s",
13670
+ ["kubectl", "-n", namespace, "delete", "secret", name2, "--ignore-not-found"],
13671
+ { timeoutMs: 6e4 }
13672
+ );
13673
+ if (secret.exitCode !== 0) {
13674
+ ports.log(
13675
+ `Could not remove the local image registry's Secret ${namespace}/${name2}: ${secret.stderr.trim().slice(0, 200)}`
13676
+ );
13677
+ }
13678
+ }
13237
13679
  const raw = await ports.fs.readFile(REGISTRIES_FILE);
13238
13680
  const parsed = parseRegistries(raw);
13239
- if (mirrorEndpoints(parsed, PERSONAL_LOCAL_REGISTRY_HOST).length === 0) return;
13681
+ const next = LOCAL_REGISTRY_AUTH_HOSTS.reduce(
13682
+ removeRegistryAuth,
13683
+ removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST)
13684
+ );
13685
+ if (serializeRegistries(next) === serializeRegistries(parsed)) return;
13240
13686
  const backup = await writeRegistriesWithBackup(
13241
13687
  ports.fs,
13242
13688
  raw,
13243
- removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST),
13689
+ next,
13244
13690
  (ports.now ?? (() => /* @__PURE__ */ new Date()))()
13245
13691
  );
13246
13692
  ports.log(
13247
- `Removed the local image registry's mirror from ${REGISTRIES_FILE} (backup: ${backup}).`
13693
+ `Removed the local image registry's mirror and credential from ${REGISTRIES_FILE} (backup: ${backup}).`
13248
13694
  );
13249
13695
  }
13250
13696
  async function removeIsolation(ports) {
@@ -13301,6 +13747,10 @@ async function uninstall(ports, options = {}) {
13301
13747
  }
13302
13748
  await removeLocalRegistry(ports);
13303
13749
  await removeIsolation(ports);
13750
+ const lifted = await liftPodCpuQuota(ports.exec).catch(
13751
+ (error) => error instanceof Error ? error.message : String(error)
13752
+ );
13753
+ if (lifted) ports.log(`Could not lift Conveyor's CPU cap on kubepods.slice: ${lifted}`);
13304
13754
  for (const path of OWNED_FILES) {
13305
13755
  await ports.fs.removeFile(path);
13306
13756
  result.removedFiles.push(path);
@@ -13353,6 +13803,74 @@ async function uninstall(ports, options = {}) {
13353
13803
  return result;
13354
13804
  }
13355
13805
 
13806
+ // src/lib/bins.ts
13807
+ var INSTALLER_PATHS = {
13808
+ bun: "/root/.bun/bin/bun",
13809
+ cli: "/root/.bun/bin/conveyor-k3",
13810
+ cloudflared: "/usr/local/bin/cloudflared"
13811
+ };
13812
+ async function whichOrNull(exec, bin) {
13813
+ const result = await exec("which", [bin]).catch(() => null);
13814
+ if (!result || result.exitCode !== 0) return null;
13815
+ const path = result.stdout.trim().split("\n")[0]?.trim();
13816
+ return path && path.startsWith("/") ? path : null;
13817
+ }
13818
+ async function resolveBinaries(exec, state, selfPath) {
13819
+ const recorded = state.bins;
13820
+ const cliSelf = selfPath?.startsWith("/") ? selfPath : null;
13821
+ return {
13822
+ bun: recorded?.bun ?? await whichOrNull(exec, "bun") ?? INSTALLER_PATHS.bun,
13823
+ // `cli` prefers the running process's own path: if this code is executing,
13824
+ // that path is by definition correct.
13825
+ cli: cliSelf ?? recorded?.cli ?? await whichOrNull(exec, "conveyor-k3") ?? INSTALLER_PATHS.cli,
13826
+ cloudflared: recorded?.cloudflared ?? await whichOrNull(exec, "cloudflared") ?? INSTALLER_PATHS.cloudflared
13827
+ };
13828
+ }
13829
+ var CLI_PATH_LINK = "/usr/local/bin/conveyor-k3";
13830
+ async function linkCliOnPath(exec, target2 = INSTALLER_PATHS.cli) {
13831
+ if (target2 === CLI_PATH_LINK) return { linked: false };
13832
+ const present = await exec("test", ["-x", target2]).catch(() => null);
13833
+ if (present?.exitCode !== 0) return { linked: false };
13834
+ const link = await exec("ln", ["-sfn", target2, CLI_PATH_LINK]).catch(() => null);
13835
+ if (link?.exitCode === 0) return { linked: true };
13836
+ return {
13837
+ linked: false,
13838
+ problem: `Could not link ${CLI_PATH_LINK} to ${target2}: ${link?.stderr.trim().slice(0, 200) || "ln failed"}`
13839
+ };
13840
+ }
13841
+ var NPM_GLOBAL_PACKAGE_DIRS = [
13842
+ "/usr/local/lib/node_modules/@rallycry/conveyor-k3",
13843
+ "/usr/lib/node_modules/@rallycry/conveyor-k3"
13844
+ ];
13845
+ async function npmCopyHint(readFile9) {
13846
+ for (const dir of NPM_GLOBAL_PACKAGE_DIRS) {
13847
+ if (await readFile9(`${dir}/package.json`) !== null) {
13848
+ 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`;
13849
+ }
13850
+ }
13851
+ return null;
13852
+ }
13853
+
13854
+ // src/lib/cloudflared.ts
13855
+ var PACKAGE_MANAGER_REFUSAL = "installed by a package manager";
13856
+ async function resolveRealPath(exec, bin) {
13857
+ const result = await exec("readlink", ["-f", bin]).catch(() => null);
13858
+ const resolved = result?.exitCode === 0 ? result.stdout.trim() : "";
13859
+ return resolved.startsWith("/") ? resolved : bin;
13860
+ }
13861
+ async function cloudflaredUpdater(exec, bin) {
13862
+ const real = await resolveRealPath(exec, bin);
13863
+ const owner = await exec("dpkg", ["-S", real]).catch(() => null);
13864
+ return owner?.exitCode === 0 ? "package-manager" : "conveyor";
13865
+ }
13866
+ function isPackageManagerRefusal(output) {
13867
+ return `${output.stdout}
13868
+ ${output.stderr}`.toLowerCase().includes(PACKAGE_MANAGER_REFUSAL);
13869
+ }
13870
+ function describeCloudflaredUpdater(updater) {
13871
+ return updater === "package-manager" ? "cloudflared is updated by your package manager, not by Conveyor" : "cloudflared is updated by Conveyor's daily update";
13872
+ }
13873
+
13356
13874
  // src/commands/update.ts
13357
13875
  init_paths();
13358
13876
  var CLOUDFLARED_UPDATED_EXIT = 11;
@@ -13441,7 +13959,22 @@ async function updatePackage(ports, result, targetVersion) {
13441
13959
  await ports.exec("systemctl", ["restart", DAEMON_UNIT]);
13442
13960
  }
13443
13961
  async function updateCloudflared(ports, result) {
13962
+ const packaged = () => {
13963
+ result.cloudflaredUpdater = "package-manager";
13964
+ result.notes.push(
13965
+ `cloudflared update skipped: ${describeCloudflaredUpdater("package-manager")}`
13966
+ );
13967
+ };
13968
+ if (await cloudflaredUpdater(ports.exec, ports.bins.cloudflared) === "package-manager") {
13969
+ packaged();
13970
+ return;
13971
+ }
13444
13972
  const update = await ports.exec(ports.bins.cloudflared, ["update"], { timeoutMs: 5 * 6e4 });
13973
+ if (isPackageManagerRefusal(update)) {
13974
+ packaged();
13975
+ return;
13976
+ }
13977
+ result.cloudflaredUpdater = "conveyor";
13445
13978
  if (update.exitCode !== CLOUDFLARED_UPDATED_EXIT) return;
13446
13979
  result.cloudflaredUpdated = true;
13447
13980
  ports.log("cloudflared was updated; restarting the tunnel.");
@@ -13451,10 +13984,13 @@ async function runUpdate(ports, decision, channel, targetVersion) {
13451
13984
  const result = {
13452
13985
  packageUpdated: false,
13453
13986
  cloudflaredUpdated: false,
13987
+ cloudflaredUpdater: null,
13454
13988
  k3sUpgraded: false,
13455
13989
  notes: []
13456
13990
  };
13457
13991
  await updatePackage(ports, result, targetVersion);
13992
+ const linked = await linkCliOnPath(ports.exec);
13993
+ if (linked.problem) result.notes.push(linked.problem);
13458
13994
  await updateCloudflared(ports, result);
13459
13995
  if (!decision.upgrade) {
13460
13996
  result.notes.push(`k3s upgrade skipped: ${decision.reason}`);
@@ -13656,7 +14192,7 @@ async function ensureCaPool(ports) {
13656
14192
  const existing = await ports.fs.readFile(CA_POOL_PATH);
13657
14193
  if (existing !== ca) await ports.fs.writeFile(CA_POOL_PATH, ca, 420);
13658
14194
  }
13659
- async function mintServiceAccountToken(ports) {
14195
+ async function ensureServiceAccount(ports) {
13660
14196
  await runOrThrow(ports.exec, "k3s", [
13661
14197
  "kubectl",
13662
14198
  "create",
@@ -13685,6 +14221,9 @@ async function mintServiceAccountToken(ports) {
13685
14221
  stdin: result.stdout
13686
14222
  });
13687
14223
  });
14224
+ }
14225
+ async function mintServiceAccountToken(ports) {
14226
+ await ensureServiceAccount(ports);
13688
14227
  const token = await runOrThrow(ports.exec, "k3s", [
13689
14228
  "kubectl",
13690
14229
  "-n",
@@ -13752,13 +14291,13 @@ function buildConveyorSocketOptions(auth) {
13752
14291
 
13753
14292
  // src/daemon/control-endpoint.ts
13754
14293
  import { lookup as dnsLookup } from "dns";
13755
- import { existsSync, readFileSync } from "fs";
14294
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
13756
14295
  import { Agent } from "https";
13757
14296
  var CONTROL_FILE = "/etc/conveyor-vm/control.json";
13758
14297
  var CONTROL_ALIAS_IP = /^192\.168\.5\.24[0-7]$/;
13759
14298
  function readControlFile(path) {
13760
14299
  try {
13761
- return readFileSync(path, "utf8");
14300
+ return readFileSync2(path, "utf8");
13762
14301
  } catch {
13763
14302
  return null;
13764
14303
  }
@@ -13804,8 +14343,8 @@ function controlPlaneRoute(apiUrl, readFile9 = readControlFile, fileExists = exi
13804
14343
  }
13805
14344
 
13806
14345
  // src/daemon/device-proof.ts
13807
- import { lstatSync, readFileSync as readFileSync2 } from "fs";
13808
- import { createPrivateKey, randomBytes, sign } from "crypto";
14346
+ import { lstatSync, readFileSync as readFileSync3 } from "fs";
14347
+ import { createPrivateKey, randomBytes as randomBytes2, sign } from "crypto";
13809
14348
  var DEVICE_KEY_PATH = "/etc/conveyor/device-key.pem";
13810
14349
  function privateDeviceKey(path, ownerUid) {
13811
14350
  let stat2;
@@ -13819,7 +14358,7 @@ function privateDeviceKey(path, ownerUid) {
13819
14358
  throw new Error("Conveyor device key must be a regular file.");
13820
14359
  if ((stat2.mode & 511) !== 384 || stat2.uid !== ownerUid)
13821
14360
  throw new Error("Conveyor device key must be owned by root with mode 0600.");
13822
- const pem = readFileSync2(path, "utf8");
14361
+ const pem = readFileSync3(path, "utf8");
13823
14362
  if (Buffer.byteLength(pem) > 16 * 1024) throw new Error("Conveyor device key is too large.");
13824
14363
  const key = createPrivateKey(pem);
13825
14364
  if (key.asymmetricKeyType !== "ed25519")
@@ -13829,7 +14368,7 @@ function privateDeviceKey(path, ownerUid) {
13829
14368
  function createClusterProof(apiUrl, clusterToken, key) {
13830
14369
  const origin = new URL(apiUrl).origin;
13831
14370
  const timestamp = Date.now();
13832
- const nonce = randomBytes(32).toString("base64url");
14371
+ const nonce = randomBytes2(32).toString("base64url");
13833
14372
  const message3 = `conveyor-device-v1
13834
14373
  ${origin}
13835
14374
  ${clusterToken}
@@ -14003,7 +14542,7 @@ var ClusterConnection = class {
14003
14542
  };
14004
14543
 
14005
14544
  // src/lib/manifest-guard.ts
14006
- import { isMap, isScalar, parseAllDocuments } from "yaml";
14545
+ import { isAlias, isMap, isScalar, parseAllDocuments } from "yaml";
14007
14546
  var ALLOWED_MANIFEST_KINDS = [
14008
14547
  "Namespace",
14009
14548
  "Role",
@@ -14015,6 +14554,14 @@ var ALLOWED_MANIFEST_KINDS = [
14015
14554
  var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
14016
14555
  var MISSING_KIND = "(missing kind)";
14017
14556
  var UNPARSEABLE = "(unparseable)";
14557
+ var LIST_SHAPED = "(list-shaped document)";
14558
+ function isListShaped(map) {
14559
+ return map.items.some(({ key }) => {
14560
+ if (isAlias(key)) return true;
14561
+ const name2 = isScalar(key) ? key.value : key;
14562
+ return typeof name2 === "string" && (name2 === "<<" || name2.toLowerCase() === "items");
14563
+ });
14564
+ }
14018
14565
  function findDisallowedKinds(manifest) {
14019
14566
  let documents;
14020
14567
  try {
@@ -14037,6 +14584,10 @@ function findDisallowedKinds(manifest) {
14037
14584
  record2(MISSING_KIND);
14038
14585
  continue;
14039
14586
  }
14587
+ if (isListShaped(doc.contents)) {
14588
+ record2(LIST_SHAPED);
14589
+ continue;
14590
+ }
14040
14591
  const kind = doc.get("kind");
14041
14592
  if (typeof kind !== "string" || kind.length === 0) {
14042
14593
  record2(MISSING_KIND);
@@ -14046,14 +14597,64 @@ function findDisallowedKinds(manifest) {
14046
14597
  }
14047
14598
  return disallowed;
14048
14599
  }
14600
+ function withoutKind(manifest, kind) {
14601
+ return parseAllDocuments(manifest).filter((doc) => !(isMap(doc.contents) && doc.get("kind") === kind)).map((doc) => doc.toString()).join("---\n");
14602
+ }
14603
+ function declaresPriorityClass(manifest, name2, value) {
14604
+ return parseAllDocuments(manifest).some(
14605
+ (doc) => isMap(doc.contents) && doc.get("kind") === "PriorityClass" && doc.getIn(["metadata", "name"]) === name2 && doc.get("value") === value
14606
+ );
14607
+ }
14049
14608
 
14050
14609
  // src/daemon/reconcile.ts
14051
14610
  init_safe_arg();
14611
+
14612
+ // src/daemon/kubelet-upkeep.ts
14052
14613
  init_paths();
14614
+ function ownerRestartWarning(change) {
14615
+ return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
14616
+ }
14617
+ async function reconcileKubelet(ctx, desired, result, managed) {
14618
+ const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
14619
+ const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
14620
+ result.skippedKubeletFlags = rendered.skipped;
14621
+ result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
14622
+ for (const flag of rendered.skipped) {
14623
+ result.warnings.push(
14624
+ `Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
14625
+ );
14626
+ }
14627
+ const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
14628
+ if (rendered.content === null) {
14629
+ if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
14630
+ return;
14631
+ }
14632
+ if (current === rendered.content) return;
14633
+ await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
14634
+ await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
14635
+ if (!managed) {
14636
+ result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
14637
+ return;
14638
+ }
14639
+ if (!mayDisrupt(ctx.runningConveyorPods)) {
14640
+ result.warnings.push(
14641
+ `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
14642
+ );
14643
+ return;
14644
+ }
14645
+ const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
14646
+ if (restart.exitCode === 0) {
14647
+ result.restartedK3s = true;
14648
+ } else {
14649
+ result.warnings.push(
14650
+ `Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
14651
+ );
14652
+ }
14653
+ }
14053
14654
 
14054
14655
  // src/daemon/local-registry-upkeep.ts
14055
14656
  init_dist();
14056
- import { createHash } from "crypto";
14657
+ import { createHash as createHash3 } from "crypto";
14057
14658
  init_paths();
14058
14659
  function nowDate() {
14059
14660
  return /* @__PURE__ */ new Date();
@@ -14062,33 +14663,47 @@ function mirrorConfigured(raw) {
14062
14663
  const endpoints = mirrorEndpoints(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST);
14063
14664
  return endpoints.length === 1 && endpoints[0] === PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT;
14064
14665
  }
14065
- async function applyUnlessCollecting(ctx) {
14666
+ async function applyUnlessCollecting(ctx, auth) {
14066
14667
  const deployment = await readRegistryDeployment(ctx.exec);
14067
14668
  if (!deployment) throw new Error("the registry's Deployment could not be read");
14068
14669
  if (gcMarkIsLive(deployment.mark, (ctx.now ?? nowDate)().getTime(), ctx.isAlive)) return false;
14069
14670
  if (deployment.mark !== null && !await deleteSweepJob(ctx.exec)) {
14070
14671
  throw new Error("a stopped cleanup's Job could not be deleted, so the registry stays stopped");
14071
14672
  }
14072
- await kubectlApply(ctx.exec, renderLocalRegistryManifests());
14673
+ await kubectlApply(ctx.exec, renderLocalRegistryManifests(auth));
14073
14674
  if (deployment.mark !== null) await clearGcMark(ctx.exec);
14074
14675
  return true;
14075
14676
  }
14076
14677
  async function deployLocalRegistry(ctx) {
14077
14678
  await ctx.fs.ensureDir(LOCAL_REGISTRY_DATA_DIR, 448);
14078
- const applied = await applyUnlessCollecting(ctx);
14679
+ const credentials = await loadOrCreateRegistryCredentials(ctx.fs, ctx.random);
14079
14680
  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 };
14681
+ let file = parseRegistries(raw);
14682
+ const mirrorWritten = !mirrorConfigured(raw);
14683
+ const authWritten = !registryAuthConfigured(file, credentials);
14684
+ let backupPath = null;
14685
+ if (mirrorWritten) {
14686
+ file = upsertRegistryMirror(file, PERSONAL_LOCAL_REGISTRY_HOST, [
14687
+ PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
14688
+ ]);
14689
+ }
14690
+ if (authWritten) file = withLocalRegistryAuth(file, credentials);
14691
+ if (mirrorWritten || authWritten) {
14692
+ backupPath = await writeRegistriesWithBackup(ctx.fs, raw, file, (ctx.now ?? nowDate)());
14693
+ }
14694
+ const authEnforced = await settleRegistryAuth(ctx, credentials, authWritten);
14695
+ await kubectlApply(ctx.exec, renderLocalRegistrySecrets(credentials));
14696
+ const applied = await applyUnlessCollecting(ctx, authEnforced ? credentials : null);
14697
+ return { applied, mirrorWritten, authWritten, backupPath, authEnforced };
14086
14698
  }
14087
14699
  async function readMirrorState(fs2) {
14088
14700
  const raw = await fs2.readFile(REGISTRIES_FILE);
14089
- const fingerprint = createHash("sha256").update(raw ?? "").digest("hex");
14701
+ const authPending = !await registryAuthEnforced((path) => fs2.readFile(path));
14702
+ const fingerprint = createHash3("sha256").update(raw ?? "").digest("hex");
14090
14703
  if (!mirrorConfigured(raw)) return { state: "absent", fingerprint };
14091
- if (await mirrorLoaded((path) => fs2.readFile(path))) return { state: "loaded", fingerprint };
14704
+ if (!authPending && await mirrorLoaded((path) => fs2.readFile(path))) {
14705
+ return { state: "loaded", fingerprint };
14706
+ }
14092
14707
  const tried = (await fs2.readFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE))?.trim() === fingerprint;
14093
14708
  return { state: tried ? "tried" : "owed", fingerprint };
14094
14709
  }
@@ -14100,14 +14715,14 @@ async function loadRegistryMirror(ctx) {
14100
14715
  if (state === "tried") {
14101
14716
  return {
14102
14717
  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}.`
14718
+ 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
14719
  };
14105
14720
  }
14106
14721
  if (state !== "owed") return { restarted: false, warning: null };
14107
14722
  if (!mayDisrupt(ctx.runningConveyorPods)) {
14108
14723
  return {
14109
14724
  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.`
14725
+ 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
14726
  };
14112
14727
  }
14113
14728
  const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
@@ -14138,7 +14753,18 @@ async function stdoutOf(exec, bin, args) {
14138
14753
  const run = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
14139
14754
  return run && run.exitCode === 0 ? run.stdout : null;
14140
14755
  }
14756
+ async function dropInDirExists(exec) {
14757
+ const run = await exec("test", ["-d", K3S_CONFIG_DROPIN_DIR], { timeoutMs: 1e4 }).catch(
14758
+ () => null
14759
+ );
14760
+ if (run === null) return null;
14761
+ if (run.exitCode === 0) return true;
14762
+ return run.exitCode === 1 && run.stderr.trim() === "" ? false : null;
14763
+ }
14141
14764
  async function readK3sConfigs(ctx) {
14765
+ const dirExists = await dropInDirExists(ctx.exec);
14766
+ if (dirExists === null) return null;
14767
+ if (!dirExists) return [await ctx.fs.readFile(K3S_OWNER_CONFIG)];
14142
14768
  const listed = await stdoutOf(ctx.exec, "find", [
14143
14769
  "-L",
14144
14770
  K3S_CONFIG_DROPIN_DIR,
@@ -14504,14 +15130,22 @@ async function readImageInventory(exec) {
14504
15130
  }
14505
15131
  return { images, referencedIds };
14506
15132
  }
15133
+ var CRICTL_RMI_TIMEOUT = "120s";
15134
+ var RMI_EXEC_TIMEOUT_MS = 15e4;
15135
+ var DEADLINE_EXCEEDED = "context deadline exceeded";
14507
15136
  async function pruneStrandedImages(exec, inventory, wantedImages) {
14508
15137
  const candidates = selectStrandedImages(inventory, wantedImages);
14509
- const result = { removed: 0, bytesReclaimed: 0, failures: [] };
15138
+ const result = { removed: 0, bytesReclaimed: 0, failures: [], pending: [] };
14510
15139
  for (const candidate of candidates) {
14511
- const removal = await exec("crictl", ["rmi", candidate.id]);
15140
+ const removal = await exec("crictl", ["--timeout", CRICTL_RMI_TIMEOUT, "rmi", candidate.id], {
15141
+ timeoutMs: RMI_EXEC_TIMEOUT_MS
15142
+ });
14512
15143
  if (removal.exitCode === 0) {
14513
15144
  result.removed += 1;
14514
15145
  result.bytesReclaimed += candidate.sizeBytes;
15146
+ } else if (`${removal.stderr}
15147
+ ${removal.stdout}`.includes(DEADLINE_EXCEEDED)) {
15148
+ result.pending.push(candidate.id);
14515
15149
  } else {
14516
15150
  result.failures.push(`${candidate.id}: ${removal.stderr.trim().slice(0, 120)}`);
14517
15151
  }
@@ -14613,17 +15247,26 @@ function countPinnedWanted(inventory, wantedImages) {
14613
15247
  // src/daemon/telemetry.ts
14614
15248
  init_paths();
14615
15249
  var LOCAL_REGISTRY_WARN_AFTER_MS = 10 * 6e4;
15250
+ 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
15251
  async function reportLocalRegistry(ports, watch2, now = Date.now()) {
14617
15252
  if (!watch2) return { fields: {}, warnings: [] };
14618
- const ready = await localRegistryReady({ readFile: ports.readFile, fetch: ports.fetch });
15253
+ const { ready, authEnforced } = await localRegistryStatus({
15254
+ readFile: ports.readFile,
15255
+ fetch: ports.fetch
15256
+ });
14619
15257
  const paused = (watch2.pausedUntil ?? 0) > now;
14620
15258
  watch2.notReadySince = ready || paused ? null : watch2.notReadySince ?? now;
14621
15259
  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 ? [
15260
+ const warnings = [];
15261
+ if (stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS) {
15262
+ warnings.push(
14625
15263
  `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
- ] : []
15264
+ );
15265
+ }
15266
+ if (!authEnforced) warnings.push(LOCAL_REGISTRY_OPEN_WARNING);
15267
+ return {
15268
+ fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready, authEnforced } },
15269
+ warnings
14627
15270
  };
14628
15271
  }
14629
15272
  var VM_CAPACITY_FILE = "/etc/conveyor-vm/capacity.json";
@@ -14746,6 +15389,7 @@ async function collectHeartbeat(ports, state, inventory) {
14746
15389
  ...localImages ? { localImages } : {},
14747
15390
  ...registry.fields,
14748
15391
  ...state.isolation ? { isolation: state.isolation } : {},
15392
+ ...state.cpuQuotaApplied === void 0 ? {} : { cpuQuotaApplied: state.cpuQuotaApplied },
14749
15393
  localWarnings: withinHeartbeatBounds([
14750
15394
  ...state.localWarnings,
14751
15395
  ...gcWarning ? [gcWarning] : [],
@@ -14796,7 +15440,15 @@ async function collect(ports, now) {
14796
15440
  if (watch2) watch2.pausedUntil = now.getTime() + REGISTRY_GC_MARK_TTL_MS;
14797
15441
  try {
14798
15442
  const outcome = await collectRegistryGarbage(
14799
- { exec, log: log2, requestDesiredState, fetch: fetch2, sleep: sleep4, isAlive },
15443
+ {
15444
+ exec,
15445
+ log: log2,
15446
+ requestDesiredState,
15447
+ fetch: fetch2,
15448
+ readFile: (path) => ports.fs.readFile(path),
15449
+ sleep: sleep4,
15450
+ isAlive
15451
+ },
14800
15452
  { dryRun: false }
14801
15453
  );
14802
15454
  if (outcome.skipped) return outcome.skipped;
@@ -14842,48 +15494,7 @@ async function runRegistryUpkeep(ports, lastGcAt, maintenanceHour) {
14842
15494
  }
14843
15495
 
14844
15496
  // 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");
15497
+ async function livePriorityClassValue(ctx, name2) {
14887
15498
  const read = await ctx.exec("k3s", [
14888
15499
  "kubectl",
14889
15500
  "get",
@@ -14892,30 +15503,59 @@ async function reconcilePriorityClass(ctx, desired, result) {
14892
15503
  "-o",
14893
15504
  "jsonpath={.value}"
14894
15505
  ]);
14895
- if (read.exitCode !== 0) return;
14896
- const existingValue = Number.parseInt(read.stdout.trim(), 10);
14897
- if (existingValue === desired.priorityClass.value) return;
15506
+ const value = Number.parseInt(read.stdout.trim(), 10);
15507
+ return read.exitCode === 0 && Number.isFinite(value) ? value : null;
15508
+ }
15509
+ async function reconcilePriorityClass(ctx, desired, result) {
15510
+ const name2 = assertK8sName(desired.priorityClass.name, "priorityClass.name");
15511
+ const wanted = desired.priorityClass.value;
15512
+ const existingValue = await livePriorityClassValue(ctx, name2);
15513
+ if (existingValue === null || existingValue === wanted) return "ready";
15514
+ if (!declaresPriorityClass(desired.rbacManifest, name2, wanted)) {
15515
+ result.warnings.push(
15516
+ `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.`
15517
+ );
15518
+ return "kept";
15519
+ }
14898
15520
  if (!mayDisrupt(ctx.runningConveyorPods)) {
14899
15521
  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)}.`
15522
+ `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
15523
  );
14902
- return;
15524
+ return "kept";
14903
15525
  }
14904
- const removed = await ctx.exec("k3s", ["kubectl", "delete", "priorityclass", name2]);
15526
+ const removed = await ctx.exec("k3s", [
15527
+ "kubectl",
15528
+ "delete",
15529
+ "priorityclass",
15530
+ name2,
15531
+ "--ignore-not-found"
15532
+ ]);
14905
15533
  if (removed.exitCode !== 0) {
14906
15534
  result.warnings.push(
14907
- `Could not replace the \`${name2}\` PriorityClass (value ${existingValue}, needs ${desired.priorityClass.value}): ${removed.stderr.trim().slice(0, 160)}`
15535
+ `Could not replace the \`${name2}\` PriorityClass (value ${existingValue}, needs ${wanted}): ${removed.stderr.trim().slice(0, 160)}`
14908
15536
  );
14909
- return;
15537
+ return "kept";
14910
15538
  }
15539
+ return "removed";
15540
+ }
15541
+ async function applyRbac(ctx, desired, result) {
15542
+ const outcome = await reconcilePriorityClass(ctx, desired, result);
15543
+ const manifest = outcome === "kept" ? withoutKind(desired.rbacManifest, "PriorityClass") : desired.rbacManifest;
14911
15544
  try {
14912
- await kubectlApply(ctx.exec, desired.rbacManifest);
14913
- result.recreatedPriorityClass = true;
15545
+ await kubectlApply(ctx.exec, manifest);
14914
15546
  } catch (error) {
15547
+ if (outcome !== "removed") throw error;
15548
+ const value = await livePriorityClassValue(ctx, desired.priorityClass.name).catch(() => null);
15549
+ if (value === desired.priorityClass.value) {
15550
+ result.recreatedPriorityClass = true;
15551
+ throw error;
15552
+ }
14915
15553
  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\`.`
15554
+ `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
15555
  );
15556
+ return;
14918
15557
  }
15558
+ if (outcome === "removed") result.recreatedPriorityClass = true;
14919
15559
  }
14920
15560
  async function step(result, label2, run) {
14921
15561
  try {
@@ -14941,12 +15581,7 @@ async function reconcile(ctx, desired) {
14941
15581
  `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
15582
  );
14943
15583
  } 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
- );
15584
+ await step(result, "Could not apply RBAC", () => applyRbac(ctx, desired, result));
14950
15585
  }
14951
15586
  await step(
14952
15587
  result,
@@ -14978,11 +15613,23 @@ async function reconcile(ctx, desired) {
14978
15613
  "Could not apply kubelet settings",
14979
15614
  () => reconcileKubelet(ctx, desired, result, managed)
14980
15615
  );
15616
+ if (desired.isolation !== void 0) {
15617
+ await step(result, "Could not cap pod CPU", async () => {
15618
+ result.cpuQuotaApplied = await reconcilePodCpuQuota(ctx.exec, desired, result.warnings);
15619
+ });
15620
+ if (desired.kubelet.systemReserved?.cpu && result.cpuQuotaApplied === void 0) {
15621
+ result.cpuQuotaApplied = false;
15622
+ }
15623
+ }
14981
15624
  if (registry && !result.restartedK3s) {
14982
15625
  await step(result, "Could not load the local image registry mirror", async () => {
14983
15626
  if (!managed) {
14984
15627
  if (await registryMirrorPending(ctx.fs))
14985
- result.warnings.push(ownerRestartWarning("The local image registry's mirror is new."));
15628
+ result.warnings.push(
15629
+ ownerRestartWarning(
15630
+ "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."
15631
+ )
15632
+ );
14986
15633
  return;
14987
15634
  }
14988
15635
  const loaded = await loadRegistryMirror(ctx);
@@ -15039,6 +15686,7 @@ async function enrollMachine(ports, env, cloudflaredBin, recorded = {}) {
15039
15686
  const enrollment = await connection.enroll();
15040
15687
  log2(`Enrolled as ${enrollment.hostname}.`);
15041
15688
  await installUnits(ports, enrollment, cloudflaredBin, enrollment.desiredState.maintenance.hour);
15689
+ await ensureServiceAccount(ports);
15042
15690
  const runningConveyorPods = await countRunningConveyorPods(exec);
15043
15691
  const result = await reconcile({ exec, fs: fs2, runningConveyorPods }, enrollment.desiredState);
15044
15692
  for (const warning of result.warnings) log2(warning);
@@ -15060,30 +15708,6 @@ async function enrollMachine(ports, env, cloudflaredBin, recorded = {}) {
15060
15708
  for (const line of renderRegistryAuthSummary(missing)) log2(line);
15061
15709
  }
15062
15710
 
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
15711
  // src/lib/guest-vm.ts
15088
15712
  async function requireGuestVm(exec) {
15089
15713
  try {
@@ -15171,6 +15795,8 @@ var ClusterDaemon = class {
15171
15795
  * against k3s's own settings before reporting it.
15172
15796
  */
15173
15797
  isolation = void 0;
15798
+ /** Whether the pod slice's CPU cap is in force, from the last reconcile. */
15799
+ cpuQuotaApplied = void 0;
15174
15800
  timers = [];
15175
15801
  /** When the last store walk started, so one that failed is not retried on every reconcile. */
15176
15802
  storeWalkStartedAt = 0;
@@ -15253,6 +15879,7 @@ var ClusterDaemon = class {
15253
15879
  );
15254
15880
  this.warnings = result.warnings;
15255
15881
  this.isolation = result.isolation;
15882
+ this.cpuQuotaApplied = result.cpuQuotaApplied;
15256
15883
  this.scheduleIsolationRetry();
15257
15884
  for (const warning of result.warnings) this.ports.log(warning);
15258
15885
  if (result.prunedNamespaces.length > 0) {
@@ -15409,6 +16036,11 @@ var ClusterDaemon = class {
15409
16036
  );
15410
16037
  }
15411
16038
  for (const failure of result.failures) this.ports.log(`Image prune failed \u2014 ${failure}`);
16039
+ if (result.pending.length > 0) {
16040
+ this.ports.log(
16041
+ `Image removal still in progress for ${result.pending.length} image(s); the next prune re-checks them.`
16042
+ );
16043
+ }
15412
16044
  } catch (error) {
15413
16045
  this.ports.log(
15414
16046
  `Skipping image prune: ${error instanceof Error ? error.message : String(error)}`
@@ -15447,6 +16079,7 @@ var ClusterDaemon = class {
15447
16079
  localImageHashes: this.localImageHashes,
15448
16080
  ...registryOn ? { localRegistry: this.registryWatch } : {},
15449
16081
  ...isolation.isolation ? { isolation: isolation.isolation } : {},
16082
+ cpuQuotaApplied: this.cpuQuotaApplied,
15450
16083
  k3sStoreBytes: this.measuredStoreBytes
15451
16084
  },
15452
16085
  inventory
@@ -15643,7 +16276,7 @@ var log = (line) => {
15643
16276
  function resolveVersion() {
15644
16277
  try {
15645
16278
  const url = new URL("../package.json", import.meta.url);
15646
- const pkg = JSON.parse(readFileSync5(url, "utf8"));
16279
+ const pkg = JSON.parse(readFileSync6(url, "utf8"));
15647
16280
  return pkg.version ?? "0.0.0";
15648
16281
  } catch {
15649
16282
  return "0.0.0";
@@ -15662,7 +16295,7 @@ function setupPorts(exec) {
15662
16295
  async function runSetup(exec) {
15663
16296
  await requireRoot(exec);
15664
16297
  await requireGuestVm(exec);
15665
- const env = readCliEnv();
16298
+ const env = readCliEnv(process.env, null);
15666
16299
  const ports = setupPorts(exec);
15667
16300
  const { cloudflaredBin } = await prepareMachine(ports);
15668
16301
  await enrollMachine(ports, env, cloudflaredBin);
@@ -15672,15 +16305,23 @@ async function runHostSetup(exec) {
15672
16305
  const { cloudflaredBin } = await prepareMachine(ports);
15673
16306
  let env;
15674
16307
  try {
15675
- env = readCliEnv();
16308
+ env = readCliEnv(process.env, null);
15676
16309
  } catch {
15677
16310
  const state = await readState(nodeFileSystem);
15678
16311
  log(
15679
16312
  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
16313
  );
16314
+ await reportCliInstall(exec);
15681
16315
  return;
15682
16316
  }
15683
16317
  await enrollMachine(ports, env, cloudflaredBin, { runtime: "host" });
16318
+ await reportCliInstall(exec);
16319
+ }
16320
+ async function reportCliInstall(exec) {
16321
+ const linked = await linkCliOnPath(exec);
16322
+ if (linked.problem) log(linked.problem);
16323
+ const hint = await npmCopyHint((path) => nodeFileSystem.readFile(path));
16324
+ if (hint) log(hint);
15684
16325
  }
15685
16326
  function hostPorts(exec) {
15686
16327
  return {
@@ -15755,6 +16396,8 @@ async function runStatus(exec, asJson, notEnrolledHint = "conveyor-k3 setup") {
15755
16396
  }
15756
16397
  const k3s = await exec("k3s", ["--version"]).catch(() => null);
15757
16398
  const version = k3s ? parseK3sVersion(k3s.stdout || k3s.stderr) : null;
16399
+ const bins = await resolveBinaries(exec, state, process.argv[1]);
16400
+ const cloudflaredUpdates = await cloudflaredUpdater(exec, bins.cloudflared);
15758
16401
  const report = {
15759
16402
  cliVersion: resolveVersion(),
15760
16403
  hostname: state.hostname,
@@ -15765,6 +16408,7 @@ async function runStatus(exec, asJson, notEnrolledHint = "conveyor-k3 setup") {
15765
16408
  lastWarmAt: state.lastWarmAt,
15766
16409
  lastPruneAt: state.lastPruneAt,
15767
16410
  runningConveyorPods: await countRunningConveyorPods(exec),
16411
+ cloudflaredUpdates,
15768
16412
  ...state.runtime ? { runtime: state.runtime } : {}
15769
16413
  };
15770
16414
  if (asJson) {
@@ -15778,6 +16422,7 @@ async function runStatus(exec, asJson, notEnrolledHint = "conveyor-k3 setup") {
15778
16422
  if (state.runtime) log(`runtime: ${state.runtime}`);
15779
16423
  log(`k3s: ${report.k3sVersion ?? "not detected"}`);
15780
16424
  for (const [unit, active] of Object.entries(units)) log(`${unit}: ${active}`);
16425
+ log(describeCloudflaredUpdater(cloudflaredUpdates));
15781
16426
  log(`Settings version applied: ${report.appliedSettingsVersion}`);
15782
16427
  log(`Conveyor pods running: ${report.runningConveyorPods}`);
15783
16428
  }