claude-yes 1.156.0 → 1.158.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.
- package/dist/SUPPORTED_CLIS-BSuNAuIv.js +8 -0
- package/dist/{SUPPORTED_CLIS-BrOIMMzT.js → SUPPORTED_CLIS-CeyKUwT5.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{schedule-DLMYKfDJ.js → schedule-BVKN6cO5.js} +5 -5
- package/dist/{serve-BXUK9_Ex.js → serve-Dij1y--H.js} +59 -14
- package/dist/{setup-BXIWWZAP.js → setup-DymiMHke.js} +3 -3
- package/dist/{subcommands-DTWNUJAa.js → subcommands-COLw05N-.js} +4 -4
- package/dist/{subcommands-bpbzbGmp.js → subcommands-CyKom1IU.js} +1 -1
- package/dist/{ts-CLHiUE0V.js → ts-IAAUoBpk.js} +2 -2
- package/dist/{versionChecker-C4EICUab.js → versionChecker-Dwe07Fe6.js} +2 -2
- package/dist/{workspaceConfig-DT3cCBZK.js → workspaceConfig-B0Q9-q2B.js} +38 -3
- package/lab/ui/console-logic.js +48 -0
- package/lab/ui/index.html +36 -2
- package/package.json +1 -1
- package/ts/serve.ts +84 -4
- package/ts/workspaceConfig.spec.ts +42 -1
- package/ts/workspaceConfig.ts +46 -1
- package/dist/SUPPORTED_CLIS-DmRDiLen.js +0 -8
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./ts-IAAUoBpk.js";
|
|
2
|
+
import "./logger-CDIsZ-Pp.js";
|
|
3
|
+
import "./versionChecker-Dwe07Fe6.js";
|
|
4
|
+
import "./pidStore-fqXqTKkh.js";
|
|
5
|
+
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CeyKUwT5.js";
|
|
7
|
+
|
|
8
|
+
export { SUPPORTED_CLIS };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-IAAUoBpk.js";
|
|
2
2
|
|
|
3
3
|
//#region ts/SUPPORTED_CLIS.ts
|
|
4
4
|
const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
5
5
|
|
|
6
6
|
//#endregion
|
|
7
7
|
export { SUPPORTED_CLIS as t };
|
|
8
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
8
|
+
//# sourceMappingURL=SUPPORTED_CLIS-CeyKUwT5.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
|
|
3
3
|
import { n as logger } from "./logger-CDIsZ-Pp.js";
|
|
4
|
-
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-
|
|
4
|
+
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-Dwe07Fe6.js";
|
|
5
5
|
import { argv } from "process";
|
|
6
6
|
import { execFileSync, spawn } from "child_process";
|
|
7
7
|
import ms from "ms";
|
|
@@ -481,7 +481,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
481
481
|
const rawArg = process.argv[2];
|
|
482
482
|
const managerCommands = !invokedCliName(process.argv);
|
|
483
483
|
const isHelpFlag = rawArg === "-h" || rawArg === "--help";
|
|
484
|
-
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-
|
|
484
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-CyKom1IU.js");
|
|
485
485
|
if (isHelpFlag && process.argv.length === 3) {
|
|
486
486
|
cmdHelp(managerCommands);
|
|
487
487
|
process.exit(0);
|
|
@@ -514,7 +514,7 @@ if (config.useRust) {
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
if (rustBinary) {
|
|
517
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
517
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-BSuNAuIv.js");
|
|
518
518
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
519
519
|
if (config.verbose) {
|
|
520
520
|
console.log(`[rust] Using binary: ${rustBinary}`);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-IAAUoBpk.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-Dwe07Fe6.js";
|
|
4
4
|
import "./pidStore-fqXqTKkh.js";
|
|
5
5
|
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
6
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-IAAUoBpk.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-Dwe07Fe6.js";
|
|
4
4
|
import "./pidStore-fqXqTKkh.js";
|
|
5
5
|
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
7
|
-
import {
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CeyKUwT5.js";
|
|
7
|
+
import { o as resolveSpawnCwd } from "./workspaceConfig-B0Q9-q2B.js";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
|
|
10
10
|
//#region ts/oxmgrService.ts
|
|
@@ -141,4 +141,4 @@ async function cmdSchedule(rest) {
|
|
|
141
141
|
|
|
142
142
|
//#endregion
|
|
143
143
|
export { cmdSchedule };
|
|
144
|
-
//# sourceMappingURL=schedule-
|
|
144
|
+
//# sourceMappingURL=schedule-BVKN6cO5.js.map
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-IAAUoBpk.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
3
|
+
import { r as getInstalledPackage } from "./versionChecker-Dwe07Fe6.js";
|
|
4
4
|
import "./pidStore-fqXqTKkh.js";
|
|
5
5
|
import { a as updateGlobalPidStatus } from "./globalPidIndex-DlmmJlO8.js";
|
|
6
6
|
import { t as pgidForWrapper } from "./reaper-C-eWAxIj.js";
|
|
7
7
|
import "./configShared-CEyhl0WH.js";
|
|
8
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
8
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CeyKUwT5.js";
|
|
9
9
|
import "./e2e-Bfw7qL9O.js";
|
|
10
10
|
import "./webrtcLink-CBZkZ-LT.js";
|
|
11
11
|
import "./remotes-DBCvpp3B.js";
|
|
12
|
-
import { i as
|
|
13
|
-
import { E as writeToIpc, _ as readPtysize, b as resolveOne, c as extractTaskCounts, g as readNotes, i as controlCodeFromName, m as listRecords, o as deriveLiveStatus, v as renderRawLog, w as snapshotStatus } from "./subcommands-
|
|
12
|
+
import { a as isProvisionAllowed, i as hasSpawnHook, n as getSpawnHook, o as resolveSpawnCwd, t as getProvisionRoot } from "./workspaceConfig-B0Q9-q2B.js";
|
|
13
|
+
import { E as writeToIpc, _ as readPtysize, b as resolveOne, c as extractTaskCounts, g as readNotes, i as controlCodeFromName, m as listRecords, o as deriveLiveStatus, v as renderRawLog, w as snapshotStatus } from "./subcommands-COLw05N-.js";
|
|
14
14
|
import yargs from "yargs";
|
|
15
|
-
import { mkdir, open, readFile, stat, writeFile } from "fs/promises";
|
|
15
|
+
import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
|
|
16
16
|
import { homedir, hostname, userInfo } from "os";
|
|
17
17
|
import path from "path";
|
|
18
18
|
import { execFileSync } from "node:child_process";
|
|
@@ -789,6 +789,7 @@ Options:
|
|
|
789
789
|
return Response.json({ host: user ? `${user}@${host}` : host });
|
|
790
790
|
}
|
|
791
791
|
if (req.method === "GET" && p === "/api/version") return Response.json({ version: getInstalledPackage().version });
|
|
792
|
+
if (req.method === "GET" && p === "/api/spawn-config") return Response.json({ hasSpawnHook: hasSpawnHook() });
|
|
792
793
|
if (req.method === "GET" && p === "/api/notes") {
|
|
793
794
|
const notes = await readNotes();
|
|
794
795
|
return Response.json(Object.fromEntries(notes));
|
|
@@ -1154,15 +1155,59 @@ Options:
|
|
|
1154
1155
|
}
|
|
1155
1156
|
process.stderr.write(`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd}${provisioned ? `, ${provisioned.action}` : ""})\n`);
|
|
1156
1157
|
const ayBin = Bun.which("ay") ?? process.argv[1];
|
|
1157
|
-
const
|
|
1158
|
+
const agentArgv = [
|
|
1159
|
+
...process.platform === "win32" && ayBin.toLowerCase().endsWith(".exe") ? [ayBin] : [process.execPath, ayBin],
|
|
1160
|
+
cli,
|
|
1161
|
+
...prompt ? ["--", prompt] : []
|
|
1162
|
+
];
|
|
1163
|
+
const agentEnv = freshAgentEnv();
|
|
1158
1164
|
try {
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1165
|
+
const hook = getSpawnHook();
|
|
1166
|
+
if (hook) {
|
|
1167
|
+
const shell = process.env.AGENT_YES_SPAWN_SHELL?.trim() || "/bin/sh";
|
|
1168
|
+
const script = `set -e\n${hook}\nexec "$@"`;
|
|
1169
|
+
const errPath = path.join(agentYesHome(), `spawn-hook-${process.pid}-${performance.now().toString(36).replace(".", "")}.err`);
|
|
1170
|
+
const child = Bun.spawn([
|
|
1171
|
+
shell,
|
|
1172
|
+
"-c",
|
|
1173
|
+
script,
|
|
1174
|
+
"ay-spawn",
|
|
1175
|
+
...agentArgv
|
|
1176
|
+
], {
|
|
1177
|
+
cwd,
|
|
1178
|
+
env: {
|
|
1179
|
+
...agentEnv,
|
|
1180
|
+
AGENT_YES_CWD: cwd,
|
|
1181
|
+
AGENT_YES_CLI: cli
|
|
1182
|
+
},
|
|
1183
|
+
stdin: "ignore",
|
|
1184
|
+
stdout: "ignore",
|
|
1185
|
+
stderr: Bun.file(errPath)
|
|
1186
|
+
});
|
|
1187
|
+
const windowMs = Number(process.env.AGENT_YES_SPAWN_HOOK_TIMEOUT_MS) || 5e3;
|
|
1188
|
+
const exitCode = await Promise.race([child.exited, new Promise((r) => setTimeout(() => r(null), windowMs))]);
|
|
1189
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
1190
|
+
let detail = "";
|
|
1191
|
+
try {
|
|
1192
|
+
detail = (await Bun.file(errPath).text()).slice(0, 4096);
|
|
1193
|
+
} catch {}
|
|
1194
|
+
await unlink(errPath).catch(() => {});
|
|
1195
|
+
return new Response(`spawn hook failed (exit ${exitCode})${detail ? `:\n${detail.trimEnd()}` : ""}`, { status: 502 });
|
|
1196
|
+
}
|
|
1197
|
+
child.unref();
|
|
1198
|
+
setTimeout(() => void unlink(errPath).catch(() => {}), 6e4).unref?.();
|
|
1199
|
+
return Response.json({
|
|
1200
|
+
ok: true,
|
|
1201
|
+
pid: child.pid,
|
|
1202
|
+
cli,
|
|
1203
|
+
cwd,
|
|
1204
|
+
hook: true,
|
|
1205
|
+
...provisioned ? { provisioned } : {}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
const child = Bun.spawn(agentArgv, {
|
|
1164
1209
|
cwd,
|
|
1165
|
-
env:
|
|
1210
|
+
env: agentEnv,
|
|
1166
1211
|
stdin: "ignore",
|
|
1167
1212
|
stdout: "ignore",
|
|
1168
1213
|
stderr: "ignore"
|
|
@@ -1303,4 +1348,4 @@ Options:
|
|
|
1303
1348
|
|
|
1304
1349
|
//#endregion
|
|
1305
1350
|
export { cmdServe };
|
|
1306
|
-
//# sourceMappingURL=serve-
|
|
1351
|
+
//# sourceMappingURL=serve-Dij1y--H.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as getWorkspaceRoot, s as setWorkspaceRoot } from "./workspaceConfig-B0Q9-q2B.js";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { stdin, stdout } from "node:process";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
|
|
|
32
32
|
if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
|
|
33
33
|
if (noShare) return 0;
|
|
34
34
|
process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
|
|
35
|
-
const { cmdServe } = await import("./serve-
|
|
35
|
+
const { cmdServe } = await import("./serve-Dij1y--H.js");
|
|
36
36
|
return cmdServe([
|
|
37
37
|
"install",
|
|
38
38
|
"--share",
|
|
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
|
|
|
42
42
|
|
|
43
43
|
//#endregion
|
|
44
44
|
export { cmdSetup };
|
|
45
|
-
//# sourceMappingURL=setup-
|
|
45
|
+
//# sourceMappingURL=setup-DymiMHke.js.map
|
|
@@ -554,15 +554,15 @@ async function runSubcommand(argv) {
|
|
|
554
554
|
case "restart": return await cmdRestart(rest);
|
|
555
555
|
case "note": return await cmdNote(rest);
|
|
556
556
|
case "serve": {
|
|
557
|
-
const { cmdServe } = await import("./serve-
|
|
557
|
+
const { cmdServe } = await import("./serve-Dij1y--H.js");
|
|
558
558
|
return cmdServe(rest);
|
|
559
559
|
}
|
|
560
560
|
case "setup": {
|
|
561
|
-
const { cmdSetup } = await import("./setup-
|
|
561
|
+
const { cmdSetup } = await import("./setup-DymiMHke.js");
|
|
562
562
|
return cmdSetup(rest);
|
|
563
563
|
}
|
|
564
564
|
case "schedule": {
|
|
565
|
-
const { cmdSchedule } = await import("./schedule-
|
|
565
|
+
const { cmdSchedule } = await import("./schedule-BVKN6cO5.js");
|
|
566
566
|
return cmdSchedule(rest);
|
|
567
567
|
}
|
|
568
568
|
case "remote": {
|
|
@@ -2710,4 +2710,4 @@ async function cmdResultSet(rest) {
|
|
|
2710
2710
|
|
|
2711
2711
|
//#endregion
|
|
2712
2712
|
export { runSubcommand as C, writeToIpc as E, resolveResumeArgs as S, stopTipForCli as T, readPtysize as _, cursorAbs as a, resolveOne as b, extractTaskCounts as c, isExitRequest as d, isPidAlive as f, readNotes as g, matchKeyword as h, controlCodeFromName as i, finalizedLines as l, listRecords as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isSubcommand as p, cmdHelp as r, extractNeedsInput as s, GRACEFUL_EXIT_COMMANDS as t, isAgentStuck as u, renderRawLog as v, snapshotStatus as w, resolveReadWindow as x, renderRawLogLines as y };
|
|
2713
|
-
//# sourceMappingURL=subcommands-
|
|
2713
|
+
//# sourceMappingURL=subcommands-COLw05N-.js.map
|
|
@@ -4,6 +4,6 @@ import "./configShared-CEyhl0WH.js";
|
|
|
4
4
|
import "./e2e-Bfw7qL9O.js";
|
|
5
5
|
import "./webrtcLink-CBZkZ-LT.js";
|
|
6
6
|
import "./remotes-DBCvpp3B.js";
|
|
7
|
-
import { C as runSubcommand, E as writeToIpc, S as resolveResumeArgs, T as stopTipForCli, _ as readPtysize, a as cursorAbs, b as resolveOne, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLog, w as snapshotStatus, x as resolveReadWindow, y as renderRawLogLines } from "./subcommands-
|
|
7
|
+
import { C as runSubcommand, E as writeToIpc, S as resolveResumeArgs, T as stopTipForCli, _ as readPtysize, a as cursorAbs, b as resolveOne, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLog, w as snapshotStatus, x as resolveReadWindow, y as renderRawLogLines } from "./subcommands-COLw05N-.js";
|
|
8
8
|
|
|
9
9
|
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-Dwe07Fe6.js";
|
|
3
3
|
import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
|
|
4
4
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-V4qvXgAw.js";
|
|
5
5
|
import { t as PidStore } from "./pidStore-fqXqTKkh.js";
|
|
@@ -1802,4 +1802,4 @@ function sleep(ms) {
|
|
|
1802
1802
|
|
|
1803
1803
|
//#endregion
|
|
1804
1804
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1805
|
-
//# sourceMappingURL=ts-
|
|
1805
|
+
//# sourceMappingURL=ts-IAAUoBpk.js.map
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var name = "claude-yes";
|
|
10
|
-
var version = "1.
|
|
10
|
+
var version = "1.158.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-Dwe07Fe6.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import path from "path";
|
|
5
5
|
|
|
@@ -61,6 +61,41 @@ function isProvisionAllowed(owner, repo) {
|
|
|
61
61
|
const full = `${owner}/${repo}`.toLowerCase();
|
|
62
62
|
return list.some((e) => e.replace(/\/\*$/, "") === o || e === full);
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* A trusted, HOST-LOCAL shell hook run before each agent spawn. It is a POSIX
|
|
66
|
+
* `sh -c` script that prepares the environment (provisioning, env, cd) and that
|
|
67
|
+
* agent-yes runs as `sh -c "set -e\n<hook>\nexec \"$@\"" ay-spawn <agent argv…>`
|
|
68
|
+
* — so the real agent argv is passed as positional params and the prompt is
|
|
69
|
+
* never shell-parsed. `set -e` aborts the spawn if any hook step fails.
|
|
70
|
+
*
|
|
71
|
+
* This is arbitrary local code that runs on EVERY spawn, so it is deliberately
|
|
72
|
+
* NOT writable over the network. Set it on the machine by editing
|
|
73
|
+
* `~/.agent-yes/config.json` (`"spawnHook"`), or via env `AGENT_YES_SPAWN_HOOK`.
|
|
74
|
+
*
|
|
75
|
+
* Tampering guard (POSIX): a file-backed hook is ignored when the config file is
|
|
76
|
+
* a symlink, is not owned by us, or is group/world-writable — we refuse to run a
|
|
77
|
+
* hook from a file other users can swap or rewrite. The env override is trusted
|
|
78
|
+
* (it comes from the daemon's own environment). Returns null when unset/guarded.
|
|
79
|
+
*/
|
|
80
|
+
function getSpawnHook() {
|
|
81
|
+
const env = process.env.AGENT_YES_SPAWN_HOOK;
|
|
82
|
+
if (env && env.trim()) return env;
|
|
83
|
+
const h = readConfig().spawnHook;
|
|
84
|
+
if (!h || !h.trim()) return null;
|
|
85
|
+
if (process.platform !== "win32") try {
|
|
86
|
+
if (lstatSync(configPath()).isSymbolicLink()) return null;
|
|
87
|
+
const st = statSync(configPath());
|
|
88
|
+
if ((st.mode & 18) !== 0) return null;
|
|
89
|
+
if (typeof process.getuid === "function" && st.uid !== process.getuid()) return null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return h;
|
|
94
|
+
}
|
|
95
|
+
/** Whether a usable {@link getSpawnHook} is configured (for read-only disclosure). */
|
|
96
|
+
function hasSpawnHook() {
|
|
97
|
+
return getSpawnHook() !== null;
|
|
98
|
+
}
|
|
64
99
|
/** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
|
|
65
100
|
function setWorkspaceRoot(dir) {
|
|
66
101
|
const abs = path.resolve(expandTilde(dir));
|
|
@@ -87,5 +122,5 @@ function resolveSpawnCwd(input) {
|
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
//#endregion
|
|
90
|
-
export {
|
|
91
|
-
//# sourceMappingURL=workspaceConfig-
|
|
125
|
+
export { isProvisionAllowed as a, hasSpawnHook as i, getSpawnHook as n, resolveSpawnCwd as o, getWorkspaceRoot as r, setWorkspaceRoot as s, getProvisionRoot as t };
|
|
126
|
+
//# sourceMappingURL=workspaceConfig-B0Q9-q2B.js.map
|
package/lab/ui/console-logic.js
CHANGED
|
@@ -305,6 +305,54 @@ function agentForestNodes(list) {
|
|
|
305
305
|
return roots;
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// ---- display sort order -----------------------------------------------------
|
|
309
|
+
// The console cycles a button through these; the chosen comparator runs over the
|
|
310
|
+
// flat entry list BEFORE layeredRows builds the room/peer/agent tree (which keeps
|
|
311
|
+
// the given order for siblings and first-seen order for room/peer groups). So
|
|
312
|
+
// sorting reorders roots and siblings without breaking the nesting.
|
|
313
|
+
export const SORT_MODES = ["state", "created", "identity"];
|
|
314
|
+
|
|
315
|
+
// Attention-first state ranking: someone scanning the fleet wants the agents that
|
|
316
|
+
// need them (needs_input) up top, then the wedged ones (stuck), then live work,
|
|
317
|
+
// then quiet/idle, then finished. Unknown states sort last.
|
|
318
|
+
const STATE_RANK = {
|
|
319
|
+
needs_input: 0,
|
|
320
|
+
stuck: 1,
|
|
321
|
+
active: 2,
|
|
322
|
+
running: 2,
|
|
323
|
+
idle: 3,
|
|
324
|
+
stopped: 4,
|
|
325
|
+
exited: 4,
|
|
326
|
+
};
|
|
327
|
+
function stateRank(e) {
|
|
328
|
+
const r = STATE_RANK[e.status];
|
|
329
|
+
return r === undefined ? 5 : r;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Git "busyness" for the state mode's secondary key: a dirty / ahead / behind
|
|
333
|
+
// repo outranks a clean one, and more outstanding changes rank higher. The +0.5
|
|
334
|
+
// for `dirty` breaks a 0-count tie toward the dirty tree.
|
|
335
|
+
function gitWeight(e) {
|
|
336
|
+
const g = e.git || {};
|
|
337
|
+
return (g.changed || 0) + (g.ahead || 0) + (g.behind || 0) + (g.dirty ? 0.5 : 0);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Return a NEW array sorted for display per `mode` (default "state"):
|
|
341
|
+
// - "state": attention-first state, then git busyness, then newest.
|
|
342
|
+
// - "created": newest first (started_at desc).
|
|
343
|
+
// - "identity": user@host:owner/repo/branch (alphabetical).
|
|
344
|
+
// Every comparator falls back to newest-first so order is total & deterministic.
|
|
345
|
+
export function sortEntries(entries, mode = "state") {
|
|
346
|
+
const byNewest = (a, b) => (b.started_at || 0) - (a.started_at || 0);
|
|
347
|
+
const cmp =
|
|
348
|
+
mode === "created"
|
|
349
|
+
? byNewest
|
|
350
|
+
: mode === "identity"
|
|
351
|
+
? (a, b) => fullIdent(a).localeCompare(fullIdent(b)) || byNewest(a, b)
|
|
352
|
+
: (a, b) => stateRank(a) - stateRank(b) || gitWeight(b) - gitWeight(a) || byNewest(a, b);
|
|
353
|
+
return entries.slice().sort(cmp);
|
|
354
|
+
}
|
|
355
|
+
|
|
308
356
|
// Order entries as agent>subagent forests so a nested `ay` (one agent spawning
|
|
309
357
|
// another) renders indented under its parent. SCOPED PER HOST. Returns a NEW
|
|
310
358
|
// array in depth-first order; each entry is shallow-copied with `_branch` (a
|
package/lab/ui/index.html
CHANGED
|
@@ -838,6 +838,11 @@
|
|
|
838
838
|
.dot.stuck {
|
|
839
839
|
background: var(--red);
|
|
840
840
|
}
|
|
841
|
+
/* Waiting on you: alive and quiet, but parked on an unanswered prompt/menu.
|
|
842
|
+
The UI accent (blue) reads as "your turn" — distinct from idle-amber. */
|
|
843
|
+
.dot.needs_input {
|
|
844
|
+
background: var(--accent);
|
|
845
|
+
}
|
|
841
846
|
.dot.stopped,
|
|
842
847
|
.dot.exited {
|
|
843
848
|
background: var(--muted);
|
|
@@ -1346,6 +1351,7 @@
|
|
|
1346
1351
|
<div class="meta">
|
|
1347
1352
|
<span id="count"></span>
|
|
1348
1353
|
<span class="metaright">
|
|
1354
|
+
<button id="sortbtn" class="viewbtn" title="cycle sort order">⇅ state</button>
|
|
1349
1355
|
<button id="viewbtn" class="viewbtn" title="toggle compact list">☰</button>
|
|
1350
1356
|
<button id="newbtn" class="newbtn" title="spawn a new agent on this fleet">
|
|
1351
1357
|
+ New agent
|
|
@@ -1529,6 +1535,8 @@
|
|
|
1529
1535
|
hasIdent,
|
|
1530
1536
|
deviceCount,
|
|
1531
1537
|
layeredRows,
|
|
1538
|
+
sortEntries,
|
|
1539
|
+
SORT_MODES,
|
|
1532
1540
|
taskLabel,
|
|
1533
1541
|
hashHue,
|
|
1534
1542
|
selFromBottom,
|
|
@@ -3155,6 +3163,22 @@
|
|
|
3155
3163
|
// Compact list: one line per agent (dot + cli + title), persisted per device.
|
|
3156
3164
|
let compactList = localStorage.getItem("ay.compactList") === "1";
|
|
3157
3165
|
|
|
3166
|
+
// Display sort order, cycled by the sort button and persisted per device.
|
|
3167
|
+
// Default "state" (attention-first state, then git busyness). Guard against
|
|
3168
|
+
// a stale/garbage stored value so we never start in an unknown mode.
|
|
3169
|
+
let sortMode = localStorage.getItem("ay.sortMode") || "state";
|
|
3170
|
+
if (!SORT_MODES.includes(sortMode)) sortMode = "state";
|
|
3171
|
+
|
|
3172
|
+
// The filtered + sorted entry list, in the SAME order the left panel renders
|
|
3173
|
+
// and the keyboard nav steps. Sorting runs before layeredRows builds the
|
|
3174
|
+
// room/peer/agent tree, so it reorders roots/siblings without breaking nesting.
|
|
3175
|
+
function orderedEntries(toks) {
|
|
3176
|
+
return sortEntries(
|
|
3177
|
+
entries.filter((e) => matches(e, toks)),
|
|
3178
|
+
sortMode,
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3158
3182
|
// A small git chip (±changed ↑ahead ↓behind) for a row, amber when the tree
|
|
3159
3183
|
// is dirty. Empty for clean/in-sync repos and non-git cwds — no noise.
|
|
3160
3184
|
function gitChipHtml(e) {
|
|
@@ -3195,10 +3219,14 @@
|
|
|
3195
3219
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
3196
3220
|
// Build the full signalling-server > rooms > peers > agents > subagents
|
|
3197
3221
|
// tree. Single-node room/peer layers fold away; ≥2 become ├ └ │ branches.
|
|
3198
|
-
const rows = layeredRows(
|
|
3222
|
+
const rows = layeredRows(orderedEntries(toks));
|
|
3199
3223
|
const agentRows = rows.filter((r) => r.kind === "agent");
|
|
3200
3224
|
$("count").textContent = `${agentRows.length} / ${entries.length} agents`;
|
|
3201
3225
|
$("viewbtn").classList.toggle("on", compactList);
|
|
3226
|
+
// Show the active sort mode on the button so it's self-documenting; the
|
|
3227
|
+
// default ("state") is also highlighted to hint it's the implicit order.
|
|
3228
|
+
$("sortbtn").textContent = `⇅ ${sortMode}`;
|
|
3229
|
+
$("sortbtn").classList.toggle("on", sortMode !== "state");
|
|
3202
3230
|
// identContext blanks any field uniform across the shown list (so a
|
|
3203
3231
|
// single-device fleet shows no device); multiDevice gates the detailed
|
|
3204
3232
|
// host tag so it only appears when machines are actually mixed.
|
|
@@ -3658,6 +3686,12 @@
|
|
|
3658
3686
|
localStorage.setItem("ay.compactList", compactList ? "1" : "0");
|
|
3659
3687
|
renderList();
|
|
3660
3688
|
});
|
|
3689
|
+
// Cycle the sort order: state → created → identity → state.
|
|
3690
|
+
$("sortbtn").addEventListener("click", () => {
|
|
3691
|
+
sortMode = SORT_MODES[(SORT_MODES.indexOf(sortMode) + 1) % SORT_MODES.length];
|
|
3692
|
+
localStorage.setItem("ay.sortMode", sortMode);
|
|
3693
|
+
renderList();
|
|
3694
|
+
});
|
|
3661
3695
|
window.addEventListener("resize", () => {
|
|
3662
3696
|
// Resizing our window is strong intent → re-fit to our pane (we become the
|
|
3663
3697
|
// size driver; the push rides term.onResize). applyCanvasScale then clears
|
|
@@ -3695,7 +3729,7 @@
|
|
|
3695
3729
|
function stepSelection(dir) {
|
|
3696
3730
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
3697
3731
|
// Must match renderList's order; step only over agent rows (skip headers).
|
|
3698
|
-
const shown = layeredRows(
|
|
3732
|
+
const shown = layeredRows(orderedEntries(toks))
|
|
3699
3733
|
.filter((r) => r.kind === "agent")
|
|
3700
3734
|
.map((r) => r.entry);
|
|
3701
3735
|
if (!shown.length) return;
|
package/package.json
CHANGED
package/ts/serve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, open, readFile, stat, writeFile } from "fs/promises";
|
|
1
|
+
import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
|
|
2
2
|
import { renameSync, watch, writeFileSync } from "node:fs";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -23,7 +23,13 @@ import { updateGlobalPidStatus } from "./globalPidIndex.ts";
|
|
|
23
23
|
import { pgidForWrapper } from "./reaper.ts";
|
|
24
24
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
25
25
|
import { getInstalledPackage } from "./versionChecker.ts";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
getProvisionRoot,
|
|
28
|
+
getSpawnHook,
|
|
29
|
+
hasSpawnHook,
|
|
30
|
+
isProvisionAllowed,
|
|
31
|
+
resolveSpawnCwd,
|
|
32
|
+
} from "./workspaceConfig.ts";
|
|
27
33
|
|
|
28
34
|
const DEFAULT_PORT = 7432;
|
|
29
35
|
|
|
@@ -1157,6 +1163,15 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1157
1163
|
return Response.json({ version: getInstalledPackage().version });
|
|
1158
1164
|
}
|
|
1159
1165
|
|
|
1166
|
+
// GET /api/spawn-config — read-only disclosure of WHETHER a host-local spawn
|
|
1167
|
+
// hook is configured. Deliberately returns only a boolean, never the hook
|
|
1168
|
+
// body: the hook is arbitrary local code and console/room clients holding the
|
|
1169
|
+
// share token are not necessarily host admins. Setting it stays host-local
|
|
1170
|
+
// (edit ~/.agent-yes/config.json) — there is no PUT.
|
|
1171
|
+
if (req.method === "GET" && p === "/api/spawn-config") {
|
|
1172
|
+
return Response.json({ hasSpawnHook: hasSpawnHook() });
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1160
1175
|
// GET /api/notes
|
|
1161
1176
|
if (req.method === "GET" && p === "/api/notes") {
|
|
1162
1177
|
const notes = await readNotes();
|
|
@@ -1685,10 +1700,75 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1685
1700
|
process.platform === "win32" && ayBin.toLowerCase().endsWith(".exe")
|
|
1686
1701
|
? [ayBin]
|
|
1687
1702
|
: [process.execPath, ayBin];
|
|
1703
|
+
const agentArgv = [...ayCmd, cli, ...(prompt ? ["--", prompt] : [])];
|
|
1704
|
+
// don't leak our Claude Code session into the agent
|
|
1705
|
+
const agentEnv = freshAgentEnv();
|
|
1688
1706
|
try {
|
|
1689
|
-
const
|
|
1707
|
+
const hook = getSpawnHook();
|
|
1708
|
+
if (hook) {
|
|
1709
|
+
// Host-local spawn hook (trusted local code, never network-writable). We
|
|
1710
|
+
// run it via POSIX `sh -c` then `exec "$@"` the real agent — the agent
|
|
1711
|
+
// argv is passed as positional params ($1…), so the prompt is NEVER
|
|
1712
|
+
// shell-parsed (no quoting/injection surface). `set -e` aborts the spawn
|
|
1713
|
+
// if any hook step fails. No `-l`: we don't re-source rc files here (the
|
|
1714
|
+
// env is already the recovered login-shell env via freshAgentEnv). cwd
|
|
1715
|
+
// and cli are exposed as env for the hook to consume.
|
|
1716
|
+
const shell = process.env.AGENT_YES_SPAWN_SHELL?.trim() || "/bin/sh";
|
|
1717
|
+
const script = `set -e\n${hook}\nexec "$@"`;
|
|
1718
|
+
const errPath = path.join(
|
|
1719
|
+
agentYesHome(),
|
|
1720
|
+
`spawn-hook-${process.pid}-${performance.now().toString(36).replace(".", "")}.err`,
|
|
1721
|
+
);
|
|
1722
|
+
// File-backed stderr never blocks the child (a pipe we stop draining
|
|
1723
|
+
// after exec would), and bounds our read to the first few KB.
|
|
1724
|
+
const child = Bun.spawn([shell, "-c", script, "ay-spawn", ...agentArgv], {
|
|
1725
|
+
cwd,
|
|
1726
|
+
env: { ...agentEnv, AGENT_YES_CWD: cwd, AGENT_YES_CLI: cli },
|
|
1727
|
+
stdin: "ignore",
|
|
1728
|
+
stdout: "ignore",
|
|
1729
|
+
stderr: Bun.file(errPath),
|
|
1730
|
+
});
|
|
1731
|
+
// Handshake: surface an EARLY hook/provision failure synchronously. If
|
|
1732
|
+
// the child exits non-zero within the window, the hook failed before the
|
|
1733
|
+
// agent settled — return the captured stderr. If it's still alive after
|
|
1734
|
+
// the window, `exec` succeeded (or the agent is running) → detach and
|
|
1735
|
+
// report success. We can't always attribute a failure to the hook vs the
|
|
1736
|
+
// agent, so we just surface the captured output.
|
|
1737
|
+
const windowMs = Number(process.env.AGENT_YES_SPAWN_HOOK_TIMEOUT_MS) || 5000;
|
|
1738
|
+
const exitCode = await Promise.race([
|
|
1739
|
+
child.exited,
|
|
1740
|
+
new Promise<null>((r) => setTimeout(() => r(null), windowMs)),
|
|
1741
|
+
]);
|
|
1742
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
1743
|
+
let detail = "";
|
|
1744
|
+
try {
|
|
1745
|
+
detail = (await Bun.file(errPath).text()).slice(0, 4096);
|
|
1746
|
+
} catch {
|
|
1747
|
+
/* no stderr captured */
|
|
1748
|
+
}
|
|
1749
|
+
await unlink(errPath).catch(() => {});
|
|
1750
|
+
return new Response(
|
|
1751
|
+
`spawn hook failed (exit ${exitCode})${detail ? `:\n${detail.trimEnd()}` : ""}`,
|
|
1752
|
+
{ status: 502 },
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
child.unref();
|
|
1756
|
+
// Clean up the stderr sidecar. On POSIX the running agent may still hold
|
|
1757
|
+
// the fd; unlinking an open file is harmless (the inode lives until
|
|
1758
|
+
// close), so a brief delay is enough to keep early-failure reads valid.
|
|
1759
|
+
setTimeout(() => void unlink(errPath).catch(() => {}), 60_000).unref?.();
|
|
1760
|
+
return Response.json({
|
|
1761
|
+
ok: true,
|
|
1762
|
+
pid: child.pid,
|
|
1763
|
+
cli,
|
|
1764
|
+
cwd,
|
|
1765
|
+
hook: true,
|
|
1766
|
+
...(provisioned ? { provisioned } : {}),
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
const child = Bun.spawn(agentArgv, {
|
|
1690
1770
|
cwd,
|
|
1691
|
-
env:
|
|
1771
|
+
env: agentEnv,
|
|
1692
1772
|
stdin: "ignore",
|
|
1693
1773
|
stdout: "ignore",
|
|
1694
1774
|
stderr: "ignore",
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
2
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir, tmpdir } from "os";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import {
|
|
6
6
|
expandTilde,
|
|
7
7
|
getProvisionAllowlist,
|
|
8
8
|
getProvisionRoot,
|
|
9
|
+
getSpawnHook,
|
|
9
10
|
getWorkspaceRoot,
|
|
11
|
+
hasSpawnHook,
|
|
10
12
|
isProvisionAllowed,
|
|
11
13
|
resolveSpawnCwd,
|
|
12
14
|
setWorkspaceRoot,
|
|
@@ -25,6 +27,7 @@ describe("workspaceConfig", () => {
|
|
|
25
27
|
else process.env.AGENT_YES_HOME = original;
|
|
26
28
|
delete process.env.CODEHOST_WS_ROOT;
|
|
27
29
|
delete process.env.CODEHOST_PROVISION_ALLOWLIST;
|
|
30
|
+
delete process.env.AGENT_YES_SPAWN_HOOK;
|
|
28
31
|
rmSync(tmp, { recursive: true, force: true });
|
|
29
32
|
});
|
|
30
33
|
|
|
@@ -142,4 +145,42 @@ describe("workspaceConfig", () => {
|
|
|
142
145
|
expect(isProvisionAllowed("org", "anything")).toBe(true);
|
|
143
146
|
});
|
|
144
147
|
});
|
|
148
|
+
|
|
149
|
+
describe("getSpawnHook / hasSpawnHook", () => {
|
|
150
|
+
const isPosix = process.platform !== "win32";
|
|
151
|
+
|
|
152
|
+
it("is null/false when unset", () => {
|
|
153
|
+
expect(getSpawnHook()).toBeNull();
|
|
154
|
+
expect(hasSpawnHook()).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("returns the configured hook from a private (0600) config", () => {
|
|
158
|
+
writeConfig({ spawnHook: 'echo hi >&2\nexec "$@"' });
|
|
159
|
+
if (isPosix) chmodSync(path.join(tmp, "config.json"), 0o600);
|
|
160
|
+
expect(getSpawnHook()).toBe('echo hi >&2\nexec "$@"');
|
|
161
|
+
expect(hasSpawnHook()).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("ignores a blank hook", () => {
|
|
165
|
+
writeConfig({ spawnHook: " " });
|
|
166
|
+
expect(getSpawnHook()).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("env AGENT_YES_SPAWN_HOOK overrides the config", () => {
|
|
170
|
+
writeConfig({ spawnHook: "from-file" });
|
|
171
|
+
process.env.AGENT_YES_SPAWN_HOOK = "from-env";
|
|
172
|
+
expect(getSpawnHook()).toBe("from-env");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it.skipIf(!isPosix)(
|
|
176
|
+
"refuses a file-backed hook when the config is group/world-writable (tampering guard)",
|
|
177
|
+
() => {
|
|
178
|
+
writeConfig({ spawnHook: "echo pwned" });
|
|
179
|
+
chmodSync(path.join(tmp, "config.json"), 0o666);
|
|
180
|
+
expect(getSpawnHook()).toBeNull();
|
|
181
|
+
chmodSync(path.join(tmp, "config.json"), 0o600);
|
|
182
|
+
expect(getSpawnHook()).toBe("echo pwned");
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
});
|
|
145
186
|
});
|
package/ts/workspaceConfig.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1
|
+
import { lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { agentYesHome } from "./agentYesHome.ts";
|
|
@@ -19,6 +19,12 @@ interface Config {
|
|
|
19
19
|
provisionRoot?: string;
|
|
20
20
|
/** Owners/repos permitted for `from`-provisioning; empty = deny all. */
|
|
21
21
|
provisionAllowlist?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* A trusted, HOST-LOCAL shell hook run before each console/CLI spawn. See
|
|
24
|
+
* {@link getSpawnHook}. Intentionally NOT settable over the network — it is
|
|
25
|
+
* arbitrary local code that runs on every spawn.
|
|
26
|
+
*/
|
|
27
|
+
spawnHook?: string;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
function configPath(): string {
|
|
@@ -86,6 +92,45 @@ export function isProvisionAllowed(owner: string, repo: string): boolean {
|
|
|
86
92
|
return list.some((e) => e.replace(/\/\*$/, "") === o || e === full);
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* A trusted, HOST-LOCAL shell hook run before each agent spawn. It is a POSIX
|
|
97
|
+
* `sh -c` script that prepares the environment (provisioning, env, cd) and that
|
|
98
|
+
* agent-yes runs as `sh -c "set -e\n<hook>\nexec \"$@\"" ay-spawn <agent argv…>`
|
|
99
|
+
* — so the real agent argv is passed as positional params and the prompt is
|
|
100
|
+
* never shell-parsed. `set -e` aborts the spawn if any hook step fails.
|
|
101
|
+
*
|
|
102
|
+
* This is arbitrary local code that runs on EVERY spawn, so it is deliberately
|
|
103
|
+
* NOT writable over the network. Set it on the machine by editing
|
|
104
|
+
* `~/.agent-yes/config.json` (`"spawnHook"`), or via env `AGENT_YES_SPAWN_HOOK`.
|
|
105
|
+
*
|
|
106
|
+
* Tampering guard (POSIX): a file-backed hook is ignored when the config file is
|
|
107
|
+
* a symlink, is not owned by us, or is group/world-writable — we refuse to run a
|
|
108
|
+
* hook from a file other users can swap or rewrite. The env override is trusted
|
|
109
|
+
* (it comes from the daemon's own environment). Returns null when unset/guarded.
|
|
110
|
+
*/
|
|
111
|
+
export function getSpawnHook(): string | null {
|
|
112
|
+
const env = process.env.AGENT_YES_SPAWN_HOOK;
|
|
113
|
+
if (env && env.trim()) return env;
|
|
114
|
+
const h = readConfig().spawnHook;
|
|
115
|
+
if (!h || !h.trim()) return null;
|
|
116
|
+
if (process.platform !== "win32") {
|
|
117
|
+
try {
|
|
118
|
+
if (lstatSync(configPath()).isSymbolicLink()) return null;
|
|
119
|
+
const st = statSync(configPath());
|
|
120
|
+
if ((st.mode & 0o022) !== 0) return null; // group/world-writable
|
|
121
|
+
if (typeof process.getuid === "function" && st.uid !== process.getuid()) return null;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return h;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Whether a usable {@link getSpawnHook} is configured (for read-only disclosure). */
|
|
130
|
+
export function hasSpawnHook(): boolean {
|
|
131
|
+
return getSpawnHook() !== null;
|
|
132
|
+
}
|
|
133
|
+
|
|
89
134
|
/** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
|
|
90
135
|
export function setWorkspaceRoot(dir: string): string {
|
|
91
136
|
const abs = path.resolve(expandTilde(dir));
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-CLHiUE0V.js";
|
|
2
|
-
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-C4EICUab.js";
|
|
4
|
-
import "./pidStore-fqXqTKkh.js";
|
|
5
|
-
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BrOIMMzT.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|