Generated by pr-reviewer v0.5.0
{
"coverage_gaps": [
"No separate track for test quality; the test surface is small enough to review inside the two behavior tracks.",
"No integration/runtime track for Docker or actual sidecar startup because the diff is narrow and the main review risk is semantic regression, not orchestration plumbing volume.",
"No style/refactor track because the extracted helper itself is low risk unless it changes semantics, which the first track already owns."
],
"recommended_provider_mix": "Use a strongest-reasoning reviewer on the capability policy track because the main bug surface is subtle security semantics around undefined, empty, and invalid env values. Use a fast code-accurate reviewer on the driver-defaults track to trace helper behavior through constructor paths and confirm caller override semantics. Two tracks are sufficient; more would create overlap without improving defect discovery.",
"risks": [
"Silent security posture change: containers created through driver helpers may now gain `SETUID` and `SETGID` where they previously had no added capabilities.",
"Config defaults and driver defaults can drift again because both now depend on a shared helper but still apply defaults in separate places.",
"Unit tests validate object construction, but not whether the downstream container runtime interprets these capabilities safely or as intended."
],
"summary": "This change extracts container capability resolution into a shared helper and, more importantly, changes orchestrator driver defaults from `add: []` to `add: [\"SYS_PTRACE\", \"SETUID\", \"SETGID\"]` unless `DEFAULT_CAP_ADD` overrides them. The real risk is not the refactor itself; it is the behavioral change in default security posture across driver-created containers and whether existing override/disable paths still work exactly as intended.",
"tracks": [
{
"evidence_targets": [
"resolveDefaultContainerCapabilities() parsing of undefined vs empty string vs comma-separated values",
"ALLOWED_CONTAINER_CAPABILITIES and whether the new default set is intentionally broader than before",
"InvalidConfigError path and whether bad values fail at config load consistently",
"config.defaults.container.security.capabilities.add import-time behavior after vi.resetModules()",
"tests covering disable-by-empty-string and rejection of unsupported capabilities"
],
"goal": "Verify the shared capability resolver preserves the intended policy: allowed list enforcement, correct defaults, and explicit hardening via empty `DEFAULT_CAP_ADD` without accidental fallback.",
"scope": [
"apps/orchestrator/src/config.ts",
"apps/orchestrator/src/config/container-capabilities.ts",
"apps/orchestrator/tests/unit/config.test.ts"
],
"should_use_subagents": false,
"suggested_provider": "gpt-5",
"track_id": "capability-policy-and-env-semantics"
},
{
"evidence_targets": [
"createDriverConfig() default security.capabilities behavior before vs after this PR",
"createSecurityOptions() behavior when callers omit capabilities entirely",
"behavior when callers pass partial security options or explicit empty capability arrays/objects",
"all call sites that rely on driver defaults rather than config.defaults.container.security",
"tests proving helpers honor env overrides and do not overwrite explicit caller-provided capabilities"
],
"goal": "Check whether changing driver/security helper defaults from no added capabilities to subprocess-safe capabilities introduces privilege expansion or breaks caller override expectations.",
"scope": [
"apps/orchestrator/src/driver/interface/index.ts",
"apps/orchestrator/tests/unit/config.test.ts"
],
"should_use_subagents": false,
"suggested_provider": "claude-sonnet",
"track_id": "driver-defaults-and-override-regressions"
}
]
}
Scope: apps/orchestrator/src/config.ts, apps/orchestrator/src/config/container-capabilities.ts, apps/orchestrator/tests/unit/config.test.ts
- 🟠 [medium] resolveDefaultContainerCapabilities() evaluated lazily in driver interface but eagerly in config.ts (
apps/orchestrator/src/driver/interface/index.ts:112)In
config.ts,resolveDefaultContainerCapabilities()is called at module load time (it's inside a module-levelconst config = { ... }initializer). Indriver/interface/index.ts, it's called insidecreateDriverConfig()(line 112) andcreateSecurityOptions()(line 147) — meaning it runs on every invocation when no capabilities are provided.
This creates two issues:
- If the driver interface is used without importing
config.ts(e.g., in tests, or a future entry point), an invalidDEFAULT_CAP_ADDvalue won't fail at startup — it'll throwInvalidConfigErrorat container creation time, potentially mid-operation. - Each call re-parses and re-validates the env var. While negligible for performance, it means the resolved capabilities aren't guaranteed to be consistent across calls if
process.envis mutated between invocations.
Neither is likely to cause production issues today since the orchestrator imports config.ts at startup (catching bad values early), but it's a latent inconsistency worth noting.
config.ts line 410: `add: resolveDefaultContainerCapabilities()` — called once at import.
driver/interface/index.ts line 112: `add: resolveDefaultContainerCapabilities()` — called per `createDriverConfig()` invocation.
driver/interface/index.ts line 147: `add: resolveDefaultContainerCapabilities()` — called per `createSecurityOptions()` invocation.
Scope: apps/orchestrator/src/config.ts, apps/orchestrator/src/config/container-capabilities.ts, apps/orchestrator/tests/unit/config.test.ts
No findings from this reviewer.
Scope: apps/orchestrator/src/driver/interface/index.ts, apps/orchestrator/tests/unit/config.test.ts
-
🟠 [medium] No test verifies explicit caller-provided capabilities are preserved by createSecurityOptions or createDriverConfig (
apps/orchestrator/tests/unit/config.test.ts:112)Every test of createSecurityOptions passes an empty object ({}), and every test of createDriverConfig omits the security field entirely. This means there is no test asserting that when a caller provides explicit capabilities — e.g. createSecurityOptions({ capabilities: { drop: ['ALL'], add: ['NET_BIND_SERVICE'] } }) — those capabilities are returned unchanged rather than being replaced by the new subprocess-safe defaults. The only thing preventing the defaults from stomping caller-provided caps is the || operator on line 145 of index.ts (and line 106 for createDriverConfig). If someone refactors this to use nullish coalescing, deep-merge, or spread semantics, there is no test to catch the regression. Given that this PR specifically widens the default privilege set, a test proving caller overrides are respected is important to prevent unintentional privilege expansion for callers who deliberately restrict capabilities.
All createSecurityOptions test calls use createSecurityOptions({}) — grep for 'createSecurityOptions({' in tests returns only empty-object invocations. No createDriverConfig test passes an explicit security field. -
🟡 [low] resolveDefaultContainerCapabilities() is evaluated at call time, not module load — capabilities can drift between calls (
apps/orchestrator/src/driver/interface/index.ts:112)In createDriverConfig (line 112) and createSecurityOptions (line 147), resolveDefaultContainerCapabilities() is called on every invocation rather than being captured once at module load. This means if process.env.DEFAULT_CAP_ADD is mutated at runtime between two calls, the two containers would get different capability sets. In the config.ts path this is fine (evaluated once at module load), but in the driver helpers it's evaluated per-call. This is likely intentional for testability (the tests rely on vi.resetModules + env mutation), but it's worth noting that the config.ts and driver paths have subtly different evaluation semantics for the same function.
config.ts calls resolveDefaultContainerCapabilities() once during module evaluation. createDriverConfig and createSecurityOptions call it on every invocation. If process.env.DEFAULT_CAP_ADD changes between calls, containers get different caps.
Scope: apps/orchestrator/src/driver/interface/index.ts, apps/orchestrator/tests/unit/config.test.ts
- 🔴 [high] Partial security overrides now implicitly add three Linux capabilities (
apps/orchestrator/src/driver/interface/index.ts:145)createSecurityOptions()acceptsPartial<SecurityOptions>, so callers are allowed to override a subset of security fields and rely on helper defaults for the rest. After this change, omittingoptions.capabilitiesno longer means "no added capabilities"; line 145 now injectsresolveDefaultContainerCapabilities(), which expands privileges toSYS_PTRACE,SETUID, andSETGID. In production, any caller that only wanted to setreadOnly,noNewPrivileges, oruserwill now get those extra capabilities unless it also remembers to send an explicit emptycapabilities.add. The new tests reinforce this behavior by asserting thatcreateSecurityOptions({})returns those capabilities, but they never cover the safer partial-override case.`capabilities: options.capabilities || { drop: ["ALL"], add: resolveDefaultContainerCapabilities(), }` at `createSecurityOptions()`, plus `apps/orchestrator/tests/unit/config.test.ts:112-149` asserting `createSecurityOptions({}).capabilities.add` becomes `["SYS_PTRACE", "SETUID", "SETGID"]` or env-driven equivalents.
{
"command": [
"claude",
"--dangerously-skip-permissions",
"-p",
"You are a principal engineer consolidating the outputs of parallel audit tracks into a final verdict. You've seen audit pipelines produce noise disguised as signal \u2014 your job is to cut through it.\n\nYou will receive findings from multiple independent audit tracks, each with their own scope and confidence levels.\n\n## How to validate\n\nRead every finding critically. A finding earns acceptance by having three things: a specific code location, a concrete failure mode, and evidence from the diff or codebase. Findings missing any of these get rejected.\n\nWhen two tracks report the same issue, keep the stronger version and reject the duplicate. When tracks contradict each other, investigate \u2014 one of them is wrong, and you need to determine which based on the evidence quality.\n\nScore the audit honestly:\n- **coverage_score** (0-100): How much of the actual risk surface did the tracks inspect? A perfect score means every changed code path was examined by at least one track. Penalize for tracks that reviewed boilerplate while missing critical paths.\n- **evidence_score** (0-100): How well-evidenced are the accepted findings? High means every finding points to specific lines with demonstrated failure modes. Low means findings rely on speculation or pattern-matching without proof.\n- **signal_score** (0-100): What's the signal-to-noise ratio across all tracks? High means most findings were worth reporting. Low means tracks padded their output with style nits or speculative concerns.\n\n## Acceptance criteria\n\nAccept a finding when:\n- It names a specific file, line, and failure mode\n- The failure mode is demonstrated, not hypothetical\n- It would change how you ship or test the code\n\nReject a finding when:\n- It's a style preference disguised as a bug\n- It says \"could potentially\" without showing how\n- It duplicates a stronger finding from another track\n- The evidence contradicts the conclusion\n- It's outside the track's assigned scope (scope creep)\n\n## LOC discipline\n\nWhen findings propose fixes or implementations, evaluate their LOC efficiency. Flag findings whose suggested fix is disproportionate to the issue \u2014 a 3-line bug shouldn't generate a 30-line refactor. Prefer findings that propose minimal, targeted changes. If a finding's fix would exceed 2x the lines of the code it references, it needs explicit justification or it should be flagged as over-engineered.\n\n## What to avoid\n\n- Accepting findings just because they sound serious \u2014 severity labels from tracks are claims, not facts\n- Rejecting findings because they're low severity \u2014 a real low-severity issue is better than a fabricated high one\n- Marking coverage gaps you can't actually identify \u2014 only flag gaps where you can name what wasn't reviewed\n\n## Output\n\nReturn JSON only, no markdown fences.\n\n```\n{\n \"status\": \"ok|error\",\n \"coverage_score\": 0,\n \"evidence_score\": 0,\n \"signal_score\": 0,\n \"accepted_findings\": [\n {\n \"severity\": \"high|medium|low\",\n \"confidence\": \"high|medium|low\",\n \"category\": \"correctness|security|regression|testing|operational\",\n \"title\": \"short, specific title\",\n \"body\": \"what's wrong and why it matters\",\n \"file\": \"path/to/file\",\n \"line\": 0,\n \"source_track\": \"which track reported this\",\n \"evidence\": \"the code or behavior demonstrating the issue\"\n }\n ],\n \"rejected_findings\": [\n {\n \"title\": \"what was reported\",\n \"source_track\": \"which track\",\n \"rejection_reason\": \"why it didn't meet the bar\"\n }\n ],\n \"coverage_gaps\": [\"specific areas of the change that no track adequately reviewed\"],\n \"recommendation\": \"ship|needs-work|needs-followup \u2014 with a one-sentence justification\"\n}\n```\n\n## Request\n\n{\n \"changed_files\": [\n \"apps/orchestrator/src/config.ts\",\n \"apps/orchestrator/src/config/container-capabilities.ts\",\n \"apps/orchestrator/src/driver/interface/index.ts\",\n \"apps/orchestrator/tests/unit/config.test.ts\"\n ],\n \"plan\": {\n \"coverage_gaps\": [\n \"No separate track for test quality; the test surface is small enough to review inside the two behavior tracks.\",\n \"No integration/runtime track for Docker or actual sidecar startup because the diff is narrow and the main review risk is semantic regression, not orchestration plumbing volume.\",\n \"No style/refactor track because the extracted helper itself is low risk unless it changes semantics, which the first track already owns.\"\n ],\n \"recommended_provider_mix\": \"Use a strongest-reasoning reviewer on the capability policy track because the main bug surface is subtle security semantics around undefined, empty, and invalid env values. Use a fast code-accurate reviewer on the driver-defaults track to trace helper behavior through constructor paths and confirm caller override semantics. Two tracks are sufficient; more would create overlap without improving defect discovery.\",\n \"risks\": [\n \"Silent security posture change: containers created through driver helpers may now gain `SETUID` and `SETGID` where they previously had no added capabilities.\",\n \"Config defaults and driver defaults can drift again because both now depend on a shared helper but still apply defaults in separate places.\",\n \"Unit tests validate object construction, but not whether the downstream container runtime interprets these capabilities safely or as intended.\"\n ],\n \"summary\": \"This change extracts container capability resolution into a shared helper and, more importantly, changes orchestrator driver defaults from `add: []` to `add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"]` unless `DEFAULT_CAP_ADD` overrides them. The real risk is not the refactor itself; it is the behavioral change in default security posture across driver-created containers and whether existing override/disable paths still work exactly as intended.\",\n \"tracks\": [\n {\n \"evidence_targets\": [\n \"resolveDefaultContainerCapabilities() parsing of undefined vs empty string vs comma-separated values\",\n \"ALLOWED_CONTAINER_CAPABILITIES and whether the new default set is intentionally broader than before\",\n \"InvalidConfigError path and whether bad values fail at config load consistently\",\n \"config.defaults.container.security.capabilities.add import-time behavior after vi.resetModules()\",\n \"tests covering disable-by-empty-string and rejection of unsupported capabilities\"\n ],\n \"goal\": \"Verify the shared capability resolver preserves the intended policy: allowed list enforcement, correct defaults, and explicit hardening via empty `DEFAULT_CAP_ADD` without accidental fallback.\",\n \"scope\": [\n \"apps/orchestrator/src/config.ts\",\n \"apps/orchestrator/src/config/container-capabilities.ts\",\n \"apps/orchestrator/tests/unit/config.test.ts\"\n ],\n \"should_use_subagents\": false,\n \"suggested_provider\": \"gpt-5\",\n \"track_id\": \"capability-policy-and-env-semantics\"\n },\n {\n \"evidence_targets\": [\n \"createDriverConfig() default security.capabilities behavior before vs after this PR\",\n \"createSecurityOptions() behavior when callers omit capabilities entirely\",\n \"behavior when callers pass partial security options or explicit empty capability arrays/objects\",\n \"all call sites that rely on driver defaults rather than config.defaults.container.security\",\n \"tests proving helpers honor env overrides and do not overwrite explicit caller-provided capabilities\"\n ],\n \"goal\": \"Check whether changing driver/security helper defaults from no added capabilities to subprocess-safe capabilities introduces privilege expansion or breaks caller override expectations.\",\n \"scope\": [\n \"apps/orchestrator/src/driver/interface/index.ts\",\n \"apps/orchestrator/tests/unit/config.test.ts\"\n ],\n \"should_use_subagents\": false,\n \"suggested_provider\": \"claude-sonnet\",\n \"track_id\": \"driver-defaults-and-override-regressions\"\n }\n ]\n },\n \"pr\": 386,\n \"repo\": \"tangle-network/agent-dev-container\",\n \"track_outputs\": [\n {\n \"command\": [\n \"codex\",\n \"exec\",\n \"--full-auto\",\n \"You are a senior engineer reviewing one assigned track of a code change. You are thorough but you respect the reader's time \\u2014 every finding you report should be worth someone stopping to look at.\\n\\nYou own only your assigned track. Stay in scope. Do not report on files or concerns outside your track's scope unless you find something genuinely dangerous.\\n\\n## How to review\\n\\nRead the evidence in your track scope. Think about what could go wrong in production, what would break silently, what a test wouldn't catch. Look at error paths, edge cases, and implicit assumptions.\\n\\nUse sub-agents when it helps \\u2014 for example, to check test coverage while you're reviewing implementation logic. But don't use them just because you can.\\n\\n## What makes a good finding\\n\\nA good finding names a specific file and line, explains what's wrong, and shows evidence. \\\"This nil check on line 47 of auth.go doesn't cover the case where the token is expired but structurally valid \\u2014 the downstream handler will panic\\\" is a finding. \\\"Consider adding error handling\\\" is not.\\n\\nAsk yourself: would a staff engineer read this finding and say \\\"good catch\\\" or \\\"obvious filler\\\"? Only report the former.\\n\\n## What to avoid\\n\\n- Findings about style, naming, or formatting unless they create actual confusion\\n- \\\"Consider adding tests for X\\\" without explaining what specific behavior is untested and why it matters\\n- Speculative findings without code evidence \\u2014 if you can't point to the line, don't report it\\n- Duplicating findings that clearly belong to another track\\n\\n## Confidence\\n\\nBe honest about what you're sure about versus what you suspect. Flag uncertainty in `confidence_notes` rather than inflating finding confidence.\\n\\n## Output\\n\\nReturn JSON only, no markdown fences.\\n\\n```\\n{\\n \\\"status\\\": \\\"ok|error\\\",\\n \\\"track_id\\\": \\\"from the assigned track\\\",\\n \\\"summary\\\": \\\"what you found, in one paragraph\\\",\\n \\\"findings\\\": [\\n {\\n \\\"severity\\\": \\\"high|medium|low\\\",\\n \\\"confidence\\\": \\\"high|medium|low\\\",\\n \\\"category\\\": \\\"correctness|security|regression|testing|operational\\\",\\n \\\"title\\\": \\\"short, specific title\\\",\\n \\\"body\\\": \\\"what's wrong, why it matters, what evidence you found\\\",\\n \\\"file\\\": \\\"path/to/file\\\",\\n \\\"line\\\": 0,\\n \\\"evidence\\\": \\\"the actual code or behavior that demonstrates the issue\\\"\\n }\\n ],\\n \\\"questions\\\": [\\\"things you want to ask the author \\u2014 genuine questions, not passive-aggressive suggestions\\\"],\\n \\\"confidence_notes\\\": [\\\"where you're uncertain and why\\\"]\\n}\\n```\\n\\n## Request\\n\\n{\\n \\\"changed_files\\\": [\\n \\\"apps/orchestrator/src/config.ts\\\",\\n \\\"apps/orchestrator/src/config/container-capabilities.ts\\\",\\n \\\"apps/orchestrator/src/driver/interface/index.ts\\\",\\n \\\"apps/orchestrator/tests/unit/config.test.ts\\\"\\n ],\\n \\\"pr\\\": 386,\\n \\\"repo\\\": \\\"tangle-network/agent-dev-container\\\",\\n \\\"track\\\": {\\n \\\"evidence_targets\\\": [\\n \\\"resolveDefaultContainerCapabilities() parsing of undefined vs empty string vs comma-separated values\\\",\\n \\\"ALLOWED_CONTAINER_CAPABILITIES and whether the new default set is intentionally broader than before\\\",\\n \\\"InvalidConfigError path and whether bad values fail at config load consistently\\\",\\n \\\"config.defaults.container.security.capabilities.add import-time behavior after vi.resetModules()\\\",\\n \\\"tests covering disable-by-empty-string and rejection of unsupported capabilities\\\"\\n ],\\n \\\"goal\\\": \\\"Verify the shared capability resolver preserves the intended policy: allowed list enforcement, correct defaults, and explicit hardening via empty `DEFAULT_CAP_ADD` without accidental fallback.\\\",\\n \\\"scope\\\": [\\n \\\"apps/orchestrator/src/config.ts\\\",\\n \\\"apps/orchestrator/src/config/container-capabilities.ts\\\",\\n \\\"apps/orchestrator/tests/unit/config.test.ts\\\"\\n ],\\n \\\"should_use_subagents\\\": false,\\n \\\"suggested_provider\\\": \\\"gpt-5\\\",\\n \\\"track_id\\\": \\\"capability-policy-and-env-semantics\\\"\\n }\\n}\\n\\n## Diff\\n\\ndiff --git a/apps/orchestrator/src/config.ts b/apps/orchestrator/src/config.ts\\nindex 46b33e6..74de0b9 100644\\n--- a/apps/orchestrator/src/config.ts\\n+++ b/apps/orchestrator/src/config.ts\\n@@ -10,6 +10,7 @@ import {\\n validateTlsClientConfig,\\n validateTlsServerConfig,\\n } from \\\"@repo/shared\\\";\\n+import { resolveDefaultContainerCapabilities } from \\\"./config/container-capabilities.js\\\";\\n import {\\n DEFAULT_POOL_CONFIG,\\n type PoolConfig,\\n@@ -406,31 +407,7 @@ export const config = {\\n user: process.env.DEFAULT_CONTAINER_USER || \\\"root\\\",\\n capabilities: {\\n drop: [\\\"ALL\\\"] as string[],\\n- // SYS_PTRACE required for Bun runtime (opencode binary crashes without it).\\n- // Configurable via env var to allow disabling for hardened deployments.\\n- add: (() => {\\n- const ALLOWED_CAPABILITIES = new Set([\\n- \\\"SYS_PTRACE\\\",\\n- \\\"NET_BIND_SERVICE\\\",\\n- \\\"CHOWN\\\",\\n- \\\"DAC_OVERRIDE\\\",\\n- \\\"FOWNER\\\",\\n- \\\"SETGID\\\",\\n- \\\"SETUID\\\",\\n- ]);\\n- const raw = (process.env.DEFAULT_CAP_ADD || \\\"SYS_PTRACE\\\")\\n- .split(\\\",\\\")\\n- .map((c) => c.trim())\\n- .filter(Boolean);\\n- const rejected = raw.filter((c) => !ALLOWED_CAPABILITIES.has(c));\\n- if (rejected.length > 0) {\\n- throw new InvalidConfigError(\\n- \\\"DEFAULT_CAP_ADD\\\",\\n- `disallowed capabilities: ${rejected.join(\\\", \\\")}. Allowed: ${[...ALLOWED_CAPABILITIES].join(\\\", \\\")}`,\\n- );\\n- }\\n- return raw;\\n- })(),\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n },\\n },\\ndiff --git a/apps/orchestrator/src/config/container-capabilities.ts b/apps/orchestrator/src/config/container-capabilities.ts\\nnew file mode 100644\\nindex 0000000..1a0fbc7\\n--- /dev/null\\n+++ b/apps/orchestrator/src/config/container-capabilities.ts\\n@@ -0,0 +1,40 @@\\n+import { InvalidConfigError } from \\\"@repo/shared\\\";\\n+\\n+const ALLOWED_CONTAINER_CAPABILITIES = new Set([\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"NET_BIND_SERVICE\\\",\\n+ \\\"CHOWN\\\",\\n+ \\\"DAC_OVERRIDE\\\",\\n+ \\\"FOWNER\\\",\\n+ \\\"SETGID\\\",\\n+ \\\"SETUID\\\",\\n+]);\\n+\\n+export const DEFAULT_CONTAINER_CAPABILITIES = [\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"SETUID\\\",\\n+ \\\"SETGID\\\",\\n+] as const;\\n+\\n+export function resolveDefaultContainerCapabilities(\\n+ env: NodeJS.ProcessEnv = process.env,\\n+): string[] {\\n+ const raw = env.DEFAULT_CAP_ADD ?? DEFAULT_CONTAINER_CAPABILITIES.join(\\\",\\\");\\n+ const capabilities = raw\\n+ .split(\\\",\\\")\\n+ .map((capability) => capability.trim())\\n+ .filter(Boolean);\\n+\\n+ const rejected = capabilities.filter(\\n+ (capability) => !ALLOWED_CONTAINER_CAPABILITIES.has(capability),\\n+ );\\n+\\n+ if (rejected.length > 0) {\\n+ throw new InvalidConfigError(\\n+ \\\"DEFAULT_CAP_ADD\\\",\\n+ `disallowed capabilities: ${rejected.join(\\\", \\\")}. Allowed: ${[...ALLOWED_CONTAINER_CAPABILITIES].join(\\\", \\\")}`,\\n+ );\\n+ }\\n+\\n+ return capabilities;\\n+}\\ndiff --git a/apps/orchestrator/src/driver/interface/index.ts b/apps/orchestrator/src/driver/interface/index.ts\\nindex 75e4677..a7a887d 100644\\n--- a/apps/orchestrator/src/driver/interface/index.ts\\n+++ b/apps/orchestrator/src/driver/interface/index.ts\\n@@ -60,6 +60,7 @@ const DEFAULT_PIDS_LIMIT = 1000; // 1000 processes for OpenCode + child processe\\n // - Model response buffering (~500MB for large contexts)\\n const MIN_MEMORY_LIMIT = 2048; // 2GB minimum\\n \\n+import { resolveDefaultContainerCapabilities } from \\\"../../config/container-capabilities.js\\\";\\n import {\\n type ContainerConfig,\\n ContainerConfigSchema,\\n@@ -108,7 +109,7 @@ export function createDriverConfig(\\n user: \\\"root\\\", // Default to root for compatibility with standard images\\n capabilities: {\\n drop: [\\\"ALL\\\"],\\n- add: [],\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n },\\n maxVolumeSizeBytes: config.maxVolumeSizeBytes,\\n@@ -143,7 +144,7 @@ export function createSecurityOptions(\\n user: options.user || \\\"root\\\", // Default to root for compatibility with standard images\\n capabilities: options.capabilities || {\\n drop: [\\\"ALL\\\"],\\n- add: [],\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n };\\n }\\ndiff --git a/apps/orchestrator/tests/unit/config.test.ts b/apps/orchestrator/tests/unit/config.test.ts\\nindex 020969b..6e30dbd 100644\\n--- a/apps/orchestrator/tests/unit/config.test.ts\\n+++ b/apps/orchestrator/tests/unit/config.test.ts\\n@@ -19,6 +19,50 @@ afterAll(() => {\\n });\\n \\n describe(\\\"config\\\", () => {\\n+ it(\\\"defaults sidecar container capabilities to subprocess-safe caps\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"SETUID\\\",\\n+ \\\"SETGID\\\",\\n+ ]);\\n+ });\\n+\\n+ it(\\\"honors explicit DEFAULT_CAP_ADD overrides\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_PTRACE\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([\\n+ \\\"SYS_PTRACE\\\",\\n+ ]);\\n+ });\\n+\\n+ it(\\\"allows hardened deployments to disable default capabilities\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([]);\\n+ });\\n+\\n+ it(\\\"rejects invalid DEFAULT_CAP_ADD values\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_ADMIN\\\",\\n+ });\\n+ vi.resetModules();\\n+\\n+ await expect(import(\\\"../../src/config\\\")).rejects.toThrowError(\\n+ /DEFAULT_CAP_ADD/,\\n+ );\\n+ });\\n+\\n it(\\\"provides Pangolin defaults\\\", async () => {\\n setEnv({\\n PANGOLIN_ENABLED: \\\"true\\\",\\n@@ -64,4 +108,64 @@ describe(\\\"config\\\", () => {\\n \\\"Missing required configuration: PANGOLIN_API_KEY\\\",\\n );\\n });\\n+\\n+ it(\\\"uses subprocess-safe capability defaults when security options omit caps\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { createSecurityOptions } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(createSecurityOptions({}).capabilities).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"],\\n+ });\\n+ });\\n+\\n+ it(\\\"uses env-configured capabilities in security helpers\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_PTRACE\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { createDriverConfig, createSecurityOptions } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(createSecurityOptions({}).capabilities).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\"],\\n+ });\\n+\\n+ expect(\\n+ createDriverConfig({\\n+ image: \\\"test-image\\\",\\n+ sessionId: \\\"session-1\\\",\\n+ }).security.capabilities,\\n+ ).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\"],\\n+ });\\n+ });\\n+\\n+ it(\\\"uses subprocess-safe capability defaults in createDriverConfig\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { createDriverConfig } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(\\n+ createDriverConfig({\\n+ image: \\\"test-image\\\",\\n+ sessionId: \\\"session-1\\\",\\n+ }).security.capabilities,\\n+ ).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"],\\n+ });\\n+ });\\n });\\n\"\n ],\n \"error\": null,\n \"provider\": \"codex\",\n \"result\": {\n \"confidence_notes\": [\n \"I could not execute the Vitest file in this checkout because dependencies are not installed (`vitest: not found` / missing `node_modules`), so confidence is based on static review rather than a local test run.\",\n \"The broader default set (`SYS_PTRACE`,`SETUID`,`SETGID`) is a policy change relative to the diff you provided, but within this scope I did not find evidence that the implementation violates the stated intent; sidecar code elsewhere in the repo does appear to rely on uid/gid demotion behavior.\"\n ],\n \"findings\": [],\n \"questions\": [],\n \"status\": \"ok\",\n \"summary\": \"I did not find a production defect in this track. The shared resolver preserves the intended env semantics: `DEFAULT_CAP_ADD` unset falls back to the default capability set, an explicit empty string resolves to `[]` without falling back, comma-separated values are trimmed and validated against the allowlist, and unsupported values raise `InvalidConfigError`. In the main server path, that still fails fast at startup because `server.ts` imports `config` and calls `validateConfig()` before serving traffic. The new tests cover the important hardening and rejection cases in scope.\",\n \"track_id\": \"capability-policy-and-env-semantics\"\n },\n \"status\": \"ok\",\n \"track\": {\n \"evidence_targets\": [\n \"resolveDefaultContainerCapabilities() parsing of undefined vs empty string vs comma-separated values\",\n \"ALLOWED_CONTAINER_CAPABILITIES and whether the new default set is intentionally broader than before\",\n \"InvalidConfigError path and whether bad values fail at config load consistently\",\n \"config.defaults.container.security.capabilities.add import-time behavior after vi.resetModules()\",\n \"tests covering disable-by-empty-string and rejection of unsupported capabilities\"\n ],\n \"goal\": \"Verify the shared capability resolver preserves the intended policy: allowed list enforcement, correct defaults, and explicit hardening via empty `DEFAULT_CAP_ADD` without accidental fallback.\",\n \"scope\": [\n \"apps/orchestrator/src/config.ts\",\n \"apps/orchestrator/src/config/container-capabilities.ts\",\n \"apps/orchestrator/tests/unit/config.test.ts\"\n ],\n \"should_use_subagents\": false,\n \"suggested_provider\": \"gpt-5\",\n \"track_id\": \"capability-policy-and-env-semantics\"\n }\n },\n {\n \"command\": [\n \"claude\",\n \"--dangerously-skip-permissions\",\n \"-p\",\n \"You are a senior engineer reviewing one assigned track of a code change. You are thorough but you respect the reader's time \\u2014 every finding you report should be worth someone stopping to look at.\\n\\nYou own only your assigned track. Stay in scope. Do not report on files or concerns outside your track's scope unless you find something genuinely dangerous.\\n\\n## How to review\\n\\nRead the evidence in your track scope. Think about what could go wrong in production, what would break silently, what a test wouldn't catch. Look at error paths, edge cases, and implicit assumptions.\\n\\nUse sub-agents when it helps \\u2014 for example, to check test coverage while you're reviewing implementation logic. But don't use them just because you can.\\n\\n## What makes a good finding\\n\\nA good finding names a specific file and line, explains what's wrong, and shows evidence. \\\"This nil check on line 47 of auth.go doesn't cover the case where the token is expired but structurally valid \\u2014 the downstream handler will panic\\\" is a finding. \\\"Consider adding error handling\\\" is not.\\n\\nAsk yourself: would a staff engineer read this finding and say \\\"good catch\\\" or \\\"obvious filler\\\"? Only report the former.\\n\\n## What to avoid\\n\\n- Findings about style, naming, or formatting unless they create actual confusion\\n- \\\"Consider adding tests for X\\\" without explaining what specific behavior is untested and why it matters\\n- Speculative findings without code evidence \\u2014 if you can't point to the line, don't report it\\n- Duplicating findings that clearly belong to another track\\n\\n## Confidence\\n\\nBe honest about what you're sure about versus what you suspect. Flag uncertainty in `confidence_notes` rather than inflating finding confidence.\\n\\n## Output\\n\\nReturn JSON only, no markdown fences.\\n\\n```\\n{\\n \\\"status\\\": \\\"ok|error\\\",\\n \\\"track_id\\\": \\\"from the assigned track\\\",\\n \\\"summary\\\": \\\"what you found, in one paragraph\\\",\\n \\\"findings\\\": [\\n {\\n \\\"severity\\\": \\\"high|medium|low\\\",\\n \\\"confidence\\\": \\\"high|medium|low\\\",\\n \\\"category\\\": \\\"correctness|security|regression|testing|operational\\\",\\n \\\"title\\\": \\\"short, specific title\\\",\\n \\\"body\\\": \\\"what's wrong, why it matters, what evidence you found\\\",\\n \\\"file\\\": \\\"path/to/file\\\",\\n \\\"line\\\": 0,\\n \\\"evidence\\\": \\\"the actual code or behavior that demonstrates the issue\\\"\\n }\\n ],\\n \\\"questions\\\": [\\\"things you want to ask the author \\u2014 genuine questions, not passive-aggressive suggestions\\\"],\\n \\\"confidence_notes\\\": [\\\"where you're uncertain and why\\\"]\\n}\\n```\\n\\n## Request\\n\\n{\\n \\\"changed_files\\\": [\\n \\\"apps/orchestrator/src/config.ts\\\",\\n \\\"apps/orchestrator/src/config/container-capabilities.ts\\\",\\n \\\"apps/orchestrator/src/driver/interface/index.ts\\\",\\n \\\"apps/orchestrator/tests/unit/config.test.ts\\\"\\n ],\\n \\\"pr\\\": 386,\\n \\\"repo\\\": \\\"tangle-network/agent-dev-container\\\",\\n \\\"track\\\": {\\n \\\"evidence_targets\\\": [\\n \\\"resolveDefaultContainerCapabilities() parsing of undefined vs empty string vs comma-separated values\\\",\\n \\\"ALLOWED_CONTAINER_CAPABILITIES and whether the new default set is intentionally broader than before\\\",\\n \\\"InvalidConfigError path and whether bad values fail at config load consistently\\\",\\n \\\"config.defaults.container.security.capabilities.add import-time behavior after vi.resetModules()\\\",\\n \\\"tests covering disable-by-empty-string and rejection of unsupported capabilities\\\"\\n ],\\n \\\"goal\\\": \\\"Verify the shared capability resolver preserves the intended policy: allowed list enforcement, correct defaults, and explicit hardening via empty `DEFAULT_CAP_ADD` without accidental fallback.\\\",\\n \\\"scope\\\": [\\n \\\"apps/orchestrator/src/config.ts\\\",\\n \\\"apps/orchestrator/src/config/container-capabilities.ts\\\",\\n \\\"apps/orchestrator/tests/unit/config.test.ts\\\"\\n ],\\n \\\"should_use_subagents\\\": false,\\n \\\"suggested_provider\\\": \\\"gpt-5\\\",\\n \\\"track_id\\\": \\\"capability-policy-and-env-semantics\\\"\\n }\\n}\\n\\n## Diff\\n\\ndiff --git a/apps/orchestrator/src/config.ts b/apps/orchestrator/src/config.ts\\nindex 46b33e6..74de0b9 100644\\n--- a/apps/orchestrator/src/config.ts\\n+++ b/apps/orchestrator/src/config.ts\\n@@ -10,6 +10,7 @@ import {\\n validateTlsClientConfig,\\n validateTlsServerConfig,\\n } from \\\"@repo/shared\\\";\\n+import { resolveDefaultContainerCapabilities } from \\\"./config/container-capabilities.js\\\";\\n import {\\n DEFAULT_POOL_CONFIG,\\n type PoolConfig,\\n@@ -406,31 +407,7 @@ export const config = {\\n user: process.env.DEFAULT_CONTAINER_USER || \\\"root\\\",\\n capabilities: {\\n drop: [\\\"ALL\\\"] as string[],\\n- // SYS_PTRACE required for Bun runtime (opencode binary crashes without it).\\n- // Configurable via env var to allow disabling for hardened deployments.\\n- add: (() => {\\n- const ALLOWED_CAPABILITIES = new Set([\\n- \\\"SYS_PTRACE\\\",\\n- \\\"NET_BIND_SERVICE\\\",\\n- \\\"CHOWN\\\",\\n- \\\"DAC_OVERRIDE\\\",\\n- \\\"FOWNER\\\",\\n- \\\"SETGID\\\",\\n- \\\"SETUID\\\",\\n- ]);\\n- const raw = (process.env.DEFAULT_CAP_ADD || \\\"SYS_PTRACE\\\")\\n- .split(\\\",\\\")\\n- .map((c) => c.trim())\\n- .filter(Boolean);\\n- const rejected = raw.filter((c) => !ALLOWED_CAPABILITIES.has(c));\\n- if (rejected.length > 0) {\\n- throw new InvalidConfigError(\\n- \\\"DEFAULT_CAP_ADD\\\",\\n- `disallowed capabilities: ${rejected.join(\\\", \\\")}. Allowed: ${[...ALLOWED_CAPABILITIES].join(\\\", \\\")}`,\\n- );\\n- }\\n- return raw;\\n- })(),\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n },\\n },\\ndiff --git a/apps/orchestrator/src/config/container-capabilities.ts b/apps/orchestrator/src/config/container-capabilities.ts\\nnew file mode 100644\\nindex 0000000..1a0fbc7\\n--- /dev/null\\n+++ b/apps/orchestrator/src/config/container-capabilities.ts\\n@@ -0,0 +1,40 @@\\n+import { InvalidConfigError } from \\\"@repo/shared\\\";\\n+\\n+const ALLOWED_CONTAINER_CAPABILITIES = new Set([\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"NET_BIND_SERVICE\\\",\\n+ \\\"CHOWN\\\",\\n+ \\\"DAC_OVERRIDE\\\",\\n+ \\\"FOWNER\\\",\\n+ \\\"SETGID\\\",\\n+ \\\"SETUID\\\",\\n+]);\\n+\\n+export const DEFAULT_CONTAINER_CAPABILITIES = [\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"SETUID\\\",\\n+ \\\"SETGID\\\",\\n+] as const;\\n+\\n+export function resolveDefaultContainerCapabilities(\\n+ env: NodeJS.ProcessEnv = process.env,\\n+): string[] {\\n+ const raw = env.DEFAULT_CAP_ADD ?? DEFAULT_CONTAINER_CAPABILITIES.join(\\\",\\\");\\n+ const capabilities = raw\\n+ .split(\\\",\\\")\\n+ .map((capability) => capability.trim())\\n+ .filter(Boolean);\\n+\\n+ const rejected = capabilities.filter(\\n+ (capability) => !ALLOWED_CONTAINER_CAPABILITIES.has(capability),\\n+ );\\n+\\n+ if (rejected.length > 0) {\\n+ throw new InvalidConfigError(\\n+ \\\"DEFAULT_CAP_ADD\\\",\\n+ `disallowed capabilities: ${rejected.join(\\\", \\\")}. Allowed: ${[...ALLOWED_CONTAINER_CAPABILITIES].join(\\\", \\\")}`,\\n+ );\\n+ }\\n+\\n+ return capabilities;\\n+}\\ndiff --git a/apps/orchestrator/src/driver/interface/index.ts b/apps/orchestrator/src/driver/interface/index.ts\\nindex 75e4677..a7a887d 100644\\n--- a/apps/orchestrator/src/driver/interface/index.ts\\n+++ b/apps/orchestrator/src/driver/interface/index.ts\\n@@ -60,6 +60,7 @@ const DEFAULT_PIDS_LIMIT = 1000; // 1000 processes for OpenCode + child processe\\n // - Model response buffering (~500MB for large contexts)\\n const MIN_MEMORY_LIMIT = 2048; // 2GB minimum\\n \\n+import { resolveDefaultContainerCapabilities } from \\\"../../config/container-capabilities.js\\\";\\n import {\\n type ContainerConfig,\\n ContainerConfigSchema,\\n@@ -108,7 +109,7 @@ export function createDriverConfig(\\n user: \\\"root\\\", // Default to root for compatibility with standard images\\n capabilities: {\\n drop: [\\\"ALL\\\"],\\n- add: [],\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n },\\n maxVolumeSizeBytes: config.maxVolumeSizeBytes,\\n@@ -143,7 +144,7 @@ export function createSecurityOptions(\\n user: options.user || \\\"root\\\", // Default to root for compatibility with standard images\\n capabilities: options.capabilities || {\\n drop: [\\\"ALL\\\"],\\n- add: [],\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n };\\n }\\ndiff --git a/apps/orchestrator/tests/unit/config.test.ts b/apps/orchestrator/tests/unit/config.test.ts\\nindex 020969b..6e30dbd 100644\\n--- a/apps/orchestrator/tests/unit/config.test.ts\\n+++ b/apps/orchestrator/tests/unit/config.test.ts\\n@@ -19,6 +19,50 @@ afterAll(() => {\\n });\\n \\n describe(\\\"config\\\", () => {\\n+ it(\\\"defaults sidecar container capabilities to subprocess-safe caps\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"SETUID\\\",\\n+ \\\"SETGID\\\",\\n+ ]);\\n+ });\\n+\\n+ it(\\\"honors explicit DEFAULT_CAP_ADD overrides\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_PTRACE\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([\\n+ \\\"SYS_PTRACE\\\",\\n+ ]);\\n+ });\\n+\\n+ it(\\\"allows hardened deployments to disable default capabilities\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([]);\\n+ });\\n+\\n+ it(\\\"rejects invalid DEFAULT_CAP_ADD values\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_ADMIN\\\",\\n+ });\\n+ vi.resetModules();\\n+\\n+ await expect(import(\\\"../../src/config\\\")).rejects.toThrowError(\\n+ /DEFAULT_CAP_ADD/,\\n+ );\\n+ });\\n+\\n it(\\\"provides Pangolin defaults\\\", async () => {\\n setEnv({\\n PANGOLIN_ENABLED: \\\"true\\\",\\n@@ -64,4 +108,64 @@ describe(\\\"config\\\", () => {\\n \\\"Missing required configuration: PANGOLIN_API_KEY\\\",\\n );\\n });\\n+\\n+ it(\\\"uses subprocess-safe capability defaults when security options omit caps\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { createSecurityOptions } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(createSecurityOptions({}).capabilities).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"],\\n+ });\\n+ });\\n+\\n+ it(\\\"uses env-configured capabilities in security helpers\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_PTRACE\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { createDriverConfig, createSecurityOptions } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(createSecurityOptions({}).capabilities).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\"],\\n+ });\\n+\\n+ expect(\\n+ createDriverConfig({\\n+ image: \\\"test-image\\\",\\n+ sessionId: \\\"session-1\\\",\\n+ }).security.capabilities,\\n+ ).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\"],\\n+ });\\n+ });\\n+\\n+ it(\\\"uses subprocess-safe capability defaults in createDriverConfig\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { createDriverConfig } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(\\n+ createDriverConfig({\\n+ image: \\\"test-image\\\",\\n+ sessionId: \\\"session-1\\\",\\n+ }).security.capabilities,\\n+ ).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"],\\n+ });\\n+ });\\n });\\n\"\n ],\n \"error\": null,\n \"provider\": \"claude\",\n \"result\": {\n \"confidence_notes\": [\n \"I'm confident the `??` vs `||` change is intentional \\u2014 the empty-string test explicitly validates the new behavior. The old `||` would have treated `DEFAULT_CAP_ADD=\\\"\\\"` as falsy and fallen through to the default; the new `??` correctly treats it as an explicit opt-out.\",\n \"I'm less certain whether the driver interface defaulting to `resolveDefaultContainerCapabilities()` instead of `add: []` was a deliberate security decision or an unintended side-effect of the refactor. The test coverage suggests it's deliberate, but the prior `add: []` default in the driver interface was a more restrictive posture.\"\n ],\n \"findings\": [\n {\n \"body\": \"In `config.ts`, `resolveDefaultContainerCapabilities()` is called at module load time (it's inside a module-level `const config = { ... }` initializer). In `driver/interface/index.ts`, it's called inside `createDriverConfig()` (line 112) and `createSecurityOptions()` (line 147) \\u2014 meaning it runs on every invocation when no capabilities are provided.\\n\\nThis creates two issues:\\n1. If the driver interface is used without importing `config.ts` (e.g., in tests, or a future entry point), an invalid `DEFAULT_CAP_ADD` value won't fail at startup \\u2014 it'll throw `InvalidConfigError` at container creation time, potentially mid-operation.\\n2. Each call re-parses and re-validates the env var. While negligible for performance, it means the resolved capabilities aren't guaranteed to be consistent across calls if `process.env` is mutated between invocations.\\n\\nNeither is likely to cause production issues today since the orchestrator imports `config.ts` at startup (catching bad values early), but it's a latent inconsistency worth noting.\",\n \"category\": \"correctness\",\n \"confidence\": \"medium\",\n \"evidence\": \"config.ts line 410: `add: resolveDefaultContainerCapabilities()` \\u2014 called once at import.\\ndriver/interface/index.ts line 112: `add: resolveDefaultContainerCapabilities()` \\u2014 called per `createDriverConfig()` invocation.\\ndriver/interface/index.ts line 147: `add: resolveDefaultContainerCapabilities()` \\u2014 called per `createSecurityOptions()` invocation.\",\n \"file\": \"apps/orchestrator/src/driver/interface/index.ts\",\n \"line\": 112,\n \"severity\": \"medium\",\n \"title\": \"resolveDefaultContainerCapabilities() evaluated lazily in driver interface but eagerly in config.ts\"\n }\n ],\n \"questions\": [\n \"Was the broadening of defaults from `[SYS_PTRACE]` to `[SYS_PTRACE, SETUID, SETGID]` intended to apply to the driver interface fallback path too? Previously `createDriverConfig` and `createSecurityOptions` defaulted to `add: []` (no caps at all), so this PR changes their behavior significantly \\u2014 containers created through the driver interface now get SETUID/SETGID caps by default where they previously got none.\"\n ],\n \"status\": \"ok\",\n \"summary\": \"The extracted `resolveDefaultContainerCapabilities()` correctly implements the intended policy: `??` (nullish coalescing) distinguishes `undefined` (use defaults) from empty string (disable all caps), the allowed-list is enforced with a clear `InvalidConfigError`, and tests cover all four env semantics (unset, override, empty-disable, invalid-reject). The default set is intentionally broadened from `[SYS_PTRACE]` to `[SYS_PTRACE, SETUID, SETGID]` to support sidecar subprocesses, consistent with the prior commit 952c0f9. One medium finding on the lazy-vs-eager evaluation split between config.ts and the driver interface.\",\n \"track_id\": \"capability-policy-and-env-semantics\"\n },\n \"status\": \"ok\",\n \"track\": {\n \"evidence_targets\": [\n \"resolveDefaultContainerCapabilities() parsing of undefined vs empty string vs comma-separated values\",\n \"ALLOWED_CONTAINER_CAPABILITIES and whether the new default set is intentionally broader than before\",\n \"InvalidConfigError path and whether bad values fail at config load consistently\",\n \"config.defaults.container.security.capabilities.add import-time behavior after vi.resetModules()\",\n \"tests covering disable-by-empty-string and rejection of unsupported capabilities\"\n ],\n \"goal\": \"Verify the shared capability resolver preserves the intended policy: allowed list enforcement, correct defaults, and explicit hardening via empty `DEFAULT_CAP_ADD` without accidental fallback.\",\n \"scope\": [\n \"apps/orchestrator/src/config.ts\",\n \"apps/orchestrator/src/config/container-capabilities.ts\",\n \"apps/orchestrator/tests/unit/config.test.ts\"\n ],\n \"should_use_subagents\": false,\n \"suggested_provider\": \"gpt-5\",\n \"track_id\": \"capability-policy-and-env-semantics\"\n }\n },\n {\n \"command\": [\n \"codex\",\n \"exec\",\n \"--full-auto\",\n \"You are a senior engineer reviewing one assigned track of a code change. You are thorough but you respect the reader's time \\u2014 every finding you report should be worth someone stopping to look at.\\n\\nYou own only your assigned track. Stay in scope. Do not report on files or concerns outside your track's scope unless you find something genuinely dangerous.\\n\\n## How to review\\n\\nRead the evidence in your track scope. Think about what could go wrong in production, what would break silently, what a test wouldn't catch. Look at error paths, edge cases, and implicit assumptions.\\n\\nUse sub-agents when it helps \\u2014 for example, to check test coverage while you're reviewing implementation logic. But don't use them just because you can.\\n\\n## What makes a good finding\\n\\nA good finding names a specific file and line, explains what's wrong, and shows evidence. \\\"This nil check on line 47 of auth.go doesn't cover the case where the token is expired but structurally valid \\u2014 the downstream handler will panic\\\" is a finding. \\\"Consider adding error handling\\\" is not.\\n\\nAsk yourself: would a staff engineer read this finding and say \\\"good catch\\\" or \\\"obvious filler\\\"? Only report the former.\\n\\n## What to avoid\\n\\n- Findings about style, naming, or formatting unless they create actual confusion\\n- \\\"Consider adding tests for X\\\" without explaining what specific behavior is untested and why it matters\\n- Speculative findings without code evidence \\u2014 if you can't point to the line, don't report it\\n- Duplicating findings that clearly belong to another track\\n\\n## Confidence\\n\\nBe honest about what you're sure about versus what you suspect. Flag uncertainty in `confidence_notes` rather than inflating finding confidence.\\n\\n## Output\\n\\nReturn JSON only, no markdown fences.\\n\\n```\\n{\\n \\\"status\\\": \\\"ok|error\\\",\\n \\\"track_id\\\": \\\"from the assigned track\\\",\\n \\\"summary\\\": \\\"what you found, in one paragraph\\\",\\n \\\"findings\\\": [\\n {\\n \\\"severity\\\": \\\"high|medium|low\\\",\\n \\\"confidence\\\": \\\"high|medium|low\\\",\\n \\\"category\\\": \\\"correctness|security|regression|testing|operational\\\",\\n \\\"title\\\": \\\"short, specific title\\\",\\n \\\"body\\\": \\\"what's wrong, why it matters, what evidence you found\\\",\\n \\\"file\\\": \\\"path/to/file\\\",\\n \\\"line\\\": 0,\\n \\\"evidence\\\": \\\"the actual code or behavior that demonstrates the issue\\\"\\n }\\n ],\\n \\\"questions\\\": [\\\"things you want to ask the author \\u2014 genuine questions, not passive-aggressive suggestions\\\"],\\n \\\"confidence_notes\\\": [\\\"where you're uncertain and why\\\"]\\n}\\n```\\n\\n## Request\\n\\n{\\n \\\"changed_files\\\": [\\n \\\"apps/orchestrator/src/config.ts\\\",\\n \\\"apps/orchestrator/src/config/container-capabilities.ts\\\",\\n \\\"apps/orchestrator/src/driver/interface/index.ts\\\",\\n \\\"apps/orchestrator/tests/unit/config.test.ts\\\"\\n ],\\n \\\"pr\\\": 386,\\n \\\"repo\\\": \\\"tangle-network/agent-dev-container\\\",\\n \\\"track\\\": {\\n \\\"evidence_targets\\\": [\\n \\\"createDriverConfig() default security.capabilities behavior before vs after this PR\\\",\\n \\\"createSecurityOptions() behavior when callers omit capabilities entirely\\\",\\n \\\"behavior when callers pass partial security options or explicit empty capability arrays/objects\\\",\\n \\\"all call sites that rely on driver defaults rather than config.defaults.container.security\\\",\\n \\\"tests proving helpers honor env overrides and do not overwrite explicit caller-provided capabilities\\\"\\n ],\\n \\\"goal\\\": \\\"Check whether changing driver/security helper defaults from no added capabilities to subprocess-safe capabilities introduces privilege expansion or breaks caller override expectations.\\\",\\n \\\"scope\\\": [\\n \\\"apps/orchestrator/src/driver/interface/index.ts\\\",\\n \\\"apps/orchestrator/tests/unit/config.test.ts\\\"\\n ],\\n \\\"should_use_subagents\\\": false,\\n \\\"suggested_provider\\\": \\\"claude-sonnet\\\",\\n \\\"track_id\\\": \\\"driver-defaults-and-override-regressions\\\"\\n }\\n}\\n\\n## Diff\\n\\ndiff --git a/apps/orchestrator/src/config.ts b/apps/orchestrator/src/config.ts\\nindex 46b33e6..74de0b9 100644\\n--- a/apps/orchestrator/src/config.ts\\n+++ b/apps/orchestrator/src/config.ts\\n@@ -10,6 +10,7 @@ import {\\n validateTlsClientConfig,\\n validateTlsServerConfig,\\n } from \\\"@repo/shared\\\";\\n+import { resolveDefaultContainerCapabilities } from \\\"./config/container-capabilities.js\\\";\\n import {\\n DEFAULT_POOL_CONFIG,\\n type PoolConfig,\\n@@ -406,31 +407,7 @@ export const config = {\\n user: process.env.DEFAULT_CONTAINER_USER || \\\"root\\\",\\n capabilities: {\\n drop: [\\\"ALL\\\"] as string[],\\n- // SYS_PTRACE required for Bun runtime (opencode binary crashes without it).\\n- // Configurable via env var to allow disabling for hardened deployments.\\n- add: (() => {\\n- const ALLOWED_CAPABILITIES = new Set([\\n- \\\"SYS_PTRACE\\\",\\n- \\\"NET_BIND_SERVICE\\\",\\n- \\\"CHOWN\\\",\\n- \\\"DAC_OVERRIDE\\\",\\n- \\\"FOWNER\\\",\\n- \\\"SETGID\\\",\\n- \\\"SETUID\\\",\\n- ]);\\n- const raw = (process.env.DEFAULT_CAP_ADD || \\\"SYS_PTRACE\\\")\\n- .split(\\\",\\\")\\n- .map((c) => c.trim())\\n- .filter(Boolean);\\n- const rejected = raw.filter((c) => !ALLOWED_CAPABILITIES.has(c));\\n- if (rejected.length > 0) {\\n- throw new InvalidConfigError(\\n- \\\"DEFAULT_CAP_ADD\\\",\\n- `disallowed capabilities: ${rejected.join(\\\", \\\")}. Allowed: ${[...ALLOWED_CAPABILITIES].join(\\\", \\\")}`,\\n- );\\n- }\\n- return raw;\\n- })(),\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n },\\n },\\ndiff --git a/apps/orchestrator/src/config/container-capabilities.ts b/apps/orchestrator/src/config/container-capabilities.ts\\nnew file mode 100644\\nindex 0000000..1a0fbc7\\n--- /dev/null\\n+++ b/apps/orchestrator/src/config/container-capabilities.ts\\n@@ -0,0 +1,40 @@\\n+import { InvalidConfigError } from \\\"@repo/shared\\\";\\n+\\n+const ALLOWED_CONTAINER_CAPABILITIES = new Set([\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"NET_BIND_SERVICE\\\",\\n+ \\\"CHOWN\\\",\\n+ \\\"DAC_OVERRIDE\\\",\\n+ \\\"FOWNER\\\",\\n+ \\\"SETGID\\\",\\n+ \\\"SETUID\\\",\\n+]);\\n+\\n+export const DEFAULT_CONTAINER_CAPABILITIES = [\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"SETUID\\\",\\n+ \\\"SETGID\\\",\\n+] as const;\\n+\\n+export function resolveDefaultContainerCapabilities(\\n+ env: NodeJS.ProcessEnv = process.env,\\n+): string[] {\\n+ const raw = env.DEFAULT_CAP_ADD ?? DEFAULT_CONTAINER_CAPABILITIES.join(\\\",\\\");\\n+ const capabilities = raw\\n+ .split(\\\",\\\")\\n+ .map((capability) => capability.trim())\\n+ .filter(Boolean);\\n+\\n+ const rejected = capabilities.filter(\\n+ (capability) => !ALLOWED_CONTAINER_CAPABILITIES.has(capability),\\n+ );\\n+\\n+ if (rejected.length > 0) {\\n+ throw new InvalidConfigError(\\n+ \\\"DEFAULT_CAP_ADD\\\",\\n+ `disallowed capabilities: ${rejected.join(\\\", \\\")}. Allowed: ${[...ALLOWED_CONTAINER_CAPABILITIES].join(\\\", \\\")}`,\\n+ );\\n+ }\\n+\\n+ return capabilities;\\n+}\\ndiff --git a/apps/orchestrator/src/driver/interface/index.ts b/apps/orchestrator/src/driver/interface/index.ts\\nindex 75e4677..a7a887d 100644\\n--- a/apps/orchestrator/src/driver/interface/index.ts\\n+++ b/apps/orchestrator/src/driver/interface/index.ts\\n@@ -60,6 +60,7 @@ const DEFAULT_PIDS_LIMIT = 1000; // 1000 processes for OpenCode + child processe\\n // - Model response buffering (~500MB for large contexts)\\n const MIN_MEMORY_LIMIT = 2048; // 2GB minimum\\n \\n+import { resolveDefaultContainerCapabilities } from \\\"../../config/container-capabilities.js\\\";\\n import {\\n type ContainerConfig,\\n ContainerConfigSchema,\\n@@ -108,7 +109,7 @@ export function createDriverConfig(\\n user: \\\"root\\\", // Default to root for compatibility with standard images\\n capabilities: {\\n drop: [\\\"ALL\\\"],\\n- add: [],\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n },\\n maxVolumeSizeBytes: config.maxVolumeSizeBytes,\\n@@ -143,7 +144,7 @@ export function createSecurityOptions(\\n user: options.user || \\\"root\\\", // Default to root for compatibility with standard images\\n capabilities: options.capabilities || {\\n drop: [\\\"ALL\\\"],\\n- add: [],\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n };\\n }\\ndiff --git a/apps/orchestrator/tests/unit/config.test.ts b/apps/orchestrator/tests/unit/config.test.ts\\nindex 020969b..6e30dbd 100644\\n--- a/apps/orchestrator/tests/unit/config.test.ts\\n+++ b/apps/orchestrator/tests/unit/config.test.ts\\n@@ -19,6 +19,50 @@ afterAll(() => {\\n });\\n \\n describe(\\\"config\\\", () => {\\n+ it(\\\"defaults sidecar container capabilities to subprocess-safe caps\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"SETUID\\\",\\n+ \\\"SETGID\\\",\\n+ ]);\\n+ });\\n+\\n+ it(\\\"honors explicit DEFAULT_CAP_ADD overrides\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_PTRACE\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([\\n+ \\\"SYS_PTRACE\\\",\\n+ ]);\\n+ });\\n+\\n+ it(\\\"allows hardened deployments to disable default capabilities\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([]);\\n+ });\\n+\\n+ it(\\\"rejects invalid DEFAULT_CAP_ADD values\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_ADMIN\\\",\\n+ });\\n+ vi.resetModules();\\n+\\n+ await expect(import(\\\"../../src/config\\\")).rejects.toThrowError(\\n+ /DEFAULT_CAP_ADD/,\\n+ );\\n+ });\\n+\\n it(\\\"provides Pangolin defaults\\\", async () => {\\n setEnv({\\n PANGOLIN_ENABLED: \\\"true\\\",\\n@@ -64,4 +108,64 @@ describe(\\\"config\\\", () => {\\n \\\"Missing required configuration: PANGOLIN_API_KEY\\\",\\n );\\n });\\n+\\n+ it(\\\"uses subprocess-safe capability defaults when security options omit caps\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { createSecurityOptions } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(createSecurityOptions({}).capabilities).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"],\\n+ });\\n+ });\\n+\\n+ it(\\\"uses env-configured capabilities in security helpers\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_PTRACE\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { createDriverConfig, createSecurityOptions } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(createSecurityOptions({}).capabilities).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\"],\\n+ });\\n+\\n+ expect(\\n+ createDriverConfig({\\n+ image: \\\"test-image\\\",\\n+ sessionId: \\\"session-1\\\",\\n+ }).security.capabilities,\\n+ ).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\"],\\n+ });\\n+ });\\n+\\n+ it(\\\"uses subprocess-safe capability defaults in createDriverConfig\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { createDriverConfig } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(\\n+ createDriverConfig({\\n+ image: \\\"test-image\\\",\\n+ sessionId: \\\"session-1\\\",\\n+ }).security.capabilities,\\n+ ).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"],\\n+ });\\n+ });\\n });\\n\"\n ],\n \"error\": null,\n \"provider\": \"codex\",\n \"result\": {\n \"confidence_notes\": [\n \"I could not run the Vitest file in this checkout because `node_modules` are absent (`vitest: not found`), so the review is based on static inspection.\",\n \"I am confident about the behavioral change in the helper itself; the only uncertainty is whether the privilege expansion for partial security overrides was intentional policy or an unintended side effect.\"\n ],\n \"findings\": [\n {\n \"body\": \"`createSecurityOptions()` accepts `Partial<SecurityOptions>`, so callers are allowed to override a subset of security fields and rely on helper defaults for the rest. After this change, omitting `options.capabilities` no longer means \\\"no added capabilities\\\"; line 145 now injects `resolveDefaultContainerCapabilities()`, which expands privileges to `SYS_PTRACE`, `SETUID`, and `SETGID`. In production, any caller that only wanted to set `readOnly`, `noNewPrivileges`, or `user` will now get those extra capabilities unless it also remembers to send an explicit empty `capabilities.add`. The new tests reinforce this behavior by asserting that `createSecurityOptions({})` returns those capabilities, but they never cover the safer partial-override case.\",\n \"category\": \"security\",\n \"confidence\": \"high\",\n \"evidence\": \"`capabilities: options.capabilities || { drop: [\\\"ALL\\\"], add: resolveDefaultContainerCapabilities(), }` at `createSecurityOptions()`, plus `apps/orchestrator/tests/unit/config.test.ts:112-149` asserting `createSecurityOptions({}).capabilities.add` becomes `[\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"]` or env-driven equivalents.\",\n \"file\": \"apps/orchestrator/src/driver/interface/index.ts\",\n \"line\": 145,\n \"severity\": \"high\",\n \"title\": \"Partial security overrides now implicitly add three Linux capabilities\"\n }\n ],\n \"questions\": [\n \"Should `createSecurityOptions()` only use subprocess-safe capabilities when constructing a full default container config, rather than whenever a caller omits `capabilities` from a partial security override?\"\n ],\n \"status\": \"ok\",\n \"summary\": \"I found one material regression in scope: `createSecurityOptions()` now silently grants `SYS_PTRACE`, `SETUID`, and `SETGID` whenever a caller provides a partial security object without explicitly setting `capabilities`. That is a privilege expansion relative to the previous helper behavior, and the new unit tests codify that expanded behavior instead of guarding against it. I did not find a separate issue with explicit empty capability arrays; the current helper still preserves those when a full `capabilities` object is passed.\",\n \"track_id\": \"driver-defaults-and-override-regressions\"\n },\n \"status\": \"ok\",\n \"track\": {\n \"evidence_targets\": [\n \"createDriverConfig() default security.capabilities behavior before vs after this PR\",\n \"createSecurityOptions() behavior when callers omit capabilities entirely\",\n \"behavior when callers pass partial security options or explicit empty capability arrays/objects\",\n \"all call sites that rely on driver defaults rather than config.defaults.container.security\",\n \"tests proving helpers honor env overrides and do not overwrite explicit caller-provided capabilities\"\n ],\n \"goal\": \"Check whether changing driver/security helper defaults from no added capabilities to subprocess-safe capabilities introduces privilege expansion or breaks caller override expectations.\",\n \"scope\": [\n \"apps/orchestrator/src/driver/interface/index.ts\",\n \"apps/orchestrator/tests/unit/config.test.ts\"\n ],\n \"should_use_subagents\": false,\n \"suggested_provider\": \"claude-sonnet\",\n \"track_id\": \"driver-defaults-and-override-regressions\"\n }\n },\n {\n \"command\": [\n \"claude\",\n \"--dangerously-skip-permissions\",\n \"-p\",\n \"You are a senior engineer reviewing one assigned track of a code change. You are thorough but you respect the reader's time \\u2014 every finding you report should be worth someone stopping to look at.\\n\\nYou own only your assigned track. Stay in scope. Do not report on files or concerns outside your track's scope unless you find something genuinely dangerous.\\n\\n## How to review\\n\\nRead the evidence in your track scope. Think about what could go wrong in production, what would break silently, what a test wouldn't catch. Look at error paths, edge cases, and implicit assumptions.\\n\\nUse sub-agents when it helps \\u2014 for example, to check test coverage while you're reviewing implementation logic. But don't use them just because you can.\\n\\n## What makes a good finding\\n\\nA good finding names a specific file and line, explains what's wrong, and shows evidence. \\\"This nil check on line 47 of auth.go doesn't cover the case where the token is expired but structurally valid \\u2014 the downstream handler will panic\\\" is a finding. \\\"Consider adding error handling\\\" is not.\\n\\nAsk yourself: would a staff engineer read this finding and say \\\"good catch\\\" or \\\"obvious filler\\\"? Only report the former.\\n\\n## What to avoid\\n\\n- Findings about style, naming, or formatting unless they create actual confusion\\n- \\\"Consider adding tests for X\\\" without explaining what specific behavior is untested and why it matters\\n- Speculative findings without code evidence \\u2014 if you can't point to the line, don't report it\\n- Duplicating findings that clearly belong to another track\\n\\n## Confidence\\n\\nBe honest about what you're sure about versus what you suspect. Flag uncertainty in `confidence_notes` rather than inflating finding confidence.\\n\\n## Output\\n\\nReturn JSON only, no markdown fences.\\n\\n```\\n{\\n \\\"status\\\": \\\"ok|error\\\",\\n \\\"track_id\\\": \\\"from the assigned track\\\",\\n \\\"summary\\\": \\\"what you found, in one paragraph\\\",\\n \\\"findings\\\": [\\n {\\n \\\"severity\\\": \\\"high|medium|low\\\",\\n \\\"confidence\\\": \\\"high|medium|low\\\",\\n \\\"category\\\": \\\"correctness|security|regression|testing|operational\\\",\\n \\\"title\\\": \\\"short, specific title\\\",\\n \\\"body\\\": \\\"what's wrong, why it matters, what evidence you found\\\",\\n \\\"file\\\": \\\"path/to/file\\\",\\n \\\"line\\\": 0,\\n \\\"evidence\\\": \\\"the actual code or behavior that demonstrates the issue\\\"\\n }\\n ],\\n \\\"questions\\\": [\\\"things you want to ask the author \\u2014 genuine questions, not passive-aggressive suggestions\\\"],\\n \\\"confidence_notes\\\": [\\\"where you're uncertain and why\\\"]\\n}\\n```\\n\\n## Request\\n\\n{\\n \\\"changed_files\\\": [\\n \\\"apps/orchestrator/src/config.ts\\\",\\n \\\"apps/orchestrator/src/config/container-capabilities.ts\\\",\\n \\\"apps/orchestrator/src/driver/interface/index.ts\\\",\\n \\\"apps/orchestrator/tests/unit/config.test.ts\\\"\\n ],\\n \\\"pr\\\": 386,\\n \\\"repo\\\": \\\"tangle-network/agent-dev-container\\\",\\n \\\"track\\\": {\\n \\\"evidence_targets\\\": [\\n \\\"createDriverConfig() default security.capabilities behavior before vs after this PR\\\",\\n \\\"createSecurityOptions() behavior when callers omit capabilities entirely\\\",\\n \\\"behavior when callers pass partial security options or explicit empty capability arrays/objects\\\",\\n \\\"all call sites that rely on driver defaults rather than config.defaults.container.security\\\",\\n \\\"tests proving helpers honor env overrides and do not overwrite explicit caller-provided capabilities\\\"\\n ],\\n \\\"goal\\\": \\\"Check whether changing driver/security helper defaults from no added capabilities to subprocess-safe capabilities introduces privilege expansion or breaks caller override expectations.\\\",\\n \\\"scope\\\": [\\n \\\"apps/orchestrator/src/driver/interface/index.ts\\\",\\n \\\"apps/orchestrator/tests/unit/config.test.ts\\\"\\n ],\\n \\\"should_use_subagents\\\": false,\\n \\\"suggested_provider\\\": \\\"claude-sonnet\\\",\\n \\\"track_id\\\": \\\"driver-defaults-and-override-regressions\\\"\\n }\\n}\\n\\n## Diff\\n\\ndiff --git a/apps/orchestrator/src/config.ts b/apps/orchestrator/src/config.ts\\nindex 46b33e6..74de0b9 100644\\n--- a/apps/orchestrator/src/config.ts\\n+++ b/apps/orchestrator/src/config.ts\\n@@ -10,6 +10,7 @@ import {\\n validateTlsClientConfig,\\n validateTlsServerConfig,\\n } from \\\"@repo/shared\\\";\\n+import { resolveDefaultContainerCapabilities } from \\\"./config/container-capabilities.js\\\";\\n import {\\n DEFAULT_POOL_CONFIG,\\n type PoolConfig,\\n@@ -406,31 +407,7 @@ export const config = {\\n user: process.env.DEFAULT_CONTAINER_USER || \\\"root\\\",\\n capabilities: {\\n drop: [\\\"ALL\\\"] as string[],\\n- // SYS_PTRACE required for Bun runtime (opencode binary crashes without it).\\n- // Configurable via env var to allow disabling for hardened deployments.\\n- add: (() => {\\n- const ALLOWED_CAPABILITIES = new Set([\\n- \\\"SYS_PTRACE\\\",\\n- \\\"NET_BIND_SERVICE\\\",\\n- \\\"CHOWN\\\",\\n- \\\"DAC_OVERRIDE\\\",\\n- \\\"FOWNER\\\",\\n- \\\"SETGID\\\",\\n- \\\"SETUID\\\",\\n- ]);\\n- const raw = (process.env.DEFAULT_CAP_ADD || \\\"SYS_PTRACE\\\")\\n- .split(\\\",\\\")\\n- .map((c) => c.trim())\\n- .filter(Boolean);\\n- const rejected = raw.filter((c) => !ALLOWED_CAPABILITIES.has(c));\\n- if (rejected.length > 0) {\\n- throw new InvalidConfigError(\\n- \\\"DEFAULT_CAP_ADD\\\",\\n- `disallowed capabilities: ${rejected.join(\\\", \\\")}. Allowed: ${[...ALLOWED_CAPABILITIES].join(\\\", \\\")}`,\\n- );\\n- }\\n- return raw;\\n- })(),\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n },\\n },\\ndiff --git a/apps/orchestrator/src/config/container-capabilities.ts b/apps/orchestrator/src/config/container-capabilities.ts\\nnew file mode 100644\\nindex 0000000..1a0fbc7\\n--- /dev/null\\n+++ b/apps/orchestrator/src/config/container-capabilities.ts\\n@@ -0,0 +1,40 @@\\n+import { InvalidConfigError } from \\\"@repo/shared\\\";\\n+\\n+const ALLOWED_CONTAINER_CAPABILITIES = new Set([\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"NET_BIND_SERVICE\\\",\\n+ \\\"CHOWN\\\",\\n+ \\\"DAC_OVERRIDE\\\",\\n+ \\\"FOWNER\\\",\\n+ \\\"SETGID\\\",\\n+ \\\"SETUID\\\",\\n+]);\\n+\\n+export const DEFAULT_CONTAINER_CAPABILITIES = [\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"SETUID\\\",\\n+ \\\"SETGID\\\",\\n+] as const;\\n+\\n+export function resolveDefaultContainerCapabilities(\\n+ env: NodeJS.ProcessEnv = process.env,\\n+): string[] {\\n+ const raw = env.DEFAULT_CAP_ADD ?? DEFAULT_CONTAINER_CAPABILITIES.join(\\\",\\\");\\n+ const capabilities = raw\\n+ .split(\\\",\\\")\\n+ .map((capability) => capability.trim())\\n+ .filter(Boolean);\\n+\\n+ const rejected = capabilities.filter(\\n+ (capability) => !ALLOWED_CONTAINER_CAPABILITIES.has(capability),\\n+ );\\n+\\n+ if (rejected.length > 0) {\\n+ throw new InvalidConfigError(\\n+ \\\"DEFAULT_CAP_ADD\\\",\\n+ `disallowed capabilities: ${rejected.join(\\\", \\\")}. Allowed: ${[...ALLOWED_CONTAINER_CAPABILITIES].join(\\\", \\\")}`,\\n+ );\\n+ }\\n+\\n+ return capabilities;\\n+}\\ndiff --git a/apps/orchestrator/src/driver/interface/index.ts b/apps/orchestrator/src/driver/interface/index.ts\\nindex 75e4677..a7a887d 100644\\n--- a/apps/orchestrator/src/driver/interface/index.ts\\n+++ b/apps/orchestrator/src/driver/interface/index.ts\\n@@ -60,6 +60,7 @@ const DEFAULT_PIDS_LIMIT = 1000; // 1000 processes for OpenCode + child processe\\n // - Model response buffering (~500MB for large contexts)\\n const MIN_MEMORY_LIMIT = 2048; // 2GB minimum\\n \\n+import { resolveDefaultContainerCapabilities } from \\\"../../config/container-capabilities.js\\\";\\n import {\\n type ContainerConfig,\\n ContainerConfigSchema,\\n@@ -108,7 +109,7 @@ export function createDriverConfig(\\n user: \\\"root\\\", // Default to root for compatibility with standard images\\n capabilities: {\\n drop: [\\\"ALL\\\"],\\n- add: [],\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n },\\n maxVolumeSizeBytes: config.maxVolumeSizeBytes,\\n@@ -143,7 +144,7 @@ export function createSecurityOptions(\\n user: options.user || \\\"root\\\", // Default to root for compatibility with standard images\\n capabilities: options.capabilities || {\\n drop: [\\\"ALL\\\"],\\n- add: [],\\n+ add: resolveDefaultContainerCapabilities(),\\n },\\n };\\n }\\ndiff --git a/apps/orchestrator/tests/unit/config.test.ts b/apps/orchestrator/tests/unit/config.test.ts\\nindex 020969b..6e30dbd 100644\\n--- a/apps/orchestrator/tests/unit/config.test.ts\\n+++ b/apps/orchestrator/tests/unit/config.test.ts\\n@@ -19,6 +19,50 @@ afterAll(() => {\\n });\\n \\n describe(\\\"config\\\", () => {\\n+ it(\\\"defaults sidecar container capabilities to subprocess-safe caps\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([\\n+ \\\"SYS_PTRACE\\\",\\n+ \\\"SETUID\\\",\\n+ \\\"SETGID\\\",\\n+ ]);\\n+ });\\n+\\n+ it(\\\"honors explicit DEFAULT_CAP_ADD overrides\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_PTRACE\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([\\n+ \\\"SYS_PTRACE\\\",\\n+ ]);\\n+ });\\n+\\n+ it(\\\"allows hardened deployments to disable default capabilities\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { config } = await import(\\\"../../src/config\\\");\\n+ expect(config.defaults.container.security.capabilities.add).toEqual([]);\\n+ });\\n+\\n+ it(\\\"rejects invalid DEFAULT_CAP_ADD values\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_ADMIN\\\",\\n+ });\\n+ vi.resetModules();\\n+\\n+ await expect(import(\\\"../../src/config\\\")).rejects.toThrowError(\\n+ /DEFAULT_CAP_ADD/,\\n+ );\\n+ });\\n+\\n it(\\\"provides Pangolin defaults\\\", async () => {\\n setEnv({\\n PANGOLIN_ENABLED: \\\"true\\\",\\n@@ -64,4 +108,64 @@ describe(\\\"config\\\", () => {\\n \\\"Missing required configuration: PANGOLIN_API_KEY\\\",\\n );\\n });\\n+\\n+ it(\\\"uses subprocess-safe capability defaults when security options omit caps\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { createSecurityOptions } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(createSecurityOptions({}).capabilities).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"],\\n+ });\\n+ });\\n+\\n+ it(\\\"uses env-configured capabilities in security helpers\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: \\\"SYS_PTRACE\\\",\\n+ });\\n+ vi.resetModules();\\n+ const { createDriverConfig, createSecurityOptions } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(createSecurityOptions({}).capabilities).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\"],\\n+ });\\n+\\n+ expect(\\n+ createDriverConfig({\\n+ image: \\\"test-image\\\",\\n+ sessionId: \\\"session-1\\\",\\n+ }).security.capabilities,\\n+ ).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\"],\\n+ });\\n+ });\\n+\\n+ it(\\\"uses subprocess-safe capability defaults in createDriverConfig\\\", async () => {\\n+ setEnv({\\n+ DEFAULT_CAP_ADD: undefined,\\n+ });\\n+ vi.resetModules();\\n+ const { createDriverConfig } = await import(\\n+ \\\"../../src/driver/interface/index.js\\\"\\n+ );\\n+\\n+ expect(\\n+ createDriverConfig({\\n+ image: \\\"test-image\\\",\\n+ sessionId: \\\"session-1\\\",\\n+ }).security.capabilities,\\n+ ).toEqual({\\n+ drop: [\\\"ALL\\\"],\\n+ add: [\\\"SYS_PTRACE\\\", \\\"SETUID\\\", \\\"SETGID\\\"],\\n+ });\\n+ });\\n });\\n\"\n ],\n \"error\": null,\n \"provider\": \"claude\",\n \"result\": {\n \"confidence_notes\": [\n \"High confidence on the missing test finding \\u2014 verified via grep that no test passes explicit capabilities to either helper.\",\n \"Medium confidence on the evaluation-time drift finding \\u2014 it may be intentional design, but the inconsistency with config.ts is real and could cause subtle bugs if env is mutated at runtime.\"\n ],\n \"findings\": [\n {\n \"body\": \"Every test of createSecurityOptions passes an empty object ({}), and every test of createDriverConfig omits the security field entirely. This means there is no test asserting that when a caller provides explicit capabilities \\u2014 e.g. createSecurityOptions({ capabilities: { drop: ['ALL'], add: ['NET_BIND_SERVICE'] } }) \\u2014 those capabilities are returned unchanged rather than being replaced by the new subprocess-safe defaults. The only thing preventing the defaults from stomping caller-provided caps is the || operator on line 145 of index.ts (and line 106 for createDriverConfig). If someone refactors this to use nullish coalescing, deep-merge, or spread semantics, there is no test to catch the regression. Given that this PR specifically widens the default privilege set, a test proving caller overrides are respected is important to prevent unintentional privilege expansion for callers who deliberately restrict capabilities.\",\n \"category\": \"testing\",\n \"confidence\": \"high\",\n \"evidence\": \"All createSecurityOptions test calls use createSecurityOptions({}) \\u2014 grep for 'createSecurityOptions({' in tests returns only empty-object invocations. No createDriverConfig test passes an explicit security field.\",\n \"file\": \"apps/orchestrator/tests/unit/config.test.ts\",\n \"line\": 112,\n \"severity\": \"medium\",\n \"title\": \"No test verifies explicit caller-provided capabilities are preserved by createSecurityOptions or createDriverConfig\"\n },\n {\n \"body\": \"In createDriverConfig (line 112) and createSecurityOptions (line 147), resolveDefaultContainerCapabilities() is called on every invocation rather than being captured once at module load. This means if process.env.DEFAULT_CAP_ADD is mutated at runtime between two calls, the two containers would get different capability sets. In the config.ts path this is fine (evaluated once at module load), but in the driver helpers it's evaluated per-call. This is likely intentional for testability (the tests rely on vi.resetModules + env mutation), but it's worth noting that the config.ts and driver paths have subtly different evaluation semantics for the same function.\",\n \"category\": \"correctness\",\n \"confidence\": \"medium\",\n \"evidence\": \"config.ts calls resolveDefaultContainerCapabilities() once during module evaluation. createDriverConfig and createSecurityOptions call it on every invocation. If process.env.DEFAULT_CAP_ADD changes between calls, containers get different caps.\",\n \"file\": \"apps/orchestrator/src/driver/interface/index.ts\",\n \"line\": 112,\n \"severity\": \"low\",\n \"title\": \"resolveDefaultContainerCapabilities() is evaluated at call time, not module load \\u2014 capabilities can drift between calls\"\n }\n ],\n \"questions\": [\n \"Is the intent that createDriverConfig/createSecurityOptions should always reflect the current env at call time, or should they snapshot the capabilities once at startup like config.ts does? If the latter, the driver helpers should reference config.defaults.container.security.capabilities instead of re-resolving.\"\n ],\n \"status\": \"ok\",\n \"summary\": \"The change correctly aligns driver helper defaults with the config.ts defaults by using the shared resolveDefaultContainerCapabilities() function. The privilege expansion from empty add-caps to [SYS_PTRACE, SETUID, SETGID] is intentional and the security posture remains sound (noNewPrivileges:true neutralizes setuid binary escalation while still allowing the sidecar to demote via setuid(2)/setgid(2) syscalls). Env overrides are well-tested. However, there is no test proving that explicit caller-provided capabilities survive the helper defaults \\u2014 the only guard is an || operator that could be silently broken in a future refactor.\",\n \"track_id\": \"driver-defaults-and-override-regressions\"\n },\n \"status\": \"ok\",\n \"track\": {\n \"evidence_targets\": [\n \"createDriverConfig() default security.capabilities behavior before vs after this PR\",\n \"createSecurityOptions() behavior when callers omit capabilities entirely\",\n \"behavior when callers pass partial security options or explicit empty capability arrays/objects\",\n \"all call sites that rely on driver defaults rather than config.defaults.container.security\",\n \"tests proving helpers honor env overrides and do not overwrite explicit caller-provided capabilities\"\n ],\n \"goal\": \"Check whether changing driver/security helper defaults from no added capabilities to subprocess-safe capabilities introduces privilege expansion or breaks caller override expectations.\",\n \"scope\": [\n \"apps/orchestrator/src/driver/interface/index.ts\",\n \"apps/orchestrator/tests/unit/config.test.ts\"\n ],\n \"should_use_subagents\": false,\n \"suggested_provider\": \"claude-sonnet\",\n \"track_id\": \"driver-defaults-and-override-regressions\"\n }\n }\n ]\n}"
],
"result": {
"accepted_findings": [
{
"body": "Every test of createSecurityOptions passes an empty object, and every createDriverConfig test omits the security field. No test asserts that when a caller provides explicit capabilities (e.g. `{ capabilities: { drop: ['ALL'], add: ['NET_BIND_SERVICE'] } }`), those are returned unchanged. The only guard is the `||` operator on line 145. If someone refactors to nullish coalescing, spread, or deep-merge, there's no test to catch the regression. Given this PR widens the default privilege set, a test proving caller overrides are respected matters.",
"category": "testing",
"confidence": "high",
"evidence": "All createSecurityOptions test calls use createSecurityOptions({}) \u2014 no test passes explicit capabilities. No createDriverConfig test passes an explicit security.capabilities field.",
"file": "apps/orchestrator/tests/unit/config.test.ts",
"line": 112,
"severity": "medium",
"source_track": "driver-defaults-and-override-regressions (claude)",
"title": "No test verifies explicit caller-provided capabilities survive helper defaults"
},
{
"body": "config.ts evaluates resolveDefaultContainerCapabilities() once during module initialization. createDriverConfig (line 112) and createSecurityOptions (line 147) call it on every invocation. If process.env.DEFAULT_CAP_ADD is mutated at runtime between calls, containers get different capability sets. Likely intentional for testability, but the inconsistency means the driver path can produce results that disagree with config.defaults.container.security.capabilities.add at runtime.",
"category": "correctness",
"confidence": "medium",
"evidence": "config.ts line 410: called once at import. driver/interface/index.ts lines 112 and 147: called per function invocation.",
"file": "apps/orchestrator/src/driver/interface/index.ts",
"line": 112,
"severity": "low",
"source_track": "driver-defaults-and-override-regressions (claude)",
"title": "resolveDefaultContainerCapabilities() evaluated per-call in driver helpers but once-at-load in config.ts"
}
],
"coverage_gaps": [
"No track examined call sites of createDriverConfig/createSecurityOptions in the broader codebase to verify which callers rely on the old `add: []` default and whether the privilege expansion affects them in practice.",
"No track verified whether noNewPrivileges is set on the same containers receiving SETUID/SETGID caps \u2014 the interaction between these two settings determines real-world privilege escalation risk."
],
"coverage_score": 82,
"evidence_score": 78,
"recommendation": "ship \u2014 The behavioral change is intentional and well-tested for the happy path. The missing caller-override test is a real gap but doesn't block shipping; it should be added as a fast follow.",
"rejected_findings": [
{
"rejection_reason": "Frames the PR's intended behavior as a bug. The entire purpose of this change is to align driver helper defaults with config.ts defaults, adding SYS_PTRACE/SETUID/SETGID for sidecar subprocess support (per prior commit 952c0f9). Tests explicitly assert this new behavior. The valid sub-concern about missing caller-override tests is covered by the accepted testing finding.",
"source_track": "driver-defaults-and-override-regressions (codex)",
"title": "Partial security overrides now implicitly add three Linux capabilities"
},
{
"rejection_reason": "Duplicate of the stronger evaluation-time drift finding from the driver-defaults track, which provides more specific evidence about the consequence (containers getting different caps if env mutates).",
"source_track": "capability-policy-and-env-semantics (claude)",
"title": "resolveDefaultContainerCapabilities() evaluated lazily in driver interface but eagerly in config.ts"
}
],
"signal_score": 65,
"status": "ok"
}
}