- AWS account with Bedrock AgentCore previews enabled (Runtime, Harness, Gateway, Policy, Evaluation) in
us-west-2; Agent Registry is GA (agent-registrynamespace since 2026-08-06, see registry-ga-migration.md) and needs no preview enablement - Credentials with administrator-level access (
aws sts get-caller-identityworks) uv≥ 0.8, Node.js ≥ 20 (npm), AWS CDK CLI v2 (npm i -g aws-cdk), Docker (ARM64-capable, phase 5)- One-time CDK bootstrap per account/region:
cdk bootstrap aws://<account>/us-west-2
# 1. install dependencies
cd backend && uv sync && cd ..
cd frontend && npm install && cd ..
cd infra && uv sync && cd ..
# 2. deploy shared infra + AgentCore singletons, write config/launchpad.yaml
make bootstrap # = cd backend && uv run python ../scripts/bootstrap.pyThe bootstrap is idempotent: the CDK stack (launchpad-base) is deployed only
when missing, and the AgentCore registry (launchpad-registry) / memory
(launchpad_memory) are created once and reused on every later run.
再次运行只会打印 reused,不会产生重复资源。
Bootstrap also owns the CLI used to convert managed Harness agents into Runtime
agents. It installs exactly @aws/agentcore@0.21.1 at
data/agentcore-cli/node_modules/.bin/agentcore without a global npm install,
verifies the version, and reuses it on later runs. Conversion never uses an
agentcore executable from PATH; if the managed installation is deleted or
unusable, rerun make bootstrap. This CLI version supports both Harness exports
without Skills and Skill-bearing exports whose generated code calls
get_or_create_agent(session_id, user_id, _skill_plugins).
What it creates / 创建内容:
| Resource | Name |
|---|---|
| S3 artifacts bucket | launchpad-artifacts-<acct>-<region> |
| ECR repo | launchpad-agents |
| CodeBuild (ARM64) | launchpad-agent-builder |
| Cognito user pool | launchpad-users (+ groups platform-admin, hr-analyst, demo users admin/demo) |
| IAM execution role | launchpad-agent-execution-role |
| AgentCore Registry | launchpad-registry |
| AgentCore Memory | launchpad_memory (short-term events + semantic, user-preference, session-summary and episodic long-term strategies; re-running bootstrap adds missing strategies to an existing memory) |
| AgentCore Gateway | launchpad-gw-<suffix> |
| Managed AgentCore CLI | data/agentcore-cli/ (@aws/agentcore@0.21.1) |
Demo user passwords are generated and stored in config/launchpad.yaml
(gitignored — treat as local secrets; a sanitized config/launchpad.example.yaml is committed).
Bootstrap creates the shared Gateway but deliberately does not create a Policy Engine, create Cedar policies, attach an Engine, or select a Gateway enforcement mode. Existing Policy resources and attachments are also left untouched when bootstrap is rerun.
Use Governance to opt in: manage the selected Gateway, create or select an
Engine, and choose its initial Gateway attachment mode (ENFORCE by default or
LOG_ONLY). ENFORCE is default-deny, so create and review permitting policies
before relying on Gateway traffic.
Bootstrap still enables CloudWatch Transaction Search for general observability. It does not create the per-Gateway CloudWatch Logs delivery used for detailed Policy decision spans. Policy decision counts come from CloudWatch metrics and remain available after Policy is configured; detailed decision rows require separately managed trace delivery.
./start.py # detached development mode
./start.py --prod # build and run the local production preview
./stop.shUse make dev for the foreground, terminal-attached development stack.
Every setting named in this document is a key in config/launchpad.yaml, or the
same name upper-cased with a LAUNCHPAD_ prefix in the process environment
(database_url → LAUNCHPAD_DATABASE_URL). Precedence is defaults <
config/launchpad.yaml < environment < init kwargs, so an env var always wins
over the file bootstrap generated. Three keys decide how the backend itself
starts:
| Setting | Default | Effect |
|---|---|---|
database_url |
sqlite:///<repo>/data/launchpad.db |
SQLAlchemy URL of the ledger. The default is a single SQLite file under data/; it holds identifiers and derived progress only — AWS stays the source of truth — so a lost ledger costs console history, not resources. Point it elsewhere to relocate the file. |
cors_origins |
["http://localhost:5173", "http://127.0.0.1:5173"] |
Browser origins allowed to call /api. The defaults cover the dev frontend under either loopback spelling; add your own origin when the console is served from a different host or port. ./start.py --prod serves console and API from one origin and needs no entry. |
agentcore_read_timeout_s |
1000 |
boto read timeout for the AgentCore data-plane client (backend/app/services/agentcore/client.py:30). A synchronous AgentCore invoke may run for up to 15 minutes, so this has to stay above that service limit — botocore's 60 s default abandons a slow buffered agent before it can return its final response. |
The console can use local accounts without Cognito or any other AWS dependency.
Authentication is disabled until a password is configured, and the console shows
an AUTH OFF badge in its top bar while that is the case.
An unauthenticated console only answers loopback callers. ./start.py --prod
binds both servers to 0.0.0.0, so a reachable deployment must configure a
password; without one, requests to /api from any non-loopback address are
refused with auth.open_console_refused, and ./start.py fails its pre-flight
rather than starting. /api/health and the sign-in endpoints stay reachable so a
locked-out operator can still see the gate.
export LAUNCHPAD_AUTH_USERNAME=admin
export LAUNCHPAD_AUTH_PASSWORD='replace-with-a-strong-password'
./start.pySessions use a 12-hour HttpOnly cookie. Secure is set automatically in
production mode (run_mode: prod, which ./start.py --prod sets), together with
an HSTS response header; LAUNCHPAD_AUTH_COOKIE_SECURE=true forces it on in
development too. Both require HTTPS end to end — a Secure cookie is never
sent back over plain HTTP, so sign-in silently fails if TLS terminates somewhere
that then forwards over HTTP without the console knowing.
The same values may be placed in config/launchpad.yaml as auth_username,
auth_password, and auth_cookie_secure, following the normal configuration
precedence. Prefer the process environment for the password. Changing the
credentials and restarting the backend invalidates existing sessions.
There are two roles, and since 2026-08-11 they differ in exactly one place:
user management (/users) is administrator-only; everything else in the
console is open to members — registry register/edit/import, knowledge-base
mutations, evaluation datasets/evaluators/runs, AB experiments and canaries,
Cedar policy writes, API keys, the Studio canvas, and the browser /
code-interpreter demos. The authoritative list is the table in
backend/app/core/route_policy.py; a route missing from it is refused rather
than served. (Local code execution additionally stays disabled in production
for every role unless LAUNCHPAD_STUDIO_LOCAL_EXEC_ENABLED opts in.)
A small set of capabilities remains revocable per user in the User Management console: the agent lifecycle (deploy, import, delete, convert) and starting evaluation/insights runs. Members hold them by default; revoking them shuts off deploys and billable eval jobs for that account.
Note the console has no per-user data partitioning: every member sees and can mutate the same shared resources, so hand member accounts only to people you would let operate the environment.
The Create page's SYSTEM PRESETS panel lists the platform-owned presets
(currently aws-agent-solution-architect, a managed Harness). Nothing is installed
by bootstrap, startup or any page read: an administrator installs a preset per
workspace with the panel's INSTALL button (or POST /api/system-agents/<key>/install), which is a billable deploy through the normal
pipeline (~30 s). The workspace must be ready (bootstrap done, artifacts_bucket
and execution_role_arn present, per-agent execution roles enabled). A first install
uses the preset's defaults — for the architect: us.openai.gpt-5.6-sol on native
Bedrock (the US cross-region inference profile; the region must serve it),
max_tokens: 65536 per model call and reasoning_effort: high. Afterwards an
administrator changes the stored settings with the panel's CONFIGURE button —
which opens the same configure page an existing agent's EDIT uses (the table's EDIT
on the preset's row does the same for an administrator) — or the same install API
body: model source/id, max output tokens per model call (the
Harness bedrockModelConfig.maxTokens — one response's ceiling, not a session or
spend cap), reasoning effort (OpenAI GPT-5.x on native Bedrock only), system prompt,
max iterations, timeout and existing knowledge bases to mount (verified in the
workspace during provision; none is created for you). SAVE & RE-PUBLISH posts only the changed
fields to the maintenance route (never the ordinary redeploy) and runs the normal
update job; members see the same page read-only (VIEW SETTINGS). Re-publishes and
bundle updates keep the stored settings — a preset installed by an
earlier build keeps its model and prompt until you save a change or USE PRESET
DEFAULTS. With nothing changed the same page offers RE-PUBLISH, which re-publishes
in place with the stored settings — use it to retry a failed deploy or after an
out-of-band change (there is no separate repair button); UNINSTALL queues a teardown
job — the preset shows UNINSTALLING and keeps its identity until the Harness and
role are gone, a failed teardown shows its reason with RETRY UNINSTALL, and install
or repair are refused meanwhile. The preset runs with persistent memory disabled
(AgentCore memory only — chat transcripts stay in the ledger and CloudWatch logs are
kept) and on its own restricted execution role (never the shared role). Members can chat with the preset
like any agent but see a SYSTEM chip with the edit/re-publish/convert/delete actions
disabled, and the backend refuses those calls regardless of permissions; a knowledge
base mounted on the preset cannot be force-deleted until an administrator detaches
it through the preset. If an ordinary agent already holds the reserved name, the
install is refused — delete or rename that agent first; the preset never adopts it.
The preset's versioned Skill is also listed in the Registry as its own record:
every install or re-publish made with this build registers it in the deploy's
register stage (an explicit POST /api/system-agents/<key>/skill-registration does
the same from the API). That points an AGENT_SKILLS record at the release already
published under system-skills/ (nothing is uploaded and the Harness is not
re-published), submits it for review, and it becomes mountable by other agents only
after you approve it in the Registry — the Registry is where you manage the
record; the System presets card shows no Skill-record controls. After a bundle update
the record rolls forward, which needs approval again. The record shows a SYSTEM chip: nobody edits, re-imports or
deletes it from the console, members can view and mount it, administrators operate its
approval; uninstalling the preset keeps the record.
Live smoke is still pending (see architecture.md → System-managed presets).
Upgrade note: this release changes the console session cookie to version 2 (bound to the account id). Every signed-in user — members and the administrator — is signed out once and must log in again; nothing else changes for them.
Once the aws-agent-solution-architect preset is ACTIVE, every member of the workspace
can open the architect assistant (/create/assistant, linked from the Managed
Harness entrance card and from the SYSTEM PRESETS panel). Paste your Workshop output,
let the assistant confirm the baseline and ask only the impactful missing questions,
and review its proposal for one new managed Harness business agent. The proposal
only references tools, S3 skills and knowledge bases that already exist in the
workspace (APPROVED registry records, ACTIVE managed KBs) and shows the exact
bindings it will deploy (gateway ARN and auth identity, skill S3 path and content
digest of every file in the skill directory, the workspace's shared memory or none);
approved skills deploy from an immutable copy of exactly those bytes in the workspace's
artifacts bucket. It is inert until you click
APPROVE & DEPLOY — a separate, billable action that requires the agents.deploy
permission (re-checked, together with your account and workspace grant, at the moment
of execution) and creates the agent through the normal deploy job in the workspace's
account and Region; if any bound resource changed since you reviewed it, the approval
is refused and the job itself fails closed before touching AWS. Editing a proposal
creates a new revision that needs its own approval; cancelling makes it
non-executable. Memory is disabled or the workspace's existing shared memory (with
all its strategies) — nothing in between. Mounting a knowledge base requires the
workspace's existing KB gateway; where there is none, it is manual work. The assistant
never edits or deletes existing agents and creates no knowledge base, gateway or
evaluator — those, and the golden tests it recommends, are guidance for manual
implementation. Conversations are private to the account that opened them (by
immutable account id, not by username), per workspace, and are hidden from other
members in Observability too. The live smoke of this flow
(a real conversation, proposal and approval) is still pending.
Evaluation assets from a proposal (SE-047). Below the proposal panel, EVALUATION
ASSETS lets the conversation owner prepare a typed plan from any shape-valid proposal
revision (including an already-approved one), review the mapping table (golden tests →
Dataset scenarios with their steps and references; recommendations → existing
evaluator / new LLM judge / new derived / new code rules / runner check / human review /
metric baseline / external control), confirm or block the review-required scenarios
(legacy golden tests are drafted as single turns and are never parsed into
procedures — multi-session tests need typed steps), edit as JSON (every save is a new
plan revision) and — as an administrator who owns the conversation — CREATE ASSETS
after acknowledging the disclosure. Creation
registers a local Dataset, AgentCore evaluators and, for code rules, one Lambda with its
own role; it does not deploy, run, sync to AWS Datasets, enable online evaluation or
call a model, and "created" does not mean passed. Members can prepare and edit; the
create button tells them why it is disabled. Retry resumes the same resources; DELETE
OWNED CLOUD ASSETS removes exactly what the operation created (the local Dataset stays —
remove it in Evaluation → Datasets). Owned artifacts are named launchpad-evalfn-<op>
(Lambda, its role, /aws/lambda/launchpad-evalfn-<op>) and launchpad-evalop-<op> (the
additive policy on the workspace execution role); scripts/teardown.py does not know
them — clean them through the operation before tearing the stack down. Live creation
against a real account is still pending; see architecture → Evaluation-assets plan for
the unverified AWS assumptions.
| Variable | Effect |
|---|---|
LAUNCHPAD_ALLOW_OPEN_CONSOLE=true |
Serve an unauthenticated console on a reachable interface. Restores the pre-hardening behavior; use only on a trusted network. |
LAUNCHPAD_STUDIO_LOCAL_EXEC_ENABLED=true |
Re-enable local code execution in production (see below). |
LAUNCHPAD_STUDIO_EXEC_BACKEND=docker |
Run local-debug code in one-shot Docker containers instead of host subprocesses — also serves the endpoints in production (see below). |
LAUNCHPAD_AUTH_COOKIE_SECURE=false |
Drop Secure when TLS is not actually terminated in front of the console. |
There is no switch that disables role authorization: a flag that turns
authorization off is the vulnerability. Correcting a misclassified route means
editing route_policy.py.
Each deployed agent gets its own IAM execution role, derived from its spec, instead of
every agent assuming one shared launchpad-agent-execution-role. The point is
isolation: on the shared role, any agent could mount any other agent's file systems,
read every agent's skill bundles, retrieve from every knowledge base, and rewrite
gateway routing.
Roles are named launchpad-agent-{name}-{agent-id-prefix} and tagged
launchpad:agent-id. They are created in the provision stage, reconciled on
re-publish (a dropped capability shrinks the policy), and deleted with the agent.
| Setting | Default | Effect |
|---|---|---|
per_agent_execution_roles |
true |
Set false to fall back to the shared role. |
agent_role_count_warn_threshold |
800 |
Warn once this many roles exist. |
The IAM default quota is 1000 roles per account, and this consumes one per agent. At demo scale that is not a concern, but hitting it would surface as an otherwise-mysterious deploy failure.
Existing agents keep working on the shared role. Migration is a re-publish —
not a hand-rolled UpdateAgentRuntime, which resets omitted fields and would silently
clear file-system mounts, protocol config or the environment. Check what is
outstanding with:
cd backend && uv run python scripts/migrate_agent_roles.pyRun scripts/migrate_pin_requirements.py --apply first: a re-publish re-validates
the spec, so an unpinned requirement is now refused.
Why the shared role still exists and still carries broad grants. It backs agents that have not been re-published yet. Reducing it before every agent has moved would strip grants from agents still using it. That reduction is deliberately not done yet, and neither is the trust-policy
aws:SourceArncondition (off pending a probe of whether AgentCore sends that key). Note also what per-agent roles do not give you: memory stays a single shared instance partitioned by actor id, not by IAM, and the account's 1000-role quota is now consumed one role per agent.
A green deploy does not prove a policy is correct. These policies are scoped per
spec, and an over-tight statement fails at invoke time, not deploy time. After
migrating, invoke each agent and check CloudTrail for AccessDenied.
Requirements must be pinned. spec.requirements entries have to name one
immutable artifact — name==version, a direct URL with #sha256=, or
pkg @ git+https://…@<40-char commit>. A range is refused at validation with the
required form in the message. The platform's own requirement lists keep ranges
deliberately; reproducibility comes from the lockfile below, not from hand-pinning
them.
Existing agents may predate this. Nothing breaks until their next deploy — check with:
cd backend && uv run python scripts/migrate_pin_requirements.py
cd backend && uv run python scripts/migrate_pin_requirements.py --applyThe same script lists git skill records with no recorded commit; those need a re-import from the Registry, because a commit SHA needs a fetch.
Every zip build is locked. The package stage resolves the declared
requirements with uv pip compile --generate-hashes for the deploy target
(aarch64, Python 3.13) and installs with --require-hashes, so a substituted or
re-uploaded distribution fails the build instead of shipping. The lock travels
inside the deployment zip as requirements.lock. The backend therefore needs the
uv CLI on PATH and access to the package index at deploy time; if the resolve
fails, the deploy fails — there is no fall back to an unverified install.
Container images are scanned and deployed by digest. ECR scans on push, and
the package stage refuses to continue when the image carries findings at or above
image_scan_block_severities:
| Setting | Default | Effect |
|---|---|---|
image_scan_enabled |
true |
Set false to skip the gate (the job log then says the image was not scanned). |
image_scan_block_severities |
["CRITICAL"] |
Severities that block a deploy. |
image_scan_timeout_s |
300 |
How long to wait for the scan; a timeout is logged, not treated as clean. |
Deployment references the image by immutable digest, not by its {agent}-v{version}
tag, and the digest is recorded on the deployment. Image tags stay mutable on
purpose: packaging runs before the version is bumped, so a re-publish pushes the
same tag twice and an immutable-tag policy would fail that push.
Applies to both stacks. Scan-on-push is a CDK change, so
make bootstraphas to be run inus-west-2and on theus-east-1host. Until it is, the gate on that host will report that it could not read a scan.
Expect the default to block on day one. A scan of a current demo image found 4 CRITICAL findings, all unpatched OS packages in the Debian base image (
glibc,perl) rather than anything this project installs — so with the["CRITICAL"]default, enabling scanning stops container deploys until the base image ships fixes. The block message names the CVEs and packages so you can tell whose problem it is. Decide deliberately between rebuilding on a newer base, relaxingimage_scan_block_severitiesto[](report-only: findings are logged on every deploy, nothing is blocked), and accepting the block.
Not implemented: SBOM generation, build provenance/attestation, image signing, approved-mirror enforcement, and skill content review. Pinning makes a source immutable, not trustworthy.
The Studio local-debug endpoints (/api/execute, /api/execute/stream, and the
/api/conversations multi-turn surface) run caller-supplied Python on the
server. They are therefore disabled in production mode, and Studio local
debug plus AI Fix stop working there. Set
LAUNCHPAD_STUDIO_LOCAL_EXEC_ENABLED=true to accept the risk.
In development the subprocess gets a scrubbed environment (an allowlist, so the
ledger URL, LAUNCHPAD_* settings and your shell's secrets do not reach it) plus
memory/CPU/process/file-size ceilings.
It still runs as the backend user by default, and still reaches your AWS
credentials. Setting studio_exec_forward_aws_credentials: false keeps them
out of the child's environment and sets AWS_EC2_METADATA_DISABLED=true, which
is enough to stop the AWS SDKs and CLI from picking up the instance role — but
that variable is an SDK convention, not a boundary. On EC2 credentials arrive
over the network, so code that talks to 169.254.169.254 itself still gets them.
Measured on an EC2 host: with the environment scrubbed, ~20 lines of urllib
still returned live instance-role keys. To close that:
sudo scripts/setup_exec_env.sh --hardened # Linux onlyThat creates a dedicated unprivileged account and a firewall rule denying that uid egress to the metadata endpoint, then prints the two settings to add. The same probe under it times out.
Precondition: switching the subprocess to another account needs privilege, so
studio_exec_user only works when the backend itself runs as root. make dev
and start.py run it as your own account, where the drop would fail — the
execution endpoints therefore refuse with studio.exec.user_unavailable (503)
rather than failing mid-run, and you must either run the backend as root or leave
studio_exec_user empty (tier 1: limits and environment scrubbing only).
Note the trade-off the script describes: the default Bedrock Mantle path mints
its bearer token from the ambient credentials, so a credential-less subprocess
requires an explicit bedrock_api_key / openai_api_key with each local-debug
request.
Alternatively, run the generated code in a one-shot Docker container instead of a host subprocess:
scripts/setup_exec_docker.sh # builds launchpad-studio-exec:latest
export LAUNCHPAD_STUDIO_EXEC_BACKEND=docker # or studio_exec_backend: docker in launchpad.yamlEach run gets a fresh container with --cap-drop ALL, no-new-privileges, a
read-only rootfs (tmpfs /tmp), the same environment allowlist, and the
memory/CPU/pids/file-size ceilings mapped onto docker flags. Timeouts kill the
container itself, and a startup janitor sweeps any strands-exec-* containers a
backend crash left behind. Per-run overhead is ~0.3 s.
Those ceilings, and the image the container comes from, are settings:
| Setting | Default | Docker flag |
|---|---|---|
studio_exec_memory_mb |
2048 |
--memory and --memory-swap, pinned to the same value so the container cannot page around the limit |
studio_exec_cpu_seconds |
300 |
--ulimit cpu=<n>:<n> — CPU seconds burned, not wall clock; wall clock is execute_timeout_s |
studio_exec_max_processes |
64 |
--pids-limit |
studio_exec_max_file_mb |
256 |
--ulimit fsize=<bytes> — the largest file the generated code may write |
studio_exec_docker_image |
launchpad-studio-exec:latest |
the image docker run starts; change it only if you build the sandbox image under another name |
The same four ceilings apply to the subprocess backend, where they become
resource.RLIMIT_* values lowered in the forked child instead of docker flags
(backend/app/services/local_exec.py: _docker_run_argv assembles the flags
from line 380, the rlimit list is built around line 557). One asymmetry:
studio_exec_max_processes maps to RLIMIT_NPROC, which counts processes and
threads per uid, so the subprocess backend applies it only when
studio_exec_user is set — against the backend's own uid it would count your
whole login session and every thread the child tried to start would fail.
Because the code no longer runs on the control-plane host, selecting the
docker backend is itself the production opt-in: run_mode=prod +
studio_exec_backend=docker serves the local-debug endpoints without
LAUNCHPAD_STUDIO_LOCAL_EXEC_ENABLED=true (an explicit false still disables
them). Requirements: a docker daemon the backend user may talk to (docker
group), and the image built by the script above.
One boundary does not come free: on EC2 with an IMDS hop limit ≥ 2 (the default on these boxes), a container on the default bridge can still reach the instance metadata service — which is exactly what keeps the default Mantle path working with no API keys. For a genuinely credential-less sandbox:
sudo scripts/setup_exec_docker.sh --harden-net # Linux onlycreates a dedicated launchpad-exec bridge network plus a DOCKER-USER
iptables rule denying that subnet egress to 169.254.169.254, then prints the
settings to add (studio_exec_docker_network, and
studio_exec_forward_aws_credentials: false with the same Mantle trade-off as
above). The backend refuses the hardened-credentials configuration without the
network set, rather than pretending.
A deeper sandbox (AgentCore Code Interpreter re-host) remains unimplemented; the docker backend is the recommended middle tier.
Studio's AI Fix hands the failing flow's generated code, its traceback and
the validation errors to a coding agent, which rewrites generated_agent.py in
a scratch workspace and hands it back over SSE. Four settings control that:
| Setting | Default | Effect |
|---|---|---|
codegen_backend |
claude |
Which coding agent performs the fix. claude (the Claude Agent SDK) is the only registered backend today; an unregistered name fails the request with the list of registered names rather than silently falling back. |
codegen_model |
global.anthropic.claude-sonnet-5 |
Model that coding agent runs on. A stronger model repairs more per attempt and costs more per attempt. |
codegen_timeout_s |
180.0 |
End-to-end budget for one AI-fix request, repair rounds included. Raise it if fixes are being cut off before the agent finishes writing the file. |
codegen_max_repair_rounds |
2 |
How many times a rewrite may be re-validated and re-attempted before the request gives up. Each extra round is another model call on a flow whose first fix did not import cleanly. |
Skill Lab evaluates and trains Registry skill records: the vendored SkillOpt
CLIs run as subprocesses on the backend host, each task's agent rollout runs
in its own AgentCore Runtime microVM session on launchpad_skill_lab_worker,
and the LLM judge calls Bedrock directly. make bootstrap provisions both
halves — the dedicated interpreter and the worker image — so the settings below
tune a provisioned Skill Lab rather than switch it on.
| Setting | Default | Effect |
|---|---|---|
skill_lab_python |
<repo>/data/skill-lab-venv/bin/python |
Interpreter the vendored evaluate_skill.py / train.py / task-set validators run in. Bootstrap builds it from vendor/skillopt/requirements-launchpad.txt and rebuilds it when that file changes; the backend process never imports the vendored tree itself. GET /api/skill-lab/status reports venv_ready: false while this path is missing — that is what an un-provisioned workspace looks like. |
skill_lab_max_concurrent_jobs |
1 (1–4) |
How many evaluation/training jobs run at once. Each job is a CLI subprocess plus its own worker sessions, so this trades wall clock against host CPU/memory and pressure on the worker runtime; excess jobs queue instead of failing. |
skill_lab_judge_model_id |
us.openai.gpt-5.6-sol |
Model that scores rollouts. It must be a Bedrock Converse inference-profile id — the bedrock_chat judge rejects bare model ids. It also selects the agentic judge's host CLI by family: an openai.* id routes to the host codex binary with the profile prefix stripped, anything else to the host claude binary (runner.judge_exec_route). Changing this therefore changes which CLI the host must have installed. |
skill_lab_target_model_id |
global.anthropic.claude-opus-5 |
Default model the skill under test runs on for the claude_code_exec target backend — again a Converse inference-profile id. Per-job parameters override it; a blank value is never sent, because an empty --model would let the vendored CLI substitute its own non-Bedrock default. |
skill_lab_codex_target_model_id |
openai.gpt-5.6-sol |
The same default for the codex_exec target backend, but a Bedrock catalog slug rather than a Converse profile id: codex resolves models itself through the amazon-bedrock provider baked into its config. Kept as a separate key so neither backend can ever be handed the other's id family. |
skill_lab_codex_catalog_path |
~/.codex/model-catalogs/bedrock-models.json |
Bedrock model catalog read from the backend host and staged into the worker image's codex-home at build time. The file embeds proprietary model instructions and is never committed, so when it is absent the build stages an empty {} catalog and logs that it did — codex targets on that image then have no catalog to resolve against. |
skill_lab_judge_sandbox |
bwrap |
Sandbox launcher argv (shlex-split) for the agentic judge's artifact parsers, which run on the backend host — the worker microVM cannot run bubblewrap. On hosts where unprivileged bwrap is blocked by AppArmor, set sudo -n bwrap. GET /api/skill-lab/status probes this argv's first word for agentic_judge_ready, and the vendored fail-closed boundary check still applies on top. |
skill_lab_worker_cli_version |
2.1.234 |
Reports the claude CLI version baked into the worker image. |
skill_lab_worker_codex_version |
0.147.0 |
Reports the codex CLI version baked into the worker image. |
The two *_version keys are mirrors, not inputs. The build uses the
ARG CLAUDE_CLI_VERSION / ARG CODEX_CLI_VERSION defaults in
vendor/skillopt/deploy/agentcore/Dockerfile — that Dockerfile is what the
image's content hash covers — while these settings only feed the console's
display. Bump both together:
backend/tests/test_skill_lab_foundation.py asserts the parity, so a one-sided
change fails make verify. A version bump also changes the build-context hash,
which is how the next bootstrap knows to rebuild and re-push the worker image.
The experiment RECOMMEND stage asks a 3rd-party provider
(backend/app/optimization/providers) to rewrite an agent's system prompt and
tool descriptions from a pinned evaluation run's worst- and best-scoring
sessions. Model ids here are Bedrock Converse inference-profile ids, invoked
through the same workspace client funnel as everything else.
| Setting | Default | Effect |
|---|---|---|
prompt_opt_models |
["global.anthropic.claude-opus-5", "global.anthropic.claude-sonnet-5", "global.anthropic.claude-sonnet-4-6", "us.openai.gpt-5.6-sol"] |
The reflection models the console offers. Add an id here to make it selectable. |
prompt_opt_default_model_id |
global.anthropic.claude-opus-5 |
Which of them leads that list and is used when a request names none. |
prompt_opt_max_sessions |
30 (3–100) |
How many sessions of the pinned run a provider reads (worst-first plus a best-scoring contrast set). Fewer is cheaper and faster; more gives the reflection more evidence to generalise from. |
prompt_opt_max_tokens |
8192 (512–16000) |
Output budget of the reflection call. A two-component reflection (prompt plus several tool descriptions) ran past 4096, and the provider doubles this once on its own when a response comes back truncated. |
prompt_opt_read_timeout_s |
900 |
Read timeout of the reflection call. The call streams, so this bounds the gap between chunks rather than the total. botocore's 60 s default is far too low: in production a large model over 30 sessions ran past it and botocore silently re-sent the whole request five times before failing. |
While the gate is enabled, the login page also offers registration: a
visitor supplies a username, a company email, and a password. By default the
new account lands in pending and cannot sign in until an admin approves it;
the 7-day validity window starts at approval. The built-in admin above is
never stored in the database, so it cannot be locked out.
Public / disposable mail domains (Gmail, QQ, 163, Outlook, mailinator, …) are rejected. Tune the policy with:
export LAUNCHPAD_AUTH_REGISTRATION_ENABLED=true # false closes registration
export LAUNCHPAD_AUTH_REGISTRATION_REQUIRE_APPROVAL=true # false = active on registration
export LAUNCHPAD_AUTH_REGISTRATION_VALID_DAYS=7 # validity granted at approval
# allow list wins when non-empty; otherwise the built-in block list applies
export LAUNCHPAD_AUTH_ALLOWED_EMAIL_DOMAINS='["your-company.com"]'
export LAUNCHPAD_AUTH_BLOCKED_EMAIL_DOMAINS='["gmail.com","qq.com"]'The admin sees a User Management module (/users) with an approval queue
(AWAITING APPROVAL tile + PENDING filter, APPROVE / REJECT per row),
registration statistics, and per-account actions: extend validity (+7 / +30 /
custom days or an absolute date), disable / enable, change role, reset the
password (shown once), and delete. Expiry and disabling are enforced on every request, so an account
loses console access immediately — it does not have to wait for the session
cookie to lapse.
./start.py --prod is a local preview: it builds the frontend, serves the built
bundle, drops backend auto-reload, and binds to 0.0.0.0. For a host that stays
up, supervise the two processes instead and keep the console behind an edge that
terminates TLS. The reference deployment (workshop EC2 + CloudFront) — its unit
files and the verified update sequence — is written up in
agent-runbook-prod.md; its shape is:
browser → CloudFront (TLS, no caching, all methods, injects a secret origin header)
└─ nginx :80 on the instance — rejects any request without that header
├─ /api/, /v1/ → 127.0.0.1:8000 (backend, proxy_buffering off for SSE)
└─ /, /assets/ → 127.0.0.1:5173 (vite preview serving frontend/dist)
1. Supervise the two processes. The backend unit carries the auth configuration; nothing else enables the gate for you:
# /etc/systemd/system/launchpad-backend.service (excerpt)
[Service]
WorkingDirectory=/home/ubuntu/workspace/agentcore_launchpad/backend
Environment=LAUNCHPAD_RUN_MODE=prod
Environment=LAUNCHPAD_AUTH_USERNAME=admin
Environment=LAUNCHPAD_AUTH_PASSWORD=<strong-password>
Environment=LAUNCHPAD_AUTH_COOKIE_SECURE=true
ExecStart=/home/ubuntu/.local/bin/uv run uvicorn app.main:app --host 127.0.0.1 --port 8000
Restart=on-failure# /etc/systemd/system/launchpad-frontend.service (excerpt)
[Service]
WorkingDirectory=/home/ubuntu/workspace/agentcore_launchpad/frontend
Requires=launchpad-backend.service
ExecStart=/usr/bin/npm run preview -- --host 127.0.0.1 --port 5173 --strictPort
Restart=on-failurevite preview serves frontend/dist, so every frontend change needs
npm run build before the restart. Both processes bind to 127.0.0.1: only
the reverse proxy is exposed.
2. Close the origin. CloudFront adds a custom header (e.g.
X-Launchpad-Origin-Key) and nginx refuses anything without it, so the public
instance IP cannot bypass the CDN:
if ($http_x_launchpad_origin_key != "<shared-secret>") { return 403; }
proxy_set_header X-Forwarded-Proto https; # TLS terminates at CloudFrontBecause TLS terminates at the edge, keep LAUNCHPAD_AUTH_COOKIE_SECURE=true;
over plain HTTP the browser would drop the session cookie.
3. Update an existing host.
cp data/launchpad.db data/launchpad.db.bak-$(date +%Y%m%d-%H%M)
git merge --ff-only origin/main
cd backend && uv sync && cd ..
cd frontend && npm run build && cd .. # required: preview serves dist/
sudo systemctl restart launchpad-backend launchpad-frontend
curl -s localhost:8000/api/auth/status # expect auth_required: trueNew ledger tables (such as users) are created on startup, so no migration step
is needed. Registration is open as soon as the gate is on — set
LAUNCHPAD_AUTH_REGISTRATION_ENABLED=false or pin
LAUNCHPAD_AUTH_ALLOWED_EMAIL_DOMAINS if the deployment should not accept
requests from anyone who has the URL.
cd backend
uv run python ../scripts/teardown.py --dry-run # list what would be removed
uv run python ../scripts/teardown.py --yes # delete (memory → registry → CDK stack)Deletion is best-effort and ordered dependents-first; the S3 bucket auto-empties and the ECR repo force-deletes via the stack.