agentgather 0.1.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -0
  3. package/SECURITY.md +104 -0
  4. package/dist/src/auth/index.js +1 -0
  5. package/dist/src/auth/tokens.js +12 -0
  6. package/dist/src/browser/room.css +666 -0
  7. package/dist/src/browser/room.html +80 -0
  8. package/dist/src/browser/room.js +435 -0
  9. package/dist/src/cli/args.js +29 -0
  10. package/dist/src/cli/commands/attend/index.js +26 -0
  11. package/dist/src/cli/commands/broker/index.js +61 -0
  12. package/dist/src/cli/commands/doctor/index.js +93 -0
  13. package/dist/src/cli/commands/export/index.js +42 -0
  14. package/dist/src/cli/commands/handoff/index.js +41 -0
  15. package/dist/src/cli/commands/instructions/index.js +7 -0
  16. package/dist/src/cli/commands/message/index.js +50 -0
  17. package/dist/src/cli/commands/message/transport.js +108 -0
  18. package/dist/src/cli/commands/room/index.js +350 -0
  19. package/dist/src/cli/commands/tunnel/index.js +131 -0
  20. package/dist/src/cli/commands/watch/index.js +16 -0
  21. package/dist/src/cli/context.js +9 -0
  22. package/dist/src/cli/help.js +53 -0
  23. package/dist/src/cli/index.js +63 -0
  24. package/dist/src/cli/state.js +40 -0
  25. package/dist/src/protocol/attendance.js +20 -0
  26. package/dist/src/protocol/index.js +7 -0
  27. package/dist/src/protocol/instructions.js +29 -0
  28. package/dist/src/protocol/mentions.js +48 -0
  29. package/dist/src/protocol/messages.js +71 -0
  30. package/dist/src/protocol/types.js +1 -0
  31. package/dist/src/protocol/urls.js +9 -0
  32. package/dist/src/protocol/validation.js +21 -0
  33. package/dist/src/server/errors.js +12 -0
  34. package/dist/src/server/http.js +583 -0
  35. package/dist/src/server/index.js +2 -0
  36. package/dist/src/server/wait.js +44 -0
  37. package/dist/src/storage/index.js +4 -0
  38. package/dist/src/storage/lock.js +93 -0
  39. package/dist/src/storage/paths.js +18 -0
  40. package/dist/src/storage/room-store.js +302 -0
  41. package/dist/src/storage/secure-fs.js +28 -0
  42. package/dist/src/tunnel/broker.js +440 -0
  43. package/dist/src/tunnel/client.js +144 -0
  44. package/dist/src/tunnel/forwarding.js +176 -0
  45. package/dist/src/tunnel/host-session.js +133 -0
  46. package/dist/src/tunnel/index.js +8 -0
  47. package/dist/src/tunnel/limits.js +81 -0
  48. package/dist/src/tunnel/logging.js +70 -0
  49. package/dist/src/tunnel/protocol.js +46 -0
  50. package/dist/src/tunnel/relay.js +106 -0
  51. package/docs/FOUNDING-TICKETS.md +759 -0
  52. package/docs/PROPOSAL.md +2120 -0
  53. package/docs/agentgather-dev-deployment-guide.md +305 -0
  54. package/docs/agentgather-dev-tunnel-architecture.md +349 -0
  55. package/docs/deploy-rooms-agentgather-dev.md +152 -0
  56. package/docs/dogfood/release-dogfood.md +61 -0
  57. package/docs/dogfood/sanitized-room-log.jsonl +6 -0
  58. package/docs/host-guide.md +282 -0
  59. package/docs/operator-runbook.md +248 -0
  60. package/docs/remote-exposure.md +269 -0
  61. package/docs/room-brief-and-attend-card.md +110 -0
  62. package/package.json +49 -0
@@ -0,0 +1,93 @@
1
+ import { chmod, open, readFile, rm, stat } from "node:fs/promises";
2
+ import { SECURE_FILE_MODE } from "./secure-fs.js";
3
+ export async function withWriterLock(lockPath, fn, options = {}) {
4
+ const release = await acquireWriterLock(lockPath, options);
5
+ try {
6
+ return await fn();
7
+ }
8
+ finally {
9
+ await release();
10
+ }
11
+ }
12
+ async function acquireWriterLock(lockPath, options) {
13
+ const retryDelayMs = options.retryDelayMs ?? 10;
14
+ const timeoutMs = options.timeoutMs ?? 5_000;
15
+ const staleAfterMs = options.staleAfterMs ?? 30_000;
16
+ const startedAt = Date.now();
17
+ const record = { pid: process.pid, createdAt: new Date().toISOString() };
18
+ while (true) {
19
+ try {
20
+ const handle = await open(lockPath, "wx", SECURE_FILE_MODE);
21
+ await handle.writeFile(JSON.stringify(record));
22
+ await handle.close();
23
+ await chmod(lockPath, SECURE_FILE_MODE);
24
+ return async () => {
25
+ await rm(lockPath, { force: true });
26
+ };
27
+ }
28
+ catch (error) {
29
+ if (!isFileExistsError(error))
30
+ throw error;
31
+ if (Date.now() - startedAt > timeoutMs) {
32
+ throw new Error(`timed out waiting for writer lock: ${lockPath}`);
33
+ }
34
+ if (await removeStaleLock(lockPath, staleAfterMs))
35
+ continue;
36
+ await sleep(retryDelayMs);
37
+ }
38
+ }
39
+ }
40
+ async function removeStaleLock(lockPath, staleAfterMs) {
41
+ try {
42
+ const raw = await readFile(lockPath, "utf8");
43
+ const parsed = JSON.parse(raw);
44
+ if (typeof parsed.pid !== "number") {
45
+ return removeMalformedLockIfOld(lockPath, staleAfterMs);
46
+ }
47
+ if (!isProcessAlive(parsed.pid)) {
48
+ await rm(lockPath, { force: true });
49
+ return true;
50
+ }
51
+ }
52
+ catch (error) {
53
+ if (isNotFoundError(error))
54
+ return false;
55
+ return removeMalformedLockIfOld(lockPath, staleAfterMs);
56
+ }
57
+ return false;
58
+ }
59
+ async function removeMalformedLockIfOld(lockPath, staleAfterMs) {
60
+ try {
61
+ const info = await stat(lockPath);
62
+ if (Date.now() - info.mtimeMs < staleAfterMs)
63
+ return false;
64
+ }
65
+ catch (error) {
66
+ return isNotFoundError(error);
67
+ }
68
+ await rm(lockPath, { force: true });
69
+ return true;
70
+ }
71
+ function isProcessAlive(pid) {
72
+ try {
73
+ process.kill(pid, 0);
74
+ return true;
75
+ }
76
+ catch (error) {
77
+ return !isNoSuchProcessError(error);
78
+ }
79
+ }
80
+ function sleep(ms) {
81
+ return new Promise((resolve) => {
82
+ setTimeout(resolve, ms);
83
+ });
84
+ }
85
+ function isFileExistsError(error) {
86
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
87
+ }
88
+ function isNotFoundError(error) {
89
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
90
+ }
91
+ function isNoSuchProcessError(error) {
92
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
93
+ }
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import { assertSafeSlug } from "../protocol/validation.js";
3
+ export function roomPaths(root, roomId) {
4
+ assertSafeSlug(roomId, "room id");
5
+ const rooms = path.join(root, "rooms");
6
+ const room = path.join(rooms, roomId);
7
+ return {
8
+ root,
9
+ rooms,
10
+ room,
11
+ state: path.join(room, "room.json"),
12
+ participants: path.join(room, "participants.json"),
13
+ brief: path.join(room, "brief.md"),
14
+ messages: path.join(room, "messages.jsonl"),
15
+ cursors: path.join(room, "cursors"),
16
+ lock: path.join(room, "write.lock")
17
+ };
18
+ }
@@ -0,0 +1,302 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { assertSafeSlug, buildMessage, clientMessageInputFromRecord, parseMentions } from "../protocol/index.js";
4
+ import { withWriterLock } from "./lock.js";
5
+ import { roomPaths } from "./paths.js";
6
+ import { appendSecureFile, ensureSecureDir, writeSecureFile } from "./secure-fs.js";
7
+ export const MAX_BRIEF_LENGTH = 16_000;
8
+ export class RoomLogFullError extends Error {
9
+ constructor() {
10
+ super("room message log is full");
11
+ }
12
+ }
13
+ export async function createRoom(options) {
14
+ assertSafeSlug(options.roomId, "room id");
15
+ assertSafeSlug(options.hostAlias, "host alias");
16
+ const now = options.now ?? new Date();
17
+ const paths = roomPaths(options.root, options.roomId);
18
+ await ensureRoomDirectories(paths);
19
+ const briefBody = options.briefBody ?? "";
20
+ assertBriefSize(briefBody);
21
+ const state = {
22
+ id: options.roomId,
23
+ status: "open",
24
+ attendance_policy: options.attendancePolicy ?? "manual-ok",
25
+ createdAt: now.toISOString(),
26
+ updatedAt: now.toISOString(),
27
+ next_message_id: 1,
28
+ brief_version: 1,
29
+ brief_updated_at: now.toISOString(),
30
+ brief_updated_by: options.hostAlias
31
+ };
32
+ if (options.expiresAt !== undefined) {
33
+ state.expires_at = options.expiresAt.toISOString();
34
+ }
35
+ await writeNewJson(paths.state, state);
36
+ await writeNewJson(paths.participants, []);
37
+ await writeSecureFile(paths.brief, briefBody, { flag: "wx" });
38
+ await writeSecureFile(paths.messages, "", { flag: "wx" });
39
+ return state;
40
+ }
41
+ export async function updateAttendancePolicy(options) {
42
+ assertSafeSlug(options.roomId, "room id");
43
+ assertSafeSlug(options.updatedBy, "updated by");
44
+ const now = options.now ?? new Date();
45
+ const paths = roomPaths(options.root, options.roomId);
46
+ return withWriterLock(paths.lock, async () => {
47
+ const state = withRoomDefaults(await readRoomState(paths));
48
+ const updatedState = {
49
+ ...state,
50
+ attendance_policy: options.policy,
51
+ updatedAt: now.toISOString()
52
+ };
53
+ await writeJson(paths.state, updatedState);
54
+ return updatedState;
55
+ });
56
+ }
57
+ export async function updateBrief(options) {
58
+ assertSafeSlug(options.roomId, "room id");
59
+ assertSafeSlug(options.updatedBy, "updated by");
60
+ assertBriefSize(options.body);
61
+ const now = options.now ?? new Date();
62
+ const paths = roomPaths(options.root, options.roomId);
63
+ return withWriterLock(paths.lock, async () => {
64
+ const state = await readRoomState(paths);
65
+ const updatedState = {
66
+ ...state,
67
+ updatedAt: now.toISOString(),
68
+ brief_version: state.brief_version + 1,
69
+ brief_updated_at: now.toISOString(),
70
+ brief_updated_by: options.updatedBy
71
+ };
72
+ await writeSecureFile(paths.brief, options.body);
73
+ await writeJson(paths.state, updatedState);
74
+ return {
75
+ body: options.body,
76
+ brief_version: updatedState.brief_version,
77
+ brief_updated_at: updatedState.brief_updated_at,
78
+ brief_updated_by: updatedState.brief_updated_by
79
+ };
80
+ });
81
+ }
82
+ export async function appendMessage(options) {
83
+ return (await appendMessageResult(options)).message;
84
+ }
85
+ export async function appendMessageResult(options) {
86
+ assertSafeSlug(options.roomId, "room id");
87
+ assertSafeSlug(options.from, "from");
88
+ const paths = roomPaths(options.root, options.roomId);
89
+ const input = clientMessageInputFromRecord(options.input);
90
+ const now = options.now ?? new Date();
91
+ return withWriterLock(paths.lock, async () => {
92
+ const state = await readRoomState(paths);
93
+ const participants = await readParticipants(paths);
94
+ const messages = await readJsonLines(paths.messages);
95
+ if (input.client_msg_id !== undefined) {
96
+ const existing = messages.find((message) => message.from === options.from && message.client_msg_id === input.client_msg_id);
97
+ if (existing !== undefined)
98
+ return { message: existing, idempotent: true };
99
+ }
100
+ if (options.maxMessages !== undefined && messages.length >= options.maxMessages) {
101
+ throw new RoomLogFullError();
102
+ }
103
+ const message = createMessage(input, {
104
+ id: state.next_message_id,
105
+ room: state.id,
106
+ from: options.from,
107
+ roster: participants.map((participant) => participant.alias),
108
+ now
109
+ });
110
+ await appendJsonLine(paths.messages, message);
111
+ await writeJson(paths.state, {
112
+ ...state,
113
+ updatedAt: now.toISOString(),
114
+ next_message_id: message.id + 1
115
+ });
116
+ return { message, idempotent: false };
117
+ });
118
+ }
119
+ export async function appendServerMessage(options) {
120
+ assertSafeSlug(options.roomId, "room id");
121
+ assertSafeSlug(options.from, "from");
122
+ const paths = roomPaths(options.root, options.roomId);
123
+ const now = options.now ?? new Date();
124
+ return withWriterLock(paths.lock, async () => {
125
+ const state = await readRoomState(paths);
126
+ const participants = await readParticipants(paths);
127
+ const message = buildMessage({ text: options.text }, {
128
+ id: state.next_message_id,
129
+ room: state.id,
130
+ from: options.from,
131
+ now,
132
+ mentions: parseMentions(options.text, participants.map((participant) => participant.alias)),
133
+ type: "system"
134
+ });
135
+ await appendJsonLine(paths.messages, message);
136
+ await writeJson(paths.state, {
137
+ ...state,
138
+ updatedAt: now.toISOString(),
139
+ next_message_id: message.id + 1
140
+ });
141
+ return message;
142
+ });
143
+ }
144
+ export async function readMessages(root, roomId) {
145
+ const paths = roomPaths(root, roomId);
146
+ return readJsonLines(paths.messages);
147
+ }
148
+ export async function readBrief(root, roomId) {
149
+ const paths = roomPaths(root, roomId);
150
+ const [state, body] = await Promise.all([readRoomState(paths), readFile(paths.brief, "utf8")]);
151
+ return {
152
+ body,
153
+ brief_version: state.brief_version,
154
+ brief_updated_at: state.brief_updated_at,
155
+ brief_updated_by: state.brief_updated_by
156
+ };
157
+ }
158
+ export async function readCursor(root, roomId, alias) {
159
+ assertSafeSlug(alias, "alias");
160
+ const paths = roomPaths(root, roomId);
161
+ try {
162
+ const record = await readJson(cursorPath(paths, alias));
163
+ return record.sinceId;
164
+ }
165
+ catch (error) {
166
+ if (isNotFoundError(error))
167
+ return 0;
168
+ throw error;
169
+ }
170
+ }
171
+ export async function writeCursor(root, roomId, alias, sinceId, now = new Date()) {
172
+ assertSafeSlug(alias, "alias");
173
+ if (!Number.isSafeInteger(sinceId) || sinceId < 0) {
174
+ throw new Error("cursor sinceId must be a non-negative safe integer");
175
+ }
176
+ const paths = roomPaths(root, roomId);
177
+ await ensureSecureDir(paths.cursors);
178
+ const record = {
179
+ alias,
180
+ sinceId,
181
+ updatedAt: now.toISOString()
182
+ };
183
+ await writeJson(cursorPath(paths, alias), record);
184
+ return record;
185
+ }
186
+ export async function writeParticipants(root, roomId, participants) {
187
+ const paths = roomPaths(root, roomId);
188
+ for (const participant of participants) {
189
+ assertSafeSlug(participant.alias, "participant alias");
190
+ }
191
+ await withWriterLock(paths.lock, async () => {
192
+ await writeJson(paths.participants, participants);
193
+ });
194
+ }
195
+ export async function upsertParticipant(root, roomId, participant) {
196
+ assertSafeSlug(participant.alias, "participant alias");
197
+ const paths = roomPaths(root, roomId);
198
+ await withWriterLock(paths.lock, async () => {
199
+ const participants = await readParticipants(paths);
200
+ const existingIndex = participants.findIndex((current) => current.alias === participant.alias);
201
+ if (existingIndex === -1) {
202
+ participants.push(participant);
203
+ }
204
+ else {
205
+ participants[existingIndex] = participant;
206
+ }
207
+ await writeJson(paths.participants, participants);
208
+ });
209
+ }
210
+ export async function closeRoom(root, roomId, now = new Date()) {
211
+ const paths = roomPaths(root, roomId);
212
+ return withWriterLock(paths.lock, async () => {
213
+ const state = await readRoomState(paths);
214
+ const closed = {
215
+ ...state,
216
+ status: "closed",
217
+ updatedAt: now.toISOString()
218
+ };
219
+ await writeJson(paths.state, closed);
220
+ return closed;
221
+ });
222
+ }
223
+ export async function readRoomState(paths) {
224
+ return withRoomDefaults(await readJson(paths.state));
225
+ }
226
+ export async function readParticipants(paths) {
227
+ return readJson(paths.participants);
228
+ }
229
+ export async function recoverNextMessageId(messagesPath) {
230
+ const messages = await readJsonLines(messagesPath);
231
+ return messages.reduce((highest, message) => Math.max(highest, message.id), 0) + 1;
232
+ }
233
+ export async function recoverRoomState(root, roomId) {
234
+ const paths = roomPaths(root, roomId);
235
+ return withWriterLock(paths.lock, async () => {
236
+ const state = await readRoomState(paths);
237
+ const recoveredNextId = await recoverNextMessageId(paths.messages);
238
+ if (state.next_message_id >= recoveredNextId)
239
+ return state;
240
+ const recoveredState = {
241
+ ...state,
242
+ next_message_id: recoveredNextId
243
+ };
244
+ await writeJson(paths.state, recoveredState);
245
+ return recoveredState;
246
+ });
247
+ }
248
+ function createMessage(input, options) {
249
+ return buildMessage(input, {
250
+ id: options.id,
251
+ room: options.room,
252
+ from: options.from,
253
+ now: options.now,
254
+ mentions: parseMentions(input.text, options.roster)
255
+ });
256
+ }
257
+ async function ensureRoomDirectories(paths) {
258
+ await ensureSecureDir(paths.root);
259
+ await ensureSecureDir(paths.rooms);
260
+ await ensureSecureDir(paths.room);
261
+ await ensureSecureDir(paths.cursors);
262
+ }
263
+ function assertBriefSize(body) {
264
+ if (body.length > MAX_BRIEF_LENGTH) {
265
+ throw new Error(`brief body must be <= ${MAX_BRIEF_LENGTH} characters`);
266
+ }
267
+ }
268
+ async function appendJsonLine(path, value) {
269
+ const line = `${JSON.stringify(value)}\n`;
270
+ await appendSecureFile(path, line);
271
+ }
272
+ async function readJson(path) {
273
+ const raw = await readFile(path, "utf8");
274
+ return JSON.parse(raw);
275
+ }
276
+ async function writeJson(path, value) {
277
+ await writeSecureFile(path, `${JSON.stringify(value, null, 2)}\n`);
278
+ }
279
+ function withRoomDefaults(state) {
280
+ return {
281
+ ...state,
282
+ attendance_policy: state.attendance_policy ?? "manual-ok"
283
+ };
284
+ }
285
+ async function writeNewJson(path, value) {
286
+ await writeSecureFile(path, `${JSON.stringify(value, null, 2)}\n`, { flag: "wx" });
287
+ }
288
+ async function readJsonLines(path) {
289
+ const raw = await readFile(path, "utf8");
290
+ if (raw.trim().length === 0)
291
+ return [];
292
+ return raw
293
+ .split("\n")
294
+ .filter((line) => line.length > 0)
295
+ .map((line) => JSON.parse(line));
296
+ }
297
+ function cursorPath(paths, alias) {
298
+ return path.join(paths.cursors, `${alias}.json`);
299
+ }
300
+ function isNotFoundError(error) {
301
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
302
+ }
@@ -0,0 +1,28 @@
1
+ import { chmod, mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export const SECURE_DIR_MODE = 0o700;
4
+ export const SECURE_FILE_MODE = 0o600;
5
+ export async function ensureSecureDir(dir) {
6
+ await mkdir(dir, { recursive: true, mode: SECURE_DIR_MODE });
7
+ await chmodIfPresent(dir, SECURE_DIR_MODE);
8
+ }
9
+ export async function writeSecureFile(file, data, options = {}) {
10
+ await ensureSecureDir(path.dirname(file));
11
+ await writeFile(file, data, { ...options, mode: SECURE_FILE_MODE });
12
+ await chmodIfPresent(file, SECURE_FILE_MODE);
13
+ }
14
+ export async function appendSecureFile(file, data) {
15
+ await ensureSecureDir(path.dirname(file));
16
+ await writeFile(file, data, { flag: "a", mode: SECURE_FILE_MODE });
17
+ await chmodIfPresent(file, SECURE_FILE_MODE);
18
+ }
19
+ async function chmodIfPresent(target, mode) {
20
+ try {
21
+ await chmod(target, mode);
22
+ }
23
+ catch (error) {
24
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
25
+ throw error;
26
+ }
27
+ }
28
+ }