typeclaw 0.30.0 → 0.31.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.
@@ -36,14 +36,35 @@ export function buildSandboxedCommand(command: string, policy: SandboxPolicy = {
36
36
 
37
37
  function buildArgv(command: string, policy: SandboxPolicy): string[] {
38
38
  const bwrap = policy.bwrapPath ?? 'bwrap'
39
- const argv: string[] = [bwrap, '--unshare-all']
40
-
41
- if (policy.network === 'inherit') {
42
- // --unshare-all already unshared the net namespace; --share-net rejoins
43
- // the outer container's network. Other namespaces (user/pid/mount/ipc/
44
- // uts/cgroup) stay unshared. Default ('none' / undefined) leaves the net
45
- // namespace isolated prompt-injected bash cannot exfiltrate over the
46
- // network without the consumer explicitly opting in.
39
+ const procStrategy = policy.proc ?? 'tmpfs'
40
+ const realProc = procStrategy === 'real-proc'
41
+
42
+ // 'real-proc' splits PID-namespace ownership from bwrap. `unshare --pid
43
+ // --fork --mount --mount-proc` (util-linux, baseline) creates the new PID +
44
+ // mount namespaces as REAL root and mounts a fresh procfs scoped to that PID
45
+ // namespace — which OrbStack permits only with CAP_SYS_ADMIN and NOT from
46
+ // bwrap's user namespace (bwrap's --proc is blocked there). bwrap then runs
47
+ // INSIDE that namespace and must NOT re-unshare pid (it would create a second
48
+ // PID ns with no matching procfs and reintroduce the ENOTDIR crash), so we
49
+ // unshare each namespace EXCEPT pid explicitly instead of --unshare-all. The
50
+ // freshly mounted /proc contains only the sandbox subtree, so --ro-bind /proc
51
+ // (below) binds that scoped procfs, never the agent runtime's /proc/N/environ.
52
+ const argv: string[] = realProc
53
+ ? ['unshare', '--pid', '--fork', '--mount', '--mount-proc', '--', bwrap]
54
+ : [bwrap, '--unshare-all']
55
+ if (realProc) {
56
+ argv.push('--unshare-user', '--unshare-ipc', '--unshare-uts', '--unshare-cgroup')
57
+ }
58
+
59
+ if (policy.network !== 'inherit') {
60
+ // Default ('none' / undefined) isolates the net namespace — prompt-injected
61
+ // bash cannot exfiltrate over the network unless the consumer opts in.
62
+ // --unshare-all already covers this in the non-real-proc path; under
63
+ // real-proc the explicit unshares above omit net, so add it here.
64
+ if (realProc) argv.push('--unshare-net')
65
+ } else if (!realProc) {
66
+ // --unshare-all unshared the net namespace; --share-net rejoins the outer
67
+ // container's network. Under real-proc we simply never add --unshare-net.
47
68
  argv.push('--share-net')
48
69
  }
49
70
 
@@ -97,7 +118,15 @@ function buildArgv(command: string, policy: SandboxPolicy): string[] {
97
118
  '/lib64',
98
119
  )
99
120
 
100
- if ((policy.proc ?? 'tmpfs') === 'tmpfs') {
121
+ if (realProc) {
122
+ // The outer `unshare --mount-proc` already mounted a fresh procfs scoped to
123
+ // the new PID namespace. --ro-bind /proc /proc binds THAT procfs (not the
124
+ // outer container's), so the child gets real /proc/self/{fd,maps} and the
125
+ // agent runtime's pids — and their /proc/N/environ secrets — are simply
126
+ // absent from this namespace. No /proc/self/exe symlink is needed: a real
127
+ // /proc/self/exe already resolves correctly.
128
+ argv.push('--ro-bind', '/proc', '/proc')
129
+ } else if (procStrategy === 'tmpfs') {
101
130
  // --tmpfs /proc, never --proc /proc (OrbStack's kernel blocks
102
131
  // mount("proc",...) from user namespaces) and never --dev-bind /proc /proc
103
132
  // (leaks the outer container's /proc/N/environ — including
@@ -111,6 +140,9 @@ function buildArgv(command: string, policy: SandboxPolicy): string[] {
111
140
  // /proc/self/exe. --symlink (not --ro-bind /proc/self/exe): /proc/self at
112
141
  // setup time is bwrap's pid, so a bind would capture bwrap's own binary.
113
142
  // Must come AFTER --tmpfs /proc (last-op-wins) or the tmpfs erases it.
143
+ // This restores only the runner's SELF-location; a spawned child still
144
+ // reads /proc/self/fd + /proc/self/maps, which the empty tmpfs lacks, so
145
+ // external-package execution requires the 'real-proc' strategy above.
114
146
  if (policy.procSelfExe !== undefined) {
115
147
  argv.push('--ro-bind', policy.procSelfExe, policy.procSelfExe)
116
148
  argv.push('--symlink', policy.procSelfExe, '/proc/self/exe')
@@ -6,7 +6,15 @@ export type SandboxMount =
6
6
 
7
7
  export type SandboxNetwork = 'none' | 'inherit'
8
8
 
9
- export type SandboxProcStrategy = 'tmpfs' | 'none'
9
+ // 'tmpfs' (default): empty /proc + a single /proc/self/exe symlink. Works on
10
+ // every host but gives no /proc/self/{fd,maps}, so a JS package runner's CHILD
11
+ // (the spawned bin) crashes with ENOTDIR reading /proc/self/fd. 'none': no
12
+ // /proc at all. 'real-proc': mount a fresh procfs scoped to a NEW pid namespace
13
+ // so the child gets a real /proc/self/{fd,maps} WITHOUT seeing the agent
14
+ // runtime's pids (no /proc/<agent>/environ leak). 'real-proc' requires the
15
+ // outer container to hold CAP_SYS_ADMIN (mount(2) of proc); start.ts only grants
16
+ // it when the operator opts in via typeclaw.json#sandbox.realProc.
17
+ export type SandboxProcStrategy = 'tmpfs' | 'none' | 'real-proc'
10
18
 
11
19
  export type SandboxEnvPolicy = {
12
20
  set?: Record<string, string>
@@ -0,0 +1,327 @@
1
+ ---
2
+ name: typeclaw-markdown-pdf
3
+ description: "Turn any Markdown into a polished, professional PDF and (optionally) attach it to a channel. Load this whenever you need to deliver a document as a PDF rather than raw markdown — reports, summaries, briefs, meeting notes, docs, anything a human would want to download, print, or forward. Triggers: 'make a PDF', 'export to PDF', 'markdown to PDF', 'PDF report', 'attach the report', 'send me a PDF', 'as a PDF', 'turn this into a document', a researcher/subagent result you want to ship as a file, 'PDF로', 'PDF로 만들어', 'PDF로 변환', 'PDF 첨부'. Also load before saying you cannot produce PDFs — you can: this skill installs a tiny Typst toolchain into workspace/ on first use, then renders. Covers the one-time setup, the styled wrapper, the render command, and how to attach the PDF to Slack/Discord/Telegram/KakaoTalk. For operating on EXISTING PDFs (merge, split, extract text, fill forms), this is not the skill — use pypdf/qpdf instead."
4
+ ---
5
+
6
+ # typeclaw-markdown-pdf
7
+
8
+ You can produce professional PDFs from Markdown. This skill installs a small,
9
+ self-contained [Typst](https://typst.app) toolchain into your `workspace/` the
10
+ **first time** you need a PDF, then reuses it. No Pandoc, no LaTeX, no headless
11
+ browser — just an npm-installed Typst compiler plus the
12
+ [`cmarker`](https://typst.app/universe/package/cmarker/) package that reads your
13
+ Markdown.
14
+
15
+ The flow is: **(1)** run the one-time setup (`bun add` the Typst compiler +
16
+ vendor `cmarker` into `workspace/.tools/`), **(2)** write a styled `.typ` wrapper
17
+ that reads your Markdown, **(3)** run the render script. If a channel asked for
18
+ the PDF, attach the result with `channel_send`.
19
+
20
+ You do **not** need to learn Typst markup. `cmarker` renders your CommonMark
21
+ (headings, lists, tables, code, blockquotes, footnotes, links, images). The
22
+ wrapper only sets _styling_ — fonts, margins, headings, page numbers — so the
23
+ output looks deliberate, not like a default-template export.
24
+
25
+ ## When to use this
26
+
27
+ - A research report, brief, or summary the user wants as a downloadable file.
28
+ - A subagent (e.g. the `researcher`) handed you a `research-<slug>.md` to ship as a PDF.
29
+ - Any channel message asking for "a PDF" / "the report attached" / "PDF로 보내줘".
30
+
31
+ When plain markdown in chat is fine, **don't** make a PDF. This is for when a
32
+ _file_ is the deliverable.
33
+
34
+ ## Step 0 — one-time setup (install the toolchain)
35
+
36
+ Run this `bash` block once per container life. It is **idempotent** — if the
37
+ tools are already present it does nothing and exits fast. It `bun add`s the
38
+ version-pinned Typst compiler (npm pulls only this platform's prebuilt binary —
39
+ Linux x64/arm64, glibc or musl) and vendors the SHA256-verified `cmarker` package
40
+ into `workspace/.tools/` so `@preview/cmarker` resolves offline.
41
+
42
+ ```sh
43
+ set -eu
44
+ cd workspace
45
+ mkdir -p .tools
46
+ cd .tools
47
+
48
+ # Pinned to the exact versions validated for this skill. COMPILER_VERSION is the
49
+ # npm package version of the Typst compiler; it embeds Typst 0.14.2. Bumping
50
+ # either is a deliberate edit — keep the embedded-Typst note below in sync.
51
+ COMPILER_VERSION="0.7.0" # @myriaddreamin/typst-ts-node-compiler (embeds Typst 0.14.2)
52
+ CMARKER_VERSION="0.1.8"
53
+ PKGDIR="typst-packages/preview/cmarker/$CMARKER_VERSION"
54
+
55
+ if [ -f "node_modules/@myriaddreamin/typst-ts-node-compiler/package.json" ] && [ -f "$PKGDIR/lib.typ" ]; then
56
+ echo "markdown-pdf toolchain already installed"
57
+ else
58
+ # The Typst compiler, version-pinned. `bun add` resolves the right prebuilt
59
+ # NAPI binary for this platform via optionalDependencies — no Rust toolchain,
60
+ # no manual download. The exact pin keeps the toolchain reproducible: a future
61
+ # npm release can't silently change the embedded Typst version or the API that
62
+ # Step 3 depends on.
63
+ [ -f package.json ] || echo '{"name":"typeclaw-markdown-pdf-tools","private":true}' > package.json
64
+ bun add "@myriaddreamin/typst-ts-node-compiler@$COMPILER_VERSION"
65
+
66
+ # cmarker (Markdown -> Typst), vendored so compilation needs no network.
67
+ mkdir -p "$PKGDIR"
68
+ curl -fsSL -o cmarker.tar.gz \
69
+ "https://packages.typst.org/preview/cmarker-$CMARKER_VERSION.tar.gz"
70
+ echo "157cc40db2716f12c7eabb95df1f60714a4d95ebfb1c6087cf4aec224e49392a cmarker.tar.gz" | sha256sum -c -
71
+ tar -xzf cmarker.tar.gz -C "$PKGDIR"
72
+ rm cmarker.tar.gz
73
+ echo "markdown-pdf toolchain installed"
74
+ fi
75
+ ```
76
+
77
+ Notes:
78
+
79
+ - It writes only under `workspace/`, the directory your `bash`/`write` tools can
80
+ write to. `workspace/.tools/` is gitignored scratch — it does not get committed.
81
+ - It needs network the first time (to `bun add` the compiler + fetch the package).
82
+ After that the tools persist for the life of the container.
83
+ - **Everything is version-pinned and reproducible.** The validated toolchain is
84
+ `@myriaddreamin/typst-ts-node-compiler@0.7.0` (which embeds Typst **0.14.2**) and
85
+ `cmarker@0.1.8` (SHA256-verified). The `bun add` uses the exact `@0.7.0` pin, so
86
+ a future npm release can't change the embedded Typst version or the API Step 3
87
+ uses. To upgrade, bump both `COMPILER_VERSION` and the embedded-Typst note
88
+ together after re-validating.
89
+
90
+ ## Step 1 — have the markdown ready
91
+
92
+ Use an existing markdown file (yours or a subagent's), or `write` your content to
93
+ `workspace/<slug>.md`. Standard CommonMark plus tables and footnotes all work.
94
+
95
+ ## Step 2 — write the styled wrapper
96
+
97
+ `write` this to `workspace/<slug>.typ`, changing only the `read("...")` filename
98
+ to match your markdown. The defaults are a clean, professional house style; adjust
99
+ fonts/margins only if the user asks.
100
+
101
+ ```typst
102
+ #set document(title: "Report")
103
+ #set page(
104
+ paper: "a4",
105
+ margin: (x: 2.5cm, y: 2.75cm),
106
+ numbering: "1",
107
+ footer: context align(center, text(size: 9pt, fill: luma(120))[
108
+ #counter(page).display("1 / 1", both: true)
109
+ ]),
110
+ )
111
+ #set text(font: ("Libertinus Serif", "New Computer Modern"), size: 11pt, lang: "en")
112
+ #set par(justify: true, leading: 0.68em, spacing: 1.1em)
113
+
114
+ #show heading: set text(weight: "semibold")
115
+ #show heading.where(level: 1): it => block(width: 100%, above: 1.4em, below: 0.9em)[
116
+ #text(size: 1.5em, it.body)
117
+ #v(-0.4em)
118
+ #line(length: 100%, stroke: 0.5pt + luma(200))
119
+ ]
120
+ #show link: it => text(fill: rgb("#1a56db"), underline(it))
121
+ #show quote.where(block: true): it => block(
122
+ inset: (left: 1em), stroke: (left: 2pt + luma(200)),
123
+ text(style: "italic", fill: luma(80), it.body),
124
+ )
125
+ #show raw.where(block: true): it => block(
126
+ fill: luma(245), inset: 8pt, radius: 4pt, width: 100%, text(size: 9pt, it),
127
+ )
128
+ #show table: set table(stroke: 0.5pt + luma(200))
129
+
130
+ #import "@preview/cmarker:0.1.8"
131
+ #cmarker.render(read("report.md"), h1-level: 1, blockquote: quote.with(block: true))
132
+ ```
133
+
134
+ Notes:
135
+
136
+ - `read("report.md")` is **relative to the workspace** (the compiler's `workspace`
137
+ is set to `workspace/` — see Step 3). Keep the `.typ` and `.md` in `workspace/`.
138
+ - Fonts `Libertinus Serif` / `New Computer Modern` are bundled with Typst (no font
139
+ install). For Korean/CJK body text, add `"Noto Serif CJK KR"` to the `font:` list
140
+ and pass that font dir to `fontPaths` in Step 3 (the container's `cjkFonts` toggle
141
+ installs `fonts-noto-cjk` under `/usr/share/fonts`).
142
+
143
+ ## Step 3 — render
144
+
145
+ `write` this tiny renderer to `workspace/.tools/render.ts`, then run it. It loads
146
+ the npm-installed compiler, points the package cache at the vendored `cmarker`, and
147
+ writes the PDF. Pass the wrapper and output paths as arguments.
148
+
149
+ ```ts
150
+ // workspace/.tools/render.ts
151
+ import { NodeCompiler } from '@myriaddreamin/typst-ts-node-compiler'
152
+ import { writeFileSync } from 'node:fs'
153
+
154
+ const [, , mainFile, outFile] = process.argv
155
+ if (!mainFile || !outFile) throw new Error('usage: render.ts <main.typ> <out.pdf>')
156
+
157
+ const compiler = NodeCompiler.create({
158
+ workspace: '.', // run from workspace/, so read("report.md") resolves
159
+ // Add CJK / extra font dirs here if needed:
160
+ // fontArgs: [{ fontPaths: ["/usr/share/fonts"] }],
161
+ })
162
+ const pdf = compiler.pdf({ mainFilePath: mainFile })
163
+ writeFileSync(outFile, Buffer.from(pdf))
164
+ console.log(`wrote ${outFile} (${pdf.length} bytes)`)
165
+ ```
166
+
167
+ Run it from `workspace/`, with the package cache pointed at the vendored packages:
168
+
169
+ ```sh
170
+ cd workspace
171
+ TYPST_PACKAGE_CACHE_PATH="$PWD/.tools/typst-packages" \
172
+ TYPST_PACKAGE_PATH="$PWD/.tools/typst-packages" \
173
+ bun .tools/render.ts report.typ report.pdf
174
+ ```
175
+
176
+ Verify: the command prints `wrote report.pdf (...)` and `workspace/report.pdf`
177
+ exists. On a compile error the compiler throws with the offending Typst line —
178
+ usually raw HTML or a markdown extension `cmarker` doesn't support; simplify that
179
+ part and re-run.
180
+
181
+ ## Rich elements (optional)
182
+
183
+ When plain markdown isn't enough — you want a cover banner, callout boxes,
184
+ multi-column sections, captioned figures — you don't switch to HTML (Typst
185
+ doesn't render HTML). Instead, drop **raw Typst** into the markdown via
186
+ `<!--raw-typst ... -->` comments. `cmarker` evaluates them as Typst (the
187
+ `raw-typst: true` option is already the default and is set in the wrapper above).
188
+ The rest of the document stays plain markdown.
189
+
190
+ Each snippet below is self-contained — paste it into your `.md` where you want the
191
+ element. They use Typst built-ins only (no extra packages).
192
+
193
+ **Cover banner** (top of a report):
194
+
195
+ ```markdown
196
+ <!--raw-typst
197
+ #block(width: 100%, fill: rgb("#0f172a"), inset: 18pt, radius: 6pt)[
198
+ #text(fill: white, size: 1.6em, weight: "bold")[Quarterly Business Review]
199
+ #v(2pt)
200
+ #text(fill: rgb("#94a3b8"), size: 0.95em)[Acme Robotics · Q2 2026 · Confidential]
201
+ ]
202
+ #v(1em)
203
+ -->
204
+ ```
205
+
206
+ **Callout boxes** (info / warning — change the two colors for other variants):
207
+
208
+ ```markdown
209
+ <!--raw-typst
210
+ #block(fill: rgb("#eff6ff"), stroke: (left: 3pt + rgb("#3b82f6")), inset: 12pt, radius: 4pt, width: 100%)[
211
+ #text(weight: "bold")[Note.] Revenue grew 31% YoY.
212
+ ]
213
+ #v(0.6em)
214
+ #block(fill: rgb("#fef2f2"), stroke: (left: 3pt + rgb("#ef4444")), inset: 12pt, radius: 4pt, width: 100%)[
215
+ #text(weight: "bold")[Risk.] A single supplier covers 40% of NPUs.
216
+ ]
217
+ -->
218
+ ```
219
+
220
+ **Two-column section** (use `#colbreak()` to split):
221
+
222
+ ```markdown
223
+ <!--raw-typst
224
+ #columns(2, gutter: 1.4em)[
225
+ #text(weight: "bold")[Strengths]
226
+ - Net retention 124%
227
+ - Margin +240bps
228
+ #colbreak()
229
+ #text(weight: "bold")[Risks]
230
+ - Supplier concentration
231
+ - Partial FX hedging
232
+ ]
233
+ -->
234
+ ```
235
+
236
+ **Figure with caption** (swap the `rect(...)` for `image("chart.png")` to embed an
237
+ image written to `workspace/`):
238
+
239
+ ```markdown
240
+ <!--raw-typst
241
+ #figure(
242
+ rect(width: 60%, height: 48pt, fill: luma(245), stroke: 0.5pt + luma(180)),
243
+ caption: [Revenue trend, Q1–Q2 2026.],
244
+ )
245
+ -->
246
+ ```
247
+
248
+ **Definition grid** (label column + description column):
249
+
250
+ ```markdown
251
+ <!--raw-typst
252
+ #grid(columns: (auto, 1fr), row-gutter: 6pt, column-gutter: 12pt,
253
+ text(weight: "bold")[NPU], [Neural processing unit — on-device inference accelerator.],
254
+ text(weight: "bold")[Net retention], [Revenue from existing customers vs. a year ago.],
255
+ )
256
+ -->
257
+ ```
258
+
259
+ Keep it tasteful — a banner, a couple of callouts, and one good figure read as
260
+ deliberate; a wall of colored boxes reads as noise.
261
+
262
+ ## Rendering an _existing_ web page or HTML to PDF
263
+
264
+ This skill renders **markdown you author**. If instead you need to capture an
265
+ **existing web page or a live URL** as a PDF — something Typst cannot do — use the
266
+ already-installed `agent-browser` (Chrome): `agent-browser --allow-file-access open
267
+ file:///agent/workspace/page.html` (or a URL), then `agent-browser pdf
268
+ /agent/workspace/out.pdf`. Note its output is fixed US-Letter with default margins
269
+ (no page-size flags), and launching the browser needs a trusted/owner session — so
270
+ it's the right tool for _archiving web content_, not for authoring styled reports.
271
+ For authored documents, stay on the Typst path above.
272
+
273
+ ## Step 4 — deliver
274
+
275
+ - **Channel asked for the PDF** — attach it:
276
+
277
+ ```
278
+ channel_send(text: "Here's the report.", attachments: [{ path: "/agent/workspace/report.pdf", filename: "Edge-AI-Brief.pdf" }])
279
+ ```
280
+
281
+ Use a human-friendly `filename` and an absolute `/agent/workspace/...` path. Slack,
282
+ Discord, Telegram, and KakaoTalk upload the file; the GitHub adapter has no
283
+ attachment support, so there post a link or paste the markdown.
284
+
285
+ - **Replying in a thread** — use `channel_reply` with the same `attachments` shape.
286
+
287
+ - **No channel** (TUI session) — just report the path: `workspace/report.pdf`.
288
+
289
+ ## If you got the markdown from a subagent
290
+
291
+ The `researcher` subagent writes its report to `workspace/research-<slug>.md` and
292
+ returns a `<report>` block naming the file. Point the wrapper's `read(...)` at that
293
+ file, render, and attach. You do the PDF step — the researcher's `bash` is
294
+ read-only and it only emits markdown by design.
295
+
296
+ ## Customizing this skill
297
+
298
+ This is a bundled default. Want a different house style, a different converter, or
299
+ a cover page with a logo? Copy this file to `.agents/skills/<your-name>/SKILL.md`
300
+ (use a **different** `name`; bundled skills win name collisions) and edit the setup
301
+ or the wrapper there. Because the whole pipeline — install + render — lives in the
302
+ skill, you can change either half without touching the container image.
303
+
304
+ ## Known limitations
305
+
306
+ `cmarker` covers CommonMark well, but a few markdown features don't render as you
307
+ might expect:
308
+
309
+ - **Task-list checkboxes** (`- [ ]` / `- [x]`) render as literal `[ ]` text, not
310
+ checkboxes. Use a plain bullet list or a status column in a table instead.
311
+ - **Bold/italic directly adjacent to CJK + parenthetical Latin** (e.g.
312
+ `**로컬 우선(local-first)**`) may not be recognized as emphasis — CommonMark's
313
+ flanking rules treat that boundary as non-emphasis. Put a space inside, or bold a
314
+ pure run of text.
315
+ - **Raw HTML** in the markdown is mostly ignored. Express structure in markdown
316
+ (tables, lists) rather than HTML.
317
+
318
+ ## Don'ts
319
+
320
+ - **Don't** hand-write Typst markup for the body. Let `cmarker` render the
321
+ markdown; only style via `#set` / `#show` rules in the wrapper.
322
+ - **Don't** write the `.typ`, `.md`, `.pdf`, or `.tools/` outside `workspace/` —
323
+ the sandbox blocks it.
324
+ - **Don't** re-run Step 0's install if the tools already exist — the guard at the
325
+ top skips it. Re-installing every time is wasteful.
326
+ - **Don't** attach a PDF to a GitHub channel — that adapter rejects attachments.
327
+ Link or inline instead.
@@ -190,6 +190,18 @@
190
190
  "minLength": 1
191
191
  }
192
192
  },
193
+ "compose": {
194
+ "default": {
195
+ "exclude": false
196
+ },
197
+ "type": "object",
198
+ "properties": {
199
+ "exclude": {
200
+ "default": false,
201
+ "type": "boolean"
202
+ }
203
+ }
204
+ },
193
205
  "channels": {
194
206
  "default": {},
195
207
  "type": "object",
@@ -1114,6 +1126,18 @@
1114
1126
  }
1115
1127
  }
1116
1128
  },
1129
+ "sandbox": {
1130
+ "default": {
1131
+ "realProc": false
1132
+ },
1133
+ "type": "object",
1134
+ "properties": {
1135
+ "realProc": {
1136
+ "default": false,
1137
+ "type": "boolean"
1138
+ }
1139
+ }
1140
+ },
1117
1141
  "docker": {
1118
1142
  "default": {
1119
1143
  "file": {