agent-office-studio 0.7.0 → 0.9.1
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/CHANGELOG.md +61 -1
- package/README.md +13 -1
- package/bin/agentoffice.js +6 -1
- package/dist/assets/code-Dtdckl8K.js +32 -0
- package/dist/assets/index-DxWppRAd.css +1 -0
- package/dist/assets/index-Y1a-xw5C.js +5040 -0
- package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/{jetbrains-mono-cyrillic-400-normal-CIEtRijs.woff → jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff} +0 -0
- package/dist/assets/{jetbrains-mono-greek-400-normal-CNojz0t3.woff → jetbrains-mono-greek-400-normal-B9oWc5Lo.woff} +0 -0
- package/dist/assets/{jetbrains-mono-latin-400-normal-B11XCQ5g.woff → jetbrains-mono-latin-400-normal-6-qcROiO.woff} +0 -0
- package/dist/assets/{jetbrains-mono-latin-ext-400-normal-DHMOLxAi.woff → jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff} +0 -0
- package/dist/assets/{jetbrains-mono-vietnamese-400-normal-BeU3T8wf.woff → jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff} +0 -0
- package/dist/assets/pdf-BtCWNl9F.js +55 -0
- package/dist/assets/pdf-a7kwa8Fi.js +1 -0
- package/dist/assets/pdf.worker.min-BJd0dv5W.js +53 -0
- package/dist/assets/pdf.worker.min-D_0FwaWY.js +1 -0
- package/dist/index.html +32 -26
- package/dist/theme-boot.js +13 -0
- package/docs/desktop.md +52 -0
- package/docs/phone.md +51 -3
- package/docs/security.md +8 -3
- package/package.json +20 -2
- package/server/changes.js +13 -2
- package/server/chief.js +17 -0
- package/server/cloudflared.js +84 -0
- package/server/economy.js +478 -0
- package/server/files.js +104 -0
- package/server/guests.js +59 -21
- package/server/index.js +743 -59
- package/server/journeys.js +101 -0
- package/server/links.js +11 -4
- package/server/machine.js +164 -0
- package/server/office-mcp.js +29 -0
- package/server/phone.js +70 -0
- package/server/procstat.js +12 -6
- package/server/settings.js +5 -0
- package/server/talk.js +51 -0
- package/server/tunnel.js +136 -0
- package/server/visits.js +14 -4
- package/shared/shop.js +134 -0
- package/dist/assets/index-BtHSEs3B.js +0 -4475
- package/dist/assets/index-DVuFVmWF.css +0 -1
- package/dist/assets/inter-cyrillic-ext-wght-normal-B2xhLi22.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-CMZtQduZ.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-CGAr0uHJ.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CaVNZxsx.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-CFHvXkgd.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-C2S99t-D.woff2 +0 -0
package/server/guests.js
CHANGED
|
@@ -32,6 +32,9 @@ const REMOTE_LOG = 600;
|
|
|
32
32
|
const MAX_THREADS = 300;
|
|
33
33
|
const utcDay = (ms = Date.now()) => new Date(ms).toISOString().slice(0, 10);
|
|
34
34
|
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
35
|
+
// Handles and project names match exactly, ignoring case only: the relay
|
|
36
|
+
// treats bob-x and bobx as two people, and so must we.
|
|
37
|
+
const same = (a, b) => String(a).toLowerCase() === String(b).toLowerCase();
|
|
35
38
|
|
|
36
39
|
// ---------- what a host may put on our page ----------
|
|
37
40
|
//
|
|
@@ -152,13 +155,13 @@ export class Guests {
|
|
|
152
155
|
this.office = office;
|
|
153
156
|
this.copies = visits;
|
|
154
157
|
this.file = path.join(dataDir, 'guests.json');
|
|
155
|
-
this.used =
|
|
158
|
+
this.used = Object.create(null);
|
|
156
159
|
// Where each of our jobs at a host was asked for (chat thread and
|
|
157
160
|
// channel), and the project last used at each host.
|
|
158
161
|
this.asked = { threads: {}, lastRepo: {} };
|
|
159
162
|
try {
|
|
160
163
|
const saved = JSON.parse(fs.readFileSync(this.file, 'utf8'));
|
|
161
|
-
this.used = saved.used
|
|
164
|
+
this.used = Object.assign(Object.create(null), saved.used);
|
|
162
165
|
this.asked = { threads: saved.asked?.threads || {}, lastRepo: saved.asked?.lastRepo || {} };
|
|
163
166
|
} catch {}
|
|
164
167
|
this.remote = new Map(); // link id -> { link, host, staff, repos, clearance, jobs: Map(host job id -> job) }
|
|
@@ -207,6 +210,17 @@ export class Guests {
|
|
|
207
210
|
return mode;
|
|
208
211
|
}
|
|
209
212
|
|
|
213
|
+
// A job's mode, lowered to what the host allows now.
|
|
214
|
+
lowerMode(p, mode) {
|
|
215
|
+
return CLEARANCE.includes(mode) && CLEARANCE.indexOf(mode) <= CLEARANCE.indexOf(p.clearance) ? mode : p.clearance;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Which of our agents may take the job: only those still lent to the guest.
|
|
219
|
+
lend(job, p) {
|
|
220
|
+
job.pool = p.agents || undefined;
|
|
221
|
+
if (p.agents && job.wantAgent && !p.agents.includes(job.wantAgent)) job.wantAgent = null;
|
|
222
|
+
}
|
|
223
|
+
|
|
210
224
|
checkTokens(link, p) {
|
|
211
225
|
if (this.tokensToday(link.id) >= p.tokensPerDay) throw fail(429, 'You have used today’s tokens for this office. They reset at midnight UTC.');
|
|
212
226
|
}
|
|
@@ -229,12 +243,13 @@ export class Guests {
|
|
|
229
243
|
switch (op) {
|
|
230
244
|
case 'hello':
|
|
231
245
|
return {
|
|
232
|
-
staff: this.office.staff(),
|
|
246
|
+
staff: this.office.staff().filter((s) => !p.agents || p.agents.includes(s.id)),
|
|
233
247
|
modes: Object.fromEntries(Object.entries(this.office.modes()).filter(([k]) => CLEARANCE.indexOf(k) <= CLEARANCE.indexOf(p.clearance))),
|
|
234
248
|
jobs: this.jobsOf(link.id).map((j) => guestJob(j)),
|
|
235
249
|
policy: {
|
|
236
250
|
repos: p.repos, clearance: p.clearance, tokensPerDay: p.tokensPerDay, tokensToday: this.tokensToday(link.id),
|
|
237
251
|
ourAgents: p.ourAgents !== false, theirAgents: this.copies ? p.theirAgents || 'off' : 'off',
|
|
252
|
+
agents: p.agents || null,
|
|
238
253
|
},
|
|
239
254
|
// We can run a lent agent in one of the guest's own projects (createJob's `home`).
|
|
240
255
|
home: true,
|
|
@@ -258,17 +273,23 @@ export class Guests {
|
|
|
258
273
|
this.checkTokens(link, p);
|
|
259
274
|
if (this.jobsOf(link.id, true).length >= MAX_OPEN_JOBS) throw fail(429, `You can have ${MAX_OPEN_JOBS} jobs going at once here. Wait for one to finish.`);
|
|
260
275
|
const prompt = String(args.prompt ?? '').slice(0, MAX_PROMPT);
|
|
276
|
+
const agentId = typeof args.agentId === 'string' ? args.agentId : '';
|
|
277
|
+
if (p.agents && agentId && !p.agents.includes(agentId)) throw fail(403, 'That agent isn’t lent to you.');
|
|
261
278
|
const job = this.office.createJob(
|
|
262
|
-
{ repo: home ? `${home.repo}@${home.host}` : args.repo, prompt, mode, model: typeof args.model === 'string' ? args.model : '', agentId
|
|
263
|
-
{ from: `@${link.peer}`, guest: { link: link.id }, ...(home && { away: home }) },
|
|
279
|
+
{ repo: home ? `${home.repo}@${home.host}` : args.repo, prompt, mode, model: typeof args.model === 'string' ? args.model : '', agentId },
|
|
280
|
+
{ from: `@${link.peer}`, guest: { link: link.id }, ...(home && { away: home }), ...(p.agents && { pool: p.agents }) },
|
|
264
281
|
);
|
|
265
282
|
return { job: guestJob(job) };
|
|
266
283
|
}
|
|
267
284
|
case 'reply': {
|
|
268
285
|
if (p.ourAgents === false) throw fail(403, 'This office doesn’t lend its agents any more.');
|
|
269
286
|
const job = this.ownJob(link, args.job);
|
|
270
|
-
|
|
287
|
+
// Held to the host's limits as they are now: a job started at Full
|
|
288
|
+
// autonomy, in a project since unshared, doesn't carry on as it was.
|
|
289
|
+
const mode = args.mode === undefined ? this.lowerMode(p, job.mode) : this.capMode(p, args.mode);
|
|
290
|
+
if (!job.away && !p.repos.includes(job.repo)) throw fail(403, 'That project isn’t shared with you any more.');
|
|
271
291
|
this.checkTokens(link, p);
|
|
292
|
+
this.lend(job, p);
|
|
272
293
|
this.office.replyJob(job, { message: String(args.text ?? '').slice(0, MAX_PROMPT), mode });
|
|
273
294
|
return { ok: true };
|
|
274
295
|
}
|
|
@@ -288,17 +309,33 @@ export class Guests {
|
|
|
288
309
|
// host, as an away job's `away`. The relay names both ends of a link, so a
|
|
289
310
|
// guest can only point us at its own office.
|
|
290
311
|
async homeFolder(link, repo) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const back = !e && this.links.links.find((l) => l.kind === 'guest' && l.role === 'guest' && norm(l.peer) === norm(link.peer));
|
|
312
|
+
let e = [...this.remote.values()].find((x) => same(x.host, link.peer));
|
|
313
|
+
const back = !e && this.links.links.find((l) => l.kind === 'guest' && l.role === 'guest' && same(l.peer, link.peer));
|
|
294
314
|
if (back) e = await this.remoteHello(back).catch(() => null);
|
|
295
315
|
if (!e) throw fail(403, `To use our agents in your own projects, make @${this.links.status().handle || 'us'} your guest too, with their agents allowed in your projects.`);
|
|
296
316
|
if (!['read', 'edit'].includes(e.theirAgents)) throw fail(403, `You haven’t let @${this.links.status().handle || 'us'}’s agents work in your projects. Turn that on where we’re your guest.`);
|
|
297
|
-
const r = e.repos.find((x) =>
|
|
317
|
+
const r = e.repos.find((x) => same(x, repo));
|
|
298
318
|
if (!r) throw fail(403, `You haven’t shared ${repo} with @${this.links.status().handle || 'us'}. Add it where we’re your guest.`);
|
|
299
319
|
return { link: e.link, host: e.host, repo: r, access: e.theirAgents };
|
|
300
320
|
}
|
|
301
321
|
|
|
322
|
+
// The host changed a guest's limits: open jobs the new ones rule out stop,
|
|
323
|
+
// and queued ones drop to the clearance and agents now allowed. A running
|
|
324
|
+
// session can't change its mode, so one above the new clearance stops too.
|
|
325
|
+
policyChanged(linkId) {
|
|
326
|
+
const p = this.links.guestPolicy(linkId);
|
|
327
|
+
for (const job of this.jobsOf(linkId, true)) {
|
|
328
|
+
const why = !p || p.ourAgents === false ? 'This office stopped lending its agents.'
|
|
329
|
+
: !job.away && !p.repos.includes(job.repo) ? 'That project isn’t shared with the guest any more.'
|
|
330
|
+
: job.status === 'working' && this.lowerMode(p, job.mode) !== job.mode ? 'The host lowered what this guest’s jobs may do.'
|
|
331
|
+
: job.status === 'working' && p.agents && !p.agents.includes(job.agentId) ? 'That agent isn’t lent to this guest any more.'
|
|
332
|
+
: null;
|
|
333
|
+
if (why) { this.office.stopJob(job, `Stopped: ${why}`); continue; }
|
|
334
|
+
job.mode = this.lowerMode(p, job.mode);
|
|
335
|
+
this.lend(job, p);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
302
339
|
// Tokens a guest job used: counted against its guest, and a guest over its
|
|
303
340
|
// day's allowance has its running jobs stopped.
|
|
304
341
|
addTokens(job, n) {
|
|
@@ -308,11 +345,14 @@ export class Guests {
|
|
|
308
345
|
const u = this.used[linkId]?.day === day ? this.used[linkId] : { day, tokens: 0 };
|
|
309
346
|
u.tokens += n;
|
|
310
347
|
this.used[linkId] = u;
|
|
311
|
-
this.save();
|
|
312
348
|
const p = this.links.guestPolicy(linkId);
|
|
313
349
|
if (p && u.tokens >= p.tokensPerDay) {
|
|
350
|
+
this.save();
|
|
314
351
|
for (const j of this.jobsOf(linkId, true)) this.office.stopJob(j, 'Stopped: the guest used today’s tokens.');
|
|
352
|
+
return;
|
|
315
353
|
}
|
|
354
|
+
// This runs on every API call, so the file is written at most every few seconds.
|
|
355
|
+
if (!this.saveTimer) { this.saveTimer = setTimeout(() => { this.saveTimer = null; this.save(); }, 5000); this.saveTimer.unref?.(); }
|
|
316
356
|
}
|
|
317
357
|
|
|
318
358
|
save() {
|
|
@@ -323,7 +363,7 @@ export class Guests {
|
|
|
323
363
|
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
324
364
|
const threads = Object.entries(this.asked.threads);
|
|
325
365
|
if (threads.length > MAX_THREADS) this.asked.threads = Object.fromEntries(threads.slice(-MAX_THREADS));
|
|
326
|
-
fs.writeFileSync(this.file + '.tmp', JSON.stringify({ used: this.used, asked: this.asked }));
|
|
366
|
+
fs.writeFileSync(this.file + '.tmp', JSON.stringify({ used: this.used, asked: this.asked }), { mode: 0o600 });
|
|
327
367
|
fs.renameSync(this.file + '.tmp', this.file);
|
|
328
368
|
} catch (err) { console.error('[guests] could not save', err.message); }
|
|
329
369
|
}
|
|
@@ -611,7 +651,7 @@ export class Guests {
|
|
|
611
651
|
const offices = this.remoteStatus();
|
|
612
652
|
const find = (name, host) => {
|
|
613
653
|
for (const o of offices) {
|
|
614
|
-
if (host &&
|
|
654
|
+
if (host && !same(o.host, host)) continue;
|
|
615
655
|
const s = o.staff.find((x) => x.here && norm(x.name) === norm(name));
|
|
616
656
|
if (s) return { link: o.link, host: o.host, staff: s };
|
|
617
657
|
}
|
|
@@ -636,14 +676,13 @@ export class Guests {
|
|
|
636
676
|
// file tools come back to us over the link where the host is our guest
|
|
637
677
|
// (visits.js), so that link must let their agents into this project.
|
|
638
678
|
lentHome(entry, repo) {
|
|
639
|
-
const norm = (v) => String(v).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
640
679
|
const h = entry.host;
|
|
641
680
|
if (!entry.home) throw fail(400, `@${h}’s office is on an older version that can only run its agents in its own projects. Ask @${h} to update.`);
|
|
642
|
-
const back = this.links.links.find((l) => l.kind === 'guest' && l.role === 'host' &&
|
|
681
|
+
const back = this.links.links.find((l) => l.kind === 'guest' && l.role === 'host' && same(l.peer, h));
|
|
643
682
|
const p = back && this.links.guestPolicy(back.id);
|
|
644
683
|
if (!p) throw fail(400, `To use @${h}’s agents in #${repo}, make @${h} your guest too, with their agents allowed in your projects.`);
|
|
645
684
|
if (!['read', 'edit'].includes(p.theirAgents)) throw fail(400, `To use @${h}’s agents in #${repo}, let their agents work in your projects: Change, where @${h} is your guest.`);
|
|
646
|
-
const r = p.repos.find((x) =>
|
|
685
|
+
const r = p.repos.find((x) => same(x, repo));
|
|
647
686
|
if (!r) throw fail(400, `You haven’t shared ${repo} with @${h}. Add it under Change, where @${h} is your guest, and ask again.`);
|
|
648
687
|
return r;
|
|
649
688
|
}
|
|
@@ -657,11 +696,11 @@ export class Guests {
|
|
|
657
696
|
const tags = [...text.matchAll(/(^|\s)#([\w.-]+)/g)];
|
|
658
697
|
// A project of ours, not theirs: their agent would run in their folder
|
|
659
698
|
// and never see ours. Say so, rather than quietly working somewhere else.
|
|
660
|
-
const ours = tags.find((t) => !repos.some((r) =>
|
|
699
|
+
const ours = tags.find((t) => !repos.some((r) => same(r, t[2])) && this.office.repoPath(t[2]));
|
|
661
700
|
if (ours) return { repo: this.lentHome(entry, ours[2]), home: true, text: text.replace(ours[0], ' ').replace(/[ \t]+/g, ' ').trim() };
|
|
662
701
|
if (repos.length === 1) return { repo: repos[0], text };
|
|
663
702
|
for (const t of tags) {
|
|
664
|
-
const repo = repos.find((r) =>
|
|
703
|
+
const repo = repos.find((r) => same(r, t[2]));
|
|
665
704
|
if (repo) return { repo, text: text.replace(t[0], ' ').replace(/[ \t]+/g, ' ').trim() };
|
|
666
705
|
}
|
|
667
706
|
const said = norm(text);
|
|
@@ -680,10 +719,9 @@ export class Guests {
|
|
|
680
719
|
|
|
681
720
|
// `#web@friend`: a host's project our agents may work in, or null.
|
|
682
721
|
awayFolder(repo, host) {
|
|
683
|
-
const norm = (v) => String(v).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
684
722
|
for (const e of this.remote.values()) {
|
|
685
|
-
if (
|
|
686
|
-
const r = e.repos.find((x) =>
|
|
723
|
+
if (!same(e.host, host) || !['read', 'edit'].includes(e.theirAgents)) continue;
|
|
724
|
+
const r = e.repos.find((x) => same(x, repo));
|
|
687
725
|
if (r) return { link: e.link, host: e.host, repo: r, access: e.theirAgents };
|
|
688
726
|
}
|
|
689
727
|
return null;
|