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 +10 -4
- package/dist/cli.js +4 -4
- package/dist/key.js +22 -2
- package/dist/network.js +5 -2
- package/dist/propose.js +1 -1
- package/dist/standing.js +3 -3
- package/dist/state.js +6 -5
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
290
|
-
if (
|
|
291
|
-
throw held(pid,
|
|
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.
|
|
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>",
|