diff --git a/packages/core/src/__tests__/permissions-integration.test.ts b/packages/core/src/__tests__/permissions-integration.test.ts index 7d340d8..342389f 100644 --- a/packages/core/src/__tests__/permissions-integration.test.ts +++ b/packages/core/src/__tests__/permissions-integration.test.ts @@ -662,6 +662,39 @@ describe('WorkflowRunner permission lifecycle integration', () => { expect((runner as any).agentTokens.size).toBe(0); }); + it('warns when a write pattern resolves to no existing paths during dry-run', () => { + const projectDir = createProject({ + 'src/input.ts': 'export const input = true;\n', + 'gate.sh': '#!/usr/bin/env bash\nexit 0\n', + }); + const runner = makeRunner(projectDir); + const config = makeConfig([ + { + name: 'writer', + cli: 'claude', + permissions: { + access: 'restricted', + files: { + read: ['**'], + write: ['impl.txt'], + deny: ['gate.sh'], + }, + }, + }, + ]); + + const report = runner.dryRun(config, 'default'); + const permissionEntry = report.permissions?.find((entry) => entry.agent === 'writer'); + + expect(report.valid).toBe(true); + expect(permissionEntry?.writePaths).toBe(0); + expect(report.warnings).toContain( + 'Agent "writer" write pattern(s) "impl.txt" resolved to no existing writable paths. ' + + 'The installed @agent-relay/cloud resolver only grants writes to existing paths; ' + + 'files created under these patterns may be denied.' + ); + }); + it('rejects invalid permission config during validation before provisioning', async () => { const projectDir = createBaseProject(); const runner = makeRunner(projectDir); diff --git a/packages/core/src/runner.ts b/packages/core/src/runner.ts index ee17545..f8c9b88 100644 --- a/packages/core/src/runner.ts +++ b/packages/core/src/runner.ts @@ -3080,6 +3080,14 @@ export class WorkflowRunner { const permissions = resolved.agents.map((agent) => { const compiled = resolveAgentPermissions(agent.name, agent.permissions, this.cwd, this.workspaceId); + if (compiled.readwritePatterns.length > 0 && compiled.summary.readwrite === 0) { + const patterns = compiled.readwritePatterns.map((pattern) => `"${pattern}"`).join(', '); + warnings.push( + `Agent "${agent.name}" write pattern(s) ${patterns} resolved to no existing writable paths. ` + + 'The installed @agent-relay/cloud resolver only grants writes to existing paths; ' + + 'files created under these patterns may be denied.' + ); + } const source: NonNullable[number]['source'] = compiled.sources.some( (entry) => entry.type === 'yaml' )