Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,29 @@ export class AmbientClient {
if (!config.baseUrl) {
throw new Error('baseUrl is required');
}
if (!config.token) {
throw new Error('token is required');
if (config.token) {
if (config.token.length < 20) {
throw new Error('token is too short (minimum 20 characters)');
}
if (config.token === 'YOUR_TOKEN_HERE' || config.token === 'PLACEHOLDER_TOKEN') {
throw new Error('placeholder token is not allowed');
}
}
if (config.token.length < 20) {
throw new Error('token is too short (minimum 20 characters)');
}
if (config.token === 'YOUR_TOKEN_HERE' || config.token === 'PLACEHOLDER_TOKEN') {
throw new Error('placeholder token is not allowed');
}
if (!config.project) {
throw new Error('project is required');
}
if (config.project.length > 63) {
if (config.project && config.project.length > 63) {
throw new Error('project name cannot exceed 63 characters');
}

const url = new URL(config.baseUrl);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('only HTTP and HTTPS schemes are supported');
// Reject protocol-relative URLs (e.g. "//evil.com/path")
if (config.baseUrl.startsWith('//')) {
throw new Error('Protocol-relative URLs are not allowed for baseUrl');
}

// Skip URL parsing for relative paths (e.g. "/api/proxy")
if (!config.baseUrl.startsWith('/')) {
const url = new URL(config.baseUrl);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('only HTTP and HTTPS schemes are supported');
}
}

this.config = {
Expand All @@ -54,10 +58,7 @@ export class AmbientClient {
if (!token) {
throw new Error('AMBIENT_TOKEN environment variable is required');
}
if (!project) {
throw new Error('AMBIENT_PROJECT environment variable is required');
}

return new AmbientClient({ baseUrl, token, project });
return new AmbientClient({ baseUrl, token, ...(project ? { project } : {}) });
}
}
8 changes: 4 additions & 4 deletions components/ambient-sdk/generator/templates/ts/base.ts.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export type RequestOptions = {

export type AmbientClientConfig = {
baseUrl: string;
token: string;
project: string;
token?: string;
project?: string;
};

export async function ambientFetch<T>(
Expand All @@ -83,8 +83,8 @@ export async function ambientFetch<T>(
): Promise<T> {
const url = `${config.baseUrl}{{.Spec.BasePath}}${path}`;
const headers: Record<string, string> = {
'Authorization': `Bearer ${config.token}`,
'X-Ambient-Project': config.project,
...(config.token ? { 'Authorization': `Bearer ${config.token}` } : {}),
...(config.project ? { 'X-Ambient-Project': config.project } : {}),
};
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
Expand Down
3 changes: 3 additions & 0 deletions components/ambient-sdk/generator/templates/ts/client.ts.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export class {{.Resource.Name}}API {

{{- if .Resource.IsSubResource}}
private basePath(): string {
if (!this.config.project) {
throw new Error('project is required for {{.Resource.Name}} operations');
}
return '/{{.Resource.PathSegment}}'.replace('{id}', encodeURIComponent(this.config.project));
}
{{end}}
Expand Down
11 changes: 7 additions & 4 deletions components/ambient-sdk/go-sdk/types/scheduled_session.go
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ 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 {
AgentID *string `json:"agent_id,omitempty"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
AgentID *string `json:"agent_id,omitempty"`
Schedule *string `json:"schedule,omitempty"`
Timezone *string `json:"timezone,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
SessionPrompt *string `json:"session_prompt,omitempty"`
Timeout *int32 `json:"timeout,omitempty"`
InactivityTimeout *int32 `json:"inactivity_timeout,omitempty"`
StopOnRunFinished *bool `json:"stop_on_run_finished,omitempty"`
RunnerType *string `json:"runner_type,omitempty"`
SessionPrompt *string `json:"session_prompt,omitempty"`
StopOnRunFinished *bool `json:"stop_on_run_finished,omitempty"`
Timeout *int32 `json:"timeout,omitempty"`
}

// ScheduledSessionBuilder builds a ScheduledSession for creation.
Expand Down Expand Up @@ -120,6 +120,9 @@ 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"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT.
# Source: ../../ambient-api-server/openapi/openapi.yaml
# Spec SHA256: c9d4494778eb0a006db1f289630460fb9b5dc58df1895903c42735b4f56b0314
# Generated: 2026-05-05T15:33:32Z

from __future__ import annotations

from typing import Any, Iterator, Optional, TYPE_CHECKING
from urllib.parse import quote

from ._base import ListOptions
from .credential import Credential, CredentialList

if TYPE_CHECKING:
from .client import AmbientClient


class CredentialAPI:
def __init__(self, client: AmbientClient) -> None:
self._client = client
def _base_path(self) -> str:
return "/projects/{id}/credentials".replace("{id}", quote(self._client._project, safe=""))


def create(self, data: dict) -> Credential:
resp = self._client._request("POST", self._base_path(), json=data)
return Credential.from_dict(resp)

def get(self, resource_id: str) -> Credential:
resp = self._client._request("GET", f"{self._base_path()}/{resource_id}")
return Credential.from_dict(resp)

def list(self, opts: Optional[ListOptions] = None) -> CredentialList:
params = opts.to_params() if opts else None
resp = self._client._request("GET", self._base_path(), params=params)
return CredentialList.from_dict(resp)
def update(self, resource_id: str, patch: Any) -> Credential:
data = patch.to_dict() if hasattr(patch, "to_dict") else patch
resp = self._client._request("PATCH", f"{self._base_path()}/{resource_id}", json=data)
return Credential.from_dict(resp)

def delete(self, resource_id: str) -> None:
self._client._request("DELETE", f"{self._base_path()}/{resource_id}", expect_json=False)

def list_all(self, size: int = 100, **kwargs: Any) -> Iterator[Credential]:
page = 1
while True:
result = self.list(ListOptions().page(page).size(size))
yield from result.items
if page * size >= result.total:
break
page += 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT.
# Source: ../../ambient-api-server/openapi/openapi.yaml
# Spec SHA256: c9d4494778eb0a006db1f289630460fb9b5dc58df1895903c42735b4f56b0314
# Generated: 2026-05-05T15:33:32Z

from __future__ import annotations

from typing import Any, Iterator, Optional, TYPE_CHECKING
from urllib.parse import quote

from ._base import ListOptions
from .scheduled_session import ScheduledSession, ScheduledSessionList

if TYPE_CHECKING:
from .client import AmbientClient


class ScheduledSessionAPI:
def __init__(self, client: AmbientClient) -> None:
self._client = client
def _base_path(self) -> str:
return "/projects/{id}/scheduled-sessions".replace("{id}", quote(self._client._project, safe=""))


def create(self, data: dict) -> ScheduledSession:
resp = self._client._request("POST", self._base_path(), json=data)
return ScheduledSession.from_dict(resp)

def get(self, resource_id: str) -> ScheduledSession:
resp = self._client._request("GET", f"{self._base_path()}/{resource_id}")
return ScheduledSession.from_dict(resp)

def list(self, opts: Optional[ListOptions] = None) -> ScheduledSessionList:
params = opts.to_params() if opts else None
resp = self._client._request("GET", self._base_path(), params=params)
return ScheduledSessionList.from_dict(resp)
def update(self, resource_id: str, patch: Any) -> ScheduledSession:
data = patch.to_dict() if hasattr(patch, "to_dict") else patch
resp = self._client._request("PATCH", f"{self._base_path()}/{resource_id}", json=data)
return ScheduledSession.from_dict(resp)

def delete(self, resource_id: str) -> None:
self._client._request("DELETE", f"{self._base_path()}/{resource_id}", expect_json=False)

def list_all(self, size: int = 100, **kwargs: Any) -> Iterator[ScheduledSession]:
page = 1
while True:
result = self.list(ListOptions().page(page).size(size))
yield from result.items
if page * size >= result.total:
break
page += 1
164 changes: 164 additions & 0 deletions components/ambient-sdk/python-sdk/ambient_platform/credential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT.
# Source: ../../ambient-api-server/openapi/openapi.yaml
# Spec SHA256: c9d4494778eb0a006db1f289630460fb9b5dc58df1895903c42735b4f56b0314
# Generated: 2026-05-05T15:33:32Z

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Optional

from ._base import ListMeta, _parse_datetime


@dataclass(frozen=True)
class Credential:
id: str = ""
kind: str = ""
href: str = ""
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
annotations: str = ""
description: str = ""
email: str = ""
labels: str = ""
name: str = ""
project_id: str = ""
provider: str = ""
token: str = ""
url: str = ""

@classmethod
def from_dict(cls, data: dict) -> Credential:
return cls(
id=data.get("id", ""),
kind=data.get("kind", ""),
href=data.get("href", ""),
created_at=_parse_datetime(data.get("created_at")),
updated_at=_parse_datetime(data.get("updated_at")),
annotations=data.get("annotations", ""),
description=data.get("description", ""),
email=data.get("email", ""),
labels=data.get("labels", ""),
name=data.get("name", ""),
project_id=data.get("project_id", ""),
provider=data.get("provider", ""),
token=data.get("token", ""),
url=data.get("url", ""),
)

@classmethod
def builder(cls) -> CredentialBuilder:
return CredentialBuilder()


@dataclass(frozen=True)
class CredentialList:
kind: str = ""
page: int = 0
size: int = 0
total: int = 0
items: list[Credential] = ()

@classmethod
def from_dict(cls, data: dict) -> CredentialList:
return cls(
kind=data.get("kind", ""),
page=data.get("page", 0),
size=data.get("size", 0),
total=data.get("total", 0),
items=[Credential.from_dict(item) for item in data.get("items", [])],
)


class CredentialBuilder:
def __init__(self) -> None:
self._data: dict[str, Any] = {}


def annotations(self, value: str) -> CredentialBuilder:
self._data["annotations"] = value
return self

def description(self, value: str) -> CredentialBuilder:
self._data["description"] = value
return self

def email(self, value: str) -> CredentialBuilder:
self._data["email"] = value
return self

def labels(self, value: str) -> CredentialBuilder:
self._data["labels"] = value
return self

def name(self, value: str) -> CredentialBuilder:
self._data["name"] = value
return self

def project_id(self, value: str) -> CredentialBuilder:
self._data["project_id"] = value
return self

def provider(self, value: str) -> CredentialBuilder:
self._data["provider"] = value
return self

def token(self, value: str) -> CredentialBuilder:
self._data["token"] = value
return self

def url(self, value: str) -> CredentialBuilder:
self._data["url"] = value
return self

def build(self) -> dict:
if "name" not in self._data:
raise ValueError("name is required")
if "project_id" not in self._data:
raise ValueError("project_id is required")
if "provider" not in self._data:
raise ValueError("provider is required")
return dict(self._data)


class CredentialPatch:
def __init__(self) -> None:
self._data: dict[str, Any] = {}


def annotations(self, value: str) -> CredentialPatch:
self._data["annotations"] = value
return self

def description(self, value: str) -> CredentialPatch:
self._data["description"] = value
return self

def email(self, value: str) -> CredentialPatch:
self._data["email"] = value
return self

def labels(self, value: str) -> CredentialPatch:
self._data["labels"] = value
return self

def name(self, value: str) -> CredentialPatch:
self._data["name"] = value
return self

def provider(self, value: str) -> CredentialPatch:
self._data["provider"] = value
return self

def token(self, value: str) -> CredentialPatch:
self._data["token"] = value
return self

def url(self, value: str) -> CredentialPatch:
self._data["url"] = value
return self

def to_dict(self) -> dict:
return dict(self._data)
Loading
Loading