fix(config): harden doctor checks and enable Codex full access
CI / CI configuration security checks (push) Successful in 3s
CI / Capabilities and shell quality (push) Successful in 18s
CI / Python tests and compilation (push) Successful in 25s
CI / QMD unit tests (push) Successful in 11s
CI / Build and inspect generated packages (push) Successful in 17s
CI / PowerShell tests on Ubuntu (push) Successful in 1m11s
CI / Installed client configuration smoke tests (push) Successful in 1m10s
CI / Bash integration and installation guards (push) Failing after 1m13s
CI / CI configuration security checks (push) Successful in 3s
CI / Capabilities and shell quality (push) Successful in 18s
CI / Python tests and compilation (push) Successful in 25s
CI / QMD unit tests (push) Successful in 11s
CI / Build and inspect generated packages (push) Successful in 17s
CI / PowerShell tests on Ubuntu (push) Successful in 1m11s
CI / Installed client configuration smoke tests (push) Successful in 1m10s
CI / Bash integration and installation guards (push) Failing after 1m13s
This commit is contained in:
1 parent
8aae370bdb
commit
5e80d30936
11 files changed
+113
-24
No files matched your search
@@ -301,7 +301,9 @@ keep outside this setup's ledger, or files this setup never installed.
|
||||
|
||||
Codex and Claude Code receive the same shared rules, skills, agents, hooks, and status data. Codex loads applicable rules from generated files, while Claude Code exposes technology- and situation-specific rules as generated skills.
|
||||
|
||||
Codex defaults to workspace-write sandboxing and automatic review for eligible escalation requests. Claude uses
|
||||
Codex defaults to full access under the account that launches it, with unrestricted command network access.
|
||||
Launch Codex as the repository owner; Codex does not switch to that owner automatically. Full access removes
|
||||
Codex's filesystem sandbox boundaries. Eligible approval requests still use automatic review. Claude uses
|
||||
automatic permissions and enables its sandbox where the platform supports it; explicit deny rules still apply where it
|
||||
does not. Both clients include the same logical agent roles and compact status displays. See
|
||||
[Configuration](docs/configuration.md) for settings, permissions, and limitations.
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
approval_policy = "on-request"
|
||||
approvals_reviewer = "auto_review"
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
writable_roots = ['__AI_CONFIG_ROOT__/../.my-ai-configuration/qmd', '__AI_CONFIG_ROOT__/../.cache/qmd']
|
||||
|
||||
[windows]
|
||||
sandbox = "elevated"
|
||||
sandbox_mode = "danger-full-access"
|
||||
|
||||
[tui]
|
||||
status_line = ["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]
|
||||
|
||||
+27
-9
@@ -6,7 +6,7 @@ Installation targets `~/.codex` and `~/.claude`; Codex skills go to `~/.agents/s
|
||||
Choose the PowerShell or Bash package independently of the client.
|
||||
|
||||
Codex receives a generated global `AGENTS.md`, agent TOML files, skills, rule files, and a `config.toml` adapter.
|
||||
It sets workspace sandboxing with shell network access, automatic approval review, `agents.enabled = true`,
|
||||
It sets full access for commands, automatic approval review, `agents.enabled = true`,
|
||||
`agents.max_concurrent_threads_per_session = 5`, and `agents.max_depth = 1`.
|
||||
|
||||
Claude Code receives a generated global `CLAUDE.md`, agent Markdown files, skills, and `settings.json` with
|
||||
@@ -25,9 +25,26 @@ Client settings remain thin adapters. Shared semantics remain in `shared/`. User
|
||||
|
||||
## Permission defaults
|
||||
|
||||
Codex sets `approval_policy = "on-request"`, `approvals_reviewer = "auto_review"`, and `sandbox_mode = "workspace-write"`. Eligible approval requests go through automatic review. Claude sets `permissions.defaultMode = "auto"` and shell-specific deny rules. Its sandbox is enabled on supported macOS, Linux, and WSL2 environments and disabled where unavailable, including native Windows. These are generated defaults, not a guarantee that a running session, account policy, or explicit launch override uses the same mode.
|
||||
Codex sets `sandbox_mode = "danger-full-access"`, `approval_policy = "on-request"`, and
|
||||
`approvals_reviewer = "auto_review"`. All Codex commands run without Codex filesystem or network sandbox
|
||||
restrictions, under the account that launches Codex. Launch Codex as the repository owner to use that identity;
|
||||
this configuration does not impersonate another user or change filesystem ownership. OS permissions, firewalls,
|
||||
and managed organization policies still apply. Eligible approval requests retain automatic review, which does
|
||||
not restore a filesystem boundary. See the [official OpenAI security documentation](https://learn.chatgpt.com/docs/agent-approvals-security).
|
||||
|
||||
On native Windows, the Codex adapter selects `windows.sandbox = "elevated"`, the [recommended Windows sandbox](https://learn.chatgpt.com/docs/windows/windows-sandbox). Its setup may require administrator approval; sandboxed commands run as dedicated lower-privilege users. The previous `unelevated` default reproduced `spawnSync git EPERM` when QMD's Node wrapper captured child-process output through pipes. The configuration change preserves workspace boundaries, network settings, and automatic approval review. Apply the generated configuration through the normal update process, complete Codex's sandbox setup if prompted, and start a new session. Verify semantic search there; package validation cannot establish runtime pipe compatibility. Use `unelevated` only as an explicit local fallback when elevated setup is unavailable, with that compatibility limitation in mind.
|
||||
The adapter no longer selects the Windows sandbox or workspace writable roots. This avoids using the dedicated
|
||||
`CodexSandboxOnline` account, whose identity differs from the repository owner and can trigger Git's ownership
|
||||
check. Old `[windows]` and `[sandbox_workspace_write]` tables may survive an update as preserved local settings;
|
||||
they do not activate sandboxing while `sandbox_mode` is `danger-full-access`.
|
||||
|
||||
Apply the generated configuration through the normal update process, then start a new Codex session under the
|
||||
repository owner's account. Existing sessions keep their active permissions. Check `whoami` and
|
||||
`git rev-parse --show-toplevel` there; package validation cannot prove the identity or network access of a future
|
||||
session. Launch overrides and managed policies may restrict or reject the configured mode.
|
||||
|
||||
Claude retains `permissions.defaultMode = "auto"` and shell-specific deny rules. Its sandbox is enabled on
|
||||
supported macOS, Linux, and WSL2 environments and disabled where unavailable, including native Windows.
|
||||
These are generated defaults; active session or account policy may differ.
|
||||
|
||||
## Status displays
|
||||
|
||||
@@ -97,10 +114,10 @@ reports the limitation and continues with `rg`.
|
||||
| `$XDG_CACHE_HOME/qmd/` (default `~/.cache/qmd/`) | Shared `index.sqlite` and model cache. |
|
||||
| `$QMD_CONFIG_DIR/index.yml` | Configuration when `QMD_CONFIG_DIR` is set; otherwise `$XDG_CONFIG_HOME/qmd/index.yml`, defaulting to `~/.config/qmd/index.yml`. |
|
||||
|
||||
Codex's adapter grants writable roots for `~/.my-ai-configuration/qmd` and `~/.cache/qmd`.
|
||||
A custom cache location needs a matching writable root. QMD also writes its configuration when registering
|
||||
collections and persisting ignore lists; the resolved configuration directory must be writable under the
|
||||
active sandbox. These adapter roots do not themselves grant access to `~/.config/qmd`.
|
||||
With the default Codex full-access configuration, QMD uses the launching account's filesystem and network
|
||||
permissions, including custom cache and configuration paths. If a session overrides this with a sandbox,
|
||||
permit writes to the runtime directory, resolved cache, and QMD configuration directory. QMD writes configuration
|
||||
when registering collections and persisting ignore lists.
|
||||
|
||||
Disabling search or uninstalling one client keeps the shared runtime while another client remains enabled.
|
||||
After the last client disables it, cleanup stops the daemon, removes recorded repository collections and
|
||||
@@ -112,8 +129,9 @@ old setup-owned Python retrieval directory and QMD extension ownership records.
|
||||
### Verification and troubleshooting
|
||||
|
||||
Check `~/.my-ai-configuration/qmd/warm.log` for background warm-up errors. First startup or changed repositories
|
||||
can take longer than a warm query. Windows sandbox setup and child-process pipe restrictions can prevent
|
||||
repository discovery or daemon startup; see [Permission defaults](#permission-defaults).
|
||||
can take longer than a warm query. An older session still using the Windows sandbox can fail Git ownership
|
||||
checks or child-process pipe operations. Apply the configuration and start a new session as described in
|
||||
[Permission defaults](#permission-defaults).
|
||||
|
||||
Model-free unit tests do not verify downloads, GPU support, sandbox access, or real search quality.
|
||||
See [QMD tests](scripts.md#qmd-tests) for the separate test commands and dependency setup.
|
||||
|
||||
@@ -53,6 +53,10 @@ The A/B benchmark measures whole sessions. Auto selection of local QMD search us
|
||||
|
||||
Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
|
||||
|
||||
Doctor captures native-command diagnostics and checks exit codes. A warning on stderr does not abort the
|
||||
PowerShell 5.1 script; failed version or configuration checks contribute to the final failure summary.
|
||||
On Windows, an unavailable profile-folder lookup falls back to PowerShell's home path.
|
||||
|
||||
| Script | Purpose |
|
||||
| --- | --- |
|
||||
| `checks/validate-config.py` | Validate generated Codex TOML and Claude JSON. |
|
||||
|
||||
@@ -4,6 +4,7 @@ $ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
. (Join-Path $root 'scripts/lib/manifest.ps1')
|
||||
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||
if ([string]::IsNullOrWhiteSpace($homePath)) { $homePath = $HOME }
|
||||
$script:fail = $false
|
||||
$script:checkCount = 0
|
||||
function Result($state, $message) {
|
||||
@@ -14,7 +15,27 @@ function Result($state, $message) {
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } }
|
||||
function Invoke-DoctorCommand {
|
||||
param([string]$Command, [string[]]$Arguments)
|
||||
# PowerShell 5.1 turns native stderr into error records, even when the process succeeds.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
try {
|
||||
$executable = Get-Command $Command -ErrorAction Stop
|
||||
$output = @(& $executable @Arguments 2>&1)
|
||||
return [pscustomobject]@{ Output = $output; ExitCode = $LASTEXITCODE }
|
||||
} catch {
|
||||
return [pscustomobject]@{ Output = @($_); ExitCode = 1 }
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tool in @('codex','claude')) {
|
||||
if (Get-Command $tool -ErrorAction SilentlyContinue) {
|
||||
$probe = Invoke-DoctorCommand -Command $tool -Arguments @('--version')
|
||||
if ($probe.ExitCode -eq 0) { Result 'PASS' "$tool available ($($probe.Output -join ' '))" }
|
||||
else { Result 'FAIL' "$tool --version failed (exit $($probe.ExitCode)): $($probe.Output -join ' ')" }
|
||||
} else { Result 'WARN' "$tool unavailable" }
|
||||
}
|
||||
if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude Bash status line' } else { Result 'WARN' 'jq unavailable; Claude Bash status line is disabled' }
|
||||
|
||||
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory -ErrorAction SilentlyContinue).Count
|
||||
@@ -50,14 +71,14 @@ if (Test-Path -LiteralPath $settingsPath) {
|
||||
}
|
||||
$configTomlPath = Join-Path $homePath '.codex/config.toml'
|
||||
if (Test-Path -LiteralPath $configTomlPath) {
|
||||
$parseResult = & python -c "import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8-sig'))" $configTomlPath 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" }
|
||||
$parseResult = Invoke-DoctorCommand -Command 'python' -Arguments @('-c', "import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8-sig'))", $configTomlPath)
|
||||
if ($parseResult.ExitCode -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $($parseResult.Output -join ' ')" }
|
||||
if (Get-Command codex -ErrorAction SilentlyContinue) {
|
||||
$previousCodexHome = $env:CODEX_HOME
|
||||
try {
|
||||
$env:CODEX_HOME = Split-Path $configTomlPath -Parent
|
||||
$schemaResult = & codex --strict-config --help 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml matches the installed Codex schema' } else { Result 'FAIL' "installed .codex/config.toml has unsupported Codex settings: $schemaResult" }
|
||||
$schemaResult = Invoke-DoctorCommand -Command 'codex' -Arguments @('--strict-config', '--help')
|
||||
if ($schemaResult.ExitCode -eq 0) { Result 'PASS' 'installed .codex/config.toml matches the installed Codex schema' } else { Result 'FAIL' "installed .codex/config.toml has unsupported Codex settings: $($schemaResult.Output -join ' ')" }
|
||||
} finally { $env:CODEX_HOME = $previousCodexHome }
|
||||
} else { Result 'WARN' 'Codex CLI unavailable; skipped installed Codex schema validation' }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,13 @@ result() {
|
||||
}
|
||||
|
||||
for tool in codex claude; do
|
||||
if command -v "$tool" >/dev/null; then result PASS "$tool available ($($tool --version 2>&1 | head -n 1))"; else result WARN "$tool unavailable"; fi
|
||||
if command -v "$tool" >/dev/null; then
|
||||
if version=$("$tool" --version 2>&1); then result PASS "$tool available ($version)"
|
||||
else
|
||||
status=$?
|
||||
result FAIL "$tool --version failed (exit $status): $version"
|
||||
fi
|
||||
else result WARN "$tool unavailable"; fi
|
||||
done
|
||||
command -v jq >/dev/null && result PASS 'jq available for Claude Bash status line' || result WARN 'jq unavailable; Claude Bash status line is disabled'
|
||||
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
param([switch]$Summary)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$doctorPath = Join-Path $root 'scripts/commands/doctor.ps1'
|
||||
$doctorAst = [System.Management.Automation.Language.Parser]::ParseFile($doctorPath, [ref]$null, [ref]$null)
|
||||
$probe = $doctorAst.Find({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Invoke-DoctorCommand' }, $true)
|
||||
if (-not $probe) { throw 'Doctor command probe is missing.' }
|
||||
Invoke-Expression $probe.Extent.Text
|
||||
foreach ($code in @(0, 7)) {
|
||||
$result = Invoke-DoctorCommand -Command 'python' -Arguments @('-c', "import sys; print('fixture 1.0'); print('fixture warning', file=sys.stderr); sys.exit($code)")
|
||||
if ($result.ExitCode -ne $code -or ($result.Output -join ' ') -notmatch 'fixture warning' -or ($result.Output -join ' ') -notmatch 'fixture 1.0') { throw 'Doctor must capture native warnings and preserve the exit code.' }
|
||||
if ($ErrorActionPreference -ne 'Stop') { throw 'Doctor command probe changed the caller error preference.' }
|
||||
}
|
||||
$missing = Invoke-DoctorCommand -Command 'ai-config-missing-doctor-test-command'
|
||||
if ($missing.ExitCode -eq 0) { throw 'Doctor must not report a missing command as successful.' }
|
||||
$doctorSummaryOutput = & (Join-Path $root 'scripts/commands/doctor.ps1') -Summary
|
||||
if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' }
|
||||
if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" }
|
||||
|
||||
@@ -8,6 +8,22 @@ while [ $# -gt 0 ]; do
|
||||
esac
|
||||
done
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
(
|
||||
codex() { printf 'fixture warning\n' >&2; printf 'fixture 1.0\n'; return "$DOCTOR_TEST_CODE"; }
|
||||
export -f codex
|
||||
for DOCTOR_TEST_CODE in 0 7; do
|
||||
export DOCTOR_TEST_CODE
|
||||
if probe_output=$(bash "$root/scripts/commands/doctor.sh" --summary); then probe_status=0; else probe_status=$?; fi
|
||||
if [ "$DOCTOR_TEST_CODE" -eq 0 ]; then
|
||||
test "$probe_status" -eq 0
|
||||
printf '%s\n' "$probe_output" | grep -Eq '^Doctor: PASS \| [0-9]+ checks$'
|
||||
else
|
||||
test "$probe_status" -eq 1
|
||||
printf '%s\n' "$probe_output" | grep -Fq 'FAIL codex --version failed (exit 7): fixture warning'
|
||||
printf '%s\n' "$probe_output" | grep -Eq '^Doctor: FAIL \| [0-9]+ checks$'
|
||||
fi
|
||||
done
|
||||
)
|
||||
doctor_summary_output=$(bash "$root/scripts/commands/doctor.sh" --summary)
|
||||
if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then
|
||||
printf 'Doctor summary mode must not print individual PASS lines.\n' >&2
|
||||
|
||||
@@ -18,6 +18,21 @@ SPEC.loader.exec_module(OPTIONS)
|
||||
|
||||
|
||||
class InstallOptionsTests(unittest.TestCase):
|
||||
def test_codex_update_replaces_old_sandbox_mode_and_preserves_user_settings(self):
|
||||
current = ('sandbox_mode = "workspace-write"\nmodel = "custom-model"\n'
|
||||
'[windows]\nsandbox = "elevated"\n'
|
||||
'[sandbox_workspace_write]\nnetwork_access = false\n'
|
||||
'[mcp_servers.demo]\ncommand = "demo"\n')
|
||||
managed = (ROOT / 'adapters/codex/config/config.toml').read_text(encoding='utf-8')
|
||||
|
||||
result = tomllib.loads(OPTIONS.merge_toml(current, managed))
|
||||
|
||||
self.assertEqual(result['sandbox_mode'], 'danger-full-access')
|
||||
self.assertEqual(result['approval_policy'], 'on-request')
|
||||
self.assertEqual(result['approvals_reviewer'], 'auto_review')
|
||||
self.assertEqual(result['model'], 'custom-model')
|
||||
self.assertEqual(result['mcp_servers']['demo'], {'command': 'demo'})
|
||||
|
||||
def test_merge_toml_preserves_local_plugin_table_indentation(self):
|
||||
plugin_state = (' [marketplaces.ponytail]\n'
|
||||
' source_type = "git"\n'
|
||||
|
||||
@@ -63,6 +63,7 @@ foreach ($shell in @('powershell','bash')) {
|
||||
$events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value })
|
||||
if ((($events | Sort-Object) -join ',') -ne 'SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" }
|
||||
if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" }
|
||||
if ($content -notmatch '(?m)^sandbox_mode\s*=\s*"danger-full-access"\s*$') { throw "Codex full access is missing in $package" }
|
||||
if ($content -notmatch '(?m)^max_depth\s*=\s*1\s*$') { throw "Codex agent depth limit is missing in $package" }
|
||||
if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" }
|
||||
} elseif ((($settings.hooks.PSObject.Properties.Name | Sort-Object) -join ',') -ne 'PreCompact,SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) {
|
||||
|
||||
@@ -83,6 +83,7 @@ for shell in powershell bash; do
|
||||
! grep -q 'flashbang-if-input' "$package/$file"
|
||||
! grep -Eq '^async[[:space:]]*=[[:space:]]*true[[:space:]]*$' "$package/$file"
|
||||
grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file"
|
||||
grep -Eq '^sandbox_mode[[:space:]]*=[[:space:]]*"danger-full-access"[[:space:]]*$' "$package/$file"
|
||||
grep -Eq '^max_depth[[:space:]]*=[[:space:]]*1[[:space:]]*$' "$package/$file"
|
||||
grep -Fq 'status_line = ["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file"
|
||||
else
|
||||
|
||||
Reference in new issue
Block a user