@fungi.computer/oyster 0.0.0-stage → 0.0.1

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 (242) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1082 -2
  3. package/dist/agents.d.ts +114 -0
  4. package/dist/agents.js +322 -0
  5. package/dist/app-builds.d.ts +151 -0
  6. package/dist/app-builds.js +355 -0
  7. package/dist/app-sandbox.d.ts +2 -0
  8. package/dist/app-sandbox.js +2 -0
  9. package/dist/app-sources.d.ts +46 -0
  10. package/dist/app-sources.js +335 -0
  11. package/dist/app-storage.d.ts +141 -0
  12. package/dist/app-storage.js +281 -0
  13. package/dist/apps/gate.d.ts +105 -0
  14. package/dist/apps/gate.js +273 -0
  15. package/dist/apps/index.d.ts +5 -0
  16. package/dist/apps/index.js +5 -0
  17. package/dist/apps/installation.d.ts +248 -0
  18. package/dist/apps/installation.js +307 -0
  19. package/dist/apps/operations.d.ts +8 -0
  20. package/dist/apps/operations.js +98 -0
  21. package/dist/apps/tools.d.ts +33 -0
  22. package/dist/apps/tools.js +17 -0
  23. package/dist/auth.d.ts +11 -0
  24. package/dist/auth.js +93 -0
  25. package/dist/backend-request.d.ts +3 -0
  26. package/dist/backend-request.js +53 -0
  27. package/dist/build/build-jobs.d.ts +128 -0
  28. package/dist/build/build-jobs.js +197 -0
  29. package/dist/build/build-step.d.ts +26 -0
  30. package/dist/build/build-step.js +20 -0
  31. package/dist/build/bundled-html.d.ts +7 -0
  32. package/dist/build/bundled-html.js +64 -0
  33. package/dist/build/definition-emission.d.ts +34 -0
  34. package/dist/build/definition-emission.js +201 -0
  35. package/dist/build/effect.d.ts +231 -0
  36. package/dist/build/effect.js +396 -0
  37. package/dist/build/git-environment.d.ts +6 -0
  38. package/dist/build/git-environment.js +48 -0
  39. package/dist/build/html-recipe.d.ts +20 -0
  40. package/dist/build/html-recipe.js +51 -0
  41. package/dist/build/local-build.d.ts +57 -0
  42. package/dist/build/local-build.js +1078 -0
  43. package/dist/build/r2-artifact-writer.d.ts +31 -0
  44. package/dist/build/r2-artifact-writer.js +179 -0
  45. package/dist/build/snapshot-package-store.d.ts +2 -0
  46. package/dist/build/snapshot-package-store.js +21 -0
  47. package/dist/build/snapshot-program-scripts.d.ts +6 -0
  48. package/dist/build/snapshot-program-scripts.js +2 -0
  49. package/dist/build/submission-contract.d.ts +384 -0
  50. package/dist/build/submission-contract.js +70 -0
  51. package/dist/build/workspace-build.d.ts +50 -0
  52. package/dist/build/workspace-build.js +156 -0
  53. package/dist/build-contract.d.ts +4 -0
  54. package/dist/build-contract.js +4 -0
  55. package/dist/build-jobs.d.ts +2 -0
  56. package/dist/build-jobs.js +2 -0
  57. package/dist/build.d.ts +11 -0
  58. package/dist/build.js +13 -0
  59. package/dist/capability-descriptor.d.ts +46 -0
  60. package/dist/capability-descriptor.js +51 -0
  61. package/dist/catalog-frame.d.ts +39 -0
  62. package/dist/catalog-frame.js +72 -0
  63. package/dist/cause-class.d.ts +19 -0
  64. package/dist/cause-class.js +54 -0
  65. package/dist/cloudflare.d.ts +123 -0
  66. package/dist/cloudflare.js +391 -0
  67. package/dist/computer-terminal.d.ts +38 -0
  68. package/dist/computer-terminal.js +410 -0
  69. package/dist/computer.d.ts +60 -0
  70. package/dist/computer.js +33 -0
  71. package/dist/event-recipients.d.ts +57 -0
  72. package/dist/event-recipients.js +144 -0
  73. package/dist/facet-invocation.d.ts +4 -0
  74. package/dist/facet-invocation.js +45 -0
  75. package/dist/files.d.ts +42 -0
  76. package/dist/files.js +123 -0
  77. package/dist/frame-binding-wire-script.d.ts +2 -0
  78. package/dist/frame-binding-wire-script.js +1 -0
  79. package/dist/frame-binding-wire.d.ts +28 -0
  80. package/dist/frame-binding-wire.js +54 -0
  81. package/dist/git-admission.d.ts +52 -0
  82. package/dist/git-admission.js +188 -0
  83. package/dist/git-auth.d.ts +11 -0
  84. package/dist/git-auth.js +27 -0
  85. package/dist/git-door.d.ts +35 -0
  86. package/dist/git-door.js +108 -0
  87. package/dist/git-journal.d.ts +31 -0
  88. package/dist/git-journal.js +188 -0
  89. package/dist/git.d.ts +31 -0
  90. package/dist/git.js +64 -0
  91. package/dist/guest-policy.d.ts +9 -0
  92. package/dist/guest-policy.js +13 -0
  93. package/dist/host.d.ts +244 -0
  94. package/dist/host.js +1043 -0
  95. package/dist/index.d.ts +166 -0
  96. package/dist/index.js +320 -0
  97. package/dist/installation-consent.d.ts +79 -0
  98. package/dist/installation-consent.js +82 -0
  99. package/dist/internal/background-origins.d.ts +3 -0
  100. package/dist/internal/background-origins.js +8 -0
  101. package/dist/internal/delivery.d.ts +12 -0
  102. package/dist/internal/delivery.js +167 -0
  103. package/dist/internal/host-gate.d.ts +14 -0
  104. package/dist/internal/host-gate.js +62 -0
  105. package/dist/internal/registry-installations.d.ts +6 -0
  106. package/dist/internal/registry-installations.js +12 -0
  107. package/dist/invocation.d.ts +251 -0
  108. package/dist/invocation.js +377 -0
  109. package/dist/keys.d.ts +48 -0
  110. package/dist/keys.js +52 -0
  111. package/dist/mcp-owner.d.ts +27 -0
  112. package/dist/mcp-owner.js +143 -0
  113. package/dist/mcp.d.ts +89 -0
  114. package/dist/mcp.js +429 -0
  115. package/dist/media.d.ts +23 -0
  116. package/dist/media.js +12 -0
  117. package/dist/pack.d.ts +76 -0
  118. package/dist/pack.js +134 -0
  119. package/dist/page/capabilities.d.ts +23 -0
  120. package/dist/page/capabilities.js +256 -0
  121. package/dist/page/computer-handles.d.ts +79 -0
  122. package/dist/page/computer-handles.js +44 -0
  123. package/dist/page/lifetime.d.ts +6 -0
  124. package/dist/page/lifetime.js +19 -0
  125. package/dist/page/mount.d.ts +163 -0
  126. package/dist/page/mount.js +390 -0
  127. package/dist/page/storage.d.ts +3 -0
  128. package/dist/page/storage.js +82 -0
  129. package/dist/page/terminal.d.ts +5 -0
  130. package/dist/page/terminal.js +7 -0
  131. package/dist/page-script.d.ts +14 -0
  132. package/dist/page-script.js +5 -0
  133. package/dist/r2-files.d.ts +3 -0
  134. package/dist/r2-files.js +79 -0
  135. package/dist/release.d.ts +72 -0
  136. package/dist/release.js +164 -0
  137. package/dist/root-data.d.ts +86 -0
  138. package/dist/root-data.js +69 -0
  139. package/dist/shell/app-directory.d.ts +537 -0
  140. package/dist/shell/app-directory.js +195 -0
  141. package/dist/shell/browser-actions.d.ts +7 -0
  142. package/dist/shell/browser-actions.js +56 -0
  143. package/dist/shell/command-host.d.ts +112 -0
  144. package/dist/shell/command-host.js +502 -0
  145. package/dist/shell/commands.d.ts +25 -0
  146. package/dist/shell/commands.js +78 -0
  147. package/dist/shell/index.d.ts +8 -0
  148. package/dist/shell/index.js +8 -0
  149. package/dist/shell/keys.d.ts +41 -0
  150. package/dist/shell/keys.js +134 -0
  151. package/dist/shell/shell-module.d.ts +31 -0
  152. package/dist/shell/shell-module.js +37 -0
  153. package/dist/shell/window-manager.d.ts +124 -0
  154. package/dist/shell/window-manager.js +711 -0
  155. package/dist/shell/workbench-contract.d.ts +68 -0
  156. package/dist/shell/workbench-contract.js +1 -0
  157. package/dist/shell/workbench-host-types.d.ts +96 -0
  158. package/dist/shell/workbench-host-types.js +1 -0
  159. package/dist/shell/workbench-host.d.ts +34 -0
  160. package/dist/shell/workbench-host.js +347 -0
  161. package/dist/shell/workbench-model.d.ts +192 -0
  162. package/dist/shell/workbench-model.js +53 -0
  163. package/dist/shell/workbench.d.ts +4 -0
  164. package/dist/shell/workbench.js +2 -0
  165. package/dist/source/fork.d.ts +27 -0
  166. package/dist/source/fork.js +46 -0
  167. package/dist/source/index.d.ts +11 -0
  168. package/dist/source/index.js +11 -0
  169. package/dist/source/seed-receipt.d.ts +53 -0
  170. package/dist/source/seed-receipt.js +18 -0
  171. package/dist/source/team-source.d.ts +32 -0
  172. package/dist/source/team-source.js +42 -0
  173. package/dist/source/template-repository.d.ts +2 -0
  174. package/dist/source/template-repository.js +8 -0
  175. package/dist/source/template-write.d.ts +69 -0
  176. package/dist/source/template-write.js +344 -0
  177. package/dist/storage-frame.d.ts +64 -0
  178. package/dist/storage-frame.js +28 -0
  179. package/dist/tenant.d.ts +81 -0
  180. package/dist/tenant.js +71 -0
  181. package/dist/testing/capabilities.d.ts +29 -0
  182. package/dist/testing/capabilities.js +130 -0
  183. package/dist/testing/catalog.d.ts +3 -0
  184. package/dist/testing/catalog.js +53 -0
  185. package/dist/testing/index.d.ts +10 -0
  186. package/dist/testing/index.js +10 -0
  187. package/dist/testing/installation.d.ts +11 -0
  188. package/dist/testing/installation.js +25 -0
  189. package/dist/testing/native-worker.d.ts +21 -0
  190. package/dist/testing/native-worker.js +106 -0
  191. package/dist/testing/worker.d.ts +18 -0
  192. package/dist/testing/worker.js +67 -0
  193. package/dist/testing/workers-runtime.d.ts +24 -0
  194. package/dist/testing/workers-runtime.js +33 -0
  195. package/dist/webhook-delivery.d.ts +14 -0
  196. package/dist/webhook-delivery.js +17 -0
  197. package/dist/workers-git.d.ts +285 -0
  198. package/dist/workers-git.js +1174 -0
  199. package/dist/workers.d.ts +352 -0
  200. package/dist/workers.js +866 -0
  201. package/docs/api/README.md +82 -0
  202. package/docs/api/agents.md +332 -0
  203. package/docs/api/app-storage.md +209 -0
  204. package/docs/api/apps.md +1375 -0
  205. package/docs/api/auth.md +63 -0
  206. package/docs/api/build-contract.md +507 -0
  207. package/docs/api/build.md +1476 -0
  208. package/docs/api/cloudflare.md +421 -0
  209. package/docs/api/computer.md +203 -0
  210. package/docs/api/files.md +117 -0
  211. package/docs/api/git.md +470 -0
  212. package/docs/api/host.md +1328 -0
  213. package/docs/api/index.md +430 -0
  214. package/docs/api/invocation.md +738 -0
  215. package/docs/api/keys.md +175 -0
  216. package/docs/api/mcp.md +301 -0
  217. package/docs/api/media.md +40 -0
  218. package/docs/api/modules.md +42 -0
  219. package/docs/api/pack.md +138 -0
  220. package/docs/api/page/mount.md +298 -0
  221. package/docs/api/root-data.md +111 -0
  222. package/docs/api/shell/app-directory.md +332 -0
  223. package/docs/api/shell/command-host.md +224 -0
  224. package/docs/api/shell/commands.md +197 -0
  225. package/docs/api/shell/keys.md +183 -0
  226. package/docs/api/shell/shell-module.md +143 -0
  227. package/docs/api/shell/window-manager.md +267 -0
  228. package/docs/api/shell/workbench.md +186 -0
  229. package/docs/api/shell.md +1788 -0
  230. package/docs/api/tenant.md +194 -0
  231. package/docs/api/workers-git.md +1527 -0
  232. package/docs/api/workers.md +1678 -0
  233. package/docs/media/architecture.mmd +32 -0
  234. package/docs/media/architecture.png +0 -0
  235. package/docs/media/bonsai-network.png +0 -0
  236. package/docs/media/bonsai-repository.png +0 -0
  237. package/docs/site/agent-quickstart.mdx +349 -0
  238. package/examples/git/worker.ts +39 -0
  239. package/examples/git/wrangler.jsonc +32 -0
  240. package/examples/standalone/worker.ts +14 -0
  241. package/examples/standalone/wrangler.jsonc +21 -0
  242. package/package.json +249 -3
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { type AppToolPolicy } from "./apps/tools.js";
2
+ import type { DeclaredOperations, Installation } from "./apps/installation.js";
3
+ import { type CapabilitySource, type Grants } from "@fungi.computer/mycelium";
4
+ import { appToolSchemas } from "@fungi.computer/forage";
5
+ import type { OysterHost, OysterPrincipal } from "./host.js";
6
+ import type { McpPrincipal, McpCatalogSnapshot } from "@fungi.computer/mycelium/mcp";
7
+ import { type VerifiedRelease } from "./release.js";
8
+ /** An installation ID as a guest binding name: `spawn-git` becomes `SPAWN_GIT`. */
9
+ export declare function appBindingName(installationId: string): string;
10
+ /** Why an App method call from the MCP catalog was refused. */
11
+ export declare class AppMethodRefused extends Error {
12
+ readonly code: string;
13
+ readonly name = "AppMethodRefused";
14
+ /** `detail` is the App's own caller-facing refusal message, when it gave one. */
15
+ constructor(code: string, detail?: string);
16
+ }
17
+ /**
18
+ * The App-tool access one caller's catalog capture used, re-checked at call
19
+ * time against a fresh read. An Agent's policy is one blanket level for
20
+ * every installation (`host.appToolPolicy`); a key's is its own grants,
21
+ * resolved per installation (`keyAppAccess`) -- an installation the key has
22
+ * no grant for is captured as absent, never merely refused, so it is never
23
+ * listed at all.
24
+ */
25
+ export type OysterCallerAccess = Readonly<{
26
+ kind: "agent";
27
+ policy: AppToolPolicy;
28
+ }> | Readonly<{
29
+ kind: "key";
30
+ keyId: string;
31
+ byInstallation: ReadonlyMap<string, AppToolPolicy>;
32
+ }> | null;
33
+ /** What one Release contributes to every MCP catalog: its verified build,
34
+ * its declared operations and its compiled tool schemas. */
35
+ export type ReleaseSurface = Readonly<{
36
+ release: VerifiedRelease;
37
+ api: NonNullable<VerifiedRelease["api"]>;
38
+ declaredOperations: DeclaredOperations;
39
+ toolSchemas: ReturnType<typeof appToolSchemas>;
40
+ }>;
41
+ /**
42
+ * Per-host memo of each installed Release's catalog surface. A Release's
43
+ * artifacts are immutable and content-addressed, so verifying the build,
44
+ * deriving its operations and compiling its tool schemas happens once per
45
+ * Release ID and artifact digest, not on every catalog capture: an MCP call
46
+ * captures the catalog several times (refresh, session acquire, the pre-call
47
+ * authority check), and redoing this for every installed App on each one
48
+ * exhausted a Durable Object's CPU and memory. Only Release-derived facts are
49
+ * cached; installation authority, caller access and tool-family listings
50
+ * are still read fresh on every capture. A failed build is not cached.
51
+ */
52
+ export type AppSurfaceCache = (release: Installation["release"]) => Promise<ReleaseSurface | null>;
53
+ /** Make one {@link AppSurfaceCache} for a host whose artifact store and capabilities stay fixed. */
54
+ export declare function appSurfaceCache(host: Pick<OysterHost, "artifacts" | "capabilities">): AppSurfaceCache;
55
+ /**
56
+ * Capture installed App methods for MCP or agents as one principal-scoped source.
57
+ * Enabled installations become `env.<APP>` modules using the Instance call path; a dotted method such as `repos.list` is `env.<APP>.repos.list()`.
58
+ * Visitors see only public read methods; owners see every discoverable
59
+ * method. A key sees only the installations its own grants admit, each at
60
+ * the level (`read`/`write`) those grants give it; Mycelium itself refuses a
61
+ * write call up front when the module's own grant is `read`
62
+ * (`mycelium/src/access.ts`). Call again to capture changes to Releases,
63
+ * installation authority or the caller's own grants.
64
+ */
65
+ export declare function oysterAppMethods(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "capabilities" | "appToolPolicy" | "keys">, principal: OysterPrincipal, access: OysterCallerAccess, surfaces: AppSurfaceCache, preloadedInstallations?: readonly Installation[]): Promise<CapabilitySource & {
66
+ readonly grants: Grants;
67
+ }>;
68
+ /**
69
+ * Capture the host's fixed Computer for one caller as a Knapsack source.
70
+ * The host decides whether the caller holds Computer access; the Computer
71
+ * owner accepts every call. Null when the host has no Computer or the caller
72
+ * has no access. Grants name exactly the one captured module.
73
+ */
74
+ export declare function oysterComputerTools(host: Pick<OysterHost, "computerId" | "computer">, principal: OysterPrincipal): Promise<(CapabilitySource & {
75
+ readonly grants: Grants;
76
+ }) | null>;
77
+ /**
78
+ * Load the current catalog for an authenticated owner or key: installed App
79
+ * methods, plus the host's Computer when the caller holds Computer access.
80
+ * A key sees exactly the installations and levels its own grants admit, and
81
+ * the Computer only with its own `computer.use` grant; each App retains its
82
+ * own caller checks regardless. The owner's stored write approval of an
83
+ * Agent's App tools is that principal's confirmation for App writes, so a
84
+ * headless client can write; the shared MCP server asks the client to
85
+ * approve every other write (an owner's own calls, the Computer). Each call
86
+ * still re-admits the key, rechecks that same stored approval, and passes
87
+ * the App's gate.
88
+ */
89
+ export declare function oysterMcpCatalogLoad(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "computer" | "capabilities" | "appToolPolicy" | "keys">): (principal: McpPrincipal) => Promise<McpCatalogSnapshot>;
package/dist/mcp.js ADDED
@@ -0,0 +1,429 @@
1
+ import { admits } from "./apps/gate.js";
2
+ import { causeClass } from "./cause-class.js";
3
+ import { deriveDeclaredOperations } from "./apps/operations.js";
4
+ import { appToolPolicySchema } from "./apps/tools.js";
5
+ import { emptyAppDeclaration } from "@fungi.computer/puffball/app";
6
+ import { admittedMethodCallerFacts, invocationCallerContext, } from "./invocation.js";
7
+ import { guestLimits } from "./guest-policy.js";
8
+ import { Mycelium, } from "@fungi.computer/mycelium";
9
+ import { appToolSchemas, appToolFamilySchemas } from "@fungi.computer/forage";
10
+ import { Knapsack } from "@fungi.computer/knapsack";
11
+ import * as Schema from "effect/Schema";
12
+ import { defineIdFamily } from "@fungi.computer/cairn/id";
13
+ import { keyAppAccess } from "./keys.js";
14
+ import { defaultAssetPolicy, sha256, verifyRelease, } from "./release.js";
15
+ /** Mints the requestId a tool family tool sends on the caller's behalf to its
16
+ * declared `call` method: each MCP call is its own independent run, never a
17
+ * replay the caller controls. */
18
+ const toolFamilyCallIds = defineIdFamily({
19
+ family: "oyster-tool-family-call",
20
+ version: 1,
21
+ kinds: { call: { prefix: "call" } },
22
+ });
23
+ /** An installation ID as a guest binding name: `spawn-git` becomes `SPAWN_GIT`. */
24
+ export function appBindingName(installationId) {
25
+ return installationId.replace(/[^A-Za-z0-9]+/gu, "_").toUpperCase();
26
+ }
27
+ /** Why an App method call from the MCP catalog was refused. */
28
+ export class AppMethodRefused extends Error {
29
+ code;
30
+ name = "AppMethodRefused";
31
+ /** `detail` is the App's own caller-facing refusal message, when it gave one. */
32
+ constructor(code, detail) {
33
+ super(detail === undefined
34
+ ? `App method refused: ${code}`
35
+ : `App method refused: ${code}: ${detail}`);
36
+ this.code = code;
37
+ }
38
+ }
39
+ function capturedAccessFor(access, installationId) {
40
+ if (access === null)
41
+ return null;
42
+ return access.kind === "agent"
43
+ ? access.policy
44
+ : (access.byInstallation.get(installationId) ?? null);
45
+ }
46
+ /** Re-read one actor's current App-tool access for one installation, fresh. */
47
+ async function freshAccessFor(host, actor, installationId) {
48
+ if (actor.kind === "agent")
49
+ return (await host.appToolPolicy?.(actor)) ?? null;
50
+ const key = (await host.keys?.live(actor.keyId)) ?? null;
51
+ return key === null ? null : keyAppAccess(key, installationId);
52
+ }
53
+ /** Make one {@link AppSurfaceCache} for a host whose artifact store and capabilities stay fixed. */
54
+ export function appSurfaceCache(host) {
55
+ const surfaces = new Map();
56
+ const build = async (installed) => {
57
+ const release = await verifyRelease(host.artifacts, installed, defaultAssetPolicy, false);
58
+ if (release.api === null)
59
+ return null;
60
+ return {
61
+ release,
62
+ api: release.api,
63
+ declaredOperations: await deriveDeclaredOperations(installed, release.declaration ?? emptyAppDeclaration, host.capabilities ?? []),
64
+ toolSchemas: appToolSchemas(release.api),
65
+ };
66
+ };
67
+ return (installed) => {
68
+ const key = `${installed.releaseId}:${installed.artifact.artifactDigest}`;
69
+ let entry = surfaces.get(key);
70
+ if (entry === undefined) {
71
+ entry = build(installed).catch((cause) => {
72
+ surfaces.delete(key);
73
+ throw cause;
74
+ });
75
+ surfaces.set(key, entry);
76
+ }
77
+ return entry;
78
+ };
79
+ }
80
+ async function appModules(host, surfaces, principal, installations, access, sessionId,
81
+ /** Shares one tool-family listing per Instance across the two
82
+ * `appModules` passes one `oysterAppMethods` call can make (the eager
83
+ * grants/version pass, and a session-scoped re-derivation); never shared
84
+ * across separate calls, so it never serves another request's listing. */
85
+ familyListingCache = new Map()) {
86
+ const limits = guestLimits(host.guestLimits);
87
+ const caller = invocationCallerContext(principal, host.computerId ?? null);
88
+ const { actor } = principal;
89
+ const visitor = actor.kind === "visitor";
90
+ const modules = {};
91
+ const levels = {};
92
+ for (const installation of installations) {
93
+ if (installation.status !== "enabled" || installation.instanceId === null)
94
+ continue;
95
+ if (visitor && installation.visibility !== "public")
96
+ continue;
97
+ const captured = capturedAccessFor(access, installation.installationId);
98
+ // A key that holds no grant for this installation never sees it listed
99
+ // at all. An Agent's one blanket policy either covers every
100
+ // installation or none, so this never hides one from an Agent; an
101
+ // unapproved Agent still sees the list, refused at call time, as today.
102
+ if (access?.kind === "key" && captured === null)
103
+ continue;
104
+ const surface = await surfaces(installation.release);
105
+ if (surface === null)
106
+ continue;
107
+ const { release, api, declaredOperations, toolSchemas } = surface;
108
+ const instanceId = installation.instanceId;
109
+ const declaration = release.declaration ?? emptyAppDeclaration;
110
+ const id = `oyster-app:${installation.installationId}`;
111
+ // A member/owner/visitor, or an Agent/key with no captured access,
112
+ // defaults to "write" here exactly as every module always did before:
113
+ // the deeper gate (`admits`), not this coarse Mycelium-level grant, is
114
+ // what actually enforces visitor/role ceilings. Only a captured
115
+ // Agent/key access with its own level narrows this.
116
+ levels[id] = captured?.level ?? "write";
117
+ // Shared by every operation this installation exposes (ordinary methods
118
+ // and, below, a tool family's dynamic per-row tools): the same staleness
119
+ // check, the same owner-approved policy check, the same `admits` gate and
120
+ // the same Instance call. A tool family tool is sugar over this same
121
+ // admitted call to its declared `call` method; it gets no separate check.
122
+ const callAppMethod = async (name, effect, input, signal) => {
123
+ signal?.throwIfAborted();
124
+ const currentInstallation = await host.installations.get(installation.installationId);
125
+ signal?.throwIfAborted();
126
+ if (currentInstallation?.status !== "enabled" ||
127
+ currentInstallation.instanceId !== instanceId ||
128
+ currentInstallation.authorityVersion !==
129
+ installation.authorityVersion ||
130
+ currentInstallation.release.releaseId !==
131
+ installation.release.releaseId ||
132
+ currentInstallation.release.artifact.artifactDigest !==
133
+ installation.release.artifact.artifactDigest)
134
+ throw new AppMethodRefused("stale_authority");
135
+ if (actor.kind === "agent" || actor.kind === "team-key") {
136
+ const fresh = Schema.decodeUnknownOption(appToolPolicySchema, {
137
+ onExcessProperty: "error",
138
+ })(await freshAccessFor(host, actor, installation.installationId));
139
+ const capturedParsed = Schema.decodeUnknownOption(appToolPolicySchema, {
140
+ onExcessProperty: "error",
141
+ })(captured);
142
+ // A call never pre-approved in the first place (no captured
143
+ // policy) is `not_approved`; one that WAS approved but whose
144
+ // authority has since changed or vanished entirely -- a key
145
+ // revoked or expired after capture, say -- is `stale_authority`,
146
+ // just like the other authority drift below. Only the absence
147
+ // of a captured policy gets the former code.
148
+ if (capturedParsed._tag === "None")
149
+ throw new AppMethodRefused("not_approved");
150
+ if (fresh._tag === "None" ||
151
+ fresh.value.revision !== capturedParsed.value.revision ||
152
+ fresh.value.grantedBy !== capturedParsed.value.grantedBy ||
153
+ fresh.value.level !== capturedParsed.value.level)
154
+ throw new AppMethodRefused("stale_authority");
155
+ }
156
+ signal?.throwIfAborted();
157
+ const json = Schema.decodeUnknownSync(Schema.Json)(input);
158
+ const decision = admits({
159
+ installation,
160
+ declaration,
161
+ declaredOperations,
162
+ api,
163
+ principal: {
164
+ actor,
165
+ role: "role" in principal ? principal.role : null,
166
+ },
167
+ access: captured === null
168
+ ? null
169
+ : {
170
+ level: captured.level,
171
+ revision: captured.revision,
172
+ grantedBy: captured.grantedBy,
173
+ bindingName: appBindingName(installation.installationId),
174
+ },
175
+ }, { kind: "method", name, input: json, via: "mcp" });
176
+ if (decision.kind === "refused")
177
+ throw new AppMethodRefused(decision.code);
178
+ const result = await host.instances.get(instanceId, limits).call(await admittedMethodCallerFacts(host, installation, release, caller, decision.grants, { name, effect }, [], sessionId, actor.kind === "agent" && captured !== null
179
+ ? {
180
+ policyRevision: captured.revision,
181
+ grantorMemberId: captured.grantedBy,
182
+ }
183
+ : undefined), api, name, json);
184
+ if (!result.ok)
185
+ throw new AppMethodRefused(result.code);
186
+ if (result.value.kind === "refused")
187
+ throw new AppMethodRefused(result.value.code, result.value.message);
188
+ return result.value.value;
189
+ };
190
+ const toolOperations = Object.entries(toolSchemas)
191
+ .filter(([, method]) => !visitor || method.effect === "read")
192
+ .map(([name, method]) => {
193
+ const appMethod = Mycelium.operation({
194
+ replay: "unsafe",
195
+ access: method.effect === "read" ? "read" : "write",
196
+ description: `${method.description}\nEffect: ${method.effect}.`,
197
+ input: method.input,
198
+ output: method.output,
199
+ execute: async (input, context) => {
200
+ const value = await callAppMethod(name, method.effect, input, context.signal);
201
+ return method.output.parse(value);
202
+ },
203
+ });
204
+ return [name, appMethod];
205
+ });
206
+ // A tool family publishes a dynamic, signed set of typed tools from one
207
+ // App (e.g. each published Workflow becomes its own tool). forage decodes
208
+ // that listing and compiles its schemas; it supplies no admission or
209
+ // invocation arguments by design, so the host supplies both here, reusing
210
+ // the exact same `callAppMethod` gate as every other method on this App.
211
+ // A visitor who cannot see non-read ordinary methods cannot see these
212
+ // either; a caller the App itself refuses to list for (stale authority,
213
+ // an unapproved App-tool policy, a missing capability grant) sees none.
214
+ const family = api.toolFamily;
215
+ const familyOperations = family === undefined ||
216
+ (visitor && api.methods[family.call]?.effect !== "read")
217
+ ? []
218
+ : await (async () => {
219
+ try {
220
+ let listing = familyListingCache.get(instanceId);
221
+ if (listing === undefined) {
222
+ listing = await callAppMethod(family.list, "read", {});
223
+ familyListingCache.set(instanceId, listing);
224
+ }
225
+ const compiled = appToolFamilySchemas(api, listing);
226
+ if (compiled === null)
227
+ return [];
228
+ return compiled.tools.map((tool) => {
229
+ const operation = Mycelium.operation({
230
+ replay: "unsafe",
231
+ access: compiled.effect === "read" ? "read" : "write",
232
+ description: `${tool.description}\nEffect: ${compiled.effect}.`,
233
+ input: tool.input,
234
+ output: tool.output,
235
+ execute: async (input, context) => {
236
+ // The declaring App owns every field below except
237
+ // `requestId` (this call's own identity) and `input`
238
+ // (the caller's tool-shaped payload): `tool.row.call` is
239
+ // opaque data this host never reads into or defaults.
240
+ // `requestId` is last: a row whose own `call` data named
241
+ // that key would otherwise replay the same run forever.
242
+ const value = await callAppMethod(compiled.call, compiled.effect, {
243
+ ...tool.row.call,
244
+ input,
245
+ requestId: toolFamilyCallIds.call.generate(),
246
+ }, context.signal);
247
+ return tool.output.parse(value);
248
+ },
249
+ });
250
+ return [
251
+ `${compiled.name}.${tool.row.tool}`,
252
+ operation,
253
+ ];
254
+ });
255
+ }
256
+ catch (error) {
257
+ // One App's bad listing -- a refused or throwing Instance
258
+ // call, or a reply forage's signed-listing codec rejects --
259
+ // withholds only this App's family tools. Every other App's
260
+ // ordinary and family operations are built independently and
261
+ // stay unaffected; the whole catalog build never aborts for it.
262
+ // An ordinary closed refusal (the same gate any App method
263
+ // already produces) stays quiet; anything else is reported
264
+ // exactly like any other AppInstance failure.
265
+ if (!(error instanceof AppMethodRefused) ||
266
+ error.code === "failed")
267
+ console.error("AppInstance failure", {
268
+ stage: "tool_family_listing",
269
+ installationId: installation.installationId,
270
+ causeClass: error instanceof AppMethodRefused
271
+ ? "Refused"
272
+ : causeClass(error),
273
+ });
274
+ return [];
275
+ }
276
+ })();
277
+ modules[id] = Mycelium.module({
278
+ id,
279
+ name: appBindingName(installation.installationId),
280
+ description: release.declaration?.displayName ?? installation.installationId,
281
+ operations: Object.fromEntries([...toolOperations, ...familyOperations]),
282
+ });
283
+ }
284
+ return { modules, levels };
285
+ }
286
+ function serializableAccess(access) {
287
+ return access === null || access.kind === "agent"
288
+ ? access
289
+ : {
290
+ kind: access.kind,
291
+ keyId: access.keyId,
292
+ byInstallation: [...access.byInstallation.entries()].sort(([left], [right]) => left.localeCompare(right)),
293
+ };
294
+ }
295
+ /**
296
+ * Capture installed App methods for MCP or agents as one principal-scoped source.
297
+ * Enabled installations become `env.<APP>` modules using the Instance call path; a dotted method such as `repos.list` is `env.<APP>.repos.list()`.
298
+ * Visitors see only public read methods; owners see every discoverable
299
+ * method. A key sees only the installations its own grants admit, each at
300
+ * the level (`read`/`write`) those grants give it; Mycelium itself refuses a
301
+ * write call up front when the module's own grant is `read`
302
+ * (`mycelium/src/access.ts`). Call again to capture changes to Releases,
303
+ * installation authority or the caller's own grants.
304
+ */
305
+ export async function oysterAppMethods(host, principal, access, surfaces, preloadedInstallations) {
306
+ const installations = preloadedInstallations ?? (await host.installations.list());
307
+ // Shared so a session-scoped re-derivation below reuses this call's tool-
308
+ // family listings instead of asking each Instance for them twice.
309
+ const familyListingCache = new Map();
310
+ const { modules: entries, levels } = await appModules(host, surfaces, principal, installations, access, undefined, familyListingCache);
311
+ const modules = Object.values(entries);
312
+ const version = await sha256(new TextEncoder().encode(JSON.stringify({
313
+ access: serializableAccess(access),
314
+ installations: [...installations]
315
+ .sort((a, b) => a.installationId.localeCompare(b.installationId))
316
+ .map((installation) => [
317
+ installation.installationId,
318
+ installation.instanceId,
319
+ installation.release.releaseId,
320
+ installation.release.artifact.artifactDigest,
321
+ installation.authorityVersion,
322
+ installation.status,
323
+ installation.visibility,
324
+ ]),
325
+ })));
326
+ return {
327
+ id: "oyster-apps",
328
+ grants: levels,
329
+ acquire: async ({ signal, session }) => {
330
+ signal.throwIfAborted();
331
+ const acquiredModules = session === undefined
332
+ ? modules
333
+ : Object.values((await appModules(host, surfaces, principal, installations, access, session.sessionId, familyListingCache)).modules);
334
+ signal.throwIfAborted();
335
+ return {
336
+ snapshot: { version, modules: acquiredModules },
337
+ release: () => Promise.resolve(),
338
+ };
339
+ },
340
+ };
341
+ }
342
+ /**
343
+ * Capture the host's fixed Computer for one caller as a Knapsack source.
344
+ * The host decides whether the caller holds Computer access; the Computer
345
+ * owner accepts every call. Null when the host has no Computer or the caller
346
+ * has no access. Grants name exactly the one captured module.
347
+ */
348
+ export async function oysterComputerTools(host, principal) {
349
+ if (host.computerId === undefined || host.computer === undefined)
350
+ return null;
351
+ const computer = await host.computer(principal);
352
+ if (computer === null)
353
+ return null;
354
+ const id = `computer:${host.computerId}`;
355
+ return {
356
+ ...Knapsack.computer({
357
+ id,
358
+ name: "workspace",
359
+ namespace: ["computers"],
360
+ computer,
361
+ }),
362
+ grants: { [id]: "write" },
363
+ };
364
+ }
365
+ /**
366
+ * Load the current catalog for an authenticated owner or key: installed App
367
+ * methods, plus the host's Computer when the caller holds Computer access.
368
+ * A key sees exactly the installations and levels its own grants admit, and
369
+ * the Computer only with its own `computer.use` grant; each App retains its
370
+ * own caller checks regardless. The owner's stored write approval of an
371
+ * Agent's App tools is that principal's confirmation for App writes, so a
372
+ * headless client can write; the shared MCP server asks the client to
373
+ * approve every other write (an owner's own calls, the Computer). Each call
374
+ * still re-admits the key, rechecks that same stored approval, and passes
375
+ * the App's gate.
376
+ */
377
+ export function oysterMcpCatalogLoad(host) {
378
+ const surfaces = appSurfaceCache(host);
379
+ return async (principal) => {
380
+ const { actor } = principal;
381
+ // One read of the installation list serves both a key's grant check
382
+ // below and oysterAppMethods' own capture, instead of two.
383
+ const installations = await host.installations.list();
384
+ let access = null;
385
+ if (actor.kind === "agent") {
386
+ const policy = (await host.appToolPolicy?.(actor)) ?? null;
387
+ access = policy === null ? null : { kind: "agent", policy };
388
+ }
389
+ else if (actor.kind === "team-key") {
390
+ // Always a "key" access, even an empty one for an unknown, revoked,
391
+ // expired or suspended key: that keeps every installation invisible
392
+ // (via appModules' own `access?.kind === "key"` check) rather than
393
+ // falling through to the member/owner default of "write" below.
394
+ const key = (await host.keys?.live(actor.keyId)) ?? null;
395
+ const byInstallation = new Map();
396
+ if (key !== null)
397
+ for (const installation of installations) {
398
+ const level = keyAppAccess(key, installation.installationId);
399
+ if (level !== null)
400
+ byInstallation.set(installation.installationId, level);
401
+ }
402
+ access = { kind: "key", keyId: actor.keyId, byInstallation };
403
+ }
404
+ const apps = await oysterAppMethods(host, principal, access, surfaces, installations);
405
+ // Pre-approval ("never asked") is what a captured Agent/key policy
406
+ // itself grants; an owner's own calls, or an Agent/key with none, are
407
+ // never pre-approved even though their modules still default to the
408
+ // coarse "write" ceiling above.
409
+ const approvedWrites = access === null
410
+ ? []
411
+ : Object.entries(apps.grants)
412
+ .filter(([, level]) => level === "write")
413
+ .map(([id]) => id);
414
+ const computer = await oysterComputerTools(host, principal);
415
+ if (computer === null)
416
+ return {
417
+ instructions: "Installed Apps on this Oyster. Each App is env.<APP>; dotted method names are nested properties, e.g. env.BONSAI.repos.list({}). Use forage.search / forage.describe to find methods.",
418
+ grants: apps.grants,
419
+ approvedWrites,
420
+ sources: [apps],
421
+ };
422
+ return {
423
+ instructions: 'Installed Apps and this Oyster\'s Computer. Each App is env.<APP>; dotted method names are nested properties, e.g. env.BONSAI.repos.list({}). The Computer is env.computers.workspace, e.g. env.computers.workspace.exec({command: "ls"}). Use forage.search / forage.describe to find methods.',
424
+ grants: { ...apps.grants, ...computer.grants },
425
+ approvedWrites,
426
+ sources: [apps, computer],
427
+ };
428
+ };
429
+ }
@@ -0,0 +1,23 @@
1
+ /** Media slot supplied by the host; Oyster owns routing, never generation. */
2
+ export declare const mediaSlot: {
3
+ readonly id: "media.generation";
4
+ readonly version: number;
5
+ readonly base: "/media/";
6
+ readonly methods: {
7
+ readonly describe: {
8
+ readonly kind: "json";
9
+ readonly method: "GET";
10
+ readonly path: "describe";
11
+ };
12
+ readonly generate: {
13
+ readonly kind: "json";
14
+ readonly method: "POST";
15
+ readonly path: "generate";
16
+ };
17
+ readonly cancelAttempt: {
18
+ readonly kind: "json";
19
+ readonly method: "POST";
20
+ readonly path: "cancel-attempt";
21
+ };
22
+ };
23
+ };
package/dist/media.js ADDED
@@ -0,0 +1,12 @@
1
+ import { mediaGeneration } from "@fungi.computer/agar/media";
2
+ /** Media slot supplied by the host; Oyster owns routing, never generation. */
3
+ export const mediaSlot = {
4
+ id: "media.generation",
5
+ version: mediaGeneration.version,
6
+ base: "/media/",
7
+ methods: {
8
+ describe: { kind: "json", method: "GET", path: "describe" },
9
+ generate: { kind: "json", method: "POST", path: "generate" },
10
+ cancelAttempt: { kind: "json", method: "POST", path: "cancel-attempt" },
11
+ },
12
+ };
package/dist/pack.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { type ArtifactRef, type Release } from "@fungi.computer/puffball";
2
+ import { type AppPublisherCoreConfigInput } from "./build.js";
3
+ /** One committed repository and its canonical App build recipe. */
4
+ export type PackOptions = Readonly<{
5
+ /** The App's ID, `app_<name>`. */
6
+ appId: string;
7
+ /** The Release ID; defaults to `rel_<commit prefix>`. */
8
+ releaseId?: string;
9
+ /** Repository root; all recipe paths are relative to this captured root. */
10
+ sourceDirectory: string;
11
+ /** Exact committed source, `git:<full commit SHA>`; dirty files are not inputs. */
12
+ sourceRevision: string;
13
+ /** The same pinned recipe input accepted by `oyster/build`. */
14
+ recipe: AppPublisherCoreConfigInput["recipe"];
15
+ /** Optional local pnpm content store used by the captured source compiler. */
16
+ packageStoreDirectory?: string;
17
+ /** Where to write `release.json` and the immutable `artifacts/` directory. */
18
+ outDirectory: string;
19
+ }>;
20
+ /** A packed Release on disk. */
21
+ export type PackedRelease = Readonly<{
22
+ release: Release;
23
+ /** Every artifact file by reference. */
24
+ artifacts: readonly Readonly<{
25
+ artifactRef: ArtifactRef;
26
+ path: string;
27
+ }>[];
28
+ }>;
29
+ /** Build an exact local Git commit with the canonical compiler and artifact writer. */
30
+ export declare function packRelease(options: PackOptions): Promise<PackedRelease>;
31
+ /**
32
+ * Capture an Artifacts Git commit, build its Release, upload it to an Oyster,
33
+ * and install it, or switch an existing installation to it
34
+ * ({@link installRelease}).
35
+ */
36
+ export declare function installFromSource(input: Readonly<{
37
+ appId: string;
38
+ releaseId?: string;
39
+ recipe: PackOptions["recipe"];
40
+ /** Credential-free trusted Artifacts Git URL and its exact committed source. */
41
+ source: Readonly<{
42
+ remote: string;
43
+ token: string;
44
+ sourceRevision: string;
45
+ }>;
46
+ packageStoreDirectory?: string;
47
+ /** Destination Oyster origin. */
48
+ url: string;
49
+ /** Destination Oyster owner key; distinct from the source Git credential. */
50
+ key: string;
51
+ installationId: string;
52
+ }>): Promise<Response>;
53
+ /**
54
+ * Upload a packed Release to an Oyster and install it. The host installs a
55
+ * new installation (201) or switches an existing one to this Release (200),
56
+ * keeping its state; both go through its one install-or-select owner.
57
+ */
58
+ export declare function installRelease(input: Readonly<{
59
+ directory: string;
60
+ url: string;
61
+ /** The Oyster owner key. */
62
+ key: string;
63
+ installationId?: string;
64
+ }>): Promise<Response>;
65
+ /** Project one exact first-party commit's editable source and seed it into
66
+ * an Oyster's own template, so `APPS.fork` can later fork it. The operator
67
+ * owner key authenticates; no provider credential ever leaves this call. */
68
+ export declare function publishSource(input: Readonly<{
69
+ appId: string;
70
+ entry: string;
71
+ repositoryPath: string;
72
+ sourceRevision: string;
73
+ recipe: PackOptions["recipe"];
74
+ url: string;
75
+ key: string;
76
+ }>): Promise<Response>;