Compare commits
3 Commits
720cd53924
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a3c582ec8 | |||
| 32d33b330f | |||
| 8ff0f475de |
15
README.md
15
README.md
@ -14,7 +14,8 @@ 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.
|
- [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.
|
- [Project Plugin Assignments](docs/project-plugin-assignments.md): enable plugins per project and assign skills and MCP tools to agents.
|
||||||
@ -23,6 +24,18 @@ 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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
@ -26,14 +26,16 @@ manifest contribution -> command id -> registered command handler -> optional ta
|
|||||||
- 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 human menu-click UI presents minimal host-owned feedback for `skipped`
|
- The human menu-click UI presents minimal host-owned feedback for `skipped`
|
||||||
and `failed` results. It intentionally does not expose the raw result payload.
|
and `failed` results. It intentionally does not expose the raw result payload
|
||||||
|
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
|
For successful actions and richer status, open or focus a plugin layout/window
|
||||||
from the handler with
|
from the handler with
|
||||||
`ctx.services.windows.open({ layoutType, state })`.
|
`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.
|
||||||
@ -50,6 +52,12 @@ Check preconditions before calling `runCommand()`:
|
|||||||
- Do not use `ctx.pluginRoot` as `cwd`; `cwd` remains relative to the project root.
|
- 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
|
||||||
@ -67,21 +75,30 @@ 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 and agents.
|
- Command return value: immediate feedback for programmatic callers and agents.
|
||||||
The human menu host maps `skipped` and `failed` results to concise feedback;
|
The human menu host maps `skipped` and `failed` results to concise feedback;
|
||||||
plugin-specific fields and messages are not displayed verbatim.
|
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
|
- 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
|
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.
|
declared in `idea-plugin.json` and registered during activation.
|
||||||
@ -146,6 +163,11 @@ const plugin: IdeAPluginModule = {
|
|||||||
export default plugin;
|
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:
|
Background task feedback:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@ -141,6 +141,39 @@ 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
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -47,9 +50,16 @@ export function activate(ctx: ActivateContext): void {
|
|||||||
- A missing command handler is surfaced by the host as a retryable reload error;
|
- A missing command handler is surfaced by the host as a retryable reload error;
|
||||||
menu clicks must never fail silently.
|
menu clicks must never fail silently.
|
||||||
- A registered handler's rejected promise is not swallowed: the host logs the
|
- A registered handler's rejected promise is not swallowed: the host logs the
|
||||||
plugin id, command id and reason, then presents a retryable error. In
|
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
|
particular, return/await `services.windows.open(...)` so native-window
|
||||||
failures follow this path.
|
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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
@ -93,6 +125,13 @@ locations. Use it for configuration the user expects to review/version.
|
|||||||
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
|
## Scheduled tasks
|
||||||
|
|
||||||
Project scheduled tasks are exposed through native Tauri/HTTP commands and the
|
Project scheduled tasks are exposed through native Tauri/HTTP commands and the
|
||||||
|
|||||||
@ -36,3 +36,26 @@ duplicate.contributes.mcpServers[0].tools.push({ name: "read_scene" });
|
|||||||
const duplicateResult = validatePluginManifest(duplicate);
|
const duplicateResult = validatePluginManifest(duplicate);
|
||||||
assert.equal(duplicateResult.success, false, "duplicate tool metadata is rejected");
|
assert.equal(duplicateResult.success, false, "duplicate tool metadata is rejected");
|
||||||
assert.ok(duplicateResult.errors.some((error) => error.includes("must be unique")));
|
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");
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,9 @@ export type {
|
|||||||
IdeAPluginActivationScope,
|
IdeAPluginActivationScope,
|
||||||
IdeAPluginCapability,
|
IdeAPluginCapability,
|
||||||
IdeAPluginManifest,
|
IdeAPluginManifest,
|
||||||
|
IdeAPluginPermissions,
|
||||||
|
IdeAPluginWorkspaceExecEffect,
|
||||||
|
IdeAPluginWorkspaceExecRule,
|
||||||
IdeAPluginEngineConstraints,
|
IdeAPluginEngineConstraints,
|
||||||
IdeAPluginLayoutContribution,
|
IdeAPluginLayoutContribution,
|
||||||
IdeAPluginMcpServerContribution,
|
IdeAPluginMcpServerContribution,
|
||||||
@ -78,6 +81,8 @@ export type {
|
|||||||
WorkspaceBinaryFile,
|
WorkspaceBinaryFile,
|
||||||
WorkspaceDirEntry,
|
WorkspaceDirEntry,
|
||||||
WorkspaceDirectoryListing,
|
WorkspaceDirectoryListing,
|
||||||
|
WorkspaceExecToolRequest,
|
||||||
|
WorkspaceExecToolResult,
|
||||||
WorkspaceFileChangedEvent,
|
WorkspaceFileChangedEvent,
|
||||||
WorkspaceProject,
|
WorkspaceProject,
|
||||||
WorkspaceResolvedPath,
|
WorkspaceResolvedPath,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ 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[];
|
skills?: IdeAPluginSkillContribution[];
|
||||||
menus?: IdeAPluginTopLevelMenuContribution[];
|
menus?: IdeAPluginTopLevelMenuContribution[];
|
||||||
@ -23,6 +24,24 @@ 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";
|
||||||
@ -145,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) {
|
||||||
@ -154,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user