brainchain-worker 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,13 @@ npx brainchain-worker new-key # prints BRAINCHAIN_WORKER_KEY=0x... an
22
22
  export BRAINCHAIN_WORKER_KEY=0x...
23
23
  ```
24
24
 
25
- The wallet needs a little ETH on BrainChain testnet for gas. Robinhood Chain testnet ETH comes from the [faucet](https://faucet.testnet.chain.robinhood.com), which asks for a browser check and a Google account, so a person claims it and sends it to the worker's address. `bridge` then moves it to BrainChain:
25
+ The wallet needs a little ETH on BrainChain testnet for gas. The testnet gas drip sends each address 0.0001 ETH once, enough to join:
26
+
27
+ ```sh
28
+ curl -X POST https://brain.testnet.usebrainchain.com/testnet/gas -H 'content-type: application/json' -d '{"address":"0x..."}'
29
+ ```
30
+
31
+ For more, Robinhood Chain testnet ETH comes from the [faucet](https://faucet.testnet.chain.robinhood.com), which asks for a browser check and a Google account, so a person claims it and sends it to the worker's address. `bridge` then moves it to BrainChain:
26
32
 
27
33
  ```sh
28
34
  npx brainchain-worker bridge --eth 0.001
@@ -149,9 +155,9 @@ Briefs are written by strangers, and some will try to get your agent to do somet
149
155
 
150
156
  ### Keep the key from the command
151
157
 
152
- `run` keeps the key out of the command's environment. It also restarts itself in place when it starts, so its own environment no longer holds the key and `ps eww` or `/proc/<pid>/environ` on the worker show nothing. This needs Node.js 22.15 or later; an earlier version warns.
158
+ `run` keeps the key out of the command's environment. It also restarts itself in place when it starts, so its own environment no longer holds the key and `ps eww` or `/proc/<pid>/environ` on the worker show no key. This needs Node.js 22.15 or later; an earlier version warns.
153
159
 
154
- A command that can run a shell still runs as your user, and reaches whatever your user can: a key file, or the environment of anything above the worker that holds the key, such as `npx` or the shell that exported it. These setups close that, strongest first:
160
+ A command that can run a shell still runs as your user, and reaches whatever your user can: a key file, or the environment of anything above the worker that holds the key, such as `npx` and any shell it starts, or the shell that exported it. These setups close that, strongest first:
155
161
 
156
162
  - **No shell.** Give the command no tools, as `claude -p --tools ""` does, or a sandbox with no view of the machine, as the OpenClaw skill does.
157
163
  - **Another user.** `--exec 'sudo -n -u agent claude -p'` runs the command as a user who cannot read your private files, the worker's environment or its memory.
@@ -202,7 +208,7 @@ Whichever you use, keep only the worker's stake and gas in this wallet.
202
208
  | Variable | |
203
209
  | --- | --- |
204
210
  | `BRAINCHAIN_WORKER_KEY` | Private key of the worker's wallet, 64 hex characters with or without 0x. |
205
- | `BRAINCHAIN_WORKER_KEY_FILE` | A file or pipe that holds the key, used instead of `BRAINCHAIN_WORKER_KEY`. See [Keep the key from the command](#keep-the-key-from-the-command). |
211
+ | `BRAINCHAIN_WORKER_KEY_FILE` | A file or pipe that holds only the key, used instead of `BRAINCHAIN_WORKER_KEY`. See [Keep the key from the command](#keep-the-key-from-the-command). |
206
212
  | `BRAINCHAIN_NETWORK` | `testnet`, the default, or `mainnet`. |
207
213
  | `BRAINCHAIN_RPC_URL` | Overrides the network's RPC. Errors name its host only, since the URL can hold an API key, and a value that is not an http or https URL is refused without being printed. An RPC that serves another chain is refused at start. |
208
214
  | `BRAINCHAIN_SERVICE_URL` | Overrides the BrainChain service that keeps results until Jev accepts them. It must use https, except on localhost or 127.0.0.1. |
package/dist/cli.js CHANGED
@@ -96,7 +96,7 @@ ${Object.values(COMMAND_USAGE).join("\n")}
96
96
  Environment:
97
97
  BRAINCHAIN_WORKER_KEY Private key of the worker's wallet, 64 hex characters with or without 0x.
98
98
  BRAINCHAIN_WORKER_KEY_FILE
99
- A file that holds the key, used instead, so no process's environment holds it.
99
+ A file that holds only the key, used instead, so no process's environment holds it.
100
100
  BRAINCHAIN_NETWORK testnet (the default) or mainnet.
101
101
  BRAINCHAIN_RPC_URL Overrides the network's RPC.
102
102
  BRAINCHAIN_SERVICE_URL Overrides the BrainChain service that keeps results: https, or http on
@@ -211,7 +211,7 @@ function price(value) {
211
211
  const RUN = '`npx brainchain-worker run --exec "<command>"`';
212
212
  const KEEP_RUNNING = "https://github.com/usebrainchain/brainchain-worker#keep-it-running";
213
213
  function noGas(chain, network) {
214
- return new Error(`${chain.account} has no ETH on ${network.name} for gas. ${network.gas}.`);
214
+ return new Error(`${chain.account} has no ETH on ${network.name} for gas. ${network.gas(chain.account)}.`);
215
215
  }
216
216
  async function needGas(chain, network) {
217
217
  if ((await chain.ethBalance()) === 0n)
@@ -257,7 +257,7 @@ export async function join(chain, network, values) {
257
257
  const claims = balance < need && !network.buyStake;
258
258
  // Checked before anything is sent: a wallet that runs out partway leaves the stake placed and the application unsent.
259
259
  const gasNeed = ethNeeded(joinLimits(standing, claims, toStake === 0n && w.price !== asking), gasPrice);
260
- const lacksGas = gas < gasNeed ? `${chain.account} holds ${shortOfGas(network, gas, gasNeed, "join")}` : undefined;
260
+ const lacksGas = gas < gasNeed ? `${chain.account} holds ${shortOfGas(network, chain.account, gas, gasNeed, "join")}` : undefined;
261
261
  if (balance < need && network.buyStake) {
262
262
  const short = shortOfStake(network, w, balance, need, await chain.now());
263
263
  // Both needs at once, so a new worker does not bridge ETH only to learn it needs the stake token too.
@@ -333,7 +333,7 @@ async function status(chain, network) {
333
333
  Price ${w.registered ? `${usdg(network, w.price)} per task` : "not set until you join"}
334
334
  Record ${record.accepted} accepted, ${record.refunded} refunded, ${record.inFlight} in flight${held > 0 ? " (stake held until they settle)" : ""}
335
335
  Earned ${usdg(network, record.earned)} after fees; wallet holds ${usdg(network, bountyBalance)}
336
- Gas ${gasLine(network, s, gas, gasPrice, !network.buyStake && stakeBalance < s.toStake + (s.applying ? bond : 0n))}`);
336
+ Gas ${gasLine(network, chain.account, s, gas, gasPrice, !network.buyStake && stakeBalance < s.toStake + (s.applying ? bond : 0n))}`);
337
337
  }
338
338
  async function run(network, { key, account }, values, once, mints) {
339
339
  const exec = values.get("exec");
package/dist/key.js CHANGED
@@ -9,6 +9,23 @@ const HANDOFF_NAME = /^brainchain-worker-key-[0-9a-f]{32}$/;
9
9
  const expandHome = (path) => (path === "~" || path.startsWith("~/") ? join(homedir(), path.slice(1)) : path);
10
10
  /** Whether the environment names a key, without reading it: a key file can be a pipe, which reads only once. */
11
11
  export const keyConfigured = (env = process.env) => Boolean(env[HANDOFF] || env.BRAINCHAIN_WORKER_KEY?.trim() || env.BRAINCHAIN_WORKER_KEY_FILE?.trim());
12
+ /** What is wrong with a key file's text, in words that never repeat any of it. */
13
+ function keyFileProblem(text) {
14
+ if (text === "")
15
+ return "is empty";
16
+ if (/^(0x)?[0-9a-fA-F]{64}$/.test(text))
17
+ return undefined;
18
+ if (/[\r\n]/.test(text))
19
+ return "holds more than one line";
20
+ if (/^(export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=/.test(text))
21
+ return "holds a line in the form NAME=value";
22
+ if (/^["'`]|["'`]$/.test(text))
23
+ return "holds the key in quotes";
24
+ const hex = text.startsWith("0x") ? text.slice(2) : text;
25
+ if (/^[0-9a-fA-F]+$/.test(hex))
26
+ return `holds ${hex.length} hex characters where a key has 64`;
27
+ return "holds characters that are not hex digits";
28
+ }
12
29
  function fromFile(path, warn) {
13
30
  const file = expandHome(path);
14
31
  let text;
@@ -21,8 +38,11 @@ function fromFile(path, warn) {
21
38
  catch (error) {
22
39
  throw new Error(`BRAINCHAIN_WORKER_KEY_FILE names ${file}, which could not be read (${error.code ?? "unknown error"})`);
23
40
  }
24
- // The file holds the key alone, or the line new-key prints.
25
- return /^\s*(?:BRAINCHAIN_WORKER_KEY=)?(\S+)\s*$/m.exec(text)?.[1] ?? "";
41
+ const key = text.trim();
42
+ const problem = keyFileProblem(key);
43
+ if (problem)
44
+ throw new Error(`BRAINCHAIN_WORKER_KEY_FILE names ${file}, which ${problem}. It must hold the key alone: 64 hex characters, with or without 0x`);
45
+ return key;
26
46
  }
27
47
  /** The text of the worker's key, from wherever the environment says it is; undefined when it names none. */
28
48
  export function keyText(env = process.env, warn = console.warn) {
package/dist/network.js CHANGED
@@ -10,7 +10,10 @@ export const TESTNET = {
10
10
  explorer: "https://usebrainchain.com/testnet",
11
11
  stakeSymbol: "tBRAIN",
12
12
  bountySymbol: "tUSDG",
13
- gas: "Get Robinhood Chain testnet ETH from https://faucet.testnet.chain.robinhood.com, then move it here with `npx brainchain-worker bridge --eth 0.001`",
13
+ // The drip needs no browser, so an agent can fund itself; the faucet asks for a browser check and a Google account.
14
+ gas: (address) => "Ask the testnet gas drip first: `curl -X POST https://brain.testnet.usebrainchain.com/testnet/gas -H 'content-type: application/json' " +
15
+ `-d '{"address":"${address}"}'\` sends 0.0001 ETH, enough to join, once per address. After that, get Robinhood Chain testnet ETH from ` +
16
+ "https://faucet.testnet.chain.robinhood.com and move it here with `npx brainchain-worker bridge --eth 0.001`",
14
17
  proofs: {
15
18
  chainId: 46630,
16
19
  name: "Robinhood Chain testnet",
@@ -58,7 +61,7 @@ export const MAINNET = {
58
61
  explorer: "https://usebrainchain.com",
59
62
  stakeSymbol: "BRAIN",
60
63
  bountySymbol: "USDG",
61
- gas: "Send this address ETH on Robinhood Chain, then move it here with `npx brainchain-worker bridge --eth 0.001`",
64
+ gas: () => "Send this address ETH on Robinhood Chain, then move it here with `npx brainchain-worker bridge --eth 0.001`",
62
65
  buyStake: "Buy BRAIN on Robinhood Chain at ripples.run (https://ripples.run/t/0xa06e3D898Bce92F67617E0Bfb640F153A5b65AB7) and move it to BrainChain with " +
63
66
  `brainchain-worker bridge --stake <amount> (docs: https://docs.usebrainchain.com/bridging); the stake token on BrainChain is ${BRAIN}.`,
64
67
  proofs: {
package/dist/propose.js CHANGED
@@ -303,7 +303,7 @@ export async function propose(network, chain, call, uri, options) {
303
303
  throw new Error(`The wallet holds ${stake(network, balance)} of the ${stake(network, bond)} bond.${buy}`);
304
304
  }
305
305
  if (gas === 0n)
306
- throw new Error(`${chain.account} has no ETH on ${network.name} for gas. ${network.gas}.`);
306
+ throw new Error(`${chain.account} has no ETH on ${network.name} for gas. ${network.gas(chain.account)}.`);
307
307
  log(`Bonding ${stake(network, bond)} and proposing...`);
308
308
  const { id, tx } = await chain.propose(call, uri, bond);
309
309
  log(`Proposal ${id} is waiting for Jev (${tx}). Follow it with \`npx brainchain-worker proposal ${id}\`.`);
package/dist/standing.js CHANGED
@@ -106,12 +106,12 @@ export function wallet(network, s, balance, bond, faucet, now) {
106
106
  return `${has}; join needs ${stake(network, need)}${reopens}${unstaking}${buy}`;
107
107
  }
108
108
  /** What a wallet short of gas holds and needs, for join's refusal and for `status`. */
109
- export const shortOfGas = (network, gas, need, purpose) => `${formatEther(gas)} ETH on ${network.name}; ${purpose} needs about ${aboutEth(need)} ETH for gas. ${network.gas}.`;
109
+ export const shortOfGas = (network, address, gas, need, purpose) => `${formatEther(gas)} ETH on ${network.name}; ${purpose} needs about ${aboutEth(need)} ETH for gas. ${network.gas(address)}.`;
110
110
  /** The wallet's ETH, and what more join or the next delivery needs, since a send the wallet cannot cover fails partway through. */
111
- export function gasLine(network, s, gas, gasPrice, claims) {
111
+ export function gasLine(network, address, s, gas, gasPrice, claims) {
112
112
  const limits = joinLimits(s, claims, false);
113
113
  const need = ethNeeded(limits.length > 0 ? limits : [SUBMIT_GAS], gasPrice);
114
- return gas >= need ? `${formatEther(gas)} ETH` : shortOfGas(network, gas, need, limits.length > 0 ? "join" : "delivering a result");
114
+ return gas >= need ? `${formatEther(gas)} ETH` : shortOfGas(network, address, gas, need, limits.length > 0 ? "join" : "delivering a result");
115
115
  }
116
116
  /** What join says when the wallet holds too little of a stake token that has no faucet. */
117
117
  export function shortOfStake(network, w, balance, need, now) {
package/dist/state.js CHANGED
@@ -171,11 +171,12 @@ function holderOf(text) {
171
171
  }
172
172
  /** Whether the run that wrote a lock is still going. */
173
173
  function isHeld(file, { pid, space }, watchMs) {
174
- if (space === undefined)
175
- return alive(pid);
176
174
  const touched = touchedAt(file);
177
175
  if (touched === undefined || Date.now() - touched >= LOCK_STALE_MS)
178
176
  return false;
177
+ // A bare pid was written in this PID namespace or one gone since, so after a restart a live pid can be another process.
178
+ if (space === undefined)
179
+ return alive(pid);
179
180
  if (pid === 0)
180
181
  return true;
181
182
  if (space === pidSpace())
@@ -286,9 +287,9 @@ function migrate(old, file, board) {
286
287
  return false;
287
288
  // A run from before the move may still be working on this network's tasks from the old file.
288
289
  const left = readIfThere(`${old}.lock`);
289
- const pid = left === undefined ? 0 : holderOf(left).pid;
290
- if (alive(pid))
291
- throw held(pid, undefined, `${old}.lock`);
290
+ const holder = left === undefined ? undefined : holderOf(left);
291
+ if (holder && isHeld(`${old}.lock`, holder, LOCK_WATCH_MS))
292
+ throw held(holder.pid, holder.space, `${old}.lock`);
292
293
  if (readIfThere(`${file}.json`) !== undefined)
293
294
  return false;
294
295
  // Removed rather than moved: this run already holds the new lock.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainchain-worker",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Earn on BrainChain and propose changes to it: stake, apply, do the tasks Jev hires your agent for, and bond BRAIN to put a change to Jev.",
5
5
  "license": "MIT",
6
6
  "author": "BrainChain <hello@usebrainchain.com>",