@ai-ecoverse/slicc-agent 5.4.2 → 5.5.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 (43) hide show
  1. package/README.md +25 -7
  2. package/dist/approvals/index.js +2 -1
  3. package/dist/approvals/index.js.map +2 -2
  4. package/dist/approvals/policy.js +110 -0
  5. package/dist/approvals/policy.js.map +7 -0
  6. package/dist/client.js +1 -0
  7. package/dist/client.js.map +2 -2
  8. package/dist/control.js.map +2 -2
  9. package/dist/guests/client.js +6 -3
  10. package/dist/guests/client.js.map +2 -2
  11. package/dist/guests/index.js +4 -1
  12. package/dist/guests/index.js.map +2 -2
  13. package/dist/guests/policy.js +75 -0
  14. package/dist/guests/policy.js.map +7 -0
  15. package/dist/guests/seats.js +16 -6
  16. package/dist/guests/seats.js.map +2 -2
  17. package/dist/guests/serve.js +8 -2
  18. package/dist/guests/serve.js.map +2 -2
  19. package/dist/host.js +35 -28
  20. package/dist/host.js.map +2 -2
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +2 -2
  23. package/dist/services.js +12 -3
  24. package/dist/services.js.map +2 -2
  25. package/dist/spectrum/approvals-port.js +1 -1
  26. package/dist/spectrum/approvals-port.js.map +1 -1
  27. package/dist/spectrum/guest-port.js +14 -4
  28. package/dist/spectrum/guest-port.js.map +2 -2
  29. package/dist/spectrum/index.js +3 -1
  30. package/dist/spectrum/index.js.map +2 -2
  31. package/dist/types/approvals/index.d.ts +2 -1
  32. package/dist/types/approvals/policy.d.ts +38 -0
  33. package/dist/types/client.d.ts +2 -0
  34. package/dist/types/control.d.ts +1 -1
  35. package/dist/types/guests/index.d.ts +4 -0
  36. package/dist/types/guests/policy.d.ts +23 -0
  37. package/dist/types/guests/seats.d.ts +2 -1
  38. package/dist/types/host.d.ts +1 -3
  39. package/dist/types/index.d.ts +2 -0
  40. package/dist/types/services.d.ts +11 -3
  41. package/dist/types/spectrum/guest-port.d.ts +8 -3
  42. package/dist/types/spectrum/index.d.ts +1 -1
  43. package/package.json +1 -1
@@ -50,19 +50,22 @@ async function connectGuest(endpoint) {
50
50
  const proposalsMember = handle.proposals;
51
51
  const keeping = [transcriptMember, seatMember, proposalsMember].map(kept);
52
52
  await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), link.ended]);
53
- const call = handle.propose;
54
- const propose = async (request) => {
53
+ const through = (name) => async (...args) => {
54
+ const call = handle[name];
55
55
  try {
56
- return await host.root((ctx) => call([request], ctx), {});
56
+ return await host.root((ctx) => call(args, ctx), {});
57
57
  } catch (error) {
58
58
  throw plain(error);
59
59
  }
60
60
  };
61
+ const propose = (request) => through("propose")(request);
61
62
  const guest = seatMember.value;
62
63
  const refuse = () => Promise.reject(refusal(GUEST_ONLY));
63
64
  const control = Object.fromEntries(
64
65
  CONTROL_METHODS.map((name) => [name, refuse])
65
66
  );
67
+ control.guestOpen = (draft) => through("guestOpen")(draft);
68
+ control.guestRevoke = (id) => through("guestRevoke")(id);
66
69
  control.send = async (request) => {
67
70
  if (request.attachments?.length)
68
71
  return {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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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",
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 GUEST_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 { GuestOpened, 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 through =\n <T>(name: (typeof GUEST_METHODS)[number]) =>\n async (...args: unknown[]): Promise<T> => {\n const call = handle[name] as (args: unknown[], ctx: unknown) => Promise<T>;\n try {\n return await host.root((ctx) => call(args, ctx), {});\n } catch (error) {\n throw plain(error);\n }\n };\n const propose = (request: ProposeRequest) => through<Proposed>('propose')(request);\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.guestOpen = (draft) => through<GuestOpened>('guestOpen')(draft);\n control.guestRevoke = (id) => through<{ revoked: boolean }>('guestRevoke')(id);\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
6
  "names": []
7
7
  }
@@ -4,6 +4,7 @@ import { InboxDoc } from "@earendil-works/pi-durable";
4
4
  import { scrubText } from "../secrets/scrub.js";
5
5
  import { GUEST_INPUT_KIND, guestContent } from "./attribution.js";
6
6
  import { GUEST_REQUEST } from "./guard.js";
7
+ import { DEFAULT_GUEST_POLICY } from "./policy.js";
7
8
  import {
8
9
  checkDraft,
9
10
  cleanLabel,
@@ -110,6 +111,7 @@ async function createGuests(options) {
110
111
  const { harness, agents } = options;
111
112
  const now = options.now ?? Date.now;
112
113
  const context = options.context ?? BACKGROUND_CONTEXT;
114
+ const guestPolicy = () => options.policy?.current().guests ?? DEFAULT_GUEST_POLICY;
113
115
  const records = await harness.commit(
114
116
  async (tx) => copy((await tx.doc(GuestsDoc)).seats),
115
117
  context
@@ -193,7 +195,7 @@ async function createGuests(options) {
193
195
  seats,
194
196
  async open(draft, using) {
195
197
  const at = now();
196
- const problem = checkDraft(draft, at);
198
+ const problem = checkDraft(draft, at, guestPolicy());
197
199
  if (problem) return { seat: null, error: problem };
198
200
  if (!agents.state().cones[draft.agentId])
199
201
  return { seat: null, error: `There is no cone ${draft.agentId}.` };
@@ -268,6 +270,7 @@ async function createGuests(options) {
268
270
  return still ? place(still, prepared, options, using) : unanswered("revoked");
269
271
  });
270
272
  },
273
+ policy: guestPolicy,
271
274
  dispose() {
272
275
  if (timer) clearTimeout(timer);
273
276
  offPending?.();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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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",
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 type { ApprovalPolicies } from '../approvals/policy.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 { DEFAULT_GUEST_POLICY, type GuestPolicy } from './policy.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 policy(): GuestPolicy;\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 policy?: Pick<ApprovalPolicies, 'current'>;\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 guestPolicy = () => options.policy?.current().guests ?? DEFAULT_GUEST_POLICY;\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, guestPolicy());\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 policy: guestPolicy,\n dispose() {\n if (timer) clearTimeout(timer);\n offPending?.();\n ended.clear();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,75 @@
1
+ const GATE_KINDS = ["user", "agent", "cone", "scoop", "off"];
2
+ const DEFAULT_GUEST_POLICY = {
3
+ manageGuests: "deny",
4
+ allowedToolGates: ["user", "agent", "scoop"],
5
+ allowedMessageGates: ["user", "agent", "cone", "scoop", "off"],
6
+ maxSeatDays: 30,
7
+ always: { messages: false, tools: false }
8
+ };
9
+ const MAX_SEAT_DAYS = 365;
10
+ const DAY_MS = 24 * 60 * 60 * 1e3;
11
+ const GUESTS_MANAGE_DENIED = "The owner's guest policy doesn't let guests manage guests.";
12
+ const GUESTS_MANAGE_LATER = "Guests can't manage guests yet, whatever the policy says.";
13
+ const POLICY_MANAGE_DENY_ONLY = "manageGuests can only be deny: guests can't manage guests yet.";
14
+ const gateKind = (gate) => gate.startsWith("scoop:") ? "scoop" : gate;
15
+ const BY = {
16
+ user: "a person",
17
+ agent: "the judge",
18
+ scoop: "a scoop",
19
+ cone: "the cone",
20
+ off: ""
21
+ };
22
+ function gateRefusal(what, allowed) {
23
+ const by = allowed.filter((kind) => kind !== "off").map((kind) => BY[kind]);
24
+ if (!by.length) return `The owner's guest policy allows no review for ${what}.`;
25
+ const listed = by.length > 1 ? `${by.slice(0, -1).join(", ")} or ${by.at(-1)}` : by[0];
26
+ return `${what[0]?.toUpperCase()}${what.slice(1)} need a review: by ${listed}.`;
27
+ }
28
+ const gatesOf = (name) => (value, next) => {
29
+ if (!Array.isArray(value) || !value.every((kind) => GATE_KINDS.includes(kind)))
30
+ return `${name} lists gates from ${GATE_KINDS.join(", ")}.`;
31
+ next[name] = GATE_KINDS.filter((kind) => value.includes(kind));
32
+ return null;
33
+ };
34
+ const ALWAYS_KEYS = /* @__PURE__ */ new Set(["messages", "tools"]);
35
+ const ENTRIES = {
36
+ manageGuests: (value) => value === "deny" ? null : POLICY_MANAGE_DENY_ONLY,
37
+ allowedToolGates: gatesOf("allowedToolGates"),
38
+ allowedMessageGates: gatesOf("allowedMessageGates"),
39
+ maxSeatDays(value, next) {
40
+ if (!Number.isInteger(value) || value < 1 || value > MAX_SEAT_DAYS)
41
+ return `maxSeatDays is a whole number from 1 to ${MAX_SEAT_DAYS}.`;
42
+ next.maxSeatDays = value;
43
+ return null;
44
+ },
45
+ always(value, next) {
46
+ const given = value && typeof value === "object" && !Array.isArray(value) ? value : null;
47
+ if (!given || Object.entries(given).some(([key, on]) => !ALWAYS_KEYS.has(key) || typeof on !== "boolean"))
48
+ return "always says, for messages and tools, true or false.";
49
+ next.always = { ...next.always, ...given };
50
+ return null;
51
+ }
52
+ };
53
+ function changedGuests(next, change) {
54
+ if (!change || typeof change !== "object" || Array.isArray(change))
55
+ return "The guests policy is an object of policy entries.";
56
+ for (const [name, value] of Object.entries(change)) {
57
+ const entry = Object.hasOwn(ENTRIES, name) ? ENTRIES[name] : void 0;
58
+ const error = entry ? entry(value, next) : `There is no guest policy entry ${name}.`;
59
+ if (error) return error;
60
+ }
61
+ return null;
62
+ }
63
+ export {
64
+ DAY_MS,
65
+ DEFAULT_GUEST_POLICY,
66
+ GATE_KINDS,
67
+ GUESTS_MANAGE_DENIED,
68
+ GUESTS_MANAGE_LATER,
69
+ MAX_SEAT_DAYS,
70
+ POLICY_MANAGE_DENY_ONLY,
71
+ changedGuests,
72
+ gateKind,
73
+ gateRefusal
74
+ };
75
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/guests/policy.ts"],
4
+ "sourcesContent": ["import type { ApprovalTier } from '../approvals/record.ts';\n\nexport type GateKind = 'user' | 'agent' | 'scoop' | 'cone' | 'off';\n\nexport type GuestPolicy = {\n manageGuests: 'deny' | ApprovalTier;\n allowedToolGates: GateKind[];\n allowedMessageGates: GateKind[];\n maxSeatDays: number;\n always: { messages: boolean; tools: boolean };\n};\n\nexport type GuestPolicyChange = Partial<GuestPolicy>;\n\nexport const GATE_KINDS: readonly GateKind[] = ['user', 'agent', 'cone', 'scoop', 'off'];\n\nexport const DEFAULT_GUEST_POLICY: GuestPolicy = {\n manageGuests: 'deny',\n allowedToolGates: ['user', 'agent', 'scoop'],\n allowedMessageGates: ['user', 'agent', 'cone', 'scoop', 'off'],\n maxSeatDays: 30,\n always: { messages: false, tools: false },\n};\n\nexport const MAX_SEAT_DAYS = 365;\nexport const DAY_MS = 24 * 60 * 60 * 1000;\n\nexport const GUESTS_MANAGE_DENIED = \"The owner's guest policy doesn't let guests manage guests.\";\nexport const GUESTS_MANAGE_LATER = \"Guests can't manage guests yet, whatever the policy says.\";\nexport const POLICY_MANAGE_DENY_ONLY =\n \"manageGuests can only be deny: guests can't manage guests yet.\";\n\nexport const gateKind = (gate: string): GateKind =>\n gate.startsWith('scoop:') ? 'scoop' : (gate as GateKind);\n\nconst BY: Record<GateKind, string> = {\n user: 'a person',\n agent: 'the judge',\n scoop: 'a scoop',\n cone: 'the cone',\n off: '',\n};\n\nexport function gateRefusal(what: string, allowed: readonly GateKind[]): string {\n const by = allowed.filter((kind) => kind !== 'off').map((kind) => BY[kind]);\n if (!by.length) return `The owner's guest policy allows no review for ${what}.`;\n const listed = by.length > 1 ? `${by.slice(0, -1).join(', ')} or ${by.at(-1)}` : by[0];\n return `${what[0]?.toUpperCase()}${what.slice(1)} need a review: by ${listed}.`;\n}\n\ntype Entry = (value: unknown, next: GuestPolicy) => string | null;\n\nconst gatesOf =\n (name: 'allowedToolGates' | 'allowedMessageGates'): Entry =>\n (value, next) => {\n if (!Array.isArray(value) || !value.every((kind) => GATE_KINDS.includes(kind as GateKind)))\n return `${name} lists gates from ${GATE_KINDS.join(', ')}.`;\n next[name] = GATE_KINDS.filter((kind) => value.includes(kind));\n return null;\n };\n\nconst ALWAYS_KEYS = new Set(['messages', 'tools']);\n\nconst ENTRIES: Record<keyof GuestPolicy, Entry> = {\n manageGuests: (value) => (value === 'deny' ? null : POLICY_MANAGE_DENY_ONLY),\n allowedToolGates: gatesOf('allowedToolGates'),\n allowedMessageGates: gatesOf('allowedMessageGates'),\n maxSeatDays(value, next) {\n if (!Number.isInteger(value) || (value as number) < 1 || (value as number) > MAX_SEAT_DAYS)\n return `maxSeatDays is a whole number from 1 to ${MAX_SEAT_DAYS}.`;\n next.maxSeatDays = value as number;\n return null;\n },\n always(value, next) {\n const given = value && typeof value === 'object' && !Array.isArray(value) ? value : null;\n if (\n !given ||\n Object.entries(given).some(([key, on]) => !ALWAYS_KEYS.has(key) || typeof on !== 'boolean')\n )\n return 'always says, for messages and tools, true or false.';\n next.always = { ...next.always, ...(given as Partial<GuestPolicy['always']>) };\n return null;\n },\n};\n\nexport function changedGuests(next: GuestPolicy, change: unknown): string | null {\n if (!change || typeof change !== 'object' || Array.isArray(change))\n return 'The guests policy is an object of policy entries.';\n for (const [name, value] of Object.entries(change)) {\n const entry = Object.hasOwn(ENTRIES, name) ? ENTRIES[name as keyof GuestPolicy] : undefined;\n const error = entry ? entry(value, next) : `There is no guest policy entry ${name}.`;\n if (error) return error;\n }\n return null;\n}\n"],
5
+ "mappings": "AAcO,MAAM,aAAkC,CAAC,QAAQ,SAAS,QAAQ,SAAS,KAAK;AAEhF,MAAM,uBAAoC;AAAA,EAC/C,cAAc;AAAA,EACd,kBAAkB,CAAC,QAAQ,SAAS,OAAO;AAAA,EAC3C,qBAAqB,CAAC,QAAQ,SAAS,QAAQ,SAAS,KAAK;AAAA,EAC7D,aAAa;AAAA,EACb,QAAQ,EAAE,UAAU,OAAO,OAAO,MAAM;AAC1C;AAEO,MAAM,gBAAgB;AACtB,MAAM,SAAS,KAAK,KAAK,KAAK;AAE9B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAC5B,MAAM,0BACX;AAEK,MAAM,WAAW,CAAC,SACvB,KAAK,WAAW,QAAQ,IAAI,UAAW;AAEzC,MAAM,KAA+B;AAAA,EACnC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AACP;AAEO,SAAS,YAAY,MAAc,SAAsC;AAC9E,QAAM,KAAK,QAAQ,OAAO,CAAC,SAAS,SAAS,KAAK,EAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;AAC1E,MAAI,CAAC,GAAG,OAAQ,QAAO,iDAAiD,IAAI;AAC5E,QAAM,SAAS,GAAG,SAAS,IAAI,GAAG,GAAG,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,GAAG,CAAC;AACrF,SAAO,GAAG,KAAK,CAAC,GAAG,YAAY,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,sBAAsB,MAAM;AAC9E;AAIA,MAAM,UACJ,CAAC,SACD,CAAC,OAAO,SAAS;AACf,MAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,CAAC,MAAM,MAAM,CAAC,SAAS,WAAW,SAAS,IAAgB,CAAC;AACvF,WAAO,GAAG,IAAI,qBAAqB,WAAW,KAAK,IAAI,CAAC;AAC1D,OAAK,IAAI,IAAI,WAAW,OAAO,CAAC,SAAS,MAAM,SAAS,IAAI,CAAC;AAC7D,SAAO;AACT;AAEF,MAAM,cAAc,oBAAI,IAAI,CAAC,YAAY,OAAO,CAAC;AAEjD,MAAM,UAA4C;AAAA,EAChD,cAAc,CAAC,UAAW,UAAU,SAAS,OAAO;AAAA,EACpD,kBAAkB,QAAQ,kBAAkB;AAAA,EAC5C,qBAAqB,QAAQ,qBAAqB;AAAA,EAClD,YAAY,OAAO,MAAM;AACvB,QAAI,CAAC,OAAO,UAAU,KAAK,KAAM,QAAmB,KAAM,QAAmB;AAC3E,aAAO,2CAA2C,aAAa;AACjE,SAAK,cAAc;AACnB,WAAO;AAAA,EACT;AAAA,EACA,OAAO,OAAO,MAAM;AAClB,UAAM,QAAQ,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,QAAQ;AACpF,QACE,CAAC,SACD,OAAO,QAAQ,KAAK,EAAE,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,YAAY,IAAI,GAAG,KAAK,OAAO,OAAO,SAAS;AAE1F,aAAO;AACT,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,GAAI,MAAyC;AAC7E,WAAO;AAAA,EACT;AACF;AAEO,SAAS,cAAc,MAAmB,QAAgC;AAC/E,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM;AAC/D,WAAO;AACT,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAClD,UAAM,QAAQ,OAAO,OAAO,SAAS,IAAI,IAAI,QAAQ,IAAyB,IAAI;AAClF,UAAM,QAAQ,QAAQ,MAAM,OAAO,IAAI,IAAI,kCAAkC,IAAI;AACjF,QAAI,MAAO,QAAO;AAAA,EACpB;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -1,11 +1,18 @@
1
1
  import { defineDoc } from "@earendil-works/pi-durable";
2
+ import {
3
+ DAY_MS,
4
+ DEFAULT_GUEST_POLICY,
5
+ gateKind,
6
+ gateRefusal,
7
+ MAX_SEAT_DAYS
8
+ } from "./policy.js";
2
9
  const GuestsDoc = defineDoc({
3
10
  kind: "slicc.guests",
4
11
  version: 1,
5
12
  scope: "session",
6
13
  initial: () => ({ seats: {} })
7
14
  });
8
- const MAX_SEAT_MS = 30 * 24 * 60 * 60 * 1e3;
15
+ const MAX_SEAT_MS = MAX_SEAT_DAYS * DAY_MS;
9
16
  const MAX_LABEL = 64;
10
17
  const MAX_ID = 256;
11
18
  function cleanLabel(raw) {
@@ -16,20 +23,23 @@ function isGate(value) {
16
23
  if (value === "user" || value === "agent" || value === "cone" || value === "off") return true;
17
24
  return /^scoop:\S+$/.test(value);
18
25
  }
19
- function checkDraft(draft, now) {
26
+ function checkDraft(draft, now, policy = DEFAULT_GUEST_POLICY) {
20
27
  if (typeof draft?.id !== "string" || !draft.id || draft.id.length > MAX_ID)
21
28
  return "A guest seat needs an id from the tray hub.";
22
29
  if (!cleanLabel(draft.label)) return "A guest seat needs a label.";
23
30
  if (!isGate(draft.gates?.messages) || !isGate(draft.gates?.tools))
24
31
  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.";
32
+ if (!policy.allowedMessageGates.includes(gateKind(draft.gates.messages)))
33
+ return gateRefusal("a guest's messages", policy.allowedMessageGates);
34
+ if (!policy.allowedToolGates.includes(gateKind(draft.gates.tools)))
35
+ return gateRefusal("a guest's tool calls", policy.allowedToolGates);
27
36
  const { expiresAt } = draft;
28
37
  if (expiresAt !== null && (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)))
29
38
  return "A guest seat expires at a time, or never.";
30
39
  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.";
40
+ const days = policy.maxSeatDays;
41
+ if (expiresAt !== null && expiresAt - now > days * DAY_MS)
42
+ return `A guest seat lasts at most ${days} ${days === 1 ? "day" : "days"}.`;
33
43
  return null;
34
44
  }
35
45
  function liveSeat(seat, now) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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;",
4
+ "sourcesContent": ["import { defineDoc } from '@earendil-works/pi-durable';\nimport type { ApprovalTier } from '../approvals/record.ts';\nimport {\n DAY_MS,\n DEFAULT_GUEST_POLICY,\n type GuestPolicy,\n gateKind,\n gateRefusal,\n MAX_SEAT_DAYS,\n} from './policy.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 = MAX_SEAT_DAYS * DAY_MS;\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(\n draft: GuestDraft,\n now: number,\n policy: GuestPolicy = DEFAULT_GUEST_POLICY\n): 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 (!policy.allowedMessageGates.includes(gateKind(draft.gates.messages)))\n return gateRefusal(\"a guest's messages\", policy.allowedMessageGates);\n if (!policy.allowedToolGates.includes(gateKind(draft.gates.tools)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\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 const days = policy.maxSeatDays;\n if (expiresAt !== null && expiresAt - now > days * DAY_MS)\n return `A guest seat lasts at most ${days} ${days === 1 ? 'day' : '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;AAE1B;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAqCA,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,gBAAgB;AACpC,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,WACd,OACA,KACA,SAAsB,sBACP;AACf,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,CAAC,OAAO,oBAAoB,SAAS,SAAS,MAAM,MAAM,QAAQ,CAAC;AACrE,WAAO,YAAY,sBAAsB,OAAO,mBAAmB;AACrE,MAAI,CAAC,OAAO,iBAAiB,SAAS,SAAS,MAAM,MAAM,KAAK,CAAC;AAC/D,WAAO,YAAY,wBAAwB,OAAO,gBAAgB;AACpE,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,QAAM,OAAO,OAAO;AACpB,MAAI,cAAc,QAAQ,YAAY,MAAM,OAAO;AACjD,WAAO,8BAA8B,IAAI,IAAI,SAAS,IAAI,QAAQ,MAAM;AAC1E,SAAO;AACT;AAEO,SAAS,SAAS,MAA8B,KAAsB;AAC3E,SAAO;AAAA,IACL,QAAQ,KAAK,YAAY,SAAS,KAAK,cAAc,QAAQ,KAAK,YAAY;AAAA,EAChF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,6 @@
1
- import { replicatedState } from "@earendil-works/chord";
1
+ import { forbidden, replicatedState } from "@earendil-works/chord";
2
2
  import { PortLink } from "../wire.js";
3
+ import { GUESTS_MANAGE_DENIED, GUESTS_MANAGE_LATER } from "./policy.js";
3
4
  import { guestView } from "./view.js";
4
5
  function proposalsOf(items, seat) {
5
6
  return (items ?? []).filter((item) => item.kind === "guest-message" && item.seat === seat).map((item) => ({
@@ -11,6 +12,9 @@ function proposalsOf(items, seat) {
11
12
  decidedAt: item.decidedAt ?? null
12
13
  }));
13
14
  }
15
+ const managing = (guests) => Promise.reject(
16
+ forbidden(guests.policy().manageGuests === "deny" ? GUESTS_MANAGE_DENIED : GUESTS_MANAGE_LATER)
17
+ );
14
18
  function guestOpener(guests, agents, views, context, approvals) {
15
19
  return (principal) => {
16
20
  const record = guests.current(principal);
@@ -44,7 +48,9 @@ function guestOpener(guests, agents, views, context, approvals) {
44
48
  transcript,
45
49
  seat,
46
50
  proposals,
47
- propose: (request, using) => guests.propose(principal, request, using)
51
+ propose: (request, using) => guests.propose(principal, request, using),
52
+ guestOpen: () => managing(guests),
53
+ guestRevoke: () => managing(guests)
48
54
  },
49
55
  dispose() {
50
56
  off();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/guests/serve.ts"],
4
- "sourcesContent": ["import type { Context, Host, ReplicatedState } from '@earendil-works/chord';\nimport { replicatedState } from '@earendil-works/chord';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { Approval } from '../approvals/index.ts';\nimport type { AgentGuest, GuestProposal, GuestShown } from '../services.ts';\nimport type { AgentPrincipal, GuestPrincipal } from '../serving.ts';\nimport { type PortEndpoint, PortLink } from '../wire.ts';\nimport type { Guests } from './index.ts';\nimport { guestView } from './view.ts';\n\nexport function proposalsOf(items: readonly Approval[] | undefined, seat: string): GuestProposal[] {\n return (items ?? [])\n .filter((item) => item.kind === 'guest-message' && item.seat === seat)\n .map((item) => ({\n id: item.id,\n text: (item.detail as { text?: string }).text ?? '',\n state: item.state,\n reason: item.reason ?? null,\n createdAt: item.createdAt,\n decidedAt: item.decidedAt ?? null,\n }));\n}\n\nexport function guestOpener(\n guests: Guests,\n agents: Pick<Agents, 'state'>,\n views: ReplicatedState<Record<string, ConversationView>>,\n context: Context,\n approvals?: ReplicatedState<Approval[]>\n) {\n return (principal: GuestPrincipal): { value: AgentGuest; dispose(): void } | undefined => {\n const record = guests.current(principal);\n if (!record) return undefined;\n const pick = (all: Record<string, ConversationView> | undefined) => all?.[record.agentId];\n let shown = pick(views.value);\n const transcript = replicatedState<ConversationView>(guestView(shown, record.id));\n const off = views.subscribe((all) => {\n if (pick(all) === shown) return;\n shown = pick(all);\n transcript.replace(context, guestView(shown, record.id));\n });\n const seat = replicatedState<GuestShown>({\n id: record.id,\n label: record.label,\n agentId: record.agentId,\n name: agents.state().cones[record.agentId]?.name ?? record.agentId,\n expiresAt: record.expiresAt,\n gates: { ...record.gates },\n });\n let listed = JSON.stringify(proposalsOf(approvals?.value, record.id));\n const proposals = replicatedState<GuestProposal[]>(JSON.parse(listed) as GuestProposal[]);\n const offProposals = approvals?.subscribe((items) => {\n const next = JSON.stringify(proposalsOf(items, record.id));\n if (next === listed) return;\n listed = next;\n proposals.replace(context, JSON.parse(next) as GuestProposal[]);\n });\n return {\n value: {\n transcript,\n seat,\n proposals,\n propose: (request, using) => guests.propose(principal, request, using),\n },\n dispose() {\n off();\n offProposals?.();\n },\n };\n };\n}\n\nexport function admitGuest(\n guests: Guests,\n endpoint: PortEndpoint,\n asked: { id: string; label: string },\n serverId: string,\n link: number,\n host: Pick<Host, 'accept'>\n): void {\n const port = new PortLink(endpoint);\n const guest = guests.admit(String(asked.id), String(asked.label));\n if (!guest) {\n port.close('this guest seat is not valid');\n return;\n }\n void port.ended.then(guests.joined(guest.id));\n port.greet(serverId);\n host.accept(port, { principal: { seat: 'guest', link, guest } satisfies AgentPrincipal });\n}\n"],
5
- "mappings": "AACA,SAAS,uBAAuB;AAMhC,SAA4B,gBAAgB;AAE5C,SAAS,iBAAiB;AAEnB,SAAS,YAAY,OAAwC,MAA+B;AACjG,UAAQ,SAAS,CAAC,GACf,OAAO,CAAC,SAAS,KAAK,SAAS,mBAAmB,KAAK,SAAS,IAAI,EACpE,IAAI,CAAC,UAAU;AAAA,IACd,IAAI,KAAK;AAAA,IACT,MAAO,KAAK,OAA6B,QAAQ;AAAA,IACjD,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK,UAAU;AAAA,IACvB,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK,aAAa;AAAA,EAC/B,EAAE;AACN;AAEO,SAAS,YACd,QACA,QACA,OACA,SACA,WACA;AACA,SAAO,CAAC,cAAkF;AACxF,UAAM,SAAS,OAAO,QAAQ,SAAS;AACvC,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,OAAO,CAAC,QAAsD,MAAM,OAAO,OAAO;AACxF,QAAI,QAAQ,KAAK,MAAM,KAAK;AAC5B,UAAM,aAAa,gBAAkC,UAAU,OAAO,OAAO,EAAE,CAAC;AAChF,UAAM,MAAM,MAAM,UAAU,CAAC,QAAQ;AACnC,UAAI,KAAK,GAAG,MAAM,MAAO;AACzB,cAAQ,KAAK,GAAG;AAChB,iBAAW,QAAQ,SAAS,UAAU,OAAO,OAAO,EAAE,CAAC;AAAA,IACzD,CAAC;AACD,UAAM,OAAO,gBAA4B;AAAA,MACvC,IAAI,OAAO;AAAA,MACX,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,MAChB,MAAM,OAAO,MAAM,EAAE,MAAM,OAAO,OAAO,GAAG,QAAQ,OAAO;AAAA,MAC3D,WAAW,OAAO;AAAA,MAClB,OAAO,EAAE,GAAG,OAAO,MAAM;AAAA,IAC3B,CAAC;AACD,QAAI,SAAS,KAAK,UAAU,YAAY,WAAW,OAAO,OAAO,EAAE,CAAC;AACpE,UAAM,YAAY,gBAAiC,KAAK,MAAM,MAAM,CAAoB;AACxF,UAAM,eAAe,WAAW,UAAU,CAAC,UAAU;AACnD,YAAM,OAAO,KAAK,UAAU,YAAY,OAAO,OAAO,EAAE,CAAC;AACzD,UAAI,SAAS,OAAQ;AACrB,eAAS;AACT,gBAAU,QAAQ,SAAS,KAAK,MAAM,IAAI,CAAoB;AAAA,IAChE,CAAC;AACD,WAAO;AAAA,MACL,OAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,CAAC,SAAS,UAAU,OAAO,QAAQ,WAAW,SAAS,KAAK;AAAA,MACvE;AAAA,MACA,UAAU;AACR,YAAI;AACJ,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,WACd,QACA,UACA,OACA,UACA,MACA,MACM;AACN,QAAM,OAAO,IAAI,SAAS,QAAQ;AAClC,QAAM,QAAQ,OAAO,MAAM,OAAO,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,CAAC;AAChE,MAAI,CAAC,OAAO;AACV,SAAK,MAAM,8BAA8B;AACzC;AAAA,EACF;AACA,OAAK,KAAK,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE,CAAC;AAC5C,OAAK,MAAM,QAAQ;AACnB,OAAK,OAAO,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,MAAM,MAAM,EAA2B,CAAC;AAC1F;",
4
+ "sourcesContent": ["import type { Context, Host, ReplicatedState } from '@earendil-works/chord';\nimport { forbidden, replicatedState } from '@earendil-works/chord';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { Approval } from '../approvals/index.ts';\nimport type { AgentGuest, GuestProposal, GuestShown } from '../services.ts';\nimport type { AgentPrincipal, GuestPrincipal } from '../serving.ts';\nimport { type PortEndpoint, PortLink } from '../wire.ts';\nimport type { Guests } from './index.ts';\nimport { GUESTS_MANAGE_DENIED, GUESTS_MANAGE_LATER } from './policy.ts';\nimport { guestView } from './view.ts';\n\nexport function proposalsOf(items: readonly Approval[] | undefined, seat: string): GuestProposal[] {\n return (items ?? [])\n .filter((item) => item.kind === 'guest-message' && item.seat === seat)\n .map((item) => ({\n id: item.id,\n text: (item.detail as { text?: string }).text ?? '',\n state: item.state,\n reason: item.reason ?? null,\n createdAt: item.createdAt,\n decidedAt: item.decidedAt ?? null,\n }));\n}\n\nconst managing = (guests: Pick<Guests, 'policy'>): Promise<never> =>\n Promise.reject(\n forbidden(guests.policy().manageGuests === 'deny' ? GUESTS_MANAGE_DENIED : GUESTS_MANAGE_LATER)\n );\n\nexport function guestOpener(\n guests: Guests,\n agents: Pick<Agents, 'state'>,\n views: ReplicatedState<Record<string, ConversationView>>,\n context: Context,\n approvals?: ReplicatedState<Approval[]>\n) {\n return (principal: GuestPrincipal): { value: AgentGuest; dispose(): void } | undefined => {\n const record = guests.current(principal);\n if (!record) return undefined;\n const pick = (all: Record<string, ConversationView> | undefined) => all?.[record.agentId];\n let shown = pick(views.value);\n const transcript = replicatedState<ConversationView>(guestView(shown, record.id));\n const off = views.subscribe((all) => {\n if (pick(all) === shown) return;\n shown = pick(all);\n transcript.replace(context, guestView(shown, record.id));\n });\n const seat = replicatedState<GuestShown>({\n id: record.id,\n label: record.label,\n agentId: record.agentId,\n name: agents.state().cones[record.agentId]?.name ?? record.agentId,\n expiresAt: record.expiresAt,\n gates: { ...record.gates },\n });\n let listed = JSON.stringify(proposalsOf(approvals?.value, record.id));\n const proposals = replicatedState<GuestProposal[]>(JSON.parse(listed) as GuestProposal[]);\n const offProposals = approvals?.subscribe((items) => {\n const next = JSON.stringify(proposalsOf(items, record.id));\n if (next === listed) return;\n listed = next;\n proposals.replace(context, JSON.parse(next) as GuestProposal[]);\n });\n return {\n value: {\n transcript,\n seat,\n proposals,\n propose: (request, using) => guests.propose(principal, request, using),\n guestOpen: () => managing(guests),\n guestRevoke: () => managing(guests),\n },\n dispose() {\n off();\n offProposals?.();\n },\n };\n };\n}\n\nexport function admitGuest(\n guests: Guests,\n endpoint: PortEndpoint,\n asked: { id: string; label: string },\n serverId: string,\n link: number,\n host: Pick<Host, 'accept'>\n): void {\n const port = new PortLink(endpoint);\n const guest = guests.admit(String(asked.id), String(asked.label));\n if (!guest) {\n port.close('this guest seat is not valid');\n return;\n }\n void port.ended.then(guests.joined(guest.id));\n port.greet(serverId);\n host.accept(port, { principal: { seat: 'guest', link, guest } satisfies AgentPrincipal });\n}\n"],
5
+ "mappings": "AACA,SAAS,WAAW,uBAAuB;AAM3C,SAA4B,gBAAgB;AAE5C,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,iBAAiB;AAEnB,SAAS,YAAY,OAAwC,MAA+B;AACjG,UAAQ,SAAS,CAAC,GACf,OAAO,CAAC,SAAS,KAAK,SAAS,mBAAmB,KAAK,SAAS,IAAI,EACpE,IAAI,CAAC,UAAU;AAAA,IACd,IAAI,KAAK;AAAA,IACT,MAAO,KAAK,OAA6B,QAAQ;AAAA,IACjD,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK,UAAU;AAAA,IACvB,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK,aAAa;AAAA,EAC/B,EAAE;AACN;AAEA,MAAM,WAAW,CAAC,WAChB,QAAQ;AAAA,EACN,UAAU,OAAO,OAAO,EAAE,iBAAiB,SAAS,uBAAuB,mBAAmB;AAChG;AAEK,SAAS,YACd,QACA,QACA,OACA,SACA,WACA;AACA,SAAO,CAAC,cAAkF;AACxF,UAAM,SAAS,OAAO,QAAQ,SAAS;AACvC,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,OAAO,CAAC,QAAsD,MAAM,OAAO,OAAO;AACxF,QAAI,QAAQ,KAAK,MAAM,KAAK;AAC5B,UAAM,aAAa,gBAAkC,UAAU,OAAO,OAAO,EAAE,CAAC;AAChF,UAAM,MAAM,MAAM,UAAU,CAAC,QAAQ;AACnC,UAAI,KAAK,GAAG,MAAM,MAAO;AACzB,cAAQ,KAAK,GAAG;AAChB,iBAAW,QAAQ,SAAS,UAAU,OAAO,OAAO,EAAE,CAAC;AAAA,IACzD,CAAC;AACD,UAAM,OAAO,gBAA4B;AAAA,MACvC,IAAI,OAAO;AAAA,MACX,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,MAChB,MAAM,OAAO,MAAM,EAAE,MAAM,OAAO,OAAO,GAAG,QAAQ,OAAO;AAAA,MAC3D,WAAW,OAAO;AAAA,MAClB,OAAO,EAAE,GAAG,OAAO,MAAM;AAAA,IAC3B,CAAC;AACD,QAAI,SAAS,KAAK,UAAU,YAAY,WAAW,OAAO,OAAO,EAAE,CAAC;AACpE,UAAM,YAAY,gBAAiC,KAAK,MAAM,MAAM,CAAoB;AACxF,UAAM,eAAe,WAAW,UAAU,CAAC,UAAU;AACnD,YAAM,OAAO,KAAK,UAAU,YAAY,OAAO,OAAO,EAAE,CAAC;AACzD,UAAI,SAAS,OAAQ;AACrB,eAAS;AACT,gBAAU,QAAQ,SAAS,KAAK,MAAM,IAAI,CAAoB;AAAA,IAChE,CAAC;AACD,WAAO;AAAA,MACL,OAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,CAAC,SAAS,UAAU,OAAO,QAAQ,WAAW,SAAS,KAAK;AAAA,QACrE,WAAW,MAAM,SAAS,MAAM;AAAA,QAChC,aAAa,MAAM,SAAS,MAAM;AAAA,MACpC;AAAA,MACA,UAAU;AACR,YAAI;AACJ,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,WACd,QACA,UACA,OACA,UACA,MACA,MACM;AACN,QAAM,OAAO,IAAI,SAAS,QAAQ;AAClC,QAAM,QAAQ,OAAO,MAAM,OAAO,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,CAAC;AAChE,MAAI,CAAC,OAAO;AACV,SAAK,MAAM,8BAA8B;AACzC;AAAA,EACF;AACA,OAAK,KAAK,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE,CAAC;AAC5C,OAAK,MAAM,QAAQ;AACnB,OAAK,OAAO,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,MAAM,MAAM,EAA2B,CAAC;AAC1F;",
6
6
  "names": []
7
7
  }
package/dist/host.js CHANGED
@@ -8,6 +8,7 @@ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
8
8
  import {
9
9
  attachApprovals
10
10
  } from "./approvals/index.js";
11
+ import { alwaysFor, attachApprovalPolicy } from "./approvals/policy.js";
11
12
  import { createAgentControl } from "./control.js";
12
13
  import { trackDeliveries } from "./deliveries.js";
13
14
  import { createGuests } from "./guests/index.js";
@@ -26,6 +27,7 @@ import {
26
27
  CONTROL_METHODS,
27
28
  CONTROL_READS,
28
29
  GUEST_METHODS,
30
+ OWNER_DEVICE_METHODS,
29
31
  PAGE_METHODS,
30
32
  REMOTE_METHODS,
31
33
  REMOTE_READS,
@@ -110,8 +112,13 @@ async function hostAgent(agent, options = {}) {
110
112
  const untouch = [state.subscribe(touch), mounted.views.subscribe(touch)];
111
113
  const pages = options.pages ?? createPages();
112
114
  const deliveries = trackDeliveries(agent.harness, state, context);
115
+ const policy = await attachApprovalPolicy(agent.harness, context);
113
116
  const approvals = options.approvals ?? await attachApprovals(
114
- { harness: agent.harness, ...options.judge ? { judge: options.judge } : {} },
117
+ {
118
+ harness: agent.harness,
119
+ guestAlways: alwaysFor(policy),
120
+ ...options.judge ? { judge: options.judge } : {}
121
+ },
115
122
  context
116
123
  );
117
124
  const monitor = createMonitor({
@@ -125,25 +132,29 @@ async function hostAgent(agent, options = {}) {
125
132
  harness: agent.harness,
126
133
  agents: agent.agents,
127
134
  approvals,
135
+ policy,
128
136
  context
129
137
  });
130
- const control = createAgentControl(
131
- agent.harness,
132
- agent,
133
- deliveries,
134
- options.licks,
135
- options.scoops,
136
- options.skills,
137
- options.sprinkles,
138
- options.memory,
139
- options.freezer,
140
- options.changes,
141
- monitor,
142
- options.secrets?.list,
143
- options.uploads,
144
- options.labels,
145
- guests
146
- );
138
+ const control = {
139
+ ...createAgentControl(
140
+ agent.harness,
141
+ agent,
142
+ deliveries,
143
+ options.licks,
144
+ options.scoops,
145
+ options.skills,
146
+ options.sprinkles,
147
+ options.memory,
148
+ options.freezer,
149
+ options.changes,
150
+ monitor,
151
+ options.secrets?.list,
152
+ options.uploads,
153
+ options.labels,
154
+ guests
155
+ ),
156
+ approvalPolicySet: (change, using) => policy.set(change, using)
157
+ };
147
158
  const remote = createRemote({
148
159
  harness: agent.harness,
149
160
  send: (request, using) => control.send(request, using),
@@ -174,7 +185,8 @@ async function hostAgent(agent, options = {}) {
174
185
  vault: options.secrets?.vault ?? idleVault,
175
186
  labels: labels.state,
176
187
  guests: guests.seats,
177
- approvals: approvals.state
188
+ approvals: approvals.state,
189
+ approvalPolicy: policy.state
178
190
  };
179
191
  const settings = options.settings ?? noSettings;
180
192
  const gates = gatedControls(control, slims.shown, shownViews, transcriptService.labels);
@@ -273,21 +285,18 @@ function lazily(state) {
273
285
  events: (listener) => readable(state()).events(listener)
274
286
  };
275
287
  }
276
- const GUEST_MANAGERS = /* @__PURE__ */ new Set(["owner"]);
277
- const OWNER_GUESTS = "only the owner can invite or revoke guests";
278
- const notOwner = () => Promise.reject(forbidden(OWNER_GUESTS));
279
288
  const seated = (serving, seat) => ({
280
- ...serving.trusted,
281
- decide: serving.decide(seat === "owner" ? "owner" : "follower"),
282
- ...GUEST_MANAGERS.has(seat) ? {} : { guestOpen: notOwner, guestRevoke: notOwner }
289
+ ...seat === "view" ? serving.viewed : serving.trusted,
290
+ decide: serving.decide(seat === "owner" ? "owner" : "follower")
283
291
  });
292
+ const VIEW_METHODS = [...CONTROL_READS, ...OWNER_DEVICE_METHODS];
284
293
  function serveAgent(serving) {
285
294
  const host = closedToGuests(createHost({ name: AGENT_HOST, principalKey }));
286
295
  const viewing = { ...serving.transcript, monitor: serving.viewMonitor };
287
296
  host.serve(AgentControl, ((_input, principal) => {
288
297
  const seat = seatOf(principal);
289
- if (seat === "view") return { kind: "view", ...served(() => serving.viewed, CONTROL_READS) };
290
298
  const control = seated(serving, seat);
299
+ if (seat === "view") return { kind: "view", ...served(() => control, VIEW_METHODS) };
291
300
  return { kind: "control", ...served(() => control, CONTROL_METHODS) };
292
301
  }));
293
302
  host.serve(AgentTranscript, ((_input, principal) => ({
@@ -340,8 +349,6 @@ function serveAgent(serving) {
340
349
  }
341
350
  export {
342
351
  CLOSED_TO_GUESTS,
343
- GUEST_MANAGERS,
344
- OWNER_GUESTS,
345
352
  closedToGuests,
346
353
  hostAgent,
347
354
  lazily,