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: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ seed/**/.pytest_cache/
seed/**/.mypy_cache/
seed/**/.ruff_cache/
seed/**/*.egg-info/
seed/**/.golangci.yml

# Seed remote-local test outputs, except for seed.yml files
seed-remote-local/*
Expand All @@ -119,5 +120,9 @@ seed-remote-local/*
# Test logs from test-remote-local.sh
test-remote-local-logs-*/

# macOS system dirs leaked by local Poetry/Python execution
seed/**/Library/Application Support/
seed/**/Library/Caches/

# Devbox gradle properties
/gradle.properties
12 changes: 9 additions & 3 deletions devbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
"poetry@1.8",
"jdk@17",
"gradle@8.5",
"ruby@3.3",
"ruby@3.4",
"rubocop@latest",
"php@8.4",
"php84Packages.composer@2.7",
"rustup@latest",
"ruff@0.15",
"buf@1.50.0",
Expand All @@ -28,10 +27,17 @@
"printf '\\033]0;%s\\007' 'fern (devbox)'",
"unset GOROOT # Unset GOROOT from parent shell to avoid conflicts with devbox's Go",
"export PATH=\"$(go env GOPATH)/bin:$PATH\"",
"command -v golangci-lint &>/dev/null || { echo 'Installing golangci-lint...'; go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 2>/dev/null || true; }",
"mkdir -p \"$HOME/.local/bin\"",
"if [ ! -f \"$HOME/.local/bin/composer\" ]; then echo 'Installing composer...'; curl -sSL https://getcomposer.org/download/latest-2.x/composer.phar -o \"$HOME/.local/bin/composer\" && chmod +x \"$HOME/.local/bin/composer\"; fi",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Unverified binary downloads in devbox init hook

Two PHP PHAR executables are downloaded via curl -sSL … | chmod +x with no integrity check (no --sha256 or signature verification). The latest-2.x URL path for composer is mutable — it resolves to whatever the server currently serves. If either getcomposer.org or cs.symfony.com is compromised, or if DNS/routing is hijacked, a malicious PHP executable will be silently installed and placed on $PATH of every developer who initialises this devbox, giving the attacker arbitrary code execution on their machine. The once-written guard ([ ! -f … ]) prevents re-download but provides no tamper detection after the fact.

Prompt To Fix With AI
Pin the composer and php-cs-fixer downloads to a specific, immutable release URL and verify a SHA-256 checksum before making the file executable. For example:

```bash
# Composer: pin to a specific version and verify checksum
COMPOSER_VERSION="2.8.4"
COMPOSER_EXPECTED_SHA="<sha256 from https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar.sha256sum>"
if [ ! -f "$HOME/.local/bin/composer" ]; then
  curl -sSL "https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar" -o "$HOME/.local/bin/composer"
  echo "${COMPOSER_EXPECTED_SHA}  $HOME/.local/bin/composer" | sha256sum -c - || { rm "$HOME/.local/bin/composer"; echo 'Composer checksum mismatch!'; exit 1; }
  chmod +x "$HOME/.local/bin/composer"
fi

# php-cs-fixer: similarly pin to a versioned URL
PHP_CS_FIXER_VERSION="v3.65.0"
PHP_CS_FIXER_EXPECTED_SHA="<sha256 from the release>"
if [ ! -f "$HOME/.local/bin/php-cs-fixer" ]; then
  curl -sSL "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/download/${PHP_CS_FIXER_VERSION}/php-cs-fixer.phar" -o "$HOME/.local/bin/php-cs-fixer"
  echo "${PHP_CS_FIXER_EXPECTED_SHA}  $HOME/.local/bin/php-cs-fixer" | sha256sum -c - || { rm "$HOME/.local/bin/php-cs-fixer"; echo 'php-cs-fixer checksum mismatch!'; exit 1; }
  chmod +x "$HOME/.local/bin/php-cs-fixer"
fi
```

Update the expected SHAs any time you bump the version. This eliminates the mutable-URL risk and detects tampering of already-cached files.

Severity: medium | Confidence: 80%

"if [ ! -f \"$HOME/.local/bin/php-cs-fixer\" ]; then echo 'Installing php-cs-fixer...'; curl -sSL https://cs.symfony.com/download/php-cs-fixer-v3.phar -o \"$HOME/.local/bin/php-cs-fixer\" && chmod +x \"$HOME/.local/bin/php-cs-fixer\"; fi",
"export PATH=\"$HOME/.local/bin:$PATH\"",
"if [ -f /usr/bin/xcrun ]; then mkdir -p \"$HOME/.local/bin\" && ln -sf /usr/bin/xcrun \"$HOME/.local/bin/xcrun\"; fi # Use system xcrun instead of nix xcbuild",
"rustup default stable 2>/dev/null || true",
"rustup component add rustfmt 2>/dev/null || true",
"bash scripts/setup-dotnet.sh 2>/dev/null || true",
"export PATH=\"$HOME/.dotnet:$PATH\"",
"export DOTNET_ROOT=\"$HOME/.dotnet\"",
"export PATH=\"$HOME/.dotnet:$HOME/.dotnet/tools:$PATH\"",
"echo 'Node.js:' $(node --version)",
"echo 'pnpm:' $(pnpm --version 2>/dev/null || echo 'not available')",
"echo 'Go:' $(go version)",
Expand Down
80 changes: 16 additions & 64 deletions devbox.lock
Original file line number Diff line number Diff line change
Expand Up @@ -483,54 +483,6 @@
}
}
},
"php84Packages.composer@2.7": {
"last_modified": "2024-10-02T21:32:52Z",
"resolved": "github:NixOS/nixpkgs/fd698a4ab779fb7fb95425f1b56974ba9c2fa16c#php84Packages.composer",
"source": "devbox-search",
"version": "2.7.9",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/nh3pqv4bsgivzvmc919sqvv8km265c77-composer-2.7.9",
"default": true
}
],
"store_path": "/nix/store/nh3pqv4bsgivzvmc919sqvv8km265c77-composer-2.7.9"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/iardqmy3i0cy4l9r045p7g5qb9dca8p9-composer-2.7.9",
"default": true
}
],
"store_path": "/nix/store/iardqmy3i0cy4l9r045p7g5qb9dca8p9-composer-2.7.9"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/x6z4hy24dn5l9inqcs4a5wm5layiil67-composer-2.7.9",
"default": true
}
],
"store_path": "/nix/store/x6z4hy24dn5l9inqcs4a5wm5layiil67-composer-2.7.9"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/9ajnzd7i6ywxv7mav92032iryrc4jm0g-composer-2.7.9",
"default": true
}
],
"store_path": "/nix/store/9ajnzd7i6ywxv7mav92032iryrc4jm0g-composer-2.7.9"
}
}
},
"php@8.4": {
"last_modified": "2026-03-21T07:29:51Z",
"plugin_version": "0.0.3",
Expand Down Expand Up @@ -750,68 +702,68 @@
}
}
},
"ruby@3.3": {
"last_modified": "2026-01-23T17:20:52Z",
"ruby@3.4": {
"last_modified": "2026-03-21T07:29:51Z",
"plugin_version": "0.0.2",
"resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#ruby",
"resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#ruby",
"source": "devbox-search",
"version": "3.3.10",
"version": "3.4.8",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/d9wal8y7w1zpvyas3x1q4ykz880mmklk-ruby-3.3.10",
"path": "/nix/store/88sazm3fj9kabg4pxmnjc53cx0syp8fk-ruby-3.4.8",
"default": true
},
{
"name": "devdoc",
"path": "/nix/store/1rfqp0848j3gnm222ls3bipk1azcrrq3-ruby-3.3.10-devdoc"
"path": "/nix/store/mk7a50jsk138apqrnlzc077cik8cr47m-ruby-3.4.8-devdoc"
}
],
"store_path": "/nix/store/d9wal8y7w1zpvyas3x1q4ykz880mmklk-ruby-3.3.10"
"store_path": "/nix/store/88sazm3fj9kabg4pxmnjc53cx0syp8fk-ruby-3.4.8"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/1hlahw0ijkxx1aqy3x41k3gxpgv34g7d-ruby-3.3.10",
"path": "/nix/store/rpcpk0lxd8iliza4nl9apsgrpsb2yk4d-ruby-3.4.8",
"default": true
},
{
"name": "devdoc",
"path": "/nix/store/arvi0gqvw07ngbi2ci20dn5ka2jz5irv-ruby-3.3.10-devdoc"
"path": "/nix/store/9wnq7dd2yw2lbjs69y19lm08ranqa930-ruby-3.4.8-devdoc"
}
],
"store_path": "/nix/store/1hlahw0ijkxx1aqy3x41k3gxpgv34g7d-ruby-3.3.10"
"store_path": "/nix/store/rpcpk0lxd8iliza4nl9apsgrpsb2yk4d-ruby-3.4.8"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/as6xdshxpansvkag8zqr602ajkn9079z-ruby-3.3.10",
"path": "/nix/store/gq0vdqfz16vf64ip8p749rmygxd7n3x1-ruby-3.4.8",
"default": true
},
{
"name": "devdoc",
"path": "/nix/store/wix1487x3br4gxa0il4q6llz5xyqxspl-ruby-3.3.10-devdoc"
"path": "/nix/store/k3yn26wvgk4mfrd5nw4a23wnijd9hz6c-ruby-3.4.8-devdoc"
}
],
"store_path": "/nix/store/as6xdshxpansvkag8zqr602ajkn9079z-ruby-3.3.10"
"store_path": "/nix/store/gq0vdqfz16vf64ip8p749rmygxd7n3x1-ruby-3.4.8"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/6jz2pgmsh06z9a83qi33f6lp9w2q6mzm-ruby-3.3.10",
"path": "/nix/store/s57rf37r1i38kpm3nlv4h8f8z2w2n80d-ruby-3.4.8",
"default": true
},
{
"name": "devdoc",
"path": "/nix/store/kah8xsbcd10iakxqmlw558iarhsrd5vi-ruby-3.3.10-devdoc"
"path": "/nix/store/4k6c15chplw6cicc6f3lgzmi7i4792j6-ruby-3.4.8-devdoc"
}
],
"store_path": "/nix/store/6jz2pgmsh06z9a83qi33f6lp9w2q6mzm-ruby-3.3.10"
"store_path": "/nix/store/s57rf37r1i38kpm3nlv4h8f8z2w2n80d-ruby-3.4.8"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions generators/base/src/AbstractGeneratorAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ReferenceConfigBuilder } from "./reference/index.js";
import { RawGithubConfig, resolveGitHubConfig } from "./utils/index.js";

const FEATURES_CONFIG_PATHS = [
...(process.env.FERN_FEATURES_YML_PATH != null ? [process.env.FERN_FEATURES_YML_PATH] : []),
"/assets/features.yml",
path.join(__dirname, "./features.yml"),
path.join(__dirname, "./assets/features.yml"),
Expand Down
6 changes: 3 additions & 3 deletions generators/python/setup-python.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Use 3.9 because some of our internal SDKs are on extremely old generator
# Use 3.10 because some of our internal SDKs are on extremely old generator
# versions that don't support 3.8
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
pyenv shell 3.9
pyenv shell 3.10
pip install poetry
poetry env use 3.9
poetry env use 3.10
14 changes: 10 additions & 4 deletions generators/python/src/fern_python/generators/sdk/as_is_copier.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,11 @@ def _copy_directory_to_project(
path_in_project: str,
replacements: Optional[Dict[str, str]] = None,
) -> None:
source = (
os.path.join(os.path.dirname(__file__), "../../../../../") if "PYTEST_CURRENT_TEST" in os.environ else "/assets"
source = os.environ.get(
"FERN_ASSETS_PATH",
os.path.join(os.path.dirname(__file__), "../../../../../")
if "PYTEST_CURRENT_TEST" in os.environ
else "/assets",
)

for _, _, files in os.walk(os.path.join(source, relative_path_on_disk)):
Expand All @@ -130,8 +133,11 @@ def _copy_file_to_project(
replacements: Optional[Dict[str, str]] = None,
) -> None:
# Project root source, so all from_ requests should be relative to that
source = (
os.path.join(os.path.dirname(__file__), "../../../../../") if "PYTEST_CURRENT_TEST" in os.environ else "/assets"
source = os.environ.get(
"FERN_ASSETS_PATH",
os.path.join(os.path.dirname(__file__), "../../../../../")
if "PYTEST_CURRENT_TEST" in os.environ
else "/assets",
)
SourceFileFactory.add_source_file_from_disk(
project=project,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,23 @@ def copy_to_project(self, *, project: Project) -> None:
else:
project.add_dependency(PYDANTIC_DEPENDENCY)

@staticmethod
def _resolve_core_utilities_path(relative_filepath: str) -> str:
"""Resolve the core utilities source directory.
Supports FERN_CORE_UTILITIES_PATH env var with colon-separated paths
for local execution where sdk/ and shared/ are separate directories.
"""
env_paths = os.environ.get("FERN_CORE_UTILITIES_PATH")
if env_paths is not None:
for source in env_paths.split(":"):
if os.path.exists(os.path.join(source, relative_filepath)):
return source
return env_paths.split(":")[0]
Comment on lines +338 to +343
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback returns first path even when file doesn't exist in any path. If FERN_CORE_UTILITIES_PATH contains multiple colon-separated paths but relative_filepath doesn't exist in any of them, line 343 returns the first path regardless. This will cause file-not-found errors downstream.

Should raise an error or return a default when no valid path is found:

if env_paths is not None:
    for source in env_paths.split(":"):
        if os.path.exists(os.path.join(source, relative_filepath)):
            return source
    # File not found in any provided path - fall through to default behavior

Remove line 343 to fall through to the default logic instead of blindly returning an invalid path.

Suggested change
env_paths = os.environ.get("FERN_CORE_UTILITIES_PATH")
if env_paths is not None:
for source in env_paths.split(":"):
if os.path.exists(os.path.join(source, relative_filepath)):
return source
return env_paths.split(":")[0]
env_paths = os.environ.get("FERN_CORE_UTILITIES_PATH")
if env_paths is not None:
for source in env_paths.split(":"):
if os.path.exists(os.path.join(source, relative_filepath)):
return source

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

if "PYTEST_CURRENT_TEST" in os.environ:
return os.path.join(os.path.dirname(__file__), "../../../../../core_utilities/sdk")
return "/assets/core_utilities"

def _copy_file_to_project(
self,
*,
Expand All @@ -337,11 +354,7 @@ def _copy_file_to_project(
exports: Set[str],
string_replacements: Optional[dict[str, str]] = None,
) -> None:
source = (
os.path.join(os.path.dirname(__file__), "../../../../../core_utilities/sdk")
if "PYTEST_CURRENT_TEST" in os.environ
else "/assets/core_utilities"
)
source = self._resolve_core_utilities_path(relative_filepath_on_disk)
SourceFileFactory.add_source_file_from_disk(
project=project,
path_on_disk=os.path.join(source, relative_filepath_on_disk),
Expand All @@ -352,11 +365,7 @@ def _copy_file_to_project(

def _copy_http_sse_folder_to_project(self, *, project: Project) -> None:
"""Copy the http_sse folder using the same approach as individual file copying"""
source = (
os.path.join(os.path.dirname(__file__), "../../../../../core_utilities/sdk")
if "PYTEST_CURRENT_TEST" in os.environ
else "/assets/core_utilities"
)
source = self._resolve_core_utilities_path("http_sse")
folder_path_on_disk = os.path.join(source, "http_sse")

# Define exports for each file
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import subprocess
import sys
from pathlib import Path
Expand All @@ -7,8 +8,8 @@
import fern.generator_exec as generator_exec

# v2BinPath is the path to the python-v2 binary included in the SDK
# generator docker image.
V2_BIN_PATH = "/bin/python-v2"
# generator docker image. Can be overridden via PYTHON_V2_PATH for local execution.
V2_BIN_PATH = os.environ.get("PYTHON_V2_PATH", "/bin/python-v2")


class PythonV2Generator:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"java:build": "turbo run dockerTagLatest --filter @fern-api/java-sdk",
"php:build": "turbo run dockerTagLatest --filter @fern-api/php-sdk",
"python:build": "turbo run dist:cli --filter @fern-api/python-sdk && docker build . -f ./generators/python/sdk/Dockerfile -t fernapi/fern-python-sdk:latest",
"python:check:fix": "cd generators/python && bash setup-python.sh && poetry run pre-commit run --all-files",
"ruby:build": "turbo run dockerTagLatest --filter @fern-api/ruby-sdk",
"rust:build": "turbo run dockerTagLatest --filter @fern-api/rust-sdk",
"swift:build": "turbo run dockerTagLatest --filter @fern-api/swift-sdk",
Expand Down
19 changes: 17 additions & 2 deletions scripts/setup-dotnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,25 @@ main() {
fi
done

# Ensure tools path is available for installing global tools
export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:$PATH"

# Install CSharpier if not already installed
if ! command -v csharpier &> /dev/null; then
echo -e "${YELLOW}Installing CSharpier...${NC}"
if dotnet tool install -g csharpier; then
echo -e "${GREEN}✓${NC} CSharpier installed successfully"
else
echo -e "${RED}✗${NC} Failed to install CSharpier"
fi
else
echo -e "${GREEN}✓${NC} CSharpier is already installed"
fi

echo ""
echo -e "${YELLOW}Make sure \$HOME/.dotnet is in your PATH.${NC}"
echo -e "${YELLOW}Make sure \$HOME/.dotnet and \$HOME/.dotnet/tools are in your PATH.${NC}"
echo "Add to your shell profile (~/.zshrc, ~/.bashrc, etc):"
echo " export PATH=\"\$HOME/.dotnet:\$PATH\""
echo " export PATH=\"\$HOME/.dotnet:\$HOME/.dotnet/tools:\$PATH\""
echo ""
echo "Alternatively, follow the official Microsoft installation guide:"
echo " https://learn.microsoft.com/en-us/dotnet/core/install/"
Expand Down
11 changes: 8 additions & 3 deletions seed/java-sdk/seed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,14 @@ fixtures:
outputFolder: source-root
skipScripts: true
scripts:
- image: fernapi/java-seed
commands:
- source ~/.bash_profile && jenv shell 1.8 && source ~/.bash_profile && ./gradlew compileJava spotlessCheck
docker:
- image: fernapi/java-seed
commands:
- source ~/.bash_profile && jenv shell 1.8 && source ~/.bash_profile && ./gradlew compileJava spotlessCheck
local:
- commands:
- ./gradlew compileJava spotlessCheck

allowedFailures:
- alias-extends
- enum:forward-compatible-enums
Expand Down
6 changes: 5 additions & 1 deletion seed/python-sdk/seed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ test:
- chmod +x setup-python.sh && ./setup-python.sh
- poetry config virtualenvs.in-project true
- poetry install
# The envvar is needed to trick the generator to look in the right place for the core_utils files
- (cd ../../ && pnpm turbo run dist:cli --filter @fern-api/python-sdk)
runCommand: poetry run python -m src.fern_python.generators.sdk.cli {CONFIG_PATH}
env:
PYTEST_CURRENT_TEST: "some value"
PYTHON_V2_PATH: ../../generators/python-v2/sdk/dist/cli.cjs
FERN_CORE_UTILITIES_PATH: ../../generators/python/core_utilities/sdk:../../generators/python/core_utilities/shared
FERN_ASSETS_PATH: ../../generators/python
FERN_FEATURES_YML_PATH: ../../generators/python/sdk/features.yml

language: python
generatorType: SDK
Expand Down
Loading
Loading