@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 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 and returns its exit status and output (64 KiB per stream). |
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.
@@ -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/operations";
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/operations";
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?: Partial<ShedComputer> &
46
+ COMPUTER: ShedComputer &
46
47
  Readonly<{
47
- ports?(input: Record<string, never>): Promise<WhistleJsonValue>;
48
- preview?(input: { port: number }): Promise<z.core.util.JSONType>;
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?(input: {
53
+ read(input: {
53
54
  path: string;
54
55
  byteOffset: number;
55
56
  byteLength: number;
56
57
  }): Promise<z.core.util.JSONType>;
57
- exec?(input: z.output<typeof command>): Promise<WhistleJsonValue>;
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 || !context.env.COMPUTER?.preview)
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.env.COMPUTER?.read || !context.runs)
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", "computer.writeFile"],
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, workflowGitPass, write } = computer;
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 || !context.env.COMPUTER?.processKill)
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(await context.env.COMPUTER.processKill(input));
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", "computer.writeFile"],
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, workflowGitPass, write } = computer;
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, workflowGitPass, write },
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 || !context.env.COMPUTER?.ports
264
+ caller.computerId === null
277
265
  ? refusal.parse({ error: "not_granted" })
278
- : portsReply.parse(await context.env.COMPUTER.ports(input)),
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 || !context.env.COMPUTER?.processes)
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
- "Execute a shell command on the attached Computer, with an optional absolute cwd and a timeout up to 45 seconds. A Workflow step's admitted gitAccess reaches the command as $FUNGI_GIT_REMOTE with command-local Git credentials. The host mints request identity, owns the per-attachment execution budget and attribution. Each output stream is bounded to the Computer’s 64 KiB byte slice, with up to two extra UTF-8 bytes from a replacement character, and may be truncated.",
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", "computer.writeFile"],
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 || !computer?.exec)
302
+ if (caller.computerId === null)
313
303
  return refusal.parse({ error: "not_granted" });
314
- const { exec, workflowGitPass, write } = computer;
315
- return runShedCommand(input, { exec, workflowGitPass, write });
304
+ const { exec } = computer;
305
+ return runShedCommand(input, { exec });
316
306
  },
317
307
  }),
318
308
  },
@@ -1,5 +1,5 @@
1
1
  import * as z from "zod";
2
- import { processListOutputSchema } from "@fungi.computer/computer/operations";
2
+ import { processListOutputSchema } from "@fungi.computer/computer-contract";
3
3
 
4
4
  const process = processListOutputSchema.shape.processes.element;
5
5
  const processCursorSchema = z.strictObject({