@fungi.computer/oyster 0.1.0 → 0.3.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 (292) hide show
  1. package/README.md +376 -72
  2. package/dist/agent-avatars.d.ts +103 -0
  3. package/dist/agent-avatars.js +66 -0
  4. package/dist/agents.d.ts +15 -4
  5. package/dist/agents.js +43 -4
  6. package/dist/app-builds.d.ts +81 -8
  7. package/dist/app-builds.js +210 -60
  8. package/dist/app-storage.d.ts +3 -3
  9. package/dist/approval-capability.d.ts +5 -0
  10. package/dist/approval-capability.js +46 -0
  11. package/dist/approval-home.d.ts +51 -0
  12. package/dist/approval-home.js +226 -0
  13. package/dist/apps/catalog.d.ts +335 -0
  14. package/dist/apps/catalog.js +46 -0
  15. package/dist/apps/gate.d.ts +26 -12
  16. package/dist/apps/gate.js +48 -22
  17. package/dist/apps/index.d.ts +3 -1
  18. package/dist/apps/index.js +3 -1
  19. package/dist/apps/installation.d.ts +147 -66
  20. package/dist/apps/installation.js +193 -86
  21. package/dist/apps/permissions.d.ts +247 -0
  22. package/dist/apps/permissions.js +255 -0
  23. package/dist/boot-capability-route.d.ts +15 -0
  24. package/dist/boot-capability-route.js +196 -0
  25. package/dist/boot-capability.d.ts +103 -0
  26. package/dist/boot-capability.js +141 -0
  27. package/dist/boot-construction.d.ts +12 -0
  28. package/dist/boot-construction.js +175 -0
  29. package/dist/boot-gate.d.ts +267 -0
  30. package/dist/boot-gate.js +236 -0
  31. package/dist/boot-host-script.d.ts +2 -0
  32. package/dist/boot-host-script.js +1 -0
  33. package/dist/boot-mount.d.ts +21 -0
  34. package/dist/boot-mount.js +65 -0
  35. package/dist/browser/boot.d.ts +73 -0
  36. package/dist/browser/boot.js +502 -0
  37. package/dist/browser/browser.d.ts +104 -0
  38. package/dist/browser/browser.js +374 -0
  39. package/dist/browser/capability-source.d.ts +76 -0
  40. package/dist/browser/capability-source.js +338 -0
  41. package/dist/browser/cdp-client.d.ts +56 -0
  42. package/dist/browser/cdp-client.js +184 -0
  43. package/dist/browser/errors.d.ts +79 -0
  44. package/dist/browser/errors.js +32 -0
  45. package/dist/browser/index.d.ts +16 -0
  46. package/dist/browser/index.js +14 -0
  47. package/dist/browser/lease-registry.d.ts +61 -0
  48. package/dist/browser/lease-registry.js +101 -0
  49. package/dist/browser/operations.d.ts +225 -0
  50. package/dist/browser/operations.js +137 -0
  51. package/dist/browser/page.d.ts +25 -0
  52. package/dist/browser/page.js +172 -0
  53. package/dist/browser/quick-reads.d.ts +29 -0
  54. package/dist/browser/quick-reads.js +160 -0
  55. package/dist/browser/sessions.d.ts +52 -0
  56. package/dist/browser/sessions.js +118 -0
  57. package/dist/browser/url-policy.d.ts +18 -0
  58. package/dist/browser/url-policy.js +58 -0
  59. package/dist/build/app-imports.d.ts +12 -0
  60. package/dist/build/app-imports.js +105 -0
  61. package/dist/build/build-policy.d.ts +5 -3
  62. package/dist/build/build-policy.js +5 -3
  63. package/dist/build/candidate-builder.d.ts +1 -1
  64. package/dist/build/candidate-cli.mjs +8428 -337
  65. package/dist/build/local-build.d.ts +2 -0
  66. package/dist/build/local-build.js +28 -6
  67. package/dist/build/snapshot-package-store.d.ts +1 -1
  68. package/dist/build/snapshot-package-store.js +7 -2
  69. package/dist/build/workspace-build.d.ts +92 -5
  70. package/dist/build/workspace-build.js +93 -19
  71. package/dist/build-egress.js +4 -4
  72. package/dist/build.d.ts +1 -1
  73. package/dist/build.js +1 -1
  74. package/dist/catalog-frame.js +1 -1
  75. package/dist/cloudflare.d.ts +13 -3
  76. package/dist/cloudflare.js +106 -8
  77. package/dist/frame-binding-wire-script.js +1 -1
  78. package/dist/frame-binding-wire.d.ts +1 -1
  79. package/dist/frame-binding-wire.js +5 -3
  80. package/dist/git-auth.d.ts +0 -2
  81. package/dist/git-auth.js +0 -2
  82. package/dist/git-journal.js +4 -4
  83. package/dist/git.d.ts +0 -1
  84. package/dist/git.js +0 -2
  85. package/dist/host.d.ts +39 -13
  86. package/dist/host.js +268 -142
  87. package/dist/index.d.ts +26 -15
  88. package/dist/index.js +40 -16
  89. package/dist/install-release-cli.d.ts +2 -0
  90. package/dist/install-release-cli.js +46 -0
  91. package/dist/installation-client.d.ts +43 -0
  92. package/dist/installation-client.js +178 -0
  93. package/dist/installation-execution.d.ts +11 -0
  94. package/dist/installation-execution.js +184 -0
  95. package/dist/installation-operations.d.ts +1437 -0
  96. package/dist/installation-operations.js +664 -0
  97. package/dist/installation-store-generation.d.ts +3 -0
  98. package/dist/installation-store-generation.js +16 -0
  99. package/dist/instance-id.d.ts +10 -0
  100. package/dist/instance-id.js +10 -0
  101. package/dist/instance-refusal.d.ts +11 -0
  102. package/dist/instance-refusal.js +21 -0
  103. package/dist/internal/delivery.js +4 -2
  104. package/dist/internal/host-gate.js +7 -1
  105. package/dist/internal/registry-installations.js +4 -4
  106. package/dist/keys.d.ts +5 -1
  107. package/dist/keys.js +10 -2
  108. package/dist/machine-link/authority.d.ts +89 -0
  109. package/dist/machine-link/authority.js +37 -0
  110. package/dist/machine-link/client.d.ts +66 -0
  111. package/dist/machine-link/client.js +94 -0
  112. package/dist/machine-link/delivery.d.ts +18 -0
  113. package/dist/machine-link/delivery.js +55 -0
  114. package/dist/machine-link/enrollment.d.ts +111 -0
  115. package/dist/machine-link/enrollment.js +286 -0
  116. package/dist/machine-link/fences.d.ts +37 -0
  117. package/dist/machine-link/fences.js +166 -0
  118. package/dist/machine-link/home.d.ts +20 -0
  119. package/dist/machine-link/home.js +24 -0
  120. package/dist/machine-link/host.d.ts +43 -0
  121. package/dist/machine-link/host.js +189 -0
  122. package/dist/machine-link/index.d.ts +15 -0
  123. package/dist/machine-link/index.js +15 -0
  124. package/dist/machine-link/link.d.ts +48 -0
  125. package/dist/machine-link/link.js +223 -0
  126. package/dist/machine-link/presence.d.ts +10 -0
  127. package/dist/machine-link/presence.js +25 -0
  128. package/dist/machine-link/workers.d.ts +6 -0
  129. package/dist/machine-link/workers.js +6 -0
  130. package/dist/mcp-owner.d.ts +2 -2
  131. package/dist/mcp-owner.js +41 -33
  132. package/dist/mcp.d.ts +15 -3
  133. package/dist/mcp.js +146 -25
  134. package/dist/pack.d.ts +14 -3
  135. package/dist/pack.js +44 -19
  136. package/dist/page/app-access.d.ts +7 -0
  137. package/dist/page/app-access.js +75 -0
  138. package/dist/page/boot-host.js +57 -0
  139. package/dist/page/capabilities.d.ts +3 -3
  140. package/dist/page/capabilities.js +16 -12
  141. package/dist/page/computer-handles.d.ts +3 -3
  142. package/dist/page/computer-target.d.ts +3 -0
  143. package/dist/page/computer-target.js +9 -0
  144. package/dist/page/mount.d.ts +23 -7
  145. package/dist/page/mount.js +152 -16
  146. package/dist/page-assets.d.ts +22 -0
  147. package/dist/page-assets.js +28 -0
  148. package/dist/page-script.d.ts +3 -0
  149. package/dist/page-script.js +5 -4
  150. package/dist/release.d.ts +13 -6
  151. package/dist/release.js +10 -7
  152. package/dist/root-data.d.ts +16 -6
  153. package/dist/root-data.js +2 -2
  154. package/dist/source/index.d.ts +1 -1
  155. package/dist/source/team-source.d.ts +20 -8
  156. package/dist/source/team-source.js +39 -10
  157. package/dist/source/template-seed.d.ts +2 -2
  158. package/dist/standalone/agent-mail.d.ts +24 -0
  159. package/dist/standalone/agent-mail.js +62 -0
  160. package/dist/standalone/agent-store.d.ts +9 -0
  161. package/dist/standalone/agent-store.js +30 -0
  162. package/dist/standalone/agents.d.ts +78 -12
  163. package/dist/standalone/agents.js +470 -59
  164. package/dist/standalone/app-builds-binding.d.ts +20 -2
  165. package/dist/standalone/app-builds-binding.js +151 -27
  166. package/dist/standalone/app-set.d.ts +5 -2
  167. package/dist/standalone/app-set.js +174 -143
  168. package/dist/standalone/browser.d.ts +11 -0
  169. package/dist/standalone/browser.js +72 -0
  170. package/dist/standalone/builds.d.ts +13 -4
  171. package/dist/standalone/builds.js +28 -44
  172. package/dist/standalone/computer-egress.d.ts +2 -0
  173. package/dist/standalone/computer-egress.js +13 -4
  174. package/dist/standalone/computer-fleet.d.ts +43 -0
  175. package/dist/standalone/computer-fleet.js +200 -0
  176. package/dist/standalone/computer.d.ts +44 -9
  177. package/dist/standalone/computer.js +132 -25
  178. package/dist/standalone/install-apps-cli.js +43 -6
  179. package/dist/standalone/instance.d.ts +2 -2
  180. package/dist/standalone/instance.js +8 -11
  181. package/dist/standalone/machine-link.d.ts +354 -0
  182. package/dist/standalone/machine-link.js +368 -0
  183. package/dist/standalone/media.d.ts +2 -2
  184. package/dist/standalone/media.js +19 -16
  185. package/dist/standalone/preview.d.ts +2 -2
  186. package/dist/standalone/preview.js +39 -12
  187. package/dist/standalone/registry.d.ts +149 -9
  188. package/dist/standalone/registry.js +299 -8
  189. package/dist/standalone/worker.d.ts +19 -5
  190. package/dist/standalone/worker.js +90 -54
  191. package/dist/standalone/workflow-binding.d.ts +28 -32
  192. package/dist/standalone/workflow-binding.js +20 -20
  193. package/dist/standalone/workflow-control.d.ts +4 -0
  194. package/dist/standalone/workflow-control.js +40 -0
  195. package/dist/standalone/workflow-events.d.ts +2 -2
  196. package/dist/standalone/workflow-events.js +9 -10
  197. package/dist/standalone/workflow-git-capture.js +4 -3
  198. package/dist/standalone/workflow-host.d.ts +177 -152
  199. package/dist/standalone/workflow-host.js +137 -30
  200. package/dist/standalone/workflow-node-capture.js +11 -4
  201. package/dist/standalone/workflow-schedules.d.ts +5 -2
  202. package/dist/standalone/workflow-schedules.js +9 -10
  203. package/dist/standalone/workflow-trigger-host.d.ts +88 -6
  204. package/dist/standalone/workflow-trigger-host.js +2 -2
  205. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  206. package/dist/standalone/workflow-webhooks.js +39 -0
  207. package/dist/standalone/workflows.d.ts +203 -50
  208. package/dist/standalone/workflows.js +198 -39
  209. package/dist/storage-frame.d.ts +6 -6
  210. package/dist/testing/capabilities.js +14 -3
  211. package/dist/testing/installation.js +26 -2
  212. package/dist/testing/worker.js +1 -1
  213. package/dist/testing/workers-runtime.d.ts +8 -2
  214. package/dist/testing/workers-runtime.js +20 -2
  215. package/dist/webhook-abuse.d.ts +25 -0
  216. package/dist/webhook-abuse.js +54 -0
  217. package/dist/webhook-custody.d.ts +144 -0
  218. package/dist/webhook-custody.js +222 -0
  219. package/dist/webhook-ingress.d.ts +36 -0
  220. package/dist/webhook-ingress.js +146 -0
  221. package/dist/webhooks.d.ts +111 -0
  222. package/dist/webhooks.js +189 -0
  223. package/dist/workers-git.d.ts +12 -17
  224. package/dist/workers-git.js +60 -55
  225. package/dist/workers.d.ts +224 -13
  226. package/dist/workers.js +351 -51
  227. package/dist/workflow-agent-authority.d.ts +8 -0
  228. package/dist/workflow-agent-authority.js +42 -0
  229. package/dist/workflows.d.ts +66 -0
  230. package/dist/workflows.js +97 -0
  231. package/docs/api/README.md +63 -37
  232. package/docs/api/agents.md +17 -0
  233. package/docs/api/app-storage.md +1 -1
  234. package/docs/api/apps.md +1394 -543
  235. package/docs/api/browser/boot.md +132 -0
  236. package/docs/api/browser.md +1440 -0
  237. package/docs/api/build-contract.md +38 -25
  238. package/docs/api/build.md +263 -84
  239. package/docs/api/cloudflare.md +93 -7
  240. package/docs/api/git.md +3 -13
  241. package/docs/api/host.md +944 -118
  242. package/docs/api/index.md +320 -18
  243. package/docs/api/keys.md +24 -1
  244. package/docs/api/mcp.md +59 -12
  245. package/docs/api/modules.md +33 -40
  246. package/docs/api/pack.md +57 -8
  247. package/docs/api/page/mount.md +92 -4
  248. package/docs/api/workers-git.md +103 -24
  249. package/docs/api/workers.md +1581 -244
  250. package/docs/site/agent-quickstart.mdx +28 -14
  251. package/examples/standalone/worker.ts +3 -1
  252. package/examples/standalone/wrangler.jsonc +2 -0
  253. package/package.json +73 -69
  254. package/dist/installation-consent.d.ts +0 -79
  255. package/dist/installation-consent.js +0 -82
  256. package/dist/shell/app-directory.d.ts +0 -542
  257. package/dist/shell/app-directory.js +0 -199
  258. package/dist/shell/browser-actions.d.ts +0 -7
  259. package/dist/shell/browser-actions.js +0 -56
  260. package/dist/shell/command-host.d.ts +0 -112
  261. package/dist/shell/command-host.js +0 -502
  262. package/dist/shell/commands.d.ts +0 -25
  263. package/dist/shell/commands.js +0 -78
  264. package/dist/shell/index.d.ts +0 -8
  265. package/dist/shell/index.js +0 -8
  266. package/dist/shell/keys.d.ts +0 -41
  267. package/dist/shell/keys.js +0 -134
  268. package/dist/shell/shell-module.d.ts +0 -55
  269. package/dist/shell/shell-module.js +0 -86
  270. package/dist/shell/window-manager.d.ts +0 -124
  271. package/dist/shell/window-manager.js +0 -711
  272. package/dist/shell/workbench-contract.d.ts +0 -68
  273. package/dist/shell/workbench-directory.d.ts +0 -332
  274. package/dist/shell/workbench-directory.js +0 -35
  275. package/dist/shell/workbench-host-types.d.ts +0 -96
  276. package/dist/shell/workbench-host-types.js +0 -1
  277. package/dist/shell/workbench-host.d.ts +0 -34
  278. package/dist/shell/workbench-host.js +0 -347
  279. package/dist/shell/workbench-model.d.ts +0 -192
  280. package/dist/shell/workbench-model.js +0 -53
  281. package/dist/shell/workbench.d.ts +0 -4
  282. package/dist/shell/workbench.js +0 -2
  283. package/docs/api/shell/app-directory.md +0 -599
  284. package/docs/api/shell/command-host.md +0 -224
  285. package/docs/api/shell/commands.md +0 -197
  286. package/docs/api/shell/keys.md +0 -183
  287. package/docs/api/shell/shell-module.md +0 -204
  288. package/docs/api/shell/window-manager.md +0 -267
  289. package/docs/api/shell/workbench-directory.md +0 -119
  290. package/docs/api/shell/workbench.md +0 -205
  291. package/docs/api/shell.md +0 -1915
  292. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -10,13 +10,16 @@ import { createStandaloneWorkflowSchedules, } from "./workflow-schedules.js";
10
10
  import { createStandaloneWorkflowEvents, } from "./workflow-events.js";
11
11
  import { createStandaloneWorkflowTriggerHost, standaloneTriggerErrors, } from "./workflow-trigger-host.js";
12
12
  import { createWorkflowPublishedDefinitionPort, readPublishedWorkflow, } from "@fungi.computer/workflows-wire";
13
+ import { workflowAgentAuthority } from "../workflow-agent-authority.js";
13
14
  import { defaultTenant, tenantScope } from "../tenant.js";
14
15
  import { AppInstanceError } from "../index.js";
15
- import { workflowGitPassPrefix } from "../git.js";
16
16
  import { createWorkflowGitPasses, createWorkflowGitPassStore, workflowGitPassWriter, workflowRunReport, wireRunInitiator, } from "@fungi.computer/braid/runtime";
17
17
  import { canonicalJson } from "@fungi.computer/braid";
18
18
  import { runKey, reviewKey, workflowGitPassRequestSchema, } from "./workflow-binding.js";
19
19
  import { agentDirectoryForScope } from "./agent-directory.js";
20
+ import { createWorkflowWebhooks, workflowWebhookCommandSchema, workflowWebhookControlResponse, } from "../webhooks.js";
21
+ import { workflowWebhookRefusal } from "../webhook-ingress.js";
22
+ import { Hono } from "hono";
20
23
  /** SQLite and the native Workflow binding belong to this standalone host; its root supplies `hostFor`. */
21
24
  export class StandaloneWorkflowRunsBase extends DurableObject {
22
25
  composed;
@@ -37,7 +40,7 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
37
40
  ) STRICT`);
38
41
  this.composed = ctx.blockConcurrencyWhile(async () => {
39
42
  const shared = await standaloneTenancy(env);
40
- const host = this.hostFor(tenantScope(defaultTenant, env.OYSTER_ORIGIN));
43
+ const host = await this.hostFor(tenantScope(defaultTenant, env.OYSTER_ORIGIN));
41
44
  const agentToolPolicy = async (agentId) => (await host.appToolPolicy?.({ kind: "agent", agentId })) ?? null;
42
45
  // A key's App access is its own grants (Ear Tag), read through
43
46
  // `host.keys.live`, never through `appToolPolicy`.
@@ -45,6 +48,15 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
45
48
  // The one owner reaches every Agent; the report needs one that exists.
46
49
  const admitDelivery = (_memberId, delivery) => agentDirectoryForScope(env, tenantScope(defaultTenant, env.OYSTER_ORIGIN)).has(delivery.agentId);
47
50
  const owner = createStandaloneWorkflowHost(ctx.storage, host, {
51
+ async lifecycleMember() {
52
+ const member = await shared.owner();
53
+ return member === null
54
+ ? null
55
+ : {
56
+ memberId: member.actor.memberId,
57
+ membershipVersion: member.membershipVersion,
58
+ };
59
+ },
48
60
  authority(libraryInstallationId) {
49
61
  return createStandaloneWorkflowRootAuthority({
50
62
  shared,
@@ -80,6 +92,7 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
80
92
  sql: ctx.storage.sql,
81
93
  transactionSync: (body) => ctx.storage.transactionSync(body),
82
94
  teamId: () => libraryId,
95
+ now: () => this.scheduleClock.now(),
83
96
  },
84
97
  triggers: {
85
98
  published: createWorkflowPublishedDefinitionPort((git) => ({
@@ -93,10 +106,20 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
93
106
  memberId,
94
107
  });
95
108
  },
96
- // Standalone has no Connector-attachment store (Hub's own
97
- // mechanism for Agent arming); an Agent's owner-approved tool
98
- // policy governs dispatch, not arming here. See W-1/B-4(b).
99
- readAgentAccess: () => Promise.resolve(null),
109
+ readAgentAccess: async (lookup) => {
110
+ if (lookup.kind !== "policy")
111
+ return null;
112
+ const policy = await agentToolPolicy(lookup.agentId);
113
+ return policy === null
114
+ ? null
115
+ : {
116
+ kind: "policy",
117
+ agentId: lookup.agentId,
118
+ policyRevision: policy.revision,
119
+ grantorMemberId: policy.grantedBy,
120
+ level: policy.level,
121
+ };
122
+ },
100
123
  // The member's current Workflows Git policy; mints no pass itself.
101
124
  admitGitAccess: async (memberId, repository, scope) => {
102
125
  if (!(await gitAccess(libraryId)(memberId, repository, scope)))
@@ -122,20 +145,71 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
122
145
  const admitted = await owner.admitInvocation(facts, operation);
123
146
  if (admitted.libraryId !== libraryId)
124
147
  throw new AppInstanceError("revoked");
125
- return admitted;
148
+ return {
149
+ ...admitted,
150
+ authority: await workflowAgentAuthority(facts, operation, agentToolPolicy),
151
+ };
126
152
  },
127
153
  };
128
- return {
129
- schedules: createStandaloneWorkflowSchedules({
130
- ...controls,
154
+ let webhooks = null;
155
+ const wake = async () => {
156
+ const due = [
157
+ schedules.nextWake(),
158
+ webhooks?.nextWake() ?? null,
159
+ ].filter((value) => value !== null);
160
+ if (due.length === 0)
161
+ await ctx.storage.deleteAlarm();
162
+ else
163
+ await ctx.storage.setAlarm(this.scheduleClock.alarmAt(Math.min(...due)));
164
+ };
165
+ const schedules = createStandaloneWorkflowSchedules({
166
+ ...controls,
167
+ now: () => this.scheduleClock.now(),
168
+ wake,
169
+ reportFailure: ({ source, cause }) => {
170
+ console.error("Standalone Workflow schedule failed", source, cause);
171
+ },
172
+ });
173
+ if (env.WORKFLOW_WEBHOOK_HOST_RATE !== undefined &&
174
+ env.WORKFLOW_WEBHOOK_BINDING_RATE !== undefined &&
175
+ env.WORKFLOW_WEBHOOK_POLICY !== undefined)
176
+ webhooks = createWorkflowWebhooks({
177
+ sql: ctx.storage.sql,
178
+ transactionSync: (body) => ctx.storage.transactionSync(body),
131
179
  now: () => this.scheduleClock.now(),
132
- wake: (due) => due === null
133
- ? ctx.storage.deleteAlarm()
134
- : ctx.storage.setAlarm(this.scheduleClock.alarmAt(due)),
135
- reportFailure: ({ source, cause }) => {
136
- console.error("Standalone Workflow schedule failed", source, cause);
180
+ scope: { host: env.OYSTER_ORIGIN, tenant: libraryId },
181
+ owner: async (principal) => {
182
+ const current = await shared.owner();
183
+ return principal.actor.kind === "member" &&
184
+ current?.actor.memberId === principal.actor.memberId
185
+ ? principal.actor.memberId
186
+ : null;
187
+ },
188
+ runs: triggers.runs,
189
+ triggers: triggers.triggers,
190
+ launch: async (run) => {
191
+ const stored = triggers.runs.graph.read(run.runId);
192
+ if (stored === null)
193
+ throw new Error("Reserved Workflow run is missing");
194
+ // Recover the existing run's dispatch context after a crash between
195
+ // durable admission and the standalone adapter's caller write.
196
+ owner.recordTriggerCaller(run.runId, stored.actorId, stored.computerId);
197
+ await this.launch(libraryId, run);
198
+ },
199
+ abuse: {
200
+ hostRate: env.WORKFLOW_WEBHOOK_HOST_RATE,
201
+ bindingRate: env.WORKFLOW_WEBHOOK_BINDING_RATE,
202
+ concurrency: env.WORKFLOW_WEBHOOK_POLICY.concurrency,
203
+ },
204
+ limits: env.WORKFLOW_WEBHOOK_POLICY.limits,
205
+ wake,
206
+ reportFailure: () => {
207
+ console.error("Standalone Workflow webhook failed");
137
208
  },
138
- }),
209
+ });
210
+ return {
211
+ schedules,
212
+ webhooks,
139
213
  events: createStandaloneWorkflowEvents(controls),
140
214
  };
141
215
  };
@@ -151,13 +225,12 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
151
225
  transactionSync: (body) => ctx.storage.transactionSync(body),
152
226
  ownerId: () => this.libraryId(),
153
227
  computerId: () => computerId,
154
- mintPassId: () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => byte.toString(16).padStart(2, "0")).join(""),
155
- remote: (_passId, repository) => {
228
+ remote: (repository) => {
156
229
  const libraryId = this.libraryId();
157
230
  if (repository.kind !== "installation" || libraryId === null)
158
231
  throw new AppInstanceError("revoked");
159
- // The tenant's ordinary smart-HTTP door; the pass never enters the
160
- // URL and travels only as the bearer header.
232
+ // The tenant's ordinary smart-HTTP door; the credential never
233
+ // enters the URL and travels only as the bearer header.
161
234
  return new URL(`/git/${repository.installationId}/${repository.repository}.git`, env.OYSTER_ORIGIN).href;
162
235
  },
163
236
  denied: () => new AppInstanceError("revoked"),
@@ -184,13 +257,6 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
184
257
  if (!(await access(memberId, repository, scope)))
185
258
  throw new AppInstanceError("revoked");
186
259
  },
187
- open: (_computer, grant) => {
188
- const pass = passStore.openWorkflowPass(grant);
189
- return Promise.resolve({
190
- ...pass,
191
- bearer: `${workflowGitPassPrefix}${libraryId}.${pass.passId}`,
192
- });
193
- },
194
260
  revoke: (_computer, input) => {
195
261
  passStore.revokeWorkflowRunGitPasses(input);
196
262
  return Promise.resolve();
@@ -200,8 +266,20 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
200
266
  acknowledge: () => undefined,
201
267
  });
202
268
  };
203
- return { owner, libraryTriggers, passStore, gitAccess, gitPasses };
269
+ return {
270
+ owner,
271
+ libraryTriggers,
272
+ passStore,
273
+ gitAccess,
274
+ gitPasses,
275
+ computerId,
276
+ };
204
277
  });
278
+ ctx.waitUntil(this.composed.then(async ({ libraryTriggers }) => {
279
+ const libraryId = this.libraryId();
280
+ if (libraryId !== null)
281
+ await libraryTriggers(libraryId).webhooks?.recover();
282
+ }));
205
283
  this.owner = this.composed.then(({ owner }) => owner);
206
284
  }
207
285
  /** Record the library this object serves; another name is a revocation. */
@@ -213,11 +291,12 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
213
291
  this.ctx.storage.sql.exec("INSERT INTO standalone_workflow_library(singleton,installation_id) VALUES(1,?)", libraryId);
214
292
  }
215
293
  /**
216
- * Mint (or renew) the Git pass for the Git step a node frame is executing.
217
- * Null when this library dispatched no such step; a step that does not match
218
- * the frame's attested Workflow, member and Computer is a revocation.
294
+ * Admit the Git step a node frame is executing: its public grant, never a
295
+ * credential. Null when this library dispatched no such step; a step that
296
+ * does not match the frame's attested Workflow, member and Computer is a
297
+ * revocation. Only NEW native admission then issues, through issueWorkflowGit.
219
298
  */
220
- async openWorkflowGitPass(raw) {
299
+ async admitWorkflowGit(raw) {
221
300
  const request = workflowGitPassRequestSchema.parse(raw);
222
301
  const { owner, gitPasses } = await this.composed;
223
302
  const libraryId = this.libraryId();
@@ -234,18 +313,37 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
234
313
  canonicalJson(node.context.armedBy) !==
235
314
  canonicalJson(request.workflow.armedBy))
236
315
  throw new AppInstanceError("revoked");
237
- return gitPasses(libraryId).openWorkflowPass(node.context, {
316
+ return gitPasses(libraryId).admitWorkflowGit(node.context, {
238
317
  installationId: node.installationId,
239
318
  method: node.method,
240
319
  });
241
320
  }
242
- /** The tenant Git door's per-request admission of a presented pass. */
243
- async admitWorkflowGitPass(passId, repository, scope) {
244
- const { owner, passStore, gitAccess } = await this.composed;
321
+ /** One fresh credential for a NEW native invocation of an admitted step. */
322
+ async issueWorkflowGit(invocationId, grant) {
323
+ const { passStore } = await this.composed;
324
+ return await passStore.issue(invocationId, grant);
325
+ }
326
+ /** A live native replay extends its lease; it never issues another. */
327
+ async renewWorkflowGit(invocationId, grant) {
328
+ const { passStore } = await this.composed;
329
+ passStore.renew(invocationId, grant);
330
+ }
331
+ /** Close an invocation's credential, even before its issue arrives. */
332
+ async closeWorkflowGit(invocationId, grant) {
333
+ const { passStore } = await this.composed;
334
+ passStore.close(invocationId, grant);
335
+ }
336
+ /** The tenant Git door's per-request admission of a presented bearer. */
337
+ async admitWorkflowGitPass(bearer, repository, scope) {
338
+ const { owner, passStore, gitAccess, computerId } = await this.composed;
245
339
  const libraryId = this.libraryId();
246
- const grant = passStore.admit({ passId, repository, scope });
247
- if (libraryId === null ||
248
- grant === null ||
340
+ const admitted = await passStore.admit({ bearer, repository, scope });
341
+ if (libraryId === null || admitted === null || computerId === undefined)
342
+ return null;
343
+ const { grant, invocationId } = admitted;
344
+ if (
345
+ // The selected Computer proves the credential's native command live.
346
+ !(await this.env.COMPUTER.getByName(computerId).executionIsLive(invocationId)) ||
249
347
  owner.store.outcome(grant.runId) !== "active" ||
250
348
  !(await gitAccess(libraryId)(grant.memberId, repository, scope)))
251
349
  return null;
@@ -347,6 +445,42 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
347
445
  if (libraryId === null)
348
446
  return;
349
447
  await libraryTriggers(libraryId).schedules.alarm();
448
+ await libraryTriggers(libraryId).webhooks?.recover();
449
+ }
450
+ /** Signed traffic uses the native fetch interface; no sender principal is accepted. */
451
+ async fetch(request) {
452
+ return new Hono()
453
+ .all("/webhooks/:bindingId", async (context) => {
454
+ const { libraryTriggers } = await this.composed;
455
+ const libraryId = this.libraryId();
456
+ const webhooks = libraryId === null ? null : libraryTriggers(libraryId).webhooks;
457
+ return webhooks === null
458
+ ? workflowWebhookRefusal()
459
+ : webhooks.receive(context.req.param("bindingId"), context.req.raw);
460
+ })
461
+ .fetch(request);
462
+ }
463
+ async #isWebhookOwner(principal) {
464
+ const shared = await standaloneTenancy(this.env);
465
+ const current = await shared.owner();
466
+ return (principal.actor.kind === "member" &&
467
+ current?.actor.memberId === principal.actor.memberId);
468
+ }
469
+ /** Root-authenticated owner RPC; command data cannot nominate an actor or pin. */
470
+ async webhookControl(principal, installationId, raw) {
471
+ if (!(await this.#isWebhookOwner(principal)))
472
+ return workflowWebhookRefusal();
473
+ const parsed = workflowWebhookCommandSchema.safeParse(raw);
474
+ if (!parsed.success)
475
+ return workflowWebhookRefusal();
476
+ const { owner, libraryTriggers } = await this.composed;
477
+ const webhooks = libraryTriggers(installationId).webhooks;
478
+ if (webhooks === null)
479
+ return workflowWebhookRefusal();
480
+ return workflowWebhookControlResponse(webhooks, principal, parsed.data, async (target) => ({
481
+ target: { ...target, installationId },
482
+ authority: (await owner.triggerAuthority(installationId)).pin,
483
+ }));
350
484
  }
351
485
  /**
352
486
  * The emitting App's Instance delivers each accepted event here. The
@@ -465,6 +599,13 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
465
599
  const run = owner.store.read(runId);
466
600
  if (run?.teamId !== installationId)
467
601
  return { error: "not_found" };
602
+ return this.runStatus(runId);
603
+ }
604
+ async runStatus(runId) {
605
+ const owner = await this.owner;
606
+ const run = owner.store.read(runId);
607
+ if (run === null)
608
+ return null;
468
609
  const outcome = owner.store.outcome(runId);
469
610
  let status;
470
611
  try {
@@ -499,6 +640,24 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
499
640
  const run = owner.store.read(runId);
500
641
  if (run?.teamId !== installationId)
501
642
  return { kind: "refused", reason: "denied" };
643
+ return this.cancelRun(runId);
644
+ }
645
+ /** Host-only MCP control reopens the configured owner's authority on every call. */
646
+ async control(principal, operation, runId) {
647
+ const shared = await standaloneTenancy(this.env);
648
+ const current = await shared.owner();
649
+ if (principal.actor.kind !== "member" ||
650
+ current?.actor.memberId !== principal.actor.memberId)
651
+ throw new AppInstanceError("revoked");
652
+ const owner = await this.owner;
653
+ if (owner.store.read(runId) === null)
654
+ return null;
655
+ return operation === "status"
656
+ ? this.runStatus(runId)
657
+ : this.cancelRun(runId);
658
+ }
659
+ async cancelRun(runId) {
660
+ const owner = await this.owner;
502
661
  if (owner.store.outcome(runId) !== "active")
503
662
  return { kind: "terminal" };
504
663
  const instance = await this.env.GRAPH_WORKFLOWS.get(runId);
@@ -8,7 +8,7 @@ export declare const storageFrameMethods: {
8
8
  readonly kind: "accepted";
9
9
  readonly value: {
10
10
  readonly value: Schema.Json;
11
- readonly source: "team" | "member" | "none";
11
+ readonly source: "member" | "team" | "none";
12
12
  readonly team: {
13
13
  readonly value: Schema.Json;
14
14
  readonly revision: number;
@@ -21,7 +21,7 @@ export declare const storageFrameMethods: {
21
21
  };
22
22
  } | {
23
23
  readonly kind: "rejected";
24
- readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded" | "conflict";
24
+ readonly code: "unavailable" | "invalid" | "conflict" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded";
25
25
  };
26
26
  requiredOperations: () => string[];
27
27
  };
@@ -29,7 +29,7 @@ export declare const storageFrameMethods: {
29
29
  decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [{
30
30
  readonly value: Schema.Json;
31
31
  readonly key: string;
32
- readonly scope: "team" | "member";
32
+ readonly scope: "member" | "team";
33
33
  readonly basedOn: number | null;
34
34
  }];
35
35
  decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
@@ -39,14 +39,14 @@ export declare const storageFrameMethods: {
39
39
  };
40
40
  } | {
41
41
  readonly kind: "rejected";
42
- readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded" | "conflict";
42
+ readonly code: "unavailable" | "invalid" | "conflict" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded";
43
43
  };
44
44
  requiredOperations: () => string[];
45
45
  };
46
46
  reset: {
47
47
  decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [{
48
48
  readonly key: string;
49
- readonly scope: "team" | "member";
49
+ readonly scope: "member" | "team";
50
50
  readonly basedOn: number | null;
51
51
  }];
52
52
  decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
@@ -56,7 +56,7 @@ export declare const storageFrameMethods: {
56
56
  };
57
57
  } | {
58
58
  readonly kind: "rejected";
59
- readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded" | "conflict";
59
+ readonly code: "unavailable" | "invalid" | "conflict" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded";
60
60
  };
61
61
  requiredOperations: () => string[];
62
62
  };
@@ -72,12 +72,20 @@ export async function capabilityFixture(options = {}) {
72
72
  descriptors: capabilities,
73
73
  });
74
74
  if (options.admitted === false)
75
- installation = { ...installation, approvedGrants: [], approval: null };
75
+ installation = {
76
+ ...installation,
77
+ authority: {
78
+ ...installation.authority,
79
+ approvedGrants: [],
80
+ approval: null,
81
+ },
82
+ };
76
83
  // An anonymous reader reaches only a public App, as the host's visitor.
77
84
  if (options.visitor)
78
85
  installation = { ...installation, visibility: "public" };
79
86
  const readOnly = () => Promise.reject(new Error("Fixture installations are read-only"));
80
87
  const base = {
88
+ bootUi: () => Promise.resolve({ kind: "passed" }),
81
89
  auth: {
82
90
  resolve: () => Promise.reject(new Error("Capability fixture resolves no actor")),
83
91
  authenticate: options.authenticate ??
@@ -96,9 +104,9 @@ export async function capabilityFixture(options = {}) {
96
104
  put: () => Promise.reject(new Error("Fixture artifacts are read-only")),
97
105
  },
98
106
  installations: {
107
+ generation: () => Promise.resolve("fixture-generation"),
99
108
  list: () => Promise.resolve([installation]),
100
109
  get: (id) => Promise.resolve(id === installation.installationId ? installation : null),
101
- put: readOnly,
102
110
  compareAndPut: readOnly,
103
111
  },
104
112
  instances: {
@@ -123,7 +131,10 @@ export async function capabilityFixture(options = {}) {
123
131
  installation = {
124
132
  ...installation,
125
133
  release: await release("rel_undeclared", []),
126
- authorityVersion: installation.authorityVersion + 1,
134
+ authority: {
135
+ ...installation.authority,
136
+ authorityVersion: installation.authority.authorityVersion + 1,
137
+ },
127
138
  };
128
139
  },
129
140
  };
@@ -1,3 +1,4 @@
1
+ import { permissionCatalog } from "../apps/catalog.js";
1
2
  import * as Result from "effect/Result";
2
3
  import { applyInstallationCommand, deriveDeclaredOperations, } from "../apps/index.js";
3
4
  /** Build fixture state through the same approval owner used by installation routes. */
@@ -6,7 +7,6 @@ export async function approvedInstallation(input) {
6
7
  kind: "install",
7
8
  release: input.release,
8
9
  releasePin: "pinned",
9
- grants: "declared",
10
10
  }, {
11
11
  by: { kind: "member", memberId: "fixture-owner" },
12
12
  role: "owner",
@@ -21,5 +21,29 @@ export async function approvedInstallation(input) {
21
21
  });
22
22
  if (Result.isFailure(result))
23
23
  throw new Error(result.failure.code);
24
- return { ...result.success.next, visibility: input.visibility ?? "private" };
24
+ const installed = result.success.next;
25
+ const declaredOperations = await deriveDeclaredOperations(input.release, input.declaration, input.descriptors ?? []);
26
+ const changes = declaredOperations.capabilities
27
+ .filter(({ capability }) => permissionCatalog[capability].consent === "ask")
28
+ .map(({ capability, operations }) => ({
29
+ capability,
30
+ grant: { operations, resources: "tenant-scope" },
31
+ }));
32
+ if (changes.length === 0)
33
+ return { ...installed, visibility: input.visibility ?? "private" };
34
+ const approved = applyInstallationCommand(installed, { kind: "change-grants", changes }, {
35
+ by: { kind: "member", memberId: "fixture-owner" },
36
+ role: "owner",
37
+ expectedVersion: installed.authority.authorityVersion,
38
+ declaration: input.declaration,
39
+ declaredOperations,
40
+ now: 1001,
41
+ identity: installed,
42
+ });
43
+ if (Result.isFailure(approved))
44
+ throw new Error(approved.failure.code);
45
+ return {
46
+ ...approved.success.next,
47
+ visibility: input.visibility ?? "private",
48
+ };
25
49
  }
@@ -16,7 +16,7 @@ export class ConfiguredInstance extends DurableObject {
16
16
  this.catalog = createFixtureCatalog(env.CATALOG_FIXTURE);
17
17
  this.gate = createCloudflareInvocationGate(ctx, () => Promise.resolve());
18
18
  void ctx.blockConcurrencyWhile(() => seedFixtureCatalog(env.CATALOG_FIXTURE, this.catalog));
19
- this.runtime = createCloudflareAppInstance(ctx, env.LOADER, (id) => resolveDurableBackend(this.catalog, id).pipe(Effect.mapError((error) => new Error(String(error)))), () => ({}));
19
+ this.runtime = createCloudflareAppInstance(ctx, env.LOADER, (id) => resolveDurableBackend(this.catalog, id).pipe(Effect.mapError((error) => new Error(String(error)))), () => ({}), () => Promise.resolve(null));
20
20
  }
21
21
  async fetch(request) {
22
22
  const url = new URL(request.url);
@@ -1,9 +1,15 @@
1
1
  /**
2
- * Node stand-in for `cloudflare:workers` and `cloudflare:workflows`: only the
3
- * platform constructors are replaced, so adapters and SQLite run unchanged.
2
+ * Node-only stand-in for `cloudflare:workers` and `cloudflare:workflows`. It
3
+ * supplies constructors, an explicit retained-work recorder for tests, and
4
+ * empty binding lookup; no Cloudflare service or lifetime semantics are
5
+ * supplied. Adapters and SQLite run unchanged.
4
6
  * Resolve it with `import.meta.resolve("@fungi.computer/oyster/testing/workers-runtime")`
5
7
  * from a module hook.
6
8
  */
9
+ export declare const env: {};
10
+ /** Node tests retain and explicitly drain the same platform-owned work. */
11
+ export declare function waitUntil(promise: Promise<unknown>): void;
12
+ export declare function drainWaitUntil(): Promise<void>;
7
13
  export declare class DurableObject<Env = unknown> {
8
14
  readonly ctx: DurableObjectState;
9
15
  readonly env: Env;
@@ -1,9 +1,27 @@
1
1
  /**
2
- * Node stand-in for `cloudflare:workers` and `cloudflare:workflows`: only the
3
- * platform constructors are replaced, so adapters and SQLite run unchanged.
2
+ * Node-only stand-in for `cloudflare:workers` and `cloudflare:workflows`. It
3
+ * supplies constructors, an explicit retained-work recorder for tests, and
4
+ * empty binding lookup; no Cloudflare service or lifetime semantics are
5
+ * supplied. Adapters and SQLite run unchanged.
4
6
  * Resolve it with `import.meta.resolve("@fungi.computer/oyster/testing/workers-runtime")`
5
7
  * from a module hook.
6
8
  */
9
+ export const env = {};
10
+ const retainedWork = new Set();
11
+ /** Node tests retain and explicitly drain the same platform-owned work. */
12
+ export function waitUntil(promise) {
13
+ retainedWork.add(Promise.resolve(promise).then(() => ({ kind: "completed" }), (cause) => ({
14
+ kind: "failed",
15
+ causeKind: cause instanceof Error ? cause.name : "non-error rejection",
16
+ })));
17
+ }
18
+ export async function drainWaitUntil() {
19
+ const outcomes = await Promise.all(retainedWork);
20
+ retainedWork.clear();
21
+ const failure = outcomes.find((outcome) => outcome.kind === "failed");
22
+ if (failure?.kind === "failed")
23
+ throw new Error(`Retained Worker work failed: ${failure.causeKind}`);
24
+ }
7
25
  export class DurableObject {
8
26
  ctx;
9
27
  env;
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import type { RateLimit } from "@cloudflare/workers-types";
3
+ declare const webhookConcurrencySchema: z.ZodObject<{
4
+ hostConcurrency: z.ZodNumber;
5
+ bindingConcurrency: z.ZodNumber;
6
+ }, z.core.$strict>;
7
+ /** Native rate bindings own quotas; this adapter never implements a rate clock. */
8
+ export declare function workflowWebhookAbuse(options: {
9
+ scope: Readonly<{
10
+ host: string;
11
+ tenant: string;
12
+ }>;
13
+ hostRate: Pick<RateLimit, "limit">;
14
+ bindingRate: Pick<RateLimit, "limit">;
15
+ concurrency: z.input<typeof webhookConcurrencySchema>;
16
+ }): {
17
+ concurrency: {
18
+ hostConcurrency: number;
19
+ bindingConcurrency: number;
20
+ };
21
+ /** Per-instance streaming bound; durable run concurrency belongs to Braid. */
22
+ hostLease(): Promise<null | (() => void)>;
23
+ bindingAdmission(bindingId: string): Promise<boolean>;
24
+ };
25
+ export {};
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { createHash } from "node:crypto";
3
+ // The maintained edge rate-limit contract bounds keys to 256 characters.
4
+ // Hash the full scoped tuple, rather than truncating identities or depending
5
+ // on a provider to accept an oversized concatenation.
6
+ const rateKey = (parts) => createHash("sha256").update(JSON.stringify(parts)).digest("hex");
7
+ const webhookConcurrencySchema = z.strictObject({
8
+ hostConcurrency: z.number().int().positive(),
9
+ bindingConcurrency: z.number().int().positive(),
10
+ });
11
+ /** Native rate bindings own quotas; this adapter never implements a rate clock. */
12
+ export function workflowWebhookAbuse(options) {
13
+ const scope = z
14
+ .strictObject({
15
+ host: z.string().min(1).max(256),
16
+ tenant: z.string().min(1).max(256),
17
+ })
18
+ .parse(options.scope);
19
+ const concurrency = webhookConcurrencySchema.parse(options.concurrency);
20
+ const prefix = ["workflow-webhooks", scope.host, scope.tenant];
21
+ let active = 0;
22
+ return {
23
+ concurrency,
24
+ /** Per-instance streaming bound; durable run concurrency belongs to Braid. */
25
+ async hostLease() {
26
+ if (active >= concurrency.hostConcurrency)
27
+ return null;
28
+ const { success } = await options.hostRate.limit({
29
+ key: rateKey(["workflow-webhooks", scope.host, "host"]),
30
+ });
31
+ // Native admission awaits I/O; simultaneous successful replies still
32
+ // cannot overbook the local streaming ceiling.
33
+ if (!success || active >= concurrency.hostConcurrency)
34
+ return null;
35
+ active++;
36
+ let released = false;
37
+ return () => {
38
+ if (released)
39
+ return;
40
+ released = true;
41
+ active--;
42
+ };
43
+ },
44
+ async bindingAdmission(bindingId) {
45
+ const id = z.string().min(1).max(256).safeParse(bindingId);
46
+ if (!id.success)
47
+ return false;
48
+ const { success } = await options.bindingRate.limit({
49
+ key: rateKey([...prefix, "binding", id.data]),
50
+ });
51
+ return success;
52
+ },
53
+ };
54
+ }