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:
2026-10-12 14:04:08 +07:00
parent 3a8be55b55
commit 943ba03b76
17 changed files with 271 additions and 34 deletions
+16
View File
@@ -2060,6 +2060,22 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
try {
switch (action) {
case "start": {
// A folder that is gone (or never was) would start the pane in the
// account's home instead, under this workspace's name. EACCES is not
// "gone": a member's folder may be unreadable to this process.
try {
if (!fs.statSync(lane.cwd).isDirectory())
throw Object.assign(new Error(), { code: "ENOTDIR" });
} catch (err) {
if (err.code === "ENOENT" || err.code === "ENOTDIR") {
return res.status(409).json({
error: {
code: "ENOCWD",
message: `the workspace folder does not exist: ${lane.cwd}`,
},
});
}
}
// `"auto"`: the first free tab, picked under the lane lock below — a
// client's own pick comes from a list that may already be stale.
const auto = body.slot === "auto";