@fungi.computer/oyster 0.0.1 → 0.2.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 +228 -176
- package/dist/agent-avatars.d.ts +103 -0
- package/dist/agent-avatars.js +66 -0
- package/dist/agents.d.ts +12 -2
- package/dist/agents.js +39 -2
- package/dist/app-builds.d.ts +76 -17
- package/dist/app-builds.js +207 -63
- package/dist/app-sources.d.ts +7 -22
- package/dist/app-sources.js +49 -14
- package/dist/apps/gate.d.ts +7 -7
- package/dist/apps/gate.js +6 -6
- package/dist/apps/installation.d.ts +35 -35
- package/dist/apps/installation.js +20 -19
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.js +7 -1
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +12 -1
- package/dist/build/build-step.js +9 -1
- package/dist/build/candidate-builder.d.ts +68 -0
- package/dist/build/candidate-builder.js +91 -0
- package/dist/build/candidate-cli.d.ts +1 -0
- package/dist/build/candidate-cli.js +119 -0
- package/dist/build/candidate-cli.mjs +28220 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/effect.d.ts +18 -0
- package/dist/build/effect.js +52 -12
- package/dist/build/git-environment.d.ts +3 -0
- package/dist/build/git-environment.js +21 -9
- package/dist/build/local-build.d.ts +13 -3
- package/dist/build/local-build.js +149 -99
- package/dist/build/snapshot-package-store.d.ts +10 -0
- package/dist/build/snapshot-package-store.js +15 -0
- package/dist/build/snapshot-program-scripts.js +1 -1
- package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
- package/dist/build/snapshot-programs/snapshot-path.js +6 -0
- package/dist/build/submission-contract.d.ts +461 -17
- package/dist/build/submission-contract.js +136 -26
- package/dist/build/workspace-build.d.ts +7 -2
- package/dist/build/workspace-build.js +55 -11
- package/dist/build-contract.d.ts +3 -0
- package/dist/build-contract.js +3 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +1 -0
- package/dist/cloudflare.js +4 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.d.ts +1 -1
- package/dist/frame-binding-wire.js +1 -1
- package/dist/git-journal.js +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/installation-consent.d.ts +3 -1
- package/dist/installation-consent.js +2 -0
- package/dist/mcp-owner.js +20 -8
- package/dist/pack.d.ts +6 -3
- package/dist/pack.js +28 -11
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/mount.d.ts +4 -8
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.js +4 -4
- package/dist/release.d.ts +11 -0
- package/dist/release.js +4 -1
- package/dist/shell/app-directory.d.ts +50 -45
- package/dist/shell/app-directory.js +19 -18
- package/dist/shell/shell-module.d.ts +26 -2
- package/dist/shell/shell-module.js +51 -2
- package/dist/shell/window-manager.d.ts +5 -1
- package/dist/shell/window-manager.js +21 -2
- package/dist/shell/workbench-directory.d.ts +332 -0
- package/dist/shell/workbench-directory.js +35 -0
- package/dist/shell/workbench-host-types.d.ts +1 -1
- package/dist/shell/workbench-host.js +5 -0
- package/dist/shell/workbench-model.d.ts +3 -1
- package/dist/source/index.d.ts +6 -7
- package/dist/source/index.js +6 -7
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +2 -0
- package/dist/source/template-repository.js +13 -0
- package/dist/source/{template-write.d.ts → template-seed.d.ts} +19 -20
- package/dist/source/template-seed.js +80 -0
- package/dist/standalone/agent-directory.d.ts +4 -0
- package/dist/standalone/agent-directory.js +4 -0
- package/dist/standalone/agents.d.ts +227 -0
- package/dist/standalone/agents.js +834 -0
- package/dist/standalone/app-builds-binding.d.ts +36 -0
- package/dist/standalone/app-builds-binding.js +119 -0
- package/dist/standalone/app-set.d.ts +91 -0
- package/dist/standalone/app-set.js +395 -0
- package/dist/standalone/app-source.d.ts +41 -0
- package/dist/standalone/app-source.js +59 -0
- package/dist/standalone/builds.d.ts +18 -0
- package/dist/standalone/builds.js +101 -0
- package/dist/standalone/computer-egress.d.ts +37 -0
- package/dist/standalone/computer-egress.js +111 -0
- package/dist/standalone/computer.d.ts +122 -0
- package/dist/standalone/computer.js +714 -0
- package/dist/standalone/install-apps-cli.d.ts +2 -0
- package/dist/standalone/install-apps-cli.js +126 -0
- package/dist/standalone/instance.d.ts +35 -0
- package/dist/standalone/instance.js +74 -0
- package/dist/standalone/media.d.ts +76 -0
- package/dist/standalone/media.js +227 -0
- package/dist/standalone/preview.d.ts +51 -0
- package/dist/standalone/preview.js +167 -0
- package/dist/standalone/registry.d.ts +102 -0
- package/dist/standalone/registry.js +193 -0
- package/dist/standalone/tenancy.d.ts +42 -0
- package/dist/standalone/tenancy.js +241 -0
- package/dist/standalone/worker.d.ts +55 -0
- package/dist/standalone/worker.js +147 -0
- package/dist/standalone/workflow-authority-readers.d.ts +16 -0
- package/dist/standalone/workflow-authority-readers.js +35 -0
- package/dist/standalone/workflow-binding.d.ts +216 -0
- package/dist/standalone/workflow-binding.js +217 -0
- package/dist/standalone/workflow-events.d.ts +46 -0
- package/dist/standalone/workflow-events.js +39 -0
- package/dist/standalone/workflow-git-capture.d.ts +7 -0
- package/dist/standalone/workflow-git-capture.js +62 -0
- package/dist/standalone/workflow-host.d.ts +955 -0
- package/dist/standalone/workflow-host.js +778 -0
- package/dist/standalone/workflow-node-capture.d.ts +13 -0
- package/dist/standalone/workflow-node-capture.js +72 -0
- package/dist/standalone/workflow-root-authority.d.ts +27 -0
- package/dist/standalone/workflow-root-authority.js +87 -0
- package/dist/standalone/workflow-schedules.d.ts +52 -0
- package/dist/standalone/workflow-schedules.js +40 -0
- package/dist/standalone/workflow-trigger-host.d.ts +1716 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +713 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/tenancy/auth0.d.ts +64 -0
- package/dist/tenancy/auth0.js +198 -0
- package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
- package/dist/tenancy/cloudflare/rate-limit.js +19 -0
- package/dist/tenancy/cloudflare/registry.d.ts +50 -0
- package/dist/tenancy/cloudflare/registry.js +301 -0
- package/dist/tenancy/cloudflare.d.ts +2 -0
- package/dist/tenancy/cloudflare.js +2 -0
- package/dist/tenancy/cold-directory.d.ts +13 -0
- package/dist/tenancy/cold-directory.js +43 -0
- package/dist/tenancy/contracts.d.ts +195 -0
- package/dist/tenancy/contracts.js +1 -0
- package/dist/tenancy/index.d.ts +19 -0
- package/dist/tenancy/index.js +156 -0
- package/dist/tenancy/owner-key.d.ts +26 -0
- package/dist/tenancy/owner-key.js +63 -0
- package/dist/tenancy/oyster.d.ts +4 -0
- package/dist/tenancy/oyster.js +25 -0
- package/dist/tenancy/registry-record.d.ts +7 -0
- package/dist/tenancy/registry-record.js +44 -0
- package/dist/tenancy/registry.d.ts +27 -0
- package/dist/tenancy/registry.js +138 -0
- package/dist/tenancy/resolve.d.ts +7 -0
- package/dist/tenancy/resolve.js +64 -0
- package/dist/workers-git.js +1 -4
- package/dist/workers.d.ts +2 -2
- package/dist/workers.js +3 -1
- package/docs/api/build-contract.md +829 -32
- package/docs/api/build.md +253 -17
- package/docs/api/modules.md +40 -32
- package/docs/api/pack.md +6 -3
- package/docs/api/shell/app-directory.md +267 -0
- package/docs/api/shell/shell-module.md +64 -3
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +19 -0
- package/docs/api/shell.md +127 -0
- package/docs/api/tenancy/auth0.md +110 -0
- package/docs/api/tenancy/cloudflare.md +221 -0
- package/docs/api/tenancy/cold-directory.md +46 -0
- package/docs/api/tenancy/owner-key.md +63 -0
- package/docs/api/tenancy/registry.md +77 -0
- package/docs/api/tenancy/resolve.md +69 -0
- package/docs/api/tenancy.md +430 -0
- package/docs/api/workers-git.md +140 -21
- package/docs/api/workers.md +15 -2
- package/docs/site/agent-quickstart.mdx +52 -55
- package/examples/standalone/worker.ts +7 -14
- package/examples/standalone/wrangler.jsonc +67 -6
- package/package.json +78 -25
- package/dist/build/r2-artifact-writer.d.ts +0 -31
- package/dist/build/r2-artifact-writer.js +0 -179
- package/dist/source/template-write.js +0 -344
- package/docs/media/architecture.mmd +0 -32
- package/docs/media/architecture.png +0 -0
- package/docs/media/bonsai-network.png +0 -0
- package/docs/media/bonsai-repository.png +0 -0
- package/examples/git/worker.ts +0 -39
- package/examples/git/wrangler.jsonc +0 -32
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { appMethodRefusalStatus, appMethodRefusedReplySchema, } from "@fungi.computer/agar/backend";
|
|
3
|
+
import { workflowGitPassResultSchema, } from "@fungi.computer/agar/computer-contracts";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import { computerPortsSchema, } from "@fungi.computer/agar/computer";
|
|
6
|
+
import { verifyRelease } from "../host.js";
|
|
7
|
+
import { r2ArtifactStore } from "../workers.js";
|
|
8
|
+
import { createStandalonePreview, previewOrigins, previewSelectorSchema, PreviewRefused, } from "./preview.js";
|
|
9
|
+
import { standaloneTenancy } from "./tenancy.js";
|
|
10
|
+
import { Base64 } from "effect/encoding";
|
|
11
|
+
import * as z from "zod";
|
|
12
|
+
import { ComputerPortConflictError, computerByteReadReplySchema, computerProcessSchema, installationProcessScope, processStartInputSchema, processStartToolInputSchema, computerCapabilities, computerToolResultSchema, readOutputSchema, } from "@fungi.computer/computer/operations";
|
|
13
|
+
import { ComputerDO as NativeComputerDO, ComputerFileConflict, ComputerOperationConflictError, computerExecutionRequestSchema, computerValueSchema, computerPortInputSchema, listInputSchema, listOutputSchema, readBytesInputSchema, writeInputSchema, replaceInputSchema, writeOutputSchema, mkdirOutputSchema, computerPortsOutputSchema, computerPortOutputSchema, computerPortRemovalOutputSchema, processListOutputSchema, processKillOutputSchema, computerOperationResultSchema, mintComputerRequestId, processIdInputSchema, } from "@fungi.computer/computer";
|
|
14
|
+
import { checkComputerGrant, computerActionSchema, computerGrantSchema, createFleetBacking, FleetGrantRefused, } from "@fungi.computer/computer/fleet";
|
|
15
|
+
import { AppInstanceError, selectionOf } from "../index.js";
|
|
16
|
+
import {} from "../cloudflare.js";
|
|
17
|
+
import { computerSlots } from "../computer.js";
|
|
18
|
+
import { oysterPrincipalId, oysterPrincipalIsOwner, } from "../host.js";
|
|
19
|
+
import { keyAllowsComputerUse } from "../keys.js";
|
|
20
|
+
import { oysterAppBuildTimeoutMs } from "../app-builds.js";
|
|
21
|
+
const missingFile = z.lazy(() => z.object({ code: z.literal("ENOENT") }));
|
|
22
|
+
const workspaceId = computerGrantSchema.shape.computerId;
|
|
23
|
+
const command = computerExecutionRequestSchema({ maximumTimeMs: 45_000 }).omit({
|
|
24
|
+
requestId: true,
|
|
25
|
+
backend: true,
|
|
26
|
+
});
|
|
27
|
+
const empty = z.strictObject({});
|
|
28
|
+
// Frame-bound callers (Agent/Workflow methods) never supply requestId or
|
|
29
|
+
// requestScope; the host mints both from the invoking frame's idempotency
|
|
30
|
+
// key and installation (see computerInvocation's "processStart" case).
|
|
31
|
+
const processStart = processStartInputSchema.omit({
|
|
32
|
+
requestId: true,
|
|
33
|
+
requestScope: true,
|
|
34
|
+
});
|
|
35
|
+
// The Oyster capability HTTP route has no per-field header mapping (the
|
|
36
|
+
// generic page capability handle in packages/oyster/src/page/capabilities.ts
|
|
37
|
+
// just JSON-encodes the whole input), so requestId travels in the body here;
|
|
38
|
+
// requestScope still stays host-minted, never client-supplied.
|
|
39
|
+
const processStartHttpBody = processStartInputSchema.omit({
|
|
40
|
+
requestScope: true,
|
|
41
|
+
});
|
|
42
|
+
const processStartReply = z.lazy(() => z.union([
|
|
43
|
+
computerProcessSchema,
|
|
44
|
+
z.strictObject({ error: z.literal("conflict") }),
|
|
45
|
+
]));
|
|
46
|
+
const bindingWrite = writeInputSchema.extend({
|
|
47
|
+
expectedContent: replaceInputSchema.shape.expectedContent.optional(),
|
|
48
|
+
});
|
|
49
|
+
const allActions = new Set(computerActionSchema.options);
|
|
50
|
+
const terminalGrantHeader = "X-Oyster-Computer-Grant";
|
|
51
|
+
const terminalGrant = computerGrantSchema.extend({
|
|
52
|
+
actions: z
|
|
53
|
+
.array(computerActionSchema)
|
|
54
|
+
.transform((actions) => new Set(actions)),
|
|
55
|
+
});
|
|
56
|
+
function grant(computerId, actor) {
|
|
57
|
+
return {
|
|
58
|
+
computerId,
|
|
59
|
+
actions: allActions,
|
|
60
|
+
expiresAt: Date.now() + 60_000,
|
|
61
|
+
provenance: { actor },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The Computer grant actor for an admitted owner or member; null for an
|
|
66
|
+
* Agent or visitor. A key only when its own `computer.use` grant allows it
|
|
67
|
+
* -- off by default, so an unknown, revoked, expired or suspended key, or
|
|
68
|
+
* one issued with no Computer grant, is also null. `keysLive` is the one
|
|
69
|
+
* owner-checked port every caller reaches a key's liveness through (the
|
|
70
|
+
* same one `host.keys.live` is); a caller with none -- the Media binding's
|
|
71
|
+
* own workspace selection never calls through to a principal at all --
|
|
72
|
+
* fails every team-key closed rather than falling back to an unchecked read.
|
|
73
|
+
*/
|
|
74
|
+
async function computerActor(principal, keysLive) {
|
|
75
|
+
if (principal.actor.kind === "visitor" || principal.actor.kind === "agent")
|
|
76
|
+
return null;
|
|
77
|
+
if (principal.actor.kind === "team-key") {
|
|
78
|
+
const key = keysLive === undefined ? null : await keysLive(principal.actor.keyId);
|
|
79
|
+
if (key === null || !keyAllowsComputerUse(key))
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return JSON.stringify(principal);
|
|
83
|
+
}
|
|
84
|
+
/** Each advertised Computer tool, the grant action that accepts it, and its result. */
|
|
85
|
+
const computerTools = {
|
|
86
|
+
read: { action: "files.read", output: readOutputSchema },
|
|
87
|
+
list: { action: "files.list", output: listOutputSchema },
|
|
88
|
+
write: { action: "files.write", output: writeOutputSchema },
|
|
89
|
+
mkdir: { action: "mkdir", output: mkdirOutputSchema },
|
|
90
|
+
bash: { action: "bash", output: computerToolResultSchema },
|
|
91
|
+
exec: { action: "exec", output: computerToolResultSchema },
|
|
92
|
+
start_process: { action: "processStart", output: computerProcessSchema },
|
|
93
|
+
list_processes: { action: "processList", output: processListOutputSchema },
|
|
94
|
+
kill_process: { action: "processKill", output: processKillOutputSchema },
|
|
95
|
+
};
|
|
96
|
+
const computerTool = z.enum([
|
|
97
|
+
"read",
|
|
98
|
+
"list",
|
|
99
|
+
"write",
|
|
100
|
+
"mkdir",
|
|
101
|
+
"bash",
|
|
102
|
+
"exec",
|
|
103
|
+
"start_process",
|
|
104
|
+
"list_processes",
|
|
105
|
+
"kill_process",
|
|
106
|
+
]);
|
|
107
|
+
const previewRefusal = appMethodRefusedReplySchema.parse({
|
|
108
|
+
kind: "refused",
|
|
109
|
+
code: "forbidden",
|
|
110
|
+
message: "Port previews belong to the current owner of an enabled installation.",
|
|
111
|
+
});
|
|
112
|
+
/** Durable workspace and existing backings, with fleet checks at native acceptance. */
|
|
113
|
+
export class ComputerDO extends NativeComputerDO {
|
|
114
|
+
previewBindings;
|
|
115
|
+
#backends;
|
|
116
|
+
#preview;
|
|
117
|
+
constructor(state, previewBindings) {
|
|
118
|
+
// The Computer's HTTP(S) egress goes only through the gateway the
|
|
119
|
+
// standalone Worker exports beside this object.
|
|
120
|
+
super(state, {
|
|
121
|
+
...previewBindings,
|
|
122
|
+
COMPUTER_EGRESS: (options) => state.exports.OysterComputerEgress({ props: options.props }),
|
|
123
|
+
});
|
|
124
|
+
this.previewBindings = previewBindings;
|
|
125
|
+
}
|
|
126
|
+
#previewGrant(ownerId) {
|
|
127
|
+
return {
|
|
128
|
+
computerId: this.#id(),
|
|
129
|
+
actions: new Set(["listPorts"]),
|
|
130
|
+
expiresAt: Date.now() + 60_000,
|
|
131
|
+
provenance: { actor: `owner:${ownerId}` },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A preview selector names the owner by tenant member ID. The configured
|
|
136
|
+
* OYSTER_OWNER_NAME is only the owner's sign-in subject, so the selector is
|
|
137
|
+
* compared against the live canonical owner membership on every check.
|
|
138
|
+
*/
|
|
139
|
+
async #previewInstallation(selector) {
|
|
140
|
+
const owner = await (await standaloneTenancy(this.previewBindings)).owner();
|
|
141
|
+
if (selector.ownerId !== owner?.actor.memberId)
|
|
142
|
+
throw new PreviewRefused();
|
|
143
|
+
const current = await this.previewBindings.REGISTRY.getByName("registry").get(selector.installationId);
|
|
144
|
+
if (current === null ||
|
|
145
|
+
"kind" in current ||
|
|
146
|
+
current.status !== "enabled" ||
|
|
147
|
+
JSON.stringify(selectionOf(current)) !==
|
|
148
|
+
JSON.stringify(selector.selection) ||
|
|
149
|
+
current.release.artifact.artifactDigest !== selector.artifactDigest)
|
|
150
|
+
throw new PreviewRefused();
|
|
151
|
+
return current;
|
|
152
|
+
}
|
|
153
|
+
async #currentPreview(selector) {
|
|
154
|
+
const current = await this.#previewInstallation(selector);
|
|
155
|
+
const verified = await verifyRelease(r2ArtifactStore(this.previewBindings.RELEASES), current.release, undefined, false);
|
|
156
|
+
if (!verified.declaration?.capabilities.includes("computer.execute"))
|
|
157
|
+
throw new PreviewRefused();
|
|
158
|
+
await this.#previewInstallation(selector);
|
|
159
|
+
}
|
|
160
|
+
#previewOwner() {
|
|
161
|
+
if (this.#preview !== undefined)
|
|
162
|
+
return this.#preview;
|
|
163
|
+
const origins = previewOrigins(this.previewBindings);
|
|
164
|
+
if (origins === null)
|
|
165
|
+
throw new PreviewRefused();
|
|
166
|
+
this.#preview = createStandalonePreview(this.state.storage, this.#id(), origins.preview, {
|
|
167
|
+
current: (selector) => this.#currentPreview(selector),
|
|
168
|
+
accept: (selector) => {
|
|
169
|
+
this.#accept(this.#previewGrant(selector.ownerId), "listPorts");
|
|
170
|
+
},
|
|
171
|
+
list: (accept) => this.listPortsAfterAcceptance(accept),
|
|
172
|
+
forward: (port, request, accept) => this.portPreviewAfterAcceptance(port, request, accept),
|
|
173
|
+
});
|
|
174
|
+
return this.#preview;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Run one App build job's fixed command, under the build budget. The
|
|
178
|
+
* registry that minted the job re-admits it for this Computer first.
|
|
179
|
+
*/
|
|
180
|
+
async runBuild(input) {
|
|
181
|
+
const admitted = await this.previewBindings.REGISTRY.getByName("registry").admitBuildExecution(input.jobId, this.state.id.toString());
|
|
182
|
+
if (!admitted)
|
|
183
|
+
throw new AppInstanceError("revoked");
|
|
184
|
+
const result = await this.executeOperationAfterAcceptance({
|
|
185
|
+
command: input.command,
|
|
186
|
+
cwd: "/workspace",
|
|
187
|
+
timeoutMs: input.timeoutMs,
|
|
188
|
+
requestId: mintComputerRequestId(),
|
|
189
|
+
}, () => undefined, "bash", { maximumTimeMs: oysterAppBuildTimeoutMs });
|
|
190
|
+
const output = z
|
|
191
|
+
.object({ exitCode: z.number().nullable(), stderr: z.string() })
|
|
192
|
+
.safeParse(result.output);
|
|
193
|
+
return { output: output.success ? output.data : null };
|
|
194
|
+
}
|
|
195
|
+
/** Issue preview tickets for every exposed port, or refuse typed. */
|
|
196
|
+
async portPreviews(authority, selectorInput) {
|
|
197
|
+
try {
|
|
198
|
+
return {
|
|
199
|
+
kind: "ok",
|
|
200
|
+
value: await this.#portPreviews(authority, selectorInput),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch (cause) {
|
|
204
|
+
if (cause instanceof PreviewRefused)
|
|
205
|
+
return previewRefusal;
|
|
206
|
+
throw cause;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async #portPreviews(authority, selectorInput) {
|
|
210
|
+
const selector = Schema.decodeUnknownSync(previewSelectorSchema)(selectorInput);
|
|
211
|
+
if (authority.provenance.actor !== `owner:${selector.ownerId}`)
|
|
212
|
+
throw new PreviewRefused();
|
|
213
|
+
this.#accept(authority, "listPorts");
|
|
214
|
+
await this.#currentPreview(selector);
|
|
215
|
+
const ports = await this.listPortsAfterAcceptance(() => {
|
|
216
|
+
this.#accept(authority, "listPorts");
|
|
217
|
+
});
|
|
218
|
+
const projected = [];
|
|
219
|
+
for (const port of ports.ports) {
|
|
220
|
+
projected.push({
|
|
221
|
+
port,
|
|
222
|
+
...(await this.#previewOwner().issue(selector, port)),
|
|
223
|
+
openMode: "external-only",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return Schema.decodeUnknownSync(computerPortsSchema)({ ports: projected });
|
|
227
|
+
}
|
|
228
|
+
async portPreview(signature, port, request) {
|
|
229
|
+
return this.#previewOwner().fetch(signature, port, request);
|
|
230
|
+
}
|
|
231
|
+
#id() {
|
|
232
|
+
return workspaceId.parse(this.state.id.name);
|
|
233
|
+
}
|
|
234
|
+
attachedBackends() {
|
|
235
|
+
if (this.#backends !== undefined)
|
|
236
|
+
return this.#backends;
|
|
237
|
+
const owner = createFleetBacking(this.state.storage);
|
|
238
|
+
owner.ensure();
|
|
239
|
+
const hostId = this.bindings.COMPUTER_VM_DEFAULT_HOST;
|
|
240
|
+
this.#backends = owner.computerCreated(this.#id(), [
|
|
241
|
+
hostId === undefined
|
|
242
|
+
? { kind: "platform-container" }
|
|
243
|
+
: { kind: "on-box-vm", hostId },
|
|
244
|
+
]).backends;
|
|
245
|
+
return this.#backends;
|
|
246
|
+
}
|
|
247
|
+
#accept(authority, action) {
|
|
248
|
+
if (checkComputerGrant(authority, {
|
|
249
|
+
computerId: this.#id(),
|
|
250
|
+
action,
|
|
251
|
+
now: Date.now(),
|
|
252
|
+
}).kind !== "admitted")
|
|
253
|
+
throw new FleetGrantRefused();
|
|
254
|
+
}
|
|
255
|
+
/** One advertised Computer tool, accepted against the host-issued grant. */
|
|
256
|
+
async tool(authority, capabilityId, input) {
|
|
257
|
+
const tool = computerTool.parse(capabilityId);
|
|
258
|
+
const { action, output } = computerTools[tool];
|
|
259
|
+
const accept = () => {
|
|
260
|
+
this.#accept(authority, action);
|
|
261
|
+
};
|
|
262
|
+
if (tool === "start_process")
|
|
263
|
+
return this.startProcessAfterAcceptance(computerValueSchema.parse({
|
|
264
|
+
...processStartToolInputSchema.parse(input),
|
|
265
|
+
requestId: mintComputerRequestId(),
|
|
266
|
+
requestScope: `tool:${authority.provenance.actor}`,
|
|
267
|
+
}), accept);
|
|
268
|
+
return output.parse(await this.callAfterAcceptance(tool, input, accept));
|
|
269
|
+
}
|
|
270
|
+
/** JSON operations keep the Computer's maintained parsers and execution engine. */
|
|
271
|
+
async value(authority, action, input) {
|
|
272
|
+
const accept = () => {
|
|
273
|
+
this.#accept(authority, action);
|
|
274
|
+
};
|
|
275
|
+
switch (action) {
|
|
276
|
+
case "listPorts":
|
|
277
|
+
return this.listPortsAfterAcceptance(accept);
|
|
278
|
+
case "exposePort":
|
|
279
|
+
return this.exposePortAfterAcceptance(computerPortInputSchema.parse(input), accept);
|
|
280
|
+
case "unexposePort":
|
|
281
|
+
return this.unexposePortAfterAcceptance(computerPortInputSchema.parse(input), accept);
|
|
282
|
+
case "processList":
|
|
283
|
+
return this.listProcessesAfterAcceptance(accept);
|
|
284
|
+
case "processStart":
|
|
285
|
+
try {
|
|
286
|
+
return await this.startProcessAfterAcceptance(input, accept);
|
|
287
|
+
}
|
|
288
|
+
catch (cause) {
|
|
289
|
+
if (cause instanceof ComputerOperationConflictError ||
|
|
290
|
+
cause instanceof ComputerPortConflictError)
|
|
291
|
+
return { error: "conflict" };
|
|
292
|
+
throw cause;
|
|
293
|
+
}
|
|
294
|
+
case "processKill":
|
|
295
|
+
return this.killProcessAfterAcceptance(processIdInputSchema.parse(input), accept);
|
|
296
|
+
case "exec":
|
|
297
|
+
return this.executeOperationAfterAcceptance({ ...command.parse(input), requestId: mintComputerRequestId() }, accept, "bash", { maximumTimeMs: 45_000 });
|
|
298
|
+
case "files.list":
|
|
299
|
+
return listOutputSchema.parse(await this.callAfterAcceptance("list", input, accept));
|
|
300
|
+
case "files.readBytes": {
|
|
301
|
+
const parsed = readBytesInputSchema.parse(input);
|
|
302
|
+
const file = await this.readFileAfterAcceptance(parsed, accept);
|
|
303
|
+
const bytes = new Uint8Array(await new Response(file.body).arrayBuffer());
|
|
304
|
+
return {
|
|
305
|
+
path: parsed.path,
|
|
306
|
+
byteOffset: file.offset,
|
|
307
|
+
content: Base64.encode(bytes),
|
|
308
|
+
encoding: "base64",
|
|
309
|
+
truncated: (parsed.byteOffset !== undefined ||
|
|
310
|
+
parsed.byteLength !== undefined) &&
|
|
311
|
+
file.length > 0,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
case "files.edit":
|
|
315
|
+
try {
|
|
316
|
+
return await this.replaceFileIfUnchangedAfterAcceptance(replaceInputSchema.parse(input), accept);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
if (error instanceof ComputerFileConflict)
|
|
320
|
+
return { error: "conflict" };
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
case "files.write":
|
|
324
|
+
return writeOutputSchema.parse(await this.callAfterAcceptance("write", input, accept));
|
|
325
|
+
case "mkdir":
|
|
326
|
+
return mkdirOutputSchema.parse(await this.callAfterAcceptance("mkdir", input, accept));
|
|
327
|
+
default:
|
|
328
|
+
throw new Error(`Computer action is not exposed by this adapter: ${action}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async fileList(authority, path, limit, offset) {
|
|
332
|
+
try {
|
|
333
|
+
return listOutputSchema.parse(await this.callAfterAcceptance("list", { path, limit: Math.min(limit, 256), offset }, () => {
|
|
334
|
+
this.#accept(authority, "files.list");
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (missingFile.safeParse(error).success)
|
|
339
|
+
return null;
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/** Binary files share the same native filesystem as commands and PTYs. */
|
|
344
|
+
async fileRead(authority, path) {
|
|
345
|
+
try {
|
|
346
|
+
const file = await this.readFileAfterAcceptance({ path }, () => {
|
|
347
|
+
this.#accept(authority, "files.readBytes");
|
|
348
|
+
}, 25 * 1024 * 1024);
|
|
349
|
+
return new Uint8Array(await new Response(file.body).arrayBuffer());
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
if (missingFile.safeParse(error).success)
|
|
353
|
+
return null;
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async fileWrite(authority, path, bytes) {
|
|
358
|
+
await this.writeFileAfterAcceptance({ path }, new ReadableStream({
|
|
359
|
+
start(controller) {
|
|
360
|
+
controller.enqueue(bytes);
|
|
361
|
+
controller.close();
|
|
362
|
+
},
|
|
363
|
+
}), () => {
|
|
364
|
+
this.#accept(authority, "files.writeBytes");
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async fileReplace(authority, path, expectedContent, content) {
|
|
368
|
+
try {
|
|
369
|
+
const result = await this.replaceFileIfUnchangedAfterAcceptance({ path, expectedContent, content }, () => {
|
|
370
|
+
this.#accept(authority, "files.edit");
|
|
371
|
+
});
|
|
372
|
+
return { kind: "replaced", ...result };
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (error instanceof ComputerFileConflict)
|
|
376
|
+
return { kind: "conflict" };
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** WebSocket upgrades use native Fetch, which can return a 101 response. */
|
|
381
|
+
async fetch(request) {
|
|
382
|
+
if (new URL(request.url).pathname !== "/_oyster/terminal")
|
|
383
|
+
return super.fetch(request);
|
|
384
|
+
let authority;
|
|
385
|
+
try {
|
|
386
|
+
authority = terminalGrant.parse(JSON.parse(request.headers.get(terminalGrantHeader) ?? "null"));
|
|
387
|
+
this.#accept(authority, "terminal");
|
|
388
|
+
}
|
|
389
|
+
catch (cause) {
|
|
390
|
+
if (cause instanceof SyntaxError ||
|
|
391
|
+
cause instanceof z.ZodError ||
|
|
392
|
+
cause instanceof FleetGrantRefused)
|
|
393
|
+
return new Response("Computer grant refused", { status: 403 });
|
|
394
|
+
throw cause;
|
|
395
|
+
}
|
|
396
|
+
return this.terminalAfterAcceptance(request, authority.provenance.actor, () => {
|
|
397
|
+
this.#accept(authority, "terminal");
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Select the fixed native Computer once for the HTTP/MCP host and its
|
|
403
|
+
* adapters. `keysLive` is the host's own owner-checked `host.keys.live`
|
|
404
|
+
* port, threaded in by `oysterGitWorkerHost` before this workspace exists;
|
|
405
|
+
* a caller that only needs `.files` (the Media binding's own dispatch) may
|
|
406
|
+
* omit it, since a team-key principal then simply never reaches the
|
|
407
|
+
* Computer, rather than reading `env.KEYS` directly and skipping the check.
|
|
408
|
+
*/
|
|
409
|
+
export function standaloneWorkspace(env, keysLive) {
|
|
410
|
+
const computerId = workspaceId.parse(env.OYSTER_COMPUTER_ID);
|
|
411
|
+
const computer = () => env.COMPUTER.getByName(computerId);
|
|
412
|
+
const authority = () => grant(computerId, "oyster-workspace");
|
|
413
|
+
return {
|
|
414
|
+
computerId,
|
|
415
|
+
computer: async (principal) => {
|
|
416
|
+
const actor = await computerActor(principal, keysLive);
|
|
417
|
+
if (actor === null)
|
|
418
|
+
return null;
|
|
419
|
+
return {
|
|
420
|
+
// Every Computer advertises the same static operations.
|
|
421
|
+
getCapabilities: () => Promise.resolve(computerValueSchema.parse(computerCapabilities())),
|
|
422
|
+
call: async (capabilityId, input) => computerValueSchema.parse(await computer().tool(grant(computerId, actor), capabilityId, input)),
|
|
423
|
+
};
|
|
424
|
+
},
|
|
425
|
+
files: {
|
|
426
|
+
list: (path, limit, offset) => computer().fileList(authority(), path, limit, offset),
|
|
427
|
+
read: (path) => computer().fileRead(authority(), path),
|
|
428
|
+
write: (path, bytes) => computer().fileWrite(authority(), path, bytes),
|
|
429
|
+
mkdir: async (path) => {
|
|
430
|
+
await computer().value(authority(), "mkdir", { path, recursive: true });
|
|
431
|
+
},
|
|
432
|
+
replace: (path, expected, content) => computer().fileReplace(authority(), path, expected, content),
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Owner/key capability routes over the selected Computer's native
|
|
438
|
+
* namespace. `keysLive` is the same owner-checked port `standaloneWorkspace`
|
|
439
|
+
* takes; a caller with none fails every team-key request closed.
|
|
440
|
+
*/
|
|
441
|
+
export function standaloneComputerCapabilities(env, workspace, keysLive) {
|
|
442
|
+
const routes = new Hono();
|
|
443
|
+
routes.use("*", async (context, next) => {
|
|
444
|
+
const actor = await computerActor(context.env.principal, keysLive);
|
|
445
|
+
if (actor === null)
|
|
446
|
+
return context.json({ error: "Computer access requires an owner or key" }, 403);
|
|
447
|
+
context.set("authority", grant(workspace.computerId, actor));
|
|
448
|
+
return next();
|
|
449
|
+
});
|
|
450
|
+
const computer = () => env.COMPUTER.getByName(workspace.computerId);
|
|
451
|
+
routes.get("/ports", async (context) => {
|
|
452
|
+
if (previewOrigins(env) === null)
|
|
453
|
+
return context.json({ error: "port_previews_not_configured" });
|
|
454
|
+
const { principal, installation } = context.env;
|
|
455
|
+
if (!oysterPrincipalIsOwner(principal))
|
|
456
|
+
return context.json({ error: "Owner preview access required" }, 403);
|
|
457
|
+
const ownerId = oysterPrincipalId(principal);
|
|
458
|
+
const authority = context.get("authority");
|
|
459
|
+
const reply = await computer().portPreviews({
|
|
460
|
+
...authority,
|
|
461
|
+
provenance: { ...authority.provenance, actor: `owner:${ownerId}` },
|
|
462
|
+
}, {
|
|
463
|
+
ownerId,
|
|
464
|
+
installationId: installation.installationId,
|
|
465
|
+
selection: selectionOf(installation),
|
|
466
|
+
artifactDigest: installation.release.artifact.artifactDigest,
|
|
467
|
+
});
|
|
468
|
+
return reply.kind === "ok"
|
|
469
|
+
? context.json(reply.value)
|
|
470
|
+
: context.json({ error: reply.code, message: reply.message }, appMethodRefusalStatus[reply.code]);
|
|
471
|
+
});
|
|
472
|
+
// The host wire body is `{ port }`, as on Hub; it is parsed here.
|
|
473
|
+
routes.post("/ports/expose", async (context) => {
|
|
474
|
+
const input = computerPortInputSchema.safeParse(await context.req.json());
|
|
475
|
+
if (!input.success)
|
|
476
|
+
return context.json({ error: "invalid_input" }, 400);
|
|
477
|
+
return Response.json(await computer().value(context.get("authority"), "exposePort", input.data));
|
|
478
|
+
});
|
|
479
|
+
// The host wire body is `{ port }`, as on Hub; it is parsed here.
|
|
480
|
+
routes.post("/ports/unexpose", async (context) => {
|
|
481
|
+
const input = computerPortInputSchema.safeParse(await context.req.json());
|
|
482
|
+
if (!input.success)
|
|
483
|
+
return context.json({ error: "invalid_input" }, 400);
|
|
484
|
+
return Response.json(await computer().value(context.get("authority"), "unexposePort", input.data));
|
|
485
|
+
});
|
|
486
|
+
routes.get("/processes", async (context) => Response.json(await computer().value(context.get("authority"), "processList", {})));
|
|
487
|
+
routes.post("/processes/start", async (context) => {
|
|
488
|
+
const input = processStartHttpBody.safeParse(await context.req.json());
|
|
489
|
+
if (!input.success)
|
|
490
|
+
return context.json({ error: "invalid_input" }, 400);
|
|
491
|
+
const result = processStartReply.parse(await computer().value(context.get("authority"), "processStart", computerValueSchema.parse({
|
|
492
|
+
...input.data,
|
|
493
|
+
requestScope: `owner:${env.OYSTER_OWNER_NAME ?? "owner"}`,
|
|
494
|
+
})));
|
|
495
|
+
return Response.json(result, { status: "error" in result ? 409 : 200 });
|
|
496
|
+
});
|
|
497
|
+
// The host wire body is `{ id }`, as on Hub; it is parsed here.
|
|
498
|
+
routes.post("/processes/kill", async (context) => {
|
|
499
|
+
const input = processIdInputSchema.safeParse(await context.req.json());
|
|
500
|
+
if (!input.success)
|
|
501
|
+
return context.json({ error: "invalid_input" }, 400);
|
|
502
|
+
return Response.json(await computer().value(context.get("authority"), "processKill", input.data));
|
|
503
|
+
});
|
|
504
|
+
routes.get("/terminal", async (context) => {
|
|
505
|
+
const authority = context.get("authority");
|
|
506
|
+
const url = new URL(context.req.url);
|
|
507
|
+
url.pathname = "/_oyster/terminal";
|
|
508
|
+
return computer().fetch(new Request(url, {
|
|
509
|
+
headers: {
|
|
510
|
+
Upgrade: "websocket",
|
|
511
|
+
[terminalGrantHeader]: JSON.stringify({
|
|
512
|
+
...authority,
|
|
513
|
+
actions: [...authority.actions],
|
|
514
|
+
}),
|
|
515
|
+
},
|
|
516
|
+
}));
|
|
517
|
+
});
|
|
518
|
+
return Object.values(computerSlots).map((slot) => ({
|
|
519
|
+
...slot,
|
|
520
|
+
permission: "computer.execute",
|
|
521
|
+
routes: new Hono().route("/", routes),
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
/** Closed COMPUTER binding codecs, joined into the standalone Instance. */
|
|
525
|
+
export const computerFrameMethods = {
|
|
526
|
+
COMPUTER: {
|
|
527
|
+
workflowGitPass: {
|
|
528
|
+
decodeArgs: (args) => z.tuple([empty]).parse(args),
|
|
529
|
+
decodeResult: (value) => workflowGitPassResultSchema.parse(value),
|
|
530
|
+
requiredOperations: () => ["workflowGitPass"],
|
|
531
|
+
},
|
|
532
|
+
list: {
|
|
533
|
+
decodeArgs: (args) => z.tuple([listInputSchema]).parse(args),
|
|
534
|
+
decodeResult: (value) => listOutputSchema.parse(value),
|
|
535
|
+
requiredOperations: () => ["list"],
|
|
536
|
+
},
|
|
537
|
+
read: {
|
|
538
|
+
decodeArgs: (args) => z.tuple([readBytesInputSchema]).parse(args),
|
|
539
|
+
decodeResult: (value) => computerByteReadReplySchema.parse(value),
|
|
540
|
+
requiredOperations: () => ["read"],
|
|
541
|
+
},
|
|
542
|
+
write: {
|
|
543
|
+
decodeArgs: (args) => z.tuple([bindingWrite]).parse(args),
|
|
544
|
+
decodeResult: (value) => z
|
|
545
|
+
.union([
|
|
546
|
+
writeOutputSchema,
|
|
547
|
+
z.strictObject({ error: z.literal("conflict") }),
|
|
548
|
+
])
|
|
549
|
+
.parse(value),
|
|
550
|
+
requiredOperations: () => ["write"],
|
|
551
|
+
},
|
|
552
|
+
ports: {
|
|
553
|
+
decodeArgs: (args) => z.tuple([empty]).parse(args),
|
|
554
|
+
decodeResult: (value) => computerPortsOutputSchema.parse(value),
|
|
555
|
+
requiredOperations: () => ["ports"],
|
|
556
|
+
},
|
|
557
|
+
processes: {
|
|
558
|
+
decodeArgs: (args) => z.tuple([empty]).parse(args),
|
|
559
|
+
decodeResult: (value) => processListOutputSchema.parse(value),
|
|
560
|
+
requiredOperations: () => ["processes"],
|
|
561
|
+
},
|
|
562
|
+
exec: {
|
|
563
|
+
decodeArgs: (args) => z.tuple([command]).parse(args),
|
|
564
|
+
decodeResult: (value) => computerOperationResultSchema.parse(value),
|
|
565
|
+
requiredOperations: () => ["exec"],
|
|
566
|
+
},
|
|
567
|
+
processStart: {
|
|
568
|
+
decodeArgs: (args) => z.tuple([processStart]).parse(args),
|
|
569
|
+
decodeResult: (value) => processStartReply.parse(value),
|
|
570
|
+
requiredOperations: () => ["processStart"],
|
|
571
|
+
},
|
|
572
|
+
processKill: {
|
|
573
|
+
decodeArgs: (args) => z.tuple([processIdInputSchema]).parse(args),
|
|
574
|
+
decodeResult: (value) => processKillOutputSchema.parse(value),
|
|
575
|
+
requiredOperations: () => ["processKill"],
|
|
576
|
+
},
|
|
577
|
+
preview: {
|
|
578
|
+
decodeArgs: (args) => z.tuple([computerPortInputSchema]).parse(args),
|
|
579
|
+
decodeResult: (value) => {
|
|
580
|
+
const ports = Schema.decodeUnknownSync(computerPortsSchema, {
|
|
581
|
+
onExcessProperty: "error",
|
|
582
|
+
})({ ports: [value] });
|
|
583
|
+
const preview = ports.ports[0];
|
|
584
|
+
if (preview === undefined)
|
|
585
|
+
throw new AppInstanceError("revoked");
|
|
586
|
+
return preview;
|
|
587
|
+
},
|
|
588
|
+
requiredOperations: () => ["preview"],
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
/** Owner-only preview tickets carry the verified frame's Installation selection. */
|
|
593
|
+
async function computerPreviewInvocation(env, frame, args) {
|
|
594
|
+
const subject = frame.facts.subject;
|
|
595
|
+
const computerId = workspaceId.parse(env.OYSTER_COMPUTER_ID);
|
|
596
|
+
const owner = frame.facts.selection.owner;
|
|
597
|
+
if (subject.kind !== "call" ||
|
|
598
|
+
subject.context.actor.kind !== "member" ||
|
|
599
|
+
subject.context.role !== "owner" ||
|
|
600
|
+
owner.kind !== "installation")
|
|
601
|
+
throw new AppInstanceError("revoked");
|
|
602
|
+
// The admitted owner's member ID; the Computer owner proves it is still
|
|
603
|
+
// the live canonical owner before issuing any ticket.
|
|
604
|
+
const ownerId = subject.context.actor.memberId;
|
|
605
|
+
const [parsed] = z.tuple([computerPortInputSchema]).parse(args);
|
|
606
|
+
const native = env.COMPUTER.getByName(computerId);
|
|
607
|
+
frame.signal.throwIfAborted();
|
|
608
|
+
const result = await native.portPreviews({
|
|
609
|
+
computerId,
|
|
610
|
+
actions: new Set(["listPorts"]),
|
|
611
|
+
expiresAt: Math.min(frame.facts.deadlineAt, Date.now() + 60_000),
|
|
612
|
+
provenance: { actor: `owner:${ownerId}` },
|
|
613
|
+
}, {
|
|
614
|
+
ownerId,
|
|
615
|
+
installationId: owner.id,
|
|
616
|
+
selection: frame.facts.selection,
|
|
617
|
+
artifactDigest: frame.host.artifact.release.artifact.artifactDigest,
|
|
618
|
+
});
|
|
619
|
+
if (result.kind === "refused")
|
|
620
|
+
throw new AppInstanceError("revoked");
|
|
621
|
+
const preview = result.value.ports.find((row) => row.port === parsed.port);
|
|
622
|
+
if (preview === undefined)
|
|
623
|
+
throw new AppInstanceError("revoked");
|
|
624
|
+
return preview;
|
|
625
|
+
}
|
|
626
|
+
/** Dispatch one admitted COMPUTER operation under its captured frame. */
|
|
627
|
+
export function computerInvocation(env, frame, method, args) {
|
|
628
|
+
frame.signal.throwIfAborted();
|
|
629
|
+
const subject = frame.facts.subject;
|
|
630
|
+
const computerId = workspaceId.parse(env.OYSTER_COMPUTER_ID);
|
|
631
|
+
if (subject.kind !== "call" || subject.context.computerId !== computerId)
|
|
632
|
+
throw new AppInstanceError("revoked");
|
|
633
|
+
const actor = subject.context.actor;
|
|
634
|
+
if (method === "preview")
|
|
635
|
+
return computerPreviewInvocation(env, frame, args);
|
|
636
|
+
const actorId = actor.kind === "member"
|
|
637
|
+
? actor.memberId
|
|
638
|
+
: actor.kind === "agent"
|
|
639
|
+
? actor.agentId
|
|
640
|
+
: actor.keyId;
|
|
641
|
+
let action;
|
|
642
|
+
let input;
|
|
643
|
+
switch (method) {
|
|
644
|
+
case "list":
|
|
645
|
+
action = "files.list";
|
|
646
|
+
[input] = z.tuple([listInputSchema]).parse(args);
|
|
647
|
+
break;
|
|
648
|
+
case "read":
|
|
649
|
+
action = "files.readBytes";
|
|
650
|
+
[input] = z.tuple([computerValueSchema]).parse(args);
|
|
651
|
+
break;
|
|
652
|
+
case "write": {
|
|
653
|
+
const [parsed] = z.tuple([bindingWrite]).parse(args);
|
|
654
|
+
action =
|
|
655
|
+
parsed.expectedContent === undefined ? "files.write" : "files.edit";
|
|
656
|
+
input =
|
|
657
|
+
parsed.expectedContent === undefined
|
|
658
|
+
? { path: parsed.path, content: parsed.content }
|
|
659
|
+
: {
|
|
660
|
+
path: parsed.path,
|
|
661
|
+
content: parsed.content,
|
|
662
|
+
expectedContent: parsed.expectedContent,
|
|
663
|
+
};
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "ports":
|
|
667
|
+
action = "listPorts";
|
|
668
|
+
[input] = z.tuple([empty]).parse(args);
|
|
669
|
+
break;
|
|
670
|
+
case "processes":
|
|
671
|
+
action = "processList";
|
|
672
|
+
[input] = z.tuple([empty]).parse(args);
|
|
673
|
+
break;
|
|
674
|
+
case "exec":
|
|
675
|
+
action = "exec";
|
|
676
|
+
[input] = z.tuple([computerValueSchema]).parse(args);
|
|
677
|
+
break;
|
|
678
|
+
case "processStart": {
|
|
679
|
+
const [parsed] = z.tuple([processStart]).parse(args);
|
|
680
|
+
const requestId = subject.context.idempotencyKey;
|
|
681
|
+
const owner = frame.facts.selection.owner;
|
|
682
|
+
const methodName = frame.host.method?.name;
|
|
683
|
+
if (requestId === undefined ||
|
|
684
|
+
actor.kind !== "member" ||
|
|
685
|
+
owner.kind !== "installation" ||
|
|
686
|
+
methodName === undefined)
|
|
687
|
+
throw new AppInstanceError("revoked");
|
|
688
|
+
action = "processStart";
|
|
689
|
+
input = computerValueSchema.parse({
|
|
690
|
+
...parsed,
|
|
691
|
+
requestId,
|
|
692
|
+
requestScope: installationProcessScope(owner.id, methodName, {
|
|
693
|
+
kind: actor.kind,
|
|
694
|
+
id: actorId,
|
|
695
|
+
}),
|
|
696
|
+
});
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case "processKill":
|
|
700
|
+
action = "processKill";
|
|
701
|
+
[input] = z.tuple([processIdInputSchema]).parse(args);
|
|
702
|
+
break;
|
|
703
|
+
default:
|
|
704
|
+
throw new AppInstanceError("revoked");
|
|
705
|
+
}
|
|
706
|
+
const native = env.COMPUTER.getByName(computerId);
|
|
707
|
+
frame.signal.throwIfAborted();
|
|
708
|
+
return native.value({
|
|
709
|
+
computerId,
|
|
710
|
+
actions: new Set([action]),
|
|
711
|
+
expiresAt: Math.min(frame.facts.deadlineAt, Date.now() + 60_000),
|
|
712
|
+
provenance: { actor: `${actor.kind}:${actorId}` },
|
|
713
|
+
}, action, input);
|
|
714
|
+
}
|