@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.
- package/README.md +8 -1
- package/dist/rrr.js +74 -17
- 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(
|
|
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
|
|
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
|
-
|
|
16217
|
-
|
|
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(
|
|
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("--
|
|
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), {
|
|
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(
|
|
16440
|
-
const child = spawn2(claudeBin(), args, {
|
|
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(
|
|
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
|
|
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 ||
|
|
16561
|
+
return join7(process.env.HOME || homedir3(), ".config", "rrr", "credentials.json");
|
|
16505
16562
|
}
|
|
16506
16563
|
function loadCredentials() {
|
|
16507
16564
|
const file = credentialsFile();
|