Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ docs/node_modules/
test-agents/*
tos_doc_example
agentkit*.yaml
# harness deploy registry — contains the runtime API key in plaintext
harness.json
Dockerfile
Dockerfile-base
local_build.py
Expand Down
16 changes: 15 additions & 1 deletion agentkit/apps/agent_server_app/origin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import inspect
import logging
import os
import re
from collections.abc import Callable
Expand All @@ -21,6 +22,8 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

logger = logging.getLogger(__name__)

_REGEX_PREFIX = "regex:"

DEFAULT_AGENTKIT_ALLOW_ORIGINS = ["*"]
Expand Down Expand Up @@ -114,11 +117,22 @@ def add_cors_compat_middleware(app: FastAPI, allow_origins: list[str]) -> None:
"""Add CORS middleware for ADK versions that cannot parse regex origins."""

literal_origins, allow_origin_regex = split_allow_origins(allow_origins)
# A wildcard origin combined with allow_credentials=True is unsafe: Starlette
# then echoes the caller's Origin back with Access-Control-Allow-Credentials,
# which lets *any* site make credentialed cross-origin calls. Only enable
# credentials when the origin set is an explicit allowlist.
allow_credentials = "*" not in literal_origins
if not allow_credentials:
logger.warning(
"CORS allow_origins includes '*'; disabling allow_credentials. Set "
"AGENTKIT_ALLOW_ORIGINS (or allow_origins=) to an explicit allowlist "
"to permit credentialed cross-origin requests."
)
app.add_middleware(
CORSMiddleware,
allow_origins=literal_origins,
allow_origin_regex=allow_origin_regex,
allow_credentials=True,
allow_credentials=allow_credentials,
allow_methods=["*"],
allow_headers=["*"],
)
Expand Down
26 changes: 19 additions & 7 deletions agentkit/sdk/identity/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,21 @@
from agentkit.toolkit.errors import ApiError


def requires_api_key(*, provider_name: str, into: str = "api_key"):
def requires_api_key(
*,
provider_name: str,
into: str = "api_key",
identity_token: str | None = None,
):
"""Decorator that fetches an API key before calling the decorated function.

Args:
provider_name: The credential provider name
into: Parameter name to inject the API key into
identity_token: Workload identity token forwarded to ``GetResourceApiKey``
as ``IdentityToken``. Supply this when the provider validates it;
when omitted the field is not sent (previously a hard-coded
``"identity_token"`` placeholder was sent, which never validated).

Returns:
Decorator function
Expand All @@ -41,17 +50,20 @@ def _get_api_key() -> str:
endpoint = resolve_endpoint("identity")
access_key = creds.access_key
secret_key = creds.secret_key
session_token = ""
# Forward the STS session token so signing works under temporary
# credentials (ve_request signs X-Security-Token when present).
session_token = creds.session_token or None

request_body: dict[str, str] = {"ProviderName": provider_name}
if identity_token:
request_body["IdentityToken"] = identity_token

response = ve_request(
request_body={
"ProviderName": provider_name,
"IdentityToken": "identity_token",
},
request_body=request_body,
action="GetResourceApiKey",
header={"X-Security-Token": session_token} if session_token else {},
ak=access_key,
sk=secret_key,
session_token=session_token,
version=endpoint.api_version,
service=endpoint.service,
host=endpoint.host,
Expand Down
6 changes: 4 additions & 2 deletions agentkit/toolkit/builders/ve_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1611,8 +1611,10 @@ def download_and_show_logs(run_id: str):
self.reporter.success(f"Pipeline triggered successfully, run ID: {run_id}")
self.reporter.info("Waiting for build completion...")

# Wait for build completion using reporter's long task interface
max_wait_time = 900 # 15 minutes
# Wait for build completion using reporter's long task interface.
# Honor the configured build_timeout (default 3600s); large images
# (heavy deps, multi-stage Go builds) routinely exceed 15 minutes.
max_wait_time = config.build_timeout or 900
check_interval = 3 # Check every 3 seconds
expected_time = (
30 # Controls progress curve speed (smaller = faster initial progress)
Expand Down
48 changes: 48 additions & 0 deletions agentkit/toolkit/cli/cli_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
to its id via ``ListInboundAuthConfigs``.
"""

import sys
from typing import Optional

import typer
Expand All @@ -33,6 +34,27 @@
)


def _stdin_is_interactive() -> bool:
"""Whether a human is at the keyboard (as opposed to CI / a pipe)."""
try:
return sys.stdin.isatty()
except (AttributeError, ValueError):
return False


def _confirm_deletion(message: str, *, force: bool) -> None:
"""Guard a destructive delete with a confirmation prompt.

Only prompts in an interactive terminal: non-interactive callers (CI,
scripts, pipes) keep the historical behavior of deleting without a prompt,
so adding this guard is not a breaking change for automation. Pass
``--force`` to skip the prompt interactively too.
"""
if force or not _stdin_is_interactive():
return
typer.confirm(message, abort=True)


@delete_app.command("credential")
def delete_credential_command(
name: str = typer.Argument(..., help="Credential name to delete."),
Expand All @@ -44,6 +66,9 @@ def delete_credential_command(
"Defaults to VOLCENGINE_AGENTKIT_REGION/VOLCENGINE_REGION/global config."
),
),
force: bool = typer.Option(
False, "--force", "-f", help="Skip the confirmation prompt."
),
):
"""Delete a credential (inbound auth config) by name."""
from agentkit.toolkit.cli.utils import PaginationHelper
Expand Down Expand Up @@ -73,6 +98,20 @@ def build_request(next_token_val):
console.print(f"[red]Error: credential '{name}' not found.[/red]")
raise typer.Exit(1)

if len(matches) > 1:
console.print(
f"[yellow]{len(matches)} credentials named '{name}' will be deleted:"
"[/yellow]"
)
for config in matches:
console.print(f" - {config.inbound_auth_config_id}")

_confirm_deletion(
f"Delete credential '{name}' ({len(matches)} config(s))? "
"This cannot be undone.",
force=force,
)

for config in matches:
config_id = config.inbound_auth_config_id
if not config_id:
Expand All @@ -98,6 +137,9 @@ def delete_harness_command(
timeout: int = typer.Option(
300, "--timeout", help="Max seconds to wait for the async deletion to finish."
),
force: bool = typer.Option(
False, "--force", "-f", help="Skip the confirmation prompt."
),
):
"""Delete a harness runtime by name.

Expand Down Expand Up @@ -151,6 +193,12 @@ def _is_harness(runtime) -> bool:
)
raise typer.Exit(1)

_confirm_deletion(
f"Delete harness '{name}' ({len(harness_matches)} runtime(s))? "
"This cannot be undone.",
force=force,
)

for runtime in harness_matches:
runtime_id = runtime.runtime_id
console.print(
Expand Down
9 changes: 7 additions & 2 deletions agentkit/toolkit/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,13 @@ def _deploy_harness(
console.print(f"[green]Runtime id: {meta['runtime_id']}[/green]")
console.print(f"[green]Endpoint: {endpoint or '(see AgentKit console)'}[/green]")
if meta.get("runtime_apikey"):
console.print(f"[green]API key: {meta['runtime_apikey']}[/green]")
from agentkit.utils.redact import mask

# Avoid leaking the full key into terminal scrollback / CI logs; the
# complete value is persisted (0600) in harness.json below.
console.print(f"[green]API key: {mask(meta['runtime_apikey'])}[/green]")
if endpoint:
console.print(
f"[green]Recorded in {(Path.cwd() / 'harness.json')}[/green]"
f"[green]Recorded in {(Path.cwd() / 'harness.json')} "
"(contains the API key — do not commit)[/green]"
)
19 changes: 17 additions & 2 deletions agentkit/toolkit/cli/sandbox/cli_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@
import shutil
import signal
import sys
import termios
import threading
import tty
from contextlib import contextmanager

try:
# POSIX-only terminal APIs; absent on Windows. Import lazily so that merely
# importing the CLI (e.g. `agentkit --help`) does not crash on Windows.
import termios
import tty
except ImportError: # pragma: no cover - exercised only on Windows
termios = None # type: ignore[assignment]
tty = None # type: ignore[assignment]
from pathlib import Path
from typing import Iterator, Optional

Expand Down Expand Up @@ -77,6 +84,14 @@ def _terminal_size() -> dict[str, int]:

@contextmanager
def _raw_terminal_mode() -> Iterator[None]:
if termios is None or tty is None:
typer.echo(
"Interactive sandbox exec/shell requires a POSIX terminal "
"(termios/tty) and is not supported on this platform.",
err=True,
)
raise typer.Exit(1)

if not sys.stdin.isatty():
yield
return
Expand Down
9 changes: 8 additions & 1 deletion agentkit/toolkit/harness/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,14 @@ def _record_harness(
}
else:
data[name] = {"url": url, "key": key or "", "runtime_id": runtime_id}
path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
payload = json.dumps(data, indent=2, ensure_ascii=False)
# harness.json stores the runtime API key in plaintext, so keep it private
# (0600). os.open applies the mode only when creating the file; chmod after
# covers the case where an older, world-readable harness.json already exists.
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(payload)
os.chmod(path, 0o600)
return path


Expand Down
13 changes: 13 additions & 0 deletions agentkit/toolkit/volcengine/code_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(
secret_key: str = "",
region: str = "",
provider: str | None = None,
session_token: str | None = None,
) -> None:
# Use provided region or None to trigger auto-detection in VolcConfiguration
config = VolcConfiguration(
Expand All @@ -49,13 +50,16 @@ def __init__(
creds = config.get_service_credentials("cp")
self.volcengine_access_key = creds.access_key
self.volcengine_secret_key = creds.secret_key
# STS credentials carry a session token that must be signed in.
self.session_token = creds.session_token
elif not all([access_key, secret_key]):
raise ValueError(
"Error create cp instance: missing access key or secret key",
)
else:
self.volcengine_access_key = access_key
self.volcengine_secret_key = secret_key
self.session_token = session_token

endpoint = config.get_service_endpoint("cp")

Expand All @@ -73,6 +77,7 @@ def _get_default_workspace(self) -> str:
action="GetDefaultWorkspaceInner",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down Expand Up @@ -143,6 +148,7 @@ def create_workspace(
action="CreateWorkspace",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down Expand Up @@ -215,6 +221,7 @@ def list_workspaces(
action="ListWorkspaces",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down Expand Up @@ -315,6 +322,7 @@ def _create_pipeline(
action="CreatePipeline",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down Expand Up @@ -375,6 +383,7 @@ def run_pipeline(
action="RunPipeline",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down Expand Up @@ -468,6 +477,7 @@ def list_pipeline_runs(
action="ListPipelineRuns",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down Expand Up @@ -578,6 +588,7 @@ def list_pipelines(
action="ListPipelines",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down Expand Up @@ -693,6 +704,7 @@ def list_pipeline_run_stages_inner(
action="ListPipelineRunStagesInner",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down Expand Up @@ -766,6 +778,7 @@ def get_task_run_log_download_uri(
action="GetTaskRunLogDownloadURI",
ak=self.volcengine_access_key,
sk=self.volcengine_secret_key,
session_token=self.session_token,
service=self.service,
version=self.version,
region=self.region,
Expand Down
Loading
Loading