@fungi.computer/oyster 0.6.0 → 0.8.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 +133 -78
- package/dist/agent-archive-projection.d.ts +8 -0
- package/dist/agent-archive-projection.js +49 -0
- package/dist/agent-schedules.d.ts +61 -0
- package/dist/agent-schedules.js +19 -0
- package/dist/agents.d.ts +35 -14
- package/dist/agents.js +93 -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.d.ts +1 -24
- package/dist/auth.js +9 -39
- 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 +70 -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 +9 -1
- package/dist/build/local-build.js +11 -9
- package/dist/cloudflare.d.ts +4 -1
- package/dist/cloudflare.js +1 -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 +33 -40
- package/dist/host.js +156 -275
- package/dist/installation-execution.js +14 -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 +3 -2
- package/dist/internal/host-gate.js +5 -18
- 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 +15 -14
- package/dist/invocation.js +58 -26
- 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/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 +7 -93
- package/dist/page-script.d.ts +0 -3
- package/dist/page-script.js +3 -4
- 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 +186 -20
- package/dist/standalone/agents.js +251 -30
- 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 +13 -42
- 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.d.ts +0 -3
- package/dist/standalone/tenancy.js +2 -5
- 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 +10 -3
- package/dist/standalone/workflow-binding.js +22 -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 +31 -4
- package/dist/standalone/workflow-schedules.js +18 -5
- package/dist/standalone/workflow-webhooks.d.ts +4 -18
- package/dist/standalone/workflow-webhooks.js +5 -5
- package/dist/standalone/workflows.d.ts +53 -27
- package/dist/standalone/workflows.js +9 -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/workers-git.d.ts +12 -4
- package/dist/workers-git.js +38 -35
- package/dist/workers.d.ts +9 -1
- package/dist/workers.js +44 -8
- package/dist/workflows.d.ts +4 -0
- package/dist/workflows.js +2 -0
- package/docs/api/README.md +7 -0
- package/docs/api/agent-schedules.md +176 -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/auth.md +0 -55
- package/docs/api/authorization.md +1300 -0
- package/docs/api/build.md +42 -38
- package/docs/api/cloudflare.md +3 -3
- package/docs/api/git.md +20 -11
- package/docs/api/host.md +64 -192
- package/docs/api/index.md +1 -1
- package/docs/api/invocation.md +14 -14
- 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 +111 -222
- package/package.json +41 -22
- package/dist/page/computer-target.d.ts +0 -3
- package/dist/page/computer-target.js +0 -9
- package/dist/standalone/channel-session-door.d.ts +0 -4
- package/dist/standalone/channel-session-door.js +0 -72
|
@@ -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,9 +1,9 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
1
|
import { endTime, startTime } from "hono/timing";
|
|
3
2
|
import { HTTPException } from "hono/http-exception";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { computerCreateRequestSchema, computerPageSchema, } from "@fungi.computer/computer/fleet";
|
|
6
|
-
import {
|
|
5
|
+
import {} from "../host.js";
|
|
6
|
+
import { oysterPrincipalIsOwner, owner as ownerRoute, routes as routeTable, } from "../authorization.js";
|
|
7
7
|
import { computerTargetParam } from "../computer-target.js";
|
|
8
8
|
/** Fleet controls never accept an actor from the request body. */
|
|
9
9
|
const fleetComputerId = z.string().min(1).max(256);
|
|
@@ -79,19 +79,14 @@ export async function standaloneComputerSelection(registry, incoming) {
|
|
|
79
79
|
}
|
|
80
80
|
/** Tenant-owner management controls over the same durable fleet as Hub. */
|
|
81
81
|
export function standaloneComputerFleetRoutes(registryFor) {
|
|
82
|
-
const routes =
|
|
83
|
-
|
|
84
|
-
? next()
|
|
85
|
-
: context.json({ error: "Owner only" }, 403));
|
|
82
|
+
const routes = routeTable();
|
|
83
|
+
const owner = ownerRoute("direct-or-app");
|
|
86
84
|
routes.onError((error, context) => {
|
|
87
85
|
if (error instanceof z.ZodError || error instanceof SyntaxError)
|
|
88
86
|
return context.json({ error: "invalid_input" }, 400);
|
|
89
87
|
throw error;
|
|
90
88
|
});
|
|
91
|
-
routes.get("/api/workbench/directory", async (context) => {
|
|
92
|
-
const principal = context.get("principal");
|
|
93
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
94
|
-
return context.json({ error: "Owner only" }, 403);
|
|
89
|
+
routes.get("/api/workbench/directory", owner, async (context, { principal }) => {
|
|
95
90
|
const registry = registryFor();
|
|
96
91
|
startTime(context, "fleet");
|
|
97
92
|
const admitted = await registry.computerFleet(principal.actor.memberId, {
|
|
@@ -113,10 +108,7 @@ export function standaloneComputerFleetRoutes(registryFor) {
|
|
|
113
108
|
computerContext: "owner-fleet",
|
|
114
109
|
});
|
|
115
110
|
});
|
|
116
|
-
routes.get("/api/computers", async (context) => {
|
|
117
|
-
const principal = context.get("principal");
|
|
118
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
119
|
-
return context.json({ error: "Owner only" }, 403);
|
|
111
|
+
routes.get("/api/computers", owner, async (context, { principal }) => {
|
|
120
112
|
const page = computerPageSchema.parse({
|
|
121
113
|
limit: Number(context.req.query("limit") ?? 100),
|
|
122
114
|
offset: Number(context.req.query("offset") ?? 0),
|
|
@@ -126,10 +118,7 @@ export function standaloneComputerFleetRoutes(registryFor) {
|
|
|
126
118
|
page,
|
|
127
119
|
}));
|
|
128
120
|
});
|
|
129
|
-
routes.get("/api/workbench/computers", async (context) => {
|
|
130
|
-
const principal = context.get("principal");
|
|
131
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
132
|
-
return context.json({ error: "Owner only" }, 403);
|
|
121
|
+
routes.get("/api/workbench/computers", owner, async (context, { principal }) => {
|
|
133
122
|
return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
|
|
134
123
|
operation: "choices",
|
|
135
124
|
page: computerPageSchema.parse({
|
|
@@ -138,39 +127,27 @@ export function standaloneComputerFleetRoutes(registryFor) {
|
|
|
138
127
|
}),
|
|
139
128
|
}));
|
|
140
129
|
});
|
|
141
|
-
routes.post("/api/computers", async (context) => {
|
|
142
|
-
const principal = context.get("principal");
|
|
143
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
144
|
-
return context.json({ error: "Owner only" }, 403);
|
|
130
|
+
routes.post("/api/computers", owner, async (context, { principal }) => {
|
|
145
131
|
const input = computerCreateRequestSchema.parse(await context.req.json());
|
|
146
132
|
return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
|
|
147
133
|
operation: "create",
|
|
148
134
|
request: input,
|
|
149
135
|
}));
|
|
150
136
|
});
|
|
151
|
-
routes.post("/api/computers/:computerId/start", async (context) => {
|
|
152
|
-
const principal = context.get("principal");
|
|
153
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
154
|
-
return context.json({ error: "Owner only" }, 403);
|
|
137
|
+
routes.post("/api/computers/:computerId/start", owner, async (context, { principal }) => {
|
|
155
138
|
const result = await registryFor().computerFleet(principal.actor.memberId, {
|
|
156
139
|
operation: "start",
|
|
157
140
|
computerId: context.req.param("computerId"),
|
|
158
141
|
});
|
|
159
142
|
return fleetResponse(result);
|
|
160
143
|
});
|
|
161
|
-
routes.get("/api/computers/:computerId/internet-access", async (context) => {
|
|
162
|
-
const principal = context.get("principal");
|
|
163
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
164
|
-
return context.json({ error: "Owner only" }, 403);
|
|
144
|
+
routes.get("/api/computers/:computerId/internet-access", owner, async (context, { principal }) => {
|
|
165
145
|
return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
|
|
166
146
|
operation: "read-internet-access",
|
|
167
147
|
computerId: context.req.param("computerId"),
|
|
168
148
|
}));
|
|
169
149
|
});
|
|
170
|
-
routes.put("/api/computers/:computerId/internet-access", async (context) => {
|
|
171
|
-
const principal = context.get("principal");
|
|
172
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
173
|
-
return context.json({ error: "Owner only" }, 403);
|
|
150
|
+
routes.put("/api/computers/:computerId/internet-access", owner, async (context, { principal }) => {
|
|
174
151
|
const input = z
|
|
175
152
|
.strictObject({ enabled: z.boolean() })
|
|
176
153
|
.parse(await context.req.json());
|
|
@@ -180,20 +157,14 @@ export function standaloneComputerFleetRoutes(registryFor) {
|
|
|
180
157
|
enabled: input.enabled,
|
|
181
158
|
}));
|
|
182
159
|
});
|
|
183
|
-
routes.get("/api/computers/:computerId", async (context) => {
|
|
184
|
-
const principal = context.get("principal");
|
|
185
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
186
|
-
return context.json({ error: "Owner only" }, 403);
|
|
160
|
+
routes.get("/api/computers/:computerId", owner, async (context, { principal }) => {
|
|
187
161
|
const result = await registryFor().computerFleet(principal.actor.memberId, {
|
|
188
162
|
operation: "read",
|
|
189
163
|
computerId: context.req.param("computerId"),
|
|
190
164
|
});
|
|
191
165
|
return fleetResponse(result);
|
|
192
166
|
});
|
|
193
|
-
routes.put("/api/computers/selected", async (context) => {
|
|
194
|
-
const principal = context.get("principal");
|
|
195
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
196
|
-
return context.json({ error: "Owner only" }, 403);
|
|
167
|
+
routes.put("/api/computers/selected", owner, async (context, { principal }) => {
|
|
197
168
|
const input = z
|
|
198
169
|
.strictObject({ computerId: fleetComputerId })
|
|
199
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
|
+
}
|