@kvell007/embed-labs-cli 0.1.0-alpha.21 → 0.1.0-alpha.23
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/README.md +6 -2
- package/dist/index.js +839 -76
- package/dist/index.js.map +1 -1
- package/dist/local-toolchain.d.ts +2 -0
- package/dist/local-toolchain.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { createHash, createHmac, generateKeyPairSync, randomBytes, sign as signCrypto } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { access, cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { arch, hostname, homedir, platform, tmpdir } from "node:os";
|
|
8
8
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { startServer } from "@embed-labs/local-bridge";
|
|
11
10
|
import { composeBootLogoPackage } from "./image-compose.js";
|
|
12
11
|
import { buildTaishanPiQtSmoke, compileTaishanPiSingleFile, currentLocalToolchain, installLocalToolchain, latestLocalToolchain, validateLocalToolchain } from "./local-toolchain.js";
|
|
13
12
|
import { fail, ok } from "@embed-labs/protocol";
|
|
@@ -16,10 +15,14 @@ const CLI_MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
|
16
15
|
const SOURCE_CHECKOUT_ROOT = resolve(CLI_MODULE_DIR, "..", "..", "..");
|
|
17
16
|
const qrcodeTerminal = require("qrcode-terminal");
|
|
18
17
|
const DEFAULT_BRIDGE_URL = process.env.EMBED_BRIDGE_URL ?? "http://127.0.0.1:18083";
|
|
19
|
-
const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "
|
|
18
|
+
const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "https://api.embedboard.com";
|
|
20
19
|
const DEFAULT_AUTH_FILE = process.env.EMBED_AUTH_FILE ?? ".embed-labs/auth.json";
|
|
20
|
+
const DEFAULT_DEVICE_FILE = process.env.EMBED_DEVICE_FILE ?? join(dirname(DEFAULT_AUTH_FILE), "device.json");
|
|
21
21
|
const DEFAULT_DASHBOARD_URL = process.env.EMBED_DASHBOARD_URL ?? "https://api.embedboard.com/dashboard";
|
|
22
|
+
const EMBED_CLIENT_NAME = "embedlabs-cli";
|
|
23
|
+
const EMBED_CLIENT_VERSION = process.env.EMBED_CLIENT_VERSION ?? "0.1.0";
|
|
22
24
|
const DEFAULT_AGENT_ARTIFACT_DIR = process.env.EMBED_AGENT_ARTIFACT_DIR ?? ".embed-labs/artifacts";
|
|
25
|
+
const TOOL_INTEGRITY_RELOGIN_MESSAGE = "工具完整性校验失败,请在当前电脑重新执行 embedlabs auth login --token <key>";
|
|
23
26
|
const DOCTOR_USAGE = "Usage: embed doctor [--json]";
|
|
24
27
|
const DOCTOR_HTTP_TIMEOUT_MS = 8000;
|
|
25
28
|
const MIN_NODE_MAJOR = 20;
|
|
@@ -30,7 +33,14 @@ const DEFAULT_PLUGIN_RELEASE_URL = process.env.EMBED_PLUGIN_RELEASE_URL?.trim()
|
|
|
30
33
|
const PLUGIN_LIST_USAGE = "Usage: embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]";
|
|
31
34
|
const CODEX_PLUGIN_NAME = "embed-labs";
|
|
32
35
|
const CODEX_MARKETPLACE_NAME = "embed-labs";
|
|
33
|
-
const
|
|
36
|
+
const LEGACY_CODEX_PLUGIN_NAMES = [
|
|
37
|
+
"dbt-agent",
|
|
38
|
+
"Dbt Agent",
|
|
39
|
+
"development-board-toolchain",
|
|
40
|
+
"development-board-toolchain-dev",
|
|
41
|
+
"deve"
|
|
42
|
+
];
|
|
43
|
+
const LEGACY_CODEX_MARKETPLACE_NAMES = new Set(["embed-labs-plugins", "plugins", "Plugins", "deve"]);
|
|
34
44
|
const PLUGIN_INSTALL_USAGE = "Usage: embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]";
|
|
35
45
|
const CLOUD_TASK_ARTIFACTS_USAGE = "Usage: embed cloud task artifacts <task_id> [--json]";
|
|
36
46
|
const CLOUD_TASK_EVIDENCE_USAGE = "Usage: embed cloud task evidence <task_id> [--json]";
|
|
@@ -91,6 +101,10 @@ const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--b
|
|
|
91
101
|
const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--release-root <path>] [--mode minimal|compile|qt|full|images] [--json]";
|
|
92
102
|
const LOCAL_COMPILE_TAISHANPI_USAGE = "Usage: embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]";
|
|
93
103
|
const LOCAL_BUILD_QT_SMOKE_USAGE = "Usage: embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]";
|
|
104
|
+
const AUTH_DEVICE_STATUS_USAGE = "Usage: embed auth device status [--json]";
|
|
105
|
+
const AUTH_DEVICE_LIST_USAGE = "Usage: embed auth device list [--json]";
|
|
106
|
+
const AUTH_DEVICE_REVOKE_USAGE = "Usage: embed auth device revoke <device_id> [--json]";
|
|
107
|
+
const AUTH_DEVICE_RENAME_USAGE = "Usage: embed auth device rename <device_id> --label <name> [--json]";
|
|
94
108
|
const BOARD_REGISTRY_LIST_USAGE = "Usage: embed board registry list [--json]";
|
|
95
109
|
const BOARD_REGISTRY_SHOW_USAGE = "Usage: embed board registry show <template_id> [--json]";
|
|
96
110
|
const BOARD_METHODS_USAGE = "Usage: embed board methods <template_id> [--json]";
|
|
@@ -100,9 +114,10 @@ const MODEL_LIST_USAGE = "Usage: embed model list [--json]";
|
|
|
100
114
|
const MODEL_DEFAULT_USAGE = "Usage: embed model default [--json]";
|
|
101
115
|
const SERVICE_MODES_USAGE = "Usage: embed service modes [--json]";
|
|
102
116
|
const AGENT_RUN_USAGE = "Usage: embed agent run --prompt <request> [--account <account_id>] [--workspace <workspace_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--max-tool-calls 6] [--host <ip>] [--ports 22,15301] [--artifact <local_file>|--artifact-id <artifact_id>|--artifact-task <task_id>] [--artifact-output <path>] [--remote-path <path>] [--run] [--approve] [--json]";
|
|
117
|
+
let cachedLocalHardwareFingerprint;
|
|
103
118
|
const TOOL_LIST_USAGE = "Usage: embed tool list [--json]";
|
|
104
119
|
const TOOL_CALL_USAGE = "Usage: embed tool call <capability_id> [--input-json '<json>'] [--approve] [--json]";
|
|
105
|
-
const MCP_TOOL_EVENT_USAGE = "Usage: embed mcp log --tool <tool_name> [--client codex|opencode] [--mode local_ai|server_ai] [--server-model-used true|false] [--success true|false] [--request-id <id>] [--duration-ms <ms>] [--input-summary <text>] [--output-summary <text>] [--json]";
|
|
120
|
+
const MCP_TOOL_EVENT_USAGE = "Usage: embed mcp log --tool <tool_name> [--client codex|opencode] [--mode local_ai|server_ai] [--local-device-id <id>] [--server-model-used true|false] [--success true|false] [--request-id <id>] [--duration-ms <ms>] [--input-summary <text>] [--output-summary <text>] [--json]";
|
|
106
121
|
const BOARD_DEPLOY_TAISHANPI_USAGE = "Usage: embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--user root] [--remote-path /userdata/embed-labs/apps/app] [--run] [--timeout 30] [--json]";
|
|
107
122
|
const CLOUD_TASK_EVENT_APPEND_USAGE = "Usage: embed cloud task event append <task_id> [--state <state>] [--progress-stage <stage>|--stage <stage>] [--progress-text <text>|--message <text>] [--progress-percent 0-100] [--severity info|warning|error] [--type <event_type>] [--artifact-json '<json>'] [--evidence-json '<json>'] [--json]";
|
|
108
123
|
const TASK_STATES = new Set([
|
|
@@ -152,11 +167,7 @@ async function main(argv) {
|
|
|
152
167
|
return output(parsed, await bridgePost("/v1/board/taishanpi/deploy", request), renderBoardDeployResult);
|
|
153
168
|
}
|
|
154
169
|
if (area === "bridge" && action === "start") {
|
|
155
|
-
|
|
156
|
-
host: stringFlag(parsed, "host"),
|
|
157
|
-
port: numberFlag(parsed, "port")
|
|
158
|
-
});
|
|
159
|
-
return await waitForever();
|
|
170
|
+
return await runBridgeStart(parsed);
|
|
160
171
|
}
|
|
161
172
|
if (area === "bridge" && action === "status") {
|
|
162
173
|
return output(parsed, await bridgeGet("/healthz"), renderBridgeStatus);
|
|
@@ -293,6 +304,31 @@ async function main(argv) {
|
|
|
293
304
|
if (area === "auth" && action === "status") {
|
|
294
305
|
return output(parsed, ok(await authStatus()), renderAuthStatus);
|
|
295
306
|
}
|
|
307
|
+
if (area === "auth" && action === "device") {
|
|
308
|
+
const deviceAction = parsed.command[2] ?? "status";
|
|
309
|
+
if (deviceAction === "status") {
|
|
310
|
+
const result = await authDeviceStatus(parsed);
|
|
311
|
+
return output(parsed, result, renderAuthDeviceStatus, result.ok ? 0 : 2);
|
|
312
|
+
}
|
|
313
|
+
if (deviceAction === "list") {
|
|
314
|
+
const result = await authDeviceList(parsed);
|
|
315
|
+
return output(parsed, result, renderAuthDeviceList, result.ok ? 0 : 2);
|
|
316
|
+
}
|
|
317
|
+
if (deviceAction === "revoke") {
|
|
318
|
+
const result = await authDeviceRevoke(parsed);
|
|
319
|
+
return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
|
|
320
|
+
}
|
|
321
|
+
if (deviceAction === "rename") {
|
|
322
|
+
const result = await authDeviceRename(parsed);
|
|
323
|
+
return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
|
|
324
|
+
}
|
|
325
|
+
return output(parsed, fail("invalid_args", [
|
|
326
|
+
AUTH_DEVICE_STATUS_USAGE,
|
|
327
|
+
AUTH_DEVICE_LIST_USAGE,
|
|
328
|
+
AUTH_DEVICE_REVOKE_USAGE,
|
|
329
|
+
AUTH_DEVICE_RENAME_USAGE
|
|
330
|
+
].join("\n")), undefined, 2);
|
|
331
|
+
}
|
|
296
332
|
if (area === "auth" && action === "logout") {
|
|
297
333
|
await rm(DEFAULT_AUTH_FILE, { force: true });
|
|
298
334
|
return output(parsed, ok(await authStatus()), renderAuthStatus);
|
|
@@ -1356,19 +1392,34 @@ function isApiResponse(value) {
|
|
|
1356
1392
|
return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
|
|
1357
1393
|
}
|
|
1358
1394
|
async function bridgeGet(path) {
|
|
1359
|
-
|
|
1360
|
-
headers: bridgeHeaders("GET", path, "")
|
|
1361
|
-
});
|
|
1362
|
-
return await response.json();
|
|
1395
|
+
return await bridgeRequest("GET", path);
|
|
1363
1396
|
}
|
|
1364
1397
|
async function bridgePost(path, body) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1398
|
+
return await bridgeRequest("POST", path, body);
|
|
1399
|
+
}
|
|
1400
|
+
async function bridgeRequest(method, path, body) {
|
|
1401
|
+
const bodyText = body === undefined ? "" : JSON.stringify(body);
|
|
1402
|
+
const makeRequest = async () => {
|
|
1403
|
+
const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
|
|
1404
|
+
method,
|
|
1405
|
+
headers: bridgeHeaders(method, path, method === "POST" ? bodyText : "", method === "POST" ? { "content-type": "application/json" } : {}),
|
|
1406
|
+
body: method === "POST" ? bodyText : undefined
|
|
1407
|
+
});
|
|
1408
|
+
return await response.json();
|
|
1409
|
+
};
|
|
1410
|
+
try {
|
|
1411
|
+
return await makeRequest();
|
|
1412
|
+
}
|
|
1413
|
+
catch (error) {
|
|
1414
|
+
if (!isBridgeConnectionFailure(error)) {
|
|
1415
|
+
throw error;
|
|
1416
|
+
}
|
|
1417
|
+
const started = await ensureBridgeStartedForRequest();
|
|
1418
|
+
if (!started.ok) {
|
|
1419
|
+
return started;
|
|
1420
|
+
}
|
|
1421
|
+
return await makeRequest();
|
|
1422
|
+
}
|
|
1372
1423
|
}
|
|
1373
1424
|
function bridgeHeaders(method, path, bodyText, base = {}) {
|
|
1374
1425
|
const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
|
|
@@ -1397,6 +1448,87 @@ function addBridgeRequestSignature(headers, method, pathWithQuery, bodyText, tok
|
|
|
1397
1448
|
headers["x-embed-body-sha256"] = bodySha256;
|
|
1398
1449
|
headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
|
|
1399
1450
|
}
|
|
1451
|
+
function isBridgeConnectionFailure(error) {
|
|
1452
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1453
|
+
return message.includes("fetch failed") ||
|
|
1454
|
+
message.includes("ECONNREFUSED") ||
|
|
1455
|
+
message.includes("ECONNRESET") ||
|
|
1456
|
+
message.includes("UND_ERR_SOCKET");
|
|
1457
|
+
}
|
|
1458
|
+
async function ensureBridgeStartedForRequest() {
|
|
1459
|
+
if (process.env.EMBED_BRIDGE_AUTO_START === "0") {
|
|
1460
|
+
return fail("bridge_unavailable", `embed-local-bridge is not running at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1461
|
+
remediation: `Start it with: embed bridge start`
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
let bridgeURL;
|
|
1465
|
+
try {
|
|
1466
|
+
bridgeURL = new URL(DEFAULT_BRIDGE_URL);
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
return fail("bridge_url_invalid", `EMBED_BRIDGE_URL is not a valid URL: ${DEFAULT_BRIDGE_URL}`);
|
|
1470
|
+
}
|
|
1471
|
+
if (!isLocalBridgeURL(bridgeURL)) {
|
|
1472
|
+
return fail("bridge_unavailable", `embed-local-bridge is not reachable at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1473
|
+
remediation: `Start the bridge for that host, or set EMBED_BRIDGE_URL to a local bridge URL.`
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
const launcher = await resolveBridgeLauncher();
|
|
1477
|
+
const host = bridgeURL.hostname === "::1" ? "::1" : bridgeURL.hostname || "127.0.0.1";
|
|
1478
|
+
const port = bridgeURL.port || "18083";
|
|
1479
|
+
const env = {
|
|
1480
|
+
...process.env,
|
|
1481
|
+
EMBED_BRIDGE_HOST: host,
|
|
1482
|
+
EMBED_BRIDGE_PORT: port
|
|
1483
|
+
};
|
|
1484
|
+
const child = spawn(launcher.command, [...launcher.args, "--host", host, "--port", port], {
|
|
1485
|
+
cwd: process.cwd(),
|
|
1486
|
+
detached: true,
|
|
1487
|
+
stdio: "ignore",
|
|
1488
|
+
env
|
|
1489
|
+
});
|
|
1490
|
+
child.unref();
|
|
1491
|
+
const ready = await waitForBridgeHealth(bridgeURL, 8000);
|
|
1492
|
+
if (!ready.ok) {
|
|
1493
|
+
return ready;
|
|
1494
|
+
}
|
|
1495
|
+
return ok({
|
|
1496
|
+
started: true,
|
|
1497
|
+
bridge_url: DEFAULT_BRIDGE_URL,
|
|
1498
|
+
command: launcher.command
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
function isLocalBridgeURL(url) {
|
|
1502
|
+
const host = url.hostname.toLowerCase();
|
|
1503
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1504
|
+
}
|
|
1505
|
+
async function waitForBridgeHealth(bridgeURL, timeoutMs) {
|
|
1506
|
+
const deadline = Date.now() + timeoutMs;
|
|
1507
|
+
let lastError = "";
|
|
1508
|
+
while (Date.now() < deadline) {
|
|
1509
|
+
try {
|
|
1510
|
+
const response = await fetch(new URL("/healthz", bridgeURL), {
|
|
1511
|
+
headers: bridgeHeaders("GET", "/healthz", "")
|
|
1512
|
+
});
|
|
1513
|
+
const parsed = await response.json();
|
|
1514
|
+
if (parsed.ok) {
|
|
1515
|
+
return parsed;
|
|
1516
|
+
}
|
|
1517
|
+
lastError = parsed.error?.message ?? `HTTP ${response.status}`;
|
|
1518
|
+
}
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1521
|
+
}
|
|
1522
|
+
await delay(100);
|
|
1523
|
+
}
|
|
1524
|
+
return fail("bridge_start_failed", `embed-local-bridge did not become healthy at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1525
|
+
remediation: `Run embed bridge start in a separate terminal and retry.`,
|
|
1526
|
+
details: { last_error: lastError }
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
function delay(ms) {
|
|
1530
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
1531
|
+
}
|
|
1400
1532
|
async function cloudGet(path) {
|
|
1401
1533
|
return await cloudRequest("GET", path);
|
|
1402
1534
|
}
|
|
@@ -1406,17 +1538,23 @@ async function cloudPost(path, body) {
|
|
|
1406
1538
|
async function cloudDownloadArtifact(artifactId, outputPath) {
|
|
1407
1539
|
try {
|
|
1408
1540
|
const headers = {};
|
|
1409
|
-
const
|
|
1410
|
-
if (token) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1541
|
+
const auth = await cloudAuthConfig();
|
|
1542
|
+
if (auth.token) {
|
|
1543
|
+
if (auth.device) {
|
|
1544
|
+
const integrity = await validateLocalDeviceIntegrity(auth.device);
|
|
1545
|
+
if (!integrity.ok) {
|
|
1546
|
+
return integrity;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
1550
|
+
addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
|
|
1413
1551
|
}
|
|
1414
1552
|
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
|
|
1415
1553
|
headers: Object.keys(headers).length > 0 ? headers : undefined
|
|
1416
1554
|
});
|
|
1417
1555
|
if (!response.ok) {
|
|
1418
1556
|
const parsed = await parseErrorResponse(response);
|
|
1419
|
-
return parsed ? enrichCloudAuthFailure(parsed, Boolean(token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
|
|
1557
|
+
return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
|
|
1420
1558
|
}
|
|
1421
1559
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1422
1560
|
const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
|
|
@@ -1449,10 +1587,16 @@ async function cloudRequest(method, path, body) {
|
|
|
1449
1587
|
if (bodyText) {
|
|
1450
1588
|
headers["content-type"] = "application/json";
|
|
1451
1589
|
}
|
|
1452
|
-
const
|
|
1453
|
-
if (token) {
|
|
1454
|
-
|
|
1455
|
-
|
|
1590
|
+
const auth = await cloudAuthConfig();
|
|
1591
|
+
if (auth.token) {
|
|
1592
|
+
if (auth.device) {
|
|
1593
|
+
const integrity = await validateLocalDeviceIntegrity(auth.device);
|
|
1594
|
+
if (!integrity.ok) {
|
|
1595
|
+
return integrity;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
1599
|
+
addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
|
|
1456
1600
|
}
|
|
1457
1601
|
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
|
|
1458
1602
|
method,
|
|
@@ -1460,7 +1604,7 @@ async function cloudRequest(method, path, body) {
|
|
|
1460
1604
|
body: body === undefined ? undefined : bodyText
|
|
1461
1605
|
});
|
|
1462
1606
|
const parsed = await response.json();
|
|
1463
|
-
return enrichCloudAuthFailure(parsed, Boolean(token));
|
|
1607
|
+
return enrichCloudAuthFailure(parsed, Boolean(auth.token));
|
|
1464
1608
|
}
|
|
1465
1609
|
catch (error) {
|
|
1466
1610
|
return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
|
|
@@ -1468,8 +1612,42 @@ async function cloudRequest(method, path, body) {
|
|
|
1468
1612
|
});
|
|
1469
1613
|
}
|
|
1470
1614
|
}
|
|
1615
|
+
async function validateLocalDeviceIntegrity(device) {
|
|
1616
|
+
const current = await localHardwareFingerprint();
|
|
1617
|
+
if (current.fingerprint_hash === device.fingerprint_hash) {
|
|
1618
|
+
return ok(undefined);
|
|
1619
|
+
}
|
|
1620
|
+
return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
|
|
1621
|
+
remediation: [
|
|
1622
|
+
"当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
|
|
1623
|
+
TOOL_INTEGRITY_RELOGIN_MESSAGE,
|
|
1624
|
+
"如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
|
|
1625
|
+
].join("\n"),
|
|
1626
|
+
details: {
|
|
1627
|
+
expected_fingerprint_hash: device.fingerprint_hash,
|
|
1628
|
+
current_fingerprint_hash: current.fingerprint_hash,
|
|
1629
|
+
platform: current.platform,
|
|
1630
|
+
arch: current.arch,
|
|
1631
|
+
fingerprint_source: current.source
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1471
1635
|
function enrichCloudAuthFailure(response, hadToken) {
|
|
1472
|
-
if (response.ok
|
|
1636
|
+
if (response.ok) {
|
|
1637
|
+
return response;
|
|
1638
|
+
}
|
|
1639
|
+
if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
|
|
1640
|
+
return fail(response.error.code, response.error.message, {
|
|
1641
|
+
remediation: [
|
|
1642
|
+
"This computer is not fully registered for the configured Embed Labs API Token.",
|
|
1643
|
+
"Run: embedlabs auth login --token <your_token>",
|
|
1644
|
+
"Then verify with: embedlabs auth device status",
|
|
1645
|
+
"If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
|
|
1646
|
+
].join("\n"),
|
|
1647
|
+
details: response.error.details
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
if (response.error.code !== "unauthorized") {
|
|
1473
1651
|
return response;
|
|
1474
1652
|
}
|
|
1475
1653
|
if (!hadToken) {
|
|
@@ -1501,7 +1679,7 @@ function cloudAuthSetupDetails() {
|
|
|
1501
1679
|
auth_file: DEFAULT_AUTH_FILE
|
|
1502
1680
|
};
|
|
1503
1681
|
}
|
|
1504
|
-
function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token) {
|
|
1682
|
+
function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
|
|
1505
1683
|
if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
|
|
1506
1684
|
return;
|
|
1507
1685
|
}
|
|
@@ -1509,11 +1687,21 @@ function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, toke
|
|
|
1509
1687
|
const nonce = randomBytes(16).toString("hex");
|
|
1510
1688
|
const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
|
|
1511
1689
|
const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
1512
|
-
const canonical =
|
|
1690
|
+
const canonical = device
|
|
1691
|
+
? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
|
|
1692
|
+
: cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
|
|
1513
1693
|
headers["x-embed-key-id"] = keyId;
|
|
1514
1694
|
headers["x-embed-timestamp"] = timestamp;
|
|
1515
1695
|
headers["x-embed-nonce"] = nonce;
|
|
1516
1696
|
headers["x-embed-body-sha256"] = bodySha256;
|
|
1697
|
+
if (device) {
|
|
1698
|
+
headers["x-embed-signature-version"] = "v2";
|
|
1699
|
+
headers["x-embed-device-id"] = device.device_id;
|
|
1700
|
+
headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
|
|
1701
|
+
headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
|
|
1702
|
+
headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
|
|
1703
|
+
headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
|
|
1704
|
+
}
|
|
1517
1705
|
headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
|
|
1518
1706
|
}
|
|
1519
1707
|
function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
|
|
@@ -1525,6 +1713,19 @@ function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bo
|
|
|
1525
1713
|
bodySha256
|
|
1526
1714
|
].join("\n");
|
|
1527
1715
|
}
|
|
1716
|
+
function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
|
|
1717
|
+
return [
|
|
1718
|
+
method.toUpperCase(),
|
|
1719
|
+
normalizeCloudPathForSignature(pathWithQuery),
|
|
1720
|
+
timestamp,
|
|
1721
|
+
nonce,
|
|
1722
|
+
bodySha256,
|
|
1723
|
+
device.device_id,
|
|
1724
|
+
device.fingerprint_hash,
|
|
1725
|
+
clientName,
|
|
1726
|
+
clientVersion
|
|
1727
|
+
].join("\n");
|
|
1728
|
+
}
|
|
1528
1729
|
function normalizeCloudPathForSignature(pathWithQuery) {
|
|
1529
1730
|
try {
|
|
1530
1731
|
const parsed = new URL(pathWithQuery, "http://embed.local");
|
|
@@ -1951,17 +2152,22 @@ async function cleanupLegacyCodexPluginRemnants(targetRoot) {
|
|
|
1951
2152
|
const warnings = [];
|
|
1952
2153
|
const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
|
|
1953
2154
|
const legacyPaths = [
|
|
1954
|
-
join(targetRoot, "
|
|
1955
|
-
join(targetRoot, "development-board-toolchain"),
|
|
1956
|
-
join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME),
|
|
1957
|
-
join(targetRoot, "cache", "embed-labs", "dbt-agent"),
|
|
1958
|
-
join(targetRoot, "cache", "dbt-agent"),
|
|
1959
|
-
join(targetRoot, "cache", "plugins", "dbt-agent")
|
|
2155
|
+
join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
|
|
1960
2156
|
];
|
|
2157
|
+
for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
|
|
2158
|
+
legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
|
|
2159
|
+
}
|
|
2160
|
+
for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
|
|
2161
|
+
legacyPaths.push(join(targetRoot, pluginName));
|
|
2162
|
+
legacyPaths.push(join(targetRoot, "cache", pluginName));
|
|
2163
|
+
for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
|
|
2164
|
+
legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
1961
2167
|
legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
|
|
1962
2168
|
if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
|
|
1963
2169
|
legacyPaths.push(join(defaultCodexHome(), ".tmp", "plugins"), join(defaultCodexHome(), ".tmp", "plugins.sha"), join(defaultCodexHome(), "ambient-suggestions"), join(defaultCodexHome(), "skills", "dbt-agent"), join(defaultCodexHome(), "skills", "development-board-toolchain-dev"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-live-board-ops"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-platform-plugin-maintenance"));
|
|
1964
|
-
legacyPaths.push(...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
|
|
2170
|
+
legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
|
|
1965
2171
|
}
|
|
1966
2172
|
for (const candidate of legacyPaths) {
|
|
1967
2173
|
try {
|
|
@@ -2054,6 +2260,11 @@ function legacyDevelopmentBoardRuntimePluginPaths() {
|
|
|
2054
2260
|
join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
|
|
2055
2261
|
];
|
|
2056
2262
|
}
|
|
2263
|
+
function legacyCodexLocalMarketplacePaths() {
|
|
2264
|
+
return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
|
|
2265
|
+
.filter((name) => name !== CODEX_MARKETPLACE_NAME)
|
|
2266
|
+
.map((name) => join(defaultCodexHome(), "local-marketplaces", name));
|
|
2267
|
+
}
|
|
2057
2268
|
async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
|
|
2058
2269
|
const paths = [];
|
|
2059
2270
|
const pluginRoot = join(homedir(), ".agents", "plugins");
|
|
@@ -2080,9 +2291,72 @@ async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
|
|
|
2080
2291
|
return paths;
|
|
2081
2292
|
}
|
|
2082
2293
|
function isLegacyHomeAgentsMarketplace(text) {
|
|
2083
|
-
return
|
|
2084
|
-
|
|
2085
|
-
|
|
2294
|
+
return marketplaceTextHasLegacyCodexPlugin(text);
|
|
2295
|
+
}
|
|
2296
|
+
async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
|
|
2297
|
+
const configPath = codexConfigPath();
|
|
2298
|
+
let text = "";
|
|
2299
|
+
try {
|
|
2300
|
+
text = await readFile(configPath, "utf8");
|
|
2301
|
+
}
|
|
2302
|
+
catch {
|
|
2303
|
+
return [];
|
|
2304
|
+
}
|
|
2305
|
+
const paths = new Set();
|
|
2306
|
+
for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
|
|
2307
|
+
for (const candidate of [
|
|
2308
|
+
join(projectPath, ".agents", "plugins", "marketplace.json"),
|
|
2309
|
+
join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
|
|
2310
|
+
join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
|
|
2311
|
+
]) {
|
|
2312
|
+
try {
|
|
2313
|
+
if (!await pathExists(candidate))
|
|
2314
|
+
continue;
|
|
2315
|
+
const current = await readFile(candidate, "utf8");
|
|
2316
|
+
if (marketplaceTextHasLegacyCodexPlugin(current)) {
|
|
2317
|
+
paths.add(candidate);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
catch (error) {
|
|
2321
|
+
warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return Array.from(paths);
|
|
2326
|
+
}
|
|
2327
|
+
function legacyCodexProjectPathsFromConfig(text) {
|
|
2328
|
+
const paths = [];
|
|
2329
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2330
|
+
for (const line of lines) {
|
|
2331
|
+
const table = parseTomlTableHeader(line);
|
|
2332
|
+
const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
|
|
2333
|
+
if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
|
|
2334
|
+
paths.push(match[1].replace(/\\"/g, '"'));
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
return paths;
|
|
2338
|
+
}
|
|
2339
|
+
function marketplaceTextHasLegacyCodexPlugin(text) {
|
|
2340
|
+
try {
|
|
2341
|
+
const parsed = JSON.parse(text);
|
|
2342
|
+
const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
|
|
2343
|
+
const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
|
|
2344
|
+
const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
|
|
2345
|
+
|| legacyTextHasCodexPluginResidue(marketplaceDisplayName)
|
|
2346
|
+
|| LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
|
|
2347
|
+
return (parsed.plugins ?? []).some((plugin) => {
|
|
2348
|
+
const values = [
|
|
2349
|
+
plugin.name,
|
|
2350
|
+
plugin.category,
|
|
2351
|
+
plugin.source?.path,
|
|
2352
|
+
plugin.interface?.displayName
|
|
2353
|
+
].filter((value) => typeof value === "string");
|
|
2354
|
+
return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
catch {
|
|
2358
|
+
return legacyTextHasCodexPluginResidue(text);
|
|
2359
|
+
}
|
|
2086
2360
|
}
|
|
2087
2361
|
async function discoverLegacyCodexCachePaths(targetRoot) {
|
|
2088
2362
|
const paths = [];
|
|
@@ -2092,8 +2366,12 @@ async function discoverLegacyCodexCachePaths(targetRoot) {
|
|
|
2092
2366
|
for (const entry of marketplaces) {
|
|
2093
2367
|
if (!entry.isDirectory())
|
|
2094
2368
|
continue;
|
|
2095
|
-
|
|
2096
|
-
|
|
2369
|
+
if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
|
|
2370
|
+
paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
|
|
2371
|
+
}
|
|
2372
|
+
for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
|
|
2373
|
+
paths.push(join(cacheRoot, entry.name, pluginName));
|
|
2374
|
+
}
|
|
2097
2375
|
}
|
|
2098
2376
|
}
|
|
2099
2377
|
catch {
|
|
@@ -2139,6 +2417,8 @@ function isLegacyCodexHistoryMention(line) {
|
|
|
2139
2417
|
|| line.includes("plugin://dbt-agent@embed-labs")
|
|
2140
2418
|
|| line.includes("development-board-toolchain-dev")
|
|
2141
2419
|
|| line.includes("development-board-toolchain")
|
|
2420
|
+
|| /plugin:\/\/Dbt Agent@/i.test(line)
|
|
2421
|
+
|| /plugin:\/\/deve@/i.test(line)
|
|
2142
2422
|
|| /dbt-agent/i.test(line);
|
|
2143
2423
|
}
|
|
2144
2424
|
function removeLegacyCodexConfigTables(text) {
|
|
@@ -2167,11 +2447,15 @@ function parseTomlTableHeader(line) {
|
|
|
2167
2447
|
}
|
|
2168
2448
|
function isLegacyCodexConfigTable(table) {
|
|
2169
2449
|
return /^plugins\."dbt-agent@[^"]+"$/.test(table)
|
|
2450
|
+
|| /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
|
|
2451
|
+
|| /^plugins\."deve@[^"]+"$/i.test(table)
|
|
2170
2452
|
|| isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
|
|
2171
2453
|
|| table === "mcp_servers.dbt-agent"
|
|
2172
2454
|
|| table.startsWith("mcp_servers.dbt-agent.")
|
|
2173
2455
|
|| table === 'mcp_servers."dbt-agent"'
|
|
2174
2456
|
|| table.startsWith('mcp_servers."dbt-agent".')
|
|
2457
|
+
|| table === "mcp_servers.deve"
|
|
2458
|
+
|| table.startsWith("mcp_servers.deve.")
|
|
2175
2459
|
|| /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
|
|
2176
2460
|
}
|
|
2177
2461
|
function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
|
|
@@ -2185,6 +2469,12 @@ function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
|
|
|
2185
2469
|
}
|
|
2186
2470
|
return false;
|
|
2187
2471
|
}
|
|
2472
|
+
function legacyTextHasCodexPluginResidue(value) {
|
|
2473
|
+
return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
|
|
2474
|
+
|| value === "deve"
|
|
2475
|
+
|| value.replace(/\\/g, "/").includes("/plugins/deve")
|
|
2476
|
+
|| value.includes("plugin://deve@");
|
|
2477
|
+
}
|
|
2188
2478
|
function legacyCodexCleanupWarning(cleanup) {
|
|
2189
2479
|
const parts = [];
|
|
2190
2480
|
if (cleanup.legacy_removed_paths.length > 0) {
|
|
@@ -2209,12 +2499,19 @@ async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
|
|
|
2209
2499
|
const warnings = [];
|
|
2210
2500
|
const legacyPaths = [
|
|
2211
2501
|
join(targetRoot, "plugins", "development-board-toolchain.js"),
|
|
2502
|
+
join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
|
|
2212
2503
|
join(targetRoot, "plugins", "dbt-agent.js"),
|
|
2504
|
+
join(targetRoot, "plugins", "Dbt Agent.js"),
|
|
2505
|
+
join(targetRoot, "plugins", "deve.js"),
|
|
2506
|
+
join(targetRoot, "plugins", "deve"),
|
|
2507
|
+
join(targetRoot, "node_modules", "development-board-toolchain"),
|
|
2508
|
+
join(targetRoot, "node_modules", "development-board-toolchain-dev"),
|
|
2213
2509
|
join(targetRoot, "node_modules", "dbt-agent")
|
|
2214
2510
|
];
|
|
2215
2511
|
if (globalInstall) {
|
|
2216
2512
|
legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
|
|
2217
2513
|
legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
|
|
2514
|
+
legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
|
|
2218
2515
|
}
|
|
2219
2516
|
for (const candidate of legacyPaths) {
|
|
2220
2517
|
try {
|
|
@@ -2243,7 +2540,7 @@ async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
|
|
|
2243
2540
|
const filePath = join(targetRoot, entry.name);
|
|
2244
2541
|
try {
|
|
2245
2542
|
const current = await readFile(filePath, "utf8");
|
|
2246
|
-
if (
|
|
2543
|
+
if (legacyTextHasCodexPluginResidue(current)) {
|
|
2247
2544
|
paths.push(filePath);
|
|
2248
2545
|
}
|
|
2249
2546
|
}
|
|
@@ -2257,6 +2554,26 @@ async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
|
|
|
2257
2554
|
}
|
|
2258
2555
|
return paths;
|
|
2259
2556
|
}
|
|
2557
|
+
async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
|
|
2558
|
+
const paths = [];
|
|
2559
|
+
const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
|
|
2560
|
+
try {
|
|
2561
|
+
const entries = await readdir(cacheRoot, { withFileTypes: true });
|
|
2562
|
+
for (const entry of entries) {
|
|
2563
|
+
if (!entry.isFile())
|
|
2564
|
+
continue;
|
|
2565
|
+
if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
|
|
2566
|
+
paths.push(join(cacheRoot, entry.name));
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
catch (error) {
|
|
2571
|
+
if (error.code !== "ENOENT") {
|
|
2572
|
+
warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return paths;
|
|
2576
|
+
}
|
|
2260
2577
|
function legacyOpenCodeCleanupWarning(cleanup) {
|
|
2261
2578
|
const parts = [];
|
|
2262
2579
|
if (cleanup.legacy_removed_paths.length > 0) {
|
|
@@ -2553,6 +2870,97 @@ async function resolveExecutableOnPath(name) {
|
|
|
2553
2870
|
}
|
|
2554
2871
|
return undefined;
|
|
2555
2872
|
}
|
|
2873
|
+
async function runBridgeStart(parsed) {
|
|
2874
|
+
const host = stringFlag(parsed, "host");
|
|
2875
|
+
const port = numberFlag(parsed, "port");
|
|
2876
|
+
const bridge = await resolveBridgeLauncher();
|
|
2877
|
+
const args = [...bridge.args];
|
|
2878
|
+
if (host) {
|
|
2879
|
+
args.push("--host", host);
|
|
2880
|
+
}
|
|
2881
|
+
if (port !== undefined) {
|
|
2882
|
+
args.push("--port", String(port));
|
|
2883
|
+
}
|
|
2884
|
+
const env = {
|
|
2885
|
+
...process.env,
|
|
2886
|
+
...(host ? { EMBED_BRIDGE_HOST: host } : {}),
|
|
2887
|
+
...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
|
|
2888
|
+
};
|
|
2889
|
+
const child = spawn(bridge.command, args, {
|
|
2890
|
+
stdio: "inherit",
|
|
2891
|
+
env
|
|
2892
|
+
});
|
|
2893
|
+
const forwardSignal = (signal) => {
|
|
2894
|
+
if (!child.killed) {
|
|
2895
|
+
child.kill(signal);
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
process.once("SIGINT", forwardSignal);
|
|
2899
|
+
process.once("SIGTERM", forwardSignal);
|
|
2900
|
+
return await new Promise((resolveCode) => {
|
|
2901
|
+
child.on("error", (error) => {
|
|
2902
|
+
process.off("SIGINT", forwardSignal);
|
|
2903
|
+
process.off("SIGTERM", forwardSignal);
|
|
2904
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2905
|
+
resolveCode(1);
|
|
2906
|
+
});
|
|
2907
|
+
child.on("close", (code, signal) => {
|
|
2908
|
+
process.off("SIGINT", forwardSignal);
|
|
2909
|
+
process.off("SIGTERM", forwardSignal);
|
|
2910
|
+
if (signal === "SIGINT" || signal === "SIGTERM") {
|
|
2911
|
+
resolveCode(0);
|
|
2912
|
+
}
|
|
2913
|
+
else {
|
|
2914
|
+
resolveCode(code ?? 0);
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
async function resolveBridgeLauncher() {
|
|
2920
|
+
const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
|
|
2921
|
+
if (explicitBinary) {
|
|
2922
|
+
try {
|
|
2923
|
+
await access(explicitBinary, constants.X_OK);
|
|
2924
|
+
return { command: explicitBinary, args: [] };
|
|
2925
|
+
}
|
|
2926
|
+
catch {
|
|
2927
|
+
// Fall through so the package launcher can print its clearer repair message.
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
|
|
2931
|
+
if (pathBinary) {
|
|
2932
|
+
return { command: pathBinary, args: [] };
|
|
2933
|
+
}
|
|
2934
|
+
const packageLauncher = await resolveBridgePackageLauncher();
|
|
2935
|
+
if (packageLauncher) {
|
|
2936
|
+
return { command: process.execPath, args: [packageLauncher] };
|
|
2937
|
+
}
|
|
2938
|
+
return {
|
|
2939
|
+
command: process.execPath,
|
|
2940
|
+
args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
async function resolveBridgePackageLauncher() {
|
|
2944
|
+
const candidates = [];
|
|
2945
|
+
try {
|
|
2946
|
+
const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
|
|
2947
|
+
candidates.push(join(dirname(packageJson), "dist", "index.js"));
|
|
2948
|
+
}
|
|
2949
|
+
catch {
|
|
2950
|
+
// Source checkout fallback below.
|
|
2951
|
+
}
|
|
2952
|
+
candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
|
|
2953
|
+
for (const candidate of candidates) {
|
|
2954
|
+
try {
|
|
2955
|
+
await access(candidate, constants.R_OK);
|
|
2956
|
+
return candidate;
|
|
2957
|
+
}
|
|
2958
|
+
catch {
|
|
2959
|
+
// Keep looking.
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
return undefined;
|
|
2963
|
+
}
|
|
2556
2964
|
function defaultOpenCodeRoot() {
|
|
2557
2965
|
return globalOpenCodeRoot();
|
|
2558
2966
|
}
|
|
@@ -2592,12 +3000,26 @@ async function ensureOpenCodeGlobalPluginConfig() {
|
|
|
2592
3000
|
return removed;
|
|
2593
3001
|
}
|
|
2594
3002
|
function isLegacyOpenCodePluginConfigEntry(item) {
|
|
2595
|
-
const normalized = item.replace(/\\/g, "/");
|
|
2596
|
-
|
|
2597
|
-
|
|
3003
|
+
const normalized = item.trim().replace(/\\/g, "/");
|
|
3004
|
+
const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
|
|
3005
|
+
return normalized === "dbt-agent"
|
|
3006
|
+
|| normalized === "Dbt Agent"
|
|
3007
|
+
|| normalized === "deve"
|
|
3008
|
+
|| normalized === "development-board-toolchain"
|
|
3009
|
+
|| normalized === "development-board-toolchain-dev"
|
|
3010
|
+
|| normalized === "./plugins/deve"
|
|
3011
|
+
|| normalized === "./plugins/deve.js"
|
|
3012
|
+
|| /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
|
|
3013
|
+
|| /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
|
|
2598
3014
|
|| normalized === "./plugins/development-board-toolchain"
|
|
2599
3015
|
|| normalized === "./plugins/development-board-toolchain.js"
|
|
2600
|
-
|| normalized
|
|
3016
|
+
|| normalized === "./plugins/development-board-toolchain-dev"
|
|
3017
|
+
|| normalized === "./plugins/development-board-toolchain-dev.js"
|
|
3018
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain")
|
|
3019
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain.js")
|
|
3020
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain-dev")
|
|
3021
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
|
|
3022
|
+
|| normalized.includes("dbt-agent")
|
|
2601
3023
|
|| normalized.includes("development-board-toolchain");
|
|
2602
3024
|
}
|
|
2603
3025
|
async function openCodeDuplicatePluginWarning(targetRoot) {
|
|
@@ -2691,19 +3113,65 @@ async function parseErrorResponse(response) {
|
|
|
2691
3113
|
return undefined;
|
|
2692
3114
|
}
|
|
2693
3115
|
async function cloudAuthToken() {
|
|
3116
|
+
return (await cloudAuthConfig()).token;
|
|
3117
|
+
}
|
|
3118
|
+
async function cloudAuthConfig() {
|
|
2694
3119
|
const envToken = process.env.EMBED_API_TOKEN?.trim();
|
|
2695
3120
|
if (envToken) {
|
|
2696
|
-
|
|
3121
|
+
const fileConfig = await readLocalAuthFile();
|
|
3122
|
+
return {
|
|
3123
|
+
...fileConfig,
|
|
3124
|
+
token: envToken,
|
|
3125
|
+
profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
|
|
3126
|
+
source: "env"
|
|
3127
|
+
};
|
|
2697
3128
|
}
|
|
3129
|
+
const fileConfig = await readLocalAuthFile();
|
|
3130
|
+
return {
|
|
3131
|
+
...fileConfig,
|
|
3132
|
+
token: fileConfig.token?.trim() || undefined,
|
|
3133
|
+
profile: fileConfig.profile ?? "default",
|
|
3134
|
+
source: fileConfig.token ? "file" : undefined
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
async function readLocalAuthFile() {
|
|
2698
3138
|
try {
|
|
2699
3139
|
const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
|
|
2700
|
-
|
|
2701
|
-
return fileToken || undefined;
|
|
3140
|
+
return normalizeLocalAuthFile(parsed);
|
|
2702
3141
|
}
|
|
2703
3142
|
catch {
|
|
2704
|
-
return
|
|
3143
|
+
return {};
|
|
2705
3144
|
}
|
|
2706
3145
|
}
|
|
3146
|
+
function normalizeLocalAuthFile(parsed) {
|
|
3147
|
+
const device = isJsonObject(parsed.device) ? parsed.device : undefined;
|
|
3148
|
+
const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
|
|
3149
|
+
? {
|
|
3150
|
+
device_id: device.device_id,
|
|
3151
|
+
fingerprint_hash: device.fingerprint_hash,
|
|
3152
|
+
private_key_pem: device.private_key_pem,
|
|
3153
|
+
public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
|
|
3154
|
+
label: typeof device.label === "string" ? device.label : undefined,
|
|
3155
|
+
platform: typeof device.platform === "string" ? device.platform : undefined,
|
|
3156
|
+
arch: typeof device.arch === "string" ? device.arch : undefined,
|
|
3157
|
+
hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
|
|
3158
|
+
registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
|
|
3159
|
+
}
|
|
3160
|
+
: undefined;
|
|
3161
|
+
return {
|
|
3162
|
+
profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
|
|
3163
|
+
token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
|
|
3164
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
|
|
3165
|
+
account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
|
|
3166
|
+
api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
|
|
3167
|
+
device: normalizedDevice
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
async function writeLocalAuthFile(config) {
|
|
3171
|
+
await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
|
|
3172
|
+
await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
3173
|
+
await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
|
|
3174
|
+
}
|
|
2707
3175
|
function serviceBaseUrl(url) {
|
|
2708
3176
|
return url.replace(/\/+$/, "");
|
|
2709
3177
|
}
|
|
@@ -4162,30 +4630,272 @@ async function authLogin(parsed) {
|
|
|
4162
4630
|
return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
|
|
4163
4631
|
}
|
|
4164
4632
|
const updatedAt = new Date().toISOString();
|
|
4165
|
-
|
|
4166
|
-
await
|
|
4167
|
-
|
|
4633
|
+
const current = await readLocalAuthFile();
|
|
4634
|
+
const localDevice = await buildLocalDeviceAuth(parsed, current.device);
|
|
4635
|
+
const registration = await registerLocalDevice(token.trim(), localDevice.registration);
|
|
4636
|
+
if (!registration.ok) {
|
|
4637
|
+
return fail(registration.error.code, registration.error.message, {
|
|
4638
|
+
remediation: registration.error.remediation,
|
|
4639
|
+
details: registration.error.details
|
|
4640
|
+
});
|
|
4641
|
+
}
|
|
4642
|
+
const device = {
|
|
4643
|
+
device_id: registration.data.device.device_id,
|
|
4644
|
+
fingerprint_hash: localDevice.device.fingerprint_hash,
|
|
4645
|
+
private_key_pem: localDevice.device.private_key_pem,
|
|
4646
|
+
public_key_pem: localDevice.device.public_key_pem,
|
|
4647
|
+
label: registration.data.device.label ?? localDevice.device.label,
|
|
4648
|
+
platform: registration.data.device.platform ?? localDevice.device.platform,
|
|
4649
|
+
arch: registration.data.device.arch ?? localDevice.device.arch,
|
|
4650
|
+
hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
|
|
4651
|
+
registered_at: registration.data.device.first_seen_at
|
|
4652
|
+
};
|
|
4653
|
+
await writeLocalAuthFile({
|
|
4654
|
+
profile,
|
|
4655
|
+
token: token.trim(),
|
|
4656
|
+
updated_at: updatedAt,
|
|
4657
|
+
account_id: registration.data.device.account_id,
|
|
4658
|
+
api_key_id: registration.data.device.api_key_id,
|
|
4659
|
+
device
|
|
4660
|
+
});
|
|
4661
|
+
return ok({
|
|
4662
|
+
authenticated: true,
|
|
4663
|
+
profile,
|
|
4664
|
+
source: "file",
|
|
4665
|
+
updated_at: updatedAt,
|
|
4666
|
+
account_id: registration.data.device.account_id,
|
|
4667
|
+
api_key_id: registration.data.device.api_key_id,
|
|
4668
|
+
device_id: device.device_id,
|
|
4669
|
+
device_fingerprint_hash: device.fingerprint_hash,
|
|
4670
|
+
device_label: device.label,
|
|
4671
|
+
device_registered_at: device.registered_at,
|
|
4672
|
+
device_private_key_configured: true
|
|
4673
|
+
});
|
|
4168
4674
|
}
|
|
4169
4675
|
async function authStatus() {
|
|
4170
|
-
|
|
4676
|
+
const envToken = process.env.EMBED_API_TOKEN?.trim();
|
|
4677
|
+
const file = await readLocalAuthFile();
|
|
4678
|
+
const deviceIntegrity = file.device
|
|
4679
|
+
? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
|
|
4680
|
+
: "unbound";
|
|
4681
|
+
if (envToken) {
|
|
4171
4682
|
return {
|
|
4172
4683
|
authenticated: true,
|
|
4173
4684
|
profile: process.env.EMBED_AUTH_PROFILE ?? "default",
|
|
4174
|
-
source: "env"
|
|
4685
|
+
source: "env",
|
|
4686
|
+
account_id: file.account_id,
|
|
4687
|
+
api_key_id: file.api_key_id,
|
|
4688
|
+
device_id: file.device?.device_id,
|
|
4689
|
+
device_fingerprint_hash: file.device?.fingerprint_hash,
|
|
4690
|
+
device_label: file.device?.label,
|
|
4691
|
+
device_registered_at: file.device?.registered_at,
|
|
4692
|
+
device_private_key_configured: Boolean(file.device?.private_key_pem),
|
|
4693
|
+
device_integrity: deviceIntegrity
|
|
4175
4694
|
};
|
|
4176
4695
|
}
|
|
4696
|
+
return {
|
|
4697
|
+
authenticated: Boolean(file.token?.trim()),
|
|
4698
|
+
profile: file.profile ?? "default",
|
|
4699
|
+
source: file.token ? "file" : undefined,
|
|
4700
|
+
updated_at: file.updated_at,
|
|
4701
|
+
account_id: file.account_id,
|
|
4702
|
+
api_key_id: file.api_key_id,
|
|
4703
|
+
device_id: file.device?.device_id,
|
|
4704
|
+
device_fingerprint_hash: file.device?.fingerprint_hash,
|
|
4705
|
+
device_label: file.device?.label,
|
|
4706
|
+
device_registered_at: file.device?.registered_at,
|
|
4707
|
+
device_private_key_configured: Boolean(file.device?.private_key_pem),
|
|
4708
|
+
device_integrity: deviceIntegrity
|
|
4709
|
+
};
|
|
4710
|
+
}
|
|
4711
|
+
async function authDeviceStatus(parsed) {
|
|
4712
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4713
|
+
const unexpected = parsed.command.slice(3);
|
|
4714
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
4715
|
+
return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
|
|
4716
|
+
}
|
|
4717
|
+
const local = await authStatus();
|
|
4718
|
+
const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
|
|
4719
|
+
if (remote && !remote.ok) {
|
|
4720
|
+
return ok({ local });
|
|
4721
|
+
}
|
|
4722
|
+
return ok({ local, remote: remote?.data });
|
|
4723
|
+
}
|
|
4724
|
+
async function authDeviceList(parsed) {
|
|
4725
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4726
|
+
const unexpected = parsed.command.slice(3);
|
|
4727
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
4728
|
+
return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
|
|
4729
|
+
}
|
|
4730
|
+
return await cloudGet("/v1/me/devices");
|
|
4731
|
+
}
|
|
4732
|
+
async function authDeviceRevoke(parsed) {
|
|
4733
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4734
|
+
if (unknownFlag) {
|
|
4735
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
|
|
4736
|
+
}
|
|
4737
|
+
const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
|
|
4738
|
+
if (!id.ok) {
|
|
4739
|
+
return fail("invalid_args", id.error);
|
|
4740
|
+
}
|
|
4741
|
+
return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
|
|
4742
|
+
}
|
|
4743
|
+
async function authDeviceRename(parsed) {
|
|
4744
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
|
|
4745
|
+
if (unknownFlag) {
|
|
4746
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
|
|
4747
|
+
}
|
|
4748
|
+
const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
|
|
4749
|
+
if (!id.ok) {
|
|
4750
|
+
return fail("invalid_args", id.error);
|
|
4751
|
+
}
|
|
4752
|
+
const label = stringFlag(parsed, "label");
|
|
4753
|
+
if (!label?.trim()) {
|
|
4754
|
+
return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
|
|
4755
|
+
}
|
|
4756
|
+
const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
|
|
4757
|
+
if (updated.ok) {
|
|
4758
|
+
const auth = await readLocalAuthFile();
|
|
4759
|
+
if (auth.device?.device_id === updated.data.device_id) {
|
|
4760
|
+
await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
return updated;
|
|
4764
|
+
}
|
|
4765
|
+
async function buildLocalDeviceAuth(parsed, existing) {
|
|
4766
|
+
const fingerprint = await localHardwareFingerprint();
|
|
4767
|
+
const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
|
|
4768
|
+
? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
|
|
4769
|
+
: generateLocalDeviceKeyPair();
|
|
4770
|
+
const label = stringFlag(parsed, "label")?.trim()
|
|
4771
|
+
|| existing?.label
|
|
4772
|
+
|| `${fingerprint.platform} ${fingerprint.arch}`;
|
|
4773
|
+
const device = {
|
|
4774
|
+
device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
|
|
4775
|
+
fingerprint_hash: fingerprint.fingerprint_hash,
|
|
4776
|
+
private_key_pem: keyPair.privateKeyPem,
|
|
4777
|
+
public_key_pem: keyPair.publicKeyPem,
|
|
4778
|
+
label,
|
|
4779
|
+
platform: fingerprint.platform,
|
|
4780
|
+
arch: fingerprint.arch,
|
|
4781
|
+
hostname_hash: fingerprint.hostname_hash,
|
|
4782
|
+
registered_at: existing?.registered_at
|
|
4783
|
+
};
|
|
4784
|
+
return {
|
|
4785
|
+
device,
|
|
4786
|
+
registration: {
|
|
4787
|
+
fingerprint_hash: fingerprint.fingerprint_hash,
|
|
4788
|
+
public_key: keyPair.publicKeyPem,
|
|
4789
|
+
label,
|
|
4790
|
+
platform: fingerprint.platform,
|
|
4791
|
+
arch: fingerprint.arch,
|
|
4792
|
+
hostname_hash: fingerprint.hostname_hash,
|
|
4793
|
+
client_name: EMBED_CLIENT_NAME,
|
|
4794
|
+
client_version: EMBED_CLIENT_VERSION,
|
|
4795
|
+
metadata: {
|
|
4796
|
+
fingerprint_version: "v1",
|
|
4797
|
+
fingerprint_source: fingerprint.source
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
};
|
|
4801
|
+
}
|
|
4802
|
+
function generateLocalDeviceKeyPair() {
|
|
4803
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
4804
|
+
return {
|
|
4805
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
4806
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
|
|
4807
|
+
};
|
|
4808
|
+
}
|
|
4809
|
+
async function registerLocalDevice(token, body) {
|
|
4177
4810
|
try {
|
|
4178
|
-
const
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
source: "file",
|
|
4183
|
-
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined
|
|
4811
|
+
const bodyText = JSON.stringify(body);
|
|
4812
|
+
const headers = {
|
|
4813
|
+
"content-type": "application/json",
|
|
4814
|
+
authorization: `Bearer ${token}`
|
|
4184
4815
|
};
|
|
4816
|
+
addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
|
|
4817
|
+
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
|
|
4818
|
+
method: "POST",
|
|
4819
|
+
headers,
|
|
4820
|
+
body: bodyText
|
|
4821
|
+
});
|
|
4822
|
+
const parsed = await response.json();
|
|
4823
|
+
return enrichCloudAuthFailure(parsed, true);
|
|
4824
|
+
}
|
|
4825
|
+
catch (error) {
|
|
4826
|
+
return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
|
|
4827
|
+
remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
|
|
4828
|
+
});
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
async function localHardwareFingerprint() {
|
|
4832
|
+
cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
|
|
4833
|
+
return await cachedLocalHardwareFingerprint;
|
|
4834
|
+
}
|
|
4835
|
+
async function localHardwareFingerprintUncached() {
|
|
4836
|
+
const platformName = platform();
|
|
4837
|
+
const archName = arch();
|
|
4838
|
+
const raw = await localHardwareId(platformName);
|
|
4839
|
+
const fingerprintHash = createHash("sha256")
|
|
4840
|
+
.update(`embed-labs:device:v1:${platformName}:${raw.value}`)
|
|
4841
|
+
.digest("hex");
|
|
4842
|
+
const hostnameHash = createHash("sha256")
|
|
4843
|
+
.update(`embed-labs:hostname:v1:${hostname()}`)
|
|
4844
|
+
.digest("hex");
|
|
4845
|
+
return {
|
|
4846
|
+
fingerprint_hash: fingerprintHash,
|
|
4847
|
+
platform: platformName,
|
|
4848
|
+
arch: archName,
|
|
4849
|
+
hostname_hash: hostnameHash,
|
|
4850
|
+
source: raw.source
|
|
4851
|
+
};
|
|
4852
|
+
}
|
|
4853
|
+
async function localHardwareId(platformName) {
|
|
4854
|
+
if (platformName === "darwin") {
|
|
4855
|
+
const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
|
|
4856
|
+
const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
|
|
4857
|
+
if (match?.[1]) {
|
|
4858
|
+
return { value: match[1], source: "macos_ioplatformuuid" };
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
if (platformName === "win32") {
|
|
4862
|
+
const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
|
|
4863
|
+
const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
|
|
4864
|
+
if (match?.[1]?.trim()) {
|
|
4865
|
+
return { value: match[1].trim(), source: "windows_machineguid" };
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
if (platformName === "linux") {
|
|
4869
|
+
for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
4870
|
+
try {
|
|
4871
|
+
const value = (await readFile(pathValue, "utf8")).trim();
|
|
4872
|
+
if (value) {
|
|
4873
|
+
return { value, source: `linux:${pathValue}` };
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
catch {
|
|
4877
|
+
// Try the next stable machine id location.
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
const generated = await localGeneratedInstallId();
|
|
4882
|
+
return { value: generated, source: "generated_install_id" };
|
|
4883
|
+
}
|
|
4884
|
+
async function localGeneratedInstallId() {
|
|
4885
|
+
try {
|
|
4886
|
+
const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
|
|
4887
|
+
if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
|
|
4888
|
+
return parsed.generated_install_id.trim();
|
|
4889
|
+
}
|
|
4185
4890
|
}
|
|
4186
4891
|
catch {
|
|
4187
|
-
|
|
4892
|
+
// Fall through and create a local-only fallback id.
|
|
4188
4893
|
}
|
|
4894
|
+
const generated = `install_${randomBytes(24).toString("base64url")}`;
|
|
4895
|
+
await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
|
|
4896
|
+
await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
4897
|
+
await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
|
|
4898
|
+
return generated;
|
|
4189
4899
|
}
|
|
4190
4900
|
function accountCreateBody(parsed) {
|
|
4191
4901
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
|
|
@@ -4351,6 +5061,8 @@ function mcpToolEventBody(parsed) {
|
|
|
4351
5061
|
"mode",
|
|
4352
5062
|
"server-model-used",
|
|
4353
5063
|
"success",
|
|
5064
|
+
"local-device-id",
|
|
5065
|
+
"local_device_id",
|
|
4354
5066
|
"request-id",
|
|
4355
5067
|
"duration-ms",
|
|
4356
5068
|
"input-summary",
|
|
@@ -4377,6 +5089,9 @@ function mcpToolEventBody(parsed) {
|
|
|
4377
5089
|
const modeResult = optionalTrimmedStringFlag(parsed, "mode");
|
|
4378
5090
|
if (modeResult.error)
|
|
4379
5091
|
return modeResult.error;
|
|
5092
|
+
const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
|
|
5093
|
+
if (localDeviceResult.error)
|
|
5094
|
+
return localDeviceResult.error;
|
|
4380
5095
|
const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
|
|
4381
5096
|
if (requestIdResult.error)
|
|
4382
5097
|
return requestIdResult.error;
|
|
@@ -4400,12 +5115,14 @@ function mcpToolEventBody(parsed) {
|
|
|
4400
5115
|
tool_name: toolResult.value,
|
|
4401
5116
|
client: clientResult.value,
|
|
4402
5117
|
mode: modeResult.value,
|
|
5118
|
+
local_device_id: localDeviceResult.value,
|
|
4403
5119
|
server_model_used: serverModelUsed,
|
|
4404
5120
|
success,
|
|
4405
5121
|
request_id: requestIdResult.value,
|
|
4406
5122
|
duration_ms: durationResult.value,
|
|
4407
5123
|
input_summary: inputSummaryResult.value,
|
|
4408
|
-
output_summary: outputSummaryResult.value
|
|
5124
|
+
output_summary: outputSummaryResult.value,
|
|
5125
|
+
metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
|
|
4409
5126
|
});
|
|
4410
5127
|
}
|
|
4411
5128
|
function usageSummaryRequest(parsed) {
|
|
@@ -4923,7 +5640,9 @@ function localToolchainAuthContext(auth, accountId) {
|
|
|
4923
5640
|
authenticated: auth.authenticated,
|
|
4924
5641
|
profile: auth.profile,
|
|
4925
5642
|
source: auth.source,
|
|
4926
|
-
account_id: accountId
|
|
5643
|
+
account_id: accountId,
|
|
5644
|
+
api_key_id: auth.api_key_id,
|
|
5645
|
+
device_id: auth.device_id
|
|
4927
5646
|
};
|
|
4928
5647
|
}
|
|
4929
5648
|
function usageEventsRequest(parsed) {
|
|
@@ -5394,9 +6113,53 @@ function renderAgentRunResult(result) {
|
|
|
5394
6113
|
return lines.join("\n");
|
|
5395
6114
|
}
|
|
5396
6115
|
function renderAuthStatus(status) {
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
6116
|
+
if (!status.authenticated) {
|
|
6117
|
+
return `Not authenticated profile=${status.profile}`;
|
|
6118
|
+
}
|
|
6119
|
+
return [
|
|
6120
|
+
`Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
|
|
6121
|
+
status.account_id ? `account=${status.account_id}` : "",
|
|
6122
|
+
status.api_key_id ? `api_key=${status.api_key_id}` : "",
|
|
6123
|
+
status.device_id ? `device=${status.device_id}` : "device=not_registered",
|
|
6124
|
+
status.device_label ? `device_label=${status.device_label}` : "",
|
|
6125
|
+
status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
|
|
6126
|
+
status.device_private_key_configured === false ? "device_private_key=missing" : ""
|
|
6127
|
+
].filter(Boolean).join("\n");
|
|
6128
|
+
}
|
|
6129
|
+
function renderAuthDeviceStatus(status) {
|
|
6130
|
+
const lines = [renderAuthStatus(status.local)];
|
|
6131
|
+
if (status.remote) {
|
|
6132
|
+
const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
|
|
6133
|
+
lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
|
|
6134
|
+
const localDevice = status.local.device_id
|
|
6135
|
+
? status.remote.devices.find((device) => device.device_id === status.local.device_id)
|
|
6136
|
+
: undefined;
|
|
6137
|
+
if (localDevice) {
|
|
6138
|
+
lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
return lines.join("\n");
|
|
6142
|
+
}
|
|
6143
|
+
function renderAuthDeviceList(result) {
|
|
6144
|
+
if (result.devices.length === 0) {
|
|
6145
|
+
return `No registered devices. device_limit=${result.device_limit}`;
|
|
6146
|
+
}
|
|
6147
|
+
return [
|
|
6148
|
+
`device_limit=${result.device_limit}`,
|
|
6149
|
+
...result.devices.map(renderAuthDevice)
|
|
6150
|
+
].join("\n");
|
|
6151
|
+
}
|
|
6152
|
+
function renderAuthDevice(device) {
|
|
6153
|
+
return [
|
|
6154
|
+
`${device.device_id} account=${device.account_id}`,
|
|
6155
|
+
device.api_key_id ? `api_key=${device.api_key_id}` : "",
|
|
6156
|
+
`status=${device.status}`,
|
|
6157
|
+
device.label ? `label=${device.label}` : "",
|
|
6158
|
+
device.platform ? `platform=${device.platform}` : "",
|
|
6159
|
+
device.arch ? `arch=${device.arch}` : "",
|
|
6160
|
+
`last_seen_at=${device.last_seen_at}`,
|
|
6161
|
+
device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
|
|
6162
|
+
].filter(Boolean).join(" ");
|
|
5400
6163
|
}
|
|
5401
6164
|
function renderAccount(account) {
|
|
5402
6165
|
return [
|
|
@@ -6909,7 +7672,7 @@ Help:
|
|
|
6909
7672
|
|
|
6910
7673
|
Environment:
|
|
6911
7674
|
EMBED_BRIDGE_URL=http://127.0.0.1:18083
|
|
6912
|
-
EMBED_CLOUD_API_URL=
|
|
7675
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
6913
7676
|
EMBED_API_TOKEN=<token>
|
|
6914
7677
|
CODEX_HOME=~/.codex
|
|
6915
7678
|
|
|
@@ -7148,7 +7911,7 @@ Usage:
|
|
|
7148
7911
|
|
|
7149
7912
|
Environment:
|
|
7150
7913
|
EMBED_BRIDGE_URL=http://127.0.0.1:18083
|
|
7151
|
-
EMBED_CLOUD_API_URL=
|
|
7914
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
7152
7915
|
EMBED_API_TOKEN=<token>
|
|
7153
7916
|
CODEX_HOME=~/.codex
|
|
7154
7917
|
`);
|