github/developer-internals
Internal gh-aw architecture: validation system design, safe output message patterns, schema validation, YAML compatibility notes, and MCP logs guardrail.
npx skills add https://github.com/github/gh-aw --skill developer-internals
Use this reference when working on the gh-aw compiler internals, validation system, safe output processing, or MCP server features.
The validation system ensures workflow configurations are correct, secure, and compatible with GitHub Actions before compilation.
graph LR
WF[Workflow] --> CV[Centralized Validation]
WF --> DV[Domain-Specific Validation]
CV --> validation.go
DV --> strict_mode.go
DV --> pip.go
DV --> npm.go
DV --> expression_safety.go
DV --> engine.go
DV --> mcp-config.go
Location: pkg/workflow/validation.go (782 lines)
Purpose: General-purpose validation that applies across the entire workflow system
Key Functions:
validateExpressionSizes() - Ensures GitHub Actions expression size limitsvalidateContainerImages() - Verifies Docker images exist and are accessiblevalidateRuntimePackages() - Validates runtime package dependenciesvalidateGitHubActionsSchema() - Validates against GitHub Actions YAML schemavalidateNoDuplicateCacheIDs() - Ensures unique cache identifiersvalidateSecretReferences() - Validates secret reference syntaxvalidateRepositoryFeatures() - Checks repository capabilitiesvalidateHTTPTransportSupport() - Validates HTTP transport configurationvalidateWorkflowRunBranches() - Validates workflow run branch configurationWhen to add validation here:
Domain-specific validation is organized into separate files:
Files: pkg/workflow/strict_mode.go, pkg/workflow/validation_strict_mode.go
Enforces security and safety constraints in strict mode:
validateStrictPermissions() - Refuses write permissionsvalidateStrictNetwork() - Requires explicit network configurationvalidateStrictMCPNetwork() - Requires network config on custom MCP serversvalidateStrictBashTools() - Refuses bash wildcard toolsFile: pkg/workflow/pip.go
Validates Python package availability on PyPI:
validatePipPackages() - Validates pip packagesvalidateUvPackages() - Validates uv packagesFile: pkg/workflow/npm.go
Validates NPX package availability on npm registry.
File: pkg/workflow/expression_safety.go
Validates GitHub Actions expression security with allowlist-based validation.
graph TD
A[New Validation Requirement] --> B{Security or strict mode?}
B -->|Yes| C[strict_mode.go]
B -->|No| D{Only applies to one domain?}
D -->|Yes| E{Domain-specific file exists?}
E -->|Yes| F[Add to domain file]
E -->|No| G[Create new domain file]
D -->|No| H{Cross-cutting concern?}
H -->|Yes| I[validation.go]
H -->|No| J{Validates external resources?}
J -->|Yes| K[Domain-specific file]
J -->|No| I
Used for security-sensitive validation with limited set of valid options:
func validateExpressionSafety(content string) error {
matches := expressionRegex.FindAllStringSubmatch(content, -1)
var unauthorizedExpressions []string
for _, match := range matches {
expression := strings.TrimSpace(match[1])
if !isAllowed(expression) {
unauthorizedExpressions = append(unauthorizedExpressions, expression)
}
}
if len(unauthorizedExpressions) > 0 {
return fmt.Errorf("unauthorized expressions: %v", unauthorizedExpressions)
}
return nil
}
Used for validating external dependencies:
func validateDockerImage(image string, verbose bool) error {
cmd := exec.Command("docker", "inspect", image)
output, err := cmd.CombinedOutput()
if err != nil {
pullCmd := exec.Command("docker", "pull", image)
if pullErr := pullCmd.Run(); pullErr != nil {
return fmt.Errorf("docker image not found: %s", image)
}
}
return nil
}
Used for configuration file validation:
func (c *Compiler) validateGitHubActionsSchema(yamlContent string) error {
schema := loadGitHubActionsSchema()
var data interface{}
if err := yaml.Unmarshal([]byte(yamlContent), &data); err != nil {
return err
}
if err := schema.Validate(data); err != nil {
return fmt.Errorf("schema validation failed: %w", err)
}
return nil
}
Used for applying multiple validation checks in sequence:
func (c *Compiler) validateStrictMode(frontmatter map[string]any, networkPermissions *NetworkPermissions) error {
if !c.strictMode {
return nil
}
if err := c.validateStrictPermissions(frontmatter); err != nil {
return err
}
if err := c.validateStrictNetwork(networkPermissions); err != nil {
return err
}
return nil
}
Safe output functions handle GitHub API write operations (creating issues, discussions, comments, PRs) from AI-generated content with consistent messaging patterns.
The following diagram illustrates how AI-generated content flows through the safe output system to GitHub API operations:
graph TD
A[AI Agent Output] --> B{Staged Mode?}
B -->|Yes| C[Generate Preview Messages]
B -->|No| D[Process Safe Output]
C --> E[Show 🎭 Staged Mode Preview]
E --> F[Display in Step Summary]
D --> G{Safe Output Type}
G -->|create-issue| H[Create GitHub Issue]
G -->|create-discussion| I[Create GitHub Discussion]
G -->|add-comment| J[Add GitHub Comment]
G -->|create-pull-request| K[Create Pull Request]
G -->|create-pr-review-comment| L[Create PR Review Comment]
G -->|update-issue| M[Update GitHub Issue]
H --> N[Apply Message Patterns]
I --> N
J --> N
K --> N
L --> N
M --> N
N --> O[Add AI Attribution Footer]
N --> P[Add Installation Instructions]
N --> Q[Add Related Items Links]
N --> R[Add Patch Preview]
O --> S[Execute GitHub API Operation]
P --> S
Q --> S
R --> S
S --> T[Generate Success Summary]
T --> U[Display in Step Summary]
Flow Stages:
Identifies content as AI-generated and links to workflow run:
> AI generated by [WorkflowName](run_url)
With triggering context:
> AI generated by [WorkflowName](run_url) for #123
>
> To add this workflow in your repository, run `gh aw add owner/repo/path@ref`. See [usage guide](https://github.github.com/gh-aw/setup/cli/).
All staged mode previews use consistent format with 🎭 emoji:
## 🎭 Staged Mode: [Operation Type] Preview
The following [items] would be [action] if staged mode was disabled:
Display git patches in pull request bodies with size limits:
<details><summary>Show patch (45 lines)</summary>
\`\`\`diff
diff --git a/src/auth.js b/src/auth.js
index 1234567..abcdefg 100644
--- a/src/auth.js
+++ b/src/auth.js
@@ -10,7 +10,10 @@ export async function login(username, password) {
- throw new Error('Login failed');
+ if (response.status === 401) {
+ throw new Error('Invalid credentials');
+ }
+ throw new Error('Login error: ' + response.statusText);
\`\`\`
</details>
Limits: Max 500 lines or 2000 characters (truncated with "... (truncated)" if exceeded)
All three JSON schema files enforce strict validation with "additionalProperties": false at the root level, preventing typos and undefined fields from silently passing validation.
| File | Purpose |
|------|---------|
| pkg/parser/schemas/main_workflow_schema.json | Validates agentic workflow frontmatter in .github/workflows/*.md files |
| pkg/parser/schemas/mcp_config_schema.json | Validates MCP (Model Context Protocol) server configuration |
When "additionalProperties": false is set at the root level, the validator rejects any properties not explicitly defined in the schema's properties section. This catches common typos:
permisions instead of permissionsengnie instead of enginetoolz instead of toolstimeout_minute instead of timeout-minutesruns_on instead of runs-onsafe_outputs instead of safe-outputs$ gh aw compile workflow-with-typo.md
✗ error: Unknown properties: toolz, engnie, permisions. Valid fields are: tools, engine, permissions, ...
graph LR
A[Read workflow frontmatter] --> B[Parse YAML]
B --> C[Validate against JSON schema]
C --> D{Valid?}
D -->|Yes| E[Continue compilation]
D -->|No| F[Provide detailed error]
F --> G[Show invalid fields]
F --> H[Show valid field names]
Schemas are embedded in the Go binary using //go:embed directives:
//go:embed schemas/main_workflow_schema.json
var mainWorkflowSchema string
This means:
make build to take effectWhen adding new fields to schemas:
make buildYAML has two major versions with incompatible boolean parsing behavior that affects workflow validation.
In YAML 1.1, certain plain strings are automatically converted to boolean values. The workflow trigger key on: can be misinterpreted as boolean true instead of string "on".
Example:
# Python yaml.safe_load (YAML 1.1 parser)
import yaml
content = """
on:
issues:
types: [opened]
"""
result = yaml.safe_load(content)
print(result)
# Output: {True: {'issues': {'types': ['opened']}}}
# ^^^^ The key is boolean True, not string "on"!
This creates false positives when validating workflows with Python-based tools.
YAML 1.2 parsers treat on, off, yes, and no as regular strings, not booleans. Only explicit boolean literals true and false are treated as booleans.
Example:
// Go goccy/go-yaml (YAML 1.2 parser) - Used by gh-aw
var result map[string]interface{}
yaml.Unmarshal([]byte(content), &result)
fmt.Printf("%+v\n", result)
// Output: map[on:map[issues:map[types:[opened]]]]
// ^^^ The key is string "on" ✓
GitHub Agentic Workflows uses goccy/go-yaml v1.18.0, which is a YAML 1.2 compliant parser:
on: is correctly parsed as a string key, not a booleangraph TD
A[Workflow File] --> B{Parser Type?}
B -->|YAML 1.1| C[Python yaml.safe_load]
B -->|YAML 1.2| D[gh-aw / goccy/go-yaml]
C --> E[on: parsed as True]
D --> F[on: parsed as string]
E --> G[False Positive]
F --> H[Correct Validation]
YAML 1.1 treats these as booleans (parsed as true or false):
Parsed as true: on, yes, y, Y, YES, Yes, ON, On
Parsed as false: off, no, n, N, NO, No, OFF, Off
YAML 1.2 treats all of the above as strings. Only these are booleans: true, false
gh aw compile workflow.md
on: trigger key. enabled: true # Explicit boolean
disabled: false # Explicit boolean
# Avoid for boolean values:
enabled: yes # Might be confusing across parsers
disabled: no # Might be confusing across parsers
github.com/goccy/go-yamlruamel.yaml (with YAML 1.2 mode)yaml package v2+ (YAML 1.2 by default)Psych (YAML 1.2 by default in Ruby 2.6+)The MCP server logs command includes an automatic guardrail to prevent overwhelming responses when fetching workflow logs.
graph TD
A[logs command called] --> B[Generate output]
B --> C{Output size check}
C -->|≤ 12000 tokens| D[Return full JSON data]
C -->|> 12000 tokens| E[Return guardrail message]
E --> F[Include schema description]
E --> G[Provide suggested jq queries]
When output is within the token limit (default: 12000 tokens), the command returns full JSON data:
{
"summary": {
"total_runs": 5,
"total_duration": "2h30m",
"total_tokens": 45000,
"total_cost": 0.23
},
"runs": [...],
"tool_usage": [...]
}
When output exceeds the token limit, the command returns structured response with:
{
"message": "⚠️ Output size (15000 tokens) exceeds the limit (12000 tokens). To reduce output size, use the 'jq' parameter with one of the suggested queries below.",
"output_tokens": 15000,
"output_size_limit": 12000,
"schema": { ... },
"suggested_queries": [
{
"description": "Get only the summary statistics",
"query": ".summary",
"example": "Use jq parameter: \".summary\""
},
...
]
}
Default limit is 12000 tokens (approximately 48KB of text). Customize using the max_tokens parameter:
{
"name": "logs",
"arguments": {
"count": 100,
"max_tokens": 20000
}
}
Token estimation uses approximately 4 characters per token (OpenAI's rule of thumb).
Filter output using jq syntax:
Get only summary statistics:
{ "jq": ".summary" }
Get run IDs and basic info:
{ "jq": ".runs | map({database_id, workflow_name, status})" }
Get only failed runs:
{ "jq": ".runs | map(select(.conclusion == \"failure\"))" }
Get high token usage runs:
{ "jq": ".runs | map(select(.token_usage > 10000))" }
Constants:
DefaultMaxMCPLogsOutputTokens: 12000 tokens (default limit)CharsPerToken: 4 characters per token (estimation factor)Files:
pkg/cli/mcp_logs_guardrail.go - Core guardrail implementationpkg/cli/mcp_logs_guardrail_test.go - Unit testspkg/cli/mcp_logs_guardrail_integration_test.go - Integration testspkg/cli/mcp_server.go - Integration with MCP serverTake github/developer-internals from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.