@fungi.computer/oyster 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +2 -1
  2. package/dist/agent-computer-source.d.ts +42 -0
  3. package/dist/agent-computer-source.js +88 -0
  4. package/dist/agent-computers.d.ts +104 -0
  5. package/dist/agent-computers.js +227 -0
  6. package/dist/agents.d.ts +10 -0
  7. package/dist/agents.js +33 -0
  8. package/dist/boot-capability.d.ts +1 -1
  9. package/dist/boot-capability.js +1 -1
  10. package/dist/browser/boot.d.ts +2 -2
  11. package/dist/browser/boot.js +10 -3
  12. package/dist/browser/browser.d.ts +1 -1
  13. package/dist/browser/browser.js +2 -1
  14. package/dist/browser/cdp-client.d.ts +2 -0
  15. package/dist/browser/errors.d.ts +2 -2
  16. package/dist/browser/errors.js +2 -2
  17. package/dist/browser/index.d.ts +3 -2
  18. package/dist/browser/index.js +3 -2
  19. package/dist/browser/page.js +11 -4
  20. package/dist/browser/sessions.d.ts +7 -4
  21. package/dist/browser/sessions.js +159 -0
  22. package/dist/browser/url-policy.d.ts +4 -2
  23. package/dist/browser/url-policy.js +4 -2
  24. package/dist/computer-control-port.d.ts +26 -0
  25. package/dist/computer-control-port.js +1 -0
  26. package/dist/host.d.ts +3 -0
  27. package/dist/machine-link/delivery.d.ts +1 -1
  28. package/dist/machine-link/delivery.js +2 -3
  29. package/dist/machine-link/host.d.ts +37 -6
  30. package/dist/machine-link/host.js +209 -14
  31. package/dist/machine-link/link.d.ts +13 -1
  32. package/dist/machine-link/link.js +20 -7
  33. package/dist/mcp.d.ts +152 -11
  34. package/dist/mcp.js +131 -13
  35. package/dist/page/computer-handles.d.ts +3 -3
  36. package/dist/page-script.js +1 -1
  37. package/dist/standalone/agent-computers.d.ts +29 -0
  38. package/dist/standalone/agent-computers.js +101 -0
  39. package/dist/standalone/agent-mail.d.ts +41 -18
  40. package/dist/standalone/agent-model-choice.d.ts +26 -0
  41. package/dist/standalone/agent-model-choice.js +72 -0
  42. package/dist/standalone/agents.d.ts +50 -5
  43. package/dist/standalone/agents.js +505 -53
  44. package/dist/standalone/app-builds-binding.js +1 -1
  45. package/dist/standalone/computer-control.d.ts +4 -0
  46. package/dist/standalone/computer-control.js +62 -0
  47. package/dist/standalone/computer-fleet.d.ts +1 -0
  48. package/dist/standalone/computer-fleet.js +1 -1
  49. package/dist/standalone/computer.d.ts +21 -2
  50. package/dist/standalone/computer.js +12 -0
  51. package/dist/standalone/machine-link.d.ts +10 -0
  52. package/dist/standalone/machine-link.js +6 -0
  53. package/dist/standalone/registry.d.ts +12 -0
  54. package/dist/standalone/registry.js +18 -0
  55. package/dist/standalone/tenancy.d.ts +14 -1
  56. package/dist/standalone/tenancy.js +37 -3
  57. package/dist/standalone/worker.js +36 -9
  58. package/dist/standalone/workflow-binding.d.ts +2 -2
  59. package/dist/standalone/workflow-host.d.ts +11 -11
  60. package/dist/standalone/workflows.d.ts +22 -22
  61. package/dist/storage-frame.d.ts +3 -3
  62. package/dist/workers-git.d.ts +2 -0
  63. package/dist/workers-git.js +11 -1
  64. package/dist/workers.d.ts +8 -8
  65. package/docs/api/agents.md +66 -0
  66. package/docs/api/apps.md +104 -104
  67. package/docs/api/build-contract.md +35 -35
  68. package/docs/api/build.md +12 -12
  69. package/docs/api/host.md +61 -61
  70. package/docs/api/mcp.md +141 -13
  71. package/docs/api/root-data.md +1 -1
  72. package/docs/api/tenancy.md +1 -1
  73. package/docs/api/workers-git.md +3 -2
  74. package/docs/api/workers.md +81 -83
  75. package/package.json +25 -17
@@ -1,7 +1,7 @@
1
1
  import * as Data from "effect/Data";
2
2
  /**
3
3
  * The cloud-browser backend's closed, typed failure channel. Every backend
4
- * (today only Cloudflare Browser Run) throws exclusively from this union;
4
+ * (Cloudflare Browser Run or on-box Chromium) throws from this union;
5
5
  * `agent-cloud-browser-source.ts` is the one place that maps it onto
6
6
  * Mycelium's `CapabilityFailed`, so the model only ever sees a bounded,
7
7
  * owner-admitted message (never a raw provider error body or stack).
@@ -9,7 +9,7 @@ import * as Data from "effect/Data";
9
9
  /** The target URL failed policy before any provider call was made. */
10
10
  export class CloudBrowserUrlDenied extends Data.TaggedError("CloudBrowserUrlDenied") {
11
11
  }
12
- /** The provider (Cloudflare Browser Run) answered with a failure. */
12
+ /** The configured provider answered with a failure or is unreachable. */
13
13
  export class CloudBrowserUpstreamError extends Data.TaggedError("CloudBrowserUpstreamError") {
14
14
  }
15
15
  /** A call did not complete within its deadline. */
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@fungi.computer/oyster/browser`: the one agent browser for the Hub and a
3
3
  * standalone Oyster. A host builds the port from a provider (`browserRunSessions`
4
- * or `bindingSessions`, plus Browser Run Quick Reads when it has them), its
4
+ * or `bindingSessions` or `onboxBrowserSessions` on staging, plus Browser Run
5
+ * Quick Reads when it has them), its
5
6
  * required metering owner and an event sink; `createBrowser` owns lease
6
7
  * ownership, egress policy and billing order; `browserCapabilitySource` is
7
8
  * the agent's `cloudBrowser` tool. Raw CDP stays internal to this directory.
@@ -13,4 +14,4 @@ export type { LeaseOwner } from "./lease-registry.js";
13
14
  export { cloudBrowserBounds, extractInputSchema, extractResultSchema, linksResultSchema, markdownInputSchema, markdownResultSchema, navigateResultSchema, openResultSchema, scrapeInputSchema, scrapeResultSchema, snapshotResultSchema, type BinaryResult, type CloudBrowserAck, type CloudBrowserClosed, type CloudBrowserOperation, type CloudBrowserOperationResult, type ExtractInput, type ExtractResult, type LinksResult, type MarkdownInput, type MarkdownResult, type NavigateResult, type OpenResult, type ScrapeInput, type ScrapeResult, type SnapshotResult, } from "./operations.js";
14
15
  export type { CdpSocket, HttpFetch } from "./cdp-client.js";
15
16
  export { browserRunQuickReads, type BrowserRunQuickReadsConfig, type QuickRead, type QuickReads, } from "./quick-reads.js";
16
- export { bindingSessions, browserRunSessions, type BrowserBinding, type BrowserSessions, type CdpConnect, } from "./sessions.js";
17
+ export { bindingSessions, browserRunSessions, onboxBrowserSessions, type BrowserBinding, type BrowserSessions, type CdpConnect, } from "./sessions.js";
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@fungi.computer/oyster/browser`: the one agent browser for the Hub and a
3
3
  * standalone Oyster. A host builds the port from a provider (`browserRunSessions`
4
- * or `bindingSessions`, plus Browser Run Quick Reads when it has them), its
4
+ * or `bindingSessions` or `onboxBrowserSessions` on staging, plus Browser Run
5
+ * Quick Reads when it has them), its
5
6
  * required metering owner and an event sink; `createBrowser` owns lease
6
7
  * ownership, egress policy and billing order; `browserCapabilitySource` is
7
8
  * the agent's `cloudBrowser` tool. Raw CDP stays internal to this directory.
@@ -11,4 +12,4 @@ export { browserCapabilitySource, CLOUD_BROWSER_DESCRIPTIONS, } from "./capabili
11
12
  export { CloudBrowserCapacityExceeded, CloudBrowserCreditDenied, CloudBrowserLeaseNotFound, CloudBrowserLeasePaused, CloudBrowserTimeout, CloudBrowserUnavailable, CloudBrowserUpstreamError, CloudBrowserUrlDenied, } from "./errors.js";
12
13
  export { cloudBrowserBounds, extractInputSchema, extractResultSchema, linksResultSchema, markdownInputSchema, markdownResultSchema, navigateResultSchema, openResultSchema, scrapeInputSchema, scrapeResultSchema, snapshotResultSchema, } from "./operations.js";
13
14
  export { browserRunQuickReads, } from "./quick-reads.js";
14
- export { bindingSessions, browserRunSessions, } from "./sessions.js";
15
+ export { bindingSessions, browserRunSessions, onboxBrowserSessions, } from "./sessions.js";
@@ -8,7 +8,11 @@ import { cloudBrowserBounds, } from "./operations.js";
8
8
  * The session-create response shape was confirmed by the live workerd smoke
9
9
  * on 2026-10-03; see `test/browser/cdp-client.test.ts`.
10
10
  */
11
- const targetSchema = z.object({ targetId: z.string(), type: z.string() });
11
+ const targetSchema = z.object({
12
+ targetId: z.string(),
13
+ type: z.string(),
14
+ browserContextId: z.string().optional(),
15
+ });
12
16
  const getTargetsResultSchema = z.object({ targetInfos: z.array(targetSchema) });
13
17
  const attachResultSchema = z.object({ sessionId: z.string() });
14
18
  const navigateResultSchema = z.object({
@@ -31,11 +35,14 @@ const capturedDataSchema = z.object({ data: z.string() });
31
35
  /** Attach to the session's first page target (or a new blank one). */
32
36
  export async function attachPage(client) {
33
37
  const targets = getTargetsResultSchema.parse(await client.call("Target.getTargets", {}));
34
- const targetId = targets.targetInfos.find((target) => target.type === "page")?.targetId ??
38
+ const targetId = targets.targetInfos.find((target) => target.type === "page" &&
39
+ (client.browserContextId === undefined ||
40
+ target.browserContextId === client.browserContextId))?.targetId ??
35
41
  z
36
42
  .object({ targetId: z.string() })
37
- .parse(await client.call("Target.createTarget", { url: "about:blank" }))
38
- .targetId;
43
+ .parse(await client.call("Target.createTarget", client.browserContextId === undefined
44
+ ? { url: "about:blank" }
45
+ : { url: "about:blank", browserContextId: client.browserContextId })).targetId;
39
46
  const { sessionId: cdpSessionId } = attachResultSchema.parse(await client.call("Target.attachToTarget", { targetId, flatten: true }));
40
47
  await client.call("Page.enable", {}, { sessionId: cdpSessionId });
41
48
  await client.call("Accessibility.enable", {}, { sessionId: cdpSessionId });
@@ -11,16 +11,17 @@ import { type CdpClient, type CdpSocket, type HttpFetch } from "./cdp-client.js"
11
11
  * - Binding: `binding.fetch("http://fake.host/v1/devtools/…")`, the endpoints
12
12
  * a Workers Browser Run binding answers.
13
13
  *
14
+ * Chromium on staging uses browser contexts rather than Browser Run endpoints.
14
15
  * Provider session ids stay inside the module; agent leases use their own ids.
15
16
  */
16
17
  export type AcquireOptions = Readonly<{
17
- /** Provider idle keep-alive, clamped to Browser Run's 10s–20min range. */
18
+ /** Browser Run idle keep-alive (10s–20min); Chromium has no equivalent. */
18
19
  keepAliveMs: number;
19
- /** Provider guardrail: the only hosts this session may load. */
20
+ /** Browser Run provider guardrail; Chromium relies on its caller's URL gate. */
20
21
  allowedDomains?: readonly string[];
21
22
  }>;
22
23
  /**
23
- * The provider half of the host port. Built only by the two functions below.
24
+ * The provider half of the host port.
24
25
  * Each step is separate so `createBrowser` owns a provider session id from the
25
26
  * moment it exists, including when its CDP connect then fails.
26
27
  */
@@ -31,7 +32,7 @@ export type BrowserSessions = Readonly<{
31
32
  connect: (sessionId: string, signal: AbortSignal) => Promise<CdpClient>;
32
33
  /** Ask the provider to delete a session; 404 is already gone. @internal */
33
34
  release: (sessionId: string, signal: AbortSignal) => Promise<"closed" | "closing">;
34
- /** Reads the provider's session record until it reports an end time. @internal */
35
+ /** Confirm provider closure (Chromium checks context absence). @internal */
35
36
  confirmClosed: (sessionId: string, deadlineAt: number) => Promise<void>;
36
37
  }>;
37
38
  /** A Workers BROWSER binding (`Fetcher`), reduced to the call this module makes. */
@@ -50,3 +51,5 @@ export declare function browserRunSessions(config: Readonly<{
50
51
  }>): BrowserSessions;
51
52
  /** A standalone Oyster's own Workers BROWSER binding. */
52
53
  export declare function bindingSessions(binding: BrowserBinding): BrowserSessions;
54
+ /** Staging uses Chromium's native CDP browser contexts; no session store. */
55
+ export declare function onboxBrowserSessions(origin: string, fetcher?: HttpFetch): BrowserSessions;
@@ -116,3 +116,162 @@ export function bindingSessions(binding) {
116
116
  connect: (url, headers, signal) => openCdpSocket(fetcher, url, headers, signal),
117
117
  });
118
118
  }
119
+ /** Staging uses Chromium's native CDP browser contexts; no session store. */
120
+ export function onboxBrowserSessions(origin, fetcher = (url, init) => fetch(url, init)) {
121
+ const base = new URL(origin);
122
+ if (base.protocol !== "http:" ||
123
+ base.hostname !== "127.0.0.1" ||
124
+ base.username ||
125
+ base.password ||
126
+ base.pathname !== "/" ||
127
+ base.search ||
128
+ base.hash)
129
+ throw new TypeError("On-box browser origin must be bare IPv4 loopback");
130
+ const browserEndpointSchema = z.object({ webSocketDebuggerUrl: z.url() });
131
+ const contextsSchema = z.object({ browserContextIds: z.array(z.string()) });
132
+ const createdContextSchema = z.object({
133
+ browserContextId: z.string().min(1),
134
+ });
135
+ async function connectBrowser(signal) {
136
+ // Chromium's native discovery endpoint is not a Botanical session server.
137
+ let discovery;
138
+ try {
139
+ discovery = await fetcher(new URL("/json/version", base).href, {
140
+ signal,
141
+ });
142
+ }
143
+ catch (cause) {
144
+ if (signal.aborted)
145
+ throw cause;
146
+ throw new CloudBrowserUpstreamError({
147
+ op: "onbox.connect",
148
+ status: null,
149
+ message: "On-box Chromium is unavailable",
150
+ });
151
+ }
152
+ if (!discovery.ok)
153
+ throw new CloudBrowserUpstreamError({
154
+ op: "onbox.connect",
155
+ status: discovery.status,
156
+ message: "On-box Chromium is unavailable",
157
+ });
158
+ let body;
159
+ try {
160
+ body = await discovery.json();
161
+ }
162
+ catch (cause) {
163
+ if (signal.aborted)
164
+ throw cause;
165
+ throw new CloudBrowserUpstreamError({
166
+ op: "onbox.connect",
167
+ status: null,
168
+ message: "On-box browser returned an unreadable CDP endpoint",
169
+ });
170
+ }
171
+ const discovered = browserEndpointSchema.safeParse(body);
172
+ if (!discovered.success)
173
+ throw new CloudBrowserUpstreamError({
174
+ op: "onbox.connect",
175
+ status: null,
176
+ message: "On-box browser returned an unreadable CDP endpoint",
177
+ });
178
+ const { webSocketDebuggerUrl } = discovered.data;
179
+ const endpoint = new URL(webSocketDebuggerUrl);
180
+ if (endpoint.protocol !== "ws:" ||
181
+ endpoint.hostname !== "127.0.0.1" ||
182
+ endpoint.port !== base.port ||
183
+ !/^\/devtools\/browser\/[^/]+$/u.test(endpoint.pathname) ||
184
+ endpoint.username ||
185
+ endpoint.password ||
186
+ endpoint.search ||
187
+ endpoint.hash)
188
+ throw new CloudBrowserUpstreamError({
189
+ op: "onbox.connect",
190
+ status: null,
191
+ message: "On-box browser returned an invalid CDP endpoint",
192
+ });
193
+ return createCdpClient(await openCdpSocket(fetcher, endpoint.href.replace(/^ws:/u, "http:"), {}, signal));
194
+ }
195
+ async function contexts(client, signal) {
196
+ const result = contextsSchema.safeParse(await client.call("Target.getBrowserContexts", {}, { signal }));
197
+ if (!result.success)
198
+ throw new CloudBrowserUpstreamError({
199
+ op: "onbox.contexts",
200
+ status: null,
201
+ message: "On-box browser returned unreadable contexts",
202
+ });
203
+ return result.data.browserContextIds;
204
+ }
205
+ return {
206
+ create: async (_options, signal) => {
207
+ // Chromium has no idle keep-alive or allowedDomains create guardrail:
208
+ // Oyster's Agent URL policy and the boot page request gate remain active.
209
+ // A Chromium restart ends every context; no restart recovery is claimed.
210
+ const client = await connectBrowser(signal);
211
+ try {
212
+ const created = createdContextSchema.safeParse(await client.call("Target.createBrowserContext", {}, { signal }));
213
+ if (!created.success)
214
+ throw new CloudBrowserUpstreamError({
215
+ op: "onbox.create",
216
+ status: null,
217
+ message: "On-box browser returned an unreadable context",
218
+ });
219
+ const { browserContextId } = created.data;
220
+ try {
221
+ await client.call("Target.createTarget", { url: "about:blank", browserContextId }, { signal });
222
+ return browserContextId;
223
+ }
224
+ catch (error) {
225
+ await client.call("Target.disposeBrowserContext", { browserContextId }, { signal });
226
+ throw error;
227
+ }
228
+ }
229
+ finally {
230
+ client.close();
231
+ }
232
+ },
233
+ connect: async (id, signal) => {
234
+ const client = await connectBrowser(signal);
235
+ try {
236
+ if (!(await contexts(client, signal)).includes(id))
237
+ throw new CloudBrowserUpstreamError({
238
+ op: "onbox.connect",
239
+ status: null,
240
+ message: "On-box browser context is gone",
241
+ });
242
+ return { ...client, browserContextId: id };
243
+ }
244
+ catch (error) {
245
+ client.close();
246
+ throw error;
247
+ }
248
+ },
249
+ release: async (id, signal) => {
250
+ const client = await connectBrowser(signal);
251
+ try {
252
+ if ((await contexts(client, signal)).includes(id))
253
+ await client.call("Target.disposeBrowserContext", { browserContextId: id }, { signal });
254
+ return "closed";
255
+ }
256
+ finally {
257
+ client.close();
258
+ }
259
+ },
260
+ confirmClosed: async (id, deadlineAt) => {
261
+ const signal = AbortSignal.timeout(Math.max(1, deadlineAt - Date.now()));
262
+ const client = await connectBrowser(signal);
263
+ try {
264
+ if (!(await contexts(client, signal)).includes(id))
265
+ return;
266
+ }
267
+ finally {
268
+ client.close();
269
+ }
270
+ throw new CloudBrowserUpstreamError({
271
+ op: "onbox.close.confirm",
272
+ status: null,
273
+ message: "On-box browser did not confirm closure",
274
+ });
275
+ },
276
+ };
277
+ }
@@ -12,7 +12,9 @@ export type UrlCheckOptions = Readonly<{
12
12
  * `169.254.169.254`, a private range like `10/8`, the box's own address, and
13
13
  * a rebinding hostname (one whose DNS answer is private) are all refused
14
14
  * here, before Cloudflare ever sees the URL. The name is resolved once; the
15
- * caller never connects to it directly — Cloudflare does, so this is a policy
16
- * gate, not a proxy.
15
+ * caller does not connect directly: Browser Run does in production, Chromium
16
+ * does on staging. This check is not network confinement: a page can redirect,
17
+ * load subresources or resolve the host differently after this check. Staging
18
+ * must isolate Chromium's network egress before enabling Agent leases.
17
19
  */
18
20
  export declare function assertCloudBrowserUrlAllowed(raw: string, options: UrlCheckOptions): Promise<URL>;
@@ -10,8 +10,10 @@ import { CloudBrowserUrlDenied } from "./errors.js";
10
10
  * `169.254.169.254`, a private range like `10/8`, the box's own address, and
11
11
  * a rebinding hostname (one whose DNS answer is private) are all refused
12
12
  * here, before Cloudflare ever sees the URL. The name is resolved once; the
13
- * caller never connects to it directly — Cloudflare does, so this is a policy
14
- * gate, not a proxy.
13
+ * caller does not connect directly: Browser Run does in production, Chromium
14
+ * does on staging. This check is not network confinement: a page can redirect,
15
+ * load subresources or resolve the host differently after this check. Staging
16
+ * must isolate Chromium's network egress before enabling Agent leases.
15
17
  */
16
18
  export async function assertCloudBrowserUrlAllowed(raw, options) {
17
19
  let url;
@@ -0,0 +1,26 @@
1
+ import type { z } from "zod";
2
+ import type { computerCreateRequestSchema, computerPageSchema } from "@fungi.computer/computer/fleet/inventory-wire";
3
+ import type { computerStateSchema } from "@fungi.computer/computer/fleet/wire";
4
+ import type { teamComputerDirectorySchema } from "@fungi.computer/nacre/workbench";
5
+ import type { InvocationContext } from "@fungi.computer/mycelium";
6
+ /** Host policy stays outside this family: Hub admits funded slots, standalone
7
+ * admits its current owner. Both call their durable fleet through this port.
8
+ * Keep the host contract independent of the Worker-only fleet lifecycle. */
9
+ export type ComputerControlPort = Readonly<{
10
+ /** List a bounded page from the host-admitted inventory. */
11
+ list(page: z.output<typeof computerPageSchema>, context: InvocationContext): Promise<z.output<typeof teamComputerDirectorySchema>>;
12
+ /** Read liveness without waking the Computer. */
13
+ state(computerId: string, context: InvocationContext): Promise<z.output<typeof computerStateSchema>>;
14
+ /** Start one inventoried hosted Computer through the host's admission. */
15
+ start(computerId: string, context: InvocationContext): Promise<z.output<typeof computerStateSchema>>;
16
+ /** Create or replay a durable receipt tied to this request identity. */
17
+ create(input: z.output<typeof computerCreateRequestSchema>, context: InvocationContext): Promise<{
18
+ kind: "created";
19
+ computer: {
20
+ computerId: string;
21
+ name: string;
22
+ };
23
+ } | {
24
+ kind: "uncertain" | "denied" | "conflict" | "invalid";
25
+ }>;
26
+ }>;
@@ -0,0 +1 @@
1
+ export {};
package/dist/host.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { OysterApprovalCall } from "./approval-home.js";
2
2
  import type { PendingApproval, ApprovalApplyResult } from "@fungi.computer/approvals";
3
3
  import type { WorkflowControl } from "./workflows.js";
4
+ import type { ComputerControlPort } from "./computer-control-port.js";
4
5
  import { type InstallationOperations } from "./installation-operations.js";
5
6
  export type { InstallationOperations };
6
7
  import type { KnapsackComputer } from "@fungi.computer/knapsack";
@@ -192,6 +193,8 @@ export type OysterHost = Readonly<{
192
193
  * @inlineType WorkflowControl
193
194
  */
194
195
  workflows?: (principal: OysterPrincipal) => Promise<WorkflowControl | null>;
196
+ /** Owner-admitted durable Computer inventory, absent on hosts without a fleet. */
197
+ computerControl?: (principal: OysterPrincipal) => ComputerControlPort | null;
195
198
  auth: OysterAuth;
196
199
  /** Host-held sealing key for member writes; never exposed to the guest.
197
200
  * Absence refuses eligible named member writes, even when the App has no sync continuation.
@@ -13,6 +13,6 @@ export declare function createHomeAwareAgentMailNamespace(options: {
13
13
  home: (agentId: string) => Promise<MachineAgentHome | null>;
14
14
  hosted: PostRoute<never, never>["namespace"];
15
15
  machine: (deviceId: string, envelope: PostEnvelope) => Promise<PostDeliveryResult>;
16
- /** Host's Agent grammar: throws unless the receipt is a well-formed Accepted. */
16
+ /** Host's Agent grammar: validates Accepted or Held without conflating them. */
17
17
  accepted: (receipt: Schema.Json) => void;
18
18
  }): PostRoute<never, never>["namespace"];
@@ -28,8 +28,7 @@ export function createHomeAwareAgentMailNamespace(options) {
28
28
  address.scopeId !== options.sender.teamId ||
29
29
  address.agentId !== agentId ||
30
30
  mail.to.agentId !== agentId ||
31
- mail.from.agentId !== options.sender.agentId ||
32
- new TextEncoder().encode(JSON.stringify(mail)).byteLength > 256 * 1024)
31
+ mail.from.agentId !== options.sender.agentId)
33
32
  return "refused";
34
33
  const projected = await options.home(agentId);
35
34
  if (projected === null)
@@ -45,7 +44,7 @@ export function createHomeAwareAgentMailNamespace(options) {
45
44
  if (result === "accepted" ||
46
45
  result === "duplicate" ||
47
46
  !("receipt" in result))
48
- throw new Error("Agent mail acknowledgment lacks an Accepted receipt");
47
+ throw new Error("Agent mail acknowledgment lacks a valid acceptance or Held receipt");
49
48
  if (result.status !== "refused")
50
49
  options.accepted(result.receipt);
51
50
  return result;
@@ -1,5 +1,6 @@
1
- import type { PostEnvelope, PostDeliveryResult } from "@fungi.computer/carrier/post";
2
- import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
1
+ import { type SeamReplayStore } from "@fungi.computer/seam";
2
+ import { type PostEnvelope, type PostDeliveryResult } from "@fungi.computer/carrier/post";
3
+ import { type WatchdogSqliteOwner } from "@fungi.computer/watchdog";
3
4
  export type MachineLinkDevice = Readonly<{
4
5
  deviceId: string;
5
6
  memberId: string;
@@ -8,16 +9,21 @@ export type MachineLinkDevice = Readonly<{
8
9
  }>;
9
10
  /**
10
11
  * Native device socket I/O. Pins/member authority and mail admission are supplied
11
- * by their existing owners. Native attachments survive hibernation; pending
12
- * Post I/O does not claim durable custody and is retried by Carrier.
12
+ * by their existing owners. Native attachments survive hibernation; Carrier
13
+ * owns durable custody and per-Agent ordering across socket lifetimes.
13
14
  */
14
15
  export declare class MachineLinkHost {
15
16
  #private;
16
17
  private readonly context;
18
+ private readonly owner;
17
19
  private readonly options;
18
20
  private readonly channels;
19
- private readonly replay;
20
- private readonly routes;
21
+ private replayRuntime;
22
+ private readonly router;
23
+ private readonly custodyRoutes;
24
+ private deviceId;
25
+ private custodyRuntime;
26
+ private readonly receipts;
21
27
  constructor(context: Pick<DurableObjectState, "getWebSockets" | "acceptWebSocket" | "setWebSocketAutoResponse" | "waitUntil">, owner: WatchdogSqliteOwner, options: Readonly<{
22
28
  teamId: string;
23
29
  directory: () => Promise<readonly {
@@ -25,14 +31,39 @@ export declare class MachineLinkHost {
25
31
  name: string;
26
32
  }[]>;
27
33
  authorize: (deviceId: string) => Promise<MachineLinkDevice>;
34
+ replay: () => Promise<SeamReplayStore>;
28
35
  receive: (device: MachineLinkDevice, envelope: PostEnvelope) => Promise<PostDeliveryResult>;
36
+ /** Arm the Durable Object alarm for Carrier/Watchdog progress. */
37
+ scheduleWake: (at: number) => Promise<void>;
29
38
  /** The host's failure log for refused dials and closed links. */
30
39
  report: (operation: string, cause: unknown) => void;
31
40
  }>);
32
41
  fetch(request: Request): Promise<Response>;
42
+ private replayStore;
43
+ /** Start the fresh socket and then hand durable rows to its live channel. */
44
+ connected(socket: WebSocket, deviceId: string): Promise<void>;
33
45
  private publishDirectory;
34
46
  private channel;
35
47
  deliver(deviceId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
48
+ private deliverLiveOperation;
49
+ private deliverCustodyMail;
50
+ /** Run due Carrier work; expired rows remain visible as Carrier dead letters. */
51
+ alarm(): Promise<void>;
52
+ /** Expose terminal custody rows so S9 can stage its own disposition reply. */
53
+ deadLetters(afterKey?: string): Promise<Readonly<{
54
+ letters: readonly Readonly<{
55
+ key: string;
56
+ reason: "refused" | "exhausted" | "expired";
57
+ envelope: PostEnvelope;
58
+ attempts: number;
59
+ }>[];
60
+ nextCursor: string | undefined;
61
+ }>>;
62
+ private deliverToSocket;
63
+ private custody;
64
+ private createCustody;
65
+ private releaseCustody;
66
+ private drainCustody;
36
67
  message(socket: WebSocket, message: string | ArrayBuffer): Promise<void>;
37
68
  close(socket: WebSocket): void;
38
69
  private authorizeSocket;