@ai-ecoverse/slicc-agent 5.8.1 → 5.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/guests/client.js +2 -0
- package/dist/guests/client.js.map +2 -2
- package/dist/guests/guard.js +3 -3
- package/dist/guests/guard.js.map +2 -2
- package/dist/guests/index.js +2 -1
- package/dist/guests/index.js.map +2 -2
- package/dist/guests/policy.js +4 -2
- package/dist/guests/policy.js.map +2 -2
- package/dist/spectrum/approvals-port.js +1 -1
- package/dist/spectrum/approvals-port.js.map +2 -2
- package/dist/spectrum/guest-port.js.map +2 -2
- package/dist/types/guests/guard.d.ts +1 -1
- package/dist/types/guests/policy.d.ts +1 -0
- package/dist/types/spectrum/guest-port.d.ts +2 -5
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ app.model = { ...createKernelModel({ kernel, root }), ...createAgentModel(agent,
|
|
|
159
159
|
|
|
160
160
|
The adapter keeps the replicated transcript and maps it on every update. User, assistant and compaction entries become messages; tool results fill in the tool calls they answer; the generation in flight shows as a streaming message; retries and running compactions show as system messages. A failed generation ends in an error card whose action fits the error: credential errors (an invalid or missing key, 401/403, not authorized) open settings, a model the account can't use offers the model picker, and anything else offers Retry. A content-filter stop is different: every later request resends the content that tripped the filter, so Retry, and any new message, would be stopped again. Content-filter stops are Bedrock's `content_filtered` and `guardrail_intervened`, Anthropic's `sensitive` and refusals, and OpenAI's `content_filter`. Their card leads with "The model's content filter stopped this reply." and offers **Drop the last turn** (spectrum ≥ 1.12.0). `AgentControl.rewind(messageId)` finds the user turn before that message and forks the cone's conversation at the entry before it (`Conversation.fork`, so `pi.agent` comes back as it was there). It points the cone at the fork and returns the turn's prompt, which spectrum puts back into the composer. Forking the very first turn creates a fresh conversation with the same model and thinking level. The filtered branch stays in storage, off the active path. The fork starts with a `slicc.rewound` entry that shows as a "Rewound 1 turn" notice, and clicking the card of an earlier filtered turn rewinds that turn.
|
|
161
161
|
|
|
162
|
-
Each cone's active conversation is in the session document `slicc.agents` (the root until the first rewind). The host's transcript follows it across forks. Rewinding is refused while the cone itself is running or has queued input, because a fork starts with an empty inbox, but never because its scoops are busy (see Cones and scoops). A background compaction still running on the old branch finishes there. A fork also starts with fresh `pi.usage`. Credential and model errors lead with a plain sentence ("Bedrock rejected the API key.", "This model isn't available with the current account.") and keep the provider's raw error as `detail`, which spectrum (≥ 1.11.0) shows underneath. `busy()` and `queue()` read durable's live and inbox documents. Steering is the default (#4): a send or a steer goes out with `whenBusy: 'steer'`, so while a run is going it joins after the current tool round, and a `queue` send is a follow-up. User messages carry `delivered` from the host's deliveries; entries the host didn't record (from before a reload) count as steered when they follow a tool result. Cones and scoops are below; the freezer and questions come later. Licks show as spectrum's lick cards (below). slicc-spectrum is an optional peer dependency (≥ 1.
|
|
162
|
+
Each cone's active conversation is in the session document `slicc.agents` (the root until the first rewind). The host's transcript follows it across forks. Rewinding is refused while the cone itself is running or has queued input, because a fork starts with an empty inbox, but never because its scoops are busy (see Cones and scoops). A background compaction still running on the old branch finishes there. A fork also starts with fresh `pi.usage`. Credential and model errors lead with a plain sentence ("Bedrock rejected the API key.", "This model isn't available with the current account.") and keep the provider's raw error as `detail`, which spectrum (≥ 1.11.0) shows underneath. `busy()` and `queue()` read durable's live and inbox documents. Steering is the default (#4): a send or a steer goes out with `whenBusy: 'steer'`, so while a run is going it joins after the current tool round, and a `queue` send is a follow-up. User messages carry `delivered` from the host's deliveries; entries the host didn't record (from before a reload) count as steered when they follow a tool result. Cones and scoops are below; the freezer and questions come later. Licks show as spectrum's lick cards (below). slicc-spectrum is an optional peer dependency (≥ 1.84.0, the lowest whose monitor, tray, panel, guest seat and guest approval types match the agent's; `npm run typecheck` fails on anything older), used for types only, so package managers don't install it next to the agent.
|
|
163
163
|
|
|
164
164
|
Each agent in `list()` has a real `contextFill` and `unread`:
|
|
165
165
|
- **`contextFill`** is the context the agent uses, as a share of its model's `contextWindow` from the settings' model list. The tokens are those of the newest assistant's usage after the active head, counted the way pi-ai's `calculateContextTokens` does (`totalTokens`, or input, output and both cache counts). Aborted and failed turns don't count. After a compaction, the kept turns' usage no longer counts. Until the next turn reports its usage, the fill is estimated the way pi does it, at four characters a token, over the summary and the kept entries (without the system prompt and tools). An unknown model also reads 0. The Monitor's "Fullest context" and its context alerts use the same functions (`src/context.ts`).
|
|
@@ -611,7 +611,7 @@ Every approval, of a guest's prompt, of a tool call in a run a guest started, or
|
|
|
611
611
|
- **Lifetime:** an item waits 5 minutes unless its requester sets `expiresAt`, then settles `unanswered`. Pending guest items survive a reload of the worker; pending `sudo` items are withdrawn (`restart`), because a reload ends the kernel's processes. The document keeps every pending item and the last 100 settled ones.
|
|
612
612
|
- **In the worker,** `AgentHost.approvals` has `request(draft, ctx)`, `decide(id, answer, reason, by, ctx)` (`by` is a `DecidedBy`, or `{ by, device }` to record the deciding device), `settled(id, ctx)` (resolves when the item leaves `pending`), `withdraw(id, reason, ctx)` and `withdrawSeat(seat, ctx)`, which withdraws a revoked seat's pending items.
|
|
613
613
|
- **Over the link,** `AgentTranscript.approvals` replicates the items, and `AgentControl.decide(id, decision, reason, ctx)` answers one, for every owner device: the owner's pages and followers with or without Control, all with the same powers. `decidedBy` comes from the link's seat (`owner` for the owner's pages, `follower` for the others), and is only a record of who answered. A guest never gets `decide`.
|
|
614
|
-
- **In spectrum** (1.
|
|
614
|
+
- **In spectrum** (1.84 on, which shows guest approvals in `<slicc-approvals>` with Always allow when `always` is true, as notices without `decide`, and an `unsure` verdict), `createAgentModel` returns `approvals`, an `ApprovalsAdapter` for `SliccModel.approvals`. `list()` has the pending `user` items, oldest first, as spectrum's `Approval`s, with the cone's name as `agentName` and the item's own `always`, so a guest approval offers Always allow only when the policy did when it was asked. `decide(id, decision, reason?)` rejects with the agent's message, and `ended` reports `answered`, `expired` or `withdrawn` for an item that left without this tab's decision. Every owner device's adapter has `decide`; a guest connection's doesn't. So a notice instead of a dialog shows only where nothing can answer.
|
|
615
615
|
- **Kernel sudo:** `AgentOwner.approve(req, { signal, where? })` from `startAgent` takes slicc-kernel's sudo request as it comes to the kernel's `approve` hook and resolves `once`, `always` or `deny`. bios hands its hook to it while the agent runs, and to its own dialog otherwise. It rejects with `code: 'EAGAIN'` before anything is recorded when the agent can't take the request (released, restarting, or an older worker), so that's when to fall back; any other rejection is a deny. The request goes through `slicc.agent.sudo`, a service only the owner's pages get, and becomes a `sudo` item on the `user` tier with the kernel's `expiresAt`:
|
|
616
616
|
- **Requester** comes from the kernel's process tag: `{ kind: 'agent', agentId }` is the cone or scoop by its name, `{ kind: 'user', where }` is the kernel user with `where` (`terminal:…` reads "a terminal", `follower:<device>` the `where` the page passes, such as "on Pat's phone", or "a linked device", with the device id kept as `requester.device`), and anything else is the kernel user alone.
|
|
617
617
|
- **Detail:** the command as the kernel resolved it (`path` and the arguments, shell-quoted), `target`, `cwd`, and `alwaysScope: 'cwd'` when the request offers Always (`req.always === true`, which the page sets from its own checks), since the kernel's Always writes a rule for that directory.
|
|
@@ -666,7 +666,7 @@ All the limits on approvals and guests live in one owner-changeable policy, the
|
|
|
666
666
|
|
|
667
667
|
A guest seat (plan D13, PR 19) is a revocable seat on one cone: the guest sees that cone's live transcript and has a composer, and nothing else.
|
|
668
668
|
|
|
669
|
-
- **Minting.** The tray hub mints the seat and its one-time URL (v6's routes, D10); the page hands the agent adapter its routes: `createAgentModel(connection, { guests: { mint({ label, expiresAt, gates, agentId }) → { id, url }, revoke(id) } })`. Its `guests` port (spectrum's `GuestPort`) mints through the hub, then opens the seat in the worker with `AgentControl.guestOpen({ id, label, agentId, expiresAt, gates })`; if the worker refuses, the hub's seat is revoked again. `list()` is the transcript's `guests` state (`{ id, label, agentId, createdAt, expiresAt, gates, connected, pending, alwaysTools }`), and the URL is returned once and never kept. `unavailable()` says "Inviting guests needs the tray hub." without a hub, and "Only the owner's devices can invite guests." on a guest. Every owner device can invite, with or without Control. `gates()` lists the gates the approval policy allows for messages and tools, in display order,
|
|
669
|
+
- **Minting.** The tray hub mints the seat and its one-time URL (v6's routes, D10); the page hands the agent adapter its routes: `createAgentModel(connection, { guests: { mint({ label, expiresAt, gates, agentId }) → { id, url }, revoke(id) } })`. Its `guests` port (spectrum's `GuestPort`) mints through the hub, then opens the seat in the worker with `AgentControl.guestOpen({ id, label, agentId, expiresAt, gates })`; if the worker refuses, the hub's seat is revoked again. `list()` is the transcript's `guests` state (`{ id, label, agentId, createdAt, expiresAt, gates, connected, pending, alwaysTools }`), and the URL is returned once and never kept. `unavailable()` says "Inviting guests needs the tray hub." without a hub, and "Only the owner's devices can invite guests." on a guest. Every owner device can invite, with or without Control. `gates()` (spectrum's `GuestPort.gates?()`) lists the gates the approval policy allows for messages and tools, in display order, for the invite pickers; `scoop:*`, for "any scoop", shows only when the policy offers the scoop tier.
|
|
670
670
|
- **The record.** Seats live in the session document `slicc.guests`, so they survive reloads and restarts: id, label (control characters stripped, at most 64 characters), cone, expiry (at most the policy's `guests.maxSeatDays`, 30 by default), the two gates (`user`, `agent`, `cone`, `scoop:<name>` or `off`, fixed when the seat is opened and checked then against the policy's `allowedMessageGates` and `allowedToolGates`; by default the tools gate can't be `off` or `cone`, from any caller: "A guest's tool calls need a review: by a person, the judge or a scoop.") and an epoch. Revoking (`guestRevoke(id)`) keeps a tombstone, bumps the epoch, withdraws the guest's messages still queued in the cone, and ends every guest link of the seat (`invalidatePrincipal`). An expired seat ends the same way at its expiry. Tombstones go 365 days after they end, the longest a seat can be opened for.
|
|
671
671
|
- **Joining.** The hub resolves the guest's token to the seat's id and label, and the owner page opens `owner.port({ guest: { id, label } })` and bridges it like a follower's. Before Chord sees the link, the worker checks that the seat exists, isn't revoked or expired, and has that label; otherwise it closes the port. The link's principal is `{ seat: 'guest', link, guest: { id, name, epoch } }`, and anything else the port asks for (`readOnly: false`, `pages`) is ignored.
|
|
672
672
|
- **What a guest gets.** Every contract served from the agent's host refuses a guest principal (`forbidden`) unless its factory is opened with `openToGuests`, so a contract added later is closed to guests until someone opens it. The only open one is `slicc.agent.guest`: its `guest` variant has the shared cone's transcript (its user, assistant, tool and compaction entries; `pi.live`; `pi.agent`'s model and thinking level; and only the guest's own queued messages) and the seat, plus `propose({ text, requestId })`. Everyone else gets its empty `none` variant.
|
package/dist/guests/client.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../services.js";
|
|
7
7
|
import { AGENT_HOST, kept, plain, replicated } from "../serving.js";
|
|
8
8
|
import { PortLink } from "../wire.js";
|
|
9
|
+
import { SCOOP_REVIEW_LATER } from "./policy.js";
|
|
9
10
|
const GUEST_ONLY = "a guest can only watch and send messages";
|
|
10
11
|
const SEAT_REFUSED = "This guest link isn't valid any more.";
|
|
11
12
|
const SAID = {
|
|
@@ -14,6 +15,7 @@ const SAID = {
|
|
|
14
15
|
"too-long": "That message is too long for a guest seat.",
|
|
15
16
|
gone: "The cone this seat shares is gone.",
|
|
16
17
|
unavailable: "Messages from guests need a review, and this agent can't review them yet.",
|
|
18
|
+
scoop: SCOOP_REVIEW_LATER,
|
|
17
19
|
overflow: "Too many of your messages are waiting for approval. Try again once one is answered."
|
|
18
20
|
};
|
|
19
21
|
const proposedSentence = (reason) => SAID[reason] ?? "The message was not delivered.";
|
|
@@ -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, final, 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 overflow: 'Too many of your messages are waiting for approval. Try again once one is answered.',\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(\n isChordError(error) && error.code === 'forbidden' ? refusal(SEAT_REFUSED) : final(error)\n );\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 === 'queued')\n return { accepted: true, submissionId: proposed.review, 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": "AAAA,SAAS,YAAY,cAA6B,uBAAuB;AAEzE,SAA+B,OAAO,eAAe;AACrD;AAAA,EAEE;AAAA,EAEA;AAAA,OAKK;AACP,SAAS,YAAY,MAAM,OAAoB,kBAAkB;AACjE,SAA4B,gBAAgB;
|
|
4
|
+
"sourcesContent": ["import { createHost, isChordError, type Readable, replicatedState } from '@earendil-works/chord';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport { type AgentConnection, final, 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';\nimport { SCOOP_REVIEW_LATER } from './policy.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 scoop: SCOOP_REVIEW_LATER,\n overflow: 'Too many of your messages are waiting for approval. Try again once one is answered.',\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(\n isChordError(error) && error.code === 'forbidden' ? refusal(SEAT_REFUSED) : final(error)\n );\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 === 'queued')\n return { accepted: true, submissionId: proposed.review, 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
|
}
|
package/dist/guests/guard.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
ToolTask
|
|
7
7
|
} from "@earendil-works/pi-durable";
|
|
8
8
|
import { REASONS } from "../approvals/record.js";
|
|
9
|
-
import { gateKind, gateRefusal } from "./policy.js";
|
|
9
|
+
import { gateKind, gateRefusal, SCOOP_REVIEW_LATER } from "./policy.js";
|
|
10
10
|
import { GuestsDoc, isGate, liveSeat } from "./seats.js";
|
|
11
11
|
const GUEST_GUARD = "slicc-guest-guard";
|
|
12
12
|
const GUEST_REQUEST = "guest:";
|
|
@@ -19,7 +19,7 @@ const GATE_REASONS = {
|
|
|
19
19
|
judged: "The automatic check declined this tool call in a run a guest started",
|
|
20
20
|
unanswered: "Nobody approved this tool call in a run a guest started in time.",
|
|
21
21
|
unroutable: "The cone can't review a tool call it is itself waiting on.",
|
|
22
|
-
scoop:
|
|
22
|
+
scoop: SCOOP_REVIEW_LATER,
|
|
23
23
|
withdrawn: "The review of this tool call was withdrawn."
|
|
24
24
|
};
|
|
25
25
|
function canonical(value) {
|
|
@@ -62,11 +62,11 @@ ${tier}`, { seat, tier: String(tier) });
|
|
|
62
62
|
return [...found.values()];
|
|
63
63
|
}
|
|
64
64
|
function settledEarly(seat, tier, policy, at, tool, args) {
|
|
65
|
+
if (gateKind(tier) === "scoop") return GATE_REASONS.scoop;
|
|
65
66
|
if (!policy.allowedToolGates.includes(gateKind(tier)))
|
|
66
67
|
return gateRefusal("a guest's tool calls", policy.allowedToolGates);
|
|
67
68
|
if (!liveSeat(seat, at)) return GATE_REASONS.ended;
|
|
68
69
|
if (tier === "off") return "";
|
|
69
|
-
if (gateKind(tier) === "scoop") return GATE_REASONS.scoop;
|
|
70
70
|
const kept = seat.alwaysTools?.some((rule) => rule.tool === tool && rule.arguments === args);
|
|
71
71
|
return policy.always.tools && kept ? "" : null;
|
|
72
72
|
}
|
package/dist/guests/guard.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/guests/guard.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport {\n defineExtension,\n type Extension,\n type Harness,\n type HookApi,\n hook,\n LiveDoc,\n type SubmissionId,\n type TaskId,\n type ToolHookCall,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { REASONS } from '../approvals/record.ts';\nimport { type GuestPolicy, gateKind, gateRefusal } from './policy.ts';\nimport { type AlwaysTool, GuestsDoc, isGate, liveSeat, type SeatRecord } from './seats.ts';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const GUEST_TOOL_MEMO = 'slicc.guest-tool';\nexport const SUMMARY_LENGTH = 2000;\n\nexport const GATE_REASONS = {\n unknown:\n 'A guest started this run, and its seat names no review for tool calls that this agent knows.',\n ended: \"A guest started this run, and that guest's seat has ended.\",\n rejected: 'The owner declined this tool call in a run a guest started.',\n judged: 'The automatic check declined this tool call in a run a guest started',\n unanswered: 'Nobody approved this tool call in a run a guest started in time.',\n unroutable: \"The cone can't review a tool call it is itself waiting on.\",\n scoop:
|
|
5
|
-
"mappings": "AACA,SAAS,kBAAkB,0BAA0B;AACrD;AAAA,EACE;AAAA,EAIA;AAAA,EACA;AAAA,EAIA;AAAA,OACK;AAEP,SAAS,eAAe;AACxB,SAA2B,UAAU,
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport {\n defineExtension,\n type Extension,\n type Harness,\n type HookApi,\n hook,\n LiveDoc,\n type SubmissionId,\n type TaskId,\n type ToolHookCall,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { REASONS } from '../approvals/record.ts';\nimport { type GuestPolicy, gateKind, gateRefusal, SCOOP_REVIEW_LATER } from './policy.ts';\nimport { type AlwaysTool, GuestsDoc, isGate, liveSeat, type SeatRecord } from './seats.ts';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const GUEST_TOOL_MEMO = 'slicc.guest-tool';\nexport const SUMMARY_LENGTH = 2000;\n\nexport const GATE_REASONS = {\n unknown:\n 'A guest started this run, and its seat names no review for tool calls that this agent knows.',\n ended: \"A guest started this run, and that guest's seat has ended.\",\n rejected: 'The owner declined this tool call in a run a guest started.',\n judged: 'The automatic check declined this tool call in a run a guest started',\n unanswered: 'Nobody approved this tool call in a run a guest started in time.',\n unroutable: \"The cone can't review a tool call it is itself waiting on.\",\n scoop: SCOOP_REVIEW_LATER,\n withdrawn: 'The review of this tool call was withdrawn.',\n} as const;\n\nexport interface GateHost {\n approvals: Pick<Approvals, 'state' | 'request' | 'settled' | 'withdraw'>;\n policy(): GuestPolicy;\n now?: () => number;\n}\n\nexport interface GuestToolGate {\n readonly extension: Extension;\n bind(host: GateHost): void;\n}\n\ntype Held = Pick<Harness, 'submission' | 'commit' | 'waitForTask'>;\n\ntype Taint = { seat: SeatRecord | null; tier: string };\n\nexport function canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value && typeof value === 'object')\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n .join(',')}}`;\n return JSON.stringify(value ?? null);\n}\n\nexport function summaryOf(args: Record<string, unknown>): string {\n const values = Object.values(args);\n const text = values.length === 1 && typeof values[0] === 'string' ? values[0] : canonical(args);\n return text.length > SUMMARY_LENGTH ? `${text.slice(0, SUMMARY_LENGTH - 1)}\u2026` : text;\n}\n\nexport function reasonOf(item: Approval): string {\n if (item.state === 'rejected' && item.decidedBy === 'judge')\n return item.verdict\n ? `${GATE_REASONS.judged}: ${item.verdict.reason}`\n : `${GATE_REASONS.judged}.`;\n if (item.state === 'rejected')\n return item.reason && item.reason !== REASONS.unknownAnswer\n ? `${GATE_REASONS.rejected} ${item.reason}`\n : GATE_REASONS.rejected;\n if (item.state === 'withdrawn') return GATE_REASONS.withdrawn;\n if (item.reason === REASONS.unknownTier) return GATE_REASONS.unknown;\n if (item.reason === REASONS.unroutable) return GATE_REASONS.unroutable;\n return GATE_REASONS.unanswered;\n}\n\nasync function taintsOf(\n inputs: readonly SubmissionId[],\n seats: Readonly<Record<string, SeatRecord>>,\n harness: Held,\n context: Context\n): Promise<Taint[]> {\n const found = new Map<string, Taint>();\n for (const id of inputs) {\n const seat = Object.values(seats).find((record) =>\n record.submissions?.some((entry) => entry.id === String(id))\n );\n const tier = seat?.submissions?.find((entry) => entry.id === String(id))?.tools;\n if (seat) {\n found.set(`${seat.id}\\n${tier}`, { seat, tier: String(tier) });\n continue;\n }\n const submission = await harness.submission(id, context);\n const record = submission ? await submission.status(context) : undefined;\n if (record?.requestId?.startsWith(GUEST_REQUEST)) return [{ seat: null, tier: '' }];\n }\n return [...found.values()];\n}\n\nfunction settledEarly(\n seat: SeatRecord,\n tier: string,\n policy: GuestPolicy,\n at: number,\n tool: string,\n args: string\n): string | null {\n if (gateKind(tier) === 'scoop') return GATE_REASONS.scoop;\n if (!policy.allowedToolGates.includes(gateKind(tier)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n if (!liveSeat(seat, at)) return GATE_REASONS.ended;\n if (tier === 'off') return '';\n const kept = seat.alwaysTools?.some((rule) => rule.tool === tool && rule.arguments === args);\n return policy.always.tools && kept ? '' : null;\n}\n\nconst ended = (status: string) => status === 'terminal' || status === 'completing';\n\nexport function guestToolGate(harness: () => Held | undefined): GuestToolGate {\n let resolveHost: (host: GateHost) => void = () => {};\n const bound = new Promise<GateHost>((resolve) => {\n resolveHost = resolve;\n });\n const watched = new Set<string>();\n const watch = (host: GateHost, id: string, task: number) => {\n const held = harness();\n if (!held || watched.has(id)) return;\n watched.add(id);\n void held\n .waitForTask(task as unknown as TaskId, BACKGROUND_CONTEXT)\n .then((record) =>\n ended(record.state.status)\n ? host.approvals.withdraw(id, REASONS.aborted, BACKGROUND_CONTEXT)\n : undefined\n )\n .catch(() => undefined)\n .finally(() => watched.delete(id));\n };\n const remember = (seat: string, rule: AlwaysTool) =>\n (harness() as Held).commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[seat];\n if (!record || record.revoked !== null) return;\n const kept = (record.alwaysTools ?? []).filter(\n (entry) => entry.tool !== rule.tool || entry.arguments !== rule.arguments\n );\n record.alwaysTools = [...kept, rule];\n }, BACKGROUND_CONTEXT);\n const review = async (\n taint: Taint,\n call: ToolHookCall,\n api: HookApi,\n context: Context\n ): Promise<string | undefined> => {\n const { seat } = taint;\n if (seat === null || !isGate(taint.tier)) return GATE_REASONS.unknown;\n const host = await awaitWithContext(bound, context);\n const policy = host.policy();\n const now = host.now ?? Date.now;\n const args = canonical(call.arguments);\n const always = policy.always.tools;\n const early = settledEarly(seat, taint.tier, policy, now(), call.name, args);\n if (early !== null) return early || undefined;\n const id = await api.memo<string>(\n `${GUEST_TOOL_MEMO}:${seat.id}:${taint.tier}`,\n `apr-${crypto.randomUUID()}`,\n context\n );\n const task = Number(api.taskId);\n const asked = await host.approvals.request(\n {\n id,\n kind: 'guest-tool',\n seat: seat.id,\n agentId: seat.agentId,\n requester: { kind: 'guest', name: seat.label, seat: seat.id },\n tier: taint.tier,\n detail: { tool: call.name, summary: summaryOf(call.arguments), arguments: args, task },\n },\n BACKGROUND_CONTEXT\n );\n watch(host, id, task);\n const live = async () =>\n liveSeat((await api.snapshot(GuestsDoc, context))?.seats[seat.id], now());\n if (!(await live())) {\n await host.approvals.withdraw(id, REASONS.revoked, BACKGROUND_CONTEXT);\n return GATE_REASONS.ended;\n }\n const item = asked.state === 'pending' ? await host.approvals.settled(id, context) : asked;\n if (item.state !== 'approved') return reasonOf(item);\n if (!(await live())) return GATE_REASONS.ended;\n if (item.answer === 'always' && always)\n await remember(seat.id, {\n approval: item.id,\n tool: call.name,\n arguments: args,\n by: item.decidedBy,\n at: item.decidedAt ?? now(),\n });\n return undefined;\n };\n return {\n extension: 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 seats = (await api.snapshot(GuestsDoc, context))?.seats ?? {};\n const held = harness() as Held;\n for (const taint of await taintsOf(inputs, seats, held, context)) {\n const block = await review(taint, call, api, context);\n if (block) return { block };\n }\n return undefined;\n },\n }),\n ],\n }),\n bind(host) {\n resolveHost(host);\n for (const item of host.approvals.state.value ?? [])\n if (item.kind === 'guest-tool' && item.state === 'pending' && item.detail.task)\n watch(host, item.id, item.detail.task);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["record"]
|
|
7
7
|
}
|
package/dist/guests/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
guestContent
|
|
9
9
|
} from "./attribution.js";
|
|
10
10
|
import { GUEST_REQUEST } from "./guard.js";
|
|
11
|
-
import { DEFAULT_GUEST_POLICY } from "./policy.js";
|
|
11
|
+
import { DEFAULT_GUEST_POLICY, gateKind } from "./policy.js";
|
|
12
12
|
import {
|
|
13
13
|
endReview,
|
|
14
14
|
GuestReviewsDoc,
|
|
@@ -86,6 +86,7 @@ async function prepare(record, request) {
|
|
|
86
86
|
}
|
|
87
87
|
async function place(record, prepared, options, using, at) {
|
|
88
88
|
const gate = record.gates.messages;
|
|
89
|
+
if (gateKind(gate) === "scoop") return unanswered("scoop");
|
|
89
90
|
if (gate === "off") return deliver(record, prepared.text, prepared.requestId, options, using);
|
|
90
91
|
const { reviews } = options;
|
|
91
92
|
if (!reviews || !options.approvals) return unanswered("unavailable");
|
package/dist/guests/index.js.map
CHANGED
|
@@ -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 DocumentState, type Harness, InboxDoc } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { 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 {\n ASKED_KIND,\n GUEST_INPUT_KIND,\n type GuestAsked,\n type GuestInput,\n guestContent,\n} from './attribution.ts';\nimport { GUEST_REQUEST } from './guard.ts';\nimport { DEFAULT_GUEST_POLICY, type GuestPolicy } from './policy.ts';\nimport {\n endReview,\n type GuestReviews,\n GuestReviewsDoc,\n KEPT_SUBMISSIONS,\n liveReviews,\n MAX_QUEUED,\n pruneReviews,\n type ReviewsState,\n told,\n} from './review.ts';\nimport {\n checkDraft,\n cleanLabel,\n type GuestDraft,\n type GuestSeat,\n GuestsDoc,\n type GuestsState,\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: 'queued'; submissionId: null; review: 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 readonly reviews: ReplicatedState<ReviewsState | null>;\n open(draft: GuestDraft, context: Context): Promise<GuestOpened>;\n revoke(id: string, context: Context): Promise<{ revoked: boolean }>;\n forgetAlways(id: string, approval: string, context: Context): Promise<{ removed: 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' | 'documentState'>;\n agents: Pick<Agents, 'state' | 'conversation'>;\n approvals?: Pick<Approvals, 'state' | 'request' | 'withdrawSeat' | 'expire' | 'withdraw'>;\n policy?: Pick<ApprovalPolicies, 'current'>;\n reviews?: Pick<GuestReviews, 'task' | 'bind'>;\n now?: () => number;\n context?: Context;\n}\n\nexport const MAX_PROPOSAL = 32_000;\nconst MAX_REQUEST = 128;\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 requestId: string,\n options: GuestsOptions,\n using: Context\n): Promise<Proposed> {\n const conversation = await options.agents.conversation(record.agentId, using);\n if (!conversation) return unanswered('gone');\n const data: GuestInput = { seat: record.id, name: record.label, approval: null };\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 const id = String(submitted.id);\n await options.harness.commit(async (tx) => {\n const kept = (await tx.doc(GuestsDoc)).seats[record.id];\n if (!kept || kept.submissions?.some((entry) => entry.id === id)) return;\n kept.submissions = [\n ...(kept.submissions ?? []),\n { id, approval: null, tools: record.gates.tools },\n ].slice(-KEPT_SUBMISSIONS);\n }, using);\n return { state: 'submitted', submissionId: 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 at: number\n): Promise<Proposed> {\n const gate = record.gates.messages;\n if (gate === 'off') return deliver(record, prepared.text, prepared.requestId, options, using);\n const { reviews } = options;\n if (!reviews || !options.approvals) return unanswered('unavailable');\n const conversation = await options.agents.conversation(record.agentId, using);\n if (!conversation) return unanswered('gone');\n return options.harness.commit(async (tx): Promise<Proposed> => {\n const doc = await tx.doc(GuestReviewsDoc);\n const again = Object.values(doc.reviews).find(\n (review) => review.seat === record.id && review.request === prepared.requestId\n );\n if (again) return { state: 'queued', submissionId: null, review: again.id, reason: null };\n if (liveReviews(doc, record.id).length > MAX_QUEUED) return unanswered('overflow');\n const id = `apr-${crypto.randomUUID()}`;\n const task = await tx.createTask(\n reviews.task,\n { review: id },\n { ownership: { kind: 'conversation' }, conversationId: conversation.id, background: true }\n );\n doc.reviews[id] = {\n id,\n seat: record.id,\n agentId: record.agentId,\n label: record.label,\n text: prepared.text,\n request: prepared.requestId,\n tier: gate,\n tools: record.gates.tools,\n n: doc.next,\n task: Number(task),\n state: 'queued',\n reason: null,\n createdAt: at,\n decidedAt: null,\n submissionId: null,\n ended: false,\n conversation: Number(conversation.id),\n };\n doc.next += 1;\n pruneReviews(doc);\n const asked: GuestAsked = {\n seat: record.id,\n name: record.label,\n review: id,\n text: prepared.text,\n at,\n };\n await tx.appendEntry(conversation.id, { kind: ASKED_KIND, data: { ...asked } });\n return { state: 'queued', submissionId: null, review: id, reason: null };\n }, using);\n}\n\nfunction forgotten(\n harness: GuestsOptions['harness'],\n id: string,\n approval: string,\n using: Context\n): Promise<{ removed: boolean }> {\n return harness.commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[id];\n const kept = record?.alwaysTools?.filter((rule) => rule.approval !== approval);\n if (!record || !kept || kept.length === record.alwaysTools?.length) return { removed: false };\n record.alwaysTools = kept;\n return { removed: true };\n }, using);\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'], reviews: DocumentState<ReviewsState>) =>\n (id: string): number =>\n (approvals?.state.value ?? []).filter((item) => item.seat === id && item.state === 'pending')\n .length + liveReviews(reviews.value, id).filter((review) => review.state === 'queued').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 alwaysTools: copy(record.alwaysTools ?? []),\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(async (tx) => {\n await tx.doc(GuestReviewsDoc);\n return copy((await tx.doc(GuestsDoc)).seats);\n }, context);\n const seatsDoc = (await harness.documentState(GuestsDoc, context)) as DocumentState<GuestsState>;\n const reviews = (await harness.documentState(\n GuestReviewsDoc,\n context\n )) as DocumentState<ReviewsState>;\n const links = new Map<string, number>();\n const ended = new Set<(id: string) => void>();\n const gone = new Map<string, Promise<void>>();\n const shown = () => shownSeats(records, links, now(), pendingIn(options.approvals, reviews));\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 harness.commit(async (tx) => {\n const doc = await tx.doc(GuestReviewsDoc);\n for (const review of liveReviews(doc, record.id))\n if (review.state === 'queued') {\n endReview(review, 'withdrawn', 'revoked', now());\n await told(tx, review);\n }\n }, using);\n await options.approvals?.withdrawSeat(record.id, using);\n });\n const end = (id: string, using: Context = context): Promise<void> => {\n const going = gone.get(id);\n if (going !== undefined) return going;\n const cleaning = cleanup(records[id] as SeatRecord, using);\n gone.set(id, cleaning);\n for (const listener of [...ended]) listener(id);\n return cleaning;\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 offSeats = seatsDoc.subscribe((value) => {\n for (const id of Object.keys(records)) delete records[id];\n Object.assign(records, copy((value as GuestsState).seats));\n publish();\n });\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 recount = () => seats.replace(context, shown());\n const offPending = options.approvals?.state.subscribe(recount);\n const offReviews = reviews.subscribe(recount);\n options.reviews?.bind({\n approvals: options.approvals as NonNullable<GuestsOptions['approvals']>,\n agents,\n line: locked,\n alwaysAllowed: () => guestPolicy().always.messages,\n });\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 reviews,\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 return Object.assign(record, { revoked: at, epoch: record.epoch + 1, alwaysTools: [] });\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 forgetAlways: (id, approval, using) => forgotten(harness, id, approval, using),\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, now()) : unanswered('revoked');\n });\n },\n policy: guestPolicy,\n dispose() {\n if (timer) clearTimeout(timer);\n offPending?.();\n offReviews();\n offSeats();\n seatsDoc.dispose();\n reviews.dispose();\n ended.clear();\n },\n };\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAA6C,uBAAuB;AACpE,SAAS,0BAA0B;AACnC,SAA2C,gBAAgB;AAI3D,SAAS,iBAAiB;AAE1B;AAAA,EACE;AAAA,EACA;AAAA,EAGA;AAAA,OACK;AACP,SAAS,qBAAqB;AAC9B,SAAS,
|
|
4
|
+
"sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { type DocumentState, type Harness, InboxDoc } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { 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 {\n ASKED_KIND,\n GUEST_INPUT_KIND,\n type GuestAsked,\n type GuestInput,\n guestContent,\n} from './attribution.ts';\nimport { GUEST_REQUEST } from './guard.ts';\nimport { DEFAULT_GUEST_POLICY, type GuestPolicy, gateKind } from './policy.ts';\nimport {\n endReview,\n type GuestReviews,\n GuestReviewsDoc,\n KEPT_SUBMISSIONS,\n liveReviews,\n MAX_QUEUED,\n pruneReviews,\n type ReviewsState,\n told,\n} from './review.ts';\nimport {\n checkDraft,\n cleanLabel,\n type GuestDraft,\n type GuestSeat,\n GuestsDoc,\n type GuestsState,\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: 'queued'; submissionId: null; review: 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 readonly reviews: ReplicatedState<ReviewsState | null>;\n open(draft: GuestDraft, context: Context): Promise<GuestOpened>;\n revoke(id: string, context: Context): Promise<{ revoked: boolean }>;\n forgetAlways(id: string, approval: string, context: Context): Promise<{ removed: 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' | 'documentState'>;\n agents: Pick<Agents, 'state' | 'conversation'>;\n approvals?: Pick<Approvals, 'state' | 'request' | 'withdrawSeat' | 'expire' | 'withdraw'>;\n policy?: Pick<ApprovalPolicies, 'current'>;\n reviews?: Pick<GuestReviews, 'task' | 'bind'>;\n now?: () => number;\n context?: Context;\n}\n\nexport const MAX_PROPOSAL = 32_000;\nconst MAX_REQUEST = 128;\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 requestId: string,\n options: GuestsOptions,\n using: Context\n): Promise<Proposed> {\n const conversation = await options.agents.conversation(record.agentId, using);\n if (!conversation) return unanswered('gone');\n const data: GuestInput = { seat: record.id, name: record.label, approval: null };\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 const id = String(submitted.id);\n await options.harness.commit(async (tx) => {\n const kept = (await tx.doc(GuestsDoc)).seats[record.id];\n if (!kept || kept.submissions?.some((entry) => entry.id === id)) return;\n kept.submissions = [\n ...(kept.submissions ?? []),\n { id, approval: null, tools: record.gates.tools },\n ].slice(-KEPT_SUBMISSIONS);\n }, using);\n return { state: 'submitted', submissionId: 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 at: number\n): Promise<Proposed> {\n const gate = record.gates.messages;\n if (gateKind(gate) === 'scoop') return unanswered('scoop');\n if (gate === 'off') return deliver(record, prepared.text, prepared.requestId, options, using);\n const { reviews } = options;\n if (!reviews || !options.approvals) return unanswered('unavailable');\n const conversation = await options.agents.conversation(record.agentId, using);\n if (!conversation) return unanswered('gone');\n return options.harness.commit(async (tx): Promise<Proposed> => {\n const doc = await tx.doc(GuestReviewsDoc);\n const again = Object.values(doc.reviews).find(\n (review) => review.seat === record.id && review.request === prepared.requestId\n );\n if (again) return { state: 'queued', submissionId: null, review: again.id, reason: null };\n if (liveReviews(doc, record.id).length > MAX_QUEUED) return unanswered('overflow');\n const id = `apr-${crypto.randomUUID()}`;\n const task = await tx.createTask(\n reviews.task,\n { review: id },\n { ownership: { kind: 'conversation' }, conversationId: conversation.id, background: true }\n );\n doc.reviews[id] = {\n id,\n seat: record.id,\n agentId: record.agentId,\n label: record.label,\n text: prepared.text,\n request: prepared.requestId,\n tier: gate,\n tools: record.gates.tools,\n n: doc.next,\n task: Number(task),\n state: 'queued',\n reason: null,\n createdAt: at,\n decidedAt: null,\n submissionId: null,\n ended: false,\n conversation: Number(conversation.id),\n };\n doc.next += 1;\n pruneReviews(doc);\n const asked: GuestAsked = {\n seat: record.id,\n name: record.label,\n review: id,\n text: prepared.text,\n at,\n };\n await tx.appendEntry(conversation.id, { kind: ASKED_KIND, data: { ...asked } });\n return { state: 'queued', submissionId: null, review: id, reason: null };\n }, using);\n}\n\nfunction forgotten(\n harness: GuestsOptions['harness'],\n id: string,\n approval: string,\n using: Context\n): Promise<{ removed: boolean }> {\n return harness.commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[id];\n const kept = record?.alwaysTools?.filter((rule) => rule.approval !== approval);\n if (!record || !kept || kept.length === record.alwaysTools?.length) return { removed: false };\n record.alwaysTools = kept;\n return { removed: true };\n }, using);\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'], reviews: DocumentState<ReviewsState>) =>\n (id: string): number =>\n (approvals?.state.value ?? []).filter((item) => item.seat === id && item.state === 'pending')\n .length + liveReviews(reviews.value, id).filter((review) => review.state === 'queued').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 alwaysTools: copy(record.alwaysTools ?? []),\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(async (tx) => {\n await tx.doc(GuestReviewsDoc);\n return copy((await tx.doc(GuestsDoc)).seats);\n }, context);\n const seatsDoc = (await harness.documentState(GuestsDoc, context)) as DocumentState<GuestsState>;\n const reviews = (await harness.documentState(\n GuestReviewsDoc,\n context\n )) as DocumentState<ReviewsState>;\n const links = new Map<string, number>();\n const ended = new Set<(id: string) => void>();\n const gone = new Map<string, Promise<void>>();\n const shown = () => shownSeats(records, links, now(), pendingIn(options.approvals, reviews));\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 harness.commit(async (tx) => {\n const doc = await tx.doc(GuestReviewsDoc);\n for (const review of liveReviews(doc, record.id))\n if (review.state === 'queued') {\n endReview(review, 'withdrawn', 'revoked', now());\n await told(tx, review);\n }\n }, using);\n await options.approvals?.withdrawSeat(record.id, using);\n });\n const end = (id: string, using: Context = context): Promise<void> => {\n const going = gone.get(id);\n if (going !== undefined) return going;\n const cleaning = cleanup(records[id] as SeatRecord, using);\n gone.set(id, cleaning);\n for (const listener of [...ended]) listener(id);\n return cleaning;\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 offSeats = seatsDoc.subscribe((value) => {\n for (const id of Object.keys(records)) delete records[id];\n Object.assign(records, copy((value as GuestsState).seats));\n publish();\n });\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 recount = () => seats.replace(context, shown());\n const offPending = options.approvals?.state.subscribe(recount);\n const offReviews = reviews.subscribe(recount);\n options.reviews?.bind({\n approvals: options.approvals as NonNullable<GuestsOptions['approvals']>,\n agents,\n line: locked,\n alwaysAllowed: () => guestPolicy().always.messages,\n });\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 reviews,\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 return Object.assign(record, { revoked: at, epoch: record.epoch + 1, alwaysTools: [] });\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 forgetAlways: (id, approval, using) => forgotten(harness, id, approval, using),\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, now()) : unanswered('revoked');\n });\n },\n policy: guestPolicy,\n dispose() {\n if (timer) clearTimeout(timer);\n offPending?.();\n offReviews();\n offSeats();\n seatsDoc.dispose();\n reviews.dispose();\n ended.clear();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/guests/policy.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const GATE_KINDS = ["user", "agent", "cone", "scoop", "off"];
|
|
2
2
|
const DEFAULT_GUEST_POLICY = {
|
|
3
3
|
manageGuests: "deny",
|
|
4
|
-
allowedToolGates: ["user", "agent"
|
|
5
|
-
allowedMessageGates: ["user", "agent", "cone", "
|
|
4
|
+
allowedToolGates: ["user", "agent"],
|
|
5
|
+
allowedMessageGates: ["user", "agent", "cone", "off"],
|
|
6
6
|
maxSeatDays: 30,
|
|
7
7
|
always: { messages: false, tools: false }
|
|
8
8
|
};
|
|
9
|
+
const SCOOP_REVIEW_LATER = "This review isn't available yet.";
|
|
9
10
|
const MAX_SEAT_DAYS = 365;
|
|
10
11
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
11
12
|
const GUESTS_MANAGE_DENIED = "The owner's guest policy doesn't let guests manage guests.";
|
|
@@ -68,6 +69,7 @@ export {
|
|
|
68
69
|
GUESTS_MANAGE_LATER,
|
|
69
70
|
MAX_SEAT_DAYS,
|
|
70
71
|
POLICY_MANAGE_DENY_ONLY,
|
|
72
|
+
SCOOP_REVIEW_LATER,
|
|
71
73
|
changedGuests,
|
|
72
74
|
gateKind,
|
|
73
75
|
gateRefusal
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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'
|
|
5
|
-
"mappings": "AAcO,MAAM,aAAkC,CAAC,QAAQ,SAAS,QAAQ,SAAS,KAAK;AAEhF,MAAM,uBAAoC;AAAA,EAC/C,cAAc;AAAA,EACd,kBAAkB,CAAC,QAAQ,
|
|
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'],\n allowedMessageGates: ['user', 'agent', 'cone', 'off'],\n maxSeatDays: 30,\n always: { messages: false, tools: false },\n};\n\nexport const SCOOP_REVIEW_LATER = \"This review isn't available yet.\";\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,OAAO;AAAA,EAClC,qBAAqB,CAAC,QAAQ,SAAS,QAAQ,KAAK;AAAA,EACpD,aAAa;AAAA,EACb,QAAQ,EAAE,UAAU,OAAO,OAAO,MAAM;AAC1C;AAEO,MAAM,qBAAqB;AAE3B,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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -38,7 +38,7 @@ function shown(item, agentName) {
|
|
|
38
38
|
...base,
|
|
39
39
|
agentId: item.agentId,
|
|
40
40
|
...agentName ? { agentName } : {},
|
|
41
|
-
always:
|
|
41
|
+
always: item.always,
|
|
42
42
|
...item.verdict ? { verdict: { decision: item.verdict.decision, reason: item.verdict.reason } } : {}
|
|
43
43
|
};
|
|
44
44
|
if (item.kind === "guest-tool")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/spectrum/approvals-port.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n Approval,\n ApprovalDecision,\n ApprovalEnd,\n ApprovalRequester,\n ApprovalsEvents,\n ApprovalsPort,\n} from '@ai-ecoverse/slicc-spectrum/ui';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Approval as Item } from '../approvals/record.ts';\nimport type { AgentConnection } from '../client.ts';\nimport { Emitter } from './emitter.ts';\n\nconst ENDS: Record<string, ApprovalEnd> = {\n approved: 'answered',\n rejected: 'answered',\n unanswered: 'expired',\n withdrawn: 'withdrawn',\n};\n\nfunction requesterOf(item: Item): ApprovalRequester {\n const { kind, name, agentId, where } = item.requester;\n return {\n kind,\n name,\n ...(agentId ? { agentId } : {}),\n ...(where ? { where } : {}),\n };\n}\n\nexport function shown(item: Item, agentName: string | undefined): Approval {\n const base = {\n id: item.id,\n requester: requesterOf(item),\n createdAt: item.createdAt,\n expiresAt: item.expiresAt,\n };\n if (item.kind === 'sudo') {\n const { command, target, cwd, alwaysScope } = item.detail;\n return {\n ...base,\n kind: 'sudo',\n command,\n ...(target ? { target } : {}),\n ...(cwd ? { cwd } : {}),\n always: item.always,\n ...(alwaysScope ? { alwaysScope } : {}),\n };\n }\n const guest = {\n ...base,\n agentId: item.agentId,\n ...(agentName ? { agentName } : {}),\n always:
|
|
5
|
-
"mappings": "AAQA,SAAS,0BAA0B;AAGnC,SAAS,eAAe;AAExB,MAAM,OAAoC;AAAA,EACxC,UAAU;AAAA,EACV,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,WAAW;AACb;AAEA,SAAS,YAAY,MAA+B;AAClD,QAAM,EAAE,MAAM,MAAM,SAAS,MAAM,IAAI,KAAK;AAC5C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,EAC3B;AACF;AAEO,SAAS,MAAM,MAAY,WAAyC;AACzE,QAAM,OAAO;AAAA,IACX,IAAI,KAAK;AAAA,IACT,WAAW,YAAY,IAAI;AAAA,IAC3B,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK;AAAA,EAClB;AACA,MAAI,KAAK,SAAS,QAAQ;AACxB,UAAM,EAAE,SAAS,QAAQ,KAAK,YAAY,IAAI,KAAK;AACnD,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,MACN;AAAA,MACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,MAC3B,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;AAAA,MACrB,QAAQ,KAAK;AAAA,MACb,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,IACvC;AAAA,EACF;AACA,QAAM,QAAQ;AAAA,IACZ,GAAG;AAAA,IACH,SAAS,KAAK;AAAA,IACd,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC,QAAQ;AAAA,
|
|
4
|
+
"sourcesContent": ["import type {\n Approval,\n ApprovalDecision,\n ApprovalEnd,\n ApprovalRequester,\n ApprovalsEvents,\n ApprovalsPort,\n} from '@ai-ecoverse/slicc-spectrum/ui';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Approval as Item } from '../approvals/record.ts';\nimport type { AgentConnection } from '../client.ts';\nimport { Emitter } from './emitter.ts';\n\nconst ENDS: Record<string, ApprovalEnd> = {\n approved: 'answered',\n rejected: 'answered',\n unanswered: 'expired',\n withdrawn: 'withdrawn',\n};\n\nfunction requesterOf(item: Item): ApprovalRequester {\n const { kind, name, agentId, where } = item.requester;\n return {\n kind,\n name,\n ...(agentId ? { agentId } : {}),\n ...(where ? { where } : {}),\n };\n}\n\nexport function shown(item: Item, agentName: string | undefined): Approval {\n const base = {\n id: item.id,\n requester: requesterOf(item),\n createdAt: item.createdAt,\n expiresAt: item.expiresAt,\n };\n if (item.kind === 'sudo') {\n const { command, target, cwd, alwaysScope } = item.detail;\n return {\n ...base,\n kind: 'sudo',\n command,\n ...(target ? { target } : {}),\n ...(cwd ? { cwd } : {}),\n always: item.always,\n ...(alwaysScope ? { alwaysScope } : {}),\n };\n }\n const guest = {\n ...base,\n agentId: item.agentId,\n ...(agentName ? { agentName } : {}),\n always: item.always,\n ...(item.verdict\n ? { verdict: { decision: item.verdict.decision, reason: item.verdict.reason } }\n : {}),\n };\n if (item.kind === 'guest-tool')\n return { ...guest, kind: 'guest-tool', tool: item.detail.tool, summary: item.detail.summary };\n return {\n ...guest,\n kind: 'guest-message',\n text: item.detail.text,\n ...(item.detail.attachments.length\n ? { attachments: item.detail.attachments.map((file) => ({ ...file })) }\n : {}),\n };\n}\n\nexport type HostApprovals = ApprovalsPort;\n\nexport const NOT_HERE = \"This request can't be answered in this tab.\";\n\nconst asked = (item: Item) => item.state === 'pending' && item.tier === 'user';\n\nexport class ApprovalsAdapter extends Emitter<ApprovalsEvents> implements ApprovalsPort {\n readonly #connection: AgentConnection;\n readonly #host: HostApprovals | undefined;\n readonly #ours: boolean;\n readonly #mine = new Set<string>();\n readonly #deciding = new Set<string>();\n #listed = new Set<string>();\n readonly decide?: (id: string, decision: ApprovalDecision, reason?: string) => Promise<void>;\n\n constructor(connection: AgentConnection, host?: HostApprovals) {\n super();\n this.#connection = connection;\n this.#host = host;\n this.#ours = !connection.guest;\n if (this.#ours || host?.decide) this.decide = (...args) => this.#route(...args);\n this.#listed = new Set(this.#agent().map((approval) => approval.id));\n connection.approvals?.subscribe(() => this.#changed());\n host?.on('approvals', () => this.emit('approvals', this.list()));\n host?.on('ended', (detail) => this.emit('ended', detail));\n }\n\n #items(): readonly Item[] {\n return this.#connection.approvals?.value ?? [];\n }\n\n list(): readonly Approval[] {\n return [...(this.#host?.list() ?? []), ...this.#agent()].sort(\n (a, b) => a.createdAt - b.createdAt\n );\n }\n\n #agent(): readonly Approval[] {\n const names = new Map(\n (this.#connection.agents?.value?.agents ?? []).map((agent) => [agent.id, agent.name])\n );\n return this.#items()\n .filter((item) => asked(item) && !this.#mine.has(item.id))\n .sort((a, b) => a.createdAt - b.createdAt)\n .map((item) => shown(item, names.get(item.agentId)));\n }\n\n #changed(): void {\n const items = new Map(this.#items().map((item) => [item.id, item]));\n const listed = this.#agent();\n const now = new Set(listed.map((approval) => approval.id));\n for (const id of this.#listed) {\n if (now.has(id) || this.#mine.has(id) || this.#deciding.has(id)) continue;\n const state = items.get(id)?.state;\n this.emit('ended', { id, reason: (state && ENDS[state]) || 'answered' });\n }\n for (const id of this.#mine) {\n const item = items.get(id);\n if (!item || !asked(item)) this.#mine.delete(id);\n }\n this.#listed = now;\n this.emit('approvals', this.list());\n }\n\n #route(id: string, decision: ApprovalDecision, reason?: string): Promise<void> {\n const host = this.#host;\n if (host?.list().some((approval) => approval.id === id)) {\n if (!host.decide) return Promise.reject(new Error(NOT_HERE));\n return host.decide(id, decision, reason);\n }\n if (!this.#ours) return Promise.reject(new Error(NOT_HERE));\n return this.#decide(id, decision, reason);\n }\n\n async #decide(id: string, decision: ApprovalDecision, reason?: string): Promise<void> {\n this.#deciding.add(id);\n try {\n const answer = await this.#connection.control.decide(\n id,\n decision,\n reason ?? null,\n BACKGROUND_CONTEXT\n );\n if (!answer.done) throw new Error(answer.error);\n this.#mine.add(id);\n } finally {\n this.#deciding.delete(id);\n }\n this.#changed();\n }\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,0BAA0B;AAGnC,SAAS,eAAe;AAExB,MAAM,OAAoC;AAAA,EACxC,UAAU;AAAA,EACV,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,WAAW;AACb;AAEA,SAAS,YAAY,MAA+B;AAClD,QAAM,EAAE,MAAM,MAAM,SAAS,MAAM,IAAI,KAAK;AAC5C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,EAC3B;AACF;AAEO,SAAS,MAAM,MAAY,WAAyC;AACzE,QAAM,OAAO;AAAA,IACX,IAAI,KAAK;AAAA,IACT,WAAW,YAAY,IAAI;AAAA,IAC3B,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK;AAAA,EAClB;AACA,MAAI,KAAK,SAAS,QAAQ;AACxB,UAAM,EAAE,SAAS,QAAQ,KAAK,YAAY,IAAI,KAAK;AACnD,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,MACN;AAAA,MACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,MAC3B,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;AAAA,MACrB,QAAQ,KAAK;AAAA,MACb,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,IACvC;AAAA,EACF;AACA,QAAM,QAAQ;AAAA,IACZ,GAAG;AAAA,IACH,SAAS,KAAK;AAAA,IACd,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC,QAAQ,KAAK;AAAA,IACb,GAAI,KAAK,UACL,EAAE,SAAS,EAAE,UAAU,KAAK,QAAQ,UAAU,QAAQ,KAAK,QAAQ,OAAO,EAAE,IAC5E,CAAC;AAAA,EACP;AACA,MAAI,KAAK,SAAS;AAChB,WAAO,EAAE,GAAG,OAAO,MAAM,cAAc,MAAM,KAAK,OAAO,MAAM,SAAS,KAAK,OAAO,QAAQ;AAC9F,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM;AAAA,IACN,MAAM,KAAK,OAAO;AAAA,IAClB,GAAI,KAAK,OAAO,YAAY,SACxB,EAAE,aAAa,KAAK,OAAO,YAAY,IAAI,CAAC,UAAU,EAAE,GAAG,KAAK,EAAE,EAAE,IACpE,CAAC;AAAA,EACP;AACF;AAIO,MAAM,WAAW;AAExB,MAAM,QAAQ,CAAC,SAAe,KAAK,UAAU,aAAa,KAAK,SAAS;AAEjE,MAAM,yBAAyB,QAAkD;AAAA,EAC7E;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ,oBAAI,IAAY;AAAA,EACxB,YAAY,oBAAI,IAAY;AAAA,EACrC,UAAU,oBAAI,IAAY;AAAA,EACjB;AAAA,EAET,YAAY,YAA6B,MAAsB;AAC7D,UAAM;AACN,SAAK,cAAc;AACnB,SAAK,QAAQ;AACb,SAAK,QAAQ,CAAC,WAAW;AACzB,QAAI,KAAK,SAAS,MAAM,OAAQ,MAAK,SAAS,IAAI,SAAS,KAAK,OAAO,GAAG,IAAI;AAC9E,SAAK,UAAU,IAAI,IAAI,KAAK,OAAO,EAAE,IAAI,CAAC,aAAa,SAAS,EAAE,CAAC;AACnE,eAAW,WAAW,UAAU,MAAM,KAAK,SAAS,CAAC;AACrD,UAAM,GAAG,aAAa,MAAM,KAAK,KAAK,aAAa,KAAK,KAAK,CAAC,CAAC;AAC/D,UAAM,GAAG,SAAS,CAAC,WAAW,KAAK,KAAK,SAAS,MAAM,CAAC;AAAA,EAC1D;AAAA,EAEA,SAA0B;AACxB,WAAO,KAAK,YAAY,WAAW,SAAS,CAAC;AAAA,EAC/C;AAAA,EAEA,OAA4B;AAC1B,WAAO,CAAC,GAAI,KAAK,OAAO,KAAK,KAAK,CAAC,GAAI,GAAG,KAAK,OAAO,CAAC,EAAE;AAAA,MACvD,CAAC,GAAG,MAAM,EAAE,YAAY,EAAE;AAAA,IAC5B;AAAA,EACF;AAAA,EAEA,SAA8B;AAC5B,UAAM,QAAQ,IAAI;AAAA,OACf,KAAK,YAAY,QAAQ,OAAO,UAAU,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,MAAM,IAAI,CAAC;AAAA,IACtF;AACA,WAAO,KAAK,OAAO,EAChB,OAAO,CAAC,SAAS,MAAM,IAAI,KAAK,CAAC,KAAK,MAAM,IAAI,KAAK,EAAE,CAAC,EACxD,KAAK,CAAC,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS,EACxC,IAAI,CAAC,SAAS,MAAM,MAAM,MAAM,IAAI,KAAK,OAAO,CAAC,CAAC;AAAA,EACvD;AAAA,EAEA,WAAiB;AACf,UAAM,QAAQ,IAAI,IAAI,KAAK,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAClE,UAAM,SAAS,KAAK,OAAO;AAC3B,UAAM,MAAM,IAAI,IAAI,OAAO,IAAI,CAAC,aAAa,SAAS,EAAE,CAAC;AACzD,eAAW,MAAM,KAAK,SAAS;AAC7B,UAAI,IAAI,IAAI,EAAE,KAAK,KAAK,MAAM,IAAI,EAAE,KAAK,KAAK,UAAU,IAAI,EAAE,EAAG;AACjE,YAAM,QAAQ,MAAM,IAAI,EAAE,GAAG;AAC7B,WAAK,KAAK,SAAS,EAAE,IAAI,QAAS,SAAS,KAAK,KAAK,KAAM,WAAW,CAAC;AAAA,IACzE;AACA,eAAW,MAAM,KAAK,OAAO;AAC3B,YAAM,OAAO,MAAM,IAAI,EAAE;AACzB,UAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAG,MAAK,MAAM,OAAO,EAAE;AAAA,IACjD;AACA,SAAK,UAAU;AACf,SAAK,KAAK,aAAa,KAAK,KAAK,CAAC;AAAA,EACpC;AAAA,EAEA,OAAO,IAAY,UAA4B,QAAgC;AAC7E,UAAM,OAAO,KAAK;AAClB,QAAI,MAAM,KAAK,EAAE,KAAK,CAAC,aAAa,SAAS,OAAO,EAAE,GAAG;AACvD,UAAI,CAAC,KAAK,OAAQ,QAAO,QAAQ,OAAO,IAAI,MAAM,QAAQ,CAAC;AAC3D,aAAO,KAAK,OAAO,IAAI,UAAU,MAAM;AAAA,IACzC;AACA,QAAI,CAAC,KAAK,MAAO,QAAO,QAAQ,OAAO,IAAI,MAAM,QAAQ,CAAC;AAC1D,WAAO,KAAK,QAAQ,IAAI,UAAU,MAAM;AAAA,EAC1C;AAAA,EAEA,MAAM,QAAQ,IAAY,UAA4B,QAAgC;AACpF,SAAK,UAAU,IAAI,EAAE;AACrB,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,YAAY,QAAQ;AAAA,QAC5C;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA,MACF;AACA,UAAI,CAAC,OAAO,KAAM,OAAM,IAAI,MAAM,OAAO,KAAK;AAC9C,WAAK,MAAM,IAAI,EAAE;AAAA,IACnB,UAAE;AACA,WAAK,UAAU,OAAO,EAAE;AAAA,IAC1B;AACA,SAAK,SAAS;AAAA,EAChB;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/spectrum/guest-port.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n GuestDraft,\n GuestEvents,\n GuestGate,\n GuestPort,\n GuestSeat,\n} from '@ai-ecoverse/slicc-spectrum/ui';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { AgentConnection } from '../client.ts';\nimport { DEFAULT_GUEST_POLICY, type GateKind } from '../guests/policy.ts';\nimport { Emitter } from './emitter.ts';\n\nexport interface GuestHub {\n mint(request: GuestDraft & { agentId: string }): Promise<{ id: string; url: string }>;\n revoke(id: string): Promise<void>;\n}\n\nexport const NO_HUB = 'Inviting guests needs the tray hub.';\nexport const NOT_OWNER_DEVICE = \"Only the owner's devices can invite guests.\";\nexport const NO_CONTROL = NOT_OWNER_DEVICE;\nexport const NO_OWNER = NOT_OWNER_DEVICE;\n\nconst offered = (kinds: readonly GateKind[]): GuestGate[] =>\n kinds.map((kind) => (kind === 'scoop' ? 'scoop:*' : kind));\n\nexport class GuestAdapter extends Emitter<GuestEvents> implements GuestPort {\n readonly #connection: AgentConnection;\n readonly #hub: GuestHub | null;\n\n constructor(connection: AgentConnection, hub: GuestHub | null) {\n super();\n this.#connection = connection;\n this.#hub = hub;\n connection.guests?.subscribe(() => this.emit('seats', this.list()));\n }\n\n list(): readonly GuestSeat[] {\n return this.#connection.guests?.value ?? [];\n }\n\n gates():
|
|
5
|
-
"mappings": "AAOA,SAAS,0BAA0B;AAEnC,SAAS,4BAA2C;AACpD,SAAS,eAAe;AAOjB,MAAM,SAAS;AACf,MAAM,mBAAmB;AACzB,MAAM,aAAa;AACnB,MAAM,WAAW;AAExB,MAAM,UAAU,CAAC,UACf,MAAM,IAAI,CAAC,SAAU,SAAS,UAAU,YAAY,IAAK;AAEpD,MAAM,qBAAqB,QAA0C;AAAA,EACjE;AAAA,EACA;AAAA,EAET,YAAY,YAA6B,KAAsB;AAC7D,UAAM;AACN,SAAK,cAAc;AACnB,SAAK,OAAO;AACZ,eAAW,QAAQ,UAAU,MAAM,KAAK,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC;AAAA,EACpE;AAAA,EAEA,OAA6B;AAC3B,WAAO,KAAK,YAAY,QAAQ,SAAS,CAAC;AAAA,EAC5C;AAAA,EAEA,
|
|
4
|
+
"sourcesContent": ["import type {\n GuestDraft,\n GuestEvents,\n GuestGate,\n GuestPort,\n GuestSeat,\n} from '@ai-ecoverse/slicc-spectrum/ui';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { AgentConnection } from '../client.ts';\nimport { DEFAULT_GUEST_POLICY, type GateKind } from '../guests/policy.ts';\nimport { Emitter } from './emitter.ts';\n\nexport interface GuestHub {\n mint(request: GuestDraft & { agentId: string }): Promise<{ id: string; url: string }>;\n revoke(id: string): Promise<void>;\n}\n\nexport const NO_HUB = 'Inviting guests needs the tray hub.';\nexport const NOT_OWNER_DEVICE = \"Only the owner's devices can invite guests.\";\nexport const NO_CONTROL = NOT_OWNER_DEVICE;\nexport const NO_OWNER = NOT_OWNER_DEVICE;\n\nconst offered = (kinds: readonly GateKind[]): GuestGate[] =>\n kinds.map((kind) => (kind === 'scoop' ? 'scoop:*' : kind));\n\nexport class GuestAdapter extends Emitter<GuestEvents> implements GuestPort {\n readonly #connection: AgentConnection;\n readonly #hub: GuestHub | null;\n\n constructor(connection: AgentConnection, hub: GuestHub | null) {\n super();\n this.#connection = connection;\n this.#hub = hub;\n connection.guests?.subscribe(() => this.emit('seats', this.list()));\n }\n\n list(): readonly GuestSeat[] {\n return this.#connection.guests?.value ?? [];\n }\n\n gates(): ReturnType<NonNullable<GuestPort['gates']>> {\n const policy = this.#connection.approvalPolicy?.value?.guests ?? DEFAULT_GUEST_POLICY;\n return {\n messages: offered(policy.allowedMessageGates),\n tools: offered(policy.allowedToolGates),\n };\n }\n\n unavailable(): string | null {\n const connection = this.#connection;\n if (connection.guest || !connection.guests) return NOT_OWNER_DEVICE;\n return this.#hub ? null : NO_HUB;\n }\n\n async mint(agentId: string, draft: GuestDraft): Promise<{ seat: GuestSeat; url: string }> {\n const problem = this.unavailable();\n if (problem) throw new Error(problem);\n const hub = this.#hub as GuestHub;\n const gates = { messages: draft.gates.messages, tools: draft.gates.tools };\n const minted = await hub.mint({\n label: draft.label,\n expiresAt: draft.expiresAt,\n gates,\n agentId,\n });\n const opened = await this.#connection.control\n .guestOpen(\n { id: minted.id, label: draft.label, agentId, expiresAt: draft.expiresAt, gates },\n BACKGROUND_CONTEXT\n )\n .catch((error: Error) => ({ seat: null, error: error.message }));\n if (!opened.seat) {\n await hub.revoke(minted.id).catch(() => undefined);\n throw new Error(opened.error);\n }\n return { seat: opened.seat, url: minted.url };\n }\n\n async revoke(id: string): Promise<void> {\n await this.#connection.control.guestRevoke(id, BACKGROUND_CONTEXT);\n await this.#hub?.revoke(id);\n }\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,0BAA0B;AAEnC,SAAS,4BAA2C;AACpD,SAAS,eAAe;AAOjB,MAAM,SAAS;AACf,MAAM,mBAAmB;AACzB,MAAM,aAAa;AACnB,MAAM,WAAW;AAExB,MAAM,UAAU,CAAC,UACf,MAAM,IAAI,CAAC,SAAU,SAAS,UAAU,YAAY,IAAK;AAEpD,MAAM,qBAAqB,QAA0C;AAAA,EACjE;AAAA,EACA;AAAA,EAET,YAAY,YAA6B,KAAsB;AAC7D,UAAM;AACN,SAAK,cAAc;AACnB,SAAK,OAAO;AACZ,eAAW,QAAQ,UAAU,MAAM,KAAK,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC;AAAA,EACpE;AAAA,EAEA,OAA6B;AAC3B,WAAO,KAAK,YAAY,QAAQ,SAAS,CAAC;AAAA,EAC5C;AAAA,EAEA,QAAqD;AACnD,UAAM,SAAS,KAAK,YAAY,gBAAgB,OAAO,UAAU;AACjE,WAAO;AAAA,MACL,UAAU,QAAQ,OAAO,mBAAmB;AAAA,MAC5C,OAAO,QAAQ,OAAO,gBAAgB;AAAA,IACxC;AAAA,EACF;AAAA,EAEA,cAA6B;AAC3B,UAAM,aAAa,KAAK;AACxB,QAAI,WAAW,SAAS,CAAC,WAAW,OAAQ,QAAO;AACnD,WAAO,KAAK,OAAO,OAAO;AAAA,EAC5B;AAAA,EAEA,MAAM,KAAK,SAAiB,OAA8D;AACxF,UAAM,UAAU,KAAK,YAAY;AACjC,QAAI,QAAS,OAAM,IAAI,MAAM,OAAO;AACpC,UAAM,MAAM,KAAK;AACjB,UAAM,QAAQ,EAAE,UAAU,MAAM,MAAM,UAAU,OAAO,MAAM,MAAM,MAAM;AACzE,UAAM,SAAS,MAAM,IAAI,KAAK;AAAA,MAC5B,OAAO,MAAM;AAAA,MACb,WAAW,MAAM;AAAA,MACjB;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,SAAS,MAAM,KAAK,YAAY,QACnC;AAAA,MACC,EAAE,IAAI,OAAO,IAAI,OAAO,MAAM,OAAO,SAAS,WAAW,MAAM,WAAW,MAAM;AAAA,MAChF;AAAA,IACF,EACC,MAAM,CAAC,WAAkB,EAAE,MAAM,MAAM,OAAO,MAAM,QAAQ,EAAE;AACjE,QAAI,CAAC,OAAO,MAAM;AAChB,YAAM,IAAI,OAAO,OAAO,EAAE,EAAE,MAAM,MAAM,MAAS;AACjD,YAAM,IAAI,MAAM,OAAO,KAAK;AAAA,IAC9B;AACA,WAAO,EAAE,MAAM,OAAO,MAAM,KAAK,OAAO,IAAI;AAAA,EAC9C;AAAA,EAEA,MAAM,OAAO,IAA2B;AACtC,UAAM,KAAK,YAAY,QAAQ,YAAY,IAAI,kBAAkB;AACjE,UAAM,KAAK,MAAM,OAAO,EAAE;AAAA,EAC5B;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,7 +12,7 @@ export declare const GATE_REASONS: {
|
|
|
12
12
|
readonly judged: 'The automatic check declined this tool call in a run a guest started';
|
|
13
13
|
readonly unanswered: 'Nobody approved this tool call in a run a guest started in time.';
|
|
14
14
|
readonly unroutable: "The cone can't review a tool call it is itself waiting on.";
|
|
15
|
-
readonly scoop: "
|
|
15
|
+
readonly scoop: "This review isn't available yet.";
|
|
16
16
|
readonly withdrawn: 'The review of this tool call was withdrawn.';
|
|
17
17
|
};
|
|
18
18
|
export interface GateHost {
|
|
@@ -13,6 +13,7 @@ export type GuestPolicy = {
|
|
|
13
13
|
export type GuestPolicyChange = Partial<GuestPolicy>;
|
|
14
14
|
export declare const GATE_KINDS: readonly GateKind[];
|
|
15
15
|
export declare const DEFAULT_GUEST_POLICY: GuestPolicy;
|
|
16
|
+
export declare const SCOOP_REVIEW_LATER = "This review isn't available yet.";
|
|
16
17
|
export declare const MAX_SEAT_DAYS = 365;
|
|
17
18
|
export declare const DAY_MS: number;
|
|
18
19
|
export declare const GUESTS_MANAGE_DENIED = "The owner's guest policy doesn't let guests manage guests.";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GuestDraft, GuestEvents,
|
|
1
|
+
import type { GuestDraft, GuestEvents, GuestPort, GuestSeat } from '@ai-ecoverse/slicc-spectrum/ui';
|
|
2
2
|
import type { AgentConnection } from '../client.ts';
|
|
3
3
|
import { Emitter } from './emitter.ts';
|
|
4
4
|
export interface GuestHub {
|
|
@@ -18,10 +18,7 @@ export declare class GuestAdapter extends Emitter<GuestEvents> implements GuestP
|
|
|
18
18
|
#private;
|
|
19
19
|
constructor(connection: AgentConnection, hub: GuestHub | null);
|
|
20
20
|
list(): readonly GuestSeat[];
|
|
21
|
-
gates():
|
|
22
|
-
messages: GuestGate[];
|
|
23
|
-
tools: GuestGate[];
|
|
24
|
-
};
|
|
21
|
+
gates(): ReturnType<NonNullable<GuestPort['gates']>>;
|
|
25
22
|
unavailable(): string | null;
|
|
26
23
|
mint(agentId: string, draft: GuestDraft): Promise<{
|
|
27
24
|
seat: GuestSeat;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-agent",
|
|
3
|
-
"version": "5.8.
|
|
3
|
+
"version": "5.8.3",
|
|
4
4
|
"description": "SLICC's agent core on pi durable: an agent worker, session storage, remoting and pi extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
"release": "semantic-release"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@ai-ecoverse/slicc-kernel": "1.
|
|
66
|
+
"@ai-ecoverse/slicc-kernel": "1.103.4",
|
|
67
67
|
"@ai-ecoverse/slicc-scrub": "1.0.0",
|
|
68
68
|
"@ai-ecoverse/slicc-shared-web": "1.21.1",
|
|
69
|
-
"@ai-ecoverse/slicc-spectrum": "1.
|
|
69
|
+
"@ai-ecoverse/slicc-spectrum": "1.84.0",
|
|
70
70
|
"@ai-ecoverse/wasm-bash": "5.3.0-18",
|
|
71
|
-
"@ai-ecoverse/wasm-coreutils": "9.12.0-
|
|
71
|
+
"@ai-ecoverse/wasm-coreutils": "9.12.0-8",
|
|
72
72
|
"@ai-ecoverse/wasm-git": "2.55.0-15",
|
|
73
73
|
"@biomejs/biome": "2.5.15",
|
|
74
74
|
"@types/node": "24.19.1",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
95
|
"@ai-ecoverse/slicc-kernel": ">=1.90.0 <2",
|
|
96
|
-
"@ai-ecoverse/slicc-spectrum": "^1.
|
|
96
|
+
"@ai-ecoverse/slicc-spectrum": "^1.84.0"
|
|
97
97
|
},
|
|
98
98
|
"peerDependenciesMeta": {
|
|
99
99
|
"@ai-ecoverse/slicc-spectrum": {
|