@fungi.computer/oyster 0.4.1 → 0.6.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 (86) hide show
  1. package/README.md +46 -8
  2. package/dist/agents.d.ts +2 -1
  3. package/dist/agents.js +67 -8
  4. package/dist/apps/gate.d.ts +4 -0
  5. package/dist/apps/gate.js +3 -0
  6. package/dist/auth.d.ts +24 -1
  7. package/dist/auth.js +38 -0
  8. package/dist/boot-host-script.js +1 -1
  9. package/dist/build/candidate-cli.mjs +27 -5
  10. package/dist/channels/binding.d.ts +70 -0
  11. package/dist/channels/binding.js +97 -0
  12. package/dist/channels/conversations.d.ts +246 -0
  13. package/dist/channels/conversations.js +705 -0
  14. package/dist/channels/ids.d.ts +8 -0
  15. package/dist/channels/ids.js +15 -0
  16. package/dist/channels/visitor-channel.d.ts +53 -0
  17. package/dist/channels/visitor-channel.js +124 -0
  18. package/dist/channels/visitor-edge.d.ts +115 -0
  19. package/dist/channels/visitor-edge.js +582 -0
  20. package/dist/channels/visitor-request.d.ts +7 -0
  21. package/dist/channels/visitor-request.js +17 -0
  22. package/dist/channels/visitor-stream-slots.d.ts +2 -0
  23. package/dist/channels/visitor-stream-slots.js +33 -0
  24. package/dist/channels/visitors.d.ts +47 -0
  25. package/dist/channels/visitors.js +181 -0
  26. package/dist/cloudflare.d.ts +8 -2
  27. package/dist/cloudflare.js +27 -1
  28. package/dist/computer-target.d.ts +5 -0
  29. package/dist/computer-target.js +11 -0
  30. package/dist/host.d.ts +7 -4
  31. package/dist/host.js +96 -17
  32. package/dist/internal/host-gate.d.ts +6 -4
  33. package/dist/internal/host-gate.js +14 -7
  34. package/dist/invocation.d.ts +11 -4
  35. package/dist/invocation.js +134 -91
  36. package/dist/keys.d.ts +4 -1
  37. package/dist/keys.js +15 -4
  38. package/dist/machine-link/authority.d.ts +7 -0
  39. package/dist/machine-link/delivery.d.ts +12 -0
  40. package/dist/machine-link/delivery.js +22 -1
  41. package/dist/machine-link/disposition.d.ts +16 -0
  42. package/dist/machine-link/disposition.js +65 -0
  43. package/dist/machine-link/host.d.ts +11 -2
  44. package/dist/machine-link/host.js +125 -5
  45. package/dist/machine-link/link.d.ts +4 -0
  46. package/dist/machine-link/link.js +47 -1
  47. package/dist/page/capabilities.js +1 -1
  48. package/dist/page/child-selection.d.ts +27 -0
  49. package/dist/page/child-selection.js +38 -0
  50. package/dist/page/mount.js +1 -1
  51. package/dist/page-script.js +4 -4
  52. package/dist/release.d.ts +13 -0
  53. package/dist/release.js +10 -0
  54. package/dist/request-origin.d.ts +4 -0
  55. package/dist/request-origin.js +10 -0
  56. package/dist/standalone/agent-mail.js +4 -1
  57. package/dist/standalone/agents.d.ts +8 -0
  58. package/dist/standalone/agents.js +30 -11
  59. package/dist/standalone/channel-session-door.d.ts +4 -0
  60. package/dist/standalone/channel-session-door.js +72 -0
  61. package/dist/standalone/computer-fleet.js +7 -1
  62. package/dist/standalone/machine-link.d.ts +2 -0
  63. package/dist/standalone/machine-link.js +34 -1
  64. package/dist/standalone/tenancy.d.ts +3 -0
  65. package/dist/standalone/tenancy.js +7 -2
  66. package/dist/testing/capabilities.d.ts +2 -0
  67. package/dist/testing/capabilities.js +3 -1
  68. package/dist/turnstile.d.ts +27 -0
  69. package/dist/turnstile.js +51 -0
  70. package/dist/workers-git.js +21 -2
  71. package/dist/workers.d.ts +3 -0
  72. package/dist/workers.js +15 -12
  73. package/docs/api/agents.md +8 -4
  74. package/docs/api/apps.md +8 -4
  75. package/docs/api/auth.md +55 -0
  76. package/docs/api/browser.md +40 -16
  77. package/docs/api/build-contract.md +2 -2
  78. package/docs/api/cloudflare.md +26 -1
  79. package/docs/api/host.md +22 -13
  80. package/docs/api/invocation.md +7 -3
  81. package/docs/api/keys.md +25 -3
  82. package/docs/api/workers.md +38 -30
  83. package/docs/site/agent-quickstart.mdx +29 -2
  84. package/examples/standalone/wrangler.celld.jsonc +66 -0
  85. package/examples/standalone/wrangler.jsonc +3 -4
  86. package/package.json +53 -17
@@ -5,10 +5,12 @@ import { correlationKey } from "@fungi.computer/cairn";
5
5
  import { createMachineLinkPostChannel, MachineLinkPostUnavailable, machineLinkAudience, machineLinkProtocolVersion, } from "@fungi.computer/seam/machine-link-protocol";
6
6
  import { importSeamVerifiers, seamHeader, verifySeamRequest, } from "@fungi.computer/seam";
7
7
  import { createPost, definePostRoute, PostDeliveryResultSchema, } from "@fungi.computer/carrier/post";
8
+ import { undeliverableReply } from "@fungi.computer/bird-dog";
8
9
  import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
9
10
  import { machineAgentOperationSchema } from "@fungi.computer/seam/machine-link-protocol";
10
11
  import { Watchdog, } from "@fungi.computer/watchdog";
11
12
  import { createMachineLinkSockets } from "./presence.js";
13
+ import { machineLinkDisposition } from "./disposition.js";
12
14
  const attachmentSchema = z.strictObject({
13
15
  deviceId: z.string().min(1).max(256),
14
16
  fenceEpoch: z.int().nonnegative(),
@@ -48,6 +50,15 @@ const machineMailEnvelope = Schema.Struct({
48
50
  address: custodyAddress,
49
51
  event: BirdDogMailSchema,
50
52
  });
53
+ const machineDispositionEnvelope = Schema.Struct({
54
+ key: Schema.String,
55
+ kind: Schema.Literal("machineLinkDisposition"),
56
+ address: custodyAddress,
57
+ event: Schema.Struct({
58
+ original: machineMailEnvelope,
59
+ reply: BirdDogMailSchema,
60
+ }),
61
+ });
51
62
  const machineAgentEnvelope = Schema.Struct({
52
63
  key: Schema.String,
53
64
  kind: Schema.Literal("machineAgent"),
@@ -87,6 +98,25 @@ export class MachineLinkHost {
87
98
  }),
88
99
  },
89
100
  }),
101
+ machineLinkDisposition: definePostRoute({
102
+ addressKind: "machineLinkAgent",
103
+ address: custodyAddress,
104
+ event: machineDispositionEnvelope.fields.event,
105
+ name: ({ agentId }) => agentId,
106
+ namespace: {
107
+ getByName: () => ({
108
+ deliver: async (envelope) => {
109
+ const typed = Schema.decodeUnknownSync(machineDispositionEnvelope, { onExcessProperty: "error" })(envelope);
110
+ return await this.options.disposition(typed.event.original, {
111
+ key: typed.key,
112
+ kind: "agentMail",
113
+ address: typed.address,
114
+ event: typed.event.reply,
115
+ });
116
+ },
117
+ }),
118
+ },
119
+ }),
90
120
  };
91
121
  context.setWebSocketAutoResponse(new WebSocketRequestResponsePair("ping", "pong"));
92
122
  this.router = new Hono().get("/api/teams/:teamId/local/mesh/:deviceId/connect", async (context) => {
@@ -141,6 +171,8 @@ export class MachineLinkHost {
141
171
  await this.publishDirectory(socket, deviceId);
142
172
  await this.releaseCustody();
143
173
  await this.drainCustody();
174
+ await this.stageUndeliverableReplies();
175
+ await this.drainCustody();
144
176
  }
145
177
  async publishDirectory(socket, deviceId) {
146
178
  this.deviceId = deviceId;
@@ -195,7 +227,7 @@ export class MachineLinkHost {
195
227
  }
196
228
  return channel;
197
229
  }
198
- async deliver(deviceId, envelope) {
230
+ async deliver(deviceId, envelope, options = {}) {
199
231
  this.deviceId = deviceId;
200
232
  await this.options.authorize(deviceId);
201
233
  const links = createMachineLinkSockets(this.context, deviceId);
@@ -205,7 +237,7 @@ export class MachineLinkHost {
205
237
  const typedEnvelope = Schema.decodeUnknownSync(Schema.Union([machineMailEnvelope, machineAgentEnvelope]))(envelope);
206
238
  if (typedEnvelope.kind === "machineAgent")
207
239
  return await this.deliverLiveOperation(deviceId, socket, typedEnvelope);
208
- return await this.deliverCustodyMail(deviceId, socket, typedEnvelope);
240
+ return await this.deliverCustodyMail(deviceId, socket, typedEnvelope, options.refuseDuplicate === true);
209
241
  }
210
242
  async deliverLiveOperation(deviceId, socket, envelope) {
211
243
  if (socket === undefined)
@@ -213,18 +245,31 @@ export class MachineLinkHost {
213
245
  await this.publishDirectory(socket, deviceId);
214
246
  return await this.channel(socket, deviceId).deliver(envelope);
215
247
  }
216
- async deliverCustodyMail(deviceId, socket, envelope) {
248
+ async deliverCustodyMail(deviceId, socket, envelope, refuseDuplicate = false) {
217
249
  const { post } = await this.custody();
218
250
  const existing = post.read(envelope.key);
219
251
  if (existing !== undefined) {
252
+ if (refuseDuplicate)
253
+ return "refused";
220
254
  this.owner.transactionSync(() => {
221
255
  post.send(envelope);
222
256
  });
223
257
  return previousCustodyReceipt(existing);
224
258
  }
225
- this.owner.transactionSync(() => {
259
+ const duplicate = this.owner.transactionSync(() => {
260
+ if (post.read(envelope.key) !== undefined)
261
+ return true;
226
262
  post.send(envelope);
263
+ return false;
227
264
  });
265
+ if (duplicate) {
266
+ if (refuseDuplicate)
267
+ return "refused";
268
+ const raced = post.read(envelope.key);
269
+ return raced === undefined
270
+ ? recipientOffline
271
+ : previousCustodyReceipt(raced);
272
+ }
228
273
  await this.options.scheduleWake(post.nextWake() ?? Date.now());
229
274
  if (socket === undefined)
230
275
  return recipientOffline;
@@ -237,12 +282,87 @@ export class MachineLinkHost {
237
282
  /** Run due Carrier work; expired rows remain visible as Carrier dead letters. */
238
283
  async alarm() {
239
284
  await this.drainCustody();
285
+ await this.stageUndeliverableReplies();
286
+ await this.drainCustody();
287
+ }
288
+ /** Turn each terminal incoming-mail row into a durable reply outbox row. */
289
+ async stageUndeliverableReplies() {
290
+ const { post } = await this.custody();
291
+ let cursor;
292
+ do {
293
+ const page = post.deadLetters(64, cursor);
294
+ for (const letter of page.letters)
295
+ this.stageUndeliverableReply(post, letter);
296
+ cursor = page.nextCursor;
297
+ } while (cursor !== undefined);
298
+ }
299
+ stageUndeliverableReply(post, letter) {
300
+ if ((letter.reason !== "exhausted" && letter.reason !== "expired") ||
301
+ letter.envelope.kind !== "agentMail")
302
+ return;
303
+ const original = Schema.decodeUnknownSync(machineMailEnvelope, {
304
+ onExcessProperty: "error",
305
+ })(letter.envelope);
306
+ if (original.event.reply !== undefined ||
307
+ original.event.from.kind !== "agent")
308
+ return;
309
+ const replyKey = `${original.key}:reply:0`;
310
+ if (post.read(replyKey) !== undefined)
311
+ return;
312
+ const reply = {
313
+ key: replyKey,
314
+ kind: "agentMail",
315
+ address: {
316
+ scopeId: original.address.scopeId,
317
+ agentId: original.event.from.agentId,
318
+ },
319
+ event: undeliverableReply(original.event, Date.now()),
320
+ };
321
+ const disposition = {
322
+ key: replyKey,
323
+ kind: "machineLinkDisposition",
324
+ address: reply.address,
325
+ event: { original, reply: reply.event },
326
+ };
327
+ this.owner.transactionSync(() => {
328
+ if (post.read(replyKey) === undefined)
329
+ post.send(disposition);
330
+ });
240
331
  }
241
- /** Expose terminal custody rows so S9 can stage its own disposition reply. */
332
+ /** Expose terminal custody rows for the device-owned diagnostic surface. */
242
333
  async deadLetters(afterKey) {
243
334
  const { post } = await this.custody();
244
335
  return post.deadLetters(64, afterKey);
245
336
  }
337
+ /** Prove a disposition is still present in this link's durable Post outbox. */
338
+ async hasStagedDisposition(deviceId, original, reply) {
339
+ this.deviceId = deviceId;
340
+ await this.options.authorize(deviceId);
341
+ const identity = { teamId: this.options.teamId, deviceId };
342
+ const requested = machineLinkDisposition(identity, original, reply);
343
+ if (requested === undefined)
344
+ return false;
345
+ const { post } = await this.custody();
346
+ const row = post.read(requested.reply.key);
347
+ if (row?.state !== "pending")
348
+ return false;
349
+ try {
350
+ const staged = Schema.decodeUnknownSync(machineDispositionEnvelope, {
351
+ onExcessProperty: "error",
352
+ })(row.envelope);
353
+ const stagedDisposition = machineLinkDisposition(identity, staged.event.original, {
354
+ key: staged.key,
355
+ kind: "agentMail",
356
+ address: staged.address,
357
+ event: staged.event.reply,
358
+ });
359
+ return (stagedDisposition !== undefined &&
360
+ JSON.stringify(stagedDisposition) === JSON.stringify(requested));
361
+ }
362
+ catch {
363
+ return false;
364
+ }
365
+ }
246
366
  async deliverToSocket(envelope) {
247
367
  const deviceId = this.deviceId;
248
368
  if (deviceId === undefined)
@@ -29,6 +29,10 @@ export declare function createMachineLink<Readback>(state: MachineLinkState, opt
29
29
  fetch: (request: Request) => Promise<Response>;
30
30
  /** Mail addressed to this device's Agent, rechecked against its home. */
31
31
  deliver(input: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
32
+ /** A relay's typed terminal reply to an Agent homed on this device. */
33
+ deliverRelayDisposition(input: MachineLinkIdentity, relayInput: MachineLinkIdentity, original: PostEnvelope, reply: PostEnvelope): Promise<PostDeliveryResult>;
34
+ /** Attest a still-pending disposition in this device's own Post outbox. */
35
+ hasStagedDisposition(input: MachineLinkIdentity, original: PostEnvelope, reply: PostEnvelope): Promise<boolean>;
32
36
  /** One finite Agent operation on this device, returning its receipt. */
33
37
  operation(input: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, raw: MachineAgentOperation): Promise<Schema.Json>;
34
38
  /**
@@ -4,8 +4,16 @@ import * as Schema from "effect/Schema";
4
4
  import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
5
5
  import { agentMailAddressSchema, machineAgentOperationSchema, MachineLinkPostUnavailable, } from "@fungi.computer/seam/machine-link-protocol";
6
6
  import { machineLinkFenceFactSchema, machineLinkIdentitySchema, machineLinkName, MachineLinkRefused, } from "./authority.js";
7
+ import { machineLinkDisposition } from "./disposition.js";
7
8
  import { MachineLinkHost } from "./host.js";
8
9
  const pinKey = "machine-link:identity";
10
+ function isRetainedRelayFence(target, relay, fence) {
11
+ return (relay.teamId === target.teamId &&
12
+ relay.deviceId !== target.deviceId &&
13
+ fence !== null &&
14
+ fence.teamId === relay.teamId &&
15
+ fence.deviceId === relay.deviceId);
16
+ }
9
17
  /**
10
18
  * One device's link inside its own Durable Object. The host's DO class
11
19
  * delegates every entry point here. Identity is pinned on the first live
@@ -60,6 +68,12 @@ export function createMachineLink(state, options) {
60
68
  })(envelope.event);
61
69
  return { address, mail: body };
62
70
  };
71
+ const deliverUndeliverable = async (identity, rawOriginal, rawReply) => {
72
+ const checked = machineLinkDisposition(identity, rawOriginal, rawReply);
73
+ if (checked === undefined)
74
+ return "refused";
75
+ return await authority.deliverUndeliverable(identity, checked.original, checked.reply);
76
+ };
63
77
  /**
64
78
  * The link's own sender check, whatever the host answered: the facts must
65
79
  * name the caller and actor it saw, a hosted caller must be hosted, and a
@@ -94,6 +108,8 @@ export function createMachineLink(state, options) {
94
108
  if (envelope.kind !== "agentMail")
95
109
  return "refused";
96
110
  const { address, mail: body } = mail(envelope);
111
+ if (body.from.kind !== "agent")
112
+ return "refused";
97
113
  const caller = {
98
114
  kind: "device",
99
115
  deviceId: identity.deviceId,
@@ -118,6 +134,7 @@ export function createMachineLink(state, options) {
118
134
  replay: openReplayStore,
119
135
  directory: () => authority.directory(identity),
120
136
  receive: (_device, envelope) => receive(identity, envelope),
137
+ disposition: (original, reply) => deliverUndeliverable(identity, original, reply),
121
138
  scheduleWake: (at) => state.storage.setAlarm(at),
122
139
  report: options.report,
123
140
  }));
@@ -149,16 +166,45 @@ export function createMachineLink(state, options) {
149
166
  return "refused";
150
167
  const { address, mail: body } = mail(envelope);
151
168
  const facts = await checkedTarget(device, caller, agentId, null);
169
+ const senderMatches = body.from.kind === "agent" && body.from.agentId === facts.callerAgentId;
170
+ const systemOfflineDisposition = body.from.kind === "system" &&
171
+ body.reply?.outcome === "undeliverable" &&
172
+ body.reply.reason === "recipient_offline" &&
173
+ body.to.agentId === facts.targetAgentId;
152
174
  if (facts.targetHome.kind !== "machine" ||
153
175
  facts.targetHome.deviceId !== device.deviceId ||
154
176
  envelope.key !== body.key ||
155
177
  address.scopeId !== facts.teamId ||
156
178
  address.agentId !== facts.targetAgentId ||
157
179
  body.to.agentId !== facts.targetAgentId ||
158
- body.from.agentId !== facts.callerAgentId)
180
+ (!senderMatches && !systemOfflineDisposition))
159
181
  return "refused";
160
182
  return await hostFor(device).deliver(device.deviceId, envelope);
161
183
  },
184
+ /** A relay's typed terminal reply to an Agent homed on this device. */
185
+ async deliverRelayDisposition(input, relayInput, original, reply) {
186
+ const device = await current(input);
187
+ const relay = machineLinkIdentitySchema.parse(relayInput);
188
+ const retainedRelay = await authority.currentFence(relay);
189
+ if (!isRetainedRelayFence(device, relay, retainedRelay))
190
+ return "refused";
191
+ const checked = machineLinkDisposition(device, original, reply);
192
+ if (checked === undefined)
193
+ return "refused";
194
+ if (!(await authority.verifyRelayDisposition(relay, checked.original, checked.reply)))
195
+ return "refused";
196
+ const home = await authority.agentHome(device, checked.reply.address.agentId);
197
+ if (home?.kind !== "machine" || home.deviceId !== device.deviceId)
198
+ return "refused";
199
+ return await hostFor(device).deliver(device.deviceId, checked.reply, {
200
+ refuseDuplicate: true,
201
+ });
202
+ },
203
+ /** Attest a still-pending disposition in this device's own Post outbox. */
204
+ async hasStagedDisposition(input, original, reply) {
205
+ const device = await current(input);
206
+ return await hostFor(device).hasStagedDisposition(device.deviceId, original, reply);
207
+ },
162
208
  /** One finite Agent operation on this device, returning its receipt. */
163
209
  async operation(input, caller, agentId, raw) {
164
210
  const device = await current(input);
@@ -3,7 +3,7 @@ import { compile, parse, pathToRegexp } from "path-to-regexp";
3
3
  import * as Schema from "effect/Schema";
4
4
  import { AppError } from "@fungi.computer/agar";
5
5
  import { parseJsonValue } from "@fungi.computer/whistle/wire";
6
- import { computerTargetUrl } from "./computer-target.js";
6
+ import { computerTargetUrl } from "../computer-target.js";
7
7
  const queryInput = Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.Number, Schema.Boolean]));
8
8
  const capabilityInputSchema = Schema.Union([Schema.Json, Schema.Undefined]);
9
9
  function resolveRoute(path, input) {
@@ -0,0 +1,27 @@
1
+ /** The launch fields a window names; none of them is authority. */
2
+ export type ChildLaunch = Readonly<{
3
+ appId: string;
4
+ releaseId: string;
5
+ surfaceId: string;
6
+ href: string;
7
+ }>;
8
+ /** The identity the App's own configuration route answers with. */
9
+ export type ChildIdentity = Readonly<{
10
+ installationId: string;
11
+ appId: string;
12
+ releaseId: string;
13
+ computerId: string | null;
14
+ }>;
15
+ /**
16
+ * The installation page a launch names, or null when the address is not one.
17
+ * Only an `/apps/<installationId>/` page is ever asked for its configuration.
18
+ */
19
+ export declare function childInstallationAddress(href: string, origin: string): URL | null;
20
+ /**
21
+ * The launch still names what the host serves at its address: the same
22
+ * installation, App and Release, under the admitted Computer. The host's
23
+ * configuration read is the one fresh answer; no directory read precedes it.
24
+ */
25
+ export declare function childMatchesLaunch(launch: ChildLaunch, pathname: string, config: ChildIdentity, computerTarget: string | undefined): boolean;
26
+ /** Concurrent callers share the one read in flight; a settled read is never reused. */
27
+ export declare function sharedRead<A>(read: () => Promise<A>): () => Promise<A>;
@@ -0,0 +1,38 @@
1
+ import { addressKey } from "@fungi.computer/nacre/app-directory";
2
+ const installationPath = /^\/apps\/[^/]+\/$/u;
3
+ /**
4
+ * The installation page a launch names, or null when the address is not one.
5
+ * Only an `/apps/<installationId>/` page is ever asked for its configuration.
6
+ */
7
+ export function childInstallationAddress(href, origin) {
8
+ const address = new URL(href, origin);
9
+ return address.origin === origin && installationPath.test(address.pathname)
10
+ ? address
11
+ : null;
12
+ }
13
+ /**
14
+ * The launch still names what the host serves at its address: the same
15
+ * installation, App and Release, under the admitted Computer. The host's
16
+ * configuration read is the one fresh answer; no directory read precedes it.
17
+ */
18
+ export function childMatchesLaunch(launch, pathname, config, computerTarget) {
19
+ return (pathname === `/apps/${encodeURIComponent(config.installationId)}/` &&
20
+ launch.surfaceId ===
21
+ addressKey({
22
+ kind: "installation",
23
+ installationId: config.installationId,
24
+ }) &&
25
+ config.appId === launch.appId &&
26
+ config.releaseId === launch.releaseId &&
27
+ (computerTarget === undefined || config.computerId === computerTarget));
28
+ }
29
+ /** Concurrent callers share the one read in flight; a settled read is never reused. */
30
+ export function sharedRead(read) {
31
+ let inFlight;
32
+ return () => {
33
+ inFlight ??= read().finally(() => {
34
+ inFlight = undefined;
35
+ });
36
+ return inFlight;
37
+ };
38
+ }
@@ -9,7 +9,7 @@ import { RpcTarget, RpcStub } from "@fungi.computer/capnweb";
9
9
  import { EventSource } from "eventsource";
10
10
  import { connectBrowser, UiBrowserError, } from "@fungi.computer/knapsack/ui/browser";
11
11
  import { createPageLifetime } from "./lifetime.js";
12
- import { computerTargetUrl } from "./computer-target.js";
12
+ import { computerTargetUrl } from "../computer-target.js";
13
13
  import * as Schema from "effect/Schema";
14
14
  import { AppError, createCapabilityRegistry, } from "@fungi.computer/agar";
15
15
  import { appStorage } from "@fungi.computer/agar/storage";