diff --git a/components/ambient-api-server/pkg/api/openapi/model_scheduled_session.go b/components/ambient-api-server/pkg/api/openapi/model_scheduled_session.go new file mode 100644 index 000000000..72de48d37 --- /dev/null +++ b/components/ambient-api-server/pkg/api/openapi/model_scheduled_session.go @@ -0,0 +1,141 @@ +/* +Ambient API Server + +Ambient API Server + +API version: 1.0.0 +Contact: ambient-code@redhat.com +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "time" +) + +// checks if the ScheduledSession type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ScheduledSession{} + +// ScheduledSession struct for ScheduledSession +type ScheduledSession struct { + Id *string `json:"id,omitempty"` + Kind *string `json:"kind,omitempty"` + Href *string `json:"href,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + Name string `json:"name"` + Description *string `json:"description,omitempty"` + ProjectId string `json:"project_id"` + AgentId string `json:"agent_id"` + Schedule string `json:"schedule"` + Timezone *string `json:"timezone,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + SessionPrompt *string `json:"session_prompt,omitempty"` + LastRunAt *time.Time `json:"last_run_at,omitempty"` + NextRunAt *time.Time `json:"next_run_at,omitempty"` +} + +// NewScheduledSession instantiates a new ScheduledSession object +func NewScheduledSession(name string, projectId string, agentId string, schedule string) *ScheduledSession { + this := ScheduledSession{} + this.Name = name + this.ProjectId = projectId + this.AgentId = agentId + this.Schedule = schedule + return &this +} + +// NewScheduledSessionWithDefaults instantiates a new ScheduledSession object with defaults +func NewScheduledSessionWithDefaults() *ScheduledSession { + this := ScheduledSession{} + return &this +} + +func (o ScheduledSession) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ScheduledSession) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + if !IsNil(o.Kind) { + toSerialize["kind"] = o.Kind + } + if !IsNil(o.Href) { + toSerialize["href"] = o.Href + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + toSerialize["name"] = o.Name + if !IsNil(o.Description) { + toSerialize["description"] = o.Description + } + toSerialize["project_id"] = o.ProjectId + toSerialize["agent_id"] = o.AgentId + toSerialize["schedule"] = o.Schedule + if !IsNil(o.Timezone) { + toSerialize["timezone"] = o.Timezone + } + if !IsNil(o.Enabled) { + toSerialize["enabled"] = o.Enabled + } + if !IsNil(o.SessionPrompt) { + toSerialize["session_prompt"] = o.SessionPrompt + } + if !IsNil(o.LastRunAt) { + toSerialize["last_run_at"] = o.LastRunAt + } + if !IsNil(o.NextRunAt) { + toSerialize["next_run_at"] = o.NextRunAt + } + return toSerialize, nil +} + +type NullableScheduledSession struct { + value *ScheduledSession + isSet bool +} + +func (v NullableScheduledSession) Get() *ScheduledSession { + return v.value +} + +func (v *NullableScheduledSession) Set(val *ScheduledSession) { + v.value = val + v.isSet = true +} + +func (v NullableScheduledSession) IsSet() bool { + return v.isSet +} + +func (v *NullableScheduledSession) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableScheduledSession(val *ScheduledSession) *NullableScheduledSession { + return &NullableScheduledSession{value: val, isSet: true} +} + +func (v NullableScheduledSession) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableScheduledSession) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/components/ambient-api-server/pkg/api/openapi/model_scheduled_session_list.go b/components/ambient-api-server/pkg/api/openapi/model_scheduled_session_list.go new file mode 100644 index 000000000..830c7c5ba --- /dev/null +++ b/components/ambient-api-server/pkg/api/openapi/model_scheduled_session_list.go @@ -0,0 +1,136 @@ +/* +Ambient API Server + +Ambient API Server + +API version: 1.0.0 +Contact: ambient-code@redhat.com +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the ScheduledSessionList type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ScheduledSessionList{} + +// ScheduledSessionList struct for ScheduledSessionList +type ScheduledSessionList struct { + Kind string `json:"kind"` + Page int32 `json:"page"` + Size int32 `json:"size"` + Total int32 `json:"total"` + Items []ScheduledSession `json:"items"` +} + +type _ScheduledSessionList ScheduledSessionList + +// NewScheduledSessionList instantiates a new ScheduledSessionList object +func NewScheduledSessionList(kind string, page int32, size int32, total int32, items []ScheduledSession) *ScheduledSessionList { + this := ScheduledSessionList{} + this.Kind = kind + this.Page = page + this.Size = size + this.Total = total + this.Items = items + return &this +} + +// NewScheduledSessionListWithDefaults instantiates a new ScheduledSessionList object with defaults +func NewScheduledSessionListWithDefaults() *ScheduledSessionList { + this := ScheduledSessionList{} + return &this +} + +func (o ScheduledSessionList) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ScheduledSessionList) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["kind"] = o.Kind + toSerialize["page"] = o.Page + toSerialize["size"] = o.Size + toSerialize["total"] = o.Total + toSerialize["items"] = o.Items + return toSerialize, nil +} + +func (o *ScheduledSessionList) UnmarshalJSON(data []byte) (err error) { + requiredProperties := []string{ + "kind", + "page", + "size", + "total", + "items", + } + + allProperties := make(map[string]interface{}) + err = json.Unmarshal(data, &allProperties) + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varScheduledSessionList := _ScheduledSessionList{} + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varScheduledSessionList) + if err != nil { + return err + } + + *o = ScheduledSessionList(varScheduledSessionList) + return err +} + +type NullableScheduledSessionList struct { + value *ScheduledSessionList + isSet bool +} + +func (v NullableScheduledSessionList) Get() *ScheduledSessionList { + return v.value +} + +func (v *NullableScheduledSessionList) Set(val *ScheduledSessionList) { + v.value = val + v.isSet = true +} + +func (v NullableScheduledSessionList) IsSet() bool { + return v.isSet +} + +func (v *NullableScheduledSessionList) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableScheduledSessionList(val *ScheduledSessionList) *NullableScheduledSessionList { + return &NullableScheduledSessionList{value: val, isSet: true} +} + +func (v NullableScheduledSessionList) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableScheduledSessionList) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/components/ambient-api-server/pkg/api/openapi/model_scheduled_session_patch_request.go b/components/ambient-api-server/pkg/api/openapi/model_scheduled_session_patch_request.go new file mode 100644 index 000000000..efa990616 --- /dev/null +++ b/components/ambient-api-server/pkg/api/openapi/model_scheduled_session_patch_request.go @@ -0,0 +1,108 @@ +/* +Ambient API Server + +Ambient API Server + +API version: 1.0.0 +Contact: ambient-code@redhat.com +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the ScheduledSessionPatchRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ScheduledSessionPatchRequest{} + +// ScheduledSessionPatchRequest struct for ScheduledSessionPatchRequest +type ScheduledSessionPatchRequest struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Schedule *string `json:"schedule,omitempty"` + Timezone *string `json:"timezone,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + SessionPrompt *string `json:"session_prompt,omitempty"` +} + +// NewScheduledSessionPatchRequest instantiates a new ScheduledSessionPatchRequest object +func NewScheduledSessionPatchRequest() *ScheduledSessionPatchRequest { + this := ScheduledSessionPatchRequest{} + return &this +} + +// NewScheduledSessionPatchRequestWithDefaults instantiates a new ScheduledSessionPatchRequest object with defaults +func NewScheduledSessionPatchRequestWithDefaults() *ScheduledSessionPatchRequest { + this := ScheduledSessionPatchRequest{} + return &this +} + +func (o ScheduledSessionPatchRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ScheduledSessionPatchRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Name) { + toSerialize["name"] = o.Name + } + if !IsNil(o.Description) { + toSerialize["description"] = o.Description + } + if !IsNil(o.Schedule) { + toSerialize["schedule"] = o.Schedule + } + if !IsNil(o.Timezone) { + toSerialize["timezone"] = o.Timezone + } + if !IsNil(o.Enabled) { + toSerialize["enabled"] = o.Enabled + } + if !IsNil(o.SessionPrompt) { + toSerialize["session_prompt"] = o.SessionPrompt + } + return toSerialize, nil +} + +type NullableScheduledSessionPatchRequest struct { + value *ScheduledSessionPatchRequest + isSet bool +} + +func (v NullableScheduledSessionPatchRequest) Get() *ScheduledSessionPatchRequest { + return v.value +} + +func (v *NullableScheduledSessionPatchRequest) Set(val *ScheduledSessionPatchRequest) { + v.value = val + v.isSet = true +} + +func (v NullableScheduledSessionPatchRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableScheduledSessionPatchRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableScheduledSessionPatchRequest(val *ScheduledSessionPatchRequest) *NullableScheduledSessionPatchRequest { + return &NullableScheduledSessionPatchRequest{value: val, isSet: true} +} + +func (v NullableScheduledSessionPatchRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableScheduledSessionPatchRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/components/ambient-sdk/go-sdk/types/scheduled_session.go b/components/ambient-sdk/go-sdk/types/scheduled_session.go new file mode 100644 index 000000000..60eab9625 --- /dev/null +++ b/components/ambient-sdk/go-sdk/types/scheduled_session.go @@ -0,0 +1,147 @@ +package types + +import ( + "errors" + "fmt" + "time" +) + +type ScheduledSession struct { + ObjectReference + + AgentID string `json:"agent_id,omitempty"` + Description string `json:"description,omitempty"` + Enabled bool `json:"enabled,omitempty"` + LastRunAt *time.Time `json:"last_run_at,omitempty"` + Name string `json:"name"` + NextRunAt *time.Time `json:"next_run_at,omitempty"` + ProjectID string `json:"project_id,omitempty"` + Schedule string `json:"schedule,omitempty"` + SessionPrompt string `json:"session_prompt,omitempty"` + Timezone string `json:"timezone,omitempty"` +} + +type ScheduledSessionList struct { + ListMeta + Items []ScheduledSession `json:"items"` +} + +func (l *ScheduledSessionList) GetItems() []ScheduledSession { return l.Items } +func (l *ScheduledSessionList) GetTotal() int { return l.Total } +func (l *ScheduledSessionList) GetPage() int { return l.Page } +func (l *ScheduledSessionList) GetSize() int { return l.Size } + +// ScheduledSessionPatch is the request body for a PATCH operation. +// Only set fields that should be changed; omitted (nil) fields are left unchanged. +type ScheduledSessionPatch struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Schedule *string `json:"schedule,omitempty"` + Timezone *string `json:"timezone,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + SessionPrompt *string `json:"session_prompt,omitempty"` +} + +// ScheduledSessionBuilder builds a ScheduledSession for creation. +type ScheduledSessionBuilder struct { + resource ScheduledSession + errs []error +} + +func NewScheduledSessionBuilder() *ScheduledSessionBuilder { + return &ScheduledSessionBuilder{} +} + +func (b *ScheduledSessionBuilder) Name(v string) *ScheduledSessionBuilder { + b.resource.Name = v + return b +} + +func (b *ScheduledSessionBuilder) ProjectID(v string) *ScheduledSessionBuilder { + b.resource.ProjectID = v + return b +} + +func (b *ScheduledSessionBuilder) AgentID(v string) *ScheduledSessionBuilder { + b.resource.AgentID = v + return b +} + +func (b *ScheduledSessionBuilder) Schedule(v string) *ScheduledSessionBuilder { + b.resource.Schedule = v + return b +} + +func (b *ScheduledSessionBuilder) Timezone(v string) *ScheduledSessionBuilder { + b.resource.Timezone = v + return b +} + +func (b *ScheduledSessionBuilder) SessionPrompt(v string) *ScheduledSessionBuilder { + b.resource.SessionPrompt = v + return b +} + +func (b *ScheduledSessionBuilder) Description(v string) *ScheduledSessionBuilder { + b.resource.Description = v + return b +} + +func (b *ScheduledSessionBuilder) Build() (*ScheduledSession, error) { + if b.resource.Name == "" { + b.errs = append(b.errs, fmt.Errorf("name is required")) + } + if b.resource.AgentID == "" { + b.errs = append(b.errs, fmt.Errorf("agent_id is required")) + } + if b.resource.Schedule == "" { + b.errs = append(b.errs, fmt.Errorf("schedule is required")) + } + if len(b.errs) > 0 { + return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errs...)) + } + return &b.resource, nil +} + +// ScheduledSessionPatchBuilder builds a ScheduledSessionPatch for update operations. +type ScheduledSessionPatchBuilder struct { + patch ScheduledSessionPatch +} + +func NewScheduledSessionPatchBuilder() *ScheduledSessionPatchBuilder { + return &ScheduledSessionPatchBuilder{} +} + +func (b *ScheduledSessionPatchBuilder) Name(v string) *ScheduledSessionPatchBuilder { + b.patch.Name = &v + return b +} + +func (b *ScheduledSessionPatchBuilder) Description(v string) *ScheduledSessionPatchBuilder { + b.patch.Description = &v + return b +} + +func (b *ScheduledSessionPatchBuilder) Schedule(v string) *ScheduledSessionPatchBuilder { + b.patch.Schedule = &v + return b +} + +func (b *ScheduledSessionPatchBuilder) Timezone(v string) *ScheduledSessionPatchBuilder { + b.patch.Timezone = &v + return b +} + +func (b *ScheduledSessionPatchBuilder) Enabled(v bool) *ScheduledSessionPatchBuilder { + b.patch.Enabled = &v + return b +} + +func (b *ScheduledSessionPatchBuilder) SessionPrompt(v string) *ScheduledSessionPatchBuilder { + b.patch.SessionPrompt = &v + return b +} + +func (b *ScheduledSessionPatchBuilder) Build() *ScheduledSessionPatch { + return &b.patch +} diff --git a/components/manifests/base/core/operator-deployment.yaml b/components/manifests/base/core/operator-deployment.yaml index f6f543684..915547e01 100644 --- a/components/manifests/base/core/operator-deployment.yaml +++ b/components/manifests/base/core/operator-deployment.yaml @@ -125,6 +125,14 @@ spec: name: google-workflow-app-secret key: GOOGLE_OAUTH_CLIENT_SECRET optional: true + # Public backend URL used to build the OAuth redirect URI injected into runner pods. + # Must match the redirect URI registered in the Google Cloud Console OAuth app. + - name: BACKEND_PUBLIC_URL + valueFrom: + secretKeyRef: + name: google-workflow-app-secret + key: BACKEND_URL + optional: true # S3 state sync configuration (defaults - can be overridden per-project in settings) - name: STATE_SYNC_IMAGE value: "quay.io/ambient_code/vteam_state_sync:latest" diff --git a/components/operator/internal/config/config.go b/components/operator/internal/config/config.go index 5f305b48a..600c22cf1 100644 --- a/components/operator/internal/config/config.go +++ b/components/operator/internal/config/config.go @@ -23,6 +23,7 @@ var ( type Config struct { Namespace string BackendNamespace string + BackendPublicURL string AmbientCodeRunnerImage string StateSyncImage string ImagePullPolicy corev1.PullPolicy @@ -121,9 +122,12 @@ func LoadConfig() *Config { } } + backendPublicURL := os.Getenv("BACKEND_PUBLIC_URL") + return &Config{ Namespace: namespace, BackendNamespace: backendNamespace, + BackendPublicURL: backendPublicURL, AmbientCodeRunnerImage: ambientCodeRunnerImage, StateSyncImage: stateSyncImage, ImagePullPolicy: imagePullPolicy, diff --git a/components/operator/internal/handlers/sessions.go b/components/operator/internal/handlers/sessions.go index 1e03a15af..bfe44b31e 100755 --- a/components/operator/internal/handlers/sessions.go +++ b/components/operator/internal/handlers/sessions.go @@ -1078,6 +1078,15 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error { {Name: "GOOGLE_OAUTH_CLIENT_SECRET", Value: os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET")}, } + // Set the OAuth redirect URI for workspace-mcp so it uses the Ambient backend + // callback endpoint instead of defaulting to http://localhost:8000/oauth2callback. + if appConfig.BackendPublicURL != "" { + base = append(base, corev1.EnvVar{ + Name: "GOOGLE_OAUTH_REDIRECT_URI", + Value: fmt.Sprintf("%s/oauth2callback", appConfig.BackendPublicURL), + }) + } + // For e2e: use minimal MCP config (webfetch only, no credentials needed) if mcpConfigFile := os.Getenv("MCP_CONFIG_FILE"); strings.TrimSpace(mcpConfigFile) != "" { base = append(base, corev1.EnvVar{Name: "MCP_CONFIG_FILE", Value: mcpConfigFile}) diff --git a/components/runners/ambient-runner/.mcp.json b/components/runners/ambient-runner/.mcp.json index 399975c9e..fa9569581 100644 --- a/components/runners/ambient-runner/.mcp.json +++ b/components/runners/ambient-runner/.mcp.json @@ -10,11 +10,15 @@ }, "webfetch": { "command": "uvx", - "args": ["mcp-server-fetch"] + "args": [ + "mcp-server-fetch" + ] }, "mcp-atlassian": { "command": "uvx", - "args": ["mcp-atlassian"], + "args": [ + "mcp-atlassian" + ], "env": { "JIRA_URL": "${JIRA_URL}", "JIRA_USERNAME": "${JIRA_EMAIL}", @@ -27,7 +31,8 @@ "command": "uvx", "args": [ "kubernetes-mcp-server@latest", - "--kubeconfig", "/tmp/.ambient_kubeconfig", + "--kubeconfig", + "/tmp/.ambient_kubeconfig", "--disable-multi-cluster" ] }, @@ -44,6 +49,7 @@ "MCP_SINGLE_USER_MODE": "1", "GOOGLE_OAUTH_CLIENT_ID": "${GOOGLE_OAUTH_CLIENT_ID}", "GOOGLE_OAUTH_CLIENT_SECRET": "${GOOGLE_OAUTH_CLIENT_SECRET}", + "GOOGLE_OAUTH_REDIRECT_URI": "${GOOGLE_OAUTH_REDIRECT_URI}", "USER_GOOGLE_EMAIL": "${USER_GOOGLE_EMAIL:-user@example.com}" } }