feat(#2088): migrate Codex onto the Embeddable Orchestration System (ADR-1239)

Drive Codex install/uninstall through the descriptor-driven Host-Integration
Interface (declarative embedding adapter → engine surface dispatch) and fold
every positive `runtime === 'codex'` / `isCodex` projection into descriptor-driven
`runtime.hostBehaviors`. Install/uninstall output stays byte-parity-gated
(tests/fixtures/golden-install-parity/codex.json); no other runtime changes.

Three Context7-verified upgrades, each with a test on the user-reachable surface:
- Skill root → canonical $HOME/.agents/skills via a skills-kind `home` override,
  with pre-move migration cleanup (stale ~/.codex/skills/gsd-* removed on install
  and uninstall; user content preserved). Fixes getGlobalSkillsBase, writeManifest,
  and the skill-manifest inventory to honor the override so --skills-root /
  sync-skills / the manifest report the real location.
- Six new hooks.json lifecycle events (PreToolUse, PermissionRequest, PreCompact,
  PostCompact, SubagentStop, UserPromptSubmit) shared by install + uninstall;
  extendedHookEvents reconciled [] -> the schema-valid wired subset.
- Explicit `[agents] max_depth = 1` in the managed config.toml block, pinning the
  negotiated dispatch.maxDepth:1 axis. validateCodexConfigSchema now permits a
  known-scalar-only bare `[agents]` AgentsToml table (still rejects [[agents]] and
  unknown-key break-forms, #2760); mergeCodexConfig preserves the user's own
  AgentsToml scalars (max_threads etc.) instead of dropping them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-08 21:42:11 -04:00
parent e5a1d7f5c4
commit 6e773d97df
24 changed files with 1252 additions and 211 deletions

View File

@@ -18,6 +18,10 @@ const {
installRuntimeArtifacts,
} = require('../gsd-core/bin/lib/install-engine.cjs');
const {
resolveRuntimeArtifactLayout,
} = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const { cleanup } = require('./helpers.cjs');
const {
@@ -58,16 +62,55 @@ const FLAT = [
// Helpers
// ---------------------------------------------------------------------------
/**
* Codex resolves its skills-kind destination via os.homedir() + a 'skills'-kind
* 'home: ".agents"' layout override (ADR-1239 EoS upgrade 3, #2088), NOT
* tmpDir/skills. Sandbox HOME/USERPROFILE to tmpDir for the duration of the
* synchronous callback so codex's resolved skills dir becomes
* tmpDir/.agents/skills instead of the developer's real ~/.agents/skills.
*/
function withSandboxedHome(tmpDir, fn) {
const savedHome = process.env.HOME;
const savedUserProfile = process.env.USERPROFILE;
process.env.HOME = tmpDir;
process.env.USERPROFILE = tmpDir;
try {
return fn();
} finally {
if (savedHome === undefined) delete process.env.HOME;
else process.env.HOME = savedHome;
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = savedUserProfile;
}
}
/**
* Create a fresh temp dir, run installRuntimeArtifacts into it, and return
* the tmpDir path. Caller must cleanup in finally.
* the tmpDir path. Caller must cleanup in finally. HOME is sandboxed to
* tmpDir for the install call so codex never writes to the real ~/.agents/skills.
*/
function runInstall(runtime, scope, resolved) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-nest-test-${runtime}-`));
installRuntimeArtifacts(runtime, tmpDir, scope, resolved);
withSandboxedHome(tmpDir, () => installRuntimeArtifacts(runtime, tmpDir, scope, resolved));
return tmpDir;
}
/**
* Resolve the skills-kind destination directory for a runtime, honoring the
* skills-kind 'home' override (codex only — resolves under tmpDir/.agents
* once HOME is sandboxed). All other runtimes have no 'home' override, so
* this falls back to tmpDir/<destSubpath>, matching the static skillsSub
* tables above.
*/
function resolveSkillsDir(runtime, tmpDir, scope) {
return withSandboxedHome(tmpDir, () => {
const layout = resolveRuntimeArtifactLayout(runtime, tmpDir, scope);
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
assert.ok(skillsKind, `${runtime} must have skills kind`);
return path.join(skillsKind.home || tmpDir, skillsKind.destSubpath);
});
}
// Resolve the full profile once (shared by all installs)
const MANIFEST = loadSkillsManifest(COMMANDS_GSD);
const RESOLVED_FULL = resolveProfile({ modes: ['full'], manifest: MANIFEST });
@@ -267,7 +310,7 @@ describe('claude: total top-level gsd- entries >= 60 (flat layout, #924)', () =>
// FLAT runtimes: concrete skills stay top-level, no nesting
// ---------------------------------------------------------------------------
for (const { runtime, scope, skillsSub } of FLAT) {
for (const { runtime, scope } of FLAT) {
describe(`${runtime} (flat layout)`, () => {
let tmpDir;
@@ -282,7 +325,7 @@ for (const { runtime, scope, skillsSub } of FLAT) {
});
test(`${runtime}: stays flat — concrete skills remain top-level, no nesting`, () => {
const skillsDir = path.join(tmpDir, skillsSub);
const skillsDir = resolveSkillsDir(runtime, tmpDir, scope);
assert.ok(fs.existsSync(skillsDir), `skillsDir must exist: ${skillsDir}`);
const topLevel = fs.readdirSync(skillsDir);