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

7 files changed

Lines changed: 542 additions & 35 deletions

File tree

‎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+
ifengineConfig!=nil&&engineConfig.IsInlineDefinition {
42+
iferr:=c.validateEngineInlineDefinition(engineConfig); err!=nil {
43+
returnnil, 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
Agentstring// Agent identifier for copilot --agent flag (copilot engine only)
29+
30+
// Inline definition fields (populated when engine.runtime is specified in frontmatter)
31+
IsInlineDefinitionbool// true when the engine is defined inline via engine.runtime + optional engine.provider
32+
InlineProviderIDstring// engine.provider.id (e.g. "openai", "anthropic")
33+
InlineProviderSecretstring// 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+
ifruntime, hasRuntime:=engineObj["runtime"]; hasRuntime {
97+
engineLog.Print("Found inline engine definition (engine.runtime sub-object)")
98+
config.IsInlineDefinition=true
99+
100+
ifruntimeObj, ok:=runtime.(map[string]any); ok {
101+
ifid, ok:=runtimeObj["id"].(string); ok {
102+
config.ID=id
103+
engineLog.Printf("Inline engine runtime.id: %s", config.ID)
104+
}
105+
ifversion, hasVersion:=runtimeObj["version"]; hasVersion {
106+
config.Version=stringutil.ParseVersionValue(version)
107+
}
108+
}
109+
110+
// Extract optional provider sub-object
111+
ifprovider, hasProvider:=engineObj["provider"]; hasProvider {
112+
ifproviderObj, ok:=provider.(map[string]any); ok {
113+
ifid, ok:=providerObj["id"].(string); ok {
114+
config.InlineProviderID=id
115+
}
116+
ifmodel, ok:=providerObj["model"].(string); ok {
117+
config.Model=model
118+
}
119+
ifauth, hasAuth:=providerObj["auth"]; hasAuth {
120+
ifauthObj, ok:=auth.(map[string]any); ok {
121+
ifsecret, 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+
returnconfig.ID, config
131+
}
132+
90133
// Extract required 'id' field
91134
ifid, hasID:=engineObj["id"]; hasID {
92135
ifidStr, 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-
funcengineSchemaEnums(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+
funcengineSchemaOneOfVariants(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:=rangeoneOf {
86-
v, ok:=variant.(map[string]any)
87-
if!ok {
88-
continue
89-
}
90-
ifv["type"] =="string" {
91-
rawEnum, ok:=v["enum"].([]any)
92-
if!ok {
93-
continue
94-
}
95-
enums:=make([]string, 0, len(rawEnum))
96-
for_, e:=rangerawEnum {
97-
ifs, ok:=e.(string); ok {
98-
enums=append(enums, s)
99-
}
100-
}
101-
sort.Strings(enums)
102-
returnenums
84+
variants:=make([]map[string]any, 0, len(oneOf))
85+
for_, v:=rangeoneOf {
86+
ifm, 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-
returnnil
90+
returnvariants
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-
funcTestEngineCatalogMatchesSchema(t*testing.T) {
93+
// TestEngineCatalog_BuiltInsPresent verifies that the four built-in engines are always
94+
// registered in the catalog with stable IDs.
95+
funcTestEngineCatalog_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:=rangeexpected {
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+
funcTestEngineCatalogMatchesSchema(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(idstring) *EngineDefinition {
142+
returnc.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)