@jslee124/forge 0.3.0 → 0.3.2
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/index.js +1189 -90
- package/package.json +2 -1
- package/resources/docs/en/ARCHITECTURE.md +519 -0
- package/resources/docs/en/AUTHENTICATION.md +224 -0
- package/resources/docs/en/CLI_UI.md +266 -0
- package/resources/docs/en/CONFIGURATION.md +263 -0
- package/resources/docs/en/CONTEXT_MANAGEMENT.md +692 -0
- package/resources/docs/en/GETTING_STARTED.md +241 -0
- package/resources/docs/en/PLUGINS.md +622 -0
- package/resources/docs/en/PRODUCT.md +157 -0
- package/resources/docs/en/PROJECT_CONTEXT.md +225 -0
- package/resources/docs/en/RELEASING.md +94 -0
- package/resources/docs/en/SECURITY.md +272 -0
- package/resources/docs/en/SESSIONS.md +134 -0
- package/resources/docs/en/TROUBLESHOOTING.md +256 -0
- package/resources/docs/index.json +24334 -0
- package/resources/docs/zh-CN/ARCHITECTURE.md +174 -0
- package/resources/docs/zh-CN/AUTHENTICATION.md +96 -0
- package/resources/docs/zh-CN/CLI_UI.md +112 -0
- package/resources/docs/zh-CN/CONFIGURATION.md +221 -0
- package/resources/docs/zh-CN/CONTEXT_MANAGEMENT.md +200 -0
- package/resources/docs/zh-CN/GETTING_STARTED.md +193 -0
- package/resources/docs/zh-CN/PLUGINS.md +286 -0
- package/resources/docs/zh-CN/PRODUCT.md +86 -0
- package/resources/docs/zh-CN/PROJECT_CONTEXT.md +130 -0
- package/resources/docs/zh-CN/RELEASING.md +86 -0
- package/resources/docs/zh-CN/SECURITY.md +92 -0
- package/resources/docs/zh-CN/SESSIONS.md +69 -0
- package/resources/docs/zh-CN/TROUBLESHOOTING.md +185 -0
- package/resources/skills/forge-plugin-creator/SKILL.md +70 -0
- package/resources/skills/forge-plugin-creator/references/plugin-api.md +36 -0
- package/resources/skills/forge-plugin-creator/templates/index.mjs +30 -0
- package/resources/skills/forge-plugin-creator/templates/plugin.json +8 -0
- package/resources/skills/forge-plugin-creator/templates/plugin.test-template.ts +14 -0
- package/resources/skills/forge-product-help/SKILL.md +16 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
> Forge can create plugins. Ask the connected model to build one for your use
|
|
2
|
+
> case and point it to this document.
|
|
3
|
+
|
|
4
|
+
# Plugin authoring guide
|
|
5
|
+
|
|
6
|
+
简体中文 · Documentation index
|
|
7
|
+
|
|
8
|
+
Forge 0.3.2 works without plugins. A plugin is an optional in-process JavaScript
|
|
9
|
+
module that can register model-callable tools and explicit local commands,
|
|
10
|
+
contribute instructions, observe immutable run events, or make policy stricter.
|
|
11
|
+
|
|
12
|
+
This page is both a human guide and the contract a coding model should follow
|
|
13
|
+
when writing a Forge plugin. The checked-in implementation is authoritative:
|
|
14
|
+
`packages/plugin-api/src/types.ts`,
|
|
15
|
+
`schema.ts`, and
|
|
16
|
+
`host.ts`.
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Quick start](#quick-start)
|
|
21
|
+
- [Locations, enablement, and trust](#locations-enablement-and-trust)
|
|
22
|
+
- [Plugin anatomy](#plugin-anatomy)
|
|
23
|
+
- [Manifest version 1](#manifest-version-1)
|
|
24
|
+
- [Activation and API reference](#activation-and-api-reference)
|
|
25
|
+
- [Custom tools](#custom-tools)
|
|
26
|
+
- [Other extension points](#other-extension-points)
|
|
27
|
+
- [Discovery and run lifecycle](#discovery-and-run-lifecycle)
|
|
28
|
+
- [Portable Skills are different](#portable-skills-are-different)
|
|
29
|
+
- [Testing a plugin](#testing-a-plugin)
|
|
30
|
+
- [Web tools example](#web-tools-example)
|
|
31
|
+
- [Instructions for a model author](#instructions-for-a-model-author)
|
|
32
|
+
- [Security boundary](#security-boundary)
|
|
33
|
+
- [Deliberate limitations](#deliberate-limitations)
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
Create a user plugin at `$FORGE_HOME/plugins/count-text/` (normally
|
|
38
|
+
`~/.forge/plugins/count-text/`):
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
count-text/
|
|
42
|
+
|-- plugin.json
|
|
43
|
+
`-- index.mjs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`plugin.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"schemaVersion": 1,
|
|
51
|
+
"apiVersion": "1",
|
|
52
|
+
"name": "count-text",
|
|
53
|
+
"version": "1.0.0",
|
|
54
|
+
"entry": "./index.mjs",
|
|
55
|
+
"capabilities": ["tools:register"]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`index.mjs`:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
export default function activate(api) {
|
|
63
|
+
const inputSchema = api.z
|
|
64
|
+
.object({ text: api.z.string().max(10000) })
|
|
65
|
+
.strict();
|
|
66
|
+
|
|
67
|
+
api.registerTool({
|
|
68
|
+
name: "count_text",
|
|
69
|
+
description: "Count Unicode characters in supplied text.",
|
|
70
|
+
risk: "read",
|
|
71
|
+
inputSchema,
|
|
72
|
+
execute: async (input) => {
|
|
73
|
+
const parsed = inputSchema.safeParse(input);
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: {
|
|
78
|
+
code: "invalid_input",
|
|
79
|
+
message: "Invalid input for count_text.",
|
|
80
|
+
retryable: false
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
output: { characters: Array.from(parsed.data.text).length },
|
|
87
|
+
truncated: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Enable it in the user-level `$FORGE_HOME/config.json`:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"schemaVersion": 1,
|
|
99
|
+
"plugins": {
|
|
100
|
+
"enabled": ["count-text"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then verify discovery and start Forge:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
forge plugins list
|
|
109
|
+
forge
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The blue startup frame lists enabled user plugins, trusted or skipped project
|
|
113
|
+
plugins, and discovered built-in, user, and project Skills. This listing reads manifests and Skill
|
|
114
|
+
metadata only; it does not import plugin entry files early.
|
|
115
|
+
|
|
116
|
+
## Locations, enablement, and trust
|
|
117
|
+
|
|
118
|
+
Forge supports two scopes:
|
|
119
|
+
|
|
120
|
+
| Scope | Location | How it becomes loadable |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| User | `$FORGE_HOME/plugins/<name>/` | Add the name to user `plugins.enabled` |
|
|
123
|
+
| Project | `<workspace>/.forge/plugins/<name>/` | Run `forge plugins trust` for the canonical workspace |
|
|
124
|
+
|
|
125
|
+
Project configuration cannot set `plugins.enabled`. User configuration cannot
|
|
126
|
+
silently trust project code. Project trust is stored outside the repository in
|
|
127
|
+
`$FORGE_HOME/plugin-trust.json` and is keyed by the canonical workspace path.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
forge plugins list
|
|
131
|
+
forge plugins trust
|
|
132
|
+
forge plugins trust --yes # explicit non-interactive decision
|
|
133
|
+
forge plugins untrust
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The same decision is available inside an interactive Forge session. Enter
|
|
137
|
+
`/plugins`, review the project plugin versions and capabilities, press `t`, and
|
|
138
|
+
confirm with `y`. A trusted workspace can be revoked from the same panel with
|
|
139
|
+
`u`. The header updates immediately, and newly trusted plugins load on the next
|
|
140
|
+
native Forge Engine task without restarting the TUI.
|
|
141
|
+
|
|
142
|
+
Discovery is intentionally shallow. Forge does not scan arbitrary ancestors,
|
|
143
|
+
nested plugin directories, or `node_modules`, and it does not install packages
|
|
144
|
+
or run lifecycle scripts. Starting in a repository subdirectory still resolves
|
|
145
|
+
the same workspace root and trust record.
|
|
146
|
+
|
|
147
|
+
## Plugin anatomy
|
|
148
|
+
|
|
149
|
+
A dependency-free plugin needs only a strict manifest and a JavaScript entry:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
my-plugin/
|
|
153
|
+
|-- plugin.json # declarative metadata, read before trust/import
|
|
154
|
+
|-- index.mjs # activation function
|
|
155
|
+
|-- README.md # recommended setup and safety notes
|
|
156
|
+
`-- test/ # optional plugin-owned tests
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The entry may import sibling `.js`/`.mjs` files. Forge does not provide a
|
|
160
|
+
dependency installer, so a shared plugin that imports third-party packages must
|
|
161
|
+
document how the user installs them and must not rely on package-manager hooks
|
|
162
|
+
running automatically.
|
|
163
|
+
|
|
164
|
+
## Manifest version 1
|
|
165
|
+
|
|
166
|
+
Every manifest is strict: unknown keys are rejected.
|
|
167
|
+
|
|
168
|
+
| Field | Required value |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `schemaVersion` | Number `1` |
|
|
171
|
+
| `apiVersion` | String `"1"` |
|
|
172
|
+
| `name` | Lowercase kebab-case, 1–64 characters, equal to directory name |
|
|
173
|
+
| `version` | Non-empty plugin version string |
|
|
174
|
+
| `entry` | Relative `.js`, `.mjs`, or `.cjs` path that stays inside the plugin directory |
|
|
175
|
+
| `capabilities` | Array of the capabilities the entry intends to use |
|
|
176
|
+
|
|
177
|
+
Supported capabilities:
|
|
178
|
+
|
|
179
|
+
| Capability | Meaning |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `tools:register` | Call `api.registerTool()` |
|
|
182
|
+
| `commands:register` | Call `api.registerCommand()` |
|
|
183
|
+
| `prompt:contribute` | Call `api.contributePrompt()` |
|
|
184
|
+
| `subagents:register` | Call `api.registerSubagent()` to declare a host-run child role |
|
|
185
|
+
| `events:observe` | Call `api.observeRunEvents()` |
|
|
186
|
+
| `policy:restrict` | Call `api.restrictPolicy()` |
|
|
187
|
+
| `network:access` | Declare that a registered `network`-risk tool performs external I/O |
|
|
188
|
+
|
|
189
|
+
Forge rejects an unsupported API version before import. It also rejects use of
|
|
190
|
+
a registration method that was not declared. A `network`-risk tool additionally
|
|
191
|
+
requires `network:access`.
|
|
192
|
+
|
|
193
|
+
Capabilities are review and API gates, not an operating-system sandbox. Trusted
|
|
194
|
+
JavaScript can still call Node.js directly, including during activation.
|
|
195
|
+
|
|
196
|
+
## Activation and API reference
|
|
197
|
+
|
|
198
|
+
The entry exports either `default` or a named `activate` function. It may be
|
|
199
|
+
synchronous or asynchronous; Forge awaits it before starting the run.
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
export async function activate(api) {
|
|
203
|
+
// register extension points here
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The frozen `api` object contains:
|
|
208
|
+
|
|
209
|
+
| Member | Purpose |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `api.apiVersion` | Current plugin API version, currently `"1"` |
|
|
212
|
+
| `api.z` | Forge's Zod instance for input schemas; no plugin dependency needed |
|
|
213
|
+
| `api.registerTool(tool)` | Register a model-callable tool |
|
|
214
|
+
| `api.registerCommand(command)` | Register an explicit local command |
|
|
215
|
+
| `api.registerSubagent(definition)` | Register an isolated, host-managed child role as a model tool |
|
|
216
|
+
| `api.contributePrompt(hook)` | Add bounded instructions to a run |
|
|
217
|
+
| `api.observeRunEvents(observer)` | Observe immutable event snapshots |
|
|
218
|
+
| `api.restrictPolicy(hook)` | Change an effective decision only to `confirm` or `deny` |
|
|
219
|
+
|
|
220
|
+
Registration names must be unique across built-ins and loaded plugins. Treat
|
|
221
|
+
activation as setup: do not perform surprising writes, network requests, or
|
|
222
|
+
long-running work merely because Forge starts.
|
|
223
|
+
|
|
224
|
+
## Custom tools
|
|
225
|
+
|
|
226
|
+
`api.registerTool()` receives the provider-neutral `ForgeTool` contract:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
interface ForgeTool {
|
|
230
|
+
name: string;
|
|
231
|
+
description: string;
|
|
232
|
+
inputSchema: z.ZodType;
|
|
233
|
+
risk: "read" | "write" | "process" | "network" | "model";
|
|
234
|
+
execute(input: unknown, context: ToolContext): Promise<ToolResult>;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Tool names use lower snake case and must match
|
|
239
|
+
`^[a-z][a-z0-9_]{0,63}$`. Descriptions and schema descriptions are sent to the
|
|
240
|
+
model, so say exactly when the tool should be called and keep inputs bounded.
|
|
241
|
+
|
|
242
|
+
### Risk selection
|
|
243
|
+
|
|
244
|
+
| Risk | Use for | Default policy |
|
|
245
|
+
| --- | --- | --- |
|
|
246
|
+
| `read` | Workspace-bounded, side-effect-free inspection | Allow |
|
|
247
|
+
| `write` | Workspace file changes | Confirm first write in `safe`; allow in `workspace-write` |
|
|
248
|
+
| `process` | Starting any child process | Confirm every call |
|
|
249
|
+
| `network` | Sending data to or fetching data from an external service | Confirm every call |
|
|
250
|
+
| `model` | Starting an additional delegated model run | Confirm every call |
|
|
251
|
+
|
|
252
|
+
Do not label a network or process action as `read` merely because it does not
|
|
253
|
+
modify the repository. The risk describes the external effect, not only the
|
|
254
|
+
shape of the returned data.
|
|
255
|
+
|
|
256
|
+
Every plugin tool call follows the same runtime path as a built-in tool:
|
|
257
|
+
|
|
258
|
+
```text
|
|
259
|
+
model proposal
|
|
260
|
+
-> schema validation
|
|
261
|
+
-> core policy
|
|
262
|
+
-> stricter plugin policy hooks
|
|
263
|
+
-> approval when required
|
|
264
|
+
-> execute
|
|
265
|
+
-> structured RunEvents
|
|
266
|
+
-> redacted trace and observers
|
|
267
|
+
-> tool result returned to model
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The `context` passed to `execute` contains the canonical workspace root/current
|
|
271
|
+
directory, the run's `AbortSignal`, and strict limits:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
interface ToolContext {
|
|
275
|
+
workspace: { root: string; cwd: string };
|
|
276
|
+
signal: AbortSignal;
|
|
277
|
+
limits: {
|
|
278
|
+
maxOutputBytes: number;
|
|
279
|
+
maxEntries: number;
|
|
280
|
+
commandTimeoutMs?: number;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Honor cancellation promptly and let configured limits win over plugin defaults.
|
|
286
|
+
Successful and failed results are explicit:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
output: { value: "bounded JSON-serializable data" },
|
|
292
|
+
truncated: false
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
error: {
|
|
298
|
+
code: "invalid_input",
|
|
299
|
+
message: "Explain a safe corrective action without secrets.",
|
|
300
|
+
retryable: false
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Use an existing Forge error code from `@forge/core` (`invalid_input`,
|
|
306
|
+
`cancelled`, `io_error`, `output_limit`, `timed_out`, and so on). Never include
|
|
307
|
+
credentials, authorization headers, or private response bodies in errors.
|
|
308
|
+
|
|
309
|
+
## Other extension points
|
|
310
|
+
|
|
311
|
+
### Subagents
|
|
312
|
+
|
|
313
|
+
`api.registerSubagent()` is declarative: the plugin defines a role, generated
|
|
314
|
+
parent-tool name, instructions, allowed child tools, and tight limits. It never
|
|
315
|
+
receives credentials or a callable model/runtime object.
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
api.registerSubagent({
|
|
319
|
+
name: "code-reviewer",
|
|
320
|
+
toolName: "delegate_code_review",
|
|
321
|
+
description: "Delegate a focused read-only review.",
|
|
322
|
+
instructions: "Report concrete correctness and security findings.",
|
|
323
|
+
tools: ["list_files", "read_file", "search"],
|
|
324
|
+
limits: { maxModelSteps: 4, maxToolCalls: 8 }
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Declare `subagents:register`. Role names are kebab-case; generated tool names
|
|
329
|
+
are lower snake case and share the normal built-in/plugin name namespace.
|
|
330
|
+
Instructions are required and capped at 16 KiB. At most 32 unique, already
|
|
331
|
+
registered non-subagent tools may be selected. Per-child limits are capped at
|
|
332
|
+
8 model steps and 20 tool calls.
|
|
333
|
+
|
|
334
|
+
The generated tool accepts `{ task: string }`, has `model` risk, and requires
|
|
335
|
+
approval for every delegation. Forge creates a fresh adapter and isolated
|
|
336
|
+
conversation, inherits project instructions, workspace, context settings,
|
|
337
|
+
cancellation, approval channel, and the effective core-plus-plugin policy, and
|
|
338
|
+
exposes only the declared tools. Subagent tools are never included in child
|
|
339
|
+
tool sets, preventing recursive delegation.
|
|
340
|
+
|
|
341
|
+
One parent run may start at most four children. All children also share budgets
|
|
342
|
+
equal to the configured parent `maxSteps` and `maxToolCalls`; a plugin's limit
|
|
343
|
+
can only reduce those ceilings. Results are bounded by `maxToolOutputBytes`.
|
|
344
|
+
When tracing is enabled, each child gets a separate run trace whose envelopes
|
|
345
|
+
carry `parentRunId` and `subagentName`; the parent tool result carries the child
|
|
346
|
+
`runId`, status, step/tool counts, and final text.
|
|
347
|
+
|
|
348
|
+
See `examples/plugins/code-review-subagent`.
|
|
349
|
+
|
|
350
|
+
### Commands
|
|
351
|
+
|
|
352
|
+
Commands are explicit trusted-code entry points, not model tool calls:
|
|
353
|
+
|
|
354
|
+
```js
|
|
355
|
+
api.registerCommand({
|
|
356
|
+
name: "hello",
|
|
357
|
+
description: "Print a local greeting.",
|
|
358
|
+
execute: async ({ args, write }) => {
|
|
359
|
+
write(`hello ${args.join(" ") || "world"}\n`);
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Run them with `forge plugins run hello [args...]`. The context exposes `cwd`,
|
|
366
|
+
`workspaceRoot`, `args`, `signal`, `write`, and `writeError`. Commands bypass
|
|
367
|
+
model-tool approval because the user invokes them directly; they still execute
|
|
368
|
+
with the full privileges of the trusted plugin.
|
|
369
|
+
|
|
370
|
+
### Prompt contributions
|
|
371
|
+
|
|
372
|
+
`api.contributePrompt(hook)` receives an immutable snapshot containing the
|
|
373
|
+
current prompt, canonical workspace root, and working directory. A returned
|
|
374
|
+
string is limited to 32 KiB, labelled with its manifest path, added to the
|
|
375
|
+
effective instruction context, and included in instruction provenance.
|
|
376
|
+
|
|
377
|
+
Return `undefined` when no contribution is needed. Treat the user prompt as
|
|
378
|
+
untrusted data and do not use a prompt hook as a hidden command runner.
|
|
379
|
+
|
|
380
|
+
### Run-event observers
|
|
381
|
+
|
|
382
|
+
`api.observeRunEvents(observer)` receives a deeply frozen structured clone of
|
|
383
|
+
each `RunEvent`. Observers cannot mutate runtime history. Observer failures
|
|
384
|
+
produce warnings without replacing the run result or trace. Events are redacted
|
|
385
|
+
for configured secrets before observers receive them.
|
|
386
|
+
|
|
387
|
+
### Policy restrictions
|
|
388
|
+
|
|
389
|
+
`api.restrictPolicy(hook)` receives a frozen snapshot of the tool, call, and
|
|
390
|
+
validated input. It may return only:
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
{ kind: "confirm", reason: "..." }
|
|
394
|
+
{ kind: "deny", reason: "..." }
|
|
395
|
+
undefined
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Forge combines decisions using `deny > confirm > allow`. A plugin cannot turn a
|
|
399
|
+
core confirmation or denial into an allow. See
|
|
400
|
+
`examples/plugins/stricter-policy`.
|
|
401
|
+
|
|
402
|
+
## Discovery and run lifecycle
|
|
403
|
+
|
|
404
|
+
The native Forge Engine performs these steps for each prompt:
|
|
405
|
+
|
|
406
|
+
1. Load and validate configuration.
|
|
407
|
+
2. Load bounded user/project instructions.
|
|
408
|
+
3. Discover bounded built-in, user, and project Skill metadata, resolve
|
|
409
|
+
collisions, and preserve explicit `$skill-name` selection.
|
|
410
|
+
4. Discover plugin manifests.
|
|
411
|
+
5. Exclude disabled user plugins and untrusted project plugins.
|
|
412
|
+
6. Resolve each entry inside its plugin directory and import it.
|
|
413
|
+
7. Activate plugins and validate registrations/capabilities/name conflicts.
|
|
414
|
+
8. Collect bounded prompt contributions.
|
|
415
|
+
9. Build the model request and combined tool registry.
|
|
416
|
+
10. Run all proposed tools through policy, approval, execution, events, and
|
|
417
|
+
traces.
|
|
418
|
+
|
|
419
|
+
The interactive startup frame stops after metadata discovery for display. The
|
|
420
|
+
actual entry import/activation still happens only when the Forge Engine starts a
|
|
421
|
+
run. The Codex Engine is a separate runtime owned by Codex App Server and does
|
|
422
|
+
not load Forge plugins.
|
|
423
|
+
|
|
424
|
+
## Portable Skills are different
|
|
425
|
+
|
|
426
|
+
Forge discovers Markdown Skills at:
|
|
427
|
+
|
|
428
|
+
```text
|
|
429
|
+
<installed-package>/resources/skills/<skill-name>/SKILL.md
|
|
430
|
+
$FORGE_HOME/skills/<skill-name>/SKILL.md
|
|
431
|
+
<workspace-root>/.agents/skills/<skill-name>/SKILL.md
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
A Skill is guidance, not executable plugin code. Discovery never executes it.
|
|
435
|
+
Each `SKILL.md` starts with bounded YAML frontmatter containing a kebab-case
|
|
436
|
+
`name` matching its directory and a task-oriented `description`.
|
|
437
|
+
`disable-model-invocation: true` makes that Skill explicit-only.
|
|
438
|
+
|
|
439
|
+
The first model request contains only the bounded catalog fields `id`, `name`,
|
|
440
|
+
`description`, and `source`; Skill bodies are loaded lazily through the
|
|
441
|
+
host-owned `load_skill` read tool. That tool accepts only registered opaque IDs,
|
|
442
|
+
limits and deduplicates loads, revalidates canonical paths and file identities,
|
|
443
|
+
and rejects symlinks or files outside the registered root. Name collisions use
|
|
444
|
+
`project > user > builtin`; an explicit `$skill-name` overrides automatic
|
|
445
|
+
routing. Selection, load, rejection, and truncation are trace events visible in
|
|
446
|
+
`forge inspect`.
|
|
447
|
+
|
|
448
|
+
Scripts or actions described by a Skill still require normal model tool calls,
|
|
449
|
+
policy, approval, and trace events. Selecting or loading a project Skill does
|
|
450
|
+
not trust project plugin code and does not widen `read_file` beyond the active
|
|
451
|
+
workspace.
|
|
452
|
+
|
|
453
|
+
## Testing a plugin
|
|
454
|
+
|
|
455
|
+
Plugin tests must not require a paid model or a real external service. Use fake
|
|
456
|
+
transport responses and the real host/policy boundary where possible.
|
|
457
|
+
|
|
458
|
+
Recommended loop:
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
forge plugins list
|
|
462
|
+
pnpm typecheck
|
|
463
|
+
pnpm test
|
|
464
|
+
pnpm check
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
For a project plugin, inspect the manifest before trust, then make the decision
|
|
468
|
+
explicit:
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
forge plugins list
|
|
472
|
+
forge plugins trust
|
|
473
|
+
forge run "Use my plugin to perform its smallest safe task"
|
|
474
|
+
forge plugins untrust
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Alternatively, use `/plugins` in the TUI for the same explicit review,
|
|
478
|
+
confirmation, and revocation flow.
|
|
479
|
+
|
|
480
|
+
For a user plugin under test, point `FORGE_HOME` at a temporary directory and
|
|
481
|
+
enable only that plugin. A robust plugin test should cover:
|
|
482
|
+
|
|
483
|
+
- Manifest discovery without importing the entry.
|
|
484
|
+
- Host activation and registered names/capabilities.
|
|
485
|
+
- Invalid inputs and cancellation.
|
|
486
|
+
- Output/entry/timeout limits and `truncated` accuracy.
|
|
487
|
+
- Model/network/process/write policy decisions where relevant.
|
|
488
|
+
- Redaction: secrets never occur in a result, event, error, or test snapshot.
|
|
489
|
+
- Recoverable provider failures without live network calls.
|
|
490
|
+
|
|
491
|
+
## Web tools example
|
|
492
|
+
|
|
493
|
+
`examples/plugins/web-tools` is a complete,
|
|
494
|
+
dependency-free test of the current plugin system. It registers:
|
|
495
|
+
|
|
496
|
+
- `web_search`: Brave Search when `BRAVE_SEARCH_API_KEY` exists, otherwise
|
|
497
|
+
DuckDuckGo's non-JavaScript HTML search.
|
|
498
|
+
- `web_fetch`: extracts bounded readable text from public HTTP(S) pages.
|
|
499
|
+
|
|
500
|
+
The Brave path follows the official
|
|
501
|
+
[Web Search API](https://api-dashboard.search.brave.com/api-reference/web/search/get).
|
|
502
|
+
The key-free fallback uses DuckDuckGo's documented
|
|
503
|
+
[non-JavaScript search](https://duckduckgo.com/duckduckgo-help-pages/features/non-javascript).
|
|
504
|
+
|
|
505
|
+
Install it as a user plugin:
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
mkdir -p "${FORGE_HOME:-$HOME/.forge}/plugins"
|
|
509
|
+
cp -R examples/plugins/web-tools "${FORGE_HOME:-$HOME/.forge}/plugins/web-tools"
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Then add `"web-tools"` to `plugins.enabled` and restart Forge. Its own
|
|
513
|
+
`README.md` documents search-provider
|
|
514
|
+
selection and implemented controls.
|
|
515
|
+
|
|
516
|
+
The example treats web access as external I/O, so both tools use `network` risk
|
|
517
|
+
and require approval on every call. Forge's shared HTTP transport honors
|
|
518
|
+
`HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` (including lowercase aliases), so
|
|
519
|
+
plugins using the global `fetch` work with ordinary HTTP(S) proxy setups.
|
|
520
|
+
`web_fetch` validates every initial and redirect URL, blocks local hostnames and
|
|
521
|
+
private IP literals, and resolves direct destinations before connection. For a
|
|
522
|
+
proxied destination, the explicitly configured proxy owns DNS resolution;
|
|
523
|
+
`NO_PROXY` destinations retain direct-DNS validation. It also restricts ports
|
|
524
|
+
and MIME types and bounds redirects, time, downloads, characters, entries, and
|
|
525
|
+
serialized output. These controls reduce accidental SSRF and runaway output;
|
|
526
|
+
they are not a network sandbox, and a configured proxy is part of the trust
|
|
527
|
+
boundary.
|
|
528
|
+
|
|
529
|
+
## MCP, to-dos, and subagents
|
|
530
|
+
|
|
531
|
+
The examples make the current extension boundary concrete:
|
|
532
|
+
|
|
533
|
+
| Capability | Current plugin API | Example / limitation |
|
|
534
|
+
| --- | --- | --- |
|
|
535
|
+
| MCP server tools | Yes, with protocol and lifecycle limits | `mcp-stdio` registers approved `process`-risk list/call bridge tools for one configured stdio server. |
|
|
536
|
+
| Lightweight to-dos | Yes | `todos` registers an in-memory tool and bounded prompt contribution. Persistence and a custom TUI panel are not available. |
|
|
537
|
+
| Host-managed subagents | Yes | `code-review-subagent` declares a read-only child role; Forge owns its adapter, policy, budgets, cancellation, and linked trace. |
|
|
538
|
+
|
|
539
|
+
The MCP example intentionally targets session-based, newline-delimited stdio
|
|
540
|
+
revision `2025-11-25`; it is evidence that a plugin can bridge MCP tools, not a
|
|
541
|
+
claim that Forge has complete MCP host support. Streamable HTTP, current
|
|
542
|
+
handshake-free protocol support, server reuse, prompts/resources/roots,
|
|
543
|
+
sampling, tasks, and lifecycle disposal need a first-class host or expanded
|
|
544
|
+
plugin contract.
|
|
545
|
+
|
|
546
|
+
Subagents currently inherit the active parent model; plugins cannot select a
|
|
547
|
+
different provider/model, pass parent conversation history, persist a child as
|
|
548
|
+
an independently resumable session, stream child deltas into a dedicated TUI
|
|
549
|
+
panel, or enable nested delegation. Those remain deliberate host limitations,
|
|
550
|
+
not behaviors plugins should emulate with direct provider calls or recursive
|
|
551
|
+
CLI spawning.
|
|
552
|
+
|
|
553
|
+
## Instructions for a model author
|
|
554
|
+
|
|
555
|
+
When asked to write a Forge plugin, follow this sequence:
|
|
556
|
+
|
|
557
|
+
1. Read this entire document and inspect the current plugin types/schema/host.
|
|
558
|
+
2. Decide user versus project scope from the user's intent; never record trust
|
|
559
|
+
or edit user configuration unless requested.
|
|
560
|
+
3. Choose the smallest manifest capability set. A network tool needs both
|
|
561
|
+
`tools:register` and `network:access`.
|
|
562
|
+
4. Use plain ESM JavaScript and `api.z` unless a dependency is genuinely
|
|
563
|
+
necessary. Forge does not compile TypeScript plugin entries or install deps.
|
|
564
|
+
5. Validate inside `execute` even though the runtime validates first; direct
|
|
565
|
+
tests and future callers should receive a structured failure.
|
|
566
|
+
6. Choose the honest risk. Bound every input and every output, honor
|
|
567
|
+
`context.signal`, and use `context.limits` as the upper authority.
|
|
568
|
+
7. Keep activation free of surprising side effects. Do work only after an
|
|
569
|
+
explicit command or approved tool call.
|
|
570
|
+
8. Add deterministic tests with fake I/O. Exercise discovery and activation
|
|
571
|
+
through `loadPluginHost` when changing Forge itself.
|
|
572
|
+
9. Run formatting, type checks, targeted tests, and the full suite.
|
|
573
|
+
10. Document setup, required environment variables, data sent externally,
|
|
574
|
+
safety controls, and known limitations without claiming sandboxing.
|
|
575
|
+
|
|
576
|
+
Before handing off, verify:
|
|
577
|
+
|
|
578
|
+
- Directory name equals manifest `name`.
|
|
579
|
+
- Manifest/API versions are exactly supported.
|
|
580
|
+
- Entry stays inside the plugin directory and exports an activation function.
|
|
581
|
+
- All used APIs, subagents, and `network` tools declare their capabilities.
|
|
582
|
+
- Tool/command names do not collide and descriptions are model-readable.
|
|
583
|
+
- Errors are actionable and contain no secrets.
|
|
584
|
+
- Output sizes are measured after JSON serialization when that matters.
|
|
585
|
+
- Project code is not executed before explicit trust.
|
|
586
|
+
- Documentation distinguishes discovery, enablement, trust, activation, and
|
|
587
|
+
tool-call approval.
|
|
588
|
+
|
|
589
|
+
## Security boundary
|
|
590
|
+
|
|
591
|
+
Loading a plugin executes local code with the full privileges of the Forge
|
|
592
|
+
process. It may import Node.js modules, read arbitrary files, start processes,
|
|
593
|
+
or use the network without going through a registered tool. Only install or
|
|
594
|
+
trust code you have reviewed.
|
|
595
|
+
|
|
596
|
+
Forge enforces safety at its supported API boundaries:
|
|
597
|
+
|
|
598
|
+
- Project entries are not imported before canonical workspace trust.
|
|
599
|
+
- Manifest/API/capability/name/schema contracts are validated.
|
|
600
|
+
- Model-called plugin tools follow core policy and approval.
|
|
601
|
+
- `model`, `network`, `process`, and applicable write actions require confirmation.
|
|
602
|
+
- Policy hooks can only make decisions stricter.
|
|
603
|
+
- Prompt contributions and Skill content are bounded and attributed.
|
|
604
|
+
- Observer input is cloned, frozen, and secret-redacted.
|
|
605
|
+
|
|
606
|
+
Those properties do not isolate a malicious trusted entry. Strong isolation
|
|
607
|
+
would require a restricted process or OS sandbox.
|
|
608
|
+
|
|
609
|
+
## Deliberate limitations
|
|
610
|
+
|
|
611
|
+
Forge 0.3.2 has no plugin installer, dependency resolver, package registry, hot
|
|
612
|
+
reload, TypeScript entry compilation, custom interactive UI, provider
|
|
613
|
+
registration, isolated plugin process, or enforceable filesystem/network
|
|
614
|
+
capabilities. Plugin commands run only through `forge plugins run`; they do not
|
|
615
|
+
become interactive slash commands. These gaps are explicit so plugin authors
|
|
616
|
+
can target the implemented contract instead of guessing at future APIs.
|
|
617
|
+
|
|
618
|
+
## Skills and product documentation are resources
|
|
619
|
+
|
|
620
|
+
Skills are non-executable, untrusted instruction resources discovered from built-in, user, and project scopes. `forge plugins list` reports executable plugins; `forge resources list` reports Skill source, invocation status, collision shadowing, and diagnostics. The interactive equivalents are `/plugins` and `/resources`.
|
|
621
|
+
|
|
622
|
+
The built-in `forge-product-help` Skill requires a documentation lookup before implementation-specific product answers. `search_forge_docs` and `read_forge_doc` use a version-matched packaged allowlist and stable `forge-doc:<version>:<locale>:<document>#<section>` references. They reject filesystem paths and do not inherit `read_file` access.
|