Configuration
Every knob in auto-swe lives in one of three places, and which one it lives in follows a single rule: what must be true before a process can reach the database stays in the environment; everything else is data an operator can change without a deploy.
| Tier | Lives in | Contents | Changed by |
|---|---|---|---|
| Bootstrap | Environment, permanently | DATABASE_URL, CONFIG_ENCRYPTION_KEY, TEMPORAL_ADDRESS, PORT, JWT/auth secrets, BUNDLE_TRUSTED_KEYS, NEXT_PUBLIC_*; sign-in credentials (GITHUB_CLIENT_*, GOOGLE_CLIENT_*, OKTA_*); artifact storage (ARTIFACT_S3_*, AWS_*); workspace sizing, images and network (WORKSPACE_*); the harness model proxy (HARNESS_MODEL_PROXY_*, REGISTRY_PROXY_*); WORKER_MAX_CONCURRENT_ACTIVITIES; SCANNER_REGEX_BUDGET_MS; the scheduled sweeps (REPO_ACCESS_SYNC_*, REPO_DEPENDENCY_SCAN_*, MODEL_DISCOVERY_*, RUN_REAPER_*, AUTOMATION_DECISION_PRUNE_*, SKILL_SOURCE_SYNC_*, SLACK_CHANNEL_SYNC_*, CALLBACK_DELIVERY_*, CALLBACK_DELIVERY_PRUNE_*, AGENT_TRANSCRIPT_PRUNE_*, AGENT_TRACE_ROLLUP_*, AUDIT_ACTIVITY_PRUNE_*) |
The deploy pipeline |
| Integrations | Singleton config tables | GitHub, Slack, issue tracker, knowledge base, Figma, workflow defaults | Admins, at /admin/integrations and /admin/workflow-defaults |
| Policy | The setting registry | Operator knobs, not constants in the worker | Admins and grant holders, at /admin/platform-settings |
Each integration tab compares its form with the saved config: Save stays disabled with “No unsaved changes” until something differs (a typed secret or a cleared field counts), the footer shows the number of unsaved changes, and reloading, following an in-app link or switching tabs asks before it discards them.
The bootstrap tier is deliberately not DB-backed. CONFIG_ENCRYPTION_KEY decrypts every other
secret, and BUNDLE_TRUSTED_KEYS is the trust anchor for bundle signatures — anyone with database
write access could otherwise mark a malicious bundle verified.
The bootstrap tier also holds anything an admin edit could never usefully change, or should not be
able to. Sign-in credentials and the Okta issuer are read once when the gateway boots, so an
editable copy needs a restart to apply and would only add a second place to look. Storage is a
deploy-time infrastructure choice, and workspace sizing and sidecar images describe the host the
worker runs on. The worker validates those variables at boot and refuses to start on one it cannot
use — a malformed image reference, a non-numeric limit, or WORKSPACE_BLOCK_METADATA set to anything
but true or false. The gateway does the same for the sweep variables: a flag that is not true or
false, or a cron expression that is not five fields. The sweeps are environment variables rather
than settings because the gateway applies them to Temporal once, at startup, so an edit in a form
would not take effect until a restart. Past boot the resolvers fall back to defaults instead of throwing, because they
run on paths (the scanners) that must never abort their caller.
One workspace value stays in the registry: workspace.gitHelperImage, which cascades to team and
organization because a team on an isolated network may need its own mirror. The bounds on agent runs
(workspace.agentRunMaxSteps, workspace.agentRunMaxWallClockSeconds,
workspace.agentRunMaxConcurrentGlobal, workspace.agentRunMaxConcurrentPerTeam and
workspace.agentRunAllowWorkflowChanges) are registry settings too, all ADMIN-only; see
agent-runs.md.
The assistant group holds assistant.enabled (ADMIN-only, default off), the switch for the in-app
assistant: off, the gateway refuses every request made with an assistant token, including the next
call of a conversation already under way, and the dashboard does not offer the panel. It is read per
request through the settings cache. assistant.maxStepsPerTurn (default 12) bounds the model steps
one reply may take, each tool call being one; a reply that reaches it ends with what it has and says
so. assistant.generateTitles (default on) lets the assistantTitler agent name each conversation
(its model is set in the agent library); off, a conversation keeps the start of its first message as its title
until the user renames it.
The assistant’s tools in the worker call the gateway at API_INTERNAL_URL (default
http://localhost:8080; both compose files set http://gateway:8080), the same variable the web
server reads for its own fetches.
The mcp group holds the switches and bounds of the MCP server: mcp.enabled,
mcp.deviceFlowEnabled (default false; enables browser-approved RFC 8628 sign-in for headless clients),
mcp.writeToolsEnabled, mcp.writeCallsPerMinute, mcp.maxConcurrentRuns,
mcp.agentRunAllowedDeliverModes (default ['none', 'draft_pr']), and mcp.templateRunsEnabled
(default false; authored workflows may push branches and trigger CI), all ADMIN-only, GLOBAL-only and read
per request, so they take effect within the settings cache’s ~30 s with no restart; see
mcp-server.md.
The prEvaluation group holds the risk thresholds, approver counts, sensitive-path globs and the
auto-merge gate for the pull requests the platform opens: prEvaluation.enabled (default off, the
kill switch), prEvaluation.defaultMode, prEvaluation.autoMergeAllowed (default off),
prEvaluation.autoMinEvidence, prEvaluation.requiredStatusContext, the size thresholds and the
approver counts. All are ADMIN-only; the switches, the evidence minimum and the context are GLOBAL
only, the rest overridable at team and organization. Turning prEvaluation.enabled or
prEvaluation.autoMergeAllowed off also starts the disarm sweep described in
pr-evaluation.md.
The models group holds models.privateNetworkHosts (ADMIN-only, GLOBAL only, default empty): hosts a
provider credential’s apiBase may name even though they are, or resolve to, a private address. It lifts
the private-address refusal everywhere the host is checked. Saving a credential checks the apiBase as
text only; the Test button, model discovery, the catalog refresh and run-time inference, embedding
and Claude Code model-proxy calls resolve the name and pin the connection to a checked address, and
run-time calls also refuse redirects. Loopback, link-local / cloud-metadata, unspecified, multicast and
reserved addresses stay refused whatever is listed, an unreadable setting lists no host, and a Claude Code harness run without the model proxy is outside the guard. See
model-configuration.md.
It also holds models.priceListUrl (ADMIN-only, GLOBAL only, sensitive): the community price list, in
LiteLLM’s model_prices_and_context_window.json format, that suggests a new model’s prices to the
catalog’s Add form and bulk add, and gives the scheduled catalog refresh figures to cross-check.
It defaults to LiteLLM’s file on GitHub; an empty value turns the lookup
off, and a mirror on a private network needs its host in models.privateNetworkHosts. See
model-configuration.md.
The skills group holds two settings, both ADMIN-only and GLOBAL only. skills.import.privateNetworkHosts
(default empty) names hosts a skill source may live on even though they are, or are spelled as, a private
address; a host qualifies only if it is also an approved repository host, and cloud metadata and loopback
addresses stay refused whatever is listed. skills.import.blockOnScanWarnings is default on. While
it is on, importing skills from an external source refuses any chosen skill whose description or text
draws a scanner warning (an incomplete scan included) and names it in the response; off, such a skill is
installed unverified with the warnings recorded on its first revision. It is read on each import, so a
change applies within the settings cache’s ~30 s; see
agents.md.
The check that flags a moved ref on a tracked skill source is a scheduled sweep like the other gateway
sweeps, so it is environment-only and applied once at gateway startup (a change needs a restart):
SKILL_SOURCE_SYNC_ENABLED (default true; it spends one API request per source and changes no skill)
and SKILL_SOURCE_SYNC_CRON (five-field cron, UTC, default 41 5 * * *). The gateway refuses to start on
a value it cannot use. See agents.md.
The Slack channel sync, which re-checks each channel’s privacy and records who is in each private
one, is a sweep too: SLACK_CHANNEL_SYNC_ENABLED (default false: it needs the bot’s channels:read
and groups:read scopes, and its member lists show a private channel’s content to its members),
SLACK_CHANNEL_SYNC_CRON (default 29 * * * *) and SLACK_CHANNEL_SYNC_STALE_HOURS (default 6, a
whole number from 1 to 9999), how old a channel’s member list may be and still count. See
channel-assistant.md.
The automation ledger’s retention sweep is environment-only too: AUTOMATION_DECISION_PRUNE_ENABLED
(default true) and AUTOMATION_DECISION_PRUNE_CRON (default 37 4 * * *). How long it keeps decisions
is the registry setting workflow.automationDecisionRetentionDays (ADMIN, GLOBAL only, default 90 days,
at least 7), read on each sweep. See automations.md.
The run-callback sweeps are environment-only for the same reason: CALLBACK_DELIVERY_ENABLED (default
true) and CALLBACK_DELIVERY_CRON (default * * * * *) send due deliveries;
CALLBACK_DELIVERY_PRUNE_ENABLED (default true) and CALLBACK_DELIVERY_PRUNE_CRON (default
47 4 * * *) delete old ones, keeping them for workflow.callbackDeliveryRetentionDays (ADMIN, GLOBAL
only, default 30 days). See run-callbacks.md.
The agent-transcript retention sweep is environment-only for the same reason: AGENT_TRANSCRIPT_PRUNE_ENABLED
(default true) and AGENT_TRANSCRIPT_PRUNE_CRON (default 57 4 * * *). How long transcripts keep
is a setting (workflow.transcriptRetentionDays), read on each sweep; so is how long the transcript
access log keeps (workflow.transcriptAccessLogRetentionDays, ADMIN, GLOBAL only, default 2555 days,
365 to 3650). The same sweep deletes artifacts left pending by an object-store upload that never
completed. See transcripts.md.
The usage rollup sweep is environment-only as well: AGENT_TRACE_ROLLUP_ENABLED (default true) and
AGENT_TRACE_ROLLUP_CRON (default 13 1 * * *) build the daily summaries the usage report reads
for completed days, and AGENT_TRACE_ROLLUP_BACKFILL_DAYS (default 0, at most 9999; set it on the
worker) also builds days that have none, back that many days or to the oldest trace, ten days
per sweep. With the sweep off the report is slower on long windows and otherwise the same, up to floating-point tolerance and the two cases listed under Limitations in architecture.md (spend of deleted runs, traces committed late). See
architecture.md.
The audit and activity retention sweep is environment-only too: AUDIT_ACTIVITY_PRUNE_ENABLED (default
true) and AUDIT_ACTIVITY_PRUNE_CRON (default 7 5 * * *). Audit rows are kept until
audit.configLogRetentionDays is set (ADMIN, GLOBAL only, default 0 = forever, otherwise at least 365
days); activity events are kept for analytics.activityRetentionDays (ADMIN, GLOBAL only, default 90 days,
at least 7). See audit-log.md and user-activity.md.
This doc covers the third tier. For integration credentials see model-configuration.md and the admin pages themselves; for the environment see deployment.md.
1. Settings are declared, not stored ad hoc
Section titled “1. Settings are declared, not stored ad hoc”A setting is one declaration in packages/shared/src/config/registry.ts:
'channel.reactiveCooldownMinutes': defineSetting({ group: 'channel', label: 'Interjection cooldown', description: 'Minimum gap between unprompted interjections in one channel…', schema: z.number().int().positive().max(10_080), defaultValue: 10, overridableAt: ['TEAM', 'ORGANIZATION'], requiredRole: 'LEAD', runPinned: false, restartRequired: false, unit: 'minutes',}),That one declaration is what validates a write, resolves a read, decides who may change it, and
renders the admin form. There is no migration, no request schema, and no form field to write — which
is the point: a knob that costs five files to add stays a const instead.
defaultValue is always the constant the setting replaced, so a deployment that configures nothing
behaves exactly as it did before the setting existed.
A setting’s identity is the property it is stored under — channel.reactiveCooldownMinutes
above. That key is the storage key and the subject of permission grants, so renaming one orphans
every row and grant that referenced it.
| Field | Meaning |
|---|---|
schema |
Zod schema. Validates writes and values read back out of the database. |
overridableAt |
Scopes below GLOBAL where an override may be set. Empty means platform-wide only. |
requiredRole |
A floor. No grant can let an actor below it write the key. |
runPinned |
Frozen into a run’s snapshot at start; see §4. |
restartRequired |
Surfaced in the UI. The resolver does not enforce it. |
2. Resolution order
Section titled “2. Resolution order”resolveSetting(key, ctx) in packages/shared/src/config/resolveSetting.ts is the only read path:
run pin → WORKFLOW_TEMPLATE → CHANNEL → TEAM → ORGANIZATION → GLOBAL → defaultThe five scope tiers are the same cascade agents and provider credentials resolve through (see architecture.md). A tier only participates when the context carries its id, so a deployment with no organizations behaves exactly like a three-level cascade.
Two properties worth knowing:
- Every candidate override for one context is fetched in a single query and cached under one key, so resolving twenty settings for a run costs one round trip. The cache is the same ~30 s TTL the agent and credential resolvers use.
- A stored value that no longer parses is ignored, not thrown. A row written before a schema tightened degrades that one key to the next tier down, rather than failing every activity that reads configuration. The rejection is logged, so a saved value cannot vanish without a trace.
- A row at a scope the definition forbids is ignored too.
overridableAtis enforced on the way out as well as on the way in — a platform-wide security control that a stray row can switch off would not be a control.
Where a knob also has its own column on the entity it belongs to — SlackChannel carries the
per-channel interjection cooldown, for instance — that column is the narrow override and always
wins. Those keys deliberately do not offer a CHANNEL scope in the registry, because a row
there would be stored and never read, and the effective-config view would report it as winning.
resolveEffectiveSettings(ctx) returns every setting with the tier that supplied it. That is the
effective-config view behind /admin/platform-settings, and the answer to “why is this run behaving that
way”.
3. Who may change what
Section titled “3. Who may change what”Two independent gates, both of which must pass.
The definition’s requiredRole is a floor. No grant can let an ENGINEER write a key marked
ADMIN.
A grant must authorise the actor. Grants are rows in config_permissions, not a map in code, so
widening who configures what is an admin action rather than a deploy:
| Column | Meaning |
|---|---|
keyPattern |
An exact key, a group.* prefix, or *. Deliberately not a glob library — an operator reading a grant should know what it covers without learning a syntax. |
userId or role |
Exactly one, enforced by a CHECK constraint. A role grant covers every holder of that role. |
scope + teamId/orgId |
The authority’s reach. A TEAM grant covers that team and the channels and templates beneath it; an ORGANIZATION grant covers anything inside the org but never the GLOBAL row. |
A write is authorised against the tenant it actually lands in, not the ids in the request: a CHANNEL override is checked against the team that owns the channel.
Platform admins bypass grants — they can already write every singleton config table, so requiring them to grant themselves access would be ceremony. Managing grants is itself admin-only: a permission model whose holders can mint further grants widens itself.
Every write, and every grant and revocation, lands in ConfigAuditLog with its before-state.
The audit log
Section titled “The audit log”/admin/audit (ADMIN only) lists the log newest first, 50 per page, filtered by action, actor,
entity type and a UTC date range. Free-text search matches a person’s email or name (as their actor
ids), the entity type as a substring, and an entity or actor id only when the whole UUID is typed —
both id columns are uuid, which a substring match cannot reach. The table shortens ids, so a
copied prefix finds nothing.
GET /api/v1/platform/audit-log/export returns the same filter as a streamed CSV
(time,action,actor,entity_type,entity_id,before,after,ip,via,request_id; the last three come from the row’s context), newest first, one row per entry. It holds
at most 50,000 rows. X-Export-Row-Limit carries that cap and X-Export-Truncated is true only
when more entries matched than the cap, in which case the file is the newest part of the result and
the page says so with the limit. A cell starting with =, +, -, @, tab or carriage return is
prefixed with ' so a spreadsheet does not run it.
4. Run-pinned settings
Section titled “4. Run-pinned settings”Some values a run makes a structural decision on. If workflow.maxTransitions changed halfway
through a run, the second half would disagree with the first, and — because the interpreter’s path
is recorded in Temporal history — a replay could diverge from the code that produced it.
Settings marked runPinned are therefore resolved once, when the WorkflowRun row is created,
and stored on WorkflowRun.pinnedSettings alongside the existing agent-version pin. A channel
assistant turn’s row is created by startChannelRun and pins at the scope the turn’s own reads use:
the channel template, the channel, and the channel’s team and organization. For the life of
that run, resolveSetting reads the snapshot instead of the live cascade, and the effective-config
view reports the value’s source as PINNED.
Everything else re-resolves on every call. That is what lets a model or credential edit land inside an already-running workflow rather than waiting for a fresh run.
A snapshot that lacks a key — the row was created before the column existed, or before the setting
was marked runPinned — is completed on the run’s first read of its context. currentRequestContext()
in the worker, which activities resolve their settings through, adds each missing key at the value
that context resolves it to now, so the read that triggered it and every later one see the same
value. createWorkflowRun does the same when it runs against an existing row (a retry, or a reused
workflow id). A value already pinned is never changed, and the write is a compare-and-set on the
stored snapshot, so concurrent activities of one run converge on a single pin rather than
overwriting each other. A pin that cannot be written — the database errors, or the row keeps
changing for three attempts — is logged, and the read falls back to the live value; the next
context lookup, after the ~30 s cache window, tries again. A channel turn’s missing keys are pinned
at its channel’s scope, the same one startChannelRun uses.
5. Storage
Section titled “5. Storage”config_settings holds one JSON value per (key, scope). Uniqueness is a partial index per scope
— WHERE scope = 'GLOBAL', WHERE scope = 'TEAM', and so on — which Prisma cannot express in an
upsert, so writes use findFirst then create. This is the same constraint Agent and
ProviderCredential live with.
A CHECK constraint enforces that a row carries exactly the id for its own scope, so a TEAM row can never also name an organization.
Both tables are registered with the tenant guard. The settings read is a deliberate exception — its
GLOBAL branch selects rows that belong to no tenant by definition — so it is wrapped in
runUnscoped with the reason attached.
The singleton integration tables are not absorbed into the registry. Their shape is genuinely relational, their secrets use the AES-256-GCM envelope, and they work; the registry covers what had no home.
Clearing a stored secret. A save can only replace a secret, so
DELETE /api/v1/platform/config/:integration/secrets/:field removes one. It accepts the secret
fields of github (token, webhookSecret, appClientSecret, appPrivateKey), slack
(botToken, clientSecret, signingSecret), issue-tracker (apiToken, webhookSecret),
knowledge-base and figma (apiToken); any other field is refused with NOT_A_SECRET, and a
field with nothing stored returns NOT_STORED. Clearing hands the value back to its environment
variable when one is set, and to “not configured” when it is not. The call is audited.
Test connection with unsaved values. The github, slack, issue-tracker, knowledge-base
and figma /test routes accept an optional draft body (the tracker test also takes its
ticketId). A field left out, or a secret left blank, uses the stored value. Each test has a host
guard: a stored secret is valid on the stored host only, so a draft that changes the GitHub API
URL, or the tracker or knowledge-base provider or base URL, must carry the secret typed alongside
it. Figma’s host is fixed, so a typed token can only reach Figma. A typed URL goes through the same
guard the connector applies at run time: GitHub needs a public address, and the tracker and
knowledge base honour the allowPrivateNetwork value on the form. Failures report fixed wording
per status class and never echo the remote response body.
Outbound URL guard. Every request to an operator-supplied URL (connector base URLs, MCP
servers, bundle URLs, provider apiBase probes, skill sources, CI log links) is checked twice. The
text check (shared/lib/ssrfGuard.ts) refuses a bad scheme and a host written as a loopback,
private, link-local or metadata name or address, and is the early error at save time. At
connection time createGuardedFetch (shared/lib/guardedDispatcher.ts) resolves the host name
once, classifies every address it returns with the same rules (plus multicast and reserved ranges),
and refuses the request if any one is refused, so a public name such as 10.1.1.17.nip.io or a
rebinding name does not reach an internal host. The connection is then made to an address from that
checked answer (the check is the dispatcher’s lookup), while TLS server name and Host stay the
hostname, so certificate validation is unchanged. A private address is allowed only where the call
site’s opt-in (allowPrivateNetwork, or an opted-in skill-source host) allows it; loopback,
link-local, unspecified, multicast, reserved and cloud-metadata addresses are refused regardless.
A DNS failure or timeout (5 s) refuses with a fixed message and never echoes resolver detail.
Redirects are followed by hand or refused, so each hop passes through the same guard.
Where one call talks to a single trusted host and may be redirected elsewhere (the CI log download,
an MCP server), the private-network allowance is scoped to that host’s origin and every other
origin is checked strictly. The exception is a CI log download for a repository on a GitHub
Enterprise host (not github.com): its log storage lives on the same internal network, so every hop
may resolve to a private address, while loopback, link-local, unspecified, reserved and metadata
addresses stay refused on every hop. The check classifies one source of truth for addresses: besides
loopback, link-local and cloud metadata (including GCP’s fd20:ce::254 and OCI’s legacy
192.0.0.192), the IETF protocol, documentation, 6to4 relay, Teredo and discard ranges are
refused. The benchmarking block 198.18.0.0/15 is classed with the private networks, because
fake-IP proxy tools (Clash, Surge, sing-box) answer every DNS name with an address in it: it is
refused by default and allowed with the private-network opt-in, so a deployment behind such a tool
needs the opt-in on every connector it uses. Connections use HTTP/1.1 and try the checked addresses IPv4 first. Production
always drives the dispatcher with undici’s own fetch; only tests substitute one.
yarn invariants:check fails a bare fetch (a call, a = fetch default or fetch passed as a
value) in the modules whose requests are all operator-supplied: the bundle, credential-probe, MCP
probe, model-discovery, Atlassian, GitHub Issues, skill-source, MCP tool and GitHub SCM modules.
Octokit and GitHub Enterprise API calls, an aliased fetch, and another HTTP client are not seen.
6. Adding a setting
Section titled “6. Adding a setting”- Add a definition to
packages/shared/src/config/registry.ts, with the current constant asdefaultValue. - Replace the constant at its call site with
resolveSetting(key, ctx)— orresolveSettingswhen a caller needs several, which costs the same single query. - Pass the fullest scope context available. A lookup with a missing
teamIdsilently resolves a broader value than intended.
There is no step 4. Storage, validation, the API, the admin form, and the audit trail all follow from the definition.
7. Git commit identity
Section titled “7. Git commit identity”Every commit the platform makes in a workspace carries an identity, and three registry settings choose
it. They are in the git group, overridable at WORKFLOW_TEMPLATE, TEAM and ORGANIZATION, and
a team lead can change them for their own team.
| Setting | Default | Meaning |
|---|---|---|
git.authorName |
auto-swe |
Name on the commit (ADMIN to change). 1–100 characters, no control characters, no < or >, and at least one character that is not whitespace or one of . , : ; " ' \, because git refuses to commit under a name made only of those. |
git.authorEmail |
auto-swe@localhost |
Address on the commit (ADMIN to change). Up to 254 characters, one @, no whitespace. |
git.launcherAttribution |
none |
Whether the person who launched the run is credited: none, co-author-trailer or author. |
The defaults give the identity commits carried before these settings existed, so a deployment that
sets nothing changes nothing. The address auto-swe@localhost is not a real address, so GitHub links
the commits to no account. Set a real address, such as a bot account’s no-reply address, to have
GitHub attribute them.
The identity resolves once, when a workspace is created, from the run’s workflow template, the repository’s team and that team’s organization. A run that has no team, such as an eval or a standalone gate run, resolves from the global value.
git.launcherAttribution changes who is credited:
| Mode | Author | Committer | Message |
|---|---|---|---|
none |
configured identity | configured identity | unchanged |
co-author-trailer |
configured identity | configured identity | ends with Co-authored-by: <name> <email> for the launcher |
author |
the launcher | configured identity | unchanged |
The launcher is credited only when two things hold. Their account email is verified, because GitHub
credits a commit to the account that owns the author address. And they are a member of the team that
owns the repository, so a person who reaches a repository only through a team it is shared with does
not have their email written into another team’s history. A run with no launcher, such as one started
by a webhook or a schedule, a launcher who fails either test, and a run whose repository has no team
all use the configured identity in every mode. The launcher’s name is capped at 100 characters, and a
name that git would refuse, such as one made only of punctuation, is replaced by their email address.
Both credited modes write the launcher’s account email into the target repository’s history, where
it stays, so the default is none.
8. Support contact
Section titled “8. Support contact”Three optional environment variables on the web container show a way to reach whoever runs the
deployment. They are read on each request and passed to the browser with the rest of
window.__APP_CONFIG__, so changing one needs a restart of the web container and no rebuild.
| Variable | Shown as | Rule |
|---|---|---|
NEXT_PUBLIC_SUPPORT_URL |
a link | Must parse as an https: URL with no embedded username or password. |
NEXT_PUBLIC_SUPPORT_EMAIL |
a mailto: link |
One @, no whitespace, no <, > or quotes. |
NEXT_PUBLIC_SUPPORT_SLACK |
text, or a link when it is an https: URL |
Up to 80 characters. A URL with embedded credentials is dropped. |
A value that fails its rule is dropped, and the web container logs one warning per process for
each distinct message. The rest still show. With none set, nothing is rendered. The contact appears on the sign-in screen, including
the awaiting-approval screen, on the error and not-found pages, and in the sidebar footer. The
compose files pass the three variables through, because the web container does not receive .env.
9. Extra issue-tracker repositories
Section titled “9. Extra issue-tracker repositories”With the GitHub issue tracker, a run reads and writes tickets only in the repositories it targets.
A deployment that keeps its tickets in a separate issues repository lists that repository in one
setting, in the github group:
| Setting | Default | Meaning |
|---|---|---|
github.trackerIssueRepositories |
[] |
owner/repo entries on the tracker’s own host, up to 50. A ticket that names one of them (acme/issues#5) is read and written for a run on any other repository, for example acme/app. |
Only an ADMIN can change it, at TEAM, ORGANIZATION or globally: every entry widens what the
platform’s tracker token can read and write for the runs it covers, so a team lead cannot add one.
A launcher cannot supply or override it. It resolves at the team and organization of the run’s own
repository (the repository the launcher selected, at submit time), so a TEAM or ORGANIZATION
value applies to every run on that team’s repositories, including runs by members of other teams on
a repository shared with it (ConnectionTeamShare). The team’s lead controls that audience through
shares and membership. A run with no git repository connection resolves no list. The value names
internal repositories, so it is sensitive: a reader below ADMIN sees the setting but not its value.
An entry is trimmed and must be ASCII letters, digits, _, . and - on each side of one /,
with no . or .. segment. A repeat that differs only in case is dropped, and comparison with a
ticket is case-insensitive. A bare issue number (5) still means the run’s own repository and never
a listed one. Entries are taken to be on the tracker’s host, so the host comparison applied to a run
repository is not applied to them, but the tracker is only ever called at its configured base URL:
an entry cannot reach another host. An unreadable value is treated as an empty list. How the scope
applies to reads and writes is in work-views.md.
Limitations
Section titled “Limitations”-
Git identity is not pinned to a run. The identity resolves when each workspace is created. A setting edited during a long run can give one pull request commits from two identities.
-
There is no per-agent git identity. Agents never run
git commit; platform code does, so the identity follows the workflow template, team and organization, not the agent. A run that wants to show which agent produced a change has no identity to carry that. -
Launcher attribution uses the account email. There is no GitHub no-reply address option, and an account whose email is not verified is never credited. Only members of the repository’s owning team are credited. Members of a team the repository is shared with get the configured identity.
-
authormode needs git 2.22 or newer in the workspace image. Older git ignores theauthor.nameandauthor.emailkeys, so the commit carries the configured identity and the launcher is not credited, with no error. The co-author trailer is part of the message and needs no particular git version. -
The support contact is global. It does not vary by team or organization. The fallback screen that replaces the whole page after a fatal error shows it only when the page’s config script had already loaded, and it checks the values less strictly than the server does.
-
The audit log’s entity-id search is exact. Only a complete UUID matches an entity or actor id; a prefix or fragment returns nothing. The CSV export stops at 50,000 rows, newest first, and the rest of a larger result is reachable only by narrowing the filters or date range and exporting again. The browser holds the whole file in memory before saving it.
-
Pinning needs a direct connection. With a process-wide proxy (
NODE_USE_ENV_PROXY,--use-env-proxyon the command line or inNODE_OPTIONS) the proxy resolves the target, so the host is resolved and checked beforehand but the connection cannot be pinned to that answer. The check still needs local DNS: a network that only resolves through its proxy fails closed, with the fixed “could not be resolved” refusal. A literal IP address is classified as written. The guard does not cover the Okta issuer’s discovery fetch (made by the sign-in library at start-up), a Claude Code harness run without the model proxy (the harness calls the provider from inside the workspace), the GitHub Enterprise and Octokit calls (those hosts are governed bygithub.repositoryHosts, and a private host is legitimate there), or fixed public hosts such as Slack, Linear, Notion and Figma. -
The draft GitHub test cannot reach a private address. A GitHub Enterprise host that is written as a private address, or whose name resolves to one, is refused when its URL is typed but unsaved; the stored configuration is not re-checked by that guard, so save it and test the saved values.
-
The registry does not yet cover every compiled-in constant. Temporal retry and timeout profiles (
packages/worker/src/workflows/proxyOptions.ts), the context-spill budgets inrunnableContext.ts, the model price table incostTracking.ts, and several agent loop caps are still code. Each is a definition away, but they are not done. -
WorkflowDefaultsis platform-wide. Branch prefix, PR templates, budget tiers, workspace sizing, and the TDD/eval iteration caps resolve from one global row and do not cascade to a team. Moving them into the registry would give them the cascade; until then/admin/workflow-defaultsedits them for the whole deployment. -
Cache invalidation is per process. A write invalidates the writing process’s cache immediately; the other service picks the change up when its ~30 s TTL expires. Gateway and worker are separate processes and there is no cross-process invalidation, the same limitation the scanner pattern cache carries.
-
restartRequiredis advisory. The UI says a restart is needed; nothing enforces or performs it. -
Grants are not scoped below a team. Authority is expressed at GLOBAL, ORGANIZATION, or TEAM. There is no way to grant someone control of one channel’s settings without granting the team.
-
No config export or import. Bundles cover agents, skills, templates, and container steps; settings and grants are not part of a bundle, so promoting a configuration between environments is still manual.
© 2026 Jorge Barnaby
Source-available under FSL-1.1-MIT