Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/services/agent-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { generateDockerCompose, WrapperConfig, baseConfig, mockNetworkConfig, useTempWorkDir } from './service-test-setup.test-utils';
import { testHelpers } from './agent-service';
import { parseImageTag } from '../image-tag';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
Expand Down Expand Up @@ -496,3 +498,71 @@ describe('agent service', () => {
});
});
});

const nodePath = path;
const { resolveAgentImageConfig } = testHelpers;

describe('resolveAgentImageConfig', () => {
const projectRoot = '/fake/project';
const registry = 'ghcr.io/github/gh-aw-firewall';
const parsedTag = parseImageTag('latest');

const baseImageConfig = { useGHCR: true, registry, parsedTag, projectRoot };

it('returns GHCR agent image for default preset', () => {
const result = resolveAgentImageConfig(
{ agentImage: 'default', buildLocal: false } as any,
baseImageConfig,
);
expect(result).toEqual({ image: 'ghcr.io/github/gh-aw-firewall/agent:latest' });
});

it('returns GHCR agent-act image for act preset', () => {
const result = resolveAgentImageConfig(
{ agentImage: 'act', buildLocal: false } as any,
baseImageConfig,
);
expect(result).toEqual({ image: 'ghcr.io/github/gh-aw-firewall/agent-act:latest' });
});

it('returns build config for default preset with --build-local', () => {
const result = resolveAgentImageConfig(
{ agentImage: 'default', buildLocal: true } as any,
{ ...baseImageConfig, useGHCR: false },
) as any;
expect(result.build).toBeDefined();
expect(result.build.dockerfile).toBe('Dockerfile');
expect(result.build.context).toBe(nodePath.join(projectRoot, 'containers/agent'));
expect(result.build.args.BASE_IMAGE).toBeUndefined();
expect(result.image).toBeUndefined();
});

it('returns build config for act preset with --build-local', () => {
const result = resolveAgentImageConfig(
{ agentImage: 'act', buildLocal: true } as any,
{ ...baseImageConfig, useGHCR: false },
) as any;
expect(result.build).toBeDefined();
expect(result.build.args.BASE_IMAGE).toMatch(/catthehacker/);
});

it('returns build config with BASE_IMAGE for custom (non-preset) image', () => {
const result = resolveAgentImageConfig(
{ agentImage: 'ubuntu:24.04', buildLocal: false } as any,
{ ...baseImageConfig, useGHCR: false },
) as any;
expect(result.build).toBeDefined();
expect(result.build.args.BASE_IMAGE).toBe('ubuntu:24.04');
});

it('returns direct image passthrough when useGHCR is false, buildLocal is false, and preset image is specified', () => {
// Else branch fires when: !useGHCR && !buildLocal && isPreset
// (e.g. user disabled GHCR pull but did not pass --build-local, using the 'default' preset)
const result = resolveAgentImageConfig(
{ agentImage: 'default', buildLocal: false } as any,
{ ...baseImageConfig, useGHCR: false },
) as any;
expect(result.image).toBe('default');
expect(result.build).toBeUndefined();
});
});
59 changes: 41 additions & 18 deletions src/services/agent-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ interface AgentServiceParams {
*/
export function buildAgentService(params: AgentServiceParams): any {
const { config, networkConfig, environment, agentVolumes, dnsServers, imageConfig } = params;
const { useGHCR, registry, parsedTag, projectRoot } = imageConfig;

// Agent service configuration
const agentService: any = {
Expand Down Expand Up @@ -147,19 +146,39 @@ export function buildAgentService(params: AgentServiceParams): any {
environment.AWF_ENABLE_HOST_ACCESS = '1';
}

// Use GHCR image or build locally
// Priority: GHCR preset images > local build (when requested) > custom images
// For presets ('default', 'act'), use GHCR images
Object.assign(agentService, resolveAgentImageConfig(config, imageConfig));

return agentService;
}

// ─── Image Selection ─────────────────────────────────────────────────────────

/**
* Resolves the image or build configuration for the agent container.
*
* Priority: GHCR preset images > local build (when requested or non-preset) > custom image passthrough
*
* Returns either `{ image: string }` (pull from registry) or
* `{ build: { context, dockerfile, args } }` (local build), suitable for
* spreading onto a Docker Compose service object.
*/
export function resolveAgentImageConfig(
config: WrapperConfig,
imageConfig: ImageBuildConfig,
): { image: string } | { build: { context: string; dockerfile: string; args?: Record<string, string> } } {
const { useGHCR, registry, parsedTag, projectRoot } = imageConfig;
const agentImage = config.agentImage || 'default';
const isPreset = agentImage === 'default' || agentImage === 'act';

if (useGHCR && isPreset) {
// Use pre-built GHCR image for preset images
if (useGHCR && isPreset && !config.buildLocal) {
// The GHCR images already have the necessary setup for chroot mode
const imageName = agentImage === 'act' ? 'agent-act' : 'agent';
agentService.image = buildRuntimeImageRef(registry, imageName, parsedTag);
logger.debug(`Using GHCR image ${agentService.image}`);
} else if (config.buildLocal || !isPreset) {
const image = buildRuntimeImageRef(registry, imageName, parsedTag);
logger.debug(`Using GHCR image ${image}`);
return { image };
}

if (config.buildLocal || !isPreset) {
// Build locally when:
// 1. --build-local is explicitly specified, OR
// 2. A custom (non-preset) image is specified
Expand All @@ -184,20 +203,24 @@ export function buildAgentService(params: AgentServiceParams): any {
}
// For 'default' preset with --build-local, use the Dockerfile's default (ubuntu:22.04)

agentService.build = {
context: path.join(projectRoot, 'containers/agent'),
dockerfile,
args: buildArgs,
return {
build: {
context: path.join(projectRoot, 'containers/agent'),
dockerfile,
args: buildArgs,
},
};
} else {
// Custom image specified without --build-local
// Use the image directly (user is responsible for ensuring compatibility)
agentService.image = agentImage;
}

return agentService;
// Custom image specified without --build-local
// Use the image directly (user is responsible for ensuring compatibility)
return { image: agentImage };
}

// ts-prune-ignore-next
/** @internal Exported for unit testing only */
export const testHelpers = { resolveAgentImageConfig };

// ─── iptables-init Service ────────────────────────────────────────────────────

interface IptablesInitServiceParams {
Expand Down
Loading