capsule-runtime 0.11.0

The runtime that executes Murmur capsules. Runs packaged AI agents in isolation, enforcing WASI capability grants on a Wasmtime component sandbox.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
package murmur:driver@0.6.0;

/// The contract between the runtime and a process driver: an artifact that knows how to drive
/// one harness CLI. Pure translation; the runtime grants it nothing.
///
/// The runtime keeps one driver instance for a whole run, so `parse` may rely on what `launch`
/// was given.
interface process {
  /// How a turn in progress can be stopped gracefully.
  enum interrupt-method {
    /// Writing `launch-plan.interrupt-stdin` to the harness's stdin interrupts the turn.
    stdin-message,
    /// Sending `SIGINT` to the harness interrupts the turn.
    signal-int,
    /// The harness cannot be interrupted gracefully; the runtime kills it instead.
    unsupported,
  }


  /// What a harness need may do with a host path.
  enum path-access {
    /// Read files and list directories.
    read,
    /// Read, write and create, as the workdir allows, but never execute.
    read-write,
  }

  /// A host directory the harness needs beyond what the capsule's class gives every command.
  record path-need {
    /// `~/<rest>`, under the home of the account `mur` runs as, or an absolute path. A directory.
    path: string,
    access: path-access,
  }

  /// What the harness needs beyond the capsule's own grants. Each need is granted only where the
  /// capsule manifest also allows it; the launch is refused otherwise. `required-env` is the
  /// environment half.
  record harness-needs {
    paths: list<path-need>,
    /// Hosts the harness connects to, each spelled as a `capabilities.network.allow` entry.
    hosts: list<string>,
  }

  /// What the driver drives, read once when the driver is loaded.
  record description {
    /// The name of the harness this driver drives.
    harness: string,
    /// The executable the runtime resolves and runs, by name or path.
    binary: string,
    /// The arguments after `binary` that make the harness print its version.
    version-args: list<string>,
    /// The harness versions this driver release was tested against.
    tested-versions: list<string>,
    /// How a turn in progress is stopped.
    interrupt: interrupt-method,
    /// Environment variable names the harness cannot run without. Each must be declared in
    /// the manifest's `capabilities.env.allow`, or the capsule is refused at load.
    required-env: list<string>,
    /// Whether `parse` emits `text-delta` events.
    streams-text: bool,
    /// Whether `parse` emits `usage` events for the harness it drives.
    ///
    /// A driver whose harness reports no token counts sets this `false`, and the runtime then
    /// records no counts for its turns. A manifest that sets `inference.max_session_tokens`, or
    /// a machine ceiling in effect, against such a driver is refused at load: a ceiling that can
    /// never be reached is not a ceiling.
    reports-usage: bool,
    /// How the runtime asks the harness whether it is signed in for this user, without
    /// spending a turn.
    login-check: login-check,
    /// What the harness needs beyond the capsule's own grants. Each need must also be allowed by
    /// the capsule manifest — paths by `capabilities.harness.paths`, hosts by
    /// `capabilities.network.allow` — or the launch is refused with `E-CAP-021`. A malformed
    /// need is refused when the driver is loaded (`E-RUN-032`). An empty `harness-needs` is valid.
    harness-needs: harness-needs,
  }

  /// How the runtime asks the harness whether it is signed in for this user, without spending
  /// a turn.
  variant login-check {
    /// Run `description.binary` with these arguments, the way the version check runs it, and
    /// hand what it printed to `read-login`. Never empty: a driver naming no arguments is
    /// refused at load, since the bare binary may start a session.
    command(list<string>),
    /// The harness offers no way to ask without a turn; the reason, shown to the person cut to
    /// its first line and 200 characters.
    unavailable(string),
  }

  /// What the login command printed.
  record login-output {
    /// The exit code; `none` when a signal ended the command.
    exit-code: option<s32>,
    /// Everything the command wrote to stdout.
    stdout: string,
    /// Everything the command wrote to stderr.
    stderr: string,
  }

  /// Whether the harness is signed in, as the driver reads `login-output`.
  variant login-state {
    /// Signed in for this user.
    logged-in,
    /// Not signed in for this user; the reason, which names how to sign in and carries no
    /// credential.
    logged-out(string),
    /// The output could not be read; the reason.
    unknown(string),
  }

  /// The endpoint through which the harness calls the capsule's tools.
  record bridge {
    /// The server name the driver registers the bridge under in the harness's configuration.
    server-name: string,
    /// The URL the bridge listens on.
    url: string,
    /// The bearer token every request to `url` must carry.
    bearer-token: string,
    /// The capsule's tool names, bare. The driver names them the way its harness needs, and
    /// strips that naming again in `parse`.
    tool-names: list<string>,
  }

  /// Whether a turn starts a harness session or continues one.
  enum session-mode {
    /// Start a session with the given id.
    new,
    /// Continue the session with the given id.
    resume,
  }

  /// The harness session a turn runs in.
  record session {
    /// The harness session id.
    id: string,
    /// Whether the session is started or continued.
    mode: session-mode,
  }

  /// Everything the driver needs to plan one turn.
  record launch-request {
    /// The manifest's `inference.model`; `none` leaves the choice to the harness.
    model: option<string>,
    /// The runtime's full system prompt for the turn, notices included.
    system-prompt: string,
    /// The manifest's `inference.driver.config` as JSON; `none` when the manifest wrote none.
    config: option<string>,
    /// The tool bridge; `none` when the capsule exposes no tools to the harness.
    bridge: option<bridge>,
    /// The harness session this turn runs in.
    session: session,
    /// The version the harness printed for `description.version-args`; `none` when it could
    /// not be read.
    harness-version: option<string>,
    /// The task text for this turn.
    task: string,
  }

  /// A file the runtime writes before spawning the harness.
  record driver-file {
    /// The file name, relative to the files directory.
    name: string,
    /// The file's contents.
    contents: string,
  }

  /// How to spawn the harness for one turn.
  ///
  /// The runtime writes `files` into a private directory readable only by the capsule's user,
  /// and replaces the token `{files_dir}` in `args` and in `env-set` values with that
  /// directory's path.
  record launch-plan {
    /// The full argument list after the binary.
    args: list<string>,
    /// Variables to set in the harness's environment, after the ones
    /// `capabilities.env.allow` passes through.
    env-set: list<tuple<string, string>>,
    /// Files to write before spawning.
    files: list<driver-file>,
    /// Bytes written to stdin once the harness starts; `none` writes nothing.
    stdin: option<list<u8>>,
    /// Whether stdin stays open after `stdin` is written.
    keep-stdin-open: bool,
    /// Bytes that interrupt the turn when written to stdin, for `interrupt-method.stdin-message`.
    interrupt-stdin: option<list<u8>>,
  }

  /// The harness's report of the session it started.
  record session-info {
    /// The harness session id.
    id: string,
    /// How the harness is billing: `subscription`, `api-key`, or another value the driver
    /// reads from its harness. Anything but `subscription` is warned about and traced.
    auth: string,
    /// The model the harness reports it is running; `none` when it does not say.
    model: option<string>,
  }

  /// What the harness has spent on this run so far.
  ///
  /// Every member is **cumulative for the harness run**, never the increment since the last
  /// report: a harness that sends a running count as it streams and a final total at the end is
  /// reported verbatim, both times. The runtime attributes, to each turn it closes, only the
  /// growth of each member over what it has already attributed, so reporting the same total
  /// twice adds nothing and a member that shrinks contributes zero. A driver that reports
  /// deltas instead would be counted several times over.
  ///
  /// Every member is independently optional. `none` means *this harness does not report this
  /// count* and is never the same fact as `0`, which means the harness reported spending none:
  /// the runtime keeps the two apart all the way into the trace.
  ///
  /// `cache-read` and `cache-creation` sit **beside** `input`, never inside it. A harness that
  /// folds its cache counts into its input count must subtract them before reporting, so that
  /// `input` is always the uncached prompt alone. `thinking` is a **subset** of `output`, not an
  /// addition to it.
  ///
  /// No cost figure and no context-window size is carried. A price a harness computed against
  /// its own table is a different claim from a token count, and the runtime has no equivalent
  /// on its other transport to make it mean the same thing on both.
  record usage {
    /// Prompt tokens, excluding anything `cache-read` or `cache-creation` covers.
    input: option<u64>,
    /// Generated tokens, including the `thinking` subset below.
    output: option<u64>,
    /// Prompt tokens served from the provider's cache.
    cache-read: option<u64>,
    /// Prompt tokens written to the provider's cache.
    cache-creation: option<u64>,
    /// The part of `output` the harness reports as reasoning.
    thinking: option<u64>,
  }

  /// A tool the agent called.
  record tool-call-info {
    /// The call id, matching the `tool-result-info.id` of its result.
    id: string,
    /// The bare artifact name, with any prefix the driver added in `launch` stripped.
    name: string,
    /// The call's input as JSON.
    input: string,
  }

  /// A tool call the model has begun writing, before its input is complete.
  record tool-call-start {
    /// The call id: the `tool-call-info.id` the complete call will carry.
    id: string,
    /// The bare artifact name, with any prefix the driver added in `launch` stripped: the
    /// `tool-call-info.name` the complete call will carry.
    name: string,
  }

  /// How much of a started call's input the model has written so far.
  record tool-call-progress {
    /// The id of the `tool-call-start` this reports on.
    id: string,
    /// Bytes of the call's input JSON written so far, **cumulative for the call**, never the
    /// increment since the last report. A report smaller than an earlier one is ignored.
    input-bytes: u64,
  }

  /// The result of a tool call.
  record tool-result-info {
    /// The id of the `tool-call-info` this answers.
    id: string,
    /// The tool's output.
    output: string,
    /// Whether the tool reported failure.
    is-error: bool,
  }

  /// The harness is retrying a request to its provider.
  record retry-info {
    /// Which attempt this is.
    attempt: u32,
    /// Why the previous attempt failed.
    reason: string,
  }

  /// Why a turn failed.
  enum failure-kind {
    /// The harness could not authenticate.
    auth,
    /// The harness ran out of quota or was rate limited.
    quota,
    /// The harness stopped at its turn limit.
    max-turns,
    /// The turn was interrupted.
    canceled,
    /// The harness reported an error of its own.
    harness-error,
    /// Anything else.
    other,
  }

  /// A failed turn.
  record turn-failure {
    /// Why it failed.
    kind: failure-kind,
    /// The failure as the harness described it.
    message: string,
  }

  /// Whether a subscription window still has room, as the harness reports it.
  enum quota-state {
    /// The window has room.
    ok,
    /// The harness warns that the window is close to its limit.
    warning,
    /// The window is used up until it resets.
    exhausted,
  }

  /// One subscription window a harness reports: a span of time over which the person's
  /// subscription allows a certain amount of use.
  ///
  /// The driver translates its harness's spelling into these names and units: a percentage
  /// becomes a fraction, seconds become milliseconds, and a harness that numbers its windows
  /// (`primary`, `secondary`) is given names. Every member but `name` is independently optional,
  /// and `none` means the harness did not report that member, never zero.
  record quota-window {
    /// A stable lowercase label for the window, which the driver documents, such as `five-hour`
    /// or `seven-day`: 1 to 64 characters of `a`-`z`, `0`-`9` and `-`, starting with a letter or
    /// digit. A window with any other name is refused and noted in the trace.
    name: string,
    /// How much of the window is used, as a fraction from 0 to 1. A value outside that range,
    /// or not finite, refuses the window.
    used: option<f64>,
    /// When the window resets, in milliseconds since the Unix epoch.
    resets-at: option<u64>,
    /// How long the window is, in minutes.
    window-minutes: option<u64>,
    /// Whether the window still has room, as the harness reports it.
    state: option<quota-state>,
  }

  /// One thing the harness did, read out of its output.
  variant event {
    /// The harness's session id, and how it is billing.
    session-started(session-info),
    /// Streamed text, appended to the message in progress.
    text-delta(string),
    /// The complete text of one message. It replaces any `text-delta` already streamed for
    /// that message; it is never appended to them.
    text(string),
    /// Streamed thinking, appended to the thinking in progress.
    thinking-delta(string),
    /// The complete thinking of one message, replacing any `thinking-delta` streamed for it.
    thinking(string),
    /// A tool the agent called.
    tool-call(tool-call-info),
    /// The result of a tool call.
    tool-result(tool-result-info),
    /// The harness is retrying a request to its provider.
    retry(retry-info),
    /// What the harness has spent on this run so far, cumulative. May arrive any number of
    /// times; the runtime records each member's growth against the turn open when it arrives.
    usage(usage),
    /// The turn finished; the payload is the final answer.
    turn-end(string),
    /// The turn failed.
    turn-failed(turn-failure),
    /// A diagnostic for the trace only, such as a line the driver could not read.
    note(string),
    /// A tool call the model has begun writing. How much of its input is written follows as
    /// `tool-call-progress`, and the complete call as `tool-call` with the same id and name.
    /// Optional: a driver whose harness does not report a call until it is complete never sends it.
    tool-call-started(tool-call-start),
    /// How much of a started call's input has been written so far.
    tool-call-progress(tool-call-progress),
    /// The subscription windows the harness reports as used, as a snapshot of the windows it
    /// names. A later report replaces a window of the same name whole and leaves the others
    /// standing, so a driver whose harness sends sparse updates merges them into its own last
    /// report before sending one. The runtime records and forwards it and never acts on it.
    /// Optional: a driver whose harness reports no windows never sends it. May arrive after the
    /// terminal event.
    quota(list<quota-window>),
    /// The harness's prediction of what the person will ask next. Optional: a driver whose
    /// harness makes no such prediction never sends it. May arrive after the terminal event.
    suggested-prompt(string),
  }

  /// How the harness exited, for a run whose output ended without a terminal event.
  record exit-status {
    /// The exit code; `none` when the harness was ended by a signal.
    code: option<s32>,
    /// The signal that ended the harness; `none` when it exited.
    signal: option<s32>,
    /// The last part of the harness's stderr.
    stderr-tail: string,
    /// Whether the runtime interrupted the turn.
    interrupted: bool,
    /// Whether `parse` returned a `turn-end` or `turn-failed` for this run.
    saw-terminal: bool,
  }

  /// What the driver drives. Called once, when the driver is loaded.
  describe: func() -> description;

  /// Plans one turn; `err` refuses it with a message the runtime reports.
  launch: func(request: launch-request) -> result<launch-plan, string>;

  /// Reads a batch of complete stdout lines into events. May return none.
  ///
  /// After the batch holding the terminal event (`turn-end` or `turn-failed`), the runtime keeps
  /// reading the harness's output until the harness exits or a short grace runs out. From those
  /// lines, and from the rest of the terminal event's batch, only `quota`, `suggested-prompt` and
  /// `note` are taken; every other event is noted in the trace and ignored, and never changes the
  /// turn's outcome, its result or its spend.
  parse: func(lines: list<string>) -> list<event>;

  /// Classifies a run whose output ended without `turn-end` or `turn-failed`. Returns only
  /// `turn-end` or `turn-failed`; when `exit.interrupted` is true and no terminal event was
  /// seen it must return `turn-failed` with kind `canceled`, whatever the exit code says.
  classify-exit: func(exit: exit-status) -> event;

  /// Reads what the `login-check.command` printed into whether the harness is signed in.
  /// Called only for a driver whose `describe` names a `command`, and only when the command
  /// finished within the bound. The reasons it returns are shown to the person, cut to their
  /// first line and 200 characters, so they must carry no credential.
  read-login: func(output: login-output) -> login-state;
}

/// A process driver exports the `process` interface and imports nothing of Murmur's: it is
/// granted nothing.
world process-driver {
  export process;
}