typeclaw 0.35.1 → 0.36.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/package.json +2 -1
- package/src/agent/session-origin.ts +4 -3
- package/src/agent/system-prompt.ts +1 -1
- package/src/bundled-plugins/doc-render/index.ts +20 -0
- package/src/bundled-plugins/doc-render/render.ts +140 -0
- package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +314 -0
- package/src/bundled-plugins/security/index.ts +15 -0
- package/src/bundled-plugins/security/permissions.ts +1 -0
- package/src/bundled-plugins/security/policies/plugin-addition.ts +240 -0
- package/src/channels/adapters/line.ts +12 -2
- package/src/cli/channel.ts +190 -7
- package/src/cli/inspect-select.ts +121 -0
- package/src/cli/inspect.ts +15 -7
- package/src/config/channels-mutation.ts +250 -0
- package/src/container/start.ts +24 -1
- package/src/init/reconcile-plugin-deps.ts +173 -0
- package/src/inspect/index.ts +5 -2
- package/src/inspect/loop.ts +26 -9
- package/src/inspect/render.ts +28 -1
- package/src/inspect/transcript-view.ts +52 -11
- package/src/plugin/index.ts +2 -2
- package/src/plugin/loader.ts +61 -7
- package/src/plugin/manager.ts +18 -4
- package/src/run/bundled-plugins.ts +2 -0
- package/src/secrets/storage.ts +27 -0
- package/src/skills/typeclaw-markdown-pdf/SKILL.md +0 -400
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typeclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"homepage": "https://github.com/typeclaw/typeclaw#readme",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/typeclaw/typeclaw/issues"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"postinstall": "bun run scripts/generate-schema.ts"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"@clack/core": "^1.2.0",
|
|
45
46
|
"@clack/prompts": "^1.2.0",
|
|
46
47
|
"@mariozechner/pi-coding-agent": "^0.67.3",
|
|
47
48
|
"@mariozechner/pi-tui": "^0.67.3",
|
|
@@ -574,14 +574,15 @@ function renderResearchReportDeliveryGuidance(platformInfo: PlatformInfo): strin
|
|
|
574
574
|
`**Ship reports as a PDF by default.** ${platformInfo.displayName} accepts file`,
|
|
575
575
|
'attachments. When the user asks for a report, document, brief, or "the report"',
|
|
576
576
|
'— or a `researcher` subagent hands you a `research-<slug>.md` file path in its',
|
|
577
|
-
'`<report>` block — convert that markdown to a PDF with the `typeclaw-
|
|
577
|
+
'`<report>` block — convert that markdown to a PDF with the `typeclaw-render-pdf`',
|
|
578
578
|
'skill and deliver it with `channel_send({ ..., attachments: [{ path, filename }] })`,',
|
|
579
579
|
'with a one- or two-line summary as the message text. A `researcher` `<summary>`',
|
|
580
580
|
'is a teaser, NOT the deliverable: the deliverable is the report file rendered to',
|
|
581
581
|
'PDF. Never build the PDF with an ad-hoc library (jsPDF, pdfkit, a raw-text dump) —',
|
|
582
582
|
'that yields unrendered markdown and mojibake; the skill is the only correct path.',
|
|
583
|
-
"For CJK (Korean/Japanese/Chinese) reports, follow that skill's CJK
|
|
584
|
-
'never ship a tofu-rendered PDF;
|
|
583
|
+
"For CJK (Korean/Japanese/Chinese) reports, follow that skill's CJK guidance —",
|
|
584
|
+
'never ship a tofu-rendered PDF; if the output has tofu boxes, ask before',
|
|
585
|
+
'enabling the opt-in `cjkFonts` and restarting.',
|
|
585
586
|
'A downloadable file is what a human wants for a multi-page report; do not paste',
|
|
586
587
|
'the full markdown into chat, and do not attach the raw `.md` when asked for a',
|
|
587
588
|
'report or PDF. Send inline plain text only if the caller explicitly asked for it,',
|
|
@@ -63,7 +63,7 @@ Do not narrate routine, low-risk tool calls. Just call the tool. Narrate only wh
|
|
|
63
63
|
|
|
64
64
|
When the user asks for a *report*, *document*, *brief*, *PDF*, or asks you to *send/show/attach/export* a generated result — anything where the deliverable is a file a human would download, print, or forward — produce a polished file, not a wall of text pasted into chat and not a one-line summary that drops the substance. A summary (yours or a subagent's) is a pointer to the deliverable, never the deliverable itself; when the user asked for the report, ship the report.
|
|
65
65
|
|
|
66
|
-
To turn Markdown into a PDF, use the bundled \`typeclaw-
|
|
66
|
+
To turn Markdown into a PDF, use the bundled \`typeclaw-render-pdf\` skill — it is the only supported path and it renders Markdown properly (headings, lists, tables). **Never** hand-roll a PDF with an ad-hoc library (jsPDF, pdfkit, a canvas text dump, a headless-browser raw-text print, Python ReportLab): those produce unrendered raw \`##\`/\`**\` markup and mojibake for non-Latin text. CJK fonts are opt-in, so for Korean/Japanese/Chinese reports follow that skill's CJK guidance — never ship a tofu-rendered PDF; if the output has tofu boxes, ask before enabling opt-in CJK fonts and restarting. If a request is plainly satisfied by inline chat — a short answer, a snippet, a quick explanation — stay inline; this rule is for explicit document deliverables, not for every long reply.
|
|
67
67
|
|
|
68
68
|
## Long-running and interactive shell work
|
|
69
69
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { definePlugin } from '@/plugin'
|
|
4
|
+
|
|
5
|
+
// In-container path of the bundled render script, relative to the agent root
|
|
6
|
+
// (the bwrap jail ro-binds /agent, so a low-trust sandboxed `bun run` can read
|
|
7
|
+
// it here). typeclaw always installs at node_modules/typeclaw and ships src/ to
|
|
8
|
+
// npm, so this path is stable across dev and prod. The skill references the same
|
|
9
|
+
// path — keep them in lockstep.
|
|
10
|
+
export const RENDER_SCRIPT_AGENT_RELATIVE_PATH = 'node_modules/typeclaw/src/bundled-plugins/doc-render/render.ts'
|
|
11
|
+
|
|
12
|
+
export function renderScriptPath(): string {
|
|
13
|
+
return join(import.meta.dir, 'render.ts')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default definePlugin({
|
|
17
|
+
plugin: async () => ({
|
|
18
|
+
skillsDirs: [join(import.meta.dir, 'skills')],
|
|
19
|
+
}),
|
|
20
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Bundled `bun run`-able render script for the doc-render plugin: the agent runs
|
|
3
|
+
// `bun run <this> <main.typ> <out.pdf>`. Lives as a script (not a registered
|
|
4
|
+
// tool) so it costs zero always-on system-prompt context, and the agent
|
|
5
|
+
// dynamic-imports the Typst compiler from its own /agent/node_modules — installed
|
|
6
|
+
// on first use, never baked into the image. Its on-disk path is published to a
|
|
7
|
+
// /tmp hint at boot (see index.ts) so the skill can resolve it.
|
|
8
|
+
//
|
|
9
|
+
// TODO(doc-render): PDF via Typst only. docx/xlsx/pptx are tracked separately —
|
|
10
|
+
// each an npm renderer installed via the same dynamic `bun add` + dynamic-import
|
|
11
|
+
// pattern. Office *conversion* (md->docx, docx->pdf) needs a system binary
|
|
12
|
+
// (LibreOffice/Pandoc), not an npm package, so it is out of scope here. See
|
|
13
|
+
// typeclaw/typeclaw#761.
|
|
14
|
+
|
|
15
|
+
// Keep in sync with the `bun add` line in skills/typeclaw-render-pdf/SKILL.md.
|
|
16
|
+
// 0.7.0 embeds Typst 0.14.2; bumping is a deliberate, re-validated edit.
|
|
17
|
+
export const COMPILER_PACKAGE = '@myriaddreamin/typst-ts-node-compiler'
|
|
18
|
+
export const COMPILER_VERSION = '0.7.0'
|
|
19
|
+
|
|
20
|
+
// NodeCompiler does not auto-discover system font dirs the way the Typst CLI
|
|
21
|
+
// does; without these, CJK glyphs render as .notdef tofu. Filtered by existence
|
|
22
|
+
// so a missing dir on a dev/host run is skipped, not fatal.
|
|
23
|
+
export const FONT_PATHS = ['/usr/share/fonts', '/usr/local/share/fonts', '/Library/Fonts', '/System/Library/Fonts']
|
|
24
|
+
|
|
25
|
+
export function missingCompilerGuidance(): string {
|
|
26
|
+
return [
|
|
27
|
+
`doc-render: ${COMPILER_PACKAGE} is not installed.`,
|
|
28
|
+
'',
|
|
29
|
+
'One-time PDF toolchain install. Run from the agent root (writes node_modules',
|
|
30
|
+
'+ package.json + bun.lock, which survive restarts), then re-run this render:',
|
|
31
|
+
'',
|
|
32
|
+
` bun add ${COMPILER_PACKAGE}@${COMPILER_VERSION}`,
|
|
33
|
+
'',
|
|
34
|
+
'Do NOT fall back to jsPDF, pdfkit, a canvas text dump, a headless-browser',
|
|
35
|
+
'raw-text print, or Python ReportLab — those skip Markdown rendering and ship',
|
|
36
|
+
'no CJK font. This Typst path is the only supported one.',
|
|
37
|
+
].join('\n')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The per-tool sandbox falls back to a degraded `--tmpfs /proc` on a host with
|
|
41
|
+
// no usable user namespaces, where Bun's loader can't read /proc/self/{fd,maps}
|
|
42
|
+
// and aborts with ENOTDIR. Rare since the proc-bind retry fix (typeclaw 0.35.1),
|
|
43
|
+
// but still possible on exotic runtimes — and unfixable by switching libraries.
|
|
44
|
+
function isNotDirError(error: unknown): boolean {
|
|
45
|
+
return (
|
|
46
|
+
typeof error === 'object' && error !== null && 'code' in error && (error as { code?: unknown }).code === 'ENOTDIR'
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function notDirGuidance(): string {
|
|
51
|
+
return [
|
|
52
|
+
'doc-render: the renderer aborted with a Bun "NotDir" / ENOTDIR error.',
|
|
53
|
+
'',
|
|
54
|
+
'This is the sandbox /proc degraded mode, not your markup and not a missing',
|
|
55
|
+
'font: on a host with no usable user namespaces the per-tool sandbox falls',
|
|
56
|
+
"back to a tmpfs /proc where bun can't run packages. Retry once; if it",
|
|
57
|
+
'persists, report it as a sandbox/environment issue. A different PDF library',
|
|
58
|
+
'will not fix a /proc problem.',
|
|
59
|
+
].join('\n')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type NodeCompilerModule = {
|
|
63
|
+
NodeCompiler: {
|
|
64
|
+
create(options: { workspace: string; fontArgs?: { fontPaths: string[] }[] }): {
|
|
65
|
+
pdf(options: { mainFilePath: string }): Uint8Array
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Resolve from the CALLER's cwd, not this script's location: the agent installs
|
|
71
|
+
// the compiler into its own /agent/node_modules and runs `bun run <this>` from
|
|
72
|
+
// the agent root, but bare `import(pkg)` resolves relative to this file (under
|
|
73
|
+
// node_modules/typeclaw/...), which need not see the agent's deps. Resolving
|
|
74
|
+
// against cwd, then importing the absolute path, makes the lookup independent of
|
|
75
|
+
// where the bundled script physically lives.
|
|
76
|
+
export async function loadCompilerModule(cwd: string = process.cwd()): Promise<NodeCompilerModule> {
|
|
77
|
+
const resolved = Bun.resolveSync(COMPILER_PACKAGE, cwd)
|
|
78
|
+
return (await import(resolved)) as NodeCompilerModule
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function renderPdf(mainFile: string, outFile: string): Promise<number> {
|
|
82
|
+
const { existsSync, writeFileSync } = await import('node:fs')
|
|
83
|
+
|
|
84
|
+
const fontPaths = FONT_PATHS.filter((p) => existsSync(p))
|
|
85
|
+
const mod = await loadCompilerModule()
|
|
86
|
+
const compiler = mod.NodeCompiler.create({
|
|
87
|
+
workspace: '.',
|
|
88
|
+
...(fontPaths.length > 0 ? { fontArgs: [{ fontPaths }] } : {}),
|
|
89
|
+
})
|
|
90
|
+
const pdf = compiler.pdf({ mainFilePath: mainFile })
|
|
91
|
+
writeFileSync(outFile, Buffer.from(pdf))
|
|
92
|
+
return pdf.length
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Bun phrasings for an unresolved import. Broad on purpose: a false positive only
|
|
96
|
+
// shows the install hint on an unrelated error, and the package name in the
|
|
97
|
+
// message keeps it specific in practice.
|
|
98
|
+
export function isModuleNotFound(message: string): boolean {
|
|
99
|
+
return (
|
|
100
|
+
message.includes('Cannot find package') ||
|
|
101
|
+
message.includes('Cannot find module') ||
|
|
102
|
+
message.includes('Module not found') ||
|
|
103
|
+
message.includes(COMPILER_PACKAGE)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const EXIT_RENDER_FAILED = 1
|
|
108
|
+
const EXIT_BAD_USAGE = 2
|
|
109
|
+
const EXIT_COMPILER_MISSING = 3
|
|
110
|
+
|
|
111
|
+
async function main(): Promise<void> {
|
|
112
|
+
const [, , mainFile, outFile] = process.argv
|
|
113
|
+
if (!mainFile || !outFile) {
|
|
114
|
+
process.stderr.write('usage: bun run render.ts <main.typ> <out.pdf>\n')
|
|
115
|
+
process.exit(EXIT_BAD_USAGE)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let bytes: number
|
|
119
|
+
try {
|
|
120
|
+
bytes = await renderPdf(mainFile, outFile)
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (isNotDirError(error)) {
|
|
123
|
+
process.stderr.write(`${notDirGuidance()}\n`)
|
|
124
|
+
process.exit(EXIT_RENDER_FAILED)
|
|
125
|
+
}
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
127
|
+
if (isModuleNotFound(message)) {
|
|
128
|
+
process.stderr.write(`${missingCompilerGuidance()}\n`)
|
|
129
|
+
process.exit(EXIT_COMPILER_MISSING)
|
|
130
|
+
}
|
|
131
|
+
process.stderr.write(`doc-render: render failed: ${message}\n`)
|
|
132
|
+
process.exit(EXIT_RENDER_FAILED)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process.stdout.write(`wrote ${outFile} (${bytes} bytes)\n`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (import.meta.main) {
|
|
139
|
+
await main()
|
|
140
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typeclaw-render-pdf
|
|
3
|
+
description: "The ONLY supported way to render 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, render report, export document, anything a human would want to download, print, or forward, including a researcher's report file shipped as a Slack/Discord attachment. Triggers: 'make a PDF', 'export to PDF', 'markdown to PDF', 'PDF report', 'render report', 'export document', 'the 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 첨부', '리포트', '보고서'. Provided by the bundled `doc-render` plugin: a small Typst toolchain is installed on first use via `bun add` (no PDF library is baked into the image), and a bundled render script does the compile. Handles CJK/Korean/Japanese/Chinese: CJK fonts are opt-in, so if the output has tofu (□□□) boxes it tells you to enable `docker.file.cjkFonts` and restart, then regenerates — it never auto-downloads a font. Also load before saying you cannot produce PDFs — you can. NEVER build a PDF with jsPDF, pdfkit, a canvas text dump, a headless-browser raw-text print, or Python ReportLab — those produce unrendered markdown and broken CJK; this skill is the only correct path. Covers the one-time install, 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; doc-render produces documents, it does not read them."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# typeclaw-render-pdf
|
|
7
|
+
|
|
8
|
+
You can produce professional PDFs from Markdown. The bundled `doc-render` plugin
|
|
9
|
+
ships the render script; the only thing installed on demand is the
|
|
10
|
+
[Typst](https://typst.app) compiler — a single npm package the agent `bun add`s
|
|
11
|
+
into its own `node_modules` the **first time** you need a PDF, then reuses. No
|
|
12
|
+
Pandoc, no LaTeX, no headless browser, and no PDF toolchain baked into the image.
|
|
13
|
+
|
|
14
|
+
The flow is: **(1)** install the compiler once (`bun add` — it lands in the
|
|
15
|
+
agent's `node_modules`, surviving restarts), **(2)** write a styled `.typ`
|
|
16
|
+
wrapper that reads your Markdown, **(3)** run the bundled render script. If a
|
|
17
|
+
channel asked for the PDF, attach the result with `channel_send`.
|
|
18
|
+
|
|
19
|
+
You do **not** need to learn Typst markup. The
|
|
20
|
+
[`cmarker`](https://typst.app/universe/package/cmarker/) package renders your
|
|
21
|
+
CommonMark (headings, lists, tables, code, blockquotes, footnotes, links,
|
|
22
|
+
images). The wrapper only sets _styling_ — fonts, margins, headings, page numbers
|
|
23
|
+
— so the output looks deliberate, not like a default-template export.
|
|
24
|
+
|
|
25
|
+
> **This is the only supported way to make a PDF from Markdown in TypeClaw.**
|
|
26
|
+
> Do **not** reach for `jsPDF`, `pdfkit`, a `<canvas>` text dump, a
|
|
27
|
+
> headless-browser "print raw text" path, or Python `ReportLab`. Those skip
|
|
28
|
+
> Markdown rendering (you get literal `##` and `**` in the output) and ship no
|
|
29
|
+
> CJK font, so Korean/Japanese/Chinese come out as mojibake. The Typst path below
|
|
30
|
+
> renders the Markdown properly. If you catch yourself about to `bun add` a PDF
|
|
31
|
+
> library other than the Typst compiler named here, stop.
|
|
32
|
+
|
|
33
|
+
## When to use this
|
|
34
|
+
|
|
35
|
+
- A research report, brief, or summary the user wants as a downloadable file.
|
|
36
|
+
- A subagent (e.g. the `researcher`) handed you a `research-<slug>.md` to ship as a PDF.
|
|
37
|
+
- Any channel message asking for "a PDF" / "the report attached" / "PDF로 보내줘".
|
|
38
|
+
|
|
39
|
+
When plain markdown in chat is fine, **don't** make a PDF. This is for when a
|
|
40
|
+
_file_ is the deliverable.
|
|
41
|
+
|
|
42
|
+
## Step 0 — install the Typst compiler (once per container)
|
|
43
|
+
|
|
44
|
+
The PDF compiler is not baked into the image — install it on first use. It is a
|
|
45
|
+
single version-pinned npm package (npm pulls only this platform's prebuilt
|
|
46
|
+
binary — Linux x64/arm64, glibc or musl). It writes to the agent's
|
|
47
|
+
`node_modules` + `package.json` + `bun.lock`, all of which survive restarts, so
|
|
48
|
+
this only runs once per container life:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
# Idempotent: bun add is a no-op if it's already the installed version.
|
|
52
|
+
bun add @myriaddreamin/typst-ts-node-compiler@0.7.0
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `@0.7.0` pin embeds Typst 0.14.2 and keeps the toolchain reproducible — a
|
|
56
|
+
future npm release can't silently change the embedded Typst version or the API
|
|
57
|
+
the render script depends on. If you forget this step, the render script in
|
|
58
|
+
Step 3 stops with the exact `bun add` line to run, so you can also just try the
|
|
59
|
+
render and follow its guidance.
|
|
60
|
+
|
|
61
|
+
> **Where it goes:** the agent's own `node_modules` — the canonical home for
|
|
62
|
+
> executable dependencies, gitignored, not user-facing. Do **not** create a
|
|
63
|
+
> `package.json` or `node_modules` under `workspace/` for this; let `bun add`
|
|
64
|
+
> manage it at the agent root like any other dependency.
|
|
65
|
+
|
|
66
|
+
## Step 1 — have the markdown ready
|
|
67
|
+
|
|
68
|
+
Use an existing markdown file (yours or a subagent's), or `write` your content to
|
|
69
|
+
a markdown file. Standard CommonMark plus tables and footnotes all work. Put the
|
|
70
|
+
`.md` and the `.typ` (Step 2) in the same directory so the wrapper's relative
|
|
71
|
+
`read("...")` resolves — any agent-writable directory works (`workspace/`,
|
|
72
|
+
`public/`, `mounts/`, or wherever the source `.md` already lives, e.g. a
|
|
73
|
+
researcher's report under `public/`). There is no required directory; keep the
|
|
74
|
+
two files together and run the render from there.
|
|
75
|
+
|
|
76
|
+
## Step 2 — write the styled wrapper
|
|
77
|
+
|
|
78
|
+
`write` a `.typ` next to your markdown, pointing `read("...")` at the markdown
|
|
79
|
+
filename. The wrapper below is a clean, professional **starting point** — not a
|
|
80
|
+
fixed template. Design the document to fit its content and audience: a dense
|
|
81
|
+
technical brief wants tighter margins than an airy exec summary; a launch report
|
|
82
|
+
might open with a cover banner. Adjust fonts, spacing, and structure freely, and
|
|
83
|
+
reach for the **Rich elements** palette below when plain prose isn't enough.
|
|
84
|
+
|
|
85
|
+
```typst
|
|
86
|
+
#set document(title: "Report")
|
|
87
|
+
#set page(
|
|
88
|
+
paper: "a4",
|
|
89
|
+
margin: (x: 2.5cm, y: 2.75cm),
|
|
90
|
+
numbering: "1",
|
|
91
|
+
footer: context align(center, text(size: 9pt, fill: luma(120))[
|
|
92
|
+
#counter(page).display("1 / 1", both: true)
|
|
93
|
+
]),
|
|
94
|
+
)
|
|
95
|
+
#set text(font: ("Libertinus Serif", "New Computer Modern", "Noto Serif CJK KR"), size: 11pt, lang: "en")
|
|
96
|
+
#set par(justify: true, leading: 0.68em, spacing: 1.1em)
|
|
97
|
+
|
|
98
|
+
#show heading: set text(weight: "semibold")
|
|
99
|
+
#show heading.where(level: 1): it => block(width: 100%, above: 1.4em, below: 0.9em)[
|
|
100
|
+
#text(size: 1.5em, it.body)
|
|
101
|
+
#v(-0.4em)
|
|
102
|
+
#line(length: 100%, stroke: 0.5pt + luma(200))
|
|
103
|
+
]
|
|
104
|
+
#show link: it => text(fill: rgb("#1a56db"), underline(it))
|
|
105
|
+
#show quote.where(block: true): it => block(
|
|
106
|
+
inset: (left: 1em), stroke: (left: 2pt + luma(200)),
|
|
107
|
+
text(style: "italic", fill: luma(80), it.body),
|
|
108
|
+
)
|
|
109
|
+
#show raw.where(block: true): it => block(
|
|
110
|
+
fill: luma(245), inset: 8pt, radius: 4pt, width: 100%, text(size: 9pt, it),
|
|
111
|
+
)
|
|
112
|
+
#show table: set table(stroke: 0.5pt + luma(200))
|
|
113
|
+
|
|
114
|
+
#import "@preview/cmarker:0.1.8"
|
|
115
|
+
#cmarker.render(read("report.md"), h1-level: 1, blockquote: quote.with(block: true))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
|
|
120
|
+
- `read("report.md")` is **relative to the render's working directory**, so Step 3
|
|
121
|
+
`cd`s into the directory holding the `.typ` and `.md` before running. Keep them
|
|
122
|
+
together.
|
|
123
|
+
- Fonts `Libertinus Serif` / `New Computer Modern` are bundled with Typst (no font
|
|
124
|
+
install) and carry the Latin text. `"Noto Serif CJK KR"` is appended as the
|
|
125
|
+
fallback so Korean/CJK glyphs resolve per-glyph wherever the Latin fonts have no
|
|
126
|
+
glyph, leaving Latin runs untouched. It is only present when the container's
|
|
127
|
+
`cjkFonts` toggle is on — see "## Handling CJK content".
|
|
128
|
+
- `cmarker` fetches from the Typst package registry on first compile (the same
|
|
129
|
+
network the `bun add` step needs). It caches under the render's `$HOME`, which
|
|
130
|
+
in a sandboxed (channel/guest) session is per-session scratch — so a later
|
|
131
|
+
session may re-fetch it. That's a one-time network hit, not an error.
|
|
132
|
+
|
|
133
|
+
## Step 3 — render
|
|
134
|
+
|
|
135
|
+
The render script is **bundled with the plugin** — you do not write it. It lives
|
|
136
|
+
at `/agent/node_modules/typeclaw/src/bundled-plugins/doc-render/render.ts`.
|
|
137
|
+
|
|
138
|
+
**`cd` into the directory holding your `.typ` and `.md` first** — your shell
|
|
139
|
+
starts at the agent root, and the wrapper's `read("report.md")` resolves relative
|
|
140
|
+
to the render's working directory, so you must run it from there:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
cd /agent/workspace # or wherever your .typ + .md live (public/, mounts/, …)
|
|
144
|
+
bun run /agent/node_modules/typeclaw/src/bundled-plugins/doc-render/render.ts report.typ report.pdf
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
On success it prints `wrote report.pdf (<N> bytes)` and `report.pdf` exists in
|
|
148
|
+
that directory.
|
|
149
|
+
|
|
150
|
+
If it stops with **`@myriaddreamin/typst-ts-node-compiler is not installed`**
|
|
151
|
+
(exit 3), run the Step 0 `bun add` and re-run. If it stops with a **`NotDir` /
|
|
152
|
+
ENOTDIR** error, that is the sandbox `/proc` degraded mode, not your markup and
|
|
153
|
+
not a font — retry once; if it persists, report it as a sandbox/environment
|
|
154
|
+
issue and do **not** switch to another PDF library (it won't help). Any other
|
|
155
|
+
error is a real Typst compile error (usually raw HTML or an unsupported markdown
|
|
156
|
+
extension) and names the offending line — simplify that part and re-run.
|
|
157
|
+
|
|
158
|
+
## Handling CJK content
|
|
159
|
+
|
|
160
|
+
CJK fonts are **opt-in** (the `docker.file.cjkFonts` toggle). When they are off,
|
|
161
|
+
Typst still renders — it just substitutes `.notdef` tofu (□) boxes for every
|
|
162
|
+
Korean/Japanese/Chinese glyph. **Do not** download, vendor, or `curl` a font to
|
|
163
|
+
work around this, and **do not** silently deliver a tofu PDF.
|
|
164
|
+
|
|
165
|
+
You don't need a pre-render gate: render first, then verify. If the source
|
|
166
|
+
markdown contains CJK and the resulting PDF shows tofu boxes (or you know CJK
|
|
167
|
+
fonts aren't enabled on this container), tell the user honestly and offer the
|
|
168
|
+
fix:
|
|
169
|
+
|
|
170
|
+
> This report has Korean/Japanese/Chinese text but the container has no CJK font
|
|
171
|
+
> — they're opt-in, so the PDF comes out as tofu boxes. Want me to set
|
|
172
|
+
> `docker.file.cjkFonts: true` in `typeclaw.json`? It's a boot setting, so after
|
|
173
|
+
> I edit it you'll run `typeclaw restart` from the host project directory, and
|
|
174
|
+
> then I'll regenerate the PDF.
|
|
175
|
+
|
|
176
|
+
Only after the user agrees: edit `typeclaw.json` to set `docker.file.cjkFonts:
|
|
177
|
+
true` (use the `typeclaw-config` skill), ask them to `typeclaw restart`, and
|
|
178
|
+
regenerate the PDF after the restarted container comes back. If the markdown has
|
|
179
|
+
no CJK, this section doesn't apply.
|
|
180
|
+
|
|
181
|
+
## Rich elements (optional)
|
|
182
|
+
|
|
183
|
+
When plain markdown isn't enough — a cover banner, callout boxes, multi-column
|
|
184
|
+
sections, captioned figures — you don't switch to HTML (Typst doesn't render
|
|
185
|
+
HTML). Instead, drop **raw Typst** into the markdown via `<!--raw-typst ... -->`
|
|
186
|
+
comments. `cmarker` evaluates them as Typst (the `raw-typst: true` option is the
|
|
187
|
+
default). The rest of the document stays plain markdown.
|
|
188
|
+
|
|
189
|
+
Each snippet below is self-contained — paste it into your `.md` where you want the
|
|
190
|
+
element. They use Typst built-ins only (no extra packages).
|
|
191
|
+
|
|
192
|
+
**Cover banner** (top of a report):
|
|
193
|
+
|
|
194
|
+
```markdown
|
|
195
|
+
<!--raw-typst
|
|
196
|
+
#block(width: 100%, fill: rgb("#0f172a"), inset: 18pt, radius: 6pt)[
|
|
197
|
+
#text(fill: white, size: 1.6em, weight: "bold")[Quarterly Business Review]
|
|
198
|
+
#v(2pt)
|
|
199
|
+
#text(fill: rgb("#94a3b8"), size: 0.95em)[Acme Robotics · Q2 2026 · Confidential]
|
|
200
|
+
]
|
|
201
|
+
#v(1em)
|
|
202
|
+
-->
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Callout boxes** (info / warning — change the two colors for other variants):
|
|
206
|
+
|
|
207
|
+
```markdown
|
|
208
|
+
<!--raw-typst
|
|
209
|
+
#block(fill: rgb("#eff6ff"), stroke: (left: 3pt + rgb("#3b82f6")), inset: 12pt, radius: 4pt, width: 100%)[
|
|
210
|
+
#text(weight: "bold")[Note.] Revenue grew 31% YoY.
|
|
211
|
+
]
|
|
212
|
+
#v(0.6em)
|
|
213
|
+
#block(fill: rgb("#fef2f2"), stroke: (left: 3pt + rgb("#ef4444")), inset: 12pt, radius: 4pt, width: 100%)[
|
|
214
|
+
#text(weight: "bold")[Risk.] A single supplier covers 40% of NPUs.
|
|
215
|
+
]
|
|
216
|
+
-->
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Two-column section** (use `#colbreak()` to split):
|
|
220
|
+
|
|
221
|
+
```markdown
|
|
222
|
+
<!--raw-typst
|
|
223
|
+
#columns(2, gutter: 1.4em)[
|
|
224
|
+
#text(weight: "bold")[Strengths]
|
|
225
|
+
- Net retention 124%
|
|
226
|
+
- Margin +240bps
|
|
227
|
+
#colbreak()
|
|
228
|
+
#text(weight: "bold")[Risks]
|
|
229
|
+
- Supplier concentration
|
|
230
|
+
- Partial FX hedging
|
|
231
|
+
]
|
|
232
|
+
-->
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Figure with caption** (swap the `rect(...)` for `image("chart.png")` to embed an
|
|
236
|
+
image written next to the markdown):
|
|
237
|
+
|
|
238
|
+
```markdown
|
|
239
|
+
<!--raw-typst
|
|
240
|
+
#figure(
|
|
241
|
+
rect(width: 60%, height: 48pt, fill: luma(245), stroke: 0.5pt + luma(180)),
|
|
242
|
+
caption: [Revenue trend, Q1–Q2 2026.],
|
|
243
|
+
)
|
|
244
|
+
-->
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Keep it tasteful — a banner, a couple of callouts, and one good figure read as
|
|
248
|
+
deliberate; a wall of colored boxes reads as noise.
|
|
249
|
+
|
|
250
|
+
## Rendering an _existing_ web page or HTML to PDF
|
|
251
|
+
|
|
252
|
+
This skill renders **markdown you author**. To capture an **existing web page or
|
|
253
|
+
a live URL** as a PDF — something Typst cannot do — use the already-installed
|
|
254
|
+
`agent-browser` (Chrome): `agent-browser --allow-file-access open
|
|
255
|
+
file:///agent/workspace/page.html` (or a URL), then `agent-browser pdf
|
|
256
|
+
/agent/workspace/out.pdf`. Its output is fixed US-Letter with default margins, so
|
|
257
|
+
it's the right tool for _archiving web content_, not for authoring styled
|
|
258
|
+
reports. For authored documents, stay on the Typst path above.
|
|
259
|
+
|
|
260
|
+
## Step 4 — deliver
|
|
261
|
+
|
|
262
|
+
- **Channel asked for the PDF** — attach it:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
channel_send(text: "Here's the report.", attachments: [{ path: "/agent/workspace/report.pdf", filename: "Edge-AI-Brief.pdf" }])
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Use a human-friendly `filename` and an absolute path. Slack, Discord, Telegram,
|
|
269
|
+
and KakaoTalk upload the file; the GitHub adapter has no attachment support, so
|
|
270
|
+
there post a link or paste the markdown.
|
|
271
|
+
|
|
272
|
+
- **Replying in a thread** — use `channel_reply` with the same `attachments` shape.
|
|
273
|
+
|
|
274
|
+
- **No channel** (TUI session) — just report the path: `report.pdf`.
|
|
275
|
+
|
|
276
|
+
## If you got the markdown from a subagent
|
|
277
|
+
|
|
278
|
+
The `researcher` subagent writes its report to `research-<slug>.md` and returns a
|
|
279
|
+
`<report>` block naming the file. Point the wrapper's `read(...)` at that file,
|
|
280
|
+
render in that file's directory, and attach. You do the PDF step — the
|
|
281
|
+
researcher's `bash` is read-only and it only emits markdown by design.
|
|
282
|
+
|
|
283
|
+
## Customizing this skill
|
|
284
|
+
|
|
285
|
+
This is a bundled default. Want a different house style, a cover page with a
|
|
286
|
+
logo, or a different converter? Copy this file to
|
|
287
|
+
`.agents/skills/<your-name>/SKILL.md` (use a **different** `name`; bundled skills
|
|
288
|
+
win name collisions) and edit it there.
|
|
289
|
+
|
|
290
|
+
## Known limitations
|
|
291
|
+
|
|
292
|
+
`cmarker` covers CommonMark well, but a few markdown features don't render as you
|
|
293
|
+
might expect:
|
|
294
|
+
|
|
295
|
+
- **Task-list checkboxes** (`- [ ]` / `- [x]`) render as literal `[ ]` text, not
|
|
296
|
+
checkboxes. Use a plain bullet list or a status column in a table instead.
|
|
297
|
+
- **Bold/italic directly adjacent to CJK + parenthetical Latin** (e.g.
|
|
298
|
+
`**로컬 우선(local-first)**`) may not be recognized as emphasis — CommonMark's
|
|
299
|
+
flanking rules treat that boundary as non-emphasis. Put a space inside, or bold a
|
|
300
|
+
pure run of text.
|
|
301
|
+
- **Raw HTML** in the markdown is mostly ignored. Express structure in markdown
|
|
302
|
+
(tables, lists) rather than HTML.
|
|
303
|
+
|
|
304
|
+
## Don'ts
|
|
305
|
+
|
|
306
|
+
- **Don't** hand-write Typst markup for the body. Let `cmarker` render the
|
|
307
|
+
markdown; only style via `#set` / `#show` rules in the wrapper (and the optional
|
|
308
|
+
raw-typst rich elements).
|
|
309
|
+
- **Don't** build a `package.json` / `node_modules` / a render script under
|
|
310
|
+
`workspace/`. The compiler installs at the agent root via `bun add`; the render
|
|
311
|
+
script is bundled with the plugin (at
|
|
312
|
+
`/agent/node_modules/typeclaw/src/bundled-plugins/doc-render/render.ts`).
|
|
313
|
+
- **Don't** attach a PDF to a GitHub channel — that adapter rejects attachments.
|
|
314
|
+
Link or inline instead.
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
recordGitRemoteTaintIfAny,
|
|
13
13
|
} from './policies/git-exfil'
|
|
14
14
|
import { GUARD_OUTBOUND_SECRET_SEVERITY, checkOutboundSecretGuard } from './policies/outbound-secret-scan'
|
|
15
|
+
import { GUARD_PLUGIN_ADDITION_SEVERITY, checkPluginAdditionGuard } from './policies/plugin-addition'
|
|
15
16
|
import { checkPrivateSurfaceReadGuard } from './policies/private-surface-read'
|
|
16
17
|
import { applyPromptInjectionDefense } from './policies/prompt-injection'
|
|
17
18
|
import { clearSessionTaints } from './policies/remote-taint-state'
|
|
@@ -68,6 +69,8 @@ const BYPASS_ROLE_HINT = {
|
|
|
68
69
|
'owner and trusted have it by default (medium tier); member and guest do not. The privilege-escalation defense for trusted now depends on operator review of `typeclaw.json` backup commits — `roles` is restart-required, so the operator has wall-clock time to revert before the new role table takes effect. Operators who do not review can re-tighten by replacing `roles.trusted.permissions[]` with an explicit list that omits `security.bypass.medium`.',
|
|
69
70
|
[SECURITY_PERMISSIONS.bypassCronPromotion]:
|
|
70
71
|
'owner and trusted have it by default (medium tier); member and guest do not. Same shape as rolePromotion but deferred: a new cron job (or a changed scheduledByRole) fires at schedule-time as the stamped role. The operator-review window between write and execution is the trusted-tier defense.',
|
|
72
|
+
[SECURITY_PERMISSIONS.bypassPluginAddition]:
|
|
73
|
+
'owner and trusted have it by default (medium tier); member and guest do not. Same shape as cronPromotion but for host-side install: a new (or version-bumped) plugins[] entry is materialized into package.json and installed by the next host `typeclaw start`, running npm lifecycle scripts as the operator. The operator-review window between the typeclaw.json write and the next start is the trusted-tier defense.',
|
|
71
74
|
} as const satisfies Record<PerGuardSecurityPermission, string>
|
|
72
75
|
|
|
73
76
|
function withPermissionHint(
|
|
@@ -121,6 +124,18 @@ export default definePlugin({
|
|
|
121
124
|
)
|
|
122
125
|
if (cronPromotionResult) return cronPromotionResult
|
|
123
126
|
|
|
127
|
+
const pluginAdditionResult = canBypass(
|
|
128
|
+
GUARD_PLUGIN_ADDITION_SEVERITY,
|
|
129
|
+
SECURITY_PERMISSIONS.bypassPluginAddition,
|
|
130
|
+
)
|
|
131
|
+
? undefined
|
|
132
|
+
: withPermissionHint(
|
|
133
|
+
await checkPluginAdditionGuard({ tool: event.tool, args: event.args, agentDir: ctx.agentDir }),
|
|
134
|
+
SECURITY_PERMISSIONS.bypassPluginAddition,
|
|
135
|
+
GUARD_PLUGIN_ADDITION_SEVERITY,
|
|
136
|
+
)
|
|
137
|
+
if (pluginAdditionResult) return pluginAdditionResult
|
|
138
|
+
|
|
124
139
|
// Taint-recording runs FIRST, independently of the gitExfil guard.
|
|
125
140
|
// The gitRemoteTainted defense depends on it. We pass through
|
|
126
141
|
// `permittedBypass` for actors who can skip gitExfil (via either the
|
|
@@ -11,6 +11,7 @@ export const SECURITY_PERMISSIONS = {
|
|
|
11
11
|
bypassGitRemoteTainted: 'security.bypass.gitRemoteTainted',
|
|
12
12
|
bypassRolePromotion: 'security.bypass.rolePromotion',
|
|
13
13
|
bypassCronPromotion: 'security.bypass.cronPromotion',
|
|
14
|
+
bypassPluginAddition: 'security.bypass.pluginAddition',
|
|
14
15
|
// Severity-tier bypasses. Tiers classify guards on a two-axis policy:
|
|
15
16
|
// high — bypass sends data to a third-party audience outside the
|
|
16
17
|
// operator's control loop (channel readers, remote git host).
|