@kvell007/embed-labs-cli 0.1.0-alpha.21 → 0.1.0-alpha.22

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);
@@ -1406,17 +1442,23 @@ async function cloudPost(path, body) {
1406
1442
  async function cloudDownloadArtifact(artifactId, outputPath) {
1407
1443
  try {
1408
1444
  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);
1445
+ const auth = await cloudAuthConfig();
1446
+ if (auth.token) {
1447
+ if (auth.device) {
1448
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1449
+ if (!integrity.ok) {
1450
+ return integrity;
1451
+ }
1452
+ }
1453
+ headers.authorization = `Bearer ${auth.token}`;
1454
+ addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
1413
1455
  }
1414
1456
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
1415
1457
  headers: Object.keys(headers).length > 0 ? headers : undefined
1416
1458
  });
1417
1459
  if (!response.ok) {
1418
1460
  const parsed = await parseErrorResponse(response);
1419
- return parsed ? enrichCloudAuthFailure(parsed, Boolean(token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1461
+ return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1420
1462
  }
1421
1463
  const bytes = Buffer.from(await response.arrayBuffer());
1422
1464
  const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
@@ -1449,10 +1491,16 @@ async function cloudRequest(method, path, body) {
1449
1491
  if (bodyText) {
1450
1492
  headers["content-type"] = "application/json";
1451
1493
  }
1452
- const token = await cloudAuthToken();
1453
- if (token) {
1454
- headers.authorization = `Bearer ${token}`;
1455
- addCloudRequestSignature(headers, method, path, bodyText, token);
1494
+ const auth = await cloudAuthConfig();
1495
+ if (auth.token) {
1496
+ if (auth.device) {
1497
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1498
+ if (!integrity.ok) {
1499
+ return integrity;
1500
+ }
1501
+ }
1502
+ headers.authorization = `Bearer ${auth.token}`;
1503
+ addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
1456
1504
  }
1457
1505
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
1458
1506
  method,
@@ -1460,7 +1508,7 @@ async function cloudRequest(method, path, body) {
1460
1508
  body: body === undefined ? undefined : bodyText
1461
1509
  });
1462
1510
  const parsed = await response.json();
1463
- return enrichCloudAuthFailure(parsed, Boolean(token));
1511
+ return enrichCloudAuthFailure(parsed, Boolean(auth.token));
1464
1512
  }
1465
1513
  catch (error) {
1466
1514
  return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
@@ -1468,8 +1516,42 @@ async function cloudRequest(method, path, body) {
1468
1516
  });
1469
1517
  }
1470
1518
  }
1519
+ async function validateLocalDeviceIntegrity(device) {
1520
+ const current = await localHardwareFingerprint();
1521
+ if (current.fingerprint_hash === device.fingerprint_hash) {
1522
+ return ok(undefined);
1523
+ }
1524
+ return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
1525
+ remediation: [
1526
+ "当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
1527
+ TOOL_INTEGRITY_RELOGIN_MESSAGE,
1528
+ "如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
1529
+ ].join("\n"),
1530
+ details: {
1531
+ expected_fingerprint_hash: device.fingerprint_hash,
1532
+ current_fingerprint_hash: current.fingerprint_hash,
1533
+ platform: current.platform,
1534
+ arch: current.arch,
1535
+ fingerprint_source: current.source
1536
+ }
1537
+ });
1538
+ }
1471
1539
  function enrichCloudAuthFailure(response, hadToken) {
1472
- if (response.ok || response.error.code !== "unauthorized") {
1540
+ if (response.ok) {
1541
+ return response;
1542
+ }
1543
+ if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
1544
+ return fail(response.error.code, response.error.message, {
1545
+ remediation: [
1546
+ "This computer is not fully registered for the configured Embed Labs API Token.",
1547
+ "Run: embedlabs auth login --token <your_token>",
1548
+ "Then verify with: embedlabs auth device status",
1549
+ "If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
1550
+ ].join("\n"),
1551
+ details: response.error.details
1552
+ });
1553
+ }
1554
+ if (response.error.code !== "unauthorized") {
1473
1555
  return response;
1474
1556
  }
1475
1557
  if (!hadToken) {
@@ -1501,7 +1583,7 @@ function cloudAuthSetupDetails() {
1501
1583
  auth_file: DEFAULT_AUTH_FILE
1502
1584
  };
1503
1585
  }
1504
- function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token) {
1586
+ function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
1505
1587
  if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
1506
1588
  return;
1507
1589
  }
@@ -1509,11 +1591,21 @@ function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, toke
1509
1591
  const nonce = randomBytes(16).toString("hex");
1510
1592
  const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1511
1593
  const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1512
- const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1594
+ const canonical = device
1595
+ ? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
1596
+ : cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1513
1597
  headers["x-embed-key-id"] = keyId;
1514
1598
  headers["x-embed-timestamp"] = timestamp;
1515
1599
  headers["x-embed-nonce"] = nonce;
1516
1600
  headers["x-embed-body-sha256"] = bodySha256;
1601
+ if (device) {
1602
+ headers["x-embed-signature-version"] = "v2";
1603
+ headers["x-embed-device-id"] = device.device_id;
1604
+ headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
1605
+ headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
1606
+ headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
1607
+ headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
1608
+ }
1517
1609
  headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1518
1610
  }
1519
1611
  function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
@@ -1525,6 +1617,19 @@ function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bo
1525
1617
  bodySha256
1526
1618
  ].join("\n");
1527
1619
  }
1620
+ function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
1621
+ return [
1622
+ method.toUpperCase(),
1623
+ normalizeCloudPathForSignature(pathWithQuery),
1624
+ timestamp,
1625
+ nonce,
1626
+ bodySha256,
1627
+ device.device_id,
1628
+ device.fingerprint_hash,
1629
+ clientName,
1630
+ clientVersion
1631
+ ].join("\n");
1632
+ }
1528
1633
  function normalizeCloudPathForSignature(pathWithQuery) {
1529
1634
  try {
1530
1635
  const parsed = new URL(pathWithQuery, "http://embed.local");
@@ -1951,17 +2056,22 @@ async function cleanupLegacyCodexPluginRemnants(targetRoot) {
1951
2056
  const warnings = [];
1952
2057
  const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
1953
2058
  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")
2059
+ join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
1960
2060
  ];
2061
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2062
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
2063
+ }
2064
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2065
+ legacyPaths.push(join(targetRoot, pluginName));
2066
+ legacyPaths.push(join(targetRoot, "cache", pluginName));
2067
+ for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
2068
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
2069
+ }
2070
+ }
1961
2071
  legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
1962
2072
  if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
1963
2073
  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());
2074
+ legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
1965
2075
  }
1966
2076
  for (const candidate of legacyPaths) {
1967
2077
  try {
@@ -2054,6 +2164,11 @@ function legacyDevelopmentBoardRuntimePluginPaths() {
2054
2164
  join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
2055
2165
  ];
2056
2166
  }
2167
+ function legacyCodexLocalMarketplacePaths() {
2168
+ return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
2169
+ .filter((name) => name !== CODEX_MARKETPLACE_NAME)
2170
+ .map((name) => join(defaultCodexHome(), "local-marketplaces", name));
2171
+ }
2057
2172
  async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
2058
2173
  const paths = [];
2059
2174
  const pluginRoot = join(homedir(), ".agents", "plugins");
@@ -2080,9 +2195,72 @@ async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
2080
2195
  return paths;
2081
2196
  }
2082
2197
  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;
2198
+ return marketplaceTextHasLegacyCodexPlugin(text);
2199
+ }
2200
+ async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
2201
+ const configPath = codexConfigPath();
2202
+ let text = "";
2203
+ try {
2204
+ text = await readFile(configPath, "utf8");
2205
+ }
2206
+ catch {
2207
+ return [];
2208
+ }
2209
+ const paths = new Set();
2210
+ for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
2211
+ for (const candidate of [
2212
+ join(projectPath, ".agents", "plugins", "marketplace.json"),
2213
+ join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
2214
+ join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
2215
+ ]) {
2216
+ try {
2217
+ if (!await pathExists(candidate))
2218
+ continue;
2219
+ const current = await readFile(candidate, "utf8");
2220
+ if (marketplaceTextHasLegacyCodexPlugin(current)) {
2221
+ paths.add(candidate);
2222
+ }
2223
+ }
2224
+ catch (error) {
2225
+ warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2226
+ }
2227
+ }
2228
+ }
2229
+ return Array.from(paths);
2230
+ }
2231
+ function legacyCodexProjectPathsFromConfig(text) {
2232
+ const paths = [];
2233
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2234
+ for (const line of lines) {
2235
+ const table = parseTomlTableHeader(line);
2236
+ const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
2237
+ if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
2238
+ paths.push(match[1].replace(/\\"/g, '"'));
2239
+ }
2240
+ }
2241
+ return paths;
2242
+ }
2243
+ function marketplaceTextHasLegacyCodexPlugin(text) {
2244
+ try {
2245
+ const parsed = JSON.parse(text);
2246
+ const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
2247
+ const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
2248
+ const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
2249
+ || legacyTextHasCodexPluginResidue(marketplaceDisplayName)
2250
+ || LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
2251
+ return (parsed.plugins ?? []).some((plugin) => {
2252
+ const values = [
2253
+ plugin.name,
2254
+ plugin.category,
2255
+ plugin.source?.path,
2256
+ plugin.interface?.displayName
2257
+ ].filter((value) => typeof value === "string");
2258
+ return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
2259
+ });
2260
+ }
2261
+ catch {
2262
+ return legacyTextHasCodexPluginResidue(text);
2263
+ }
2086
2264
  }
2087
2265
  async function discoverLegacyCodexCachePaths(targetRoot) {
2088
2266
  const paths = [];
@@ -2092,8 +2270,12 @@ async function discoverLegacyCodexCachePaths(targetRoot) {
2092
2270
  for (const entry of marketplaces) {
2093
2271
  if (!entry.isDirectory())
2094
2272
  continue;
2095
- paths.push(join(cacheRoot, entry.name, "dbt-agent"));
2096
- paths.push(join(cacheRoot, entry.name, "development-board-toolchain"));
2273
+ if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
2274
+ paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
2275
+ }
2276
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2277
+ paths.push(join(cacheRoot, entry.name, pluginName));
2278
+ }
2097
2279
  }
2098
2280
  }
2099
2281
  catch {
@@ -2139,6 +2321,8 @@ function isLegacyCodexHistoryMention(line) {
2139
2321
  || line.includes("plugin://dbt-agent@embed-labs")
2140
2322
  || line.includes("development-board-toolchain-dev")
2141
2323
  || line.includes("development-board-toolchain")
2324
+ || /plugin:\/\/Dbt Agent@/i.test(line)
2325
+ || /plugin:\/\/deve@/i.test(line)
2142
2326
  || /dbt-agent/i.test(line);
2143
2327
  }
2144
2328
  function removeLegacyCodexConfigTables(text) {
@@ -2167,11 +2351,15 @@ function parseTomlTableHeader(line) {
2167
2351
  }
2168
2352
  function isLegacyCodexConfigTable(table) {
2169
2353
  return /^plugins\."dbt-agent@[^"]+"$/.test(table)
2354
+ || /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
2355
+ || /^plugins\."deve@[^"]+"$/i.test(table)
2170
2356
  || isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
2171
2357
  || table === "mcp_servers.dbt-agent"
2172
2358
  || table.startsWith("mcp_servers.dbt-agent.")
2173
2359
  || table === 'mcp_servers."dbt-agent"'
2174
2360
  || table.startsWith('mcp_servers."dbt-agent".')
2361
+ || table === "mcp_servers.deve"
2362
+ || table.startsWith("mcp_servers.deve.")
2175
2363
  || /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
2176
2364
  }
2177
2365
  function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
@@ -2185,6 +2373,12 @@ function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
2185
2373
  }
2186
2374
  return false;
2187
2375
  }
2376
+ function legacyTextHasCodexPluginResidue(value) {
2377
+ return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
2378
+ || value === "deve"
2379
+ || value.replace(/\\/g, "/").includes("/plugins/deve")
2380
+ || value.includes("plugin://deve@");
2381
+ }
2188
2382
  function legacyCodexCleanupWarning(cleanup) {
2189
2383
  const parts = [];
2190
2384
  if (cleanup.legacy_removed_paths.length > 0) {
@@ -2209,12 +2403,19 @@ async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
2209
2403
  const warnings = [];
2210
2404
  const legacyPaths = [
2211
2405
  join(targetRoot, "plugins", "development-board-toolchain.js"),
2406
+ join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
2212
2407
  join(targetRoot, "plugins", "dbt-agent.js"),
2408
+ join(targetRoot, "plugins", "Dbt Agent.js"),
2409
+ join(targetRoot, "plugins", "deve.js"),
2410
+ join(targetRoot, "plugins", "deve"),
2411
+ join(targetRoot, "node_modules", "development-board-toolchain"),
2412
+ join(targetRoot, "node_modules", "development-board-toolchain-dev"),
2213
2413
  join(targetRoot, "node_modules", "dbt-agent")
2214
2414
  ];
2215
2415
  if (globalInstall) {
2216
2416
  legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
2217
2417
  legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
2418
+ legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
2218
2419
  }
2219
2420
  for (const candidate of legacyPaths) {
2220
2421
  try {
@@ -2243,7 +2444,7 @@ async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
2243
2444
  const filePath = join(targetRoot, entry.name);
2244
2445
  try {
2245
2446
  const current = await readFile(filePath, "utf8");
2246
- if (current.includes("dbt-agent") || current.includes("development-board-toolchain")) {
2447
+ if (legacyTextHasCodexPluginResidue(current)) {
2247
2448
  paths.push(filePath);
2248
2449
  }
2249
2450
  }
@@ -2257,6 +2458,26 @@ async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
2257
2458
  }
2258
2459
  return paths;
2259
2460
  }
2461
+ async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
2462
+ const paths = [];
2463
+ const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
2464
+ try {
2465
+ const entries = await readdir(cacheRoot, { withFileTypes: true });
2466
+ for (const entry of entries) {
2467
+ if (!entry.isFile())
2468
+ continue;
2469
+ if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
2470
+ paths.push(join(cacheRoot, entry.name));
2471
+ }
2472
+ }
2473
+ }
2474
+ catch (error) {
2475
+ if (error.code !== "ENOENT") {
2476
+ warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
2477
+ }
2478
+ }
2479
+ return paths;
2480
+ }
2260
2481
  function legacyOpenCodeCleanupWarning(cleanup) {
2261
2482
  const parts = [];
2262
2483
  if (cleanup.legacy_removed_paths.length > 0) {
@@ -2553,6 +2774,97 @@ async function resolveExecutableOnPath(name) {
2553
2774
  }
2554
2775
  return undefined;
2555
2776
  }
2777
+ async function runBridgeStart(parsed) {
2778
+ const host = stringFlag(parsed, "host");
2779
+ const port = numberFlag(parsed, "port");
2780
+ const bridge = await resolveBridgeLauncher();
2781
+ const args = [...bridge.args];
2782
+ if (host) {
2783
+ args.push("--host", host);
2784
+ }
2785
+ if (port !== undefined) {
2786
+ args.push("--port", String(port));
2787
+ }
2788
+ const env = {
2789
+ ...process.env,
2790
+ ...(host ? { EMBED_BRIDGE_HOST: host } : {}),
2791
+ ...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
2792
+ };
2793
+ const child = spawn(bridge.command, args, {
2794
+ stdio: "inherit",
2795
+ env
2796
+ });
2797
+ const forwardSignal = (signal) => {
2798
+ if (!child.killed) {
2799
+ child.kill(signal);
2800
+ }
2801
+ };
2802
+ process.once("SIGINT", forwardSignal);
2803
+ process.once("SIGTERM", forwardSignal);
2804
+ return await new Promise((resolveCode) => {
2805
+ child.on("error", (error) => {
2806
+ process.off("SIGINT", forwardSignal);
2807
+ process.off("SIGTERM", forwardSignal);
2808
+ console.error(error instanceof Error ? error.message : String(error));
2809
+ resolveCode(1);
2810
+ });
2811
+ child.on("close", (code, signal) => {
2812
+ process.off("SIGINT", forwardSignal);
2813
+ process.off("SIGTERM", forwardSignal);
2814
+ if (signal === "SIGINT" || signal === "SIGTERM") {
2815
+ resolveCode(0);
2816
+ }
2817
+ else {
2818
+ resolveCode(code ?? 0);
2819
+ }
2820
+ });
2821
+ });
2822
+ }
2823
+ async function resolveBridgeLauncher() {
2824
+ const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
2825
+ if (explicitBinary) {
2826
+ try {
2827
+ await access(explicitBinary, constants.X_OK);
2828
+ return { command: explicitBinary, args: [] };
2829
+ }
2830
+ catch {
2831
+ // Fall through so the package launcher can print its clearer repair message.
2832
+ }
2833
+ }
2834
+ const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
2835
+ if (pathBinary) {
2836
+ return { command: pathBinary, args: [] };
2837
+ }
2838
+ const packageLauncher = await resolveBridgePackageLauncher();
2839
+ if (packageLauncher) {
2840
+ return { command: process.execPath, args: [packageLauncher] };
2841
+ }
2842
+ return {
2843
+ command: process.execPath,
2844
+ args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
2845
+ };
2846
+ }
2847
+ async function resolveBridgePackageLauncher() {
2848
+ const candidates = [];
2849
+ try {
2850
+ const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
2851
+ candidates.push(join(dirname(packageJson), "dist", "index.js"));
2852
+ }
2853
+ catch {
2854
+ // Source checkout fallback below.
2855
+ }
2856
+ candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
2857
+ for (const candidate of candidates) {
2858
+ try {
2859
+ await access(candidate, constants.R_OK);
2860
+ return candidate;
2861
+ }
2862
+ catch {
2863
+ // Keep looking.
2864
+ }
2865
+ }
2866
+ return undefined;
2867
+ }
2556
2868
  function defaultOpenCodeRoot() {
2557
2869
  return globalOpenCodeRoot();
2558
2870
  }
@@ -2592,12 +2904,26 @@ async function ensureOpenCodeGlobalPluginConfig() {
2592
2904
  return removed;
2593
2905
  }
2594
2906
  function isLegacyOpenCodePluginConfigEntry(item) {
2595
- const normalized = item.replace(/\\/g, "/");
2596
- return item === "dbt-agent"
2597
- || item === "development-board-toolchain"
2907
+ const normalized = item.trim().replace(/\\/g, "/");
2908
+ const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
2909
+ return normalized === "dbt-agent"
2910
+ || normalized === "Dbt Agent"
2911
+ || normalized === "deve"
2912
+ || normalized === "development-board-toolchain"
2913
+ || normalized === "development-board-toolchain-dev"
2914
+ || normalized === "./plugins/deve"
2915
+ || normalized === "./plugins/deve.js"
2916
+ || /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
2917
+ || /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
2598
2918
  || normalized === "./plugins/development-board-toolchain"
2599
2919
  || normalized === "./plugins/development-board-toolchain.js"
2600
- || normalized.endsWith("/plugins/development-board-toolchain.js")
2920
+ || normalized === "./plugins/development-board-toolchain-dev"
2921
+ || normalized === "./plugins/development-board-toolchain-dev.js"
2922
+ || pathOnly.endsWith("/plugins/development-board-toolchain")
2923
+ || pathOnly.endsWith("/plugins/development-board-toolchain.js")
2924
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev")
2925
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
2926
+ || normalized.includes("dbt-agent")
2601
2927
  || normalized.includes("development-board-toolchain");
2602
2928
  }
2603
2929
  async function openCodeDuplicatePluginWarning(targetRoot) {
@@ -2691,19 +3017,65 @@ async function parseErrorResponse(response) {
2691
3017
  return undefined;
2692
3018
  }
2693
3019
  async function cloudAuthToken() {
3020
+ return (await cloudAuthConfig()).token;
3021
+ }
3022
+ async function cloudAuthConfig() {
2694
3023
  const envToken = process.env.EMBED_API_TOKEN?.trim();
2695
3024
  if (envToken) {
2696
- return envToken;
3025
+ const fileConfig = await readLocalAuthFile();
3026
+ return {
3027
+ ...fileConfig,
3028
+ token: envToken,
3029
+ profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
3030
+ source: "env"
3031
+ };
2697
3032
  }
3033
+ const fileConfig = await readLocalAuthFile();
3034
+ return {
3035
+ ...fileConfig,
3036
+ token: fileConfig.token?.trim() || undefined,
3037
+ profile: fileConfig.profile ?? "default",
3038
+ source: fileConfig.token ? "file" : undefined
3039
+ };
3040
+ }
3041
+ async function readLocalAuthFile() {
2698
3042
  try {
2699
3043
  const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
2700
- const fileToken = typeof parsed.token === "string" ? parsed.token.trim() : "";
2701
- return fileToken || undefined;
3044
+ return normalizeLocalAuthFile(parsed);
2702
3045
  }
2703
3046
  catch {
2704
- return undefined;
3047
+ return {};
2705
3048
  }
2706
3049
  }
3050
+ function normalizeLocalAuthFile(parsed) {
3051
+ const device = isJsonObject(parsed.device) ? parsed.device : undefined;
3052
+ const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
3053
+ ? {
3054
+ device_id: device.device_id,
3055
+ fingerprint_hash: device.fingerprint_hash,
3056
+ private_key_pem: device.private_key_pem,
3057
+ public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
3058
+ label: typeof device.label === "string" ? device.label : undefined,
3059
+ platform: typeof device.platform === "string" ? device.platform : undefined,
3060
+ arch: typeof device.arch === "string" ? device.arch : undefined,
3061
+ hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
3062
+ registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
3063
+ }
3064
+ : undefined;
3065
+ return {
3066
+ profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
3067
+ token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
3068
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
3069
+ account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
3070
+ api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
3071
+ device: normalizedDevice
3072
+ };
3073
+ }
3074
+ async function writeLocalAuthFile(config) {
3075
+ await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
3076
+ await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
3077
+ await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
3078
+ }
2707
3079
  function serviceBaseUrl(url) {
2708
3080
  return url.replace(/\/+$/, "");
2709
3081
  }
@@ -4162,30 +4534,272 @@ async function authLogin(parsed) {
4162
4534
  return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
4163
4535
  }
4164
4536
  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 });
4537
+ const current = await readLocalAuthFile();
4538
+ const localDevice = await buildLocalDeviceAuth(parsed, current.device);
4539
+ const registration = await registerLocalDevice(token.trim(), localDevice.registration);
4540
+ if (!registration.ok) {
4541
+ return fail(registration.error.code, registration.error.message, {
4542
+ remediation: registration.error.remediation,
4543
+ details: registration.error.details
4544
+ });
4545
+ }
4546
+ const device = {
4547
+ device_id: registration.data.device.device_id,
4548
+ fingerprint_hash: localDevice.device.fingerprint_hash,
4549
+ private_key_pem: localDevice.device.private_key_pem,
4550
+ public_key_pem: localDevice.device.public_key_pem,
4551
+ label: registration.data.device.label ?? localDevice.device.label,
4552
+ platform: registration.data.device.platform ?? localDevice.device.platform,
4553
+ arch: registration.data.device.arch ?? localDevice.device.arch,
4554
+ hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
4555
+ registered_at: registration.data.device.first_seen_at
4556
+ };
4557
+ await writeLocalAuthFile({
4558
+ profile,
4559
+ token: token.trim(),
4560
+ updated_at: updatedAt,
4561
+ account_id: registration.data.device.account_id,
4562
+ api_key_id: registration.data.device.api_key_id,
4563
+ device
4564
+ });
4565
+ return ok({
4566
+ authenticated: true,
4567
+ profile,
4568
+ source: "file",
4569
+ updated_at: updatedAt,
4570
+ account_id: registration.data.device.account_id,
4571
+ api_key_id: registration.data.device.api_key_id,
4572
+ device_id: device.device_id,
4573
+ device_fingerprint_hash: device.fingerprint_hash,
4574
+ device_label: device.label,
4575
+ device_registered_at: device.registered_at,
4576
+ device_private_key_configured: true
4577
+ });
4168
4578
  }
4169
4579
  async function authStatus() {
4170
- if (process.env.EMBED_API_TOKEN?.trim()) {
4580
+ const envToken = process.env.EMBED_API_TOKEN?.trim();
4581
+ const file = await readLocalAuthFile();
4582
+ const deviceIntegrity = file.device
4583
+ ? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
4584
+ : "unbound";
4585
+ if (envToken) {
4171
4586
  return {
4172
4587
  authenticated: true,
4173
4588
  profile: process.env.EMBED_AUTH_PROFILE ?? "default",
4174
- source: "env"
4589
+ source: "env",
4590
+ account_id: file.account_id,
4591
+ api_key_id: file.api_key_id,
4592
+ device_id: file.device?.device_id,
4593
+ device_fingerprint_hash: file.device?.fingerprint_hash,
4594
+ device_label: file.device?.label,
4595
+ device_registered_at: file.device?.registered_at,
4596
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
4597
+ device_integrity: deviceIntegrity
4175
4598
  };
4176
4599
  }
4600
+ return {
4601
+ authenticated: Boolean(file.token?.trim()),
4602
+ profile: file.profile ?? "default",
4603
+ source: file.token ? "file" : undefined,
4604
+ updated_at: file.updated_at,
4605
+ account_id: file.account_id,
4606
+ api_key_id: file.api_key_id,
4607
+ device_id: file.device?.device_id,
4608
+ device_fingerprint_hash: file.device?.fingerprint_hash,
4609
+ device_label: file.device?.label,
4610
+ device_registered_at: file.device?.registered_at,
4611
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
4612
+ device_integrity: deviceIntegrity
4613
+ };
4614
+ }
4615
+ async function authDeviceStatus(parsed) {
4616
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4617
+ const unexpected = parsed.command.slice(3);
4618
+ if (unknownFlag || unexpected.length > 0) {
4619
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
4620
+ }
4621
+ const local = await authStatus();
4622
+ const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
4623
+ if (remote && !remote.ok) {
4624
+ return ok({ local });
4625
+ }
4626
+ return ok({ local, remote: remote?.data });
4627
+ }
4628
+ async function authDeviceList(parsed) {
4629
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4630
+ const unexpected = parsed.command.slice(3);
4631
+ if (unknownFlag || unexpected.length > 0) {
4632
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
4633
+ }
4634
+ return await cloudGet("/v1/me/devices");
4635
+ }
4636
+ async function authDeviceRevoke(parsed) {
4637
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4638
+ if (unknownFlag) {
4639
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
4640
+ }
4641
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
4642
+ if (!id.ok) {
4643
+ return fail("invalid_args", id.error);
4644
+ }
4645
+ return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
4646
+ }
4647
+ async function authDeviceRename(parsed) {
4648
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
4649
+ if (unknownFlag) {
4650
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
4651
+ }
4652
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
4653
+ if (!id.ok) {
4654
+ return fail("invalid_args", id.error);
4655
+ }
4656
+ const label = stringFlag(parsed, "label");
4657
+ if (!label?.trim()) {
4658
+ return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
4659
+ }
4660
+ const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
4661
+ if (updated.ok) {
4662
+ const auth = await readLocalAuthFile();
4663
+ if (auth.device?.device_id === updated.data.device_id) {
4664
+ await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
4665
+ }
4666
+ }
4667
+ return updated;
4668
+ }
4669
+ async function buildLocalDeviceAuth(parsed, existing) {
4670
+ const fingerprint = await localHardwareFingerprint();
4671
+ const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
4672
+ ? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
4673
+ : generateLocalDeviceKeyPair();
4674
+ const label = stringFlag(parsed, "label")?.trim()
4675
+ || existing?.label
4676
+ || `${fingerprint.platform} ${fingerprint.arch}`;
4677
+ const device = {
4678
+ device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
4679
+ fingerprint_hash: fingerprint.fingerprint_hash,
4680
+ private_key_pem: keyPair.privateKeyPem,
4681
+ public_key_pem: keyPair.publicKeyPem,
4682
+ label,
4683
+ platform: fingerprint.platform,
4684
+ arch: fingerprint.arch,
4685
+ hostname_hash: fingerprint.hostname_hash,
4686
+ registered_at: existing?.registered_at
4687
+ };
4688
+ return {
4689
+ device,
4690
+ registration: {
4691
+ fingerprint_hash: fingerprint.fingerprint_hash,
4692
+ public_key: keyPair.publicKeyPem,
4693
+ label,
4694
+ platform: fingerprint.platform,
4695
+ arch: fingerprint.arch,
4696
+ hostname_hash: fingerprint.hostname_hash,
4697
+ client_name: EMBED_CLIENT_NAME,
4698
+ client_version: EMBED_CLIENT_VERSION,
4699
+ metadata: {
4700
+ fingerprint_version: "v1",
4701
+ fingerprint_source: fingerprint.source
4702
+ }
4703
+ }
4704
+ };
4705
+ }
4706
+ function generateLocalDeviceKeyPair() {
4707
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
4708
+ return {
4709
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
4710
+ publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
4711
+ };
4712
+ }
4713
+ async function registerLocalDevice(token, body) {
4177
4714
  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
4715
+ const bodyText = JSON.stringify(body);
4716
+ const headers = {
4717
+ "content-type": "application/json",
4718
+ authorization: `Bearer ${token}`
4184
4719
  };
4720
+ addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
4721
+ const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
4722
+ method: "POST",
4723
+ headers,
4724
+ body: bodyText
4725
+ });
4726
+ const parsed = await response.json();
4727
+ return enrichCloudAuthFailure(parsed, true);
4728
+ }
4729
+ catch (error) {
4730
+ return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
4731
+ remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
4732
+ });
4733
+ }
4734
+ }
4735
+ async function localHardwareFingerprint() {
4736
+ cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
4737
+ return await cachedLocalHardwareFingerprint;
4738
+ }
4739
+ async function localHardwareFingerprintUncached() {
4740
+ const platformName = platform();
4741
+ const archName = arch();
4742
+ const raw = await localHardwareId(platformName);
4743
+ const fingerprintHash = createHash("sha256")
4744
+ .update(`embed-labs:device:v1:${platformName}:${raw.value}`)
4745
+ .digest("hex");
4746
+ const hostnameHash = createHash("sha256")
4747
+ .update(`embed-labs:hostname:v1:${hostname()}`)
4748
+ .digest("hex");
4749
+ return {
4750
+ fingerprint_hash: fingerprintHash,
4751
+ platform: platformName,
4752
+ arch: archName,
4753
+ hostname_hash: hostnameHash,
4754
+ source: raw.source
4755
+ };
4756
+ }
4757
+ async function localHardwareId(platformName) {
4758
+ if (platformName === "darwin") {
4759
+ const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
4760
+ const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
4761
+ if (match?.[1]) {
4762
+ return { value: match[1], source: "macos_ioplatformuuid" };
4763
+ }
4764
+ }
4765
+ if (platformName === "win32") {
4766
+ const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
4767
+ const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
4768
+ if (match?.[1]?.trim()) {
4769
+ return { value: match[1].trim(), source: "windows_machineguid" };
4770
+ }
4771
+ }
4772
+ if (platformName === "linux") {
4773
+ for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
4774
+ try {
4775
+ const value = (await readFile(pathValue, "utf8")).trim();
4776
+ if (value) {
4777
+ return { value, source: `linux:${pathValue}` };
4778
+ }
4779
+ }
4780
+ catch {
4781
+ // Try the next stable machine id location.
4782
+ }
4783
+ }
4784
+ }
4785
+ const generated = await localGeneratedInstallId();
4786
+ return { value: generated, source: "generated_install_id" };
4787
+ }
4788
+ async function localGeneratedInstallId() {
4789
+ try {
4790
+ const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
4791
+ if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
4792
+ return parsed.generated_install_id.trim();
4793
+ }
4185
4794
  }
4186
4795
  catch {
4187
- return { authenticated: false, profile: "default" };
4796
+ // Fall through and create a local-only fallback id.
4188
4797
  }
4798
+ const generated = `install_${randomBytes(24).toString("base64url")}`;
4799
+ await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
4800
+ await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
4801
+ await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
4802
+ return generated;
4189
4803
  }
4190
4804
  function accountCreateBody(parsed) {
4191
4805
  const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
@@ -4351,6 +4965,8 @@ function mcpToolEventBody(parsed) {
4351
4965
  "mode",
4352
4966
  "server-model-used",
4353
4967
  "success",
4968
+ "local-device-id",
4969
+ "local_device_id",
4354
4970
  "request-id",
4355
4971
  "duration-ms",
4356
4972
  "input-summary",
@@ -4377,6 +4993,9 @@ function mcpToolEventBody(parsed) {
4377
4993
  const modeResult = optionalTrimmedStringFlag(parsed, "mode");
4378
4994
  if (modeResult.error)
4379
4995
  return modeResult.error;
4996
+ const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
4997
+ if (localDeviceResult.error)
4998
+ return localDeviceResult.error;
4380
4999
  const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
4381
5000
  if (requestIdResult.error)
4382
5001
  return requestIdResult.error;
@@ -4400,12 +5019,14 @@ function mcpToolEventBody(parsed) {
4400
5019
  tool_name: toolResult.value,
4401
5020
  client: clientResult.value,
4402
5021
  mode: modeResult.value,
5022
+ local_device_id: localDeviceResult.value,
4403
5023
  server_model_used: serverModelUsed,
4404
5024
  success,
4405
5025
  request_id: requestIdResult.value,
4406
5026
  duration_ms: durationResult.value,
4407
5027
  input_summary: inputSummaryResult.value,
4408
- output_summary: outputSummaryResult.value
5028
+ output_summary: outputSummaryResult.value,
5029
+ metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
4409
5030
  });
4410
5031
  }
4411
5032
  function usageSummaryRequest(parsed) {
@@ -4923,7 +5544,9 @@ function localToolchainAuthContext(auth, accountId) {
4923
5544
  authenticated: auth.authenticated,
4924
5545
  profile: auth.profile,
4925
5546
  source: auth.source,
4926
- account_id: accountId
5547
+ account_id: accountId,
5548
+ api_key_id: auth.api_key_id,
5549
+ device_id: auth.device_id
4927
5550
  };
4928
5551
  }
4929
5552
  function usageEventsRequest(parsed) {
@@ -5394,9 +6017,53 @@ function renderAgentRunResult(result) {
5394
6017
  return lines.join("\n");
5395
6018
  }
5396
6019
  function renderAuthStatus(status) {
5397
- return status.authenticated
5398
- ? `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`
5399
- : `Not authenticated profile=${status.profile}`;
6020
+ if (!status.authenticated) {
6021
+ return `Not authenticated profile=${status.profile}`;
6022
+ }
6023
+ return [
6024
+ `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
6025
+ status.account_id ? `account=${status.account_id}` : "",
6026
+ status.api_key_id ? `api_key=${status.api_key_id}` : "",
6027
+ status.device_id ? `device=${status.device_id}` : "device=not_registered",
6028
+ status.device_label ? `device_label=${status.device_label}` : "",
6029
+ status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
6030
+ status.device_private_key_configured === false ? "device_private_key=missing" : ""
6031
+ ].filter(Boolean).join("\n");
6032
+ }
6033
+ function renderAuthDeviceStatus(status) {
6034
+ const lines = [renderAuthStatus(status.local)];
6035
+ if (status.remote) {
6036
+ const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
6037
+ lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
6038
+ const localDevice = status.local.device_id
6039
+ ? status.remote.devices.find((device) => device.device_id === status.local.device_id)
6040
+ : undefined;
6041
+ if (localDevice) {
6042
+ lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
6043
+ }
6044
+ }
6045
+ return lines.join("\n");
6046
+ }
6047
+ function renderAuthDeviceList(result) {
6048
+ if (result.devices.length === 0) {
6049
+ return `No registered devices. device_limit=${result.device_limit}`;
6050
+ }
6051
+ return [
6052
+ `device_limit=${result.device_limit}`,
6053
+ ...result.devices.map(renderAuthDevice)
6054
+ ].join("\n");
6055
+ }
6056
+ function renderAuthDevice(device) {
6057
+ return [
6058
+ `${device.device_id} account=${device.account_id}`,
6059
+ device.api_key_id ? `api_key=${device.api_key_id}` : "",
6060
+ `status=${device.status}`,
6061
+ device.label ? `label=${device.label}` : "",
6062
+ device.platform ? `platform=${device.platform}` : "",
6063
+ device.arch ? `arch=${device.arch}` : "",
6064
+ `last_seen_at=${device.last_seen_at}`,
6065
+ device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
6066
+ ].filter(Boolean).join(" ");
5400
6067
  }
5401
6068
  function renderAccount(account) {
5402
6069
  return [
@@ -6909,7 +7576,7 @@ Help:
6909
7576
 
6910
7577
  Environment:
6911
7578
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
6912
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
7579
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
6913
7580
  EMBED_API_TOKEN=<token>
6914
7581
  CODEX_HOME=~/.codex
6915
7582
 
@@ -7148,7 +7815,7 @@ Usage:
7148
7815
 
7149
7816
  Environment:
7150
7817
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
7151
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
7818
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
7152
7819
  EMBED_API_TOKEN=<token>
7153
7820
  CODEX_HOME=~/.codex
7154
7821
  `);