@fungi.computer/oyster 0.3.0 → 0.3.2
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/README.md +22 -1
- package/dist/app-builds.d.ts +1 -1
- package/dist/app-builds.js +4 -1
- package/dist/boot-capability.d.ts +1 -1
- package/dist/boot-capability.js +1 -1
- package/dist/mcp.d.ts +9 -2
- package/dist/mcp.js +1 -0
- package/dist/older-oyster-store-refusal.d.ts +4 -0
- package/dist/older-oyster-store-refusal.js +18 -0
- package/dist/older-oyster-store.d.ts +21 -0
- package/dist/older-oyster-store.js +43 -0
- package/dist/standalone/agents.d.ts +6 -1
- package/dist/standalone/agents.js +9 -13
- package/dist/standalone/app-builds-binding.js +1 -1
- package/dist/standalone/app-set.js +12 -1
- package/dist/standalone/registry.d.ts +5 -0
- package/dist/standalone/registry.js +15 -0
- package/dist/standalone/tenancy.d.ts +14 -1
- package/dist/standalone/tenancy.js +37 -3
- package/dist/standalone/worker.js +24 -9
- package/dist/workers-git.js +13 -1
- package/dist/workers.d.ts +2 -0
- package/dist/workers.js +12 -0
- package/docs/api/mcp.md +14 -0
- package/docs/api/workers.md +13 -0
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -124,7 +124,8 @@ npx wrangler deploy --var OYSTER_ORIGIN:$OYSTER_URL
|
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
Set `OYSTER_ORIGIN` in `wrangler.jsonc` instead if you deploy again later; every
|
|
127
|
-
deploy replaces the Worker's variables.
|
|
127
|
+
deploy replaces the Worker's variables. If you rename the Worker's `name` in
|
|
128
|
+
`wrangler.jsonc`, update `OYSTER_ORIGIN` to that Worker's URL at the same time.
|
|
128
129
|
|
|
129
130
|
### API-token permissions and teardown
|
|
130
131
|
|
|
@@ -172,6 +173,26 @@ repositories and namespace. Use the configured resource names and confirm each
|
|
|
172
173
|
is gone through Cloudflare's dashboard or API. Capture teardown errors and the
|
|
173
174
|
Wrangler version rather than silently adding unrelated token permissions.
|
|
174
175
|
|
|
176
|
+
### Upgrading from Oyster 0.2 or earlier
|
|
177
|
+
|
|
178
|
+
Oyster 0.3 does not read data created by Oyster 0.1 or 0.2, and it does not
|
|
179
|
+
convert it. An Oyster deployed on 0.2 and redeployed on 0.3 answers every
|
|
180
|
+
request with `409` and `{"error": "store_from_older_oyster", ...}` (a short
|
|
181
|
+
explanation page in a browser), and `oyster install-apps` prints the same
|
|
182
|
+
message. Old data is left untouched, so redeploying 0.2.x brings the Oyster back
|
|
183
|
+
exactly as it was.
|
|
184
|
+
|
|
185
|
+
To move to 0.3, start from a fresh store. The smallest way is to deploy under
|
|
186
|
+
new names, leaving the old Oyster in place until you no longer need it. In
|
|
187
|
+
`wrangler.jsonc`, rename `name`, both `bucket_name` values,
|
|
188
|
+
`artifacts.namespace` and every `workflows[].name`; create the new release
|
|
189
|
+
bucket; then run the quick start again. To reuse the names instead, tear the old
|
|
190
|
+
Oyster down as in the teardown steps above (delete the Worker, which deletes its
|
|
191
|
+
Durable Object data; empty and delete the release bucket; delete the Computer's
|
|
192
|
+
container application, the Workflow, and the Artifacts repositories and
|
|
193
|
+
namespace), then deploy 0.3 and reinstall the Apps. Data in the old Oyster, such
|
|
194
|
+
as Apps' stored data, is not carried over.
|
|
195
|
+
|
|
175
196
|
### Set the Oyster owner key
|
|
176
197
|
|
|
177
198
|
Choose an owner key (separate from the Cloudflare API token) and store it as a
|
package/dist/app-builds.d.ts
CHANGED
|
@@ -166,7 +166,7 @@ export type OysterAppBuildOptions = Readonly<{
|
|
|
166
166
|
templates: Pick<PublisherTemplateSeedReceipts, "read">;
|
|
167
167
|
now?: () => number;
|
|
168
168
|
}>;
|
|
169
|
-
/** Create the current build tables and index atomically;
|
|
169
|
+
/** Create the current build tables and index atomically; an older store is refused, never converted. */
|
|
170
170
|
export declare function migrateOysterAppBuilds(owner: WatchdogSqliteOwner): void;
|
|
171
171
|
/** The build owner over one tenant's SQLite, Computer and artifact store. */
|
|
172
172
|
export declare function createOysterAppBuilds(options: OysterAppBuildOptions): Readonly<{
|
package/dist/app-builds.js
CHANGED
|
@@ -29,6 +29,7 @@ import { writeArtifactRoot, } from "./build/artifact-writer.js";
|
|
|
29
29
|
import { buildGitPassPrefix } from "./git-auth.js";
|
|
30
30
|
import { defaultAssetPolicy, readVerified, verifyRelease, } from "./release.js";
|
|
31
31
|
import { appForTemplateRepository, templateRepositoryForApp, } from "./source/template-repository.js";
|
|
32
|
+
import { buildJobsFromOlderOyster, OlderOysterStoreError, } from "./older-oyster-store.js";
|
|
32
33
|
/** A Computer's whole-build budget: a cold build on a 6 GiB Computer measured 185-270 s. */
|
|
33
34
|
export const oysterAppBuildTimeoutMs = 900_000;
|
|
34
35
|
/** The job, its clone admission and its upload grant end together. */
|
|
@@ -147,9 +148,11 @@ function sameDigest(left, right) {
|
|
|
147
148
|
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
148
149
|
return difference === 0;
|
|
149
150
|
}
|
|
150
|
-
/** Create the current build tables and index atomically;
|
|
151
|
+
/** Create the current build tables and index atomically; an older store is refused, never converted. */
|
|
151
152
|
export function migrateOysterAppBuilds(owner) {
|
|
152
153
|
const { sql } = owner;
|
|
154
|
+
if (buildJobsFromOlderOyster(sql))
|
|
155
|
+
throw new OlderOysterStoreError();
|
|
153
156
|
owner.transactionSync(() => {
|
|
154
157
|
sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_sources (app_id TEXT PRIMARY KEY, source TEXT NOT NULL) STRICT");
|
|
155
158
|
const jobColumns = `
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type BootUi, type UiBootCheckOutcome } from "./boot-gate.js";
|
|
3
3
|
import type { BootPageResult, BootPageTarget } from "./browser/boot.js";
|
|
4
|
-
export declare const BOOT_CAPABILITY_HEADER = "x-oyster-boot-
|
|
4
|
+
export declare const BOOT_CAPABILITY_HEADER = "x-oyster-boot-token";
|
|
5
5
|
declare const bootCapabilityAssetSchema: z.ZodObject<{
|
|
6
6
|
path: z.ZodString;
|
|
7
7
|
contentType: z.ZodString;
|
package/dist/boot-capability.js
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { parseArtifactRef, parseReleaseId, parseSha256Digest, } from "@fungi.computer/puffball";
|
|
5
5
|
import { BOOT_DEADLINE_MS, BOOT_TRANSPORT_GRACE_MS, BootGateError, bootMountDiagnosticsSchema, } from "./boot-gate.js";
|
|
6
6
|
import { defaultAssetPolicy } from "./release.js";
|
|
7
|
-
export const BOOT_CAPABILITY_HEADER = "x-oyster-boot-
|
|
7
|
+
export const BOOT_CAPABILITY_HEADER = "x-oyster-boot-token";
|
|
8
8
|
const bootCapabilityAssetSchema = z.strictObject({
|
|
9
9
|
path: z.string().min(1).max(4096),
|
|
10
10
|
contentType: z.string().min(1).max(256),
|
package/dist/mcp.d.ts
CHANGED
|
@@ -3,8 +3,15 @@ import type { DeclaredOperations, Installation } from "./apps/installation.js";
|
|
|
3
3
|
import { type CapabilitySource, type Grants, type ModuleRegistration } from "@fungi.computer/mycelium";
|
|
4
4
|
import { appToolSchemas } from "@fungi.computer/forage";
|
|
5
5
|
import type { OysterHost, OysterPrincipal } from "./host.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type { TenantPrincipal } from "./tenant.js";
|
|
7
7
|
import { type VerifiedRelease } from "./release.js";
|
|
8
|
+
/** Captured capabilities and grants offered to one admitted MCP caller. */
|
|
9
|
+
export type OysterMcpCatalogSnapshot = Readonly<{
|
|
10
|
+
instructions: string;
|
|
11
|
+
sources: readonly CapabilitySource[];
|
|
12
|
+
/** An omitted module has no grant. */
|
|
13
|
+
grants: Grants;
|
|
14
|
+
}>;
|
|
8
15
|
/** An installation ID as a guest binding name: `spawn-git` becomes `SPAWN_GIT`. */
|
|
9
16
|
export declare function appBindingName(installationId: string): string;
|
|
10
17
|
/** Why an App method call from the MCP catalog was refused. */
|
|
@@ -98,4 +105,4 @@ export declare function oysterComputerTools(host: Pick<OysterHost, "computerId"
|
|
|
98
105
|
* still re-admits the key, rechecks that same stored approval, and passes
|
|
99
106
|
* the App's gate.
|
|
100
107
|
*/
|
|
101
|
-
export declare function oysterMcpCatalogLoad(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "computer" | "capabilities" | "appToolPolicy" | "keys" | "workflows">): (principal:
|
|
108
|
+
export declare function oysterMcpCatalogLoad(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "computer" | "capabilities" | "appToolPolicy" | "keys" | "workflows">): (principal: TenantPrincipal) => Promise<OysterMcpCatalogSnapshot>;
|
package/dist/mcp.js
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TenantScope } from "./tenant.js";
|
|
2
|
+
import type { OysterWorkerEnv } from "./workers.js";
|
|
3
|
+
/** Answer with the refusal when the request's registry holds an older store. */
|
|
4
|
+
export declare function refuseOlderOysterStore(request: Request, env: Pick<OysterWorkerEnv, "REGISTRY">, scope: TenantScope): Promise<Response | null>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { olderOysterStoreResponse } from "./older-oyster-store.js";
|
|
2
|
+
/** Scopes whose registry answered "not older" in this isolate; a refusal is never cached. */
|
|
3
|
+
const cleanScopes = new WeakMap();
|
|
4
|
+
/** Answer with the refusal when the request's registry holds an older store. */
|
|
5
|
+
export async function refuseOlderOysterStore(request, env, scope) {
|
|
6
|
+
const name = scope.durableName("registry");
|
|
7
|
+
let clean = cleanScopes.get(env);
|
|
8
|
+
if (clean === undefined) {
|
|
9
|
+
clean = new Set();
|
|
10
|
+
cleanScopes.set(env, clean);
|
|
11
|
+
}
|
|
12
|
+
if (clean.has(name))
|
|
13
|
+
return null;
|
|
14
|
+
if (await env.REGISTRY.getByName(name).storeFromOlderOyster())
|
|
15
|
+
return olderOysterStoreResponse(request);
|
|
16
|
+
clean.add(name);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
2
|
+
/** The closed error code a refused request carries. */
|
|
3
|
+
export declare const olderOysterStoreCode = "store_from_older_oyster";
|
|
4
|
+
/** The one message every refusal shows. */
|
|
5
|
+
export declare const olderOysterStoreMessage = "This Oyster's data was created by Oyster 0.2 or earlier, which this version cannot read. Reset it (https://github.com/fungi-computer/oyster#upgrading-from-oyster-02-or-earlier) or redeploy 0.2.x. Old data is left untouched.";
|
|
6
|
+
/** Raised by a store owner that finds data written by Oyster 0.2 or earlier. */
|
|
7
|
+
export declare class OlderOysterStoreError extends Error {
|
|
8
|
+
constructor();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Whether the registry holds an installation row written before 0.3, which
|
|
12
|
+
* kept its authority fields at the top level. Reads only.
|
|
13
|
+
*/
|
|
14
|
+
export declare function installationsFromOlderOyster(sql: WatchdogSqliteOwner["sql"]): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether `oyster_build_jobs` was created before 0.3 added `request_id`. Reads
|
|
17
|
+
* the table's own definition, never a schema probe.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildJobsFromOlderOyster(sql: WatchdogSqliteOwner["sql"]): boolean;
|
|
20
|
+
/** The refusal as the browser or API caller asked to see it. */
|
|
21
|
+
export declare function olderOysterStoreResponse(request: Request): Response;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The closed error code a refused request carries. */
|
|
2
|
+
export const olderOysterStoreCode = "store_from_older_oyster";
|
|
3
|
+
/** Where the reset steps live. */
|
|
4
|
+
const olderOysterStoreGuide = "https://github.com/fungi-computer/oyster#upgrading-from-oyster-02-or-earlier";
|
|
5
|
+
/** The one message every refusal shows. */
|
|
6
|
+
export const olderOysterStoreMessage = `This Oyster's data was created by Oyster 0.2 or earlier, which this version cannot read. Reset it (${olderOysterStoreGuide}) or redeploy 0.2.x. Old data is left untouched.`;
|
|
7
|
+
/** Raised by a store owner that finds data written by Oyster 0.2 or earlier. */
|
|
8
|
+
export class OlderOysterStoreError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(olderOysterStoreMessage);
|
|
11
|
+
this.name = "OlderOysterStoreError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether the registry holds an installation row written before 0.3, which
|
|
16
|
+
* kept its authority fields at the top level. Reads only.
|
|
17
|
+
*/
|
|
18
|
+
export function installationsFromOlderOyster(sql) {
|
|
19
|
+
const table = sql
|
|
20
|
+
.exec("SELECT 1 AS found FROM sqlite_master WHERE name='installations'")
|
|
21
|
+
.toArray();
|
|
22
|
+
if (table.length === 0)
|
|
23
|
+
return false;
|
|
24
|
+
return (sql
|
|
25
|
+
.exec("SELECT 1 AS found FROM installations WHERE json_valid(record) AND json_type(record, '$.authority') IS NULL LIMIT 1")
|
|
26
|
+
.toArray().length > 0);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether `oyster_build_jobs` was created before 0.3 added `request_id`. Reads
|
|
30
|
+
* the table's own definition, never a schema probe.
|
|
31
|
+
*/
|
|
32
|
+
export function buildJobsFromOlderOyster(sql) {
|
|
33
|
+
return (sql
|
|
34
|
+
.exec("SELECT 1 AS found FROM sqlite_master WHERE type='table' AND name='oyster_build_jobs' AND sql NOT LIKE '%request_id%'")
|
|
35
|
+
.toArray().length > 0);
|
|
36
|
+
}
|
|
37
|
+
/** The refusal as the browser or API caller asked to see it. */
|
|
38
|
+
export function olderOysterStoreResponse(request) {
|
|
39
|
+
const status = 409;
|
|
40
|
+
if (request.headers.get("accept")?.includes("text/html") === true)
|
|
41
|
+
return new Response(`<!doctype html><meta charset="utf-8"><title>Oyster data needs a reset</title><body style="font:16px/1.5 system-ui;max-width:40rem;margin:4rem auto;padding:0 1rem"><h1>Oyster data needs a reset</h1><p>This Oyster's data was created by Oyster 0.2 or earlier, which this version cannot read. Old data is left untouched.</p><p><a href="${olderOysterStoreGuide}">Reset it</a> or redeploy 0.2.x.</p></body>`, { status, headers: { "content-type": "text/html; charset=utf-8" } });
|
|
42
|
+
return Response.json({ error: olderOysterStoreCode, message: olderOysterStoreMessage }, { status });
|
|
43
|
+
}
|
|
@@ -2,11 +2,16 @@ import { type StandaloneTenancyEnv } from "./tenancy.js";
|
|
|
2
2
|
import type { PostEnvelope } from "@fungi.computer/carrier/post";
|
|
3
3
|
import { type StandaloneMachineLinkBindings, type StandaloneMachineJoinResult } from "./machine-link.js";
|
|
4
4
|
import { type MachineAgentSettlementPrincipal, type MachineAgentHome, type MachineEnrollment, type MachineLinkCaller, type MachineLinkIdentity } from "../machine-link/index.js";
|
|
5
|
+
/** Workers AI binding surface used by the standalone provider adapter. */
|
|
6
|
+
export type AiBinding = {
|
|
7
|
+
aiGatewayLogId: string | null;
|
|
8
|
+
/** AI binding transport supplied by the Worker runtime. */
|
|
9
|
+
fetch?: (input: Request | string | URL, init?: RequestInit) => Promise<Response>;
|
|
10
|
+
};
|
|
5
11
|
import { DurableObject } from "cloudflare:workers";
|
|
6
12
|
import type { StandaloneComputerEnv } from "./computer.js";
|
|
7
13
|
import { ShiitakeDO } from "@fungi.computer/shiitake/cloudflare";
|
|
8
14
|
import type { CloudflareShiitakeOptions } from "@fungi.computer/shiitake/cloudflare";
|
|
9
|
-
import { type AiBinding } from "@earendil-works/pi-ai/api/cloudflare-ai-binding";
|
|
10
15
|
import * as Schema from "effect/Schema";
|
|
11
16
|
import { type FrameBindingProps } from "../cloudflare.js";
|
|
12
17
|
import type { Avatar } from "@fungi.computer/sprite/avatar";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeModel, streamModel } from "@fungi.computer/mule/effect";
|
|
1
2
|
import { observeProviderStatus } from "@fungi.computer/mule/effect";
|
|
2
3
|
import { standaloneTenancy, } from "./tenancy.js";
|
|
3
4
|
import { AcceptedSchema } from "@fungi.computer/shiitake/client";
|
|
@@ -41,18 +42,18 @@ const defaultAgentModel = "@cf/moonshotai/kimi-k2.6";
|
|
|
41
42
|
/** Own transport evidence and exact-message decisions for one model lease. */
|
|
42
43
|
function observedModels(model, defaults) {
|
|
43
44
|
return {
|
|
44
|
-
acquire:
|
|
45
|
-
const streamSimple =
|
|
45
|
+
acquire: ({ signal }) => {
|
|
46
|
+
const streamSimple = streamModel;
|
|
46
47
|
const retryableMessages = new WeakSet();
|
|
47
|
-
return {
|
|
48
|
+
return Promise.resolve({
|
|
48
49
|
model,
|
|
49
50
|
retryable: (message) => !signal.aborted &&
|
|
50
51
|
message.stopReason === "error" &&
|
|
51
52
|
retryableMessages.has(message),
|
|
52
|
-
stream: (selected, context, options) => {
|
|
53
|
+
stream: async (selected, context, options) => {
|
|
53
54
|
const upstream = { ...options, ...defaults };
|
|
54
55
|
const observed = observeProviderStatus(upstream);
|
|
55
|
-
const response = streamSimple(selected, context, {
|
|
56
|
+
const response = await streamSimple(selected, context, {
|
|
56
57
|
...upstream,
|
|
57
58
|
fetch: observed.fetch,
|
|
58
59
|
onProviderStreamEvent: observed.onProviderStreamEvent,
|
|
@@ -69,7 +70,7 @@ function observedModels(model, defaults) {
|
|
|
69
70
|
return response;
|
|
70
71
|
},
|
|
71
72
|
release: () => Promise.resolve(),
|
|
72
|
-
};
|
|
73
|
+
});
|
|
73
74
|
},
|
|
74
75
|
};
|
|
75
76
|
}
|
|
@@ -89,7 +90,7 @@ function workersAiModels(ai, options = {}) {
|
|
|
89
90
|
baseUrl: `https://workers-binding.ai/ai-gateway/gateways/${options.gateway ?? "default"}/workers-ai/v1`,
|
|
90
91
|
};
|
|
91
92
|
const fetch = createAiBindingFetch(ai);
|
|
92
|
-
return observedModels(model, {
|
|
93
|
+
return observedModels(normalizeModel(model), {
|
|
93
94
|
fetch,
|
|
94
95
|
apiKey: CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL,
|
|
95
96
|
headers: {
|
|
@@ -100,11 +101,6 @@ function workersAiModels(ai, options = {}) {
|
|
|
100
101
|
},
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Pi's dispatching `streamSimple` evaluates every provider's model catalog when
|
|
105
|
-
* it loads; only a run needs it, so acquisition loads it instead of startup.
|
|
106
|
-
*/
|
|
107
|
-
const loadStreamSimple = async () => (await import("@earendil-works/pi-ai/compat")).streamSimple;
|
|
108
104
|
const policyDirectorySchema = Schema.Struct({
|
|
109
105
|
directoryName: Schema.NonEmptyString,
|
|
110
106
|
tenantId: tenantIdSchema,
|
|
@@ -354,7 +350,7 @@ export class StandaloneAgentBase extends ShiitakeDO {
|
|
|
354
350
|
if (listed === undefined)
|
|
355
351
|
throw new Error("The OpenAI catalog has no Luna model");
|
|
356
352
|
const model = { ...listed, id: "openai/gpt-6-luna", baseUrl };
|
|
357
|
-
return observedModels(model, {
|
|
353
|
+
return observedModels(normalizeModel(model), {
|
|
358
354
|
apiKey: "oyster-provider-egress",
|
|
359
355
|
fetch: providerFetch(egress, "ai-gateway"),
|
|
360
356
|
}).acquire(input);
|
|
@@ -203,7 +203,7 @@ export async function dispatchAppsBuildsBinding(env, scope, frame, props, method
|
|
|
203
203
|
const registry = buildRegistry(env, scope);
|
|
204
204
|
const build = "repo" in input
|
|
205
205
|
? await registry.readRepositoryBuild(frame.facts.selection.owner.id, input.repo)
|
|
206
|
-
: await registry.
|
|
206
|
+
: await registry.readInstallationBuild(frame.facts.selection.owner.id, input.jobId);
|
|
207
207
|
frame.signal.throwIfAborted();
|
|
208
208
|
const missingJob = build === null && "jobId" in input;
|
|
209
209
|
return Schema.decodeUnknownSync(Schema.Json)({
|
|
@@ -2,7 +2,9 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
5
6
|
import * as Schema from "effect/Schema";
|
|
7
|
+
import { olderOysterStoreCode } from "../older-oyster-store.js";
|
|
6
8
|
import { publishSource } from "../pack.js";
|
|
7
9
|
import { appBuildRecipe, readBuildManifest } from "../build/workspace-build.js";
|
|
8
10
|
import { parseSourceRevision } from "@fungi.computer/puffball";
|
|
@@ -55,6 +57,11 @@ export const standaloneAppSet = [
|
|
|
55
57
|
appId: "app_grove",
|
|
56
58
|
},
|
|
57
59
|
];
|
|
60
|
+
/** The host's refusal of a store written by Oyster 0.2 or earlier. */
|
|
61
|
+
const olderOysterStoreRefusal = Schema.Struct({
|
|
62
|
+
error: Schema.Literal(olderOysterStoreCode),
|
|
63
|
+
message: Schema.String,
|
|
64
|
+
});
|
|
58
65
|
const publishedSchema = Schema.Struct({
|
|
59
66
|
kind: Schema.Literals(["recorded", "existing"]),
|
|
60
67
|
receipt: Schema.Struct({
|
|
@@ -285,8 +292,12 @@ export async function installStandaloneAppSet(options) {
|
|
|
285
292
|
const publishStarted = performance.now();
|
|
286
293
|
try {
|
|
287
294
|
const published = await publish(app, located);
|
|
288
|
-
if (!published.ok)
|
|
295
|
+
if (!published.ok) {
|
|
296
|
+
const refusal = Schema.decodeUnknownOption(Schema.fromJsonString(olderOysterStoreRefusal))(await published.text());
|
|
297
|
+
if (Option.isSome(refusal))
|
|
298
|
+
throw new Error(refusal.value.message);
|
|
289
299
|
throw new Error(`${name} source publish failed (${String(published.status)})`);
|
|
300
|
+
}
|
|
290
301
|
const { kind, receipt } = Schema.decodeUnknownSync(publishedSchema)(await published.json());
|
|
291
302
|
options.report(`${name} source ${kind} at ${receipt.seedRevision}`);
|
|
292
303
|
const ready = performance.now();
|
|
@@ -37,12 +37,15 @@ export type StandaloneRegistryRpc = {
|
|
|
37
37
|
registerRepositoryApp(input: Parameters<StandaloneRegistry["registerRepositoryApp"]>[0]): ReturnType<StandaloneRegistry["registerRepositoryApp"]>;
|
|
38
38
|
repositoryBuildSource(installationId: string, repository: string): ReturnType<StandaloneRegistry["repositoryBuildSource"]>;
|
|
39
39
|
readRepositoryBuild(installationId: string, repository: string): ReturnType<StandaloneRegistry["readRepositoryBuild"]>;
|
|
40
|
+
readInstallationBuild(installationId: string, jobId: string): ReturnType<StandaloneRegistry["readInstallationBuild"]>;
|
|
40
41
|
installWorkflowBuild(jobId: string, target: string, release: Release, memberId: string, callerInstallationId: string): ReturnType<StandaloneRegistry["installWorkflowBuild"]>;
|
|
41
42
|
};
|
|
42
43
|
export declare abstract class StandaloneRegistry extends OysterRegistry implements StandaloneRegistryRpc {
|
|
43
44
|
#private;
|
|
44
45
|
protected abstract hostFor(scope: TenantScope): Promise<OysterHost>;
|
|
45
46
|
constructor(state: DurableObjectState, env: StandaloneRegistryEnv, git?: StandaloneRegistryGit);
|
|
47
|
+
/** Whether installations or build jobs were written by Oyster 0.2 or earlier. */
|
|
48
|
+
storeFromOlderOyster(): boolean;
|
|
46
49
|
/** Re-admit the live owner before inventory access or a hosted start. */
|
|
47
50
|
computerFleet(memberId: string, request: StandaloneFleetRequest): Promise<Readonly<{
|
|
48
51
|
computerId: string;
|
|
@@ -167,6 +170,8 @@ export declare abstract class StandaloneRegistry extends OysterRegistry implemen
|
|
|
167
170
|
appId: import("@fungi.computer/puffball").AppId;
|
|
168
171
|
} | null>;
|
|
169
172
|
readBuild(jobId: string): Promise<OysterBuildView | null>;
|
|
173
|
+
/** A job ID alone never grants another installation access to its status. */
|
|
174
|
+
readInstallationBuild(installationId: string, jobId: string): Promise<OysterBuildView | null>;
|
|
170
175
|
/** Repository status belongs to the same captured Git owner as registration. */
|
|
171
176
|
readRepositoryBuild(installationId: string, repository: string): Promise<Readonly<{
|
|
172
177
|
jobId: string;
|
|
@@ -12,6 +12,7 @@ import * as Schema from "effect/Schema";
|
|
|
12
12
|
import { parseAppId } from "@fungi.computer/puffball";
|
|
13
13
|
import { appIdSchema } from "@fungi.computer/puffball/effect";
|
|
14
14
|
import { createOysterAppBuilds, OysterBuildStartError, } from "../app-builds.js";
|
|
15
|
+
import { buildJobsFromOlderOyster } from "../older-oyster-store.js";
|
|
15
16
|
import { readBuildManifest, appBuildRecipe } from "../build/workspace-build.js";
|
|
16
17
|
import { standaloneTenancy } from "./tenancy.js";
|
|
17
18
|
import { createFleetComputers, ComputerInventoryError, } from "@fungi.computer/computer/fleet";
|
|
@@ -44,6 +45,11 @@ export class StandaloneRegistry extends OysterRegistry {
|
|
|
44
45
|
});
|
|
45
46
|
this.#computers.ensure();
|
|
46
47
|
}
|
|
48
|
+
/** Whether installations or build jobs were written by Oyster 0.2 or earlier. */
|
|
49
|
+
storeFromOlderOyster() {
|
|
50
|
+
return (super.storeFromOlderOyster() ||
|
|
51
|
+
buildJobsFromOlderOyster(this.ctx.storage.sql));
|
|
52
|
+
}
|
|
47
53
|
/** Re-admit the live owner before inventory access or a hosted start. */
|
|
48
54
|
async computerFleet(memberId, request) {
|
|
49
55
|
const shared = await standaloneTenancy(this.#env);
|
|
@@ -357,6 +363,15 @@ export class StandaloneRegistry extends OysterRegistry {
|
|
|
357
363
|
async readBuild(jobId) {
|
|
358
364
|
return (await this.#appBuilds()).read(jobId);
|
|
359
365
|
}
|
|
366
|
+
/** A job ID alone never grants another installation access to its status. */
|
|
367
|
+
async readInstallationBuild(installationId, jobId) {
|
|
368
|
+
const builds = await this.#appBuilds();
|
|
369
|
+
const build = builds.read(jobId);
|
|
370
|
+
return build !== null &&
|
|
371
|
+
sourceOwnedBy(builds.readSource(build.appId), installationId)
|
|
372
|
+
? build
|
|
373
|
+
: null;
|
|
374
|
+
}
|
|
360
375
|
/** Repository status belongs to the same captured Git owner as registration. */
|
|
361
376
|
async readRepositoryBuild(installationId, repository) {
|
|
362
377
|
const latest = (await this.#appBuilds()).latestRepositoryBuild(installationId, repository);
|
|
@@ -7,12 +7,23 @@ export type StandaloneSeed = Readonly<{
|
|
|
7
7
|
origin: string;
|
|
8
8
|
owner: Identity;
|
|
9
9
|
}>;
|
|
10
|
+
export type StandaloneSeedRefusal = Readonly<{
|
|
11
|
+
kind: "refused";
|
|
12
|
+
code: "seed_configuration_changed";
|
|
13
|
+
seededOrigin: string;
|
|
14
|
+
configuredOrigin: string;
|
|
15
|
+
recovery: "fresh_worker_name_or_reset_worker_data";
|
|
16
|
+
}>;
|
|
17
|
+
export declare class StandaloneSeedConfigurationChanged extends Error {
|
|
18
|
+
readonly refusal: StandaloneSeedRefusal;
|
|
19
|
+
constructor(refusal: StandaloneSeedRefusal);
|
|
20
|
+
}
|
|
10
21
|
/** A fresh registry namespace with one host-provisioned standalone tenant. */
|
|
11
22
|
export declare class StandaloneTenants extends TenancyRegistryDO {
|
|
12
23
|
#private;
|
|
13
24
|
constructor(state: DurableObjectState);
|
|
14
25
|
/** Reuse only the exact configured tenant and its current canonical owner. */
|
|
15
|
-
seed(input: StandaloneSeed): Promise<Tenant>;
|
|
26
|
+
seed(input: StandaloneSeed): Promise<Tenant | StandaloneSeedRefusal>;
|
|
16
27
|
}
|
|
17
28
|
/** The same trusted bindings are used by HTTP, MCP and cold root readers. */
|
|
18
29
|
export type StandaloneTenancyEnv = Pick<OysterGitWorkerEnv, "OYSTER_ORIGIN" | "OYSTER_OWNER_NAME" | "OYSTER_OWNER_KEY" | "KEYS"> & Readonly<{
|
|
@@ -38,5 +49,7 @@ export type StandaloneTenancy = Readonly<{
|
|
|
38
49
|
oyster: OysterTenancy;
|
|
39
50
|
owner(): Promise<StandaloneOwner | null>;
|
|
40
51
|
}>;
|
|
52
|
+
/** A request may seed the standalone tenant only at its configured origin. */
|
|
53
|
+
export declare function standaloneTenancyForRequest(env: StandaloneTenancyEnv, request: Request): Promise<StandaloneTenancy>;
|
|
41
54
|
/** Seed once per environment, then admit every entry from one fresh registry read. */
|
|
42
55
|
export declare function standaloneTenancy(env: StandaloneTenancyEnv): Promise<StandaloneTenancy>;
|
|
@@ -3,6 +3,7 @@ import { createTenancy, } from "../tenancy/index.js";
|
|
|
3
3
|
import { ownerKey } from "../tenancy/owner-key.js";
|
|
4
4
|
import { hosts } from "../tenancy/resolve.js";
|
|
5
5
|
import { ownerOrKeyAuth } from "../auth.js";
|
|
6
|
+
import { HTTPException } from "hono/http-exception";
|
|
6
7
|
import { defaultTenant, tenantScope, } from "../tenant.js";
|
|
7
8
|
import * as Schema from "effect/Schema";
|
|
8
9
|
const seedSchema = Schema.Struct({
|
|
@@ -13,6 +14,14 @@ const seedSchema = Schema.Struct({
|
|
|
13
14
|
}),
|
|
14
15
|
});
|
|
15
16
|
const seedKey = "standalone-owner-seed";
|
|
17
|
+
export class StandaloneSeedConfigurationChanged extends Error {
|
|
18
|
+
refusal;
|
|
19
|
+
constructor(refusal) {
|
|
20
|
+
super("Standalone seed configuration changed");
|
|
21
|
+
this.name = "StandaloneSeedConfigurationChanged";
|
|
22
|
+
this.refusal = refusal;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
16
25
|
/** A fresh registry namespace with one host-provisioned standalone tenant. */
|
|
17
26
|
export class StandaloneTenants extends TenancyRegistryDO {
|
|
18
27
|
#state;
|
|
@@ -31,8 +40,15 @@ export class StandaloneTenants extends TenancyRegistryDO {
|
|
|
31
40
|
const marker = Schema.decodeUnknownSync(seedSchema)(raw);
|
|
32
41
|
if (marker.origin !== scope.origin ||
|
|
33
42
|
marker.owner.provider !== input.owner.provider ||
|
|
34
|
-
marker.owner.subject !== input.owner.subject)
|
|
35
|
-
|
|
43
|
+
marker.owner.subject !== input.owner.subject) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "refused",
|
|
46
|
+
code: "seed_configuration_changed",
|
|
47
|
+
seededOrigin: marker.origin,
|
|
48
|
+
configuredOrigin: scope.origin,
|
|
49
|
+
recovery: "fresh_worker_name_or_reset_worker_data",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
36
52
|
}
|
|
37
53
|
let tenant = await this.get(defaultTenant);
|
|
38
54
|
if (tenant === null) {
|
|
@@ -69,6 +85,22 @@ export class StandaloneTenants extends TenancyRegistryDO {
|
|
|
69
85
|
});
|
|
70
86
|
}
|
|
71
87
|
}
|
|
88
|
+
/** A request may seed the standalone tenant only at its configured origin. */
|
|
89
|
+
export async function standaloneTenancyForRequest(env, request) {
|
|
90
|
+
const configuredOrigin = tenantScope(defaultTenant, env.OYSTER_ORIGIN).origin;
|
|
91
|
+
const requestOrigin = new URL(request.url).origin;
|
|
92
|
+
if (requestOrigin !== configuredOrigin)
|
|
93
|
+
throw new HTTPException(404, {
|
|
94
|
+
res: Response.json({
|
|
95
|
+
kind: "refused",
|
|
96
|
+
code: "origin_mismatch",
|
|
97
|
+
configuredOrigin,
|
|
98
|
+
requestOrigin,
|
|
99
|
+
message: `Request origin ${requestOrigin} does not match configured OYSTER_ORIGIN ${configuredOrigin}. Set OYSTER_ORIGIN to this Worker's URL.`,
|
|
100
|
+
}, { status: 404 }),
|
|
101
|
+
});
|
|
102
|
+
return standaloneTenancy(env);
|
|
103
|
+
}
|
|
72
104
|
const environments = new WeakMap();
|
|
73
105
|
/** Seed once per environment, then admit every entry from one fresh registry read. */
|
|
74
106
|
export function standaloneTenancy(env) {
|
|
@@ -90,10 +122,12 @@ async function composeTenancy(env) {
|
|
|
90
122
|
provider: "standalone-owner",
|
|
91
123
|
subject: env.OYSTER_OWNER_NAME ?? "owner",
|
|
92
124
|
};
|
|
93
|
-
await env.TENANTS.getByName("registry").seed({
|
|
125
|
+
const seeded = await env.TENANTS.getByName("registry").seed({
|
|
94
126
|
origin: scope.origin,
|
|
95
127
|
owner: identity,
|
|
96
128
|
});
|
|
129
|
+
if ("kind" in seeded)
|
|
130
|
+
throw new StandaloneSeedConfigurationChanged(seeded);
|
|
97
131
|
const registry = durableObjectRegistry(env.TENANTS, { readonly: true });
|
|
98
132
|
/** The canonical owner, only while the one admitted record still proves it. */
|
|
99
133
|
const ownerOf = ({ tenant, membership, }) => tenant.id !== defaultTenant ||
|
|
@@ -17,7 +17,7 @@ import { StandaloneInstance as ComputerInstance } from "./instance.js";
|
|
|
17
17
|
// Every guest binding in every frame is this one entrypoint, routed by props.
|
|
18
18
|
export { OysterBinding } from "./instance.js";
|
|
19
19
|
import { StandaloneWorkflowRunsBase, } from "./workflows.js";
|
|
20
|
-
import {
|
|
20
|
+
import { standaloneTenancyForRequest, StandaloneSeedConfigurationChanged, } from "./tenancy.js";
|
|
21
21
|
export { StandaloneTenants } from "./tenancy.js";
|
|
22
22
|
import { standalonePreviewRequest } from "./preview.js";
|
|
23
23
|
import { standaloneMachineLinkRoutes, } from "./machine-link.js";
|
|
@@ -25,6 +25,7 @@ export { MachineLinkDO } from "./machine-link.js";
|
|
|
25
25
|
import { standaloneWorkflowControl } from "./workflow-control.js";
|
|
26
26
|
import { standaloneWorkflowWebhookRoutes, standaloneWorkflowWebhookControls, } from "./workflow-webhooks.js";
|
|
27
27
|
import { Hono } from "hono";
|
|
28
|
+
import { HTTPException } from "hono/http-exception";
|
|
28
29
|
import { standaloneBuildRoutes } from "./builds.js";
|
|
29
30
|
import { standaloneComputerFleetRoutes, standaloneComputerSelection, } from "./computer-fleet.js";
|
|
30
31
|
import { serveOysterComputerEgress, } from "./computer-egress.js";
|
|
@@ -59,7 +60,7 @@ export { AgentDirectory } from "./agents.js";
|
|
|
59
60
|
const standaloneRoot = (selectComputer) => {
|
|
60
61
|
const root = oysterGitRoot({
|
|
61
62
|
tenancy: (env) => ({
|
|
62
|
-
resolve: async (request) => (await
|
|
63
|
+
resolve: async (request) => (await standaloneTenancyForRequest(env, request)).oyster.resolve(request),
|
|
63
64
|
}),
|
|
64
65
|
homeInstallationId: (env) => env.OYSTER_HOME_INSTALLATION,
|
|
65
66
|
agents: standaloneAgents,
|
|
@@ -153,14 +154,21 @@ export class OysterComputerEgress extends WorkerEntrypoint {
|
|
|
153
154
|
});
|
|
154
155
|
}
|
|
155
156
|
}
|
|
157
|
+
const standaloneWorkerErrorHandler = (error, context) => {
|
|
158
|
+
if (error instanceof StandaloneSeedConfigurationChanged) {
|
|
159
|
+
const { refusal } = error;
|
|
160
|
+
return context.json({
|
|
161
|
+
...refusal,
|
|
162
|
+
message: `Standalone seed/origin mismatch: existing data was seeded for ${refusal.seededOrigin}, but OYSTER_ORIGIN is ${refusal.configuredOrigin}. Deploy under a fresh Worker name or reset this Worker's data.`,
|
|
163
|
+
}, 409);
|
|
164
|
+
}
|
|
165
|
+
if (error instanceof HTTPException)
|
|
166
|
+
return error.getResponse();
|
|
167
|
+
console.error(error);
|
|
168
|
+
return context.text("Internal Server Error", 500);
|
|
169
|
+
};
|
|
156
170
|
export default {
|
|
157
171
|
async fetch(request, env, context) {
|
|
158
|
-
const path = new URL(request.url).pathname;
|
|
159
|
-
if (path === "/local/mesh/join" ||
|
|
160
|
-
new URLPattern({
|
|
161
|
-
pathname: "/api/teams/:teamId/local/mesh/:deviceId/connect",
|
|
162
|
-
}).test(request.url))
|
|
163
|
-
return standaloneMachineLinkRoutes(env).fetch(request);
|
|
164
172
|
const preview = await standalonePreviewRequest(request, env, (computerId) => computerRegistry(env).ownsComputer(computerId), (computerId, signature, port, service) => env.COMPUTER.getByName(computerId).portPreview(signature, port, service));
|
|
165
173
|
if (preview !== null)
|
|
166
174
|
return preview;
|
|
@@ -171,6 +179,13 @@ export default {
|
|
|
171
179
|
if (fetchOyster === undefined)
|
|
172
180
|
throw new Error("Oyster fetch handler is missing");
|
|
173
181
|
return new Hono()
|
|
182
|
+
.onError(standaloneWorkerErrorHandler)
|
|
183
|
+
.all("/local/mesh/join", () => standaloneMachineLinkRoutes(env)
|
|
184
|
+
.onError(standaloneWorkerErrorHandler)
|
|
185
|
+
.fetch(request, env, context))
|
|
186
|
+
.all("/api/teams/:teamId/local/mesh/:deviceId/connect", () => standaloneMachineLinkRoutes(env)
|
|
187
|
+
.onError(standaloneWorkerErrorHandler)
|
|
188
|
+
.fetch(request, env, context))
|
|
174
189
|
.route("/", standaloneWorkflowWebhookRoutes(env, async (incoming) => {
|
|
175
190
|
const admitted = await oysterRoot.admit(env, incoming);
|
|
176
191
|
return admitted === null
|
|
@@ -178,6 +193,6 @@ export default {
|
|
|
178
193
|
: oysterRoot.hostFor(env, admitted.scope);
|
|
179
194
|
}))
|
|
180
195
|
.all("*", () => fetchOyster(request, env, context))
|
|
181
|
-
.fetch(request);
|
|
196
|
+
.fetch(request, env, context);
|
|
182
197
|
},
|
|
183
198
|
};
|
package/dist/workers-git.js
CHANGED
|
@@ -29,6 +29,7 @@ import { createRecipientDeliveries, } from "./event-recipients.js";
|
|
|
29
29
|
import { createOyster, oysterPrincipalIsOwner, } from "./host.js";
|
|
30
30
|
import { OysterInstance, oysterBindingEntrypoint, oysterBindings, oysterWorkerHost, r2FileSource, r2ArtifactStore, serveOysterBootCapability, } from "./workers.js";
|
|
31
31
|
import { registryInstallations } from "./internal/registry-installations.js";
|
|
32
|
+
import { refuseOlderOysterStore } from "./older-oyster-store-refusal.js";
|
|
32
33
|
const infoSchema = Schema.Struct({
|
|
33
34
|
name: Schema.String,
|
|
34
35
|
defaultBranch: Schema.String,
|
|
@@ -888,10 +889,21 @@ export function createOysterGitWorker(root) {
|
|
|
888
889
|
hosts = new Map();
|
|
889
890
|
environments.set(env, hosts);
|
|
890
891
|
}
|
|
891
|
-
|
|
892
|
+
let resolved;
|
|
893
|
+
try {
|
|
894
|
+
resolved = await root.admit(env, request);
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
if (error instanceof HTTPException)
|
|
898
|
+
return error.getResponse();
|
|
899
|
+
throw error;
|
|
900
|
+
}
|
|
892
901
|
if (resolved === null)
|
|
893
902
|
return new Response("Not found", { status: 404 });
|
|
894
903
|
const { scope } = resolved;
|
|
904
|
+
const refused = await refuseOlderOysterStore(request, env, scope);
|
|
905
|
+
if (refused !== null)
|
|
906
|
+
return refused;
|
|
895
907
|
const boot = await serveOysterBootCapability(request, env, scope);
|
|
896
908
|
if (boot !== null)
|
|
897
909
|
return boot;
|
package/dist/workers.d.ts
CHANGED
|
@@ -211,6 +211,8 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
|
|
|
211
211
|
*/
|
|
212
212
|
protected currentOwner(memberId: string, tenantId: TenantId): Promise<string | null>;
|
|
213
213
|
private storageView;
|
|
214
|
+
/** Whether this registry's data was written by Oyster 0.2 or earlier; hosts refuse it. */
|
|
215
|
+
storeFromOlderOyster(): boolean;
|
|
214
216
|
/** Every installation record. */
|
|
215
217
|
list(): Installation[];
|
|
216
218
|
/** One installation record, or `null`. */
|
package/dist/workers.js
CHANGED
|
@@ -10,6 +10,8 @@ import { BOOT_DEADLINE_MS, BOOT_TRANSPORT_GRACE_MS, readBootUi, } from "./boot-g
|
|
|
10
10
|
import { checkBootPage } from "./browser/boot.js";
|
|
11
11
|
import { bindingSessions } from "./browser/sessions.js";
|
|
12
12
|
import { registryInstallations } from "./internal/registry-installations.js";
|
|
13
|
+
import { installationsFromOlderOyster } from "./older-oyster-store.js";
|
|
14
|
+
import { refuseOlderOysterStore } from "./older-oyster-store-refusal.js";
|
|
13
15
|
import { createInstallationOperations, } from "./installation-operations.js";
|
|
14
16
|
import { installationWorkerEntrypoint } from "./installation-execution.js";
|
|
15
17
|
export { installationWorkerEntrypoint };
|
|
@@ -62,6 +64,8 @@ const approvalHomePinRow = Schema.Struct({
|
|
|
62
64
|
/** The installation registry: one SQLite-backed Durable Object per Oyster. */
|
|
63
65
|
export class OysterRegistry extends DurableObject {
|
|
64
66
|
#generation;
|
|
67
|
+
/** Decided once, before this version writes anything, and never revised. */
|
|
68
|
+
#installationsFromOlderOyster;
|
|
65
69
|
#approval;
|
|
66
70
|
#installationOperations;
|
|
67
71
|
/** The plain root has the same single owner-key host as OysterInstance. */
|
|
@@ -150,6 +154,7 @@ export class OysterRegistry extends DurableObject {
|
|
|
150
154
|
artifactsRepositories;
|
|
151
155
|
constructor(ctx, env) {
|
|
152
156
|
super(ctx, env);
|
|
157
|
+
this.#installationsFromOlderOyster = installationsFromOlderOyster(ctx.storage.sql);
|
|
153
158
|
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS oyster_approval_home (id INTEGER PRIMARY KEY CHECK (id = 1), tenant_id TEXT NOT NULL, origin TEXT NOT NULL) STRICT");
|
|
154
159
|
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS installations (installation_id TEXT PRIMARY KEY, record TEXT NOT NULL) STRICT");
|
|
155
160
|
this.#generation = createInstallationStoreGeneration(ctx.storage.sql);
|
|
@@ -347,6 +352,10 @@ export class OysterRegistry extends DurableObject {
|
|
|
347
352
|
: new AppStorageError("unavailable", { cause }),
|
|
348
353
|
}));
|
|
349
354
|
}
|
|
355
|
+
/** Whether this registry's data was written by Oyster 0.2 or earlier; hosts refuse it. */
|
|
356
|
+
storeFromOlderOyster() {
|
|
357
|
+
return this.#installationsFromOlderOyster;
|
|
358
|
+
}
|
|
350
359
|
/** Every installation record. */
|
|
351
360
|
list() {
|
|
352
361
|
return this.ctx.storage.sql
|
|
@@ -1151,6 +1160,9 @@ export function createOysterWorker(options = {}) {
|
|
|
1151
1160
|
return {
|
|
1152
1161
|
async fetch(request, env) {
|
|
1153
1162
|
const scope = tenantScope(defaultTenant, env.OYSTER_ORIGIN);
|
|
1163
|
+
const refused = await refuseOlderOysterStore(request, env, scope);
|
|
1164
|
+
if (refused !== null)
|
|
1165
|
+
return refused;
|
|
1154
1166
|
const boot = await serveOysterBootCapability(request, env, scope);
|
|
1155
1167
|
if (boot !== null)
|
|
1156
1168
|
return boot;
|
package/docs/api/mcp.md
CHANGED
|
@@ -67,6 +67,20 @@ readonly code: string;
|
|
|
67
67
|
|
|
68
68
|
## Type Aliases
|
|
69
69
|
|
|
70
|
+
### OysterMcpCatalogSnapshot
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
type OysterMcpCatalogSnapshot = Readonly<{
|
|
74
|
+
instructions: string;
|
|
75
|
+
sources: readonly CapabilitySource[];
|
|
76
|
+
grants: Grants;
|
|
77
|
+
}>;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Captured capabilities and grants offered to one admitted MCP caller.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
70
84
|
### OysterCallerAccess
|
|
71
85
|
|
|
72
86
|
```ts
|
package/docs/api/workers.md
CHANGED
|
@@ -1188,6 +1188,19 @@ Open the existing App-storage invalidation stream under fresh owner admission.
|
|
|
1188
1188
|
|
|
1189
1189
|
`Promise`\<`Response`\>
|
|
1190
1190
|
|
|
1191
|
+
##### storeFromOlderOyster()
|
|
1192
|
+
|
|
1193
|
+
```ts
|
|
1194
|
+
storeFromOlderOyster(): boolean;
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
Whether this registry's data was written by Oyster 0.2 or earlier; hosts refuse
|
|
1198
|
+
it.
|
|
1199
|
+
|
|
1200
|
+
###### Returns
|
|
1201
|
+
|
|
1202
|
+
`boolean`
|
|
1203
|
+
|
|
1191
1204
|
##### list()
|
|
1192
1205
|
|
|
1193
1206
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/oyster",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"author": "Levi <levi@technomancy.dev>",
|
|
5
5
|
"description": "Run your own Apps on Cloudflare Workers or celld: installable App Releases, each with its own Durable Object, SQLite database and declared bindings",
|
|
6
6
|
"type": "module",
|
|
@@ -246,33 +246,33 @@
|
|
|
246
246
|
"@earendil-works/pi-ai": "1.3.0",
|
|
247
247
|
"@fungi.computer/agar": "0.3.0",
|
|
248
248
|
"@fungi.computer/bird-dog": "0.1.0",
|
|
249
|
-
"@fungi.computer/braid": "0.2.
|
|
249
|
+
"@fungi.computer/braid": "0.2.1",
|
|
250
250
|
"@fungi.computer/burrow": "0.1.0",
|
|
251
251
|
"@fungi.computer/cairn": "0.1.0",
|
|
252
252
|
"@fungi.computer/capnweb": "0.12.0",
|
|
253
253
|
"@fungi.computer/caps": "0.2.0",
|
|
254
254
|
"@fungi.computer/carrier": "0.1.0",
|
|
255
|
-
"@fungi.computer/computer": "0.2.
|
|
256
|
-
"@fungi.computer/computer-contract": "0.0.
|
|
255
|
+
"@fungi.computer/computer": "0.2.2",
|
|
256
|
+
"@fungi.computer/computer-contract": "0.0.2",
|
|
257
257
|
"@fungi.computer/darkroom": "0.2.0",
|
|
258
258
|
"@fungi.computer/ear-tag": "0.1.0",
|
|
259
259
|
"@fungi.computer/forage": "0.2.0",
|
|
260
260
|
"@fungi.computer/gills": "0.2.0",
|
|
261
|
-
"@fungi.computer/knapsack": "0.2.
|
|
262
|
-
"@fungi.computer/mycelium": "0.2.
|
|
261
|
+
"@fungi.computer/knapsack": "0.2.1",
|
|
262
|
+
"@fungi.computer/mycelium": "0.2.1",
|
|
263
263
|
"@fungi.computer/nacre": "0.0.1",
|
|
264
264
|
"@fungi.computer/puffball": "0.2.0",
|
|
265
265
|
"@fungi.computer/seam": "0.1.0",
|
|
266
|
-
"@fungi.computer/shiitake": "0.3.
|
|
267
|
-
"@fungi.computer/shiitake-extensions": "0.1.
|
|
266
|
+
"@fungi.computer/shiitake": "0.3.1",
|
|
267
|
+
"@fungi.computer/shiitake-extensions": "0.1.1",
|
|
268
268
|
"@fungi.computer/sigils": "0.2.0",
|
|
269
269
|
"@fungi.computer/sprite": "0.2.0",
|
|
270
270
|
"@fungi.computer/sprite-zdog": "0.2.0",
|
|
271
271
|
"@fungi.computer/veil": "0.2.0",
|
|
272
272
|
"@fungi.computer/watchdog": "0.1.0",
|
|
273
273
|
"@fungi.computer/whistle": "0.1.0",
|
|
274
|
-
"@fungi.computer/workflows-wire": "0.2.
|
|
275
|
-
"@fungi.computer/zookeeper": "0.0.
|
|
274
|
+
"@fungi.computer/workflows-wire": "0.2.1",
|
|
275
|
+
"@fungi.computer/zookeeper": "0.0.2",
|
|
276
276
|
"@modelcontextprotocol/server": "2.0.0",
|
|
277
277
|
"@pnpm/lockfile.fs": "1100.2.13",
|
|
278
278
|
"@pnpm/lockfile.pruner": "1100.0.27",
|