diff --git a/docs/awf-config.schema.json b/docs/awf-config.schema.json index c80c75d90..702bbc9a6 100644 --- a/docs/awf-config.schema.json +++ b/docs/awf-config.schema.json @@ -607,6 +607,14 @@ "runnerToolCachePath": { "type": "string", "description": "Host runner tool cache directory to mount read-only into chroot mode. When set, AWF checks this path first before environment-based auto-detection." + }, + "mounts": { + "type": "array", + "items": { + "type": "string", + "pattern": "^/[^:]+:/[^:]+(:(ro|rw))?$" + }, + "description": "Custom volume mounts for the agent container. Format: \"/host_path:/container_path[:ro|rw]\" (both paths must be absolute). In chroot mode, container paths are automatically prefixed with /host." } } }, diff --git a/src/awf-config-schema.json b/src/awf-config-schema.json index c80c75d90..702bbc9a6 100644 --- a/src/awf-config-schema.json +++ b/src/awf-config-schema.json @@ -607,6 +607,14 @@ "runnerToolCachePath": { "type": "string", "description": "Host runner tool cache directory to mount read-only into chroot mode. When set, AWF checks this path first before environment-based auto-detection." + }, + "mounts": { + "type": "array", + "items": { + "type": "string", + "pattern": "^/[^:]+:/[^:]+(:(ro|rw))?$" + }, + "description": "Custom volume mounts for the agent container. Format: \"/host_path:/container_path[:ro|rw]\" (both paths must be absolute). In chroot mode, container paths are automatically prefixed with /host." } } }, diff --git a/src/config-file-mapping.test.ts b/src/config-file-mapping.test.ts index 56c19f427..5215833dc 100644 --- a/src/config-file-mapping.test.ts +++ b/src/config-file-mapping.test.ts @@ -299,6 +299,27 @@ describe('mapAwfFileConfigToCliOptions', () => { expect(result.runnerToolCachePath).toBe('/opt/hostedtoolcache'); }); + it('maps container.mounts to mount array', () => { + const result = mapAwfFileConfigToCliOptions({ + container: { + mounts: [ + '/tmp/gh-aw:/tmp/gh-aw:ro', + '/tmp/gh-aw/home:/tmp/gh-aw/home:rw', + ], + }, + }); + + expect(result.mount).toEqual([ + '/tmp/gh-aw:/tmp/gh-aw:ro', + '/tmp/gh-aw/home:/tmp/gh-aw/home:rw', + ]); + }); + + it('leaves mount undefined when container.mounts is not set', () => { + const result = mapAwfFileConfigToCliOptions({ container: {} }); + expect(result.mount).toBeUndefined(); + }); + it('maps environment fields', () => { const result = mapAwfFileConfigToCliOptions({ environment: { diff --git a/src/config-file.ts b/src/config-file.ts index 7e842b0b0..630b927ae 100644 --- a/src/config-file.ts +++ b/src/config-file.ts @@ -112,6 +112,7 @@ export interface AwfFileConfig { dockerHost?: string; dockerHostPathPrefix?: string; runnerToolCachePath?: string; + mounts?: string[]; }; chroot?: { binariesSourcePath?: string; diff --git a/src/config-mapper.ts b/src/config-mapper.ts index 677741f67..e521fd38f 100644 --- a/src/config-mapper.ts +++ b/src/config-mapper.ts @@ -104,6 +104,7 @@ export function mapAwfFileConfigToCliOptions(config: AwfFileConfig): Record { expect(validate({ container: { runnerToolCachePath: 123 } })).toBe(false); }); + it('accepts valid container.mounts array', () => { + expect(validate({ container: { mounts: ['/tmp/gh-aw:/tmp/gh-aw:ro'] } })).toBe(true); + expect(validate({ container: { mounts: ['/tmp/gh-aw:/tmp/gh-aw:rw', '/data:/data'] } })).toBe(true); + expect(validate({ container: { mounts: [] } })).toBe(true); + }); + + it('rejects invalid container.mounts entries', () => { + expect(validate({ container: { mounts: ['invalid-no-colon'] } })).toBe(false); + expect(validate({ container: { mounts: ['/src:/dst:invalid-mode'] } })).toBe(false); + expect(validate({ container: { mounts: 'not-an-array' } })).toBe(false); + // Relative paths must be rejected (runtime validator requires absolute paths) + expect(validate({ container: { mounts: ['relative/path:/container/dst'] } })).toBe(false); + expect(validate({ container: { mounts: ['/host/src:relative/container'] } })).toBe(false); + expect(validate({ container: { mounts: ['./relative:/container/dst:ro'] } })).toBe(false); + }); + it('accepts runner.topology and runner.sysrootImage', () => { expect(validate({ runner: { topology: 'arc-dind' } })).toBe(true); expect(validate({ runner: { topology: 'invalid' } })).toBe(false);