@fungi.computer/oyster 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +376 -72
- package/dist/agents.d.ts +3 -2
- package/dist/agents.js +4 -2
- package/dist/app-builds.d.ts +81 -8
- package/dist/app-builds.js +210 -60
- package/dist/app-storage.d.ts +3 -3
- package/dist/approval-capability.d.ts +5 -0
- package/dist/approval-capability.js +46 -0
- package/dist/approval-home.d.ts +51 -0
- package/dist/approval-home.js +226 -0
- package/dist/apps/catalog.d.ts +335 -0
- package/dist/apps/catalog.js +46 -0
- package/dist/apps/gate.d.ts +20 -6
- package/dist/apps/gate.js +42 -16
- package/dist/apps/index.d.ts +3 -1
- package/dist/apps/index.js +3 -1
- package/dist/apps/installation.d.ts +132 -51
- package/dist/apps/installation.js +176 -70
- package/dist/apps/permissions.d.ts +247 -0
- package/dist/apps/permissions.js +255 -0
- package/dist/boot-capability-route.d.ts +15 -0
- package/dist/boot-capability-route.js +196 -0
- package/dist/boot-capability.d.ts +103 -0
- package/dist/boot-capability.js +141 -0
- package/dist/boot-construction.d.ts +12 -0
- package/dist/boot-construction.js +175 -0
- package/dist/boot-gate.d.ts +267 -0
- package/dist/boot-gate.js +236 -0
- package/dist/boot-host-script.d.ts +2 -0
- package/dist/boot-host-script.js +1 -0
- package/dist/boot-mount.d.ts +21 -0
- package/dist/boot-mount.js +65 -0
- package/dist/browser/boot.d.ts +73 -0
- package/dist/browser/boot.js +502 -0
- package/dist/browser/browser.d.ts +104 -0
- package/dist/browser/browser.js +374 -0
- package/dist/browser/capability-source.d.ts +76 -0
- package/dist/browser/capability-source.js +338 -0
- package/dist/browser/cdp-client.d.ts +56 -0
- package/dist/browser/cdp-client.js +184 -0
- package/dist/browser/errors.d.ts +79 -0
- package/dist/browser/errors.js +32 -0
- package/dist/browser/index.d.ts +16 -0
- package/dist/browser/index.js +14 -0
- package/dist/browser/lease-registry.d.ts +61 -0
- package/dist/browser/lease-registry.js +101 -0
- package/dist/browser/operations.d.ts +225 -0
- package/dist/browser/operations.js +137 -0
- package/dist/browser/page.d.ts +25 -0
- package/dist/browser/page.js +172 -0
- package/dist/browser/quick-reads.d.ts +29 -0
- package/dist/browser/quick-reads.js +160 -0
- package/dist/browser/sessions.d.ts +52 -0
- package/dist/browser/sessions.js +118 -0
- package/dist/browser/url-policy.d.ts +18 -0
- package/dist/browser/url-policy.js +58 -0
- package/dist/build/app-imports.d.ts +12 -0
- package/dist/build/app-imports.js +105 -0
- package/dist/build/build-policy.d.ts +5 -3
- package/dist/build/build-policy.js +5 -3
- package/dist/build/candidate-builder.d.ts +1 -1
- package/dist/build/candidate-cli.mjs +8375 -334
- package/dist/build/local-build.d.ts +2 -0
- package/dist/build/local-build.js +28 -6
- package/dist/build/snapshot-package-store.d.ts +1 -1
- package/dist/build/snapshot-package-store.js +7 -2
- package/dist/build/workspace-build.d.ts +92 -5
- package/dist/build/workspace-build.js +93 -19
- package/dist/build.d.ts +1 -1
- package/dist/build.js +1 -1
- package/dist/catalog-frame.js +1 -1
- package/dist/cloudflare.d.ts +13 -3
- package/dist/cloudflare.js +102 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.js +4 -2
- package/dist/git-auth.d.ts +0 -2
- package/dist/git-auth.js +0 -2
- package/dist/git.d.ts +0 -1
- package/dist/git.js +0 -2
- package/dist/host.d.ts +39 -13
- package/dist/host.js +268 -142
- package/dist/index.d.ts +25 -15
- package/dist/index.js +39 -16
- package/dist/install-release-cli.d.ts +2 -0
- package/dist/install-release-cli.js +46 -0
- package/dist/installation-client.d.ts +43 -0
- package/dist/installation-client.js +178 -0
- package/dist/installation-execution.d.ts +11 -0
- package/dist/installation-execution.js +184 -0
- package/dist/installation-operations.d.ts +1437 -0
- package/dist/installation-operations.js +664 -0
- package/dist/installation-store-generation.d.ts +3 -0
- package/dist/installation-store-generation.js +16 -0
- package/dist/instance-id.d.ts +10 -0
- package/dist/instance-id.js +10 -0
- package/dist/instance-refusal.d.ts +11 -0
- package/dist/instance-refusal.js +21 -0
- package/dist/internal/delivery.js +4 -2
- package/dist/internal/host-gate.js +7 -1
- package/dist/internal/registry-installations.js +4 -4
- package/dist/keys.d.ts +5 -1
- package/dist/keys.js +10 -2
- package/dist/machine-link/authority.d.ts +89 -0
- package/dist/machine-link/authority.js +37 -0
- package/dist/machine-link/client.d.ts +66 -0
- package/dist/machine-link/client.js +94 -0
- package/dist/machine-link/delivery.d.ts +18 -0
- package/dist/machine-link/delivery.js +55 -0
- package/dist/machine-link/enrollment.d.ts +111 -0
- package/dist/machine-link/enrollment.js +286 -0
- package/dist/machine-link/fences.d.ts +37 -0
- package/dist/machine-link/fences.js +166 -0
- package/dist/machine-link/home.d.ts +20 -0
- package/dist/machine-link/home.js +24 -0
- package/dist/machine-link/host.d.ts +43 -0
- package/dist/machine-link/host.js +189 -0
- package/dist/machine-link/index.d.ts +15 -0
- package/dist/machine-link/index.js +15 -0
- package/dist/machine-link/link.d.ts +48 -0
- package/dist/machine-link/link.js +223 -0
- package/dist/machine-link/presence.d.ts +10 -0
- package/dist/machine-link/presence.js +25 -0
- package/dist/machine-link/workers.d.ts +6 -0
- package/dist/machine-link/workers.js +6 -0
- package/dist/mcp-owner.d.ts +2 -2
- package/dist/mcp-owner.js +41 -33
- package/dist/mcp.d.ts +15 -3
- package/dist/mcp.js +146 -25
- package/dist/pack.d.ts +14 -3
- package/dist/pack.js +44 -19
- package/dist/page/app-access.d.ts +7 -0
- package/dist/page/app-access.js +75 -0
- package/dist/page/boot-host.js +57 -0
- package/dist/page/capabilities.d.ts +3 -3
- package/dist/page/capabilities.js +16 -12
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page/computer-target.d.ts +3 -0
- package/dist/page/computer-target.js +9 -0
- package/dist/page/mount.d.ts +23 -3
- package/dist/page/mount.js +152 -16
- package/dist/page-assets.d.ts +22 -0
- package/dist/page-assets.js +28 -0
- package/dist/page-script.d.ts +3 -0
- package/dist/page-script.js +5 -4
- package/dist/release.d.ts +13 -6
- package/dist/release.js +10 -7
- package/dist/root-data.d.ts +16 -6
- package/dist/root-data.js +2 -2
- package/dist/source/index.d.ts +1 -1
- package/dist/source/team-source.d.ts +20 -8
- package/dist/source/team-source.js +39 -10
- package/dist/source/template-seed.d.ts +2 -2
- package/dist/standalone/agent-mail.d.ts +24 -0
- package/dist/standalone/agent-mail.js +62 -0
- package/dist/standalone/agent-store.d.ts +9 -0
- package/dist/standalone/agent-store.js +30 -0
- package/dist/standalone/agents.d.ts +68 -9
- package/dist/standalone/agents.js +428 -56
- package/dist/standalone/app-builds-binding.d.ts +20 -2
- package/dist/standalone/app-builds-binding.js +147 -23
- package/dist/standalone/app-set.d.ts +5 -2
- package/dist/standalone/app-set.js +174 -143
- package/dist/standalone/browser.d.ts +11 -0
- package/dist/standalone/browser.js +72 -0
- package/dist/standalone/builds.d.ts +13 -4
- package/dist/standalone/builds.js +28 -44
- package/dist/standalone/computer-egress.d.ts +2 -0
- package/dist/standalone/computer-egress.js +13 -4
- package/dist/standalone/computer-fleet.d.ts +43 -0
- package/dist/standalone/computer-fleet.js +200 -0
- package/dist/standalone/computer.d.ts +44 -9
- package/dist/standalone/computer.js +129 -22
- package/dist/standalone/install-apps-cli.js +43 -6
- package/dist/standalone/instance.d.ts +2 -2
- package/dist/standalone/instance.js +8 -11
- package/dist/standalone/machine-link.d.ts +354 -0
- package/dist/standalone/machine-link.js +368 -0
- package/dist/standalone/media.d.ts +2 -2
- package/dist/standalone/media.js +19 -16
- package/dist/standalone/preview.d.ts +2 -2
- package/dist/standalone/preview.js +37 -10
- package/dist/standalone/registry.d.ts +149 -9
- package/dist/standalone/registry.js +299 -8
- package/dist/standalone/worker.d.ts +19 -5
- package/dist/standalone/worker.js +90 -54
- package/dist/standalone/workflow-binding.d.ts +18 -22
- package/dist/standalone/workflow-binding.js +18 -18
- package/dist/standalone/workflow-control.d.ts +4 -0
- package/dist/standalone/workflow-control.js +40 -0
- package/dist/standalone/workflow-events.d.ts +2 -2
- package/dist/standalone/workflow-events.js +9 -10
- package/dist/standalone/workflow-git-capture.js +4 -3
- package/dist/standalone/workflow-host.d.ts +162 -161
- package/dist/standalone/workflow-host.js +101 -21
- package/dist/standalone/workflow-node-capture.js +11 -4
- package/dist/standalone/workflow-schedules.d.ts +5 -2
- package/dist/standalone/workflow-schedules.js +9 -10
- package/dist/standalone/workflow-trigger-host.d.ts +65 -5
- package/dist/standalone/workflow-trigger-host.js +2 -2
- package/dist/standalone/workflow-webhooks.d.ts +22 -0
- package/dist/standalone/workflow-webhooks.js +39 -0
- package/dist/standalone/workflows.d.ts +140 -43
- package/dist/standalone/workflows.js +198 -39
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.js +14 -3
- package/dist/testing/installation.js +26 -2
- package/dist/testing/worker.js +1 -1
- package/dist/testing/workers-runtime.d.ts +8 -2
- package/dist/testing/workers-runtime.js +20 -2
- package/dist/webhook-abuse.d.ts +25 -0
- package/dist/webhook-abuse.js +54 -0
- package/dist/webhook-custody.d.ts +144 -0
- package/dist/webhook-custody.js +222 -0
- package/dist/webhook-ingress.d.ts +36 -0
- package/dist/webhook-ingress.js +146 -0
- package/dist/webhooks.d.ts +111 -0
- package/dist/webhooks.js +189 -0
- package/dist/workers-git.d.ts +12 -17
- package/dist/workers-git.js +60 -52
- package/dist/workers.d.ts +224 -13
- package/dist/workers.js +351 -51
- package/dist/workflow-agent-authority.d.ts +8 -0
- package/dist/workflow-agent-authority.js +42 -0
- package/dist/workflows.d.ts +66 -0
- package/dist/workflows.js +97 -0
- package/docs/api/README.md +63 -37
- package/docs/api/agents.md +17 -0
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +1394 -543
- package/docs/api/browser/boot.md +132 -0
- package/docs/api/browser.md +1440 -0
- package/docs/api/build-contract.md +38 -25
- package/docs/api/build.md +263 -84
- package/docs/api/cloudflare.md +93 -7
- package/docs/api/git.md +3 -13
- package/docs/api/host.md +944 -118
- package/docs/api/index.md +320 -18
- package/docs/api/keys.md +24 -1
- package/docs/api/mcp.md +59 -12
- package/docs/api/modules.md +33 -40
- package/docs/api/pack.md +57 -8
- package/docs/api/page/mount.md +92 -4
- package/docs/api/workers-git.md +103 -24
- package/docs/api/workers.md +1581 -244
- package/docs/site/agent-quickstart.mdx +28 -14
- package/examples/standalone/worker.ts +3 -1
- package/examples/standalone/wrangler.jsonc +2 -0
- package/package.json +73 -71
- package/dist/installation-consent.d.ts +0 -81
- package/dist/installation-consent.js +0 -84
- package/dist/shell/app-directory.d.ts +0 -542
- package/dist/shell/app-directory.js +0 -196
- package/dist/shell/browser-actions.d.ts +0 -7
- package/dist/shell/browser-actions.js +0 -56
- package/dist/shell/command-host.d.ts +0 -112
- package/dist/shell/command-host.js +0 -502
- package/dist/shell/commands.d.ts +0 -25
- package/dist/shell/commands.js +0 -78
- package/dist/shell/index.d.ts +0 -8
- package/dist/shell/index.js +0 -8
- package/dist/shell/keys.d.ts +0 -41
- package/dist/shell/keys.js +0 -134
- package/dist/shell/shell-module.d.ts +0 -55
- package/dist/shell/shell-module.js +0 -86
- package/dist/shell/window-manager.d.ts +0 -128
- package/dist/shell/window-manager.js +0 -730
- package/dist/shell/workbench-contract.d.ts +0 -68
- package/dist/shell/workbench-directory.d.ts +0 -332
- package/dist/shell/workbench-directory.js +0 -35
- package/dist/shell/workbench-host-types.d.ts +0 -96
- package/dist/shell/workbench-host-types.js +0 -1
- package/dist/shell/workbench-host.d.ts +0 -34
- package/dist/shell/workbench-host.js +0 -352
- package/dist/shell/workbench-model.d.ts +0 -194
- package/dist/shell/workbench-model.js +0 -53
- package/dist/shell/workbench.d.ts +0 -4
- package/dist/shell/workbench.js +0 -2
- package/docs/api/shell/app-directory.md +0 -599
- package/docs/api/shell/command-host.md +0 -224
- package/docs/api/shell/commands.md +0 -197
- package/docs/api/shell/keys.md +0 -183
- package/docs/api/shell/shell-module.md +0 -204
- package/docs/api/shell/window-manager.md +0 -267
- package/docs/api/shell/workbench-directory.md +0 -119
- package/docs/api/shell/workbench.md +0 -205
- package/docs/api/shell.md +0 -1915
- /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
package/dist/mcp-owner.js
CHANGED
|
@@ -2,7 +2,6 @@ import { DurableObject } from "cloudflare:workers";
|
|
|
2
2
|
import { nanoid } from "@fungi.computer/cairn";
|
|
3
3
|
import { createCodemodeSandbox } from "@fungi.computer/mycelium/workers";
|
|
4
4
|
import * as Schema from "effect/Schema";
|
|
5
|
-
import { oysterMcpCatalogLoad } from "./mcp.js";
|
|
6
5
|
import { tenantIdSchema, tenantScope } from "./tenant.js";
|
|
7
6
|
/**
|
|
8
7
|
* The MCP protocol SDK builds hundreds of schemas when it loads. Only the first
|
|
@@ -10,11 +9,12 @@ import { tenantIdSchema, tenantScope } from "./tenant.js";
|
|
|
10
9
|
* (which every request and every deploy pays) never does.
|
|
11
10
|
*/
|
|
12
11
|
const loadMcpProtocol = async () => {
|
|
13
|
-
const [mycelium, sdk] = await Promise.all([
|
|
12
|
+
const [mycelium, sdk, catalog] = await Promise.all([
|
|
14
13
|
import("@fungi.computer/mycelium/mcp"),
|
|
15
14
|
import("@modelcontextprotocol/server"),
|
|
15
|
+
import("./mcp.js"),
|
|
16
16
|
]);
|
|
17
|
-
return { mycelium, sdk };
|
|
17
|
+
return { mycelium, sdk, catalog };
|
|
18
18
|
};
|
|
19
19
|
const pinSchema = Schema.Struct({
|
|
20
20
|
tenant_id: tenantIdSchema,
|
|
@@ -97,39 +97,47 @@ export class OysterMcpOwner extends DurableObject {
|
|
|
97
97
|
else {
|
|
98
98
|
sql.exec("INSERT INTO oyster_mcp_tenant (id, tenant_id, origin) VALUES (1,?,?)", scope.tenantId, scope.origin);
|
|
99
99
|
}
|
|
100
|
-
// Loaded before the check so the session map is still created exactly once.
|
|
101
|
-
const protocol = await loadMcpProtocol();
|
|
102
100
|
if (this.#mcp === undefined) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
101
|
+
// Publish the flight before any await: live SDK sessions belong to one owner.
|
|
102
|
+
this.#mcp = loadMcpProtocol()
|
|
103
|
+
.then(async (protocol) => {
|
|
104
|
+
const host = await this.#createHost(this.env, scope);
|
|
105
|
+
const persistence = {
|
|
106
|
+
eventStore: (id) => this.#eventStore(id, protocol),
|
|
107
|
+
initialized: (id, session) => {
|
|
108
|
+
sql.exec("INSERT INTO oyster_mcp_sessions (protocol_id, catalog_id, principal) VALUES (?,?,?)", id, session.id, JSON.stringify(session.principal));
|
|
109
|
+
return Promise.resolve();
|
|
110
|
+
},
|
|
111
|
+
closed: (id, session) => {
|
|
112
|
+
this.ctx.storage.transactionSync(() => {
|
|
113
|
+
sql.exec("DELETE FROM oyster_mcp_events WHERE catalog_id=?", session.id);
|
|
114
|
+
sql.exec("DELETE FROM oyster_mcp_sessions WHERE protocol_id=?", id);
|
|
115
|
+
});
|
|
116
|
+
return Promise.resolve();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
return protocol.mycelium.createMcp({
|
|
120
|
+
persistence,
|
|
121
|
+
catalog: protocol.mycelium.createMcpCatalog({
|
|
122
|
+
// Each acquisition captures the current durable selection. Existing
|
|
123
|
+
// operation handles retain the Computer captured by their own catalog.
|
|
124
|
+
load: async (principal) => protocol.catalog.oysterMcpCatalogLoad(await this.#createHost(this.env, scope))(principal),
|
|
125
|
+
sandbox: createCodemodeSandbox(this.env.LOADER),
|
|
126
|
+
}),
|
|
127
|
+
authenticate: async (incoming) => {
|
|
128
|
+
const admission = await host.auth.authenticate(incoming);
|
|
129
|
+
return admission.kind === "refused"
|
|
130
|
+
? admission
|
|
131
|
+
: { kind: "admitted", principal: admission.principal };
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
})
|
|
135
|
+
.catch((error) => {
|
|
136
|
+
this.#mcp = undefined;
|
|
137
|
+
throw error;
|
|
130
138
|
});
|
|
131
139
|
}
|
|
132
|
-
return this.#mcp.handleRequest(request);
|
|
140
|
+
return (await this.#mcp).handleRequest(request);
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
143
|
/** Bind the owner to the deployment's existing host factory and options. */
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AppToolPolicy } from "./apps/tools.js";
|
|
2
2
|
import type { DeclaredOperations, Installation } from "./apps/installation.js";
|
|
3
|
-
import { type CapabilitySource, type Grants } from "@fungi.computer/mycelium";
|
|
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
6
|
import type { McpPrincipal, McpCatalogSnapshot } from "@fungi.computer/mycelium/mcp";
|
|
@@ -52,6 +52,18 @@ export type ReleaseSurface = Readonly<{
|
|
|
52
52
|
export type AppSurfaceCache = (release: Installation["release"]) => Promise<ReleaseSurface | null>;
|
|
53
53
|
/** Make one {@link AppSurfaceCache} for a host whose artifact store and capabilities stay fixed. */
|
|
54
54
|
export declare function appSurfaceCache(host: Pick<OysterHost, "artifacts" | "capabilities">): AppSurfaceCache;
|
|
55
|
+
/** Derive the current principal's admitted App modules and grants. */
|
|
56
|
+
export declare function oysterAppSnapshot(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "capabilities" | "appToolPolicy" | "keys">, surfaces: AppSurfaceCache, principal: OysterPrincipal, installations: readonly Installation[], access: OysterCallerAccess, sessionId?: string,
|
|
57
|
+
/** Shares one tool-family listing per Instance across the two
|
|
58
|
+
* `appModules` passes one `oysterAppMethods` call can make (the eager
|
|
59
|
+
* grants/version pass, and a session-scoped re-derivation); never shared
|
|
60
|
+
* across separate calls, so it never serves another request's listing. */
|
|
61
|
+
familyListingCache?: Map<string, unknown>): Promise<Readonly<{
|
|
62
|
+
modules: Readonly<Record<string, ModuleRegistration>>;
|
|
63
|
+
levels: Readonly<Record<string, AppToolPolicy["level"]>>;
|
|
64
|
+
fences: Readonly<Record<string, string>>;
|
|
65
|
+
effects: Readonly<Record<string, Readonly<Record<string, "read" | "write" | "external">>>>;
|
|
66
|
+
}>>;
|
|
55
67
|
/**
|
|
56
68
|
* Capture installed App methods for MCP or agents as one principal-scoped source.
|
|
57
69
|
* Enabled installations become `env.<APP>` modules using the Instance call path; a dotted method such as `repos.list` is `env.<APP>.repos.list()`.
|
|
@@ -62,7 +74,7 @@ export declare function appSurfaceCache(host: Pick<OysterHost, "artifacts" | "ca
|
|
|
62
74
|
* (`mycelium/src/access.ts`). Call again to capture changes to Releases,
|
|
63
75
|
* installation authority or the caller's own grants.
|
|
64
76
|
*/
|
|
65
|
-
export declare function oysterAppMethods(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "capabilities" | "appToolPolicy" | "keys">, principal: OysterPrincipal, access: OysterCallerAccess, surfaces: AppSurfaceCache, preloadedInstallations?: readonly Installation[]): Promise<CapabilitySource & {
|
|
77
|
+
export declare function oysterAppMethods(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "capabilities" | "appToolPolicy" | "keys" | "approvals">, principal: OysterPrincipal, access: OysterCallerAccess, surfaces: AppSurfaceCache, preloadedInstallations?: readonly Installation[]): Promise<CapabilitySource & {
|
|
66
78
|
readonly grants: Grants;
|
|
67
79
|
}>;
|
|
68
80
|
/**
|
|
@@ -86,4 +98,4 @@ export declare function oysterComputerTools(host: Pick<OysterHost, "computerId"
|
|
|
86
98
|
* still re-admits the key, rechecks that same stored approval, and passes
|
|
87
99
|
* the App's gate.
|
|
88
100
|
*/
|
|
89
|
-
export declare function oysterMcpCatalogLoad(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "computer" | "capabilities" | "appToolPolicy" | "keys">): (principal: McpPrincipal) => Promise<McpCatalogSnapshot>;
|
|
101
|
+
export declare function oysterMcpCatalogLoad(host: Pick<OysterHost, "installations" | "instances" | "artifacts" | "guestLimits" | "backgroundOriginKey" | "computerId" | "computer" | "capabilities" | "appToolPolicy" | "keys" | "workflows">): (principal: McpPrincipal) => Promise<McpCatalogSnapshot>;
|
package/dist/mcp.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { describeModule, invokeModuleOperation, } from "@fungi.computer/mycelium/remote";
|
|
3
|
+
import { PendingApprovalSchema, ApprovalRefusalSchema, } from "@fungi.computer/approvals";
|
|
4
|
+
import { jsonSchemaToZod } from "@fungi.computer/forage";
|
|
1
5
|
import { admits } from "./apps/gate.js";
|
|
6
|
+
import { createMcpWorkflowControl, WORKFLOW_CONTROL_MODULE_ID, } from "./workflows.js";
|
|
2
7
|
import { causeClass } from "./cause-class.js";
|
|
3
8
|
import { deriveDeclaredOperations } from "./apps/operations.js";
|
|
4
9
|
import { appToolPolicySchema } from "./apps/tools.js";
|
|
@@ -77,18 +82,22 @@ export function appSurfaceCache(host) {
|
|
|
77
82
|
return entry;
|
|
78
83
|
};
|
|
79
84
|
}
|
|
80
|
-
|
|
85
|
+
/** Derive the current principal's admitted App modules and grants. */
|
|
86
|
+
export async function oysterAppSnapshot(host, surfaces, principal, installations, access, sessionId,
|
|
81
87
|
/** Shares one tool-family listing per Instance across the two
|
|
82
88
|
* `appModules` passes one `oysterAppMethods` call can make (the eager
|
|
83
89
|
* grants/version pass, and a session-scoped re-derivation); never shared
|
|
84
90
|
* across separate calls, so it never serves another request's listing. */
|
|
85
91
|
familyListingCache = new Map()) {
|
|
86
92
|
const limits = guestLimits(host.guestLimits);
|
|
93
|
+
const generation = await host.installations.generation();
|
|
87
94
|
const caller = invocationCallerContext(principal, host.computerId ?? null);
|
|
88
95
|
const { actor } = principal;
|
|
89
96
|
const visitor = actor.kind === "visitor";
|
|
90
97
|
const modules = {};
|
|
91
98
|
const levels = {};
|
|
99
|
+
const fences = {};
|
|
100
|
+
const effects = {};
|
|
92
101
|
for (const installation of installations) {
|
|
93
102
|
if (installation.status !== "enabled" || installation.instanceId === null)
|
|
94
103
|
continue;
|
|
@@ -125,8 +134,8 @@ familyListingCache = new Map()) {
|
|
|
125
134
|
signal?.throwIfAborted();
|
|
126
135
|
if (currentInstallation?.status !== "enabled" ||
|
|
127
136
|
currentInstallation.instanceId !== instanceId ||
|
|
128
|
-
currentInstallation.authorityVersion !==
|
|
129
|
-
installation.authorityVersion ||
|
|
137
|
+
currentInstallation.authority.authorityVersion !==
|
|
138
|
+
installation.authority.authorityVersion ||
|
|
130
139
|
currentInstallation.release.releaseId !==
|
|
131
140
|
installation.release.releaseId ||
|
|
132
141
|
currentInstallation.release.artifact.artifactDigest !==
|
|
@@ -156,7 +165,13 @@ familyListingCache = new Map()) {
|
|
|
156
165
|
signal?.throwIfAborted();
|
|
157
166
|
const json = Schema.decodeUnknownSync(Schema.Json)(input);
|
|
158
167
|
const decision = admits({
|
|
159
|
-
installation
|
|
168
|
+
installation: {
|
|
169
|
+
status: installation.status,
|
|
170
|
+
visibility: installation.visibility,
|
|
171
|
+
instanceId: installation.instanceId,
|
|
172
|
+
authority: installation.authority,
|
|
173
|
+
artifactDigest: installation.release.artifact.artifactDigest,
|
|
174
|
+
},
|
|
160
175
|
declaration,
|
|
161
176
|
declaredOperations,
|
|
162
177
|
api,
|
|
@@ -187,9 +202,13 @@ familyListingCache = new Map()) {
|
|
|
187
202
|
throw new AppMethodRefused(result.value.code, result.value.message);
|
|
188
203
|
return result.value.value;
|
|
189
204
|
};
|
|
205
|
+
const operationEffects = {};
|
|
206
|
+
effects[id] = operationEffects;
|
|
207
|
+
let familyFacts = null;
|
|
190
208
|
const toolOperations = Object.entries(toolSchemas)
|
|
191
209
|
.filter(([, method]) => !visitor || method.effect === "read")
|
|
192
210
|
.map(([name, method]) => {
|
|
211
|
+
operationEffects[name] = method.effect;
|
|
193
212
|
const appMethod = Mycelium.operation({
|
|
194
213
|
replay: "unsafe",
|
|
195
214
|
access: method.effect === "read" ? "read" : "write",
|
|
@@ -225,7 +244,10 @@ familyListingCache = new Map()) {
|
|
|
225
244
|
const compiled = appToolFamilySchemas(api, listing);
|
|
226
245
|
if (compiled === null)
|
|
227
246
|
return [];
|
|
247
|
+
familyFacts = Schema.decodeUnknownSync(Schema.Json)(listing);
|
|
228
248
|
return compiled.tools.map((tool) => {
|
|
249
|
+
operationEffects[compiled.name + "." + tool.row.tool] =
|
|
250
|
+
compiled.effect;
|
|
229
251
|
const operation = Mycelium.operation({
|
|
230
252
|
replay: "unsafe",
|
|
231
253
|
access: compiled.effect === "read" ? "read" : "write",
|
|
@@ -274,6 +296,15 @@ familyListingCache = new Map()) {
|
|
|
274
296
|
return [];
|
|
275
297
|
}
|
|
276
298
|
})();
|
|
299
|
+
fences[id] = await sha256(new TextEncoder().encode(JSON.stringify({
|
|
300
|
+
installation,
|
|
301
|
+
generation,
|
|
302
|
+
principal,
|
|
303
|
+
access: captured,
|
|
304
|
+
api,
|
|
305
|
+
family: familyFacts,
|
|
306
|
+
computerId: host.computerId ?? null,
|
|
307
|
+
})));
|
|
277
308
|
modules[id] = Mycelium.module({
|
|
278
309
|
id,
|
|
279
310
|
name: appBindingName(installation.installationId),
|
|
@@ -281,7 +312,7 @@ familyListingCache = new Map()) {
|
|
|
281
312
|
operations: Object.fromEntries([...toolOperations, ...familyOperations]),
|
|
282
313
|
});
|
|
283
314
|
}
|
|
284
|
-
return { modules, levels };
|
|
315
|
+
return { modules, levels, fences, effects };
|
|
285
316
|
}
|
|
286
317
|
function serializableAccess(access) {
|
|
287
318
|
return access === null || access.kind === "agent"
|
|
@@ -307,8 +338,8 @@ export async function oysterAppMethods(host, principal, access, surfaces, preloa
|
|
|
307
338
|
// Shared so a session-scoped re-derivation below reuses this call's tool-
|
|
308
339
|
// family listings instead of asking each Instance for them twice.
|
|
309
340
|
const familyListingCache = new Map();
|
|
310
|
-
const
|
|
311
|
-
const modules =
|
|
341
|
+
const initial = await oysterAppSnapshot(host, surfaces, principal, installations, access, undefined, familyListingCache);
|
|
342
|
+
const modules = admittedAppModules(host, principal, initial, null);
|
|
312
343
|
const version = await sha256(new TextEncoder().encode(JSON.stringify({
|
|
313
344
|
access: serializableAccess(access),
|
|
314
345
|
installations: [...installations]
|
|
@@ -318,19 +349,19 @@ export async function oysterAppMethods(host, principal, access, surfaces, preloa
|
|
|
318
349
|
installation.instanceId,
|
|
319
350
|
installation.release.releaseId,
|
|
320
351
|
installation.release.artifact.artifactDigest,
|
|
321
|
-
installation.authorityVersion,
|
|
352
|
+
installation.authority.authorityVersion,
|
|
322
353
|
installation.status,
|
|
323
354
|
installation.visibility,
|
|
324
355
|
]),
|
|
325
356
|
})));
|
|
326
357
|
return {
|
|
327
358
|
id: "oyster-apps",
|
|
328
|
-
grants: levels,
|
|
359
|
+
grants: initial.levels,
|
|
329
360
|
acquire: async ({ signal, session }) => {
|
|
330
361
|
signal.throwIfAborted();
|
|
331
362
|
const acquiredModules = session === undefined
|
|
332
363
|
? modules
|
|
333
|
-
:
|
|
364
|
+
: admittedAppModules(host, principal, await oysterAppSnapshot(host, surfaces, principal, installations, access, session.sessionId, familyListingCache), session.sessionId);
|
|
334
365
|
signal.throwIfAborted();
|
|
335
366
|
return {
|
|
336
367
|
snapshot: { version, modules: acquiredModules },
|
|
@@ -402,28 +433,118 @@ export function oysterMcpCatalogLoad(host) {
|
|
|
402
433
|
access = { kind: "key", keyId: actor.keyId, byInstallation };
|
|
403
434
|
}
|
|
404
435
|
const apps = await oysterAppMethods(host, principal, access, surfaces, installations);
|
|
405
|
-
// Pre-approval ("never asked") is what a captured Agent/key policy
|
|
406
|
-
// itself grants; an owner's own calls, or an Agent/key with none, are
|
|
407
|
-
// never pre-approved even though their modules still default to the
|
|
408
|
-
// coarse "write" ceiling above.
|
|
409
|
-
const approvedWrites = access === null
|
|
410
|
-
? []
|
|
411
|
-
: Object.entries(apps.grants)
|
|
412
|
-
.filter(([, level]) => level === "write")
|
|
413
|
-
.map(([id]) => id);
|
|
414
436
|
const computer = await oysterComputerTools(host, principal);
|
|
437
|
+
const control = await host.workflows?.(principal);
|
|
438
|
+
const workflowSources = control == null
|
|
439
|
+
? []
|
|
440
|
+
: [
|
|
441
|
+
{
|
|
442
|
+
id: "oyster-workflows",
|
|
443
|
+
acquire: () => Promise.resolve({
|
|
444
|
+
snapshot: {
|
|
445
|
+
version: WORKFLOW_CONTROL_MODULE_ID,
|
|
446
|
+
modules: [createMcpWorkflowControl(control).module],
|
|
447
|
+
},
|
|
448
|
+
release: () => Promise.resolve(),
|
|
449
|
+
}),
|
|
450
|
+
},
|
|
451
|
+
];
|
|
452
|
+
const workflowGrants = control == null ? {} : { [WORKFLOW_CONTROL_MODULE_ID]: "write" };
|
|
415
453
|
if (computer === null)
|
|
416
454
|
return {
|
|
417
455
|
instructions: "Installed Apps on this Oyster. Each App is env.<APP>; dotted method names are nested properties, e.g. env.BONSAI.repos.list({}). Use forage.search / forage.describe to find methods.",
|
|
418
|
-
grants: apps.grants,
|
|
419
|
-
|
|
420
|
-
sources: [apps],
|
|
456
|
+
grants: { ...apps.grants, ...workflowGrants },
|
|
457
|
+
sources: [apps, ...workflowSources],
|
|
421
458
|
};
|
|
422
459
|
return {
|
|
423
460
|
instructions: 'Installed Apps and this Oyster\'s Computer. Each App is env.<APP>; dotted method names are nested properties, e.g. env.BONSAI.repos.list({}). The Computer is env.computers.workspace, e.g. env.computers.workspace.exec({command: "ls"}). Use forage.search / forage.describe to find methods.',
|
|
424
|
-
grants: { ...apps.grants, ...computer.grants },
|
|
425
|
-
|
|
426
|
-
sources: [apps, computer],
|
|
461
|
+
grants: { ...apps.grants, ...computer.grants, ...workflowGrants },
|
|
462
|
+
sources: [apps, computer, ...workflowSources],
|
|
427
463
|
};
|
|
428
464
|
};
|
|
429
465
|
}
|
|
466
|
+
function admittedAppModules(host, principal, snapshot, sessionId) {
|
|
467
|
+
return Object.entries(snapshot.modules).map(([id, raw]) => {
|
|
468
|
+
const { description: moduleDescription, ...descriptor } = describeModule(raw);
|
|
469
|
+
const installationId = id.slice("oyster-app:".length);
|
|
470
|
+
const moduleMetadata = {};
|
|
471
|
+
if (moduleDescription !== undefined)
|
|
472
|
+
moduleMetadata.description = moduleDescription;
|
|
473
|
+
return Mycelium.module({
|
|
474
|
+
...descriptor,
|
|
475
|
+
...moduleMetadata,
|
|
476
|
+
id,
|
|
477
|
+
name: appBindingName(installationId),
|
|
478
|
+
operations: Object.fromEntries(Object.entries(descriptor.operations).map(([path, rawOperation]) => {
|
|
479
|
+
const { description: operationDescription, ...operation } = rawOperation;
|
|
480
|
+
const effect = snapshot.effects[id]?.[path];
|
|
481
|
+
const fence = snapshot.fences[id];
|
|
482
|
+
if (effect === undefined || fence === undefined)
|
|
483
|
+
throw new Error("App snapshot has no operation authority facts");
|
|
484
|
+
const declaredOutput = operation.output ?? true;
|
|
485
|
+
// Decode each document at its own root before composing: a nested
|
|
486
|
+
// JSON Schema document's root-relative $refs would otherwise escape.
|
|
487
|
+
const declared = jsonSchemaToZod(declaredOutput);
|
|
488
|
+
const output = effect === "read"
|
|
489
|
+
? declared
|
|
490
|
+
: z.union([
|
|
491
|
+
declared,
|
|
492
|
+
jsonSchemaToZod(Schema.toStandardJSONSchemaV1(PendingApprovalSchema)["~standard"].jsonSchema.output({ target: "draft-2020-12" })),
|
|
493
|
+
jsonSchemaToZod(Schema.toStandardJSONSchemaV1(ApprovalRefusalSchema)["~standard"].jsonSchema.output({ target: "draft-2020-12" })),
|
|
494
|
+
]);
|
|
495
|
+
const outputValidator = z.intersection(output, z.json());
|
|
496
|
+
const operationMetadata = {};
|
|
497
|
+
if (operationDescription !== undefined)
|
|
498
|
+
operationMetadata.description = operationDescription;
|
|
499
|
+
// App methods are never replayed by the host: a write goes through
|
|
500
|
+
// its approval record, and even a read may have App-side effects.
|
|
501
|
+
const appOperation = Mycelium.operation({
|
|
502
|
+
...operation,
|
|
503
|
+
...operationMetadata,
|
|
504
|
+
replay: "unsafe",
|
|
505
|
+
access: effect === "read" ? "read" : "write",
|
|
506
|
+
input: z.intersection(jsonSchemaToZod(operation.input), z.json()),
|
|
507
|
+
output: {
|
|
508
|
+
"~standard": {
|
|
509
|
+
...outputValidator["~standard"],
|
|
510
|
+
// As appMethodSchemas does, describe the JSON contract
|
|
511
|
+
// without nesting another redundant recursive JSON guard.
|
|
512
|
+
jsonSchema: output["~standard"].jsonSchema,
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
execute: async (input, context) => {
|
|
516
|
+
if (effect === "read")
|
|
517
|
+
return z
|
|
518
|
+
.json()
|
|
519
|
+
.parse(await invokeModuleOperation(raw, path, input, context));
|
|
520
|
+
if (principal.actor.kind === "visitor")
|
|
521
|
+
return { kind: "refused", reason: "not-granted" };
|
|
522
|
+
if (host.approvals === undefined)
|
|
523
|
+
return { kind: "refused", reason: "not-granted" };
|
|
524
|
+
// Mycelium numbers each operation call within one execute and
|
|
525
|
+
// replays the same sequence; a direct call is its own execution.
|
|
526
|
+
const ordinal = context.operationOrdinal ?? 0;
|
|
527
|
+
const result = await host.approvals.submit({
|
|
528
|
+
actor: principal.actor,
|
|
529
|
+
installationId,
|
|
530
|
+
sessionId,
|
|
531
|
+
operation: path,
|
|
532
|
+
access: effect,
|
|
533
|
+
authorityFence: fence,
|
|
534
|
+
idempotencyKey: await sha256(new TextEncoder().encode(JSON.stringify([
|
|
535
|
+
context.executionId,
|
|
536
|
+
context.toolCallId,
|
|
537
|
+
ordinal,
|
|
538
|
+
]))),
|
|
539
|
+
input,
|
|
540
|
+
});
|
|
541
|
+
return z
|
|
542
|
+
.json()
|
|
543
|
+
.parse(result.kind === "applied" ? result.output : result);
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
return [path, appOperation];
|
|
547
|
+
})),
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
}
|
package/dist/pack.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { type InstallationObservationBudget } from "./installation-client.js";
|
|
1
2
|
import { type ArtifactRef, type Release } from "@fungi.computer/puffball";
|
|
2
3
|
import { type AppPublisherCoreConfigInput } from "./build.js";
|
|
4
|
+
import type { InstallationOperation } from "./installation-operations.js";
|
|
5
|
+
export { InstallationObservationError } from "./installation-client.js";
|
|
3
6
|
/** One committed repository and its canonical App build recipe. */
|
|
4
7
|
export type PackOptions = Readonly<{
|
|
5
8
|
/** The App's ID, `app_<name>`. */
|
|
@@ -32,6 +35,8 @@ export declare function packRelease(options: PackOptions): Promise<PackedRelease
|
|
|
32
35
|
* Capture an Artifacts Git commit, build its Release, upload it to an Oyster,
|
|
33
36
|
* and install it, or switch an existing installation to it
|
|
34
37
|
* ({@link installRelease}).
|
|
38
|
+
* `waitMs` starts with the upload/observation phase after packing; a supplied
|
|
39
|
+
* caller signal retains its original lifetime through that phase.
|
|
35
40
|
*/
|
|
36
41
|
export declare function installFromSource(input: Readonly<{
|
|
37
42
|
appId: string;
|
|
@@ -49,11 +54,15 @@ export declare function installFromSource(input: Readonly<{
|
|
|
49
54
|
/** Destination Oyster owner key; distinct from the source Git credential. */
|
|
50
55
|
key: string;
|
|
51
56
|
installationId: string;
|
|
52
|
-
}>): Promise<Response>;
|
|
57
|
+
}> & InstallationObservationBudget): Promise<Response>;
|
|
53
58
|
/**
|
|
54
59
|
* Upload a packed Release to an Oyster and install it. The host installs a
|
|
55
60
|
* new installation (201) or switches an existing one to this Release (200),
|
|
56
|
-
* keeping its state;
|
|
61
|
+
* keeping its state. Uploads precede admission; this helper then waits for the
|
|
62
|
+
* correlated terminal owner receipt, not merely the HTTP acknowledgement.
|
|
63
|
+
* Uploads and observation share the caller signal or explicit `waitMs`; absent
|
|
64
|
+
* either, the installer owns a twenty-minute default. Expiry does not cancel
|
|
65
|
+
* admitted work and is reported as an unconfirmed observation, not refusal.
|
|
57
66
|
*/
|
|
58
67
|
export declare function installRelease(input: Readonly<{
|
|
59
68
|
directory: string;
|
|
@@ -61,7 +70,9 @@ export declare function installRelease(input: Readonly<{
|
|
|
61
70
|
/** The Oyster owner key. */
|
|
62
71
|
key: string;
|
|
63
72
|
installationId?: string;
|
|
64
|
-
|
|
73
|
+
/** Report owner-correlated admission without shortening the completion wait. */
|
|
74
|
+
onAdmitted?: (operation: InstallationOperation) => void;
|
|
75
|
+
}> & InstallationObservationBudget): Promise<Response>;
|
|
65
76
|
/** Project one exact first-party commit's editable source, `git push` it to
|
|
66
77
|
* an Oyster's own template for that App, and claim the pushed head, so
|
|
67
78
|
* `APPS.fork` and Computer builds can later read it. The operator owner key
|
package/dist/pack.js
CHANGED
|
@@ -4,11 +4,13 @@ import { join, relative, resolve } from "node:path";
|
|
|
4
4
|
import * as Effect from "effect/Effect";
|
|
5
5
|
import * as Schema from "effect/Schema";
|
|
6
6
|
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
7
|
+
import { installOnOyster, installationObservationSignal, completedInstallationResponse, } from "./installation-client.js";
|
|
7
8
|
import { parseArtifactRef, parseReleaseId, parseSha256Digest, parseSourceRevision, } from "@fungi.computer/puffball";
|
|
8
9
|
import { commitPreparedCandidate, createAppCandidatePreparer, createArtifactsSourceCapture, createFirstPartyAppSourceDistribution, createLocalArtifactWriter, createLocalBuildExecutor, createLocalCommitSourceCapture, normalizeAppPublisherRecipe, } from "./build.js";
|
|
9
10
|
import { templateSeedClaimSchema, } from "./source/template-seed.js";
|
|
10
11
|
import { pushTemplateSource } from "./source/template-push.js";
|
|
11
12
|
import { sha256 } from "./release.js";
|
|
13
|
+
export { InstallationObservationError } from "./installation-client.js";
|
|
12
14
|
async function filesUnder(directory) {
|
|
13
15
|
const entries = await readdir(directory, {
|
|
14
16
|
recursive: true,
|
|
@@ -26,9 +28,9 @@ async function packCapturedSource(options, sourceCapture) {
|
|
|
26
28
|
const preparer = createAppCandidatePreparer({
|
|
27
29
|
config: { appId: options.appId, recipe: options.recipe },
|
|
28
30
|
sourceCapture,
|
|
29
|
-
buildExecutor: createLocalBuildExecutor(
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
buildExecutor: createLocalBuildExecutor(options.packageStoreDirectory === undefined
|
|
32
|
+
? {}
|
|
33
|
+
: { packageStoreDirectory: options.packageStoreDirectory }),
|
|
32
34
|
});
|
|
33
35
|
const prepared = await Effect.runPromise(Effect.scoped(preparer.prepareCandidate(parseReleaseId(options.releaseId ??
|
|
34
36
|
`rel_${revision.slice("git:".length, "git:".length + 12)}`))));
|
|
@@ -52,27 +54,40 @@ export async function packRelease(options) {
|
|
|
52
54
|
* Capture an Artifacts Git commit, build its Release, upload it to an Oyster,
|
|
53
55
|
* and install it, or switch an existing installation to it
|
|
54
56
|
* ({@link installRelease}).
|
|
57
|
+
* `waitMs` starts with the upload/observation phase after packing; a supplied
|
|
58
|
+
* caller signal retains its original lifetime through that phase.
|
|
55
59
|
*/
|
|
56
60
|
export async function installFromSource(input) {
|
|
57
61
|
const outDirectory = await mkdtemp(join(tmpdir(), "oyster-source-release-"));
|
|
58
62
|
try {
|
|
59
|
-
|
|
63
|
+
let options = {
|
|
60
64
|
appId: input.appId,
|
|
61
|
-
releaseId: input.releaseId,
|
|
62
65
|
recipe: input.recipe,
|
|
63
66
|
sourceRevision: input.source.sourceRevision,
|
|
64
|
-
packageStoreDirectory: input.packageStoreDirectory,
|
|
65
67
|
outDirectory,
|
|
66
|
-
}
|
|
68
|
+
};
|
|
69
|
+
if (input.releaseId !== undefined)
|
|
70
|
+
options = { ...options, releaseId: input.releaseId };
|
|
71
|
+
if (input.packageStoreDirectory !== undefined)
|
|
72
|
+
options = {
|
|
73
|
+
...options,
|
|
74
|
+
packageStoreDirectory: input.packageStoreDirectory,
|
|
75
|
+
};
|
|
76
|
+
await packCapturedSource(options, createArtifactsSourceCapture({
|
|
67
77
|
...input.source,
|
|
68
78
|
sourceRevision: parseSourceRevision(input.source.sourceRevision),
|
|
69
79
|
}));
|
|
70
|
-
|
|
80
|
+
let installation = {
|
|
71
81
|
directory: outDirectory,
|
|
72
82
|
url: input.url,
|
|
73
83
|
key: input.key,
|
|
74
84
|
installationId: input.installationId,
|
|
75
|
-
}
|
|
85
|
+
};
|
|
86
|
+
if (input.waitMs !== undefined)
|
|
87
|
+
installation = { ...installation, waitMs: input.waitMs };
|
|
88
|
+
if (input.signal !== undefined)
|
|
89
|
+
installation = { ...installation, signal: input.signal };
|
|
90
|
+
return await installRelease(installation);
|
|
76
91
|
}
|
|
77
92
|
finally {
|
|
78
93
|
await rm(outDirectory, { recursive: true, force: true });
|
|
@@ -81,9 +96,16 @@ export async function installFromSource(input) {
|
|
|
81
96
|
/**
|
|
82
97
|
* Upload a packed Release to an Oyster and install it. The host installs a
|
|
83
98
|
* new installation (201) or switches an existing one to this Release (200),
|
|
84
|
-
* keeping its state;
|
|
99
|
+
* keeping its state. Uploads precede admission; this helper then waits for the
|
|
100
|
+
* correlated terminal owner receipt, not merely the HTTP acknowledgement.
|
|
101
|
+
* Uploads and observation share the caller signal or explicit `waitMs`; absent
|
|
102
|
+
* either, the installer owns a twenty-minute default. Expiry does not cancel
|
|
103
|
+
* admitted work and is reported as an unconfirmed observation, not refusal.
|
|
85
104
|
*/
|
|
86
105
|
export async function installRelease(input) {
|
|
106
|
+
// One installer budget for uploads + observation, not an App authority grant.
|
|
107
|
+
const signal = installationObservationSignal(input);
|
|
108
|
+
signal.throwIfAborted();
|
|
87
109
|
const directory = resolve(input.directory);
|
|
88
110
|
const release = Schema.decodeUnknownSync(Schema.fromJsonString(releaseSchema))(await readFile(join(directory, "release.json"), "utf8"));
|
|
89
111
|
const authorization = { Authorization: `Bearer ${input.key}` };
|
|
@@ -91,18 +113,21 @@ export async function installRelease(input) {
|
|
|
91
113
|
const bytes = new Uint8Array(await readFile(file));
|
|
92
114
|
const artifactRef = `artifact:${relative(join(directory, "artifacts"), file)}`;
|
|
93
115
|
const digest = parseSha256Digest(await sha256(bytes));
|
|
94
|
-
const response = await fetch(new URL(`/api/artifacts/${encodeURIComponent(artifactRef)}?digest=${digest}`, input.url), { method: "PUT", headers: authorization, body: bytes });
|
|
116
|
+
const response = await fetch(new URL(`/api/artifacts/${encodeURIComponent(artifactRef)}?digest=${digest}`, input.url), { method: "PUT", headers: authorization, body: bytes, signal });
|
|
95
117
|
if (!response.ok)
|
|
96
118
|
throw new Error(`Artifact upload failed (${String(response.status)}): ${artifactRef}`);
|
|
97
119
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
let installation = {
|
|
121
|
+
url: input.url,
|
|
122
|
+
key: input.key,
|
|
123
|
+
release,
|
|
124
|
+
installationId: input.installationId ?? release.appId.replaceAll("_", "-"),
|
|
125
|
+
signal,
|
|
126
|
+
};
|
|
127
|
+
if (input.onAdmitted !== undefined)
|
|
128
|
+
installation = { ...installation, onAdmitted: input.onAdmitted };
|
|
129
|
+
const operation = await installOnOyster(installation);
|
|
130
|
+
return completedInstallationResponse(operation);
|
|
106
131
|
}
|
|
107
132
|
/** Project one exact first-party commit's editable source, `git push` it to
|
|
108
133
|
* an Oyster's own template for that App, and claim the pushed head, so
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RequestShellAppAccess } from "@fungi.computer/nacre/shell-module";
|
|
2
|
+
import type { WorkbenchDirectory } from "@fungi.computer/nacre/workbench-directory";
|
|
3
|
+
/**
|
|
4
|
+
* Keep standalone consent in the Oyster host: the picked shell draws the
|
|
5
|
+
* prompt, while Oyster bounds the request and commits through its owner CAS.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createShellAppAccess(directory: () => Promise<WorkbenchDirectory>): RequestShellAppAccess;
|