Skip to content

Commit e72b6ec

Browse files
committed
refactor(tool): migrate RequestCredential/Debugger/DependencyAudit/Git/GitHub/ImportOrganizer to typed schemas
Single-source Schema()+Parameters() pattern, Execute parses via DecodeInput[T], wire-shape parity tests covering enums, array items, and numeric bounds. DebuggerInput exported (was debugParams).
1 parent 9e7f1e8 commit e72b6ec

8 files changed

Lines changed: 264 additions & 135 deletions

File tree

‎internal/tool/credential_gate.go‎

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,36 @@ func (RequestCredentialTool) Description() string {
4242
"available inside the sandbox. Use this when a command fails due to missing credentials."
4343
}
4444

45-
func (RequestCredentialTool) Parameters() map[string]interface{} {
46-
return map[string]interface{}{
47-
"type": "object",
48-
"properties": map[string]interface{}{
49-
"credential": map[string]interface{}{
50-
"type": "string",
51-
"description": "Credential ID to request. One of: gitconfig, kube, aws, gh, docker, gnupg, terraform.",
52-
},
53-
"reason": map[string]interface{}{
54-
"type": "string",
55-
"description": "Why this credential is needed (e.g. 'run kubectl get pods').",
56-
},
45+
// Schema returns the typed input schema. Parameters() delegates to it so the
46+
// two cannot diverge.
47+
func (RequestCredentialTool) Schema() ToolSchema {
48+
return ToolSchema{
49+
Type: "object",
50+
Properties: map[string]SchemaProperty{
51+
"credential": {Type: "string", Description: "Credential ID to request. One of: gitconfig, kube, aws, gh, docker, gnupg, terraform."},
52+
"reason": {Type: "string", Description: "Why this credential is needed (e.g. 'run kubectl get pods')."},
5753
},
58-
"required": []string{"credential", "reason"},
54+
Required: []string{"credential", "reason"},
5955
}
6056
}
6157

62-
type credentialInput struct {
58+
func (RequestCredentialTool) Parameters() map[string]interface{} {
59+
return requestCredentialSchema.ToJSONSchema()
60+
}
61+
62+
// requestCredentialSchema is the single source of truth for RequestCredential's input schema.
63+
var requestCredentialSchema = RequestCredentialTool{}.Schema()
64+
65+
// RequestCredentialInput is the typed input for RequestCredentialTool.
66+
type RequestCredentialInput struct {
6367
Credential string `json:"credential"`
6468
Reason string `json:"reason"`
6569
}
6670

6771
func (t RequestCredentialTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
68-
var p credentialInput
69-
if err := json.Unmarshal(input, &p); err != nil {
70-
return "", fmt.Errorf("invalid RequestCredential input: %w", err)
72+
p, err := DecodeInput[RequestCredentialInput]("RequestCredential", input)
73+
if err != nil {
74+
return "", err
7175
}
7276
if p.Credential == "" {
7377
return "", fmt.Errorf("credential is required")

‎internal/tool/debugger.go‎

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,44 +25,40 @@ func (DebuggerTool) Description() string {
2525
Prefer this over adding print statements when you need to understand runtime state.`
2626
}
2727

28-
func (DebuggerTool) Parameters() map[string]interface{} {
29-
return map[string]interface{}{
30-
"type": "object",
31-
"properties": map[string]interface{}{
32-
"action": map[string]interface{}{
33-
"type": "string",
34-
"description": "Action: breakpoint, run, inspect, step, continue, stack",
35-
"enum": []string{"breakpoint", "run", "inspect", "step", "continue", "stack"},
36-
},
37-
"file": map[string]interface{}{
38-
"type": "string",
39-
"description": "File path (for breakpoint action)",
40-
},
41-
"line": map[string]interface{}{
42-
"type": "integer",
43-
"description": "Line number (for breakpoint action)",
44-
},
45-
"expression": map[string]interface{}{
46-
"type": "string",
47-
"description": "Expression to evaluate (for inspect action)",
48-
},
28+
// Schema returns the typed input schema. Parameters() delegates to it so the
29+
// two cannot diverge.
30+
func (DebuggerTool) Schema() ToolSchema {
31+
return ToolSchema{
32+
Type: "object",
33+
Properties: map[string]SchemaProperty{
34+
"action": {Type: "string", Enum: []interface{}{"breakpoint", "run", "inspect", "step", "continue", "stack"}, Description: "Action: breakpoint, run, inspect, step, continue, stack"},
35+
"file": {Type: "string", Description: "File path (for breakpoint action)"},
36+
"line": {Type: "integer", Description: "Line number (for breakpoint action)"},
37+
"expression": {Type: "string", Description: "Expression to evaluate (for inspect action)"},
4938
},
50-
"required": []string{"action"},
39+
Required: []string{"action"},
5140
}
5241
}
5342

54-
// debugParams holds the parsed input parameters.
55-
type debugParams struct {
43+
func (DebuggerTool) Parameters() map[string]interface{} {
44+
return debuggerSchema.ToJSONSchema()
45+
}
46+
47+
// debuggerSchema is the single source of truth for Debugger's input schema.
48+
var debuggerSchema = DebuggerTool{}.Schema()
49+
50+
// DebuggerInput is the typed input for DebuggerTool.
51+
type DebuggerInput struct {
5652
Action string `json:"action"`
5753
File string `json:"file"`
5854
Line int `json:"line"`
5955
Expression string `json:"expression"`
6056
}
6157

6258
func (DebuggerTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
63-
var p debugParams
64-
if err := json.Unmarshal(input, &p); err != nil {
65-
return "", fmt.Errorf("invalid input: %w", err)
59+
p, err := DecodeInput[DebuggerInput]("Debug", input)
60+
if err != nil {
61+
return "", err
6662
}
6763

6864
if err := validateDebugParams(p); err != nil {
@@ -88,7 +84,7 @@ func (DebuggerTool) Execute(ctx context.Context, input json.RawMessage) (string,
8884
}
8985

9086
// validateDebugParams ensures required fields are present for each action.
91-
func validateDebugParams(p debugParams) error {
87+
func validateDebugParams(p DebuggerInput) error {
9288
switch p.Action {
9389
case "":
9490
return fmt.Errorf("action is required")
@@ -126,7 +122,7 @@ func detectDebugLanguage(file string) string {
126122
}
127123
}
128124

129-
func debugBreakpoint(ctx context.Context, p debugParams) (string, error) {
125+
func debugBreakpoint(ctx context.Context, p DebuggerInput) (string, error) {
130126
lang := detectDebugLanguage(p.File)
131127
switch lang {
132128
case "go":
@@ -148,7 +144,7 @@ func debugBreakpoint(ctx context.Context, p debugParams) (string, error) {
148144
}
149145
}
150146

151-
func debugRun(ctx context.Context, p debugParams) (string, error) {
147+
func debugRun(ctx context.Context, p DebuggerInput) (string, error) {
152148
file := p.File
153149
if file == "" {
154150
file = "."
@@ -184,7 +180,7 @@ func debugRun(ctx context.Context, p debugParams) (string, error) {
184180
}
185181
}
186182

187-
func debugInspect(ctx context.Context, p debugParams) (string, error) {
183+
func debugInspect(ctx context.Context, p DebuggerInput) (string, error) {
188184
// For Go, use dlv eval.
189185
cmd := exec.CommandContext(ctx, "dlv", "eval", p.Expression) // #nosec G204 -- debugger/interpreter invocation with file path or expression from tool params
190186
out, err := cmd.CombinedOutput()

‎internal/tool/debugger_test.go‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,73 +42,73 @@ func TestDebuggerTool_Metadata(t *testing.T) {
4242
func TestDebuggerTool_ValidateParams(t *testing.T) {
4343
tests := []struct {
4444
name string
45-
params debugParams
45+
params DebuggerInput
4646
wantErr bool
4747
errMsg string
4848
}{
4949
{
5050
name: "empty action",
51-
params: debugParams{},
51+
params: DebuggerInput{},
5252
wantErr: true,
5353
errMsg: "action is required",
5454
},
5555
{
5656
name: "breakpoint without file",
57-
params: debugParams{Action: "breakpoint", Line: 10},
57+
params: DebuggerInput{Action: "breakpoint", Line: 10},
5858
wantErr: true,
5959
errMsg: "file is required",
6060
},
6161
{
6262
name: "breakpoint without line",
63-
params: debugParams{Action: "breakpoint", File: "main.go"},
63+
params: DebuggerInput{Action: "breakpoint", File: "main.go"},
6464
wantErr: true,
6565
errMsg: "line must be a positive integer",
6666
},
6767
{
6868
name: "breakpoint with negative line",
69-
params: debugParams{Action: "breakpoint", File: "main.go", Line: -1},
69+
params: DebuggerInput{Action: "breakpoint", File: "main.go", Line: -1},
7070
wantErr: true,
7171
errMsg: "line must be a positive integer",
7272
},
7373
{
7474
name: "inspect without expression",
75-
params: debugParams{Action: "inspect"},
75+
params: DebuggerInput{Action: "inspect"},
7676
wantErr: true,
7777
errMsg: "expression is required",
7878
},
7979
{
8080
name: "valid breakpoint",
81-
params: debugParams{Action: "breakpoint", File: "main.go", Line: 10},
81+
params: DebuggerInput{Action: "breakpoint", File: "main.go", Line: 10},
8282
wantErr: false,
8383
},
8484
{
8585
name: "valid inspect",
86-
params: debugParams{Action: "inspect", Expression: "x + 1"},
86+
params: DebuggerInput{Action: "inspect", Expression: "x + 1"},
8787
wantErr: false,
8888
},
8989
{
9090
name: "valid run",
91-
params: debugParams{Action: "run"},
91+
params: DebuggerInput{Action: "run"},
9292
wantErr: false,
9393
},
9494
{
9595
name: "valid step",
96-
params: debugParams{Action: "step"},
96+
params: DebuggerInput{Action: "step"},
9797
wantErr: false,
9898
},
9999
{
100100
name: "valid continue",
101-
params: debugParams{Action: "continue"},
101+
params: DebuggerInput{Action: "continue"},
102102
wantErr: false,
103103
},
104104
{
105105
name: "valid stack",
106-
params: debugParams{Action: "stack"},
106+
params: DebuggerInput{Action: "stack"},
107107
wantErr: false,
108108
},
109109
{
110110
name: "unknown action",
111-
params: debugParams{Action: "dance"},
111+
params: DebuggerInput{Action: "dance"},
112112
wantErr: true,
113113
errMsg: "unknown action",
114114
},

‎internal/tool/dependency_audit.go‎

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,38 @@ func (DependencyAuditTool) Description() string {
2424
return "Audit dependency integrity and report outdated packages without installing or changing anything. Supports Go, npm, Python, and Cargo projects with structured results."
2525
}
2626

27-
func (DependencyAuditTool) Parameters() map[string]interface{} {
28-
return map[string]interface{}{
29-
"type": "object",
30-
"properties": map[string]interface{}{
31-
"action": map[string]interface{}{
32-
"type": "string",
33-
"enum": []string{"check", "outdated", "all"},
34-
"description": "check validates dependency integrity; outdated reports available updates; all runs both.",
35-
},
36-
"path": map[string]interface{}{"type": "string", "description": "Project directory (default: session working directory)."},
37-
"timeout_seconds": map[string]interface{}{"type": "integer", "minimum": 1, "maximum": 300, "description": "Per-command timeout (default 60 seconds)."},
27+
// Schema returns the typed input schema. Parameters() delegates to it so the
28+
// two cannot diverge.
29+
func (DependencyAuditTool) Schema() ToolSchema {
30+
return ToolSchema{
31+
Type: "object",
32+
Properties: map[string]SchemaProperty{
33+
"action": {Type: "string", Enum: []interface{}{"check", "outdated", "all"}, Description: "check validates dependency integrity; outdated reports available updates; all runs both."},
34+
"path": {Type: "string", Description: "Project directory (default: session working directory)."},
35+
"timeout_seconds": {Type: "integer", Minimum: 1, Maximum: 300, Description: "Per-command timeout (default 60 seconds)."},
3836
},
39-
"required": []string{"action"},
37+
Required: []string{"action"},
4038
}
4139
}
4240

41+
func (DependencyAuditTool) Parameters() map[string]interface{} {
42+
return dependencyAuditSchema.ToJSONSchema()
43+
}
44+
45+
// dependencyAuditSchema is the single source of truth for DependencyAudit's input schema.
46+
var dependencyAuditSchema = DependencyAuditTool{}.Schema()
47+
48+
// DependencyAuditInput is the typed input for DependencyAuditTool.
49+
type DependencyAuditInput struct {
50+
Action string `json:"action"`
51+
Path string `json:"path"`
52+
TimeoutSeconds int `json:"timeout_seconds"`
53+
}
54+
4355
func (DependencyAuditTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
44-
var params struct {
45-
Action string `json:"action"`
46-
Path string `json:"path"`
47-
TimeoutSeconds int `json:"timeout_seconds"`
48-
}
49-
if err := json.Unmarshal(input, &params); err != nil {
50-
return "", fmt.Errorf("invalid input: %w", err)
56+
params, err := DecodeInput[DependencyAuditInput]("DependencyAudit", input)
57+
if err != nil {
58+
return "", err
5159
}
5260
params.Action = strings.ToLower(strings.TrimSpace(params.Action))
5361
if params.Action != "check" && params.Action != "outdated" && params.Action != "all" {
@@ -67,7 +75,7 @@ func (DependencyAuditTool) Execute(ctx context.Context, input json.RawMessage) (
6775
root, _ = os.Getwd()
6876
}
6977
}
70-
root, err := filepath.Abs(root)
78+
root, err = filepath.Abs(root)
7179
if err != nil {
7280
return "", fmt.Errorf("resolve project path: %w", err)
7381
}

‎internal/tool/git.go‎

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,30 +44,35 @@ func (GitTool) Description() string {
4444
return "Run git commands in the project worktree. Supports: status, diff, log, show, branch, checkout, add, commit, pull, push, fetch, stash, rebase, merge, reset, tag."
4545
}
4646

47-
func (GitTool) Parameters() map[string]interface{} {
48-
return map[string]interface{}{
49-
"type": "object",
50-
"properties": map[string]interface{}{
51-
"subcommand": map[string]interface{}{
52-
"type": "string",
53-
"description": "Git subcommand to run (e.g. status, diff, add, commit)",
54-
},
55-
"args": map[string]interface{}{
56-
"type": "array",
57-
"items": map[string]interface{}{"type": "string"},
58-
"description": "Arguments for the subcommand (e.g. [\"-m\", \"fix bug\"])",
59-
},
47+
// Schema returns the typed input schema. Parameters() delegates to it so the
48+
// two cannot diverge.
49+
func (GitTool) Schema() ToolSchema {
50+
return ToolSchema{
51+
Type: "object",
52+
Properties: map[string]SchemaProperty{
53+
"subcommand": {Type: "string", Description: "Git subcommand to run (e.g. status, diff, add, commit)"},
54+
"args": {Type: "array", Items: &SchemaProperty{Type: "string"}, Description: "Arguments for the subcommand (e.g. [\"-m\", \"fix bug\"])"},
6055
},
61-
"required": []string{"subcommand"},
56+
Required: []string{"subcommand"},
6257
}
6358
}
6459

60+
func (GitTool) Parameters() map[string]interface{} {
61+
return gitSchema.ToJSONSchema()
62+
}
63+
64+
// gitSchema is the single source of truth for Git's input schema.
65+
var gitSchema = GitTool{}.Schema()
66+
67+
// GitInput is the typed input for GitTool.
68+
type GitInput struct {
69+
Subcommand string `json:"subcommand"`
70+
Args []string `json:"args,omitempty"`
71+
}
72+
6573
func (t GitTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
66-
var in struct {
67-
Subcommand string `json:"subcommand"`
68-
Args []string `json:"args,omitempty"`
69-
}
70-
if err := json.Unmarshal(input, &in); err != nil {
74+
in, err := DecodeInput[GitInput]("Git", input)
75+
if err != nil {
7176
return "", err
7277
}
7378

0 commit comments

Comments
 (0)