fix(security): disposition all 104 code-scanning + dependabot alerts (#375)

Fix the 4 real CodeQL path-injection alerts (open_conventions_pr trusted the
API-settable project.workspace_path with no containment) plus defense-in-depth
segment validation at the get_workspace_path chokepoint. Bump next 16.1.1->16.1.7
and transitive lockfile deps to clear 24 Dependabot alerts. Close the intake
subagent-ban gap: the Claude intake driver still carried the Task tool and the
prompter prompt told it to fan out research subagents, contradicting the
fleet-wide ban. The remaining 47 CodeQL + 29 Dependabot alerts are dismissed on
GitHub with per-alert justifications (guard patterns CodeQL can't model across
call hops; next 16.2.x blocked by the verified tab-hostage router regression).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-10 00:15:04 +02:00
committed by GitHub
co-authored by Renn F
parent 0450ec9e89
commit 7be725cc13
12 changed files with 438 additions and 179 deletions
+2 -2
View File
@@ -31,7 +31,7 @@ Keep each unit to one concern. A unit that bundles several unrelated changes is
## Read first, then ask
Before your first question, use `Read` / `Grep` / `Glob` and the read-only git verbs to learn the real surface. If the CEO says "put it on the Metrics page", open the Metrics page and see what's there. If they mention an endpoint, find it. Spawn research subagents (`Task`) when the codebase is large. **Ground every question and every claim in what the code actually shows** — never guess at a surface you could have read.
Before your first question, use `Read` / `Grep` / `Glob` and the read-only git verbs to learn the real surface. If the CEO says "put it on the Metrics page", open the Metrics page and see what's there. If they mention an endpoint, find it. Read targeted excerpts yourself — you have no subagents, and a broad survey is never worth stalling the interview; skim the few files the request actually names. **Ground every question and every claim in what the code actually shows** — never guess at a surface you could have read.
## Interview discipline
@@ -42,7 +42,7 @@ Before your first question, use `Read` / `Grep` / `Glob` and the read-only git v
## Your tools
You have the built-in read tools `Read`, `Grep`, `Glob`, and `Task` (research subagents for a large codebase), plus **two** action tools: **`propose_draft`** (one task) and **`propose_batch`** (a MegaTask — several tasks at once). That's everything you have and everything you need — you read the code, you talk to the human, and when the spec is ready you call `propose_draft` (or `propose_batch`). You have **no** `dm`, `notify`, git, or lifecycle verbs, no `Write`/`Edit`/`Bash`, **no plan mode / `ExitPlanMode`**, **no `ToolSearch`**, and **no `AskUserQuestion`** or any structured question/prompt tool — you never speak to another agent, never write code, never create or route a task. **You ask the human by simply writing your questions as plain text in this chat** — they read every message you send live, so the chat itself is your question channel. None of those Claude Code built-ins exist for you; reaching for one only stalls the turn. **You do not "plan" and wait** — when the spec is ready you call `propose_draft` (or `propose_batch`) directly; never announce that a plan is written and ask whether to proceed. **Your replies in this conversation are your entire output to the human, and `propose_draft` / `propose_batch` is the only way a draft leaves this chat.**
You have the built-in read tools `Read`, `Grep`, `Glob` (no `Task` — the fleet-wide subagent ban includes you), plus **two** action tools: **`propose_draft`** (one task) and **`propose_batch`** (a MegaTask — several tasks at once). That's everything you have and everything you need — you read the code, you talk to the human, and when the spec is ready you call `propose_draft` (or `propose_batch`). You have **no** `dm`, `notify`, git, or lifecycle verbs, no `Write`/`Edit`/`Bash`, **no plan mode / `ExitPlanMode`**, **no `ToolSearch`**, and **no `AskUserQuestion`** or any structured question/prompt tool — you never speak to another agent, never write code, never create or route a task. **You ask the human by simply writing your questions as plain text in this chat** — they read every message you send live, so the chat itself is your question channel. None of those Claude Code built-ins exist for you; reaching for one only stalls the turn. **You do not "plan" and wait** — when the spec is ready you call `propose_draft` (or `propose_batch`) directly; never announce that a plan is written and ask whether to proceed. **Your replies in this conversation are your entire output to the human, and `propose_draft` / `propose_batch` is the only way a draft leaves this chat.**
## Presenting the draft
+1 -1
View File
@@ -42,7 +42,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.562.0",
"next": "16.1.1",
"next": "16.1.7",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
+164 -153
View File
@@ -90,8 +90,8 @@ importers:
specifier: ^0.562.0
version: 0.562.0(react@19.2.3)
next:
specifier: 16.1.1
version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
specifier: 16.1.7
version: 16.1.7(@babel/core@7.29.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -206,32 +206,36 @@ packages:
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.28.5':
resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
'@babel/core@7.28.5':
resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==}
'@babel/compat-data@7.29.7':
resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.28.5':
resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
'@babel/core@7.29.7':
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.27.2':
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
'@babel/generator@7.29.7':
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-globals@7.28.0':
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
'@babel/helper-compilation-targets@7.29.7':
resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-imports@7.27.1':
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
'@babel/helper-globals@7.29.7':
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.28.3':
resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
'@babel/helper-module-imports@7.29.7':
resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.29.7':
resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -244,12 +248,12 @@ packages:
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
'@babel/helper-validator-option@7.29.7':
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.28.4':
resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
'@babel/helpers@7.29.7':
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.7':
@@ -261,12 +265,12 @@ packages:
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
'@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.28.5':
resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
'@babel/traverse@7.29.7':
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.7':
@@ -612,60 +616,60 @@ packages:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@next/env@16.1.1':
resolution: {integrity: sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==}
'@next/env@16.1.7':
resolution: {integrity: sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==}
'@next/eslint-plugin-next@16.2.6':
resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==}
'@next/swc-darwin-arm64@16.1.1':
resolution: {integrity: sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==}
'@next/swc-darwin-arm64@16.1.7':
resolution: {integrity: sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@16.1.1':
resolution: {integrity: sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==}
'@next/swc-darwin-x64@16.1.7':
resolution: {integrity: sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@16.1.1':
resolution: {integrity: sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==}
'@next/swc-linux-arm64-gnu@16.1.7':
resolution: {integrity: sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.1':
resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
'@next/swc-linux-arm64-musl@16.1.7':
resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@16.1.1':
resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
'@next/swc-linux-x64-gnu@16.1.7':
resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@16.1.1':
resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
'@next/swc-linux-x64-musl@16.1.7':
resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.1':
resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
'@next/swc-win32-arm64-msvc@16.1.7':
resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@16.1.1':
resolution: {integrity: sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==}
'@next/swc-win32-x64-msvc@16.1.7':
resolution: {integrity: sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -2352,8 +2356,8 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
@@ -2368,8 +2372,8 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
form-data@4.0.6:
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
engines: {node: '>= 6'}
fsevents@2.3.3:
@@ -2468,6 +2472,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hasown@2.0.4:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
hast-util-to-jsx-runtime@2.3.6:
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
@@ -2685,8 +2693,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
js-yaml@4.3.0:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
hasBin: true
jsdom@29.1.1:
@@ -3086,11 +3094,11 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
@@ -3099,11 +3107,6 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -3123,8 +3126,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@16.1.1:
resolution: {integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==}
next@16.1.7:
resolution: {integrity: sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -3226,18 +3229,18 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
picomatch@2.3.2:
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
engines: {node: '>=8.6'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
picomatch@4.0.5:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -3254,8 +3257,8 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
postcss@8.5.16:
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
@@ -3985,18 +3988,24 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.28.5': {}
'@babel/core@7.28.5':
'@babel/code-frame@7.29.7':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.28.5
'@babel/helper-compilation-targets': 7.27.2
'@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5)
'@babel/helpers': 7.28.4
'@babel/helper-validator-identifier': 7.29.7
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.29.7': {}
'@babel/core@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.27.2
'@babel/traverse': 7.28.5
'@babel/template': 7.29.7
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
@@ -4007,7 +4016,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/generator@7.28.5':
'@babel/generator@7.29.7':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
@@ -4015,29 +4024,29 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/helper-compilation-targets@7.27.2':
'@babel/helper-compilation-targets@7.29.7':
dependencies:
'@babel/compat-data': 7.28.5
'@babel/helper-validator-option': 7.27.1
'@babel/compat-data': 7.29.7
'@babel/helper-validator-option': 7.29.7
browserslist: 4.28.1
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-globals@7.28.0': {}
'@babel/helper-globals@7.29.7': {}
'@babel/helper-module-imports@7.27.1':
'@babel/helper-module-imports@7.29.7':
dependencies:
'@babel/traverse': 7.28.5
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)':
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.28.5
'@babel/helper-module-imports': 7.27.1
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.28.5
'@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
@@ -4045,11 +4054,11 @@ snapshots:
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/helper-validator-option@7.27.1': {}
'@babel/helper-validator-option@7.29.7': {}
'@babel/helpers@7.28.4':
'@babel/helpers@7.29.7':
dependencies:
'@babel/template': 7.27.2
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/parser@7.29.7':
@@ -4058,19 +4067,19 @@ snapshots:
'@babel/runtime@7.29.7': {}
'@babel/template@7.27.2':
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/code-frame': 7.29.7
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse@7.28.5':
'@babel/traverse@7.29.7':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.28.5
'@babel/helper-globals': 7.28.0
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.27.2
'@babel/template': 7.29.7
'@babel/types': 7.29.7
debug: 4.4.3
transitivePeerDependencies:
@@ -4168,7 +4177,7 @@ snapshots:
dependencies:
'@eslint/object-schema': 2.1.7
debug: 4.4.3
minimatch: 3.1.2
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
@@ -4188,8 +4197,8 @@ snapshots:
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.1
minimatch: 3.1.2
js-yaml: 4.3.0
minimatch: 3.1.5
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
@@ -4368,34 +4377,34 @@ snapshots:
'@tybys/wasm-util': 0.10.2
optional: true
'@next/env@16.1.1': {}
'@next/env@16.1.7': {}
'@next/eslint-plugin-next@16.2.6':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@16.1.1':
'@next/swc-darwin-arm64@16.1.7':
optional: true
'@next/swc-darwin-x64@16.1.1':
'@next/swc-darwin-x64@16.1.7':
optional: true
'@next/swc-linux-arm64-gnu@16.1.1':
'@next/swc-linux-arm64-gnu@16.1.7':
optional: true
'@next/swc-linux-arm64-musl@16.1.1':
'@next/swc-linux-arm64-musl@16.1.7':
optional: true
'@next/swc-linux-x64-gnu@16.1.1':
'@next/swc-linux-x64-gnu@16.1.7':
optional: true
'@next/swc-linux-x64-musl@16.1.1':
'@next/swc-linux-x64-musl@16.1.7':
optional: true
'@next/swc-win32-arm64-msvc@16.1.1':
'@next/swc-win32-arm64-msvc@16.1.7':
optional: true
'@next/swc-win32-x64-msvc@16.1.1':
'@next/swc-win32-x64-msvc@16.1.7':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -5068,7 +5077,7 @@ snapshots:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.1.18
'@tailwindcss/oxide': 4.1.18
postcss: 8.5.6
postcss: 8.5.16
tailwindcss: 4.1.18
'@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)':
@@ -5272,7 +5281,7 @@ snapshots:
'@typescript-eslint/types': 8.49.0
'@typescript-eslint/visitor-keys': 8.49.0
debug: 4.4.3
minimatch: 9.0.5
minimatch: 9.0.9
semver: 7.8.5
tinyglobby: 0.2.17
ts-api-utils: 2.1.0(typescript@5.9.3)
@@ -5541,7 +5550,7 @@ snapshots:
axios@1.18.1:
dependencies:
follow-redirects: 1.16.0
form-data: 4.0.5
form-data: 4.0.6
https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0
transitivePeerDependencies:
@@ -5974,7 +5983,7 @@ snapshots:
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
minimatch: 3.1.2
minimatch: 3.1.5
object.fromentries: 2.0.8
object.groupby: 1.0.3
object.values: 1.2.1
@@ -6002,14 +6011,14 @@ snapshots:
hasown: 2.0.2
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
minimatch: 3.1.2
minimatch: 3.1.5
object.fromentries: 2.0.8
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@babel/core': 7.28.5
'@babel/core': 7.29.7
'@babel/parser': 7.29.7
eslint: 9.39.2(jiti@2.6.1)
hermes-parser: 0.25.1
@@ -6030,7 +6039,7 @@ snapshots:
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
minimatch: 3.1.2
minimatch: 3.1.5
object.entries: 1.1.9
object.fromentries: 2.0.8
object.values: 1.2.1
@@ -6082,7 +6091,7 @@ snapshots:
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.2
minimatch: 3.1.5
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
@@ -6138,14 +6147,14 @@ snapshots:
dependencies:
reusify: 1.1.0
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
picomatch: 4.0.5
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -6161,10 +6170,10 @@ snapshots:
flat-cache@4.0.1:
dependencies:
flatted: 3.3.3
flatted: 3.4.2
keyv: 4.5.4
flatted@3.3.3: {}
flatted@3.4.2: {}
follow-redirects@1.16.0: {}
@@ -6172,12 +6181,12 @@ snapshots:
dependencies:
is-callable: 1.2.7
form-data@4.0.5:
form-data@4.0.6:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
hasown: 2.0.4
mime-types: 2.1.35
fsevents@2.3.3:
@@ -6273,6 +6282,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
hasown@2.0.4:
dependencies:
function-bind: 1.1.2
hast-util-to-jsx-runtime@2.3.6:
dependencies:
'@types/estree': 1.0.8
@@ -6506,7 +6519,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@4.1.1:
js-yaml@4.3.0:
dependencies:
argparse: 2.0.1
@@ -7063,7 +7076,7 @@ snapshots:
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
picomatch: 2.3.2
mime-db@1.52.0: {}
@@ -7073,11 +7086,11 @@ snapshots:
min-indent@1.0.1: {}
minimatch@3.1.2:
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.12
minimatch@9.0.5:
minimatch@9.0.9:
dependencies:
brace-expansion: 2.0.2
@@ -7085,8 +7098,6 @@ snapshots:
ms@2.1.3: {}
nanoid@3.3.11: {}
nanoid@3.3.15: {}
napi-postinstall@0.3.4: {}
@@ -7098,25 +7109,25 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next@16.1.7(@babel/core@7.29.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 16.1.1
'@next/env': 16.1.7
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.42
caniuse-lite: 1.0.30001803
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.1.1
'@next/swc-darwin-x64': 16.1.1
'@next/swc-linux-arm64-gnu': 16.1.1
'@next/swc-linux-arm64-musl': 16.1.1
'@next/swc-linux-x64-gnu': 16.1.1
'@next/swc-linux-x64-musl': 16.1.1
'@next/swc-win32-arm64-msvc': 16.1.1
'@next/swc-win32-x64-msvc': 16.1.1
'@next/swc-darwin-arm64': 16.1.7
'@next/swc-darwin-x64': 16.1.7
'@next/swc-linux-arm64-gnu': 16.1.7
'@next/swc-linux-arm64-musl': 16.1.7
'@next/swc-linux-x64-gnu': 16.1.7
'@next/swc-linux-x64-musl': 16.1.7
'@next/swc-win32-arm64-msvc': 16.1.7
'@next/swc-win32-x64-msvc': 16.1.7
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -7219,12 +7230,12 @@ snapshots:
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.3: {}
picomatch@2.3.2: {}
picomatch@4.0.4: {}
picomatch@4.0.5: {}
possible-typed-array-names@1.1.0: {}
postcss-selector-parser@6.0.10:
@@ -7244,9 +7255,9 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.6:
postcss@8.5.16:
dependencies:
nanoid: 3.3.11
nanoid: 3.3.15
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -7685,12 +7696,12 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3):
styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.3):
dependencies:
client-only: 0.0.1
react: 19.2.3
optionalDependencies:
'@babel/core': 7.28.5
'@babel/core': 7.29.7
supports-color@7.2.0:
dependencies:
@@ -7714,8 +7725,8 @@ snapshots:
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
fdir: 6.5.0(picomatch@4.0.5)
picomatch: 4.0.5
tinyglobby@0.2.17:
dependencies:
@@ -7963,7 +7974,7 @@ snapshots:
magic-string: 0.30.21
obug: 2.1.3
pathe: 2.0.3
picomatch: 4.0.3
picomatch: 4.0.5
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.4
+8 -6
View File
@@ -417,7 +417,9 @@ class IntakeDriver:
# The intake agent's hard tool allowlist: read-only built-ins + the draft tool.
_INTAKE_BASE_TOOLS: tuple[str, ...] = ("Read", "Grep", "Glob", "Task")
# No ``Task``: the fleet-wide subagent ban (CEO, 2026-07-09) includes intake —
# it reads the codebase directly instead of fanning out research subagents.
_INTAKE_BASE_TOOLS: tuple[str, ...] = ("Read", "Grep", "Glob")
def build_intake_options(
@@ -435,7 +437,7 @@ def build_intake_options(
- ``strict_mcp_config=True`` + ``setting_sources=[]`` ignore the host's
``~/.claude.json`` / ``settings.json``; use ONLY the MCP server below.
- ``permission_mode="dontAsk"`` (NOT ``bypassPermissions``) + a ``can_use_tool``
gate a hard allowlist (Read/Grep/Glob/Task + ``propose_draft`` +
gate a hard allowlist (Read/Grep/Glob + ``propose_draft`` +
``propose_batch`` + ``search_past_tasks``), no prompts.
Draft emission: the agent calls the ``propose_draft`` MCP tool, which the
@@ -557,10 +559,10 @@ def build_intake_options(
return PermissionResultDeny(
message=(
f"{tool_name} is not available to the intake agent. Your only tools "
"are Read, Grep, Glob, Task, propose_draft, propose_batch (for a "
"MegaTask), and search_past_tasks. Ask the human inline; when the "
"spec is ready, call propose_draft (one task) or propose_batch "
"(several)."
"are Read, Grep, Glob, propose_draft, propose_batch (for a "
"MegaTask), and search_past_tasks. Read the codebase yourself — "
"no subagents. Ask the human inline; when the spec is ready, call "
"propose_draft (one task) or propose_batch (several)."
)
)
+5 -6
View File
@@ -84,12 +84,11 @@ _FULL_REASONING_OVERRIDES = frozenset({"default", "full", "none", ""})
# Roles that legitimately run a shell. Review / board roles never do.
_BASH_ROLES = frozenset({"developer", "documenter", "cell_pm", "main_pm"})
# The intake interviewer reads the codebase to draft a task and may fan out
# exploration to subagents (parity with the Claude intake's ``Task`` allowance);
# every other role drives work through the gateway verbs, never CLI subagents.
_SUBAGENT_ALLOWED_ROLES: frozenset[str] = (
frozenset()
) # fleet-wide ban: no agent spawns subagents (CEO, 2026-07-09)
# Fleet-wide subagent ban (CEO, 2026-07-09): no role — intake included — spawns
# CLI subagents; the Claude path mirrors this via ``_INTAKE_BASE_TOOLS``
# (intake_driver) and ``role_config.allows_subagent``. Work is driven through
# the gateway verbs.
_SUBAGENT_ALLOWED_ROLES: frozenset[str] = frozenset()
# Grok CLI tool IDs (from the CLI's --tools/--disallowed-tools reference).
_TOOL_SHELL = "run_terminal_cmd"
+3
View File
@@ -2136,6 +2136,9 @@ class AgentOrchestrator:
git_context: SpawnGitContext | None,
) -> tuple[AgentConfig, AgentInstance, Path | None]:
"""Build AgentConfig + AgentInstance and surface per-agent settings path."""
# agent_id lands in per-agent settings/prompt/briefing filenames below;
# re-assert single-segment safety at the proximate write site.
self._safe_agent_path_segment(agent_id)
project_slug = self._resolve_project_slug(git_context, agent_id, task_id)
ambient = await self._resolve_conventions_ambient(project_slug, task_id)
blueprint_path = self._generate_composed_prompt(agent_id, ambient=ambient)
+14 -3
View File
@@ -4914,9 +4914,20 @@ class GitService(BaseService):
project = await project_service.get_by_slug(project_slug)
if project is None:
return None
ws = workspace or (
Path(project.workspace_path) if project.workspace_path else None
)
ws = workspace
if ws is None and project.workspace_path:
# workspace_path is API-settable (PM-gated route): only a path
# inside THIS project's workspace tree may receive the scaffold
# commit — anything else (arbitrary dir, another project's clone)
# is refused as "no usable workspace".
candidate = Path(project.workspace_path)
try:
candidate.resolve().relative_to(
Path(settings.workspaces_root) / project.slug
)
ws = candidate
except (ValueError, OSError):
ws = None
if ws is None or not ws.exists():
return None
base = project.default_branch or "master"
+24 -1
View File
@@ -307,6 +307,24 @@ class WorkspaceError(Exception):
pass
def _safe_segment(value: str, label: str) -> str:
"""Return ``value`` if safe as one path segment under the workspace root.
Slugs/teams are regex- or enum-validated at creation, but every workspace
path is built here reject traversal vectors (empty, ``.``/``..``,
separators, NUL) at the chokepoint so the tree is safe by construction.
"""
if (
not value
or value in {".", ".."}
or "/" in value
or "\\" in value
or "\x00" in value
):
raise WorkspaceError(f"unsafe {label} for a workspace path: {value!r}")
return value
# Marker file recording the lockfile digest the dev-deps install last ran
# against. Lives under .git/ so it never shows up in `git status` (the agent's
# clean-tree checks would otherwise trip on it) and is wiped with the clone.
@@ -449,7 +467,12 @@ class WorkspaceService:
"Add the agent to AGENT_TEAM_MAP in roboco/agents_config.py."
)
team_str = team.value if isinstance(team, Team) else str(team)
return self.root / project_slug / team_str / agent_slug
return (
self.root
/ _safe_segment(project_slug, "project slug")
/ _safe_segment(team_str, "team")
/ _safe_segment(agent_slug, "agent slug")
)
def get_clone_root_path(
self,
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING
import pytest
from roboco.agent_sdk.intake_driver import (
_INTAKE_BASE_TOOLS,
IntakeDriver,
StreamChunk,
normalize,
@@ -408,3 +409,20 @@ async def test_driver_denies_prompt_injection_without_sending() -> None:
assert "prompt-injection" in collected[0].text
assert collected[-1].kind == "text"
assert collected[-1].text == "ok"
# ---------------------------------------------------------------------------
# Fleet-wide subagent ban (CEO, 2026-07-09)
# ---------------------------------------------------------------------------
def test_intake_base_tools_carry_no_subagent_tool() -> None:
"""The intake allowlist is read-only built-ins ONLY — no ``Task``.
The fleet-wide subagent ban includes the intake interviewer: it reads the
codebase directly instead of fanning out research subagents (observed
live: intake stalled the interview waiting on seven Task spawns). Both
``allowed_tools`` and the ``can_use_tool`` gate derive from this tuple,
so pinning the literal pins the ban on the Claude path.
"""
assert _INTAKE_BASE_TOOLS == ("Read", "Grep", "Glob")
@@ -36,11 +36,15 @@ def _init_repo(repo: Path) -> None:
_git(repo, "commit", "-m", "init")
def _svc(monkeypatch: pytest.MonkeyPatch, repo: Path) -> GitService:
def _svc(monkeypatch: pytest.MonkeyPatch, repo: Path, root: Path) -> GitService:
svc = GitService.__new__(GitService)
svc.session = AsyncMock()
# The workspace-scope guard requires workspace_path under
# {workspaces_root}/{project.slug}; anchor the root at the test dir.
monkeypatch.setattr(git_module.settings, "workspaces_root", str(root))
project = MagicMock()
project.slug = "g-proj"
project.workspace_path = str(repo)
project.default_branch = "master"
project_service = MagicMock()
@@ -69,12 +73,12 @@ async def test_dirty_tree_refused_before_any_checkout(
"""A dirty working tree is the agent's active workspace — refuse, return
None, and leave the tree exactly as it was (no scaffold branch, dirty
change still uncommitted in the working tree)."""
repo = tmp_path / "repo"
repo = tmp_path / "g-proj" / "repo"
_init_repo(repo)
# Dirty the tree: modify a tracked file (the agent's in-progress work).
(repo / "README.md").write_text("# dirty work in progress\n")
svc = _svc(monkeypatch, repo)
svc = _svc(monkeypatch, repo, tmp_path)
result = await svc.open_conventions_pr(
"g-proj", content="version: 1\n", title="scaffold", body="b"
@@ -111,10 +115,10 @@ async def test_clean_tree_proceeds_and_commits_on_scaffold(
"""A clean tree proceeds: the conventions file is committed on the
scaffold branch cut from master, and master itself is untouched
(regression guard for the fix not over-rejecting the clean case)."""
repo = tmp_path / "repo"
repo = tmp_path / "g-proj" / "repo"
_init_repo(repo)
svc = _svc(monkeypatch, repo)
svc = _svc(monkeypatch, repo, tmp_path)
result = await svc.open_conventions_pr(
"g-proj", content="version: 1\n", title="scaffold", body="b"
@@ -154,12 +158,13 @@ async def test_missing_base_branch_refused(
not fall through to ``checkout -B <scaffold>`` from the current branch
that would base the scaffold on the agent's task branch. Refuse when the
checkout doesn't actually land on base."""
repo = tmp_path / "repo"
repo = tmp_path / "g-proj" / "repo"
_init_repo(repo)
svc = _svc(monkeypatch, repo)
svc = _svc(monkeypatch, repo, tmp_path)
# Override the project default to a branch that doesn't exist locally.
project = MagicMock()
project.slug = "g-proj"
project.workspace_path = str(repo)
project.default_branch = "nonexistent-base"
project_service = MagicMock()
@@ -0,0 +1,131 @@
"""``open_conventions_pr`` refuses a ``project.workspace_path`` outside the
project's own workspace tree.
``workspace_path`` is settable through the PM-gated ``POST
/projects/{id}/workspace`` route with no path validation, so a steered PM
agent could point it at an arbitrary orchestrator directory (or another
project's clone) and have the conventions flow write + commit there. Only a
path under ``{workspaces_root}/{project.slug}`` may receive the scaffold
commit; anything else is treated as "no usable workspace" (returns None).
"""
from __future__ import annotations
import subprocess
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock
import pytest
from roboco.services import git as git_module
from roboco.services.git import GitService
if TYPE_CHECKING:
from pathlib import Path
_SCAFFOLD_BRANCH = "chore/roboco-conventions-scaffold"
def _git(repo: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True)
def _init_repo(repo: Path) -> None:
repo.mkdir(parents=True, exist_ok=True)
_git(repo, "init", "-b", "master")
_git(repo, "config", "user.email", "t@example.com")
_git(repo, "config", "user.name", "T")
_git(repo, "config", "commit.gpgsign", "false")
(repo / "README.md").write_text("# r\n")
_git(repo, "add", "README.md")
_git(repo, "commit", "-m", "init")
def _svc(
monkeypatch: pytest.MonkeyPatch, workspace_path: Path, root: Path
) -> GitService:
svc = GitService.__new__(GitService)
svc.session = AsyncMock()
monkeypatch.setattr(git_module.settings, "workspaces_root", str(root))
project = MagicMock()
project.slug = "g-proj"
project.workspace_path = str(workspace_path)
project.default_branch = "master"
project_service = MagicMock()
project_service.get_by_slug = AsyncMock(return_value=project)
monkeypatch.setattr(git_module, "get_project_service", lambda _s: project_service)
monkeypatch.setattr(svc, "_token_for_project", AsyncMock(return_value=None))
return svc
def _branch_exists(repo: Path, branch: str) -> bool:
res = subprocess.run(
["git", "rev-parse", "--verify", branch],
cwd=repo,
capture_output=True,
check=False,
)
return res.returncode == 0
@pytest.mark.asyncio
async def test_workspace_path_outside_root_refused(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A workspace_path outside workspaces_root entirely is refused and never
receives a scaffold branch or file."""
root = tmp_path / "workspaces"
root.mkdir()
outside = tmp_path / "outside-repo"
_init_repo(outside)
svc = _svc(monkeypatch, outside, root)
result = await svc.open_conventions_pr(
"g-proj", content="version: 1\n", title="scaffold", body="b"
)
assert result is None
assert not _branch_exists(outside, _SCAFFOLD_BRANCH)
assert not (outside / ".roboco" / "conventions.yml").exists()
@pytest.mark.asyncio
async def test_workspace_path_in_other_projects_tree_refused(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A workspace_path under workspaces_root but inside ANOTHER project's
tree is refused cross-project commits are not allowed."""
other = tmp_path / "other-proj" / "backend" / "be-dev-1"
_init_repo(other)
svc = _svc(monkeypatch, other, tmp_path)
result = await svc.open_conventions_pr(
"g-proj", content="version: 1\n", title="scaffold", body="b"
)
assert result is None
assert not _branch_exists(other, _SCAFFOLD_BRANCH)
@pytest.mark.asyncio
async def test_explicit_workspace_argument_bypasses_db_path(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The explicit ``workspace`` argument (internally constructed by callers
from the validated workspace layout) is honored as before the scope
guard applies to the API-settable DB field, not the trusted argument."""
repo = tmp_path / "elsewhere" / "clone"
_init_repo(repo)
# DB field points somewhere invalid; explicit arg wins.
svc = _svc(monkeypatch, tmp_path / "bogus", tmp_path / "workspaces")
result = await svc.open_conventions_pr(
"g-proj",
content="version: 1\n",
title="scaffold",
body="b",
workspace=repo,
)
assert result is not None
assert result["branch"] == _SCAFFOLD_BRANCH
assert _branch_exists(repo, _SCAFFOLD_BRANCH)
@@ -0,0 +1,56 @@
"""``get_workspace_path`` rejects traversal-capable path components.
Slugs/teams are regex- or enum-validated at creation, but every workspace path
is built at this one chokepoint pin the by-construction guard so a raw
``../`` / absolute / NUL segment can never place a workspace outside the root,
regardless of what upstream validation a future caller forgets.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock
import pytest
from roboco.models.base import Team
from roboco.services.workspace import WorkspaceError, WorkspaceService
if TYPE_CHECKING:
from pathlib import Path
def _service(root: Path) -> WorkspaceService:
svc = WorkspaceService(MagicMock())
svc.session = AsyncMock()
svc.root = root
return svc
@pytest.mark.parametrize(
"bad",
["", ".", "..", "../escape", "a/b", "a\\b", "bad\x00slug", "/etc"],
)
def test_rejects_unsafe_project_slug(tmp_path: Path, bad: str) -> None:
svc = _service(tmp_path)
with pytest.raises(WorkspaceError, match="unsafe project slug"):
svc.get_workspace_path(bad, Team.BACKEND, "be-dev-1")
@pytest.mark.parametrize("bad", ["..", "back/end", ""])
def test_rejects_unsafe_team_string(tmp_path: Path, bad: str) -> None:
svc = _service(tmp_path)
with pytest.raises(WorkspaceError, match="unsafe team"):
svc.get_workspace_path("guard-core", bad, "be-dev-1")
@pytest.mark.parametrize("bad", ["..", "../../be-dev-1", "be\x00dev"])
def test_rejects_unsafe_agent_slug(tmp_path: Path, bad: str) -> None:
svc = _service(tmp_path)
with pytest.raises(WorkspaceError, match="unsafe agent slug"):
svc.get_workspace_path("guard-core", Team.BACKEND, bad)
def test_valid_segments_unchanged(tmp_path: Path) -> None:
svc = _service(tmp_path)
path = svc.get_workspace_path("guard-core", Team.BACKEND, "be-dev-1")
assert path == tmp_path / "guard-core" / "backend" / "be-dev-1"