Compare commits

..

21 Commits

Author SHA1 Message Date
2a3c582ec8 merge(sdk): fix #329 — feedbacks menu host génériques et doc SDK alignée (QA verte: npm run check, git diff --check) — docs only, aucun push 2026-09-19 00:33:57 +02:00
32d33b330f docs(sdk): feedbacks menu host génériques — #329 ; README + docs commands-and-feedback/menus/services : failed/skipped sans message Git codé en dur ni payload brut, opened/alreadyOpen succès silencieux, logs host pluginId/commandId/status/reason/message, vue de diagnostic reste accessible quand Git/ADB/Gradle ou autre outil optionnel manque, workspace.execTool rappelé ; QA verte : npm run check, git diff --check 2026-09-19 00:33:57 +02:00
8ff0f475de feat(sdk): contrat permissions.workspaceExec pour l'exécution contrôlée via l'hôte — #346
Cadrage capacités plugin SCM (#346): le manifeste plugin gagne
permissions.workspaceExec.allow — règles d'exécution contrôlée par l'hôte
(commande bare ASCII résolue sans shell, argv exact avec jeton `*` en
suffixe, cwd project-relative sûr, effect read/write/network/destructive,
64 règles max) — au lieu de lectures ad hoc du store interne .git.
Validation manifeste + exports runtime/index + docs README/manifest/
services + auto-check script étendu (règles valides acceptées, règles
unsafe rejetées).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 12:07:56 +02:00
720cd53924 merge(sdk): fix #336 — contrat docs commandes/menus apres hot reload (erreur retryable, feedback host-owned, contrat manifest/registration, reload sans refresh auto)
# Conflicts:
#	docs/menus.md
2026-09-12 14:06:54 +02:00
9efb781636 docs(sdk): rejet de promesse windows.open remonte au handler + rejet des commandes stale d'un runtime remplace (identite plugin) — #336 (docs only, aucun push) 2026-09-12 14:06:00 +02:00
116b75a024 docs(sdk): contrat commandes/menus apres reload — handler manquant = erreur retryable jamais no-op silencieux, feedback skipped/failed host-owned, pas de contributes.commands separe, reload sans refresh auto par le plugin — #336 (docs only, aucun push) 2026-09-12 14:05:52 +02:00
047c38b7ca docs(sdk): taches planifiees projet non exposees comme service plugin V1 — #305 (idea_scheduled_task_* natifs Tauri/HTTP/MCP uniquement; interdit d'emuler via cron systeme ou fichiers auto-executes dans le repo; aucun push) 2026-09-11 10:30:59 +02:00
436ed59b41 merge(sdk): intègre feature/296-unified-agent-capabilities — metadata statiques mcpServers[].tools #296 (QA verte: npm run check) 2026-09-09 19:52:23 +02:00
d93e302320 feat(sdk): metadata statiques contributes.mcpServers[].tools — #296 (type IdeAPluginMcpToolMetadata {name exact non vide, description optionnelle non vide} exporté public; validation manifest: tools optionnel, noms uniques par serveur — metadata d'affichage pour l'UI d'attribution de capacités, ne remplace ni ne cache MCP tools/list et n'enregistre rien; scripts/check-manifest-tools.mjs + npm run test:manifest intégré à check: optionnel/valide/doublon rejeté; doc manifest.md: exemple unity_get_scene/run_tests + règles; QA verte reconfirmée par Git: npm run check) 2026-09-09 19:52:23 +02:00
fdb3a907af merge(sdk): intègre feature/294-node-mcp-launch-docs — doc lancement serveurs MCP JS packagés #294 (QA verte: npm run check; vérifié contre hôte application/plugin/mod.rs) 2026-09-09 16:24:20 +02:00
b5a35563b6 docs(sdk): lancement des serveurs MCP JavaScript packagés — #294 (manifest: aucune substitution ${node}/${runtimeNode}/${hostNode} ni champ runtime, IdeA ne fournit pas de runtime Node plugin-scoped; command n'est pas du shell — démarrage direct avec args séparés, pas de fichiers de startup/alias/PATH du shell de login; command relatif résolu sous pluginRoot — command:"node" sans allowAbsoluteCommand devient ${pluginRoot}/node (cause ENOENT UnityPlugin #12); absolu littéral refusé sans allowAbsoluteCommand=true, command devenu absolu après substitution ${pluginRoot}/${appDataDir} passé tel quel; shebang #!/usr/bin/env node dépend du node visible du process app IdeA; recommandations: exécutable packagé incluant son runtime ou runtime géré sous stockage plugin/app pointé via ${pluginRoot}/${appDataDir}, sinon dépendance hôte explicite absolue /usr/bin/node avec allowAbsoluteCommand=true et scripts dans args; même règle pour command:"bash"; packaging-distribution: section Packaged Executables And Runtimes — bit exécutable et shebang valides dans le package installé, éviter le bare command:"node" en plugins distribués/multi-harness; services: distinction runCommand() (exécutable hôte de l'environnement task, aucune substitution) vs règles manifest mcpServers avec lien croisé; affirmations vérifiées par Git contre l'hôte crates/application/src/plugin/mod.rs:3199 substitution puis looks_absolute/allow_absolute_command sinon préfixage pluginRoot, et validation :3637 absolu littéral sans flag rejeté; QA verte reconfirmée par Git: npm run check — build, typecheck:examples, package:hello-plugin) 2026-09-09 16:24:16 +02:00
68fd612087 merge(sdk): intègre feature/293-mcp-tools-exposure-docs — doc contrat tools MCP plugins #293 (QA verte: npm run check) 2026-09-09 16:02:15 +02:00
42567a5305 docs(sdk): contrat d'exposition des tools MCP par les plugins — #293 (manifest: section MCP Servers And Agent Tools — contributes.mcpServers est le seul canal IdeA-native, aucun registerMcpTool/registerServer dans activate(ctx), règles id/displayName/transport stdio/autoStart requis pour la réconciliation MCP/command relatif résolu sous pluginRoot vs absolu refusé sans allowAbsoluteCommand/cwd défaut ${pluginRoot}, assignation par triplet pluginId/serverId/toolName devant matcher tools/list du serveur, example manifest aligné contributes.skills, guidance script packagé exécutable vs lancement via binaire hôte; activation-context: activate n'expose pas d'API d'enregistrement de tools MCP — ils viennent du process serveur stdio externe et de sa réponse tools/list, ctx.pluginRoot officialisé vs import.meta.url; project-plugin-assignments: assignation résolue vers serveurs manifestés autoStart uniquement, flow skill → serveur MCP → tools assignés sans chemins relatifs depuis le Markdown servi par idea_skill_read; QA verte reconfirmée par Git: npm run check — build, typecheck:examples, package:hello-plugin) 2026-09-09 16:02:05 +02:00
e3a0f46f9d merge(sdk): intègre feature/285-plugin-root-runtime-api — pluginRoot officiel activate(ctx) #285 (QA verte: npm run check) 2026-09-08 18:35:24 +02:00
9ce3a557a9 feat(sdk): pluginRoot officiel exposé dans activate(ctx) — #285 (src/runtime: champ pluginRoot = racine du package installé; docs activation-context/commands-and-feedback/services/manifest réécrits du constat d'absence vers le contrat d'usage officiel — runCommand reste project-scoped, pluginRoot utilisable dans command/args, substitution manifest ${pluginRoot}/${appDataDir} inchangée pour mcpServers; exemple hello-plugin: script packagé scripts/hello-task.mjs consommé via ctx.pluginRoot, packaging étendu; QA verte: npm run check — build, typecheck:examples, package:hello-plugin, artefact zip vérifié) 2026-09-08 18:35:24 +02:00
68019dec58 merge(sdk): intègre feature/285-plugin-root-scope-docs — clarification périmètre pluginRoot runtime #285 (QA verte: npm run build) 2026-09-08 17:20:05 +02:00
5f9c2f9193 docs(sdk): clarify pluginRoot scope for runtime plugins — #285 (activation-context: ActivateContext n'expose ni répertoire d'installation ni racine package ni file URL; commands-and-feedback + services: runCommand project/workspace-scoped, aucune résolution package-relative des scripts packagés; manifest: substitution ${pluginRoot}/${appDataDir} étendue par l'hôte uniquement dans contributes.mcpServers command/args/env/cwd; QA verte: npm run build, cohérence champs vs types SDK vérifiée) 2026-09-08 17:19:57 +02:00
7db819c647 merge(sdk): intègre feature/283-plugin-skill-assignment-docs — doc assignation skills plugin aux agents #283 (QA verte: npm run build, liens docs vérifiés) 2026-09-08 16:34:20 +02:00
1b5ab2fa86 docs(sdk): schéma project-plugins.json v1 + assignation skills/tools agents — #283 (project-plugin-assignments: workflow install→enable→assign→relaunch, schéma complet plugins/agents/skills/tools camelCase, ordre d'injection natif puis plugin, tombstones inertes uninstall/reinstall, validation UI/API idempotente vs édition manuelle, reload plugin ≠ relance agent; liens croisés manifest/codex-skills/README; QA verte: npm run build, liens docs vérifiés) 2026-09-08 16:34:03 +02:00
6eb8db3791 feat(sdk): contributes.skills — skills IdeA-native déclarables par plugin — #282 (types IdeAPluginSkillContribution/IdeAPluginSkillKind: id/name/description/kind workflow|reference/path .md package-relative, validation manifeste, export public; docs manifest + codex-skills-and-idea-plugins recentrés skills IdeA-native vs .codex-plugin Codex, README; QA verte: npm run build) 2026-09-08 15:48:47 +02:00
1239104b0f docs(sdk): feedback visible via windows.open + séparation Codex skills documentées — commands-and-feedback (surface normative + exemple), layouts-react (type↔contributes.layouts, capacité ui), manifest (exemple complet menu+layout+ui), windows (contrat ui, retour structuré), packaging-distribution (idea_plugin_install_from_directory, sémantique plugin_reloaded/ESM), nouveau codex-skills-and-idea-plugins, README (lien) — #280 QA verte 2026-09-08 13:40:52 +02:00
20 changed files with 1026 additions and 30 deletions

View File

@ -14,13 +14,28 @@ exports `activate(ctx)`.
- [Commands And Feedback](docs/commands-and-feedback.md): command return values, skipped work and background tasks. - [Commands And Feedback](docs/commands-and-feedback.md): command return values, skipped work and background tasks.
- [Layouts With React](docs/layouts-react.md): React/JSX/hooks layout authoring with host-provided React. - [Layouts With React](docs/layouts-react.md): React/JSX/hooks layout authoring with host-provided React.
- [Windows](docs/windows.md): opening plugin layouts in detached OS windows. - [Windows](docs/windows.md): opening plugin layouts in detached OS windows.
- [Services](docs/services.md): workspace, tasks, tooling, events, config, terminal and windows facades. - [Services](docs/services.md): workspace files and bounded `workspace.execTool`, tasks,
tooling, events, config, terminal and windows facades.
- [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout, hot reload and dependency rules. - [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout, hot reload and dependency rules.
- [Codex Skills And IdeA Plugins](docs/codex-skills-and-idea-plugins.md): separate IdeA-native plugin skills from Codex plugin discovery.
- [Project Plugin Assignments](docs/project-plugin-assignments.md): enable plugins per project and assign skills and MCP tools to agents.
The installable example lives in [`examples/hello-plugin`](examples/hello-plugin). The installable example lives in [`examples/hello-plugin`](examples/hello-plugin).
It demonstrates a menu command, storage, background-task feedback, a React layout It demonstrates a menu command, storage, background-task feedback, a React layout
component and `services.windows.open(...)`. component and `services.windows.open(...)`.
For short project-scoped external tools, use
`ctx.services.workspace.execTool()` with an explicit manifest
`permissions.workspaceExec` allow rule. See [Services](docs/services.md) and
[Manifest](docs/manifest.md). The primitive is generic and does not expose a
native Git/SCM API.
Menu commands that open or focus a plugin view return `status: "opened"`
(including `alreadyOpen: true`) and remain UI-silent on success. Keep diagnostic
views accessible when optional tools are missing, and use the structured
`skipped`/`failed` feedback contract for genuinely impossible actions; see
[Commands And Feedback](docs/commands-and-feedback.md) and [Menus](docs/menus.md).
## Install ## Install
```sh ```sh

View File

@ -6,7 +6,10 @@ The plugin entrypoint exports `activate(ctx)`.
import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk"; import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk";
export function activate(ctx: ActivateContext): void { export function activate(ctx: ActivateContext): void {
ctx.logger.info("activated", { pluginId: ctx.pluginId }); ctx.logger.info("activated", {
pluginId: ctx.pluginId,
pluginRoot: ctx.pluginRoot
});
} }
export default { activate } satisfies IdeAPluginModule; export default { activate } satisfies IdeAPluginModule;
@ -27,7 +30,8 @@ focused project at invocation/render time.
## Context Fields ## Context Fields
- `pluginId`, `pluginDisplayName`, `version`: host-provided identity. - `pluginId`: host-provided plugin identity.
- `pluginRoot`: absolute host-local path to the active installed plugin package.
- `logger`: `debug`, `info`, `warn`, `error`. - `logger`: `debug`, `info`, `warn`, `error`.
- `subscriptions`: push disposables returned by command/layout/watch - `subscriptions`: push disposables returned by command/layout/watch
registrations. registrations.
@ -41,6 +45,23 @@ focused project at invocation/render time.
The context never exposes internal IdeA gateways or Tauri commands. Use The context never exposes internal IdeA gateways or Tauri commands. Use
`ctx.services` and the registration APIs instead. `ctx.services` and the registration APIs instead.
`activate(ctx)` does not expose an API for registering MCP tools directly.
Agent-callable plugin tools are provided by stdio MCP servers declared in
`idea-plugin.json` under `contributes.mcpServers`. The plugin runtime can
register menu commands and layouts in-process, but MCP tools come from the
external server process and its MCP `tools/list` response.
`pluginRoot` identifies the committed package currently loaded by IdeA. It is
not the original source directory or archive path and may change after reinstall.
Treat it as read-only, do not persist it, and resolve packaged scripts/assets
under it only while the plugin is active. Workspace services remain confined to
project-owned paths.
Use `ctx.pluginRoot` as the official way to resolve packaged scripts/assets from
runtime code. `import.meta.url` can still be useful inside a bundle for ordinary
module-relative JavaScript resolution, but it is not the host/plugin contract
for the installed package root.
## Disposal ## Disposal
Push every returned disposable to `ctx.subscriptions`: Push every returned disposable to `ctx.subscriptions`:

View File

@ -0,0 +1,53 @@
# Codex Skills And IdeA Plugins
IdeA runtime plugins, IdeA-native plugin skills and Codex plugins are distinct
installation and discovery systems.
An IdeA runtime plugin is described by `idea-plugin.json`. Installing it through
IdeA makes its global runtime package available. Project settings then decide
which installed plugins are enabled for a project, and agent settings decide
which plugin skills and plugin MCP tools are assigned to each agent.
An IdeA-native plugin skill is declared in `idea-plugin.json` under
`contributes.skills`. Its body is a package-relative Markdown file such as
`skills/<name>/SKILL.md`. When the plugin is enabled for a project and the skill
is assigned to an agent, IdeA injects the skill affordance into that agent's
skill catalogue and serves the body through `idea_skill_read`. This is not
Codex-specific; it is projected by IdeA for every harness that supports IdeA
capabilities, including Claude Code, Codex, OpenCode local and OpenCode cloud.
A Codex plugin is described by `.codex-plugin/plugin.json`. Its `skills` entry
points Codex at skill directories such as `skills/*/SKILL.md`. This metadata is
for Codex plugin installation and skill discovery, not for IdeA runtime plugin
loading.
## Normative Behavior
- Installing or reloading an IdeA plugin makes the package available globally,
but does not automatically enable it in every project.
- Enabling a plugin for a project does not automatically assign every skill or
MCP tool to every agent.
- A plugin skill becomes visible to an agent only when the plugin is enabled for
the project and that skill is assigned to that agent.
- Installing or reloading an IdeA plugin does not automatically install a bundled
Codex plugin, and `.codex-plugin/plugin.json` remains inert for IdeA-native
plugin loading unless a separate bridge explicitly consumes it.
- Bundling `.codex-plugin/plugin.json` is still allowed as distribution content
for Codex-specific workflows, but it is not the multi-harness IdeA contract.
## Required Mechanisms
For multi-harness IdeA agent skills, declare `contributes.skills` in
`idea-plugin.json`, ship the referenced Markdown files inside the package,
install the plugin in IdeA, enable it in the target project, and assign the
skills to the target agents. See
[Project Plugin Assignments](project-plugin-assignments.md) for the project
settings schema and the complete install, enable, assign, and relaunch workflow.
For Codex-specific plugin skills, install the Codex plugin through the Codex
plugin mechanism for the intended scope, then assign or enable those skills
according to Codex/IdeA agent configuration.
Do not rely on `.codex-plugin/plugin.json` as a cross-harness skill propagation
mechanism. Cross-harness plugin skill availability is driven by
`idea-plugin.json` and IdeA project/agent assignments.

View File

@ -1,7 +1,7 @@
# Commands And Feedback # Commands And Feedback
This document is the normative SDK contract for plugin commands that may launch This document is the normative SDK contract for plugin commands, including
background work. commands that open a view or launch background work.
## Contract ## Contract
@ -25,13 +25,17 @@ manifest contribution -> command id -> registered command handler -> optional ta
not by a fake task. not by a fake task.
- Feedback objects must be stable enough for agents and programmatic callers to - Feedback objects must be stable enough for agents and programmatic callers to
parse, and their messages must be readable by humans. parse, and their messages must be readable by humans.
- The current human menu-click UI does not guarantee display of a command - The human menu-click UI presents minimal host-owned feedback for `skipped`
handler return value. Use logs, Work/task state or plugin-owned UI/files when and `failed` results. It intentionally does not expose the raw result payload
a human needs visible feedback today. or assume which tool (Git, ADB, Gradle, and so on) the plugin uses.
For successful actions and richer status, open or focus a plugin layout/window
from the handler with
`ctx.services.windows.open({ layoutType, state })`.
## Preconditions ## Preconditions
Check preconditions before calling `runCommand()`: Check preconditions before calling `runCommand()`. These are task-launch
preconditions, not prerequisites for opening an explanatory view:
- `ctx.services` exists. Plugins need the `ui` or `tooling` capability for the - `ctx.services` exists. Plugins need the `ui` or `tooling` capability for the
service facade. service facade.
@ -42,8 +46,18 @@ Check preconditions before calling `runCommand()`:
- `ownerAgentId` is a real agent id when the work is owned by an agent workflow. - `ownerAgentId` is a real agent id when the work is owned by an agent workflow.
Do not use all-zero placeholders in production. Do not use all-zero placeholders in production.
- `cwd` is relative to the project root. - `cwd` is relative to the project root.
- `command` and `args` do not resolve package-relative plugin resources
implicitly. Build an explicit absolute script path from `ctx.pluginRoot` when
launching read-only files shipped in the installed package.
- Do not use `ctx.pluginRoot` as `cwd`; `cwd` remains relative to the project root.
- `command` and `args` are separate values. Do not shell-join user input. - `command` and `args` are separate values. Do not shell-join user input.
A command whose purpose is to open or focus a diagnostic/configuration view
should call `services.windows.open(...)` before probing optional executables.
Missing Git, ADB, Gradle, or another tool must not block that view when the view
can explain the degraded state and the next step. Probe tools from the view (or
from its data loader) and render the degraded state there.
If any required precondition fails, return a skipped result: If any required precondition fails, return a skipped result:
```ts ```ts
@ -61,21 +75,33 @@ with equivalent fields:
```ts ```ts
type CommandFeedback = type CommandFeedback =
| { status: "opened"; message?: string; alreadyOpen: boolean }
| { status: "skipped"; reason: string; message: string } | { status: "skipped"; reason: string; message: string }
| { status: "failed"; reason?: string; message?: string }
| { status: "launched"; taskId: string; state: string; message: string }; | { status: "launched"; taskId: string; state: string; message: string };
``` ```
Use the same `status` vocabulary consistently: Use the same `status` vocabulary consistently:
- `opened`: the requested view was opened or focused. `alreadyOpen: true` is a
successful focus operation, not a failure or skip.
- `skipped`: no background task was created. - `skipped`: no background task was created.
- `launched`: a task was created; inspect the task state for later progress. - `launched`: a task was created; inspect the task state for later progress.
- `failed`: the handler itself failed before it could return normally. - `failed`: the handler caught a failure and returned structured failure
feedback. An uncaught/rejected handler is also treated as a failure by the
host.
Visible surfaces are intentionally distinct: Visible surfaces are intentionally distinct:
- Command return value: immediate feedback for programmatic callers, agents and - Command return value: immediate feedback for programmatic callers and agents.
future host surfaces. It is not a guaranteed visible UI surface for current The human menu host maps `skipped` and `failed` results to concise feedback;
human menu clicks. plugin-specific fields and messages are not displayed verbatim. `opened`
results, including `alreadyOpen: true`, succeed without an alert. The host
logs structured diagnostics with `pluginId`, `commandId`, `status`, `reason`
and `message`; raw objects, stack traces and internal ids are not user-facing.
- Plugin layout/window: the normative visible feedback surface for menu actions
that need to show status, results or next steps to a human. The layout must be
declared in `idea-plugin.json` and registered during activation.
- Plugin logs: diagnostics for developers and operators. - Plugin logs: diagnostics for developers and operators.
- Work/background-task surfaces: only for tasks actually launched through - Work/background-task surfaces: only for tasks actually launched through
`runCommand()`. `runCommand()`.
@ -105,6 +131,45 @@ state when invoked instead of assuming a watch was installed at activation.
## Example ## Example
Visible menu feedback:
```tsx
import type { IdeAPluginModule, PluginLayoutProps } from "@idea/plugin-sdk";
function HealthView(_props: PluginLayoutProps<{ source?: string }>) {
return <section>Unity tools are ready.</section>;
}
const plugin: IdeAPluginModule = {
activate(ctx) {
ctx.layouts?.register({
type: "unity-plugin.health",
component: HealthView
});
ctx.commands?.registerCommand("unity-plugin.health", async () => {
const win = await ctx.services?.windows.open({
layoutType: "unity-plugin.health",
state: { source: "menu" }
});
return win
? { status: "opened", message: "Opened Unity Health.", alreadyOpen: win.alreadyOpen }
: { status: "skipped", reason: "ui-service-unavailable", message: "UI service unavailable." };
});
}
};
export default plugin;
```
The `opened` branch above must remain successful even when an optional tool
diagnosed by `HealthView` is absent. If `windows.open(...)` itself rejects, log
the technical cause and either let the promise reject or return `failed`; the
host will show generic, plugin-neutral feedback rather than the raw message.
Background task feedback:
```ts ```ts
const project = await ctx.services?.workspace.getCurrentProject(); const project = await ctx.services?.workspace.getCurrentProject();
if (!project) { if (!project) {
@ -158,3 +223,20 @@ await ctx.services?.tasks.runCommand({
This creates misleading Work history, uses a placeholder owner and turns a This creates misleading Work history, uses a placeholder owner and turns a
precondition failure into a fake task. Return `status: "skipped"` instead. precondition failure into a fake task. Return `status: "skipped"` instead.
Do not try to reach packaged plugin scripts through workspace-relative paths:
```ts
await ctx.services?.tasks.runCommand({
projectId: project.id,
ownerAgentId,
label: "Run packaged tests",
command: "scripts/run-unity-tests.sh",
cwd: "."
});
```
`scripts/run-unity-tests.sh` above is resolved like any other executable visible
from the project task environment; it is not resolved relative to the plugin
package. Use a project-owned script or an executable available on `PATH` until a
dedicated packaged-resource API exists.

View File

@ -32,6 +32,26 @@ ctx.layouts?.register({
}); });
``` ```
The `type` must exactly match one entry in `contributes.layouts`. The host
rejects registrations for undeclared layout types. A plugin that opens this
layout for human feedback must also declare the `ui` capability so the UI
runtime and window services are part of the public contract.
```json
{
"capabilities": ["ui", "tooling"],
"contributes": {
"layouts": [
{
"type": "hello-plugin.dashboard",
"label": "Dashboard",
"component": "Dashboard"
}
]
}
}
```
## Props ## Props
- `projectId`: project hosting the layout. - `projectId`: project hosting the layout.

View File

@ -115,6 +115,7 @@ development loop.
"capabilities": ["ui", "tooling"], "capabilities": ["ui", "tooling"],
"activationScope": "app", "activationScope": "app",
"contributes": { "contributes": {
"skills": [],
"menus": [], "menus": [],
"menuItems": [], "menuItems": [],
"layouts": [], "layouts": [],
@ -140,20 +141,274 @@ development loop.
- `activationScope`: `"app"` by default, or `"project"` when activation needs a - `activationScope`: `"app"` by default, or `"project"` when activation needs a
focused project immediately. focused project immediately.
- `capabilities`: `"ui"`, `"tooling"` and/or `"mcp"`. - `capabilities`: `"ui"`, `"tooling"` and/or `"mcp"`.
- `permissions.workspaceExec`: explicit generic executable rules used by
`ctx.services.workspace.execTool()`.
## Workspace execution permissions
Short project-scoped executable access is denied unless the active plugin
manifest contains a matching rule:
```json
{
"permissions": {
"workspaceExec": {
"allow": [
{
"command": "tool",
"args": ["status", "*"],
"cwd": ".",
"effect": "read"
}
]
}
}
}
```
`command` is a bare ASCII executable name; paths and shell command lines are
rejected. `args` is an argv pattern: literal tokens match exactly, `*` matches
one token, and a final `*` matches the remaining suffix (including no token).
`cwd` is an exact normalized project-relative directory; `*` permits any cwd
that the host still proves is under the project root. `effect` is one of
`read`, `write`, `network`, or `destructive` and drives sandbox/audit policy.
The manifest declares generic process access only; it does not add native SCM
semantics to IdeA.
## Contributions ## Contributions
`contributes.skills` declares read-only agent skills shipped inside the IdeA
plugin package. These skills are IdeA-native: IdeA can expose them in project
plugin settings, assign them to agents, inject them into the agent skill
catalogue, and serve their Markdown body through `idea_skill_read` for every
supported harness that receives IdeA capabilities, including Claude Code,
Codex, OpenCode local and OpenCode cloud.
```json
{
"contributes": {
"skills": [
{
"id": "unity.build-debug",
"name": "unity-build-debug",
"description": "Diagnose Unity build failures.",
"kind": "workflow",
"path": "skills/unity-build-debug/SKILL.md"
}
]
}
}
```
Skill contribution rules:
- `id` is stable within the plugin and is used by project/agent assignments.
- `name` is the agent-facing name shown in the injected skill catalogue and
accepted by `idea_skill_read`.
- `description` is the short affordance shown before the agent loads the skill.
- `kind` defaults to `"workflow"`; `"reference"` is also supported.
- `path` must be a package-relative Markdown file path ending in `.md`.
- Installing a plugin globally is not enough: a project must enable the plugin,
then assign the skill to the target agent.
See [Project Plugin Assignments](project-plugin-assignments.md) for the
`.ideai/project-plugins.json` schema, validation rules, assignment order, and
agent relaunch requirements.
`contributes.layouts` is the only surface for plugin layouts. The same layout `contributes.layouts` is the only surface for plugin layouts. The same layout
contribution can be mounted inside an IdeA layout cell or opened in a detached OS contribution can be mounted inside an IdeA layout cell or opened in a detached OS
window. Do not add a separate manifest contribution type for windows. window. Do not add a separate manifest contribution type for windows.
For a command that opens a visible plugin window, declare both the menu command
and the layout type:
```json
{
"ideaPluginManifestVersion": 1,
"id": "com.example.unity-plugin",
"displayName": "Unity Developer Tools",
"publisher": "Example",
"version": "0.1.0",
"description": "Unity integration for IdeA.",
"main": "dist/index.js",
"engines": {
"idea": ">=0.1.0"
},
"trustLevel": "full",
"capabilities": ["ui", "tooling"],
"activationScope": "app",
"contributes": {
"menus": [
{
"id": "unity-plugin.menu",
"label": "Unity Developer Tools",
"topLevel": true,
"order": 100
}
],
"menuItems": [
{
"id": "unity-plugin.health.item",
"targetMenuId": "unity-plugin.menu",
"label": "Health",
"command": "unity-plugin.health",
"order": 10,
"when": "projectOpen"
}
],
"layouts": [
{
"type": "unity-plugin.health",
"label": "Unity Health",
"component": "UnityHealth",
"order": 10
}
]
}
}
```
`capabilities: ["ui"]` is required when a plugin relies on layouts or windows as
human-facing UI. Add `"tooling"` when the same plugin uses workspace, task,
tooling or terminal services. The `contributes.layouts[].type` value is the
public id used by both `ctx.layouts.register({ type, component })` and
`ctx.services.windows.open({ layoutType })`.
Contribution ids are part of the runtime contract: Contribution ids are part of the runtime contract:
- commands can only register ids declared by `contributes.menuItems[*].command`; - there is no separate `contributes.commands` collection; commands can only
register ids declared by `contributes.menuItems[*].command` or
`contributes.slashCommands[*].command`;
- layouts can only register `type` values declared by `contributes.layouts`; - layouts can only register `type` values declared by `contributes.layouts`;
- `services.windows.open({ layoutType })` only accepts a layout type declared by - `services.windows.open({ layoutType })` only accepts a layout type declared by
the calling plugin. the calling plugin.
## MCP Servers And Agent Tools
`contributes.mcpServers` is the IdeA-native contract for plugin-provided
agent tools. A plugin does not register individual MCP tools from `activate(ctx)`.
Instead, the plugin ships or references a stdio MCP server process, declares it
in the manifest, and that server advertises its own tools through the MCP
`tools/list` protocol.
```json
{
"capabilities": ["mcp"],
"contributes": {
"mcpServers": [
{
"id": "unity-editor",
"displayName": "Unity Editor Tools",
"command": "scripts/unity-mcp-server.mjs",
"args": [],
"cwd": "${pluginRoot}",
"transport": "stdio",
"autoStart": true,
"tools": [
{
"name": "unity_get_scene",
"description": "Read the active Unity scene."
},
{
"name": "unity_run_tests"
}
]
}
]
}
}
```
MCP server contribution rules:
- `id` is stable within the plugin and is used by project/agent assignments.
- `displayName` is the human-readable server name shown in plugin settings.
- `transport` must currently be `"stdio"`.
- `autoStart: true` is required for IdeA to start the server during plugin MCP
reconciliation. Non-auto-start servers are manifest metadata only today.
- A relative `command` is resolved under the installed plugin package root.
- A literal absolute `command` is rejected unless `allowAbsoluteCommand: true`
is set. Use this only for an intentional dependency on a host binary.
- A `command` that becomes absolute after `${pluginRoot}` or `${appDataDir}`
substitution is allowed and is passed through as substituted.
- `cwd` defaults to `${pluginRoot}` when omitted.
- `tools` is optional static display metadata for IdeA's capability-assignment
UI. Each entry has an exact non-empty `name` and an optional non-empty
`description`; names must be unique within the server.
- This metadata does not replace or cache MCP `tools/list`, and declaring a tool
here does not register it or prove that the running server exposes it.
The tools an agent can call are assigned with the triplet
`pluginId`/`serverId`/`toolName` in project plugin settings. `toolName` must
match a tool name advertised by the MCP server. IdeA does not currently provide
a runtime API such as `ctx.mcp.registerTool()` or `ctx.mcp.registerServer()`.
For multi-harness workflows, keep `contributes.skills` as instructions and
expose executable behavior through assigned MCP tools. Do not make skill
Markdown depend on filesystem paths relative to the Markdown returned by
`idea_skill_read`; that body is served as read-only content, not mounted as a
working directory. If a skill needs packaged scripts, put the script behind a
manifest-declared MCP server and tell the agent to call the assigned tool.
## MCP Server Paths
`contributes.mcpServers` entries are resolved by the host before starting a
declared MCP server. In `command`, `args`, `env` and `cwd`, the host expands:
- `${pluginRoot}` to the installed plugin package root.
- `${appDataDir}` to the host-owned application data directory.
There is currently no `${node}`, `${runtimeNode}`, `${hostNode}` substitution
and no `runtime` field. IdeA does not provide a plugin-scoped Node.js runtime
for MCP servers.
This string substitution is limited to manifest-declared MCP server startup.
Runtime handlers receive the same installed package location separately as
`ctx.pluginRoot`; `ctx.services.tasks.runCommand()` does not perform placeholder
substitution, and workspace APIs remain project-confined.
`command` is not shell syntax. IdeA starts it directly and passes `args`
separately. Do not rely on shell startup files, shell aliases, or a user's login
shell `PATH`.
Prefer a packaged executable for plugin-owned MCP servers:
```json
{
"command": "scripts/unity-mcp-server",
"transport": "stdio",
"autoStart": true
}
```
Because the command is package-relative, IdeA resolves it to
`${pluginRoot}/scripts/unity-mcp-server`. If this file is a script, its shebang
and executable bit must be valid in the installed package. A JavaScript shebang
such as `#!/usr/bin/env node` still depends on a `node` binary visible to the
IdeA app process, not necessarily the user's interactive shell.
For JavaScript MCP servers that must not depend on the user's shell `PATH`, use
one of these approaches:
- Ship an executable server that includes its runtime, or install a managed
runtime under plugin/app-owned storage and point `command` at that executable
with `${pluginRoot}` or `${appDataDir}`.
- Declare an explicit host dependency with an absolute command such as
`/usr/bin/node`, set `allowAbsoluteCommand: true`, and keep plugin-owned
script paths in `args`.
Avoid `command: "node"` for a packaged MCP server. With
`allowAbsoluteCommand: false`, it is treated as package-relative and resolves to
`${pluginRoot}/node`. With `allowAbsoluteCommand: true`, it is passed as a host
command name and depends on the environment inherited by IdeA, which may differ
from an interactive shell.
The same rule applies to shell wrappers. `command: "bash"` resolves to
`${pluginRoot}/bash` unless absolute-command handling is explicitly enabled. Use
a package-relative wrapper executable, or use an absolute shell such as
`/bin/bash` with `allowAbsoluteCommand: true` when that host dependency is
intentional.
## Validation ## Validation
Use the SDK validator in tests or build tooling: Use the SDK validator in tests or build tooling:

View File

@ -33,7 +33,10 @@ Then register the command id:
```ts ```ts
export function activate(ctx: ActivateContext): void { export function activate(ctx: ActivateContext): void {
const disposable = ctx.commands?.registerCommand("hello-plugin.open", async () => { const disposable = ctx.commands?.registerCommand("hello-plugin.open", async () => {
await ctx.services?.windows.open({ layoutType: "hello-plugin.dashboard" }); const win = await ctx.services?.windows.open({ layoutType: "hello-plugin.dashboard" });
return win
? { status: "opened", alreadyOpen: win.alreadyOpen }
: { status: "skipped", reason: "ui-service-unavailable", message: "UI service unavailable." };
}); });
if (disposable) ctx.subscriptions.push(disposable); if (disposable) ctx.subscriptions.push(disposable);
} }
@ -41,9 +44,22 @@ export function activate(ctx: ActivateContext): void {
## Rules ## Rules
- A command can only be registered if at least one manifest menu item declares - There is no separate `contributes.commands` collection. A command can only be
that exact `command` id. registered if at least one manifest `menuItems[].command` or
- Missing command handlers are a no-op when the user clicks the menu item. `slashCommands[].command` entry declares that exact id.
- A missing command handler is surfaced by the host as a retryable reload error;
menu clicks must never fail silently.
- A registered handler's rejected promise is not swallowed: the host logs the
plugin id, command id, status, reason and message, then presents a generic
retryable error without raw payloads. In
particular, return/await `services.windows.open(...)` so native-window
failures follow this path.
- An open/focus handler returns `status: "opened"` whether `alreadyOpen` is
`false` (new window) or `true` (existing window focused). Neither success
produces an alert.
- Keep diagnostic views reachable when optional executables such as Git, ADB or
Gradle are missing. Let the view explain that degraded state instead of
skipping the menu action before opening it.
- A handler owns precondition checks and feedback. See - A handler owns precondition checks and feedback. See
[Commands And Feedback](commands-and-feedback.md). [Commands And Feedback](commands-and-feedback.md).
- Menu item `when` expressions are evaluated by the host; unsupported or false - Menu item `when` expressions are evaluated by the host; unsupported or false

View File

@ -43,6 +43,25 @@ IdeA resolves React/ReactDOM bare imports to the host instance. Other bare
dependencies are not host-resolved. Bundle or vendor third-party dependencies dependencies are not host-resolved. Bundle or vendor third-party dependencies
other than React/ReactDOM into package-relative files. other than React/ReactDOM into package-relative files.
## Packaged Executables And Runtimes
Manifest-declared MCP servers are launched from `contributes.mcpServers` by
starting the declared `command` directly. IdeA does not inject a shell and does
not provide a plugin-scoped Node.js runtime.
For a packaged MCP server, prefer a package-relative executable such as
`scripts/my-plugin-mcp-server`. The installed file must keep the executable bit
and, when it is a script, a valid shebang. A JavaScript server with
`#!/usr/bin/env node` still requires `node` in the environment inherited by the
IdeA app process; this is not guaranteed to match the user's interactive shell,
especially when Node is provided by shell startup tooling.
For reproducible JavaScript MCP servers, either ship/install a runtime under the
plugin package or app-owned storage and point the manifest at that executable,
or declare an explicit absolute host dependency with `allowAbsoluteCommand:
true`. Avoid relying on a bare `command: "node"` in distributed or multi-harness
plugins.
## TypeScript Build ## TypeScript Build
The hello plugin uses plain `tsc`: The hello plugin uses plain `tsc`:
@ -102,8 +121,23 @@ After editing plugin source files, rebuild the plugin output first:
npm run build npm run build
``` ```
Then ask an IdeA agent that has the plugin administration tool available to run An IdeA agent can install a development plugin directory only when that agent has
`idea_plugin_reload` with the installed plugin id: the plugin administration MCP tool explicitly allowed. The public tool is
`idea_plugin_install_from_directory`:
```json
{
"path": "relative/path/to/hello-plugin",
"expectedPluginId": "com.example.hello-plugin"
}
```
The path must resolve under the requester's project root. This installs the
directory as the recorded development source for later reloads.
After source edits, rebuild the plugin output and ask an IdeA agent that has the
plugin administration tool available to run `idea_plugin_reload` with the
installed plugin id:
```json ```json
{ {
@ -122,7 +156,17 @@ reload. Archive installs are fixed package snapshots. They are appropriate for
distribution, but the reload command is only defined for plugins installed from distribution, but the reload command is only defined for plugins installed from
a directory source. a directory source.
Current limitation: backend registry state and plugin MCP/tool contributions are On reload, `plugin_reloaded` tells the UI to rebuild the plugin runtime registry.
reloaded without restarting IdeA. Frontend React contributions that are already When the package content hash changes, plugin ESM URLs change and React layout
loaded in the current UI session may keep their existing module instance until components are loaded from the new package. Existing plugin layout instances may
the relevant plugin surface is recreated or the app session is restarted. remount and lose local React state; host-persisted layout state written through
`setState` remains the durable state channel.
JavaScript module instances cannot be forcibly removed from the browser ESM
cache. Register handlers, layouts, watches and subscriptions through the SDK
registries and push disposable side effects into `ctx.subscriptions`; global
module side effects can otherwise survive a reload. If the package hash did not
change, the reload is idempotent and may reuse the same module instance.
IdeA runtime plugin installation is separate from Codex plugin/skill discovery.
See [Codex Skills And IdeA Plugins](codex-skills-and-idea-plugins.md).

View File

@ -0,0 +1,169 @@
# Project Plugin Assignments
IdeA installs plugin packages globally, but enables their capabilities per
project and assigns agent-facing capabilities per agent. Project choices are
stored in the project-owned `.ideai/project-plugins.json` file.
The normal workflow is:
1. Install the plugin globally in IdeA.
2. Enable the installed plugin for the target project.
3. Assign selected plugin skills and MCP tools to each target agent.
4. Relaunch the agent so its capability snapshot includes the new assignments.
Installation alone does not enable a plugin in a project. Enabling a plugin does
not automatically grant all of its capabilities to every agent.
## Version 1 Schema
The current schema version is `1`. A complete example is:
```json
{
"version": 1,
"plugins": [
{
"pluginId": "com.example.unity-plugin",
"enabled": true
}
],
"agents": [
{
"agentId": "2a0e42e2-3c5f-4f30-a679-034b460cd59e",
"skills": [
{
"pluginId": "com.example.unity-plugin",
"skillId": "unity.build-debug"
}
],
"tools": [
{
"pluginId": "com.example.unity-plugin",
"serverId": "unity-editor",
"toolName": "unity_build"
}
]
}
]
}
```
The top-level fields are:
- `version`: required schema version. It must currently be `1`; unsupported
versions are rejected.
- `plugins`: project activation entries. Each entry contains the stable global
`pluginId` and an `enabled` boolean.
- `agents`: per-agent assignments. Each entry identifies an IdeA project agent
by `agentId` and contains ordered `skills` and `tools` arrays.
An agent skill reference contains:
- `pluginId`: the id from the owning plugin's `idea-plugin.json`.
- `skillId`: the stable `contributes.skills[].id` within that plugin.
An agent MCP tool reference contains:
- `pluginId`: the id from the owning plugin's `idea-plugin.json`.
- `serverId`: the stable `contributes.mcpServers[].id` within that plugin.
- `toolName`: the exact tool name advertised by that MCP server. The UI accepts
this name as text because a dynamic MCP tool catalogue is not yet available.
IdeA resolves plugin MCP assignments to manifest-declared servers, not to
in-process handlers registered from `activate(ctx)`. A tool assignment is useful
only when the plugin is enabled, runtime-active, the referenced MCP server has
`autoStart: true`, and the server process advertises the named tool through MCP
`tools/list`.
Missing `plugins`, `agents`, `skills`, or `tools` arrays default to empty arrays
when IdeA reads the document. Prefer writing them explicitly for clarity and
forward-compatible review.
## Assignment And Injection Order
Array order is significant. IdeA keeps plugin skill assignments in the order in
which they were added. At agent launch, IdeA resolves the agent's native skills
first, in agent-manifest order, then its plugin skills in the order stored in
`agents[].skills`. Unavailable assignments are skipped without reordering the
remaining skills.
The resolved skill catalogue is injected into the agent's orchestration context
before the agent persona. Each supported harness receives the same ordered
IdeA-native capability snapshot. The skill body remains read-only package
content and is available through `idea_skill_read` only when that skill was
resolved for the launched agent.
## Tombstones
Plugin activation entries and agent assignments are intentionally retained when
a plugin, skill, or MCP server is temporarily missing. These dangling entries
are tombstones: they preserve project intent across uninstall/reinstall or a
temporary manifest change.
A tombstone is inert. IdeA skips it when resolving capabilities, and the project
settings UI marks stale skill or server references as missing or obsolete. The
UI lets users remove those references explicitly. Reinstalling and activating a
matching plugin makes a retained assignment resolvable again.
Disabling a plugin also keeps its assignments. They become effective again only
after the plugin is enabled and runtime-active for the project.
## UI And API Validation
Prefer the project plugin settings UI or its backing API commands over manual
editing. They apply the following rules:
- Enabling requires the plugin package to be installed globally. Disabling a
missing plugin is allowed so a retained activation can be cleared.
- Assigning a skill requires an existing project agent, an enabled and
runtime-active plugin, and a matching `contributes.skills[].id`.
- Assigning a tool requires an existing project agent, an enabled and
runtime-active plugin, a matching `contributes.mcpServers[].id`, and a
non-empty `toolName`.
- Assign and unassign operations are idempotent. Assigning an existing reference
does not duplicate it; unassigning a missing reference is harmless.
- Unassign operations remain available for tombstones, so stale references can
be removed even when their plugin capability no longer resolves.
The project settings read API returns both the persisted document and the
currently installed plugin manifests. The UI uses this catalogue to offer only
skills and MCP servers from plugins enabled for the project, while still
surfacing persisted tombstones.
## Manual Editing
The file is project-owned and may be edited manually, for example in version
control or automation. Stop or relaunch affected agents around the edit, keep
the document valid JSON, preserve `version: 1`, use the real project agent UUID,
and copy plugin, skill, and server ids exactly from the plugin manifest.
Manual editing bypasses the mutation checks described above. A syntactically
valid document may therefore contain duplicates, unknown agents, disabled or
missing plugins, unknown capability ids, or an invalid tool name. Such entries
may remain inert or fail later operations. Use the UI/API whenever possible to
receive immediate validation and idempotent updates.
## Reloading Plugins And Relaunching Agents
These actions solve different problems:
- Reload the plugin after rebuilding or replacing its installed package. Reload
refreshes the plugin manifest, runtime bundle, contributions, and packaged
skill Markdown. It does not change project activation or agent assignments.
Plugins do not call a contribution-refresh API themselves: rebuild or replace
the installed package, then use the host reload action.
- Relaunch the agent after enabling or disabling a project plugin, changing its
skill/tool assignments, or manually editing `.ideai/project-plugins.json`.
Agent capabilities are resolved into a launch-time snapshot, so an already
running agent does not acquire assignment changes retroactively.
When both package content and assignments changed, reload the plugin first,
confirm it is enabled for the project, update the assignments, then relaunch the
agent.
If a plugin skill describes a workflow backed by packaged scripts, prefer this
flow: ship the scripts in the plugin package, expose them through a
manifest-declared MCP server, assign the specific MCP tools to the target agent,
and have the skill instruct the agent to call those tools. Avoid relative paths
from the skill Markdown to packaged scripts; the Markdown served by
`idea_skill_read` is content, not a stable filesystem anchor.

View File

@ -29,6 +29,38 @@ Key APIs:
- `stat(path, projectId?)` - `stat(path, projectId?)`
- `watch(path, handler, projectId?)` - `watch(path, handler, projectId?)`
- `queryStructure(query?)` - `queryStructure(query?)`
- `execTool(request)`
### Short external tools
`services.workspace.execTool()` runs a short external executable already available
to the host. It is generic: IdeA does not expose Git, SVN, formatter, or linter
semantics. The plugin owns the arguments and parses the returned output.
```ts
const result = await ctx.services!.workspace.execTool({
projectId,
command: "tool",
args: ["status", "--short"],
cwd: ".",
timeoutMs: 5_000,
maxStdoutBytes: 256_000,
maxStderrBytes: 64_000
});
```
The host passes `command` and `args` directly without a shell. The plugin must
declare a matching `permissions.workspaceExec.allow` rule, be active for the
project, and use a project-relative `cwd`. IdeA canonicalizes the cwd, bounds
time/stdin/output/concurrency, exposes only an allowlisted environment, applies
the project filesystem sandbox where supported, and audits metadata without
captured stdout/stderr contents. This API returns output directly and never
creates a visible Work/background-task entry.
Current host maxima are 30 seconds, 1,000,000 stdout bytes, 256,000 stderr bytes,
and 64,000 stdin bytes. Requested output/time values above a maximum are clamped;
zero is rejected. Output is returned as lossy UTF-8 with explicit truncation
flags.
## Tasks ## Tasks
@ -37,6 +69,32 @@ Key APIs:
Use `runCommand()` only after preconditions are satisfied. `ownerAgentId` must be Use `runCommand()` only after preconditions are satisfied. `ownerAgentId` must be
a real agent id when work belongs to an agent workflow. a real agent id when work belongs to an agent workflow.
`runCommand()` keeps its `cwd` project/workspace scoped: `cwd` must be a relative
path under the project root. The host does not implicitly resolve `command` or
`args` against the plugin package. Use the absolute `ctx.pluginRoot` supplied at
activation time to construct an explicit path to a packaged script, and pass
that path as `command` or as an argument to its interpreter. Treat package files
as read-only. Unlike manifest-declared MCP servers, `runCommand()` does not
substitute `${pluginRoot}` or `${appDataDir}`.
```ts
const script = `${ctx.pluginRoot.replace(/[\\/]+$/, "")}/scripts/check.mjs`;
await ctx.services.tasks.runCommand({
projectId,
ownerAgentId,
label: "Run packaged check",
command: "node",
args: [script],
cwd: "."
});
```
This task example uses `node` as a host executable visible to the task
environment. That is not the contract for `contributes.mcpServers`: MCP server
`command` values are resolved by the plugin manifest rules documented in
[Manifest](manifest.md), where a bare `command: "node"` is package-relative
unless external host-command handling is explicitly enabled.
## Tooling ## Tooling
`services.tooling.diagnose()` checks executables, environment values and files `services.tooling.diagnose()` checks executables, environment values and files
@ -66,3 +124,17 @@ locations. Use it for configuration the user expects to review/version.
`services.windows.open({ layoutType, state? })` opens a detached OS window for `services.windows.open({ layoutType, state? })` opens a detached OS window for
one of the calling plugin's declared layout contributions. See one of the calling plugin's declared layout contributions. See
[Windows](windows.md). [Windows](windows.md).
Resolving this promise means the window was opened or focused. Its
`alreadyOpen` flag reports which happened; both outcomes are successful and a
menu handler should return `status: "opened"`. Do not gate a diagnostic window
on optional tool availability when that window can present the unavailable
state. A rejection means the window operation itself was impossible and may be
logged or returned as `status: "failed"`; do not expose its raw error to users.
## Scheduled tasks
Project scheduled tasks are exposed through native Tauri/HTTP commands and the
native `idea_scheduled_task_*` MCP tools. They are intentionally not available as
an `activate(ctx)` plugin service in V1, and plugins must not emulate them with
system cron or repository-owned auto-executed files.

View File

@ -10,11 +10,27 @@ await ctx.services?.windows.open({
}); });
``` ```
`windows.open` is the public API for commands that need to make feedback visible
to a human. A command handler may still return structured data, but menu clicks
do not currently guarantee that return value is displayed.
## Contract ## Contract
- `layoutType` must match a layout type declared by the calling plugin. - `layoutType` must match a layout type declared by the calling plugin.
- The plugin must declare `capabilities: ["ui"]` when it relies on plugin
layouts/windows as a human-facing surface. Add `"tooling"` only when the
plugin also uses tooling/workspace/task services.
- There is no `contributes.windows` manifest key. Windows always host an
existing `contributes.layouts` layout.
- The host validates the plugin is runtime-active and the layout exists before - The host validates the plugin is runtime-active and the layout exists before
opening or focusing the window. opening or focusing the window.
- The calling plugin package must still be installed, enabled globally and
enabled for the focused project. A stale command from a replaced runtime is
rejected instead of opening a window under another plugin's identity.
- `windows.open(...)` rejects its promise when any validation or native-window
operation fails. Let that rejection reach the command handler (or catch it,
log it and return explicit failed feedback); the IdeA menu host logs the
rejection and shows a retryable error to the user.
- Reopening the same plugin/layout pair focuses the existing window. - Reopening the same plugin/layout pair focuses the existing window.
- `state` is JSON-serializable initial window state. The mounted component can - `state` is JSON-serializable initial window state. The mounted component can
later call `setState(next)` to update its local host state. later call `setState(next)` to update its local host state.
@ -38,9 +54,14 @@ The returned `label` is a host-owned window identity. Treat it as opaque.
```ts ```ts
ctx.commands?.registerCommand("hello-plugin.open-dashboard", async () => { ctx.commands?.registerCommand("hello-plugin.open-dashboard", async () => {
return ctx.services?.windows.open({ if (!ctx.services) {
return { status: "skipped", reason: "ui-service-unavailable", message: "UI service unavailable." };
}
const win = await ctx.services.windows.open({
layoutType: "hello-plugin.dashboard", layoutType: "hello-plugin.dashboard",
state: { source: "menu" } state: { source: "menu" }
}); });
return { status: "opened", message: "Opened dashboard.", alreadyOpen: win.alreadyOpen };
}); });
``` ```

View File

@ -16,6 +16,8 @@ It exercises the current plugin primitives end to end:
`ctx.services.windows.open(...)` when services are available. `ctx.services.windows.open(...)` when services are available.
- plugin-owned storage: activation count, command run count and initialization flag; - plugin-owned storage: activation count, command run count and initialization flag;
- tooling capability: logs the focused workspace project when `ctx.services` is available. - tooling capability: logs the focused workspace project when `ctx.services` is available.
- packaged command script: resolves `scripts/hello-task.mjs` from the official
`ctx.pluginRoot` and launches it with the project kept as the task working directory.
The source is intentionally split across multiple TypeScript modules: The source is intentionally split across multiple TypeScript modules:
@ -23,6 +25,7 @@ The source is intentionally split across multiple TypeScript modules:
- `src/constants.ts` owns shared command/layout identifiers; - `src/constants.ts` owns shared command/layout identifiers;
- `src/core/layout.tsx` and `src/core/workspace.ts` hold feature logic. - `src/core/layout.tsx` and `src/core/workspace.ts` hold feature logic.
- `src/core/storage.ts` keeps plugin-owned counters and flags in `ctx.storage`. - `src/core/storage.ts` keeps plugin-owned counters and flags in `ctx.storage`.
- `scripts/hello-task.mjs` is a read-only runtime resource included in the archive.
The build uses plain `tsc`; it does not bundle the plugin into one file. The archive includes all The build uses plain `tsc`; it does not bundle the plugin into one file. The archive includes all
compiled `dist/**/*.js` files so IdeA can load `dist/index.js` and serve its package-relative imports compiled `dist/**/*.js` files so IdeA can load `dist/index.js` and serve its package-relative imports
@ -36,8 +39,8 @@ npm run package:hello-plugin
``` ```
The installable archive is emitted at `examples/hello-plugin/build/hello-plugin-0.1.0.zip`. The installable archive is emitted at `examples/hello-plugin/build/hello-plugin-0.1.0.zip`.
It contains `idea-plugin.json` at the ZIP root and the compiled multi-file ESM output under `dist/`, It contains `idea-plugin.json` at the ZIP root, the compiled multi-file ESM output under `dist/`,
including `dist/index.js`, matching the manifest `main` field. including `dist/index.js`, and the packaged handler under `scripts/hello-task.mjs`.
## Diagnostics ## Diagnostics

View File

@ -0,0 +1 @@
console.log("hello from a script packaged with @idea/plugin-sdk");

View File

@ -122,12 +122,13 @@ export async function runHelloCommandTask(ctx: ActivateContext): Promise<HelloCo
return feedback; return feedback;
} }
const pluginRoot = ctx.pluginRoot.replace(/[\\/]+$/, "");
const task = await ctx.services.tasks.runCommand({ const task = await ctx.services.tasks.runCommand({
projectId: project.id, projectId: project.id,
ownerAgentId, ownerAgentId,
label: "Hello plugin command", label: "Hello plugin command",
command: "echo", command: "node",
args: ["hello from @idea/plugin-sdk"], args: [`${pluginRoot}/scripts/hello-task.mjs`],
cwd: ".", cwd: ".",
recordOnly: true recordOnly: true
}); });

View File

@ -18,10 +18,11 @@
], ],
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"test:manifest": "npm run build && node scripts/check-manifest-tools.mjs",
"build:hello-plugin": "tsc -p examples/hello-plugin/tsconfig.build.json", "build:hello-plugin": "tsc -p examples/hello-plugin/tsconfig.build.json",
"package:hello-plugin": "npm run build && npm run build:hello-plugin && node scripts/package-hello-plugin.mjs", "package:hello-plugin": "npm run build && npm run build:hello-plugin && node scripts/package-hello-plugin.mjs",
"typecheck:examples": "tsc -p examples/hello-plugin/tsconfig.json --noEmit", "typecheck:examples": "tsc -p examples/hello-plugin/tsconfig.json --noEmit",
"check": "npm run build && npm run typecheck:examples && npm run package:hello-plugin" "check": "npm run test:manifest && npm run typecheck:examples && npm run package:hello-plugin"
}, },
"keywords": [ "keywords": [
"idea", "idea",

View File

@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import { validatePluginManifest } from "../dist/index.js";
const base = {
ideaPluginManifestVersion: 1,
id: "dev.example.tools",
displayName: "Tools",
version: "1.0.0",
main: "dist/index.js",
trustLevel: "full",
capabilities: ["mcp"],
contributes: {
mcpServers: [
{
id: "server",
displayName: "Server",
command: "bin/server",
transport: "stdio"
}
]
}
};
assert.equal(validatePluginManifest(base).success, true, "tools metadata remains optional");
const withTools = structuredClone(base);
withTools.contributes.mcpServers[0].tools = [
{ name: "read_scene", description: "Read the active scene." },
{ name: "run_tests" }
];
assert.equal(validatePluginManifest(withTools).success, true, "valid tools metadata is accepted");
const duplicate = structuredClone(withTools);
duplicate.contributes.mcpServers[0].tools.push({ name: "read_scene" });
const duplicateResult = validatePluginManifest(duplicate);
assert.equal(duplicateResult.success, false, "duplicate tool metadata is rejected");
assert.ok(duplicateResult.errors.some((error) => error.includes("must be unique")));
const withWorkspaceExec = structuredClone(base);
withWorkspaceExec.permissions = {
workspaceExec: {
allow: [{ command: "tool", args: ["status", "*"], cwd: ".", effect: "read" }]
}
};
assert.equal(
validatePluginManifest(withWorkspaceExec).success,
true,
"valid generic workspace execution permissions are accepted"
);
for (const invalidRule of [
{ command: "sh -c", args: [], cwd: ".", effect: "read" },
{ command: "tool", args: ["sta*"], cwd: ".", effect: "read" },
{ command: "tool", args: [], cwd: "../outside", effect: "read" },
{ command: "tool", args: [], cwd: ".", effect: "git" }
]) {
const invalid = structuredClone(base);
invalid.permissions = { workspaceExec: { allow: [invalidRule] } };
assert.equal(validatePluginManifest(invalid).success, false, "unsafe workspace exec rule is rejected");
}

View File

@ -9,12 +9,14 @@ const main = requireString(manifest, "main");
const version = requireString(manifest, "version"); const version = requireString(manifest, "version");
const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`); const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`);
const distEntries = await collectFiles(join(pluginRoot, "dist"), "dist"); const distEntries = await collectFiles(join(pluginRoot, "dist"), "dist");
const scriptEntries = await collectFiles(join(pluginRoot, "scripts"), "scripts");
if (!distEntries.some((entry) => entry.archivePath === main)) { if (!distEntries.some((entry) => entry.archivePath === main)) {
throw new Error(`Built plugin dist does not contain manifest main: ${main}`); throw new Error(`Built plugin dist does not contain manifest main: ${main}`);
} }
const archiveEntries = [ const archiveEntries = [
{ archivePath: "idea-plugin.json", sourcePath: manifestPath }, { archivePath: "idea-plugin.json", sourcePath: manifestPath },
...distEntries, ...distEntries,
...scriptEntries,
{ archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") } { archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") }
]; ];
const DOS_TIME_MIDNIGHT = 0; const DOS_TIME_MIDNIGHT = 0;

View File

@ -2,10 +2,16 @@ export type {
IdeAPluginActivationScope, IdeAPluginActivationScope,
IdeAPluginCapability, IdeAPluginCapability,
IdeAPluginManifest, IdeAPluginManifest,
IdeAPluginPermissions,
IdeAPluginWorkspaceExecEffect,
IdeAPluginWorkspaceExecRule,
IdeAPluginEngineConstraints, IdeAPluginEngineConstraints,
IdeAPluginLayoutContribution, IdeAPluginLayoutContribution,
IdeAPluginMcpServerContribution, IdeAPluginMcpServerContribution,
IdeAPluginMcpToolMetadata,
IdeAPluginMenuItemContribution, IdeAPluginMenuItemContribution,
IdeAPluginSkillContribution,
IdeAPluginSkillKind,
IdeAPluginTopLevelMenuContribution IdeAPluginTopLevelMenuContribution
} from "./manifest.js"; } from "./manifest.js";
export { export {
@ -75,6 +81,8 @@ export type {
WorkspaceBinaryFile, WorkspaceBinaryFile,
WorkspaceDirEntry, WorkspaceDirEntry,
WorkspaceDirectoryListing, WorkspaceDirectoryListing,
WorkspaceExecToolRequest,
WorkspaceExecToolResult,
WorkspaceFileChangedEvent, WorkspaceFileChangedEvent,
WorkspaceProject, WorkspaceProject,
WorkspaceResolvedPath, WorkspaceResolvedPath,

View File

@ -13,7 +13,9 @@ export interface IdeAPluginManifest {
* Defaults to "app". Use "project" when activate(ctx) requires a focused project. * Defaults to "app". Use "project" when activate(ctx) requires a focused project.
*/ */
activationScope?: IdeAPluginActivationScope; activationScope?: IdeAPluginActivationScope;
permissions?: IdeAPluginPermissions;
contributes?: { contributes?: {
skills?: IdeAPluginSkillContribution[];
menus?: IdeAPluginTopLevelMenuContribution[]; menus?: IdeAPluginTopLevelMenuContribution[];
menuItems?: IdeAPluginMenuItemContribution[]; menuItems?: IdeAPluginMenuItemContribution[];
slashCommands?: IdeAPluginSlashCommandContribution[]; slashCommands?: IdeAPluginSlashCommandContribution[];
@ -22,14 +24,46 @@ export interface IdeAPluginManifest {
}; };
} }
export type IdeAPluginWorkspaceExecEffect = "read" | "write" | "network" | "destructive";
export interface IdeAPluginWorkspaceExecRule {
/** Bare executable name resolved by the host without a shell. */
command: string;
/** Exact argv tokens. `*` matches one token, or the remaining suffix when last. */
args?: string[];
/** Exact project-relative cwd; `*` permits any host-confined cwd. Defaults to `.`. */
cwd?: string;
effect: IdeAPluginWorkspaceExecEffect;
}
export interface IdeAPluginPermissions {
workspaceExec?: {
allow: IdeAPluginWorkspaceExecRule[];
};
}
export type IdeAPluginCapability = "ui" | "mcp" | "tooling"; export type IdeAPluginCapability = "ui" | "mcp" | "tooling";
export type IdeAPluginActivationScope = "app" | "project"; export type IdeAPluginActivationScope = "app" | "project";
export type IdeAPluginSkillKind = "workflow" | "reference";
export interface IdeAPluginEngineConstraints { export interface IdeAPluginEngineConstraints {
idea?: string; idea?: string;
} }
export interface IdeAPluginSkillContribution {
id: string;
/** Agent-facing name shown in the injected skill catalogue and accepted by idea_skill_read. */
name: string;
/** Short affordance shown to agents before they load the skill body. */
description: string;
/** Defaults to "workflow". */
kind?: IdeAPluginSkillKind;
/** Package-relative Markdown file, usually skills/<name>/SKILL.md. */
path: string;
}
export interface IdeAPluginTopLevelMenuContribution { export interface IdeAPluginTopLevelMenuContribution {
id: string; id: string;
label: string; label: string;
@ -80,6 +114,15 @@ export interface IdeAPluginMcpServerContribution {
transport: "stdio"; transport: "stdio";
autoStart?: boolean; autoStart?: boolean;
allowAbsoluteCommand?: boolean; allowAbsoluteCommand?: boolean;
/** Optional static UI metadata; runtime MCP tools/list remains authoritative. */
tools?: IdeAPluginMcpToolMetadata[];
}
export interface IdeAPluginMcpToolMetadata {
/** Exact tool name advertised by the MCP server. */
name: string;
/** Optional human-readable summary for capability-management UI. */
description?: string;
} }
export type PluginManifestValidationResult = export type PluginManifestValidationResult =
@ -121,6 +164,7 @@ export function validatePluginManifest(input: unknown): PluginManifestValidation
validateEngines(input.engines, errors); validateEngines(input.engines, errors);
validateCapabilities(input.capabilities, errors); validateCapabilities(input.capabilities, errors);
validateActivationScope(input.activationScope, errors); validateActivationScope(input.activationScope, errors);
validatePermissions(input.permissions, errors);
validateContributes(input.contributes, errors); validateContributes(input.contributes, errors);
if (errors.length > 0) { if (errors.length > 0) {
@ -130,6 +174,56 @@ export function validatePluginManifest(input: unknown): PluginManifestValidation
return { success: true, data: input as unknown as IdeAPluginManifest, errors: [] }; return { success: true, data: input as unknown as IdeAPluginManifest, errors: [] };
} }
function validatePermissions(value: unknown, errors: string[]): void {
if (value === undefined) return;
if (!isRecord(value)) {
errors.push("permissions must be an object when provided");
return;
}
const workspaceExec = value.workspaceExec;
if (workspaceExec === undefined) return;
if (!isRecord(workspaceExec) || !Array.isArray(workspaceExec.allow)) {
errors.push("permissions.workspaceExec.allow must be an array");
return;
}
if (workspaceExec.allow.length > 64) {
errors.push("permissions.workspaceExec.allow must contain at most 64 rules");
}
workspaceExec.allow.forEach((rule, index) => {
const label = `permissions.workspaceExec.allow[${index}]`;
if (!isRecord(rule)) {
errors.push(`${label} must be an object`);
return;
}
requireString(rule, "command", errors, `${label}.command`);
if (typeof rule.command === "string" && !/^[A-Za-z0-9._+-]{1,128}$/.test(rule.command)) {
errors.push(`${label}.command must be a bare ASCII executable name`);
}
optionalStringArray(rule, "args", errors, `${label}.args`);
if (Array.isArray(rule.args)) {
if (rule.args.length > 64 || rule.args.some((arg) =>
typeof arg === "string" && (arg.length === 0 || arg.length > 4096 || (arg.includes("*") && arg !== "*")))) {
errors.push(`${label}.args contains an invalid token`);
}
}
optionalString(rule, "cwd", errors, `${label}.cwd`);
if (typeof rule.cwd === "string" && rule.cwd !== "*" && !isSafeWorkspaceCwd(rule.cwd)) {
errors.push(`${label}.cwd must be project-relative without empty, dot, or parent segments`);
}
if (!["read", "write", "network", "destructive"].includes(String(rule.effect))) {
errors.push(`${label}.effect must be "read", "write", "network" or "destructive"`);
}
});
}
function isSafeWorkspaceCwd(value: string): boolean {
if (value === ".") return true;
if (value.length === 0 || value.startsWith("/") || value.startsWith("\\") || value.includes(":")) {
return false;
}
return value.replaceAll("\\", "/").split("/").every((part) => part !== "" && part !== "." && part !== "..");
}
export function isPluginManifest(input: unknown): input is IdeAPluginManifest { export function isPluginManifest(input: unknown): input is IdeAPluginManifest {
return validatePluginManifest(input).success; return validatePluginManifest(input).success;
} }
@ -192,6 +286,20 @@ function validateContributes(value: unknown, errors: string[]): void {
return; return;
} }
validateArray(value, "skills", errors, (skill, index) => {
requireString(skill, "id", errors, `contributes.skills[${index}].id`);
requireString(skill, "name", errors, `contributes.skills[${index}].name`);
requireString(skill, "description", errors, `contributes.skills[${index}].description`);
requireString(skill, "path", errors, `contributes.skills[${index}].path`);
if (typeof skill.kind === "string" && skill.kind !== "workflow" && skill.kind !== "reference") {
errors.push(`contributes.skills[${index}].kind must be "workflow" or "reference" when provided`);
} else if (skill.kind !== undefined && typeof skill.kind !== "string") {
errors.push(`contributes.skills[${index}].kind must be a string when provided`);
}
if (typeof skill.path === "string" && !skill.path.endsWith(".md")) {
errors.push(`contributes.skills[${index}].path must point to a .md file`);
}
});
validateArray(value, "menus", errors, (menu, index) => { validateArray(value, "menus", errors, (menu, index) => {
requireString(menu, "id", errors, `contributes.menus[${index}].id`); requireString(menu, "id", errors, `contributes.menus[${index}].id`);
requireString(menu, "label", errors, `contributes.menus[${index}].label`); requireString(menu, "label", errors, `contributes.menus[${index}].label`);
@ -240,6 +348,22 @@ function validateContributes(value: unknown, errors: string[]): void {
optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`); optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`);
optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`); optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`);
optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`); optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`);
validateArray(server, "tools", errors, (tool, toolIndex) => {
requireString(tool, "name", errors, `contributes.mcpServers[${index}].tools[${toolIndex}].name`);
if (tool.description !== undefined) {
requireString(tool, "description", errors, `contributes.mcpServers[${index}].tools[${toolIndex}].description`);
}
});
if (Array.isArray(server.tools)) {
const names = new Set<string>();
server.tools.forEach((tool, toolIndex) => {
if (!isRecord(tool) || typeof tool.name !== "string" || tool.name.trim() === "") return;
if (names.has(tool.name)) {
errors.push(`contributes.mcpServers[${index}].tools[${toolIndex}].name must be unique within the server`);
}
names.add(tool.name);
});
}
}); });
} }

View File

@ -2,6 +2,8 @@ import type { ComponentType, ReactNode } from "react";
export interface ActivateContext { export interface ActivateContext {
pluginId: string; pluginId: string;
/** Absolute host-local root of this active installed plugin package. */
pluginRoot: string;
logger: PluginLogger; logger: PluginLogger;
subscriptions: CommandDisposable[]; subscriptions: CommandDisposable[];
commands?: CommandRegistry; commands?: CommandRegistry;
@ -135,6 +137,31 @@ export interface WorkspaceService {
watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>; watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>;
/** Queries a bounded, generic project structure read model. */ /** Queries a bounded, generic project structure read model. */
queryStructure(query?: WorkspaceStructureQuery): Promise<ProjectStructure>; queryStructure(query?: WorkspaceStructureQuery): Promise<ProjectStructure>;
/** Runs one manifest-allowlisted binary directly, without a visible task. */
execTool(request: WorkspaceExecToolRequest): Promise<WorkspaceExecToolResult>;
}
export interface WorkspaceExecToolRequest {
projectId?: string;
command: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
timeoutMs?: number;
maxStdoutBytes?: number;
maxStderrBytes?: number;
stdin?: string;
}
export interface WorkspaceExecToolResult {
exitCode: number | null;
signal?: string;
timedOut: boolean;
stdout: string;
stderr: string;
stdoutTruncated: boolean;
stderrTruncated: boolean;
durationMs: number;
} }
export interface WorkspaceResolvedPath { export interface WorkspaceResolvedPath {