@divance/rrr 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/dist/rrr.js +74 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,12 +4,19 @@ Runs AI agents, records each run and its handoff on an rrrrr server, and fires p
4
4
 
5
5
  ```bash
6
6
  npm i -g @divance/rrr
7
- rrr login # paste a runner token; the hosted server is the default
7
+ rrr login # paste a runner token (minted with your pops' secrets); the hosted server is the default
8
8
  rrr agent list --config <config repo>
9
9
  rrr run <agent> --origin <ref> # or --chain <ch_id> to continue a chain
10
10
  ```
11
11
 
12
12
  `RRR_SERVER` / `--server` point at another server, `RRR_TOKEN` overrides the saved token, `RRR_CONFIG` sets the config repo.
13
+ In CI, pass the token only as `RRR_TOKEN` and don't `rrr login` (no credentials file). The agent runs in a temp
14
+ dir with an env allowlist. Read and sandboxed Bash are denied `~/.config/rrr`, rrr's cwd and your shell
15
+ startup files (`~/.bashrc`, `~/.bash_profile`, `~/.bash_login`, `~/.profile`, `~/.zshrc`, `~/.zshenv`, `~/.zprofile`,
16
+ `~/.zlogin`), and `RRR_TOKEN` is unset in Bash. Other variables your shell rc exports still reach Bash. MCP servers in
17
+ run.yaml run outside the sandbox with your file access (trusted config). Where Claude's sandbox can't start
18
+ (e.g. no bubblewrap), Bash is not sandboxed and only the Read rules hold. The real boundary is a container
19
+ ([design](https://github.com/divanc/rrrrr/blob/main/docs/design.md#harness)).
13
20
  The CLI is one bundled file (`dist/rrr.js`); it needs Node 22+ and Claude Code on PATH.
14
21
 
15
22
  Design: https://github.com/divanc/rrrrr/blob/main/docs/design.md
package/dist/rrr.js CHANGED
@@ -15623,8 +15623,8 @@ var program = new Command;
15623
15623
 
15624
15624
  // src/domain/run/index.ts
15625
15625
  import { execFileSync } from "node:child_process";
15626
- import { existsSync as existsSync5, mkdtempSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
15627
- import { hostname, tmpdir, userInfo } from "node:os";
15626
+ import { existsSync as existsSync5, mkdtempSync, readFileSync as readFileSync4, realpathSync, writeFileSync } from "node:fs";
15627
+ import { homedir, hostname, tmpdir, userInfo } from "node:os";
15628
15628
  import { join as join5 } from "node:path";
15629
15629
  // ../shared/src/ids.ts
15630
15630
  var ID_RE = /^(ch|rn|tk)_[0-9a-hjkmnp-tv-z]{10}$/;
@@ -15688,7 +15688,7 @@ function validateHandoff(handoff, agentSchema) {
15688
15688
  }
15689
15689
  // src/config.ts
15690
15690
  import { existsSync, readFileSync } from "node:fs";
15691
- import { join } from "node:path";
15691
+ import { isAbsolute, join, relative, sep } from "node:path";
15692
15692
 
15693
15693
  // ../../node_modules/.bun/yaml@2.9.1/node_modules/yaml/dist/index.js
15694
15694
  var composer = require_composer();
@@ -15764,10 +15764,17 @@ function envWithoutToken() {
15764
15764
  delete env.RRR_TOKEN;
15765
15765
  return env;
15766
15766
  }
15767
+ function isInside(dir, root) {
15768
+ const path2 = relative(root, dir);
15769
+ return path2 === "" || path2.split(sep)[0] !== ".." && !isAbsolute(path2);
15770
+ }
15771
+ function pickEnv(names) {
15772
+ return Object.fromEntries(names.filter((name) => process.env[name] !== undefined).map((name) => [name, process.env[name]]));
15773
+ }
15767
15774
 
15768
15775
  // src/domain/agent/index.ts
15769
15776
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
15770
- import { join as join2, relative } from "node:path";
15777
+ import { join as join2, relative as relative2 } from "node:path";
15771
15778
  var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
15772
15779
  function list(configDir) {
15773
15780
  return names(configDir).map((name) => {
@@ -15785,7 +15792,7 @@ function names(configDir) {
15785
15792
  const found = [];
15786
15793
  for (const entry of readdirSync(root, { recursive: true, withFileTypes: true })) {
15787
15794
  if (entry.isFile() && entry.name === "run.yaml") {
15788
- found.push(relative(root, entry.parentPath));
15795
+ found.push(relative2(root, entry.parentPath));
15789
15796
  }
15790
15797
  }
15791
15798
  return found.sort();
@@ -15816,6 +15823,10 @@ function load(configDir, agentName) {
15816
15823
  throw problem("mcp", "a list of server names");
15817
15824
  if (settings.tools !== undefined && !isStringList(settings.tools))
15818
15825
  throw problem("tools", "a list of tool names");
15826
+ if (settings.env !== undefined && !isStringList(settings.env))
15827
+ throw problem("env", "a list of env var names");
15828
+ if (settings.env?.includes("RRR_TOKEN"))
15829
+ throw problem("env", "without RRR_TOKEN (the agent never gets the runner token)");
15819
15830
  const promptFile = join2(dir, "prompt.md");
15820
15831
  if (!existsSync2(promptFile))
15821
15832
  throw new Error(`${name}: missing ${promptFile}`);
@@ -15833,6 +15844,7 @@ function load(configDir, agentName) {
15833
15844
  timeoutMs: parseDuration(settings.timeout),
15834
15845
  mcp,
15835
15846
  tools: settings.tools ?? [],
15847
+ env: settings.env ?? [],
15836
15848
  promptFile,
15837
15849
  schema: schema2,
15838
15850
  mcpConfig: agentMcpConfig(configDir, name, mcp)
@@ -16165,7 +16177,11 @@ async function execute(deps, agentName, options) {
16165
16177
  const agent = load(options.configDir, agentName);
16166
16178
  if (options.pop)
16167
16179
  checkConfig(options.configDir);
16168
- const dir = mkdtempSync(join5(tmpdir(), "rrr-"));
16180
+ const denyRead = deniedCwd();
16181
+ if (denyRead.length > 0 && isInside(realpathSync(tmpdir()), denyRead[0])) {
16182
+ throw new Error(`the temp dir ${tmpdir()} is inside the current directory, which the agent may not read: set TMPDIR outside it`);
16183
+ }
16184
+ const dir = realpathSync(mkdtempSync(join5(tmpdir(), "rrr-")));
16169
16185
  let mcpConfigFile;
16170
16186
  if (agent.mcpConfig) {
16171
16187
  mcpConfigFile = join5(dir, "mcp.json");
@@ -16186,6 +16202,7 @@ async function execute(deps, agentName, options) {
16186
16202
  agent,
16187
16203
  mode: options.interactive ? "interactive" : agent.mode,
16188
16204
  dir,
16205
+ denyRead,
16189
16206
  handoffPath: join5(dir, "handoff.json"),
16190
16207
  mcpConfigFile,
16191
16208
  runId: start.run_id,
@@ -16213,8 +16230,9 @@ async function startAndStream(deps, run) {
16213
16230
  tools: agent.tools,
16214
16231
  mcp: agent.mcp,
16215
16232
  mcpConfigFile: run.mcpConfigFile,
16216
- handoffDir: run.dir,
16217
- env: { ...envWithoutToken(), RRR_HANDOFF: handoffPath, RRR_RUN_ID: run.runId, RRR_CHAIN_ID: run.chainId },
16233
+ cwd: run.dir,
16234
+ denyRead: run.denyRead,
16235
+ env: { ...pickEnv(agent.env), RRR_HANDOFF: handoffPath, RRR_RUN_ID: run.runId, RRR_CHAIN_ID: run.chainId },
16218
16236
  timeoutMs: mode === "headless" ? agent.timeoutMs : undefined
16219
16237
  };
16220
16238
  const events = createUploader(deps.sdk, run.runId, deps.log);
@@ -16308,6 +16326,10 @@ async function ignoringSignals(fn, onSignal = () => {}) {
16308
16326
  function describe(handoff) {
16309
16327
  return handoff.status === "done" ? `done/${handoff.decision}` : "blocked";
16310
16328
  }
16329
+ function deniedCwd() {
16330
+ const cwd = process.cwd();
16331
+ return isInside(realpathSync(homedir()), cwd) ? [] : [cwd];
16332
+ }
16311
16333
  function gitSha(dir) {
16312
16334
  try {
16313
16335
  return execFileSync("git", ["-C", dir, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
@@ -16328,12 +16350,12 @@ import { createInterface } from "node:readline";
16328
16350
 
16329
16351
  // src/harness/transcript.ts
16330
16352
  import { existsSync as existsSync6, openSync, readSync, closeSync, readdirSync as readdirSync3, statSync } from "node:fs";
16331
- import { homedir } from "node:os";
16353
+ import { homedir as homedir2 } from "node:os";
16332
16354
  import { join as join6 } from "node:path";
16333
16355
  import { StringDecoder } from "node:string_decoder";
16334
16356
  import { setTimeout as sleep } from "node:timers/promises";
16335
16357
  function projectsDir() {
16336
- return join6(process.env.CLAUDE_CONFIG_DIR ?? join6(homedir(), ".claude"), "projects");
16358
+ return join6(process.env.CLAUDE_CONFIG_DIR ?? join6(homedir2(), ".claude"), "projects");
16337
16359
  }
16338
16360
  function projectSlug(cwd) {
16339
16361
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -16393,6 +16415,23 @@ async function* tailTranscript(cwd, sessionId, startOffset, isDone) {
16393
16415
  function claudeBin() {
16394
16416
  return process.env.RRR_CLAUDE_BIN ?? "claude";
16395
16417
  }
16418
+ var CLAUDE_ENV = [
16419
+ "PATH",
16420
+ "HOME",
16421
+ "USER",
16422
+ "SHELL",
16423
+ "TERM",
16424
+ "LANG",
16425
+ "TMPDIR",
16426
+ "CLAUDE_CONFIG_DIR",
16427
+ "ANTHROPIC_API_KEY",
16428
+ "ANTHROPIC_AUTH_TOKEN",
16429
+ "ANTHROPIC_BASE_URL",
16430
+ "CLAUDE_CODE_OAUTH_TOKEN"
16431
+ ];
16432
+ function claudeEnv(options) {
16433
+ return { ...pickEnv(CLAUDE_ENV), ...options.env };
16434
+ }
16396
16435
  function claudeArgs(options) {
16397
16436
  const tools = options.mcp.length > 0 && !options.tools.includes("ToolSearch") ? [...options.tools, "ToolSearch"] : options.tools;
16398
16437
  const args = ["--setting-sources", "", "--tools", tools.join(","), "--append-system-prompt-file", options.promptFile];
@@ -16407,9 +16446,27 @@ function claudeArgs(options) {
16407
16446
  const allowed = [...tools, ...options.mcp.map((name) => `mcp__${name}`)];
16408
16447
  if (allowed.length > 0)
16409
16448
  args.push("--allowedTools", allowed.join(","));
16410
- args.push("--add-dir", options.handoffDir);
16449
+ args.push("--settings", claudeSettings(options));
16411
16450
  return args;
16412
16451
  }
16452
+ var SHELL_STARTUP_FILES = ["~/.bashrc", "~/.bash_profile", "~/.bash_login", "~/.profile", "~/.zshrc", "~/.zshenv", "~/.zprofile", "~/.zlogin"];
16453
+ function claudeSettings(options) {
16454
+ const deny = [
16455
+ "Read(~/.config/rrr/**)",
16456
+ ...SHELL_STARTUP_FILES.map((file) => `Read(${file})`),
16457
+ ...options.denyRead.map((dir) => `Read(/${dir.replace(/[\\*?[\]]/g, "\\$&")}/**)`)
16458
+ ];
16459
+ return JSON.stringify({
16460
+ permissions: { deny },
16461
+ sandbox: {
16462
+ enabled: true,
16463
+ allowUnsandboxedCommands: false,
16464
+ filesystem: { denyRead: options.denyRead },
16465
+ credentials: { envVars: [{ name: "RRR_TOKEN", mode: "deny" }] },
16466
+ network: { allowedDomains: ["*"] }
16467
+ }
16468
+ });
16469
+ }
16413
16470
  function headlessArgs(options) {
16414
16471
  return ["-p", "--output-format", "stream-json", "--verbose", ...claudeArgs(options)];
16415
16472
  }
@@ -16425,7 +16482,7 @@ var claudeCode = {
16425
16482
  }
16426
16483
  };
16427
16484
  function startHeadless(options) {
16428
- const { child, exit, stop } = spawnGroup(claudeBin(), headlessArgs(options), { env: options.env, stdio: ["pipe", "pipe", "inherit"] }, options.timeoutMs);
16485
+ const { child, exit, stop } = spawnGroup(claudeBin(), headlessArgs(options), { cwd: options.cwd, env: claudeEnv(options), stdio: ["pipe", "pipe", "inherit"] }, options.timeoutMs);
16429
16486
  child.stdin.on("error", () => {});
16430
16487
  child.stdin.end(options.message);
16431
16488
  const lines = createInterface({ input: child.stdout });
@@ -16436,8 +16493,8 @@ function startHeadless(options) {
16436
16493
  return { lines, exit: exited, stop };
16437
16494
  }
16438
16495
  function startInteractive(sessionId, args, options) {
16439
- const startOffset = fileSize(findTranscript(process.cwd(), sessionId));
16440
- const child = spawn2(claudeBin(), args, { env: options.env, stdio: "inherit" });
16496
+ const startOffset = fileSize(findTranscript(options.cwd, sessionId));
16497
+ const child = spawn2(claudeBin(), args, { cwd: options.cwd, env: claudeEnv(options), stdio: "inherit" });
16441
16498
  let exited = false;
16442
16499
  const exit = new Promise((resolve2) => {
16443
16500
  child.on("error", (error) => {
@@ -16449,7 +16506,7 @@ function startInteractive(sessionId, args, options) {
16449
16506
  resolve2({ code });
16450
16507
  });
16451
16508
  });
16452
- const lines = tailTranscript(process.cwd(), sessionId, startOffset, () => exited);
16509
+ const lines = tailTranscript(options.cwd, sessionId, startOffset, () => exited);
16453
16510
  return { sessionId, lines, exit, stop: () => child.kill("SIGTERM") };
16454
16511
  }
16455
16512
  // ../sdk/src/index.ts
@@ -16497,11 +16554,11 @@ function createRunnerSdk(options) {
16497
16554
 
16498
16555
  // src/cli/credentials.ts
16499
16556
  import { chmodSync, existsSync as existsSync7, mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
16500
- import { homedir as homedir2 } from "node:os";
16557
+ import { homedir as homedir3 } from "node:os";
16501
16558
  import { dirname, join as join7 } from "node:path";
16502
16559
  var DEFAULT_SERVER = "https://rrrrr.me-4de.workers.dev";
16503
16560
  function credentialsFile() {
16504
- return join7(process.env.HOME || homedir2(), ".config", "rrr", "credentials.json");
16561
+ return join7(process.env.HOME || homedir3(), ".config", "rrr", "credentials.json");
16505
16562
  }
16506
16563
  function loadCredentials() {
16507
16564
  const file = credentialsFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divance/rrr",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "rrr: run AI agents, record their runs and handoffs, fire pops",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",