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
82 changes: 78 additions & 4 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7778,22 +7778,41 @@
"group": "gh-aw-claude",
"cancel-in-progress": false
}
},
{
"runtime": {
"id": "codex",
"version": "0.105.0"
},
"provider": {
"id": "openai",
"model": "gpt-5",
"auth": {
"secret": "OPENAI_API_KEY"
}
}
},
{
"runtime": {
"id": "claude"
},
"provider": {
"model": "claude-3-7-sonnet-20250219"
}
}
],
"oneOf": [
{
"type": "string",
"enum": ["claude", "codex", "copilot", "gemini"],
"description": "Simple engine name: 'claude' (default, Claude Code), 'copilot' (GitHub Copilot CLI), 'codex' (OpenAI Codex CLI), or 'gemini' (Google Gemini CLI)"
"description": "Engine name: built-in ('claude', 'codex', 'copilot', 'gemini') or a named catalog entry"
},
{
"type": "object",
"description": "Extended engine configuration object with advanced options for model selection, turn limiting, environment variables, and custom steps",
"properties": {
"id": {
"type": "string",
"enum": ["claude", "codex", "copilot", "gemini"],
"description": "AI engine identifier: 'claude' (Claude Code), 'codex' (OpenAI Codex CLI), 'copilot' (GitHub Copilot CLI), or 'gemini' (Google Gemini CLI)"
"description": "AI engine identifier: built-in ('claude', 'codex', 'copilot', 'gemini') or a named catalog entry"
},
"version": {
"type": ["string", "number"],
Expand Down Expand Up @@ -7914,6 +7933,61 @@
},
"required": ["id"],
"additionalProperties": false
},
{
"type": "object",
"description": "Inline engine definition: specifies a runtime adapter and optional provider settings directly in the workflow frontmatter, without requiring a named catalog entry",
"properties": {
"runtime": {
"type": "object",
"description": "Runtime adapter reference for the inline engine definition",
"properties": {
"id": {
"type": "string",
"description": "Runtime adapter identifier (e.g. 'codex', 'claude', 'copilot', 'gemini')",
"examples": ["codex", "claude", "copilot", "gemini"]
},
"version": {
"type": ["string", "number"],
"description": "Optional version of the runtime adapter (e.g. '0.105.0', 'beta')",
"examples": ["0.105.0", "beta", "latest"]
}
},
"required": ["id"],
"additionalProperties": false
},
"provider": {
"type": "object",
"description": "Optional provider configuration for the inline engine definition",
"properties": {
"id": {
"type": "string",
"description": "Provider identifier (e.g. 'openai', 'anthropic', 'github', 'google')",
"examples": ["openai", "anthropic", "github", "google"]
},
"model": {
"type": "string",
"description": "Optional specific LLM model to use (e.g. 'gpt-5', 'claude-3-5-sonnet-20241022')",
"examples": ["gpt-5", "claude-3-5-sonnet-20241022", "gpt-4o"]
},
"auth": {
"type": "object",
"description": "Authentication configuration for the provider",
"properties": {
"secret": {
"type": "string",
"description": "Name of the GitHub Actions secret that contains the API key for this provider",
"examples": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "CUSTOM_API_KEY"]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": ["runtime"],
"additionalProperties": false
}
]
},
Expand Down
9 changes: 9 additions & 0 deletions pkg/workflow/compiler_orchestrator_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ func (c *Compiler) setupEngineAndImports(result *parser.FrontmatterResult, clean
// Extract AI engine setting from frontmatter
engineSetting, engineConfig := c.ExtractEngineConfig(result.Frontmatter)

// Validate and register inline engine definitions (engine.runtime sub-object).
// Must happen before catalog resolution so the inline definition is visible to Resolve().
if engineConfig != nil && engineConfig.IsInlineDefinition {
if err := c.validateEngineInlineDefinition(engineConfig); err != nil {
return nil, err
}
c.registerInlineEngineDefinition(engineConfig)
Comment on lines +41 to +45
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This registers inline definitions into c.engineCatalog, but the same Compiler instance is used to compile multiple markdown files in a single run (see pkg/cli/compile_orchestration.go loop). That means an inline override (e.g., changing provider/auth for "codex") can leak into subsequent workflow compilations. To avoid cross-file state, consider resolving with a per-workflow catalog copy (clone definitions map), or temporarily overriding and restoring the original definition after Resolve().

Copilot uses AI. Check for mistakes.
}

// Extract network permissions from frontmatter
networkPermissions := c.extractNetworkPermissions(result.Frontmatter)

Expand Down
43 changes: 43 additions & 0 deletions pkg/workflow/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ type EngineConfig struct {
Args []string
Firewall *FirewallConfig // AWF firewall configuration
Agent string // Agent identifier for copilot --agent flag (copilot engine only)

// Inline definition fields (populated when engine.runtime is specified in frontmatter)
IsInlineDefinition bool // true when the engine is defined inline via engine.runtime + optional engine.provider
InlineProviderID string // engine.provider.id (e.g. "openai", "anthropic")
InlineProviderSecret string // engine.provider.auth.secret (name of the GitHub Actions secret for the provider API key)
}

// NetworkPermissions represents network access permissions for workflow execution
Expand Down Expand Up @@ -87,6 +92,44 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng
engineLog.Print("Found engine in object format, parsing configuration")
config := &EngineConfig{}

// Detect inline definition: engine.runtime sub-object present instead of engine.id
if runtime, hasRuntime := engineObj["runtime"]; hasRuntime {
engineLog.Print("Found inline engine definition (engine.runtime sub-object)")
config.IsInlineDefinition = true

if runtimeObj, ok := runtime.(map[string]any); ok {
if id, ok := runtimeObj["id"].(string); ok {
config.ID = id
engineLog.Printf("Inline engine runtime.id: %s", config.ID)
}
if version, hasVersion := runtimeObj["version"]; hasVersion {
config.Version = stringutil.ParseVersionValue(version)
}
}

// Extract optional provider sub-object
if provider, hasProvider := engineObj["provider"]; hasProvider {
if providerObj, ok := provider.(map[string]any); ok {
if id, ok := providerObj["id"].(string); ok {
config.InlineProviderID = id
}
if model, ok := providerObj["model"].(string); ok {
config.Model = model
}
if auth, hasAuth := providerObj["auth"]; hasAuth {
if authObj, ok := auth.(map[string]any); ok {
if secret, ok := authObj["secret"].(string); ok {
config.InlineProviderSecret = secret
}
}
}
}
}

engineLog.Printf("Extracted inline engine definition: runtimeID=%s, providerID=%s", config.ID, config.InlineProviderID)
return config.ID, config
}

// Extract required 'id' field
if id, hasID := engineObj["id"]; hasID {
if idStr, ok := id.(string); ok {
Expand Down
85 changes: 54 additions & 31 deletions pkg/workflow/engine_catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ func TestEngineCatalog_All(t *testing.T) {
}
}

// engineSchemaEnums parses the main workflow schema and extracts engine enum values
// from both the string variant and the object id property of engine_config.
func engineSchemaEnums(t *testing.T) []string {
// engineSchemaOneOfVariants parses the main workflow schema and returns the
// type identifiers of each variant in engine_config.oneOf for structural assertions.
func engineSchemaOneOfVariants(t *testing.T) []map[string]any {
t.Helper()

schemaBytes, err := os.ReadFile("../parser/schemas/main_workflow_schema.json")
Expand All @@ -81,40 +81,63 @@ func engineSchemaEnums(t *testing.T) []string {
oneOf, ok := engineConfig["oneOf"].([]any)
require.True(t, ok, "engine_config should have oneOf")

// The first oneOf variant is the plain string enum
for _, variant := range oneOf {
v, ok := variant.(map[string]any)
if !ok {
continue
}
if v["type"] == "string" {
rawEnum, ok := v["enum"].([]any)
if !ok {
continue
}
enums := make([]string, 0, len(rawEnum))
for _, e := range rawEnum {
if s, ok := e.(string); ok {
enums = append(enums, s)
}
}
sort.Strings(enums)
return enums
variants := make([]map[string]any, 0, len(oneOf))
for _, v := range oneOf {
if m, ok := v.(map[string]any); ok {
variants = append(variants, m)
}
}
t.Fatal("could not find string enum in engine_config oneOf")
return nil
return variants
}

// TestEngineCatalogMatchesSchema asserts that the schema engine enum values exactly
// match the catalog IDs. A failure here means the schema and catalog have drifted apart.
func TestEngineCatalogMatchesSchema(t *testing.T) {
// TestEngineCatalog_BuiltInsPresent verifies that the four built-in engines are always
// registered in the catalog with stable IDs.
func TestEngineCatalog_BuiltInsPresent(t *testing.T) {
registry := NewEngineRegistry()
catalog := NewEngineCatalog(registry)

catalogIDs := catalog.IDs() // already sorted
schemaEnums := engineSchemaEnums(t)
expected := []string{"claude", "codex", "copilot", "gemini"}
catalogIDs := catalog.IDs()
for _, id := range expected {
assert.Contains(t, catalogIDs, id,
"built-in engine %q must always be present in the catalog", id)
}
}

assert.Equal(t, catalogIDs, schemaEnums,
"schema engine enum must match catalog IDs exactly — run 'make build' after updating the schema")
// TestEngineCatalogMatchesSchema asserts that the engine_config schema has the expected
// structure: a plain-string variant (for built-ins and named catalog entries), an
// object-with-id variant, and an inline-definition variant (object-with-runtime).
// A failure here means the schema structure has changed unexpectedly.
func TestEngineCatalogMatchesSchema(t *testing.T) {
variants := engineSchemaOneOfVariants(t)

require.Len(t, variants, 3, "engine_config oneOf should have exactly 3 variants: string, object-with-id, object-with-runtime")

// Variant 0: plain string (no enum — allows built-ins and custom named catalog entries)
assert.Equal(t, "string", variants[0]["type"],
"first variant should be type string")
assert.Nil(t, variants[0]["enum"],
"string variant must NOT have an enum so that named catalog entries are allowed")

// Variant 1: object with 'id' field for extended engine configuration
assert.Equal(t, "object", variants[1]["type"],
"second variant should be type object (extended config with id)")
props1, ok := variants[1]["properties"].(map[string]any)
require.True(t, ok, "second variant should have properties")
assert.Contains(t, props1, "id",
"second variant should have an 'id' property")
idProp, ok := props1["id"].(map[string]any)
require.True(t, ok, "id property should be a map")
assert.Nil(t, idProp["enum"],
"id property must NOT have an enum so that named catalog entries are allowed")

// Variant 2: object with 'runtime' sub-object for inline definitions
assert.Equal(t, "object", variants[2]["type"],
"third variant should be type object (inline definition with runtime)")
props2, ok := variants[2]["properties"].(map[string]any)
require.True(t, ok, "third variant should have properties")
assert.Contains(t, props2, "runtime",
"third variant should have a 'runtime' property for inline engine definitions")
assert.Contains(t, props2, "provider",
"third variant should have a 'provider' property for inline engine definitions")
Comment on lines +116 to +142
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test assumes the order of engine_config.oneOf variants (variants[0], [1], [2]), but JSON Schema does not guarantee semantic meaning by order and schema generation could reorder entries without changing behavior. To make the assertion robust, identify variants by their shape (e.g., type=="string"; object with properties.id; object with properties.runtime) instead of relying on array positions.

Suggested change
// Variant 0: plain string (no enum — allows built-ins and custom named catalog entries)
assert.Equal(t, "string", variants[0]["type"],
"first variant should be type string")
assert.Nil(t, variants[0]["enum"],
"string variant must NOT have an enum so that named catalog entries are allowed")
// Variant 1: object with 'id' field for extended engine configuration
assert.Equal(t, "object", variants[1]["type"],
"second variant should be type object (extended config with id)")
props1, ok := variants[1]["properties"].(map[string]any)
require.True(t, ok, "second variant should have properties")
assert.Contains(t, props1, "id",
"second variant should have an 'id' property")
idProp, ok := props1["id"].(map[string]any)
require.True(t, ok, "id property should be a map")
assert.Nil(t, idProp["enum"],
"id property must NOT have an enum so that named catalog entries are allowed")
// Variant 2: object with 'runtime' sub-object for inline definitions
assert.Equal(t, "object", variants[2]["type"],
"third variant should be type object (inline definition with runtime)")
props2, ok := variants[2]["properties"].(map[string]any)
require.True(t, ok, "third variant should have properties")
assert.Contains(t, props2, "runtime",
"third variant should have a 'runtime' property for inline engine definitions")
assert.Contains(t, props2, "provider",
"third variant should have a 'provider' property for inline engine definitions")
// Identify variants by shape rather than relying on their order in the oneOf array.
var (
stringVariant map[string]any
idObjectVariant map[string]any
runtimeObjectVariant map[string]any
)
for _, v := range variants {
vType, _ := v["type"].(string)
switch vType {
case "string":
stringVariant = v
case "object":
props, _ := v["properties"].(map[string]any)
if props == nil {
continue
}
if _, hasRuntime := props["runtime"]; hasRuntime {
runtimeObjectVariant = v
} else if _, hasID := props["id"]; hasID {
idObjectVariant = v
}
}
}
require.NotNil(t, stringVariant, "schema must include a plain string variant")
require.NotNil(t, idObjectVariant, "schema must include an object-with-id variant")
require.NotNil(t, runtimeObjectVariant, "schema must include an object-with-runtime variant")
// Plain string variant (no enum — allows built-ins and custom named catalog entries)
assert.Equal(t, "string", stringVariant["type"],
"string variant should be type string")
assert.Nil(t, stringVariant["enum"],
"string variant must NOT have an enum so that named catalog entries are allowed")
// Object with 'id' field for extended engine configuration
assert.Equal(t, "object", idObjectVariant["type"],
"object-with-id variant should be type object (extended config with id)")
props1, ok := idObjectVariant["properties"].(map[string]any)
require.True(t, ok, "object-with-id variant should have properties")
assert.Contains(t, props1, "id",
"object-with-id variant should have an 'id' property")
idProp, ok := props1["id"].(map[string]any)
require.True(t, ok, "id property should be a map")
assert.Nil(t, idProp["enum"],
"id property must NOT have an enum so that named catalog entries are allowed")
// Object with 'runtime' sub-object for inline definitions
assert.Equal(t, "object", runtimeObjectVariant["type"],
"object-with-runtime variant should be type object (inline definition with runtime)")
props2, ok := runtimeObjectVariant["properties"].(map[string]any)
require.True(t, ok, "object-with-runtime variant should have properties")
assert.Contains(t, props2, "runtime",
"object-with-runtime variant should have a 'runtime' property for inline engine definitions")
assert.Contains(t, props2, "provider",
"object-with-runtime variant should have a 'provider' property for inline engine definitions")

Copilot uses AI. Check for mistakes.
}
5 changes: 5 additions & 0 deletions pkg/workflow/engine_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ func (c *EngineCatalog) Register(def *EngineDefinition) {
c.definitions[def.ID] = def
}

// Get returns the EngineDefinition for the given ID, or nil if not found.
func (c *EngineCatalog) Get(id string) *EngineDefinition {
return c.definitions[id]
}

// IDs returns a sorted list of all engine IDs in the catalog.
func (c *EngineCatalog) IDs() []string {
ids := make([]string, 0, len(c.definitions))
Expand Down
Loading
Loading