feat(server-mode): pane presence and typing audit (sharing phase 4)

A pane's tab shows the other people who have it open (pane-presence.js,
in memory, fed by the /ws-pty attach; GET /api/lanes/:id/presence and the
pane_presence broadcast to the owner and full collaborators) and says
"Billed to <name>" for a pane a collaborator opened (startedBy on the run).
The first real keystroke of each attach into a shared workspace's pane by
anyone but its owner is audited as `type into <run id>` (mouse, focus and
terminal replies to tmux do not count). The pty socket now pings, so a
vanished browser is ended at the next re-check and leaves the presence list.
This commit is contained in:
2026-11-06 11:32:25 +07:00
parent 879a6bb02d
commit 18e574a765
21 changed files with 568 additions and 8 deletions
+128
View File
@@ -0,0 +1,128 @@
/**
* @file pane-presence.test.js
* @description Who has which pane open (server/lib/pane-presence.js, sharing
* phase 4): a user counted once however many attaches they hold, gone with
* their last one, listed per workspace; the first keystroke into a SHARED
* workspace's pane is audited (who and where, never what) and an unshared
* one's is not; `pane_presence` reaches the workspace's owner and the users
* it is shared with, nobody else.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after, beforeEach } = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const fs = require("node:fs");
const os = require("node:os");
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "pane-presence-test-"));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
const { db } = require("../db");
const presence = require("../lib/pane-presence");
const scope = require("../lib/scope");
let alice;
let bob;
let carol;
let lane;
let lonely;
before(() => {
const user = (name) =>
Number(
db
.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, 'x', 'developer')")
.run(name).lastInsertRowid
);
alice = user("alice");
bob = user("bob");
carol = user("carol");
const mk = () =>
Number(
db
.prepare("INSERT INTO lanes (cwd, tool, owner_id) VALUES (?, 'claude', ?)")
.run(`/w/${Math.random()}`, alice).lastInsertRowid
);
lane = mk();
lonely = mk();
db.prepare("INSERT INTO lane_shares (lane_id, user_id) VALUES (?, ?)").run(lane, bob);
});
after(() => fs.rmSync(TMP, { recursive: true, force: true }));
beforeEach(() => presence.__reset());
describe("pane presence", () => {
it("counts a user once per pane, until their last attach closes", () => {
const run = `agenthub-lane-${lane}-1`;
presence.join(run, { id: bob, username: "bob", displayName: "Bob B" });
presence.join(run, { id: bob, username: "bob" });
presence.join(run, { id: alice, username: "alice" });
assert.deepEqual(presence.forLane(lane)[run], [
{ id: bob, name: "Bob B" },
{ id: alice, name: "alice" },
]);
presence.leave(run, { id: bob });
assert.equal(presence.forLane(lane)[run].length, 2, "one of bob's attaches is still open");
presence.leave(run, { id: bob });
presence.leave(run, { id: alice });
assert.deepEqual(presence.forLane(lane), {});
presence.join("not-a-run", { id: bob });
assert.deepEqual(presence.users("not-a-run"), []);
});
it("audits the first keystroke only in a shared workspace", () => {
const count = () =>
db.prepare("SELECT COUNT(*) AS n FROM audit_log WHERE path LIKE 'type into %'").get().n;
presence.auditInput(`agenthub-lane-${lonely}`, { id: alice });
assert.equal(count(), 0);
presence.auditInput(`agenthub-lane-${lane}-2`, { id: bob });
const row = db
.prepare(
"SELECT user_id, owner_id, method, path FROM audit_log WHERE path LIKE 'type into %'"
)
.get();
assert.deepEqual(row, {
user_id: bob,
owner_id: alice,
method: "PTY",
path: `type into agenthub-lane-${lane}-2`,
});
});
it("is news for the workspace's owner and collaborators only", () => {
const aud = scope.broadcastAudience("pane_presence", { laneId: lane, runId: "x", users: [] });
const dev = (id) => ({ id, role: "developer" });
assert.equal(scope.audienceIncludes(aud, dev(alice)), true);
assert.equal(scope.audienceIncludes(aud, dev(bob)), true);
assert.equal(scope.audienceIncludes(aud, dev(carol)), false);
});
});
describe("what counts as typing", () => {
const { isTyping } = require("../lib/pty-attach");
it("ignores mouse, focus and the terminal answering tmux", () => {
for (const s of [
"\x1b[<0;10;5M",
"\x1b[I",
"\x1b[>0;276;0c",
"\x1b[?1;2c",
"\x1b[12;40R",
"\x1b[0n",
"\x1b[?2004;1$y",
"\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\",
"\x1b]10;rgb:ffff/ffff/ffff\x07",
"\x1bP>|xterm.js(6.0.0)\x1b\\",
]) {
assert.equal(isTyping(s), false, JSON.stringify(s));
}
assert.equal(isTyping("ls\r"), true);
assert.equal(isTyping("\x1b[A"), true, "an arrow key is typing");
});
it("never audits the owner's own typing", () => {
const before = db.prepare("SELECT COUNT(*) AS n FROM audit_log").get().n;
presence.auditInput(`agenthub-lane-${lane}`, { id: alice });
assert.equal(db.prepare("SELECT COUNT(*) AS n FROM audit_log").get().n, before);
});
});