@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/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 ?? "http://127.0.0.1:18100";
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 LEGACY_CODEX_MARKETPLACE_NAMES = new Set(["embed-labs-plugins"]);
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
- startServer({
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
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
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
- const bodyText = JSON.stringify(body);
1366
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1367
- method: "POST",
1368
- headers: bridgeHeaders("POST", path, bodyText, { "content-type": "application/json" }),
1369
- body: bodyText
1370
- });
1371
- return await response.json();
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 token = await cloudAuthToken();
1410
- if (token) {
1411
- headers.authorization = `Bearer ${token}`;
1412
- addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", token);
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 token = await cloudAuthToken();
1453
- if (token) {
1454
- headers.authorization = `Bearer ${token}`;
1455
- addCloudRequestSignature(headers, method, path, bodyText, token);
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 || response.error.code !== "unauthorized") {
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 = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
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, "dbt-agent"),
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 text.includes('"name" : "plugins"') || text.includes('"name": "plugins"')
2084
- ? text.includes('"dbt-agent"') || text.includes('"./.codex/plugins/dbt-agent"') || text.includes('"./plugins/dbt-agent"')
2085
- : false;
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
- paths.push(join(cacheRoot, entry.name, "dbt-agent"));
2096
- paths.push(join(cacheRoot, entry.name, "development-board-toolchain"));
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 (current.includes("dbt-agent") || current.includes("development-board-toolchain")) {
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
- return item === "dbt-agent"
2597
- || item === "development-board-toolchain"
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.endsWith("/plugins/development-board-toolchain.js")
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
- return envToken;
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
- const fileToken = typeof parsed.token === "string" ? parsed.token.trim() : "";
2701
- return fileToken || undefined;
3140
+ return normalizeLocalAuthFile(parsed);
2702
3141
  }
2703
3142
  catch {
2704
- return undefined;
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
- await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
4166
- await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify({ profile, token, updated_at: updatedAt }, null, 2)}\n`, "utf8");
4167
- return ok({ authenticated: true, profile, source: "file", updated_at: updatedAt });
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
- if (process.env.EMBED_API_TOKEN?.trim()) {
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 parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
4179
- return {
4180
- authenticated: typeof parsed.token === "string" && parsed.token.trim().length > 0,
4181
- profile: typeof parsed.profile === "string" ? parsed.profile : "default",
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
- return { authenticated: false, profile: "default" };
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
- return status.authenticated
5398
- ? `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`
5399
- : `Not authenticated profile=${status.profile}`;
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=http://127.0.0.1:18100
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=http://127.0.0.1:18100
7914
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
7152
7915
  EMBED_API_TOKEN=<token>
7153
7916
  CODEX_HOME=~/.codex
7154
7917
  `);