diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 357b4d7f24b..d5d1fd3b29c 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -48,6 +48,11 @@ "version": "v5.0.5", "sha": "27d5ce7f107fe9357f9df03efb73ab90386fccae" }, + "actions/checkout@v4": { + "repo": "actions/checkout", + "version": "v4", + "sha": "34e114876b0b11c390a56381ad16ebd13914f8d5" + }, "actions/checkout@v6.0.2": { "repo": "actions/checkout", "version": "v6.0.2", @@ -88,6 +93,11 @@ "version": "v6.4.0", "sha": "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" }, + "actions/setup-python@v5": { + "repo": "actions/setup-python", + "version": "v5", + "sha": "a26af69be951a213d495a4c3e4e4022e16d87065" + }, "actions/setup-python@v6.2.0": { "repo": "actions/setup-python", "version": "v6.2.0", @@ -153,6 +163,11 @@ "version": "v9.0.11", "sha": "56718cc2bafc8c0fc5fd66751e0b32a33ed36c22" }, + "github/stale-repos@v9.0.8": { + "repo": "github/stale-repos", + "version": "v9.0.8", + "sha": "5f2e18fc5432823f96c1feb69327f665c2acab59" + }, "haskell-actions/setup@v2.11.0": { "repo": "haskell-actions/setup", "version": "v2.11.0", diff --git a/.github/workflows/daily-geo-optimizer.lock.yml b/.github/workflows/daily-geo-optimizer.lock.yml index df6482847ce..bbe3efb6ced 100644 --- a/.github/workflows/daily-geo-optimizer.lock.yml +++ b/.github/workflows/daily-geo-optimizer.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"27a7b4958a6add9df71c41451c3c21f040c6bbb617f15e47162b869981485e20","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0 (source v5)"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"34e114876b0b11c390a56381ad16ebd13914f8d5","version":"v4"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a26af69be951a213d495a4c3e4e4022e16d87065","version":"v5"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -41,13 +41,13 @@ # - GITHUB_TOKEN # # Custom actions used: +# - actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v4) # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 -# - actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 (source v5) +# - actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 # # Container images used: @@ -1367,9 +1367,9 @@ jobs: GH_HOST="${GH_HOST#http://}" echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v4) + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 (source v5) + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.11" - name: Install geo-optimizer-skill diff --git a/.github/workflows/smoke-otel-backends.lock.yml b/.github/workflows/smoke-otel-backends.lock.yml index 89ae744243e..41b1154bea1 100644 --- a/.github/workflows/smoke-otel-backends.lock.yml +++ b/.github/workflows/smoke-otel-backends.lock.yml @@ -773,7 +773,7 @@ jobs: "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core", "headers": { "DD_API_KEY": "\${DD_API_KEY}", - "DD_APPLICATION_KEY": "\${DD_APPLICATION_KEY}", + "DD_APPLICATION_KEY": "\${DD_APP_KEY}", "DD_SITE": "\${DD_SITE}" }, "tools": [ diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml index 25038b14f96..aa42ddf712b 100644 --- a/.github/workflows/stale-repo-identifier.lock.yml +++ b/.github/workflows/stale-repo-identifier.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"ca710270b7c296c9ee15b284e32ab507e83b565725c762d3a6bac4ecd1d5c749","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/stale-repos","sha":"56718cc2bafc8c0fc5fd66751e0b32a33ed36c22","version":"v9.0.11 (source v9.0.8)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/stale-repos","sha":"5f2e18fc5432823f96c1feb69327f665c2acab59","version":"v9.0.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -57,7 +57,7 @@ # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/stale-repos@56718cc2bafc8c0fc5fd66751e0b32a33ed36c22 # v9.0.11 (source v9.0.8) +# - github/stale-repos@5f2e18fc5432823f96c1feb69327f665c2acab59 # v9.0.8 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.49 @@ -491,7 +491,7 @@ jobs: retention-days: 30 - name: Run stale-repos id: stale-repos - uses: github/stale-repos@56718cc2bafc8c0fc5fd66751e0b32a33ed36c22 # v9.0.11 (source v9.0.8) + uses: github/stale-repos@5f2e18fc5432823f96c1feb69327f665c2acab59 # v9.0.8 env: ADDITIONAL_METRICS: release,pr EXEMPT_TOPICS: keep,template diff --git a/actions/setup/md/agentic_workflows_guide.md b/actions/setup/md/agentic_workflows_guide.md index 5be95663512..5ee4197430b 100644 --- a/actions/setup/md/agentic_workflows_guide.md +++ b/actions/setup/md/agentic_workflows_guide.md @@ -27,5 +27,6 @@ Call all operations as MCP tools with JSON parameters. - Logs are saved to `/tmp/gh-aw/aw-mcp/logs/` #### `audit` — Inspect a specific run -- `run_id_or_url`: numeric run ID, run URL, job URL, or job URL with step anchor +- `run_id`: run ID or run/job URL (including step anchors), as string or number (alias for `run_id_or_url`) +- `run_id_or_url`: run ID or run/job URL (including step anchors), as string or number diff --git a/pkg/cli/mcp_argument_validation_test.go b/pkg/cli/mcp_argument_validation_test.go index 91444ded156..ff213c22dae 100644 --- a/pkg/cli/mcp_argument_validation_test.go +++ b/pkg/cli/mcp_argument_validation_test.go @@ -351,6 +351,7 @@ func TestMCPToolParams(t *testing.T) { case "audit": // experiment and variant were previously missing from the hardcoded map; // reflection must now include them automatically. + assert.Contains(t, toolParams, "run_id", "audit tool must include 'run_id' alias param") assert.Contains(t, toolParams, "experiment", "audit tool must include 'experiment' param") assert.Contains(t, toolParams, "variant", "audit tool must include 'variant' param") } diff --git a/pkg/cli/mcp_tools_privileged.go b/pkg/cli/mcp_tools_privileged.go index a4e9b0d9350..031f03b7a97 100644 --- a/pkg/cli/mcp_tools_privileged.go +++ b/pkg/cli/mcp_tools_privileged.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "os" "os/exec" "strconv" @@ -250,7 +251,8 @@ from where the previous request stopped due to timeout.`, // auditArgs holds the input parameters for the audit tool. type auditArgs struct { - RunIDOrURL string `json:"run_id_or_url,omitempty" jsonschema:"Deprecated: use run_ids_or_urls instead. Single GitHub Actions workflow run ID or URL."` + RunID any `json:"run_id,omitempty" jsonschema:"Alias for run_id_or_url. Accepts run ID or run/job URL (including step anchors). String or number."` + RunIDOrURL any `json:"run_id_or_url,omitempty" jsonschema:"Deprecated: use run_ids_or_urls instead. Accepts run ID or run/job URL (including step anchors). String or number."` RunIDsOrURLs []string `json:"run_ids_or_urls,omitempty" jsonschema:"One or more workflow run IDs or URLs. Single item: detailed audit report. Multiple items: diff mode with first as base (see tool description for accepted formats)."` Artifacts []string `json:"artifacts,omitempty" jsonschema:"Artifact sets to download (default: all). Valid sets: all, activation, agent, detection, firewall, github-api, mcp"` MaxTokens int `json:"max_tokens,omitempty" jsonschema:"Deprecated: accepted for backward compatibility but ignored."` @@ -258,6 +260,30 @@ type auditArgs struct { Variant string `json:"variant,omitempty" jsonschema:"Filter to runs assigned this specific variant value. Requires experiment to be set."` } +// normalizeAuditRunInput converts a single-run audit input (run_id or +// run_id_or_url) from supported MCP argument types into the CLI positional +// argument format. The bool return indicates whether a non-empty value was +// provided. +func normalizeAuditRunInput(input any, fieldName string) (string, bool, error) { + switch v := input.(type) { + case nil: + return "", false, nil + case string: + if strings.TrimSpace(v) == "" { + return "", false, nil + } + return v, true, nil + case float64: + return strconv.FormatFloat(v, 'f', -1, 64), true, nil + case int: + return strconv.Itoa(v), true, nil + case int64: + return strconv.FormatInt(v, 10), true, nil + default: + return "", false, fmt.Errorf("%s must be a string or number", fieldName) + } +} + // registerAuditTool registers the audit tool with the MCP server. // The audit tool requires write+ access and checks actor permissions. // Returns an error if schema generation fails. @@ -329,13 +355,28 @@ Multi-run diff returns JSON describing changes between the base and each compari } // Resolve the list of run IDs/URLs to pass to the audit command. - // run_ids_or_urls takes precedence; fall back to the deprecated run_id_or_url field. + // run_ids_or_urls takes precedence; fall back to run_id, then deprecated run_id_or_url. runItems := args.RunIDsOrURLs - if len(runItems) == 0 && args.RunIDOrURL != "" { - runItems = []string{args.RunIDOrURL} + if len(runItems) == 0 { + runID, hasRunID, err := normalizeAuditRunInput(args.RunID, "run_id") + if err != nil { + return nil, nil, newMCPError(jsonrpc.CodeInvalidParams, err.Error(), nil) + } + if hasRunID { + runItems = []string{runID} + } + } + if len(runItems) == 0 { + runIDOrURL, hasRunIDOrURL, err := normalizeAuditRunInput(args.RunIDOrURL, "run_id_or_url") + if err != nil { + return nil, nil, newMCPError(jsonrpc.CodeInvalidParams, err.Error(), nil) + } + if hasRunIDOrURL { + runItems = []string{runIDOrURL} + } } if len(runItems) == 0 { - return nil, nil, newMCPError(jsonrpc.CodeInvalidParams, "at least one run ID or URL must be provided via run_ids_or_urls or run_id_or_url", nil) + return nil, nil, newMCPError(jsonrpc.CodeInvalidParams, "at least one run ID or URL must be provided via run_ids_or_urls, run_id, or run_id_or_url", nil) } // Build command arguments. diff --git a/pkg/cli/mcp_tools_privileged_test.go b/pkg/cli/mcp_tools_privileged_test.go index bc009479705..eae9f260f41 100644 --- a/pkg/cli/mcp_tools_privileged_test.go +++ b/pkg/cli/mcp_tools_privileged_test.go @@ -379,6 +379,96 @@ func TestAuditTool_AcceptsDeprecatedMaxTokensParameter(t *testing.T) { assert.NotContains(t, strings.Join(capturedArgs, " "), "max_tokens", "audit command args should ignore max_tokens") } +func TestAuditTool_AcceptsRunIDAlias(t *testing.T) { + testCases := []struct { + name string + runIDValue any + expectedRunArg string + }{ + { + name: "string_run_id", + runIDValue: "1234567890", + expectedRunArg: "1234567890", + }, + { + name: "numeric_run_id", + runIDValue: 1234567890, + expectedRunArg: "1234567890", + }, + { + name: "run_url", + runIDValue: "https://github.com/owner/repo/actions/runs/1234567890", + expectedRunArg: "https://github.com/owner/repo/actions/runs/1234567890", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + const expectedStdout = `{"overview":{"run_id":"1234567890"}}` + + var capturedArgs []string + mockExecCmd := func(ctx context.Context, args ...string) *exec.Cmd { + capturedArgs = slices.Clone(args) + return exec.CommandContext(ctx, "sh", "-c", `printf '%s' "$1"`, "sh", expectedStdout) + } + + server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "1.0"}, nil) + err := registerAuditTool(server, mockExecCmd, "", false) + require.NoError(t, err, "registerAuditTool should succeed") + + session := connectInMemory(t, server) + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: "audit", + Arguments: map[string]any{ + "run_id": tc.runIDValue, + }, + }) + require.NoError(t, err, "audit tool should accept run_id alias") + require.NotNil(t, result, "result should not be nil") + + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected text content in audit response") + assert.JSONEq(t, expectedStdout, textContent.Text, "audit tool should return subprocess stdout") + + require.GreaterOrEqual(t, len(capturedArgs), 2, "captured args should include command and run ID") + assert.Equal(t, "audit", capturedArgs[0], "first arg should be audit command") + assert.Equal(t, tc.expectedRunArg, capturedArgs[1], "run_id alias should be forwarded as positional run input") + }) + } +} + +func TestAuditTool_AcceptsNumericRunIDOrURLField(t *testing.T) { + const expectedStdout = `{"overview":{"run_id":"1234567890"}}` + + var capturedArgs []string + mockExecCmd := func(ctx context.Context, args ...string) *exec.Cmd { + capturedArgs = slices.Clone(args) + return exec.CommandContext(ctx, "sh", "-c", `printf '%s' "$1"`, "sh", expectedStdout) + } + + server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "1.0"}, nil) + err := registerAuditTool(server, mockExecCmd, "", false) + require.NoError(t, err, "registerAuditTool should succeed") + + session := connectInMemory(t, server) + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: "audit", + Arguments: map[string]any{ + "run_id_or_url": 1234567890, + }, + }) + require.NoError(t, err, "audit tool should accept numeric run_id_or_url") + require.NotNil(t, result, "result should not be nil") + + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected text content in audit response") + assert.JSONEq(t, expectedStdout, textContent.Text, "audit tool should return subprocess stdout") + + require.GreaterOrEqual(t, len(capturedArgs), 2, "captured args should include command and run ID") + assert.Equal(t, "audit", capturedArgs[0], "first arg should be audit command") + assert.Equal(t, "1234567890", capturedArgs[1], "numeric run_id_or_url should be normalized to positional string run ID") +} + // TestAuditTool_MultiRunDiffMode verifies that when run_ids_or_urls contains // multiple entries the audit tool passes all of them as positional arguments // to the audit command (which then runs in diff mode).