@fungi.computer/oyster 0.5.0 → 0.7.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 (185) hide show
  1. package/README.md +155 -63
  2. package/dist/agent-archive-projection.d.ts +8 -0
  3. package/dist/agent-archive-projection.js +49 -0
  4. package/dist/agent-schedules.d.ts +50 -0
  5. package/dist/agent-schedules.js +17 -0
  6. package/dist/agents.d.ts +35 -14
  7. package/dist/agents.js +97 -144
  8. package/dist/app-builds.d.ts +7 -2
  9. package/dist/app-builds.js +19 -2
  10. package/dist/approval-capability.d.ts +1 -1
  11. package/dist/approval-capability.js +11 -16
  12. package/dist/approval-home.d.ts +2 -1
  13. package/dist/approval-home.js +5 -3
  14. package/dist/apps/catalog.js +1 -1
  15. package/dist/apps/gate.d.ts +4 -7
  16. package/dist/apps/gate.js +14 -29
  17. package/dist/apps/index.d.ts +1 -1
  18. package/dist/apps/installation.d.ts +4 -42
  19. package/dist/apps/installation.js +7 -75
  20. package/dist/apps/operations.js +0 -10
  21. package/dist/apps/permissions.d.ts +18 -8
  22. package/dist/apps/permissions.js +7 -1
  23. package/dist/auth.js +9 -1
  24. package/dist/authorization.d.ts +307 -0
  25. package/dist/authorization.js +542 -0
  26. package/dist/boot-host-script.js +1 -1
  27. package/dist/build/build-jobs.d.ts +2 -2
  28. package/dist/build/candidate-builder.d.ts +2 -0
  29. package/dist/build/candidate-builder.js +5 -1
  30. package/dist/build/candidate-cli.js +3 -0
  31. package/dist/build/candidate-cli.mjs +67 -22
  32. package/dist/build/job-log.d.ts +23 -0
  33. package/dist/build/job-log.js +88 -0
  34. package/dist/build/local-build.d.ts +2 -0
  35. package/dist/build/local-build.js +11 -9
  36. package/dist/channels/binding.d.ts +8 -1
  37. package/dist/channels/binding.js +12 -0
  38. package/dist/channels/conversations.d.ts +16 -5
  39. package/dist/channels/visitor-channel.d.ts +53 -0
  40. package/dist/channels/visitor-channel.js +124 -0
  41. package/dist/channels/visitor-edge.d.ts +115 -0
  42. package/dist/channels/visitor-edge.js +582 -0
  43. package/dist/channels/visitor-request.d.ts +7 -0
  44. package/dist/channels/visitor-request.js +17 -0
  45. package/dist/channels/visitor-stream-slots.d.ts +2 -0
  46. package/dist/channels/visitor-stream-slots.js +33 -0
  47. package/dist/channels/visitors.d.ts +47 -0
  48. package/dist/channels/visitors.js +181 -0
  49. package/dist/cloudflare.d.ts +11 -2
  50. package/dist/cloudflare.js +27 -1
  51. package/dist/{page/computer-target.d.ts → computer-target.d.ts} +2 -0
  52. package/dist/{page/computer-target.js → computer-target.js} +3 -1
  53. package/dist/files.js +9 -8
  54. package/dist/git-admission.d.ts +3 -3
  55. package/dist/git-admission.js +4 -6
  56. package/dist/git-door.d.ts +8 -7
  57. package/dist/git-door.js +18 -17
  58. package/dist/git.d.ts +1 -1
  59. package/dist/host.d.ts +38 -42
  60. package/dist/host.js +182 -239
  61. package/dist/installation-execution.js +10 -5
  62. package/dist/installation-operations.d.ts +4 -180
  63. package/dist/installation-operations.js +1 -5
  64. package/dist/installation-store-generation.d.ts +4 -1
  65. package/dist/installation-store-generation.js +59 -6
  66. package/dist/internal/delivery.js +0 -1
  67. package/dist/internal/host-gate.d.ts +9 -6
  68. package/dist/internal/host-gate.js +19 -25
  69. package/dist/internal/invocation-timing.d.ts +16 -0
  70. package/dist/internal/invocation-timing.js +23 -0
  71. package/dist/internal/registry-installations.js +1 -0
  72. package/dist/invocation.d.ts +19 -11
  73. package/dist/invocation.js +171 -96
  74. package/dist/keys.d.ts +2 -19
  75. package/dist/keys.js +1 -37
  76. package/dist/mcp.d.ts +15 -27
  77. package/dist/mcp.js +105 -155
  78. package/dist/page/agent-browser.d.ts +15 -0
  79. package/dist/page/agent-browser.js +162 -0
  80. package/dist/page/app-permissions-port.d.ts +3 -0
  81. package/dist/page/app-permissions-port.js +43 -0
  82. package/dist/page/app-permissions.d.ts +11 -0
  83. package/dist/page/app-permissions.js +296 -0
  84. package/dist/page/capabilities.js +1 -1
  85. package/dist/page/child-selection.d.ts +27 -0
  86. package/dist/page/child-selection.js +38 -0
  87. package/dist/page/isolated-page-theme.d.ts +2 -0
  88. package/dist/page/isolated-page-theme.js +11 -0
  89. package/dist/page/mount.d.ts +2 -13
  90. package/dist/page/mount.js +8 -94
  91. package/dist/page-script.d.ts +0 -3
  92. package/dist/page-script.js +3 -4
  93. package/dist/release.d.ts +13 -0
  94. package/dist/release.js +10 -0
  95. package/dist/request-origin.d.ts +4 -0
  96. package/dist/request-origin.js +10 -0
  97. package/dist/standalone/agent-archive-owner.d.ts +42 -0
  98. package/dist/standalone/agent-archive-owner.js +116 -0
  99. package/dist/standalone/agent-archive-schedules.d.ts +6 -0
  100. package/dist/standalone/agent-archive-schedules.js +76 -0
  101. package/dist/standalone/agent-archive.d.ts +31 -0
  102. package/dist/standalone/agent-archive.js +67 -0
  103. package/dist/standalone/agent-computers.d.ts +7 -6
  104. package/dist/standalone/agent-computers.js +4 -7
  105. package/dist/standalone/agent-mail.d.ts +2 -2
  106. package/dist/standalone/agents.d.ts +157 -20
  107. package/dist/standalone/agents.js +242 -38
  108. package/dist/standalone/app-builds-binding.js +1 -1
  109. package/dist/standalone/app-set.js +2 -54
  110. package/dist/standalone/app-source.d.ts +3 -4
  111. package/dist/standalone/app-source.js +12 -9
  112. package/dist/standalone/builds.d.ts +2 -14
  113. package/dist/standalone/builds.js +15 -13
  114. package/dist/standalone/computer-fleet.d.ts +4 -4
  115. package/dist/standalone/computer-fleet.js +20 -43
  116. package/dist/standalone/computer.d.ts +4 -3
  117. package/dist/standalone/computer.js +52 -53
  118. package/dist/standalone/dashboard-approval.d.ts +10 -0
  119. package/dist/standalone/dashboard-approval.js +66 -0
  120. package/dist/standalone/machine-link.d.ts +4 -221
  121. package/dist/standalone/machine-link.js +21 -15
  122. package/dist/standalone/media.d.ts +1 -0
  123. package/dist/standalone/media.js +20 -14
  124. package/dist/standalone/owner-routes.d.ts +16 -0
  125. package/dist/standalone/owner-routes.js +21 -0
  126. package/dist/standalone/permission-resources.d.ts +5 -0
  127. package/dist/standalone/permission-resources.js +31 -0
  128. package/dist/standalone/registry.d.ts +7 -0
  129. package/dist/standalone/registry.js +3 -0
  130. package/dist/standalone/tenancy.js +3 -1
  131. package/dist/standalone/worker-routes.d.ts +19 -0
  132. package/dist/standalone/worker-routes.js +9 -0
  133. package/dist/standalone/worker.d.ts +1 -1
  134. package/dist/standalone/worker.js +27 -35
  135. package/dist/standalone/workflow-binding.d.ts +8 -3
  136. package/dist/standalone/workflow-binding.js +14 -5
  137. package/dist/standalone/workflow-git-capture.js +2 -1
  138. package/dist/standalone/workflow-host.d.ts +8 -103
  139. package/dist/standalone/workflow-host.js +1 -1
  140. package/dist/standalone/workflow-node-capture.js +2 -2
  141. package/dist/standalone/workflow-root-authority.d.ts +1 -1
  142. package/dist/standalone/workflow-root-authority.js +1 -1
  143. package/dist/standalone/workflow-schedules.d.ts +14 -5
  144. package/dist/standalone/workflow-schedules.js +11 -5
  145. package/dist/standalone/workflow-webhooks.d.ts +4 -18
  146. package/dist/standalone/workflow-webhooks.js +5 -5
  147. package/dist/standalone/workflows.d.ts +36 -28
  148. package/dist/standalone/workflows.js +6 -3
  149. package/dist/storage-frame.d.ts +6 -6
  150. package/dist/testing/capabilities.d.ts +7 -6
  151. package/dist/testing/capabilities.js +3 -6
  152. package/dist/testing/installation.d.ts +0 -1
  153. package/dist/testing/installation.js +2 -5
  154. package/dist/turnstile.d.ts +27 -0
  155. package/dist/turnstile.js +51 -0
  156. package/dist/workers-git.d.ts +12 -4
  157. package/dist/workers-git.js +58 -36
  158. package/dist/workers.d.ts +9 -1
  159. package/dist/workers.js +59 -20
  160. package/dist/workflows.d.ts +2 -0
  161. package/dist/workflows.js +1 -0
  162. package/docs/api/README.md +7 -0
  163. package/docs/api/agent-schedules.md +175 -0
  164. package/docs/api/agents.md +34 -13
  165. package/docs/api/app-storage.md +1 -1
  166. package/docs/api/apps.md +323 -225
  167. package/docs/api/authorization.md +1300 -0
  168. package/docs/api/build-contract.md +2 -2
  169. package/docs/api/build.md +40 -37
  170. package/docs/api/cloudflare.md +27 -2
  171. package/docs/api/git.md +20 -11
  172. package/docs/api/host.md +80 -199
  173. package/docs/api/index.md +1 -1
  174. package/docs/api/invocation.md +15 -11
  175. package/docs/api/keys.md +1 -98
  176. package/docs/api/mcp.md +43 -40
  177. package/docs/api/modules.md +2 -0
  178. package/docs/api/page/mount.md +16 -19
  179. package/docs/api/workers-git.md +46 -12
  180. package/docs/api/workers.md +125 -235
  181. package/docs/site/agent-quickstart.mdx +6 -2
  182. package/examples/standalone/wrangler.celld.jsonc +66 -0
  183. package/package.json +64 -21
  184. package/dist/standalone/channel-session-door.d.ts +0 -4
  185. package/dist/standalone/channel-session-door.js +0 -42
@@ -13,7 +13,6 @@ import { APP_INVOCATION_CEILING_MS, invocationFacts, invocationCallerContext, }
13
13
  import { AppInstanceError, selectionOf } from "./index.js";
14
14
  import { ownerOrKeyAuth } from "./auth.js";
15
15
  import { causeClass } from "./cause-class.js";
16
- import { Hono } from "hono";
17
16
  import { HTTPException } from "hono/http-exception";
18
17
  import * as Schema from "effect/Schema";
19
18
  import { defineIdFamily } from "@fungi.computer/cairn";
@@ -26,7 +25,8 @@ import { agentCapabilities, agentApprovalRoutes, agentMcpServerRoutes, } from ".
26
25
  import { createArtifactsRestNamespace, } from "@fungi.computer/burrow";
27
26
  import { attemptWebhook } from "./webhook-delivery.js";
28
27
  import { createRecipientDeliveries, } from "./event-recipients.js";
29
- import { createOyster, oysterPrincipalIsOwner, } from "./host.js";
28
+ import { createOyster, } from "./host.js";
29
+ import { oysterPrincipalIsOwner, owner as ownerRoute, routes as routeTable, } from "./authorization.js";
30
30
  import { OysterInstance, oysterBindingEntrypoint, oysterBindings, oysterWorkerHost, r2FileSource, r2ArtifactStore, serveOysterBootCapability, } from "./workers.js";
31
31
  import { registryInstallations } from "./internal/registry-installations.js";
32
32
  import { refuseOlderOysterStore } from "./older-oyster-store-refusal.js";
@@ -763,36 +763,29 @@ const subscribeSchema = Schema.Struct({
763
763
  secret: Schema.optional(Schema.String.check(Schema.isMinLength(16))),
764
764
  });
765
765
  const keyNamePattern = /^[a-z0-9][a-z0-9-]{0,39}$/u;
766
- /** `invocation.ts:486` uses `team-key:visitor` for anonymous readers; a key can never claim that name. */
767
- const reservedKeyName = "visitor";
768
766
  const keysWire = keyWire(oysterKeyVocabulary);
769
767
  /** Owner routes for keys and webhooks; `createOyster` mounts them owner-only. */
770
768
  export function gitAdminRoutes(env, scope) {
771
- const routes = new Hono();
769
+ const routes = routeTable();
772
770
  const keys = () => env.KEYS.getByName(scope.durableName("keys"));
773
- routes.get("/api/keys", async (context) => {
774
- const owner = context.get("principal");
775
- if (!oysterPrincipalIsOwner(owner))
776
- return Response.json({ error: "Owner only" }, { status: 403 });
777
- const listed = await keys().list(owner.actor.memberId);
771
+ // Key and webhook custody is the owner's own, never through an App.
772
+ const owner = ownerRoute("direct");
773
+ routes.get("/api/keys", owner, async (_context, { principal }) => {
774
+ const listed = await keys().list(principal.actor.memberId);
778
775
  if (listed.kind === "error")
779
776
  return Response.json({ error: listed.error.code }, { status: 500 });
780
777
  return Response.json({ keys: listed.value.map(keysWire.toSummary) });
781
778
  });
782
- routes.post("/api/keys", async (context) => {
779
+ routes.post("/api/keys", owner, async (context, { principal }) => {
783
780
  const body = keysWire.issue.safeParse(await context.req.json());
784
781
  if (!body.success)
785
782
  return Response.json({ error: "Invalid key" }, { status: 400 });
786
783
  // Named identities and rate policy belong to this host, not the wire grammar.
787
784
  if (!keyNamePattern.test(body.data.name) ||
788
- body.data.name === reservedKeyName ||
789
785
  body.data.keyId !== undefined ||
790
786
  body.data.rateLimit !== undefined)
791
787
  return Response.json({ error: "Invalid key name" }, { status: 400 });
792
- const owner = context.get("principal");
793
- if (!oysterPrincipalIsOwner(owner))
794
- return Response.json({ error: "Owner only" }, { status: 403 });
795
- const issued = await keys().issue(owner.actor.memberId, {
788
+ const issued = await keys().issue(principal.actor.memberId, {
796
789
  name: body.data.name,
797
790
  grants: body.data.grants,
798
791
  expiresAt: body.data.expiresAt,
@@ -804,19 +797,16 @@ export function gitAdminRoutes(env, scope) {
804
797
  key: keysWire.toSummary(issued.value.record),
805
798
  }, { status: 201 });
806
799
  });
807
- routes.post("/api/keys/revoke", async (context) => {
800
+ routes.post("/api/keys/revoke", owner, async (context, { principal }) => {
808
801
  const body = keysWire.revoke.safeParse(await context.req.json());
809
802
  if (!body.success)
810
803
  return Response.json({ error: "Invalid request" }, { status: 400 });
811
- const owner = context.get("principal");
812
- if (!oysterPrincipalIsOwner(owner))
813
- return Response.json({ error: "Owner only" }, { status: 403 });
814
- const revoked = await keys().revoke(owner.actor.memberId, body.data.requestId, body.data.keyId);
804
+ const revoked = await keys().revoke(principal.actor.memberId, body.data.requestId, body.data.keyId);
815
805
  if (revoked.kind === "error")
816
806
  return Response.json({ error: revoked.error.code }, { status: revoked.error.code === "denied" ? 404 : 400 });
817
807
  return Response.json({ key: keysWire.toSummary(revoked.value) });
818
808
  });
819
- routes.post("/api/webhooks", async (context) => {
809
+ routes.post("/api/webhooks", owner, async (context) => {
820
810
  const body = Schema.decodeUnknownOption(subscribeSchema)(await context.req.json());
821
811
  if (body._tag === "None")
822
812
  return Response.json({ error: "Invalid subscription" }, { status: 400 });
@@ -825,9 +815,9 @@ export function gitAdminRoutes(env, scope) {
825
815
  types: body.value.types ?? [],
826
816
  }), { status: 201 });
827
817
  });
828
- routes.get("/api/webhooks", async () => Response.json(await env.WEBHOOKS.getByName(scope.durableName("webhooks")).subscriptions()));
829
- routes.get("/api/webhooks/:id/deliveries", async (context) => Response.json(await env.WEBHOOKS.getByName(scope.durableName("webhooks")).deliveries(context.req.param("id"))));
830
- routes.delete("/api/webhooks/:id", async (context) => (await env.WEBHOOKS.getByName(scope.durableName("webhooks")).unsubscribe(context.req.param("id")))
818
+ routes.get("/api/webhooks", owner, async () => Response.json(await env.WEBHOOKS.getByName(scope.durableName("webhooks")).subscriptions()));
819
+ routes.get("/api/webhooks/:id/deliveries", owner, async (context) => Response.json(await env.WEBHOOKS.getByName(scope.durableName("webhooks")).deliveries(context.req.param("id"))));
820
+ routes.delete("/api/webhooks/:id", owner, async (context) => (await env.WEBHOOKS.getByName(scope.durableName("webhooks")).unsubscribe(context.req.param("id")))
831
821
  ? new Response(null, { status: 204 })
832
822
  : Response.json({ error: "Not found" }, { status: 404 }));
833
823
  return routes;
@@ -889,6 +879,7 @@ export function createOysterGitWorker(root) {
889
879
  hosts = new Map();
890
880
  environments.set(env, hosts);
891
881
  }
882
+ const started = Date.now();
892
883
  let resolved;
893
884
  try {
894
885
  resolved = await root.admit(env, request);
@@ -907,6 +898,7 @@ export function createOysterGitWorker(root) {
907
898
  const boot = await serveOysterBootCapability(request, env, scope);
908
899
  if (boot !== null)
909
900
  return boot;
901
+ const admitted = Date.now();
910
902
  let host;
911
903
  try {
912
904
  host = await root.hostFor(env, scope, request);
@@ -916,6 +908,7 @@ export function createOysterGitWorker(root) {
916
908
  return error.getResponse();
917
909
  throw error;
918
910
  }
911
+ const hosted = Date.now();
919
912
  const key = `${scope.tenantId}:${scope.origin}:${host.computerId ?? ""}`;
920
913
  let entry = hosts.get(key);
921
914
  let cache = true;
@@ -938,7 +931,10 @@ export function createOysterGitWorker(root) {
938
931
  }
939
932
  entry.active += 1;
940
933
  try {
941
- return await entry.app.fetch(request, env, ctx);
934
+ return withStageTimings(await entry.app.fetch(request, env, ctx), {
935
+ admit: admitted - started,
936
+ host: hosted - admitted,
937
+ });
942
938
  }
943
939
  finally {
944
940
  entry.active -= 1;
@@ -946,6 +942,19 @@ export function createOysterGitWorker(root) {
946
942
  },
947
943
  };
948
944
  }
945
+ /**
946
+ * Add the root's own stages (tenant admission, host selection) to the App
947
+ * server's Server-Timing, so a slow request names the hop it waited on. An
948
+ * upgraded socket keeps its response untouched.
949
+ */
950
+ function withStageTimings(response, stages) {
951
+ if (response.status === 101)
952
+ return response;
953
+ const timed = new Response(response.body, response);
954
+ for (const [name, duration] of Object.entries(stages))
955
+ timed.headers.append("Server-Timing", `${name};dur=${String(duration)}`);
956
+ return timed;
957
+ }
949
958
  /**
950
959
  * `host.git` for an Oyster Worker: the git door's writer projection and the
951
960
  * admitted, installation-scoped Artifacts path with the App's push policy and
@@ -1117,8 +1126,8 @@ export async function oysterGitWorkerHost(env, options, selected, request) {
1117
1126
  const base = oysterWorkerHost(env, auth, policyOptions);
1118
1127
  const capabilities = [
1119
1128
  ...(base.capabilities ?? []),
1120
- ...(agents === undefined ? [] : agentCapabilities(agents, keysLive)),
1121
- ...(options.capabilities?.(env, workspace, keysLive) ?? []),
1129
+ ...(agents === undefined ? [] : agentCapabilities(agents)),
1130
+ ...(options.capabilities?.(env, workspace) ?? []),
1122
1131
  ];
1123
1132
  let gitOptions = { scope };
1124
1133
  if (options.workflowGitPass !== undefined)
@@ -1126,14 +1135,7 @@ export async function oysterGitWorkerHost(env, options, selected, request) {
1126
1135
  if (options.buildGitPass !== undefined)
1127
1136
  gitOptions = { ...gitOptions, buildGitPass: options.buildGitPass };
1128
1137
  const git = oysterWorkerGit(env, gitOptions);
1129
- const ownerRoutes = gitAdminRoutes(env, scope);
1130
- const hostOwnerRoutes = options.ownerRoutes?.(env, scope);
1131
- if (hostOwnerRoutes !== undefined)
1132
- ownerRoutes.route("/", hostOwnerRoutes);
1133
- if (agents !== undefined) {
1134
- ownerRoutes.route("/", agentApprovalRoutes(agents));
1135
- ownerRoutes.route("/", agentMcpServerRoutes(agents));
1136
- }
1138
+ const ownerRoutes = oysterGitOwnerRoutes(env, scope, options.ownerRoutes?.(env, scope), agents);
1137
1139
  let host = {
1138
1140
  ...base,
1139
1141
  capabilities,
@@ -1149,8 +1151,28 @@ export async function oysterGitWorkerHost(env, options, selected, request) {
1149
1151
  host = { ...host, computerControl: options.computerControl(env, scope) };
1150
1152
  if (options.homeInstallationId !== undefined)
1151
1153
  host = { ...host, homeInstallationId: options.homeInstallationId(env) };
1154
+ if (options.permissionResources !== undefined)
1155
+ host = {
1156
+ ...host,
1157
+ permissionResources: options.permissionResources(env, scope),
1158
+ };
1152
1159
  return host;
1153
1160
  }
1161
+ /**
1162
+ * A Git host's whole owner table: keys and webhooks, the root's own owner
1163
+ * routes, and Agent approval and MCP server custody. The host and the
1164
+ * authorization law both compose it here.
1165
+ */
1166
+ export function oysterGitOwnerRoutes(env, scope, hostOwnerRoutes, agents) {
1167
+ const ownerRoutes = gitAdminRoutes(env, scope);
1168
+ if (hostOwnerRoutes !== undefined)
1169
+ ownerRoutes.include(hostOwnerRoutes);
1170
+ if (agents !== undefined) {
1171
+ ownerRoutes.include(agentApprovalRoutes(agents));
1172
+ ownerRoutes.include(agentMcpServerRoutes(agents));
1173
+ }
1174
+ return ownerRoutes;
1175
+ }
1154
1176
  /**
1155
1177
  * Authenticate against the pinned tenant. A request admitted at the Worker's
1156
1178
  * top reuses that one admission; any other entry resolves its own.
package/dist/workers.d.ts CHANGED
@@ -34,6 +34,11 @@ export { r2ArtifactStore, r2FileSource, ownerKeyAuth };
34
34
  export type OysterBindingResult = Awaited<ReturnType<GitBinding[keyof GitBinding]>> | Schema.Json | undefined;
35
35
  /** Verified backend custody is captured in the frame, never in a current-context field. */
36
36
  export type OysterInvocationHost = BackgroundFrameState & Readonly<{
37
+ /** This frame's guest binding packets and the wall time they waited. */
38
+ bindingWaits: {
39
+ count: number;
40
+ ms: number;
41
+ };
37
42
  artifact: Backend;
38
43
  declaration: AppDeclaration | null;
39
44
  /** The verified declared method; opaque backend and lifecycle frames have none. */
@@ -124,10 +129,10 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
124
129
  readonly kind: "policy";
125
130
  } | null;
126
131
  readonly access: "write" | "external";
132
+ readonly state: "pending" | "expired" | "denied" | "applied" | "requested" | "approved" | "claimed" | "outcome_unknown";
127
133
  readonly reason: "pending" | "expired" | "not-found" | "not-eligible" | "already-answered" | "stale-revision" | "declined" | "fenced" | "not-granted" | "read-only" | "in-flight" | "outcome-unknown" | "identity-conflict" | null;
128
134
  readonly updatedAt: number;
129
135
  readonly module: string;
130
- readonly state: "pending" | "expired" | "denied" | "applied" | "requested" | "approved" | "claimed" | "outcome_unknown";
131
136
  readonly recordId: string;
132
137
  readonly authorityFence: string;
133
138
  readonly requester: {
@@ -222,6 +227,8 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
222
227
  get(installationId: string): Installation | null;
223
228
  /** Durable store identity, regenerated only when the store itself is replaced. */
224
229
  generation(): string;
230
+ /** Permanent audit fact for the owner Home's empty installation notice. */
231
+ installationResetAt(): number | null;
225
232
  /** Native RPC returns a closed result; callers never classify a transported Error. */
226
233
  compareAndPut(installation: Installation, expectedAuthorityVersion: number, expectedGeneration: string, tenantId?: TenantId): Promise<InstallationCommit>;
227
234
  private writeInstallation;
@@ -387,6 +394,7 @@ export declare class OysterInstance<Env extends OysterWorkerEnv = OysterWorkerEn
387
394
  /** Resolve each guest packet against its immutable frame before any host effect. */
388
395
  bindingCall(raw: FrameBindingProps, method: string, input: readonly unknown[]): Promise<FrameBindingReply<OysterBindingResult>>;
389
396
  private executeBindingCall;
397
+ private dispatchBindingCall;
390
398
  /** Last provider-accepted ref recorded by this installation's journal. */
391
399
  gitTip(installationId: string, repository: string, ref: string, tenantId?: TenantId): Promise<string | null>;
392
400
  /** Join accepted fact identity and Carrier delivery in one local transaction. */
package/dist/workers.js CHANGED
@@ -35,10 +35,12 @@ import * as Schema from "effect/Schema";
35
35
  import * as Stream from "effect/Stream";
36
36
  import { releaseSchema } from "@fungi.computer/puffball/effect";
37
37
  import { AppInstanceError, } from "./index.js";
38
- import { createCloudflareAppInstance, r2ArtifactStore, createCloudflareInvocationGate, decodeFrameBindingProps, captureFrameBindingReply, frameBindingEntrypoint, backgroundFrameMethods, } from "./cloudflare.js";
38
+ import { createCloudflareAppInstance, r2ArtifactStore, createCloudflareInvocationGate, edgeVerifiedObjectCache, decodeFrameBindingProps, captureFrameBindingReply, frameBindingEntrypoint, backgroundFrameMethods, } from "./cloudflare.js";
39
39
  import { invocationFactsSchema, assertBackgroundBinding, admitBackgroundFrame, } from "./invocation.js";
40
40
  import { deliverBackground, deliverGitPush } from "./internal/delivery.js";
41
- import { createOyster, verifyRelease, defaultAssetPolicy, } from "./host.js";
41
+ import { reportSlowInvocation } from "./internal/invocation-timing.js";
42
+ import { releaseCache } from "./internal/host-gate.js";
43
+ import { createOyster, } from "./host.js";
42
44
  import { createGitPushJournal } from "./git-journal.js";
43
45
  import { AppStorageError, } from "@fungi.computer/agar/storage";
44
46
  import { storageFrameMethods } from "./storage-frame.js";
@@ -65,7 +67,7 @@ const approvalHomePinRow = Schema.Struct({
65
67
  });
66
68
  /** The installation registry: one SQLite-backed Durable Object per Oyster. */
67
69
  export class OysterRegistry extends DurableObject {
68
- #generation;
70
+ #installationStore;
69
71
  /** Decided once, before this version writes anything, and never revised. */
70
72
  #installationsFromOlderOyster;
71
73
  #approval;
@@ -148,6 +150,7 @@ export class OysterRegistry extends DurableObject {
148
150
  return this.approvalHome(pin).capability(actor).setAutoApprove(input);
149
151
  }
150
152
  #rootData;
153
+ #releases = releaseCache(r2ArtifactStore(this.env.RELEASES));
151
154
  /** Append-only App source seed receipts; a Git-capable subclass forks over these. */
152
155
  appSourceReceipts;
153
156
  /** Team-owned fork registrations, one App publisher record per App. */
@@ -159,7 +162,7 @@ export class OysterRegistry extends DurableObject {
159
162
  this.#installationsFromOlderOyster = installationsFromOlderOyster(ctx.storage.sql);
160
163
  ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS oyster_approval_home (id INTEGER PRIMARY KEY CHECK (id = 1), tenant_id TEXT NOT NULL, origin TEXT NOT NULL) STRICT");
161
164
  ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS installations (installation_id TEXT PRIMARY KEY, record TEXT NOT NULL) STRICT");
162
- this.#generation = createInstallationStoreGeneration(ctx.storage.sql);
165
+ this.#installationStore = createInstallationStoreGeneration(ctx.storage);
163
166
  ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS boot_capabilities (id TEXT PRIMARY KEY, token_digest TEXT NOT NULL, expires_at INTEGER NOT NULL, record TEXT NOT NULL) STRICT");
164
167
  ensureAppSourceTables(ctx.storage.sql);
165
168
  this.#installationOperations = createInstallationOperations({
@@ -196,7 +199,7 @@ export class OysterRegistry extends DurableObject {
196
199
  installations: {
197
200
  list: () => Promise.resolve(this.list()),
198
201
  },
199
- resolveRelease: (release) => verifyRelease(r2ArtifactStore(env.RELEASES), release, defaultAssetPolicy, false),
202
+ resolveRelease: this.#releases,
200
203
  });
201
204
  }
202
205
  /** The installation owner's RPC: admit, read, load, plan or commit one
@@ -319,7 +322,7 @@ export class OysterRegistry extends DurableObject {
319
322
  expected.selection.releaseId ||
320
323
  installation.release.appId !== expected.selection.appId)))
321
324
  throw new AppStorageError("unavailable");
322
- const resolved = await verifyRelease(r2ArtifactStore(this.env.RELEASES), installation.release, defaultAssetPolicy, false);
325
+ const resolved = await this.#releases(installation.release);
323
326
  if (expected !== undefined &&
324
327
  resolved.backend?.digest !== expected.artifactDigest)
325
328
  throw new AppStorageError("unavailable");
@@ -374,7 +377,11 @@ export class OysterRegistry extends DurableObject {
374
377
  }
375
378
  /** Durable store identity, regenerated only when the store itself is replaced. */
376
379
  generation() {
377
- return this.#generation();
380
+ return this.#installationStore.generation();
381
+ }
382
+ /** Permanent audit fact for the owner Home's empty installation notice. */
383
+ installationResetAt() {
384
+ return this.#installationStore.resetAt();
378
385
  }
379
386
  /** Native RPC returns a closed result; callers never classify a transported Error. */
380
387
  async compareAndPut(installation, expectedAuthorityVersion, expectedGeneration, tenantId = defaultTenant) {
@@ -756,13 +763,23 @@ export class OysterInstance extends DurableObject {
756
763
  }
757
764
  async executeBindingCall(raw, method, input) {
758
765
  const props = decodeFrameBindingProps(raw);
759
- const frame = this.frames.active();
760
- if (props.instanceId !== frame.facts.instanceId ||
761
- props.frameId !== frame.frameId)
766
+ // The packet's own frame: a closed or foreign frame id refuses.
767
+ const frame = this.frames.active(props.frameId);
768
+ if (props.instanceId !== frame.facts.instanceId)
762
769
  throw new AppInstanceError("revoked");
770
+ const started = Date.now();
771
+ try {
772
+ return await this.dispatchBindingCall(frame, props, method, input);
773
+ }
774
+ finally {
775
+ frame.host.bindingWaits.count += 1;
776
+ frame.host.bindingWaits.ms += Date.now() - started;
777
+ }
778
+ }
779
+ async dispatchBindingCall(frame, props, method, input) {
763
780
  assertBackgroundBinding(frame, props.binding);
764
781
  // A guest packet is its own entry: exactly one registry read re-admits it.
765
- await this.assertCurrent(frame);
782
+ const current = await this.assertCurrent(frame);
766
783
  const row = this.bindings[props.binding];
767
784
  const descriptor = row?.methods[method];
768
785
  // Operations declared by the verified Release; absent bindings grant nothing.
@@ -782,12 +799,16 @@ export class OysterInstance extends DurableObject {
782
799
  throw new AppInstanceError("binding_unavailable");
783
800
  }
784
801
  const args = descriptor.decodeArgs(input);
785
- if (!descriptor
786
- .requiredOperations(args)
787
- .every((operation) => granted.includes(operation)))
802
+ const required = descriptor.requiredOperations(args);
803
+ if (!required.every((operation) => granted.includes(operation)))
788
804
  throw new AppInstanceError("binding_not_granted");
805
+ if (descriptor.requestedResource !== undefined) {
806
+ const selected = approvedBindingGrants(current, frame.host.declaration ?? emptyAppDeclaration, descriptor.requestedResource(args)).find((grant) => grant.binding === props.binding)?.operations ?? [];
807
+ if (!required.every((operation) => selected.includes(operation)))
808
+ throw new AppInstanceError("binding_not_granted");
809
+ }
789
810
  const result = await row.dispatch(this.#bindingContext, frame, props, method, args);
790
- if (this.frames.active() !== frame)
811
+ if (this.frames.active(props.frameId) !== frame)
791
812
  throw new AppInstanceError("revoked");
792
813
  assertBackgroundBinding(frame, props.binding);
793
814
  return descriptor.decodeResult(result);
@@ -846,8 +867,10 @@ export class OysterInstance extends DurableObject {
846
867
  .find((currentGrant) => currentGrant.binding === grant.binding)
847
868
  ?.operations.includes(operation) !== true)))
848
869
  throw new AppInstanceError("revoked");
870
+ return current;
849
871
  }
850
872
  async withFrame(raw, limits, work, tenantId = defaultTenant, methodName) {
873
+ const entered = Date.now();
851
874
  this.requireTenant(tenantId);
852
875
  const facts = Schema.decodeUnknownSync(invocationFactsSchema, {
853
876
  onExcessProperty: "error",
@@ -858,6 +881,7 @@ export class OysterInstance extends DurableObject {
858
881
  throw new AppInstanceError("instance_identity_mismatch");
859
882
  const release = this.release(facts.selection.releaseId);
860
883
  const resolved = await this.backends(release);
884
+ const verified = Date.now();
861
885
  if (resolved?.digest !== facts.artifactDigest)
862
886
  throw new AppInstanceError("invalid_invocation_facts");
863
887
  if (facts.subject.kind === "call" &&
@@ -893,6 +917,7 @@ export class OysterInstance extends DurableObject {
893
917
  };
894
918
  }
895
919
  let host = {
920
+ bindingWaits: { count: 0, ms: 0 },
896
921
  artifact: { release, ...resolved },
897
922
  declaration: resolved.declaration,
898
923
  origin: "unchecked",
@@ -902,7 +927,20 @@ export class OysterInstance extends DurableObject {
902
927
  // Facts carry the authority admitted at the request's top. The Instance
903
928
  // checks them against the owner state the host applied to it, never by
904
929
  // calling back up to the registry.
905
- return this.frames.run(scoped, host, (frame) => work(this.runtimeFor(limits), frame));
930
+ let admitted = verified;
931
+ try {
932
+ return await this.frames.run(scoped, host, (frame) => {
933
+ admitted = Date.now();
934
+ return work(this.runtimeFor(limits), frame);
935
+ });
936
+ }
937
+ finally {
938
+ reportSlowInvocation(methodName ?? facts.subject.kind, host, {
939
+ entered,
940
+ verified,
941
+ admitted,
942
+ });
943
+ }
906
944
  }
907
945
  release(releaseId) {
908
946
  const row = this.ctx.storage.sql
@@ -1102,10 +1140,7 @@ export function oysterWorkerHost(env, auth, options = {}) {
1102
1140
  },
1103
1141
  auth,
1104
1142
  workbench: {
1105
- directory: async () => {
1106
- await installations.list();
1107
- return registry().directory();
1108
- },
1143
+ directory: () => registry().directory(),
1109
1144
  storage: (installationId, ownerId) => ({
1110
1145
  read: (key) => registry().storageRead(installationId, ownerId, key),
1111
1146
  write: (input) => registry().storageWrite(installationId, ownerId, input),
@@ -1115,6 +1150,10 @@ export function oysterWorkerHost(env, auth, options = {}) {
1115
1150
  },
1116
1151
  guestLimits: guestLimits(options.guestLimits),
1117
1152
  artifacts,
1153
+ // Workers and workerd keep a zone cache; a runtime without one reads R2.
1154
+ ...("caches" in globalThis && {
1155
+ uiCache: edgeVerifiedObjectCache(caches.default, scope.origin),
1156
+ }),
1118
1157
  bootUi: oysterBootUi(env, scope),
1119
1158
  // File-only hosts may keep workspace files beside their Release artifacts.
1120
1159
  capabilities: [
@@ -10,6 +10,7 @@ export declare const agentWorkflowOperationSchema: z.ZodEnum<{
10
10
  catalog: "catalog";
11
11
  armSchedule: "armSchedule";
12
12
  disarmSchedule: "disarmSchedule";
13
+ pauseSchedule: "pauseSchedule";
13
14
  schedules: "schedules";
14
15
  armEventTrigger: "armEventTrigger";
15
16
  disarmEventTrigger: "disarmEventTrigger";
@@ -26,6 +27,7 @@ export declare const agentAppWorkflowRequestSchema: z.ZodObject<{
26
27
  catalog: "catalog";
27
28
  armSchedule: "armSchedule";
28
29
  disarmSchedule: "disarmSchedule";
30
+ pauseSchedule: "pauseSchedule";
29
31
  schedules: "schedules";
30
32
  armEventTrigger: "armEventTrigger";
31
33
  disarmEventTrigger: "disarmEventTrigger";
package/dist/workflows.js CHANGED
@@ -11,6 +11,7 @@ export const agentWorkflowOperationSchema = z.enum([
11
11
  "catalog",
12
12
  "schedules",
13
13
  "armSchedule",
14
+ "pauseSchedule",
14
15
  "disarmSchedule",
15
16
  "eventTriggers",
16
17
  "armEventTrigger",
@@ -36,6 +36,13 @@
36
36
  | `@fungi.computer/oyster/git` | `EventSink`, `createGitDoor`, `signStandardWebhook` | The EVENTS binding contract, the git smart-HTTP door and Standard Webhooks signing. `GitBinding` comes from `@fungi.computer/agar/git`. |
37
37
  | `@fungi.computer/oyster/workers-git` | `oysterGitRoot`, `createOysterGitWorker`, `ownerOrKeyTenancy`, `oysterWorkerGit`, `gitAdminRoutes` | One Git root whose `hostFor` serves HTTP, MCP and Instances, or `host.git` and key/webhook routes for your own host. |
38
38
 
39
+ Standalone Oyster owners can `POST /agents/:agentId/archive` and
40
+ `POST /agents/:agentId/unarchive`. `GET /agents` lists active Agents;
41
+ `GET /agents?archived=true` lists only archived Agents. Archive preserves
42
+ existing Sessions and reads but refuses new Shiitake work with
43
+ `contract/archived`. It pauses Almanac and Loop schedules; restore resumes only
44
+ schedules paused by archive. The generated Agent API is in `docs/api/agents.md`.
45
+
39
46
  The host owns admission: it decides who may apply owner state and which
40
47
  selection a request carries. Oyster owns what happens to one Instance once the
41
48
  host has that authority: owner-state replay and conflict rules, Release
@@ -0,0 +1,175 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ [**@fungi.computer/oyster**](README.md)
4
+
5
+ ---
6
+
7
+ [@fungi.computer/oyster](modules.md) / agent-schedules
8
+
9
+ # agent-schedules
10
+
11
+ ## Type Aliases
12
+
13
+ ### AppAgentScheduleListInput
14
+
15
+ ```ts
16
+ type AppAgentScheduleListInput = z.input<
17
+ typeof appAgentScheduleListInputSchema
18
+ >;
19
+ ```
20
+
21
+ A single bounded native read, not an all-Agent inventory grant.
22
+
23
+ ---
24
+
25
+ ### AgentScheduleTarget
26
+
27
+ ```ts
28
+ type AgentScheduleTarget = z.output<typeof agentScheduleTargetSchema>;
29
+ ```
30
+
31
+ Identity to admit before acquiring the corresponding Agent owner.
32
+
33
+ ---
34
+
35
+ ### AgentSchedulePage
36
+
37
+ ```ts
38
+ type AgentSchedulePage = typeof agentSchedulePageSchema.Type;
39
+ ```
40
+
41
+ One Agent's page, with no directory fan-out or availability policy.
42
+
43
+ ---
44
+
45
+ ### AgentScheduleControlReceipt
46
+
47
+ ```ts
48
+ type AgentScheduleControlReceipt =
49
+ typeof agentScheduleControlReceiptSchema.Type;
50
+ ```
51
+
52
+ Content-free acknowledgement of the native owner's completed transition.
53
+
54
+ ## Variables
55
+
56
+ ### appAgentScheduleListInputSchema
57
+
58
+ ```ts
59
+ const appAgentScheduleListInputSchema: ZodObject<
60
+ {
61
+ agentId: ZodString;
62
+ after: ZodOptional<
63
+ ZodObject<
64
+ {
65
+ agentId: ZodString;
66
+ kind: ZodUnion<readonly [ZodLiteral<"almanac">, ZodLiteral<"loop">]>;
67
+ after: ZodOptional<ZodString>;
68
+ },
69
+ $strict
70
+ >
71
+ >;
72
+ limit: ZodDefault<ZodNumber>;
73
+ },
74
+ $strict
75
+ >;
76
+ ```
77
+
78
+ The App selects one Agent; its host must admit that target before opening it.
79
+
80
+ ---
81
+
82
+ ### agentScheduleBindingArgs
83
+
84
+ ```ts
85
+ const agentScheduleBindingArgs: object;
86
+ ```
87
+
88
+ Both hosts decode the ordinary AGENTS packet with the native owner's grammar.
89
+
90
+ #### Type Declaration
91
+
92
+ | Name | Type | Description |
93
+ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
94
+ | <a id="property-schedules"></a> `schedules` | `ZodTuple`\<\[`ZodObject`\<\{ `agentId`: `ZodString`; `after`: `ZodOptional`\<`ZodObject`\<\{ `agentId`: `ZodString`; `kind`: `ZodUnion`\<readonly \[`ZodLiteral`\<...\>, `ZodLiteral`\<...\>\]\>; `after`: `ZodOptional`\<`ZodString`\>; \}, `$strict`\>\>; `limit`: `ZodDefault`\<`ZodNumber`\>; \}, `$strict`\>\], `null`\> | One bounded content-bearing read, admitted independently of manage. |
95
+ | <a id="property-pauseschedule"></a> `pauseSchedule` | `ZodTuple`\<\[`ZodDiscriminatedUnion`\<\[`ZodObject`\<\{ `kind`: `ZodLiteral`\<`"almanac"`\>; `agentId`: `ZodString`; `scheduleId`: `ZodString` & `ZodType`\<`string`, `string`, `$ZodTypeInternals`\<`string`, `string`\>\>; \}, `$strict`\>, `ZodObject`\<\{ `kind`: `ZodLiteral`\<`"loop"`\>; `agentId`: `ZodString`; `sessionId`: `ZodString` & `ZodType`\<`string`, `string`, `$ZodTypeInternals`\<`string`, `string`\>\>; `loopId`: `ZodString` & `ZodType`\<`string`, `string`, `$ZodTypeInternals`\<`string`, `string`\>\>; \}, `$strict`\>\], `"kind"`\>\], `null`\> | Pause the exact listed schedule, retaining future resume and accepted work. |
96
+ | <a id="property-deleteschedule"></a> `deleteSchedule` | `ZodTuple`\<\[`ZodDiscriminatedUnion`\<\[`ZodObject`\<\{ `kind`: `ZodLiteral`\<`"almanac"`\>; `agentId`: `ZodString`; `scheduleId`: `ZodString` & `ZodType`\<`string`, `string`, `$ZodTypeInternals`\<`string`, `string`\>\>; \}, `$strict`\>, `ZodObject`\<\{ `kind`: `ZodLiteral`\<`"loop"`\>; `agentId`: `ZodString`; `sessionId`: `ZodString` & `ZodType`\<`string`, `string`, `$ZodTypeInternals`\<`string`, `string`\>\>; `loopId`: `ZodString` & `ZodType`\<`string`, `string`, `$ZodTypeInternals`\<`string`, `string`\>\>; \}, `$strict`\>\], `"kind"`\>\], `null`\> | Cancel future occurrences of the exact identity without erasing history. |
97
+
98
+ ---
99
+
100
+ ### agentSchedulePageSchema
101
+
102
+ ```ts
103
+ const agentSchedulePageSchema: Schema.Struct<{
104
+ items: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
105
+ agentId: Schema.String;
106
+ kind: Schema.Literal<"almanac">;
107
+ schedule: Schema.Struct<{
108
+ id: Schema.String;
109
+ sessionId: Schema.String;
110
+ prompt: Schema.String;
111
+ cron: Schema.NullOr<...>;
112
+ nextDueAt: Schema.Int;
113
+ state: Schema.Literals<...>;
114
+ lastCommandId: Schema.NullOr<...>;
115
+ lastError: Schema.NullOr<...>;
116
+ attempt: Schema.Int;
117
+ }>;
118
+ }>, Schema.Struct<{
119
+ agentId: Schema.String;
120
+ kind: Schema.Literal<"loop">;
121
+ schedule: Schema.Struct<{
122
+ sessionId: Schema.String;
123
+ loopId: Schema.String;
124
+ origin: Schema.Literals<...>;
125
+ expiresAt: Schema.NullOr<...>;
126
+ prompt: Schema.String;
127
+ mode: Schema.Literals<...>;
128
+ cron: Schema.NullOr<...>;
129
+ state: Schema.Literals<...>;
130
+ commandId: Schema.String;
131
+ reason: Schema.NullOr<...>;
132
+ nextDueAt: Schema.NullOr<...>;
133
+ lastError: Schema.NullOr<...>;
134
+ consecutiveFailures: Schema.Int;
135
+ }>;
136
+ }>]>>;
137
+ next: Schema.NullOr<Schema.Struct<{
138
+ agentId: Schema.String;
139
+ kind: Schema.Literals<readonly ["almanac", "loop"]>;
140
+ after: Schema.optionalKey<Schema.String>;
141
+ }>>;
142
+ }>;
143
+ ```
144
+
145
+ A bounded content-bearing page; owner failures reject rather than look empty.
146
+
147
+ ---
148
+
149
+ ### agentScheduleControlReceiptSchema
150
+
151
+ ```ts
152
+ const agentScheduleControlReceiptSchema: Schema.Union<
153
+ readonly [
154
+ Schema.Struct<{
155
+ agentId: Schema.String;
156
+ kind: Schema.Literal<"almanac">;
157
+ scheduleId: Schema.String;
158
+ state: Schema.Literals<
159
+ readonly ["active", "paused", "cancelled", "finished"]
160
+ >;
161
+ }>,
162
+ Schema.Struct<{
163
+ agentId: Schema.String;
164
+ kind: Schema.Literal<"loop">;
165
+ sessionId: Schema.String;
166
+ loopId: Schema.String;
167
+ state: Schema.Literals<
168
+ readonly ["active", "paused", "finished", "failed", "cancelled"]
169
+ >;
170
+ }>,
171
+ ]
172
+ >;
173
+ ```
174
+
175
+ Manage-only result: exact target and native state, never prompt or history.