@fungi.computer/shed 0.1.0 → 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 +17 -5
- package/backend/methods.ts +34 -44
- package/backend/process-page.ts +1 -1
- package/backend/runs.ts +72 -183
- package/backend/test/methods.test.ts +299 -106
- package/backend/test/runs.node.test.ts +447 -268
- package/dist/assets/index-BnXCnX-l.css +1 -0
- package/dist/assets/index-CRz0hCtN.js +1769 -0
- package/dist/index.html +2 -2
- package/package.json +35 -19
- package/dist/assets/index-2RtcU_CN.css +0 -1
- package/dist/assets/index-bW5A7RuN.js +0 -1769
package/README.md
CHANGED
|
@@ -78,11 +78,11 @@ Agents call Shed through Oyster's MCP endpoint, or at
|
|
|
78
78
|
`POST $OYSTER_URL/apps/shed/methods/<name>` with a key; see
|
|
79
79
|
[Git, keys and MCP](https://github.com/fungi-computer/oyster#git-keys-and-mcp).
|
|
80
80
|
|
|
81
|
-
| Method | What it does
|
|
82
|
-
| ----------------------------------------- |
|
|
83
|
-
| `runCommand({command, cwd?, timeoutMs?})` | Runs a shell command for up to 45 seconds
|
|
84
|
-
| `listProcesses({})` | Lists up to 64 processes on the Computer. Starts nothing.
|
|
85
|
-
| `listPorts({})` | Lists up to 64 forwarded ports. Starts nothing.
|
|
81
|
+
| Method | What it does |
|
|
82
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
83
|
+
| `runCommand({command, cwd?, timeoutMs?})` | Runs a shell command for up to 45 seconds. Returns `stdout`, `stderr`, `exitCode`, `stdoutTruncated` and `stderrTruncated` (64 KiB per stream), or `{ error: code }`. A budget refusal also supplies `kind` and `retryAfterMs`. Workflow Git commands use `$FUNGI_GIT_REMOTE`. |
|
|
84
|
+
| `listProcesses({})` | Lists up to 64 processes on the Computer. Starts nothing. |
|
|
85
|
+
| `listPorts({})` | Lists up to 64 forwarded ports. Starts nothing. |
|
|
86
86
|
|
|
87
87
|
Workflows (see [Trellis](https://github.com/fungi-computer/trellis)) use a
|
|
88
88
|
second set for longer work:
|
|
@@ -121,3 +121,15 @@ agent tools are in [backend/methods.ts](backend/methods.ts), and
|
|
|
121
121
|
## License
|
|
122
122
|
|
|
123
123
|
MIT. See [LICENSE](LICENSE).
|
|
124
|
+
|
|
125
|
+
## Local film replay
|
|
126
|
+
|
|
127
|
+
`TerminalPanel` optionally accepts `onTerminalReady(handle)` after Ghostty and
|
|
128
|
+
its font metrics are ready. The typed `TerminalFilmHandle` exposes `write`,
|
|
129
|
+
`reset` and `resize` with Ghostty's ordinary signatures, plus
|
|
130
|
+
`await handle.paint()` to finish drawing the current buffer to the canvas before
|
|
131
|
+
capturing it. The handle follows theme/width renderer replacements and throws
|
|
132
|
+
after unmount. Production callers omit the callback; terminal sessions and input
|
|
133
|
+
stay owned by Shed. Replay callers should reset, resize, write their frame, then
|
|
134
|
+
await `paint()`; a native write callback alone is not a canvas completion
|
|
135
|
+
signal.
|
package/backend/methods.ts
CHANGED
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
} from "./process-page.js";
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import { Base64 } from "effect/encoding";
|
|
10
|
-
import { computerPortInputSchema } from "@fungi.computer/computer
|
|
10
|
+
import { computerPortInputSchema } from "@fungi.computer/computer-contract";
|
|
11
11
|
import {
|
|
12
12
|
runShedCommand,
|
|
13
|
+
computerCall,
|
|
13
14
|
shedRunCommandInputSchema,
|
|
14
15
|
shedRunInputSchema,
|
|
15
16
|
shedStartInputSchema,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
computerProcessSchema,
|
|
32
33
|
processIdInputSchema,
|
|
33
34
|
processKillOutputSchema,
|
|
34
|
-
} from "@fungi.computer/computer
|
|
35
|
+
} from "@fungi.computer/computer-contract";
|
|
35
36
|
const empty = z.strictObject({});
|
|
36
37
|
const command = runCommandInputSchema;
|
|
37
38
|
const refusal = shedComputerRefusalSchema;
|
|
@@ -42,19 +43,19 @@ const commandReply = runCommandResultSchema;
|
|
|
42
43
|
export type ShedMethodContext = Readonly<{
|
|
43
44
|
runs?: ShedRuns;
|
|
44
45
|
env: Readonly<{
|
|
45
|
-
COMPUTER
|
|
46
|
+
COMPUTER: ShedComputer &
|
|
46
47
|
Readonly<{
|
|
47
|
-
ports
|
|
48
|
-
preview
|
|
49
|
-
processKill
|
|
48
|
+
ports(input: Record<string, never>): Promise<WhistleJsonValue>;
|
|
49
|
+
preview(input: { port: number }): Promise<z.core.util.JSONType>;
|
|
50
|
+
processKill(
|
|
50
51
|
input: z.output<typeof processIdInputSchema>,
|
|
51
52
|
): Promise<z.core.util.JSONType>;
|
|
52
|
-
read
|
|
53
|
+
read(input: {
|
|
53
54
|
path: string;
|
|
54
55
|
byteOffset: number;
|
|
55
56
|
byteLength: number;
|
|
56
57
|
}): Promise<z.core.util.JSONType>;
|
|
57
|
-
exec
|
|
58
|
+
exec(input: z.output<typeof command>): Promise<WhistleJsonValue>;
|
|
58
59
|
}>;
|
|
59
60
|
}>;
|
|
60
61
|
}>;
|
|
@@ -97,7 +98,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
97
98
|
input: computerPortInputSchema,
|
|
98
99
|
output: preview,
|
|
99
100
|
handler: async (input, context: ShedMethodContext, caller) => {
|
|
100
|
-
if (!caller.computerId
|
|
101
|
+
if (!caller.computerId)
|
|
101
102
|
throw new Error("Computer preview access is not granted");
|
|
102
103
|
// Project the issued URL; preview-row presentation policy stays with the host.
|
|
103
104
|
return z
|
|
@@ -120,7 +121,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
120
121
|
z.strictObject({ error: z.enum(["not_found", "ambiguous"]) }),
|
|
121
122
|
]),
|
|
122
123
|
handler: async (input, context: ShedMethodContext, caller) => {
|
|
123
|
-
if (!caller.computerId || !context.
|
|
124
|
+
if (!caller.computerId || !context.runs)
|
|
124
125
|
throw new Error("Computer file access is not granted");
|
|
125
126
|
const log = context.runs.logPath(
|
|
126
127
|
input.run,
|
|
@@ -151,7 +152,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
151
152
|
description:
|
|
152
153
|
"Start one process on the pinned Computer and return its process and log path without waiting for exit.",
|
|
153
154
|
effect: "write",
|
|
154
|
-
requires: ["computer.execute"
|
|
155
|
+
requires: ["computer.execute"],
|
|
155
156
|
pending: true,
|
|
156
157
|
input: shedStartInputSchema,
|
|
157
158
|
output: z.union([
|
|
@@ -164,29 +165,19 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
164
165
|
]),
|
|
165
166
|
handler: (input, context: ShedMethodContext, caller) => {
|
|
166
167
|
const computer = context.env.COMPUTER;
|
|
167
|
-
if (
|
|
168
|
-
!caller.computerId ||
|
|
169
|
-
!computer?.processStart ||
|
|
170
|
-
!computer.processes ||
|
|
171
|
-
!context.runs
|
|
172
|
-
)
|
|
168
|
+
if (!caller.computerId || !context.runs)
|
|
173
169
|
throw new Error("Computer access is not granted");
|
|
174
170
|
if (!caller.idempotencyKey)
|
|
175
171
|
throw new Error(
|
|
176
172
|
"A host idempotency key is required to start a process",
|
|
177
173
|
);
|
|
178
|
-
const { processStart, processes
|
|
174
|
+
const { processStart, processes } = computer;
|
|
179
175
|
return context.runs.start(
|
|
180
176
|
input,
|
|
181
177
|
caller.computerId,
|
|
182
178
|
caller.idempotencyKey,
|
|
183
179
|
caller.actor,
|
|
184
|
-
{
|
|
185
|
-
processStart,
|
|
186
|
-
processes,
|
|
187
|
-
workflowGitPass,
|
|
188
|
-
write,
|
|
189
|
-
},
|
|
180
|
+
{ processStart, processes },
|
|
190
181
|
);
|
|
191
182
|
},
|
|
192
183
|
}),
|
|
@@ -197,20 +188,22 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
197
188
|
input: processIdInputSchema,
|
|
198
189
|
output: z.union([processKillOutputSchema, refusal]),
|
|
199
190
|
handler: async (input, context: ShedMethodContext, caller) => {
|
|
200
|
-
if (!caller.computerId
|
|
191
|
+
if (!caller.computerId)
|
|
201
192
|
throw new Error("Computer access is not granted");
|
|
202
193
|
if (!context.runs?.owns(input.id, caller.computerId, caller.actor))
|
|
203
194
|
return refusal.parse({ error: "not_found" });
|
|
204
195
|
return z
|
|
205
196
|
.union([processKillOutputSchema, refusal])
|
|
206
|
-
.parse(
|
|
197
|
+
.parse(
|
|
198
|
+
await computerCall(() => context.env.COMPUTER.processKill(input)),
|
|
199
|
+
);
|
|
207
200
|
},
|
|
208
201
|
}),
|
|
209
202
|
run: defineAppMethod({
|
|
210
203
|
description:
|
|
211
204
|
"Run one command on the pinned Computer, retaining a log and polling until it exits.",
|
|
212
205
|
effect: "write",
|
|
213
|
-
requires: ["computer.execute"
|
|
206
|
+
requires: ["computer.execute"],
|
|
214
207
|
pending: true,
|
|
215
208
|
input: shedRunInputSchema,
|
|
216
209
|
output: z.union([
|
|
@@ -245,14 +238,9 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
245
238
|
},
|
|
246
239
|
handler: (input, context: ShedMethodContext, caller) => {
|
|
247
240
|
const computer = context.env.COMPUTER;
|
|
248
|
-
if (
|
|
249
|
-
!caller.computerId ||
|
|
250
|
-
!computer?.processStart ||
|
|
251
|
-
!computer.processes ||
|
|
252
|
-
!context.runs
|
|
253
|
-
)
|
|
241
|
+
if (!caller.computerId || !context.runs)
|
|
254
242
|
throw new Error("Computer access is not granted");
|
|
255
|
-
const { processStart, processes
|
|
243
|
+
const { processStart, processes } = computer;
|
|
256
244
|
if (!caller.idempotencyKey)
|
|
257
245
|
throw new Error("A host idempotency key is required to start a run");
|
|
258
246
|
return context.runs.run(
|
|
@@ -260,7 +248,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
260
248
|
caller.computerId,
|
|
261
249
|
caller.idempotencyKey,
|
|
262
250
|
caller.actor,
|
|
263
|
-
{ processStart, processes
|
|
251
|
+
{ processStart, processes },
|
|
264
252
|
);
|
|
265
253
|
},
|
|
266
254
|
}),
|
|
@@ -273,9 +261,11 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
273
261
|
input: empty,
|
|
274
262
|
output: portsReply,
|
|
275
263
|
handler: async (input, context, caller) =>
|
|
276
|
-
caller.computerId === null
|
|
264
|
+
caller.computerId === null
|
|
277
265
|
? refusal.parse({ error: "not_granted" })
|
|
278
|
-
: portsReply.parse(
|
|
266
|
+
: portsReply.parse(
|
|
267
|
+
await computerCall(() => context.env.COMPUTER.ports(input)),
|
|
268
|
+
),
|
|
279
269
|
}),
|
|
280
270
|
listProcesses: defineAppMethod({
|
|
281
271
|
description:
|
|
@@ -285,7 +275,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
285
275
|
input: processPageInputSchema,
|
|
286
276
|
output: processesReply,
|
|
287
277
|
handler: async (input, context, caller) => {
|
|
288
|
-
if (caller.computerId === null
|
|
278
|
+
if (caller.computerId === null)
|
|
289
279
|
return refusal.parse({ error: "not_granted" });
|
|
290
280
|
if (
|
|
291
281
|
input.after !== undefined &&
|
|
@@ -293,7 +283,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
293
283
|
)
|
|
294
284
|
return refusal.parse({ error: "invalid" });
|
|
295
285
|
const inventory = processesReply.parse(
|
|
296
|
-
await context.env.COMPUTER.processes({}),
|
|
286
|
+
await computerCall(() => context.env.COMPUTER.processes({})),
|
|
297
287
|
);
|
|
298
288
|
return "error" in inventory
|
|
299
289
|
? inventory
|
|
@@ -302,17 +292,17 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
302
292
|
}),
|
|
303
293
|
runCommand: defineAppMethod({
|
|
304
294
|
description:
|
|
305
|
-
"
|
|
295
|
+
"Run a shell command on the attached Computer. Use cwd to choose an absolute working directory. The timeout is at most 45 seconds. In a Workflow Git step, use $FUNGI_GIT_REMOTE for the repository. Returns stdout, stderr, exitCode and a truncation flag for each stream, or an error code if the command cannot return output. A nonzero exitCode indicates failure or cancellation. Output may be truncated.",
|
|
306
296
|
effect: "write",
|
|
307
|
-
requires: ["computer.execute"
|
|
297
|
+
requires: ["computer.execute"],
|
|
308
298
|
input: shedRunCommandInputSchema,
|
|
309
299
|
output: commandReply,
|
|
310
300
|
handler: async (input, context, caller) => {
|
|
311
301
|
const computer = context.env.COMPUTER;
|
|
312
|
-
if (caller.computerId === null
|
|
302
|
+
if (caller.computerId === null)
|
|
313
303
|
return refusal.parse({ error: "not_granted" });
|
|
314
|
-
const { exec
|
|
315
|
-
return runShedCommand(input, { exec
|
|
304
|
+
const { exec } = computer;
|
|
305
|
+
return runShedCommand(input, { exec });
|
|
316
306
|
},
|
|
317
307
|
}),
|
|
318
308
|
},
|
package/backend/process-page.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import { processListOutputSchema } from "@fungi.computer/computer
|
|
2
|
+
import { processListOutputSchema } from "@fungi.computer/computer-contract";
|
|
3
3
|
|
|
4
4
|
const process = processListOutputSchema.shape.processes.element;
|
|
5
5
|
const processCursorSchema = z.strictObject({
|