factory.config.ts is committed application
configuration. The gateway and Workflows service both import it.
Edit it before the first deployment and whenever you change repositories, labels, model assignments, permissions, publishing limits, or Sandbox lifetimes.
export default defineFactoryConfig({
repos: {
"acme/web": {
baseBranch: "main",
install: "npm ci",
verify: ["npm run build", "npm test"],
egress: ["registry.npmjs.org"],
},
"acme/api": {
baseBranch: "main",
install: "go mod download",
verify: ["go test ./..."],
egress: ["proxy.golang.org", "sum.golang.org"],
},
},
labels: {
triage: "factory:triage",
specApproved: "factory:spec-approved",
prReview: "factory:pr-review",
autopilot: "factory:autopilot",
},
commandPrefix: "/factory",
minimumPermission: "write",
limits: {
repairRounds: 2,
maxChangedFiles: 200,
maxFileBytes: 1_000_000,
},
sandbox: {
timeoutSeconds: 4 * 60 * 60,
},
});repos is keyed by exact GitHub owner/name. The factory ignores webhooks
from any other repository, even if the App is installed there.
| Field | Required | Meaning |
|---|---|---|
baseBranch |
Yes | Branch used for Triage, implementation, and snapshots |
install |
No | Dependency command run after clone credentials are removed |
verify |
Yes | Commands required before publishing and during PR review |
egress |
No | Extra HTTPS hosts this repository's Sandboxes may reach, or "allow-all" |
Use deterministic install commands such as npm ci. Snapshot creation fails
if installation changes a tracked file. Use verify: [] only when you
intentionally want no command-based verification.
Sandboxes reach the network through an HTTPS allow-list that Render enforces. Every other destination fails as a reset connection. The factory builds each list:
| Sandbox | Allowed hosts |
|---|---|
| Task Sandbox | github.com, every agent harness's model API, and the repository's egress |
| Snapshot builder | github.com, deb.debian.org, and the repository's egress |
List in egress every host the repository's install and verify commands
download from. Task Sandboxes need the list too: they reinstall when the
lockfiles change. Common hosts:
| Ecosystem | Hosts |
|---|---|
| npm | registry.npmjs.org |
| Go modules | proxy.golang.org, sum.golang.org |
| Go toolchain download | go.dev, dl.google.com |
| PyPI | pypi.org, files.pythonhosted.org |
A rule matches one exact hostname. Use *.example.com to admit its
subdomains; a wildcard may only be the leftmost label. Only HTTPS is admitted.
An invalid host stops both services at startup. An install that fails under
the allow-list names the allowed hosts in its error.
Set egress: "allow-all" to lift the restriction for one repository. Prefer
listing hosts: the allow-list is what stops dependency scripts and
prompt-injected agents from sending code or the model key to arbitrary
destinations.
Render fixes a Sandbox's policy when it is created, so an egress change
applies to the next Sandbox after a Workflows deploy. No snapshot refresh is
needed.
After adding a repository:
- commit and deploy the configuration;
- install the GitHub App on the repository;
- run
npm run setup:github; and - push to the base branch, start a factory task, or refresh the snapshot directly.
| Setting | Default | Meaning |
|---|---|---|
labels.triage |
factory:triage |
Starts or continues Triage |
labels.specApproved |
factory:spec-approved |
Starts Spec-to-PR |
labels.prReview |
factory:pr-review |
Starts PR Reviewer |
labels.autopilot |
factory:autopilot |
Runs all three stages through a reviewed draft PR |
commandPrefix |
/factory |
Prefix for commands such as /factory cancel |
minimumPermission |
write |
Live repository permission required to start work |
limits.repairRounds |
2 |
Repair attempts after failed verification |
limits.maxChangedFiles |
200 |
Maximum files in a published change |
limits.maxFileBytes |
1000000 |
Maximum size of one changed file |
sandbox.timeoutSeconds |
14400 |
Provider-side Sandbox lifetime in seconds |
minimumPermission accepts triage, write, maintain, or admin. The
factory checks the triggering actor's current permission at the start of
every GitHub task.
setup:github also creates factory:working. This label records an active
run; it is not a trigger.
If you rename a trigger label, deploy both services and rerun
npm run setup:github. The command creates required labels but does not
remove old ones.
Keep sandbox.timeoutSeconds at least as long as the longest Workflows task.
Each agent runs on a harness: an agent library and its configuration, built by
an adapter in app/harness/. The built-in agents share two
harnesses from app/agents/models.ts:
export const piSonnet = pi({ model: getBuiltinModel("anthropic", "claude-sonnet-5") });
export const piOpus = pi({ model: getBuiltinModel("anthropic", "claude-opus-5") });Triage and PR Reviewer use piSonnet; the spec writer and implementer use
piOpus. Each value is named for its library and model. To change a model,
add or edit a value and rename it to match, or give one agent its own harness
in its definition. Model IDs are type-checked against the library's
catalog, so a typo fails npm run typecheck. The pi adapter also takes
thinkingLevel (off, minimal, low, medium, or high; default
medium).
Each harness declares the env var of its key, its credential. Setup and
doctor derive the keys they manage from the agents' harnesses, so a harness that needs a new key
needs no other configuration change. The pi adapter supports Anthropic
(ANTHROPIC_API_KEY), Google (GEMINI_API_KEY), and OpenAI
(OPENAI_API_KEY). After a harness change that needs a new key:
- add the key to local
.env; - run
npm run setup:renderto add it to thesoftware-factory-workflowsenv group; - deploy
factory-workflows; and - run
npm run doctor -- --service workflows.
Each harness also declares its egress: the hosts its model requests go to.
Every task Sandbox's allow-list includes the hosts of all registered agents.
The pi adapter uses the host of the model's baseUrl, so switching
providers needs no egress change. Pass egress to pi() when a custom
models registry serves the model from another host, such as a proxy.
Every harness gives the model the factory's own tools, so an agent's tools and the read-only shell policy behave the same on any harness.
Use a key dedicated to this deployment with a provider-enforced spend limit. The factory has no hard dollar budget.
| Change | Required action |
|---|---|
| Repository, label, permission, or publishing limit | Deploy both services |
| Agent harness or model | Deploy factory-workflows |
| Prompt, agent definition, or Workflows service code | Deploy factory-workflows |
| Gateway routing or validation | Deploy factory-gateway |
| Label, App permission, or subscribed event | Run npm run setup:github; update the App when prompted |
app/runner/ or app/harness/ sources |
Deploy factory-workflows; each Sandbox gets the new runner payload when it opens |
sandbox/ |
Deploy factory-workflows, then refresh each repository snapshot |
Repository install command or lockfiles |
Refresh the snapshot, or let the next task reinstall |
Each configured repository has a dependency-installed Sandbox snapshot. A push to its base branch refreshes it. If no snapshot exists, the first task builds one.
A failed refresh does not delete the previous usable snapshot.
Start the refresh-snapshot task with the repository, its GitHub App
installation ID, and the base branch's current commit:
git ls-remote https://github.com/<owner>/<name>.git refs/heads/<base-branch>
cat > input.json <<'JSON'
[{"repo":"<owner>/<name>","installationId":<id>,"sha":"<commit>"}]
JSON
render workflows tasks runs start \
<workflow-slug>/refresh-snapshot \
--input-file input.jsonThe installation ID is the number at the end of the installation settings URL:
https://github.com/settings/installations/<id>
Organization installations use the equivalent organization settings URL.
While a GitHub task runs, the issue or pull request has factory:working.
The App-authored status comment shows progress, the run ID, and the final
result. Its hidden marker records the current Sandbox for cancellation.
Open that run under factory-workflows in the Render Dashboard to inspect its
input, logs, and result.
The service also writes structured events:
run.finishedrecords the task, state, outcome, and duration.agent.finishedrecords the agent, model, turns, token usage, reported cost, and stop reason.
Search them with the Render CLI:
render logs -r <factory-workflows-id> --text run.finished
render logs -r <factory-workflows-id> --text agent.finishedTo cancel a run, post this as the first line of an issue or pull request comment:
/factory cancel
The commenter needs minimumPermission. Cancellation stops the run named in
the status comment and terminates its recorded Sandbox. It also clears a stale
running status when the task has already stopped.
To restart work, remove the trigger label and apply it again.
factory:autopilot grants upfront approval to draft a spec, implement it,
open a draft pull request, and post a COMMENT review. It never approves or
merges the pull request, and it does not apply the three manual transition
labels. Triage may still clear a stale spec-approval label when it revises the
spec. The trigger label is removed when a run starts. If Triage asks questions,
the factory adds it back so a human reply can start the continuation. Use
/factory cancel to stop an active root workflow.
doctor is read-only, does not print secret values, and exits nonzero when a
required check fails or cannot be verified. This includes unreachable task APIs,
missing task registration, and installation permissions or events that have not
been accepted. The gateway intentionally skips GitHub checks because it has no
App credentials.
Webhook delivery is verified only when the latest attempt went to the configured gateway and received a 2xx response. If no such evidence is available, confirm Webhook > Active in the App's settings, save, and redeliver a ping from Advanced before rerunning doctor. GitHub's API does not report the Active setting, so a successful delivery proves delivery only at the reported time; it cannot guarantee that the hook has not since been disabled.
From a local checkout, this checks the services and credentials represented
by local .env:
npm run doctorIt cannot inspect values stored only in Render. Check the deployed gateway from its Render Shell:
npm run doctor -- --service gatewayWorkflows services have no Shell. Put the deployed Workflows values and exact
RENDER_WORKFLOW_SLUG in local .env, then run:
npm run doctor -- --service workflowsThe setup runbook lists the required values and common failures.
Local variables are listed in .env.example. The
Blueprint supplies RENDER_WORKFLOW_SLUG to the gateway from the Workflows
service. Every other deployed value comes from an
environment group.
| Variable | Purpose |
|---|---|
GITHUB_WEBHOOK_SECRET |
Verify GitHub webhook signatures |
RENDER_API_KEY |
Start Workflows tasks |
RENDER_WORKFLOW_SLUG |
Address the deployed Workflows service |
PORT |
HTTP port; defaults to 3000 |
| Variable | Purpose |
|---|---|
RENDER_API_KEY |
Manage Sandboxes, snapshots, and task cancellation |
RENDER_WORKSPACE_ID |
Workspace that owns Sandboxes and snapshots |
GITHUB_APP_ID |
Authenticate the GitHub App |
GITHUB_APP_PRIVATE_KEY |
Mint installation tokens |
ANTHROPIC_API_KEY |
Call the default models; each agent's harness declares the key it needs |
GITHUB_API_URL |
Optional GitHub API base URL |
GITHUB_API_URL is not in the Blueprint. GitHub Enterprise Server users must
set it in local .env for setup commands and add it to the
software-factory-workflows env group in the Render Dashboard.
Never commit .env or provider keys.
Secrets never pass through render.yaml or the Blueprint form. Each service
links Render
environment groups
instead:
| Group | Linked to | Variables |
|---|---|---|
software-factory-shared |
both services | RENDER_API_KEY |
software-factory-gateway |
factory-gateway |
GITHUB_WEBHOOK_SECRET |
software-factory-workflows |
factory-workflows |
RENDER_WORKSPACE_ID, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, ANTHROPIC_API_KEY |
The gateway links no group that holds GitHub App credentials or a model key.
ENV_GROUPS in
scripts/lib/requirements.ts defines this
layout for setup and doctor.
A Blueprint can't define a group that holds secrets it doesn't commit, so the
groups must exist in the workspace before the Blueprint is applied. Create or
update them from local .env:
npm run setup:render -- --workspace <tea-...>The command runs the pre-deployment checks
before writing any group: required values, pinned Node version, workspace
access, repository Issues availability, and configured base branches must
pass. Run npm run setup:preflight -- --workspace <tea-...> to check without
updating groups. Model and Sandbox execution remain unverified until the
approved smoke test.
It updates only values that changed, leaves other variables in each group alone, and never prints a secret. A second group with the same name makes it stop, because a Blueprint links groups by name.
To rotate a secret, change it in .env, rerun npm run setup:render, and
redeploy the services that link the group.
To keep the Render API key out of local .env, create the groups by hand
before applying the Blueprint. In the workspace, open Environment Groups
and create each group in the table with the exact name and variables shown.
Paste values from .env or from their source without showing them in chat.
Workflows doctor still reads these values locally, so export them in the
shell where it runs.