@fungi.computer/oyster 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +1134 -2
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +322 -0
- package/dist/app-builds.d.ts +210 -0
- package/dist/app-builds.js +499 -0
- package/dist/app-sandbox.d.ts +2 -0
- package/dist/app-sandbox.js +2 -0
- package/dist/app-sources.d.ts +31 -0
- package/dist/app-sources.js +370 -0
- package/dist/app-storage.d.ts +141 -0
- package/dist/app-storage.js +281 -0
- package/dist/apps/gate.d.ts +105 -0
- package/dist/apps/gate.js +273 -0
- package/dist/apps/index.d.ts +5 -0
- package/dist/apps/index.js +5 -0
- package/dist/apps/installation.d.ts +248 -0
- package/dist/apps/installation.js +307 -0
- package/dist/apps/operations.d.ts +8 -0
- package/dist/apps/operations.js +98 -0
- package/dist/apps/tools.d.ts +33 -0
- package/dist/apps/tools.js +17 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.js +93 -0
- package/dist/backend-request.d.ts +3 -0
- package/dist/backend-request.js +53 -0
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.d.ts +128 -0
- package/dist/build/build-jobs.js +203 -0
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +37 -0
- package/dist/build/build-step.js +28 -0
- package/dist/build/bundled-html.d.ts +7 -0
- package/dist/build/bundled-html.js +64 -0
- package/dist/build/candidate-builder.d.ts +68 -0
- package/dist/build/candidate-builder.js +91 -0
- package/dist/build/candidate-cli.d.ts +1 -0
- package/dist/build/candidate-cli.js +119 -0
- package/dist/build/candidate-cli.mjs +28170 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/definition-emission.d.ts +34 -0
- package/dist/build/definition-emission.js +201 -0
- package/dist/build/effect.d.ts +249 -0
- package/dist/build/effect.js +436 -0
- package/dist/build/git-environment.d.ts +9 -0
- package/dist/build/git-environment.js +60 -0
- package/dist/build/html-recipe.d.ts +20 -0
- package/dist/build/html-recipe.js +51 -0
- package/dist/build/local-build.d.ts +67 -0
- package/dist/build/local-build.js +1128 -0
- package/dist/build/snapshot-package-store.d.ts +12 -0
- package/dist/build/snapshot-package-store.js +36 -0
- package/dist/build/snapshot-program-scripts.d.ts +6 -0
- package/dist/build/snapshot-program-scripts.js +2 -0
- package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
- package/dist/build/snapshot-programs/snapshot-path.js +6 -0
- package/dist/build/submission-contract.d.ts +828 -0
- package/dist/build/submission-contract.js +180 -0
- package/dist/build/workspace-build.d.ts +55 -0
- package/dist/build/workspace-build.js +200 -0
- package/dist/build-contract.d.ts +7 -0
- package/dist/build-contract.js +7 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build-jobs.d.ts +2 -0
- package/dist/build-jobs.js +2 -0
- package/dist/build.d.ts +12 -0
- package/dist/build.js +14 -0
- package/dist/capability-descriptor.d.ts +46 -0
- package/dist/capability-descriptor.js +51 -0
- package/dist/catalog-frame.d.ts +39 -0
- package/dist/catalog-frame.js +72 -0
- package/dist/cause-class.d.ts +19 -0
- package/dist/cause-class.js +54 -0
- package/dist/cloudflare.d.ts +123 -0
- package/dist/cloudflare.js +391 -0
- package/dist/computer-terminal.d.ts +38 -0
- package/dist/computer-terminal.js +410 -0
- package/dist/computer.d.ts +60 -0
- package/dist/computer.js +33 -0
- package/dist/event-recipients.d.ts +57 -0
- package/dist/event-recipients.js +144 -0
- package/dist/facet-invocation.d.ts +4 -0
- package/dist/facet-invocation.js +45 -0
- package/dist/files.d.ts +42 -0
- package/dist/files.js +123 -0
- package/dist/frame-binding-wire-script.d.ts +2 -0
- package/dist/frame-binding-wire-script.js +1 -0
- package/dist/frame-binding-wire.d.ts +28 -0
- package/dist/frame-binding-wire.js +54 -0
- package/dist/git-admission.d.ts +52 -0
- package/dist/git-admission.js +188 -0
- package/dist/git-auth.d.ts +11 -0
- package/dist/git-auth.js +27 -0
- package/dist/git-door.d.ts +35 -0
- package/dist/git-door.js +108 -0
- package/dist/git-journal.d.ts +31 -0
- package/dist/git-journal.js +188 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.js +64 -0
- package/dist/guest-policy.d.ts +9 -0
- package/dist/guest-policy.js +13 -0
- package/dist/host.d.ts +244 -0
- package/dist/host.js +1043 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +320 -0
- package/dist/installation-consent.d.ts +79 -0
- package/dist/installation-consent.js +82 -0
- package/dist/internal/background-origins.d.ts +3 -0
- package/dist/internal/background-origins.js +8 -0
- package/dist/internal/delivery.d.ts +12 -0
- package/dist/internal/delivery.js +167 -0
- package/dist/internal/host-gate.d.ts +14 -0
- package/dist/internal/host-gate.js +62 -0
- package/dist/internal/registry-installations.d.ts +6 -0
- package/dist/internal/registry-installations.js +12 -0
- package/dist/invocation.d.ts +251 -0
- package/dist/invocation.js +377 -0
- package/dist/keys.d.ts +48 -0
- package/dist/keys.js +52 -0
- package/dist/mcp-owner.d.ts +27 -0
- package/dist/mcp-owner.js +155 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +429 -0
- package/dist/media.d.ts +23 -0
- package/dist/media.js +12 -0
- package/dist/pack.d.ts +79 -0
- package/dist/pack.js +151 -0
- package/dist/page/capabilities.d.ts +23 -0
- package/dist/page/capabilities.js +256 -0
- package/dist/page/computer-handles.d.ts +79 -0
- package/dist/page/computer-handles.js +44 -0
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/lifetime.d.ts +6 -0
- package/dist/page/lifetime.js +19 -0
- package/dist/page/mount.d.ts +163 -0
- package/dist/page/mount.js +390 -0
- package/dist/page/storage.d.ts +3 -0
- package/dist/page/storage.js +82 -0
- package/dist/page/terminal.d.ts +5 -0
- package/dist/page/terminal.js +7 -0
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.d.ts +14 -0
- package/dist/page-script.js +5 -0
- package/dist/r2-files.d.ts +3 -0
- package/dist/r2-files.js +79 -0
- package/dist/release.d.ts +83 -0
- package/dist/release.js +167 -0
- package/dist/root-data.d.ts +86 -0
- package/dist/root-data.js +69 -0
- package/dist/shell/app-directory.d.ts +542 -0
- package/dist/shell/app-directory.js +199 -0
- package/dist/shell/browser-actions.d.ts +7 -0
- package/dist/shell/browser-actions.js +56 -0
- package/dist/shell/command-host.d.ts +112 -0
- package/dist/shell/command-host.js +502 -0
- package/dist/shell/commands.d.ts +25 -0
- package/dist/shell/commands.js +78 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +8 -0
- package/dist/shell/keys.d.ts +41 -0
- package/dist/shell/keys.js +134 -0
- package/dist/shell/shell-module.d.ts +55 -0
- package/dist/shell/shell-module.js +86 -0
- package/dist/shell/window-manager.d.ts +124 -0
- package/dist/shell/window-manager.js +711 -0
- package/dist/shell/workbench-contract.d.ts +68 -0
- package/dist/shell/workbench-contract.js +1 -0
- package/dist/shell/workbench-directory.d.ts +332 -0
- package/dist/shell/workbench-directory.js +35 -0
- package/dist/shell/workbench-host-types.d.ts +96 -0
- package/dist/shell/workbench-host-types.js +1 -0
- package/dist/shell/workbench-host.d.ts +34 -0
- package/dist/shell/workbench-host.js +347 -0
- package/dist/shell/workbench-model.d.ts +192 -0
- package/dist/shell/workbench-model.js +53 -0
- package/dist/shell/workbench.d.ts +4 -0
- package/dist/shell/workbench.js +2 -0
- package/dist/source/fork.d.ts +27 -0
- package/dist/source/fork.js +46 -0
- package/dist/source/index.d.ts +10 -0
- package/dist/source/index.js +10 -0
- package/dist/source/seed-receipt.d.ts +53 -0
- package/dist/source/seed-receipt.js +18 -0
- package/dist/source/team-source.d.ts +32 -0
- package/dist/source/team-source.js +42 -0
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +4 -0
- package/dist/source/template-repository.js +21 -0
- package/dist/source/template-seed.d.ts +68 -0
- package/dist/source/template-seed.js +80 -0
- package/dist/standalone/agent-directory.d.ts +4 -0
- package/dist/standalone/agent-directory.js +4 -0
- package/dist/standalone/agents.d.ts +220 -0
- package/dist/standalone/agents.js +795 -0
- package/dist/standalone/app-builds-binding.d.ts +36 -0
- package/dist/standalone/app-builds-binding.js +119 -0
- package/dist/standalone/app-set.d.ts +91 -0
- package/dist/standalone/app-set.js +395 -0
- package/dist/standalone/app-source.d.ts +41 -0
- package/dist/standalone/app-source.js +59 -0
- package/dist/standalone/builds.d.ts +18 -0
- package/dist/standalone/builds.js +101 -0
- package/dist/standalone/computer-egress.d.ts +37 -0
- package/dist/standalone/computer-egress.js +111 -0
- package/dist/standalone/computer.d.ts +122 -0
- package/dist/standalone/computer.js +714 -0
- package/dist/standalone/install-apps-cli.d.ts +2 -0
- package/dist/standalone/install-apps-cli.js +126 -0
- package/dist/standalone/instance.d.ts +35 -0
- package/dist/standalone/instance.js +74 -0
- package/dist/standalone/media.d.ts +76 -0
- package/dist/standalone/media.js +227 -0
- package/dist/standalone/preview.d.ts +51 -0
- package/dist/standalone/preview.js +167 -0
- package/dist/standalone/registry.d.ts +102 -0
- package/dist/standalone/registry.js +193 -0
- package/dist/standalone/tenancy.d.ts +42 -0
- package/dist/standalone/tenancy.js +241 -0
- package/dist/standalone/worker.d.ts +55 -0
- package/dist/standalone/worker.js +147 -0
- package/dist/standalone/workflow-authority-readers.d.ts +16 -0
- package/dist/standalone/workflow-authority-readers.js +35 -0
- package/dist/standalone/workflow-binding.d.ts +216 -0
- package/dist/standalone/workflow-binding.js +217 -0
- package/dist/standalone/workflow-events.d.ts +46 -0
- package/dist/standalone/workflow-events.js +39 -0
- package/dist/standalone/workflow-git-capture.d.ts +7 -0
- package/dist/standalone/workflow-git-capture.js +62 -0
- package/dist/standalone/workflow-host.d.ts +931 -0
- package/dist/standalone/workflow-host.js +751 -0
- package/dist/standalone/workflow-node-capture.d.ts +13 -0
- package/dist/standalone/workflow-node-capture.js +72 -0
- package/dist/standalone/workflow-root-authority.d.ts +27 -0
- package/dist/standalone/workflow-root-authority.js +87 -0
- package/dist/standalone/workflow-schedules.d.ts +52 -0
- package/dist/standalone/workflow-schedules.js +40 -0
- package/dist/standalone/workflow-trigger-host.d.ts +1694 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +657 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/storage-frame.d.ts +64 -0
- package/dist/storage-frame.js +28 -0
- package/dist/tenancy/auth0.d.ts +64 -0
- package/dist/tenancy/auth0.js +198 -0
- package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
- package/dist/tenancy/cloudflare/rate-limit.js +19 -0
- package/dist/tenancy/cloudflare/registry.d.ts +50 -0
- package/dist/tenancy/cloudflare/registry.js +301 -0
- package/dist/tenancy/cloudflare.d.ts +2 -0
- package/dist/tenancy/cloudflare.js +2 -0
- package/dist/tenancy/cold-directory.d.ts +13 -0
- package/dist/tenancy/cold-directory.js +43 -0
- package/dist/tenancy/contracts.d.ts +195 -0
- package/dist/tenancy/contracts.js +1 -0
- package/dist/tenancy/index.d.ts +19 -0
- package/dist/tenancy/index.js +156 -0
- package/dist/tenancy/owner-key.d.ts +26 -0
- package/dist/tenancy/owner-key.js +63 -0
- package/dist/tenancy/oyster.d.ts +4 -0
- package/dist/tenancy/oyster.js +25 -0
- package/dist/tenancy/registry-record.d.ts +7 -0
- package/dist/tenancy/registry-record.js +44 -0
- package/dist/tenancy/registry.d.ts +27 -0
- package/dist/tenancy/registry.js +138 -0
- package/dist/tenancy/resolve.d.ts +7 -0
- package/dist/tenancy/resolve.js +64 -0
- package/dist/tenant.d.ts +81 -0
- package/dist/tenant.js +71 -0
- package/dist/testing/capabilities.d.ts +29 -0
- package/dist/testing/capabilities.js +130 -0
- package/dist/testing/catalog.d.ts +3 -0
- package/dist/testing/catalog.js +53 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/installation.d.ts +11 -0
- package/dist/testing/installation.js +25 -0
- package/dist/testing/native-worker.d.ts +21 -0
- package/dist/testing/native-worker.js +106 -0
- package/dist/testing/worker.d.ts +18 -0
- package/dist/testing/worker.js +67 -0
- package/dist/testing/workers-runtime.d.ts +24 -0
- package/dist/testing/workers-runtime.js +33 -0
- package/dist/webhook-delivery.d.ts +14 -0
- package/dist/webhook-delivery.js +17 -0
- package/dist/workers-git.d.ts +285 -0
- package/dist/workers-git.js +1174 -0
- package/dist/workers.d.ts +352 -0
- package/dist/workers.js +868 -0
- package/docs/api/README.md +82 -0
- package/docs/api/agents.md +332 -0
- package/docs/api/app-storage.md +209 -0
- package/docs/api/apps.md +1375 -0
- package/docs/api/auth.md +63 -0
- package/docs/api/build-contract.md +1304 -0
- package/docs/api/build.md +1712 -0
- package/docs/api/cloudflare.md +421 -0
- package/docs/api/computer.md +203 -0
- package/docs/api/files.md +117 -0
- package/docs/api/git.md +470 -0
- package/docs/api/host.md +1328 -0
- package/docs/api/index.md +430 -0
- package/docs/api/invocation.md +738 -0
- package/docs/api/keys.md +175 -0
- package/docs/api/mcp.md +301 -0
- package/docs/api/media.md +40 -0
- package/docs/api/modules.md +50 -0
- package/docs/api/pack.md +141 -0
- package/docs/api/page/mount.md +298 -0
- package/docs/api/root-data.md +111 -0
- package/docs/api/shell/app-directory.md +599 -0
- package/docs/api/shell/command-host.md +224 -0
- package/docs/api/shell/commands.md +197 -0
- package/docs/api/shell/keys.md +183 -0
- package/docs/api/shell/shell-module.md +204 -0
- package/docs/api/shell/window-manager.md +267 -0
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +205 -0
- package/docs/api/shell.md +1915 -0
- package/docs/api/tenancy/auth0.md +110 -0
- package/docs/api/tenancy/cloudflare.md +221 -0
- package/docs/api/tenancy/cold-directory.md +46 -0
- package/docs/api/tenancy/owner-key.md +63 -0
- package/docs/api/tenancy/registry.md +77 -0
- package/docs/api/tenancy/resolve.md +69 -0
- package/docs/api/tenancy.md +430 -0
- package/docs/api/tenant.md +194 -0
- package/docs/api/workers-git.md +1646 -0
- package/docs/api/workers.md +1691 -0
- package/docs/site/agent-quickstart.mdx +346 -0
- package/examples/standalone/worker.ts +7 -0
- package/examples/standalone/wrangler.jsonc +82 -0
- package/package.json +300 -3
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import { standaloneTenancy, } from "./tenancy.js";
|
|
2
|
+
import { AcceptedSchema } from "@fungi.computer/shiitake/client";
|
|
3
|
+
import { memberInboxItemSchema } from "@fungi.computer/puffball/inbox";
|
|
4
|
+
/**
|
|
5
|
+
* Agents on a standalone Oyster Worker. Each agent is Shiitake's public
|
|
6
|
+
* Cloudflare host (`ShiitakeDO`), one Durable Object per agent; a directory
|
|
7
|
+
* Durable Object names them and keeps the member's visits. The model comes
|
|
8
|
+
* from a pluggable `ModelSource`; the default runs Workers AI through the
|
|
9
|
+
* Worker's AI binding, with no API token.
|
|
10
|
+
*/
|
|
11
|
+
import { agentDirectoryForScope } from "./agent-directory.js";
|
|
12
|
+
import { DurableObject } from "cloudflare:workers";
|
|
13
|
+
import { ShiitakeDO } from "@fungi.computer/shiitake/cloudflare";
|
|
14
|
+
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
|
|
15
|
+
import { configuredProviderEgress, providerFetch, } from "@fungi.computer/veil/gatekeeper";
|
|
16
|
+
import { CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL, createAiBindingFetch, } from "@earendil-works/pi-ai/api/cloudflare-ai-binding";
|
|
17
|
+
import { cloudflareWorkersAIProvider } from "@earendil-works/pi-ai/providers/cloudflare-workers-ai";
|
|
18
|
+
import * as Schema from "effect/Schema";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import {} from "../cloudflare.js";
|
|
21
|
+
import { AppInstanceError } from "../index.js";
|
|
22
|
+
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
23
|
+
import { agentIds, createAgentDirectory, createFleetSessions, DirectoryError, } from "@fungi.computer/shiitake/fleet";
|
|
24
|
+
import { appSurfaceCache, oysterAppMethods, } from "../mcp.js";
|
|
25
|
+
import { appToolPolicySchema } from "../apps/index.js";
|
|
26
|
+
import { oysterPrincipalIsOwner, oysterPrincipalId, } from "../host.js";
|
|
27
|
+
import { defaultTenant, tenantIdSchema, tenantScope, } from "../tenant.js";
|
|
28
|
+
import { createCodemodeSandbox } from "@fungi.computer/mycelium/workers";
|
|
29
|
+
import { Knapsack } from "@fungi.computer/knapsack";
|
|
30
|
+
import { maxAgentMcpServers, } from "../agents.js";
|
|
31
|
+
/** Default Workers AI chat model: one that reliably calls tools. */
|
|
32
|
+
const defaultAgentModel = "@cf/moonshotai/kimi-k2.6";
|
|
33
|
+
/**
|
|
34
|
+
* Workers AI over the AI binding. Requests go through AI Gateway's
|
|
35
|
+
* `workers-ai` passthrough on the binding, so no API token is needed.
|
|
36
|
+
*/
|
|
37
|
+
function workersAiModels(ai, options = {}) {
|
|
38
|
+
const id = options.model ?? defaultAgentModel;
|
|
39
|
+
const listed = cloudflareWorkersAIProvider()
|
|
40
|
+
.getModels()
|
|
41
|
+
.find((model) => model.id === id);
|
|
42
|
+
if (listed === undefined)
|
|
43
|
+
throw new Error(`Workers AI has no chat model ${id}`);
|
|
44
|
+
const model = {
|
|
45
|
+
...listed,
|
|
46
|
+
baseUrl: `https://workers-binding.ai/ai-gateway/gateways/${options.gateway ?? "default"}/workers-ai/v1`,
|
|
47
|
+
};
|
|
48
|
+
const fetch = createAiBindingFetch(ai);
|
|
49
|
+
return {
|
|
50
|
+
acquire: async () => {
|
|
51
|
+
const streamSimple = await loadStreamSimple();
|
|
52
|
+
return {
|
|
53
|
+
model,
|
|
54
|
+
stream: (selected, context, streamOptions) => streamSimple(selected, context, {
|
|
55
|
+
...streamOptions,
|
|
56
|
+
fetch,
|
|
57
|
+
apiKey: CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL,
|
|
58
|
+
headers: {
|
|
59
|
+
"cf-aig-authorization": `Bearer ${CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL}`,
|
|
60
|
+
// The binding carries identity; a request auth header would be
|
|
61
|
+
// taken as a provider key that overrides the gateway's own.
|
|
62
|
+
Authorization: null,
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
release: () => Promise.resolve(),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Pi's dispatching `streamSimple` evaluates every provider's model catalog when
|
|
72
|
+
* it loads; only a run needs it, so acquisition loads it instead of startup.
|
|
73
|
+
*/
|
|
74
|
+
const loadStreamSimple = async () => (await import("@earendil-works/pi-ai/compat")).streamSimple;
|
|
75
|
+
const policyDirectorySchema = Schema.Struct({
|
|
76
|
+
directoryName: Schema.NonEmptyString,
|
|
77
|
+
tenantId: tenantIdSchema,
|
|
78
|
+
});
|
|
79
|
+
/** This Agent's remote MCP servers; the bearer stays in this Durable Object. */
|
|
80
|
+
const storedMcpServersSchema = Schema.Array(Schema.Struct({
|
|
81
|
+
serverId: Schema.NonEmptyString,
|
|
82
|
+
name: Schema.NonEmptyString,
|
|
83
|
+
url: Schema.NonEmptyString,
|
|
84
|
+
bearer: Schema.NullOr(Schema.NonEmptyString),
|
|
85
|
+
}));
|
|
86
|
+
const mcpServersKey = "agent-mcp-servers";
|
|
87
|
+
const mcpServerIds = defineIdFamily({
|
|
88
|
+
family: "oyster-mcp",
|
|
89
|
+
version: 1,
|
|
90
|
+
kinds: { server: { prefix: "mcp" } },
|
|
91
|
+
});
|
|
92
|
+
/** The Knapsack connection for one stored server; its bearer goes only to that endpoint. */
|
|
93
|
+
function mcpConnection(server) {
|
|
94
|
+
const connection = {
|
|
95
|
+
id: `mcp.${server.serverId}`,
|
|
96
|
+
name: server.name,
|
|
97
|
+
url: new URL(server.url),
|
|
98
|
+
};
|
|
99
|
+
if (server.bearer === null)
|
|
100
|
+
return connection;
|
|
101
|
+
return {
|
|
102
|
+
...connection,
|
|
103
|
+
headers: { authorization: `Bearer ${server.bearer}` },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const toMcpServer = (stored) => ({
|
|
107
|
+
serverId: stored.serverId,
|
|
108
|
+
name: stored.name,
|
|
109
|
+
url: stored.url,
|
|
110
|
+
hasBearer: stored.bearer !== null,
|
|
111
|
+
});
|
|
112
|
+
const instructions = [
|
|
113
|
+
"You are an agent on a personal Oyster: a small, self-hosted home for Apps.",
|
|
114
|
+
"Your tool is execute: it runs JavaScript in an isolated sandbox. Each installed App is an env.<APP> module; dotted method names are nested properties, e.g. `return await env.BONSAI.repos.list({})`.",
|
|
115
|
+
"Remote MCP servers the owner connected are env.mcp.<server> modules; call a tool as `return await env.mcp.<server>.<tool>({...})`.",
|
|
116
|
+
"Discover what exists with `return await forage.list({limit:50})` and `return await forage.describe({path})` before calling a method you have not seen.",
|
|
117
|
+
"Only the value your program returns is visible to you; console.log output is not. Always `return` the result.",
|
|
118
|
+
"Be direct and brief. Say plainly when you cannot do something.",
|
|
119
|
+
].join("\n");
|
|
120
|
+
/** One agent: Shiitake's Cloudflare host over this Durable Object; its root supplies `hostFor`. */
|
|
121
|
+
export class StandaloneAgentBase extends ShiitakeDO {
|
|
122
|
+
/** Each installed Release's verified catalog surface, built once per Release for this Agent's lifetime. */
|
|
123
|
+
#appSurfaces;
|
|
124
|
+
/** The root host for this Agent's pinned policy directory; unpinned Agents have no App tools. */
|
|
125
|
+
async appHost() {
|
|
126
|
+
const stored = await this.ctx.storage.get("app-policy-directory");
|
|
127
|
+
if (stored === undefined)
|
|
128
|
+
return null;
|
|
129
|
+
const pinned = Schema.decodeUnknownSync(policyDirectorySchema)(stored);
|
|
130
|
+
const scope = tenantScope(pinned.tenantId, this.env.OYSTER_ORIGIN);
|
|
131
|
+
return pinned.directoryName === scope.durableName("agents")
|
|
132
|
+
? this.hostFor(scope)
|
|
133
|
+
: null;
|
|
134
|
+
}
|
|
135
|
+
/** Choose this agent's model door; override to plug in another. */
|
|
136
|
+
modelSource() {
|
|
137
|
+
if (this.env.OYSTER_AGENT_MODEL === "@fungi/luna") {
|
|
138
|
+
const egress = configuredProviderEgress(this.env);
|
|
139
|
+
const baseUrl = egress.baseUrl("ai-gateway");
|
|
140
|
+
if (baseUrl === null)
|
|
141
|
+
throw new Error("Configure the Luna AI Gateway Worker secret and gateway");
|
|
142
|
+
const provider = openaiProvider();
|
|
143
|
+
const listed = provider
|
|
144
|
+
.getModels()
|
|
145
|
+
.find((item) => item.id === "gpt-6-luna");
|
|
146
|
+
if (listed === undefined)
|
|
147
|
+
throw new Error("The OpenAI catalog has no Luna model");
|
|
148
|
+
const model = { ...listed, id: "openai/gpt-6-luna", baseUrl };
|
|
149
|
+
return {
|
|
150
|
+
acquire: async () => {
|
|
151
|
+
const streamSimple = await loadStreamSimple();
|
|
152
|
+
return {
|
|
153
|
+
model,
|
|
154
|
+
stream: (selected, context, options) => streamSimple(selected, context, {
|
|
155
|
+
...options,
|
|
156
|
+
apiKey: "oyster-provider-egress",
|
|
157
|
+
fetch: providerFetch(egress, "ai-gateway"),
|
|
158
|
+
}),
|
|
159
|
+
release: () => Promise.resolve(),
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (this.env.AI === undefined)
|
|
165
|
+
throw new Error("Configure the AI binding to run agents");
|
|
166
|
+
const options = {};
|
|
167
|
+
if (this.env.OYSTER_AGENT_MODEL !== undefined)
|
|
168
|
+
options.model = this.env.OYSTER_AGENT_MODEL;
|
|
169
|
+
if (this.env.OYSTER_AI_GATEWAY !== undefined)
|
|
170
|
+
options.gateway = this.env.OYSTER_AI_GATEWAY;
|
|
171
|
+
return workersAiModels(this.env.AI, options);
|
|
172
|
+
}
|
|
173
|
+
/** Host-only initialization; Agent/App transports never receive this directory binding. */
|
|
174
|
+
async usePolicyDirectory(input) {
|
|
175
|
+
const directory = Schema.decodeUnknownSync(policyDirectorySchema)(input);
|
|
176
|
+
const previous = await this.ctx.storage.get("app-policy-directory");
|
|
177
|
+
if (previous !== undefined) {
|
|
178
|
+
const pinned = Schema.decodeUnknownSync(policyDirectorySchema)(previous);
|
|
179
|
+
if (pinned.directoryName !== directory.directoryName ||
|
|
180
|
+
pinned.tenantId !== directory.tenantId)
|
|
181
|
+
throw new Error("Agent policy directory mismatch");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
await this.ctx.storage.put("app-policy-directory", directory);
|
|
185
|
+
}
|
|
186
|
+
/** Host-only policy pin read, used before an owner can approve tools. */
|
|
187
|
+
async policyDirectory() {
|
|
188
|
+
const stored = await this.ctx.storage.get("app-policy-directory");
|
|
189
|
+
return stored === undefined
|
|
190
|
+
? null
|
|
191
|
+
: Schema.decodeUnknownSync(policyDirectorySchema)(stored);
|
|
192
|
+
}
|
|
193
|
+
/** Listeners from live codemode acquisitions, told when the server list changes. */
|
|
194
|
+
mcpInvalidations = new Set();
|
|
195
|
+
async storedMcpServers() {
|
|
196
|
+
const stored = await this.ctx.storage.get(mcpServersKey);
|
|
197
|
+
return stored === undefined
|
|
198
|
+
? []
|
|
199
|
+
: Schema.decodeUnknownSync(storedMcpServersSchema)(stored);
|
|
200
|
+
}
|
|
201
|
+
async writeMcpServers(servers) {
|
|
202
|
+
await this.ctx.storage.put(mcpServersKey, servers);
|
|
203
|
+
for (const invalidate of this.mcpInvalidations)
|
|
204
|
+
invalidate();
|
|
205
|
+
}
|
|
206
|
+
/** Host-only: this Agent's MCP servers, without their bearer tokens. */
|
|
207
|
+
async mcpServers() {
|
|
208
|
+
return (await this.storedMcpServers()).map(toMcpServer);
|
|
209
|
+
}
|
|
210
|
+
/** Host-only: connect one remote MCP server, admitted by the owner route. */
|
|
211
|
+
async addMcpServer(input) {
|
|
212
|
+
const servers = await this.storedMcpServers();
|
|
213
|
+
if (servers.some((server) => server.name === input.name))
|
|
214
|
+
return { error: "duplicate_name" };
|
|
215
|
+
if (servers.length >= maxAgentMcpServers)
|
|
216
|
+
return { error: "limit" };
|
|
217
|
+
const added = {
|
|
218
|
+
serverId: mcpServerIds.server.generate(),
|
|
219
|
+
name: input.name,
|
|
220
|
+
url: new URL(input.url).href,
|
|
221
|
+
bearer: input.bearer,
|
|
222
|
+
};
|
|
223
|
+
await this.writeMcpServers([...servers, added]);
|
|
224
|
+
return toMcpServer(added);
|
|
225
|
+
}
|
|
226
|
+
/** Host-only: disconnect one MCP server. */
|
|
227
|
+
async removeMcpServer(serverId) {
|
|
228
|
+
const servers = await this.storedMcpServers();
|
|
229
|
+
const kept = servers.filter((server) => server.serverId !== serverId);
|
|
230
|
+
if (kept.length === servers.length)
|
|
231
|
+
return false;
|
|
232
|
+
await this.writeMcpServers(kept);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
shiitakeOptions() {
|
|
236
|
+
const principal = {
|
|
237
|
+
actor: { kind: "agent", agentId: this.ctx.id.name ?? "agent" },
|
|
238
|
+
};
|
|
239
|
+
return {
|
|
240
|
+
model: this.modelSource(),
|
|
241
|
+
instructions: {
|
|
242
|
+
load: () => Promise.resolve({ revision: "oyster-4", text: instructions }),
|
|
243
|
+
},
|
|
244
|
+
tools: {
|
|
245
|
+
sources: [
|
|
246
|
+
{
|
|
247
|
+
id: "oyster-apps",
|
|
248
|
+
// Capture once per catalog acquisition, so the next run sees
|
|
249
|
+
// changed Releases, installation authority and removals.
|
|
250
|
+
acquire: async (context) => {
|
|
251
|
+
const host = await this.appHost();
|
|
252
|
+
if (host === null)
|
|
253
|
+
return {
|
|
254
|
+
snapshot: { version: "unpinned", modules: [] },
|
|
255
|
+
release: () => Promise.resolve(),
|
|
256
|
+
};
|
|
257
|
+
const policy = (await host.appToolPolicy?.(principal.actor)) ?? null;
|
|
258
|
+
this.#appSurfaces ??= appSurfaceCache(host);
|
|
259
|
+
const source = await oysterAppMethods(host, principal, policy === null ? null : { kind: "agent", policy }, this.#appSurfaces);
|
|
260
|
+
return source.acquire(context);
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
Knapsack.mcpServers({
|
|
264
|
+
id: "agent-mcp",
|
|
265
|
+
read: async () => {
|
|
266
|
+
const servers = await this.storedMcpServers();
|
|
267
|
+
return {
|
|
268
|
+
// Secret-free: ids and endpoints, never the bearer.
|
|
269
|
+
version: JSON.stringify(servers.map((server) => [server.serverId, server.url])),
|
|
270
|
+
servers: servers.map(mcpConnection),
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
subscribe: (invalidate) => {
|
|
274
|
+
this.mcpInvalidations.add(invalidate);
|
|
275
|
+
return () => this.mcpInvalidations.delete(invalidate);
|
|
276
|
+
},
|
|
277
|
+
onUnavailable: (error) => {
|
|
278
|
+
console.warn("agent MCP server unavailable", {
|
|
279
|
+
serverId: error.serverId,
|
|
280
|
+
reason: error.reason,
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
// The Worker's own egress: public internet only.
|
|
284
|
+
fetch: (url, init) => fetch(url, init),
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
sandbox: createCodemodeSandbox(this.env.LOADER, "oyster-workerd"),
|
|
288
|
+
},
|
|
289
|
+
extensions: { roster: [], grants: [] },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/** Send an installation-admitted message through fleet Session operations. */
|
|
293
|
+
async sendFromApp(installationId, sessionId, message) {
|
|
294
|
+
const id = this.ctx.id.name;
|
|
295
|
+
if (id === undefined)
|
|
296
|
+
throw new Error("Agent address is unnamed");
|
|
297
|
+
const sessions = createFleetSessions(async () => ({
|
|
298
|
+
agent: await this.shiitake(),
|
|
299
|
+
release: () => Promise.resolve(),
|
|
300
|
+
}));
|
|
301
|
+
const grant = {
|
|
302
|
+
agents: [id],
|
|
303
|
+
actions: ["send"],
|
|
304
|
+
expiresAt: Date.now() + 60_000,
|
|
305
|
+
provenance: { actor: installationId },
|
|
306
|
+
};
|
|
307
|
+
return sessions.send(grant, id, sessionId, message);
|
|
308
|
+
}
|
|
309
|
+
/** Latest inbox clock from the same Shiitake owner, without reading Session rows. */
|
|
310
|
+
async revision() {
|
|
311
|
+
await this.shiitake();
|
|
312
|
+
return this.readInboxRevision();
|
|
313
|
+
}
|
|
314
|
+
/** Every live Session of this agent, as inbox rows. */
|
|
315
|
+
async inboxRows() {
|
|
316
|
+
const changes = await this.readInboxChanges(0, 1_000);
|
|
317
|
+
const rows = [];
|
|
318
|
+
for (const row of changes.rows) {
|
|
319
|
+
if ("deleted" in row)
|
|
320
|
+
continue;
|
|
321
|
+
rows.push(row);
|
|
322
|
+
}
|
|
323
|
+
return { rows, revision: changes.latest };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const actingInstallationSchema = Schema.Union([
|
|
327
|
+
Schema.Literal("owner-direct"),
|
|
328
|
+
Schema.Struct({ installationId: Schema.NonEmptyString }),
|
|
329
|
+
]);
|
|
330
|
+
const approverVersionSchema = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }));
|
|
331
|
+
const storedPolicySchema = Schema.Union([
|
|
332
|
+
Schema.Struct({
|
|
333
|
+
status: Schema.Literal("pending"),
|
|
334
|
+
level: Schema.Literals(["read", "write"]),
|
|
335
|
+
revision: Schema.NonEmptyString,
|
|
336
|
+
actingInstallation: actingInstallationSchema,
|
|
337
|
+
}),
|
|
338
|
+
Schema.Struct({
|
|
339
|
+
status: Schema.Literal("approved"),
|
|
340
|
+
policy: appToolPolicySchema,
|
|
341
|
+
membershipVersion: approverVersionSchema,
|
|
342
|
+
actingInstallation: actingInstallationSchema,
|
|
343
|
+
}),
|
|
344
|
+
]);
|
|
345
|
+
const requestIds = defineIdFamily({
|
|
346
|
+
family: "oyster-agent",
|
|
347
|
+
version: 1,
|
|
348
|
+
kinds: { request: { prefix: "request" } },
|
|
349
|
+
});
|
|
350
|
+
const visitRow = Schema.Struct({
|
|
351
|
+
agent_id: Schema.String,
|
|
352
|
+
session_id: Schema.String,
|
|
353
|
+
visited_at: Schema.Number,
|
|
354
|
+
});
|
|
355
|
+
const counterRow = Schema.Struct({ value: Schema.Number });
|
|
356
|
+
/** Names this Oyster's agents and remembers which Sessions the member opened. */
|
|
357
|
+
export class AgentDirectory extends DurableObject {
|
|
358
|
+
directory;
|
|
359
|
+
composedTenancy;
|
|
360
|
+
grant() {
|
|
361
|
+
return {
|
|
362
|
+
agents: "all",
|
|
363
|
+
actions: ["create", "read", "manage"],
|
|
364
|
+
expiresAt: Date.now() + 60_000,
|
|
365
|
+
provenance: { actor: this.env.OYSTER_OWNER_NAME ?? "owner" },
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
constructor(ctx, env) {
|
|
369
|
+
super(ctx, env);
|
|
370
|
+
this.directory = createAgentDirectory(ctx.storage.sql);
|
|
371
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS visits (agent_id TEXT NOT NULL, session_id TEXT NOT NULL, visited_at INTEGER NOT NULL, PRIMARY KEY (agent_id, session_id)) STRICT");
|
|
372
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS counters (name TEXT PRIMARY KEY, value INTEGER NOT NULL) STRICT");
|
|
373
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS app_tool_policies (agent_id TEXT PRIMARY KEY, policy TEXT NOT NULL) STRICT");
|
|
374
|
+
}
|
|
375
|
+
list() {
|
|
376
|
+
return this.directory
|
|
377
|
+
.list(this.grant())
|
|
378
|
+
.toReversed()
|
|
379
|
+
.map(({ agentId, name }) => ({ agentId, name }));
|
|
380
|
+
}
|
|
381
|
+
has(agentId) {
|
|
382
|
+
const id = agentIds.agent.parse(agentId);
|
|
383
|
+
return id.ok && this.directory.read(this.grant(), id.value) !== null;
|
|
384
|
+
}
|
|
385
|
+
/** Resolve the existing recorded creator for host-side owner admission. */
|
|
386
|
+
readCreator(agentId) {
|
|
387
|
+
return this.directory.readCreator(this.grant(), agentId);
|
|
388
|
+
}
|
|
389
|
+
/** Owner-only rename; refuses a name already held by another Agent. */
|
|
390
|
+
rename(agentId, name, principal) {
|
|
391
|
+
if (!oysterPrincipalIsOwner(principal))
|
|
392
|
+
return { error: "not_granted" };
|
|
393
|
+
const id = agentIds.agent.parse(agentId);
|
|
394
|
+
if (!id.ok)
|
|
395
|
+
return { error: "not_found" };
|
|
396
|
+
return this.ctx.storage.transactionSync(() => {
|
|
397
|
+
try {
|
|
398
|
+
const renamed = this.directory.rename(this.grant(), id.value, {
|
|
399
|
+
name,
|
|
400
|
+
});
|
|
401
|
+
this.bump();
|
|
402
|
+
return { agentId: renamed.agentId, name: renamed.name };
|
|
403
|
+
}
|
|
404
|
+
catch (cause) {
|
|
405
|
+
if (cause instanceof DirectoryError && cause.code === "not_found")
|
|
406
|
+
return { error: "not_found" };
|
|
407
|
+
if (cause instanceof DirectoryError && cause.code === "name_taken")
|
|
408
|
+
return { error: "name_taken" };
|
|
409
|
+
throw cause;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
create(name, requestId, principal, actor) {
|
|
414
|
+
if (!oysterPrincipalIsOwner(principal))
|
|
415
|
+
throw new Error("Only the authenticated owner creates Agents");
|
|
416
|
+
return this.ctx.storage.transactionSync(() => {
|
|
417
|
+
const { agent, fact } = this.directory.create({
|
|
418
|
+
...this.grant(),
|
|
419
|
+
provenance: { actor: oysterPrincipalId(principal) },
|
|
420
|
+
}, { name, requestId, configuration: "none" });
|
|
421
|
+
if (fact !== null) {
|
|
422
|
+
this.bump();
|
|
423
|
+
this.writePolicy(agent.agentId, {
|
|
424
|
+
status: "pending",
|
|
425
|
+
level: "write",
|
|
426
|
+
revision: `${agent.agentId}:${String(this.revision())}`,
|
|
427
|
+
actingInstallation: actor,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const stored = this.storedPolicy(agent.agentId);
|
|
431
|
+
return {
|
|
432
|
+
agentId: agent.agentId,
|
|
433
|
+
name: agent.name,
|
|
434
|
+
policyRevision: stored === null
|
|
435
|
+
? null
|
|
436
|
+
: stored.status === "pending"
|
|
437
|
+
? stored.revision
|
|
438
|
+
: null,
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
writePolicy(agentId, policy) {
|
|
443
|
+
const encoded = Schema.encodeSync(Schema.fromJsonString(storedPolicySchema))(policy);
|
|
444
|
+
this.ctx.storage.sql.exec("INSERT INTO app_tool_policies(agent_id,policy) VALUES(?,?) ON CONFLICT(agent_id) DO UPDATE SET policy=excluded.policy", agentId, encoded);
|
|
445
|
+
}
|
|
446
|
+
storedPolicy(agentId) {
|
|
447
|
+
if (!this.has(agentId))
|
|
448
|
+
return null;
|
|
449
|
+
return this.readPolicy("SELECT policy FROM app_tool_policies WHERE agent_id=?", agentId);
|
|
450
|
+
}
|
|
451
|
+
readPolicy(query, id) {
|
|
452
|
+
const row = this.ctx.storage.sql.exec(query, id).toArray()[0];
|
|
453
|
+
if (row === undefined)
|
|
454
|
+
return null;
|
|
455
|
+
const stored = Schema.decodeUnknownSync(Schema.Struct({ policy: Schema.String }))(row);
|
|
456
|
+
const parsed = Schema.decodeUnknownOption(Schema.fromJsonString(storedPolicySchema), { onExcessProperty: "error" })(stored.policy);
|
|
457
|
+
return parsed._tag === "Some" ? parsed.value : null;
|
|
458
|
+
}
|
|
459
|
+
/** Current revision for the direct owner's compare-and-swap request. */
|
|
460
|
+
policyRevision(agentId) {
|
|
461
|
+
const stored = this.storedPolicy(agentId);
|
|
462
|
+
return stored === null
|
|
463
|
+
? null
|
|
464
|
+
: stored.status === "pending"
|
|
465
|
+
? stored.revision
|
|
466
|
+
: stored.policy.revision;
|
|
467
|
+
}
|
|
468
|
+
/** Whether this Agent's App-tool policy still awaits the owner's approval. */
|
|
469
|
+
policyStatus(agentId) {
|
|
470
|
+
const stored = this.storedPolicy(agentId);
|
|
471
|
+
if (stored === null)
|
|
472
|
+
return null;
|
|
473
|
+
return stored.status === "pending"
|
|
474
|
+
? { status: "pending", revision: stored.revision }
|
|
475
|
+
: { status: "approved", revision: stored.policy.revision };
|
|
476
|
+
}
|
|
477
|
+
async currentMember(tenantId, memberId) {
|
|
478
|
+
if (this.env.TENANCY !== undefined)
|
|
479
|
+
return this.env.TENANCY.memberById(tenantId, memberId);
|
|
480
|
+
// KEYS is always present on StandaloneAgentEnv (via StandaloneComputerEnv).
|
|
481
|
+
if (this.env.TENANTS === undefined)
|
|
482
|
+
return null;
|
|
483
|
+
let configured = {
|
|
484
|
+
TENANTS: this.env.TENANTS,
|
|
485
|
+
KEYS: this.env.KEYS,
|
|
486
|
+
OYSTER_ORIGIN: this.env.OYSTER_ORIGIN,
|
|
487
|
+
OYSTER_OWNER_KEY: this.env.OYSTER_OWNER_KEY,
|
|
488
|
+
};
|
|
489
|
+
if (this.env.OYSTER_OWNER_NAME !== undefined)
|
|
490
|
+
configured = {
|
|
491
|
+
...configured,
|
|
492
|
+
OYSTER_OWNER_NAME: this.env.OYSTER_OWNER_NAME,
|
|
493
|
+
};
|
|
494
|
+
const pending = (this.composedTenancy ??= standaloneTenancy(configured));
|
|
495
|
+
const composed = await pending.catch((cause) => {
|
|
496
|
+
if (this.composedTenancy === pending)
|
|
497
|
+
this.composedTenancy = undefined;
|
|
498
|
+
throw cause;
|
|
499
|
+
});
|
|
500
|
+
const owner = await composed.owner();
|
|
501
|
+
if (owner === null ||
|
|
502
|
+
tenantId !== composed.tenantId ||
|
|
503
|
+
owner.actor.memberId !== memberId)
|
|
504
|
+
return null;
|
|
505
|
+
return composed.tenancy.memberById(tenantId, memberId);
|
|
506
|
+
}
|
|
507
|
+
/** Approval stays revoked after any membership authority revision. */
|
|
508
|
+
async appToolPolicy(actor, tenantId = defaultTenant) {
|
|
509
|
+
const stored = this.storedPolicy(actor.agentId);
|
|
510
|
+
if (stored?.status !== "approved")
|
|
511
|
+
return null;
|
|
512
|
+
const current = await this.currentMember(tenantId, stored.policy.grantedBy);
|
|
513
|
+
return current !== null &&
|
|
514
|
+
oysterPrincipalIsOwner(current) &&
|
|
515
|
+
oysterPrincipalId(current) === stored.policy.grantedBy &&
|
|
516
|
+
current.membershipVersion === stored.membershipVersion
|
|
517
|
+
? stored.policy
|
|
518
|
+
: null;
|
|
519
|
+
}
|
|
520
|
+
/** Only direct human approval records an authenticated canonical grantor. */
|
|
521
|
+
async approveAppToolPolicy(agentId, principal, level, expectedRevision, actor, tenantId) {
|
|
522
|
+
if (!oysterPrincipalIsOwner(principal))
|
|
523
|
+
return { error: "not_granted" };
|
|
524
|
+
if (!this.has(agentId))
|
|
525
|
+
return { error: "not_found" };
|
|
526
|
+
const member = actor === "owner-direct"
|
|
527
|
+
? await this.currentMember(tenantId, oysterPrincipalId(principal))
|
|
528
|
+
: null;
|
|
529
|
+
if (actor === "owner-direct" &&
|
|
530
|
+
(member === null ||
|
|
531
|
+
!oysterPrincipalIsOwner(member) ||
|
|
532
|
+
!Schema.is(approverVersionSchema)(member.membershipVersion) ||
|
|
533
|
+
oysterPrincipalId(member) !== oysterPrincipalId(principal)))
|
|
534
|
+
return { error: "not_granted" };
|
|
535
|
+
return this.ctx.storage.transactionSync(() => {
|
|
536
|
+
const current = this.storedPolicy(agentId);
|
|
537
|
+
const revision = current === null
|
|
538
|
+
? null
|
|
539
|
+
: current.status === "pending"
|
|
540
|
+
? current.revision
|
|
541
|
+
: current.policy.revision;
|
|
542
|
+
if (revision !== expectedRevision)
|
|
543
|
+
return { error: "revision_conflict" };
|
|
544
|
+
this.bump();
|
|
545
|
+
const nextRevision = `${agentId}:${String(this.revision())}`;
|
|
546
|
+
if (actor !== "owner-direct" || member === null) {
|
|
547
|
+
this.writePolicy(agentId, {
|
|
548
|
+
status: "pending",
|
|
549
|
+
level,
|
|
550
|
+
revision: nextRevision,
|
|
551
|
+
actingInstallation: actor,
|
|
552
|
+
});
|
|
553
|
+
return { status: "pending", revision: nextRevision };
|
|
554
|
+
}
|
|
555
|
+
const policy = {
|
|
556
|
+
level,
|
|
557
|
+
revision: nextRevision,
|
|
558
|
+
grantedBy: oysterPrincipalId(member),
|
|
559
|
+
};
|
|
560
|
+
this.writePolicy(agentId, {
|
|
561
|
+
status: "approved",
|
|
562
|
+
policy,
|
|
563
|
+
membershipVersion: member.membershipVersion,
|
|
564
|
+
actingInstallation: actor,
|
|
565
|
+
});
|
|
566
|
+
return policy;
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
visit(agentId, sessionId) {
|
|
570
|
+
this.ctx.storage.sql.exec("INSERT INTO visits(agent_id, session_id, visited_at) VALUES(?,?,?) ON CONFLICT(agent_id, session_id) DO UPDATE SET visited_at=excluded.visited_at", agentId, sessionId, Date.now());
|
|
571
|
+
this.bump();
|
|
572
|
+
}
|
|
573
|
+
visits() {
|
|
574
|
+
return this.ctx.storage.sql
|
|
575
|
+
.exec("SELECT agent_id, session_id, visited_at FROM visits")
|
|
576
|
+
.toArray()
|
|
577
|
+
.map((row) => {
|
|
578
|
+
const decoded = Schema.decodeUnknownSync(visitRow)(row);
|
|
579
|
+
return {
|
|
580
|
+
agentId: decoded.agent_id,
|
|
581
|
+
sessionId: decoded.session_id,
|
|
582
|
+
visitedAt: decoded.visited_at,
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
/** Grows on every directory change (a new agent, a visit). */
|
|
587
|
+
revision() {
|
|
588
|
+
const row = this.ctx.storage.sql
|
|
589
|
+
.exec("SELECT value FROM counters WHERE name='revision'")
|
|
590
|
+
.toArray()[0];
|
|
591
|
+
return row === undefined
|
|
592
|
+
? 0
|
|
593
|
+
: Schema.decodeUnknownSync(counterRow)(row).value;
|
|
594
|
+
}
|
|
595
|
+
bump() {
|
|
596
|
+
this.ctx.storage.sql.exec("INSERT INTO counters(name, value) VALUES('revision', 1) ON CONFLICT(name) DO UPDATE SET value=value+1");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const everyOperation = [
|
|
600
|
+
"agent.read",
|
|
601
|
+
"agent.send",
|
|
602
|
+
"agent.manage",
|
|
603
|
+
];
|
|
604
|
+
/** The `agents` adapter over a Worker's agent bindings. */
|
|
605
|
+
export function standaloneAgents(env, scope) {
|
|
606
|
+
const directoryName = scope === undefined ? "agents" : scope.durableName("agents");
|
|
607
|
+
const directory = () => scope === undefined
|
|
608
|
+
? env.AGENT_DIRECTORY.getByName(directoryName)
|
|
609
|
+
: agentDirectoryForScope(env, scope);
|
|
610
|
+
const toAgent = (entry) => ({
|
|
611
|
+
agentId: entry.agentId,
|
|
612
|
+
name: entry.name,
|
|
613
|
+
sessionOperations: everyOperation,
|
|
614
|
+
});
|
|
615
|
+
return {
|
|
616
|
+
list: async () => (await directory().list()).map(toAgent),
|
|
617
|
+
create: async (name, requestId, principal, actor) => {
|
|
618
|
+
const entry = await directory().create(name, requestId, principal, actor);
|
|
619
|
+
await env.AGENTS.getByName(entry.agentId).usePolicyDirectory({
|
|
620
|
+
directoryName,
|
|
621
|
+
tenantId: scope?.tenantId ?? defaultTenant,
|
|
622
|
+
});
|
|
623
|
+
if (actor === "owner-direct" && entry.policyRevision !== null) {
|
|
624
|
+
// Best-effort: a stale membership lookup leaves the Agent pending
|
|
625
|
+
// rather than failing its creation. Grove's "Allow App tools"
|
|
626
|
+
// control recovers it through the same approval path.
|
|
627
|
+
await directory().approveAppToolPolicy(entry.agentId, principal, "write", entry.policyRevision, actor, scope?.tenantId ?? defaultTenant);
|
|
628
|
+
}
|
|
629
|
+
return toAgent(entry);
|
|
630
|
+
},
|
|
631
|
+
rename: async (agentId, name, principal) => {
|
|
632
|
+
const result = await directory().rename(agentId, name, principal);
|
|
633
|
+
return "error" in result ? result : toAgent(result);
|
|
634
|
+
},
|
|
635
|
+
policyStatus: async (agentId) => directory().policyStatus(agentId),
|
|
636
|
+
appToolPolicy: (actor) => directory().appToolPolicy(actor, scope?.tenantId ?? defaultTenant),
|
|
637
|
+
policyRevision: (agentId) => directory().policyRevision(agentId),
|
|
638
|
+
approveAppToolPolicy: async (agentId, principal, level, expectedRevision, actor) => {
|
|
639
|
+
if (!(await directory().has(agentId)))
|
|
640
|
+
return { error: "not_found" };
|
|
641
|
+
const pinned = await env.AGENTS.getByName(agentId).policyDirectory();
|
|
642
|
+
if (pinned?.directoryName !== directoryName ||
|
|
643
|
+
pinned.tenantId !== (scope?.tenantId ?? defaultTenant))
|
|
644
|
+
return { error: "not_found" };
|
|
645
|
+
return directory().approveAppToolPolicy(agentId, principal, level, expectedRevision, actor, scope?.tenantId ?? defaultTenant);
|
|
646
|
+
},
|
|
647
|
+
mcpServers: async (agentId) => (await directory().has(agentId))
|
|
648
|
+
? await env.AGENTS.getByName(agentId).mcpServers()
|
|
649
|
+
: null,
|
|
650
|
+
addMcpServer: async (agentId, input) => (await directory().has(agentId))
|
|
651
|
+
? await env.AGENTS.getByName(agentId).addMcpServer(input)
|
|
652
|
+
: { error: "not_found" },
|
|
653
|
+
removeMcpServer: async (agentId, serverId) => (await directory().has(agentId)) &&
|
|
654
|
+
(await env.AGENTS.getByName(agentId).removeMcpServer(serverId)),
|
|
655
|
+
fetch: async (agentId, request) => (await directory().has(agentId))
|
|
656
|
+
? await env.AGENTS.getByName(agentId).fetch(request)
|
|
657
|
+
: null,
|
|
658
|
+
inbox: async () => {
|
|
659
|
+
const [agents, visits, directoryRevision] = await Promise.all([
|
|
660
|
+
directory().list(),
|
|
661
|
+
directory().visits(),
|
|
662
|
+
directory().revision(),
|
|
663
|
+
]);
|
|
664
|
+
const visited = new Map(visits.map((visit) => [
|
|
665
|
+
`${visit.agentId}\u0000${visit.sessionId}`,
|
|
666
|
+
visit.visitedAt,
|
|
667
|
+
]));
|
|
668
|
+
const items = [];
|
|
669
|
+
// Each agent's revision only grows, so their sum with the directory's does too.
|
|
670
|
+
let revision = directoryRevision;
|
|
671
|
+
const inboxes = await Promise.all(agents.map(async (agent) => {
|
|
672
|
+
const agentHost = env.AGENTS.getByName(agent.agentId);
|
|
673
|
+
return { agent, own: await agentHost.inboxRows() };
|
|
674
|
+
}));
|
|
675
|
+
for (const { agent, own } of inboxes) {
|
|
676
|
+
revision += own.revision;
|
|
677
|
+
for (const row of own.rows) {
|
|
678
|
+
const visitedAt = visited.get(`${agent.agentId}\u0000${row.sessionId}`);
|
|
679
|
+
if (row.state === "unavailable" || visitedAt === undefined)
|
|
680
|
+
items.push({ ...row, agentId: agent.agentId });
|
|
681
|
+
else
|
|
682
|
+
items.push({ ...row, agentId: agent.agentId, visitedAt });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return { items, revision };
|
|
686
|
+
},
|
|
687
|
+
revision: async () => {
|
|
688
|
+
const [agents, directoryRevision] = await Promise.all([
|
|
689
|
+
directory().list(),
|
|
690
|
+
directory().revision(),
|
|
691
|
+
]);
|
|
692
|
+
const revisions = await Promise.all(agents.map((agent) => env.AGENTS.getByName(agent.agentId).revision()));
|
|
693
|
+
return revisions.reduce((total, revision) => total + revision, directoryRevision);
|
|
694
|
+
},
|
|
695
|
+
visit: async (agentId, sessionId) => {
|
|
696
|
+
if (!(await directory().has(agentId)))
|
|
697
|
+
return false;
|
|
698
|
+
await directory().visit(agentId, sessionId);
|
|
699
|
+
return true;
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const agentTarget = z.strictObject({
|
|
704
|
+
agentId: z.string(),
|
|
705
|
+
sessionId: z.string(),
|
|
706
|
+
});
|
|
707
|
+
const agentBindingArgs = {
|
|
708
|
+
INBOX: {
|
|
709
|
+
page: z.tuple([
|
|
710
|
+
z.strictObject({ after: z.string().nullable().optional() }).optional(),
|
|
711
|
+
]),
|
|
712
|
+
visit: z.tuple([agentTarget]),
|
|
713
|
+
},
|
|
714
|
+
AGENTS: {
|
|
715
|
+
send: z.tuple([
|
|
716
|
+
agentTarget.extend({ text: z.string(), delivery: z.literal("followUp") }),
|
|
717
|
+
]),
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
const inboxPageSchema = Schema.Struct({
|
|
721
|
+
items: Schema.Array(memberInboxItemSchema),
|
|
722
|
+
next: Schema.Null,
|
|
723
|
+
cursor: Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)),
|
|
724
|
+
});
|
|
725
|
+
const inboxVisitSchema = Schema.Struct({ visited: Schema.Literal(true) });
|
|
726
|
+
const decodeInboxPage = Schema.decodeUnknownSync(inboxPageSchema, {
|
|
727
|
+
onExcessProperty: "error",
|
|
728
|
+
});
|
|
729
|
+
const decodeInboxVisit = Schema.decodeUnknownSync(inboxVisitSchema, {
|
|
730
|
+
onExcessProperty: "error",
|
|
731
|
+
});
|
|
732
|
+
const decodeAgentAccepted = Schema.decodeUnknownSync(AcceptedSchema, {
|
|
733
|
+
onExcessProperty: "error",
|
|
734
|
+
});
|
|
735
|
+
/** Shared packet codecs; the Instance alone admits the current frame's operations. */
|
|
736
|
+
export const agentFrameMethods = {
|
|
737
|
+
INBOX: {
|
|
738
|
+
page: {
|
|
739
|
+
decodeArgs: (raw) => agentBindingArgs.INBOX.page.parse(raw),
|
|
740
|
+
decodeResult: decodeInboxPage,
|
|
741
|
+
requiredOperations: () => ["page"],
|
|
742
|
+
},
|
|
743
|
+
visit: {
|
|
744
|
+
decodeArgs: (raw) => agentBindingArgs.INBOX.visit.parse(raw),
|
|
745
|
+
decodeResult: decodeInboxVisit,
|
|
746
|
+
requiredOperations: () => ["visit"],
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
AGENTS: {
|
|
750
|
+
send: {
|
|
751
|
+
decodeArgs: (raw) => agentBindingArgs.AGENTS.send.parse(raw),
|
|
752
|
+
decodeResult: decodeAgentAccepted,
|
|
753
|
+
requiredOperations: () => ["send"],
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
/** Provider effects are admitted by the originating Instance's live frame. */
|
|
758
|
+
export async function dispatchAgentBinding(env, scope, frame, props, method, args) {
|
|
759
|
+
frame.signal.throwIfAborted();
|
|
760
|
+
if (frame.facts.selection.owner.kind !== "installation")
|
|
761
|
+
throw new AppInstanceError("revoked");
|
|
762
|
+
const installationId = frame.facts.selection.owner.id;
|
|
763
|
+
const directory = agentDirectoryForScope(env, scope);
|
|
764
|
+
let result;
|
|
765
|
+
if (props.binding === "INBOX" && method === "page") {
|
|
766
|
+
agentBindingArgs.INBOX.page.parse(args);
|
|
767
|
+
const inbox = await standaloneAgents(env, scope).inbox();
|
|
768
|
+
result = { items: inbox.items, next: null, cursor: inbox.revision };
|
|
769
|
+
}
|
|
770
|
+
else if (props.binding === "INBOX" && method === "visit") {
|
|
771
|
+
const [input] = agentBindingArgs.INBOX.visit.parse(args);
|
|
772
|
+
if (!(await directory.has(input.agentId)))
|
|
773
|
+
throw new Error("No such agent");
|
|
774
|
+
frame.signal.throwIfAborted();
|
|
775
|
+
await directory.visit(input.agentId, input.sessionId);
|
|
776
|
+
result = { visited: true };
|
|
777
|
+
}
|
|
778
|
+
else if (props.binding === "AGENTS" && method === "send") {
|
|
779
|
+
const [input] = agentBindingArgs.AGENTS.send.parse(args);
|
|
780
|
+
if (!(await directory.has(input.agentId)))
|
|
781
|
+
throw new Error("No such agent");
|
|
782
|
+
frame.signal.throwIfAborted();
|
|
783
|
+
const accepted = await env.AGENTS.getByName(input.agentId).sendFromApp(installationId, input.sessionId, {
|
|
784
|
+
requestId: requestIds.request.generate(),
|
|
785
|
+
text: input.text,
|
|
786
|
+
delivery: input.delivery,
|
|
787
|
+
});
|
|
788
|
+
result = { status: accepted.status, commandId: accepted.commandId };
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
throw new AppInstanceError("revoked");
|
|
792
|
+
}
|
|
793
|
+
frame.signal.throwIfAborted();
|
|
794
|
+
return Schema.decodeUnknownSync(Schema.Union([inboxPageSchema, inboxVisitSchema, AcceptedSchema]), { onExcessProperty: "error" })(result);
|
|
795
|
+
}
|