@@ -169,6 +169,145 @@ func TestGateCheck_DeniesDestructiveBash(t *testing.T) {
169169 }
170170}
171171
172+ func TestGateCheck_AppliesRepoAgentlockOnlyInsideCwdTree (t * testing.T ) {
173+ fx := newGateFixture (t , enforcePolicyYAML )
174+ repo := filepath .Join (fx .home , "repo" )
175+ sibling := filepath .Join (fx .home , "sibling" )
176+ if err := os .MkdirAll (filepath .Join (repo , "pkg" ), 0o755 ); err != nil {
177+ t .Fatal (err )
178+ }
179+ if err := os .MkdirAll (sibling , 0o755 ); err != nil {
180+ t .Fatal (err )
181+ }
182+ if err := os .WriteFile (filepath .Join (repo , ".agentlock.yaml" ), []byte (`
183+ version: 1
184+ gates:
185+ - id: repo.block-secret-print
186+ match:
187+ tool: Bash
188+ any_command_regex:
189+ - 'cat\s+secrets\.txt'
190+ evaluate:
191+ - kind: always
192+ action: deny
193+ ` ), 0o644 ); err != nil {
194+ t .Fatal (err )
195+ }
196+
197+ inside := fmt .Sprintf (`{
198+ "session_id": %q,
199+ "source": "codex",
200+ "tool": "Bash",
201+ "cwd": %q,
202+ "input": {"command": "cat secrets.txt"}
203+ }` , fx .sessionID , filepath .Join (repo , "pkg" ))
204+ res , out := postGateCheck (t , fx .srv , inside )
205+ if res .StatusCode != http .StatusOK {
206+ t .Fatalf ("inside status = %d" , res .StatusCode )
207+ }
208+ if out ["verdict" ] != "deny" || out ["rule_id" ] != "repo.block-secret-print" {
209+ t .Fatalf ("inside should hit repo rule, got %+v" , out )
210+ }
211+
212+ outside := fmt .Sprintf (`{
213+ "session_id": %q,
214+ "source": "codex",
215+ "tool": "Bash",
216+ "cwd": %q,
217+ "input": {"command": "cat secrets.txt"}
218+ }` , fx .sessionID , sibling )
219+ res , out = postGateCheck (t , fx .srv , outside )
220+ if res .StatusCode != http .StatusOK {
221+ t .Fatalf ("outside status = %d" , res .StatusCode )
222+ }
223+ if out ["verdict" ] != "allow" || out ["rule_id" ] != "default" {
224+ t .Fatalf ("sibling repo should not inherit repo rule, got %+v" , out )
225+ }
226+ }
227+
228+ func TestGateCheck_IgnoresPermissiveRepoAgentlockUntilApproved (t * testing.T ) {
229+ fx := newGateFixture (t , enforcePolicyYAML )
230+ repo := filepath .Join (fx .home , "repo" )
231+ if err := os .MkdirAll (repo , 0o755 ); err != nil {
232+ t .Fatal (err )
233+ }
234+ if err := os .WriteFile (filepath .Join (repo , ".agentlock.yaml" ), []byte (`
235+ version: 1
236+ gates:
237+ - id: rogue.destructive-bash
238+ disabled: true
239+ match:
240+ tool: Bash
241+ any_command_regex:
242+ - 'rm\s+-rf\b'
243+ evaluate:
244+ - kind: always
245+ action: allow
246+ ` ), 0o644 ); err != nil {
247+ t .Fatal (err )
248+ }
249+
250+ body := fmt .Sprintf (`{
251+ "session_id": %q,
252+ "source": "codex",
253+ "tool": "Bash",
254+ "cwd": %q,
255+ "input": {"command": "rm -rf /tmp/demo"}
256+ }` , fx .sessionID , repo )
257+ res , out := postGateCheck (t , fx .srv , body )
258+ if res .StatusCode != http .StatusOK {
259+ t .Fatalf ("status = %d" , res .StatusCode )
260+ }
261+ if out ["verdict" ] != "deny" || out ["rule_id" ] != "rogue.destructive-bash" {
262+ t .Fatalf ("permissive repo override must not weaken daemon policy, got %+v" , out )
263+ }
264+ }
265+
266+ func TestGateCheck_AppliesRepoGateWithConditionalDenyEvaluator (t * testing.T ) {
267+ fx := newGateFixture (t , enforcePolicyYAML )
268+ repo := filepath .Join (fx .home , "repo" )
269+ allowlist := filepath .Join (repo , "allowed.txt" )
270+ if err := os .MkdirAll (repo , 0o755 ); err != nil {
271+ t .Fatal (err )
272+ }
273+ if err := os .WriteFile (allowlist , []byte ("safe-package\n " ), 0o644 ); err != nil {
274+ t .Fatal (err )
275+ }
276+ if err := os .WriteFile (filepath .Join (repo , ".agentlock.yaml" ), []byte (fmt .Sprintf (`
277+ version: 1
278+ gates:
279+ - id: repo.npm-allowlist
280+ match:
281+ tool: Bash
282+ any_command_regex:
283+ - '^npm\s+install\s+'
284+ evaluate:
285+ - kind: allowlist
286+ list: %q
287+ on_hit: allow
288+ on_miss: deny
289+ - kind: always
290+ action: allow
291+ ` , allowlist )), 0o644 ); err != nil {
292+ t .Fatal (err )
293+ }
294+
295+ body := fmt .Sprintf (`{
296+ "session_id": %q,
297+ "source": "codex",
298+ "tool": "Bash",
299+ "cwd": %q,
300+ "input": {"command": "npm install evil-package"}
301+ }` , fx .sessionID , repo )
302+ res , out := postGateCheck (t , fx .srv , body )
303+ if res .StatusCode != http .StatusOK {
304+ t .Fatalf ("status = %d" , res .StatusCode )
305+ }
306+ if out ["verdict" ] != "deny" || out ["rule_id" ] != "repo.npm-allowlist" {
307+ t .Fatalf ("conditional deny repo gate should apply, got %+v" , out )
308+ }
309+ }
310+
172311func TestGateCheck_WritesLedgerEntry (t * testing.T ) {
173312 fx := newGateFixture (t , enforcePolicyYAML )
174313
0 commit comments