fix: v2.7.3 — Update now restarts through our own service unit, member panes stay live on a dev server, a start refuses a missing folder
- self-restart: a server in our unit's cgroup (/agenthub.service, system or user) restarts via `systemctl restart --no-block` (sudo -n for the system unit), our launchd job by exiting into KeepAlive. The detached restart-helper replacement held the port while Restart=always started the unit every 3 s (v2.7.2 on the S3 box: 240+ restarts, MainPID 0). - run-as.mayHaveTmux: only ENOENT means "no tmux". A dev server in server mode without CAP_DAC_READ_SEARCH got EACCES on a member's 0700 .agenthub/tmux, dropped the member from the list and ended their runs within seconds — the real cause of `401 not a live pane's token`. - tmux: a member's list is re-asked every 5 s, not 1 s (one sudo + three auth.log lines each). - POST /api/lanes/:id/start: 409 ENOCWD for a folder that no longer exists (the pane started in the account's home otherwise). - Settings: Remote sources only for staff (a developer got a 403). - scripts/test-server.js drops every inherited AGENTHUB_* (pane env). Tests: self-restart.test.js, server-s3-panes (mayHaveTmux), lanes-api (ENOCWD); touched suites 771 + client pages 191, tsc -b.
This commit is contained in:
+24
-12
@@ -133,13 +133,22 @@ function memberHomes() {
|
||||
function membersWithTmux() {
|
||||
return memberHomes()
|
||||
.map((home) => accountForPath(home))
|
||||
.filter((acct) => {
|
||||
try {
|
||||
return fs.statSync(tmuxSocket(acct)).isSocket();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
.filter(mayHaveTmux);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the member may have a tmux server. Only a missing socket says no:
|
||||
* a member's `.agenthub/tmux` is theirs (0700), and a dashboard without
|
||||
* CAP_DAC_READ_SEARCH (a dev server on the same database) gets EACCES there
|
||||
* — that proves nothing, and reading it as "no server" ended every run of
|
||||
* theirs within seconds. Their list is asked through sudo, which can tell.
|
||||
*/
|
||||
function mayHaveTmux(acct) {
|
||||
try {
|
||||
return fs.statSync(tmuxSocket(acct)).isSocket();
|
||||
} catch (err) {
|
||||
return err.code !== "ENOENT" && err.code !== "ENOTDIR";
|
||||
}
|
||||
}
|
||||
|
||||
// uid by login, from /etc/passwd (world-readable; node has no getpwnam).
|
||||
@@ -357,8 +366,7 @@ function openSafe(file) {
|
||||
// at no cost): MEMBER_READ_MAX unless the caller passes its own `max`, and
|
||||
// never past the size `fstat` gave — a file growing under the read is cut.
|
||||
const MEMBER_READ_MAX = 64 * 1024 * 1024;
|
||||
const tooBig = (file) =>
|
||||
Object.assign(new Error(`${file}: too big to read`), { code: "ETOOBIG" });
|
||||
const tooBig = (file) => Object.assign(new Error(`${file}: too big to read`), { code: "ETOOBIG" });
|
||||
|
||||
/** `fs.readFileSync(file, enc)`, guarded (a member's file: ≤ `max` bytes). */
|
||||
function readFileSafe(file, enc, { max = MEMBER_READ_MAX } = {}) {
|
||||
@@ -369,7 +377,7 @@ function readFileSafe(file, enc, { max = MEMBER_READ_MAX } = {}) {
|
||||
if (size > max) throw tooBig(file);
|
||||
const buf = Buffer.alloc(size);
|
||||
let off = 0;
|
||||
for (let n; off < size && (n = fs.readSync(fd, buf, off, size - off, off)) > 0; ) off += n;
|
||||
for (let n; off < size && (n = fs.readSync(fd, buf, off, size - off, off)) > 0;) off += n;
|
||||
return enc ? buf.toString(enc, 0, off) : buf.subarray(0, off);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
@@ -398,7 +406,7 @@ async function readFileSafeAsync(file, enc, { max = MEMBER_READ_MAX } = {}) {
|
||||
if (size > max) throw tooBig(file);
|
||||
const buf = Buffer.alloc(size);
|
||||
let off = 0;
|
||||
for (let n; off < size && (n = (await fh.read(buf, off, size - off, off)).bytesRead) > 0; )
|
||||
for (let n; off < size && (n = (await fh.read(buf, off, size - off, off)).bytesRead) > 0;)
|
||||
off += n;
|
||||
return enc ? buf.toString(enc, 0, off) : buf.subarray(0, off);
|
||||
} finally {
|
||||
@@ -438,7 +446,10 @@ async function* readLines(file, { maxLine = 64 * 1024 * 1024 } = {}) {
|
||||
let start = 0;
|
||||
for (let i = view.indexOf(0x0a); i !== -1; i = view.indexOf(0x0a, start)) {
|
||||
if (skipping || partLen + (i - start) > maxLine) yield "";
|
||||
else yield text(partLen ? Buffer.concat([...parts, view.subarray(start, i)]) : view.subarray(start, i));
|
||||
else
|
||||
yield text(
|
||||
partLen ? Buffer.concat([...parts, view.subarray(start, i)]) : view.subarray(start, i)
|
||||
);
|
||||
parts = [];
|
||||
partLen = 0;
|
||||
skipping = false;
|
||||
@@ -502,6 +513,7 @@ module.exports = {
|
||||
assertOwnerFits,
|
||||
hasLinuxAccount,
|
||||
membersWithTmux,
|
||||
mayHaveTmux,
|
||||
memberHomes,
|
||||
ownerForRead,
|
||||
tmuxSocket,
|
||||
|
||||
Reference in New Issue
Block a user