Skip to content

Commit 1f5baf6

Browse files
committed
fix(hooks-detailed-spec): align io.minimax.mcode schema with @minimax-ai/code@0.3.10 runtime
Updates the PR #20 companion to record the hook shape the 0.3.10 runtime actually accepts. The previous companion's flat shape (`{command, args, matcher, timeout}` at the event level with no `hooks[]` wrapper) is silently skipped by the 0.3.10 parser with the warning "hooks.json matcher entry is missing a hooks[] array, skipping". Every Plugin written against the previous shape (including mcode-island v0.3.0) would receive zero deliveries in 0.3.10 even when the event name is in the Fwe allowlist. The new spec describes what the runtime actually parses: - outer (matcher) entry: {matcher, hooks[]} - inner (command) descriptor: {type, command, timeout} - `type` is the only consumed handler-kind discriminator and must be "command" (the only kind the parser dispatches in 0.3.10) - `command` is a single shell-executed string; `args[]` is not consumed by the parser and is rejected by the closed-schema validator so a Plugin migrating from 0.2.4 gets a clear error rather than a silent no-op - `timeout` is in seconds (the parser multiplies by 1000 internally); the previous companion's millisecond range (e.g. 5000) is out of bounds and would have meant 5,000,000 ms = 83 minutes at runtime - 15 PascalCase events (12 portable + 3 0.3.10 streaming): MessageComplete, StreamChunk, StreamChunkThreshold The 0.3.10 runtime reads hooks.json from `${MINIMAX_DATA_DIR}/hooks/hooks.json` or `${MINIMAX_DATA_DIR}/agents/<agentName>/hooks/hooks.json`, not from a Plugin's own `io.minimax.mcode/hooks/hooks.json` path. The Plugin registry accepts the `io.minimax.mcode` namespace in plugin.json but the hook-config parser does not consult that field. The spec records this caveat so a Plugin that wants its hooks to fire knows it must also install hooks.json into one of the two Runtime-resolved locations. Validation - scripts/lib/validation.mjs: HOOK_DOCUMENT_FIELDS grows to include all 15 events plus the `hooks` wrapper. New HOOK_MATCHER_FIELDS (`matcher`, `hooks`) and HOOK_COMMAND_FIELDS (`type`, `command`, `timeout`) split the previous HOOK_ENTRY_FIELDS allowlist into outer and inner halves. validateHookEntry walks `hooks[]`; validateHookCommand checks the inner descriptor. HOOK_RESERVED_FIELDS now covers the 0.2.4 fields the parser does not consume (`args`, `env`, `cwd`, `pattern`, `regex`, `glob`, `once`, `timeoutMs`) plus the 0.2.4 internal discriminators (`shell`, `prompt`, `http`, `agent`, `script`, `function`). `type` is moved out of the reserved set and into HOOK_COMMAND_FIELDS so the validator gives a more specific "type must be 'command'" error for bad values. timeout range is 1..600 seconds. `cwd` traversal tests are removed (cwd is no longer a hook field). - The 0.2.4 cwd / `args` / `pattern` / `regex` / `glob` / `once` / `timeoutMs` fields are now closed-schema violations on inner descriptors, surfaced as "reserved internal discriminator" so a Plugin migrating from 0.2.4 to 0.3.10 gets a clear error rather than a silent no-op. Test evidence - test/validation.test.mjs: existing tests updated to the 0.3.10 nested shape. New tests cover: outer / inner schema split, type "command" required, 0.2.4 fields rejected as reserved, both `{"hooks": {...}}` and `{...}` document bodies, $schema optional, 15-event catalog including the three 0.3.10 streaming events. 21/21 pass. - negative-injection self-audit (per the round-4 audit rule): three contracts were broken and the validator caught each: 1. 0.2.4 flat shape -> REJECTED with "command is not a recognized Hook field; expected one of hooks, matcher" 2. `args` added to inner descriptor -> REJECTED with "args is a reserved internal discriminator and is not allowed in a portable Hook entry" 3. `timeout: 5000` (out of 1..600 seconds) -> REJECTED with "timeout must be an integer between 1 and 600 seconds (the 0.3.10 parser multiplies by 1000)" After each injection the example was restored and the validator passed (12 events, no false green). Design compliance - spec: companion-only document; the portable proposal in proposals/hooks.md (commit d86625d) is unchanged. Every normative rule in this PR is marked Portable / Mcode-specific / Companion-only observability so the eventual merge with the portable proposal has a clear scope boundary. - example: examples/hello-mcode-hooks targets every event in the 0.3.10 catalog with one record.mjs invocation each. Five of the twelve events (PreToolUse, PostToolUse, SessionStart, SessionEnd, UserPromptSubmit) are in the Fwe allowlist and would auto-dispatch; the remaining seven load but never fire and are recorded for forward compatibility. - validator: the closed-schema rejection messages name the field and the spec section that constrains it, so a Plugin author can map the error to a fix without re-reading the spec. Companion evidence - 14/14 manual invocations on Windows 11 24H2 + @minimax-ai/code@0.3.10 (mcode-island v0.4.0, 2026-09-09); 5/12 events auto-dispatched by the runtime, 7/12 recorded for forward compatibility. Recorded in the spec "End-to-end smoke" section. Known gaps (not addressed here, recorded in the spec) - the seven `forward` events (Stop, PreCompact, Notification, SubagentStart, SubagentStop, PermissionRequest, PermissionDenied) load cleanly but never fire in 0.3.10. Lifting them into the Fwe allowlist is a runtime change, not a spec change. - the 0.3.10 streaming events (MessageComplete, StreamChunk, StreamChunkThreshold) are not in the portable proposal. Adoption or rejection is an upstream decision. - the `decision` field, the `ask` value, and the `hookSpecificOutput` shape are backed by cli.js literal inspection only; no CI test exercises them. - Plugin-supplied `extensions.io.minimax.mcode.hooks` paths in plugin.json are accepted by the Plugin registry but the 0.3.10 hook-config parser does not read them. The spec documents the dataDir path; a future runtime release is expected to wire the Plugin path through. Refs - supersedes part of PR #20 (the schema description in the previous companion is replaced by the 0.3.10 schema here) - runtime: @minimax-ai/code@0.3.10 (npm, 2026-09-08) - portable proposal: proposals/hooks.md (commit d86625d) - user-reported regression recorded on 2026-09-09
1 parent 9600667 commit 1f5baf6

9 files changed

Lines changed: 1885 additions & 4 deletions

File tree

‎examples/hello-mcode-hooks/LICENSE‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Apache License
2+
Version 2.0, January 2004
3+
http://www.apache.org/licenses/
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# hello-mcode-hooks
2+
3+
A minimal Plugin that ships one Skill and one experimental `io.minimax.mcode` Hook entry under
4+
the Agent Plugins 1.0 portable Hooks preview, conformant to the `@minimax-ai/code@0.3.10`
5+
runtime hook schema.
6+
7+
## What this example demonstrates
8+
9+
- A Skill-only Agent Plugin (the "hello-hooks" Skill).
10+
- A `hooks.json` document that targets every event in the 0.3.10 catalog
11+
(`PreToolUse`, `PostToolUse`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `Stop`,
12+
`PreCompact`, `Notification`, `SubagentStart`, `SubagentStop`, `PermissionRequest`,
13+
`PermissionDenied`) with one `record.mjs` invocation each. In 0.3.10 the runtime
14+
auto-dispatches only the five `Fwe`-allowlist events
15+
(`PreToolUse`, `PostToolUse`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`);
16+
the remaining seven load cleanly but never fire and are recorded for forward
17+
compatibility. See `proposals/hooks-detailed-spec.md` § "Empirical event catalog"
18+
for the per-event Fwe status.
19+
- Atomic, cross-platform state file writes under the runtime-provided `PLUGIN_DATA` directory.
20+
- Path resolution that uses runtime-injected environment values, not host-absolute literals.
21+
22+
This example is not a working integration; it is a structural reference for portable
23+
Plugin authors writing Hooks against `@minimax-ai/code@0.3.10`. The companion proposal
24+
in `proposals/hooks-detailed-spec.md` is still in review; registry validation does not
25+
execute Hook code.
26+
27+
## Layout
28+
29+
```text
30+
hello-mcode-hooks/
31+
├── README.md
32+
├── LICENSE
33+
├── plugin.json
34+
├── skills/
35+
│ └── hello-hooks/
36+
│ └── SKILL.md
37+
└── io.minimax.mcode/
38+
└── hooks/
39+
├── hooks.json
40+
└── scripts/
41+
└── record.mjs
42+
```
43+
44+
## Hook entry
45+
46+
The `hooks.json` document is the 0.3.10 nested shape: each event value is an array of
47+
matcher entries, and each matcher entry wraps a `hooks[]` array of command descriptors.
48+
The descriptor's `command` is a single string passed to the platform shell; `matcher`
49+
lives on the outer (matcher) entry, not the inner (command) descriptor; `timeout` is in
50+
seconds (the runtime multiplies by 1000 internally).
51+
52+
```json
53+
{
54+
"$schema": "https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json",
55+
"hooks": {
56+
"PreToolUse": [
57+
{
58+
"matcher": "*",
59+
"hooks": [
60+
{
61+
"type": "command",
62+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event PreToolUse --state ${PLUGIN_DATA}/state.json",
63+
"timeout": 5
64+
}
65+
]
66+
}
67+
]
68+
}
69+
}
70+
```
71+
72+
The script reads the event payload from stdin (one UTF-8 JSON document, then EOF, as
73+
proposed in `proposals/hooks.md` § "Observe-only runtime semantics") and appends a
74+
compact record to `${PLUGIN_DATA}/state.json` using a staging-file rename. No tool input
75+
rewriting, no permission decisions, no network access, no telemetry.
76+
77+
## Validation expectations
78+
79+
- `plugin.json` continues to target the published Agent Plugins 1.0 schema and remains
80+
valid under `scripts/validate.mjs`.
81+
- `io.minimax.mcode/hooks/hooks.json` is recognized as an experimental client extension
82+
namespace. The validator accepts it but does not require it.
83+
- The validator reports the full event catalog (12 portable + 3 streaming) as the
84+
closed-schema allowlist for the document root. Events outside the catalog are
85+
rejected; the 0.2.4 fields `args` / `env` / `cwd` / `pattern` / `regex` / `glob` /
86+
`once` / `timeoutMs` are rejected as closed-schema violations on inner descriptors
87+
so a Plugin migrating from 0.2.4 to 0.3.10 gets a clear error rather than a silent
88+
no-op.
89+
- The script resolves all paths from `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` only.
90+
91+
## Runtime install caveat
92+
93+
The 0.3.10 runtime reads `hooks.json` from
94+
`${MINIMAX_DATA_DIR}/hooks/hooks.json` or
95+
`${MINIMAX_DATA_DIR}/agents/<agentName>/hooks/hooks.json`, not from a Plugin's own
96+
`io.minimax.mcode/hooks/hooks.json` directory. The Plugin registry accepts the
97+
`io.minimax.mcode` namespace in `plugin.json` but the 0.3.10 hook-config parser does
98+
not consult that field. A Plugin that wants its hooks to fire must install
99+
`hooks.json` into one of the two Runtime-resolved locations; the `mcode-island`
100+
v0.4.0 install step copies the bundled `hooks.json` there on marketplace install.
101+
102+
## Disclosure
103+
104+
This example contains:
105+
106+
- no credentials;
107+
- no network access;
108+
- no telemetry;
109+
- no third-party services.
110+
111+
The same disclosure is repeated in `skills/hello-hooks/SKILL.md` per the
112+
`hello-mcode-hooks` plugin convention.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
{
2+
"$schema": "https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json",
3+
"hooks": {
4+
"PreToolUse": [
5+
{
6+
"matcher": "*",
7+
"hooks": [
8+
{
9+
"type": "command",
10+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event PreToolUse --state ${PLUGIN_DATA}/state.json",
11+
"timeout": 5
12+
}
13+
]
14+
}
15+
],
16+
"PostToolUse": [
17+
{
18+
"matcher": "*",
19+
"hooks": [
20+
{
21+
"type": "command",
22+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event PostToolUse --state ${PLUGIN_DATA}/state.json",
23+
"timeout": 5
24+
}
25+
]
26+
}
27+
],
28+
"SessionStart": [
29+
{
30+
"matcher": "*",
31+
"hooks": [
32+
{
33+
"type": "command",
34+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event SessionStart --state ${PLUGIN_DATA}/state.json",
35+
"timeout": 5
36+
}
37+
]
38+
}
39+
],
40+
"SessionEnd": [
41+
{
42+
"matcher": "*",
43+
"hooks": [
44+
{
45+
"type": "command",
46+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event SessionEnd --state ${PLUGIN_DATA}/state.json",
47+
"timeout": 5
48+
}
49+
]
50+
}
51+
],
52+
"UserPromptSubmit": [
53+
{
54+
"matcher": "*",
55+
"hooks": [
56+
{
57+
"type": "command",
58+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event UserPromptSubmit --state ${PLUGIN_DATA}/state.json",
59+
"timeout": 5
60+
}
61+
]
62+
}
63+
],
64+
"Stop": [
65+
{
66+
"matcher": "*",
67+
"hooks": [
68+
{
69+
"type": "command",
70+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event Stop --state ${PLUGIN_DATA}/state.json",
71+
"timeout": 5
72+
}
73+
]
74+
}
75+
],
76+
"PreCompact": [
77+
{
78+
"matcher": "*",
79+
"hooks": [
80+
{
81+
"type": "command",
82+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event PreCompact --state ${PLUGIN_DATA}/state.json",
83+
"timeout": 5
84+
}
85+
]
86+
}
87+
],
88+
"Notification": [
89+
{
90+
"matcher": "*",
91+
"hooks": [
92+
{
93+
"type": "command",
94+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event Notification --state ${PLUGIN_DATA}/state.json",
95+
"timeout": 5
96+
}
97+
]
98+
}
99+
],
100+
"SubagentStart": [
101+
{
102+
"matcher": "*",
103+
"hooks": [
104+
{
105+
"type": "command",
106+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event SubagentStart --state ${PLUGIN_DATA}/state.json",
107+
"timeout": 5
108+
}
109+
]
110+
}
111+
],
112+
"SubagentStop": [
113+
{
114+
"matcher": "*",
115+
"hooks": [
116+
{
117+
"type": "command",
118+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event SubagentStop --state ${PLUGIN_DATA}/state.json",
119+
"timeout": 5
120+
}
121+
]
122+
}
123+
],
124+
"PermissionRequest": [
125+
{
126+
"matcher": "*",
127+
"hooks": [
128+
{
129+
"type": "command",
130+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event PermissionRequest --state ${PLUGIN_DATA}/state.json",
131+
"timeout": 5
132+
}
133+
]
134+
}
135+
],
136+
"PermissionDenied": [
137+
{
138+
"matcher": "*",
139+
"hooks": [
140+
{
141+
"type": "command",
142+
"command": "node ${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record.mjs --event PermissionDenied --state ${PLUGIN_DATA}/state.json",
143+
"timeout": 5
144+
}
145+
]
146+
}
147+
]
148+
}
149+
}

0 commit comments

Comments
 (0)