From ad05177451982f24daab21965751b013f3b802ac Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Wed, 1 Apr 2026 21:29:02 -0700 Subject: [PATCH 1/2] Added support for exporting agent definitions --- .github/copilot-instructions.md | 23 ++++ client.go | 12 ++ generated/seclai.gen.go | 233 ++++++++++++++++++++++++++++++-- openapi/seclai.openapi.json | 179 ++++++++++++++++++++++++ types.go | 3 + 5 files changed, 442 insertions(+), 8 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 285a409..44caa60 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,6 +8,15 @@ go test ./... # tests go build ./... # build ``` +## Quality gates (must pass to report completion) + +- **ALL tests must pass with ZERO failures. No exceptions.** CI/CD runs the full test suite on every PR. A test failure blocks the build. +- **`go vet ./...` must pass with ZERO warnings.** Vet catches real bugs; treat all warnings as errors. +- **`go build ./...` must succeed.** Compilation errors block the build. +- **Do not dismiss test or vet failures as pre-existing or unrelated.** The `main` branch CI/CD is green. Any failure on a feature branch was caused by changes on that branch. +- **CRITICAL — NEVER INVESTIGATE ERROR ORIGIN OR BLAME**: When a vet, build, or test error appears, **fix it immediately**. Do NOT run `git diff`, `git log`, `git show`, `git blame`, or any other command to determine whether the error is "pre-existing" or "from our changes". There is no scenario where knowing the origin of an error changes what you must do: **fix it**. +- **CRITICAL — NEVER PIPE TEST OR BUILD OUTPUT**: Do not append `| tail`, `| head`, `| grep`, or any pipe to `go test`, `go vet`, `go build`, or similar commands. Piping hides errors. Use `-count=1` or `-v` flags to control verbosity — never pipe. + ## Key Rules - Go exported field names use PascalCase with acronyms fully uppercased: `APIKey` (not `ApiKey`), `BaseURL` (not `BaseUrl`), `HTTPClient` (not `HttpClient`). @@ -18,3 +27,17 @@ go build ./... # build - `resolveSsoToken` distinguishes nil cache ("No cached SSO token found") vs expired ("SSO token is missing or has expired"). - `LoadSsoProfile` always returns a valid profile — missing config values fall back to `SECLAI_SSO_*` env vars, then built-in defaults (`DefaultSsoDomain`, `DefaultSsoClientID`, `DefaultSsoRegion`). - Auth modes: `api_key`, bearer (static string / provider function), `sso` with auto-refresh. SSO is the default fallback when no explicit credentials are provided. +- Do not run ad-hoc Go snippets; add tests instead. + +## Git rules + +- **NEVER use `git stash`.** Use `git diff`, `git log`, or `git show` instead. +- Do not run `git checkout` to switch branches, `git reset`, or any other destructive git operation without explicit user approval. + +## Editing rules + +- Do not use CLI text tools (sed/awk). Use the editor-based patch tool. + +## Self-correction rules + +- **NEVER promise to "do better" without updating these instruction files.** If a recurring mistake is identified, edit this file with a concrete rule that prevents the mistake. Do that FIRST, then continue work. diff --git a/client.go b/client.go index d049f4b..e319115 100644 --- a/client.go +++ b/client.go @@ -313,6 +313,18 @@ func (c *Client) DeleteAgent(ctx context.Context, agentID string) error { return c.Do(ctx, http.MethodDelete, fmt.Sprintf("/agents/%s", url.PathEscape(agentID)), nil, nil, nil, nil) } +// ── Agent Export ──────────────────────────────────────────────────────────── + +// ExportAgent exports an agent definition as a portable JSON snapshot. +func (c *Client) ExportAgent(ctx context.Context, agentID string, download bool) (*AgentExportResponse, error) { + query := map[string]string{"download": fmt.Sprintf("%t", download)} + var out AgentExportResponse + if err := c.Do(ctx, http.MethodGet, fmt.Sprintf("/agents/%s/export", url.PathEscape(agentID)), query, nil, nil, &out); err != nil { + return nil, err + } + return &out, nil +} + // ── Agent Definitions ─────────────────────────────────────────────────────── // GetAgentDefinition retrieves the definition (step configuration) for an agent. diff --git a/generated/seclai.gen.go b/generated/seclai.gen.go index ed74191..1448c47 100644 --- a/generated/seclai.gen.go +++ b/generated/seclai.gen.go @@ -99,6 +99,36 @@ type AgentDefinitionResponse struct { // THOROUGH → "Slow and thorough" type AgentEvaluationTier string +// AgentExportResponse Portable JSON snapshot of an agent definition. +type AgentExportResponse struct { + // Agent Agent metadata and full definition. Keys: name, description, schema_version, definition, default_evaluation_tier, evaluation_mode, sampling_config, max_retries, retry_on_failure, prompt_model_auto_upgrade_strategy, prompt_model_auto_rollback_enabled, prompt_model_auto_rollback_triggers, created_at, updated_at. + Agent map[string]interface{} `json:"agent"` + + // AlertConfigs Alert configurations. + AlertConfigs *[]map[string]interface{} `json:"alert_configs"` + + // Dependencies Resolved dependency manifest. Keys: knowledge_bases, memory_banks, source_connections, agents, users — each a list of {id, name, description, …}. + Dependencies *map[string]interface{} `json:"dependencies"` + + // EvaluationCriteria Evaluation criteria for agent steps. + EvaluationCriteria *[]map[string]interface{} `json:"evaluation_criteria"` + + // ExportVersion Schema version of the export format (currently "2"). + ExportVersion string `json:"export_version"` + + // ExportedAt ISO-8601 timestamp of when the export was generated. + ExportedAt string `json:"exported_at"` + + // GovernancePolicies Agent-scoped governance policies. + GovernancePolicies *[]map[string]interface{} `json:"governance_policies"` + + // SoftwareVersion Application version that produced this export. + SoftwareVersion string `json:"software_version"` + + // Trigger Trigger configuration with schedules. + Trigger *map[string]interface{} `json:"trigger"` +} + // AgentRunAttemptResponse defines model for AgentRunAttemptResponse. type AgentRunAttemptResponse struct { // Duration Duration of the attempt in seconds. @@ -172,6 +202,9 @@ type AgentRunStepResponse struct { // EndedAt Timestamp when the step attempt ended. EndedAt *string `json:"ended_at"` + // Input Input provided to the step, if any. + Input *string `json:"input"` + // Output Output produced by the step, if any. Output *string `json:"output"` @@ -2433,6 +2466,15 @@ type ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetParams struct { XAccountId *XAccountId `json:"X-Account-Id,omitempty"` } +// ExportAgentApiAgentsAgentIdExportGetParams defines parameters for ExportAgentApiAgentsAgentIdExportGet. +type ExportAgentApiAgentsAgentIdExportGetParams struct { + // Download Return as file download + Download *bool `form:"download,omitempty" json:"download,omitempty"` + + // XAccountId Target a different organization account (OAuth only). When omitted, the user's default account is used. Ignored for API key authentication — the key's account is always used. + XAccountId *XAccountId `json:"X-Account-Id,omitempty"` +} + // ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetParams defines parameters for ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGet. type ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetParams struct { // XAccountId Target a different organization account (OAuth only). When omitted, the user's default account is used. Ignored for API key authentication — the key's account is always used. @@ -2757,6 +2799,12 @@ type UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPutParams struct { XAccountId *XAccountId `json:"X-Account-Id,omitempty"` } +// GetMeApiMeGetParams defines parameters for GetMeApiMeGet. +type GetMeApiMeGetParams struct { + // XAccountId Target a different organization account (OAuth only). When omitted, the user's default account is used. Ignored for API key authentication — the key's account is always used. + XAccountId *XAccountId `json:"X-Account-Id,omitempty"` +} + // ListMemoryBanksApiMemoryBanksGetParams defines parameters for ListMemoryBanksApiMemoryBanksGet. type ListMemoryBanksApiMemoryBanksGetParams struct { // Page Page number (1-based). @@ -3093,7 +3141,7 @@ type ListSourcesApiSourcesGetParams struct { // Order Sort order Order *string `form:"order,omitempty" json:"order,omitempty"` - // AccountId List sources for the given account. Defaults to the api key's account. + // AccountId List sources for the given account. Defaults to the caller's account. AccountId *string `form:"account_id,omitempty" json:"account_id,omitempty"` // XAccountId Target a different organization account (OAuth only). When omitted, the user's default account is used. Ignored for API key authentication — the key's account is always used. @@ -3602,6 +3650,9 @@ type ClientInterface interface { // ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGet request ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGet(ctx context.Context, agentId string, params *ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // ExportAgentApiAgentsAgentIdExportGet request + ExportAgentApiAgentsAgentIdExportGet(ctx context.Context, agentId string, params *ExportAgentApiAgentsAgentIdExportGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGet request ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGet(ctx context.Context, agentId string, uploadId string, params *ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -3765,7 +3816,7 @@ type ClientInterface interface { UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPut(ctx context.Context, knowledgeBaseId string, params *UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPutParams, body UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPutJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // GetMeApiMeGet request - GetMeApiMeGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + GetMeApiMeGet(ctx context.Context, params *GetMeApiMeGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) // ListMemoryBanksApiMemoryBanksGet request ListMemoryBanksApiMemoryBanksGet(ctx context.Context, params *ListMemoryBanksApiMemoryBanksGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -4447,6 +4498,18 @@ func (c *Client) ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGet(ctx context return c.Client.Do(req) } +func (c *Client) ExportAgentApiAgentsAgentIdExportGet(ctx context.Context, agentId string, params *ExportAgentApiAgentsAgentIdExportGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExportAgentApiAgentsAgentIdExportGetRequest(c.Server, agentId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGet(ctx context.Context, agentId string, uploadId string, params *ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetRequest(c.Server, agentId, uploadId, params) if err != nil { @@ -5167,8 +5230,8 @@ func (c *Client) UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPut(ctx cont return c.Client.Do(req) } -func (c *Client) GetMeApiMeGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetMeApiMeGetRequest(c.Server) +func (c *Client) GetMeApiMeGet(ctx context.Context, params *GetMeApiMeGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetMeApiMeGetRequest(c.Server, params) if err != nil { return nil, err } @@ -8176,6 +8239,77 @@ func NewListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetRequest(server string return req, nil } +// NewExportAgentApiAgentsAgentIdExportGetRequest generates requests for ExportAgentApiAgentsAgentIdExportGet +func NewExportAgentApiAgentsAgentIdExportGetRequest(server string, agentId string, params *ExportAgentApiAgentsAgentIdExportGetParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "agent_id", runtime.ParamLocationPath, agentId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/agents/%s/export", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Download != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "download", runtime.ParamLocationQuery, *params.Download); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + if params != nil { + + if params.XAccountId != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Account-Id", runtime.ParamLocationHeader, *params.XAccountId) + if err != nil { + return nil, err + } + + req.Header.Set("X-Account-Id", headerParam0) + } + + } + + return req, nil +} + // NewApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetRequest generates requests for ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGet func NewApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetRequest(server string, agentId string, uploadId string, params *ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetParams) (*http.Request, error) { var err error @@ -10864,7 +10998,7 @@ func NewUpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPutRequestWithBody(se } // NewGetMeApiMeGetRequest generates requests for GetMeApiMeGet -func NewGetMeApiMeGetRequest(server string) (*http.Request, error) { +func NewGetMeApiMeGetRequest(server string, params *GetMeApiMeGetParams) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -10887,6 +11021,21 @@ func NewGetMeApiMeGetRequest(server string) (*http.Request, error) { return nil, err } + if params != nil { + + if params.XAccountId != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Account-Id", runtime.ParamLocationHeader, *params.XAccountId) + if err != nil { + return nil, err + } + + req.Header.Set("X-Account-Id", headerParam0) + } + + } + return req, nil } @@ -14714,6 +14863,9 @@ type ClientWithResponsesInterface interface { // ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetWithResponse request ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetWithResponse(ctx context.Context, agentId string, params *ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetParams, reqEditors ...RequestEditorFn) (*ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetResponse, error) + // ExportAgentApiAgentsAgentIdExportGetWithResponse request + ExportAgentApiAgentsAgentIdExportGetWithResponse(ctx context.Context, agentId string, params *ExportAgentApiAgentsAgentIdExportGetParams, reqEditors ...RequestEditorFn) (*ExportAgentApiAgentsAgentIdExportGetResponse, error) + // ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetWithResponse request ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetWithResponse(ctx context.Context, agentId string, uploadId string, params *ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetParams, reqEditors ...RequestEditorFn) (*ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetResponse, error) @@ -14877,7 +15029,7 @@ type ClientWithResponsesInterface interface { UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPutWithResponse(ctx context.Context, knowledgeBaseId string, params *UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPutParams, body UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPutJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseIdPutResponse, error) // GetMeApiMeGetWithResponse request - GetMeApiMeGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetMeApiMeGetResponse, error) + GetMeApiMeGetWithResponse(ctx context.Context, params *GetMeApiMeGetParams, reqEditors ...RequestEditorFn) (*GetMeApiMeGetResponse, error) // ListMemoryBanksApiMemoryBanksGetWithResponse request ListMemoryBanksApiMemoryBanksGetWithResponse(ctx context.Context, params *ListMemoryBanksApiMemoryBanksGetParams, reqEditors ...RequestEditorFn) (*ListMemoryBanksApiMemoryBanksGetResponse, error) @@ -15722,6 +15874,29 @@ func (r ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetResponse) StatusCode( return 0 } +type ExportAgentApiAgentsAgentIdExportGetResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *AgentExportResponse + JSON422 *HTTPValidationError +} + +// Status returns HTTPResponse.Status +func (r ExportAgentApiAgentsAgentIdExportGetResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ExportAgentApiAgentsAgentIdExportGetResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetResponse struct { Body []byte HTTPResponse *http.Response @@ -18335,6 +18510,15 @@ func (c *ClientWithResponses) ListEvaluationRunsApiAgentsAgentIdEvaluationRunsGe return ParseListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetResponse(rsp) } +// ExportAgentApiAgentsAgentIdExportGetWithResponse request returning *ExportAgentApiAgentsAgentIdExportGetResponse +func (c *ClientWithResponses) ExportAgentApiAgentsAgentIdExportGetWithResponse(ctx context.Context, agentId string, params *ExportAgentApiAgentsAgentIdExportGetParams, reqEditors ...RequestEditorFn) (*ExportAgentApiAgentsAgentIdExportGetResponse, error) { + rsp, err := c.ExportAgentApiAgentsAgentIdExportGet(ctx, agentId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseExportAgentApiAgentsAgentIdExportGetResponse(rsp) +} + // ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetWithResponse request returning *ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetResponse func (c *ClientWithResponses) ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetWithResponse(ctx context.Context, agentId string, uploadId string, params *ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetParams, reqEditors ...RequestEditorFn) (*ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetResponse, error) { rsp, err := c.ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGet(ctx, agentId, uploadId, params, reqEditors...) @@ -18858,8 +19042,8 @@ func (c *ClientWithResponses) UpdateKnowledgeBaseApiKnowledgeBasesKnowledgeBaseI } // GetMeApiMeGetWithResponse request returning *GetMeApiMeGetResponse -func (c *ClientWithResponses) GetMeApiMeGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetMeApiMeGetResponse, error) { - rsp, err := c.GetMeApiMeGet(ctx, reqEditors...) +func (c *ClientWithResponses) GetMeApiMeGetWithResponse(ctx context.Context, params *GetMeApiMeGetParams, reqEditors ...RequestEditorFn) (*GetMeApiMeGetResponse, error) { + rsp, err := c.GetMeApiMeGet(ctx, params, reqEditors...) if err != nil { return nil, err } @@ -20464,6 +20648,39 @@ func ParseListEvaluationRunsApiAgentsAgentIdEvaluationRunsGetResponse(rsp *http. return response, nil } +// ParseExportAgentApiAgentsAgentIdExportGetResponse parses an HTTP response from a ExportAgentApiAgentsAgentIdExportGetWithResponse call +func ParseExportAgentApiAgentsAgentIdExportGetResponse(rsp *http.Response) (*ExportAgentApiAgentsAgentIdExportGetResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ExportAgentApiAgentsAgentIdExportGetResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest AgentExportResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest HTTPValidationError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + + } + + return response, nil +} + // ParseApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetResponse parses an HTTP response from a ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetWithResponse call func ParseApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetResponse(rsp *http.Response) (*ApiGetAgentInputUploadStatusApiAgentsAgentIdInputUploadsUploadIdGetResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index a20fe19..68b3863 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -107,6 +107,114 @@ "title": "AgentEvaluationTier", "type": "string" }, + "AgentExportResponse": { + "description": "Portable JSON snapshot of an agent definition.", + "properties": { + "agent": { + "additionalProperties": true, + "description": "Agent metadata and full definition. Keys: name, description, schema_version, definition, default_evaluation_tier, evaluation_mode, sampling_config, max_retries, retry_on_failure, prompt_model_auto_upgrade_strategy, prompt_model_auto_rollback_enabled, prompt_model_auto_rollback_triggers, created_at, updated_at.", + "title": "Agent", + "type": "object" + }, + "alert_configs": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Alert configurations.", + "title": "Alert Configs" + }, + "dependencies": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Resolved dependency manifest. Keys: knowledge_bases, memory_banks, source_connections, agents, users \u2014 each a list of {id, name, description, \u2026}.", + "title": "Dependencies" + }, + "evaluation_criteria": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Evaluation criteria for agent steps.", + "title": "Evaluation Criteria" + }, + "export_version": { + "description": "Schema version of the export format (currently \"2\").", + "title": "Export Version", + "type": "string" + }, + "exported_at": { + "description": "ISO-8601 timestamp of when the export was generated.", + "title": "Exported At", + "type": "string" + }, + "governance_policies": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Agent-scoped governance policies.", + "title": "Governance Policies" + }, + "software_version": { + "description": "Application version that produced this export.", + "title": "Software Version", + "type": "string" + }, + "trigger": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Trigger configuration with schedules.", + "title": "Trigger" + } + }, + "required": [ + "export_version", + "exported_at", + "software_version", + "agent" + ], + "title": "AgentExportResponse", + "type": "object" + }, "AgentRunAttemptResponse": { "properties": { "duration": { @@ -354,6 +462,18 @@ "description": "Timestamp when the step attempt ended.", "title": "Ended At" }, + "input": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Input provided to the step, if any.", + "title": "Input" + }, "output": { "anyOf": [ { @@ -404,6 +524,7 @@ "agent_step_id", "step_type", "status", + "input", "output", "output_content_type", "started_at", @@ -8871,6 +8992,64 @@ ] } }, + "/agents/{agent_id}/export": { + "get": { + "description": "Export an agent definition as a portable JSON snapshot.\n\nThe response contains the full definition, trigger configuration with schedules, alert configs, evaluation criteria, agent-scoped governance policies, and a resolved dependency manifest that maps every referenced external entity UUID to its human-readable name.\n\nResponse shape:\n- `export_version`: schema version (currently `\"2\"`)\n- `exported_at`: ISO-8601 timestamp\n- `agent`: name, description, schema_version, definition, timestamps\n- `trigger`: trigger type, input template, schedules\n- `alert_configs`: alert type, thresholds, recipients\n- `evaluation_criteria`: evaluation settings per step\n- `governance_policies`: agent-scoped governance policies\n- `dependencies`: knowledge_bases, memory_banks, source_connections, agents, users\n\nQuery params:\n- `download` (default true): when true, sets `Content-Disposition: attachment` so clients treat the response as a file download.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only export agents belonging to your account.", + "operationId": "export_agent_api_agents__agent_id__export_get", + "parameters": [ + { + "in": "path", + "name": "agent_id", + "required": true, + "schema": { + "title": "Agent Id", + "type": "string" + } + }, + { + "description": "Return as file download", + "in": "query", + "name": "download", + "required": false, + "schema": { + "default": true, + "description": "Return as file download", + "title": "Download", + "type": "boolean" + } + }, + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentExportResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Export agent definition", + "tags": [ + "agents" + ] + } + }, "/agents/{agent_id}/input-uploads/{upload_id}": { "get": { "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", diff --git a/types.go b/types.go index 7d9e528..675fe2b 100644 --- a/types.go +++ b/types.go @@ -47,6 +47,9 @@ type UpdateAgentRequest = generated.RoutersApiAgentsUpdateAgentRequest // AgentDefinitionResponse is the agent's step workflow definition. type AgentDefinitionResponse = generated.AgentDefinitionResponse +// AgentExportResponse is a portable JSON snapshot of an agent definition. +type AgentExportResponse = generated.AgentExportResponse + // UpdateAgentDefinitionRequest is the request body for updating an agent definition. type UpdateAgentDefinitionRequest = generated.UpdateAgentDefinitionRequest From 35961e4f02fda21dd77ced77e3b17914c18fb8c4 Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Wed, 1 Apr 2026 21:57:05 -0700 Subject: [PATCH 2/2] Addressed review comments --- .github/copilot-instructions.md | 4 ++- client_typed_test.go | 43 +++++++++++++++++++++++++++++++++ openapi/seclai.openapi.json | 4 +-- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 44caa60..98f6396 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ go build ./... # build - **`go vet ./...` must pass with ZERO warnings.** Vet catches real bugs; treat all warnings as errors. - **`go build ./...` must succeed.** Compilation errors block the build. - **Do not dismiss test or vet failures as pre-existing or unrelated.** The `main` branch CI/CD is green. Any failure on a feature branch was caused by changes on that branch. -- **CRITICAL — NEVER INVESTIGATE ERROR ORIGIN OR BLAME**: When a vet, build, or test error appears, **fix it immediately**. Do NOT run `git diff`, `git log`, `git show`, `git blame`, or any other command to determine whether the error is "pre-existing" or "from our changes". There is no scenario where knowing the origin of an error changes what you must do: **fix it**. +- **CRITICAL — NEVER INVESTIGATE ERROR ORIGIN OR BLAME**: When a vet, build, or test error appears, **fix it immediately**. Do NOT run `git blame` or use git history to argue that an error is "pre-existing" or not your responsibility. Tools like `git diff`, `git log`, and `git show` may be used to understand and review changes, but never to avoid fixing an error. There is no scenario where knowing the origin of an error changes what you must do: **fix it**. - **CRITICAL — NEVER PIPE TEST OR BUILD OUTPUT**: Do not append `| tail`, `| head`, `| grep`, or any pipe to `go test`, `go vet`, `go build`, or similar commands. Piping hides errors. Use `-count=1` or `-v` flags to control verbosity — never pipe. ## Key Rules @@ -22,6 +22,8 @@ go build ./... # build - Go exported field names use PascalCase with acronyms fully uppercased: `APIKey` (not `ApiKey`), `BaseURL` (not `BaseUrl`), `HTTPClient` (not `HttpClient`). - `doc.go` examples must compile — field names in code samples must match the `Options` struct exactly. - The OpenAPI spec at `openapi/seclai.openapi.json` is shared identically with `seclai-python`. Changes must be synced to both repos. +- OpenAPI specs are generated from the main `seclai` app repo. Description or endpoint changes made here must also be applied upstream, or they will be overwritten on the next generation. +- `.github/copilot-instructions.md` shares common sections (quality gates, git rules, editing rules, self-correction rules) across all SDK repos. When updating shared rules, apply the same change to all repos: `seclai-python`, `seclai-javascript`, `seclai-go`, `seclai-csharp`, `seclai-cli`, `seclai-mcp`. - Generated client is in `generated/` — produced from the OpenAPI spec via oapi-codegen. Do not edit manually. - `WriteSsoCache`: uses `os.Remove` before `os.Rename` for Windows compatibility (`os.Rename` fails if dest exists on Windows). - `resolveSsoToken` distinguishes nil cache ("No cached SSO token found") vs expired ("SSO token is missing or has expired"). diff --git a/client_typed_test.go b/client_typed_test.go index c6f126e..89e2e0c 100644 --- a/client_typed_test.go +++ b/client_typed_test.go @@ -588,6 +588,49 @@ func TestClient_DeleteAgent(t *testing.T) { } } +// ── Agent Export tests ────────────────────────────────────────────────────── + +func TestClient_ExportAgent(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/agents/a_1/export" { + w.WriteHeader(404) + return + } + if r.URL.Query().Get("download") != "true" { + t.Errorf("expected download=true, got %q", r.URL.Query().Get("download")) + } + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"export_version":"2","exported_at":"2026-01-01T00:00:00Z","software_version":"1.0.0","agent":{"name":"test"}}`) + })) + t.Cleanup(srv.Close) + + c, _ := NewClient(Options{APIKey: "k", BaseURL: srv.URL}) + resp, err := c.ExportAgent(context.Background(), "a_1", true) + if err != nil { + t.Fatalf("ExportAgent: %v", err) + } + if resp.ExportVersion != "2" { + t.Fatalf("expected export_version 2, got %q", resp.ExportVersion) + } +} + +func TestClient_ExportAgent_DownloadFalse(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("download") != "false" { + t.Errorf("expected download=false, got %q", r.URL.Query().Get("download")) + } + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"export_version":"2","exported_at":"2026-01-01T00:00:00Z","software_version":"1.0.0","agent":{"name":"test"}}`) + })) + t.Cleanup(srv.Close) + + c, _ := NewClient(Options{APIKey: "k", BaseURL: srv.URL}) + _, err := c.ExportAgent(context.Background(), "a_1", false) + if err != nil { + t.Fatalf("ExportAgent download=false: %v", err) + } +} + // ── Agent Definition tests ────────────────────────────────────────────────── func TestClient_GetAgentDefinition(t *testing.T) { diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index 68b3863..6e6e6f8 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -8994,7 +8994,7 @@ }, "/agents/{agent_id}/export": { "get": { - "description": "Export an agent definition as a portable JSON snapshot.\n\nThe response contains the full definition, trigger configuration with schedules, alert configs, evaluation criteria, agent-scoped governance policies, and a resolved dependency manifest that maps every referenced external entity UUID to its human-readable name.\n\nResponse shape:\n- `export_version`: schema version (currently `\"2\"`)\n- `exported_at`: ISO-8601 timestamp\n- `agent`: name, description, schema_version, definition, timestamps\n- `trigger`: trigger type, input template, schedules\n- `alert_configs`: alert type, thresholds, recipients\n- `evaluation_criteria`: evaluation settings per step\n- `governance_policies`: agent-scoped governance policies\n- `dependencies`: knowledge_bases, memory_banks, source_connections, agents, users\n\nQuery params:\n- `download` (default true): when true, sets `Content-Disposition: attachment` so clients treat the response as a file download.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only export agents belonging to your account.", + "description": "Export an agent definition as a portable JSON snapshot.\n\nThe response contains the full definition, trigger configuration with schedules, alert configs, evaluation criteria, agent-scoped governance policies, and a resolved dependency manifest that maps every referenced external entity UUID to its human-readable name.\n\nResponse shape:\n- `export_version`: schema version (currently `\"2\"`)\n- `exported_at`: ISO-8601 timestamp\n- `agent`: name, description, schema_version, definition, timestamps\n- `trigger`: trigger type, input template, schedules\n- `alert_configs`: alert type, thresholds, recipients\n- `evaluation_criteria`: evaluation settings per step\n- `governance_policies`: agent-scoped governance policies\n- `dependencies`: knowledge_bases, memory_banks, source_connections, agents, users\n\nQuery params:\n- `download` (default true): when true, sets `Content-Disposition: attachment` so clients treat the response as a file download.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.\n- When using OAuth, you may target a different organization account with `X-Account-Id`; for API keys, the key's account is always used.\n- You can only export agents belonging to the resolved account.", "operationId": "export_agent_api_agents__agent_id__export_get", "parameters": [ { @@ -11469,7 +11469,7 @@ }, "/me": { "get": { - "description": "Returns the authenticated user's personal account ID and a list of organisations they belong to. Each organisation entry includes the organisation's own id, display name, and account_id. Useful for CLI tooling that needs to let the user pick an org context.", + "description": "Returns the authenticated user's personal account ID and a list of organizations they belong to. Each organization entry includes the organization's id, name, and account_id. Useful for CLI tooling that needs to let the user pick an organization context.", "operationId": "get_me_api_me_get", "parameters": [ {