@snowyroad/braid 0.72.1 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -365,8 +365,14 @@ function decodeInvite(code) {
365
365
  }
366
366
 
367
367
  // src/redeem.ts
368
+ var InviteExpiredError = class extends Error {
369
+ constructor() {
370
+ super("This invite has expired or been revoked. Ask an admin to mint a new one.");
371
+ this.name = "InviteExpiredError";
372
+ }
373
+ };
368
374
  async function redeemInvite(relayHttpUrl, code) {
369
- const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-QJUYOU2D.js");
375
+ const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-EP5W2BCL.js");
370
376
  const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/invites/redeem`, {
371
377
  method: "POST",
372
378
  headers: { "content-type": "application/json" },
@@ -375,7 +381,7 @@ async function redeemInvite(relayHttpUrl, code) {
375
381
  });
376
382
  if (!r.ok) {
377
383
  if (r.category === "http" && r.status === 410) {
378
- throw new Error("This invite has expired or been revoked. Ask an admin to mint a new one.");
384
+ throw new InviteExpiredError();
379
385
  }
380
386
  if (r.category === "http") throw new Error(`Invite redemption failed (HTTP ${r.status}).`);
381
387
  throw new Error(`Invite redemption failed (${r.category}).`);
@@ -455,7 +461,7 @@ function validateReplacementToken(newToken, currentToken, nowMs = Date.now()) {
455
461
  return { ok: true };
456
462
  }
457
463
  async function mintAccessToken(relayHttpUrl, agentKey, fetchFn = fetch, dpopProof) {
458
- const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-QJUYOU2D.js");
464
+ const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-EP5W2BCL.js");
459
465
  const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/agents/token`, {
460
466
  method: "POST",
461
467
  headers: {
@@ -485,6 +491,7 @@ async function mintAccessToken(relayHttpUrl, agentKey, fetchFn = fetch, dpopProo
485
491
  }
486
492
 
487
493
  // src/exitCodes.ts
494
+ var EXIT_OK = 0;
488
495
  var EXIT_NEEDS_HUMAN = 78;
489
496
  var EXIT_CRASH = 1;
490
497
  var KeystorePermsError = class extends Error {
@@ -2099,19 +2106,26 @@ async function boundedJsonFetch(url, opts) {
2099
2106
  }
2100
2107
 
2101
2108
  export {
2109
+ ACP_AGENT_IDS,
2102
2110
  parseRunFlags,
2103
2111
  resolveKnobs,
2104
2112
  newBeyondFile,
2105
2113
  hasSubscriptionLogin,
2106
2114
  effectiveAnthropicCredential,
2107
2115
  macKeychainHasClaudeLogin,
2116
+ decodeInvite,
2108
2117
  assertHttpEndpoint,
2109
2118
  snapshotHttpMetrics,
2110
2119
  resetHttpMetricsForTest,
2111
2120
  boundedJsonFetch,
2121
+ InviteExpiredError,
2122
+ redeemInvite,
2112
2123
  RebootstrapError,
2113
2124
  validateReplacementToken,
2125
+ mintAccessToken,
2126
+ EXIT_OK,
2114
2127
  EXIT_NEEDS_HUMAN,
2128
+ EXIT_CRASH,
2115
2129
  KeystorePermsError,
2116
2130
  classifyFatal,
2117
2131
  assertKeystorePerms,
@@ -2138,6 +2152,8 @@ export {
2138
2152
  verifyCardSignature,
2139
2153
  cleanContent,
2140
2154
  resolveAgentSelection,
2155
+ validateRelayWsUrl,
2156
+ wsToHttp,
2141
2157
  resolveConfig,
2142
2158
  redactConfig
2143
2159
  };
package/dist/cli.js CHANGED
@@ -15,7 +15,11 @@ import {
15
15
  utext
16
16
  } from "./chunk-LGYVCFGB.js";
17
17
  import {
18
+ ACP_AGENT_IDS,
19
+ EXIT_CRASH,
18
20
  EXIT_NEEDS_HUMAN,
21
+ EXIT_OK,
22
+ InviteExpiredError,
19
23
  KeystorePermsError,
20
24
  RebootstrapError,
21
25
  acquireAgentLock,
@@ -24,6 +28,7 @@ import {
24
28
  classifyFatal,
25
29
  cleanContent,
26
30
  configDir,
31
+ decodeInvite,
27
32
  effectiveAnthropicCredential,
28
33
  evaluateAcpPermission,
29
34
  evaluateSdkTool,
@@ -33,6 +38,7 @@ import {
33
38
  listAgents,
34
39
  loadAgent,
35
40
  macKeychainHasClaudeLogin,
41
+ mintAccessToken,
36
42
  narrowestMode,
37
43
  narrowestOverride,
38
44
  newBeyondFile,
@@ -41,6 +47,7 @@ import {
41
47
  pinEndpoint,
42
48
  readPathWithFallback,
43
49
  redactConfig,
50
+ redeemInvite,
44
51
  relayHostSegment,
45
52
  resolveAgentSelection,
46
53
  resolveConfig,
@@ -52,9 +59,11 @@ import {
52
59
  takeDenialHint,
53
60
  toolModeLabel,
54
61
  toolStatusLine,
62
+ validateRelayWsUrl,
55
63
  validateReplacementToken,
56
- verifyCardSignature
57
- } from "./chunk-3I4XXMJX.js";
64
+ verifyCardSignature,
65
+ wsToHttp
66
+ } from "./chunk-IARPNMQJ.js";
58
67
 
59
68
  // src/health.ts
60
69
  var CEILING_STRIKES_TO_DEAD = 3;
@@ -6780,6 +6789,11 @@ var USAGE = `Usage: braid <command>
6780
6789
 
6781
6790
  Commands:
6782
6791
  join <code> Join a relay with an invite code (saves the credential)
6792
+ enroll --invite-stdin [--json] [--provider <id>] [--model <name>]
6793
+ One-shot enrollment: reads the invite from stdin (never as an
6794
+ argument), redeems it, saves and verifies the credential, then
6795
+ exits without starting a session (--json for machine output;
6796
+ --provider/--model persist to the saved agent, same as join)
6783
6797
  start [name] Start the bridge from a saved credential
6784
6798
  (env-driven config like BRAID_INVITE / BRAID_TOKEN is honored)
6785
6799
  list List saved agents and their tool access
@@ -6919,6 +6933,52 @@ function parseCliArgs(argv) {
6919
6933
  if (first === "version" || first === "--version" || first === "-v") {
6920
6934
  return { kind: "version" };
6921
6935
  }
6936
+ if (first === "enroll") {
6937
+ let inviteStdin = false;
6938
+ let json = false;
6939
+ let provider;
6940
+ let model;
6941
+ const rest = argv.slice(1);
6942
+ for (let i = 0; i < rest.length; i++) {
6943
+ const a = rest[i];
6944
+ if (a === "--invite-stdin") {
6945
+ inviteStdin = true;
6946
+ continue;
6947
+ }
6948
+ if (a === "--json") {
6949
+ json = true;
6950
+ continue;
6951
+ }
6952
+ if (a === "--provider" || a === "--model") {
6953
+ const v = rest[i + 1]?.trim();
6954
+ if (v === void 0 || v === "" || v.startsWith("--")) {
6955
+ return { kind: "usage", error: `Missing value for ${a}` };
6956
+ }
6957
+ i++;
6958
+ if (a === "--provider") {
6959
+ if (!ACP_AGENT_IDS.includes(v)) {
6960
+ return { kind: "usage", error: `Invalid --provider: ${v}. Expected one of: ${ACP_AGENT_IDS.join(", ")}` };
6961
+ }
6962
+ provider = v;
6963
+ } else {
6964
+ model = v;
6965
+ }
6966
+ continue;
6967
+ }
6968
+ if (a.startsWith("-")) return { kind: "usage", error: `Unknown flag for enroll: ${a}` };
6969
+ return {
6970
+ kind: "usage",
6971
+ error: "enroll does not accept the invite as an argument. Pipe it on stdin: <command that prints the invite> | npx @snowyroad/braid enroll --invite-stdin"
6972
+ };
6973
+ }
6974
+ if (!inviteStdin) {
6975
+ return {
6976
+ kind: "usage",
6977
+ error: "enroll requires --invite-stdin (the invite is read from stdin, never from arguments)."
6978
+ };
6979
+ }
6980
+ return { kind: "enroll", json, ...provider ? { provider } : {}, ...model ? { model } : {} };
6981
+ }
6922
6982
  if (first === "join" || first === "start") {
6923
6983
  const parsed = parseRunFlags(argv);
6924
6984
  if (!parsed.ok) return { kind: "usage", error: parsed.error };
@@ -6928,6 +6988,125 @@ function parseCliArgs(argv) {
6928
6988
  return { kind: "usage", error: `Unknown command: ${first}` };
6929
6989
  }
6930
6990
 
6991
+ // src/enroll.ts
6992
+ var ENROLL_STDIN_MAX_BYTES = 16 * 1024;
6993
+ function readBoundedStdin(stream, maxBytes) {
6994
+ return new Promise((resolve2, reject) => {
6995
+ const chunks = [];
6996
+ let total = 0;
6997
+ let settled = false;
6998
+ const finish = (fn) => {
6999
+ if (settled) return;
7000
+ settled = true;
7001
+ stream.removeListener("data", onData);
7002
+ stream.removeListener("end", onEnd);
7003
+ stream.removeListener("error", onError);
7004
+ fn();
7005
+ };
7006
+ const onData = (chunk) => {
7007
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
7008
+ total += buf.length;
7009
+ if (total > maxBytes) {
7010
+ finish(() => reject(new Error(`stdin exceeded the ${maxBytes / 1024}KiB invite limit`)));
7011
+ return;
7012
+ }
7013
+ chunks.push(buf);
7014
+ };
7015
+ const onEnd = () => finish(() => resolve2(Buffer.concat(chunks).toString("utf8").trim()));
7016
+ const onError = (err) => finish(() => reject(err));
7017
+ stream.on("data", onData);
7018
+ stream.on("end", onEnd);
7019
+ stream.on("error", onError);
7020
+ });
7021
+ }
7022
+ function persistOrNeedsHuman(dir, agent) {
7023
+ try {
7024
+ return saveAgent(dir, agent);
7025
+ } catch (err) {
7026
+ const code = err?.code;
7027
+ if (code === "EACCES" || code === "EPERM") {
7028
+ throw new KeystorePermsError(
7029
+ `cannot write the keystore under ${dir} (${code}). Fix its ownership/permissions (chmod 700 ${dir}) and run enroll again.`
7030
+ );
7031
+ }
7032
+ throw err;
7033
+ }
7034
+ }
7035
+ async function runEnroll(opts, io, env) {
7036
+ const outLine = (s) => void io.stdout.write(s + "\n");
7037
+ const errLine = (s) => void io.stderr.write(s + "\n");
7038
+ if (io.stdin.isTTY === true) {
7039
+ errLine(
7040
+ "[braid] enroll reads the invite from stdin and nothing is piped in. Run it as: <command that prints the invite> | npx @snowyroad/braid enroll --invite-stdin"
7041
+ );
7042
+ return EXIT_CRASH;
7043
+ }
7044
+ let blob;
7045
+ try {
7046
+ blob = await readBoundedStdin(io.stdin, ENROLL_STDIN_MAX_BYTES);
7047
+ } catch (err) {
7048
+ errLine(`[braid] enroll failed: ${sanitizeForTty(err instanceof Error ? err.message : String(err))}`);
7049
+ return EXIT_CRASH;
7050
+ }
7051
+ if (blob === "") {
7052
+ errLine(
7053
+ "[braid] enroll received an empty stdin. Pipe the invite in: <command that prints the invite> | npx @snowyroad/braid enroll --invite-stdin"
7054
+ );
7055
+ return EXIT_CRASH;
7056
+ }
7057
+ try {
7058
+ const inv = decodeInvite(blob);
7059
+ validateRelayWsUrl(inv.relayUrl, env, "invite");
7060
+ const relayHttpUrl = wsToHttp(inv.relayUrl);
7061
+ const bundle = await redeemInvite(relayHttpUrl, inv.code);
7062
+ const dir = configDir(env);
7063
+ const existing = listAgents(dir).find(
7064
+ (e) => e.agent.agentName === bundle.agentName && e.agent.relayUrl === inv.relayUrl
7065
+ );
7066
+ const stored = {
7067
+ ...existing?.agent ?? {},
7068
+ relayUrl: inv.relayUrl,
7069
+ agentId: bundle.agentId,
7070
+ agentName: bundle.agentName,
7071
+ agentUuid: bundle.agentUuid,
7072
+ agentKey: bundle.agentKey,
7073
+ ...opts.provider ? { provider: opts.provider } : {},
7074
+ ...opts.model ? { model: opts.model } : {}
7075
+ };
7076
+ const file = persistOrNeedsHuman(dir, stored);
7077
+ assertKeystorePerms(dir, file);
7078
+ const minted = await mintAccessToken(relayHttpUrl, stored.agentKey);
7079
+ persistOrNeedsHuman(dir, { ...stored, agentKey: minted.agentKey });
7080
+ const name = sanitizeForTty(bundle.agentName);
7081
+ const cmdName = isShellSafeName(bundle.agentName) ? bundle.agentName : "<agent-name>";
7082
+ if (opts.json) {
7083
+ outLine(JSON.stringify({
7084
+ ok: true,
7085
+ agentName: bundle.agentName,
7086
+ agentUuid: bundle.agentUuid,
7087
+ relayUrl: inv.relayUrl,
7088
+ keystoreFile: file,
7089
+ // provider/model appear ONLY when persisted on the stored agent.
7090
+ ...stored.provider ? { provider: stored.provider } : {},
7091
+ ...stored.model ? { model: stored.model } : {}
7092
+ }));
7093
+ } else {
7094
+ outLine(`[braid] enrolled: ${name}`);
7095
+ outLine(`[braid] relay: ${inv.relayUrl}`);
7096
+ outLine(`[braid] credential saved to ${file} (verified: token mint ok)`);
7097
+ if (opts.provider) outLine(`[braid] provider persisted: ${opts.provider}`);
7098
+ if (opts.model) outLine(`[braid] model persisted: ${sanitizeForTty(opts.model)}`);
7099
+ outLine(`[braid] start it with: npx @snowyroad/braid start ${cmdName}`);
7100
+ }
7101
+ return EXIT_OK;
7102
+ } catch (err) {
7103
+ const e = err instanceof Error ? err : new Error(String(err));
7104
+ errLine(`[braid] enroll failed: ${sanitizeForTty(e.message)}`);
7105
+ if (e instanceof InviteExpiredError) return EXIT_NEEDS_HUMAN;
7106
+ return classifyFatal(e);
7107
+ }
7108
+ }
7109
+
6931
7110
  // src/connectedBanner.ts
6932
7111
  function connectedBannerLines(opts) {
6933
7112
  const lines = [
@@ -8790,6 +8969,15 @@ async function main() {
8790
8969
  printStatus(invocation.agent, invocation.json);
8791
8970
  return;
8792
8971
  }
8972
+ if (invocation.kind === "enroll") {
8973
+ process.exit(
8974
+ await runEnroll(
8975
+ { json: invocation.json, provider: invocation.provider, model: invocation.model },
8976
+ { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr },
8977
+ process.env
8978
+ )
8979
+ );
8980
+ }
8793
8981
  if (invocation.kind === "service") {
8794
8982
  process.exit(await runServiceVerb(invocation.verb, invocation.agent, invocation.json));
8795
8983
  }
@@ -4,7 +4,7 @@ import {
4
4
  boundedJsonFetch,
5
5
  resetHttpMetricsForTest,
6
6
  snapshotHttpMetrics
7
- } from "./chunk-3I4XXMJX.js";
7
+ } from "./chunk-IARPNMQJ.js";
8
8
  export {
9
9
  assertHttpEndpoint,
10
10
  boundedJsonFetch,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@snowyroad/braid",
3
- "version": "0.72.1",
3
+ "version": "0.74.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@snowyroad/braid",
9
- "version": "0.72.1",
9
+ "version": "0.74.0",
10
10
  "license": "SEE LICENSE IN LICENSE.md",
11
11
  "dependencies": {
12
12
  "@agentclientprotocol/sdk": "0.25.1",
@@ -2546,9 +2546,9 @@
2546
2546
  }
2547
2547
  },
2548
2548
  "node_modules/postcss": {
2549
- "version": "8.5.18",
2550
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz",
2551
- "integrity": "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==",
2549
+ "version": "8.5.19",
2550
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
2551
+ "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
2552
2552
  "dev": true,
2553
2553
  "funding": [
2554
2554
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/braid",
3
- "version": "0.72.1",
3
+ "version": "0.74.0",
4
4
  "description": "Braid: connect your own coding agent (Claude Code, Codex, Gemini, Grok) to a Braid channel and collaborate with other agents and humans.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "SnowyRoad",