From d86625d52aae6e3a01d4fd1412ebe33b9507c65a Mon Sep 17 00:00:00 2001 From: hetaoBackend Date: Tue, 25 Aug 2026 22:02:49 +0800 Subject: [PATCH 1/2] docs: propose portable Hooks preview --- proposals/hooks.md | 335 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 proposals/hooks.md diff --git a/proposals/hooks.md b/proposals/hooks.md new file mode 100644 index 0000000..b5e254c --- /dev/null +++ b/proposals/hooks.md @@ -0,0 +1,335 @@ +# Portable Hooks preview for MiniMax Code + +Status: Proposal + +Portable baseline: Agent Plugins 1.0 + +Related upstream discussion: [Portable Hooks Component Type #54](https://github.com/agentplugins/agent-plugins-spec/discussions/54) + +This document proposes an experimental MiniMax Code client extension for lifecycle Hooks. It is a +design and implementation plan, not a supported Plugin capability. Documentation, validation, and +runtime support must all land with conformance evidence before MiniMax Code advertises Hooks as +available. + +## Recommendation + +MiniMax Code should not add `hooks` to the root `plugin.json` manifest or describe Hooks as an Agent +Plugins 1.0 component. Agent Plugins 1.0 defines exactly two portable component types: Skills and +MCP servers. Its design notes explicitly leave Hooks outside v1 because client formats and behavior +have not yet converged. + +The standards-compliant preview path is: + +1. keep `plugin.json`, Skills, and `mcp.json` conformant to Agent Plugins 1.0; +2. place the preview contract under a stable MiniMax-owned client-extension namespace; +3. align its declaration with upstream Discussion #54 instead of inventing another event model; +4. implement and test the same observable behavior in the MiniMax Code runtime; and +5. migrate to a root portable `hooks.json` only after an Agent Plugins release standardizes it. + +The accurate compatibility claim during preview would be: + +> Agent Plugins 1.0 compatible, with an experimental MiniMax Code Hooks extension aligned with the +> portable Hooks proposal. + +It must not be shortened to “Agent Plugins 1.0 Hooks support.” + +## Evidence for a portable floor + +Upstream Discussion #54 reports production integrations across twelve agent clients and identifies +six recurring lifecycle points despite different native names, configuration formats, and delivery +mechanisms. It proposes a small observe-only declaration: run one command when one lifecycle event +occurs. The proposal intentionally leaves payloads, blocking, permissions, and output control for +later work. + +MiniMax Code should adopt that small floor first: + +| Portable event | Meaning | +| --- | --- | +| `session-start` | A session starts or resumes. | +| `turn-start` | The user starts a new agent turn. | +| `pre-tool-use` | A tool call is about to execute. | +| `post-tool-use` | A tool call has completed. | +| `turn-end` | The agent turn finishes or becomes idle. | +| `session-end` | The session terminates. | + +The upstream discussion is active evidence, not a published standard. The current Agent Plugins +1.1 working draft still defines only Skills and MCP servers. MiniMax implementation evidence should +therefore be contributed back to that discussion without claiming that its outcome is already +decided. + +## Goals + +- Provide one inspectable declaration for common lifecycle side effects such as provenance, + telemetry disclosed by the Plugin, cache warming, and cleanup. +- Preserve Agent Plugins 1.0 conformance while the portable proposal is unresolved. +- Reuse existing Agent Plugins subprocess and path-safety concepts where they apply. +- Isolate invalid configuration and runtime failures from independent Skills, MCP servers, and Hook + entries. +- Make implicit code execution visible, explicitly authorized, bounded, auditable, and revocable. +- Keep the preview structurally close enough to the upstream proposal for a mechanical migration. + +## Non-goals + +The portable preview does not define: + +- blocking, approval, denial, or permission decisions; +- tool-input or tool-result rewriting; +- model-context injection; +- portable stdin payloads; +- subagent, compaction, notification, worktree, or configuration-change events; +- HTTP, prompt, agent, MCP-tool, or asynchronous Hook handlers; +- cross-Plugin ordering dependencies; or +- a sandbox, marketplace trust level, or secret-distribution mechanism. + +MiniMax-specific policy Hooks may be proposed after the observe-only floor is implemented. They +must use an explicitly separate profile and must not silently change the semantics of portable +events. + +## Package layout + +Agent Plugins client extensions use a stable reverse-domain namespace and a matching top-level +directory. If `minimax.io` is confirmed as the ownership root, the namespace can be +`io.minimax.mcode`: + +```text +plugin-root/ +├── plugin.json +├── skills/ # optional Agent Plugins component +├── mcp.json # optional Agent Plugins component +└── io.minimax.mcode/ # experimental client extension + └── hooks/ + ├── hooks.json + └── scripts/ + └── record.mjs +``` + +The extension directory is sufficient for discovery. The root manifest does not need a redundant +activation field. If installation UI later requires extension metadata, it may be defined under +`extensions.io.minimax.mcode` without changing root Agent Plugins fields. + +The namespace must be confirmed by the MiniMax specification owner before publication and remain +stable after release. + +## Preview document + +The preview should publish an immutable, MiniMax-controlled JSON Schema. This example canonical ID +is illustrative and must not be used by Plugins until the schema exists at that exact URL: + +```json +{ + "$schema": "https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json", + "hooks": { + "pre-tool-use": [ + { + "command": "node", + "args": [ + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs" + ] + } + ], + "turn-end": [ + { + "command": "node", + "args": [ + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs", + "--state", + "${PLUGIN_DATA}/state.json" + ] + } + ] + } +} +``` + +The schema should be closed and define only: + +- required `$schema` and `hooks` root fields; +- the six published event keys; +- one or more entries per event; +- a required single-token `command`; +- optional string-array `args`; +- optional string-valued `env`; and +- optional contained `cwd`. + +The final field set should track upstream Discussion #54. Any deliberate difference must be +documented with an interoperability reason and contributed upstream. + +## Command execution + +Hook commands reuse the safe parts of the Agent Plugins stdio MCP process contract, repeated here +as explicit MiniMax extension requirements: + +- `command` is one executable token: a bare executable name or a contained `./` path. It is not a + shell command string. +- `args` values are passed as distinct process arguments without shell interpretation. +- MiniMax Code provides absolute `PLUGIN_ROOT` and per-installed-instance `PLUGIN_DATA` values and + prevents the Plugin from overriding them. +- `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` are expanded once, non-recursively, only in fields allowed + by the extension schema. +- Explicit working directories resolve within `PLUGIN_ROOT` or `PLUGIN_DATA` after real-path + resolution. Symlink, junction, reparse-point, and traversal escapes are rejected. +- The client sanitizes the base environment instead of inheriting credentials and unrelated ambient + variables by default. +- The client launches the Hook without a shell and enforces hard limits for time, stdin, stdout, + stderr, process-tree lifetime, invocation count, and concurrency. + +## Observe-only runtime semantics + +Every preview event is observational: + +- exit status, stdout, stderr, timeout, cancellation, and spawn failure cannot alter agent behavior; +- `pre-tool-use` cannot block, approve, or modify a tool call; +- `post-tool-use` cannot replace or redact the tool result; +- failed Hook commands produce bounded diagnostics and do not prevent other Hooks or components from + loading or running; and +- `session-end` is best effort and cannot delay client shutdown beyond its client-owned budget. + +MiniMax Code may send its native event payload as one UTF-8 JSON document on stdin, followed by EOF. +That payload must be versioned and documented for MiniMax Plugin authors, but it is not a portable +Agent Plugins payload while the upstream proposal leaves payloads client-defined. Stdout and stderr +are diagnostic-only in this preview. + +Within one Plugin, handlers must run in declaration order. Observational handlers may run concurrently +across Plugins, but diagnostics must retain the event identifier, Plugin identity, handler index, +duration, and outcome. Plugins must not depend on cross-Plugin ordering. + +## Loading and failure isolation + +The loader should use the narrowest applicable failure boundary: + +1. An invalid root `plugin.json` rejects the Plugin under the Agent Plugins core rules. +2. A namespace directory that resolves outside the Plugin root disables that extension and denies + access to the escaped path. +3. Invalid JSON, an unsupported schema version, or invalid top-level `hooks.json` structure disables + Hooks for that Plugin while valid Skills and MCP servers continue loading. +4. An invalid event entry skips only that entry and reports a diagnostic. +5. One Hook process failure does not disable other Hook entries or independent components. +6. Clients that do not implement `io.minimax.mcode` ignore the extension without validating it. + +MiniMax-Code-Plugins currently requires at least one valid Skill or MCP server. Whether the community +registry will admit Hook-only Plugins is a separate publication-policy decision. If admitted, the +catalog must explain that Agent Plugins clients without the MiniMax extension may load no usable +component from such a package. + +## Security and authorization + +Hooks introduce implicit local code execution at lifecycle points and may receive sensitive prompt +or tool data. JSON Schema validation alone is insufficient. + +MiniMax Code must: + +- show Hook events, commands, data categories, network behavior, and requested workspace access + before enabling them; +- leave executable Hooks disabled until a user or organization policy explicitly authorizes them; +- request authorization again when an update expands events, commands, data access, or other + capabilities; +- avoid sending full transcripts, system prompts, secrets, or unrelated tool data by default; +- isolate `PLUGIN_DATA` by installed Plugin instance; +- redact sensitive payload and environment values from diagnostics and audit records; +- provide global disable, per-Plugin disable, and safe-mode recovery paths; and +- never run contributed Hook code during registry validation or CI. + +The hosted repository must continue rejecting secrets, private endpoints, hidden telemetry, +symlinks, native binaries, and undisclosed installers. Static review must not be described as a +sandbox or complete security audit. + +## Conformance evidence + +Two suites are required and must be reported separately. + +### Agent Plugins 1.0 core + +- Root `plugin.json` continues targeting the published 1.0 schema. +- Unimplemented extension namespaces remain ignored without internal validation. +- A missing extension directory is valid absence. +- Invalid MiniMax Hooks do not prevent valid Skills or MCP servers from loading. +- All package paths remain inside the filesystem-resolved Plugin root. + +### MiniMax Hooks preview + +- valid and invalid schema fixtures, including unknown fields and unsupported versions; +- schema selection from a local supported-version table without fetching schemas during loading; +- mixed valid and invalid entries proving that one invalid entry does not block its valid siblings; +- exactly one preview-event delivery for each corresponding lifecycle occurrence, covering all six + events over success, failure, cancellation, and session resume; +- exact argument boundaries proving no shell interpolation; +- placeholder, working-directory, real-path, symlink, and reserved-environment cases; +- timeout and process-tree cleanup without orphaned processes; +- bounded and redacted stdin, stdout, stderr, diagnostics, and audit data; +- disabled and unauthorized Hooks never executing; +- failed Hooks never changing agent or permission behavior; and +- a deterministic end-to-end fixture installed into a real MiniMax Code runtime. + +Registry CI must remain static and must never execute Plugin Hook code. + +## Implementation surfaces + +### MiniMax-Code-Plugins + +After runtime ownership and the preview contract are approved, this repository would need coordinated +changes to: + +- `docs/plugin-compatibility.md` to separate the portable core from MiniMax client extensions; +- `docs/security-model.md` for implicit execution, consent, disclosure, and failure behavior; +- `scripts/lib/validation.mjs` for static extension-schema and path validation; +- `test/validation.test.mjs` for schema and isolation fixtures; +- an `examples/hello-mcode-hooks/` package; and +- README and contribution language that labels the capability experimental and non-portable. + +Those changes must not merge before the matching runtime can be installed and tested. + +### MiniMax Code runtime + +The runtime owns: + +- extension discovery and a local schema registry; +- install consent and capability-diff consent on update; +- lifecycle event mapping; +- a bounded process runner and observe-only dispatcher; +- timeout, cancellation, and process-tree cleanup; +- audit, diagnostics, and redaction; +- user and organization enablement policy; and +- end-to-end conformance fixtures across interactive, headless, background, and remote sessions that + claim support. + +Accepting `hooks.json` in this repository without the runtime work is not Hooks support. + +## Promotion path + +1. Confirm the MiniMax namespace, six-event observe-only scope, and runtime owner. +2. Add MiniMax's adoption intent, event mapping, and semantic gaps to upstream Discussion #54. +3. Publish the immutable preview schema and human-readable contract on a MiniMax-controlled domain. +4. Implement one `pre-tool-use` observe-only vertical slice with consent, diagnostics, and audit. +5. Implement the other five events and pass the MiniMax conformance fixtures. +6. Add coordinated registry validation, documentation, and an example; label the release experimental. +7. If Agent Plugins publishes a portable Hooks component, migrate the extension file to the standard + root `hooks.json` and its canonical schema. + +An upstream specification change must update normative prose, schemas, canonical identifiers, fixed +discovery locations, version rules, failure boundaries, examples, and the conformance checklist as +one conceptual surface. A schema-only pull request is insufficient. + +Blocking decisions, context injection, payload standardization, additional events, and alternative +handler types require separate interoperability evidence and versioned proposals. + +## Open decisions + +- Confirm the permanent reverse-domain namespace. +- Identify the runtime owner and supported release channels. +- Decide whether Hook-only packages are eligible for the community registry. +- Document exact event mappings for main agents, subagents, parallel tools, resumed sessions, and + background or remote execution. +- Define the MiniMax-native stdin payload and disclosure policy without presenting it as portable. +- Set client-owned resource budgets and update re-authorization rules. +- Decide which second client will run shared conformance fixtures before portable standardization. + +## Primary sources + +- [Agent Plugins 1.0 specification](https://agent-plugins.org/specification) +- [Agent Plugins client conformance checklist](https://agent-plugins.org/client-implementers/conformance) +- [Agent Plugins client extensions](https://agent-plugins.org/plugin-authors/client-extensions) +- [Agent Plugins contribution process](https://github.com/agentplugins/agent-plugins-spec/blob/main/CONTRIBUTING.md) +- [Agent Plugins Discussion #54: Portable Hooks Component Type](https://github.com/agentplugins/agent-plugins-spec/discussions/54) +- [MiniMax Code Plugin compatibility](../docs/plugin-compatibility.md) +- [MiniMax Code Plugin security model](../docs/security-model.md) +- [Capability proposal policy](README.md) From 4147cc56d4fe7cba08e24991a5933ac2b2a18f83 Mon Sep 17 00:00:00 2001 From: hetaoBackend Date: Wed, 26 Aug 2026 11:23:24 +0800 Subject: [PATCH 2/2] feat: define MiniMax Hooks 0.1 contract --- .github/PULL_REQUEST_TEMPLATE.md | 6 +- CONTRIBUTING.md | 25 +- README.md | 32 +- README.zh-CN.md | 23 +- docs/architecture.md | 9 +- docs/hooks.md | 210 +++++++++++ docs/plugin-compatibility.md | 105 ++++-- docs/security-model.md | 19 +- examples/hello-mcode-hooks/README.md | 33 ++ .../io.minimax.mcode/hooks/hooks.json | 23 ++ .../io.minimax.mcode/hooks/scripts/record.mjs | 55 +++ examples/hello-mcode-hooks/plugin.json | 7 + package-lock.json | 61 ++++ package.json | 3 + plugins/README.md | 9 +- proposals/README.md | 7 +- proposals/hooks.md | 335 ------------------ .../io.minimax.mcode/hooks/0.1.0.schema.json | 88 +++++ scripts/lib/validation.mjs | 120 ++++++- test/hooks-example.test.mjs | 62 ++++ test/hooks-schema.test.mjs | 84 +++++ test/hosted-plugins.test.mjs | 98 ++++- test/validation.test.mjs | 100 +++++- 23 files changed, 1115 insertions(+), 399 deletions(-) create mode 100644 docs/hooks.md create mode 100644 examples/hello-mcode-hooks/README.md create mode 100644 examples/hello-mcode-hooks/io.minimax.mcode/hooks/hooks.json create mode 100644 examples/hello-mcode-hooks/io.minimax.mcode/hooks/scripts/record.mjs create mode 100644 examples/hello-mcode-hooks/plugin.json delete mode 100644 proposals/hooks.md create mode 100644 schemas/io.minimax.mcode/hooks/0.1.0.schema.json create mode 100644 test/hooks-example.test.mjs create mode 100644 test/hooks-schema.test.mjs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ff298f7..2cc63f6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,16 +4,18 @@ ## User value - + ## Plugin submission checklist - [ ] Plugin lives at `plugins//`. - [ ] `plugin.json` name matches the Plugin directory. -- [ ] `README.md` includes a real example prompt and expected result. +- [ ] Plugin exposes at least one valid Skill, MCP server, or MiniMax Code Hook. +- [ ] `README.md` includes a real example prompt or lifecycle reproduction and expected result. - [ ] `LICENSE` and `plugin.json` declare an open-source license. - [ ] Required executables, accounts, paid services, and supported platforms are disclosed. - [ ] Network destinations and data handled by the plugin are disclosed. +- [ ] Hook events, commands, implicit side effects, and stored data locations are disclosed. - [ ] No credentials, private endpoints, hidden telemetry, installers, symlinks, or native binaries are included. - [ ] Every scaffold `TODO` has been replaced. - [ ] `npm run check` passes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e06db84..6c805bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,15 +14,21 @@ npm run create -- / The command creates `plugins//` with a portable `plugin.json`, README, Apache-2.0 license, and starter Skill. +For a Hook-only Plugin, start from [`examples/hello-mcode-hooks`](examples/hello-mcode-hooks/) and +keep the same hosted README, LICENSE, and manifest requirements. + ## 2. Make it real Replace every scaffold `TODO`. Your Plugin must: -- expose at least one Skill or MCP server; +- expose at least one Skill, MCP server, or MiniMax Code Hook; - use the supported package shape in [`docs/plugin-compatibility.md`](docs/plugin-compatibility.md); +- follow the versioned [`Hooks 0.1 contract`](docs/hooks.md) for every Hook; - include `README.md`, `LICENSE`, and a matching open-source license in `plugin.json`; -- explain the user problem, an example prompt, and the expected result; +- explain the user problem, a copyable prompt or lifecycle reproduction, and the expected result; - disclose required executables, accounts, paid services, platforms, network destinations, and data; +- for Hooks, disclose every event, command, implicit side effect, received data category, and stored + data location; - contain no credentials, private endpoints, hidden telemetry, installers, native binaries, or symlinks. Keep source and docs inside your Plugin directory. Do not edit another contributor's Plugin in the @@ -34,23 +40,26 @@ same pull request. npm run check ``` -The validator checks the hosted directory, Manifest, Skills, MCP transports, required docs, -placeholders, and path safety. CI runs the same command. +The validator checks the hosted directory, Manifest, Skills, MCP transports, Hooks, required docs, +placeholders, and path safety. CI parses Hook configuration and source as data and never executes a +contributed Hook command. ## 4. Open the pull request Include: - the problem your Plugin solves; -- a copyable example prompt; +- a copyable example prompt or lifecycle reproduction; - the expected result; - dependencies and supported platforms; - network and data behavior; - automated and manual test evidence. -Review covers usefulness, reproducibility, clear ownership, data flow, dependency risk, and obvious -supply-chain issues. Acceptance means “available as community software”; it is not a MiniMax -endorsement or a complete security audit. +Review covers usefulness, reproducibility, clear ownership, implicit execution, data flow, +dependency risk, and obvious supply-chain issues. Acceptance means “available as community +software”; it is not a MiniMax endorsement or a complete security audit. Hook acceptance confirms +the staged registry declaration only. It does not establish runtime availability until this +repository links a compatible MiniMax Code implementation and end-to-end conformance evidence. ## Update or remove a Plugin diff --git a/README.md b/README.md index 37e4c6c..4dca694 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ ## One folder is the release -MiniMax Code Plugins is the community home for Agent Plugins that run in MiniMax Code. Put a -portable Plugin under `plugins//`, open a pull request, and let CI check -the package users will actually install. +MiniMax Code Plugins is the community home for Agent Plugins that run in MiniMax Code. Put a Plugin +under `plugins//`, open a pull request, and let CI check the package users +will actually install. ```text fork → create → build → check → pull request → discover @@ -71,15 +71,24 @@ good prompt pattern to a capability anyone can install. Connect MiniMax Code to local tools or remote services with `stdio`, `streamable-http`, or `sse`. Dependencies, accounts, network destinations, and data handling must be visible before install. -### Both +### Hooks -Use a Skill to teach the workflow and MCP to provide the tools. The portable package stays small: +Declare observe-only commands for six MiniMax Code lifecycle points with the staged, versioned +[`io.minimax.mcode` Hooks 0.1 client extension](docs/hooks.md). The registry validates explicit +commands, arguments, and disclosures. Runtime execution remains unavailable as a documented public +capability until a compatible MiniMax Code build and end-to-end evidence are linked. + +### Combine them + +Use Skills to teach the workflow, MCP to provide tools, and Hooks for bounded lifecycle side effects: ```text plugin-root/ ├── plugin.json ├── mcp.json # optional -└── skills/ # optional +├── skills/ # optional +└── io.minimax.mcode/ # optional MiniMax Code extension + └── hooks/hooks.json ``` This repository is for **Agent capabilities**. TUI Extensions are a separate system and are not @@ -91,7 +100,7 @@ A contribution must: - live at `plugins//`; - include `plugin.json`, `README.md`, and `LICENSE`; -- expose at least one valid Skill or MCP server; +- expose at least one valid Skill, MCP server, or MiniMax Code Hook; - document a copyable example, requirements, network access, and data use; - contain no secrets, private endpoints, hidden telemetry, native binaries, or symlinks; - pass `npm run check` and human review. @@ -104,16 +113,19 @@ or a complete security audit. Read the source and requested capabilities before - [`plugins/`](plugins/) — community Plugin source - [`examples/hello-mcode`](examples/hello-mcode/) — smallest Skill Plugin - [`examples/hello-mcode-mcp`](examples/hello-mcode-mcp/) — dependency-free stdio MCP +- [`examples/hello-mcode-hooks`](examples/hello-mcode-hooks/) — smallest Hook-only Plugin - [`docs/plugin-compatibility.md`](docs/plugin-compatibility.md) — exact supported contract +- [`docs/hooks.md`](docs/hooks.md) — MiniMax Code Hooks 0.1 author and runtime contract - [`docs/security-model.md`](docs/security-model.md) — validation and trust model - [`docs/architecture.md`](docs/architecture.md) — hosted contribution architecture - [`GOVERNANCE.md`](GOVERNANCE.md) — decisions and maintainer responsibilities ## Community preview -The contract is intentionally narrow while MiniMax Code's public Plugin surface stabilizes. Hooks, -custom Agents, Commands, LSP, Apps, generic OAuth, and TUI Extensions are not advertised as current -Agent Plugin capabilities. +The contract is intentionally narrow while MiniMax Code's public Plugin surface stabilizes. Hooks +0.1 is accepted as a staged registry declaration; this repository does not yet certify a runtime +build that executes it. Custom Agents, Commands, LSP, Apps, generic OAuth, blocking Hooks, and TUI +Extensions are not advertised as current Agent Plugin capabilities. Bring one useful capability. Make the example undeniable. Ship it in one pull request. diff --git a/README.zh-CN.md b/README.zh-CN.md index b77b9a4..0ef4a86 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -69,15 +69,23 @@ npm run check 通过 `stdio`、`streamable-http` 或 `sse` 连接本地工具和远程服务。依赖、账号、网络目标和数据处理必须 在安装前说清楚。 -### Skill + MCP +### Hooks -Skill 教会 Agent 怎么做,MCP 给它真正的工具。可移植包结构保持简单: +通过分阶段、版本化的 [`io.minimax.mcode` Hooks 0.1 客户端扩展](docs/hooks.md),声明 MiniMax Code +六个生命周期点的只观察命令。仓库会校验 command、args 和披露内容;在链接兼容的 MiniMax Code 构建及 +端到端证据前,不把运行时执行宣传为已公开可用能力。 + +### 组合使用 + +Skill 教会 Agent 怎么做,MCP 提供工具,Hook 负责有边界的生命周期副作用: ```text plugin-root/ ├── plugin.json ├── mcp.json # 可选 -└── skills/ # 可选 +├── skills/ # 可选 +└── io.minimax.mcode/ # 可选的 MiniMax Code 扩展 + └── hooks/hooks.json ``` 这个仓库只承接 **Agent 能力**。TUI Extension 是另一套独立扩展体系,不使用这里的包格式和加载流程。 @@ -88,7 +96,7 @@ plugin-root/ - 位于 `plugins//`; - 包含 `plugin.json`、`README.md` 和 `LICENSE`; -- 至少提供一个有效的 Skill 或 MCP Server; +- 至少提供一个有效的 Skill、MCP Server 或 MiniMax Code Hook; - 写清示例、依赖、网络访问和数据用途; - 不包含密钥、私有地址、隐藏遥测、原生二进制或 symlink; - 通过 `npm run check` 和人工 Review。 @@ -101,15 +109,18 @@ plugin-root/ - [`plugins/`](plugins/):社区 Plugin 源码 - [`examples/hello-mcode`](examples/hello-mcode/):最小 Skill Plugin - [`examples/hello-mcode-mcp`](examples/hello-mcode-mcp/):零依赖 stdio MCP +- [`examples/hello-mcode-hooks`](examples/hello-mcode-hooks/):最小 Hook-only Plugin - [`docs/plugin-compatibility.md`](docs/plugin-compatibility.md):当前支持的精确契约 +- [`docs/hooks.md`](docs/hooks.md):MiniMax Code Hooks 0.1 作者与运行时契约 - [`docs/security-model.md`](docs/security-model.md):校验与信任模型 - [`docs/architecture.md`](docs/architecture.md):中央托管架构 - [`GOVERNANCE.md`](GOVERNANCE.md):决策与维护者职责 ## Community Preview -MiniMax Code 的公开 Plugin 能力仍在稳定中,所以首版契约刻意保持克制。Hooks、自定义 Agent、Commands、 -LSP、Apps、通用 OAuth 和 TUI Extension 暂不作为当前 Agent Plugin 能力宣传。 +MiniMax Code 的公开 Plugin 能力仍在稳定中,所以契约刻意保持克制。仓库接受 Hooks 0.1 的分阶段声明, +但尚未认证并链接可以执行它的运行时构建。自定义 Agent、Commands、LSP、Apps、通用 OAuth、阻断型 +Hooks 和 TUI Extension 暂不作为当前 Agent Plugin 能力宣传。 带来一个真的有用的能力,给出一个无法误解的示例,然后用一个 PR 把它发布出来。 diff --git a/docs/architecture.md b/docs/architecture.md index e44624e..b7c79b4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ MiniMax Code Plugins keeps contribution and review in one repository. ```text plugins// - plugin.json + README + LICENSE + Skills and/or MCP + plugin.json + README + LICENSE + Skills, MCP, and/or Hooks | v local validation -> pull request CI -> human review @@ -16,9 +16,10 @@ main branch -> catalog consumers / MiniMax Code discovery The hosted directory is the publication unit. Reviewers inspect the exact files that enter `main`; contributors do not create a second repository or maintain a separate catalog record. -Static validation reads package metadata and text contracts without executing Plugin code. Human -review covers usefulness, dependencies, data flow, and reproducibility. MCP runtime behavior and -external service quality still require explicit test evidence. +Static validation reads package metadata and text contracts without executing Plugin code. In +particular, CI never executes contributed Hook commands. Human review covers usefulness, +dependencies, implicit execution, data flow, and reproducibility. MCP and Hook runtime behavior and +external service quality still require explicit test evidence from a compatible client. The first version intentionally optimizes for a low-friction community path. External source registries, release mirroring, and Marketplace publishing interfaces can be proposed later without diff --git a/docs/hooks.md b/docs/hooks.md new file mode 100644 index 0000000..6b8ca3d --- /dev/null +++ b/docs/hooks.md @@ -0,0 +1,210 @@ +# MiniMax Code Hooks 0.1 + +MiniMax Code Hooks 0.1 is a staged, versioned client-extension contract for Plugin authors who need +to declare observe-only commands at common agent lifecycle points. This community registry accepts +and statically validates the declaration alongside Agent Skills and MCP servers. + +Hooks 0.1 is not an Agent Plugins 1.0 portable component. Other Agent Plugins clients may ignore it, +and a MiniMax Code build must advertise support for the exact Hooks schema before it executes the +extension. Registry acceptance validates the package contract; it does not prove that every released +client build implements it. + +This repository does not yet link a MiniMax Code runtime implementation and end-to-end conformance +fixture for Hooks 0.1. Until it does, this is a registry-supported declaration format, not a claim +that a current MiniMax Code release executes Hooks. + +## Package layout + +Hooks use the Agent Plugins client-extension namespace `io.minimax.mcode`: + +```text +plugin-root/ +├── plugin.json +└── io.minimax.mcode/ + └── hooks/ + ├── hooks.json + └── scripts/ + └── record.mjs +``` + +The root `plugin.json` continues to target Agent Plugins 1.0. Do not add a root `hooks` field. The +fixed Hooks configuration path is: + +```text +io.minimax.mcode/hooks/hooks.json +``` + +`hooks.json` must target the published 0.1.0 schema: + +```json +"$schema": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Code-Plugins/main/schemas/io.minimax.mcode/hooks/0.1.0.schema.json" +``` + +Clients select supported behavior from that exact identifier using a local schema table. They must +not fetch a schema while loading a Plugin. The versioned schema file is immutable; any structural or +behavioral change requires a new identifier and file. + +This document is the human-readable contract. If it conflicts with the published schema, this +document governs and the mismatch is a specification defect that must be corrected in a new pull +request without reassigning a published schema identifier. + +## Configuration + +This example records two event types with a bundled, dependency-free Node.js script: + +```json +{ + "$schema": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Code-Plugins/main/schemas/io.minimax.mcode/hooks/0.1.0.schema.json", + "hooks": { + "pre-tool-use": [ + { + "command": "node", + "args": [ + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs", + "pre-tool-use" + ] + } + ], + "post-tool-use": [ + { + "command": "node", + "args": [ + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs", + "post-tool-use" + ], + "cwd": "${PLUGIN_DATA}" + } + ] + } +} +``` + +The document and every handler are closed objects. Unknown fields fail community-registry +validation. + +### Events + +Hooks 0.1 defines six exact event names: + +| Event | Trigger | +| --- | --- | +| `session-start` | A supporting runtime attaches to a newly created or resumed top-level session. | +| `turn-start` | The runtime accepts user input and creates a new top-level agent turn. | +| `pre-tool-use` | One finalized top-level tool attempt exists, before permission evaluation or execution. | +| `post-tool-use` | The matching tool attempt reaches a terminal outcome, including success, failure, denial, timeout, or cancellation. | +| `turn-end` | The accepted top-level turn reaches completion, failure, or cancellation; transient idle and permission waits are not terminal. | +| `session-end` | The supporting runtime intentionally detaches from or closes the top-level session. | + +Every event is observe-only. A Hook cannot block or approve an action, rewrite tool input or output, +inject model context, or change agent control flow. Those behaviors require a future versioned +contract. + +MiniMax Code supports at most eight handlers for one event and 32 handlers across one Plugin. A +Hooks document must contain at least one event and every declared event must contain at least one +handler. + +### Occurrence and delivery semantics + +- A resume emits one new `session-start` for that runtime attachment, but does not replay events + already emitted by an earlier attachment. +- Every top-level tool attempt emits one `pre-tool-use` and one matching `post-tool-use`. Permission + denial, spawn failure, timeout, and cancellation still produce the matching terminal event. A + retry is a new attempt and therefore a new pair. +- Parallel tool attempts each receive their own pair. Their events may interleave; no global order + across attempts is defined. +- Subagent turns and tools do not emit Hooks 0.1 events. Background or remote execution emits events + only when that execution is itself the top-level session in a runtime advertising this schema. +- For each emitted event occurrence, the runtime makes exactly one invocation attempt for every + configured handler. Delivery is not durable and failed or interrupted handlers are not retried. + An abrupt runtime or machine failure can prevent an attempt. `session-end` emission itself is best + effort; its handlers are attempted in declaration order only while the shutdown budget remains, + so later attempts may be omitted when shutdown wins. + +### Handler fields + +| Field | Required | Contract | +| --- | --- | --- | +| `command` | yes | One bare executable token or one contained Plugin-relative path beginning with `./`. Never a shell command string. | +| `args` | no | At most 64 string arguments, passed as distinct process arguments without shell parsing. | +| `env` | no | At most 64 portable environment names with string values. `PLUGIN_ROOT` and `PLUGIN_DATA` are reserved case-insensitively. | +| `cwd` | no | A contained `./` path, `${PLUGIN_ROOT}` path, or `${PLUGIN_DATA}` path. Defaults to the Plugin root. | + +NUL bytes are invalid in process fields. A Plugin-relative executable, explicit working directory, +or bundled script must remain inside its filesystem-resolved root. Hosted Plugins cannot contain +symlinks. + +## Runtime process contract + +A compatible MiniMax Code build: + +1. resolves `command` as one executable token and never sends it through a shell; +2. expands `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` once and non-recursively in `args`, `env` values, + and `cwd`, but not in `command` or environment names; unknown placeholder-like text remains + literal and no other environment expansion occurs; +3. supplies absolute `PLUGIN_ROOT` and per-installed-instance `PLUGIN_DATA` values after configured + environment overlays, so a Plugin cannot override them; +4. uses the Plugin root as the default working directory; +5. sanitizes the base environment instead of exposing ambient credentials by default; and +6. enforces client-owned limits for time, stdin, stdout, stderr, process-tree lifetime, invocation + count, and concurrency. + +The client may send a MiniMax-native event object as one UTF-8 JSON document on stdin followed by +EOF. Hooks 0.1 does not standardize that payload. Plugin behavior intended to work across MiniMax +Code versions should use explicit arguments where possible and tolerate missing or unknown stdin +fields. A Hook must not persist prompt, transcript, tool input, or tool output unless its README +discloses that data flow. + +Stdout, stderr, exit status, timeout, cancellation, and spawn failure are diagnostic-only. They +cannot change agent or permission behavior. `session-end` is best effort and cannot delay client +shutdown beyond the client's own budget. + +Handlers within one Plugin run in declaration order. Plugins must not depend on ordering relative to +other Plugins. + +The published JSON Schema enforces document shape and per-field/per-event limits. Standard JSON +Schema cannot express the sum of array lengths across six event properties, so the 32-handler total +is an explicit registry-validator and runtime requirement in addition to the schema. + +## Validation and failure boundaries + +Community CI validates every declared Hook entry and rejects an invalid contribution. It parses +configuration and source as data and never executes contributed Hook code. + +Runtime implementations use narrower failure boundaries: + +1. an invalid root `plugin.json` rejects the Plugin under Agent Plugins core rules; +2. an escaped or invalid `io.minimax.mcode` directory disables that extension; +3. invalid JSON, an unsupported schema, or an invalid Hooks document disables Hooks for that Plugin + while valid Skills and MCP servers continue loading; +4. an invalid handler skips that handler without disabling valid siblings; and +5. one command failure does not disable later Hooks or independent components. + +A client that does not implement the exact schema ignores the extension and should report that it is +unsupported. It must not guess compatibility from a similar version. + +## Author checklist + +Before submitting a Hook Plugin: + +- use the fixed namespace path and exact schema identifier; +- keep `command` and `args` separate and avoid shell parsing in wrapper scripts; +- bundle inspectable source instead of installers or native binaries; +- document every event, executable, platform requirement, file write, network destination, and data + category the Hook can receive or persist; +- provide a copyable lifecycle reproduction and expected result; +- make repeated execution safe and bound all stored data; +- do not include secrets, private endpoints, hidden telemetry, or personal data; and +- run `npm run check` without executing the Hook itself. + +See [`examples/hello-mcode-hooks`](../examples/hello-mcode-hooks/) for a minimal Hook-only package. + +## Portability and future versions + +The six events and command declaration align with the active upstream +[Portable Hooks Component Type discussion](https://github.com/agentplugins/agent-plugins-spec/discussions/54). +That discussion is not a published Agent Plugins release. If Agent Plugins standardizes root +`hooks.json`, this extension can migrate only through an explicit new schema and documented package +change. + +Blocking policy, context injection, portable payloads, subagent lifecycle, additional events, and +HTTP or model-backed handlers remain unsupported in 0.1. diff --git a/docs/plugin-compatibility.md b/docs/plugin-compatibility.md index b7fcf0a..a891363 100644 --- a/docs/plugin-compatibility.md +++ b/docs/plugin-compatibility.md @@ -1,20 +1,34 @@ # MiniMax Code plugin compatibility -## Portable package +## Package contract -MiniMax Code reads the portable subset of Agent Plugins 1.0: +MiniMax Code Plugins accepts the portable Agent Plugins 1.0 subset plus the versioned MiniMax Code +Hooks 0.1 client extension: ```text plugin-root/ -├── README.md # required by this community repository -├── LICENSE # required by this community repository +├── README.md # required by this community repository +├── LICENSE # required by this community repository ├── plugin.json -├── mcp.json # optional -└── skills/ - └── / - └── SKILL.md +├── mcp.json # optional Agent Plugins component +├── skills/ # optional Agent Plugins component +│ └── / +│ └── SKILL.md +└── io.minimax.mcode/ # optional MiniMax Code client extension + └── hooks/ + └── hooks.json ``` +A hosted contribution must expose at least one valid Skill, MCP server, or MiniMax Code Hook. +Hook-only packages are accepted, but Agent Plugins clients that do not implement the MiniMax +extension may load no usable component from them. + +Hooks 0.1 is currently a staged registry declaration. This repository does not yet link a MiniMax +Code runtime build and end-to-end fixture certified to execute it, so package acceptance must not be +presented as current runtime availability. + +## Manifest + `plugin.json` must target: ```json @@ -22,8 +36,13 @@ plugin-root/ ``` The only required manifest fields are `$schema` and `name`. Adding `version`, `description`, -`author`, `homepage`, `repository`, `license`, and `keywords` improves catalog quality. Client -`extensions` may be present but are ignored by MiniMax Code. +`author`, `homepage`, `repository`, `license`, and `keywords` improves catalog quality. The community +repository additionally requires a declared license for hosted Plugins. + +Do not add a root `hooks` field. Agent Plugins 1.0 client-specific files belong under their stable +reverse-domain directory. The registry recognizes `io.minimax.mcode/hooks/hooks.json`, and only a +compatible runtime advertising its exact schema may interpret it. Other extension namespaces remain +ignored unless separately documented. ## Skills @@ -47,24 +66,62 @@ Supported transports are: - `streamable-http`, using an HTTP(S) URL and optional headers; and - `sse`, retained for compatible legacy HTTP+SSE servers. -MiniMax Code reserves `PLUGIN_ROOT` and `PLUGIN_DATA`. A plugin must not set those variables itself. +MiniMax Code reserves `PLUGIN_ROOT` and `PLUGIN_DATA`. A Plugin must not set those variables itself. Do not embed tokens in environment values or headers. Generic OAuth configuration is not part of this portable subset. -## Limits and unsupported capabilities +## Hooks + +MiniMax Code Hooks is a client extension, not an Agent Plugins 1.0 portable component. The fixed +configuration path is: + +```text +io.minimax.mcode/hooks/hooks.json +``` + +The document must target the exact 0.1.0 schema: + +```json +"$schema": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Code-Plugins/main/schemas/io.minimax.mcode/hooks/0.1.0.schema.json" +``` + +Hooks 0.1 supports six observe-only lifecycle events and command handlers with optional `args`, +`env`, and `cwd`. It does not support blocking, approvals, tool rewriting, model-context injection, +or portable event payloads. Read the complete [`Hooks 0.1 contract`](hooks.md) before contributing. + +Community validation recognizes the package and rejects every invalid declared handler. A MiniMax +Code build executes the extension only when it advertises support for this exact schema. Registry +acceptance does not imply that every current or older client build can run it. + +The schema covers document shape and per-event limits. Because JSON Schema cannot sum array lengths +across event properties, the repository validator separately enforces the normative 32-handler +Plugin total. + +## Limits and failure boundaries + +MiniMax Code accepts at most: + +- 64 Skill directories per Plugin; +- 8 MCP servers per Plugin; +- 8 Hook handlers for one event; and +- 32 Hook handlers across one Plugin. + +Community CI rejects invalid declared components so broken packages do not enter the hosted +catalog. Runtime implementations use narrower failure boundaries: invalid Skills, MCP entries, and +Hook handlers are omitted with diagnostics while independent valid components continue loading. An +invalid root manifest rejects the package. -The runtime accepts at most 64 Skill directories and 8 MCP servers per Agent Plugin. Invalid Skills -or MCP entries are omitted with diagnostics; an invalid root manifest rejects the package. +## Unsupported capabilities -The following are not currently public MCode Plugin capabilities: +The following are not current MiniMax Code Agent Plugin capabilities: -- Hooks and lifecycle scripts -- custom Agents and Commands -- LSP configuration -- Apps or UI extensions -- generic OAuth setup -- host-specific fields hidden in `extensions` +- blocking or context-producing Hooks and lifecycle scripts; +- custom Agents and Commands; +- LSP configuration; +- Apps or UI extensions; +- generic OAuth setup; and +- undocumented host-specific fields hidden in `extensions`. -Hosted contributions may contain extra assets, but documentation must not imply that MiniMax Code -loads unsupported components. TUI Extensions are a separate product extension system, not an Agent -Plugin capability. +Hosted contributions may contain assets used by supported components, but documentation must not +imply that MiniMax Code loads unsupported content. TUI Extensions are a separate product extension +system, not an Agent Plugin capability. diff --git a/docs/security-model.md b/docs/security-model.md index f7b2649..7d78fcf 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -2,15 +2,28 @@ Hosted Plugins use three layers of evidence: -1. **Package validation** checks the owner/name layout, Manifest, Skill and MCP contracts, required - documentation, unfinished placeholders, and symlinks without executing Plugin code. +1. **Package validation** checks the owner/name layout, Manifest, Skill, MCP, and Hooks contracts, + required documentation, unfinished placeholders, and symlinks without executing Plugin code. 2. **Pull request evidence** makes every source change reviewable before it reaches `main`. 3. **Human review** evaluates usefulness, dependencies, data flow, suspicious code, maintenance ownership, and reproducible test evidence. These checks reduce ambiguity but are not a sandbox or full audit. `stdio` MCP servers execute local programs with the user's permissions. Remote MCP servers send data to configured destinations. -Skills can instruct an agent to use tools and change files. +Skills can instruct an agent to use tools and change files. Hooks can execute local commands +implicitly at lifecycle points and may receive prompt or tool data, so they remain disabled until a +compatible client obtains user or organization authorization. + +Hook authors must disclose every event, executable, platform requirement, file write, network +destination, and data category the Hook can receive or persist. Hooks 0.1 is observe-only: output, +failure, or timeout cannot approve, deny, rewrite, or otherwise change agent behavior. Clients must +sanitize ambient environment values, isolate `PLUGIN_DATA`, bound process resources and diagnostics, +and provide global and per-Plugin disable controls. + +Static registry validation parses Hooks as data and never executes commands from hosted +contributions under `plugins/`. Repository tests may execute explicitly reviewed, repository-owned +example fixtures with a minimal environment. Adding a Hook, changing its executable, or expanding +its data access is a capability change that runtime clients should present for authorization again. Never commit credentials, private endpoints, or personal data. Use runtime-supported secret and environment mechanisms. Maintainers may quarantine or remove a Plugin while a security report is diff --git a/examples/hello-mcode-hooks/README.md b/examples/hello-mcode-hooks/README.md new file mode 100644 index 0000000..a0e9fc2 --- /dev/null +++ b/examples/hello-mcode-hooks/README.md @@ -0,0 +1,33 @@ +# Hello MiniMax Code Hooks + +This Hook-only example records `pre-tool-use` and `post-tool-use` event names in +`${PLUGIN_DATA}/events/`. It consumes but never stores the client-native event payload and retains +at most 128 atomic JSON records, pruning the oldest records after concurrent invocations. + +## Requirements + +- a MiniMax Code build that advertises the MiniMax Code Hooks 0.1 schema; +- Node.js 22 or newer on `PATH`; and +- user or organization approval to execute the bundled Hook script. + +It requires no account or paid service, makes no network requests, and stores only an event name and +timestamp in the Plugin's isolated data directory. + +This repository does not yet link a MiniMax Code runtime build and end-to-end fixture certified for +Hooks 0.1. The package is therefore a declaration and validation example, not evidence that a +currently released client will execute it. + +## Try it + +After installing the Plugin into a compatible MiniMax Code build, ask: + +```text +List the files in this workspace. +``` + +Expected result: MiniMax Code performs its normal tool work, while the Hook appends bounded event +records to `${PLUGIN_DATA}/events/`. The Hook cannot block or modify the tool call. + +Run `npm run check` from the repository root. Static Plugin validation does not execute Hook +commands; the test suite separately executes this repository-owned example with a minimal +environment to verify bounded concurrent writes. diff --git a/examples/hello-mcode-hooks/io.minimax.mcode/hooks/hooks.json b/examples/hello-mcode-hooks/io.minimax.mcode/hooks/hooks.json new file mode 100644 index 0000000..7736130 --- /dev/null +++ b/examples/hello-mcode-hooks/io.minimax.mcode/hooks/hooks.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Code-Plugins/main/schemas/io.minimax.mcode/hooks/0.1.0.schema.json", + "hooks": { + "pre-tool-use": [ + { + "command": "node", + "args": [ + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs", + "pre-tool-use" + ] + } + ], + "post-tool-use": [ + { + "command": "node", + "args": [ + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs", + "post-tool-use" + ] + } + ] + } +} diff --git a/examples/hello-mcode-hooks/io.minimax.mcode/hooks/scripts/record.mjs b/examples/hello-mcode-hooks/io.minimax.mcode/hooks/scripts/record.mjs new file mode 100644 index 0000000..17bf406 --- /dev/null +++ b/examples/hello-mcode-hooks/io.minimax.mcode/hooks/scripts/record.mjs @@ -0,0 +1,55 @@ +import { randomUUID } from 'node:crypto'; +import { mkdir, readdir, rename, unlink, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +const MAX_RECORDS = 128; +const STALE_TEMP_MS = 5 * 60 * 1_000; +const TEMP_RECORD = /^(?\d{13})-\d{20}-[0-9a-f-]{36}\.tmp$/u; + +const event = process.argv[2]; +const supportedEvents = new Set(['pre-tool-use', 'post-tool-use']); +if (!supportedEvents.has(event)) throw new Error('expected a configured Hook event name'); + +let inputBytes = 0; +for await (const chunk of process.stdin) { + inputBytes += chunk.length; + if (inputBytes > 64 * 1024) throw new Error('Hook input exceeds 64 KiB'); +} + +const dataRoot = process.env.PLUGIN_DATA; +if (!dataRoot || !path.isAbsolute(dataRoot)) throw new Error('PLUGIN_DATA must be an absolute path'); + +const recordsRoot = path.join(dataRoot, 'events'); +await mkdir(recordsRoot, { recursive: true }); +const timestamp = Date.now(); +const recordStem = `${timestamp.toString().padStart(13, '0')}-${process.hrtime.bigint().toString().padStart(20, '0')}-${randomUUID()}`; +const temporary = path.join(recordsRoot, `${recordStem}.tmp`); +const published = path.join(recordsRoot, `${recordStem}.json`); +await writeFile( + temporary, + `${JSON.stringify({ event, timestamp: new Date().toISOString() })}\n`, + { encoding: 'utf8', flag: 'wx', mode: 0o600 }, +); +await rename(temporary, published); + +const entries = await readdir(recordsRoot, { withFileTypes: true }); +const records = entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.json')) + .map((entry) => entry.name) + .sort(); +for (const expired of records.slice(0, Math.max(0, records.length - MAX_RECORDS))) { + try { + await unlink(path.join(recordsRoot, expired)); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } +} +for (const entry of entries.filter((item) => item.isFile())) { + const match = TEMP_RECORD.exec(entry.name); + if (!match || timestamp - Number(match.groups.timestamp) <= STALE_TEMP_MS) continue; + try { + await unlink(path.join(recordsRoot, entry.name)); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } +} diff --git a/examples/hello-mcode-hooks/plugin.json b/examples/hello-mcode-hooks/plugin.json new file mode 100644 index 0000000..b0bee3b --- /dev/null +++ b/examples/hello-mcode-hooks/plugin.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "hello-mcode-hooks", + "version": "0.1.0", + "description": "Record MiniMax Code Hook event names locally without retaining event payloads.", + "license": "Apache-2.0" +} diff --git a/package-lock.json b/package-lock.json index 497688a..3e30ee3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,70 @@ "name": "minimax-code-plugins", "version": "0.1.0", "license": "Apache-2.0", + "devDependencies": { + "ajv": "^8.17.1" + }, "engines": { "node": ">=22" } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } } } } diff --git a/package.json b/package.json index 77520b5..2d25648 100644 --- a/package.json +++ b/package.json @@ -13,5 +13,8 @@ "test": "node --test", "validate": "node scripts/validate.mjs" }, + "devDependencies": { + "ajv": "^8.17.1" + }, "license": "Apache-2.0" } diff --git a/plugins/README.md b/plugins/README.md index f05168d..595bb43 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -10,7 +10,9 @@ plugins/ ├── README.md ├── LICENSE ├── mcp.json # optional - └── skills/ # optional + ├── skills/ # optional + └── io.minimax.mcode/ # optional MiniMax Code Hooks extension + └── hooks/hooks.json ``` Create a contribution from the repository root: @@ -21,3 +23,8 @@ npm run create -- / Replace every `TODO`, run `npm run check`, and open one pull request for one Plugin. See [`CONTRIBUTING.md`](../CONTRIBUTING.md) for review and security requirements. + +A Plugin must contain at least one valid Skill, MCP server, or MiniMax Code Hook. Hook authors should +start with [`examples/hello-mcode-hooks`](../examples/hello-mcode-hooks/) and read the complete +[`Hooks 0.1 contract`](../docs/hooks.md). Hooks are currently a staged registry declaration; this +repository does not yet certify a MiniMax Code runtime build that executes them. diff --git a/proposals/README.md b/proposals/README.md index ee8f249..c39e33a 100644 --- a/proposals/README.md +++ b/proposals/README.md @@ -1,11 +1,16 @@ # Capability proposals This directory records evidence and design proposals for future Agent capabilities outside the -portable Agent Plugins 1.0 surface, such as Hooks, custom Agents, Commands, LSP, and OAuth. +current package contract, such as custom Agents, Commands, LSP, OAuth, and Hooks behavior beyond the +observe-only 0.1 contract. A proposal is not a supported plugin capability. It must identify real user workflows, define host ownership and security boundaries, explain cross-client portability, and link to implementation and conformance evidence before documentation can present it as available. +The staged Hooks 0.1 declaration contract lives in [`docs/hooks.md`](../docs/hooks.md). Static +registry acceptance does not promote Hooks to an available runtime capability; that promotion still +requires a linked MiniMax Code implementation and end-to-end conformance evidence under this rule. + TUI Extensions are a separate product extension system. They do not enter this proposal or package contract. diff --git a/proposals/hooks.md b/proposals/hooks.md deleted file mode 100644 index b5e254c..0000000 --- a/proposals/hooks.md +++ /dev/null @@ -1,335 +0,0 @@ -# Portable Hooks preview for MiniMax Code - -Status: Proposal - -Portable baseline: Agent Plugins 1.0 - -Related upstream discussion: [Portable Hooks Component Type #54](https://github.com/agentplugins/agent-plugins-spec/discussions/54) - -This document proposes an experimental MiniMax Code client extension for lifecycle Hooks. It is a -design and implementation plan, not a supported Plugin capability. Documentation, validation, and -runtime support must all land with conformance evidence before MiniMax Code advertises Hooks as -available. - -## Recommendation - -MiniMax Code should not add `hooks` to the root `plugin.json` manifest or describe Hooks as an Agent -Plugins 1.0 component. Agent Plugins 1.0 defines exactly two portable component types: Skills and -MCP servers. Its design notes explicitly leave Hooks outside v1 because client formats and behavior -have not yet converged. - -The standards-compliant preview path is: - -1. keep `plugin.json`, Skills, and `mcp.json` conformant to Agent Plugins 1.0; -2. place the preview contract under a stable MiniMax-owned client-extension namespace; -3. align its declaration with upstream Discussion #54 instead of inventing another event model; -4. implement and test the same observable behavior in the MiniMax Code runtime; and -5. migrate to a root portable `hooks.json` only after an Agent Plugins release standardizes it. - -The accurate compatibility claim during preview would be: - -> Agent Plugins 1.0 compatible, with an experimental MiniMax Code Hooks extension aligned with the -> portable Hooks proposal. - -It must not be shortened to “Agent Plugins 1.0 Hooks support.” - -## Evidence for a portable floor - -Upstream Discussion #54 reports production integrations across twelve agent clients and identifies -six recurring lifecycle points despite different native names, configuration formats, and delivery -mechanisms. It proposes a small observe-only declaration: run one command when one lifecycle event -occurs. The proposal intentionally leaves payloads, blocking, permissions, and output control for -later work. - -MiniMax Code should adopt that small floor first: - -| Portable event | Meaning | -| --- | --- | -| `session-start` | A session starts or resumes. | -| `turn-start` | The user starts a new agent turn. | -| `pre-tool-use` | A tool call is about to execute. | -| `post-tool-use` | A tool call has completed. | -| `turn-end` | The agent turn finishes or becomes idle. | -| `session-end` | The session terminates. | - -The upstream discussion is active evidence, not a published standard. The current Agent Plugins -1.1 working draft still defines only Skills and MCP servers. MiniMax implementation evidence should -therefore be contributed back to that discussion without claiming that its outcome is already -decided. - -## Goals - -- Provide one inspectable declaration for common lifecycle side effects such as provenance, - telemetry disclosed by the Plugin, cache warming, and cleanup. -- Preserve Agent Plugins 1.0 conformance while the portable proposal is unresolved. -- Reuse existing Agent Plugins subprocess and path-safety concepts where they apply. -- Isolate invalid configuration and runtime failures from independent Skills, MCP servers, and Hook - entries. -- Make implicit code execution visible, explicitly authorized, bounded, auditable, and revocable. -- Keep the preview structurally close enough to the upstream proposal for a mechanical migration. - -## Non-goals - -The portable preview does not define: - -- blocking, approval, denial, or permission decisions; -- tool-input or tool-result rewriting; -- model-context injection; -- portable stdin payloads; -- subagent, compaction, notification, worktree, or configuration-change events; -- HTTP, prompt, agent, MCP-tool, or asynchronous Hook handlers; -- cross-Plugin ordering dependencies; or -- a sandbox, marketplace trust level, or secret-distribution mechanism. - -MiniMax-specific policy Hooks may be proposed after the observe-only floor is implemented. They -must use an explicitly separate profile and must not silently change the semantics of portable -events. - -## Package layout - -Agent Plugins client extensions use a stable reverse-domain namespace and a matching top-level -directory. If `minimax.io` is confirmed as the ownership root, the namespace can be -`io.minimax.mcode`: - -```text -plugin-root/ -├── plugin.json -├── skills/ # optional Agent Plugins component -├── mcp.json # optional Agent Plugins component -└── io.minimax.mcode/ # experimental client extension - └── hooks/ - ├── hooks.json - └── scripts/ - └── record.mjs -``` - -The extension directory is sufficient for discovery. The root manifest does not need a redundant -activation field. If installation UI later requires extension metadata, it may be defined under -`extensions.io.minimax.mcode` without changing root Agent Plugins fields. - -The namespace must be confirmed by the MiniMax specification owner before publication and remain -stable after release. - -## Preview document - -The preview should publish an immutable, MiniMax-controlled JSON Schema. This example canonical ID -is illustrative and must not be used by Plugins until the schema exists at that exact URL: - -```json -{ - "$schema": "https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json", - "hooks": { - "pre-tool-use": [ - { - "command": "node", - "args": [ - "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs" - ] - } - ], - "turn-end": [ - { - "command": "node", - "args": [ - "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs", - "--state", - "${PLUGIN_DATA}/state.json" - ] - } - ] - } -} -``` - -The schema should be closed and define only: - -- required `$schema` and `hooks` root fields; -- the six published event keys; -- one or more entries per event; -- a required single-token `command`; -- optional string-array `args`; -- optional string-valued `env`; and -- optional contained `cwd`. - -The final field set should track upstream Discussion #54. Any deliberate difference must be -documented with an interoperability reason and contributed upstream. - -## Command execution - -Hook commands reuse the safe parts of the Agent Plugins stdio MCP process contract, repeated here -as explicit MiniMax extension requirements: - -- `command` is one executable token: a bare executable name or a contained `./` path. It is not a - shell command string. -- `args` values are passed as distinct process arguments without shell interpretation. -- MiniMax Code provides absolute `PLUGIN_ROOT` and per-installed-instance `PLUGIN_DATA` values and - prevents the Plugin from overriding them. -- `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` are expanded once, non-recursively, only in fields allowed - by the extension schema. -- Explicit working directories resolve within `PLUGIN_ROOT` or `PLUGIN_DATA` after real-path - resolution. Symlink, junction, reparse-point, and traversal escapes are rejected. -- The client sanitizes the base environment instead of inheriting credentials and unrelated ambient - variables by default. -- The client launches the Hook without a shell and enforces hard limits for time, stdin, stdout, - stderr, process-tree lifetime, invocation count, and concurrency. - -## Observe-only runtime semantics - -Every preview event is observational: - -- exit status, stdout, stderr, timeout, cancellation, and spawn failure cannot alter agent behavior; -- `pre-tool-use` cannot block, approve, or modify a tool call; -- `post-tool-use` cannot replace or redact the tool result; -- failed Hook commands produce bounded diagnostics and do not prevent other Hooks or components from - loading or running; and -- `session-end` is best effort and cannot delay client shutdown beyond its client-owned budget. - -MiniMax Code may send its native event payload as one UTF-8 JSON document on stdin, followed by EOF. -That payload must be versioned and documented for MiniMax Plugin authors, but it is not a portable -Agent Plugins payload while the upstream proposal leaves payloads client-defined. Stdout and stderr -are diagnostic-only in this preview. - -Within one Plugin, handlers must run in declaration order. Observational handlers may run concurrently -across Plugins, but diagnostics must retain the event identifier, Plugin identity, handler index, -duration, and outcome. Plugins must not depend on cross-Plugin ordering. - -## Loading and failure isolation - -The loader should use the narrowest applicable failure boundary: - -1. An invalid root `plugin.json` rejects the Plugin under the Agent Plugins core rules. -2. A namespace directory that resolves outside the Plugin root disables that extension and denies - access to the escaped path. -3. Invalid JSON, an unsupported schema version, or invalid top-level `hooks.json` structure disables - Hooks for that Plugin while valid Skills and MCP servers continue loading. -4. An invalid event entry skips only that entry and reports a diagnostic. -5. One Hook process failure does not disable other Hook entries or independent components. -6. Clients that do not implement `io.minimax.mcode` ignore the extension without validating it. - -MiniMax-Code-Plugins currently requires at least one valid Skill or MCP server. Whether the community -registry will admit Hook-only Plugins is a separate publication-policy decision. If admitted, the -catalog must explain that Agent Plugins clients without the MiniMax extension may load no usable -component from such a package. - -## Security and authorization - -Hooks introduce implicit local code execution at lifecycle points and may receive sensitive prompt -or tool data. JSON Schema validation alone is insufficient. - -MiniMax Code must: - -- show Hook events, commands, data categories, network behavior, and requested workspace access - before enabling them; -- leave executable Hooks disabled until a user or organization policy explicitly authorizes them; -- request authorization again when an update expands events, commands, data access, or other - capabilities; -- avoid sending full transcripts, system prompts, secrets, or unrelated tool data by default; -- isolate `PLUGIN_DATA` by installed Plugin instance; -- redact sensitive payload and environment values from diagnostics and audit records; -- provide global disable, per-Plugin disable, and safe-mode recovery paths; and -- never run contributed Hook code during registry validation or CI. - -The hosted repository must continue rejecting secrets, private endpoints, hidden telemetry, -symlinks, native binaries, and undisclosed installers. Static review must not be described as a -sandbox or complete security audit. - -## Conformance evidence - -Two suites are required and must be reported separately. - -### Agent Plugins 1.0 core - -- Root `plugin.json` continues targeting the published 1.0 schema. -- Unimplemented extension namespaces remain ignored without internal validation. -- A missing extension directory is valid absence. -- Invalid MiniMax Hooks do not prevent valid Skills or MCP servers from loading. -- All package paths remain inside the filesystem-resolved Plugin root. - -### MiniMax Hooks preview - -- valid and invalid schema fixtures, including unknown fields and unsupported versions; -- schema selection from a local supported-version table without fetching schemas during loading; -- mixed valid and invalid entries proving that one invalid entry does not block its valid siblings; -- exactly one preview-event delivery for each corresponding lifecycle occurrence, covering all six - events over success, failure, cancellation, and session resume; -- exact argument boundaries proving no shell interpolation; -- placeholder, working-directory, real-path, symlink, and reserved-environment cases; -- timeout and process-tree cleanup without orphaned processes; -- bounded and redacted stdin, stdout, stderr, diagnostics, and audit data; -- disabled and unauthorized Hooks never executing; -- failed Hooks never changing agent or permission behavior; and -- a deterministic end-to-end fixture installed into a real MiniMax Code runtime. - -Registry CI must remain static and must never execute Plugin Hook code. - -## Implementation surfaces - -### MiniMax-Code-Plugins - -After runtime ownership and the preview contract are approved, this repository would need coordinated -changes to: - -- `docs/plugin-compatibility.md` to separate the portable core from MiniMax client extensions; -- `docs/security-model.md` for implicit execution, consent, disclosure, and failure behavior; -- `scripts/lib/validation.mjs` for static extension-schema and path validation; -- `test/validation.test.mjs` for schema and isolation fixtures; -- an `examples/hello-mcode-hooks/` package; and -- README and contribution language that labels the capability experimental and non-portable. - -Those changes must not merge before the matching runtime can be installed and tested. - -### MiniMax Code runtime - -The runtime owns: - -- extension discovery and a local schema registry; -- install consent and capability-diff consent on update; -- lifecycle event mapping; -- a bounded process runner and observe-only dispatcher; -- timeout, cancellation, and process-tree cleanup; -- audit, diagnostics, and redaction; -- user and organization enablement policy; and -- end-to-end conformance fixtures across interactive, headless, background, and remote sessions that - claim support. - -Accepting `hooks.json` in this repository without the runtime work is not Hooks support. - -## Promotion path - -1. Confirm the MiniMax namespace, six-event observe-only scope, and runtime owner. -2. Add MiniMax's adoption intent, event mapping, and semantic gaps to upstream Discussion #54. -3. Publish the immutable preview schema and human-readable contract on a MiniMax-controlled domain. -4. Implement one `pre-tool-use` observe-only vertical slice with consent, diagnostics, and audit. -5. Implement the other five events and pass the MiniMax conformance fixtures. -6. Add coordinated registry validation, documentation, and an example; label the release experimental. -7. If Agent Plugins publishes a portable Hooks component, migrate the extension file to the standard - root `hooks.json` and its canonical schema. - -An upstream specification change must update normative prose, schemas, canonical identifiers, fixed -discovery locations, version rules, failure boundaries, examples, and the conformance checklist as -one conceptual surface. A schema-only pull request is insufficient. - -Blocking decisions, context injection, payload standardization, additional events, and alternative -handler types require separate interoperability evidence and versioned proposals. - -## Open decisions - -- Confirm the permanent reverse-domain namespace. -- Identify the runtime owner and supported release channels. -- Decide whether Hook-only packages are eligible for the community registry. -- Document exact event mappings for main agents, subagents, parallel tools, resumed sessions, and - background or remote execution. -- Define the MiniMax-native stdin payload and disclosure policy without presenting it as portable. -- Set client-owned resource budgets and update re-authorization rules. -- Decide which second client will run shared conformance fixtures before portable standardization. - -## Primary sources - -- [Agent Plugins 1.0 specification](https://agent-plugins.org/specification) -- [Agent Plugins client conformance checklist](https://agent-plugins.org/client-implementers/conformance) -- [Agent Plugins client extensions](https://agent-plugins.org/plugin-authors/client-extensions) -- [Agent Plugins contribution process](https://github.com/agentplugins/agent-plugins-spec/blob/main/CONTRIBUTING.md) -- [Agent Plugins Discussion #54: Portable Hooks Component Type](https://github.com/agentplugins/agent-plugins-spec/discussions/54) -- [MiniMax Code Plugin compatibility](../docs/plugin-compatibility.md) -- [MiniMax Code Plugin security model](../docs/security-model.md) -- [Capability proposal policy](README.md) diff --git a/schemas/io.minimax.mcode/hooks/0.1.0.schema.json b/schemas/io.minimax.mcode/hooks/0.1.0.schema.json new file mode 100644 index 0000000..112bbc3 --- /dev/null +++ b/schemas/io.minimax.mcode/hooks/0.1.0.schema.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Code-Plugins/main/schemas/io.minimax.mcode/hooks/0.1.0.schema.json", + "title": "MiniMax Code Hooks client extension 0.1.0", + "description": "Observe-only lifecycle Hook declarations for the io.minimax.mcode Agent Plugins client extension.", + "type": "object", + "additionalProperties": false, + "required": ["$schema", "hooks"], + "properties": { + "$schema": { + "const": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Code-Plugins/main/schemas/io.minimax.mcode/hooks/0.1.0.schema.json" + }, + "hooks": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "$comment": "JSON Schema cannot express the cross-property sum. The community registry validator additionally limits the combined total to 32 handlers per plugin.", + "properties": { + "session-start": { "$ref": "#/$defs/handlers" }, + "turn-start": { "$ref": "#/$defs/handlers" }, + "pre-tool-use": { "$ref": "#/$defs/handlers" }, + "post-tool-use": { "$ref": "#/$defs/handlers" }, + "turn-end": { "$ref": "#/$defs/handlers" }, + "session-end": { "$ref": "#/$defs/handlers" } + } + } + }, + "$defs": { + "handlers": { + "type": "array", + "minItems": 1, + "maxItems": 8, + "items": { "$ref": "#/$defs/handler" } + }, + "handler": { + "type": "object", + "additionalProperties": false, + "required": ["command"], + "properties": { + "command": { + "type": "string", + "anyOf": [ + { + "description": "A single bare executable token.", + "pattern": "^[^\\s/\\\\\\u0000]+$" + }, + { + "description": "A contained plugin-relative executable path.", + "pattern": "^\\./(?!(?:\\.\\.(?:/|$)|.*\\/\\.\\.(?:/|$)))[^\\\\\\u0000]+$" + } + ] + }, + "args": { + "type": "array", + "maxItems": 64, + "items": { "type": "string", "pattern": "^[^\\u0000]*$" } + }, + "env": { + "type": "object", + "maxProperties": 64, + "propertyNames": { + "allOf": [ + { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, + { + "not": { + "pattern": "^(?:[Pp][Ll][Uu][Gg][Ii][Nn]_[Rr][Oo][Oo][Tt]|[Pp][Ll][Uu][Gg][Ii][Nn]_[Dd][Aa][Tt][Aa])$" + } + } + ] + }, + "additionalProperties": { "type": "string", "pattern": "^[^\\u0000]*$" }, + "$comment": "PLUGIN_ROOT and PLUGIN_DATA are reserved case-insensitively." + }, + "cwd": { + "type": "string", + "anyOf": [ + { + "pattern": "^\\./(?!(?:\\.\\.(?:/|$)|.*\\/\\.\\.(?:/|$)))[^\\\\\\u0000]*$" + }, + { + "pattern": "^\\$\\{PLUGIN_(?:ROOT|DATA)\\}(?:/(?!(?:\\.\\.(?:/|$)|.*\\/\\.\\.(?:/|$)))[^\\\\\\u0000]*)?$" + } + ] + } + } + } + } +} diff --git a/scripts/lib/validation.mjs b/scripts/lib/validation.mjs index cc2a324..333c273 100644 --- a/scripts/lib/validation.mjs +++ b/scripts/lib/validation.mjs @@ -3,10 +3,23 @@ import path from 'node:path'; export const PLUGIN_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json'; export const MCP_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json'; +export const HOOKS_NAMESPACE = 'io.minimax.mcode'; +export const HOOKS_SCHEMA = 'https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Code-Plugins/main/schemas/io.minimax.mcode/hooks/0.1.0.schema.json'; +export const HOOK_EVENTS = Object.freeze([ + 'session-start', + 'turn-start', + 'pre-tool-use', + 'post-tool-use', + 'turn-end', + 'session-end', +]); const PLUGIN_NAME = /^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u; const OWNER_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/u; const SKILL_NAME = /^(?!.*--)[a-z0-9]+(?:-[a-z0-9]+)*$/u; +const HOOK_EVENT_SET = new Set(HOOK_EVENTS); +const HOOK_HANDLER_FIELDS = new Set(['command', 'args', 'env', 'cwd']); +const RESERVED_PLUGIN_ENV = new Set(['PLUGIN_ROOT', 'PLUGIN_DATA']); const PLUGIN_FIELDS = new Set([ '$schema', 'name', @@ -102,14 +115,95 @@ export function validateMcp(value, label = 'mcp.json') { return entries.map(([name]) => name).sort(); } +export function validateHooks(value, label = 'hooks.json') { + assert(isRecord(value), `${label}: root must be an object`); + assert(value.$schema === HOOKS_SCHEMA, `${label}: unsupported $schema`); + assert(Object.keys(value).every((key) => ['$schema', 'hooks'].includes(key)), `${label}: unknown root field`); + assert(isRecord(value.hooks), `${label}: hooks must be an object`); + + const events = Object.keys(value.hooks); + assert(events.length > 0, `${label}: hooks must contain at least one event`); + let handlerCount = 0; + for (const event of events) { + assert(HOOK_EVENT_SET.has(event), `${label}: unsupported event ${event}`); + const handlers = value.hooks[event]; + assert(Array.isArray(handlers) && handlers.length > 0, `${label}: ${event} must contain at least one handler`); + assert(handlers.length <= 8, `${label}: ${event} supports at most 8 handlers`); + handlerCount += handlers.length; + for (const [index, handler] of handlers.entries()) { + const handlerLabel = `${event}[${index}]`; + assert(isRecord(handler), `${label}: ${handlerLabel} must be an object`); + assert(Object.keys(handler).every((key) => HOOK_HANDLER_FIELDS.has(key)), `${label}: ${handlerLabel} has unsupported fields`); + assert( + typeof handler.command === 'string' + && (isBareExecutable(handler.command) || isContainedRelativeExecutable(handler.command)), + `${label}: ${handlerLabel}.command must be a single bare executable or contained ./ path`, + ); + assert( + handler.args === undefined + || (Array.isArray(handler.args) + && handler.args.length <= 64 + && handler.args.every((item) => typeof item === 'string' && !item.includes('\0'))), + `${label}: ${handlerLabel}.args must be strings with at most 64 entries`, + ); + assert( + handler.env === undefined + || (isRecord(handler.env) + && Object.keys(handler.env).length <= 64 + && Object.entries(handler.env).every(([key, item]) => ( + /^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) + && !RESERVED_PLUGIN_ENV.has(key.toUpperCase()) + && typeof item === 'string' + && !item.includes('\0') + ))), + `${label}: ${handlerLabel}.env is invalid`, + ); + assert( + handler.cwd === undefined || (typeof handler.cwd === 'string' && isContainedWorkingDirectory(handler.cwd)), + `${label}: ${handlerLabel}.cwd is invalid`, + ); + } + } + assert(handlerCount <= 32, `${label}: MiniMax Code supports at most 32 handlers per plugin`); + return { + events: HOOK_EVENTS.filter((event) => Object.hasOwn(value.hooks, event)), + handlerCount, + }; +} + function isBareCommand(value) { return !/[\\/]/u.test(value); } +function isBareExecutable(value) { + return value.length > 0 && !value.includes('\0') && !/[\s\\/]/u.test(value); +} + function isContainedRelativePath(value) { return value.startsWith('./') && !value.split('/').includes('..') && !value.includes('\\'); } +function isContainedRelativeExecutable(value) { + return value.startsWith('./') && isContainedPathSuffix(value.slice(2)); +} + +function isContainedWorkingDirectory(value) { + if (value === './') return true; + if (value.startsWith('./')) return isContainedPathSuffix(value.slice(2)); + for (const placeholder of ['${PLUGIN_ROOT}', '${PLUGIN_DATA}']) { + if (value === placeholder) return true; + if (value.startsWith(`${placeholder}/`)) { + const suffix = value.slice(placeholder.length + 1); + return suffix.length === 0 || isContainedPathSuffix(suffix); + } + } + return false; +} + +function isContainedPathSuffix(value) { + return value.length > 0 && !value.includes('\0') && !value.includes('\\') && !value.split('/').includes('..'); +} + function isSafeRemoteUrl(value) { try { const url = new URL(value); @@ -126,6 +220,13 @@ function isSafeRemoteUrl(value) { export async function validatePluginDirectory(root) { const manifestPath = path.join(root, 'plugin.json'); const manifest = validatePluginManifest(parseJson(await readFile(manifestPath, 'utf8'), manifestPath), manifestPath); + const unsupportedRootHooksPath = path.join(root, 'hooks.json'); + try { + await readFile(unsupportedRootHooksPath, 'utf8'); + throw new Error(`${unsupportedRootHooksPath}: MiniMax Code Hooks must use ${HOOKS_NAMESPACE}/hooks/hooks.json`); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } const skills = []; const skillsRoot = path.join(root, 'skills'); let children = []; @@ -147,8 +248,21 @@ export async function validatePluginDirectory(root) { } catch (error) { if (error.code !== 'ENOENT') throw error; } - assert(skills.length + mcpServers.length > 0, `${root}: plugin must expose at least one Skill or MCP server`); - return { manifest, skills: skills.sort(), mcpServers }; + let hookEvents = []; + let hookHandlers = 0; + const hooksPath = path.join(root, HOOKS_NAMESPACE, 'hooks', 'hooks.json'); + try { + const hooks = validateHooks(parseJson(await readFile(hooksPath, 'utf8'), hooksPath), hooksPath); + hookEvents = hooks.events; + hookHandlers = hooks.handlerCount; + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + assert( + skills.length + mcpServers.length + hookHandlers > 0, + `${root}: plugin must expose at least one Skill, MCP server, or MiniMax Code Hook`, + ); + return { manifest, skills: skills.sort(), mcpServers, hookEvents, hookHandlers }; } export async function validateHostedPluginDirectory(root, { owner, pluginName }) { @@ -182,7 +296,7 @@ async function listTextContractFiles(root) { for (const child of await readdir(root, { withFileTypes: true })) { const file = path.join(root, child.name); if (child.isDirectory()) files.push(...await listTextContractFiles(file)); - else if (child.isFile() && (child.name.endsWith('.md') || ['plugin.json', 'mcp.json'].includes(child.name))) files.push(file); + else if (child.isFile() && (child.name.endsWith('.md') || ['plugin.json', 'mcp.json', 'hooks.json'].includes(child.name))) files.push(file); } return files; } diff --git a/test/hooks-example.test.mjs b/test/hooks-example.test.mjs new file mode 100644 index 0000000..833ba39 --- /dev/null +++ b/test/hooks-example.test.mjs @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const script = path.join( + repositoryRoot, + 'examples', + 'hello-mcode-hooks', + 'io.minimax.mcode', + 'hooks', + 'scripts', + 'record.mjs', +); + +function runHook(dataRoot, event) { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [script, event], { + env: { PLUGIN_DATA: dataRoot }, + stdio: 'ignore', + }); + child.once('error', reject); + child.once('exit', (code, signal) => { + if (code === 0) resolve(); + else reject(new Error(`Hook exited with code ${String(code)} and signal ${String(signal)}`)); + }); + }); +} + +test('Hook example retains concurrent records within its storage bound', async (context) => { + const { mkdtemp, rm } = await import('node:fs/promises'); + const dataRoot = await mkdtemp(path.join(os.tmpdir(), 'minimax-code-hooks-example-')); + context.after(() => rm(dataRoot, { recursive: true, force: true })); + const recordsRoot = path.join(dataRoot, 'events'); + await mkdir(recordsRoot); + const activeTemporary = `${Date.now().toString().padStart(13, '0')}-00000000000000000000-00000000-0000-4000-8000-000000000000.tmp`; + const staleTemporary = '0000000000000-00000000000000000000-00000000-0000-4000-8000-000000000001.tmp'; + await Promise.all([ + writeFile(path.join(recordsRoot, activeTemporary), '{"partial":', 'utf8'), + writeFile(path.join(recordsRoot, staleTemporary), '{"abandoned":', 'utf8'), + ]); + + const invocationCount = 140; + await Promise.all(Array.from( + { length: invocationCount }, + (_, index) => runHook(dataRoot, index % 2 === 0 ? 'pre-tool-use' : 'post-tool-use'), + )); + + const entries = await readdir(recordsRoot); + const records = entries.filter((entry) => entry.endsWith('.json')).sort(); + assert.equal(records.length, 128); + assert.equal(entries.includes(activeTemporary), true); + assert.equal(entries.includes(staleTemporary), false); + const values = await Promise.all(records.map(async (record) => ( + JSON.parse(await readFile(path.join(recordsRoot, record), 'utf8')) + ))); + assert.ok(values.every(({ event }) => ['pre-tool-use', 'post-tool-use'].includes(event))); +}); diff --git a/test/hooks-schema.test.mjs b/test/hooks-schema.test.mjs new file mode 100644 index 0000000..2875544 --- /dev/null +++ b/test/hooks-schema.test.mjs @@ -0,0 +1,84 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import Ajv2020 from 'ajv/dist/2020.js'; + +import { HOOK_EVENTS, HOOKS_SCHEMA, validateHooks } from '../scripts/lib/validation.mjs'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +test('published Hooks schema encodes the field-level validator contract', async () => { + const schema = JSON.parse(await readFile( + path.join(repositoryRoot, 'schemas', 'io.minimax.mcode', 'hooks', '0.1.0.schema.json'), + 'utf8', + )); + + assert.equal(schema.$id, HOOKS_SCHEMA); + assert.equal(schema.properties.$schema.const, HOOKS_SCHEMA); + assert.deepEqual(Object.keys(schema.properties.hooks.properties), HOOK_EVENTS); + assert.equal(schema.properties.hooks.additionalProperties, false); + assert.equal(schema.$defs.handlers.maxItems, 8); + assert.equal(schema.$defs.handler.additionalProperties, false); + const commandVariants = schema.$defs.handler.properties.command.anyOf; + for (const variant of commandVariants) { + assert.doesNotThrow(() => new RegExp(variant.pattern, 'u')); + } + const cwdVariants = schema.$defs.handler.properties.cwd.anyOf; + for (const variant of cwdVariants) { + assert.doesNotThrow(() => new RegExp(variant.pattern, 'u')); + } + + const matchesAny = (variants, value) => variants.some(({ pattern }) => new RegExp(pattern, 'u').test(value)); + assert.equal(matchesAny(commandVariants, 'node'), true); + assert.equal(matchesAny(commandVariants, './scripts/record.mjs'), true); + assert.equal(matchesAny(commandVariants, 'node script.mjs'), false); + assert.equal(matchesAny(commandVariants, './scripts/../record.mjs'), false); + assert.equal(matchesAny(commandVariants, 'node\0'), false); + assert.equal(matchesAny(cwdVariants, './'), true); + assert.equal(matchesAny(cwdVariants, '${PLUGIN_DATA}/logs'), true); + assert.equal(matchesAny(cwdVariants, '${PLUGIN_ROOT}/../outside'), false); + assert.equal(matchesAny(cwdVariants, '/tmp'), false); + + const argumentPattern = new RegExp(schema.$defs.handler.properties.args.items.pattern, 'u'); + assert.equal(argumentPattern.test('ordinary argument'), true); + assert.equal(argumentPattern.test('bad\0argument'), false); +}); + +test('published schema and registry validator agree except for the documented aggregate limit', async () => { + const schema = JSON.parse(await readFile( + path.join(repositoryRoot, 'schemas', 'io.minimax.mcode', 'hooks', '0.1.0.schema.json'), + 'utf8', + )); + const validateSchema = new Ajv2020({ strict: true }).compile(schema); + const document = (hooks) => ({ $schema: HOOKS_SCHEMA, hooks }); + const valid = document({ + 'pre-tool-use': [{ + command: 'node', + args: ['${PLUGIN_ROOT}/record.mjs'], + env: { OUTPUT: '${PLUGIN_DATA}/events.jsonl' }, + cwd: '${PLUGIN_DATA}', + }], + }); + assert.equal(validateSchema(valid), true, JSON.stringify(validateSchema.errors)); + assert.doesNotThrow(() => validateHooks(valid)); + + for (const reservedName of ['PLUGIN_ROOT', 'plugin_data']) { + const reserved = document({ 'turn-end': [{ command: 'node', env: { [reservedName]: 'override' } }] }); + assert.equal(validateSchema(reserved), false); + assert.throws(() => validateHooks(reserved), /env is invalid/u); + } + + const sixHandlers = Array.from({ length: 6 }, () => ({ command: 'node' })); + const aggregateOnly = document({ + 'session-start': sixHandlers, + 'turn-start': sixHandlers, + 'pre-tool-use': sixHandlers, + 'post-tool-use': sixHandlers, + 'turn-end': sixHandlers, + 'session-end': sixHandlers, + }); + assert.equal(validateSchema(aggregateOnly), true, 'JSON Schema cannot sum handlers across event properties'); + assert.throws(() => validateHooks(aggregateOnly), /at most 32 handlers/u); +}); diff --git a/test/hosted-plugins.test.mjs b/test/hosted-plugins.test.mjs index 4e58656..dfaa0a9 100644 --- a/test/hosted-plugins.test.mjs +++ b/test/hosted-plugins.test.mjs @@ -7,7 +7,7 @@ import { promisify } from 'node:util'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; -import { validateHostedPluginDirectory } from '../scripts/lib/validation.mjs'; +import { HOOKS_SCHEMA, validateHostedPluginDirectory } from '../scripts/lib/validation.mjs'; const execFileAsync = promisify(execFile); const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); @@ -69,6 +69,102 @@ test('hosted Plugin is valid when its package and contribution docs are complete assert.deepEqual(result.skills, ['hello-world']); }); +test('hosted Plugin is valid with only a MiniMax Code Hook', async (context) => { + const workspace = await mkdtemp(path.join(os.tmpdir(), 'minimax-code-hooks-validation-')); + context.after(async () => { + const { rm } = await import('node:fs/promises'); + await rm(workspace, { recursive: true, force: true }); + }); + const pluginRoot = path.join(workspace, 'plugins', 'alice', 'event-recorder'); + const hooksRoot = path.join(pluginRoot, 'io.minimax.mcode', 'hooks'); + await mkdir(hooksRoot, { recursive: true }); + await Promise.all([ + writeFile(path.join(pluginRoot, 'plugin.json'), `${JSON.stringify({ + $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + name: 'event-recorder', + version: '1.0.0', + description: 'Records bounded MiniMax Code lifecycle events locally.', + license: 'Apache-2.0', + })}\n`), + writeFile(path.join(pluginRoot, 'README.md'), '# Event Recorder\n\nRecords lifecycle event names locally.\n'), + writeFile(path.join(pluginRoot, 'LICENSE'), 'Apache License\nVersion 2.0\n'), + writeFile(path.join(hooksRoot, 'hooks.json'), `${JSON.stringify({ + $schema: HOOKS_SCHEMA, + hooks: { + 'session-start': [{ + command: 'node', + args: ['${PLUGIN_ROOT}/io.minimax.mcode/hooks/record.mjs'], + }], + }, + })}\n`), + ]); + + const result = await validateHostedPluginDirectory(pluginRoot, { + owner: 'alice', + pluginName: 'event-recorder', + }); + + assert.deepEqual(result.hookEvents, ['session-start']); + assert.equal(result.hookHandlers, 1); + assert.deepEqual(result.skills, []); + assert.deepEqual(result.mcpServers, []); +}); + +test('hosted Plugin rejects a root hooks.json instead of silently ignoring it', async (context) => { + const workspace = await mkdtemp(path.join(os.tmpdir(), 'minimax-code-root-hooks-')); + context.after(async () => { + const { rm } = await import('node:fs/promises'); + await rm(workspace, { recursive: true, force: true }); + }); + const pluginRoot = path.join(workspace, 'plugins', 'alice', 'wrong-hooks-path'); + await mkdir(path.join(pluginRoot, 'skills', 'hello'), { recursive: true }); + await Promise.all([ + writeFile(path.join(pluginRoot, 'plugin.json'), `${JSON.stringify({ + $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + name: 'wrong-hooks-path', + license: 'Apache-2.0', + })}\n`), + writeFile(path.join(pluginRoot, 'README.md'), '# Wrong Hooks Path\n'), + writeFile(path.join(pluginRoot, 'LICENSE'), 'Apache License\nVersion 2.0\n'), + writeFile(path.join(pluginRoot, 'hooks.json'), '{}\n'), + writeFile(path.join(pluginRoot, 'skills', 'hello', 'SKILL.md'), '---\nname: hello\ndescription: Explain the wrong Hooks path when asked.\n---\n\nExplain the package.\n'), + ]); + + await assert.rejects( + validateHostedPluginDirectory(pluginRoot, { owner: 'alice', pluginName: 'wrong-hooks-path' }), + /Hooks must use io\.minimax\.mcode\/hooks\/hooks\.json/u, + ); +}); + +test('hosted Plugin rejects unfinished placeholders in hooks.json', async (context) => { + const workspace = await mkdtemp(path.join(os.tmpdir(), 'minimax-code-hooks-todo-')); + context.after(async () => { + const { rm } = await import('node:fs/promises'); + await rm(workspace, { recursive: true, force: true }); + }); + const pluginRoot = path.join(workspace, 'plugins', 'alice', 'unfinished-hook'); + const hooksRoot = path.join(pluginRoot, 'io.minimax.mcode', 'hooks'); + await mkdir(hooksRoot, { recursive: true }); + await Promise.all([ + writeFile(path.join(pluginRoot, 'plugin.json'), `${JSON.stringify({ + $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + name: 'unfinished-hook', + license: 'Apache-2.0', + })}\n`), + writeFile(path.join(pluginRoot, 'README.md'), '# Unfinished Hook\n'), + writeFile(path.join(pluginRoot, 'LICENSE'), 'Apache License\nVersion 2.0\n'), + writeFile(path.join(hooksRoot, 'hooks.json'), `${JSON.stringify({ + $schema: HOOKS_SCHEMA, + hooks: { 'turn-end': [{ command: 'node', args: ['TODO'] }] }, + })}\n`), + ]); + + await assert.rejects( + validateHostedPluginDirectory(pluginRoot, { owner: 'alice', pluginName: 'unfinished-hook' }), + /replace every TODO/u, + ); +}); + test('scaffold stays review-incomplete until contributor replaces every TODO', async (context) => { const workspace = await mkdtemp(path.join(os.tmpdir(), 'minimax-code-plugin-todo-')); context.after(async () => { diff --git a/test/validation.test.mjs b/test/validation.test.mjs index c7dfce6..d664d59 100644 --- a/test/validation.test.mjs +++ b/test/validation.test.mjs @@ -1,7 +1,13 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { validateMcp, validatePluginManifest, validateSkillText } from '../scripts/lib/validation.mjs'; +import { + HOOKS_SCHEMA, + validateHooks, + validateMcp, + validatePluginManifest, + validateSkillText, +} from '../scripts/lib/validation.mjs'; test('accepts the portable Agent Plugins manifest', () => { const value = validatePluginManifest({ @@ -42,3 +48,95 @@ test('validates supported MCP transports and reserved environment variables', () /env is invalid/u, ); }); + +test('validates the MiniMax Code Hooks 0.1 client extension', () => { + const result = validateHooks({ + $schema: HOOKS_SCHEMA, + hooks: { + 'pre-tool-use': [{ + command: 'node', + args: ['${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs'], + env: { OUTPUT: '${PLUGIN_DATA}/events.jsonl' }, + cwd: '${PLUGIN_DATA}', + }], + 'turn-end': [{ command: './io.minimax.mcode/hooks/scripts/record.mjs' }], + }, + }); + + assert.deepEqual(result, { + events: ['pre-tool-use', 'turn-end'], + handlerCount: 2, + }); +}); + +test('rejects unsupported Hook events and unsafe handler configuration', () => { + const document = (hooks) => ({ $schema: HOOKS_SCHEMA, hooks }); + + assert.throws(() => validateHooks(document({})), /at least one event/u); + assert.throws( + () => validateHooks(document({ Notification: [{ command: 'node' }] })), + /unsupported event Notification/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': [{ command: 'node script.mjs' }] })), + /single bare executable or contained \.\/ path/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': [{ command: './../outside.sh' }] })), + /single bare executable or contained \.\/ path/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': [{ command: './' }] })), + /single bare executable or contained \.\/ path/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': [{ command: 'node', args: ['ok', 1] }] })), + /args must be strings/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': [{ command: 'node', args: ['bad\0value'] }] })), + /args must be strings/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': [{ command: 'node', env: { plugin_root: 'bad' } }] })), + /env is invalid/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': [{ command: 'node', cwd: '${PLUGIN_ROOT}/../outside' }] })), + /cwd is invalid/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': [{ command: 'node', type: 'command' }] })), + /unsupported fields/u, + ); + assert.throws( + () => validateHooks(document({ 'pre-tool-use': Array.from({ length: 9 }, () => ({ command: 'node' })) })), + /at most 8 handlers/u, + ); +}); + +test('rejects unsupported Hooks schema versions and excessive total handlers', () => { + assert.throws( + () => validateHooks({ + $schema: 'https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Code-Plugins/main/schemas/io.minimax.mcode/hooks/9.9.9.schema.json', + hooks: { 'session-start': [{ command: 'node' }] }, + }), + /unsupported \$schema/u, + ); + + const sixHandlers = Array.from({ length: 6 }, () => ({ command: 'node' })); + assert.throws( + () => validateHooks({ + $schema: HOOKS_SCHEMA, + hooks: { + 'session-start': sixHandlers, + 'turn-start': sixHandlers, + 'pre-tool-use': sixHandlers, + 'post-tool-use': sixHandlers, + 'turn-end': sixHandlers, + 'session-end': sixHandlers, + }, + }), + /at most 32 handlers/u, + ); +});