Skip to content
Merged
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
5 changes: 2 additions & 3 deletions agentkit/toolkit/builders/local_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from datetime import datetime
from agentkit.toolkit.config import CommonConfig
from agentkit.toolkit.config.dataclass_utils import AutoSerializableMixin
from agentkit.toolkit.docker.utils import create_dockerignore_file
from agentkit.toolkit.models import BuildResult, ImageInfo
from agentkit.toolkit.reporter import Reporter
from agentkit.toolkit.errors import ErrorCode
Expand Down Expand Up @@ -334,9 +335,7 @@ def generate_dockerfile_content() -> str:
force_regenerate=force_regenerate,
)

dockerignore_path = self.workdir / ".dockerignore"
if not dockerignore_path.exists():
renderer.create_dockerignore(str(dockerignore_path))
create_dockerignore_file(str(self.workdir))
image_name = f"{docker_config.image_name or 'agentkit-app'}"
image_tag = f"{docker_config.image_tag or 'latest'}"

Expand Down
69 changes: 56 additions & 13 deletions agentkit/toolkit/config/dataclass_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,40 @@

T = TypeVar("T")

SENSITIVE_FIELDS = {
"runtime_envs",
"runtime_apikey_name",
"runtime_apikey",
"runtime_jwt_discovery_url",
"runtime_jwt_allowed_clients",
}


def _get_safe_value(field_name: str, value: Any) -> Any:
if field_name in SENSITIVE_FIELDS:
return "******"
return value


def _sanitize_dict(data: Dict[str, Any]) -> Dict[str, Any]:
"""Sanitize sensitive fields in a dictionary."""
if not isinstance(data, dict):
return data
return {k: _get_safe_value(k, v) for k, v in data.items()}


def _sanitize_diff(diff: Dict[str, Any]) -> Dict[str, Any]:
"""Sanitize sensitive fields in a diff dictionary."""
if not isinstance(diff, dict):
return diff
sanitized = {}
for k, v in diff.items():
if k in SENSITIVE_FIELDS:
sanitized[k] = ("******", "******")
else:
sanitized[k] = v
return sanitized


class DataclassSerializer:
@staticmethod
Expand Down Expand Up @@ -50,7 +84,7 @@ def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
logger.debug(
"[DataclassSerializer] source=local field=%s value=%r",
field_name,
kwargs[field_name],
_get_safe_value(field_name, kwargs[field_name]),
)
else:
# Try aliases (backward compatibility)
Expand All @@ -65,7 +99,7 @@ def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
"[DataclassSerializer] source=alias(%s) -> local field=%s value=%r",
alias,
field_name,
kwargs[field_name],
_get_safe_value(field_name, kwargs[field_name]),
)
break

Expand All @@ -77,15 +111,15 @@ def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
logger.debug(
"[DataclassSerializer] source=default_factory field=%s value=%r",
field_name,
kwargs[field_name],
_get_safe_value(field_name, kwargs[field_name]),
)
elif field.default is not MISSING:
kwargs[field_name] = field.default
_sources[field_name] = "default"
logger.debug(
"[DataclassSerializer] source=default field=%s value=%r",
field_name,
kwargs[field_name],
_get_safe_value(field_name, kwargs[field_name]),
)
else:
kwargs[field_name] = None
Expand Down Expand Up @@ -157,7 +191,11 @@ def from_dict(cls: Type[T], data: Dict[str, Any], skip_render: bool = False) ->
from .global_config import apply_global_config_defaults

before = instance.to_dict()
logger.debug("from_dict: before globals for %s -> %r", cls.__name__, before)
logger.debug(
"from_dict: before globals for %s -> %r",
cls.__name__,
_sanitize_dict(before),
)
instance = apply_global_config_defaults(instance, data)
after = instance.to_dict()
if before != after:
Expand All @@ -169,7 +207,7 @@ def from_dict(cls: Type[T], data: Dict[str, Any], skip_render: bool = False) ->
logger.debug(
"from_dict: applied global defaults for %s; changes=%r",
cls.__name__,
diff,
_sanitize_diff(diff),
)
else:
logger.debug(
Expand Down Expand Up @@ -254,7 +292,7 @@ def _render_template_fields(self):
"[%s] [template] start field render check: name=%s, value=%r, has_placeholders=%s",
cfg_name,
field_info.name,
field_value,
_get_safe_value(field_info.name, field_value),
(
isinstance(field_value, str)
and ("{{" in field_value and "}}" in field_value)
Expand All @@ -269,7 +307,7 @@ def _render_template_fields(self):
"[%s] [template] field %s is Auto/empty -> using default_template=%r",
cfg_name,
field_info.name,
default_template,
_get_safe_value(field_info.name, default_template),
)
field_value = default_template
self._template_originals[field_info.name] = default_template
Expand Down Expand Up @@ -300,17 +338,20 @@ def _render_template_fields(self):
"[%s] [template] save original template for %s: %r",
cfg_name,
field_info.name,
field_value,
_get_safe_value(field_info.name, field_value),
)

try:
rendered = render_template(field_value)
is_sensitive = field_info.name in SENSITIVE_FIELDS
rendered = render_template(
field_value, sensitive=is_sensitive
)
logger.debug(
"[%s] [template] rendered field %s: %r -> %r",
cfg_name,
field_info.name,
field_value,
rendered,
_get_safe_value(field_info.name, field_value),
_get_safe_value(field_info.name, rendered),
)
# Fail if unresolved placeholders remain
if "{{" in str(rendered) and "}}" in str(rendered):
Expand Down Expand Up @@ -356,7 +397,9 @@ def _render_template_fields(self):
"[%s] [template] field %s is not marked for rendering, value: %r",
cfg_name,
field_info.name,
getattr(self, field_info.name),
_get_safe_value(
field_info.name, getattr(self, field_info.name)
),
)
except ImportError:
# If template utils are not available, no-op
Expand Down
68 changes: 8 additions & 60 deletions agentkit/toolkit/docker/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import docker
from docker.errors import DockerException, ImageNotFound

from agentkit.toolkit.docker.utils import create_dockerignore_file

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -84,10 +86,8 @@ def render_dockerfile(

# Create .dockerignore file if requested
if create_dockerignore:
dockerignore_path = os.path.join(
output_dir or os.path.dirname(output_path), ".dockerignore"
)
self.create_dockerignore(dockerignore_path, dockerignore_entries)
target_dir = output_dir or os.path.dirname(output_path)
create_dockerignore_file(target_dir, dockerignore_entries)

return rendered_content

Expand All @@ -106,69 +106,17 @@ def create_dockerignore(
"""
Create .dockerignore file with default and additional entries.

Deprecated: Use agentkit.toolkit.docker.utils.create_dockerignore_file instead.

Args:
dockerignore_path: Path to .dockerignore file
additional_entries: Additional entries to add to .dockerignore

Raises:
IOError: When file write fails
"""
try:
# Check if .dockerignore already exists
if os.path.exists(dockerignore_path):
logger.info(
f".dockerignore already exists at: {dockerignore_path}, skipping creation"
)
return

# Default entries to exclude
default_entries = [
"# AgentKit configuration",
"agentkit.yaml",
"agentkit*.yaml",
"",
"# Python cache",
"__pycache__/",
"*.py[cod]",
"*$py.class",
"",
"# Virtual environments",
".venv/",
"venv/",
"ENV/",
"env/",
"",
"# IDE",
".vscode/",
".idea/",
".windsurf/",
"",
"# Git",
".git/",
".gitignore",
"",
"# Docker",
"Dockerfile*",
".dockerignore",
]

# Combine default and additional entries
all_entries = default_entries.copy()
if additional_entries:
all_entries.append("")
all_entries.append("# Additional entries")
all_entries.extend(additional_entries)

# Write .dockerignore file
with open(dockerignore_path, "w", encoding="utf-8") as f:
f.write("\n".join(all_entries))
f.write("\n") # End with newline

logger.info(f"Successfully created .dockerignore at: {dockerignore_path}")

except Exception as e:
logger.error(f"Error creating .dockerignore: {str(e)}")
raise
target_dir = os.path.dirname(dockerignore_path)
create_dockerignore_file(target_dir, additional_entries)


class DockerManager:
Expand Down
98 changes: 98 additions & 0 deletions agentkit/toolkit/docker/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import os
import logging
from typing import List, Optional

logger = logging.getLogger(__name__)


def create_dockerignore_file(
target_dir: str, additional_entries: Optional[List[str]] = None
) -> bool:
"""
Create .dockerignore file with default and additional entries.

Args:
target_dir: Directory where .dockerignore should be created
additional_entries: Additional entries to add to .dockerignore

Returns:
bool: True if file was created, False if it already existed

Raises:
IOError: When file write fails
"""
dockerignore_path = os.path.join(target_dir, ".dockerignore")

try:
# Check if .dockerignore already exists
if os.path.exists(dockerignore_path):
logger.info(
f".dockerignore already exists at: {dockerignore_path}, skipping creation"
)
return False

# Default entries to exclude
default_entries = [
"# AgentKit configuration",
"agentkit.yaml",
"agentkit*.yaml",
".agentkit/",
"",
"# Python cache",
"__pycache__/",
"*.py[cod]",
"*$py.class",
"",
"# Virtual environments",
".venv/",
"venv/",
"ENV/",
"env/",
"",
"# IDE",
".vscode/",
".idea/",
".windsurf/",
"",
"# Git",
".git/",
".gitignore",
"",
"# Docker",
"Dockerfile*",
".dockerignore",
]

# Combine default and additional entries
all_entries = default_entries.copy()
if additional_entries:
all_entries.append("")
all_entries.append("# Additional entries")
all_entries.extend(additional_entries)

# Write .dockerignore file
with open(dockerignore_path, "w", encoding="utf-8") as f:
f.write("\n".join(all_entries))
f.write("\n") # End with newline

logger.info(f"Successfully created .dockerignore at: {dockerignore_path}")
return True

except Exception as e:
logger.error(f"Error creating .dockerignore: {str(e)}")
raise
Loading