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

@@ -63,6 +63,31 @@ const {
collectSkillBasenamesOnDisk,
} = require('./helpers/install-shared.cjs');
/**
* collectSkillBasenamesOnDisk(configDir, runtime, scope) re-resolves the
* runtime's skills-kind layout via os.homedir(). runMinimalInstall() already
* sandboxes HOME/USERPROFILE to `root` for the spawned install subprocess,
* but that sandboxing does not persist into this (parent) process — without
* re-sandboxing here, Codex's skills-kind `home: ".agents"` override
* (ADR-1239 upgrade 3, #2088) would resolve against the developer's REAL
* $HOME/.agents/skills instead of the sandboxed install root. Sandbox
* HOME/USERPROFILE to `root` for the synchronous duration of the on-disk scan.
*/
function collectSkillBasenamesOnDiskSandboxed(configDir, runtime, scope, root) {
const savedHome = process.env.HOME;
const savedUserProfile = process.env.USERPROFILE;
process.env.HOME = root;
process.env.USERPROFILE = root;
try {
return collectSkillBasenamesOnDisk(configDir, runtime, scope);
} 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;
}
}
// ─── Section 9: install-profiles — MINIMAL_SKILL_ALLOWLIST ───────────────────
describe('install-profiles: MINIMAL_SKILL_ALLOWLIST', () => {
@@ -391,7 +416,7 @@ describe('install: on-disk skill files match manifest for --minimal', () => {
});
try {
assert.ok(manifest);
const onDisk = collectSkillBasenamesOnDisk(configDir);
const onDisk = collectSkillBasenamesOnDiskSandboxed(configDir, runtime, scope, root);
const inManifest = manifestSkillSet(manifest);
assert.deepStrictEqual([...onDisk].sort(), [...inManifest].sort());
// Not the shared listAgentFiles() helper: asserts on the INSTALLED
@@ -542,10 +567,15 @@ describe('install: Codex full → minimal downgrade cleans stale agent state', (
].join('\n');
fs.writeFileSync(path.join(targetDir, 'config.toml'), codexConfig);
// Sandbox HOME/USERPROFILE to targetDir: Codex's skills-kind `home: ".agents"`
// override (ADR-1239 upgrade 3, #2088) resolves via os.homedir(), so an
// unsandboxed spawn here would write gsd-* skill dirs into the developer's
// real $HOME/.agents/skills. This test only asserts on agents/ and
// config.toml (both under targetDir), so the sandbox has no effect on intent.
const result = spawnSync(
process.execPath,
[INSTALL_SCRIPT, '--codex', '--global', '--config-dir', targetDir, '--minimal'],
{ encoding: 'utf8', env: installerEnv() },
{ encoding: 'utf8', env: installerEnv({ HOME: targetDir, USERPROFILE: targetDir }) },
);
assert.ok(result.stdout || result.stderr);