@lambdacurry/arbor 0.20.32 → 0.20.33

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.
Files changed (2) hide show
  1. package/dist/arbor.js +106 -12
  2. package/package.json +1 -1
package/dist/arbor.js CHANGED
@@ -20993,11 +20993,70 @@ var ACTIONS = ACTION_DEFINITIONS.map((action) => {
20993
20993
  });
20994
20994
 
20995
20995
  // src/config.ts
20996
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20996
+ import {
20997
+ chmodSync,
20998
+ existsSync,
20999
+ mkdirSync,
21000
+ readFileSync,
21001
+ rmSync,
21002
+ statSync,
21003
+ writeFileSync
21004
+ } from "node:fs";
20997
21005
  import { homedir } from "node:os";
20998
21006
  import { dirname, join } from "node:path";
20999
21007
  var CONFIG_PATH = process.env.ARBOR_CONFIG ?? join(homedir(), ".arbor", "config.json");
21000
21008
  var DEFAULT_API_URL = process.env.ARBOR_API_URL ?? "https://arborthreads.com";
21009
+ var DEFAULT_CONFIG_PATH = join(homedir(), ".arbor", "config.json");
21010
+ var DEFAULT_TOKEN_OVERWRITE_REFUSAL = "refusing to overwrite ~/.arbor/config.json — a token already exists. Set ARBOR_CONFIG to an isolated path (e.g. ~/.arbor/<agent>.config.json) and retry.";
21011
+ var LOCK_TIMEOUT_MS = 5000;
21012
+ var LOCK_STALE_MS = 30000;
21013
+ var LOCK_POLL_MS = 20;
21014
+ function sleepSync(ms) {
21015
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
21016
+ }
21017
+ function acquireConfigLock(configPath) {
21018
+ const lockPath = `${configPath}.lock`;
21019
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
21020
+ for (;; ) {
21021
+ try {
21022
+ mkdirSync(lockPath);
21023
+ return () => {
21024
+ try {
21025
+ rmSync(lockPath, { recursive: true, force: true });
21026
+ } catch {}
21027
+ };
21028
+ } catch (err) {
21029
+ if (err?.code !== "EEXIST")
21030
+ throw err;
21031
+ try {
21032
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
21033
+ rmSync(lockPath, { recursive: true, force: true });
21034
+ continue;
21035
+ }
21036
+ } catch {
21037
+ continue;
21038
+ }
21039
+ if (Date.now() >= deadline) {
21040
+ throw new Error(`timed out waiting for another arbor process to finish writing ${configPath}. If none is running, remove ${lockPath}.`);
21041
+ }
21042
+ sleepSync(LOCK_POLL_MS);
21043
+ }
21044
+ }
21045
+ }
21046
+ function defaultConfigOverwriteBlocked(opts) {
21047
+ const isolated = opts?.isolated ?? Boolean(process.env.ARBOR_CONFIG);
21048
+ const path = opts?.path ?? DEFAULT_CONFIG_PATH;
21049
+ if (isolated)
21050
+ return false;
21051
+ if (!existsSync(path))
21052
+ return false;
21053
+ try {
21054
+ const cfg = JSON.parse(readFileSync(path, "utf8"));
21055
+ return Boolean(cfg.token);
21056
+ } catch {
21057
+ return false;
21058
+ }
21059
+ }
21001
21060
  function loadConfig() {
21002
21061
  const envToken = process.env.ARBOR_TOKEN || undefined;
21003
21062
  const envUrl = process.env.ARBOR_API_URL || undefined;
@@ -21009,15 +21068,32 @@ function loadConfig() {
21009
21068
  }
21010
21069
  return { apiUrl: envUrl ?? DEFAULT_API_URL, token: envToken };
21011
21070
  }
21012
- function saveConfig(cfg) {
21013
- mkdirSync(dirname(CONFIG_PATH), { recursive: true });
21014
- writeFileSync(CONFIG_PATH, `${JSON.stringify(cfg, null, 2)}
21015
- `);
21016
- chmodSync(CONFIG_PATH, 384);
21071
+ function writeConfigGuarded(opts) {
21072
+ mkdirSync(dirname(opts.configPath), { recursive: true });
21073
+ const release = acquireConfigLock(opts.configPath);
21074
+ try {
21075
+ if (!opts.allowDefaultTokenOverwrite && defaultConfigOverwriteBlocked({ isolated: opts.isolated, path: opts.defaultPath })) {
21076
+ throw new Error(DEFAULT_TOKEN_OVERWRITE_REFUSAL);
21077
+ }
21078
+ writeFileSync(opts.configPath, `${JSON.stringify(opts.cfg, null, 2)}
21079
+ `, { mode: 384 });
21080
+ chmodSync(opts.configPath, 384);
21081
+ } finally {
21082
+ release();
21083
+ }
21084
+ }
21085
+ function saveConfig(cfg, opts) {
21086
+ writeConfigGuarded({
21087
+ cfg,
21088
+ configPath: CONFIG_PATH,
21089
+ defaultPath: DEFAULT_CONFIG_PATH,
21090
+ isolated: Boolean(process.env.ARBOR_CONFIG),
21091
+ allowDefaultTokenOverwrite: opts?.allowDefaultTokenOverwrite
21092
+ });
21017
21093
  }
21018
21094
  function clearToken() {
21019
21095
  const cfg = loadConfig();
21020
- saveConfig({ apiUrl: cfg.apiUrl });
21096
+ saveConfig({ apiUrl: cfg.apiUrl }, { allowDefaultTokenOverwrite: true });
21021
21097
  }
21022
21098
 
21023
21099
  // src/errors.ts
@@ -21747,20 +21823,38 @@ async function runObjectVerb(positionals, flags, ctx) {
21747
21823
  // src/connect.ts
21748
21824
  var DEFAULT_POLL_INTERVAL_MS = 2000;
21749
21825
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21826
+ function bareConnectUrl(apiUrl, returnedUrl) {
21827
+ const fallback = `${apiUrl.replace(/\/$/, "")}/connect`;
21828
+ if (!returnedUrl)
21829
+ return fallback;
21830
+ try {
21831
+ return `${new URL(returnedUrl).origin}/connect`;
21832
+ } catch {
21833
+ return fallback;
21834
+ }
21835
+ }
21750
21836
  async function connect(opts) {
21837
+ if (defaultConfigOverwriteBlocked()) {
21838
+ throw new Error(DEFAULT_TOKEN_OVERWRITE_REFUSAL);
21839
+ }
21751
21840
  const apiUrl = opts.url ?? loadConfig().apiUrl;
21752
21841
  const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
21753
- const body = JSON.stringify({ pairingKey: opts.pairingKey });
21754
21842
  const headers = { "content-type": "application/json" };
21755
- const initRes = await fetch(`${apiUrl}/api/agent/connect`, { method: "POST", headers, body });
21843
+ const body = JSON.stringify({ pairingKey: opts.pairingKey });
21844
+ const initRes = await fetch(`${apiUrl}/api/agent/connect`, {
21845
+ method: "POST",
21846
+ headers,
21847
+ body
21848
+ });
21756
21849
  const init = await initRes.json().catch(() => ({}));
21757
21850
  if (!initRes.ok || !init.url || !init.userCode) {
21758
21851
  throw new Error(`could not start pairing (${initRes.status}): ${init.error ?? "is the server reachable?"}`);
21759
21852
  }
21853
+ const approvalUrl = bareConnectUrl(apiUrl, init.url);
21760
21854
  process.stderr.write(`
21761
21855
  Pairing against ${apiUrl}
21762
21856
  ` + ` Ask the person who will manage this agent${init.agentName ? ` (“${init.agentName}”)` : ""} to approve it:
21763
- ` + ` 1. open ${init.url}
21857
+ ` + ` 1. open ${approvalUrl}
21764
21858
  ` + ` 2. enter the code: ${init.userCode}
21765
21859
 
21766
21860
  ` + ` Only approve an agent you intend to manage.
@@ -21835,7 +21929,7 @@ async function login(opts) {
21835
21929
  });
21836
21930
  const tok = await tokRes.json().catch(() => ({}));
21837
21931
  if (tok.access_token) {
21838
- saveConfig({ apiUrl, token: tok.access_token });
21932
+ saveConfig({ apiUrl, token: tok.access_token }, { allowDefaultTokenOverwrite: true });
21839
21933
  return;
21840
21934
  }
21841
21935
  if (tok.error === "authorization_pending")
@@ -22037,7 +22131,7 @@ async function main() {
22037
22131
  if (!token)
22038
22132
  throw new UsageError("usage: arbor auth <token> [--url <api-url>]");
22039
22133
  const url2 = stringFlag(flags.url, "url") ?? loadConfig().apiUrl;
22040
- saveConfig({ apiUrl: url2, token });
22134
+ saveConfig({ apiUrl: url2, token }, { allowDefaultTokenOverwrite: true });
22041
22135
  advise(` ✓ Token saved (${CONFIG_PATH}).
22042
22136
  `, ctx);
22043
22137
  await renderMe(ctx, "auth");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.20.32",
3
+ "version": "0.20.33",
4
4
  "description": "The Arbor CLI — a shared workspace for people and agents. The human + headless-agent write path over Arbor's guarded operation surface.",
5
5
  "keywords": [
6
6
  "agents",