Skip to content

Commit b70c0e1

Browse files
oratisclaude
andauthored
feat(core,cli): M5 — plugins manifest + hash pin + Skill tool + CLI integration (#6)
What ships ---------- - plugins/manifest.ts (175 lines) · PluginManifest schema + readManifest() · SHA-256 source hash (manifest + all SKILL.md files) via computeSourceHash() · Trust state at ~/.deepcode/plugins-trust.json · installLocal() — copy + record trust + hash · discoverPlugins() — scan + verify hashes + return enabled list · Hash drift detection flags tampered plugins · disabled list honored - skills/tool.ts (60 lines) · makeSkillTool(skills) factory — returns ToolHandler for "Skill" · Agent invokes by qualifiedName; returns body as tool_result · Supports plugin-prefixed names (plugin-x:do-thing) · Helpful error listing known skills when lookup fails - apps/cli/src/repl.ts (+50 lines) · Loads memory (DEEPCODE.md hierarchy + AGENTS.md + rules/) via loadMemory() · Loads skills via loadSkills() with skillOverrides settings respected · Loads output styles, applies active style to system prompt · Registers Skill tool when any skills loaded · Builds composite system prompt: default + memory + skills block + style · Wires mode + permissions + hooks + approval into runAgent() · Approval prompts user [y]es/[n]o via readline when verdict is 'ask' DELIBERATELY DEFERRED to M5.1 (security gate) --------------------------------------------- Plugin code does NOT yet execute in the host process. discoverPlugins() finds them and hash-verifies them; their contributed skills/agents/hooks/ MCP servers are NOT registered into live registries until sandbox subprocess lands (per docs/design/plugin-security.md §3.5). Running arbitrary plugin code in-process is the primary RCE vector enumerated in the security doc. M5 ships the trust foundation; M5.1 ships the safe execution boundary. Tests ----- - plugins/manifest.test.ts (12) — manifest validation, hash determinism + sensitivity, trust round-trip, install, discover + drift, disabled, untrusted - skills/tool.test.ts ( 6) — tool shape, lookup, args, plugin names, missing skill, missing arg Total: 258 passed / 4 skipped / 0 failed (was 240). Verified -------- pnpm typecheck → green pnpm build → green pnpm test → 258 passed pnpm format:check → conformant CLI bin: --version, --help, doctor all work Docs ---- - docs/milestones/M5.md — what shipped, what M5.1 needs, why the deferral Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 46208ec commit b70c0e1

10 files changed

Lines changed: 719 additions & 17 deletions

File tree

apps/cli/src/repl.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,22 @@ import {
55
CredentialsStore,
66
DeepSeekProvider,
77
EFFORT_PARAMS,
8+
HookDispatcher,
89
SessionManager,
910
ToolRegistry,
11+
applyStyle,
12+
buildSkillsDescriptionBlock,
13+
findStyle,
14+
loadMemory,
15+
loadOutputStyles,
1016
loadSettings,
17+
loadSkills,
18+
makeSkillTool,
1119
resolveCredentials,
1220
runAgent,
1321
type DeepCodeSettings,
1422
type Effort,
23+
type Mode,
1524
type AgentEvent,
1625
type StoredMessage,
1726
} from '@deepcode/core';
@@ -55,7 +64,7 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
5564
}
5665

5766
const model = opts.model ?? settings.model ?? 'deepseek-chat';
58-
const mode = opts.mode ?? settings.permissions?.defaultMode ?? 'default';
67+
const mode = (opts.mode ?? settings.permissions?.defaultMode ?? 'default') as Mode;
5968
const effort = opts.effort ?? settings.effortLevel ?? 'medium';
6069
const { maxTokens, temperature } = EFFORT_PARAMS[effort as Effort] ?? EFFORT_PARAMS.medium;
6170

@@ -70,6 +79,38 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
7079
const tools = new ToolRegistry();
7180
const commands = new CommandRegistry();
7281

82+
// M5: load memory, skills, output style — assemble final system prompt
83+
const memory = await loadMemory({
84+
cwd,
85+
home: opts.home,
86+
maxBytes: (settings.memoryLoadCapKB ?? 100) * 1024,
87+
});
88+
const skills = await loadSkills({
89+
cwd,
90+
home: opts.home,
91+
overrides: settings.skillOverrides,
92+
});
93+
const styles = await loadOutputStyles({ cwd, home: opts.home });
94+
const activeStyle = findStyle(styles, settings.outputStyle ?? 'default');
95+
96+
// Register Skill tool (M5)
97+
if (skills.length > 0) {
98+
tools.register(makeSkillTool(skills));
99+
}
100+
101+
// Build the composite system prompt
102+
let systemPrompt = DEFAULT_SYSTEM_PROMPT;
103+
if (memory.text) systemPrompt += '\n\n' + memory.text;
104+
const skillsBlock = buildSkillsDescriptionBlock(skills);
105+
if (skillsBlock) systemPrompt += '\n\n' + skillsBlock;
106+
systemPrompt = applyStyle(systemPrompt, activeStyle);
107+
108+
// Hook dispatcher (M3)
109+
const hooks = new HookDispatcher({
110+
hooks: settings.hooks,
111+
disableAllHooks: settings.disableAllHooks,
112+
});
113+
73114
let history: StoredMessage[] = [];
74115
const ctx: SessionContext = {
75116
cwd,
@@ -128,18 +169,26 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
128169
continue;
129170
}
130171

131-
// Otherwise: send to agent
172+
// Otherwise: send to agent (with mode/permission/hooks gating from M3b)
132173
const result = await runAgent({
133174
provider,
134175
tools,
135-
systemPrompt: DEFAULT_SYSTEM_PROMPT,
176+
systemPrompt,
136177
userMessage: userInput,
137178
history,
138179
model: ctx.model,
139180
maxTokens,
140181
temperature,
141182
cwd: ctx.cwd,
142183
session: { manager: sessions, id: session.id },
184+
mode: ctx.mode as Mode,
185+
permissions: settings.permissions,
186+
hooks,
187+
approval: async (toolName, _input, verdict) => {
188+
output.write(`\n ⏸ Approve ${toolName}? Reason: ${verdict.reason}\n`);
189+
const answer = (await rl.question(' [y]es / [n]o: ')).trim().toLowerCase();
190+
return answer === 'y' || answer === 'yes';
191+
},
143192
onEvent: (e: AgentEvent) => formatEvent(output, e),
144193
});
145194
history = result.history;

docs/milestones/M4.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,33 @@
55
66
## Shipped
77

8-
| Module | Purpose | Tests |
9-
|---|---|---|
10-
| `skills/frontmatter.ts` | Zero-dep YAML frontmatter parser (strings/numbers/bools/flow + block arrays/objects) | 10 |
11-
| `skills/loader.ts` | 4-layer loader (builtin / user / project / plugin) + `buildSkillsDescriptionBlock()` for system-prompt injection | 9 |
12-
| `sub-agents/loader.ts` | `.deepcode/agents/*.md``SubAgent` objects with isolation / tools / model / maxTurns | 6 |
13-
| `output-styles/loader.ts` | 4 built-in styles (default / explanatory / learning / proactive) + user/project overrides + `applyStyle()` | 9 |
14-
| Top-level re-exports | All new types/functions exposed from `@deepcode/core` ||
8+
| Module | Purpose | Tests |
9+
| ------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----- |
10+
| `skills/frontmatter.ts` | Zero-dep YAML frontmatter parser (strings/numbers/bools/flow + block arrays/objects) | 10 |
11+
| `skills/loader.ts` | 4-layer loader (builtin / user / project / plugin) + `buildSkillsDescriptionBlock()` for system-prompt injection | 9 |
12+
| `sub-agents/loader.ts` | `.deepcode/agents/*.md``SubAgent` objects with isolation / tools / model / maxTurns | 6 |
13+
| `output-styles/loader.ts` | 4 built-in styles (default / explanatory / learning / proactive) + user/project overrides + `applyStyle()` | 9 |
14+
| Top-level re-exports | All new types/functions exposed from `@deepcode/core` | |
1515

1616
**Total new tests**: 34. Across whole project: 240 passing / 4 skipped / 0 failed.
1717

1818
## What's in each subsystem
1919

2020
**Skills** (`SKILL.md` files in `<root>/<name>/SKILL.md`):
21+
2122
- Frontmatter spec: `name`, `description`, `allowed-tools`, `model`, `effort`, `shell`, `hooks`, `disabled`
2223
- Qualified names: bare for user/project, `<plugin>:<name>` for plugin-shipped
2324
- `disabled: true` in frontmatter OR `skillOverrides[name].disabled = true` in settings → skip load
2425
- `buildSkillsDescriptionBlock()` produces the system-prompt fragment that lists available skills (name + description only — body is loaded on Skill-tool invocation)
2526

2627
**Sub-agents** (`.deepcode/agents/<name>.md`):
28+
2729
- Frontmatter: `name`, `description`, `tools[]`, `model`, `isolation`, `maxTurns`
2830
- CLI `--agents <dir>` flag honored via `projectDirOverride` option
2931
- `findSubAgent(agents, name)` lookup helper
3032

3133
**Output styles** (`<root>/.deepcode/output-styles/<name>.md`):
34+
3235
- 4 built-in: `default`, `explanatory`, `learning`, `proactive`
3336
- Frontmatter: `name`, `description`, `keep-coding-instructions`
3437
- User → project layer order with replace semantics

docs/milestones/M5.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# M5 — Plugins (manifest + hash pin) + Skill tool + CLI integration
2+
3+
> **Status**: ✅ Foundation shipped — sandbox subprocess deferred to M5.1
4+
> **Branch**: `feat/m5-plugins-skill-tool-integration`
5+
6+
## Shipped
7+
8+
| Module | Lines | Tests |
9+
|---|---|---|
10+
| `plugins/manifest.ts` | Manifest parser, SHA-256 hash pinning, trust state JSON, installLocal(), discoverPlugins() with hash drift detection | 175 | 12 |
11+
| `skills/tool.ts` | `Skill` ToolHandler factory — agent invokes by qualified name, skill body returned as tool_result | 60 | 6 |
12+
| `apps/cli/src/repl.ts` | Wires memory + skills + output styles + mode/permissions/hooks/approval into the REPL agent loop | +50 | (smoke) |
13+
| **subtotal** | **~285** | **18** |
14+
15+
Across whole project: 258 tests / 4 skipped / 0 failed (was 240).
16+
17+
## What the CLI REPL now does end-to-end
18+
19+
When user types a message, agent receives:
20+
1. **System prompt** = default + memory (DEEPCODE.md + ~/.deepcode + AGENTS.md + rules/) + skills description block + output style append
21+
2. **Tools available** = 6 P0 + `Skill` tool (if any skills loaded)
22+
3. **Per tool call** = goes through `dispatchToolCall()`:
23+
- Mode policy (`plan` blocks writes, `dontAsk` rejects ask, etc.)
24+
- Permission rules (allow/ask/deny patterns)
25+
- PreToolUse hook chain (JSON output can override)
26+
4. **`ask` verdict** → REPL prompts user `[y]es/[n]o`
27+
5. **PostToolUse hook** fires after every tool execution
28+
6. **Snapshots** captured pre/post Edit/Write for future rewind
29+
30+
## What's NOT in M5
31+
32+
Per `docs/design/plugin-security.md` we have a deliberate gap:
33+
34+
> **Plugin sandbox subprocess (RPC over stdio) — M5.1.**
35+
> Right now `discoverPlugins()` finds installed plugins but the agent loop does
36+
> NOT yet *run* their contributed code in-process. They're discovered, their
37+
> manifest is verified, but their JS/skills/hooks/MCP servers aren't yet
38+
> registered into the active registries. That wire-up needs the sandbox
39+
> subprocess design from `plugin-security.md` §3.5 to land first — running
40+
> arbitrary plugin code in the host process is the exact RCE vector the design
41+
> doc enumerated as A1 / A3.
42+
43+
What works **today** safely:
44+
- Local install: `installLocal({ sourcePath })` copies + records trust + hashes
45+
- Discovery on startup: `discoverPlugins()` finds plugins, flags hash drift, returns enabled list
46+
- Trust manifest at `~/.deepcode/plugins-trust.json` tracks what was installed
47+
- Hash-pinning catches tampered plugins
48+
49+
What's deferred to M5.1:
50+
- Subprocess sandbox via bwrap/sandbox-exec (depends on §3.9a sandbox subsystem — M3.5)
51+
- RPC stdio bridge between host and plugin subprocess
52+
- GitHub URL install (`gh:user/repo`)
53+
- Marketplace index + ed25519 signature verification
54+
- Revoke list pull
55+
- Loading plugin-bundled skills/agents/hooks into the active registry
56+
57+
## Skill tool
58+
59+
`makeSkillTool(skills)` returns a `ToolHandler` that:
60+
- Looks up skill by `qualifiedName` (e.g. `code-review` or `plugin-x:do-thing`)
61+
- Returns the SKILL.md body as tool_result
62+
- Lets the LLM "decide to invoke" via natural tool calling
63+
- Errors clearly when skill not found (lists known skills)
64+
65+
Auto-trigger via description matching is implicit — by including `buildSkillsDescriptionBlock(skills)` in the system prompt, the model sees `## Available skills - **code-review** — Review diff for bugs.` and tool-calls Skill when the user asks.
66+
67+
## Tests added
68+
69+
- `plugins/manifest.test.ts` — 12 tests covering: manifest validation, hash determinism, hash sensitivity (manifest + SKILL.md changes), trust round-trip, install, discovery, drift detection, disabled list, untrusted skip
70+
- `skills/tool.test.ts` — 6 tests covering: tool shape, known skill lookup, args appending, plugin-qualified names, missing skill, missing arg
71+
72+
## Verified
73+
74+
```
75+
pnpm typecheck → green
76+
pnpm build → green
77+
pnpm test → 258 passed / 4 skipped / 0 failed
78+
pnpm format:check → conformant
79+
```
80+
81+
CLI smoke: `node apps/cli/dist/cli.js --version``0.1.0`. Full REPL run not validated end-to-end (would need a live DEEPSEEK_API_KEY); the wiring is type-checked and the unit tests for each piece pass.
82+
83+
## Why deferred to M5.1 is the right call
84+
85+
`docs/design/plugin-security.md` was explicit that running plugins in the host process is the **primary** RCE vector. M5 ships the trust/hash machinery as a foundation, but explicitly **does not** wire plugin code into the live agent — because doing so without sandbox is the headline security mistake we warned ourselves about. The honest M5 is: discover and verify, don't execute.
86+
87+
The user can still benefit from skills (file-based, no code) — those work via the M4 user/project layers — they just don't yet auto-load from installed plugins.

packages/core/src/index.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,37 @@ export { dispatchToolCall, type DispatchRequest, type DispatchVerdict } from './
123123
// Agent loop's approval callback type (M3b)
124124
export type { ApprovalCallback } from './agent.js';
125125

126-
// Skills (M4 — SKILL.md frontmatter loading + system-prompt builder)
126+
// Skills (M4 — SKILL.md frontmatter loading + system-prompt builder; M5 — Skill tool)
127127
export {
128128
loadSkills,
129129
buildSkillsDescriptionBlock,
130130
parseFrontmatter,
131131
parseSimpleYaml,
132+
makeSkillTool,
132133
type Skill,
133134
type SkillFrontmatter,
134135
type LoadSkillsOpts,
135136
type Frontmatter,
136137
} from './skills/index.js';
137138

139+
// Plugins (M5 — manifest + hash pinning + local install + discovery)
140+
export {
141+
installLocal,
142+
discoverPlugins,
143+
readManifest,
144+
computeSourceHash,
145+
loadTrustState,
146+
saveTrustState,
147+
pluginsDir,
148+
trustFilePath,
149+
type PluginManifest,
150+
type InstalledPlugin,
151+
type PluginTrust,
152+
type TrustState,
153+
type InstallOptions,
154+
type DiscoverOptions,
155+
} from './plugins/index.js';
156+
138157
// Sub-agents (M4 — .deepcode/agents/*.md)
139158
export {
140159
loadSubAgents,

packages/core/src/plugins/index.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
1-
// Module: plugins
1+
// Plugins subsystem entry — manifest parsing, hash pinning, local install, discovery.
2+
// Spec: docs/DEVELOPMENT_PLAN.md §3.14
23
// Milestone: M5
3-
// Spec: docs/DEVELOPMENT_PLAN.md §3.14 plugin sandbox sub-process + RPC + hash pin + marketplace (see docs/design/plugin-security.md)
4-
// Status: placeholder — implemented in M5
4+
//
5+
// What's IN this milestone:
6+
// - plugin.json manifest parsing
7+
// - SHA-256 source hash + ~/.deepcode/plugins-trust.json
8+
// - installLocal() — copy a directory + record trust
9+
// - discoverPlugins() — scan ~/.deepcode/plugins/ + verify hashes
10+
//
11+
// What's NOT in this milestone (see docs/design/plugin-security.md):
12+
// - Sandbox subprocess execution (RPC over stdio)
13+
// - GitHub URL install (gh:user/repo)
14+
// - Marketplace index + ed25519 signature verification
15+
// - Revoke list pull + enforcement
16+
// - "Trust ladder" UI tiers
17+
//
18+
// IMPORTANT: until subprocess sandbox lands (planned M5.1), plugins are
19+
// effectively untrusted code with full host access. The trust system records
20+
// what the user *thought* they were installing, but cannot enforce it.
21+
// Treat M5 as a foundation, not a security boundary.
522

6-
export {};
23+
export {
24+
installLocal,
25+
discoverPlugins,
26+
readManifest,
27+
computeSourceHash,
28+
loadTrustState,
29+
saveTrustState,
30+
pluginsDir,
31+
trustFilePath,
32+
type PluginManifest,
33+
type InstalledPlugin,
34+
type PluginTrust,
35+
type TrustState,
36+
type InstallOptions,
37+
type DiscoverOptions,
38+
} from './manifest.js';

0 commit comments

Comments
 (0)