Skip to content

Commit 6e8ff1a

Browse files
edenreichclaude
andauthored
feat(config): Add support for config subsection injection in skills (#92)
Enable skills to inject specific configuration subsections using dotted notation (e.g., config.email, config.database) instead of requiring the entire config object. This provides better scoped access, type safety, and separation of concerns by allowing skills to declare explicit dependencies on only the configuration they need. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 773c044 commit 6e8ff1a

4 files changed

Lines changed: 125 additions & 4 deletions

File tree

README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,104 @@ type CacheConfig struct {
576576
- `CACHE_MAX_ENTRIES="1000"`
577577
- `CACHE_TTL="3600"`
578578

579+
### Config Subsection Injection
580+
581+
In addition to injecting entire configuration objects, you can inject specific config subsections directly into skills using dotted notation. This provides type-safe access to focused configuration scopes.
582+
583+
**Example ADL Configuration:**
584+
585+
```yaml
586+
spec:
587+
config:
588+
database:
589+
connectionString: "postgresql://localhost:5432/db"
590+
maxConnections: "10"
591+
timeout: "30s"
592+
email:
593+
apiKey: ""
594+
fromAddress: "noreply@example.com"
595+
provider: "sendgrid"
596+
services:
597+
database:
598+
type: service
599+
interface: DatabaseService
600+
factory: NewDatabaseService
601+
description: PostgreSQL database service
602+
skills:
603+
- name: export_report
604+
description: "Export data and email report"
605+
inject:
606+
- logger
607+
- database
608+
- config.email # Inject only the email config subsection
609+
schema:
610+
type: object
611+
properties:
612+
recipient:
613+
type: string
614+
required: [recipient]
615+
```
616+
617+
**Generated Skill Code:**
618+
619+
```go
620+
type ExportReportSkill struct {
621+
logger *zap.Logger
622+
database database.DatabaseService
623+
email *config.EmailConfig // Type-safe access to email config only
624+
}
625+
626+
func NewExportReportSkill(
627+
logger *zap.Logger,
628+
database database.DatabaseService,
629+
email *config.EmailConfig,
630+
) server.Tool {
631+
skill := &ExportReportSkill{
632+
logger: logger,
633+
database: database,
634+
email: email,
635+
}
636+
// ...
637+
}
638+
639+
func (s *ExportReportSkill) ExportReportHandler(ctx context.Context, args map[string]any) (string, error) {
640+
// Direct access to email config subsection
641+
apiKey := s.email.APIKey
642+
fromAddress := s.email.FromAddress
643+
provider := s.email.Provider
644+
645+
// ... implementation
646+
}
647+
```
648+
649+
**Main Registration:**
650+
651+
```go
652+
// In main.go - config subsection is passed directly
653+
exportReportSkill := skills.NewExportReportSkill(l, databaseSvc, &cfg.Email)
654+
toolBox.AddTool(exportReportSkill)
655+
```
656+
657+
**Benefits of Config Subsection Injection:**
658+
659+
- **Scoped Access**: Skills only receive the configuration they need, following principle of least privilege
660+
- **Type Safety**: Compile-time validation ensures config fields exist
661+
- **Clear Dependencies**: Explicit declaration of which config sections each skill requires
662+
- **Easier Testing**: Mock specific config subsections without full config object
663+
- **Better Separation**: Skills don't have access to unrelated configuration
664+
- **Auto-Validation**: ADL CLI validates that injected config sections exist in `spec.config`
665+
666+
**Injection Patterns:**
667+
668+
```yaml
669+
inject:
670+
- logger # Built-in logger service
671+
- config # Entire config object (*config.Config)
672+
- config.database # Database config subsection (*config.DatabaseConfig)
673+
- config.email # Email config subsection (*config.EmailConfig)
674+
- myService # Custom service from spec.services
675+
```
676+
579677
### Service Architecture
580678
581679
The service injection system generates:

internal/schema/validator.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,21 @@ func (v *Validator) validateServiceReferences(adl *ADL) error {
7979
}
8080

8181
definedServices["logger"] = true
82+
definedServices["config"] = true
83+
84+
definedConfigSections := make(map[string]bool)
85+
for configSection := range adl.Spec.Config {
86+
definedConfigSections[configSection] = true
87+
}
8288

8389
for _, skill := range adl.Spec.Skills {
8490
for _, injectedService := range skill.Inject {
85-
if !definedServices[injectedService] {
91+
if len(injectedService) > 7 && injectedService[:7] == "config." {
92+
configSection := injectedService[7:]
93+
if !definedConfigSections[configSection] {
94+
return fmt.Errorf("skill '%s' injects config section '%s' that is not defined in spec.config", skill.ID, configSection)
95+
}
96+
} else if !definedServices[injectedService] {
8697
return fmt.Errorf("skill '%s' injects service '%s' that is not defined in spec.services", skill.ID, injectedService)
8798
}
8899
}
@@ -327,7 +338,7 @@ const adlSchema = `{
327338
"type": "array",
328339
"items": {
329340
"type": "string",
330-
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
341+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*$"
331342
}
332343
}
333344
}

internal/templates/languages/go/main.go.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func main() {
9494
{{- range .ADL.Spec.Skills }}
9595

9696
// Register {{ .Name }} skill
97-
{{ .Name | toCamelCase }}Skill := skills.New{{ .Name | toPascalCase }}Skill({{ range $index, $svc := .Inject }}{{ if $index }}, {{ end }}{{ if eq $svc "logger" }}l{{ else }}{{ $svc | toCamelCase }}Svc{{ end }}{{ end }})
97+
{{ .Name | toCamelCase }}Skill := skills.New{{ .Name | toPascalCase }}Skill({{ range $index, $svc := .Inject }}{{ if $index }}, {{ end }}{{ if eq $svc "logger" }}l{{ else if eq $svc "config" }}&cfg{{ else if hasPrefix "config." $svc }}&cfg.{{ $svc | trimPrefix "config." | toPascalCase }}{{ else }}{{ $svc | toCamelCase }}Svc{{ end }}{{ end }})
9898
toolBox.AddTool({{ .Name | toCamelCase }}Skill)
9999
l.Info("registered skill: {{ .Name }} ({{ .Description }})")
100100
{{- end }}

internal/templates/languages/go/skill.go.tmpl

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import (
88
{{- range $depID := .Inject }}
99
{{- if eq $depID "logger" }}
1010
zap "go.uber.org/zap"
11+
{{- else if eq $depID "config" }}
12+
config "{{ $.GoModule }}/config"
13+
{{- else if hasPrefix "config." $depID }}
14+
config "{{ $.GoModule }}/config"
1115
{{- else }}
1216
{{ $depID | toCamelCase }} "{{ $.GoModule }}/internal/{{ $depID | toSnakeCase }}"
1317
{{- end }}
@@ -19,6 +23,10 @@ type {{ .Name | toPascalCase }}Skill struct {
1923
{{- range $depID := .Inject }}
2024
{{- if eq $depID "logger" }}
2125
{{ $depID | toCamelCase }} *zap.Logger
26+
{{- else if eq $depID "config" }}
27+
{{ $depID | toCamelCase }} *config.Config
28+
{{- else if hasPrefix "config." $depID }}
29+
{{ $depID | trimPrefix "config." | toCamelCase }} *config.{{ $depID | trimPrefix "config." | toPascalCase }}Config
2230
{{- else }}
2331
{{- $svc := index $.ServiceMap $depID }}
2432
{{ $depID | toCamelCase }} {{ $depID | toCamelCase }}.{{ $svc.Interface }}
@@ -27,10 +35,14 @@ type {{ .Name | toPascalCase }}Skill struct {
2735
}
2836

2937
// New{{ .Name | toPascalCase }}Skill creates a new {{ .Name }} skill
30-
func New{{ .Name | toPascalCase }}Skill({{ range $index, $depID := .Inject }}{{ if $index }}, {{ end }}{{- if eq $depID "logger" }}{{ $depID | toCamelCase }} *zap.Logger{{- else }}{{- $svc := index $.ServiceMap $depID }}{{ $depID | toCamelCase }} {{ $depID | toCamelCase }}.{{ $svc.Interface }}{{- end }}{{ end }}) server.Tool {
38+
func New{{ .Name | toPascalCase }}Skill({{ range $index, $depID := .Inject }}{{ if $index }}, {{ end }}{{- if eq $depID "logger" }}{{ $depID | toCamelCase }} *zap.Logger{{- else if eq $depID "config" }}{{ $depID | toCamelCase }} *config.Config{{- else if hasPrefix "config." $depID }}{{ $depID | trimPrefix "config." | toCamelCase }} *config.{{ $depID | trimPrefix "config." | toPascalCase }}Config{{- else }}{{- $svc := index $.ServiceMap $depID }}{{ $depID | toCamelCase }} {{ $depID | toCamelCase }}.{{ $svc.Interface }}{{- end }}{{ end }}) server.Tool {
3139
skill := &{{ .Name | toPascalCase }}Skill{
3240
{{- range $depID := .Inject }}
41+
{{- if hasPrefix "config." $depID }}
42+
{{ $depID | trimPrefix "config." | toCamelCase }}: {{ $depID | trimPrefix "config." | toCamelCase }},
43+
{{- else }}
3344
{{ $depID | toCamelCase }}: {{ $depID | toCamelCase }},
45+
{{- end }}
3446
{{- end }}
3547
}
3648
return server.NewBasicTool(

0 commit comments

Comments
 (0)