Skip to content

Commit 95f4259

Browse files
authored
Phase 3: Extend schema and parser for inline and catalog-defined engine definitions (#20469)
1 parent b29e196 commit 95f4259

File tree

7 files changed

+542
-35
lines changed

7 files changed

+542
-35
lines changed

pkg/parser/schemas/main_workflow_schema.json

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7778,22 +7778,41 @@
77787778
"group": "gh-aw-claude",
77797779
"cancel-in-progress": false
77807780
}
7781+
},
7782+
{
7783+
"runtime": {
7784+
"id": "codex",
7785+
"version": "0.105.0"
7786+
},
7787+
"provider": {
7788+
"id": "openai",
7789+
"model": "gpt-5",
7790+
"auth": {
7791+
"secret": "OPENAI_API_KEY"
7792+
}
7793+
}
7794+
},
7795+
{
7796+
"runtime": {
7797+
"id": "claude"
7798+
},
7799+
"provider": {
7800+
"model": "claude-3-7-sonnet-20250219"
7801+
}
77817802
}
77827803
],
77837804
"oneOf": [
77847805
{
77857806
"type": "string",
7786-
"enum": ["claude", "codex", "copilot", "gemini"],
7787-
"description": "Simple engine name: 'claude' (default, Claude Code), 'copilot' (GitHub Copilot CLI), 'codex' (OpenAI Codex CLI), or 'gemini' (Google Gemini CLI)"
7807+
"description": "Engine name: built-in ('claude', 'codex', 'copilot', 'gemini') or a named catalog entry"
77887808
},
77897809
{
77907810
"type": "object",
77917811
"description": "Extended engine configuration object with advanced options for model selection, turn limiting, environment variables, and custom steps",
77927812
"properties": {
77937813
"id": {
77947814
"type": "string",
7795-
"enum": ["claude", "codex", "copilot", "gemini"],
7796-
"description": "AI engine identifier: 'claude' (Claude Code), 'codex' (OpenAI Codex CLI), 'copilot' (GitHub Copilot CLI), or 'gemini' (Google Gemini CLI)"
7815+
"description": "AI engine identifier: built-in ('claude', 'codex', 'copilot', 'gemini') or a named catalog entry"
77977816
},
77987817
"version": {
77997818
"type": ["string", "number"],
@@ -7914,6 +7933,61 @@
79147933
},
79157934
"required": ["id"],
79167935
"additionalProperties": false
7936+
},
7937+
{
7938+
"type": "object",
7939+
"description": "Inline engine definition: specifies a runtime adapter and optional provider settings directly in the workflow frontmatter, without requiring a named catalog entry",
7940+
"properties": {
7941+
"runtime": {
7942+
"type": "object",
7943+
"description": "Runtime adapter reference for the inline engine definition",
7944+
"properties": {
7945+
"id": {
7946+
"type": "string",
7947+
"description": "Runtime adapter identifier (e.g. 'codex', 'claude', 'copilot', 'gemini')",
7948+
"examples": ["codex", "claude", "copilot", "gemini"]
7949+
},
7950+
"version": {
7951+
"type": ["string", "number"],
7952+
"description": "Optional version of the runtime adapter (e.g. '0.105.0', 'beta')",
7953+
"examples": ["0.105.0", "beta", "latest"]
7954+
}
7955+
},
7956+
"required": ["id"],
7957+
"additionalProperties": false
7958+
},
7959+
"provider": {
7960+
"type": "object",
7961+
"description": "Optional provider configuration for the inline engine definition",
7962+
"properties": {
7963+
"id": {
7964+
"type": "string",
7965+
"description": "Provider identifier (e.g. 'openai', 'anthropic', 'github', 'google')",
7966+
"examples": ["openai", "anthropic", "github", "google"]
7967+
},
7968+
"model": {
7969+
"type": "string",
7970+
"description": "Optional specific LLM model to use (e.g. 'gpt-5', 'claude-3-5-sonnet-20241022')",
7971+
"examples": ["gpt-5", "claude-3-5-sonnet-20241022", "gpt-4o"]
7972+
},
7973+
"auth": {
7974+
"type": "object",
7975+
"description": "Authentication configuration for the provider",
7976+
"properties": {
7977+
"secret": {
7978+
"type": "string",
7979+
"description": "Name of the GitHub Actions secret that contains the API key for this provider",
7980+
"examples": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "CUSTOM_API_KEY"]
7981+
}
7982+
},
7983+
"additionalProperties": false
7984+
}
7985+
},
7986+
"additionalProperties": false
7987+
}
7988+
},
7989+
"required": ["runtime"],
7990+
"additionalProperties": false
79177991
}
79187992
]
79197993
},

pkg/workflow/compiler_orchestrator_engine.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ func (c *Compiler) setupEngineAndImports(result *parser.FrontmatterResult, clean
3636
// Extract AI engine setting from frontmatter
3737
engineSetting, engineConfig := c.ExtractEngineConfig(result.Frontmatter)
3838

39+
// Validate and register inline engine definitions (engine.runtime sub-object).
40+
// Must happen before catalog resolution so the inline definition is visible to Resolve().
41+
if engineConfig != nil && engineConfig.IsInlineDefinition {
42+
if err := c.validateEngineInlineDefinition(engineConfig); err != nil {
43+
return nil, err
44+
}
45+
c.registerInlineEngineDefinition(engineConfig)
46+
}
47+
3948
// Extract network permissions from frontmatter
4049
networkPermissions := c.extractNetworkPermissions(result.Frontmatter)
4150

pkg/workflow/engine.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ type EngineConfig struct {
2626
Args []string
2727
Firewall *FirewallConfig // AWF firewall configuration
2828
Agent string // Agent identifier for copilot --agent flag (copilot engine only)
29+
30+
// Inline definition fields (populated when engine.runtime is specified in frontmatter)
31+
IsInlineDefinition bool // true when the engine is defined inline via engine.runtime + optional engine.provider
32+
InlineProviderID string // engine.provider.id (e.g. "openai", "anthropic")
33+
InlineProviderSecret string // engine.provider.auth.secret (name of the GitHub Actions secret for the provider API key)
2934
}
3035

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

95+
// Detect inline definition: engine.runtime sub-object present instead of engine.id
96+
if runtime, hasRuntime := engineObj["runtime"]; hasRuntime {
97+
engineLog.Print("Found inline engine definition (engine.runtime sub-object)")
98+
config.IsInlineDefinition = true
99+
100+
if runtimeObj, ok := runtime.(map[string]any); ok {
101+
if id, ok := runtimeObj["id"].(string); ok {
102+
config.ID = id
103+
engineLog.Printf("Inline engine runtime.id: %s", config.ID)
104+
}
105+
if version, hasVersion := runtimeObj["version"]; hasVersion {
106+
config.Version = stringutil.ParseVersionValue(version)
107+
}
108+
}
109+
110+
// Extract optional provider sub-object
111+
if provider, hasProvider := engineObj["provider"]; hasProvider {
112+
if providerObj, ok := provider.(map[string]any); ok {
113+
if id, ok := providerObj["id"].(string); ok {
114+
config.InlineProviderID = id
115+
}
116+
if model, ok := providerObj["model"].(string); ok {
117+
config.Model = model
118+
}
119+
if auth, hasAuth := providerObj["auth"]; hasAuth {
120+
if authObj, ok := auth.(map[string]any); ok {
121+
if secret, ok := authObj["secret"].(string); ok {
122+
config.InlineProviderSecret = secret
123+
}
124+
}
125+
}
126+
}
127+
}
128+
129+
engineLog.Printf("Extracted inline engine definition: runtimeID=%s, providerID=%s", config.ID, config.InlineProviderID)
130+
return config.ID, config
131+
}
132+
90133
// Extract required 'id' field
91134
if id, hasID := engineObj["id"]; hasID {
92135
if idStr, ok := id.(string); ok {

pkg/workflow/engine_catalog_test.go

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ func TestEngineCatalog_All(t *testing.T) {
6161
}
6262
}
6363

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

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

84-
// The first oneOf variant is the plain string enum
85-
for _, variant := range oneOf {
86-
v, ok := variant.(map[string]any)
87-
if !ok {
88-
continue
89-
}
90-
if v["type"] == "string" {
91-
rawEnum, ok := v["enum"].([]any)
92-
if !ok {
93-
continue
94-
}
95-
enums := make([]string, 0, len(rawEnum))
96-
for _, e := range rawEnum {
97-
if s, ok := e.(string); ok {
98-
enums = append(enums, s)
99-
}
100-
}
101-
sort.Strings(enums)
102-
return enums
84+
variants := make([]map[string]any, 0, len(oneOf))
85+
for _, v := range oneOf {
86+
if m, ok := v.(map[string]any); ok {
87+
variants = append(variants, m)
10388
}
10489
}
105-
t.Fatal("could not find string enum in engine_config oneOf")
106-
return nil
90+
return variants
10791
}
10892

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

115-
catalogIDs := catalog.IDs() // already sorted
116-
schemaEnums := engineSchemaEnums(t)
99+
expected := []string{"claude", "codex", "copilot", "gemini"}
100+
catalogIDs := catalog.IDs()
101+
for _, id := range expected {
102+
assert.Contains(t, catalogIDs, id,
103+
"built-in engine %q must always be present in the catalog", id)
104+
}
105+
}
117106

118-
assert.Equal(t, catalogIDs, schemaEnums,
119-
"schema engine enum must match catalog IDs exactly — run 'make build' after updating the schema")
107+
// TestEngineCatalogMatchesSchema asserts that the engine_config schema has the expected
108+
// structure: a plain-string variant (for built-ins and named catalog entries), an
109+
// object-with-id variant, and an inline-definition variant (object-with-runtime).
110+
// A failure here means the schema structure has changed unexpectedly.
111+
func TestEngineCatalogMatchesSchema(t *testing.T) {
112+
variants := engineSchemaOneOfVariants(t)
113+
114+
require.Len(t, variants, 3, "engine_config oneOf should have exactly 3 variants: string, object-with-id, object-with-runtime")
115+
116+
// Variant 0: plain string (no enum — allows built-ins and custom named catalog entries)
117+
assert.Equal(t, "string", variants[0]["type"],
118+
"first variant should be type string")
119+
assert.Nil(t, variants[0]["enum"],
120+
"string variant must NOT have an enum so that named catalog entries are allowed")
121+
122+
// Variant 1: object with 'id' field for extended engine configuration
123+
assert.Equal(t, "object", variants[1]["type"],
124+
"second variant should be type object (extended config with id)")
125+
props1, ok := variants[1]["properties"].(map[string]any)
126+
require.True(t, ok, "second variant should have properties")
127+
assert.Contains(t, props1, "id",
128+
"second variant should have an 'id' property")
129+
idProp, ok := props1["id"].(map[string]any)
130+
require.True(t, ok, "id property should be a map")
131+
assert.Nil(t, idProp["enum"],
132+
"id property must NOT have an enum so that named catalog entries are allowed")
133+
134+
// Variant 2: object with 'runtime' sub-object for inline definitions
135+
assert.Equal(t, "object", variants[2]["type"],
136+
"third variant should be type object (inline definition with runtime)")
137+
props2, ok := variants[2]["properties"].(map[string]any)
138+
require.True(t, ok, "third variant should have properties")
139+
assert.Contains(t, props2, "runtime",
140+
"third variant should have a 'runtime' property for inline engine definitions")
141+
assert.Contains(t, props2, "provider",
142+
"third variant should have a 'provider' property for inline engine definitions")
120143
}

pkg/workflow/engine_definition.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ func (c *EngineCatalog) Register(def *EngineDefinition) {
137137
c.definitions[def.ID] = def
138138
}
139139

140+
// Get returns the EngineDefinition for the given ID, or nil if not found.
141+
func (c *EngineCatalog) Get(id string) *EngineDefinition {
142+
return c.definitions[id]
143+
}
144+
140145
// IDs returns a sorted list of all engine IDs in the catalog.
141146
func (c *EngineCatalog) IDs() []string {
142147
ids := make([]string, 0, len(c.definitions))

0 commit comments

Comments
 (0)