@fungi.computer/shed 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 +17 -5
- package/backend/methods.ts +75 -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/present.test.ts +40 -0
- 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 +37 -20
- package/dist/assets/ghostty-vt-DOMeXDrv.wasm +0 -0
- package/dist/assets/index-C5jExGkL.js +0 -1764
- package/dist/assets/index-Dm2F0ptq.css +0 -1
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resultRows } from "@fungi.computer/agar/method-presentation";
|
|
1
2
|
import { processesGill } from "./gills.js";
|
|
2
3
|
import {
|
|
3
4
|
processPageInputSchema,
|
|
@@ -6,9 +7,10 @@ import {
|
|
|
6
7
|
} from "./process-page.js";
|
|
7
8
|
import * as z from "zod";
|
|
8
9
|
import { Base64 } from "effect/encoding";
|
|
9
|
-
import { computerPortInputSchema } from "@fungi.computer/computer
|
|
10
|
+
import { computerPortInputSchema } from "@fungi.computer/computer-contract";
|
|
10
11
|
import {
|
|
11
12
|
runShedCommand,
|
|
13
|
+
computerCall,
|
|
12
14
|
shedRunCommandInputSchema,
|
|
13
15
|
shedRunInputSchema,
|
|
14
16
|
shedStartInputSchema,
|
|
@@ -30,7 +32,7 @@ import {
|
|
|
30
32
|
computerProcessSchema,
|
|
31
33
|
processIdInputSchema,
|
|
32
34
|
processKillOutputSchema,
|
|
33
|
-
} from "@fungi.computer/computer
|
|
35
|
+
} from "@fungi.computer/computer-contract";
|
|
34
36
|
const empty = z.strictObject({});
|
|
35
37
|
const command = runCommandInputSchema;
|
|
36
38
|
const refusal = shedComputerRefusalSchema;
|
|
@@ -41,19 +43,19 @@ const commandReply = runCommandResultSchema;
|
|
|
41
43
|
export type ShedMethodContext = Readonly<{
|
|
42
44
|
runs?: ShedRuns;
|
|
43
45
|
env: Readonly<{
|
|
44
|
-
COMPUTER
|
|
46
|
+
COMPUTER: ShedComputer &
|
|
45
47
|
Readonly<{
|
|
46
|
-
ports
|
|
47
|
-
preview
|
|
48
|
-
processKill
|
|
48
|
+
ports(input: Record<string, never>): Promise<WhistleJsonValue>;
|
|
49
|
+
preview(input: { port: number }): Promise<z.core.util.JSONType>;
|
|
50
|
+
processKill(
|
|
49
51
|
input: z.output<typeof processIdInputSchema>,
|
|
50
52
|
): Promise<z.core.util.JSONType>;
|
|
51
|
-
read
|
|
53
|
+
read(input: {
|
|
52
54
|
path: string;
|
|
53
55
|
byteOffset: number;
|
|
54
56
|
byteLength: number;
|
|
55
57
|
}): Promise<z.core.util.JSONType>;
|
|
56
|
-
exec
|
|
58
|
+
exec(input: z.output<typeof command>): Promise<WhistleJsonValue>;
|
|
57
59
|
}>;
|
|
58
60
|
}>;
|
|
59
61
|
}>;
|
|
@@ -68,6 +70,23 @@ const bytes = z.strictObject({
|
|
|
68
70
|
encoding: z.literal("base64"),
|
|
69
71
|
truncated: z.boolean(),
|
|
70
72
|
});
|
|
73
|
+
/** The script's command lines, without blanks and comments. */
|
|
74
|
+
const commandLines = (command: string) =>
|
|
75
|
+
command
|
|
76
|
+
.split("\n")
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
79
|
+
/**
|
|
80
|
+
* A script reads by its last command: a script usually opens with setup
|
|
81
|
+
* (`set -eu`, `cd`) and ends with what it is for, such as a push.
|
|
82
|
+
*/
|
|
83
|
+
const lastCommand = (command: string) =>
|
|
84
|
+
commandLines(command).at(-1)?.slice(0, 80) ?? "a command";
|
|
85
|
+
const scriptLength = (command: string) => {
|
|
86
|
+
const lines = commandLines(command).length;
|
|
87
|
+
return lines > 1 ? [`${String(lines)} commands`] : [];
|
|
88
|
+
};
|
|
89
|
+
|
|
71
90
|
export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
72
91
|
gills: [processesGill],
|
|
73
92
|
methods: {
|
|
@@ -79,7 +98,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
79
98
|
input: computerPortInputSchema,
|
|
80
99
|
output: preview,
|
|
81
100
|
handler: async (input, context: ShedMethodContext, caller) => {
|
|
82
|
-
if (!caller.computerId
|
|
101
|
+
if (!caller.computerId)
|
|
83
102
|
throw new Error("Computer preview access is not granted");
|
|
84
103
|
// Project the issued URL; preview-row presentation policy stays with the host.
|
|
85
104
|
return z
|
|
@@ -102,7 +121,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
102
121
|
z.strictObject({ error: z.enum(["not_found", "ambiguous"]) }),
|
|
103
122
|
]),
|
|
104
123
|
handler: async (input, context: ShedMethodContext, caller) => {
|
|
105
|
-
if (!caller.computerId || !context.
|
|
124
|
+
if (!caller.computerId || !context.runs)
|
|
106
125
|
throw new Error("Computer file access is not granted");
|
|
107
126
|
const log = context.runs.logPath(
|
|
108
127
|
input.run,
|
|
@@ -133,7 +152,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
133
152
|
description:
|
|
134
153
|
"Start one process on the pinned Computer and return its process and log path without waiting for exit.",
|
|
135
154
|
effect: "write",
|
|
136
|
-
requires: ["computer.execute"
|
|
155
|
+
requires: ["computer.execute"],
|
|
137
156
|
pending: true,
|
|
138
157
|
input: shedStartInputSchema,
|
|
139
158
|
output: z.union([
|
|
@@ -146,29 +165,19 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
146
165
|
]),
|
|
147
166
|
handler: (input, context: ShedMethodContext, caller) => {
|
|
148
167
|
const computer = context.env.COMPUTER;
|
|
149
|
-
if (
|
|
150
|
-
!caller.computerId ||
|
|
151
|
-
!computer?.processStart ||
|
|
152
|
-
!computer.processes ||
|
|
153
|
-
!context.runs
|
|
154
|
-
)
|
|
168
|
+
if (!caller.computerId || !context.runs)
|
|
155
169
|
throw new Error("Computer access is not granted");
|
|
156
170
|
if (!caller.idempotencyKey)
|
|
157
171
|
throw new Error(
|
|
158
172
|
"A host idempotency key is required to start a process",
|
|
159
173
|
);
|
|
160
|
-
const { processStart, processes
|
|
174
|
+
const { processStart, processes } = computer;
|
|
161
175
|
return context.runs.start(
|
|
162
176
|
input,
|
|
163
177
|
caller.computerId,
|
|
164
178
|
caller.idempotencyKey,
|
|
165
179
|
caller.actor,
|
|
166
|
-
{
|
|
167
|
-
processStart,
|
|
168
|
-
processes,
|
|
169
|
-
workflowGitPass,
|
|
170
|
-
write,
|
|
171
|
-
},
|
|
180
|
+
{ processStart, processes },
|
|
172
181
|
);
|
|
173
182
|
},
|
|
174
183
|
}),
|
|
@@ -179,20 +188,22 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
179
188
|
input: processIdInputSchema,
|
|
180
189
|
output: z.union([processKillOutputSchema, refusal]),
|
|
181
190
|
handler: async (input, context: ShedMethodContext, caller) => {
|
|
182
|
-
if (!caller.computerId
|
|
191
|
+
if (!caller.computerId)
|
|
183
192
|
throw new Error("Computer access is not granted");
|
|
184
193
|
if (!context.runs?.owns(input.id, caller.computerId, caller.actor))
|
|
185
194
|
return refusal.parse({ error: "not_found" });
|
|
186
195
|
return z
|
|
187
196
|
.union([processKillOutputSchema, refusal])
|
|
188
|
-
.parse(
|
|
197
|
+
.parse(
|
|
198
|
+
await computerCall(() => context.env.COMPUTER.processKill(input)),
|
|
199
|
+
);
|
|
189
200
|
},
|
|
190
201
|
}),
|
|
191
202
|
run: defineAppMethod({
|
|
192
203
|
description:
|
|
193
204
|
"Run one command on the pinned Computer, retaining a log and polling until it exits.",
|
|
194
205
|
effect: "write",
|
|
195
|
-
requires: ["computer.execute"
|
|
206
|
+
requires: ["computer.execute"],
|
|
196
207
|
pending: true,
|
|
197
208
|
input: shedRunInputSchema,
|
|
198
209
|
output: z.union([
|
|
@@ -202,16 +213,34 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
202
213
|
}),
|
|
203
214
|
refusal,
|
|
204
215
|
]),
|
|
216
|
+
present: {
|
|
217
|
+
rows: resultRows,
|
|
218
|
+
verb: "ran",
|
|
219
|
+
row: ({ input, output }) =>
|
|
220
|
+
"error" in output
|
|
221
|
+
? {
|
|
222
|
+
id: "refused",
|
|
223
|
+
title: "Shed could not run the command",
|
|
224
|
+
detail: output.error,
|
|
225
|
+
tone: "error",
|
|
226
|
+
}
|
|
227
|
+
: {
|
|
228
|
+
id: output.logPath,
|
|
229
|
+
title: `Shed ran ${lastCommand(input.command)}`,
|
|
230
|
+
detail: [
|
|
231
|
+
output.exitCode === 0
|
|
232
|
+
? "Finished"
|
|
233
|
+
: `Exited with code ${String(output.exitCode)}`,
|
|
234
|
+
...scriptLength(input.command),
|
|
235
|
+
].join(" · "),
|
|
236
|
+
tone: output.exitCode === 0 ? "settled" : "error",
|
|
237
|
+
},
|
|
238
|
+
},
|
|
205
239
|
handler: (input, context: ShedMethodContext, caller) => {
|
|
206
240
|
const computer = context.env.COMPUTER;
|
|
207
|
-
if (
|
|
208
|
-
!caller.computerId ||
|
|
209
|
-
!computer?.processStart ||
|
|
210
|
-
!computer.processes ||
|
|
211
|
-
!context.runs
|
|
212
|
-
)
|
|
241
|
+
if (!caller.computerId || !context.runs)
|
|
213
242
|
throw new Error("Computer access is not granted");
|
|
214
|
-
const { processStart, processes
|
|
243
|
+
const { processStart, processes } = computer;
|
|
215
244
|
if (!caller.idempotencyKey)
|
|
216
245
|
throw new Error("A host idempotency key is required to start a run");
|
|
217
246
|
return context.runs.run(
|
|
@@ -219,7 +248,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
219
248
|
caller.computerId,
|
|
220
249
|
caller.idempotencyKey,
|
|
221
250
|
caller.actor,
|
|
222
|
-
{ processStart, processes
|
|
251
|
+
{ processStart, processes },
|
|
223
252
|
);
|
|
224
253
|
},
|
|
225
254
|
}),
|
|
@@ -232,9 +261,11 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
232
261
|
input: empty,
|
|
233
262
|
output: portsReply,
|
|
234
263
|
handler: async (input, context, caller) =>
|
|
235
|
-
caller.computerId === null
|
|
264
|
+
caller.computerId === null
|
|
236
265
|
? refusal.parse({ error: "not_granted" })
|
|
237
|
-
: portsReply.parse(
|
|
266
|
+
: portsReply.parse(
|
|
267
|
+
await computerCall(() => context.env.COMPUTER.ports(input)),
|
|
268
|
+
),
|
|
238
269
|
}),
|
|
239
270
|
listProcesses: defineAppMethod({
|
|
240
271
|
description:
|
|
@@ -244,7 +275,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
244
275
|
input: processPageInputSchema,
|
|
245
276
|
output: processesReply,
|
|
246
277
|
handler: async (input, context, caller) => {
|
|
247
|
-
if (caller.computerId === null
|
|
278
|
+
if (caller.computerId === null)
|
|
248
279
|
return refusal.parse({ error: "not_granted" });
|
|
249
280
|
if (
|
|
250
281
|
input.after !== undefined &&
|
|
@@ -252,7 +283,7 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
252
283
|
)
|
|
253
284
|
return refusal.parse({ error: "invalid" });
|
|
254
285
|
const inventory = processesReply.parse(
|
|
255
|
-
await context.env.COMPUTER.processes({}),
|
|
286
|
+
await computerCall(() => context.env.COMPUTER.processes({})),
|
|
256
287
|
);
|
|
257
288
|
return "error" in inventory
|
|
258
289
|
? inventory
|
|
@@ -261,17 +292,17 @@ export const appBackend = defineAppBackend<ShedMethodContext>({
|
|
|
261
292
|
}),
|
|
262
293
|
runCommand: defineAppMethod({
|
|
263
294
|
description:
|
|
264
|
-
"
|
|
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.",
|
|
265
296
|
effect: "write",
|
|
266
|
-
requires: ["computer.execute"
|
|
297
|
+
requires: ["computer.execute"],
|
|
267
298
|
input: shedRunCommandInputSchema,
|
|
268
299
|
output: commandReply,
|
|
269
300
|
handler: async (input, context, caller) => {
|
|
270
301
|
const computer = context.env.COMPUTER;
|
|
271
|
-
if (caller.computerId === null
|
|
302
|
+
if (caller.computerId === null)
|
|
272
303
|
return refusal.parse({ error: "not_granted" });
|
|
273
|
-
const { exec
|
|
274
|
-
return runShedCommand(input, { exec
|
|
304
|
+
const { exec } = computer;
|
|
305
|
+
return runShedCommand(input, { exec });
|
|
275
306
|
},
|
|
276
307
|
}),
|
|
277
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({
|