@catalyst-cloud/cli 0.14.9 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +41 -0
  3. package/dist/args.js +34 -2
  4. package/dist/capabilities.js +2 -1
  5. package/dist/cli-update-notice.js +76 -0
  6. package/dist/cli.js +267 -94
  7. package/dist/oauth.js +80 -18
  8. package/dist/onboard-accounts.js +34 -5
  9. package/dist/onboard-automation-management.js +27 -9
  10. package/dist/onboard-capacity.js +10 -1
  11. package/dist/onboard-existing.js +132 -31
  12. package/dist/onboard-github.js +171 -6
  13. package/dist/onboard-headless.js +647 -0
  14. package/dist/onboard-interrupts.js +41 -0
  15. package/dist/onboard-next.js +215 -0
  16. package/dist/onboard-permissions.js +100 -0
  17. package/dist/onboard-personal.js +46 -10
  18. package/dist/onboard-runner-cloud.js +213 -0
  19. package/dist/onboard-runner-egress.js +67 -0
  20. package/dist/onboard-runner.js +780 -0
  21. package/dist/onboard-runtime.js +48 -8
  22. package/dist/onboard-standalone-copy.js +110 -0
  23. package/dist/onboard-team-create.js +267 -0
  24. package/dist/onboard-ui.js +623 -29
  25. package/dist/onboard-workflow.js +311 -168
  26. package/dist/onboard-workspace.js +52 -5
  27. package/dist/onboard.js +359 -207
  28. package/dist/setup-events.js +100 -0
  29. package/dist/setup-onboard-copy.js +438 -0
  30. package/dist/setup-prompt-frame.js +94 -0
  31. package/dist/setup-prompts.js +108 -0
  32. package/dist/setup-render.js +339 -0
  33. package/dist/setup-signin.js +187 -0
  34. package/dist/setup.js +426 -0
  35. package/package.json +7 -3
  36. package/skills/catalyst-github/SKILL.md +1 -1
  37. package/skills/catalyst-linear/SKILL.md +1 -1
  38. package/skills/catalyst-onboard/SKILL.md +3 -2
  39. package/skills/run-this-project/SKILL.md +1 -1
  40. package/skills/unstick/SKILL.md +1 -1
  41. package/skills/what-needs-me/SKILL.md +1 -1
  42. package/skills/what-this-repo-needs/SKILL.md +1 -1
  43. package/skills/whats-happening/SKILL.md +1 -1
  44. package/vendor/README.md +4 -0
  45. package/vendor/self-host/compose.yaml +110 -0
  46. package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.service +14 -0
  47. package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.timer +10 -0
  48. package/vendor/self-host/linux-session-egress/catalyst-session-egress.service +19 -0
  49. package/vendor/self-host/linux-session-egress/session-egress.py +291 -0
  50. package/vendor/self-host/provenance.json +11 -0
@@ -1,9 +1,12 @@
1
+ import { nativeEgressInstallCommands } from "./onboard-runner-egress.js";
1
2
  import { loadConfig, normalizeBaseUrl } from "./config.js";
3
+ import { githubInstallationPage } from "./onboard-permissions.js";
2
4
  /** What a person reads for a step that is not done. Each line ends in an action they can take. */
3
5
  const REASONS = {
4
6
  account_identity_unverified: "Your login changed while coding accounts were checked. Run catalyst onboard again to verify your person and workspace.",
5
7
  account_inventory_unavailable: "Coding accounts could not be read. Run catalyst onboard again to retry.",
6
8
  account_inventory_unverified: "The server did not return a fresh supported coding-account list. Run catalyst onboard after the server update.",
9
+ coding_account_not_found: "The coding account named by --coding-account is not enrolled in this workspace. Run catalyst accounts to list the slots, then run catalyst onboard with one of them.",
7
10
  account_validation_admin_required: "An administrator must check the workspace's coding-account access.",
8
11
  account_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
9
12
  account_validation_unavailable: "The provider check could not finish. Run catalyst onboard again to retry.",
@@ -32,6 +35,7 @@ const REASONS = {
32
35
  github_installation_admin_required: "A workspace owner or administrator approves the GitHub App installation.",
33
36
  github_installation_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
34
37
  github_installation_browser_unavailable: "This computer could not open a browser. Install the GitHub App from this workspace’s Integrations page in your browser, then run catalyst onboard to resume.",
38
+ skills_install_unverified: "The Catalyst skills are not in this user's skills folder. Run catalyst install, then run catalyst onboard.",
35
39
  step_not_available_in_this_release: "This setup step is not available yet.",
36
40
  prerequisite_not_ready: "Waiting for an earlier setup step.",
37
41
  local_sync_not_selected: "Using cloud reads. Local sync was not selected.",
@@ -54,6 +58,10 @@ const REASONS = {
54
58
  project_login_refresh_required: "Renew your login with catalyst onboard, then review the plan before creating this project.",
55
59
  project_create_unverified: "Project creation could not be confirmed. Run catalyst onboard to check for the project before trying again.",
56
60
  team_read_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
61
+ team_create_unavailable: "The Linear team could not be created, and nothing was created. Run catalyst onboard to try again, or create the team in Linear and pick it.",
62
+ team_create_unverified: "Setup could not confirm whether the Linear team was created. Check Linear for it before creating it again.",
63
+ team_create_grant_required: "Your own Linear connection is missing or has lapsed. Reconnect it in Settings → Connected accounts, then run catalyst onboard. Nothing was created.",
64
+ team_create_identity_unverified: "Your login changed while setup was creating a team. Resume with the original workspace and person.",
57
65
  team_choice_required: "Choose an existing team with catalyst onboard --team <team ID or key>.",
58
66
  team_selection_unverified: "Your selected Linear team could not be verified. Run catalyst onboard --team <team ID or key> to choose again.",
59
67
  repository_choice_required: "Choose repositories with catalyst onboard --repo <owner/name>. Repeat --repo to choose more than one.",
@@ -71,13 +79,62 @@ const REASONS = {
71
79
  settings_draft_storage_timeout: "Keeping your settings draft took too long. Run catalyst onboard to retry.",
72
80
  settings_approval_unverified: "Settings still need review and approval; kept drafts do not count. Settings are optional for a first ticket. To finish them later, run catalyst onboard from the repository's checkout.",
73
81
  repository_selection_unverified: "Your selected repositories could not be verified. Run catalyst onboard --repo <owner/name> to choose again.",
82
+ workflow_adoption_declined: "Skipped for now. Run catalyst onboard to review the changes again.",
74
83
  workflow_identity_unverified: "Setup could not confirm the signed-in workspace, person or role for this team. Run catalyst onboard again as a workspace owner or administrator.",
75
84
  workflow_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
76
85
  workflow_mapping_changed: "The team's workflow changed while setup checked it. Run catalyst onboard again.",
86
+ workflow_admin_required: "Applying the Catalyst workflow needs a workspace owner or administrator. Ask one to run catalyst onboard for this team.",
87
+ workflow_plan_changed: "The team's Linear workflow changed while setup was applying it. Run catalyst onboard again to see the new plan.",
88
+ verification_pending: "Setup made the change but could not confirm it yet. Run catalyst onboard again to check.",
89
+ automations_compatible: "Nothing to change.",
77
90
  workflow_unavailable: "Setup could not read the team's workflow. Run catalyst onboard again to retry.",
78
91
  capacity_admission_unverified: "No runner is allowed to take work for this workspace yet. Enroll a self-hosted runner host, or ask Catalyst to enable cloud runners for it. Then run catalyst onboard.",
79
92
  capacity_currently_full: "Every runner slot for this repository is in use. Run catalyst onboard again after current work finishes.",
80
93
  capacity_unavailable: "Setup could not read runner capacity. Run catalyst onboard again to retry.",
94
+ runner_not_selected: "This setup does not start a runner on this machine. To start one, run catalyst onboard --runner.",
95
+ runner_docker_missing: "A runner needs Docker with Compose (Docker Desktop, OrbStack or Docker Engine), and setup could not reach it. Start Docker, or install it, then run catalyst onboard --runner.",
96
+ runner_identity_unverified: "A workspace owner or administrator enrolls a runner. Sign in as one with catalyst login, then run catalyst onboard --runner.",
97
+ runner_context_unverified: "Setup could not confirm the selected Linear team. Run catalyst onboard again.",
98
+ runner_directory_unavailable: "Setup could not prepare the runner folder in Catalyst's state directory. Check its permissions, then run catalyst onboard again.",
99
+ runner_image_unpinned: "Each runner image must be pinned as name@sha256:<digest>, and Catalyst's runner image is not public yet. Ask Catalyst support to load it here and set CATALYST_RUNNER_IMAGE to the reference they give you. A CATALYST_SUPERVISOR_IMAGE or CATALYST_WATCHDOG_IMAGE override takes the same form. Then run catalyst onboard --runner.",
100
+ runner_image_emulated: "A runner image here is built for another processor, and Catalyst does not run work under emulation. Ask Catalyst support for the image built for this processor, then run catalyst onboard again.",
101
+ runner_session_network_misshaped: "A Docker network named catalyst-session-v1 exists with the wrong settings. Remove it with docker network rm catalyst-session-v1 when nothing uses it, then run catalyst onboard again.",
102
+ runner_session_network_failed: "Docker could not create the catalyst-session-v1 network. Check that Docker is running, then run catalyst onboard again.",
103
+ runner_docker_socket_unreadable: "Setup could not read the group of the Docker socket at /var/run/docker.sock. Check that Docker is running, then run catalyst onboard again.",
104
+ runner_engine_unsupported: "Runner setup needs local Docker Desktop or OrbStack on Mac, or native Linux Docker at /var/run/docker.sock. Select a supported local engine, then run catalyst onboard --runner.",
105
+ runner_enrollment_unavailable: "Setup could not read this workspace's runner hosts. Run catalyst onboard again to retry.",
106
+ runner_enrolled_for_other_team: "This runner is enrolled for another Linear team. Keep using that team, or ask your administrator to revoke the old enrollment before setting up this machine for the selected team.",
107
+ runner_enrollment_stale: "This machine still holds the credential of a runner enrollment that was revoked. Stop the runner with docker compose -p catalyst-host down, remove the credential with docker volume rm catalyst-host_host-credential (this also removes the organization key), then run catalyst onboard --runner.",
108
+ runner_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
109
+ runner_join_token_unavailable: "Catalyst did not issue a join token for this machine. Run catalyst onboard again to retry.",
110
+ runner_session_network_unavailable: "Docker could not inspect the runner session network. Check engine access and run the same setup command again. Setup has not created a replacement network.",
111
+ runner_capability_unavailable: "Catalyst could not read runner setup support. Run the same setup command again.",
112
+ runner_capability_unverified: "Catalyst returned runner setup information this CLI cannot verify. Ask Catalyst support to check the server contract, then run the same setup command.",
113
+ runner_org_key_journal_unavailable: "Setup cannot safely use its saved runner credential request. Keep the request file and ask Catalyst support to inspect it.",
114
+ runner_org_key_recovery_required: "The previous runner key request may already have issued a key. Ask your workspace administrator to revoke that saved request and explicitly restart it. Setup will not mint another key automatically.",
115
+ runner_org_key_reconciliation_pending: "The previous runner key request is still uncertain. Run the same setup command to check its saved request again.",
116
+ runner_org_key_response_unverified: "Setup could not verify the runner key's tenant and scopes. Ask your workspace administrator to inspect the saved request.",
117
+ runner_cloud_unavailable: "Catalyst did not complete the runner setup request. Run the same setup command to check its saved request before continuing.",
118
+ runner_admission_disabled: "The selected team does not admit runner work. Run the same setup command with runner selection to explicitly enable its restricted admission policy.",
119
+ runner_org_key_file_invalid: "CATALYST_RUNNER_ORG_KEY_FILE must name a regular file that holds one organization key. Fix the file, then run catalyst onboard again.",
120
+ runner_org_key_invalid: "The runner account key is invalid, belongs to another workspace, or lacks mirror read, write and feed permissions. Supply a scoped key for this workspace with CATALYST_RUNNER_ORG_KEY_FILE, then run catalyst onboard --runner.",
121
+ runner_org_key_unavailable: "The runner account key could not be checked with Catalyst. Run catalyst onboard --runner to retry.",
122
+ runner_org_key_write_failed: "Setup could not place the organization key on the runner. Check that Docker is running, then run catalyst onboard again.",
123
+ runner_compose_failed: "Docker Compose could not start the runner. Run docker compose -p catalyst-host logs to see why, then run catalyst onboard again.",
124
+ runner_compose_not_running: "The runner's supervisor is not running. Run docker compose -p catalyst-host logs supervisor to see why, then run catalyst onboard again.",
125
+ runner_enrollment_unverified: "The runner started but has not enrolled yet. Run docker compose -p catalyst-host logs supervisor to see why, or run catalyst onboard again in a minute.",
126
+ runner_needs_runner_flag: "This machine was set up to run Catalyst's work, but its runner is not running or not enrolled. To bring it up, run catalyst onboard --runner.",
127
+ runner_capability_pending: "The runner enrolled but has not reported its capacity yet. Run catalyst onboard again in a minute.",
128
+ runner_admission_unverified: "Setup could not read whether the team's pool admits runner hosts. Run catalyst onboard again to retry.",
129
+ github_app_permissions_outdated: "GitHub App needs updated permissions. An organization owner on GitHub accepts the pending request on the App's installation page, then run catalyst onboard.",
130
+ github_app_repository_missing: "The GitHub App installation cannot reach a repository a project registers. An organization owner on GitHub adds it under the installation's Repository access, then run catalyst onboard.",
131
+ github_app_repository_not_installed: "No GitHub App installation can reach a repository a project registers. A workspace owner or administrator installs the GitHub App on its owner: run catalyst onboard.",
132
+ github_app_permissions_unverified: "The GitHub App installation works, but its granted permissions could not be checked. Run catalyst onboard to try again.",
133
+ github_app_repository_access_unverified: "Whether the GitHub App reaches every registered repository could not be checked. Run catalyst onboard to try again.",
134
+ linear_workspace_scope_outdated: "The workspace's Linear connection (the Catalyst app) is missing scopes this version needs. A workspace owner or administrator re-authorizes it from the workspace's Integrations page, then run catalyst onboard.",
135
+ linear_workspace_permissions_unverified: "The workspace's Linear connection works, but its granted scopes could not be checked. Run catalyst onboard to try again.",
136
+ linear_personal_scope_outdated: "Your personal Linear connection is missing scopes this version needs. Re-authorize it from Connected accounts, then run catalyst onboard.",
137
+ personal_permissions_unverified: "Your personal connection's granted scopes could not be checked. Run catalyst onboard to try again.",
81
138
  housekeeping_service_unverified: "The daily update needs a user service manager, launchd on macOS or systemd --user on Linux. Setup cannot schedule it on this computer. It is optional, and work does not depend on it.",
82
139
  };
83
140
  /** The saved login's web address, or nothing when the config is missing, unreadable or not http(s). */
@@ -115,17 +172,145 @@ export function onboardTeamKey(journal) {
115
172
  ? key
116
173
  : undefined;
117
174
  }
175
+ /** Receipt evidence printed inside a sentence: names, scopes and permission labels only. */
176
+ function evidenceText(value) {
177
+ return typeof value === "string" &&
178
+ /^[A-Za-z0-9 ._:;/(),-]{1,2000}$/.test(value)
179
+ ? value
180
+ : undefined;
181
+ }
182
+ const LINEAR_START = {
183
+ linear_workspace_scope_outdated: "/settings/connections",
184
+ linear_personal_scope_outdated: "/connect/linear/personal/start",
185
+ };
186
+ /** A receipt URL, printed only when it is the page this reason's action lives on: the exact
187
+ * installation's page on github.com, or this cloud's own OAuth start for the grant in question. */
188
+ function actionUrl(step, baseUrl) {
189
+ const href = webUrl(step.evidence?.url);
190
+ if (!href)
191
+ return undefined;
192
+ const url = new URL(href);
193
+ if (url.protocol !== "https:" ||
194
+ url.hash ||
195
+ (url.search &&
196
+ !(step.reason === "linear_workspace_scope_outdated" &&
197
+ url.pathname === "/settings/connections" &&
198
+ url.search === "?reauthorize=linear")))
199
+ return undefined;
200
+ const evidence = step.evidence ?? {};
201
+ if (step.reason === "github_app_permissions_outdated" ||
202
+ step.reason === "github_app_repository_missing")
203
+ return githubInstallationPage(url, typeof evidence.installation === "string" ? evidence.installation : "", typeof evidence.org === "string" ? evidence.org : null, step.reason === "github_app_permissions_outdated")
204
+ ? href
205
+ : undefined;
206
+ const base = baseUrl ? webUrl(baseUrl) : undefined;
207
+ return base &&
208
+ new URL(base).origin === url.origin &&
209
+ url.pathname === LINEAR_START[step.reason ?? ""] &&
210
+ (step.reason !== "linear_workspace_scope_outdated" ||
211
+ url.search === "?reauthorize=linear")
212
+ ? href
213
+ : undefined;
214
+ }
215
+ /** CTC-4629: the outdated-connection lines, built from the evidence the adapters recorded. */
216
+ function permissionText(step, baseUrl) {
217
+ const evidence = step.evidence ?? {};
218
+ const url = actionUrl(step, baseUrl);
219
+ const missing = evidenceText(evidence.missing);
220
+ const org = evidenceText(evidence.org);
221
+ const repository = evidenceText(evidence.repository);
222
+ const github = evidence.actor === "github-account-owner"
223
+ ? `The owner of the ${org ?? "installing"} GitHub account`
224
+ : "An organization owner on GitHub";
225
+ switch (step.reason) {
226
+ case "github_app_permissions_outdated":
227
+ if (!url)
228
+ return undefined;
229
+ return `GitHub App needs updated permissions${org ? ` on ${org}` : ""}${missing ? `: ${missing}` : ""}. ${github} reviews and accepts the request at ${url}, then run catalyst onboard.`;
230
+ case "github_app_repository_missing":
231
+ if (!url || !repository)
232
+ return undefined;
233
+ return `The GitHub App installation${org ? ` on ${org}` : ""} cannot reach ${repository}, which a project registers. ${github} adds it under Repository access at ${url}, then run catalyst onboard.`;
234
+ case "github_app_repository_not_installed":
235
+ if (!repository)
236
+ return undefined;
237
+ return `No GitHub App installation can reach ${repository}, which a project registers. A workspace owner or administrator installs the GitHub App on ${org ?? repository.split("/")[0]}: run catalyst onboard.`;
238
+ case "linear_workspace_scope_outdated":
239
+ if (!url)
240
+ return undefined;
241
+ return `The workspace's Linear connection (the Catalyst app) is missing scopes this version needs${missing ? `: ${missing}` : ""}. A workspace owner or administrator re-authorizes it at ${url}, then run catalyst onboard.`;
242
+ case "linear_personal_scope_outdated":
243
+ if (!url)
244
+ return undefined;
245
+ return `Your personal Linear connection is missing scopes this version needs${missing ? `: ${missing}` : ""}. Re-authorize it at ${url}, then run catalyst onboard.`;
246
+ }
247
+ return undefined;
248
+ }
249
+ /** A done step's detail line: what a connection was granted, when the check recorded it. */
250
+ export function onboardStepDetail(step) {
251
+ const value = step.state === "done" ? step.evidence?.granted : undefined;
252
+ const granted = typeof value === "string" &&
253
+ value.length <= 512_000 &&
254
+ /^[A-Za-z0-9 ._:;/(),-]+$/.test(value)
255
+ ? value.length <= 2_000
256
+ ? value
257
+ : `${value.slice(0, 1_950)} (more permissions in the setup record)`
258
+ : undefined;
259
+ return granted ? `granted ${granted}` : undefined;
260
+ }
118
261
  /** One step's next action. `waitsFor` names the unfinished step a prerequisite wait is blocked on. */
119
262
  export function onboardReasonText(step, context = {}) {
120
263
  const reason = step.reason ?? "";
264
+ if ([
265
+ "workspace_approval_required",
266
+ "personal_approval_required",
267
+ "github_installation_approval_required",
268
+ ].includes(reason)) {
269
+ const path = reason === "workspace_approval_required"
270
+ ? "/settings/connections?connect=linear"
271
+ : reason === "github_installation_approval_required"
272
+ ? "/settings/connections?install=github"
273
+ : `/settings/connected-accounts?connect=${step.id === "github.personal" ? "github" : "linear"}`;
274
+ const where = context.baseUrl
275
+ ? `${normalizeBaseUrl(context.baseUrl)}${path}`
276
+ : "Catalyst settings";
277
+ return `Open ${where} and approve this connection, then run catalyst onboard.`;
278
+ }
121
279
  if (reason === "workflow_mapping_unverified")
122
280
  return `This team's workflow mapping is not verified yet. Run catalyst team adopt ${onboardTeamKey(context.journal) ?? "<TEAM KEY>"} to preview the adoption plan. Run it again with the --yes --plan-hash value it prints. Then run catalyst onboard.`;
281
+ if (reason === "team_created_not_adopted" ||
282
+ reason === "team_create_unverified") {
283
+ const key = step.evidence?.teamKey;
284
+ const team = typeof key === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(key)
285
+ ? key
286
+ : undefined;
287
+ const adopt = (key) => `Run catalyst team adopt ${key} to preview the adoption plan. Run it again with the --yes --plan-hash value it prints. Then run catalyst onboard.`;
288
+ if (reason === "team_created_not_adopted")
289
+ return `Linear team ${team ?? "<TEAM KEY>"} was created, but Catalyst's workflow is not set up on it yet. ${adopt(team ?? "<TEAM KEY>")}`;
290
+ if (team)
291
+ return `Setup could not confirm whether Linear created the team ${team}. Check Linear for it. If it is there: ${adopt(team)} If it is not, run catalyst onboard to create it again.`;
292
+ }
123
293
  if (reason === "account_enrollment_required") {
124
294
  const where = context.baseUrl
125
295
  ? `at ${normalizeBaseUrl(context.baseUrl)}/settings/coding-accounts, the Settings → AI accounts page`
126
296
  : "in the web app on the Settings → AI accounts page";
127
297
  return `No coding account is enrolled. A workspace owner or administrator adds one ${where}. A Claude account needs its email and the token printed by \`claude setup-token\`. Then run catalyst onboard.`;
128
298
  }
299
+ if (reason === "automation_management_unavailable") {
300
+ const conflicts = typeof step.evidence?.automations === "string" &&
301
+ /^(?:open|review|ready|merge)(?:,(?:open|review|ready|merge))*$/.test(step.evidence.automations);
302
+ if (!conflicts)
303
+ return "could not read them; checked again before work starts";
304
+ const key = onboardTeamKey(context.journal);
305
+ const workspace = context.journal?.steps.find((row) => row.id === "linear.workspace" && row.state === "done");
306
+ const slug = workspace?.evidence?.workspaceSlug;
307
+ if (key &&
308
+ /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(key) &&
309
+ typeof slug === "string" &&
310
+ /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(slug))
311
+ return `Open https://linear.app/${slug}/settings/teams/${key}/workflow and set each pull request automation to No action.`;
312
+ return `Open your ${key ? `${key} team's` : "team's"} workflow settings in Linear and set each pull request automation to No action.`;
313
+ }
129
314
  if (reason === "cloud_capability_unavailable") {
130
315
  // The capability guard records the web page in evidence.path only when it printed it.
131
316
  const page = webUrl(step.evidence?.path);
@@ -133,7 +318,37 @@ export function onboardReasonText(step, context = {}) {
133
318
  ? `This setup step is not available on this server yet. Continue in the web app at ${page}, then run catalyst onboard.`
134
319
  : "This setup step is not available on this server yet. Run catalyst onboard after the server update.";
135
320
  }
321
+ if (reason === "runner_native_egress_setup_required") {
322
+ const commands = nativeEgressInstallCommands();
323
+ return "Native Linux needs a root-managed session egress producer before the runner can take work. Its elevated installation creates the isolated session network and private-range and host-isolation nftables rules. Review and authorize this separately; setup has not run it. It requires systemd, python3, Docker and nftables. " + (commands ? "Install the verified bundled artifacts, then run the same setup command:\n" + commands : "Setup could not verify its bundled artifacts. Reinstall the CLI before authorizing installation.");
324
+ }
325
+ if (reason === "runner_images_unavailable") {
326
+ const image = step.evidence?.image;
327
+ const named = typeof image === "string" &&
328
+ /^[a-z0-9][a-z0-9._:/-]{0,255}@sha256:[0-9a-f]{64}$/.test(image)
329
+ ? ` ${image}`
330
+ : "";
331
+ return `This machine does not have the Catalyst image${named}, and its pull did not complete. Check network access and run the same setup command to retry the anonymous pull.`;
332
+ }
333
+ if (reason === "runner_org_key_missing") {
334
+ const where = context.baseUrl
335
+ ? `at ${normalizeBaseUrl(context.baseUrl)}/settings/account-keys`
336
+ : "on the web app's Account API keys page";
337
+ return `The runner is enrolled, but it takes no work without an organization key. A workspace owner or administrator creates one with the mirror:read, mirror:write and mirror:feed scopes ${where} and saves it in a file only they can read. Then run catalyst onboard --runner with CATALYST_RUNNER_ORG_KEY_FILE set to that file.`;
338
+ }
339
+ if (reason === "runner_host_not_ready") {
340
+ const failing = step.evidence?.failing;
341
+ const checks = typeof failing === "string" && /^[a-z0-9.,_-]{1,500}$/.test(failing)
342
+ ? `: ${failing.split(",").join(", ")}`
343
+ : "";
344
+ return `The runner is enrolled but its readiness checks fail${checks}. Run docker compose -p catalyst-host logs supervisor for the remedy, then run catalyst onboard again.`;
345
+ }
346
+ if (reason === "runner_admission_operator")
347
+ return `The admission policy for team ${onboardTeamKey(context.journal) ?? "<TEAM KEY>"} needs a Catalyst operator to check it. Setup changes only restricted admission and preserves existing policy fields. Ask Catalyst support to confirm a compatible policy, then run the same setup command.`;
136
348
  if (reason === "prerequisite_not_ready" && context.waitsFor)
137
349
  return `Runs after "${context.waitsFor}".`;
350
+ const permission = permissionText(step, context.baseUrl);
351
+ if (permission)
352
+ return permission;
138
353
  return REASONS[reason] ?? reason.replaceAll("_", " ");
139
354
  }
@@ -0,0 +1,100 @@
1
+ /** CTC-4629: the cloud's typed permissions verdict on an existing connection, verified before use.
2
+ * A verdict that fails any check is a shape error, so no unverified URL is ever printed. */
3
+ import { loadConfig, normalizeBaseUrl } from "./config.js";
4
+ /** The saved login's origin, which a re-authorize URL must share; an unreadable one matches none. */
5
+ export function savedOrigin(ctx) {
6
+ try {
7
+ const cfg = loadConfig(ctx.home);
8
+ return cfg ? new URL(normalizeBaseUrl(cfg.baseUrl)).origin : "";
9
+ }
10
+ catch {
11
+ return "";
12
+ }
13
+ }
14
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
15
+ ? v
16
+ : null;
17
+ export const LINEAR_SCOPE = /^[A-Za-z][A-Za-z0-9:_-]{0,63}$/;
18
+ export const GITHUB_PERMISSION = /^[A-Za-z0-9_-]{1,80} \((?:read|write|admin)\)$/;
19
+ const UNKNOWN = new Set([
20
+ "not-run",
21
+ "unreachable",
22
+ "expired-or-revoked",
23
+ "not-connected",
24
+ "grant-unreadable",
25
+ ]);
26
+ const labels = (value, label, min = 0) => Array.isArray(value) &&
27
+ value.length >= min &&
28
+ value.length <= 128 &&
29
+ value.every((item) => typeof item === "string" && label.test(item))
30
+ ? value
31
+ : null;
32
+ /** `undefined` when the cloud sent no verdict (an older cloud); `null` when it sent a bad one. */
33
+ export function parsePermissions(value, expected) {
34
+ if (value === undefined)
35
+ return undefined;
36
+ const row = object(value);
37
+ if (!row || row.grant !== expected.grant)
38
+ return null;
39
+ if (row.state === "unknown")
40
+ return typeof row.reason === "string" && UNKNOWN.has(row.reason)
41
+ ? { state: "unknown", reason: row.reason }
42
+ : null;
43
+ const granted = labels(row.granted, expected.label);
44
+ if (!granted)
45
+ return null;
46
+ if (row.state === "current")
47
+ return { state: "current", granted };
48
+ const missing = labels(row.missing, expected.label, 1);
49
+ const action = object(row.action);
50
+ if (row.state !== "outdated" ||
51
+ !missing ||
52
+ !action ||
53
+ action.kind !== expected.action ||
54
+ typeof action.url !== "string" ||
55
+ action.url.length > 2048 ||
56
+ typeof action.actor !== "string")
57
+ return null;
58
+ let url;
59
+ try {
60
+ url = new URL(action.url);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ if (url.protocol !== "https:" ||
66
+ url.username ||
67
+ url.password ||
68
+ (url.search &&
69
+ !(expected.grant === "linear-workspace" &&
70
+ url.pathname === "/settings/connections" &&
71
+ url.search === "?reauthorize=linear")) ||
72
+ url.hash ||
73
+ expected.actorFor(url) !== action.actor)
74
+ return null;
75
+ return {
76
+ state: "outdated",
77
+ granted,
78
+ missing,
79
+ url: url.toString(),
80
+ actor: action.actor,
81
+ };
82
+ }
83
+ const GITHUB_INSTALLATION_PAGE = /^\/(?:organizations\/([A-Za-z0-9][A-Za-z0-9-]{0,38})\/)?settings\/installations\/([0-9]{1,20})(\/permissions\/update)?$/;
84
+ /** github.com's page for one installation: its settings, or its permission-request review. The
85
+ * actor is GitHub's: an organization owner, or the owner of a personal account. */
86
+ export function githubInstallationPage(url, installationId, org, review) {
87
+ const match = GITHUB_INSTALLATION_PAGE.exec(url.pathname);
88
+ if (url.origin !== "https://github.com" ||
89
+ !match ||
90
+ match[2] !== installationId ||
91
+ Boolean(match[3]) !== review)
92
+ return null;
93
+ const login = match[1] ?? null;
94
+ if (login && org && login.toLowerCase() !== org.toLowerCase())
95
+ return null;
96
+ return {
97
+ actor: login ? "github-org-admin" : "github-account-owner",
98
+ login,
99
+ };
100
+ }
@@ -1,6 +1,7 @@
1
1
  import { finishOnTheWeb } from "./consent-browser.js";
2
2
  import { loadConfig, normalizeBaseUrl, } from "./config.js";
3
3
  import { pollConsent } from "./onboard-consent.js";
4
+ import { LINEAR_SCOPE, parsePermissions, savedOrigin, } from "./onboard-permissions.js";
4
5
  const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
5
6
  ? value
6
7
  : null;
@@ -160,15 +161,47 @@ export function personalConsentAdapter(options) {
160
161
  (body.reason !== undefined &&
161
162
  (body.connected || body.reason !== "lapsed")))
162
163
  return { state: "failed", reason: "personal_status_shape" };
163
- return body.connected
164
- ? {
165
- state: "done",
164
+ if (!body.connected)
165
+ return { state: "pending" };
166
+ // CTC-4629: only a Linear grant carries scopes; GitHub's are the App's, checked at installation.
167
+ const permissions = options.provider === "linear"
168
+ ? parsePermissions(body.permissions, {
169
+ grant: "linear-personal",
170
+ action: "reauthorize",
171
+ label: LINEAR_SCOPE,
172
+ actorFor: (url) => url.origin === savedOrigin(ctx) &&
173
+ url.pathname === "/connect/linear/personal/start"
174
+ ? "member"
175
+ : null,
176
+ })
177
+ : undefined;
178
+ if (permissions === null)
179
+ return { state: "failed", reason: "personal_status_shape" };
180
+ if (permissions?.state === "unknown")
181
+ return waiting("personal_permissions_unverified");
182
+ if (permissions?.state === "outdated")
183
+ return {
184
+ state: "waiting",
185
+ reason: "linear_personal_scope_outdated",
166
186
  evidence: {
167
- provider: options.provider,
168
- checkedAt: ctx.now().getTime(),
187
+ provider: "linear",
188
+ grant: "linear-personal",
189
+ granted: permissions.granted.join(", "),
190
+ missing: permissions.missing.join(", "),
191
+ url: permissions.url,
192
+ actor: permissions.actor,
169
193
  },
170
- }
171
- : { state: "pending" };
194
+ };
195
+ return {
196
+ state: "done",
197
+ evidence: {
198
+ provider: options.provider,
199
+ checkedAt: ctx.now().getTime(),
200
+ ...(permissions?.state === "current"
201
+ ? { granted: permissions.granted.join(", ") }
202
+ : {}),
203
+ },
204
+ };
172
205
  };
173
206
  return {
174
207
  check: inspect,
@@ -181,6 +214,8 @@ export function personalConsentAdapter(options) {
181
214
  const before = await inspect(ctx, journal, signal);
182
215
  if (before.state !== "pending")
183
216
  return before;
217
+ if (!options.wait)
218
+ return waiting("personal_approval_required");
184
219
  const start = await read(ctx, journal, `/connect/${options.provider}/personal/start`, timeout, signal);
185
220
  if ("status" in start &&
186
221
  start.status === 409 &&
@@ -197,7 +232,6 @@ export function personalConsentAdapter(options) {
197
232
  if (!same(cfg, loadConfig(ctx.home)))
198
233
  return { state: "refused", reason: "personal_identity_refused" };
199
234
  const name = options.provider === "linear" ? "Linear" : "GitHub";
200
- ctx.stderr(`Approve your personal ${name} connection in the browser.`);
201
235
  // Signed continuation credentials belong only to the browser, never transcript or journal.
202
236
  let browserUnavailable = false;
203
237
  try {
@@ -218,7 +252,9 @@ export function personalConsentAdapter(options) {
218
252
  latest = await inspect(ctx, journal, pollSignal);
219
253
  if (latest.reason === "cloud_capability_unavailable")
220
254
  return { outcome: "waiting", reason: latest.reason };
221
- if (latest.reason === "personal_login_refresh_required")
255
+ // An outdated grant is re-authorized at its own URL; polling longer cannot finish it.
256
+ if (latest.reason === "personal_login_refresh_required" ||
257
+ (latest.reason === "linear_personal_scope_outdated" || latest.reason === "personal_permissions_unverified"))
222
258
  return { outcome: "waiting", reason: latest.reason };
223
259
  if (latest.state === "done")
224
260
  return { outcome: "connected" };
@@ -232,7 +268,7 @@ export function personalConsentAdapter(options) {
232
268
  const result = options.wait
233
269
  ? await options.wait(`Waiting for your ${name} approval`, run)
234
270
  : await run();
235
- if (result.state === "done")
271
+ if (result.state === "done" || (latest.reason === "linear_personal_scope_outdated" || latest.reason === "personal_permissions_unverified"))
236
272
  return latest;
237
273
  return browserUnavailable && result.reason === "consent_timeout"
238
274
  ? waiting("personal_browser_unavailable")