@catalyst-cloud/cli 0.14.5 → 0.14.7
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/CHANGELOG.md +9 -0
- package/README.md +5 -3
- package/dist/args.js +51 -13
- package/dist/cli.js +58 -10
- package/dist/config.js +16 -3
- package/dist/daemon-authority-lifetime.js +171 -0
- package/dist/daemon-authority.js +216 -0
- package/dist/daemon-cache.js +348 -0
- package/dist/daemon-store.js +370 -0
- package/dist/daemon-supervisor.js +460 -0
- package/dist/env/onboard-draft-store.js +243 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +203 -0
- package/dist/events-cloud-tail.js +134 -0
- package/dist/events-cloud.js +500 -0
- package/dist/events.js +13 -4
- package/dist/legacy.js +145 -32
- package/dist/oauth.js +125 -33
- package/dist/onboard-accounts.js +259 -0
- package/dist/onboard-automation-management.js +15 -0
- package/dist/onboard-bootstrap.js +162 -0
- package/dist/onboard-capabilities.js +15 -7
- package/dist/onboard-capacity.js +226 -0
- package/dist/onboard-existing.js +106 -32
- package/dist/onboard-file-snapshot.js +45 -0
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +147 -37
- package/dist/onboard-runtime.js +104 -9
- package/dist/onboard-settings-context.js +141 -0
- package/dist/onboard-settings-import.js +127 -0
- package/dist/onboard-settings.js +234 -0
- package/dist/onboard-ui.js +99 -11
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard.js +359 -27
- package/dist/owned-fetch.js +182 -0
- package/dist/query.js +77 -27
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +2 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
- package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
- package/skills/catalyst-linear/SKILL.md +3 -3
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +5 -4
- package/skills/catalyst-onboard/references/local-sync.md +3 -1
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +2 -2
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +104 -26
- package/vendor/settings/index.d.ts +4 -0
- package/vendor/settings/index.js +2937 -0
- package/vendor/settings/provenance.json +148 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
2
|
+
import { readExistingOnboardJson } from "./onboard-existing.js";
|
|
3
|
+
import { verifyOnboardRoutes } from "./onboard-capabilities.js";
|
|
4
|
+
const listPath = "/api/v1/coding-accounts";
|
|
5
|
+
const validatePath = "/api/v1/coding-accounts/:slot/validate";
|
|
6
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: null;
|
|
9
|
+
const waiting = (reason) => ({
|
|
10
|
+
state: "waiting",
|
|
11
|
+
reason,
|
|
12
|
+
});
|
|
13
|
+
const slotPattern = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
14
|
+
function binding(ctx, journal) {
|
|
15
|
+
const cfg = loadConfig(ctx.home);
|
|
16
|
+
if (!cfg?.user ||
|
|
17
|
+
cfg.account !== (journal.account ?? journal.tenant) ||
|
|
18
|
+
cfg.user.id !== journal.membershipId ||
|
|
19
|
+
!journal.baseUrl ||
|
|
20
|
+
normalizeBaseUrl(cfg.baseUrl) !== normalizeBaseUrl(journal.baseUrl))
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
account: cfg.account,
|
|
24
|
+
person: cfg.user.id,
|
|
25
|
+
origin: normalizeBaseUrl(cfg.baseUrl),
|
|
26
|
+
role: cfg.user.role,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const equal = (a, b) => a?.account === b.account &&
|
|
30
|
+
a.person === b.person &&
|
|
31
|
+
a.origin === b.origin &&
|
|
32
|
+
a.role === b.role;
|
|
33
|
+
async function inventory(ctx, journal, signal) {
|
|
34
|
+
const identity = binding(ctx, journal);
|
|
35
|
+
if (!identity)
|
|
36
|
+
return { reason: "account_identity_unverified" };
|
|
37
|
+
const support = await verifyOnboardRoutes(ctx, journal, [{ method: "GET", path: listPath }], signal);
|
|
38
|
+
if ("reason" in support)
|
|
39
|
+
return support;
|
|
40
|
+
if (!equal(binding(ctx, journal), identity))
|
|
41
|
+
return { reason: "account_identity_unverified" };
|
|
42
|
+
const read = await readExistingOnboardJson(ctx, listPath, signal);
|
|
43
|
+
if (!equal(binding(ctx, journal), identity))
|
|
44
|
+
return { reason: "account_identity_unverified" };
|
|
45
|
+
if ("reason" in read)
|
|
46
|
+
return { reason: "account_inventory_unavailable" };
|
|
47
|
+
const body = object(read.body);
|
|
48
|
+
const now = ctx.now().getTime();
|
|
49
|
+
if (!Array.isArray(body?.accounts) ||
|
|
50
|
+
body.accounts.length > 1000 ||
|
|
51
|
+
typeof body.observedAtMs !== "number" ||
|
|
52
|
+
!Number.isSafeInteger(body.observedAtMs) ||
|
|
53
|
+
body.observedAtMs > now + 5000 ||
|
|
54
|
+
body.observedAtMs < now - 30000)
|
|
55
|
+
return { reason: "account_inventory_unverified" };
|
|
56
|
+
const slots = [], seen = new Set();
|
|
57
|
+
for (const value of body.accounts) {
|
|
58
|
+
const row = object(value);
|
|
59
|
+
if (!row ||
|
|
60
|
+
typeof row.accountSlot !== "string" ||
|
|
61
|
+
!slotPattern.test(row.accountSlot) ||
|
|
62
|
+
seen.has(row.accountSlot) ||
|
|
63
|
+
typeof row.provider !== "string" ||
|
|
64
|
+
!/^[a-z][a-z0-9_-]{0,63}$/.test(row.provider) ||
|
|
65
|
+
typeof row.declaredState !== "string" ||
|
|
66
|
+
!["active", "inactive"].includes(row.declaredState) ||
|
|
67
|
+
typeof row.ownedByMe !== "boolean" ||
|
|
68
|
+
typeof row.needsCredential !== "boolean" ||
|
|
69
|
+
typeof row.walled !== "boolean" ||
|
|
70
|
+
typeof row.quarantined !== "boolean" ||
|
|
71
|
+
![null, "renews", "canceled"].includes(row.renewalStatus) ||
|
|
72
|
+
!(row.revokedAtMs === null ||
|
|
73
|
+
(typeof row.revokedAtMs === "number" &&
|
|
74
|
+
Number.isSafeInteger(row.revokedAtMs))) ||
|
|
75
|
+
!(row.accessEndsAtMs === null ||
|
|
76
|
+
(typeof row.accessEndsAtMs === "number" &&
|
|
77
|
+
Number.isSafeInteger(row.accessEndsAtMs))))
|
|
78
|
+
return { reason: "account_inventory_unverified" };
|
|
79
|
+
seen.add(row.accountSlot);
|
|
80
|
+
// Project only metadata we need. Unknown server fields never enter output or receipts.
|
|
81
|
+
slots.push({
|
|
82
|
+
accountSlot: row.accountSlot,
|
|
83
|
+
provider: row.provider,
|
|
84
|
+
declaredState: row.declaredState,
|
|
85
|
+
ownedByMe: row.ownedByMe,
|
|
86
|
+
needsCredential: row.needsCredential,
|
|
87
|
+
walled: row.walled,
|
|
88
|
+
quarantined: row.quarantined,
|
|
89
|
+
renewalStatus: row.renewalStatus,
|
|
90
|
+
revokedAtMs: row.revokedAtMs,
|
|
91
|
+
accessEndsAtMs: row.accessEndsAtMs,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return { slots, identity };
|
|
95
|
+
}
|
|
96
|
+
/** Verify one existing Claude slot per run. A stored login, cached poll or Codex shape is not
|
|
97
|
+
* fresh provider access. Enrollment and a phase's actual lease eligibility remain separate. */
|
|
98
|
+
export function onboardAccountsAdapter(input = {}) {
|
|
99
|
+
let proof = null;
|
|
100
|
+
let attempted = false;
|
|
101
|
+
const candidate = (slots, now) => slots
|
|
102
|
+
.filter((row) => row.provider === "claude" &&
|
|
103
|
+
row.declaredState === "active" &&
|
|
104
|
+
row.ownedByMe &&
|
|
105
|
+
!row.needsCredential &&
|
|
106
|
+
!row.walled &&
|
|
107
|
+
!row.quarantined &&
|
|
108
|
+
row.renewalStatus !== "canceled" &&
|
|
109
|
+
row.revokedAtMs === null &&
|
|
110
|
+
(row.accessEndsAtMs === null || row.accessEndsAtMs > now))
|
|
111
|
+
.sort((a, b) => a.accountSlot.localeCompare(b.accountSlot))[0];
|
|
112
|
+
const check = async (ctx, journal, signal) => {
|
|
113
|
+
const live = await inventory(ctx, journal, signal);
|
|
114
|
+
if ("reason" in live)
|
|
115
|
+
return waiting(live.reason);
|
|
116
|
+
const selected = candidate(live.slots, ctx.now().getTime());
|
|
117
|
+
if (proof &&
|
|
118
|
+
equal(live.identity, proof) &&
|
|
119
|
+
live.slots.some((row) => row.accountSlot === proof.slot &&
|
|
120
|
+
row.provider === proof.provider &&
|
|
121
|
+
row.declaredState === "active" &&
|
|
122
|
+
row.ownedByMe &&
|
|
123
|
+
!row.needsCredential &&
|
|
124
|
+
!row.walled &&
|
|
125
|
+
!row.quarantined &&
|
|
126
|
+
row.renewalStatus !== "canceled" &&
|
|
127
|
+
row.revokedAtMs === null &&
|
|
128
|
+
(row.accessEndsAtMs === null ||
|
|
129
|
+
row.accessEndsAtMs > ctx.now().getTime())) &&
|
|
130
|
+
proof.checkedAtMs <= ctx.now().getTime() + 5000 &&
|
|
131
|
+
proof.checkedAtMs >= ctx.now().getTime() - 30000)
|
|
132
|
+
return {
|
|
133
|
+
state: "done",
|
|
134
|
+
evidence: {
|
|
135
|
+
accountSlot: proof.slot,
|
|
136
|
+
provider: proof.provider,
|
|
137
|
+
checkedAt: new Date(proof.checkedAtMs).toISOString(),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
proof = null;
|
|
141
|
+
if (!live.slots.length)
|
|
142
|
+
return waiting("account_enrollment_required");
|
|
143
|
+
if (!selected)
|
|
144
|
+
return waiting(live.slots.some((row) => row.provider === "codex")
|
|
145
|
+
? "codex_provider_access_unverified"
|
|
146
|
+
: "account_provider_access_unverified");
|
|
147
|
+
if (attempted)
|
|
148
|
+
return waiting("account_provider_access_unverified");
|
|
149
|
+
const cfg = loadConfig(ctx.home);
|
|
150
|
+
if (!cfg?.user || !["owner", "admin"].includes(cfg.user.role))
|
|
151
|
+
return waiting("account_validation_admin_required");
|
|
152
|
+
return { state: "pending" };
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
check,
|
|
156
|
+
act: async (ctx, journal, external) => {
|
|
157
|
+
if (attempted)
|
|
158
|
+
return waiting("account_provider_access_unverified");
|
|
159
|
+
const live = await inventory(ctx, journal, external);
|
|
160
|
+
if ("reason" in live)
|
|
161
|
+
return waiting(live.reason);
|
|
162
|
+
const selected = candidate(live.slots, ctx.now().getTime());
|
|
163
|
+
if (!selected)
|
|
164
|
+
return waiting("account_provider_access_unverified");
|
|
165
|
+
const support = await verifyOnboardRoutes(ctx, journal, [{ method: "POST", path: validatePath }], external);
|
|
166
|
+
if ("reason" in support)
|
|
167
|
+
return waiting(support.reason);
|
|
168
|
+
const cfg = loadConfig(ctx.home);
|
|
169
|
+
if (!equal(binding(ctx, journal), live.identity) ||
|
|
170
|
+
!cfg?.user ||
|
|
171
|
+
!["owner", "admin"].includes(cfg.user.role))
|
|
172
|
+
return waiting("account_identity_unverified");
|
|
173
|
+
const expiry = cfg.auth
|
|
174
|
+
? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime()
|
|
175
|
+
: 0;
|
|
176
|
+
const bearer = cfg.key ||
|
|
177
|
+
(cfg.auth && Number.isFinite(expiry) && expiry > 30000
|
|
178
|
+
? cfg.auth.accessToken
|
|
179
|
+
: undefined);
|
|
180
|
+
if (!bearer)
|
|
181
|
+
return waiting("account_login_refresh_required");
|
|
182
|
+
input.message?.("Checking one stored Claude account with a one-token provider request. This may use Claude quota. Codex credentials are not refreshed. Phase eligibility is checked separately.");
|
|
183
|
+
// A display callback must not be able to switch the identity used for this request.
|
|
184
|
+
if (!equal(binding(ctx, journal), live.identity))
|
|
185
|
+
return waiting("account_identity_unverified");
|
|
186
|
+
const deadline = new AbortController();
|
|
187
|
+
const signal = external
|
|
188
|
+
? AbortSignal.any([external, deadline.signal])
|
|
189
|
+
: deadline.signal;
|
|
190
|
+
const timer = setTimeout(() => deadline.abort(), 15000);
|
|
191
|
+
let remove = () => { };
|
|
192
|
+
try {
|
|
193
|
+
return await new Promise((resolve) => {
|
|
194
|
+
const stopped = () => resolve(waiting(external?.aborted
|
|
195
|
+
? "interrupted"
|
|
196
|
+
: "account_validation_unavailable"));
|
|
197
|
+
if (signal.aborted) {
|
|
198
|
+
stopped();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
signal.addEventListener("abort", stopped, { once: true });
|
|
202
|
+
remove = () => signal.removeEventListener("abort", stopped);
|
|
203
|
+
Promise.resolve()
|
|
204
|
+
.then(async () => {
|
|
205
|
+
if (signal.aborted)
|
|
206
|
+
return waiting("interrupted");
|
|
207
|
+
// UI callbacks can queue a config change before this deferred send executes.
|
|
208
|
+
if (!equal(binding(ctx, journal), live.identity))
|
|
209
|
+
return waiting("account_identity_unverified");
|
|
210
|
+
attempted = true;
|
|
211
|
+
const response = await ctx.fetch(`${support.origin}/api/v1/coding-accounts/${encodeURIComponent(selected.accountSlot)}/validate`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
redirect: "error",
|
|
214
|
+
signal,
|
|
215
|
+
headers: {
|
|
216
|
+
authorization: `Bearer ${bearer}`,
|
|
217
|
+
accept: "application/json",
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
if (response.status !== 200)
|
|
221
|
+
return waiting("account_validation_unavailable");
|
|
222
|
+
const body = object(await response.json());
|
|
223
|
+
if (signal.aborted ||
|
|
224
|
+
!equal(binding(ctx, journal), live.identity))
|
|
225
|
+
return waiting("account_identity_unverified");
|
|
226
|
+
const now = ctx.now().getTime();
|
|
227
|
+
if (!body ||
|
|
228
|
+
body.provider !== selected.provider ||
|
|
229
|
+
typeof body.checkedAtMs !== "number" ||
|
|
230
|
+
!Number.isSafeInteger(body.checkedAtMs) ||
|
|
231
|
+
body.checkedAtMs > now + 5000 ||
|
|
232
|
+
body.checkedAtMs < now - 30000 ||
|
|
233
|
+
!["working", "walled", "rejected", "inconclusive"].includes(String(body.result)))
|
|
234
|
+
return waiting("account_validation_unverified");
|
|
235
|
+
if (body.result !== "working")
|
|
236
|
+
return waiting(body.result === "walled"
|
|
237
|
+
? "account_provider_walled"
|
|
238
|
+
: body.result === "rejected"
|
|
239
|
+
? "account_provider_rejected"
|
|
240
|
+
: "account_provider_access_unverified");
|
|
241
|
+
proof = {
|
|
242
|
+
...live.identity,
|
|
243
|
+
slot: selected.accountSlot,
|
|
244
|
+
provider: selected.provider,
|
|
245
|
+
checkedAtMs: body.checkedAtMs,
|
|
246
|
+
};
|
|
247
|
+
input.message?.("Claude provider access was verified. This does not reserve a runner or prove that a phase can start.");
|
|
248
|
+
return check(ctx, journal, signal);
|
|
249
|
+
})
|
|
250
|
+
.then((result) => (signal.aborted ? stopped() : resolve(result)), () => stopped());
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
remove();
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Rule management is unbuilt. This disposition does not assess the customer's actual rules.
|
|
2
|
+
* Cloud readiness and first-ticket eligibility must still reject conflicting or unreadable rules.
|
|
3
|
+
*/
|
|
4
|
+
export function onboardAutomationManagementAdapter(input = {}) {
|
|
5
|
+
return {
|
|
6
|
+
check: async (_ctx, _journal, signal) => {
|
|
7
|
+
if (signal?.aborted)
|
|
8
|
+
return { state: "waiting", reason: "interrupted" };
|
|
9
|
+
input.message?.("Linear automation management is unavailable. Setup made no automation change. Cloud readiness still checks your existing rules.");
|
|
10
|
+
if (signal?.aborted)
|
|
11
|
+
return { state: "waiting", reason: "interrupted" };
|
|
12
|
+
return { state: "skipped", reason: "automation_management_unavailable" };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute, normalize } from "node:path";
|
|
3
|
+
import { CliError } from "./errors.js";
|
|
4
|
+
function refused() {
|
|
5
|
+
throw new CliError("The staged installation plan could not be verified. Run setup again.", "onboard-bootstrap-plan", 12);
|
|
6
|
+
}
|
|
7
|
+
function record(value) {
|
|
8
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
9
|
+
return refused();
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function exactKeys(value, keys) {
|
|
13
|
+
if (Object.keys(value).length !== keys.length ||
|
|
14
|
+
keys.some((key) => !Object.hasOwn(value, key)))
|
|
15
|
+
refused();
|
|
16
|
+
}
|
|
17
|
+
function text(value) {
|
|
18
|
+
if (typeof value !== "string" ||
|
|
19
|
+
value.length === 0 ||
|
|
20
|
+
value.length > 4096 ||
|
|
21
|
+
/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(value))
|
|
22
|
+
refused();
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function path(value) {
|
|
26
|
+
const result = text(value);
|
|
27
|
+
if (!isAbsolute(result) || normalize(result) !== result || result === "/")
|
|
28
|
+
refused();
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function origin(value) {
|
|
32
|
+
const result = text(value);
|
|
33
|
+
let url;
|
|
34
|
+
try {
|
|
35
|
+
url = new URL(result);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return refused();
|
|
39
|
+
}
|
|
40
|
+
if (url.protocol !== "https:" ||
|
|
41
|
+
url.origin !== result ||
|
|
42
|
+
url.username ||
|
|
43
|
+
url.password ||
|
|
44
|
+
url.search ||
|
|
45
|
+
url.hash)
|
|
46
|
+
refused();
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/** Copies a closed DTO before freezing it; a mutable caller object cannot alter the displayed plan. */
|
|
50
|
+
export function parseBootstrapPlan(input) {
|
|
51
|
+
const value = record(input);
|
|
52
|
+
exactKeys(value, [
|
|
53
|
+
"schema",
|
|
54
|
+
"home",
|
|
55
|
+
"origin",
|
|
56
|
+
"platform",
|
|
57
|
+
"arch",
|
|
58
|
+
"cliPath",
|
|
59
|
+
"skillsPath",
|
|
60
|
+
"statePath",
|
|
61
|
+
"dailyUpdate",
|
|
62
|
+
"artifacts",
|
|
63
|
+
]);
|
|
64
|
+
if (value.schema !== 1 ||
|
|
65
|
+
(value.platform !== "darwin" && value.platform !== "linux") ||
|
|
66
|
+
(value.arch !== "arm64" && value.arch !== "x64") ||
|
|
67
|
+
typeof value.dailyUpdate !== "boolean" ||
|
|
68
|
+
!Array.isArray(value.artifacts) ||
|
|
69
|
+
value.artifacts.length !== 4)
|
|
70
|
+
refused();
|
|
71
|
+
const kinds = new Set();
|
|
72
|
+
const artifacts = value.artifacts
|
|
73
|
+
.map((input) => {
|
|
74
|
+
const item = record(input);
|
|
75
|
+
exactKeys(item, ["kind", "version", "sha256"]);
|
|
76
|
+
if (item.kind !== "runtime" &&
|
|
77
|
+
item.kind !== "cli" &&
|
|
78
|
+
item.kind !== "skills" &&
|
|
79
|
+
item.kind !== "installer")
|
|
80
|
+
refused();
|
|
81
|
+
if (kinds.has(item.kind))
|
|
82
|
+
refused();
|
|
83
|
+
kinds.add(item.kind);
|
|
84
|
+
const version = text(item.version);
|
|
85
|
+
if (!/^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/.test(version) ||
|
|
86
|
+
typeof item.sha256 !== "string" ||
|
|
87
|
+
!/^[a-f0-9]{64}$/.test(item.sha256))
|
|
88
|
+
refused();
|
|
89
|
+
return Object.freeze({ kind: item.kind, version, sha256: item.sha256 });
|
|
90
|
+
})
|
|
91
|
+
.sort((a, b) => a.kind.localeCompare(b.kind));
|
|
92
|
+
const home = path(value.home), cliPath = path(value.cliPath), skillsPath = path(value.skillsPath), statePath = path(value.statePath);
|
|
93
|
+
if (new Set([home, cliPath, skillsPath, statePath]).size !== 4)
|
|
94
|
+
refused();
|
|
95
|
+
return Object.freeze({
|
|
96
|
+
schema: 1,
|
|
97
|
+
home,
|
|
98
|
+
origin: origin(value.origin),
|
|
99
|
+
platform: value.platform,
|
|
100
|
+
arch: value.arch,
|
|
101
|
+
cliPath,
|
|
102
|
+
skillsPath,
|
|
103
|
+
statePath,
|
|
104
|
+
dailyUpdate: value.dailyUpdate,
|
|
105
|
+
artifacts: Object.freeze(artifacts),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export function bootstrapPlanHash(plan) {
|
|
109
|
+
return createHash("sha256")
|
|
110
|
+
.update(JSON.stringify(parseBootstrapPlan(plan)))
|
|
111
|
+
.digest("hex");
|
|
112
|
+
}
|
|
113
|
+
export function bootstrapPlanLines(plan) {
|
|
114
|
+
return [
|
|
115
|
+
`Install on ${plan.platform}/${plan.arch} for ${plan.home}`,
|
|
116
|
+
`Catalyst cloud: ${plan.origin}`,
|
|
117
|
+
`Command: ${plan.cliPath}`,
|
|
118
|
+
`Skills: ${plan.skillsPath}`,
|
|
119
|
+
`Setup record: ${plan.statePath}`,
|
|
120
|
+
`Daily update: ${plan.dailyUpdate ? "schedule after installation" : "not selected"}`,
|
|
121
|
+
...plan.artifacts.map((artifact) => `${artifact.kind} ${artifact.version} (SHA-256 ${artifact.sha256})`),
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
/** A prepared native continuation is consumed before entering the first persistent machine stage.
|
|
125
|
+
* Retry after a partial install constructs a new preview from the actual machine and journal.
|
|
126
|
+
*/
|
|
127
|
+
export function createBootstrapPreview(input, ports) {
|
|
128
|
+
const plan = parseBootstrapPlan(input);
|
|
129
|
+
const planHash = bootstrapPlanHash(plan);
|
|
130
|
+
let consumed = false;
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
plan,
|
|
133
|
+
async recheck() {
|
|
134
|
+
if (consumed)
|
|
135
|
+
refused();
|
|
136
|
+
await ports.recheck();
|
|
137
|
+
},
|
|
138
|
+
assertCurrent() {
|
|
139
|
+
if (consumed)
|
|
140
|
+
refused();
|
|
141
|
+
ports.assertCurrent();
|
|
142
|
+
},
|
|
143
|
+
async continue(approved, signal) {
|
|
144
|
+
if (consumed ||
|
|
145
|
+
signal.aborted ||
|
|
146
|
+
approved.planHash !== planHash ||
|
|
147
|
+
bootstrapPlanHash(approved.plan) !== planHash ||
|
|
148
|
+
approved.origin !== plan.origin ||
|
|
149
|
+
!approved.account ||
|
|
150
|
+
!approved.person ||
|
|
151
|
+
!approved.runId ||
|
|
152
|
+
!Number.isSafeInteger(approved.ownerPid) ||
|
|
153
|
+
approved.ownerPid < 1 ||
|
|
154
|
+
!approved.ownerToken ||
|
|
155
|
+
!isAbsolute(approved.lockPath))
|
|
156
|
+
refused();
|
|
157
|
+
consumed = true;
|
|
158
|
+
ports.assertCurrent();
|
|
159
|
+
await ports.continue(approved, signal);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -119,7 +119,7 @@ async function read(ctx, journal, timeoutMs, external) {
|
|
|
119
119
|
!["GET", "POST"].includes(row.method) ||
|
|
120
120
|
typeof row.path !== "string" ||
|
|
121
121
|
row.path.length > 240 ||
|
|
122
|
-
|
|
122
|
+
!(/^\/(?:api\/v1\/|connect\/|me\/)[A-Za-z0-9_/-]+$/.test(row.path) || row.path === "/api/v1/coding-accounts/:slot/validate") ||
|
|
123
123
|
row.personalBearer !== true)
|
|
124
124
|
return { reason: "onboarding_capability_unavailable", origin };
|
|
125
125
|
const key = `${row.method} ${row.path}`;
|
|
@@ -142,6 +142,17 @@ async function read(ctx, journal, timeoutMs, external) {
|
|
|
142
142
|
remove();
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
/** Read current route support for context checks and selected writes without refreshing a token. */
|
|
146
|
+
export async function verifyOnboardRoutes(ctx, binding, requirements, signal, timeoutMs = 30_000) {
|
|
147
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000)
|
|
148
|
+
throw new Error("onboarding_capability_timeout_invalid");
|
|
149
|
+
const result = await read(ctx, binding, timeoutMs, signal);
|
|
150
|
+
if ("reason" in result)
|
|
151
|
+
return result;
|
|
152
|
+
if (requirements.some((route) => !result.capabilities.routes.has(routeKey(route))))
|
|
153
|
+
return { reason: "cloud_capability_unavailable", origin: result.origin };
|
|
154
|
+
return { origin: result.origin };
|
|
155
|
+
}
|
|
145
156
|
/** Guard both the observation and action before their first endpoint request. A route's real
|
|
146
157
|
* refusal is preserved only after the same-account cloud advertised personal bearer support. */
|
|
147
158
|
export function guardOnboardCapabilities(adapter, options) {
|
|
@@ -149,12 +160,9 @@ export function guardOnboardCapabilities(adapter, options) {
|
|
|
149
160
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000)
|
|
150
161
|
throw new Error("onboarding_capability_timeout_invalid");
|
|
151
162
|
const run = async (ctx, journal, signal, requirements, work) => {
|
|
152
|
-
const advertised = await
|
|
153
|
-
if ("reason" in advertised
|
|
154
|
-
|
|
155
|
-
const reason = "reason" in advertised
|
|
156
|
-
? advertised.reason
|
|
157
|
-
: "cloud_capability_unavailable";
|
|
163
|
+
const advertised = await verifyOnboardRoutes(ctx, journal, requirements, signal, timeoutMs);
|
|
164
|
+
if ("reason" in advertised) {
|
|
165
|
+
const reason = advertised.reason;
|
|
158
166
|
const url = advertised.origin
|
|
159
167
|
? `${advertised.origin}${safePages[options.fallback]}`
|
|
160
168
|
: undefined;
|