@fungi.computer/oyster 0.5.0 → 0.7.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 +155 -63
- package/dist/agent-archive-projection.d.ts +8 -0
- package/dist/agent-archive-projection.js +49 -0
- package/dist/agent-schedules.d.ts +50 -0
- package/dist/agent-schedules.js +17 -0
- package/dist/agents.d.ts +35 -14
- package/dist/agents.js +97 -144
- package/dist/app-builds.d.ts +7 -2
- package/dist/app-builds.js +19 -2
- package/dist/approval-capability.d.ts +1 -1
- package/dist/approval-capability.js +11 -16
- package/dist/approval-home.d.ts +2 -1
- package/dist/approval-home.js +5 -3
- package/dist/apps/catalog.js +1 -1
- package/dist/apps/gate.d.ts +4 -7
- package/dist/apps/gate.js +14 -29
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/installation.d.ts +4 -42
- package/dist/apps/installation.js +7 -75
- package/dist/apps/operations.js +0 -10
- package/dist/apps/permissions.d.ts +18 -8
- package/dist/apps/permissions.js +7 -1
- package/dist/auth.js +9 -1
- package/dist/authorization.d.ts +307 -0
- package/dist/authorization.js +542 -0
- package/dist/boot-host-script.js +1 -1
- package/dist/build/build-jobs.d.ts +2 -2
- package/dist/build/candidate-builder.d.ts +2 -0
- package/dist/build/candidate-builder.js +5 -1
- package/dist/build/candidate-cli.js +3 -0
- package/dist/build/candidate-cli.mjs +67 -22
- package/dist/build/job-log.d.ts +23 -0
- package/dist/build/job-log.js +88 -0
- package/dist/build/local-build.d.ts +2 -0
- package/dist/build/local-build.js +11 -9
- package/dist/channels/binding.d.ts +8 -1
- package/dist/channels/binding.js +12 -0
- package/dist/channels/conversations.d.ts +16 -5
- package/dist/channels/visitor-channel.d.ts +53 -0
- package/dist/channels/visitor-channel.js +124 -0
- package/dist/channels/visitor-edge.d.ts +115 -0
- package/dist/channels/visitor-edge.js +582 -0
- package/dist/channels/visitor-request.d.ts +7 -0
- package/dist/channels/visitor-request.js +17 -0
- package/dist/channels/visitor-stream-slots.d.ts +2 -0
- package/dist/channels/visitor-stream-slots.js +33 -0
- package/dist/channels/visitors.d.ts +47 -0
- package/dist/channels/visitors.js +181 -0
- package/dist/cloudflare.d.ts +11 -2
- package/dist/cloudflare.js +27 -1
- package/dist/{page/computer-target.d.ts → computer-target.d.ts} +2 -0
- package/dist/{page/computer-target.js → computer-target.js} +3 -1
- package/dist/files.js +9 -8
- package/dist/git-admission.d.ts +3 -3
- package/dist/git-admission.js +4 -6
- package/dist/git-door.d.ts +8 -7
- package/dist/git-door.js +18 -17
- package/dist/git.d.ts +1 -1
- package/dist/host.d.ts +38 -42
- package/dist/host.js +182 -239
- package/dist/installation-execution.js +10 -5
- package/dist/installation-operations.d.ts +4 -180
- package/dist/installation-operations.js +1 -5
- package/dist/installation-store-generation.d.ts +4 -1
- package/dist/installation-store-generation.js +59 -6
- package/dist/internal/delivery.js +0 -1
- package/dist/internal/host-gate.d.ts +9 -6
- package/dist/internal/host-gate.js +19 -25
- package/dist/internal/invocation-timing.d.ts +16 -0
- package/dist/internal/invocation-timing.js +23 -0
- package/dist/internal/registry-installations.js +1 -0
- package/dist/invocation.d.ts +19 -11
- package/dist/invocation.js +171 -96
- package/dist/keys.d.ts +2 -19
- package/dist/keys.js +1 -37
- package/dist/mcp.d.ts +15 -27
- package/dist/mcp.js +105 -155
- package/dist/page/agent-browser.d.ts +15 -0
- package/dist/page/agent-browser.js +162 -0
- package/dist/page/app-permissions-port.d.ts +3 -0
- package/dist/page/app-permissions-port.js +43 -0
- package/dist/page/app-permissions.d.ts +11 -0
- package/dist/page/app-permissions.js +296 -0
- package/dist/page/capabilities.js +1 -1
- package/dist/page/child-selection.d.ts +27 -0
- package/dist/page/child-selection.js +38 -0
- package/dist/page/isolated-page-theme.d.ts +2 -0
- package/dist/page/isolated-page-theme.js +11 -0
- package/dist/page/mount.d.ts +2 -13
- package/dist/page/mount.js +8 -94
- package/dist/page-script.d.ts +0 -3
- package/dist/page-script.js +3 -4
- package/dist/release.d.ts +13 -0
- package/dist/release.js +10 -0
- package/dist/request-origin.d.ts +4 -0
- package/dist/request-origin.js +10 -0
- package/dist/standalone/agent-archive-owner.d.ts +42 -0
- package/dist/standalone/agent-archive-owner.js +116 -0
- package/dist/standalone/agent-archive-schedules.d.ts +6 -0
- package/dist/standalone/agent-archive-schedules.js +76 -0
- package/dist/standalone/agent-archive.d.ts +31 -0
- package/dist/standalone/agent-archive.js +67 -0
- package/dist/standalone/agent-computers.d.ts +7 -6
- package/dist/standalone/agent-computers.js +4 -7
- package/dist/standalone/agent-mail.d.ts +2 -2
- package/dist/standalone/agents.d.ts +157 -20
- package/dist/standalone/agents.js +242 -38
- package/dist/standalone/app-builds-binding.js +1 -1
- package/dist/standalone/app-set.js +2 -54
- package/dist/standalone/app-source.d.ts +3 -4
- package/dist/standalone/app-source.js +12 -9
- package/dist/standalone/builds.d.ts +2 -14
- package/dist/standalone/builds.js +15 -13
- package/dist/standalone/computer-fleet.d.ts +4 -4
- package/dist/standalone/computer-fleet.js +20 -43
- package/dist/standalone/computer.d.ts +4 -3
- package/dist/standalone/computer.js +52 -53
- package/dist/standalone/dashboard-approval.d.ts +10 -0
- package/dist/standalone/dashboard-approval.js +66 -0
- package/dist/standalone/machine-link.d.ts +4 -221
- package/dist/standalone/machine-link.js +21 -15
- package/dist/standalone/media.d.ts +1 -0
- package/dist/standalone/media.js +20 -14
- package/dist/standalone/owner-routes.d.ts +16 -0
- package/dist/standalone/owner-routes.js +21 -0
- package/dist/standalone/permission-resources.d.ts +5 -0
- package/dist/standalone/permission-resources.js +31 -0
- package/dist/standalone/registry.d.ts +7 -0
- package/dist/standalone/registry.js +3 -0
- package/dist/standalone/tenancy.js +3 -1
- package/dist/standalone/worker-routes.d.ts +19 -0
- package/dist/standalone/worker-routes.js +9 -0
- package/dist/standalone/worker.d.ts +1 -1
- package/dist/standalone/worker.js +27 -35
- package/dist/standalone/workflow-binding.d.ts +8 -3
- package/dist/standalone/workflow-binding.js +14 -5
- package/dist/standalone/workflow-git-capture.js +2 -1
- package/dist/standalone/workflow-host.d.ts +8 -103
- package/dist/standalone/workflow-host.js +1 -1
- package/dist/standalone/workflow-node-capture.js +2 -2
- package/dist/standalone/workflow-root-authority.d.ts +1 -1
- package/dist/standalone/workflow-root-authority.js +1 -1
- package/dist/standalone/workflow-schedules.d.ts +14 -5
- package/dist/standalone/workflow-schedules.js +11 -5
- package/dist/standalone/workflow-webhooks.d.ts +4 -18
- package/dist/standalone/workflow-webhooks.js +5 -5
- package/dist/standalone/workflows.d.ts +36 -28
- package/dist/standalone/workflows.js +6 -3
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.d.ts +7 -6
- package/dist/testing/capabilities.js +3 -6
- package/dist/testing/installation.d.ts +0 -1
- package/dist/testing/installation.js +2 -5
- package/dist/turnstile.d.ts +27 -0
- package/dist/turnstile.js +51 -0
- package/dist/workers-git.d.ts +12 -4
- package/dist/workers-git.js +58 -36
- package/dist/workers.d.ts +9 -1
- package/dist/workers.js +59 -20
- package/dist/workflows.d.ts +2 -0
- package/dist/workflows.js +1 -0
- package/docs/api/README.md +7 -0
- package/docs/api/agent-schedules.md +175 -0
- package/docs/api/agents.md +34 -13
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +323 -225
- package/docs/api/authorization.md +1300 -0
- package/docs/api/build-contract.md +2 -2
- package/docs/api/build.md +40 -37
- package/docs/api/cloudflare.md +27 -2
- package/docs/api/git.md +20 -11
- package/docs/api/host.md +80 -199
- package/docs/api/index.md +1 -1
- package/docs/api/invocation.md +15 -11
- package/docs/api/keys.md +1 -98
- package/docs/api/mcp.md +43 -40
- package/docs/api/modules.md +2 -0
- package/docs/api/page/mount.md +16 -19
- package/docs/api/workers-git.md +46 -12
- package/docs/api/workers.md +125 -235
- package/docs/site/agent-quickstart.mdx +6 -2
- package/examples/standalone/wrangler.celld.jsonc +66 -0
- package/package.json +64 -21
- package/dist/standalone/channel-session-door.d.ts +0 -4
- package/dist/standalone/channel-session-door.js +0 -42
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* makes its template the build source at the returned seed revision.
|
|
8
8
|
*
|
|
9
9
|
* - `POST /api/builds` starts a build of that source at one exact commit.
|
|
10
|
-
* - `GET /api/builds/:jobId` reads it;
|
|
11
|
-
*
|
|
10
|
+
* - `GET /api/builds/:jobId` reads it; `?log` reads its combined raw output.
|
|
11
|
+
* Both require the same owner admission. A finished build carries its
|
|
12
|
+
* Release, which the owner installs or selects through the installation routes.
|
|
12
13
|
*/
|
|
13
|
-
import { Hono } from "hono";
|
|
14
14
|
import * as Schema from "effect/Schema";
|
|
15
15
|
import { oysterBuildStartMessages, } from "../app-builds.js";
|
|
16
|
-
import {
|
|
16
|
+
import {} from "../host.js";
|
|
17
|
+
import { owner as ownerRoute, routes as routeTable, } from "../authorization.js";
|
|
17
18
|
import { appIdSchema, releaseSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
18
19
|
const buildInput = Schema.Struct({
|
|
19
20
|
appId: appIdSchema,
|
|
@@ -38,14 +39,9 @@ export function buildStartFailureReply(reason) {
|
|
|
38
39
|
/** The build routes over the tenant registry that owns builds, resolved per
|
|
39
40
|
* request so mounting the routes reaches no registry. */
|
|
40
41
|
export function standaloneBuildRoutes(registryFor) {
|
|
41
|
-
const routes =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
: Response.json({ error: "Owner only" }, { status: 403 }));
|
|
45
|
-
routes.post("/api/builds", async (context) => {
|
|
46
|
-
const principal = context.get("principal");
|
|
47
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
48
|
-
return Response.json({ error: "Owner only" }, { status: 403 });
|
|
42
|
+
const routes = routeTable();
|
|
43
|
+
const owner = ownerRoute("direct-or-app");
|
|
44
|
+
routes.post("/api/builds", owner, async (context, { principal }) => {
|
|
49
45
|
let input;
|
|
50
46
|
try {
|
|
51
47
|
input = Schema.decodeUnknownSync(buildInput, {
|
|
@@ -70,7 +66,13 @@ export function standaloneBuildRoutes(registryFor) {
|
|
|
70
66
|
return Response.json(reply.body, { status: reply.status });
|
|
71
67
|
}
|
|
72
68
|
});
|
|
73
|
-
routes.get("/api/builds/:jobId", async (context) => {
|
|
69
|
+
routes.get("/api/builds/:jobId", owner, async (context) => {
|
|
70
|
+
if (context.req.query("log") !== undefined) {
|
|
71
|
+
const log = await registryFor().readBuildLog(context.req.param("jobId"));
|
|
72
|
+
return log === null
|
|
73
|
+
? Response.json({ error: "Not found" }, { status: 404 })
|
|
74
|
+
: Response.json(log);
|
|
75
|
+
}
|
|
74
76
|
const build = await registryFor().readBuild(context.req.param("jobId"));
|
|
75
77
|
return build === null
|
|
76
78
|
? Response.json({ error: "Not found" }, { status: 404 })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import { type OysterAuth, type OysterEnv } from "../host.js";
|
|
3
|
+
import { type Routes } from "../authorization.js";
|
|
4
4
|
import type { StandaloneRegistry } from "./registry.js";
|
|
5
5
|
export declare const standaloneFleetRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
6
6
|
operation: z.ZodLiteral<"create">;
|
|
@@ -20,10 +20,10 @@ export declare const standaloneFleetRequestSchema: z.ZodDiscriminatedUnion<[z.Zo
|
|
|
20
20
|
}, z.core.$strict>, z.ZodObject<{
|
|
21
21
|
operation: z.ZodEnum<{
|
|
22
22
|
read: "read";
|
|
23
|
-
resolve: "resolve";
|
|
24
23
|
select: "select";
|
|
25
|
-
start: "start";
|
|
26
24
|
state: "state";
|
|
25
|
+
resolve: "resolve";
|
|
26
|
+
start: "start";
|
|
27
27
|
}>;
|
|
28
28
|
computerId: z.ZodString;
|
|
29
29
|
}, z.core.$strict>, z.ZodObject<{
|
|
@@ -41,4 +41,4 @@ export declare function standaloneComputerSelection(registry: Pick<StandaloneReg
|
|
|
41
41
|
auth: OysterAuth;
|
|
42
42
|
}>): Promise<string>;
|
|
43
43
|
/** Tenant-owner management controls over the same durable fleet as Hub. */
|
|
44
|
-
export declare function standaloneComputerFleetRoutes(registryFor: () => DurableObjectStub<StandaloneRegistry>):
|
|
44
|
+
export declare function standaloneComputerFleetRoutes(registryFor: () => DurableObjectStub<StandaloneRegistry>): Routes<OysterEnv>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { endTime, startTime } from "hono/timing";
|
|
2
2
|
import { HTTPException } from "hono/http-exception";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { computerCreateRequestSchema, computerPageSchema, } from "@fungi.computer/computer/fleet";
|
|
5
|
-
import {
|
|
5
|
+
import {} from "../host.js";
|
|
6
|
+
import { oysterPrincipalIsOwner, owner as ownerRoute, routes as routeTable, } from "../authorization.js";
|
|
7
|
+
import { computerTargetParam } from "../computer-target.js";
|
|
6
8
|
/** Fleet controls never accept an actor from the request body. */
|
|
7
9
|
const fleetComputerId = z.string().min(1).max(256);
|
|
8
10
|
export const standaloneFleetRequestSchema = z.discriminatedUnion("operation", [
|
|
@@ -48,7 +50,7 @@ function fleetResponse(result) {
|
|
|
48
50
|
export async function standaloneComputerSelection(registry, incoming) {
|
|
49
51
|
if (incoming === undefined)
|
|
50
52
|
return registry.selectedComputer();
|
|
51
|
-
const requested = new URL(incoming.request.url).searchParams.getAll(
|
|
53
|
+
const requested = new URL(incoming.request.url).searchParams.getAll(computerTargetParam);
|
|
52
54
|
if (requested.length === 0)
|
|
53
55
|
return registry.selectedComputer();
|
|
54
56
|
const admission = await incoming.auth.authenticate(incoming.request);
|
|
@@ -77,40 +79,36 @@ export async function standaloneComputerSelection(registry, incoming) {
|
|
|
77
79
|
}
|
|
78
80
|
/** Tenant-owner management controls over the same durable fleet as Hub. */
|
|
79
81
|
export function standaloneComputerFleetRoutes(registryFor) {
|
|
80
|
-
const routes =
|
|
81
|
-
|
|
82
|
-
? next()
|
|
83
|
-
: context.json({ error: "Owner only" }, 403));
|
|
82
|
+
const routes = routeTable();
|
|
83
|
+
const owner = ownerRoute("direct-or-app");
|
|
84
84
|
routes.onError((error, context) => {
|
|
85
85
|
if (error instanceof z.ZodError || error instanceof SyntaxError)
|
|
86
86
|
return context.json({ error: "invalid_input" }, 400);
|
|
87
87
|
throw error;
|
|
88
88
|
});
|
|
89
|
-
routes.get("/api/workbench/directory", async (context) => {
|
|
90
|
-
const principal = context.get("principal");
|
|
91
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
92
|
-
return context.json({ error: "Owner only" }, 403);
|
|
89
|
+
routes.get("/api/workbench/directory", owner, async (context, { principal }) => {
|
|
93
90
|
const registry = registryFor();
|
|
91
|
+
startTime(context, "fleet");
|
|
94
92
|
const admitted = await registry.computerFleet(principal.actor.memberId, {
|
|
95
93
|
operation: "list",
|
|
96
94
|
page: { limit: 1, offset: 0 },
|
|
97
95
|
});
|
|
96
|
+
endTime(context, "fleet");
|
|
98
97
|
if (admitted === null || "error" in admitted)
|
|
99
98
|
return fleetResponse(admitted);
|
|
99
|
+
startTime(context, "registry");
|
|
100
100
|
const [directory, computerId] = await Promise.all([
|
|
101
101
|
registry.directory(),
|
|
102
102
|
registry.selectedComputer(),
|
|
103
103
|
]);
|
|
104
|
+
endTime(context, "registry");
|
|
104
105
|
return context.json({
|
|
105
106
|
...directory,
|
|
106
107
|
computer: { computerId, name: computerId },
|
|
107
108
|
computerContext: "owner-fleet",
|
|
108
109
|
});
|
|
109
110
|
});
|
|
110
|
-
routes.get("/api/computers", async (context) => {
|
|
111
|
-
const principal = context.get("principal");
|
|
112
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
113
|
-
return context.json({ error: "Owner only" }, 403);
|
|
111
|
+
routes.get("/api/computers", owner, async (context, { principal }) => {
|
|
114
112
|
const page = computerPageSchema.parse({
|
|
115
113
|
limit: Number(context.req.query("limit") ?? 100),
|
|
116
114
|
offset: Number(context.req.query("offset") ?? 0),
|
|
@@ -120,10 +118,7 @@ export function standaloneComputerFleetRoutes(registryFor) {
|
|
|
120
118
|
page,
|
|
121
119
|
}));
|
|
122
120
|
});
|
|
123
|
-
routes.get("/api/workbench/computers", async (context) => {
|
|
124
|
-
const principal = context.get("principal");
|
|
125
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
126
|
-
return context.json({ error: "Owner only" }, 403);
|
|
121
|
+
routes.get("/api/workbench/computers", owner, async (context, { principal }) => {
|
|
127
122
|
return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
|
|
128
123
|
operation: "choices",
|
|
129
124
|
page: computerPageSchema.parse({
|
|
@@ -132,39 +127,27 @@ export function standaloneComputerFleetRoutes(registryFor) {
|
|
|
132
127
|
}),
|
|
133
128
|
}));
|
|
134
129
|
});
|
|
135
|
-
routes.post("/api/computers", async (context) => {
|
|
136
|
-
const principal = context.get("principal");
|
|
137
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
138
|
-
return context.json({ error: "Owner only" }, 403);
|
|
130
|
+
routes.post("/api/computers", owner, async (context, { principal }) => {
|
|
139
131
|
const input = computerCreateRequestSchema.parse(await context.req.json());
|
|
140
132
|
return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
|
|
141
133
|
operation: "create",
|
|
142
134
|
request: input,
|
|
143
135
|
}));
|
|
144
136
|
});
|
|
145
|
-
routes.post("/api/computers/:computerId/start", async (context) => {
|
|
146
|
-
const principal = context.get("principal");
|
|
147
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
148
|
-
return context.json({ error: "Owner only" }, 403);
|
|
137
|
+
routes.post("/api/computers/:computerId/start", owner, async (context, { principal }) => {
|
|
149
138
|
const result = await registryFor().computerFleet(principal.actor.memberId, {
|
|
150
139
|
operation: "start",
|
|
151
140
|
computerId: context.req.param("computerId"),
|
|
152
141
|
});
|
|
153
142
|
return fleetResponse(result);
|
|
154
143
|
});
|
|
155
|
-
routes.get("/api/computers/:computerId/internet-access", async (context) => {
|
|
156
|
-
const principal = context.get("principal");
|
|
157
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
158
|
-
return context.json({ error: "Owner only" }, 403);
|
|
144
|
+
routes.get("/api/computers/:computerId/internet-access", owner, async (context, { principal }) => {
|
|
159
145
|
return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
|
|
160
146
|
operation: "read-internet-access",
|
|
161
147
|
computerId: context.req.param("computerId"),
|
|
162
148
|
}));
|
|
163
149
|
});
|
|
164
|
-
routes.put("/api/computers/:computerId/internet-access", async (context) => {
|
|
165
|
-
const principal = context.get("principal");
|
|
166
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
167
|
-
return context.json({ error: "Owner only" }, 403);
|
|
150
|
+
routes.put("/api/computers/:computerId/internet-access", owner, async (context, { principal }) => {
|
|
168
151
|
const input = z
|
|
169
152
|
.strictObject({ enabled: z.boolean() })
|
|
170
153
|
.parse(await context.req.json());
|
|
@@ -174,20 +157,14 @@ export function standaloneComputerFleetRoutes(registryFor) {
|
|
|
174
157
|
enabled: input.enabled,
|
|
175
158
|
}));
|
|
176
159
|
});
|
|
177
|
-
routes.get("/api/computers/:computerId", async (context) => {
|
|
178
|
-
const principal = context.get("principal");
|
|
179
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
180
|
-
return context.json({ error: "Owner only" }, 403);
|
|
160
|
+
routes.get("/api/computers/:computerId", owner, async (context, { principal }) => {
|
|
181
161
|
const result = await registryFor().computerFleet(principal.actor.memberId, {
|
|
182
162
|
operation: "read",
|
|
183
163
|
computerId: context.req.param("computerId"),
|
|
184
164
|
});
|
|
185
165
|
return fleetResponse(result);
|
|
186
166
|
});
|
|
187
|
-
routes.put("/api/computers/selected", async (context) => {
|
|
188
|
-
const principal = context.get("principal");
|
|
189
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
190
|
-
return context.json({ error: "Owner only" }, 403);
|
|
167
|
+
routes.put("/api/computers/selected", owner, async (context, { principal }) => {
|
|
191
168
|
const input = z
|
|
192
169
|
.strictObject({ computerId: fleetComputerId })
|
|
193
170
|
.parse(await context.req.json());
|
|
@@ -109,6 +109,7 @@ export declare class ComputerDO extends NativeComputerDO implements ComputerFetc
|
|
|
109
109
|
exitCode: number | null;
|
|
110
110
|
stderr: string;
|
|
111
111
|
} | null;
|
|
112
|
+
log: import("../build/job-log.js").BuildJobLog;
|
|
112
113
|
}>>;
|
|
113
114
|
/** Issue preview tickets for every exposed port, or refuse typed. */
|
|
114
115
|
portPreviews(authority: ComputerGrant, selectorInput: PreviewSelector): Promise<ComputerPortPreviewsReply>;
|
|
@@ -160,10 +161,10 @@ export declare class ComputerDO extends NativeComputerDO implements ComputerFetc
|
|
|
160
161
|
export declare function standaloneWorkspace(env: StandaloneComputerEnv, keysLive?: OysterKeysLivePort, selectedComputerId?: string): OysterWorkspace;
|
|
161
162
|
/**
|
|
162
163
|
* Owner/key capability routes over the selected Computer's native
|
|
163
|
-
* namespace.
|
|
164
|
-
*
|
|
164
|
+
* namespace. Each route declares the Computer requirement, so a key reaches
|
|
165
|
+
* it only with its own `computer.use` grant (`authorization.ts`).
|
|
165
166
|
*/
|
|
166
|
-
export declare function standaloneComputerCapabilities(env: StandaloneComputerEnv, workspace: OysterWorkspace
|
|
167
|
+
export declare function standaloneComputerCapabilities(env: StandaloneComputerEnv, workspace: OysterWorkspace): readonly OysterCapability[];
|
|
167
168
|
/** The COMPUTER row's codecs, as the standalone Instance joins them. */
|
|
168
169
|
export type ComputerFrameMethods = Readonly<{
|
|
169
170
|
COMPUTER: OysterBindingRow<OysterWorkerEnv>["methods"];
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
1
|
import { attachedBackendId, hostedBackingSchema, } from "@fungi.computer/computer/backing";
|
|
3
2
|
import { appMethodRefusalStatus, appMethodRefusedReplySchema, } from "@fungi.computer/agar/backend";
|
|
4
3
|
import * as Schema from "effect/Schema";
|
|
5
4
|
import { computerPortsSchema, } from "@fungi.computer/agar/computer";
|
|
6
5
|
import { verifyRelease } from "../host.js";
|
|
7
6
|
import { r2ArtifactStore } from "../workers.js";
|
|
7
|
+
import { createBuildJobLog } from "../build/job-log.js";
|
|
8
8
|
import { createStandalonePreview, previewOrigins, previewSelectorSchema, PreviewRefused, } from "./preview.js";
|
|
9
9
|
import { standaloneTenancy } from "./tenancy.js";
|
|
10
10
|
import { Base64 } from "effect/encoding";
|
|
@@ -18,8 +18,8 @@ import { checkComputerGrant, computerActionSchema, computerGrantSchema, createFl
|
|
|
18
18
|
import { AppInstanceError, selectionOf } from "../index.js";
|
|
19
19
|
import {} from "../cloudflare.js";
|
|
20
20
|
import { computerSlots } from "../computer.js";
|
|
21
|
-
import { oysterPrincipalId,
|
|
22
|
-
import {
|
|
21
|
+
import { oysterPrincipalId, } from "../host.js";
|
|
22
|
+
import { authorityOf, computer as computerRoute, owner, reachesComputer, routes as routeTable, } from "../authorization.js";
|
|
23
23
|
import { oysterAppBuildTimeoutMs, } from "../app-builds.js";
|
|
24
24
|
import { workflowGitStepGrant, } from "./workflow-binding.js";
|
|
25
25
|
const missingFile = z.lazy(() => z.object({ code: z.literal("ENOENT") }));
|
|
@@ -66,24 +66,18 @@ function grant(computerId, actor) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* The Computer grant actor for
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* same one `host.keys.live` is); a caller with none -- the Media binding's
|
|
75
|
-
* own workspace selection never calls through to a principal at all --
|
|
76
|
-
* fails every team-key closed rather than falling back to an unchecked read.
|
|
69
|
+
* The Computer grant actor for a principal `authorization.ts` lets reach the
|
|
70
|
+
* Computer; null otherwise. `keysLive` is the one owner-checked port every
|
|
71
|
+
* caller reaches a key's liveness through (the same one `host.keys.live`
|
|
72
|
+
* is); a caller with none -- the Media binding's own workspace selection
|
|
73
|
+
* never calls through to a principal at all -- fails every key closed.
|
|
77
74
|
*/
|
|
78
75
|
async function computerActor(principal, keysLive) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
return JSON.stringify(principal);
|
|
76
|
+
const authority = await authorityOf(principal, {
|
|
77
|
+
liveKey: keysLive ?? (() => Promise.resolve(null)),
|
|
78
|
+
appToolPolicy: () => null,
|
|
79
|
+
});
|
|
80
|
+
return reachesComputer(authority) ? JSON.stringify(principal) : null;
|
|
87
81
|
}
|
|
88
82
|
/** Each advertised Computer tool, the grant action that accepts it, and its result. */
|
|
89
83
|
const computerTools = {
|
|
@@ -243,16 +237,25 @@ export class ComputerDO extends NativeComputerDO {
|
|
|
243
237
|
const admitted = await this.previewBindings.REGISTRY.getByName("registry").admitBuildExecution(input.jobId, this.state.id.toString());
|
|
244
238
|
if (!admitted)
|
|
245
239
|
throw new AppInstanceError("revoked");
|
|
240
|
+
const log = createBuildJobLog();
|
|
246
241
|
const result = await this.executeOperationAfterAcceptance({
|
|
247
242
|
command: input.command,
|
|
248
243
|
cwd: "/workspace",
|
|
249
244
|
timeoutMs: input.timeoutMs,
|
|
250
245
|
requestId: mintComputerRequestId(),
|
|
251
|
-
}, () => undefined, "hosted", { maximumTimeMs: oysterAppBuildTimeoutMs }
|
|
246
|
+
}, () => undefined, "hosted", { maximumTimeMs: oysterAppBuildTimeoutMs }, undefined, undefined, {
|
|
247
|
+
onEvent(event) {
|
|
248
|
+
if (event.name === "stdout" || event.name === "stderr")
|
|
249
|
+
log.append(event.value);
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
252
|
const output = z
|
|
253
253
|
.object({ exitCode: z.number().nullable(), stderr: z.string() })
|
|
254
254
|
.safeParse(result.output);
|
|
255
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
output: output.success ? output.data : null,
|
|
257
|
+
log: log.finish(),
|
|
258
|
+
};
|
|
256
259
|
}
|
|
257
260
|
/** Issue preview tickets for every exposed port, or refuse typed. */
|
|
258
261
|
async portPreviews(authority, selectorInput) {
|
|
@@ -538,30 +541,26 @@ export function standaloneWorkspace(env, keysLive, selectedComputerId = env.OYST
|
|
|
538
541
|
}
|
|
539
542
|
/**
|
|
540
543
|
* Owner/key capability routes over the selected Computer's native
|
|
541
|
-
* namespace.
|
|
542
|
-
*
|
|
544
|
+
* namespace. Each route declares the Computer requirement, so a key reaches
|
|
545
|
+
* it only with its own `computer.use` grant (`authorization.ts`).
|
|
543
546
|
*/
|
|
544
|
-
export function standaloneComputerCapabilities(env, workspace
|
|
545
|
-
const routes =
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (actor === null)
|
|
549
|
-
return context.json({ error: "Computer access requires an owner or key" }, 403);
|
|
550
|
-
context.set("authority", grant(workspace.computerId, actor));
|
|
551
|
-
return next();
|
|
552
|
-
});
|
|
547
|
+
export function standaloneComputerCapabilities(env, workspace) {
|
|
548
|
+
const routes = routeTable();
|
|
549
|
+
const reach = computerRoute();
|
|
550
|
+
const authority = (principal) => grant(workspace.computerId, JSON.stringify(principal));
|
|
553
551
|
const computer = () => env.COMPUTER.getByName(workspace.computerId);
|
|
554
|
-
routes.get("/ports", async (context) => {
|
|
552
|
+
routes.get("/ports", owner("direct-or-app"), async (context, { principal }) => {
|
|
555
553
|
if (previewOrigins(env) === null)
|
|
556
554
|
return context.json({ error: "port_previews_not_configured" });
|
|
557
|
-
const {
|
|
558
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
559
|
-
return context.json({ error: "Owner preview access required" }, 403);
|
|
555
|
+
const { installation } = context.env;
|
|
560
556
|
const ownerId = oysterPrincipalId(principal);
|
|
561
|
-
const
|
|
557
|
+
const ownerAuthority = authority(principal);
|
|
562
558
|
const reply = await computer().portPreviews({
|
|
563
|
-
...
|
|
564
|
-
provenance: {
|
|
559
|
+
...ownerAuthority,
|
|
560
|
+
provenance: {
|
|
561
|
+
...ownerAuthority.provenance,
|
|
562
|
+
actor: `owner:${ownerId}`,
|
|
563
|
+
},
|
|
565
564
|
}, {
|
|
566
565
|
ownerId,
|
|
567
566
|
installationId: installation.installationId,
|
|
@@ -573,47 +572,47 @@ export function standaloneComputerCapabilities(env, workspace, keysLive) {
|
|
|
573
572
|
: context.json({ error: reply.code, message: reply.message }, appMethodRefusalStatus[reply.code]);
|
|
574
573
|
});
|
|
575
574
|
// The host wire body is `{ port }`, as on Hub; it is parsed here.
|
|
576
|
-
routes.post("/ports/expose", async (context) => {
|
|
575
|
+
routes.post("/ports/expose", reach, async (context, { principal }) => {
|
|
577
576
|
const input = computerPortInputSchema.safeParse(await context.req.json());
|
|
578
577
|
if (!input.success)
|
|
579
578
|
return context.json({ error: "invalid_input" }, 400);
|
|
580
|
-
return Response.json(await computer().value(
|
|
579
|
+
return Response.json(await computer().value(authority(principal), "exposePort", input.data));
|
|
581
580
|
});
|
|
582
581
|
// The host wire body is `{ port }`, as on Hub; it is parsed here.
|
|
583
|
-
routes.post("/ports/unexpose", async (context) => {
|
|
582
|
+
routes.post("/ports/unexpose", reach, async (context, { principal }) => {
|
|
584
583
|
const input = computerPortInputSchema.safeParse(await context.req.json());
|
|
585
584
|
if (!input.success)
|
|
586
585
|
return context.json({ error: "invalid_input" }, 400);
|
|
587
|
-
return Response.json(await computer().value(
|
|
586
|
+
return Response.json(await computer().value(authority(principal), "unexposePort", input.data));
|
|
588
587
|
});
|
|
589
|
-
routes.get("/processes", async (
|
|
590
|
-
routes.post("/processes/start", async (context) => {
|
|
588
|
+
routes.get("/processes", reach, async (_context, { principal }) => Response.json(await computer().value(authority(principal), "processList", {})));
|
|
589
|
+
routes.post("/processes/start", reach, async (context, { principal }) => {
|
|
591
590
|
const input = processStartHttpBody.safeParse(await context.req.json());
|
|
592
591
|
if (!input.success)
|
|
593
592
|
return context.json({ error: "invalid_input" }, 400);
|
|
594
|
-
const result = processStartReply.parse(await computer().value(
|
|
593
|
+
const result = processStartReply.parse(await computer().value(authority(principal), "processStart", computerValueSchema.parse({
|
|
595
594
|
...input.data,
|
|
596
595
|
requestScope: `owner:${env.OYSTER_OWNER_NAME ?? "owner"}`,
|
|
597
596
|
})));
|
|
598
597
|
return Response.json(result, { status: "error" in result ? 409 : 200 });
|
|
599
598
|
});
|
|
600
599
|
// The host wire body is `{ id }`, as on Hub; it is parsed here.
|
|
601
|
-
routes.post("/processes/kill", async (context) => {
|
|
600
|
+
routes.post("/processes/kill", reach, async (context, { principal }) => {
|
|
602
601
|
const input = processIdInputSchema.safeParse(await context.req.json());
|
|
603
602
|
if (!input.success)
|
|
604
603
|
return context.json({ error: "invalid_input" }, 400);
|
|
605
|
-
return Response.json(await computer().value(
|
|
604
|
+
return Response.json(await computer().value(authority(principal), "processKill", input.data));
|
|
606
605
|
});
|
|
607
|
-
routes.get("/terminal", async (context) => {
|
|
608
|
-
const
|
|
606
|
+
routes.get("/terminal", reach, async (context, { principal }) => {
|
|
607
|
+
const terminal = authority(principal);
|
|
609
608
|
const url = new URL(context.req.url);
|
|
610
609
|
url.pathname = "/_oyster/terminal";
|
|
611
610
|
return computer().fetch(new Request(url, {
|
|
612
611
|
headers: {
|
|
613
612
|
Upgrade: "websocket",
|
|
614
613
|
[terminalGrantHeader]: JSON.stringify({
|
|
615
|
-
...
|
|
616
|
-
actions: [...
|
|
614
|
+
...terminal,
|
|
615
|
+
actions: [...terminal.actions],
|
|
617
616
|
}),
|
|
618
617
|
},
|
|
619
618
|
}));
|
|
@@ -621,7 +620,7 @@ export function standaloneComputerCapabilities(env, workspace, keysLive) {
|
|
|
621
620
|
return Object.values(computerSlots).map((slot) => ({
|
|
622
621
|
...slot,
|
|
623
622
|
permission: "computer.execute",
|
|
624
|
-
routes
|
|
623
|
+
routes,
|
|
625
624
|
}));
|
|
626
625
|
}
|
|
627
626
|
/** Closed COMPUTER binding codecs, joined into the standalone Instance. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { type Installation } from "../apps/installation.js";
|
|
3
|
+
import type { InstallationOperation } from "../installation-operations.js";
|
|
4
|
+
/**
|
|
5
|
+
* An owner choosing a freshly installed dashboard approves its declared shell
|
|
6
|
+
* operations only. Replacements and tombstone remints never replay consent.
|
|
7
|
+
*/
|
|
8
|
+
export declare function approveChosenDashboard(operation: Extract<InstallationOperation, {
|
|
9
|
+
state: "succeeded";
|
|
10
|
+
}>, owner: (method: "GET" | "PUT", path: string, body?: Schema.Json) => Promise<Response>): Promise<Installation | undefined>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { grantCommandSchema, installationSchema, } from "../apps/installation.js";
|
|
3
|
+
import { appPermissionPageSchema } from "../apps/permissions.js";
|
|
4
|
+
/**
|
|
5
|
+
* An owner choosing a freshly installed dashboard approves its declared shell
|
|
6
|
+
* operations only. Replacements and tombstone remints never replay consent.
|
|
7
|
+
*/
|
|
8
|
+
export async function approveChosenDashboard(operation, owner) {
|
|
9
|
+
if (operation.creation !== "fresh")
|
|
10
|
+
return;
|
|
11
|
+
const installed = operation.installation;
|
|
12
|
+
const name = operation.installationId;
|
|
13
|
+
if (installed === null)
|
|
14
|
+
throw new Error(`${name} completed installation version is unavailable`);
|
|
15
|
+
if (installed.status !== "enabled")
|
|
16
|
+
throw new Error(`${name} fresh installation is not enabled`);
|
|
17
|
+
const path = `/api/installations/${encodeURIComponent(name)}/grants`;
|
|
18
|
+
const projectionResponse = await owner("GET", path);
|
|
19
|
+
if (!projectionResponse.ok)
|
|
20
|
+
throw new Error(`${name} approval read failed (${String(projectionResponse.status)})`);
|
|
21
|
+
const projection = appPermissionPageSchema.parse(await projectionResponse.json());
|
|
22
|
+
const { permissions } = projection;
|
|
23
|
+
const digest = installed.release.artifact.artifactDigest;
|
|
24
|
+
if (projection.storeGeneration !== operation.reference.generation ||
|
|
25
|
+
projection.installationId !== name ||
|
|
26
|
+
projection.artifactDigest !== digest ||
|
|
27
|
+
permissions.declaredOperations.artifactDigest !== digest ||
|
|
28
|
+
permissions.authority.authorityVersion !==
|
|
29
|
+
installed.authority.authorityVersion ||
|
|
30
|
+
permissions.authority.declaredOperationsDigest !==
|
|
31
|
+
installed.authority.declaredOperationsDigest ||
|
|
32
|
+
permissions.declaredOperations.digest !==
|
|
33
|
+
installed.authority.declaredOperationsDigest)
|
|
34
|
+
throw new Error(`${name} approval snapshot changed`);
|
|
35
|
+
const shell = permissions.declaredOperations.capabilities.find((row) => row.capability === "team.workbenchHost");
|
|
36
|
+
if (shell === undefined)
|
|
37
|
+
return;
|
|
38
|
+
const command = grantCommandSchema.parse({
|
|
39
|
+
kind: "change-grants",
|
|
40
|
+
changes: [
|
|
41
|
+
{
|
|
42
|
+
capability: shell.capability,
|
|
43
|
+
grant: { operations: shell.operations, resources: "tenant-scope" },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
const saved = await owner("PUT", path, Schema.decodeUnknownSync(Schema.Json)({
|
|
48
|
+
storeGeneration: projection.storeGeneration,
|
|
49
|
+
authorityVersion: installed.authority.authorityVersion,
|
|
50
|
+
artifactDigest: digest,
|
|
51
|
+
command,
|
|
52
|
+
}));
|
|
53
|
+
if (!saved.ok)
|
|
54
|
+
throw new Error(`${name} approval failed (${String(saved.status)})`);
|
|
55
|
+
const receipt = installationSchema.parse(await saved.json());
|
|
56
|
+
if (receipt.installationId !== name ||
|
|
57
|
+
receipt.instanceId !== installed.instanceId ||
|
|
58
|
+
receipt.release.artifact.artifactDigest !== digest ||
|
|
59
|
+
receipt.authority.authorityVersion !==
|
|
60
|
+
installed.authority.authorityVersion + 1 ||
|
|
61
|
+
!receipt.authority.approvedGrants.some((grant) => grant.capability === "team.workbenchHost" &&
|
|
62
|
+
grant.resources === "tenant-scope" &&
|
|
63
|
+
shell.operations.every((operation) => grant.operations.includes(operation))))
|
|
64
|
+
throw new Error(`${name} approval incomplete`);
|
|
65
|
+
return receipt;
|
|
66
|
+
}
|