@catalyst-cloud/cli 0.14.10 → 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.
- package/CHANGELOG.md +9 -0
- package/README.md +41 -0
- package/dist/args.js +34 -2
- package/dist/capabilities.js +2 -1
- package/dist/cli-update-notice.js +76 -0
- package/dist/cli.js +263 -101
- package/dist/oauth.js +48 -18
- package/dist/onboard-accounts.js +34 -5
- package/dist/onboard-automation-management.js +27 -9
- package/dist/onboard-capacity.js +10 -1
- package/dist/onboard-existing.js +132 -31
- package/dist/onboard-github.js +171 -6
- package/dist/onboard-headless.js +647 -0
- package/dist/onboard-interrupts.js +41 -0
- package/dist/onboard-next.js +215 -0
- package/dist/onboard-permissions.js +100 -0
- package/dist/onboard-personal.js +46 -10
- package/dist/onboard-runner-cloud.js +213 -0
- package/dist/onboard-runner-egress.js +67 -0
- package/dist/onboard-runner.js +780 -0
- package/dist/onboard-runtime.js +48 -8
- package/dist/onboard-standalone-copy.js +110 -0
- package/dist/onboard-team-create.js +267 -0
- package/dist/onboard-ui.js +623 -29
- package/dist/onboard-workflow.js +311 -168
- package/dist/onboard-workspace.js +52 -5
- package/dist/onboard.js +359 -212
- package/dist/setup-events.js +100 -0
- package/dist/setup-onboard-copy.js +438 -0
- package/dist/setup-prompt-frame.js +94 -0
- package/dist/setup-prompts.js +108 -0
- package/dist/setup-render.js +339 -0
- package/dist/setup-signin.js +187 -0
- package/dist/setup.js +426 -0
- package/package.json +7 -3
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +3 -2
- 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 +1 -1
- package/vendor/README.md +4 -0
- package/vendor/self-host/compose.yaml +110 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.service +14 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.timer +10 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress.service +19 -0
- package/vendor/self-host/linux-session-egress/session-egress.py +291 -0
- package/vendor/self-host/provenance.json +11 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { loadConfig, normalizeBaseUrl, readManifest } from "./config.js";
|
|
5
|
+
import { contractVersionInRange } from "./contract.js";
|
|
6
|
+
const ADMISSION = "/api/v1/agent/runner-admission";
|
|
7
|
+
const KEYS = "/api/v1/agent/runner-keys";
|
|
8
|
+
const KEY_RESOURCE = `${KEYS}/:requestId`;
|
|
9
|
+
const SCOPES = ["mirror:read", "mirror:write", "mirror:feed"];
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
11
|
+
const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
12
|
+
function identity(ctx, binding) {
|
|
13
|
+
const cfg = loadConfig(ctx.home);
|
|
14
|
+
if (!cfg?.user || !["owner", "admin"].includes(cfg.user.role) || cfg.account !== (binding.account ?? binding.tenant) || cfg.user.id !== binding.membershipId || !binding.baseUrl || normalizeBaseUrl(cfg.baseUrl) !== normalizeBaseUrl(binding.baseUrl))
|
|
15
|
+
return null;
|
|
16
|
+
const expiry = cfg.auth ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime() : 0;
|
|
17
|
+
const bearer = cfg.key || (cfg.auth && Number.isFinite(expiry) && expiry > 30_000 ? cfg.auth.accessToken : undefined);
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(cfg.baseUrl);
|
|
20
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
21
|
+
return null;
|
|
22
|
+
return bearer ? { account: cfg.account, origin: url.origin, bearer } : null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Mutation failures and provider bodies never become terminal/receipt text. */
|
|
29
|
+
async function request(ctx, binding, path, method, body, signal) {
|
|
30
|
+
const who = identity(ctx, binding);
|
|
31
|
+
if (!who)
|
|
32
|
+
return { reason: "runner_identity_unverified" };
|
|
33
|
+
if (signal?.aborted)
|
|
34
|
+
return { reason: "interrupted" };
|
|
35
|
+
try {
|
|
36
|
+
const response = await ctx.fetch(`${who.origin}${path}`, { method, redirect: "error", signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000), headers: { authorization: `Bearer ${who.bearer}`, accept: "application/json", "cache-control": "no-cache", ...(body ? { "content-type": "application/json" } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
37
|
+
const value = object(await response.json().catch(() => null));
|
|
38
|
+
if (signal?.aborted)
|
|
39
|
+
return { reason: "interrupted" };
|
|
40
|
+
const current = identity(ctx, binding);
|
|
41
|
+
if (!current || current.account !== who.account || current.origin !== who.origin || current.bearer !== who.bearer)
|
|
42
|
+
return { reason: "runner_identity_unverified" };
|
|
43
|
+
return { status: response.status, body: value };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { reason: signal?.aborted ? "interrupted" : "runner_cloud_unavailable" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const RUNNER_ROUTES = new Map([
|
|
50
|
+
["GET", ADMISSION], ["PUT", ADMISSION], ["POST", KEYS], ["GET", KEY_RESOURCE], ["DELETE", KEY_RESOURCE],
|
|
51
|
+
].map(([method, path]) => [`${method} ${path}`, method === "POST" ? "requestId" : null]));
|
|
52
|
+
/** Optional runner discovery never widens either legacy route parser. A fresh same-account
|
|
53
|
+
* personal response advertises exact deployed operations, not authorization or consent. */
|
|
54
|
+
export async function verifyRunnerRoutes(ctx, binding, requirements, signal) {
|
|
55
|
+
const who = identity(ctx, binding);
|
|
56
|
+
if (!who)
|
|
57
|
+
return { reason: "runner_identity_unverified" };
|
|
58
|
+
const read = await request(ctx, binding, "/api/v1/agent/contract", "GET", undefined, signal);
|
|
59
|
+
if ("reason" in read)
|
|
60
|
+
return read;
|
|
61
|
+
const body = read.body;
|
|
62
|
+
if (read.status !== 200)
|
|
63
|
+
return { reason: "runner_capability_unavailable" };
|
|
64
|
+
if (object(body?.account)?.id !== who.account)
|
|
65
|
+
return { reason: "runner_identity_unverified" };
|
|
66
|
+
if (!body || typeof body.contractVersion !== "string" || contractVersionInRange(body.contractVersion, readManifest().tenantContractRange) !== true)
|
|
67
|
+
return { reason: "runner_capability_unverified" };
|
|
68
|
+
if (body.runnerOnboarding === undefined)
|
|
69
|
+
return { reason: "cloud_capability_unavailable" };
|
|
70
|
+
const section = object(body.runnerOnboarding);
|
|
71
|
+
if (!section || section.schema !== 1 || !Array.isArray(section.routes) || section.routes.length > RUNNER_ROUTES.size)
|
|
72
|
+
return { reason: "runner_capability_unverified" };
|
|
73
|
+
const routes = new Set();
|
|
74
|
+
for (const value of section.routes) {
|
|
75
|
+
const row = object(value);
|
|
76
|
+
if (!row || typeof row.method !== "string" || typeof row.path !== "string")
|
|
77
|
+
return { reason: "runner_capability_unverified" };
|
|
78
|
+
const key = `${row.method} ${row.path}`;
|
|
79
|
+
if (!RUNNER_ROUTES.has(key) || routes.has(key) || row.personalBearer !== true || row.takesWriteBudgetUnit !== false || row.idempotencyKeyField !== RUNNER_ROUTES.get(key))
|
|
80
|
+
return { reason: "runner_capability_unverified" };
|
|
81
|
+
routes.add(key);
|
|
82
|
+
}
|
|
83
|
+
if (requirements.some(row => !routes.has(`${row.method} ${row.path}`)))
|
|
84
|
+
return { reason: "cloud_capability_unavailable" };
|
|
85
|
+
return { origin: who.origin };
|
|
86
|
+
}
|
|
87
|
+
/** Read mode never changes admission. Act sends only the approved field; safe defaults and policy
|
|
88
|
+
* refusal belong to the server. A successful PUT still requires a fresh same-team readback. */
|
|
89
|
+
export async function runnerAdmission(ctx, journal, teamId, allowAct, signal) {
|
|
90
|
+
if (!identity(ctx, journal))
|
|
91
|
+
return { reason: "runner_identity_unverified" };
|
|
92
|
+
const support = await verifyRunnerRoutes(ctx, journal, [{ method: "GET", path: ADMISSION }, ...(allowAct ? [{ method: "PUT", path: ADMISSION }] : [])], signal);
|
|
93
|
+
if ("reason" in support)
|
|
94
|
+
return { reason: support.reason };
|
|
95
|
+
const path = `${ADMISSION}?account=${encodeURIComponent((journal.account ?? journal.tenant))}&team=${encodeURIComponent(teamId)}`;
|
|
96
|
+
if (allowAct) {
|
|
97
|
+
const write = await request(ctx, journal, path, "PUT", { admissionEnabled: true }, signal);
|
|
98
|
+
if ("reason" in write)
|
|
99
|
+
return write;
|
|
100
|
+
if (write.status === 409)
|
|
101
|
+
return { reason: "runner_admission_operator" };
|
|
102
|
+
if (write.status === 403)
|
|
103
|
+
return { reason: "runner_identity_unverified" };
|
|
104
|
+
if (write.status !== 200)
|
|
105
|
+
return { reason: "runner_admission_unverified" };
|
|
106
|
+
if (write.body?.account !== (journal.account ?? journal.tenant) || write.body?.team !== teamId || write.body?.admissionEnabled !== true)
|
|
107
|
+
return { reason: "runner_admission_unverified" };
|
|
108
|
+
}
|
|
109
|
+
const read = await request(ctx, journal, path, "GET", undefined, signal);
|
|
110
|
+
if ("reason" in read)
|
|
111
|
+
return read;
|
|
112
|
+
if (read.status === 409)
|
|
113
|
+
return { reason: "runner_admission_operator" };
|
|
114
|
+
if (read.status !== 200 || read.body?.account !== (journal.account ?? journal.tenant) || read.body?.team !== teamId || typeof read.body?.admissionEnabled !== "boolean")
|
|
115
|
+
return { reason: "runner_admission_unverified" };
|
|
116
|
+
return { ready: read.body.admissionEnabled };
|
|
117
|
+
}
|
|
118
|
+
/** Exclusive private marker, fsynced before POST. Malformed/foreign markers fail closed; they are
|
|
119
|
+
* never deleted or replaced to obtain a fresh provider key after an uncertain outcome. */
|
|
120
|
+
function keyRequest(ctx, journal, setup) {
|
|
121
|
+
const who = identity(ctx, journal);
|
|
122
|
+
if (!who || !journal.membershipId)
|
|
123
|
+
return { reason: "runner_identity_unverified" };
|
|
124
|
+
const path = join(setup.dir, "key-request.json");
|
|
125
|
+
try {
|
|
126
|
+
mkdirSync(setup.dir, { recursive: true, mode: 0o700 });
|
|
127
|
+
if (lstatSync(setup.dir).isSymbolicLink())
|
|
128
|
+
return { reason: "runner_org_key_journal_unavailable" };
|
|
129
|
+
const marker = { version: 1, requestId: randomUUID(), account: who.account, origin: who.origin, membershipId: journal.membershipId, teamId: setup.teamId, hostName: setup.hostName };
|
|
130
|
+
let fd;
|
|
131
|
+
try {
|
|
132
|
+
fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
|
|
136
|
+
return { reason: "runner_org_key_journal_unavailable" };
|
|
137
|
+
const info = lstatSync(path);
|
|
138
|
+
if (!info.isFile() || (info.mode & 0o077) !== 0 || info.size > 4096)
|
|
139
|
+
return { reason: "runner_org_key_journal_unavailable" };
|
|
140
|
+
const old = object(JSON.parse(readFileSync(path, "utf8")));
|
|
141
|
+
if (!old || old.version !== 1 || typeof old.requestId !== "string" || !UUID.test(old.requestId) || Object.entries(marker).some(([key, value]) => key !== "requestId" && old[key] !== value))
|
|
142
|
+
return { reason: "runner_org_key_journal_unavailable" };
|
|
143
|
+
return { marker: { ...marker, requestId: old.requestId }, existing: true };
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
writeFileSync(fd, JSON.stringify(marker) + "\n");
|
|
147
|
+
fsyncSync(fd);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
closeSync(fd);
|
|
151
|
+
}
|
|
152
|
+
const directory = openSync(setup.dir, constants.O_RDONLY);
|
|
153
|
+
try {
|
|
154
|
+
fsyncSync(directory);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
closeSync(directory);
|
|
158
|
+
}
|
|
159
|
+
return { marker, existing: false };
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { reason: "runner_org_key_journal_unavailable" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Called only for an explicitly selected runner that needs a key. One response may carry its
|
|
166
|
+
* value; it goes directly to the private volume. Recovered issuance never silently rotates,
|
|
167
|
+
* revokes or mints a second key. The caller revalidates the stored key before reporting ready. */
|
|
168
|
+
export async function issueRunnerOrgKey(ctx, journal, setup, store, signal) {
|
|
169
|
+
if (!identity(ctx, journal))
|
|
170
|
+
return { reason: "runner_identity_unverified" };
|
|
171
|
+
const support = await verifyRunnerRoutes(ctx, journal, [{ method: "POST", path: KEYS }, { method: "GET", path: KEY_RESOURCE }], signal);
|
|
172
|
+
if ("reason" in support)
|
|
173
|
+
return { reason: support.reason };
|
|
174
|
+
if (signal?.aborted)
|
|
175
|
+
return { reason: "interrupted" };
|
|
176
|
+
const saved = keyRequest(ctx, journal, setup);
|
|
177
|
+
if ("reason" in saved)
|
|
178
|
+
return saved;
|
|
179
|
+
const { marker } = saved;
|
|
180
|
+
const suffix = `?account=${encodeURIComponent(marker.account)}`;
|
|
181
|
+
if (saved.existing) {
|
|
182
|
+
const read = await request(ctx, journal, `${KEYS}/${marker.requestId}${suffix}`, "GET", undefined, signal);
|
|
183
|
+
if ("reason" in read)
|
|
184
|
+
return read;
|
|
185
|
+
// Only the exact deployed no-record response can retry this same UUID. The server's atomic
|
|
186
|
+
// claim prevents a delayed/concurrent POST from minting twice. Pending claims, missing orgs,
|
|
187
|
+
// malformed responses and lost issued values never authorize a new UUID or rotation.
|
|
188
|
+
const absent = read.status === 404 && read.body?.error === "not_found" && Object.keys(read.body).length === 1;
|
|
189
|
+
if (!absent) {
|
|
190
|
+
if (read.status !== 200 || read.body?.account !== marker.account || read.body?.requestId !== marker.requestId)
|
|
191
|
+
return { reason: "runner_org_key_reconciliation_pending" };
|
|
192
|
+
return { reason: "runner_org_key_recovery_required" };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const issued = await request(ctx, journal, `${KEYS}${suffix}`, "POST", { requestId: marker.requestId, name: setup.hostName }, signal);
|
|
196
|
+
if ("reason" in issued)
|
|
197
|
+
return issued;
|
|
198
|
+
if (issued.status === 403)
|
|
199
|
+
return { reason: "runner_identity_unverified" };
|
|
200
|
+
if (issued.status === 409)
|
|
201
|
+
return { reason: "runner_org_key_recovery_required" };
|
|
202
|
+
const key = object(issued.body?.key);
|
|
203
|
+
if (issued.status !== 201 || issued.body?.account !== marker.account || issued.body?.requestId !== marker.requestId || issued.body?.status !== "issued" || !key || typeof key.value !== "string" || !/^[\x21-\x7e]{16,1024}$/.test(key.value) || !Array.isArray(key.permissions) || key.permissions.length !== SCOPES.length || !SCOPES.every(scope => key.permissions.includes(scope)))
|
|
204
|
+
return { reason: "runner_org_key_response_unverified" };
|
|
205
|
+
if (signal?.aborted)
|
|
206
|
+
return { reason: "interrupted" };
|
|
207
|
+
try {
|
|
208
|
+
return await store(key.value) ? { stored: true } : { reason: "runner_org_key_write_failed" };
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return { reason: "runner_org_key_write_failed" };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { packageRoot } from './config.js';
|
|
6
|
+
export const NATIVE_EGRESS_DIR = '/var/lib/catalyst/session-egress';
|
|
7
|
+
export const EGRESS_ARTIFACTS = [
|
|
8
|
+
{ name: 'session-egress.py', digest: 'a20cedfa1e883127ec461e4b7f9588d379fe1633b8244ecd703c81f589d3f142', path: '/usr/lib/catalyst/session-egress.py', mode: 0o755 },
|
|
9
|
+
{ name: 'catalyst-session-egress.service', digest: '703c150ab23ec87aea47cfc394f63e061b66c4b8c1454a88a9be879c161ce2a5', path: '/etc/systemd/system/catalyst-session-egress.service', mode: 0o644 },
|
|
10
|
+
{ name: 'catalyst-session-egress-attest.service', digest: '94fa75ea0616f6a1172fc20ca637a5727c36456357e4a977d5bdd65f2fad2e8d', path: '/etc/systemd/system/catalyst-session-egress-attest.service', mode: 0o644 },
|
|
11
|
+
{ name: 'catalyst-session-egress-attest.timer', digest: '176622c11c835601b95f36e375ba3c2e993136976c67e5d49559f13a3454fef9', path: '/etc/systemd/system/catalyst-session-egress-attest.timer', mode: 0o644 },
|
|
12
|
+
];
|
|
13
|
+
const nodeReads = {
|
|
14
|
+
read: readFileSync,
|
|
15
|
+
stat: path => { const s = lstatSync(path); return { uid: s.uid, mode: s.mode, file: s.isFile(), directory: s.isDirectory(), symlink: s.isSymbolicLink() }; },
|
|
16
|
+
systemctl: (args, signal) => new Promise(resolve => { execFile('/usr/bin/systemctl', args, { timeout: 10000, signal, maxBuffer: 65536, env: { PATH: '/usr/bin:/usr/sbin:/bin:/sbin', LC_ALL: 'C' } }, (error, stdout) => resolve(error ? null : stdout)); }),
|
|
17
|
+
};
|
|
18
|
+
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
19
|
+
/** Read-only prerequisite. It cannot install units, invoke the producer or synthesize a proof.
|
|
20
|
+
* The unprivileged supervisor independently validates the same mounted proof before work. */
|
|
21
|
+
export async function nativeEgressReady(now, signal, io = nodeReads) {
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
for (const artifact of EGRESS_ARTIFACTS) {
|
|
26
|
+
const stat = io.stat(artifact.path);
|
|
27
|
+
if (stat.symlink || !stat.file || stat.uid !== 0 || (stat.mode & 0o777) !== artifact.mode || hash(io.read(artifact.path)) !== artifact.digest)
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const dir = io.stat(NATIVE_EGRESS_DIR), path = join(NATIVE_EGRESS_DIR, 'attestation.json'), stat = io.stat(path);
|
|
31
|
+
if (dir.symlink || !dir.directory || dir.uid !== 0 || (dir.mode & 0o777) !== 0o755 || stat.symlink || !stat.file || stat.uid !== 0 || (stat.mode & 0o777) !== 0o644)
|
|
32
|
+
return false;
|
|
33
|
+
const bytes = io.read(path);
|
|
34
|
+
if (bytes.length > 4096)
|
|
35
|
+
return false;
|
|
36
|
+
const proof = JSON.parse(bytes.toString('utf8'));
|
|
37
|
+
if (!proof || typeof proof !== 'object' || !('version' in proof) || proof.version !== 1 || !('enforcement' in proof) || proof.enforcement !== 'nftables-docker-forward-v1' || !('bridge' in proof) || proof.bridge !== 'catalyst-sess0' || !('verifiedAtMs' in proof) || typeof proof.verifiedAtMs !== 'number' || !Number.isSafeInteger(proof.verifiedAtMs) || proof.verifiedAtMs > now + 300000 || now - proof.verifiedAtMs > 900000)
|
|
38
|
+
return false;
|
|
39
|
+
for (const unit of ['catalyst-session-egress.service', 'catalyst-session-egress-attest.timer'])
|
|
40
|
+
if ((await io.systemctl(['is-enabled', unit], signal))?.trim() !== 'enabled')
|
|
41
|
+
return false;
|
|
42
|
+
const boot = await io.systemctl(['show', 'catalyst-session-egress.service', '--property=ActiveState,SubState,Result'], signal);
|
|
43
|
+
const timer = await io.systemctl(['show', 'catalyst-session-egress-attest.timer', '--property=ActiveState,SubState,NextElapseUSecMonotonic'], signal);
|
|
44
|
+
if (!boot || !timer)
|
|
45
|
+
return false;
|
|
46
|
+
const fields = (text) => Object.fromEntries(text.trim().split('\n').map(line => line.split('=')));
|
|
47
|
+
const b = fields(boot), t = fields(timer);
|
|
48
|
+
return !signal?.aborted && b.ActiveState === 'active' && b.SubState === 'exited' && b.Result === 'success' && t.ActiveState === 'active' && t.SubState === 'waiting' && typeof t.NextElapseUSecMonotonic === 'string' && t.NextElapseUSecMonotonic !== '0' && t.NextElapseUSecMonotonic !== '';
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Exact bundled bytes are checked before showing the separate, elevated install commands. */
|
|
55
|
+
export function nativeEgressInstallCommands() {
|
|
56
|
+
const root = join(packageRoot(), 'vendor/self-host/linux-session-egress');
|
|
57
|
+
try {
|
|
58
|
+
for (const artifact of EGRESS_ARTIFACTS)
|
|
59
|
+
if (hash(readFileSync(join(root, artifact.name))) !== artifact.digest)
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const quote = (s) => "'" + s.replaceAll("'", "'\\''") + "'";
|
|
66
|
+
return EGRESS_ARTIFACTS.map(a => `sudo install ${a.name === 'session-egress.py' ? '-D ' : ''}-o root -g root -m ${a.mode.toString(8)} ${quote(join(root, a.name))} ${quote(a.path)}`).join('\n') + '\nsudo systemctl daemon-reload\nsudo systemctl enable --now catalyst-session-egress.service catalyst-session-egress-attest.timer\nsudo /usr/bin/python3 /usr/lib/catalyst/session-egress.py attest';
|
|
67
|
+
}
|