opencode-agent-skills-md 1.0.0 → 1.1.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/dist/cli.mjs +770 -0
- package/dist/plugin.mjs +1138 -0
- package/dist/src/cli/config.d.ts +144 -0
- package/dist/src/cli/install.d.ts +33 -0
- package/dist/src/cli/main.d.ts +11 -0
- package/dist/src/cli/real-fs.d.ts +6 -0
- package/dist/src/cli/status.d.ts +34 -0
- package/dist/src/cli/uninstall.d.ts +22 -0
- package/dist/src/host.d.ts +51 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/plugin.d.ts +35 -0
- package/dist/src/sdk.d.ts +51 -0
- package/dist/src/tools.d.ts +86 -0
- package/package.json +48 -18
- package/{packages/opencode-agent-skills-md/src → src}/cli/main.ts +20 -4
- package/.beads/.local_version +0 -1
- package/.beads/README.md +0 -81
- package/.beads/config.yaml +0 -61
- package/.beads/deletions.jsonl +0 -1
- package/.beads/issues.jsonl +0 -64
- package/.beads/metadata.json +0 -4
- package/.gitattributes +0 -3
- package/.github/CODEOWNERS +0 -1
- package/.github/copilot-instructions.md +0 -78
- package/.github/dependabot.yml +0 -13
- package/.github/workflows/release.yml +0 -51
- package/.opencode/command/test-compaction.md +0 -9
- package/.opencode/command/test-find-skills.md +0 -7
- package/.opencode/command/test-read-skill-file.md +0 -14
- package/.opencode/command/test-run-skill-script.md +0 -13
- package/.opencode/command/test-skills.md +0 -14
- package/.opencode/command/test-use-skill.md +0 -10
- package/.opencode/skills/git-helper/SKILL.md +0 -65
- package/.opencode/skills/test-skill/SKILL.md +0 -43
- package/.opencode/skills/test-skill/example-config.json +0 -16
- package/.opencode/skills/test-skill/helper-docs.md +0 -29
- package/.opencode/skills/test-skill/scripts/echo-args +0 -14
- package/.opencode/skills/test-skill/scripts/greet +0 -6
- package/AGENTS.md +0 -43
- package/CHANGELOG.md +0 -178
- package/Justfile +0 -39
- package/README.md +0 -189
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/specs/core-decoupling/spec.md +0 -74
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/tasks.md +0 -64
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/verify-report.md +0 -75
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/apply-progress.md +0 -136
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/archive-report.md +0 -77
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/design.md +0 -89
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/proposal.md +0 -65
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/specs/core-decoupling/spec.md +0 -77
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/tasks.md +0 -65
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/verify-report.md +0 -165
- package/openspec/specs/core-decoupling/spec.md +0 -110
- package/packages/core/package.json +0 -30
- package/packages/core/src/content.d.ts +0 -16
- package/packages/core/src/content.ts +0 -30
- package/packages/core/src/debug.ts +0 -16
- package/packages/core/src/discovery.d.ts +0 -86
- package/packages/core/src/discovery.ts +0 -257
- package/packages/core/src/index.d.ts +0 -20
- package/packages/core/src/index.ts +0 -55
- package/packages/core/src/match.d.ts +0 -19
- package/packages/core/src/match.ts +0 -75
- package/packages/core/src/parse.d.ts +0 -26
- package/packages/core/src/parse.ts +0 -141
- package/packages/core/src/scripts.d.ts +0 -17
- package/packages/core/src/scripts.ts +0 -79
- package/packages/core/src/search.d.ts +0 -83
- package/packages/core/src/search.ts +0 -188
- package/packages/core/src/types.d.ts +0 -82
- package/packages/core/src/types.ts +0 -131
- package/packages/core/src/walk.ts +0 -109
- package/packages/core/tests/agnostic.test.ts +0 -346
- package/packages/core/tests/content.test.ts +0 -65
- package/packages/core/tests/discovery.test.ts +0 -370
- package/packages/core/tests/package-boundary.test.ts +0 -310
- package/packages/core/tests/parse-trigger.test.ts +0 -282
- package/packages/core/tests/search.test.ts +0 -374
- package/packages/core/tests/subpath.test.ts +0 -87
- package/packages/core/tsconfig.json +0 -10
- package/packages/opencode-agent-skills-md/package.json +0 -42
- package/packages/opencode-agent-skills-md/rolldown.config.js +0 -48
- package/packages/opencode-agent-skills-md/tests/cli-commands.test.ts +0 -1423
- package/packages/opencode-agent-skills-md/tests/e2e/startup-smoke.test.ts +0 -66
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.claude/skills/claude-user-only-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/shared-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/user-only-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.claude/skills/claude-project-only-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/go-tester/SKILL.md +0 -12
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/nested/team/nested-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/rust-tester/SKILL.md +0 -11
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/bin/echo.sh +0 -2
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/docs/reference.md +0 -1
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/shared-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/using-superpowers/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/integration/helpers/mock-opencode.ts +0 -114
- package/packages/opencode-agent-skills-md/tests/integration/plugin.test.ts +0 -316
- package/packages/opencode-agent-skills-md/tests/integration/skill-discovery.test.ts +0 -315
- package/packages/opencode-agent-skills-md/tests/opencode/host.test.ts +0 -179
- package/packages/opencode-agent-skills-md/tests/opencode/plugin.test.ts +0 -551
- package/packages/opencode-agent-skills-md/tests/opencode/subpath.test.ts +0 -66
- package/packages/opencode-agent-skills-md/tests/opencode/tools.test.ts +0 -213
- package/packages/opencode-agent-skills-md/tests/package-boundary.test.ts +0 -346
- package/packages/opencode-agent-skills-md/tests/tools-security.test.ts +0 -72
- package/packages/opencode-agent-skills-md/tsconfig.build.json +0 -11
- package/packages/opencode-agent-skills-md/tsconfig.json +0 -10
- package/plans/001-ci-gate.md +0 -177
- package/plans/002-is-path-safe.md +0 -243
- package/plans/003-escape-prompts.md +0 -310
- package/plans/004-test-security-paths.md +0 -228
- package/plans/005-stop-swallowing-errors.md +0 -246
- package/plans/006-preserve-jsonc-commas.md +0 -144
- package/plans/007-write-before-purge.md +0 -144
- package/plans/008-reuse-walkdir-for-list-skill-files.md +0 -164
- package/plans/README.md +0 -43
- package/pnpm-workspace.yaml +0 -6
- package/tests/workspace.test.ts +0 -367
- package/tsconfig.json +0 -15
- /package/{packages/opencode-agent-skills-md/src → src}/cli/config.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/cli/install.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/cli/real-fs.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/cli/status.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/cli/uninstall.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/host.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/index.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/plugin.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/sdk.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/tools.ts +0 -0
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
# Plan 003: Escape XML and shell boundaries in skill tools
|
|
2
|
-
|
|
3
|
-
> **Executor instructions**: Follow this plan step by step. Run every
|
|
4
|
-
> verification command and confirm the expected result before moving to the
|
|
5
|
-
> next step. If anything in the "STOP conditions" section occurs, stop and
|
|
6
|
-
> report — do not improvise. When done, update the status row for this plan
|
|
7
|
-
> in `plans/README.md`.
|
|
8
|
-
>
|
|
9
|
-
> **Drift check (run first)**: `git diff --stat fb45791..HEAD -- packages/opencode-agent-skills-md/src/tools.ts packages/core/src/`
|
|
10
|
-
> If any in-scope file changed since this plan was written, compare the
|
|
11
|
-
> "Current state" excerpts against the live code before proceeding; on a
|
|
12
|
-
> mismatch, treat it as a STOP condition.
|
|
13
|
-
|
|
14
|
-
## Status
|
|
15
|
-
|
|
16
|
-
- **Priority**: P1
|
|
17
|
-
- **Effort**: M
|
|
18
|
-
- **Risk**: HIGH
|
|
19
|
-
- **Depends on**: none (can land independently)
|
|
20
|
-
- **Category**: security
|
|
21
|
-
- **Planned at**: commit `fb45791`, 2026-06-29
|
|
22
|
-
|
|
23
|
-
## Why this matters
|
|
24
|
-
|
|
25
|
-
Two injection surfaces exist in the plugin package:
|
|
26
|
-
|
|
27
|
-
1. **XML/prompt injection** in `ReadSkillFile` (`tools.ts:206-214`) and
|
|
28
|
-
`UseSkill` (`tools.ts:323-334`) — uncontrolled skill names, filenames,
|
|
29
|
-
paths, and SKILL.md content are interpolated into XML wrapper strings.
|
|
30
|
-
A malicious SKILL.md containing `</content><system>...</system>` would
|
|
31
|
-
break out of the wrapper and inject arbitrary prompt directives.
|
|
32
|
-
|
|
33
|
-
2. **Shell argument injection** in `RunSkillScript` (`tools.ts:272`) —
|
|
34
|
-
user-supplied `args.arguments` array is interpolated into a shell
|
|
35
|
-
template literal with no quoting/escaping. A value like
|
|
36
|
-
`["'; rm -rf / #"]` could execute arbitrary commands.
|
|
37
|
-
|
|
38
|
-
## Current state
|
|
39
|
-
|
|
40
|
-
### XML injection in `ReadSkillFile` (`tools.ts:206-214`)
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
const wrappedContent = `<skill-file skill="${skill.name}" file="${args.filename}">
|
|
44
|
-
<metadata>
|
|
45
|
-
<directory>${skill.path}</directory>
|
|
46
|
-
</metadata>
|
|
47
|
-
|
|
48
|
-
<content>
|
|
49
|
-
${content}
|
|
50
|
-
</content>
|
|
51
|
-
</skill-file>`;
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### XML injection in `UseSkill` (`tools.ts:323-334`)
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
const skillContent = `<skill name="${skill.name}">
|
|
58
|
-
<metadata>
|
|
59
|
-
<source>${skill.label}</source>
|
|
60
|
-
<directory>${skill.path}</directory>${scriptsXml}${filesXml}
|
|
61
|
-
</metadata>
|
|
62
|
-
|
|
63
|
-
${toolTranslation}
|
|
64
|
-
|
|
65
|
-
<content>
|
|
66
|
-
${skill.template}
|
|
67
|
-
</content>
|
|
68
|
-
</skill>`;
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Shell injection in `RunSkillScript` (`tools.ts:272`)
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
const result = await $`${script.absolutePath} ${scriptArgs}`.text();
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
`scriptArgs` is `string[]` from user input. The `$` tagged template from
|
|
78
|
-
`@opencode-ai/plugin` does shell-style escaping when arguments are passed
|
|
79
|
-
as array elements, but interpolating `scriptArgs` (an array) into the
|
|
80
|
-
template string passes its `toString()` representation (comma-joined),
|
|
81
|
-
not individual quoted arguments.
|
|
82
|
-
|
|
83
|
-
Conventions:
|
|
84
|
-
- The `debugLog` function from `opencode-agent-skills-md-core` is already
|
|
85
|
-
imported in `parse.ts` and available for logging.
|
|
86
|
-
- The `$` function is `PluginInput["$"]` — the OpenCode shell runner.
|
|
87
|
-
|
|
88
|
-
## Commands you will need
|
|
89
|
-
|
|
90
|
-
| Purpose | Command | Expected on success |
|
|
91
|
-
|-----------|------------------------------------|---------------------|
|
|
92
|
-
| Typecheck | `pnpm run typecheck` | exit 0, no errors |
|
|
93
|
-
| Plugin test | `pnpm -F opencode-agent-skills-md exec node --import tsx --test tests/tools.test.ts` | all pass |
|
|
94
|
-
|
|
95
|
-
## Scope
|
|
96
|
-
|
|
97
|
-
**In scope** (the only files you should modify):
|
|
98
|
-
- `packages/opencode-agent-skills-md/src/tools.ts` — XML escape + shell args fix
|
|
99
|
-
|
|
100
|
-
**Out of scope** (do NOT touch):
|
|
101
|
-
- `packages/core/` — no changes needed
|
|
102
|
-
- `packages/opencode-agent-skills-md/src/plugin.ts` — the superpowers bootstrap has a similar interpolation but is by-design for a trusted skill name
|
|
103
|
-
- `packages/opencode-agent-skills-md/src/host.ts` — no injection surface there
|
|
104
|
-
|
|
105
|
-
## Git workflow
|
|
106
|
-
|
|
107
|
-
- Branch: `advisor/003-escape-prompts`
|
|
108
|
-
- Commit per logical step; message style: conventional commits
|
|
109
|
-
- Do NOT push or open a PR unless instructed.
|
|
110
|
-
|
|
111
|
-
## Steps
|
|
112
|
-
|
|
113
|
-
### Step 1: Add XML escape helper
|
|
114
|
-
|
|
115
|
-
Add a small XML-escape function at the top of `tools.ts` (after the imports):
|
|
116
|
-
|
|
117
|
-
```ts
|
|
118
|
-
/** Escape XML special characters in attribute values and text content. */
|
|
119
|
-
function escapeXml(s: string): string {
|
|
120
|
-
return s
|
|
121
|
-
.replace(/&/g, "&")
|
|
122
|
-
.replace(/</g, "<")
|
|
123
|
-
.replace(/>/g, ">")
|
|
124
|
-
.replace(/"/g, """)
|
|
125
|
-
.replace(/'/g, "'");
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
This prevents breakout from XML attribute and text contexts.
|
|
130
|
-
|
|
131
|
-
**Verify**: `grep 'function escapeXml' packages/opencode-agent-skills-md/src/tools.ts` → matches
|
|
132
|
-
|
|
133
|
-
### Step 2: Escape XML in `ReadSkillFile`
|
|
134
|
-
|
|
135
|
-
Replace the raw interpolation with escaped values in `tools.ts:206-214`:
|
|
136
|
-
|
|
137
|
-
Old:
|
|
138
|
-
```ts
|
|
139
|
-
const wrappedContent = `<skill-file skill="${skill.name}" file="${args.filename}">
|
|
140
|
-
<metadata>
|
|
141
|
-
<directory>${skill.path}</directory>
|
|
142
|
-
</metadata>
|
|
143
|
-
|
|
144
|
-
<content>
|
|
145
|
-
${content}
|
|
146
|
-
</content>
|
|
147
|
-
</skill-file>`;
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
New:
|
|
151
|
-
```ts
|
|
152
|
-
const wrappedContent = `<skill-file skill="${escapeXml(skill.name)}" file="${escapeXml(args.filename)}">
|
|
153
|
-
<metadata>
|
|
154
|
-
<directory>${escapeXml(skill.path)}</directory>
|
|
155
|
-
</metadata>
|
|
156
|
-
|
|
157
|
-
<content>
|
|
158
|
-
${content}
|
|
159
|
-
</content>
|
|
160
|
-
</skill-file>`;
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Note: `skill.name`, `args.filename`, and `skill.path` are the only values we
|
|
164
|
-
control that should be escaped. The `content` variable is the raw file content
|
|
165
|
-
and is intentionally placed in a text context — its content will be part of
|
|
166
|
-
what the LLM reads, so no escaping needed.
|
|
167
|
-
|
|
168
|
-
**Verify**: `grep 'escapeXml(skill.name)' packages/opencode-agent-skills-md/src/tools.ts` → matches
|
|
169
|
-
|
|
170
|
-
### Step 3: Escape XML in `UseSkill`
|
|
171
|
-
|
|
172
|
-
Replace the raw interpolation with escaped values in `tools.ts:323-334`:
|
|
173
|
-
|
|
174
|
-
Old:
|
|
175
|
-
```ts
|
|
176
|
-
const skillContent = `<skill name="${skill.name}">
|
|
177
|
-
<metadata>
|
|
178
|
-
<source>${skill.label}</source>
|
|
179
|
-
<directory>${skill.path}</directory>${scriptsXml}${filesXml}
|
|
180
|
-
</metadata>
|
|
181
|
-
|
|
182
|
-
${toolTranslation}
|
|
183
|
-
|
|
184
|
-
<content>
|
|
185
|
-
${skill.template}
|
|
186
|
-
</content>
|
|
187
|
-
</skill>`;
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
New — escape `skill.name`, `skill.label`, `skill.path`:
|
|
191
|
-
```ts
|
|
192
|
-
const skillContent = `<skill name="${escapeXml(skill.name)}">
|
|
193
|
-
<metadata>
|
|
194
|
-
<source>${escapeXml(skill.label)}</source>
|
|
195
|
-
<directory>${escapeXml(skill.path)}</directory>${scriptsXml}${filesXml}
|
|
196
|
-
</metadata>
|
|
197
|
-
|
|
198
|
-
${toolTranslation}
|
|
199
|
-
|
|
200
|
-
<content>
|
|
201
|
-
${skill.template}
|
|
202
|
-
</content>
|
|
203
|
-
</skill>`;
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Note: `skill.template` is the actual skill content (the body of SKILL.md)
|
|
207
|
-
and should remain raw — it's what the skill is meant to inject.
|
|
208
|
-
|
|
209
|
-
**Verify**: `grep 'escapeXml(skill.label)' packages/opencode-agent-skills-md/src/tools.ts` → matches
|
|
210
|
-
|
|
211
|
-
### Step 4: Fix shell argument escaping in `RunSkillScript`
|
|
212
|
-
|
|
213
|
-
Replace the unsafe interpolation with individual quoted arguments in
|
|
214
|
-
`tools.ts:272`.
|
|
215
|
-
|
|
216
|
-
The `$` tagged template from `@opencode-ai/plugin` supports passing arguments
|
|
217
|
-
as separate template expressions. Instead of passing the array as a single
|
|
218
|
-
interpolation, pass each argument as its own expression:
|
|
219
|
-
|
|
220
|
-
Old:
|
|
221
|
-
```ts
|
|
222
|
-
$.cwd(skill.path);
|
|
223
|
-
const scriptArgs = args.arguments || [];
|
|
224
|
-
const result = await $`${script.absolutePath} ${scriptArgs}`.text();
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
New:
|
|
228
|
-
```ts
|
|
229
|
-
$.cwd(skill.path);
|
|
230
|
-
const scriptArgs = args.arguments || [];
|
|
231
|
-
const result = await $`${script.absolutePath}${scriptArgs.map(a => [' ', a]).flat()}`.text();
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Wait — this is tricky with the tagged template. The safest approach is to pass
|
|
235
|
-
the executable and arguments separately to the shell runner. Look at how the
|
|
236
|
-
plugin SDK's `$` handles multiple arguments — it typically accepts them as a
|
|
237
|
-
spread after the template:
|
|
238
|
-
|
|
239
|
-
If `$` is an execa-style tagged template, this should work:
|
|
240
|
-
```ts
|
|
241
|
-
const result = await $([script.absolutePath, ...scriptArgs]);
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
But if `$` only works as a template literal, then a different approach is needed.
|
|
245
|
-
|
|
246
|
-
The safest cross-approach fix is to shell-escape each argument individually and
|
|
247
|
-
join them:
|
|
248
|
-
|
|
249
|
-
```ts
|
|
250
|
-
// Shell-escape a single argument: wrap in single quotes, escape embedded single quotes
|
|
251
|
-
function escapeShellArg(arg: string): string {
|
|
252
|
-
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
$.cwd(skill.path);
|
|
256
|
-
const scriptArgs = (args.arguments || []).map(escapeShellArg).join(' ');
|
|
257
|
-
const result = await $`${script.absolutePath} ${scriptArgs}`.text();
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
This wraps each argument in single quotes (which prevent all shell metacharacter
|
|
261
|
-
interpretation) and escapes any embedded single quotes using the standard
|
|
262
|
-
`'\''` Bourne shell pattern.
|
|
263
|
-
|
|
264
|
-
**Verify**: `grep 'escapeShellArg' packages/opencode-agent-skills-md/src/tools.ts` → matches
|
|
265
|
-
|
|
266
|
-
### Step 5: Typecheck
|
|
267
|
-
|
|
268
|
-
**Verify**: `pnpm run typecheck` → exit 0, no errors
|
|
269
|
-
|
|
270
|
-
### Step 6: Run tests
|
|
271
|
-
|
|
272
|
-
**Verify**: `pnpm test` → exit 0, all tests pass
|
|
273
|
-
|
|
274
|
-
## Test plan
|
|
275
|
-
|
|
276
|
-
No new tests are needed for this plan — the existing tests must pass.
|
|
277
|
-
Test coverage for these surfaces is added in Plan 004 (`test-security-paths`).
|
|
278
|
-
Verify that existing tests still pass after the changes.
|
|
279
|
-
|
|
280
|
-
## Done criteria
|
|
281
|
-
|
|
282
|
-
Machine-checkable. ALL must hold:
|
|
283
|
-
|
|
284
|
-
- [ ] `escapeXml` function exists in `tools.ts` and is used for `skill.name`, `args.filename`, `skill.path`, `skill.label`
|
|
285
|
-
- [ ] `escapeShellArg` function exists in `tools.ts` and is used for each element of `args.arguments`
|
|
286
|
-
- [ ] `pnpm run typecheck` exits 0
|
|
287
|
-
- [ ] `pnpm test` exits 0
|
|
288
|
-
- [ ] No files outside the in-scope list are modified (`git status`)
|
|
289
|
-
- [ ] `plans/README.md` status row updated
|
|
290
|
-
|
|
291
|
-
## STOP conditions
|
|
292
|
-
|
|
293
|
-
Stop and report back (do not improvise) if:
|
|
294
|
-
|
|
295
|
-
- The `$` tagged template behaves differently than expected — test with a
|
|
296
|
-
simple experiment if unsure. The escapeShellArg approach is a safe fallback
|
|
297
|
-
that works with any shell-like execution.
|
|
298
|
-
- A step's verification fails twice after a reasonable fix attempt.
|
|
299
|
-
- The fix requires touching an out-of-scope file.
|
|
300
|
-
- The XML escape breaks any test that relied on specific unescaped output in
|
|
301
|
-
the injected content.
|
|
302
|
-
|
|
303
|
-
## Maintenance notes
|
|
304
|
-
|
|
305
|
-
- If a future version of `@opencode-ai/plugin` provides native argument
|
|
306
|
-
handling in `$`, replace the `escapeShellArg` approach with the SDK-native one.
|
|
307
|
-
- The `escapeXml` function only handles the 5 XML predefined entities. If
|
|
308
|
-
non-ASCII content or CDATA sections are ever part of the XML values, extend it.
|
|
309
|
-
- The `skill.template` and `content` variables are intentionally not escaped —
|
|
310
|
-
they represent the actual skill content the user wants injected.
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
# Plan 004: Add characterization tests for security-sensitive paths
|
|
2
|
-
|
|
3
|
-
> **Executor instructions**: Follow this plan step by step. Run every
|
|
4
|
-
> verification command and confirm the expected result before moving to the
|
|
5
|
-
> next step. If anything in the "STOP conditions" section occurs, stop and
|
|
6
|
-
> report — do not improvise. When done, update the status row for this plan
|
|
7
|
-
> in `plans/README.md`.
|
|
8
|
-
>
|
|
9
|
-
> **Drift check (run first)**: `git diff --stat fb45791..HEAD -- packages/opencode-agent-skills-md/tests/ packages/opencode-agent-skills-md/src/tools.ts`
|
|
10
|
-
> If any in-scope file changed since this plan was written, compare the
|
|
11
|
-
> "Current state" excerpts against the live code before proceeding; on a
|
|
12
|
-
> mismatch, treat it as a STOP condition.
|
|
13
|
-
|
|
14
|
-
## Status
|
|
15
|
-
|
|
16
|
-
- **Priority**: P1
|
|
17
|
-
- **Effort**: M
|
|
18
|
-
- **Risk**: LOW
|
|
19
|
-
- **Depends on**: plans/002-is-path-safe.md, plans/003-escape-prompts.md
|
|
20
|
-
- **Category**: tests
|
|
21
|
-
- **Planned at**: commit `fb45791`, 2026-06-29
|
|
22
|
-
|
|
23
|
-
## Why this matters
|
|
24
|
-
|
|
25
|
-
The three most security-sensitive code paths in the plugin (path safety, shell
|
|
26
|
-
argument escaping, and XML wrapper integrity) have zero dedicated test coverage.
|
|
27
|
-
If a regression is introduced — for example, `isPathSafe` stops checking
|
|
28
|
-
symlinks, or the XML escaping is accidentally removed — there is no test to
|
|
29
|
-
catch it. This plan adds targeted tests for each of these surfaces.
|
|
30
|
-
|
|
31
|
-
## Current state
|
|
32
|
-
|
|
33
|
-
After Plans 002 and 003, the following protections are in place:
|
|
34
|
-
|
|
35
|
-
- `packages/core/src/scripts.ts` — `isPathSafe` is async and uses `fs.realpath`
|
|
36
|
-
- `packages/opencode-agent-skills-md/src/tools.ts` — `escapeXml()` is applied to
|
|
37
|
-
XML attribute/text values, `escapeShellArg()` is applied to script arguments
|
|
38
|
-
|
|
39
|
-
Test conventions (see `packages/core/tests/agnostic.test.ts` and
|
|
40
|
-
`packages/opencode-agent-skills-md/tests/package-boundary.test.ts`):
|
|
41
|
-
- Tests use Node's built-in test runner: `import { describe, test, mock, before, after } from "node:test"`
|
|
42
|
-
- Assertions use `import assert from "node:assert/strict"`
|
|
43
|
-
- Plugin tests import from `packages/opencode-agent-skills-md/src/`
|
|
44
|
-
- Core tests import from `packages/core/src/`
|
|
45
|
-
- HTTP/mock patterns: `import { mock } from "node:test"` and `mock.method()`
|
|
46
|
-
- Temp directories use `mkdtemp` + cleanup in `after` blocks
|
|
47
|
-
|
|
48
|
-
## Commands you will need
|
|
49
|
-
|
|
50
|
-
| Purpose | Command | Expected on success |
|
|
51
|
-
|-------------|------------------------------------|---------------------|
|
|
52
|
-
| Typecheck | `pnpm run typecheck` | exit 0, no errors |
|
|
53
|
-
| Core test | `pnpm -F opencode-agent-skills-md-core exec node --import tsx --test tests/scripts.test.ts` | all pass |
|
|
54
|
-
| Plugin test | `pnpm -F opencode-agent-skills-md exec node --import tsx --test tests/tools-security.test.ts` | all pass |
|
|
55
|
-
|
|
56
|
-
## Scope
|
|
57
|
-
|
|
58
|
-
**In scope** (the only files you should modify):
|
|
59
|
-
- `packages/core/tests/scripts.test.ts` — add more cases if the file exists, or note that Plan 002 already covers it
|
|
60
|
-
- `packages/opencode-agent-skills-md/tests/tools-security.test.ts` — create
|
|
61
|
-
|
|
62
|
-
**Out of scope** (do NOT touch):
|
|
63
|
-
- Any source files in `packages/core/src/` or `packages/opencode-agent-skills-md/src/`
|
|
64
|
-
- Any existing test files other than the two listed above
|
|
65
|
-
- Any CI configuration changes
|
|
66
|
-
|
|
67
|
-
## Git workflow
|
|
68
|
-
|
|
69
|
-
- Branch: `advisor/004-test-security-paths`
|
|
70
|
-
- Commit per logical step (or single commit for all new tests)
|
|
71
|
-
- Do NOT push or open a PR unless instructed.
|
|
72
|
-
|
|
73
|
-
## Steps
|
|
74
|
-
|
|
75
|
-
### Step 1: Verify Plan 002 is applied
|
|
76
|
-
|
|
77
|
-
Check that `isPathSafe` is async and uses `fs.realpath`.
|
|
78
|
-
|
|
79
|
-
**Verify**: `grep 'fs.realpath' packages/core/src/scripts.ts` → matches
|
|
80
|
-
If not found, stop and report that the dependency Plan 002 has not been applied.
|
|
81
|
-
|
|
82
|
-
### Step 2: Verify Plan 003 is applied
|
|
83
|
-
|
|
84
|
-
Check that `escapeXml` and `escapeShellArg` exist in `tools.ts`.
|
|
85
|
-
|
|
86
|
-
**Verify**:
|
|
87
|
-
- `grep 'function escapeXml' packages/opencode-agent-skills-md/src/tools.ts` → matches
|
|
88
|
-
- `grep 'function escapeShellArg' packages/opencode-agent-skills-md/src/tools.ts` → matches
|
|
89
|
-
If not found, stop and report that the dependency Plan 003 has not been applied.
|
|
90
|
-
|
|
91
|
-
### Step 3: Create `tools-security.test.ts`
|
|
92
|
-
|
|
93
|
-
Create `packages/opencode-agent-skills-md/tests/tools-security.test.ts`:
|
|
94
|
-
|
|
95
|
-
This test file targets the three security-sensitive surfaces in the plugin.
|
|
96
|
-
Since these functions are not exported from `tools.ts` (they're module-private),
|
|
97
|
-
the tests should test the BEHAVIOR of the tools, not the helpers directly.
|
|
98
|
-
Alternatively, the helpers can be tested by injecting known-bad inputs through
|
|
99
|
-
the tool interface.
|
|
100
|
-
|
|
101
|
-
However, since these functions are internal to `tools.ts`, the cleanest
|
|
102
|
-
approach is:
|
|
103
|
-
|
|
104
|
-
1. Test `escapeXml` and `escapeShellArg` through the public tool API by
|
|
105
|
-
creating a minimal mock host + `$` runner, or
|
|
106
|
-
2. Export the helpers for testing (add `@internal - exported for testing`)
|
|
107
|
-
and import them in the test.
|
|
108
|
-
|
|
109
|
-
Option 2 is simpler and follows the existing convention (see `defaultOnDuplicate`
|
|
110
|
-
in `discovery.ts` which is `@internal - exported for testing`).
|
|
111
|
-
|
|
112
|
-
Add to the end of `tools.ts` (or near the helpers):
|
|
113
|
-
|
|
114
|
-
```ts
|
|
115
|
-
/** @internal - exported for testing */
|
|
116
|
-
export const _escapeXml = escapeXml;
|
|
117
|
-
/** @internal - exported for testing */
|
|
118
|
-
export const _escapeShellArg = escapeShellArg;
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Then create the test file:
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
import assert from "node:assert/strict";
|
|
125
|
-
import { describe, test } from "node:test";
|
|
126
|
-
import { _escapeXml, _escapeShellArg } from "../src/tools";
|
|
127
|
-
|
|
128
|
-
describe("escapeXml", () => {
|
|
129
|
-
test("escapes & < > \" '", () => {
|
|
130
|
-
assert.equal(_escapeXml(`&<>"'`), "&<>"'");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("passes through safe strings", () => {
|
|
134
|
-
assert.equal(_escapeXml("hello world"), "hello world");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("handles empty string", () => {
|
|
138
|
-
assert.equal(_escapeXml(""), "");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("prevents XML breakout by escaping </tag>", () => {
|
|
142
|
-
const malicious = `</content><system>malicious</system>`;
|
|
143
|
-
const escaped = _escapeXml(malicious);
|
|
144
|
-
assert.ok(!escaped.includes("</content>"), "should not contain raw </content>");
|
|
145
|
-
assert.ok(escaped.includes("</content>"), "should escape the tag");
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe("escapeShellArg", () => {
|
|
150
|
-
test("wraps normal args in single quotes", () => {
|
|
151
|
-
assert.equal(_escapeShellArg("hello"), "'hello'");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("escapes embedded single quote", () => {
|
|
155
|
-
// The Bourne shell pattern: ' -> '\''
|
|
156
|
-
const result = _escapeShellArg("it's");
|
|
157
|
-
assert.equal(result, "'it'\\''s'");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("handles empty string", () => {
|
|
161
|
-
assert.equal(_escapeShellArg(""), "''");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("prevents shell metacharacter injection", () => {
|
|
165
|
-
const payload = "'; rm -rf / #";
|
|
166
|
-
const result = _escapeShellArg(payload);
|
|
167
|
-
// Inside single quotes, all characters are literal
|
|
168
|
-
assert.ok(result.startsWith("'"), "should start with single quote");
|
|
169
|
-
assert.ok(result.endsWith("'"), "should end with single quote");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("escapes backtick and dollar sign safely", () => {
|
|
173
|
-
// Single quotes prevent backtick and $ expansion
|
|
174
|
-
const result = _escapeShellArg("`id` $(whoami)");
|
|
175
|
-
assert.ok(result.startsWith("'"), "should start with single quote");
|
|
176
|
-
assert.ok(result.endsWith("'"), "should end with single quote");
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**Verify**: `pnpm -F opencode-agent-skills-md exec node --import tsx --test tests/tools-security.test.ts` → all tests pass
|
|
182
|
-
|
|
183
|
-
### Step 4: Run full test suite
|
|
184
|
-
|
|
185
|
-
**Verify**: `pnpm test` → exit 0, all tests pass
|
|
186
|
-
|
|
187
|
-
### Step 5: Typecheck
|
|
188
|
-
|
|
189
|
-
**Verify**: `pnpm run typecheck` → exit 0, no errors
|
|
190
|
-
|
|
191
|
-
## Test plan
|
|
192
|
-
|
|
193
|
-
- **New file**: `packages/opencode-agent-skills-md/tests/tools-security.test.ts`
|
|
194
|
-
- Tests for `escapeXml`: 4 cases (basic entities, safe strings, empty, XML breakout)
|
|
195
|
-
- Tests for `escapeShellArg`: 5 cases (normal quoting, embedded quote, empty, injection prevention, special chars)
|
|
196
|
-
- Model the test structure after `packages/core/tests/agnostic.test.ts`
|
|
197
|
-
|
|
198
|
-
Note: Plan 002 already adds tests for `isPathSafe` in `packages/core/tests/scripts.test.ts`. Verify those exist.
|
|
199
|
-
|
|
200
|
-
## Done criteria
|
|
201
|
-
|
|
202
|
-
Machine-checkable. ALL must hold:
|
|
203
|
-
|
|
204
|
-
- [ ] `packages/opencode-agent-skills-md/tests/tools-security.test.ts` exists with tests for `escapeXml` and `escapeShellArg`
|
|
205
|
-
- [ ] The helpers are exported under `@internal` names for testing
|
|
206
|
-
- [ ] `pnpm -F opencode-agent-skills-md exec node --import tsx --test tests/tools-security.test.ts` passes all tests
|
|
207
|
-
- [ ] `pnpm run typecheck` exits 0
|
|
208
|
-
- [ ] `pnpm test` exits 0
|
|
209
|
-
- [ ] No files outside the in-scope list are modified (`git status`)
|
|
210
|
-
- [ ] `plans/README.md` status row updated
|
|
211
|
-
|
|
212
|
-
## STOP conditions
|
|
213
|
-
|
|
214
|
-
Stop and report back (do not improvise) if:
|
|
215
|
-
|
|
216
|
-
- Plan 002 or 003 has not been applied (their helpers don't exist in source).
|
|
217
|
-
- The `@internal` export convention conflicts with any build-time tree-shaking that drops internal exports.
|
|
218
|
-
- A step's verification fails twice after a reasonable fix attempt.
|
|
219
|
-
- The fix appears to require touching an out-of-scope file.
|
|
220
|
-
|
|
221
|
-
## Maintenance notes
|
|
222
|
-
|
|
223
|
-
- If the `escapeXml` or `escapeShellArg` functions are renamed or moved in the
|
|
224
|
-
future, update the `_`-prefixed re-exports and the test imports.
|
|
225
|
-
- If the functions are ever moved to core, move the tests alongside them.
|
|
226
|
-
- The `@internal` JSDoc tag is a convention only — it signals intent but doesn't
|
|
227
|
-
prevent external use. Consider TypeScript `@internal` if stronger enforcement
|
|
228
|
-
is desired.
|