Skip to content

Commit a234f01

Browse files
authored
Merge pull request #3 from vitry/fix/plugin-data-root
fix: bootstrap stable plugin data root
2 parents 5d6a72b + 2ec26f0 commit a234f01

18 files changed

Lines changed: 498 additions & 30 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes follow Semantic Versioning.
44

5+
## Unreleased
6+
7+
- Fixed installed `$zcode:*` skills failing with `DATA_ROOT_REQUIRED` when Codex does not inject `PLUGIN_DATA` into ordinary skill commands.
8+
- Added marketplace-qualified plugin-data discovery and a restart-safe `$zcode:setup` bootstrap that configures the data directory as a writable root before persisting state.
9+
510
## 0.1.0 - 2026-08-06
611

712
- Added eight native `$zcode:*` skills for review, adversarial review, Rescue, Transfer, job inspection, cancellation, and setup.

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ codex plugin marketplace add vitry/zcode-plugin-codex --ref marketplace
1717
codex plugin add zcode@vitry
1818
```
1919

20-
The release workflow builds `.agents/plugins/marketplace.json` plus `plugins/zcode/` with production dependencies on that branch. Restart Codex after installation, then run `$zcode:setup` in the target workspace. Do not copy hooks out of the installed plugin cache.
20+
The release workflow builds `.agents/plugins/marketplace.json` plus `plugins/zcode/` with production dependencies on that branch. Restart Codex after installation, then run `$zcode:setup` in the target workspace. On the first run, setup may add the marketplace-qualified plugin data directory to Codex's writable roots; if it reports `restart-required`, restart Codex and rerun setup. Do not copy hooks out of the installed plugin cache.
2121

2222
Discovery checks `ZCODE_PATH`, `zcode` on `PATH`, platform locations, and on macOS the bundled `/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs`. Setup reports missing, outdated, unauthenticated, or untrusted installations; it does not download ZCode or sign in for you.
2323

@@ -47,7 +47,7 @@ codex
4747
# In the Codex session: $zcode:setup
4848
```
4949

50-
Setup persists `${PLUGIN_DATA}/workspaces/<workspace-hash>/config/models.json` with this schema:
50+
Setup persists `$CODEX_HOME/plugins/data/zcode-<marketplace>/workspaces/<workspace-hash>/config/models.json` (the hook-provided `PLUGIN_DATA` resolves to the same root) with this schema:
5151

5252
```json
5353
{"version":1,"defaultModel":"fast","models":{"fast":{"providerId":"provider","modelId":"model","variant":"optional"}}}
@@ -59,7 +59,7 @@ To verify configuration, rerun `$zcode:setup`, then run `$zcode:rescue --fresh -
5959

6060
## Jobs, Transfer, and review gate
6161

62-
Every run is reserved as a durable, owner-scoped job. Plugin state lives beneath `${PLUGIN_DATA}/workspaces/<workspace-hash>/` with private permissions; prompts, results, session IDs, and logs are never written into the repository. `$zcode:status`, `$zcode:result`, and `$zcode:cancel` work across later turns in the same Codex session, while sibling sessions cannot adopt a job.
62+
Every run is reserved as a durable, owner-scoped job. Installed plugin state lives beneath `$CODEX_HOME/plugins/data/zcode-<marketplace>/workspaces/<workspace-hash>/` with private permissions; prompts, results, session IDs, and logs are never written into the repository or plugin cache. `$zcode:status`, `$zcode:result`, and `$zcode:cancel` work across later turns in the same Codex session, while sibling sessions cannot adopt a job.
6363

6464
Transfer reads a persisted Codex thread through `codex app-server` and imports only ordered visible user/assistant text. It does not transfer hidden reasoning, tools, permissions, or ZCode job ownership.
6565

@@ -72,6 +72,7 @@ The optional Stop review gate runs a bounded foreground read-only review only af
7272
- Authentication unavailable: authenticate with ZCode itself, then rerun setup.
7373
- Background work: use `$zcode:status <job-id> --wait`, `$zcode:result <job-id>`, or `$zcode:cancel <job-id>` exactly as reported.
7474
- Hook trust or restart required: let setup trust only this installed plugin's exact hook hashes, restart Codex, and rerun setup.
75+
- `plugin-data-root-added`: setup added the stable plugin data root to Codex configuration without writing plugin state; restart Codex and rerun setup.
7576

7677
macOS with ZCode Desktop 3.6.5 and CLI 0.16.1+ is the release qualification target. Run `ZCODE_REAL_E2E=1 ZCODE_REAL_E2E_MODEL='provider/model' npm run test:qualified` on an authenticated machine before marking a release qualified. To include the installed-marketplace-to-real-Codex bridge, also set `ZCODE_CODEX_SKILLS_E2E=1`; this consumes authenticated Codex credits. Missing opt-in, authentication, model, or credits produces a structured `unqualified` skip, never a pass. An unknown execution failure remains a test failure. Linux and Windows are code-supported by fake-protocol CI, but are not real-CLI qualified yet.
7778

README.zh-CN.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ codex plugin marketplace add vitry/zcode-plugin-codex --ref marketplace
1717
codex plugin add zcode@vitry
1818
```
1919

20-
发布 workflow 会在该分支生成 `.agents/plugins/marketplace.json` 和带生产依赖的 `plugins/zcode/`。安装后重启 Codex,再在目标工作区运行 `$zcode:setup`。不要把 hooks 从插件缓存复制到别处。
20+
发布 workflow 会在该分支生成 `.agents/plugins/marketplace.json` 和带生产依赖的 `plugins/zcode/`。安装后重启 Codex,再在目标工作区运行 `$zcode:setup`首次运行可能把 marketplace 专属的数据目录加入 Codex writable roots;若返回 `restart-required`,重启 Codex 后再次运行 setup。不要把 hooks 从插件缓存复制到别处。
2121

2222
插件依次检查 `ZCODE_PATH``PATH` 中的 `zcode`、平台目录,以及 macOS 内置路径 `/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs`。Setup 会报告缺失、版本过低、未认证或 hook 不可信,但不会下载 ZCode,也不会代替用户登录。
2323

@@ -47,7 +47,7 @@ codex
4747
# 进入 Codex session 后执行:$zcode:setup
4848
```
4949

50-
Setup 会把以下 schema 写入 `${PLUGIN_DATA}/workspaces/<workspace-hash>/config/models.json`
50+
Setup 会把以下 schema 写入 `$CODEX_HOME/plugins/data/zcode-<marketplace>/workspaces/<workspace-hash>/config/models.json`(hook 注入的 `PLUGIN_DATA` 会解析到同一目录)
5151

5252
```json
5353
{"version":1,"defaultModel":"fast","models":{"fast":{"providerId":"provider","modelId":"model","variant":"optional"}}}
@@ -59,7 +59,7 @@ Setup 会把以下 schema 写入 `${PLUGIN_DATA}/workspaces/<workspace-hash>/con
5959

6060
## 任务、Transfer 与 review gate
6161

62-
每次运行都会先建立持久、带 owner 的 job。状态保存在 `${PLUGIN_DATA}/workspaces/<workspace-hash>/`,使用私有权限;prompt、result、session ID 和日志都不会写进仓库。后续 turn 仍可使用 `$zcode:status``$zcode:result``$zcode:cancel`,但 sibling Codex session 无法接管任务。
62+
每次运行都会先建立持久、带 owner 的 job。已安装插件的状态保存在 `$CODEX_HOME/plugins/data/zcode-<marketplace>/workspaces/<workspace-hash>/`,使用私有权限;prompt、result、session ID 和日志都不会写进仓库或插件缓存。后续 turn 仍可使用 `$zcode:status``$zcode:result``$zcode:cancel`,但 sibling Codex session 无法接管任务。
6363

6464
Transfer 通过 `codex app-server` 读取持久 Codex thread,只导入按顺序排列、用户可见的 user/assistant 文本;不转移隐藏推理、工具状态、permission 或 job ownership。
6565

@@ -72,6 +72,7 @@ Transfer 通过 `codex app-server` 读取持久 Codex thread,只导入按顺
7272
- Authentication 不可用:在 ZCode 自身完成认证,再重新 setup。
7373
- 后台任务:按输出使用 `$zcode:status <job-id> --wait``$zcode:result <job-id>``$zcode:cancel <job-id>`
7474
- Hook trust / restart required:只让 setup 信任当前安装插件的精确 hook hash,重启后再次检查。
75+
- `plugin-data-root-added`:setup 只把稳定数据目录加入 Codex 配置,尚未写入插件状态;重启 Codex 后再次运行 setup。
7576

7677
macOS + ZCode Desktop 3.6.5 + CLI 0.16.1+ 是发布资格目标。发布前应在已认证机器运行 `ZCODE_REAL_E2E=1 ZCODE_REAL_E2E_MODEL='provider/model' npm run test:qualified`。如需同时验证“已安装 marketplace → 真实 Codex → ZCode”桥接,还要设置 `ZCODE_CODEX_SKILLS_E2E=1`;该测试会消耗已认证 Codex 账户的额度。缺少 opt-in、认证、模型或额度时,测试只会给出结构化的 `unqualified` skip,绝不会算作通过;未知执行错误仍会让测试失败。Linux and Windows are code-supported but are not real-CLI qualified yet;两者当前由 fake-protocol CI 覆盖。
7778

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Plugin Data Root Bootstrap Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Make installed `$zcode:*` skills resolve and bootstrap a stable writable plugin data directory when `PLUGIN_DATA` is absent.
6+
7+
**Architecture:** Introduce a focused plugin-data path resolver, use it at every companion and hook entry point, and extend setup's existing Codex app-server configuration transaction to preserve and add the required writable root before any state write. Installed cache identity determines the marketplace-qualified namespace; source checkouts use an unqualified development namespace.
8+
9+
**Tech Stack:** Node.js 22.13+ ESM, Codex app-server JSONL configuration API, Node built-in test runner, JSDoc/TypeScript checking, ESLint.
10+
11+
---
12+
13+
### Task 1: Resolve and bootstrap plugin data safely
14+
15+
**Files:**
16+
- Create: `scripts/lib/plugin-data.mjs`
17+
- Modify: `scripts/zcode-companion.mjs`
18+
- Modify: `scripts/lib/codex-config.mjs`
19+
- Modify: `hooks/session-lifecycle-hook.mjs`
20+
- Modify: `hooks/user-prompt-hook.mjs`
21+
- Modify: `hooks/subagent-hook.mjs`
22+
- Modify: `hooks/stop-review-gate-hook.mjs`
23+
- Modify: `hooks/session-end-hook.mjs`
24+
- Modify: `tests/setup.test.mjs`
25+
- Modify: `tests/integration/marketplace-install.test.mjs`
26+
- Create or modify: `tests/plugin-data.test.mjs`
27+
- Modify: `README.md`
28+
- Modify: `README.zh-CN.md`
29+
- Modify: `CHANGELOG.md`
30+
31+
- [ ] **Step 1: Write failing resolver tests**
32+
33+
Cover explicit `ZCODE_DATA_ROOT`, validated injected roots, installed cache identity,
34+
marketplace-qualified fallback, source-checkout fallback, `CODEX_HOME`, symlink-equivalent
35+
paths, and rejection/ignoring of foreign injected roots.
36+
37+
- [ ] **Step 2: Run the resolver tests and verify RED**
38+
39+
Run: `node --test tests/plugin-data.test.mjs`
40+
41+
Expected: FAIL because the shared resolver does not exist.
42+
43+
- [ ] **Step 3: Implement the minimal shared resolver**
44+
45+
Create `scripts/lib/plugin-data.mjs` with pure path/identity functions and a single
46+
`resolvePluginDataRoot({ env, pluginRoot })` public entry point. Do not create files in
47+
the resolver.
48+
49+
- [ ] **Step 4: Run resolver tests and verify GREEN**
50+
51+
Run: `node --test tests/plugin-data.test.mjs`
52+
53+
Expected: PASS.
54+
55+
- [ ] **Step 5: Write failing setup bootstrap tests**
56+
57+
Exercise the real companion setup entry without `PLUGIN_DATA`. Assert that it derives
58+
the installed root, preserves existing `sandbox_workspace_write.writable_roots`, emits
59+
one version-checked `config/batchWrite`, returns `restart-required`, performs no plugin
60+
state write before restart, and honors an already-effective writable root on rerun.
61+
62+
- [ ] **Step 6: Run setup tests and verify RED**
63+
64+
Run: `node --test tests/setup.test.mjs tests/integration/marketplace-install.test.mjs`
65+
66+
Expected: FAIL with the current `DATA_ROOT_REQUIRED` or missing writable-root edit.
67+
68+
- [ ] **Step 7: Integrate resolver and setup bootstrap**
69+
70+
Resolve the data root before companion routing. Reorder setup so Codex configuration is
71+
read and the writable root is installed before model or gate state is accessed. Preserve
72+
existing roots and user-layer version checks. Return restart guidance without writing
73+
state when the sandbox root has just changed or is overridden.
74+
75+
- [ ] **Step 8: Use the resolver in hooks**
76+
77+
Replace direct `process.env.PLUGIN_DATA` assumptions with the shared resolver so hooks
78+
and skills select the same root while retaining fail-closed hook behavior.
79+
80+
- [ ] **Step 9: Run focused tests and verify GREEN**
81+
82+
Run: `node --test tests/plugin-data.test.mjs tests/setup.test.mjs tests/hooks.test.mjs tests/integration/marketplace-install.test.mjs`
83+
84+
Expected: PASS.
85+
86+
- [ ] **Step 10: Update user-facing documentation**
87+
88+
Document the derived plugin-data location, marketplace qualification, restart-required
89+
setup behavior, and the fact that state never lives in the repository or plugin cache.
90+
91+
- [ ] **Step 11: Run the complete quality gate**
92+
93+
Run: `npm run check`
94+
95+
Expected: PASS with only the documented credential-gated E2E skips.
96+
97+
- [ ] **Step 12: Self-review and commit**
98+
99+
Run `git diff --check`, inspect the full diff for unrelated changes and secret/path
100+
leaks, then commit with a focused bug-fix message.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Plugin Data Root Bootstrap Design
2+
3+
## Problem
4+
5+
Installed `$zcode:*` skills run ordinary companion commands without a guaranteed
6+
`PLUGIN_DATA` environment variable. The companion currently rejects every command,
7+
including `$zcode:setup`, before setup can configure the environment. Codex documents
8+
`PLUGIN_DATA` for plugin hook commands, not as a guaranteed environment variable for
9+
ordinary commands launched from a skill.
10+
11+
## Design
12+
13+
Add one plugin-data resolver shared by skills, setup, and hooks. An explicit
14+
`ZCODE_DATA_ROOT` remains the test and operator override. Otherwise the resolver uses
15+
an injected `PLUGIN_DATA` or `CLAUDE_PLUGIN_DATA` only when it matches the active
16+
installed plugin identity. When no injected value is available, it derives a stable
17+
marketplace-qualified root from the canonical plugin cache path and `CODEX_HOME`:
18+
19+
```text
20+
${CODEX_HOME:-~/.codex}/plugins/data/zcode-<marketplace>
21+
```
22+
23+
A source checkout uses the unqualified development root:
24+
25+
```text
26+
${CODEX_HOME:-~/.codex}/plugins/data/zcode
27+
```
28+
29+
Workspace state remains isolated below `workspaces/<sha256-canonical-workspace>`.
30+
No state is written into the repository or plugin cache.
31+
32+
## Setup bootstrap
33+
34+
`$zcode:setup` must be able to run before the data root is writable. It reads Codex
35+
configuration first, preserves existing writable roots, and adds the resolved plugin
36+
data root through `config/batchWrite`. When the effective sandbox configuration changes,
37+
setup reports `restart-required` and defers state writes and review-gate changes until
38+
the user restarts Codex and reruns setup.
39+
40+
When the root is already writable, setup continues with the existing ZCode discovery,
41+
authentication, hook validation/trust, model policy, and review-gate flow.
42+
43+
## Safety
44+
45+
- Canonicalize and validate cache-relative marketplace/plugin/version segments.
46+
- Reject control characters, traversal, and unexpected plugin identities.
47+
- Never accept an arbitrary injected `PLUGIN_DATA` path for an installed plugin.
48+
- Preserve existing writable-root configuration and detect higher-precedence overrides.
49+
- Keep private directory/file permissions and existing workspace hashing unchanged.
50+
- Hooks and skills must resolve the same data root.
51+
52+
## Verification
53+
54+
Add regression coverage proving an installed-style setup invocation without
55+
`PLUGIN_DATA` no longer returns `DATA_ROOT_REQUIRED`, derives the expected
56+
marketplace-qualified path, requests only the required writable-root edit, requires a
57+
restart before writing state, and succeeds after restart. Keep explicit data-root tests,
58+
hook tests, marketplace installation tests, and the full suite green.

hooks/session-end-hook.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#!/usr/bin/env node
22
// @ts-nocheck
33
import process from 'node:process';
4+
import { fileURLToPath } from 'node:url';
5+
import { resolve } from 'node:path';
46
import { createIdentityStore } from '../scripts/lib/identity.mjs';
57
import { ownerIdForSession } from '../scripts/lib/job-control.mjs';
8+
import { resolvePluginDataRoot } from '../scripts/lib/plugin-data.mjs';
69
import { releaseManagedZCodeOwner } from '../scripts/lib/zcode-client.mjs';
710
import { cleanupSession } from './lib/hook-state.mjs';
811
import { readHookInput } from './lib/hook-input.mjs';
912

10-
try { const input = await readHookInput('SessionEnd'); const dataRoot = process.env.PLUGIN_DATA; if (!dataRoot) throw new Error('PLUGIN_DATA required'); await Promise.allSettled([releaseManagedZCodeOwner({ dataRoot, workspace: input.cwd, ownerId: ownerIdForSession(input.session_id), requestTimeoutMs: 750 }), cleanupSession(dataRoot, input.cwd, input.session_id), createIdentityStore({ dataRoot }).cleanupSession(input.cwd, input.session_id)]); }
13+
try { const input = await readHookInput('SessionEnd'); const dataRoot = resolvePluginDataRoot({ env: process.env, pluginRoot: resolve(fileURLToPath(new URL('../', import.meta.url))) }); await Promise.allSettled([releaseManagedZCodeOwner({ dataRoot, workspace: input.cwd, ownerId: ownerIdForSession(input.session_id), requestTimeoutMs: 750 }), cleanupSession(dataRoot, input.cwd, input.session_id), createIdentityStore({ dataRoot }).cleanupSession(input.cwd, input.session_id)]); }
1114
catch (error) { process.stderr.write(`ZCode session cleanup advisory failed: ${error?.code ?? 'HOOK_FAILED'}\n`); process.exitCode = 1; }

hooks/session-lifecycle-hook.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env node
22
// @ts-nocheck
33
import process from 'node:process';
4+
import { fileURLToPath } from 'node:url';
5+
import { resolve } from 'node:path';
6+
import { resolvePluginDataRoot } from '../scripts/lib/plugin-data.mjs';
47
import { readHookInput } from './lib/hook-input.mjs';
58
import { recordSession } from './lib/hook-state.mjs';
69

7-
try { const input = await readHookInput('SessionStart'); if (!process.env.PLUGIN_DATA) throw new Error('PLUGIN_DATA required'); await recordSession(process.env.PLUGIN_DATA, input); process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: 'ZCode companion lifecycle is active for this parent session.' } })); }
10+
try { const input = await readHookInput('SessionStart'); const dataRoot = resolvePluginDataRoot({ env: process.env, pluginRoot: resolve(fileURLToPath(new URL('../', import.meta.url))) }); await recordSession(dataRoot, input); process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: 'ZCode companion lifecycle is active for this parent session.' } })); }
811
catch (error) { process.stderr.write(`ZCode lifecycle hook failed safely: ${error?.code ?? 'HOOK_FAILED'}\n`); process.exitCode = 1; }

hooks/stop-review-gate-hook.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ownerIdForSession } from '../scripts/lib/job-control.mjs';
1212
import { readJsonFile } from '../scripts/lib/fs.mjs';
1313
import { createStateStore } from '../scripts/lib/state.mjs';
1414
import { createIdentityStore } from '../scripts/lib/identity.mjs';
15+
import { resolvePluginDataRoot } from '../scripts/lib/plugin-data.mjs';
1516
import { resolveWorkspaceStorage } from '../scripts/lib/workspace.mjs';
1617
import { fingerprintWorkspace, finishGateRun, isForwarding, isOwnedSession, writeGateRun } from './lib/hook-state.mjs';
1718
import { readHookInput } from './lib/hook-input.mjs';
@@ -34,7 +35,7 @@ export async function runStopReviewGate(input, options) {
3435
}
3536

3637
async function main() {
37-
try { const input = await readHookInput('Stop'); const dataRoot = process.env.PLUGIN_DATA; if (!dataRoot) throw new Error('PLUGIN_DATA required'); output(await runStopReviewGate(input, { dataRoot, env: process.env })); }
38+
try { const input = await readHookInput('Stop'); const dataRoot = resolvePluginDataRoot({ env: process.env, pluginRoot: root }); output(await runStopReviewGate(input, { dataRoot, env: process.env })); }
3839
catch (error) { output({ decision: 'block', reason: cap(`ZCode review gate failed safely (${error?.code ?? 'HOOK_FAILED'}). Run $zcode:setup, then retry the final check.`) }); }
3940
}
4041

0 commit comments

Comments
 (0)