@ai-ecoverse/slicc-agent 5.2.0 → 5.4.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 (78) hide show
  1. package/README.md +18 -5
  2. package/dist/agent.js +6 -1
  3. package/dist/agent.js.map +2 -2
  4. package/dist/client.js +7 -13
  5. package/dist/client.js.map +2 -2
  6. package/dist/control.js +10 -4
  7. package/dist/control.js.map +2 -2
  8. package/dist/guests/attribution.js +22 -0
  9. package/dist/guests/attribution.js.map +7 -0
  10. package/dist/guests/client.js +138 -0
  11. package/dist/guests/client.js.map +7 -0
  12. package/dist/guests/guard.js +38 -0
  13. package/dist/guests/guard.js.map +7 -0
  14. package/dist/guests/index.js +282 -0
  15. package/dist/guests/index.js.map +7 -0
  16. package/dist/guests/seats.js +49 -0
  17. package/dist/guests/seats.js.map +7 -0
  18. package/dist/guests/serve.js +72 -0
  19. package/dist/guests/serve.js.map +7 -0
  20. package/dist/guests/view.js +40 -0
  21. package/dist/guests/view.js.map +7 -0
  22. package/dist/host.js +91 -3
  23. package/dist/host.js.map +2 -2
  24. package/dist/index.js +9 -2
  25. package/dist/index.js.map +2 -2
  26. package/dist/owner.js +2 -1
  27. package/dist/owner.js.map +2 -2
  28. package/dist/page.js +8 -0
  29. package/dist/page.js.map +2 -2
  30. package/dist/secrets/extension.js +4 -1
  31. package/dist/secrets/extension.js.map +2 -2
  32. package/dist/services.js +12 -0
  33. package/dist/services.js.map +2 -2
  34. package/dist/serving.js +7 -0
  35. package/dist/serving.js.map +2 -2
  36. package/dist/spectrum/agent-port.js +14 -6
  37. package/dist/spectrum/agent-port.js.map +2 -2
  38. package/dist/spectrum/approvals-port.js +1 -1
  39. package/dist/spectrum/approvals-port.js.map +2 -2
  40. package/dist/spectrum/guest-port.js +56 -0
  41. package/dist/spectrum/guest-port.js.map +7 -0
  42. package/dist/spectrum/index.js +20 -5
  43. package/dist/spectrum/index.js.map +3 -3
  44. package/dist/spectrum/messages.js +30 -8
  45. package/dist/spectrum/messages.js.map +2 -2
  46. package/dist/spectrum/notices-port.js +2 -17
  47. package/dist/spectrum/notices-port.js.map +2 -2
  48. package/dist/spectrum/proposals.js +56 -0
  49. package/dist/spectrum/proposals.js.map +7 -0
  50. package/dist/types/client.d.ts +7 -2
  51. package/dist/types/control.d.ts +2 -1
  52. package/dist/types/guests/attribution.d.ts +9 -0
  53. package/dist/types/guests/client.d.ts +12 -0
  54. package/dist/types/guests/guard.d.ts +5 -0
  55. package/dist/types/guests/index.d.ts +54 -0
  56. package/dist/types/guests/seats.d.ts +39 -0
  57. package/dist/types/guests/serve.d.ts +17 -0
  58. package/dist/types/guests/view.d.ts +3 -0
  59. package/dist/types/host.d.ts +19 -2
  60. package/dist/types/index.d.ts +4 -1
  61. package/dist/types/owner.d.ts +4 -0
  62. package/dist/types/page.d.ts +1 -0
  63. package/dist/types/services.d.ts +34 -2
  64. package/dist/types/serving.d.ts +8 -1
  65. package/dist/types/spectrum/agent-port.d.ts +0 -2
  66. package/dist/types/spectrum/guest-port.d.ts +26 -0
  67. package/dist/types/spectrum/index.d.ts +14 -1
  68. package/dist/types/spectrum/notices-port.d.ts +1 -3
  69. package/dist/types/spectrum/proposals.d.ts +8 -0
  70. package/dist/types/wire.d.ts +0 -7
  71. package/dist/wire.js +2 -12
  72. package/dist/wire.js.map +2 -2
  73. package/dist/worker.js +2 -1
  74. package/dist/worker.js.map +2 -2
  75. package/package.json +5 -6
  76. package/dist/mismatch.js +0 -41
  77. package/dist/mismatch.js.map +0 -7
  78. package/dist/types/mismatch.d.ts +0 -3
@@ -0,0 +1,138 @@
1
+ import { createHost, isChordError, replicatedState } from "@earendil-works/chord";
2
+ import { lasting } from "../client.js";
3
+ import {
4
+ AgentGuestService,
5
+ CONTROL_METHODS
6
+ } from "../services.js";
7
+ import { AGENT_HOST, kept, plain, replicated } from "../serving.js";
8
+ import { PortLink } from "../wire.js";
9
+ const GUEST_ONLY = "a guest can only watch and send messages";
10
+ const SEAT_REFUSED = "This guest link isn't valid any more.";
11
+ const SAID = {
12
+ revoked: "This guest seat was revoked.",
13
+ empty: "There is nothing to send.",
14
+ "too-long": "That message is too long for a guest seat.",
15
+ gone: "The cone this seat shares is gone.",
16
+ unavailable: "Messages from guests need a review, and this agent can't review them yet."
17
+ };
18
+ const proposedSentence = (reason) => SAID[reason] ?? "The message was not delivered.";
19
+ const refusal = (message, code = "EACCES") => Object.assign(new Error(message), { code });
20
+ async function connectGuest(endpoint) {
21
+ const link = new PortLink(endpoint);
22
+ let serverId;
23
+ try {
24
+ serverId = await link.hello;
25
+ } catch {
26
+ throw refusal(SEAT_REFUSED);
27
+ }
28
+ const host = createHost({ name: `guest:${crypto.randomUUID()}`, kind: "client" });
29
+ host.connect(link);
30
+ const handle = host.get(AgentGuestService, {}, { at: AGENT_HOST });
31
+ const fail = async (error) => {
32
+ handle.release();
33
+ link.close("handshake failed");
34
+ await host.close();
35
+ throw error;
36
+ };
37
+ try {
38
+ await Promise.race([
39
+ handle.ready,
40
+ link.ended.then(() => {
41
+ throw refusal(SEAT_REFUSED);
42
+ })
43
+ ]);
44
+ } catch (error) {
45
+ return fail(isChordError(error) && error.code === "forbidden" ? refusal(SEAT_REFUSED) : error);
46
+ }
47
+ if (handle.kind !== "guest") return fail(refusal(SEAT_REFUSED));
48
+ const transcriptMember = handle.transcript;
49
+ const seatMember = handle.seat;
50
+ const proposalsMember = handle.proposals;
51
+ const keeping = [transcriptMember, seatMember, proposalsMember].map(kept);
52
+ await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), link.ended]);
53
+ const call = handle.propose;
54
+ const propose = async (request) => {
55
+ try {
56
+ return await host.root((ctx) => call([request], ctx), {});
57
+ } catch (error) {
58
+ throw plain(error);
59
+ }
60
+ };
61
+ const guest = seatMember.value;
62
+ const refuse = () => Promise.reject(refusal(GUEST_ONLY));
63
+ const control = Object.fromEntries(
64
+ CONTROL_METHODS.map((name) => [name, refuse])
65
+ );
66
+ control.send = async (request) => {
67
+ if (request.attachments?.length)
68
+ return {
69
+ accepted: false,
70
+ submissionId: null,
71
+ error: { code: "EACCES", message: "A guest can't attach files." }
72
+ };
73
+ const proposed = await propose({ text: request.text, requestId: request.requestId });
74
+ if (proposed.state === "submitted")
75
+ return { accepted: true, submissionId: proposed.submissionId, error: null };
76
+ if (proposed.state === "pending")
77
+ return { accepted: true, submissionId: proposed.approval, error: null };
78
+ return {
79
+ accepted: false,
80
+ submissionId: null,
81
+ error: { code: "EACCES", message: proposedSentence(proposed.reason) }
82
+ };
83
+ };
84
+ const transcript = lasting(replicated(transcriptMember));
85
+ const proposals = lasting(replicated(proposalsMember));
86
+ let open = true;
87
+ const finish = () => {
88
+ if (!open) return;
89
+ open = false;
90
+ transcript.freeze();
91
+ proposals.freeze();
92
+ for (const keep of keeping) keep.stop();
93
+ handle.release();
94
+ };
95
+ const closed = link.ended.then((reason) => {
96
+ finish();
97
+ return reason;
98
+ });
99
+ const settings = {
100
+ state: replicatedState({ models: [], accounts: [] }),
101
+ connect: refuse,
102
+ disconnect: refuse,
103
+ signIn: refuse,
104
+ dismissMoved: refuse
105
+ };
106
+ return {
107
+ serverId,
108
+ guest,
109
+ control,
110
+ transcript: transcript.state,
111
+ proposals: proposals.state,
112
+ deliveries: replicatedState({}),
113
+ agents: replicatedState({
114
+ active: guest.agentId,
115
+ agents: [
116
+ { id: guest.agentId, name: guest.name, kind: "cone", parentId: null, role: null }
117
+ ]
118
+ }),
119
+ settings,
120
+ closed,
121
+ propose,
122
+ prompt: refuse,
123
+ webhook: refuse,
124
+ async close() {
125
+ finish();
126
+ link.close("closed");
127
+ await closed;
128
+ await host.close();
129
+ }
130
+ };
131
+ }
132
+ export {
133
+ GUEST_ONLY,
134
+ SEAT_REFUSED,
135
+ connectGuest,
136
+ proposedSentence
137
+ };
138
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/guests/client.ts"],
4
+ "sourcesContent": ["import { createHost, isChordError, type Readable, replicatedState } from '@earendil-works/chord';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport { type AgentConnection, lasting } from '../client.ts';\nimport {\n type AgentControl,\n AgentGuestService,\n type AgentSettings,\n CONTROL_METHODS,\n type GuestProposal,\n type GuestShown,\n type SendResponse,\n} from '../services.ts';\nimport { AGENT_HOST, kept, plain, type Remote, replicated } from '../serving.ts';\nimport { type PortEndpoint, PortLink } from '../wire.ts';\nimport type { Proposed, ProposeRequest } from './index.ts';\n\nexport const GUEST_ONLY = 'a guest can only watch and send messages';\nexport const SEAT_REFUSED = \"This guest link isn't valid any more.\";\n\nconst SAID: Record<string, string> = {\n revoked: 'This guest seat was revoked.',\n empty: 'There is nothing to send.',\n 'too-long': 'That message is too long for a guest seat.',\n gone: 'The cone this seat shares is gone.',\n unavailable: \"Messages from guests need a review, and this agent can't review them yet.\",\n};\n\nexport const proposedSentence = (reason: string): string =>\n SAID[reason] ?? 'The message was not delivered.';\n\nconst refusal = (message: string, code = 'EACCES') => Object.assign(new Error(message), { code });\n\nexport interface GuestConnection extends AgentConnection {\n readonly guest: GuestShown;\n propose(request: ProposeRequest): Promise<Proposed>;\n}\n\nexport async function connectGuest(endpoint: PortEndpoint): Promise<GuestConnection> {\n const link = new PortLink(endpoint);\n let serverId: string;\n try {\n serverId = await link.hello;\n } catch {\n throw refusal(SEAT_REFUSED);\n }\n const host = createHost({ name: `guest:${crypto.randomUUID()}`, kind: 'client' });\n host.connect(link);\n const handle = host.get(AgentGuestService, {}, { at: AGENT_HOST }) as unknown as Remote;\n const fail = async (error: unknown): Promise<never> => {\n handle.release();\n link.close('handshake failed');\n await host.close();\n throw error;\n };\n try {\n await Promise.race([\n handle.ready,\n link.ended.then(() => {\n throw refusal(SEAT_REFUSED);\n }),\n ]);\n } catch (error) {\n return fail(isChordError(error) && error.code === 'forbidden' ? refusal(SEAT_REFUSED) : error);\n }\n if (handle.kind !== 'guest') return fail(refusal(SEAT_REFUSED));\n const transcriptMember = handle.transcript as Readable<ConversationView | undefined>;\n const seatMember = handle.seat as Readable<GuestShown | undefined>;\n const proposalsMember = handle.proposals as Readable<GuestProposal[] | undefined>;\n const keeping = [transcriptMember, seatMember, proposalsMember].map(kept);\n await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), link.ended]);\n const call = handle.propose as (args: unknown[], ctx: unknown) => Promise<Proposed>;\n const propose = async (request: ProposeRequest) => {\n try {\n return await host.root((ctx) => call([request], ctx), {});\n } catch (error) {\n throw plain(error);\n }\n };\n const guest = seatMember.value as GuestShown;\n const refuse = () => Promise.reject(refusal(GUEST_ONLY));\n const control = Object.fromEntries(\n CONTROL_METHODS.map((name) => [name, refuse])\n ) as unknown as AgentControl;\n control.send = async (request): Promise<SendResponse> => {\n if (request.attachments?.length)\n return {\n accepted: false,\n submissionId: null,\n error: { code: 'EACCES', message: \"A guest can't attach files.\" },\n };\n const proposed = await propose({ text: request.text, requestId: request.requestId });\n if (proposed.state === 'submitted')\n return { accepted: true, submissionId: proposed.submissionId, error: null };\n if (proposed.state === 'pending')\n return { accepted: true, submissionId: proposed.approval, error: null };\n return {\n accepted: false,\n submissionId: null,\n error: { code: 'EACCES', message: proposedSentence(proposed.reason) },\n };\n };\n const transcript = lasting(replicated(transcriptMember));\n const proposals = lasting(replicated(proposalsMember));\n let open = true;\n const finish = () => {\n if (!open) return;\n open = false;\n transcript.freeze();\n proposals.freeze();\n for (const keep of keeping) keep.stop();\n handle.release();\n };\n const closed = link.ended.then((reason) => {\n finish();\n return reason;\n });\n const settings: AgentSettings = {\n state: replicatedState({ models: [], accounts: [] }),\n connect: refuse,\n disconnect: refuse,\n signIn: refuse,\n dismissMoved: refuse,\n };\n return {\n serverId,\n guest,\n control,\n transcript: transcript.state,\n proposals: proposals.state,\n deliveries: replicatedState({}),\n agents: replicatedState({\n active: guest.agentId,\n agents: [\n { id: guest.agentId, name: guest.name, kind: 'cone' as const, parentId: null, role: null },\n ],\n }),\n settings,\n closed,\n propose,\n prompt: refuse,\n webhook: refuse,\n async close() {\n finish();\n link.close('closed');\n await closed;\n await host.close();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,38 @@
1
+ import {
2
+ defineExtension,
3
+ hook,
4
+ LiveDoc,
5
+ ToolTask
6
+ } from "@earendil-works/pi-durable";
7
+ const GUEST_GUARD = "slicc-guest-guard";
8
+ const GUEST_REQUEST = "guest:";
9
+ const GUEST_TOOLS_BLOCKED = "Tool calls in a run a guest started need a review, which this agent can't do yet.";
10
+ function guestGuard(harness) {
11
+ return defineExtension({
12
+ name: GUEST_GUARD,
13
+ hooks: [
14
+ hook(ToolTask, {
15
+ async beforeTool(_call, api, context) {
16
+ const live = await api.snapshot(LiveDoc, api.conversationId, context);
17
+ const inputs = live?.run?.inputs ?? [];
18
+ if (!inputs.length) return void 0;
19
+ const held = harness();
20
+ if (!held) return { block: GUEST_TOOLS_BLOCKED };
21
+ for (const id of inputs) {
22
+ const submission = await held.submission(id, context);
23
+ const record = submission ? await submission.status(context) : void 0;
24
+ if (record?.requestId?.startsWith(GUEST_REQUEST)) return { block: GUEST_TOOLS_BLOCKED };
25
+ }
26
+ return void 0;
27
+ }
28
+ })
29
+ ]
30
+ });
31
+ }
32
+ export {
33
+ GUEST_GUARD,
34
+ GUEST_REQUEST,
35
+ GUEST_TOOLS_BLOCKED,
36
+ guestGuard
37
+ };
38
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/guests/guard.ts"],
4
+ "sourcesContent": ["import {\n defineExtension,\n type Extension,\n type Harness,\n hook,\n LiveDoc,\n ToolTask,\n} from '@earendil-works/pi-durable';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const GUEST_TOOLS_BLOCKED =\n \"Tool calls in a run a guest started need a review, which this agent can't do yet.\";\n\nexport function guestGuard(harness: () => Pick<Harness, 'submission'> | undefined): Extension {\n return defineExtension({\n name: GUEST_GUARD,\n hooks: [\n hook(ToolTask, {\n async beforeTool(_call, api, context) {\n const live = await api.snapshot(LiveDoc, api.conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n if (!inputs.length) return undefined;\n const held = harness();\n if (!held) return { block: GUEST_TOOLS_BLOCKED };\n for (const id of inputs) {\n const submission = await held.submission(id, context);\n const record = submission ? await submission.status(context) : undefined;\n if (record?.requestId?.startsWith(GUEST_REQUEST)) return { block: GUEST_TOOLS_BLOCKED };\n }\n return undefined;\n },\n }),\n ],\n });\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,cAAc;AACpB,MAAM,gBAAgB;AACtB,MAAM,sBACX;AAEK,SAAS,WAAW,SAAmE;AAC5F,SAAO,gBAAgB;AAAA,IACrB,MAAM;AAAA,IACN,OAAO;AAAA,MACL,KAAK,UAAU;AAAA,QACb,MAAM,WAAW,OAAO,KAAK,SAAS;AACpC,gBAAM,OAAO,MAAM,IAAI,SAAS,SAAS,IAAI,gBAAgB,OAAO;AACpE,gBAAM,SAAS,MAAM,KAAK,UAAU,CAAC;AACrC,cAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,gBAAM,OAAO,QAAQ;AACrB,cAAI,CAAC,KAAM,QAAO,EAAE,OAAO,oBAAoB;AAC/C,qBAAW,MAAM,QAAQ;AACvB,kBAAM,aAAa,MAAM,KAAK,WAAW,IAAI,OAAO;AACpD,kBAAM,SAAS,aAAa,MAAM,WAAW,OAAO,OAAO,IAAI;AAC/D,gBAAI,QAAQ,WAAW,WAAW,aAAa,EAAG,QAAO,EAAE,OAAO,oBAAoB;AAAA,UACxF;AACA,iBAAO;AAAA,QACT;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -0,0 +1,282 @@
1
+ import { replicatedState } from "@earendil-works/chord";
2
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
3
+ import { InboxDoc } from "@earendil-works/pi-durable";
4
+ import { scrubText } from "../secrets/scrub.js";
5
+ import { GUEST_INPUT_KIND, guestContent } from "./attribution.js";
6
+ import { GUEST_REQUEST } from "./guard.js";
7
+ import {
8
+ checkDraft,
9
+ cleanLabel,
10
+ GuestsDoc,
11
+ liveSeat,
12
+ MAX_SEAT_MS
13
+ } from "./seats.js";
14
+ const MAX_PROPOSAL = 32e3;
15
+ const MAX_REQUEST = 128;
16
+ const KEPT_DELIVERED = 100;
17
+ const LONGEST_TIMER = 2 ** 31 - 1;
18
+ const copy = (value) => JSON.parse(JSON.stringify(value));
19
+ const unanswered = (reason) => ({
20
+ state: "unanswered",
21
+ submissionId: null,
22
+ reason
23
+ });
24
+ function prune(all, at) {
25
+ for (const [id, record] of Object.entries(all)) {
26
+ const over = record.revoked ?? record.expiresAt;
27
+ if (over !== null && at - over > MAX_SEAT_MS) delete all[id];
28
+ }
29
+ }
30
+ async function withdrawQueued(harness, agents, record, using) {
31
+ const conversation = await agents.conversation(record.agentId, using);
32
+ if (!conversation) return;
33
+ const queued = await harness.commit(
34
+ async (tx) => (await tx.doc(InboxDoc, conversation.id)).items.filter((item) => {
35
+ const entry = item.entry;
36
+ return entry?.kind === GUEST_INPUT_KIND && entry.data?.seat === record.id;
37
+ }).map((item) => item.id),
38
+ using
39
+ );
40
+ for (const id of queued) await harness.abortSubmission(id, using, conversation.id);
41
+ }
42
+ async function deliver(record, text, approval, requestId, agents, using) {
43
+ const conversation = await agents.conversation(record.agentId, using);
44
+ if (!conversation) return unanswered("gone");
45
+ const data = { seat: record.id, name: record.label, approval };
46
+ const submitted = await conversation.submit(
47
+ {
48
+ type: "input",
49
+ content: guestContent(record.label, text),
50
+ whenBusy: "followUp",
51
+ entry: { kind: GUEST_INPUT_KIND, data: { ...data } },
52
+ requestId
53
+ },
54
+ using
55
+ );
56
+ return { state: "submitted", submissionId: String(submitted.id), reason: null };
57
+ }
58
+ async function prepare(record, request) {
59
+ const text = typeof request?.text === "string" ? request.text.trim() : "";
60
+ if (!text) return unanswered("empty");
61
+ if (text.length > MAX_PROPOSAL) return unanswered("too-long");
62
+ const asked = typeof request.requestId === "string" && request.requestId && request.requestId.length <= MAX_REQUEST ? request.requestId : crypto.randomUUID();
63
+ return { text: await scrubText(text), requestId: `${GUEST_REQUEST}${record.id}:${asked}` };
64
+ }
65
+ async function place(record, prepared, options, using) {
66
+ const gate = record.gates.messages;
67
+ if (gate === "off")
68
+ return deliver(record, prepared.text, null, prepared.requestId, options.agents, using);
69
+ if (!options.approvals) return unanswered("unavailable");
70
+ const item = await options.approvals.request(
71
+ {
72
+ kind: "guest-message",
73
+ seat: record.id,
74
+ agentId: record.agentId,
75
+ requester: { kind: "guest", name: record.label, seat: record.id },
76
+ tier: gate,
77
+ detail: { text: prepared.text, attachments: [] }
78
+ },
79
+ using
80
+ );
81
+ if (item.state !== "pending") return unanswered(item.reason ?? item.state);
82
+ return { state: "pending", submissionId: null, approval: item.id, reason: null };
83
+ }
84
+ function keyedLine() {
85
+ const locks = /* @__PURE__ */ new Map();
86
+ return (id, run) => {
87
+ const next = (locks.get(id) ?? Promise.resolve()).then(run, run);
88
+ const settled = next.catch(() => void 0);
89
+ locks.set(id, settled);
90
+ void settled.then(() => {
91
+ if (locks.get(id) === settled) locks.delete(id);
92
+ });
93
+ return next;
94
+ };
95
+ }
96
+ const pendingIn = (approvals) => (id) => (approvals?.state.value ?? []).filter((item) => item.seat === id && item.state === "pending").length;
97
+ function shownSeats(records, links, at, pendingOf) {
98
+ return Object.values(records).filter((record) => liveSeat(record, at)).sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)).map((record) => ({
99
+ id: record.id,
100
+ label: record.label,
101
+ agentId: record.agentId,
102
+ createdAt: record.createdAt,
103
+ expiresAt: record.expiresAt,
104
+ gates: { ...record.gates },
105
+ connected: (links.get(record.id) ?? 0) > 0,
106
+ pending: pendingOf(record.id)
107
+ }));
108
+ }
109
+ async function createGuests(options) {
110
+ const { harness, agents } = options;
111
+ const now = options.now ?? Date.now;
112
+ const context = options.context ?? BACKGROUND_CONTEXT;
113
+ const records = await harness.commit(
114
+ async (tx) => copy((await tx.doc(GuestsDoc)).seats),
115
+ context
116
+ );
117
+ const links = /* @__PURE__ */ new Map();
118
+ const ended = /* @__PURE__ */ new Set();
119
+ const gone = /* @__PURE__ */ new Set();
120
+ const shown = () => shownSeats(records, links, now(), pendingIn(options.approvals));
121
+ const seats = replicatedState(shown());
122
+ let timer;
123
+ const locked = keyedLine();
124
+ const cleanup = (record, using) => locked(record.id, async () => {
125
+ await withdrawQueued(harness, agents, record, using);
126
+ await options.approvals?.withdrawSeat(record.id, using);
127
+ });
128
+ const end = (id, using = context) => {
129
+ const record = records[id];
130
+ if (gone.has(id) || !record) return Promise.resolve();
131
+ gone.add(id);
132
+ for (const listener of [...ended]) listener(id);
133
+ return cleanup(record, using);
134
+ };
135
+ const publish = () => {
136
+ const at = now();
137
+ for (const record of Object.values(records))
138
+ if (!liveSeat(record, at)) void end(record.id).catch(() => void 0);
139
+ seats.replace(context, shown());
140
+ if (timer) clearTimeout(timer);
141
+ timer = void 0;
142
+ const next = Math.min(
143
+ ...Object.values(records).filter((record) => liveSeat(record, at) && record.expiresAt !== null).map((record) => record.expiresAt)
144
+ );
145
+ if (Number.isFinite(next))
146
+ timer = setTimeout(publish, Math.min(Math.max(0, next - at), LONGEST_TIMER));
147
+ };
148
+ publish();
149
+ const write = (change) => harness.commit(async (tx) => {
150
+ const doc = await tx.doc(GuestsDoc);
151
+ const changed = change(doc.seats);
152
+ return changed ? copy(changed) : void 0;
153
+ }, context);
154
+ const delivering = /* @__PURE__ */ new Set();
155
+ const deliverOne = (seat, item) => locked(seat, async () => {
156
+ const record = records[seat];
157
+ if (!record || !liveSeat(record, now())) return;
158
+ await deliverLocked(record, item);
159
+ });
160
+ const deliverLocked = async (record, item) => {
161
+ const text = item.detail.text ?? "";
162
+ const requestId = `${GUEST_REQUEST}${record.id}:${item.id}`;
163
+ const sent = await deliver(record, text, item.id, requestId, agents, context);
164
+ if (sent.state !== "submitted") return;
165
+ const changed = await write((all) => {
166
+ const kept = all[record.id];
167
+ if (!kept) return void 0;
168
+ kept.delivered = [...kept.delivered ?? [], item.id].slice(-KEPT_DELIVERED);
169
+ return kept;
170
+ });
171
+ if (changed) records[record.id] = changed;
172
+ };
173
+ const approved = (items) => {
174
+ for (const item of items) {
175
+ if (item.kind !== "guest-message" || item.state !== "approved" || !item.seat) continue;
176
+ const record = records[item.seat];
177
+ if (!record || !liveSeat(record, now()) || delivering.has(item.id)) continue;
178
+ if (record.delivered?.includes(item.id)) continue;
179
+ delivering.add(item.id);
180
+ void deliverOne(record.id, item).catch(() => void 0);
181
+ }
182
+ };
183
+ const offPending = options.approvals?.state.subscribe((items) => {
184
+ seats.replace(context, shown());
185
+ approved(items);
186
+ });
187
+ approved(options.approvals?.state.value ?? []);
188
+ const current = (guest) => {
189
+ const record = records[guest.id];
190
+ return record && liveSeat(record, now()) && record.epoch === guest.epoch ? record : void 0;
191
+ };
192
+ return {
193
+ seats,
194
+ async open(draft, using) {
195
+ const at = now();
196
+ const problem = checkDraft(draft, at);
197
+ if (problem) return { seat: null, error: problem };
198
+ if (!agents.state().cones[draft.agentId])
199
+ return { seat: null, error: `There is no cone ${draft.agentId}.` };
200
+ const record = {
201
+ id: draft.id,
202
+ label: cleanLabel(draft.label),
203
+ agentId: draft.agentId,
204
+ expiresAt: draft.expiresAt,
205
+ gates: { messages: draft.gates.messages, tools: draft.gates.tools },
206
+ createdAt: at,
207
+ epoch: 1,
208
+ revoked: null
209
+ };
210
+ const kept = await harness.commit(async (tx) => {
211
+ const doc = await tx.doc(GuestsDoc);
212
+ if (doc.seats[record.id]) return null;
213
+ prune(doc.seats, at);
214
+ doc.seats[record.id] = copy(record);
215
+ return copy(doc.seats);
216
+ }, using);
217
+ if (!kept) return { seat: null, error: "That guest seat already exists." };
218
+ for (const id of Object.keys(records)) delete records[id];
219
+ Object.assign(records, kept);
220
+ publish();
221
+ return { seat: shown().find((seat) => seat.id === record.id), error: null };
222
+ },
223
+ async revoke(id, using) {
224
+ const at = now();
225
+ const changed = await write((all) => {
226
+ const record = all[id];
227
+ if (!record || record.revoked !== null) return void 0;
228
+ record.revoked = at;
229
+ record.epoch += 1;
230
+ return record;
231
+ });
232
+ if (!changed) return { revoked: false };
233
+ records[id] = changed;
234
+ const ending = end(id, using);
235
+ publish();
236
+ await ending;
237
+ return { revoked: true };
238
+ },
239
+ admit(id, label) {
240
+ const record = records[id];
241
+ if (!record || !liveSeat(record, now()) || record.label !== cleanLabel(label)) return null;
242
+ return { id: record.id, name: record.label, epoch: record.epoch };
243
+ },
244
+ record: (id) => records[id],
245
+ current,
246
+ joined(id) {
247
+ links.set(id, (links.get(id) ?? 0) + 1);
248
+ seats.replace(context, shown());
249
+ let left = false;
250
+ return () => {
251
+ if (left) return;
252
+ left = true;
253
+ links.set(id, (links.get(id) ?? 1) - 1);
254
+ seats.replace(context, shown());
255
+ };
256
+ },
257
+ onEnd(listener) {
258
+ ended.add(listener);
259
+ return () => ended.delete(listener);
260
+ },
261
+ async propose(guest, request, using) {
262
+ const record = current(guest);
263
+ if (!record) return unanswered("revoked");
264
+ const prepared = await prepare(record, request);
265
+ if ("state" in prepared) return prepared;
266
+ return locked(record.id, async () => {
267
+ const still = current(guest);
268
+ return still ? place(still, prepared, options, using) : unanswered("revoked");
269
+ });
270
+ },
271
+ dispose() {
272
+ if (timer) clearTimeout(timer);
273
+ offPending?.();
274
+ ended.clear();
275
+ }
276
+ };
277
+ }
278
+ export {
279
+ MAX_PROPOSAL,
280
+ createGuests
281
+ };
282
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/guests/index.ts"],
4
+ "sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { type Harness, InboxDoc } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { scrubText } from '../secrets/scrub.ts';\nimport type { GuestPrincipal } from '../serving.ts';\nimport { GUEST_INPUT_KIND, type GuestInput, guestContent } from './attribution.ts';\nimport { GUEST_REQUEST } from './guard.ts';\nimport {\n checkDraft,\n cleanLabel,\n type GuestDraft,\n type GuestSeat,\n GuestsDoc,\n liveSeat,\n MAX_SEAT_MS,\n type SeatRecord,\n} from './seats.ts';\n\nexport type GuestOpened = { seat: GuestSeat; error: null } | { seat: null; error: string };\n\nexport type Proposed =\n | { state: 'submitted'; submissionId: string; reason: null }\n | { state: 'pending'; submissionId: null; approval: string; reason: null }\n | { state: 'unanswered'; submissionId: null; reason: string };\n\nexport interface ProposeRequest {\n text: string;\n requestId: string | null;\n}\n\nexport interface Guests {\n readonly seats: ReplicatedState<GuestSeat[]>;\n open(draft: GuestDraft, context: Context): Promise<GuestOpened>;\n revoke(id: string, context: Context): Promise<{ revoked: boolean }>;\n admit(id: string, label: string): GuestPrincipal | null;\n record(id: string): SeatRecord | undefined;\n current(guest: GuestPrincipal): SeatRecord | undefined;\n joined(id: string): () => void;\n onEnd(listener: (id: string) => void): () => void;\n propose(guest: GuestPrincipal, request: ProposeRequest, context: Context): Promise<Proposed>;\n dispose(): void;\n}\n\nexport interface GuestsOptions {\n harness: Pick<Harness, 'commit' | 'abortSubmission'>;\n agents: Pick<Agents, 'state' | 'conversation'>;\n approvals?: Pick<Approvals, 'state' | 'request' | 'withdrawSeat'>;\n now?: () => number;\n context?: Context;\n}\n\nexport const MAX_PROPOSAL = 32_000;\nconst MAX_REQUEST = 128;\nconst KEPT_DELIVERED = 100;\nconst LONGEST_TIMER = 2 ** 31 - 1;\n\nconst copy = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;\n\nconst unanswered = (reason: string): Proposed => ({\n state: 'unanswered',\n submissionId: null,\n reason,\n});\n\nfunction prune(all: Record<string, SeatRecord>, at: number) {\n for (const [id, record] of Object.entries(all)) {\n const over = record.revoked ?? record.expiresAt;\n if (over !== null && at - over > MAX_SEAT_MS) delete all[id];\n }\n}\n\nasync function withdrawQueued(\n harness: GuestsOptions['harness'],\n agents: GuestsOptions['agents'],\n record: SeatRecord,\n using: Context\n) {\n const conversation = await agents.conversation(record.agentId, using);\n if (!conversation) return;\n const queued = await harness.commit(\n async (tx) =>\n (await tx.doc(InboxDoc, conversation.id)).items\n .filter((item) => {\n const entry = item.entry as { kind?: string; data?: { seat?: unknown } } | undefined;\n return entry?.kind === GUEST_INPUT_KIND && entry.data?.seat === record.id;\n })\n .map((item) => item.id),\n using\n );\n for (const id of queued) await harness.abortSubmission(id, using, conversation.id);\n}\n\nasync function deliver(\n record: SeatRecord,\n text: string,\n approval: string | null,\n requestId: string,\n agents: GuestsOptions['agents'],\n using: Context\n): Promise<Proposed> {\n const conversation = await agents.conversation(record.agentId, using);\n if (!conversation) return unanswered('gone');\n const data: GuestInput = { seat: record.id, name: record.label, approval };\n const submitted = await conversation.submit(\n {\n type: 'input',\n content: guestContent(record.label, text),\n whenBusy: 'followUp',\n entry: { kind: GUEST_INPUT_KIND, data: { ...data } },\n requestId,\n },\n using\n );\n return { state: 'submitted', submissionId: String(submitted.id), reason: null };\n}\n\ntype Prepared = { text: string; requestId: string };\n\nasync function prepare(record: SeatRecord, request: ProposeRequest): Promise<Prepared | Proposed> {\n const text = typeof request?.text === 'string' ? request.text.trim() : '';\n if (!text) return unanswered('empty');\n if (text.length > MAX_PROPOSAL) return unanswered('too-long');\n const asked =\n typeof request.requestId === 'string' &&\n request.requestId &&\n request.requestId.length <= MAX_REQUEST\n ? request.requestId\n : crypto.randomUUID();\n return { text: await scrubText(text), requestId: `${GUEST_REQUEST}${record.id}:${asked}` };\n}\n\nasync function place(\n record: SeatRecord,\n prepared: Prepared,\n options: GuestsOptions,\n using: Context\n): Promise<Proposed> {\n const gate = record.gates.messages;\n if (gate === 'off')\n return deliver(record, prepared.text, null, prepared.requestId, options.agents, using);\n if (!options.approvals) return unanswered('unavailable');\n const item = await options.approvals.request(\n {\n kind: 'guest-message',\n seat: record.id,\n agentId: record.agentId,\n requester: { kind: 'guest', name: record.label, seat: record.id },\n tier: gate,\n detail: { text: prepared.text, attachments: [] },\n },\n using\n );\n if (item.state !== 'pending') return unanswered(item.reason ?? item.state);\n return { state: 'pending', submissionId: null, approval: item.id, reason: null };\n}\n\nfunction keyedLine() {\n const locks = new Map<string, Promise<unknown>>();\n return <T>(id: string, run: () => Promise<T>): Promise<T> => {\n const next = (locks.get(id) ?? Promise.resolve()).then(run, run);\n const settled = next.catch(() => undefined);\n locks.set(id, settled);\n void settled.then(() => {\n if (locks.get(id) === settled) locks.delete(id);\n });\n return next;\n };\n}\n\nconst pendingIn =\n (approvals: GuestsOptions['approvals']) =>\n (id: string): number =>\n (approvals?.state.value ?? []).filter((item) => item.seat === id && item.state === 'pending')\n .length;\n\nfunction shownSeats(\n records: Record<string, SeatRecord>,\n links: ReadonlyMap<string, number>,\n at: number,\n pendingOf: (id: string) => number\n): GuestSeat[] {\n return Object.values(records)\n .filter((record) => liveSeat(record, at))\n .sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id))\n .map((record) => ({\n id: record.id,\n label: record.label,\n agentId: record.agentId,\n createdAt: record.createdAt,\n expiresAt: record.expiresAt,\n gates: { ...record.gates },\n connected: (links.get(record.id) ?? 0) > 0,\n pending: pendingOf(record.id),\n }));\n}\n\nexport async function createGuests(options: GuestsOptions): Promise<Guests> {\n const { harness, agents } = options;\n const now = options.now ?? Date.now;\n const context = options.context ?? BACKGROUND_CONTEXT;\n const records: Record<string, SeatRecord> = await harness.commit(\n async (tx) => copy((await tx.doc(GuestsDoc)).seats),\n context\n );\n const links = new Map<string, number>();\n const ended = new Set<(id: string) => void>();\n const gone = new Set<string>();\n const shown = () => shownSeats(records, links, now(), pendingIn(options.approvals));\n const seats = replicatedState<GuestSeat[]>(shown());\n let timer: ReturnType<typeof setTimeout> | undefined;\n const locked = keyedLine();\n const cleanup = (record: SeatRecord, using: Context) =>\n locked(record.id, async () => {\n await withdrawQueued(harness, agents, record, using);\n await options.approvals?.withdrawSeat(record.id, using);\n });\n const end = (id: string, using: Context = context): Promise<void> => {\n const record = records[id];\n if (gone.has(id) || !record) return Promise.resolve();\n gone.add(id);\n for (const listener of [...ended]) listener(id);\n return cleanup(record, using);\n };\n const publish = () => {\n const at = now();\n for (const record of Object.values(records))\n if (!liveSeat(record, at)) void end(record.id).catch(() => undefined);\n seats.replace(context, shown());\n if (timer) clearTimeout(timer);\n timer = undefined;\n const next = Math.min(\n ...Object.values(records)\n .filter((record) => liveSeat(record, at) && record.expiresAt !== null)\n .map((record) => record.expiresAt as number)\n );\n if (Number.isFinite(next))\n timer = setTimeout(publish, Math.min(Math.max(0, next - at), LONGEST_TIMER));\n };\n publish();\n const write = (change: (seats: Record<string, SeatRecord>) => SeatRecord | undefined) =>\n harness.commit(async (tx) => {\n const doc = await tx.doc(GuestsDoc);\n const changed = change(doc.seats);\n return changed ? copy(changed) : undefined;\n }, context);\n const delivering = new Set<string>();\n const deliverOne = (seat: string, item: Approval) =>\n locked(seat, async () => {\n const record = records[seat];\n if (!record || !liveSeat(record, now())) return;\n await deliverLocked(record, item);\n });\n const deliverLocked = async (record: SeatRecord, item: Approval) => {\n const text = (item.detail as { text?: string }).text ?? '';\n const requestId = `${GUEST_REQUEST}${record.id}:${item.id}`;\n const sent = await deliver(record, text, item.id, requestId, agents, context);\n if (sent.state !== 'submitted') return;\n const changed = await write((all) => {\n const kept = all[record.id];\n if (!kept) return undefined;\n kept.delivered = [...(kept.delivered ?? []), item.id].slice(-KEPT_DELIVERED);\n return kept;\n });\n if (changed) records[record.id] = changed;\n };\n const approved = (items: readonly Approval[]) => {\n for (const item of items) {\n if (item.kind !== 'guest-message' || item.state !== 'approved' || !item.seat) continue;\n const record = records[item.seat];\n if (!record || !liveSeat(record, now()) || delivering.has(item.id)) continue;\n if (record.delivered?.includes(item.id)) continue;\n delivering.add(item.id);\n void deliverOne(record.id, item).catch(() => undefined);\n }\n };\n const offPending = options.approvals?.state.subscribe((items) => {\n seats.replace(context, shown());\n approved(items);\n });\n approved(options.approvals?.state.value ?? []);\n const current = (guest: GuestPrincipal) => {\n const record = records[guest.id];\n return record && liveSeat(record, now()) && record.epoch === guest.epoch ? record : undefined;\n };\n return {\n seats,\n async open(draft, using) {\n const at = now();\n const problem = checkDraft(draft, at);\n if (problem) return { seat: null, error: problem };\n if (!agents.state().cones[draft.agentId])\n return { seat: null, error: `There is no cone ${draft.agentId}.` };\n const record: SeatRecord = {\n id: draft.id,\n label: cleanLabel(draft.label),\n agentId: draft.agentId,\n expiresAt: draft.expiresAt,\n gates: { messages: draft.gates.messages, tools: draft.gates.tools },\n createdAt: at,\n epoch: 1,\n revoked: null,\n };\n const kept = await harness.commit(async (tx) => {\n const doc = await tx.doc(GuestsDoc);\n if (doc.seats[record.id]) return null;\n prune(doc.seats, at);\n doc.seats[record.id] = copy(record);\n return copy(doc.seats);\n }, using);\n if (!kept) return { seat: null, error: 'That guest seat already exists.' };\n for (const id of Object.keys(records)) delete records[id];\n Object.assign(records, kept);\n publish();\n return { seat: shown().find((seat) => seat.id === record.id) as GuestSeat, error: null };\n },\n async revoke(id, using) {\n const at = now();\n const changed = await write((all) => {\n const record = all[id];\n if (!record || record.revoked !== null) return undefined;\n record.revoked = at;\n record.epoch += 1;\n return record;\n });\n if (!changed) return { revoked: false };\n records[id] = changed;\n const ending = end(id, using);\n publish();\n await ending;\n return { revoked: true };\n },\n admit(id, label) {\n const record = records[id];\n if (!record || !liveSeat(record, now()) || record.label !== cleanLabel(label)) return null;\n return { id: record.id, name: record.label, epoch: record.epoch };\n },\n record: (id) => records[id],\n current,\n joined(id) {\n links.set(id, (links.get(id) ?? 0) + 1);\n seats.replace(context, shown());\n let left = false;\n return () => {\n if (left) return;\n left = true;\n links.set(id, (links.get(id) ?? 1) - 1);\n seats.replace(context, shown());\n };\n },\n onEnd(listener) {\n ended.add(listener);\n return () => ended.delete(listener);\n },\n async propose(guest, request, using) {\n const record = current(guest);\n if (!record) return unanswered('revoked');\n const prepared = await prepare(record, request);\n if ('state' in prepared) return prepared;\n return locked(record.id, async () => {\n const still = current(guest);\n return still ? place(still, prepared, options, using) : unanswered('revoked');\n });\n },\n dispose() {\n if (timer) clearTimeout(timer);\n offPending?.();\n ended.clear();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,49 @@
1
+ import { defineDoc } from "@earendil-works/pi-durable";
2
+ const GuestsDoc = defineDoc({
3
+ kind: "slicc.guests",
4
+ version: 1,
5
+ scope: "session",
6
+ initial: () => ({ seats: {} })
7
+ });
8
+ const MAX_SEAT_MS = 30 * 24 * 60 * 60 * 1e3;
9
+ const MAX_LABEL = 64;
10
+ const MAX_ID = 256;
11
+ function cleanLabel(raw) {
12
+ return String(raw ?? "").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim().slice(0, MAX_LABEL);
13
+ }
14
+ function isGate(value) {
15
+ if (typeof value !== "string") return false;
16
+ if (value === "user" || value === "agent" || value === "cone" || value === "off") return true;
17
+ return /^scoop:\S+$/.test(value);
18
+ }
19
+ function checkDraft(draft, now) {
20
+ if (typeof draft?.id !== "string" || !draft.id || draft.id.length > MAX_ID)
21
+ return "A guest seat needs an id from the tray hub.";
22
+ if (!cleanLabel(draft.label)) return "A guest seat needs a label.";
23
+ if (!isGate(draft.gates?.messages) || !isGate(draft.gates?.tools))
24
+ return "A guest seat needs a known gate for messages and for tools.";
25
+ if (draft.gates.tools === "off" || draft.gates.tools === "cone")
26
+ return "A guest's tool calls need a review: by a person, the judge or a scoop.";
27
+ const { expiresAt } = draft;
28
+ if (expiresAt !== null && (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)))
29
+ return "A guest seat expires at a time, or never.";
30
+ if (expiresAt !== null && expiresAt <= now) return "That expiry has already passed.";
31
+ if (expiresAt !== null && expiresAt - now > MAX_SEAT_MS)
32
+ return "A guest seat lasts at most 30 days.";
33
+ return null;
34
+ }
35
+ function liveSeat(seat, now) {
36
+ return Boolean(
37
+ seat && seat.revoked === null && (seat.expiresAt === null || seat.expiresAt > now)
38
+ );
39
+ }
40
+ export {
41
+ GuestsDoc,
42
+ MAX_LABEL,
43
+ MAX_SEAT_MS,
44
+ checkDraft,
45
+ cleanLabel,
46
+ isGate,
47
+ liveSeat
48
+ };
49
+ //# sourceMappingURL=seats.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/guests/seats.ts"],
4
+ "sourcesContent": ["import { defineDoc } from '@earendil-works/pi-durable';\nimport type { ApprovalTier } from '../approvals/record.ts';\n\nexport type GuestGate = ApprovalTier | 'off';\n\nexport type GuestGates = {\n messages: GuestGate;\n tools: GuestGate;\n};\n\nexport type GuestDraft = {\n id: string;\n label: string;\n agentId: string;\n expiresAt: number | null;\n gates: GuestGates;\n};\n\nexport type SeatRecord = GuestDraft & {\n createdAt: number;\n epoch: number;\n revoked: number | null;\n delivered?: string[];\n};\n\nexport interface GuestSeat {\n id: string;\n label: string;\n agentId: string;\n createdAt: number;\n expiresAt: number | null;\n gates: GuestGates;\n connected: boolean;\n pending: number;\n}\n\nexport type GuestsState = { seats: Record<string, SeatRecord> };\n\nexport const GuestsDoc = defineDoc<GuestsState>({\n kind: 'slicc.guests',\n version: 1,\n scope: 'session',\n initial: () => ({ seats: {} }),\n});\n\nexport const MAX_SEAT_MS = 30 * 24 * 60 * 60 * 1000;\nexport const MAX_LABEL = 64;\nconst MAX_ID = 256;\n\nexport function cleanLabel(raw: unknown): string {\n return String(raw ?? '')\n .replace(/[\\p{Cc}\\p{Cf}]/gu, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, MAX_LABEL);\n}\n\nexport function isGate(value: unknown): value is GuestGate {\n if (typeof value !== 'string') return false;\n if (value === 'user' || value === 'agent' || value === 'cone' || value === 'off') return true;\n return /^scoop:\\S+$/.test(value);\n}\n\nexport function checkDraft(draft: GuestDraft, now: number): string | null {\n if (typeof draft?.id !== 'string' || !draft.id || draft.id.length > MAX_ID)\n return 'A guest seat needs an id from the tray hub.';\n if (!cleanLabel(draft.label)) return 'A guest seat needs a label.';\n if (!isGate(draft.gates?.messages) || !isGate(draft.gates?.tools))\n return 'A guest seat needs a known gate for messages and for tools.';\n if (draft.gates.tools === 'off' || draft.gates.tools === 'cone')\n return \"A guest's tool calls need a review: by a person, the judge or a scoop.\";\n const { expiresAt } = draft;\n if (expiresAt !== null && (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)))\n return 'A guest seat expires at a time, or never.';\n if (expiresAt !== null && expiresAt <= now) return 'That expiry has already passed.';\n if (expiresAt !== null && expiresAt - now > MAX_SEAT_MS)\n return 'A guest seat lasts at most 30 days.';\n return null;\n}\n\nexport function liveSeat(seat: SeatRecord | undefined, now: number): boolean {\n return Boolean(\n seat && seat.revoked === null && (seat.expiresAt === null || seat.expiresAt > now)\n );\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAsCnB,MAAM,YAAY,UAAuB;AAAA,EAC9C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,OAAO,CAAC,EAAE;AAC9B,CAAC;AAEM,MAAM,cAAc,KAAK,KAAK,KAAK,KAAK;AACxC,MAAM,YAAY;AACzB,MAAM,SAAS;AAER,SAAS,WAAW,KAAsB;AAC/C,SAAO,OAAO,OAAO,EAAE,EACpB,QAAQ,oBAAoB,GAAG,EAC/B,QAAQ,QAAQ,GAAG,EACnB,KAAK,EACL,MAAM,GAAG,SAAS;AACvB;AAEO,SAAS,OAAO,OAAoC;AACzD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,UAAU,UAAU,UAAU,WAAW,UAAU,UAAU,UAAU,MAAO,QAAO;AACzF,SAAO,cAAc,KAAK,KAAK;AACjC;AAEO,SAAS,WAAW,OAAmB,KAA4B;AACxE,MAAI,OAAO,OAAO,OAAO,YAAY,CAAC,MAAM,MAAM,MAAM,GAAG,SAAS;AAClE,WAAO;AACT,MAAI,CAAC,WAAW,MAAM,KAAK,EAAG,QAAO;AACrC,MAAI,CAAC,OAAO,MAAM,OAAO,QAAQ,KAAK,CAAC,OAAO,MAAM,OAAO,KAAK;AAC9D,WAAO;AACT,MAAI,MAAM,MAAM,UAAU,SAAS,MAAM,MAAM,UAAU;AACvD,WAAO;AACT,QAAM,EAAE,UAAU,IAAI;AACtB,MAAI,cAAc,SAAS,OAAO,cAAc,YAAY,CAAC,OAAO,SAAS,SAAS;AACpF,WAAO;AACT,MAAI,cAAc,QAAQ,aAAa,IAAK,QAAO;AACnD,MAAI,cAAc,QAAQ,YAAY,MAAM;AAC1C,WAAO;AACT,SAAO;AACT;AAEO,SAAS,SAAS,MAA8B,KAAsB;AAC3E,SAAO;AAAA,IACL,QAAQ,KAAK,YAAY,SAAS,KAAK,cAAc,QAAQ,KAAK,YAAY;AAAA,EAChF;AACF;",
6
+ "names": []
7
+ }