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
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: ci

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

env:
# Immutable SDK commit (Perfscale/sdk-libraries, feat/wasm-step-sdk):
# defineSteps + --kind steps build + settings delivery.
SDK_COMMIT: 522cfdd590062652c04337e6dfdcfc1c2c79389e

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
path: repo
# The SDK is a file: sibling dependency (../sdk-step-authoring/ts);
# pin it to an exact public commit.
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
repository: Perfscale/sdk-libraries
ref: feat/wasm-step-sdk
path: sdk-step-authoring
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
- name: Verify the pinned SDK commit
working-directory: sdk-step-authoring
run: test "$(git rev-parse HEAD)" = "$SDK_COMMIT"
- name: Install and build SDK (dist + type declarations)
working-directory: sdk-step-authoring/ts
run: |
npm ci
npm run build
- name: Install, typecheck, unit-test
working-directory: repo
run: |
npm install
npm run typecheck
npm test
- name: Build the WASM component
working-directory: repo
run: npm run build
- name: Inspect component imports/exports
working-directory: repo
run: ../sdk-step-authoring/ts/node_modules/.bin/jco wit python.wasm
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: python-wasm
path: repo/python.wasm
retention-days: 7

integration:
needs: build-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-wasm
path: .
- uses: dtolnay/rust-toolchain@stable
- name: Build Perfscale v0.30.0 from source
run: |
git clone --depth 1 --branch v0.30.0 https://github.com/Perfscale/perfscale.git /tmp/perfscale-src
cargo build --release -p perfscale-cli --manifest-path /tmp/perfscale-src/Cargo.toml
cp /tmp/perfscale-src/target/release/perfscale "$GITHUB_WORKSPACE/perfscale"
"$GITHUB_WORKSPACE/perfscale" --version
- name: Run the example through the engine (settings-driven install + exec)
run: |
"$GITHUB_WORKSPACE/perfscale" run -f examples/python.test.yaml -c examples/python.config.yaml | tee run.log
grep -q 'hello from python 3.13' run.log
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# @perfscale/library-python

Trusted local Python steps for perfscale (RFC 009 `process` capability),
built with [`@perfscale/library-sdk`](../sdk-step-authoring/ts) against
`perfscale:step@0.1.0`. Three steps over the bounded host `exec` import:

- `lib/python/install` — download an official
[astral-sh/python-build-standalone](https://github.com/astral-sh/python-build-standalone)
release tarball, verify its SHA-256 against the release's own
`SHA256SUMS`, and extract it into a local toolcache (idempotent).
- `lib/python/exec` — run an inline script (`python -c`) or a script file
with a chosen python binary; exit code preserved.
- `lib/python/install-exec` — compose both.

## Security model — read this first

This component exists to run **host processes**. That is powerful and
dangerous on a shared runner, so the engine keeps it fail-closed: the
component only loads when the run grants the capability twice over —
`capabilities: [process]` on the library entry AND
`allow_process_actions: true` + `allow_library_capabilities: true` in the
config. Every spawn is direct argv (no shell) with a replacement
environment — host variables are never inherited.

## Installing and granting the capability

```yaml
# perfscale.config.yaml
allow_library_capabilities: true
allow_process_actions: true
libraries:
- use: ./python.wasm
capabilities: [process]
```

## Steps

### `lib/python/install`

`with:` keys (unknown keys are rejected):

- `python-version` (string, **required** unless set via settings) — exact
full version X.Y.Z (`3.13.18`).
- `release` (string, optional) — python-build-standalone release tag
(`20261026`); unset resolves the latest release carrying the version.
- `install-dir` (string, optional, default `/tmp/perfscale-toolcache/python`).

Output: `{ "path", "python-path", "version", "release", "stdout", "stderr",
"exit_code" }`. `success` is true only after the installed
`python --version` confirms the install (or the cache).

### `lib/python/exec`

Exactly one of `script` / `file` is required.

`with:` keys (unknown keys are rejected):

- `script` / `file` (string, **exactly one required**)
- `python-path` (absolute path, optional, default `/usr/bin/python3`)
- `args` (array of strings, optional)
- `working-directory` / `cwd` (string, optional alias pair)
- `env` (map of string→string, optional; merges over the default PATH)
- `timeout_ms` / `timeoutMs` (positive number or numeric string, optional)

Output: `{ "exit_code", "stdout", "stderr" }`; `success` is
`exit_code == 0`.

### `lib/python/install-exec`

Install (or reuse the cache), then exec with the installed python — no
`python-path` needed. `with:` is the strict union of the `install` and
`exec` keys (minus `python-path`). Installation failure skips exec.

## Run settings (RFC 009)

Every step knob may be set once for the whole run instead of repeated per
step. Precedence (highest first):

1. the step's explicit `with:` params
2. the `python:` config extension block (claimed via the component's config
schema — unknown keys fail the run)
3. the `variables.python` map
4. the library entry's `with:` block
5. the built-in defaults

`env` maps merge in that same order (a step's `env` wins per key). Accepted
settings keys: `python-version` (alias `version`), `release`,
`install-dir`, `python-path` (alias `executable`), `working-directory` /
`cwd`, `timeout_ms` / `timeoutMs`, `env`. Invalid blocks (unknown key,
wrong type, relative `python-path`, bad version) are hard errors — at
instantiation for the library `with:`, at step resolution for the rest.

```yaml
python: # config extension, validated against the schema
python-version: 3.13.18
install-dir: /tmp/perfscale-toolcache/python # absolute (the default)
variables:
python: { timeout_ms: 30000 }
libraries:
- use: ./python.wasm
capabilities: [process]
with: { env: { PYTHONDONTWRITEBYTECODE: "1" } }
steps:
- name: smoke
use: lib/python/install-exec
with:
script: import sys; print(sys.version)
```

## Orchestration, not hot path

Each step invocation is **real host process spawns** (and `install` may
download tens of MB on a cold cache). Use these steps for setup, fixtures,
and one-off environment checks — the one-shot `before:` / `after:` blocks
are the natural home. Do not put `exec` in the per-iteration VU loop of a
high-RPS test.

## Local e2e

```console
$ npm install
$ npm run build # → python.wasm
$ perfscale run -f examples/python.test.yaml -c examples/python.config.yaml
```

## Development

```console
$ npm install # file: dependency on the SDK checkout
$ npm test # unit tests (node:test, fake host exec)
$ npm run typecheck
$ npm run build
```

## License

Dual-licensed under [Apache-2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT), at
your option.
47 changes: 47 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Security Policy

`@perfscale/library-python` is a **trusted-code** component: its whole
purpose is downloading CPython and executing it on the host via the
perfscale `process` capability (RFC 009). Treat any report of "this library
can run python scripts" as working as intended.

## What is in scope

Genuinely security-relevant findings are cases where the component exceeds
its contract, for example:

- Downloading from anything other than
`https://github.com/astral-sh/python-build-standalone` over HTTPS, or
skipping the SHA-256 verification against the release's `SHA256SUMS`
before extraction.
- Spawning anything other than the documented `/usr/bin/uname`,
`/usr/bin/test` (cache probe), `/usr/bin/mkdir`, `/usr/bin/curl`,
`/usr/bin/sha256sum`, `/usr/bin/tar`, `/usr/bin/rm` (the downloaded
archive only), or the selected python binary.
- Leaking host environment variables into a child (the host import already
guarantees a replacement environment; a bug that bypasses it matters).
- The cache-idempotency or bad-archive cleanup deleting anything other than
the downloaded `.tar.gz` archive.
- Validation gaps that let crafted `with:` params or settings blocks
(extension / variables / library `with:`) alter the spawned argv or
environment in undocumented ways.

## What is out of scope

- "The step ran the script the YAML told it to run." Capability grants
(`capabilities: [process]`, `allow_process_actions: true`,
`allow_library_capabilities: true`) are the operator's explicit,
double opt-in decision.
- Denial of service via long-running or noisy scripts — timeouts are
host-capped; tune `timeout_ms`.

## Reporting

Do not open a public issue for a vulnerability. Report privately to the
Perfscale maintainers (see the contact in the main perfscale repository's
SECURITY.md) with a minimal YAML + script reproducer. We aim to acknowledge
within 7 days.

## Supported versions

Only the latest release receives security fixes.
31 changes: 31 additions & 0 deletions examples/python.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Config side of the python e2e: profile, library registration with the
# process capability, the one-shot `before:` install, and the double opt-in.
vus: 1
duration: 1s
seed: 42
# Double opt-in for the process capability: these config gates AND
# `capabilities: [process]` on the libraries entry below. Missing either
# side, the engine refuses to load the component (fail-closed, like `net`).
allow_library_capabilities: true
allow_process_actions: true
# Run-level settings (RFC 009): the `python` extension block is claimed by
# the component's config schema; a step's explicit `with:` still wins over
# it, `variables.python` sits one level below it, and the library entry's
# `with:` provides defaults below both.
python:
timeout_ms: 60000
variables:
python: { timeout_ms: 30000 }
libraries:
- use: ../python.wasm
capabilities: [process]
before:
# Pinned exact version, checksum-verified against the release's
# SHA256SUMS. Installs into the default toolcache
# (/tmp/perfscale-toolcache/python); subsequent runs reuse it — nothing is
# re-downloaded.
- name: python
use: lib/python/install
with:
python-version: 3.13.18
outputs: setup # → ${{ config.setup.python-path }} in test steps
30 changes: 30 additions & 0 deletions examples/python.test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Local e2e for the `python` step library (RFC 009 process capability).
# Steps only — the capability double opt-in and the `before:` install live
# in python.config.yaml. Build first:
#
# npm run build # → ../python.wasm relative to this file's examples dir
#
# perfscale run -f examples/python.test.yaml -c examples/python.config.yaml
#
# NOTE: process steps are orchestration (setup, fixtures, one-off checks),
# not a high-RPS hot path — each invocation is a real host spawn.
steps:
# Uses the exact python installed by the config's `before:` block.
- name: inline-script
use: lib/python/exec
with:
python-path: "${{ config.setup.python-path }}"
script: import sys; print(f"hello from python {sys.version_info.major}.{sys.version_info.minor}")
outputs: said
check:
message_contains: hello from python 3.13

- name: install-exec-one-shot
use: lib/python/install-exec
with:
python-version: 3.13.18
script: import sys; print(sys.argv[1:])
args: ["perfscale"]
outputs: combo
check:
message_contains: perfscale
Loading
Loading