Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/sandbox-isolation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ jobs:
executed=$(grep -oE 'executed="[0-9]+"' "$trx" | head -1 | grep -oE '[0-9]+')
passed=$(grep -oE 'passed="[0-9]+"' "$trx" | head -1 | grep -oE '[0-9]+')
echo "executed=${executed:-0} passed=${passed:-0}"
if [ "${executed:-0}" -lt 107 ]; then
echo "::error::Expected >=107 sandbox isolation tests to run (bwrap/prlimit confinement + cap-drop + cgroup-namespace re-root + egress-allowlist filter + cgroup resource cap + durable-launch cgroup wiring + argv/envp per-string kernel ceiling + a prompt past it riding stdin + a read-only workspace mount + a network-off run reaching its broker through the relay and nothing else + an allowlist run relayed to its broker + a read-only reviewer reading its diff with the real CLIs + a bwrap probe that runs the launch argv + the MCP helper bound file by file behind a read-only socket dir + a CLI reaching its broker socket through the relay + a severed child reaching its broker over the lease socket across a worker restart + a pre-relay namespaced run re-bound at its gateway and torn down with its seal + an allowlist run's veth guarded both ways, a flow the worker opened before the run included + forwarding a root worker may not write named before an allowlist is planned + an allowlist run's port 53 open only at the resolvers of the resolv.conf its namespace reads, and still to a resolver address the worker's own NAT rewrites before its forward and its input hooks + a target repository's own CLI config kept out of the run with the real CLIs, every repository's memory read in a multi-repo workspace included + a repository whose memory links outside the workspace left out of a real Claude run, against the CLI following the link once that repository is added + nested memory loaded in place by a real Claude run, single- and multi-repo, and none of it past the in-place budget, where a read below a directory attaches only that directory's pointer + a path-scoped rule's pointer reaching a real Claude run only after a read its glob covers, single- and multi-repo, carrying none of the rule's text, against the rule copied into the config home as its control + pointers attaching on exactly the reads the unpinned CLI attached the repository's own rules on, single- and multi-repo + an Explore subagent pointed at nested memory loaded in place, which the CLI keeps out of its context + a multi-repo Codex run starting at a workspace root that is no git repository and loading no config from it + a repo-less Codex run starting in a scratch directory that is no git repository + a goal handed to the real Claude CLI as text it cannot act on, fresh and resumed, against the text channel as its control + a repository clean filter the capture runs with its egress severed + a real Codex agent that tampers its clone's .git while the platform publishes the branch from a clean repo, its Claude counterpart running in the non-root lane), but only ${executed:-0} did — the Category=Sandbox filter matched too few (trait regression?). If a case was deliberately removed, lower this number in the same PR."
if [ "${executed:-0}" -lt 108 ]; then
echo "::error::Expected >=108 sandbox isolation tests to run (bwrap/prlimit confinement + cap-drop + cgroup-namespace re-root + egress-allowlist filter + cgroup resource cap + durable-launch cgroup wiring + argv/envp per-string kernel ceiling + a prompt past it riding stdin + a read-only workspace mount + a network-off run reaching its broker through the relay and nothing else + an allowlist run relayed to its broker + a read-only reviewer reading its diff with the real CLIs + a bwrap probe that runs the launch argv + the MCP helper bound file by file behind a read-only socket dir + a CLI reaching its broker socket through the relay + a severed child reaching its broker over the lease socket across a worker restart + a pre-relay namespaced run re-bound at its gateway and torn down with its seal + an allowlist run's veth guarded both ways, a flow the worker opened before the run included + forwarding a root worker may not write named before an allowlist is planned + an allowlist run's port 53 open only at the resolvers of the resolv.conf its namespace reads, and still to a resolver address the worker's own NAT rewrites before its forward and its input hooks + a target repository's own CLI config kept out of the run with the real CLIs, every repository's memory read in a multi-repo workspace included + a repository whose memory links outside the workspace left out of a real Claude run, against the CLI following the link once that repository is added + nested memory loaded in place by a real Claude run, single- and multi-repo, and none of it past the in-place budget, where a read below a directory attaches only that directory's pointer + a path-scoped rule's pointer reaching a real Claude run only after a read its glob covers, single- and multi-repo, carrying none of the rule's text, against the rule copied into the config home as its control + pointers attaching on exactly the reads the unpinned CLI attached the repository's own rules on, single- and multi-repo + an Explore subagent pointed at nested memory loaded in place, which the CLI keeps out of its context + a multi-repo Codex run starting at a workspace root that is no git repository and loading no config from it + every repository's AGENTS.md reaching a real multi-repo Codex run once, after the operating contract, an override before the doc beside it and none that links outside the workspace + a repo-less Codex run starting in a scratch directory that is no git repository + a goal handed to the real Claude CLI as text it cannot act on, fresh and resumed, against the text channel as its control + a repository clean filter the capture runs with its egress severed + a real Codex agent that tampers its clone's .git while the platform publishes the branch from a clean repo, its Claude counterpart running in the non-root lane), but only ${executed:-0} did — the Category=Sandbox filter matched too few (trait regression?). If a case was deliberately removed, lower this number in the same PR."
exit 1
fi

Expand Down Expand Up @@ -268,10 +268,10 @@ jobs:
print(f'All {len(arms)} reviewer E2E arms ran and passed.')

# The repository-config E2E is armed by the same CLI pins and returns early the same way; require each arm's marker.
repo_config_arms = ('claude-code single-repo Confined', 'claude-code multi-repo Confined', 'memory-link-outside claude-code multi-repo Confined', 'nested-in-place claude-code single-repo Confined', 'nested-in-place claude-code multi-repo Confined', 'nested-over-budget claude-code single-repo Confined', 'scoped-rule-pointer claude-code single-repo Confined', 'scoped-rule-pointer claude-code multi-repo Confined', 'pointer-differential claude-code single-repo Confined', 'pointer-differential claude-code multi-repo Confined', 'nested-subagent-pointer claude-code single-repo Confined', 'codex-cli single-repo', 'codex-cli multi-repo', 'codex-cli scratch')
repo_config_arms = ('claude-code single-repo Confined', 'claude-code multi-repo Confined', 'memory-link-outside claude-code multi-repo Confined', 'nested-in-place claude-code single-repo Confined', 'nested-in-place claude-code multi-repo Confined', 'nested-over-budget claude-code single-repo Confined', 'scoped-rule-pointer claude-code single-repo Confined', 'scoped-rule-pointer claude-code multi-repo Confined', 'pointer-differential claude-code single-repo Confined', 'pointer-differential claude-code multi-repo Confined', 'nested-subagent-pointer claude-code single-repo Confined', 'codex-cli single-repo', 'codex-cli multi-repo', 'agents-md codex-cli multi-repo', 'codex-cli scratch')
for arm in repo_config_arms:
assert f'[repo-config-e2e] ran {arm}' in text, f'repository-config E2E arm "{arm}" did not run — check CODESPACE_REQUIRE_REVIEW_CLIS and the CLI install step'
repo_config_methods = (('A_claude_run_ignores_the_settings_its_repository_commits_and_still_reads_its_memory', 1), ('A_multi_repo_claude_run_reads_every_repositorys_memory_and_none_of_its_settings', 1), ('A_claude_run_leaves_out_repository_memory_that_links_outside_the_workspace', 1), ('A_claude_run_reads_nested_memory_in_place_and_nothing_it_must_not', 2), ('A_claude_run_over_the_in_place_budget_loads_no_nested_memory_up_front', 1), ('An_explore_subagent_is_pointed_at_nested_memory_loaded_in_place', 1), ('A_scoped_rule_reaches_the_model_only_after_a_read_it_matches', 2), ('Pointer_rules_attach_where_the_unpinned_cli_attached_project_rules', 2), ('A_codex_run_ignores_the_config_and_hooks_its_repository_commits_and_still_reads_its_agents_md', 1), ('A_multi_repo_codex_run_starts_at_a_workspace_root_that_is_no_repository_and_loads_no_config_from_it', 1), ('A_repo_less_codex_run_starts_in_a_scratch_directory_that_is_no_repository', 1))
repo_config_methods = (('A_claude_run_ignores_the_settings_its_repository_commits_and_still_reads_its_memory', 1), ('A_multi_repo_claude_run_reads_every_repositorys_memory_and_none_of_its_settings', 1), ('A_claude_run_leaves_out_repository_memory_that_links_outside_the_workspace', 1), ('A_claude_run_reads_nested_memory_in_place_and_nothing_it_must_not', 2), ('A_claude_run_over_the_in_place_budget_loads_no_nested_memory_up_front', 1), ('An_explore_subagent_is_pointed_at_nested_memory_loaded_in_place', 1), ('A_scoped_rule_reaches_the_model_only_after_a_read_it_matches', 2), ('Pointer_rules_attach_where_the_unpinned_cli_attached_project_rules', 2), ('A_codex_run_ignores_the_config_and_hooks_its_repository_commits_and_still_reads_its_agents_md', 1), ('A_multi_repo_codex_run_starts_at_a_workspace_root_that_is_no_repository_and_loads_no_config_from_it', 1), ('A_multi_repo_codex_run_reads_every_repositorys_agents_md', 1), ('A_repo_less_codex_run_starts_in_a_scratch_directory_that_is_no_repository', 1))
for method, rows in repo_config_methods:
cases = [r for r in results if 'RepositoryConfigE2ETests.' + method in r.get('testName', '')]
assert len(cases) == rows and all(r.get('outcome') == 'Passed' for r in cases), f'{method}: all {rows} case(s) must pass'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ public SandboxSpec BuildInvocation(AgentTask task)
{
EnsureWithinInputCap(task.Goal);

var guides = CodexRepositoryGuides.For(task);

// P3.2: a CONTINUE re-stage rewrites the `exec --json` seed to `exec resume <id> --json` so Codex picks up the
// prior thread. The subcommand must follow `exec` directly; --model, the `-c` overrides (incl. the sandbox on
// the resume path — see AppendSandbox), and the stdin `-` positional follow. Null (a fresh run) → the plain seed.
Expand Down Expand Up @@ -223,12 +225,14 @@ public SandboxSpec BuildInvocation(AgentTask task)
// Codex's native loader discovers them there (the same Agent-Skills format + SkillProjection as Claude —
// only the root differs, which is why it's CODEX_HOME's, not CLAUDE_CONFIG_DIR's). On a CONTINUE the prior
// session's rollout is restored alongside them under sessions/ (see BuildConfigHomeFiles).
ConfigHomeFiles = BuildConfigHomeFiles(task),
ConfigHomeFiles = BuildConfigHomeFiles(task, guides.Appendix),
// The agent reaches the network only when its permissions allow it (the sandbox severs egress otherwise).
AllowNetwork = task.Permissions.Network == AgentNetworkAccess.On,
// Codex's own sandbox is a nested bubblewrap that cannot start inside ours, so where our runner confines
// the run it stands that sandbox down and ours bounds every command instead (see SandboxStandDown).
WhenRunnerConfines = SandboxStandDown(task),
// Each repository doc the run's AGENTS.md left out or cut, for the run's timeline.
LaunchNotices = guides.Notices,
};
}

Expand Down Expand Up @@ -333,16 +337,19 @@ private static void EnsureWithinInputCap(string goal)
/// <summary>
/// The config-home files the runner materializes: (1) B1 — <c>AGENTS.md</c> carrying the persona + the always-on
/// operating contract (Codex's native instruction channel, since <c>exec</c> has no system-prompt flag; codex loads
/// <c>$CODEX_HOME/AGENTS.md</c> and merges it with any workspace AGENTS.md — verified against 0.142.2), ALWAYS present;
/// (2) the persona's projected skills; PLUS (3) — on a CONTINUE — the prior session's restored rollout at
/// <c>sessions/rollout-&lt;sessionId&gt;.jsonl</c> where <c>codex exec resume</c> finds it (codex scans <c>sessions/</c>
/// at any depth and matches the id in the <c>rollout-…</c> filename, so a deterministic id-named rollout suffices).
/// <c>$CODEX_HOME/AGENTS.md</c> and merges it with any workspace AGENTS.md — verified against 0.142.2), ALWAYS present,
/// and after them <paramref name="repositoryDocs"/>, the <c>AGENTS.md</c> of each repository below a multi-repo
/// run's cwd, which Codex never reads from there (<see cref="CodexRepositoryGuides"/>; empty for every other run, whose
/// file is byte-identical); (2) the persona's projected skills; PLUS (3) — on a CONTINUE — the prior session's
/// restored rollout at <c>sessions/rollout-&lt;sessionId&gt;.jsonl</c> where <c>codex exec resume</c> finds it (codex
/// scans <c>sessions/</c> at any depth and matches the id in the <c>rollout-…</c> filename, so a deterministic
/// id-named rollout suffices).
/// </summary>
private static IReadOnlyList<ConfigHomeFile> BuildConfigHomeFiles(AgentTask task)
private static IReadOnlyList<ConfigHomeFile> BuildConfigHomeFiles(AgentTask task, string repositoryDocs)
{
var files = new List<ConfigHomeFile>(SkillProjection.ToConfigHomeFiles(task.Skills, SkillsRoot))
{
new() { RelativePath = AgentsFile, Content = AgentOperatingContract.Compose(task.SystemPrompt) },
new() { RelativePath = AgentsFile, Content = AgentOperatingContract.Compose(task.SystemPrompt) + repositoryDocs },
};

// On a CONTINUE, restore the prior rollout so `codex exec resume` re-opens the thread.
Expand Down
Loading
Loading