App Works — cross-epic contracts (normative)
Status: Draft · Created: 2026-09-17 · Program: App Works
Thirteen epics are specified in parallel. This file fixes the names they share so the specs, plans and implementations cannot drift apart: the App spec's shape, which epic owns each entity, endpoint, capability interface, job, Activity event, flag and environment variable, and which epics only consume them. An epic that needs a shared name not listed here adds it here, in the same PR, with an owner.
Rules: owner = the only epic whose tasks create/migrate/implement it; consumers read or call it and
must not re-declare it. Names below are binding for plans and tasks; spec.md files describe behaviour
without them (Constitution IX).
Contract change (APW-03, 2026-09-17) — no top-level key renamed; read before coding against §1. The field-by-field reference is
APW-03/schema.md; catalog formats areAPW-03/catalog.md.
- C1 — Strict means strict reporting. An unknown key inside
specis an error (unknown_field), except keys starting withx-; no writer ever deletes it (the envelope's preservation rule stands). Two optional keys joinspec:kind: app(repeats the root kind) andappSpecVersion(integer, default1); a newerappSpecVersionturns unknown-key errors into warnings.- C2 — Outline clarifications.
jobs[].componentdefaults todomains.primaryComponent, and anhttpjob needs awebcomponent.http.bodystring leaves take{{…}}placeholders — the outline'sgeneratedCredential: adminis illustrative; write{{env.ADMIN_PASSWORD}}over a generated env entry. The outline'sgenerate/validateline lists option names, not a consistent pair (abase64generator of 32 bytes yields 44 characters;generateandvalidatemust agree). Asecret: trueentry cannot carryvalue. Env names startingEVER_WORKS_are reserved for platform-injected values.volumeswithreplicas > 1is an error (aligned with APW-06).license.sourceOfferUrl(https URL) is added. The object storage bucket output isdeps.objectStorage.bucket.<name>.http.authScheme(APW-13) applies to jobs and cron alike.- C3 — One license attestation record. Owner APW-03:
WorkAppSpecState.attestationandPOST /api/works/:id/app-license/attest. APW-06 reads it throughAppLicenseService.getHostingEligibilityandWorkAppRuntimeStatekeeps no license attestation of its own. The Source-link condition is shared: anetwork-source-offerobligation and (relationlinkor the Work Repository is ahead of upstream).
0. Program audit resolutions (binding — 2026-09-17, against develop @ ee45946e5)
The thirteen epics were written in parallel and then audited together (citations, hygiene, format, acceptance traceability). Where epics disagreed, the resolution below wins over any epic text written before it; every epic must be consistent with it. Ids are stable — cite them as "Resolution R-n".
| Id | Topic | Resolution | Epics to align |
|---|---|---|---|
| R-1 | Shared types folder | packages/contracts/src/apps/ (one folder for every App Works shared type). No src/app-works/. | all |
| R-2 | Activity naming | action = the dotted event name from §6 (e.g. app.build.succeeded); actionType = the family in snake case (app_source, app_fork, app_actions, app_upstream, app_spec, app_blueprint, app_license, app_provision, app_build, app_deploy, app_job, app_smoke, app_health, app_env, app_dependency, app_change, app_upstream_pr, app_launcher, app_tier). | all |
| R-3 | License attestation | One record, owned by APW-03 (C3). APW-06 reads eligibility; it stores no attestation of its own. Red and amber licenses may run on Your cluster after the owner's attestation (manager → 403); red never runs on Ever Works Apps and is never listed in the Apps catalog; amber runs on Ever Works Apps only with a recorded upstream agreement. | 03, 06, README D13 |
| R-4 | First write into the Work Repository | A repository this App Work just created (fork or private copy) whose spec holds only source → one direct commit through commitFiles (no clone). Link → always a pull request; the platform never pushes to a default branch it did not create. Blueprint application goes through APW-03's apply job so source and the App spec land together. | 01, 03, 05 |
| R-5 | Managed tier deploy path | On Ever Works Apps the platform never applies manifests; APW-06 renders and hands desired state to APW-10's apps-tier capability (ever-works-apps plugin → Work resource reconciled in-zone, reusing APW-06's renderer). APW-06's managed tasks are re-scoped to call AppsTierPolicy; APW-03, 05 and 07 ask AppsTierPolicy.isOpen() — never read EVER_WORKS_APPS_MANAGED_ENABLED directly. | 03, 05, 06, 07, 10 |
| R-6 | Kind switch | Web chip flag works-app fails closed for app; the API gate EVER_WORKS_APP_WORKS_ENABLED (default false) refuses create/inspect from every client (web, chat, MCP, CLI). The web evaluation lives in apps/web/src/lib/feature-flags/work-kinds.ts, which is fail-open today ("DEFAULT IS ENABLED … a missing flag, or an undefined value → the chip is ENABLED", :13-20,75-76) and must be made fail-closed for this one kind — APW-01 T7b owns that change, scoped so no other kind's semantics move. | 01, 13 (§7 row updated) |
| R-7 | Capability flags | WorkCapabilities gains builds and appEnvironment; APW-01 sets both true for app (and false for every other kind) in the same PR that adds the kind. | 01, 05, 07 |
| R-8 | Upstream tab | One route /works/:id/upstream. APW-02 creates the tab (relation card, readiness, sync status, Actions hygiene). APW-09 adds the "Upstream pull requests" section to it. | 02, 09 |
| R-9 | Checks in the user's CI | APW-05's build workflow carries one checks job per App spec check (a matrix), each reporting its own check run named Ever Works check: {name}, with a read-only token and no secrets. This replaces APW-08's single-job assumption. | 05, 08 |
| R-10 | Provisioner verification hooks | Accepted as requested by APW-04: APW-05 startBuild({ verification }) (boot + smoke inside the build runner), APW-06 AppRenderInput.purpose: 'verification', APW-07 ephemeral AppRuntimeEnvSource mode (throwaway values never stored). Each owner's tasks implement them. | 04, 05, 06, 07 |
| R-11 | Keypair formats | env[].generate.kind: keypair takes format: pem | base64url-raw | pkcs12 (default pem); the public half is exposed as <NAME>_PUBLIC only. pkcs12 requires keypair.passwordEnv, naming a separate generated secret entry whose value encrypts the bundle (never an empty passphrase). | 03, 07, 13 |
| R-12 | "None" deploy target | The deploy target is None everywhere (value none, label "None — don't deploy yet"). There is no separate deferred-deploy state or label. | 01, 06, 13 |
| R-13 | Zero-config build strategy | build.strategy values: dockerfile | image | auto | none. auto = the build plugin detects the language/framework and builds without a Dockerfile; which builder implements it is a plugin choice, never named in the App spec. | 03, 04, 05, 13 |
| R-14 | Public wording of existing defects | Existing, unfixed weaknesses in platform code are described generically in this public repository ("checkout directory keys are not unique across owners"); exact reproductions live in the private operations repository. Test cases use generic fixtures ("two owner/repository pairs whose normalized names collide"). | 02 (and all) |
| R-15 | Delete an App Work | Deleting an App Work removes its cluster workloads (Deployments, Services, Ingresses, Jobs, CronJobs, the app's network policies, the env Secret) on its deploy target, keeps volumes and App dependencies unless the owner ticks "Also delete stored data" and types the App Work's slug; the namespace default-deny policy stays and kept in-cluster dependency workloads are stopped while kept data remains. Removal runs before the Work row is deleted: APW-01 calls APW-06's deletion service through APP_WORK_DELETION_PORT.requestDeletion and exposes completion (completeAppWorkDeletion(workId), §3). It never deletes the upstream, and deletes the fork/private copy only with the separate explicit confirmation from APW-01. | 01, 06, 07, 10 |
| R-16 | Public URL on Your cluster (Wave 1) | An App Work is reachable by three address shapes, all supported — (a) <slug>.<EVER_WORKS_APPS_DOMAIN>, which defaults to EVER_WORKS_DOMAIN (ever.works), DNS record pointing at the deploy target's ingress — the shape that makes a template install "just work" as my-cool-company-gauzy.ever.works; (b) custom domains through the existing add → verify → remove flow, including <slug>.<tenant-domain> subdomains and a tenant apex; (c) the original dedicated user-apps apex — <slug>.<apps-domain> under an apex outside every platform domain and listed on the Public Suffix List, still operator-selectable and never deprecated, kept because it is the only shape giving hard cookie isolation between an app and the platform's own session. LG-15 / APEX_NOT_ON_PSL / PSL_UNREACHABLE stay in force for shape (c) and are simply not exercised otherwise. Shapes (a) and (c) differ only in the configured apex, so this is a configuration default, not a removal (owner decision 2026-09-17: no new PSL apex is registered by us — an operator may still configure one). What remains forbidden is a subdomain under another Ever product's domain (ever.team, gauzy.co, …); my-app.ever.works is explicitly allowed. Where shape (a) is used, cookie isolation is carried by host-only __Host- Secure cookies on platform routes, no platform session cookie on app hosts, and app hosts that never serve platform pages. See D10 in the program README. | 06, 13 |
| R-17 | Safety rails (AW-23/AW-24) | Every App Works agent run passes the run admission chain. A run parked by the stop flag or an Agent/workspace pause is a wait (no attempt consumed, deadline clock paused); a safety-gate refusal becomes needs_input. Commits and PRs in the evolve loop and the Provisioner go through Task finalize (not the agent commitToRepo/openPullRequest tools), so they are not held by the publish rung; the P0 tool fix still ships for other callers. | 04, 08 |
| R-18 | Upstream PR approvals | The upstream_pull_request approval type maps to publish in the trust-ladder proposal table (append-only edit); an off rung blocks it; it is never approvable in bulk (requiresIndividualDecision). | 09 |
| R-19 | Ever ID auth method | AuthenticatedUser.authMethod already exists ('session' | 'api-key'); APW-12 appends 'ever-id-delegated' only. Delegated tokens are admitted solely on routes marked @DelegatedRead(scope). | 11, 12 |
| R-20 | Tier stop naming | APW-10's per-App-Work stop is Quarantine (gate item LG-18 "Tenant quarantine drill"). The platform stop flag and Agent/workspace pauses stop agent runs only; they never quarantine tier workloads. | 10 |
| R-21 | Upstream sync conflicts | APW-02 creates the conflict Task (never auto-resolves); the Task's Agent is chosen by APW-08's agent-resolution rule; when no Agent resolves, the Task stays unassigned and the owner is notified. | 02, 08 |
| R-22 | Test locations | apps/api/test/*.e2e-spec.ts is not a runnable lane. API behaviour is tested by controller/service specs under apps/api/src/** (Jest) or by request-level Playwright specs under apps/web/e2e/ (flow-app-*.spec.ts). flow-app-launcher-apps.spec.ts is owned by APW-11; APW-13 only references it. | all |
| R-23 | Fixture repository branches | APW-13 creates every fixture branch other epics' tests need (e.g. APW-05's variant/services-postgres, variant/missing-value, variant/secret-in-image, variant/dockerfile-error — APW-13 T58; short names elsewhere mean variant/<name>). | 05, 13 |
| R-24 | Sandboxed runtime vs sandboxed builds | A sandboxed container runtime for tenant workloads is required from Wave 2 (LG-04). In-zone sandboxed builds arrive in Wave 3 (LG-24). | 05, 06, 10, README |
| R-25 | Workspace backup (AW-22) | Every table an App Works epic adds is classified for the workspace backup (AW-22, landed on develop @ e5f43f44d) in the same PR: either a file in a BACKUP_DOMAIN_SPECS domain (App Work children under works, scoped through the parent Work ids; launcher preferences under the account domain; tier usage history under the runs domain with its time trim) or an entry in BACKUP_DROPPED_ENTITIES with its reason. Secret-bearing data is never exported: work_app_env_values values and work_app_dependencies connection outputs are dropped or redacted to { wasSet } (names only), external_identities is dropped with sessions and auth tokens, and tier credential fingerprints are dropped. Each epic extends packages/agent/src/account-transfer/backup/collectors/collectors.spec.ts so the classification is enforced by a test. | 02, 03, 04, 05, 06, 07, 09, 10, 11, 12 |
| R-26 | Additive-only rule (TOP PRIORITY) | Owner rule, 2026-09-17: all work on this program is either an improvement or an additional feature. Nothing is ever deleted, removed, weakened, narrowed or marked obsolete unless the owner explicitly asks for that removal in the turn where it happens. The words cleanup, simplify, consolidate, unify, deprecate and refactor are not authorisation to remove anything. If a spec appears to demand a removal, the spec is wrong — rewrite it as an addition. Never reduce a default's flexibility; never mark existing behaviour "not applicable"; never park a feature behind a flag whose only purpose is to hide it. If a removal looks genuinely unavoidable, STOP and ask. Exempt: work the agent itself added earlier in the same session and that the owner then redirects. Written into the workspace repository's AGENTS.md as non-negotiable #27; see also APW-06-app-runtime/deploy-shapes.md. | all |
| R-27 | Deploy shapes are a family | An App Work has exactly one chosen deploy target (none / your-cluster / ever-works-apps), but the shapes underneath are a family and each one is kept: the Ever Works shared customer cluster (k8s-works-shared), the internal admin cluster (k8s-works), a customer kubeconfig (custom-kubeconfig), a machine connected to Ever Works (Fleet node enrollment — substrate shipped), a remote host over SSH (extension point), and any further provider as a deployment plugin (the capability is already generic; k8s and vercel implement it). Adding a shape never narrows an existing one. The full taxonomy, evidence and per-shape gate attestations are in APW-06-app-runtime/deploy-shapes.md. Owner answer 2026-09-17 (B-01): "we do NOT change anything here or remove, we may EXPAND only". | 06, 10, 13 |
| R-28 | Ever ID provider, domain and integration posture | Owner decision, 2026-09-17. The Ever ID identity provider is ZITADEL, self-hosted as-is and unmodified, one instance for every platform, at auth.ever.co (verified free in the live ever.co zone). The integration is pure addition: every platform keeps its own authentication and its own user database; nothing that authenticates a user today is removed, replaced, deprecated or routed away. Ever Works keeps Better Auth, Ever Gauzy keeps e-mail/password, magic code and its social strategies. Ever ID is the cross-platform SSO layer only; account, profile and credential data stay in each platform's own database, and duplicated profiles are accepted. Every platform integrates through standard OpenID Connect (discovery document + 1–3 allowed issuers), so the choice stays reversible. Ever Works integrates through the oidc-identity plugin; Ever Gauzy's integration is a plugin, not core — no ZITADEL code, dependency, strategy, entity or route is added to packages/core, packages/auth/src/lib/internal.ts or packages/config, and the shipped Keycloak core code moves into a per-provider plugin (zitadel, keycloak, supertokens, auth0) without changing behaviour: the strategies, their 'disabled' defaults and every existing route keep working exactly as they do today, and each plugin is optional, independent, enabled by its own configuration and fails closed when unconfigured. Decision record and the binding constraints: APW-12/idp-options.md §6–§7. | 11, 12 (README D14, §8 Q6) |
| R-29 | Catalog repositories are real, and named | Owner decision, 2026-09-17. ever-works/platforms exists (the Ever-platform launcher catalog: platforms.json, schema/platforms.schema.json, icons/, CI green) and EVER_WORKS_PLATFORM_CATALOG_REPO defaults to it; ever-works/templates exists and is public and is the EVER_WORKS_APPS_CATALOG_REPO default; ever-works/apps is no longer referenced as the platform's listing repository, and an installation that carries the old value keeps working (§7, §8). ever-works/<app>-template Blueprint repositories and ever-works/app-fixture-hello exist. No epic records these repositories as missing, and no epic may plan around a repository that does not exist without saying so in its own open questions. | 03, 11, 13 |
| R-30 | Operator kill switches | Every App Works background family has an operator switch, read by its job dispatcher and failing closed: EVER_WORKS_APP_SYNC_ENABLED (APW-02 sync + hygiene writes to user forks), EVER_WORKS_APP_PROVISION_ENABLED (APW-04), EVER_WORKS_APP_BUILDS_ENABLED (APW-05 workflow writes and secret sync), EVER_WORKS_APP_DEPS_ENABLED (APW-07 dependency provisioning and the mail relay), EVER_WORKS_APP_AUTO_DEPLOY_ENABLED (APW-08 auto-delivery and APW-06 auto-deploy), EVER_WORKS_APP_UPSTREAM_PRS_ENABLED (APW-09 open/push; Wave 2 has no other switch), EVER_WORKS_APP_MAIL_RELAY_ENABLED (APW-07 relay), EVER_WORKS_APP_CHANGES_ENABLED (APW-08 new change runs). EVER_WORKS_APP_WORKS_ENABLED (R-6) keeps its existing meaning for create/inspect. "App Works off" is defined, not implied: with every switch off, App Works stop changing anything — jobs pause (no new dispatches, running jobs finish their current step and park), the UI is read-only with a banner, existing Deployments keep running, sign-in and data reads keep working. Turning a switch back on resumes; nothing is deleted and no state is lost. Each switch has an ACC. | all |
| R-31 | Quotas and caps | Every unbounded App Works action gets a documented cap, per member and per organization, each with an environment override, a refusal code and user copy: active App Works, creates per day, private copies, Sync now calls, push builds per day (plus a runner-minute warning band), provisions, deploys and upstream PRs. The table is normative and lives in §7A. Caps are additive guard-rails: raising one is an operator action, and no cap may be introduced that narrows a documented default without recording it here. Filling a cap is a refusal with copy, never a silent drop and never a data deletion. | 01, 04, 05, 06, 09, 10 |
| R-32 | Human-only actions | Every action that spends money, deletes data, publishes outside the platform, changes a security posture or accepts a legal obligation is bound to an interactive session through @HumanOnly() (apps/api/src/safety/guards/human-actor.guard.ts, which admits only authMethod === 'session' and records refusals) and listed in the §4 human-only column. API keys, Fleet run tokens and Ever ID delegated tokens are refused with 403 and the existing non-human-actor body. A typed confirmation (confirmSlug) is an extra field, never a substitute for the guard. The MCP whitelist omits human-only routes. | 01, 03, 04, 06, 07, 09, 10, 11, 12 |
| R-33 | Fleet run containment is authoritative | A Fleet run reports the containment it actually got: FleetAgentTaskContainment with a bounded downgrade list, normalised by normalizeFleetAgentTaskContainment (packages/contracts/src/fleet/fleet-jobs.types.ts:1245,1301) and read by apps/api/src/fleet/fleet-agent-task-reconciler.service.ts; the operator's home directory is denied to the model step by apps/node/src/core/model-execution/isolated-home.ts. App Works admission reads that record rather than assuming containment: an App Work run is admitted to a Fleet node only when the reported containment includes the isolated home and no downgrade affects the workspace it will read, and a run whose record is missing or downgraded is placed on a sandboxed runtime instead (or parked with a reason). Every epic that dispatches App Work agent runs (APW-04, APW-08, and APW-09's preparation runs) states the check it performs. Nothing here weakens a Fleet path: the placement remains available, it is now conditional on what the node reports. | 04, 08, 09 (EXISTING-SUBSTRATE §3) |
| R-34 | Activity completeness | Every new ActivityActionType must, in the same PR: (1) add a FEED_KIND_RULES entry in packages/agent/src/activity-log/feed-kind.ts (feed-kind.spec.ts fails otherwise, and the family-level kind is work, decision, delivery, system or problem); (2) add a PUBLISHABLE_ACTIVITY_ACTIONS / NEVER_PUBLISH_ACTIVITY_ACTIONS classification in packages/agent/src/shared-views/publishable-activity.ts — App Works events default to NEVER_PUBLISH, because they carry repository names, paths and App Work identifiers; (3) carry a status per event (COMPLETED for opened/succeeded, FAILED for refused/failed/expired); and (4) carry a human-readable summary (English template plus its i18n key), because CreateActivityLogDto requires both and the web renders the summary rather than deriving one. A new family also gets its badge colour, filter label and locale keys in all 21 files (see §6A for notifications). | all |
| R-35 | Account and organization deletion | Deleting a user account or an organization runs an APP_WORKS_ACCOUNT_DELETION cascade owned by APW-01, on the existing UserAccountDeletionEvent: for every App Work the account owns, R-15 runs with data deleted (the account is gone, so "keep the data" has no owner); platform-written EW_ Actions secrets and webhooks are removed from user repositories; upstream-PR tracking stops and its polling state is cleared; env values, dependency connection data and tier rows are removed; and external_identities rows are deleted so no link outlives the account. The cascade is idempotent, resumable and logged (one Activity row per App Work plus a summary), and it never touches repositories or data the platform did not create. Organization deletion runs the same cascade for organization-owned App Works. | 01, 02, 05, 06, 07, 09, 10, 12 |
| R-36 | Another account's App Work answers 404 | WorkOwnershipService.ensureAccess throws 403 for a non-member and 404 only for a missing Work, but the epics, ACCEPTANCE and ACC-NEG-13 expect 404 for another account's App Work. App Works controllers therefore call the shared helper ensureCanViewOr404(workId, userId) (which maps the forbidden case to not-found) instead of ensureCanView on every GET they own, and state it in their tasks. The 403 path keeps its meaning for a member without the required role. | 06, 08, 10, 11 |
| R-37 | Threat register and operational signals are deliverables | The program keeps two normative registers, and every epic links its rows: §10 Threat register (with THREAT-MODEL.md) maps each untrusted input to its control, its owning task and its verifying ACC, with residual risk and the wave it is accepted in; §11 Operational signals names, per background job, the metric or log field, the stuck-state threshold, the alert with its owner and runbook, and the correlation id that follows merge → Build → Deployment → live. A control or a signal that no task implements is a gap, recorded as one rather than assumed. | all |
| R-38 | Merge order and prerequisite chain | The merge order in TRACKER is normative and must be satisfiable: an epic's phase may not be scheduled before a task it compiles against. Concretely, APW-13 P0 (test harness) lands in Wave 0; APW-03 P2 (T22, T26, T28, T32) and APW-06 T2–T3 (app-runtime/ports.ts, APPS_TIER_POLICY) land before APW-01 P1; APW-07 P1 splits into P1a (contracts, entities/migration, env core, providers and the job against typed fakes) and P1b (controllers, web and live acceptance, after APW-06 P1); APW-09 P1 is in Wave 1; and APW-03 P2/P3 are placed, not implied. An epic that needs a task from another epic names that task id in its own Prerequisites, and the receiving epic's phase placement is corrected in the same PR rather than the dependency being dropped. | all |
| R-39 | Migration timestamps | Program migrations stay inside the reserved 1792 block (README §7 rule 6) and are stamped in merge order within their epic's slot, not in authoring order: the block is 1792 + two-digit epic + two-digit slot + 00000, so a migration always sorts above the previous epic's when the merge order says it lands later. apps/api/src/migrations/__tests__/migrations-directory-contract.spec.ts gains an entry reserving the 1792 prefix for App Works filenames and reporting a non-App-Works file inside it, so unrelated develop work cannot take a value in the block unnoticed. Every migration remains forward-only with a down() that drops exactly what up() created. | all |
| R-40 | Non-production lane hooks | Every E2E_* / fake-source affordance an epic adds for the acceptance lanes is additive and production-refusing, and that is the standing pattern rather than a one-off: the switch is read through a config getter shaped like config.subscriptions.bypassSeatLimitsInE2E() (packages/agent/src/config/index.ts:824-829), the check runs before the handler, and the production answer is 404, proved by a spec. The programme's first such hook is APW-13's EVER_WORKS_E2E_FAKES (§7); APW-11 adds E2E_APP_LAUNCHER_SEED and EVER_WORKS_PLATFORM_CATALOG_BASE_URL under the same rule. A hook never widens a safety rule — with a catalog override active, every scheme, size, SVG-pattern and entry-cap check still applies — and no hook is reachable in production. No existing hook is removed or narrowed by a later epic. | all |
| R-41 | Upstream preparation's base, report and acknowledgement | Three facts that outrank epic text written before them. (1) The prepared branch upstream-pr/{slug}-{4 hex} is cut in the fork at the upstream default-branch head, and the single squashed commit is compared against that commit — recorded per row as upstreamBaseSha — never against the fork's own branch head; the preparation Task is provisioned with baseRef = the row's prepared branch, so the workspace's own baseSha, its empty check and its changed-file count all measure the upstream commit too. (2) A preparation reports through exactly one contract, UpstreamPreparationReport, written to .ever-works/upstream-pr-report.json, read by WorkspaceFinalizeOptions.reportFiles and dropped before the squash commit; a missing, oversized or malformed report fails with reportInvalid and nothing is inferred from prose; FR-12's check bounds are enforced on the report, and the approval labels that evidence as agent-reported. (3) extraFilesAcknowledgedAt is a precondition of an affirmative approval through every door — the approval screen, the approvals queue, the Inbox reply and the approvals API — because the approvals service refuses, not the screen. All three are additions: no state, code, limit or default is removed. | 08, 09 |
1. The App spec (owner: APW-03)
The App spec is the spec block of .works/works.yml when kind: app. It is the single source of truth for
how an App Work is built and run (Constitution III). APW-03 owns the full JSON Schema
(works.v2.schema.json + packages/agent/src/works-config/schema/) and validation; the outline below fixes
field names and meaning. Every field is optional unless marked required; unknown fields are rejected in
spec.* (strict) so typos fail validation instead of silently doing nothing.
version: 2
kind: app
spec:
source: # required — written by APW-01 at creation
relation: fork | private-copy | link # required
upstream: { repo: calcom/cal.diy, defaultBranch: main } # absent when relation = link
branch: main # the Work Repository branch that is built and deployed
blueprint: # present when resolved from the Apps catalog (APW-03)
id: cal
version: 1.0.0
repo: ever-works/cal-template
sha: <40-char commit>
license: # APW-03 license gate
spdx: MIT # or LicenseRef-* for custom licenses
class: green | amber | red | unknown
source: detected | blueprint | user
notice: 'Cal.diy® is a trademark of Cal.com, Inc.'
display:
name: 'Cal.diy (community build)'
protectedPaths: ['apps/web/public/brand/**'] # agents may not modify (D13)
build: # APW-05
strategy: dockerfile | image | auto | none # required when components exist (auto = zero-config, R-13)
dockerfile: Dockerfile
context: .
target: runner
image: ghcr.io/owner/app@sha256:... # strategy = image
args: # build-time values; reference env names, never literal secrets
- { name: NEXT_PUBLIC_WEBAPP_URL, value: 'http://localhost:3000' }
# NOT passed here: NEXTAUTH_SECRET and CALENDSO_ENCRYPTION_KEY. They are generated at run time
# (env below); the upstream Dockerfile accepts a build-only placeholder for its presence check.
# A real secret in a build argument can end up in build logs or image metadata — this is the
# rationale the real Cal.diy Blueprint records, and an earlier draft of this example passed
# `fromEnv: CALENDSO_ENCRYPTION_KEY` here, which both leaked it and failed `reference_unresolved`.
services: # ephemeral services reachable only during the build
- { name: postgres, image: 'postgres:16' }
resources: { cpu: 4, memory: 12Gi, timeoutMinutes: 60 }
components: # APW-06 — at least one when build.strategy != none
- name: web # required, DNS-label
role: web | worker # web components get Service + Ingress
command: ['/calcom/scripts/start.sh']
port: 3000
replicas: 1
writableRootFilesystem: true
probes:
startup: { http: /api/version, periodSeconds: 10, failureThreshold: 60 }
readiness: { http: /auth/login }
liveness: { http: /api/version, periodSeconds: 30 }
resources: { cpu: 500m, memory: 1Gi, memoryLimit: 3Gi }
volumes: [{ name: data, path: /data, size: 2Gi, backup: true }]
dependencies: # APW-07
postgres: { version: '16', directUrl: true }
redis: { version: '7', maxmemoryPolicy: noeviction }
objectStorage: { buckets: [uploads] }
smtp: { required: true }
env: # APW-07 — every variable the app reads
- name: NEXTAUTH_SECRET # required
secret: true
phase: runtime | build | both # default runtime
generate:
{
kind: base64 | hex | chars | uuid | keypair,
bytes: 32,
length: 32,
rotate: never,
keypair: { type: ed25519, format: pem | base64url-raw | pkcs12, passwordEnv: <ENV NAME> }
}
validate: { length: 32, pattern: '^[A-Za-z0-9+/=]+$' }
- name: CALENDSO_ENCRYPTION_KEY # the framework's own encryption key; runtime only (see `args` above)
secret: true
generate: { kind: hex, bytes: 32, rotate: never }
- { name: DATABASE_URL, secret: true, from: deps.postgres.url }
- { name: DATABASE_DIRECT_URL, secret: true, from: deps.postgres.directUrl }
- { name: NEXT_PUBLIC_WEBAPP_URL, from: domains.primary.url }
- { name: NEXTAUTH_URL, template: '{{domains.primary.url}}/api/auth' }
- { name: EMAIL_SERVER_HOST, from: deps.smtp.host }
- {
name: GOOGLE_API_CREDENTIALS,
secret: true,
prompt: { description: 'Google Calendar OAuth JSON', required: false }
}
- { name: CALCOM_TELEMETRY_DISABLED, value: '1' }
# Declared because the `cron` entries below reference it via `authEnv` — without this entry the
# example does not resolve (`reference_unresolved`). The real Cal.diy Blueprint declares it the same way.
- name: CRON_API_KEY # some cron routes compare the raw Authorization header with this value
secret: true
generate: { kind: hex, bytes: 32, rotate: never }
jobs: # APW-06 — run as Kubernetes Jobs
- name: migrate
when: pre-deploy | first-deploy | post-deploy
component: web # image + env of this component
command: ['npx', 'prisma', 'migrate', 'deploy']
timeoutSeconds: 900
- name: bootstrap-admin # first-deploy jobs run BEFORE the ingress is published
when: first-deploy
http: { method: POST, path: /api/auth/setup, body: { generatedCredential: admin } }
cron: # APW-06 — the app's own recurring calls (Kubernetes CronJobs); not a platform Schedule
# authScheme (added by APW-13): bearer (default) sends "Authorization: Bearer <authEnv value>"; raw sends "Authorization: <authEnv value>"
- {
name: booking-reminder,
schedule: '*/15 * * * *',
http: { method: POST, path: /api/cron/bookingReminder, authEnv: CRON_API_KEY, authScheme: raw }
}
domains: # APW-06
primaryComponent: web
publicUrlEnv: [NEXT_PUBLIC_WEBAPP_URL, NEXTAUTH_URL]
onChange: restart | rebuild # what a domain change requires
needsHairpin: true # the app calls its own public URL server-side
smoke: # APW-06 — run after every Deployment; APW-04 uses them as the gate
- { name: version, http: { method: GET, path: /api/version }, expect: { status: [200] } }
- {
name: login,
http: { method: GET, path: /auth/login },
expect: { status: [200], bodyNotContains: ['localhost:3000'] }
}
checks: # APW-08 — quality gates for Tasks on this App Work; run sandboxed
- { name: type-check, command: 'yarn type-check:ci --force', required: true, timeoutSeconds: 1800 }
agents: # APW-08
instructionFiles: [AGENTS.md]
maxPullRequestChangedLines: 500
upstreamSync: # APW-02
schedule: '0 6 * * 1'
mode: merge # merge-upstream for forks; fetch-and-merge for private copies
upstreamPullRequests: # APW-09
enabled: false
requireApproval: true # cannot be set false
Reference syntax (binding). from: accepts exactly domains.primary.url, domains.primary.host,
deps.<dependency>.<output> (outputs listed in APW-07's plan), and platform.smtp.<field>. template:
accepts {{…}} placeholders over the same references plus env.<NAME>. fromEnv: in build.args names an
entry of env. Generated values are produced once per App Work and never regenerated unless the user
explicitly rotates them.
Additions (APW-13, 2026-09-17 — needed to express the golden-path Blueprints). from: also accepts
build.commitSha (the 40-character data-repository commit the running image was built from; the value comes
from APW-05's Build and is rendered by APW-06) and components.<name>.internalUrl (the in-cluster URL of a
web component, reachable from the App Work's own pods and jobs before and after its ingress is published;
APW-06). Smoke requests never follow redirects, so expect.status sees the first response (APW-06).
Additions (APW-04, 2026-09-17). spec.provisioning: { autoReprovision: boolean } (default false) — the
owner's opt-in to automatic re-provisioning when an upstream sync breaks the smoke tests (APW-04 P3). Schema owned by
APW-03; the App Provisioner never writes spec.provisioning and treats it, like source, blueprint, license,
display.protectedPaths, upstreamSync and upstreamPullRequests, as a preserved field.
Additions (APW-08, 2026-09-17). spec.agents.requireHumanMergePaths: [glob] (≤ 50 entries) — paths whose
changes only a person may merge, whatever the merge policy says. Schema owned by APW-03; semantics APW-08. APW-08
requests that diffGuardedSpecBlocks also report removals from display.protectedPaths and
agents.requireHumanMergePaths (additions stay allowed). spec.checks[].name is the provider check name suffix
Ever Works check: {name} in the repository's CI (§3). App Work agent rules are always read from the Task's base
commit via AppSpecService.getEffectiveSpec(workId, baseSha).
2. Entities and persisted state
Vocabulary fix (2026-09-17). Earlier text in this file and in the epics uses "the data repository" for the repository that holds app code. That is the Work Repository — the persisted
RepositoryRolevaluewebsite, whose UI label is literally "Work Repository" (packages/contracts/src/domain/work-capabilities.ts:40-49).datais a different persisted role and holds the Work's data (content items, meta-data, setup parameters). Read every pre-2026-09-17 occurrence of "data repository" that means app code as "Work Repository"; see the repository-role note in README §1. The owner's decision also allows the-appsuffix alongside-websitefor the Work Repository, chosen by template type.
| Name | Owner | What it holds | Consumers |
|---|---|---|---|
Work.kind = 'app' + WORK_KIND_CAPABILITIES.app | APW-01 | kind, capabilities | all |
Work.sourceRepository.type values app_link, app_fork, app_private_copy; sourceRepository.upstream {owner, repo, defaultBranch} | APW-01 | how the Work Repository relates to the upstream | APW-02, 09, 11 |
Added by APW-01: sourceRepository.blueprintId (the Blueprint shown in the create preview, applied on ready); APP_SOURCE_REPOSITORY_TYPES is a new constant — IMPORT_SOURCE_TYPES is not widened (Work Import validates against it); top-level sourceRepository.owner/repo = the Work Repository (website role — see the repository-role note in README §1, not the data role, which holds the Work's data); sourceRepository.createdByThisWork?: boolean — true only when this App Work's creation issued the fork request or created the private copy; selects one commitFiles commit vs a setup pull request (R-4); APW-03's apply input | APW-01 | APW-03, 04 | |
WorkUpstreamState (table work_upstream_states) | APW-02 | fork readiness, last synced upstream sha, ahead/behind, last sync result, conflict Task id, Actions hygiene state | APW-01, 08, 09 |
WorkAppSpecState (table work_app_spec_states) | APW-03 | applied spec commit + hash, validation result, blueprint ref, license class + spdx, last evaluated at; also (APW-03 plan §3.1) tracked branch, head vs effective commit, effective-spec cache, problems, Blueprint apply/upgrade state, blueprintMatchedAt, license evidence + registry source, attestation, source-offer flag, display name, protected paths | APW-04…07, 11 |
WorkBuild (table work_builds) | APW-05 | commit sha, trigger, build plugin id, status, image digest, logs URL, duration, cost receipt ref | APW-06, 08, 13 |
WorkDeployment extended: buildId, componentStatuses, smokeResult | APW-06 | link a Deployment to its Build and record per-component + smoke outcome | APW-08, 11, 13 |
WorkDeployment extended (added by APW-06): appTarget (your-cluster | ever-works-apps), appRender (phase, namespace, spec commit, env checksum, job results, warnings, rollback facts — never values); states DEPLOYING, VERIFYING, ROLLED_BACK, SUPERSEDED | APW-06 | target, render facts and rollback outcome of an App Deployment | APW-08, 11, 13 |
WorkAppRuntimeState (table work_app_runtime_states) — added by APW-06 | APW-06 | deploy target + settings, namespace, cluster fingerprint + last connection check, current Deployment, deploy lock + queued Build, first-deploy jobs done, paused/removed, deletion request (requested at, delete stored data, attempts), ingress address, isolation enforced, health + streaks, status snapshot | APW-08, 11, 13 |
WorkAppProvisioning (table work_app_provisionings) — added by APW-04 | APW-04 | one App Provisioner run-through of an App Work: Task, Agent, trigger, status, step states, attempts (per-step verdicts, build id, image digest, smoke results), token + runner-minute totals and caps, open question, parkedReason + parkedAt, verification target + expiry, failure reason, Blueprint suggestion state; one active per App Work | APW-01, 02, 06, 13 |
WorkAppEnvValue (table work_app_env_values) | APW-07 | encrypted value per env name, origin (generated/prompted/derived/user), generated-at | APW-05, 06 |
WorkAppDependency (table work_app_dependencies) | APW-07 | dependency kind, provider plugin id, status, encrypted connection outputs, backup policy | APW-06 |
UpstreamPullRequest (table upstream_pull_requests) | APW-09 | Work, Task, upstream repo, PR number/url/state, approval id, disclosure text | APW-08 |
upstream_pull_requests full shape (added by APW-09, plan §3.1): author userId, sourceTaskId, preparation/follow-up Task ids, upstream + base, fork head owner/repo/branch/sha, state (preparing … withdrawn), checks + review summaries, signatureState, fingerprint, approval id + expiry, refusal code/detail, diff stats, check results, seen review ids, push timestamps, polling timestamps; partial unique active row per sourceTaskId; plus (2026-09-17 audit) upstreamBaseSha (the upstream default-branch commit the prepared branch was cut from — the squash base and the comparison point), preparationStartedAt / preparationPausedMs / preparationHeldSince (the 90-minute running-time deadline), claUrl, missingPieces (≤ 10), extraFilesAcknowledgedAt / extraFilesAcknowledgedById. Two further APW-09 tables: work_upstream_pr_settings (one per App Work: the requested value, the platform-authored App spec change carrying it, the applied value, a refusal) and upstream_pr_suggestions (one per Task: reason, sentAt, dismissedAt) | APW-09 | APW-08 | |
Task extended (added by APW-08): mergeCommitSha, deliveryState (merged · building · build_failed · built · deploying · deploy_failed · live · closed_without_deploy), deliveryBuildId, deliveryDeploymentId, deliveryUpdatedAt, deliveryClosedById; partial unique (workId, mergeCommitSha) | APW-08 | APW-09, 11, 13 | |
Task.branchGuardRefusal (column tasks.branchGuardRefusal, text NULL; migration 1792110100000-AddTaskBranchGuardRefusal.ts, added 2026-09-25 by APW-08 T17) | APW-08 | the refusal text (≤ 4,000 characters) when the change rules refused an App Work change that reached the remote — the post-push gate, or a node reporting a branch that is not the Task's own; cleared by a later full-branch judgement that allows it, and on discard. NULL = nothing refused | web |
Goal.workId (column goals.workId, uuid NULL) and Mission.outputMode (ideas · tasks · both), Mission.taskOutput, Mission.taskOutputNoticeAt — added by APW-08 | APW-08 | APW-13 | |
AppLauncherPreference (table app_launcher_preferences) | APW-11 | per user (and optional organization): item key, visible, order, pinned | APW-12 |
ExternalIdentity (table external_identities) | APW-12 | issuer, subject, user id, linked at, last login | APW-11 |
Work.appLauncherExposed (column works.appLauncherExposed, boolean NULL = kind default: app on, others off) — added by APW-11 | APW-11 | Work-level "Show in App Launcher"; app_launcher_preferences keyed (userId, scopeKey 'global' | 'personal' | <organizationId>, itemKey 'platform:<id>' | 'work:<uuid>') | APW-12, 13 |
Ever Works Apps tier state (added by APW-10): apps_tier_gate_runs, apps_tier_attestations, apps_tier_state_events (append-only), apps_tier_quarantines, apps_tier_abuse_signals, apps_tier_image_allowances, apps_tier_quota_profiles, apps_tier_usage_windows; column works.appsTierQuotaProfile | APW-10 | launch-gate evidence, open/closed transitions, quarantine, signals, quota profiles, idempotent usage windows | APW-06, 13 |
Added by APW-04 (audit fix 2026-09-17): Organization.appProvisionCaps (column organizations.appProvisionCaps, nullable simple-json { tokenCap?, runnerMinuteCap? }) + migration 1792040100000-AddOrganizationAppProvisionCaps.ts (one nullable column, forward-only). The resolution order for a per-provisioning cap is Organization → instance environment → the documented default | APW-04 | per-Organization overrides of the App Provisioner's token and runner-minute caps; already exported as data/organizations/organization.jsonl (R-25) | APW-01, 13 |
Added by APW-12 (audit fix 2026-09-17): session.externalIdentityId (uuid NULL, no FK) and session.externalSid (varchar(255) NULL) + indexes idx_session_external_identity, idx_session_external_sid; migration 1792120100000-AddExternalIdentityToSessions.ts. No FK on purpose: a session row must never block deleting an identity, and disconnect deletes the sessions explicitly first | APW-12 | sessions opened by an Ever ID identity and the provider sid, so a back-channel logout notice can revoke exactly those sessions and an S6 notice can be shown | APW-11 |
Added by APW-05 (P3, Wave 3): work_builds.scanSummary (simple-json NULL, { critical, high, medium, low, fixableCritical }), work_builds.signatureState (varchar(16) NULL, signed | unsigned | foreign), work_builds.blockedEgressHosts (simple-json NULL, ≤ 10); migration 1792050100000-AddWorkBuildSupplyChain.ts. The WorkBuild row above is therefore the P1 shape, not the final one — P3 adds columns and changes nothing that exists | APW-05 | supply-chain evidence and egress blocking for the managed in-zone builder | APW-06, 10 |
Added by APW-05 (2026-09-17, APW05-G03): work_build_preparations — one row per App Work holding the platform-only preparation state (lastBuildInputsHash, lastSecretsSyncedAt, writtenSecretNames[], workflowSha256, workflowState, workflowPullRequestNumber, workflowPullRequestUrl) plus the run-discovery cursor; created in the same 1792050000000 migration as work_builds. It is a table the epics' own code reads, so it belongs here as well as in data-model.md §2.5, which carries its columns, indexes and R-25 classification | APW-05 | the state a Build's verdict (secretsSyncedAt <= startedAt, buildInputsHash == currentInputsHash) and run discovery both read; platform-written, never user-writable | APW-08, 13 |
Migration blocks: README §7 rule 6 (1792 + epic + slot + 00000); stamping rule: Resolution R-39.
Retention (added 2026-09-17 — resolution R-31's sibling). Each table an App Works epic adds names its retention here and its owner ships the cleanup with the table. Retention is honoured by a scheduled job per owner; nothing is deleted before its window, and anything an operator may need for a dispute is archived to the workspace backup before deletion (R-25).
| Table / data | Owner | Retention |
|---|---|---|
work_builds (one row per run attempt) | APW-05 | 12 months, then the 50 newest per App Work are kept and the rest pruned |
work_app_provisionings evidence | APW-04 | 12 months |
upstream_pull_requests (polled for months) | APW-09 | kept while open; closed rows pruned 12 months after their last change |
apps_tier_state_events (append-only) | APW-10 | 24 months, then archived per quota profile |
apps_tier_usage_windows | APW-10 | 24 months |
Activity rows of the app_* families | all | the existing Activity retention applies unchanged — no epic shortens it |
app_launcher_preferences | APW-11 | kept while the account exists; removed by R-35 |
external_identities | APW-12 | kept while the account exists; removed by R-35 |
Restore semantics (Resolution R-25, extended 2026-09-17). The works domain is restorable
(packages/contracts/src/backup/format.ts:200), so App Works operational state is record-only: a restored App
Work arrives paused, with deploy target none, its Deployments not re-created, and a "secrets need re-entry"
notice — a restore never re-triggers a deletion, a deploy or a Build. Generated secrets are not restored (APW-07
D9: generated once, never rotated), so any value encrypted with them is reported as needing re-entry rather than
silently unreadable. Deletion requests, in-flight provisionings, deploy locks and upstream-PR polling state are
restored as inert records. Each R-25 task extends its collector spec with the paused-arrival and no-job-trigger
assertions.
2A. Shared types, services and events (added by APW-03)
| Name | Owner | Consumers |
|---|---|---|
packages/contracts/src/apps/: AppSpec types, APP_SPEC_ISSUE_CODES (append-only), AppSpecIssue, AppSpecValidationStatus, LicenseClass, HostingEligibility, ManagedHostingReason (licenseNotGreen · upstreamAgreementMissing · entryDisallows · blueprintNotVerified · managedTierDisabled), BlueprintMatchSource (includes explicit), AppsCatalogEntry, WorkAppSpecStateDto, apps-limits.ts | APW-03 | all |
AppSpecService.getEffectiveSpec(workId, commitSha?) (returns invalid for a commit whose spec has errors), validateDraft(workId, text), initialize(workId, branch) | APW-03 | APW-01, 04, 05, 06, 07, 08 |
In-process events AppSpecAppliedEvent (app.spec.applied: { workId, commitSha, previousCommitSha, specHash, addedDependencies, changedEnvNames, changedBlocks }) and AppLicenseChangedEvent (app.license.changed) | APW-03 | APW-05, 06, 07, 08 |
AppLicenseService.getHostingEligibility(workId), previewUpstream(workId, owner, repo, sha), recordEvidence(workId, commitSha, findings); pure scanLicenseHeaders(files) | APW-03 | APW-02, 04, 05, 06, 10 |
diffGuardedSpecBlocks(before, after) over source, blueprint, license, display.protectedPaths, agents.requireHumanMergePaths, upstreamPullRequests, provisioning — removals from the two protected-path lists are reported, additions are allowed (APW-08, §1 "Additions (APW-08)"); isProtectedPath(spec, path) | APW-03 | APW-04, 08 |
AppSourceCatalogAdapter — APW-03's binding of APW-01's APP_SOURCE_CATALOG_PORT | APW-03 | APW-01 |
APP_DEPENDENCY_OUTPUTS — output names per dependency kind that from: deps.<kind>.<output> may reference (proposed list: APW-03 schema.md §11) | APW-07 | APW-03, 06 |
Activity actionType values app_spec, app_blueprint, app_license (the dotted event name of §6 is stored in action) | APW-03 | — |
Activity actionType value app_build (added by APW-05); app_env, app_dependency (added by APW-07) — dotted §6 names in action, names/ids only | APW-05 / APW-07 | — |
Activity actionType app_provision (added by APW-04) — dotted §6 names in action | APW-04 | — |
Activity actionType app_tier (APW-10), app_launcher (APW-11) — dotted §6 names in action | APW-10 / APW-11 | — |
packages/contracts/src/apps/builds.ts (added by APW-05: build statuses, triggers, failure classes, blocked and not-deployable reasons, limits); app-env.ts, app-dependencies.ts (incl. APP_DEPENDENCY_OUTPUTS) and pure tenant-postgres-ddl.ts (added by APW-07) | APW-05 / APW-07 | APW-04, 06, 10 |
Added by APW-08: packages/contracts/src/apps/task-delivery.types.ts (TASK_DELIVERY_STATES, TaskDeliveryView, TaskCostView, App rule limits, MISSION_OUTPUT_MODES); TaskCheckResult.status gains 'not-admitted'; AgentEscalationReasonCode gains 'delivery-failed'; GoalLoopReasonCode gains 'awaiting-merge', 'work-unavailable'; Activity actionType app_change (dotted event in action); TaskDeliveryService.isDeliveryTracked / recordMerge / reconcile; APP_WORK_AGENT_RESOLVER in packages/agent/src/app-works/app-work-agent-resolver.ts (consumer APW-02) | APW-08 | APW-02, 09, 11, 13 |
Added by APW-09: packages/contracts/src/apps/upstream-pull-request.types.ts (UPSTREAM_PULL_REQUEST_STATES, refusal codes, limits, polling, disclosure line); Activity actionType app_upstream_pr; AgentActionProposalActionType gains 'upstream_pull_request' (cross-scope by action type, author-only decision, excluded by approve-all via requiresIndividualDecision, mapped to publish.external in the AW-24 PROPOSAL_ACTION_CATEGORY); AgentActionProposalDecidedVia gains 'expired'; GitFacadeService.getMemberAccountToken (member OAuth/PAT only — never platform PAT or installation token) | APW-09 | APW-08 |
One deploy-target constant (added 2026-09-17). APP_SOURCE_DEPLOY_TARGET_CHOICES in packages/contracts/src/apps/app-source.ts (APW-01) is the single definition of ['none', 'your-cluster', 'ever-works-apps']. APW-06's app-runtime.ts imports and re-exports it as APP_DEPLOY_TARGETS rather than declaring a second array, AppWorkDeletionOutcome.target uses the shared type, and no third literal of the three values is written anywhere (README §7 rule 2, R-1). | APW-01 | APW-06, 07, 10, 13 |
One repository-size limits source (added 2026-09-17). packages/contracts/src/apps/apps-limits.ts (APW-03, §2A) gains the per-stage repository limits: private copy ≤ 500 MB with no LFS (FR-20), provisioning checkout ≤ 3 GiB (APW-04 FR-15), build checkout and Fleet/sandbox checkout limits, and whether LFS is fetched. POST /api/works/app-source/inspect reports which stage would refuse a repository first, and every epic reads the same file instead of restating a number. | APW-03 | APW-01, 04, 05, 08 |
3. Capability interfaces (Constitution I–II)
| Interface / change | Owner | Consumers |
|---|---|---|
IGitProviderPlugin additions: getRepository → source, allowForking, archived, visibility; findExistingFork?; syncForkBranch?; getForkDivergence?; createRepositoryCopy? (private copy); createWebhook? / deleteWebhook? (APW-05 is the only caller: the app-build-prepare job installs the signed workflow_run receiver on the Work Repository and App Work deletion removes it — APW-02 owns the method, APW-05 owns the call); setActionsPermissions? | APW-02 | APW-01, 05, 09 |
Added by APW-02 (P0): GitCloneOptions.checkoutKey? + expectExisting? (→ RepositoryNotReadyError), IGitOperations.getLocalDir/removeLocalDir(owner, repo, checkoutKey?) (checkout key convention work:<workId>:<role>), ForkRepositoryOptions.waitForReady? + GitRepository.forkReadiness?. (P1): GitRepository also stars?, sizeKb?, licenseSpdx?, empty?, movedFrom?; typed GitProviderRequestError (reason: not_found · unauthorized · rate_limited · secondary_rate_limited · sso_authorization_required · oauth_app_restricted · permission_missing · conflict · unprocessable); setActionsPermissions? input { enabled?, disableWorkflowsExcept?, enableWorkflows?, skipWorkflowIds?, maxWorkflows? }. APW-02 P1 (Wave 1) also implements APW-09's createBranchFromSha? / updateBranchRef? with APW-09's signatures unchanged — whichever PR lands first creates them; APW-09 keeps their semantics | APW-02 | APW-01, 05, 09 |
APP_SOURCE_CATALOG_PORT (matchBlueprint({ owner, repo, blueprintId? }), classifyLicense(spdx)) in packages/agent/src/app-works/app-source-catalog.port.ts — added by APW-01, bound by APW-03's AppSourceCatalogAdapter | APW-01 | APW-03 |
APP_SOURCE_CATALOG_PORT licence preview (added 2026-09-25): license.spdx is 'NOASSERTION' when the provider reported a licence it cannot name (plugin contract GitRepository.licenseSpdx: null) and null only when no licence was reported (detectedLicenseSpdx); NOASSERTION classifies red — a fixed platform rule no registry row, alias or exception can change (APW-03 catalog.md §4). The adapter is bound in the packages/agent AppWorksModule and holds a Blueprint match behind the apply gate until APP_BLUEPRINT_APPLY_SERVICE (APW-03 T28) is bound there | APW-01 | APW-03 |
App Works telemetry (added by APW-01 T36, 2026-09-25) in packages/agent/src/app-works/app-works-telemetry.service.ts: APP_WORKS_TELEMETRY_SINK (token; bound only by apps/api's AppWorksTelemetryBindingModule, a @Global() useExisting alias to PostHog's AnalyticsService — packages/agent never binds it, so worker and CLI graphs count and drop), AppWorksTelemetrySink, AppWorksTelemetryService (provided and exported by AppWorksModule), APP_WORKS_TELEMETRY_EVENTS (the five events of APW-01 plan §9.1) and AppWorkCreateOutcome (created · already_existed · refused · failed — failed is an unexpected fault, reason unexpected or http_<5xx>) | APW-01 | APW-05 |
APP_FORK_READY_HANDLER (onDataRepositoryReady({ workId }) → initialized · unchanged · blueprint_requested · waiting_for_setup_pr · failed; AppForkReadyOutcome also carries setupPullRequestNumber?) in packages/agent/src/app-works/app-fork-ready-handler.port.ts — added by APW-02, bound by APW-01's AppSourceInitializerService | APW-02 | APW-01 |
APP_WORK_DELETION_PORT.requestDeletion({ workId, userId, deleteStoredData }) → { status: 'pending' | 'done', target, reason? } in packages/agent/src/app-works/app-work-deletion.port.ts, and the completion WorkLifecycleService.completeAppWorkDeletion(workId) — added by APW-01; bound by APW-06's AppRuntimeDeletionService, which calls APW-07 (dependencies) and APW-10 (removeWork on Ever Works Apps) and then the completion; done ⇒ APW-01 deletes the row now, pending ⇒ it keeps the row until the completion (R-15) | APW-01 | APW-06, 07, 10 |
APP_WORK_AGENT_RESOLVER / AppWorkAgentResolver.resolve({ userId, workId }) → { agentId, source: 'recent-task' | 'pinned' | 'assigned' } | null in packages/agent/src/app-works/app-work-agent-resolver.ts (APW-08 FR-42 rule; R-21) — APW-02's conflict Task injects this token and keeps no Agent lookup of its own | APW-08 | APW-02 |
IGitProviderPlugin.createPullRequest cross-repository head (headOwner, headRepo, maintainerCanModify) and GitPullRequest.headRepoFullName | APW-09 | APW-08 |
New capability build — IBuildPlugin (prepareRepository, startBuild, getBuild, cancelBuild, getLogsUrl); category build in everworks.plugin | APW-05 | APW-04, 06, 08 |
New capability app-dependency — IAppDependencyProvider (supports(kind, target), provision, getOutputs, deprovision, backupStatus); AppDependencyContext.ephemeral; deprovision option stopWorkloads | APW-07 | APW-06 |
IDeploymentPlugin App additions: supportsApps, deployApp(renderInput), getAppStatus, runAppJob, destroyApp (deletes volumes only with deleteVolumes: true — R-15; never deletes dependencies); destroyApp keeps ew-default-deny while kept data remains and deletes a verification namespace whole | APW-06 | APW-04, 08, 13 |
IDeploymentPlugin App additions (added by APW-06, all optional): scaleApp, getAppLogs, checkAppCluster; shared types in packages/plugin/src/contracts/capabilities/app-deployment.types.ts (AppRenderInput, AppDeployHooks, AppDeployResult, AppStatusSnapshot, …) | APW-06 | APW-04, 08, 13 |
App runtime ports in packages/agent/src/app-runtime/ports.ts — interfaces owned by APW-06, implemented by the named epic: AppsTierPolicy / APPS_TIER_POLICY (semantics owned and bound by APW-10: isOpen(), managedScope(), eligibility(userId), control-namespace credential — R-5), AppImagePullCredentialSource / APP_IMAGE_PULL_CREDENTIAL_SOURCE (APW-05), AppRuntimeEnvSource / APP_RUNTIME_ENV_SOURCE (APW-07; context carries domains.primary.*, build.commitSha, components.<name>.internalUrl); AppRuntimeEnvSource.resolveEphemeral(workId, specCommitSha, { target: 'cluster' | 'runner', … }) | APW-06 | APW-05, 07, 10 |
IBuildPlugin.startBuild option verification (requested by APW-04): after the image builds, start its pre-deploy/first-deploy jobs, components and throwaway dependency containers on the runner's private network (≤ 30 min, ≤ 12 GiB summed memory), run the App spec smoke tests, and report { jobs[], componentsReady, smoke[] } through getBuild; uses only in-memory generated/derived env values | APW-05 | APW-04 |
AppRenderInput.purpose: 'verification' (requested by APW-04): per-attempt namespace with ttlMinutes, no Ingress/DNS/custom domains, dependencies without persistent volumes, in-namespace smoke runner; destroyApp removes the whole verification namespace; runs through app-cluster-op on the isolated worker | APW-06 | APW-04 |
AppRuntimeEnvSource ephemeral mode (requested by APW-04): resolve generated + derived values in memory for a verification target without writing WorkAppEnvValue; prompted values only if already set | APW-07 | APW-04, 05, 06 |
IPipelinePlugin.enforcesRuntimeNetworking?: boolean (added by APW-04) — true only for pipelines that enforce runtimeEnvironment.networkingMode = 'limited' as a sandbox policy | APW-04 | APW-08 |
IPipelinePlugin.runSandboxSession?(input: SandboxSessionInput, signal?: AbortSignal) → SandboxSessionResult (added by APW-04 T1, plan §2.6) — the session entry point for a caller that is NOT a Work generation (the App Provisioner). It opens exactly ONE restricted session on an ephemeral agent + environment (so a per-run limited policy can never be written onto a persistent control plane), takes the Skill body as the session's system and the already-fenced brief as its single message, mounts at most one token-free github_repository resource and nothing else, and never pauses for a custom tool: a provider requires_action comes back as failed + requiresAction. Only a plugin that ALSO declares enforcesRuntimeNetworking may implement it — the flag without the method is not a provisioning runtime (plan §7.3, which is why readiness isolatedRuntime requires both). Implemented by packages/plugins/claude-managed-agent only. | APW-04 | APW-04 (T3, T48) |
APP_PROVISION_EVENTS_PORT in packages/agent/src/app-provisioning/app-provision-events.port.ts (added by APW-04): forkReady(workId), buildUpdated(buildId), targetUpdated(workId, namespace), smokeFailedAfterUpstreamSync(workId, fromSha, toSha); and AppProvisioningService.start({ workId, trigger: 'auto-create' }) called by APW-01 when no Blueprint and no valid App spec | APW-04 | APW-01, 02, 05, 06 |
Identity provider plugin (identity-provider capability) for Ever ID relying-party sign-in | APW-12 | APW-11 |
IGitProviderPlugin additions (added by APW-08, optional): GitPullRequestStatus.mergeCommitSha?; isAncestorCommit?(owner, repo, ancestorSha, descendantSha, token) → boolean | null | APW-08 | APW-09 |
Build workflow checks job (requested by APW-08) (R-9): on same-repository pull requests and on the tracked branch, one checks matrix job — one leg and check run named Ever Works check: {name} per App spec check (≤ 20, fail-fast: false, max-parallel: 5); permissions: contents: read; no secrets, EW_ variables or cache; commands passed via env (base64-encoded); job-level continue-on-error for required: false; written for every build strategy; never changes a Build's status | APW-05 | APW-08 |
IGitProviderPlugin additions (added by APW-09, optional): listPullRequestReviews?, listPullRequestReviewComments?, getInteractionLimit? (none · existing_users · contributors_only · collaborators_only), createBranchFromSha?, updateBranchRef? (fast-forward only); WorkspaceFinalizeOptions.squashOnto?: string; plus (2026-09-17 audit) ListPullRequestsOptions.head? (owner:branch), GitDiffResult.totalCommits? (compare's total_commits — what notSingleCommit is derived from), a typed GitPullRequestReview (5 states) with the review-summary rule, and WorkspaceFinalizeOptions.reportFiles?: string[] + WorkspaceFinalizeResult.reports?: { path, content }[] (the preparation report, read at finalize and dropped before the squash commit) | APW-09 | APW-08 |
IGitProviderPlugin additions (added by APW-03, all optional): GitRepository.topics?; getRepositoryTree?(owner, repo, ref, token, { recursive, maxEntries }) → { entries, truncated }; commitFiles?(owner, repo, { branch, baseSha, message, files }, token) → { commitSha } (non-force, nonFastForward error code); getFileWebUrl?(owner, repo, ref, path) → { url, lineAnchor? } | APW-03 | APW-01, 04, 05 |
Landed early (recorded 2026-09-17). The interface half of APW-09 T3 is already in the tree: packages/plugin/src/contracts/capabilities/git-provider.interface.ts:935 declares createBranchFromSha?(owner, repo, name, sha, token) and :948 declares updateBranchRef?(owner, repo, name, sha, { force: false }, token), both optional, both returning Promise<GitBranch> (chosen to match the sibling createBranch? at :635; neither APW-09 plan §3.3 nor this row fixed a return type before). APW-02 T9/T10 landed them first because its fork work needs them. APW-09 T3 therefore implements, facades and tests them and must not re-declare them (its own text already anticipates the skip); APW-02 P1 keeps whatever plugin implementation it shipped. If APW-09 wants a different return type it changes here and in both epics in one PR, never in one of them. No method, signature or default is withdrawn by this note. | APW-02 (interface) / APW-09 (implementation + facade) | APW-02, 08 |
Two measured facts about the APW-02 P0 interface (recorded 2026-09-17). (1) packages/plugins/gitlab does not exist in this repository — the plugin list under packages/plugins/ has no gitlab entry — so APW-02 plan §3.3's "every existing git-provider implementation compiles unchanged" resolves to the github plugin (packages/plugins/github) plus the agent package (packages/agent) that consumes the facade, and to no third implementation. The sentence stays true as written; it just covers two packages, not the three its wording implies. (2) GitProviderRequestError (packages/plugin/src/contracts/capabilities/git-provider.app-forks.ts:85) is constructed as super(reason) and deliberately does not set this.name, so error.name === 'Error' while error.message === reason (the class doc says exactly that at :79). Code that branches on error.name will not recognise it — branch on instanceof or on reason — which is the opposite of RepositoryNotReadyError in packages/plugin/src/git/git-operations.ts, whose name is set. Recorded rather than "fixed": changing either would be a behaviour change in another epic's landed code. | APW-02 | APW-09, 08 |
Ever ID delegated-read seam (added by APW-12): IdentityProviderFacadeService.verifyAccessToken (audience default ever-works, lifetime ≤ 3,600 s); route metadata @DelegatedRead(scope) in apps/api/src/auth/decorators/delegated-read.decorator.ts, admitted by AuthSessionGuard only on decorated handlers (request.user.authMethod = 'ever-id-delegated', request.everIdDelegation); 403 insufficientScope on a decorated handler without the scope; scopes apps:read and ever-works:session in packages/contracts/src/apps/ever-id.ts | APW-12 | APW-11 |
New capability apps-tier (added by APW-10) — IAppsTierProvider (zoneInfo, applyWork, getWork, setDesiredState, setEgressThrottle, removeWork(workId, { deleteData }), getAppLogs(workId, opts) (required in P2 — APW-06 FR-7/FR-48 route app logs through the tier), startSelfCheck, getSelfCheck, reviewCredentialScope, getHeartbeat, listUsageReports/acknowledgeUsageReports, listAbuseSignals/acknowledgeAbuseSignals, P3 submitBuild?/getBuild?); first plugin ever-works-apps, which also implements APW-06's IDeploymentPlugin App additions for target Ever Works Apps by writing desired state (never server-side applying workloads) and whose AppsTierPolicy implementation returns only a control-namespace credential. Effective enablement = EVER_WORKS_APPS_MANAGED_ENABLED (ceiling) and tier state open; consumers call AppsTierPolicy.isOpen() / managedScope() (the older name isManagedEnabled survives only as APW-10's alias of isOpen()), never the env var | APW-10 | APW-03, 05, 06, 07 |
New capability edge-hostnames (added by APW-10) — IEdgeHostnameProvider (createCustomHostname, getCustomHostname, deleteCustomHostname); custom domains on Ever Works Apps route only when status and certificateStatus are active | APW-10 | APW-06 |
Kubernetes API group hosting.ever.works/v1alpha1 (added by APW-10), namespaced in the zone's control namespace: Work, SelfCheck, UsageReport, AbuseSignal, AppBuild (P3; status scanSummary, signatureState, blockedEgressHosts) — schemas in packages/hosting-crds/src/crds/; Work.spec.desiredState: removed, Work.spec.dataDeletion, status.removal (R-15) | APW-10 | APW-05, 06, 07 |
IBuildPlugin details (added by APW-05): buildKind, supportedStrategies; prepareRepository(input, auth, writer) where writer is bound by BuildFacadeService to GitFacadeService (incl. APW-03's commitFiles?, never a clone); startBuild({ ref, sha, mode: 'build' | 'verify', reuseImageDigest?, verification? }); getBuild(ref, auth, redact) with redact from APW-07; optional checkImageAccess?. APW-05 binds APP_IMAGE_PULL_CREDENTIAL_SOURCE to a per-App-Work read-only pull token only | APW-05 | APW-04, 06 |
IBuildPlugin details, digest confirmation (added 2026-09-25, APW-05 T14): getBuild snapshots report the artifact digest as image.confirmed: false (the github-actions-build plugin never confirms its own input); AppBuildsService.finalize confirms it through the binding's checkImageAccess for the platform-derived ghcr.io/<owner>/<repo>/ever-works-app and tag sha-<commitSha>, bounded by APP_BUILD_DIGEST_READ_TIMEOUT_MS (15 s; a timeout or throw leaves it unconfirmed). Equal → confirmed and imageRepository pinned to the derived value; unequal → failureClass: digestMismatch, cleared again if a later read confirms. A later observation of the same digest never undoes a platform confirmation, and a manual/verification Build keeps the commit it was dispatched at (the run's head_sha is the branch head) | APW-05 | APW-04, 06 |
IBuildPlugin details, additive fields (2026-09-25): BuildSnapshot.image.pushLogDigest?: string (5a75913bc) is the one digest every Push-step section of the build job log names for sha-<sha> (plan §4.8's no-token fallback; any disagreement is a refusal); it is weighed only when the registry answers readable: false. AppBuildsService.reconfirmDigest(buildId, { pushLogDigest? }) re-settles a digestUnconfirmed Build without publishing an event (§7.4 recheck; no caller bound yet). PrepareRepositoryInput.workflowPullRequestNumber? / workflowPullRequestUrl? and RepositoryWriteErrorCode pullRequestExists (e74f6e045): the recorded workflow pull request is echoed back when createPullRequest is refused because the head already has an open one (ACC-05-02; APW-05 plan §4.1) | APW-05 | APW-04, 06 |
Judge-before-push workspace seam (added 2026-09-25, 5a75913bc; implemented by sandbox-workspace and local-workspace in 86e1a3ddf): IWorkspacePlugin.branchChanges?(handle, { headSha, readPaths? }) → WorkspaceBranchChanges { paths, contents } — the merge-base diff paths (--no-renames, both sides of a rename) and the blobs at headSha, read from git and never from disk — and WorkspaceFinalizeOptions.publishSha?, which publishes exactly that already-committed commit and stages nothing; IWorkspaceFacade.branchChanges refuses a provider without it, so no caller pushes unjudged. Implementations must read objects literally (no replace refs, grafts or commit-graph) and must not honour submodule ignore settings, since git push ignores them all. File: packages/plugin/src/contracts/capabilities/workspace.interface.ts. Caller: APW-08's cloud finalizeRun (APW-08 plan §2.5) | APW-08 | workspace plugins |
APP_WORK_CHANGE_GATE / AppWorkChangeGate (evaluate, checkPaths) in packages/agent/src/tasks-domain/app-work-change-gate.port.ts, bound by TasksDomainModule to AppWorkChangeGateService (APW-08 T17). The port exports APP_WORK_SPEC_PATH ('.works/works.yml'; the guard's APP_SPEC_PATH is that constant); AppWorkChangePathsInput.taskLabels?: readonly string[] (added 2026-09-25) gives the pre-push verdict evaluate's app-provision parity; checkPaths has a second caller, the cloud finalizeRun before the push | APW-08 | APW-04 |
IAppDependencyProvider details (added by APW-07): category app-dependency; a plugin declares dependencyProviders[], and "provider plugin id" is stored as providerPluginId + providerId; supports(kind, target, ctx). Provider ids P1: k8s-inline-postgres, k8s-inline-redis, k8s-inline-minio (in the k8s plugin), smtp-external, s3-external, platform-smtp-relay (app-dependencies-external); P2: managed-postgres, managed-redis, managed-object-storage (apps-tier-dependencies). APW-07 binds APP_RUNTIME_ENV_SOURCE (incl. ephemeral mode); cluster I/O runs on APW-06's app-cluster-io queue | APW-07 | APW-05, 06 |
Work.status.dependencies[] { kind, ref, phase, lastBackupAt } and in-zone substitution of ew-dep://<kind>/<output> tokens inside the sealed env, rejecting unknown tokens; tenant Postgres DDL from packages/contracts/src/apps/tenant-postgres-ddl.ts (requested by APW-07) | APW-10 | APW-07 |
In-zone dependency ownership (audit fix 2026-09-17, APW10-G01). IAppsTierProvider gains setDependencies(workId, deps) and releaseDependencies(workId, { deleteData }), and APW-07 writes dependency state through those methods instead of through applyWork (which replaces the whole desired state): the reconciler creates the tenant database from buildTenantPostgresDdl, a per-Work cache and a bucket with its own user, and writes status.dependencies[] with phase: pending | ready | failed | released and lastBackupAt; the seal step substitutes ew-dep:// tokens after unsealing and refuses an unknown token with DEPENDENCY_TOKEN_UNKNOWN. phase: released is what a kept-data removal reports, and it is the state APW-10's removal condition waits for. LG-14's probe extends beyond reachability with CONNECTION_LIMIT_NOT_ENFORCED and STATEMENT_TIMEOUT_NOT_ENFORCED | APW-10 | APW-06, 07, 13 |
A managed SMTP path for the tier (audit fix 2026-09-17, GAP-22). platform-smtp-relay is supported on target ever-works-apps (as well as your-cluster): it is an allow-listed relay endpoint exempt from the tier's outbound-port rule (LG-08 refuses 25/465/587 — the relay is reached on its own TLS port, not by opening those), reached with a per-App-Work credential and rate-limited per §7A. smtp is therefore a dependency kind Work.spec.dependencies may declare on the managed target, and its ew-dep:// token resolves there. Without this, Cal.diy and the fixture's managed profiles could never reach ready | APW-07 | APW-10, 13 |
Workflow changes are inspected before a fast-forward (audit fix 2026-09-17, XC-02). Both the sync path and the fork-readiness path diff the upstream range for .github/workflows/** before they fast-forward the tracked branch or create/update a sync pull request: when the range changes a workflow file the platform never fast-forwards (it takes the pull-request path or holds, with the reason on the card), because a workflow added upstream would otherwise run on the fork with repository secrets before Actions hygiene ever sees it. In addition, APW-05's EW_ build values are scoped so only the platform-written build workflow on the tracked branch can read them (a repository-level secret any workflow can read is not sufficient), and ACC-NEG-05's canary assertion covers an upstream commit that adds an on: push workflow which echoes secrets | APW-02 | APW-05, 13 |
One tool policy for runs over third-party code (audit fix 2026-09-17, XC-05). APP_WORK_TASK_TOOL_POLICY with APP_WORK_TASK_DENIED_TOOL_GROUPS and APP_WORK_TASK_ALWAYS_ALLOWED_TOOLS in packages/agent/src/app-works/app-work-task-tool-policy.ts — owned by APW-08, and the only such list: it denies outbound messaging, web fetch/search, MCP, sub-agents and every App Works mutation tool, allows ask_human and the read-only tools, is applied where allowed tools are resolved and re-checked at dispatch (so a repository's own instruction file cannot widen it). APW-09 consumes it for upstream-PR preparation runs and must not declare a second list. Related shared types, also APW-08's: AppWorkRunContainment (execution path, isolatedHome, downgrades, needsOwnerAllowance), APP_CHECK_MISSING_TOOL_EXIT_CODES = [127, 9009], APP_PR_FILES_GUIDANCE_DEFAULT = 50 | APW-08 | APW-09 |
Two App Launcher ports (audit fix 2026-09-17, APW11-G01/APW11-G09). MANAGED_HOST_ROOT_RESOLVER / ManagedHostRootResolver.resolve(work: Pick<Work, 'id' | 'kind'>) → string | null in packages/agent/src/app-launcher/managed-host-root.resolver.ts, added by APW-11 and injected @Optional(): the unbound default returns the platform managed root for every kind except app, where it returns EVER_WORKS_APPS_DOMAIN when set, else EVER_WORKS_DOMAIN, and null when neither is configured (R-16, D10 — no host is ever invented). APW-06 T48 binds AppManagedHostRootResolver in P1 and its value wins. APP_PUBLISHED_HOSTS / AppPublishedHostsPort.primary(workId) → string | null is consumed by APW-11 and bound by APW-06 from AppHostsService (the owner's primary custom domain, else <managedSubdomain>.<apps-domain>, else null); APW-11 prefers it for kind app and falls back to its own order when unbound | APW-11 | APW-06 |
4. HTTP API
| Route | Owner |
|---|---|
POST /api/works with kind: 'app', repositoryUrl, repositoryMode: link | fork | private-copy, targetOwner | APW-01 |
POST /api/works/app-source/inspect — ownership, push access, fork possibility/existing fork, Blueprint match, license preview (no side effects) | APW-01 |
GET /api/works/:id/upstream, POST /api/works/:id/upstream/sync (202) | APW-02 |
Added by APW-01: optional blueprintId on POST /api/works (kind app) and on POST /api/works/app-source/inspect — the supported path for repositories no entry lists (APW-03 FR-81); delete_stored_data (default false) on the existing Work delete route POST /api/works/:id/delete for kind app (R-15) | APW-01 |
Added by APW-02: POST /api/works/:id/upstream/readiness/retry (202 — the Try again of a timed-out or failed preparing App Work); web route /works/:id/upstream (Upstream tab, shared with APW-09) | APW-02 |
GET /api/apps-catalog, GET /api/apps-catalog/:id (public, cached) | APW-03 |
GET /api/works/:id/app-spec, POST /api/works/:id/app-spec/validate | APW-03 |
Added by APW-03: GET /api/apps-catalog/licenses and GET /api/schema/app-spec.schema.json (public); POST /api/works/:id/app-spec/blueprint (202), POST /api/works/:id/app-spec/blueprint/upgrade (202), POST /api/works/:id/app-spec/blueprint/dismiss; POST /api/works/:id/app-license/attest (Work owner) | APW-03 |
POST /api/works/:id/provision (202) — start or re-run the App Provisioner | APW-04 |
Added by APW-04: GET /api/works/:id/provisioning (active + 10 recent + readiness), POST /api/works/:id/provision/cancel (202), POST /api/works/:id/provisioning/:provisioningId/blueprint-suggestion (202), GET /api/admin/app-blueprint-suggestions + GET /api/admin/app-blueprint-suggestions/:provisioningId/bundle (platform admin) | APW-04 |
GET /api/works/:id/builds, GET /api/works/:id/builds/:buildId, POST /api/works/:id/builds (202) | APW-05 |
Added by APW-05: POST /api/works/:id/builds/:buildId/cancel (202); build settings and the pull token use the existing PATCH /api/works/:workId/plugins/:pluginId/settings with the plugin id returned by GET /api/works/:id/builds | APW-05 |
POST /api/deploy/works/:id (the existing deploy route — deploy.controller.ts:79 @Controller('api/deploy') + :226 @Post('/works/:id'); APW-06 extends it with a kind-app branch), GET /api/works/:id/app-status, POST /api/works/:id/app-jobs/:name/run (202) | APW-06 |
Added by APW-06: POST /api/works/:id/app-status/refresh (202), POST /api/works/:id/app-smoke (202), POST /api/works/:id/app-rollback (202), POST /api/works/:id/app-lifecycle (202; pause | resume | remove | cancel-deploy), POST /api/works/:id/app-logs (202) + GET /api/works/:id/app-logs/:requestId, GET | PUT /api/works/:id/app-target, POST /api/works/:id/app-target/check (202), GET /api/works/:id/app-deletion-preview (R-15). Note: the pre-existing deploy route is POST /api/deploy/works/:id; it and POST /api/deploy/works/:id/rollback, /api/deploy/works/:id/domains*, /api/deploy/works/:id/subdomain gain a kind-app branch that delegates to the routes above | APW-06 |
GET /api/works/:id/app-env (names, origins, set/unset — never values), PUT /api/works/:id/app-env, POST /api/works/:id/app-env/:name/rotate | APW-07 |
GET /api/works/:id/app-dependencies | APW-07 |
Added by APW-07: PUT /api/works/:id/app-dependencies/:kind (provider choice + write-only prompted config, 202), POST /api/works/:id/app-dependencies/:kind/provision (202), DELETE /api/works/:id/app-dependencies/:kind (body { confirmSlug }, 202) | APW-07 |
Added by APW-08: POST /api/works/:id/evolve (202 — start a change on an App Work); GET /api/tasks/:id/delivery, POST /api/tasks/:id/delivery/close, POST /api/tasks/:id/delivery/follow-up (202, body { escalationId } — the escalation row is a one-shot token, and every refusal answers 422 { code: 'followUpLimit' }, one status code rather than a set), GET /api/tasks/:id/cost; deliveryState / includeDelivery on GET /api/tasks; workId on POST/PATCH /api/me/goals; outputMode, taskOutput on PATCH /api/me/missions/:id; templateInputs on POST /api/me/missions | APW-08 |
Added by APW-08 (2026-09-17): POST /api/works/:id/app-runs/allow-containment (204 — the owner allows one Fleet node's containment downgrade once, R-33), GET /api/works/:id/cost (WorkCostRollup) | APW-08 |
POST /api/works/:id/upstream-pull-requests (proposal → approval), GET /api/works/:id/upstream-pull-requests | APW-09 |
Added by APW-09: GET /api/works/:id/upstream-pull-requests/eligibility?taskId, GET …/:prId, POST …/:prId/signed (202), POST …/:prId/withdraw, POST …/:prId/check, POST …/:prId/address-review (202), POST …/suggestions/:taskId/dismiss | APW-09 |
GET /api/me/apps, PUT /api/me/apps/preferences; delegated read with scope apps:read | APW-11 |
Added by APW-11: GET /api/app-launcher/platforms (public, Cache-Control: max-age=3600); optional field appLauncherExposed on existing PUT/PATCH /api/works/:id | APW-11 |
Added by APW-10: operator routes api/admin/apps-tier/* (platform admin, 404 otherwise); GET /api/me/apps-tier (eligibility + tier open/scope); GET /api/works/:id/apps-tier (quarantine, profile, usage) | APW-10 |
Ever ID relying-party routes (/api/auth/ever-id/*) | APW-12 |
Added by APW-06 (2026-09-17): PUT /api/works/:id/app-target and POST /api/works/:id/app-target/kubeconfig — the only App Work path that stores a kubeconfig. It stores it as the Work-scoped x-secret and never calls the plugin connection validator: the App path must not use PATCH /api/works/:workId/plugins/:pluginId/settings, whose save immediately dials the pasted cluster from the API process (apps/api/src/plugins/plugins.controller.ts → pluginValidationService.tryValidateConnection → the k8s plugin's validateConnection, i.e. before any App guard, before assertSupportedKubeconfig and outside the isolated worker). The new route runs only the pure structural checks in the API (no DNS, no dialing) and dispatches the connection check to the worker as app-cluster-op, which is what spec FR-3/FR-4/FR-5 and ACC-06-02…04 require. | APW-06 |
Added by APW-06 (2026-09-17): AppRuntimeTargetResolver.ensureNamespace(workId) — target-save/connection-check preparation that creates and fixes the namespace, ew-default-deny and ew-allow-same-namespace before any dependency is provisioned, exposed as the app-cluster-op op prepare-namespace. Dependency providers check for ew-allow-same-namespace (not ew-allow-deps, which is egress), so the first Deployment can no longer deadlock against its dependencies. | APW-06 |
Added by APW-08 (2026-09-17): APP_WORKS_ACCOUNT_DELETION — the account/organization deletion cascade of R-35. It is a handler on the existing UserAccountDeletionEvent, not a route; it runs R-15 with data deleted for every owned App Work, removes platform-written EW_ secrets and webhooks, stops upstream-PR tracking, and deletes external_identities. | APW-01 |
Every route row carries four further columns, and an owner that adds a route states them in the same PR (added 2026-09-17 — resolutions R-31, R-32 and the parity rule):
- Minimum role —
owner,manager,editor,viewerorplatform admin, against the existingWorkMemberRole, with platform-admin routes answering404rather than403for everyone else. A route may require a higher role than its neighbours but never silently: the epic's FR says which role, and403is reserved for a member who lacks it (R-36 covers the not-a-member case). - Human-only —
yeswhen the action spends money, deletes data, publishes outside the platform, changes a security posture or accepts a legal obligation (R-32), enforced with@HumanOnly();nootherwise. This is the list the MCP whitelist and the chat tool registry read, so a human-only route is never whitelisted. - Surface parity —
OpenAPI(with@ApiOperationand DTOs), the MCP tool name, the CLI command, the chat tool name, and/ornot exposedwith a reason. MCP tool schemas are generated from OpenAPI operations through the existing whitelist, so a route without@ApiOperationis invisible there. - Throttle — the
@Throttlevalues the controller declares, plus the §7A cap that applies.
A route reachable from a surface without columns 1–2 is a defect: the parity test each epic adds fails when a whitelisted route is human-only, or when a route that the epic's own FR exposes on chat/MCP/CLI has no row.
The human-only list, spelled out (added 2026-09-17). So the column can be filled in without re-deriving it,
these are the App Works routes whose action falls into one of R-32's five classes and therefore carries
@HumanOnly(); the reasoning per row is in contracts/agent-surfaces.md §6.1.
| Route | Owner | Why human-only |
|---|---|---|
POST /api/works/:id/delete — the delete_stored_data set | APW-01 | deletes data (typed confirm_slug, R-15) |
POST /api/works/:id/app-license/attest | APW-03 | accepts a legal obligation (R-3) |
POST /api/works/:id/provision (202) | APW-04 | spends tokens |
POST /api/works/:id/builds (202) | APW-05 | spends runner minutes |
PUT /api/works/:id/builds/pull-token | APW-05 | writes a registry credential |
POST /api/deploy/works/:id — the managed-target branch | APW-06 | spends compute |
POST /api/works/:id/app-lifecycle — remove + deleteData | APW-06 | deletes data |
PUT /api/works/:id/app-target | APW-06 | changes a security posture |
POST /api/works/:id/app-target/kubeconfig | APW-06 | stores a cluster credential |
PUT /api/works/:id/app-env | APW-07 | writes secrets |
POST /api/works/:id/app-env/:name/rotate | APW-07 | invalidates a live credential |
PUT /api/works/:id/app-dependencies/:kind (202) | APW-07 | writes provider credentials |
POST /api/works/:id/app-dependencies/:kind/provision (202) | APW-07 | provisions paid resources |
DELETE /api/works/:id/app-dependencies/:kind | APW-07 | deletes data |
POST /api/works/:id/app-runs/allow-containment | APW-08 | changes a security posture (R-33) |
POST /api/works/:id/upstream-pull-requests | APW-09 | publishes outside the platform |
POST …/:prId/signed (202) | APW-09 | signs a CLA/DCO on the member's behalf |
POST …/:prId/withdraw | APW-09 | public act on a third-party repository |
POST …/:prId/address-review (202) | APW-09 | publishes outside the platform |
The deploy_work ruling (R-32 against R-26, decided 2026-09-17). The MCP whitelist already contains a
deploy_work entry (apps/mcp/src/openapi-tools/whitelist.ts), and R-26 forbids removing an entry to satisfy a
new rule. The additive reading therefore wins, and it is the binding one: the whitelist entry stays, the
route keeps working for every caller it serves today, and @HumanOnly() is applied to the managed-target
branch — so a non-session caller is refused with 403 exactly where the action would spend the platform's
compute, instead of the entry being deleted. A registry-parity test pins both halves (entry present, managed
branch refuses a machine credential). Any future human-only route follows the same rule: guard the action, never
delete the surface.
Routes named only in epic plans (added 2026-09-17). These are referenced by the epics but are not written
literally in the table above, so a contract fragment cannot be generated from it; each is listed with the plan
that owns it in contracts/README.md §5, and each owner adds its literal row here when
it implements the route: PUT /api/works/:id/builds/pull-token (APW-05), GET /api/deploy/works/:id/deployments
(APW-06), PUT /api/deploy/works/:id/runtime-env (APW-07), POST /api/agents/:id/assign-task (APW-08),
POST /api/agent-approvals/:id/approve / reject (APW-09), POST /api/users/me/scope (APW-11), and the two
APW-08 routes POST /api/works/:id/app-runs/allow-containment and GET /api/works/:id/cost.
Note on the deploy route: the existing route is POST /api/deploy/works/:id (owned by the deploy controller,
with APW-06 adding the kind-app branch) — there is no POST /api/works/:id/deploy, and any epic text that
abbreviates it that way is referring to this one.
5. Background jobs (dispatched through the job-runtime provider)
| Job id | Owner | Trigger |
|---|---|---|
app-fork-readiness | APW-02 | fork requested; Try again, stale re-dispatch, setup PR merged |
app-upstream-sync | APW-02 | Schedule (spec.upstreamSync.schedule) or manual |
app-upstream-sync-dispatcher | APW-02 | schedule */10 * * * * — due syncs (≤ 50/tick), stale readiness re-dispatch (≤ 3), setup pull request checks (≤ 50/tick), daily re-check of unavailable upstreams (added by APW-02; dispatcher symbols APP_FORK_READINESS_DISPATCHER, APP_UPSTREAM_SYNC_DISPATCHER) |
app-license-evaluate | APW-03 | spec applied, upstream synced; also license registry changed, manual re-check, header evidence recorded (APW-03) |
app-spec-evaluate | APW-03 | push or merged PR on the tracked branch, manual re-check, stale head seen on read, App Work created, Blueprint applied (added by APW-03) |
app-blueprint-apply | APW-03 | Blueprint apply or upgrade requested (added by APW-03) |
apps-catalog-refresh | APW-03 | schedule 23 * * * * — catalog + registry refresh, upgrade notices, license re-classification fan-out (added by APW-03) |
app-provision | APW-04 | App Work created without a Blueprint, or manual |
app-provision-sweep | APW-04 | schedule, every 15 minutes — expired verification targets, stale leases, 8 h deadline, question reminder/expiry, queue promotion (added by APW-04) |
app-build-watch | APW-05 | Build started (webhook-driven, polling fallback) |
app-build-prepare | APW-05 | app.spec.applied touching build or build-phase env, build-phase env change, Rebuild, verification request (added by APW-05) |
app-build-sweep | APW-05 | schedule */2 * * * * — Builds silent > 90 s, lost Builds, unconfirmed digests after a pull token is saved (added by APW-05) |
app-deploy | APW-06 | Build succeeded on the deploy branch, or manual |
app-smoke | APW-06 | Deployment reported ready |
app-cluster-op | APW-06 | status refresh, logs, pause/resume/remove, cancel, job run, connection check, ingress/DNS reconcile, App Work deletion, verification targets (added by APW-06) |
app-health-poll | APW-06 | schedule, every minute (added by APW-06) |
app-preview-gc | APW-06 | schedule, every 5 minutes — Wave 3 (added by APW-06) |
app-dependency-provision | APW-07 | spec applied with new dependencies |
upstream-pr-status | APW-09 | open Upstream pull request (polling; upstream sends no webhooks) |
upstream-pr-open | APW-09 | approval of an Upstream pull request decided by its author (added by APW-09) |
upstream-pr-push | APW-09 | approval of a review follow-up push decided by its author (added by APW-09) |
app-change-delivery | APW-08 | schedule 1-59/2 * * * * — follow merged App Work Task changes to Build, Deployment and live; follow-up Tasks (added by APW-08) |
apps-tier-self-check | APW-10 | manual (operator) or schedule 23 */6 * * * (added by APW-10) |
apps-tier-gate-watch | APW-10 | schedule */5 * * * * — tier transitions, attestation notices, quarantine mirroring, signal import (added by APW-10) |
apps-tier-metering-import | APW-10 | schedule 7 * * * * (added by APW-10) |
apps-tier-daily-receipts | APW-10 | schedule 15 0 * * * UTC (added by APW-10) |
6. Activity event types (prefix app.)
| Events | Owner |
|---|---|
app.source.linked, app.source.forked, app.source.copied, app.source.failed | APW-01 |
app.fork.ready, app.fork.timeout, app.actions.disabled, app.upstream.synced, app.upstream.behind, app.upstream.conflict | APW-02 |
Added by APW-02: app.fork.missing (the Work Repository no longer exists), app.upstream.unavailable (upstream deleted or unreadable; sync paused) | APW-02 |
app.spec.validated, app.spec.invalid, app.spec.applied, app.blueprint.matched, app.license.classified, app.license.changed | APW-03 |
Added by APW-03: app.blueprint.applied, app.blueprint.apply_failed, app.blueprint.upgrade_available, app.license.attested, app.license.attestation_required | APW-03 |
app.provision.started, app.provision.proposed, app.provision.needs_input, app.provision.succeeded, app.provision.failed | APW-04 |
Added by APW-04: app.provision.attempted (one per verification attempt: n, verdict, failed step, target kind), app.provision.blueprint_suggested | APW-04 |
app.build.queued, app.build.started, app.build.succeeded, app.build.failed, app.build.cancelled | APW-05 |
app.deploy.started, app.deploy.succeeded, app.deploy.failed, app.job.succeeded, app.job.failed, app.smoke.passed, app.smoke.failed | APW-06 |
Added by APW-06: app.deploy.rolled_back, app.deploy.paused, app.deploy.resumed, app.deploy.removed, app.health.degraded, app.health.recovered, app.health.unreachable | APW-06 |
app.env.changed (names only), app.env.rotated, app.dependency.provisioned, app.dependency.failed | APW-07 |
app.dependency.released (kept, no longer managed), app.dependency.data_deleted (added by APW-07) | APW-07 |
app.change.merged (a merged PR that will rebuild) | APW-08 |
Added by APW-08: app.change.live, app.change.failed, app.change.closed, app.change.follow_up_opened | APW-08 |
Added by APW-08 (2026-09-17): app.mission.nothing_to_file (FR-54's once-per-Mission-per-day entry; actionType app_change) | APW-08 |
Added by APW-09 (2026-09-17): app.upstream_pr.expired (the 72-hour approval TTL) and app.upstream_pr.failed (a preparation that errored or timed out) — without them a row can leave awaiting_approval or preparing with no Activity record | APW-09 |
app.upstream_pr.proposed, app.upstream_pr.approved, app.upstream_pr.opened, app.upstream_pr.merged, app.upstream_pr.closed | APW-09 |
Added by APW-09: app.upstream_pr.refused, app.upstream_pr.needs_signature, app.upstream_pr.changes_requested, app.upstream_pr.updated, app.upstream_pr.withdrawn, app.upstream_pr.suggested | APW-09 |
app.launcher.exposed, app.launcher.hidden | APW-11 |
Added by APW-10: app.tier.quarantined, app.tier.released, app.tier.deploy_refused, app.tier.egress_threshold, app.tier.usage_daily (category only — never operator reasons) | APW-10 |
7. Feature flags and environment variables
| Name | Kind | Default | Owner |
|---|---|---|---|
works-app | flag | off in production until Wave 1 acceptance is green | APW-01 |
EVER_WORKS_APP_WORKS_ENABLED | env | false — API-side twin of works-app (chat and MCP bypass the chip); the web evaluates works-app fail-closed, unlike other works-<kind> flags (added by APW-01) | APW-01 |
EVER_WORKS_APP_FORK_READINESS_TIMEOUT_MS | env | unset — honoured only when NODE_ENV ≠ production, clamped 5 000–900 000 (added by APW-02, FR-18a) | APW-02 |
EVER_WORKS_APPS_CATALOG_REPO | env | ever-works/templates — the listing repository, created 2026-09-17; the earlier drafts called it ever-works/apps, so an installation carrying that value keeps working | APW-03 |
EVER_WORKS_APPS_CATALOG_REF | env | main (pin a SHA/tag in production) | APW-03 |
EVER_WORKS_APPS_CATALOG_TOKEN | env (secret) | unset — optional token for the authenticated catalog fallback read (added by APW-03). Also the Blueprint resolver's fallback credential for ever-works/<id>-template reads (after the GitHub App installation on ever-works, before GITHUB_TOKEN); with none of the three, the create preview answers Blueprint unavailable and licence unknown (2026-09-25) | APW-03 |
EVER_WORKS_APPS_MANAGED_ENABLED | env | false — only APW-10's launch gate may flip it | APW-10 |
EVER_WORKS_APPS_DOMAIN | env | defaults to EVER_WORKS_DOMAIN (ever.works) — so <slug>.ever.works works out of the box and no new PSL apex is on the critical path (owner decision 2026-09-17); set it to a dedicated apex and that apex keeps the full validation + PSL checks (APW-10 LG-15) | APW-06 |
EVER_WORKS_APPS_MAX_PER_USER | env | 3 | APW-06 |
EVER_WORKS_APPS_DNS_ZONE_ID | env | unset — no managed subdomains without it (added by APW-06); on the shared default this is the platform domain's own zone | APW-06 |
EVER_WORKS_APPS_DNS_API_TOKEN | env (secret) | unset (added by APW-06) | APW-06 |
EVER_WORKS_APPS_CLUSTER_WORKER_ISOLATED | env | false — production refuses App Work cluster jobs until the operator declares the isolated worker (added by APW-06) | APW-06 |
EVER_WORKS_APPS_CLUSTER_PRIVATE_ALLOWLIST | env | empty — CIDRs exempt from the public-address rule for self-hosted installations and e2e (added by APW-06) | APW-06 |
works-app-previews | flag | off — Wave 3 (added by APW-06) | APW-06 |
EVER_WORKS_APP_PROVISION_TOKEN_CAP | env | 3000000 — per-provisioning token cap, clamped 500000–10000000 (added by APW-04) | APW-04 |
EVER_WORKS_APP_PROVISION_RUNNER_MINUTE_CAP | env | 240 — per-provisioning runner-minute cap, clamped 60–600 (added by APW-04) | APW-04 |
app-launcher | flag | off | APW-11 |
EVER_WORKS_APP_LAUNCHER_ENABLED | env | false — API master switch; the web also honours flag app-launcher, fail-closed (added by APW-11) | APW-11 |
EVER_WORKS_PLATFORM_CATALOG_REPO / _REF / _ENV / _SELF_ID | env | ever-works/platforms / main (pin in production) / production / ever-works (added by APW-11) | APW-11 |
EVER_WORKS_APP_LAUNCHER_ORIGINS | env | unset — ≤ 50 exact https origins for P2 delegated reads, no credentials (added by APW-11) | APW-11 |
EVER_WORKS_APPS_MAX_SCOPE | env | verified-blueprints (any allowed in Wave 3) (added by APW-10) | APW-10 |
EVER_WORKS_APPS_CONTROL_KUBECONFIG | env (secret) | unset — control-namespace-only credential for the ever-works-apps plugin (added by APW-10) | APW-10 |
EVER_WORKS_APPS_CONTROL_NAMESPACE | env | ever-works-apps-control (added by APW-10) | APW-10 |
EVER_WORKS_APPS_GATE_MAX_AGE_HOURS | env | 24 — may be lowered, never raised above 24 (added by APW-10) | APW-10 |
EVER_WORKS_APPS_CONTROLLER_MIN_VERSION | env | unset = no minimum (added by APW-10) | APW-10 |
EVER_WORKS_E2E_FAKES | env | unset — honoured only when NODE_ENV is not production; lets the acceptance harness point Git provider calls at its fake GitHub. It is also the arming half of the GitHub connection-seeding route T63 landed (POST /api/e2e/github-connection/seed, which additionally requires APW_E2E_GITHUB_FAKE_URL), read as exactly the string '1' — the plugin's own arming rule (packages/plugins/github/src/e2e-fakes.ts:51-64) | APW-13 |
APW_E2E_GITHUB_FAKE_URL | env | unset — honoured only when NODE_ENV is not production and EVER_WORKS_E2E_FAKES is exactly '1'; the origin the GitHub plugin builds every provider URL from and the second half of the connection-seeding route's gate, so a connection is never seeded into a process whose plugin is still pointed at the real service (added by APW-13 T63) | APW-13 |
ever-id | flag | off | APW-12 |
EVER_ID_ISSUER_URL | env | unset — the Ever ID issuer (https://auth.ever.co in production; R-28). The plugin's own settings key carries the same name as its x-envVar (added by APW-12) | APW-12 |
EVER_ID_CLIENT_ID / EVER_ID_CLIENT_SECRET | env / secret | unset — the ever-works-web client; the secret is x-secret and never leaves the settings store (added by APW-12) | APW-12 |
EVER_ID_ALLOWED_ISSUERS | env | unset — 1–3 issuers during a planned provider move (R-28 reversibility); defaults to [issuerUrl] (added by APW-12) | APW-12 |
EVER_ID_API_AUDIENCE | env | ever-works — the audience a delegated apps:read token must carry (added by APW-12, APW-11 §2.4) | APW-12 |
EVER_ID_SIGN_UP_ALLOWED | env | true — a private installation may default it off (added by APW-12) | APW-12 |
EVER_ID_CLOCK_SKEW_SECONDS | env | 60, clamped 0–120 (added by APW-12) | APW-12 |
EVER_ID_ENABLED | env | unset — Gauzy-side plugin self-check, evaluated strictly as 'true' inside the plugin and failing closed (added by APW-12, R-28) | APW-12 |
FEATURE_EVER_ID_API / FEATURE_EVER_ID_LOGIN | flag | off — the Ever Teams and Ever Gauzy adoption flags, evaluated strictly as 'true' (added by APW-12, R-28) | APW-12 |
EVER_WORKS_APP_SYNC_ENABLED | env | true — R-30 kill switch; false pauses APW-02 sync and hygiene writes to user forks | APW-02 |
EVER_WORKS_APP_PROVISION_ENABLED | env | true — R-30 kill switch; false pauses new App Provisioner runs (a parked run keeps its place) | APW-04 |
EVER_WORKS_APP_BUILDS_ENABLED | env | true — R-30 kill switch; false pauses workflow writes, secret sync and new Builds | APW-05 |
EVER_WORKS_APP_DEPS_ENABLED | env | true — R-30 kill switch; false pauses dependency provisioning | APW-07 |
EVER_WORKS_APP_AUTO_DEPLOY_ENABLED | env | true — R-30 kill switch; false pauses auto-delivery (APW-08) and auto-deploy (APW-06); manual deploys stay available | APW-06 / APW-08 |
EVER_WORKS_APP_UPSTREAM_PRS_ENABLED | env | true — R-30 kill switch; false refuses new upstream PR opens and pushes (Wave 2's only switch) | APW-09 |
EVER_WORKS_APP_CHANGES_ENABLED | env | true — R-30 kill switch; false pauses new App Work change runs (chat, board, Goal, Mission). Auto-delivery and auto-deploy are covered by EVER_WORKS_APP_AUTO_DEPLOY_ENABLED; existing Tasks and chains stay readable, and a running job finishes its current step and parks | APW-08 |
APP_WORKS_CLOUD_PUSH_ENABLED | env | false — on only for exactly 'true' (config.everWorks.apps.cloudPushEnabled()). Off, the Task finalize (finalizeRun) of a cloud App Work run commits locally, pushes nothing, blocks the Task until T12; on, it judges the commit before publishing it (publishSha). The agent tools commitToRepo / openPullRequest answer to it through the same gate (T-03). Applies on API restart (owner decision 2026-09-25) | APW-08 |
EVER_WORKS_APP_MAIL_RELAY_ENABLED | env | true — R-30 kill switch; false stops the relay accepting new messages (queued ones are refused, not dropped) | APW-07 |
allowBuildValuesOnPullRequests | plugin setting | false — Work-scope APW-05 build setting: whether EW_ build values may reach a pull-request or verification build. Off by default, so agent-authored PR code never receives a real value; turning it on is a per-Work, human-only decision (R-32) (added by APW-05, XC-01) | APW-05 |
verificationPromptedValuesRequireApproval | plugin setting | true — Work-scope APW-05 build setting: a verification build gets prompted values only after the owner approves the diff of the build files (Dockerfile, lockfiles, package scripts) (added by APW-05, XC-01) | APW-05 |
EVER_WORKS_APP_RELAY_DAILY_LIMIT_PER_ACCOUNT | env | 1000 — per-account daily cap on the platform mail relay (added by APW-07, XC-21) | APW-07 |
EVER_WORKS_APP_RELAY_DAILY_LIMIT_PER_ORGANIZATION | env | 5000 — per-organization daily cap on the same relay (added by APW-07, XC-21) | APW-07 |
EVER_WORKS_APP_RELAY_SUSPEND_BOUNCE_RATE | env | 5% — a bounce/complaint rate above this suspends the relay for that App Work and notifies the operator (added by APW-07, XC-21) | APW-07 |
EVER_WORKS_AGENTS_REF | env | unset — the ref the agent-template catalog reads; APW-04 T33 pins it per environment (added by APW-04) | APW-04 |
EVER_WORKS_APPS_LOCAL_WORKER | env | false — runs the App runtime worker in-process for local work; refused when NODE_ENV=production (added by APW-06) | APW-06 |
EVER_WORKS_PLATFORM_CATALOG_BASE_URL | env | unset — replaces the raw host for the whole platform-catalog read (a fixture file or APW-13's fake GitHub); honoured only when NODE_ENV ≠ production and EVER_WORKS_E2E_FAKES is set, and refused in production (added by APW-11, R-40) | APW-11 |
E2E_APP_LAUNCHER_SEED | env | unset — non-production only; opens POST /api/e2e/app-launcher/seed, which otherwise answers 404 (shaped like E2E_BYPASS_SEAT_LIMITS, packages/agent/src/config/index.ts:824-829) (added by APW-11, R-40) | APW-11 |
EVER_WORKS_APP_DEPENDENCY_PRIVATE_ALLOWLIST | env | empty — comma-separated CIDRs exempt from the public-address rule for dependency endpoints (added by APW-07) | APW-07 |
EVER_WORKS_DB_PROVISION_IT_URL | env (test) | unset — the kind lane sets it so ACC-REG-10's integration spec runs instead of self-skipping (added by APW-13) | APW-13 |
The GitHub connection surface (APW-13 T63 — the decision, recorded here and in ACCEPTANCE §0.5). plan §8.8 offers
two surfaces and T63 lands exactly one. The surface is (b): the non-production connection-seeding path —
POST /api/e2e/github-connection/seed for the PR lanes, gated on NODE_ENV !== 'production' and
EVER_WORKS_E2E_FAKES === '1' and APW_E2E_GITHUB_FAKE_URL set, answering the platform's opaque 404 otherwise
and registered only when the gate is open at boot — the same posture as APW-11 T33's E2E_APP_LAUNCHER_SEED route, and
the second instance of R-40. The live lanes do not use it: they attach the operator-run OAuth connect of the
machine account, recorded in T20's estate file. Surface (a) — a user-scope x-secret accessToken setting on the
GitHub plugin — is not landed and not a fallback: the plugin is configurationMode: 'admin-only' and
plugin-operations.service.ts refuses user- and work-scope settings on it, so allowing that one field by name would
widen a security boundary, which is the owner's call rather than a lane's convenience. A lane asserts what it got:
connectCustomerGitHub (apps/web/e2e/helpers/github-connection.ts) reads the platform's own
GET /api/git-providers/github/connection before and after attaching, and refuses by name — never as a raw 400 —
when the surface is closed or reports a connection that is not an OAuth account row.
How a switch is read (R-30). Each switch is read by the job dispatcher that owns the family (and by the
route that starts the same work), not only by the UI. The documented default is the family running — a switch
exists to stop a family in an incident, not to keep a shipped feature dark — and the read is strict: only an
explicit false (or 0) turns the family off, and a value that is neither a recognised true nor a recognised
false is treated as off and logged, so a typo cannot leave a paused family silently running. A switch is
never a substitute for a §7A cap: it stops a family, a cap bounds an actor.
8. Catalog repositories (outside this monorepo)
| Repository | Owner | Content |
|---|---|---|
ever-works/templates (exists 2026-09-17; the drafts called it ever-works/apps, and EVER_WORKS_APPS_CATALOG_REPO accepts either name; a pure listing since 2026-09-25 — no per-template folders and no copy of any App spec: its CI fetches each app row's own .works/works.yml from the template repository) | APW-03 | manifest.json (App Blueprint index), schema/app-spec.schema.json, licenses.yml, CI validation; also evidence/<id>/ (written by APW-13) and the test-only e2e branch — formats normative in APW-03 catalog.md; entries may carry managedHosting.upstreamAgreement (amber only, R-3) |
ever-works/<app>-template (cal-template (formerly cal-diy-template, renamed 2026-09-25) and umami-template are public with the topic; app-fixture-hello-template exists, stays private and is not listed) | APW-13 | one App Blueprint: .works/works.yml (kind app), overlay files, smoke tests, README.md; topic ever-works-app-blueprint — repository layout and overlay.yml format: APW-03 catalog.md §5. The App spec lives only at .works/works.yml, the one file the Blueprint resolver reads — never a root app-spec.yml |
ever-works/app-fixture-hello (exists 2026-09-17) | APW-13 | the acceptance fixture application (source only, no .works/); its Blueprint is ever-works/app-fixture-hello-template |
<e2e-upstream-org>/* (test organization, placeholder name) | APW-13 | per-run test upstreams, the prompt-injection fixture and licence-variant fixtures; never listed in the production Apps catalog |
ever-works/agents (exists; manifest.json is the only path the platform reads today — apps/api/src/agents/agent-template-catalog.service.ts:50-51, row shape { slug, name, title, summary, scope, avatarIcon, tags } at :8-16. The shipped agent templates stay in code (packages/agent/src/agents/agent-templates.ts:62); catalog files are additive and loading them is new work) → app-provisioner | APW-04 | agent template |
ever-works/build-on-open-source-app-mission-template (the seed repository APW-08 creates and targets) | APW-08 | the runtime Mission template (.works/mission.yml, README.md, prompts/): the seed mechanism reads one repository per template (MissionTemplateConfig { owner, repo, branch }, packages/agent/src/missions/mission-template.config.ts:27-60), and the catalog-shaped copy under ever-works/missions is published alongside it, never instead of it |
ever-works/skills (exists) → provision-app, evolve-app, upstream-contribution | APW-04 / APW-08 / APW-09 | Skills — SKILL.md is read (packages/plugins/everworks-skills/src/everworks-skills.plugin.ts:20, packages/agent-plugins/src/skills.ts:38) |
ever-works/missions (exists; the mechanism note below is corrected 2026-09-17) | APW-08 | Mission templates. Note the real read path: the platform reads one repository per template (MissionTemplateConfig { owner, repo, branch }, mission-template.config.ts), and no platform code reads ever-works/missions — so the mission-template repository APW-08 targets is named in the template's own configuration, and the catalog-shaped copy is additive rather than a required layout |
ever-works/platforms (exists 2026-09-17; private at creation — the reader fetches platforms.json and the icons over the raw host, so it must be made public like ever-works/templates, or read with a token as APW-03's optional EVER_WORKS_APPS_CATALOG_TOKEN does; a private repository with no token means every read fails and the panel renders S9 permanently) | APW-11 | platforms.json (platform index: id, name ≤ 40, description ≤ 80, icon, order, status, one https address per environment), schema/platforms.schema.json, tools/validate.mjs + .github/workflows/validate.yml (schema, unique ids, ≤ 24 entries, 16 KB icons, https-only, SVG deny patterns), icons/, fixtures/platforms.fixture.json; drafts in APW-11-app-launcher/catalog-draft/ |
ever-works/platforms (exists 2026-09-17) | APW-11 | platforms.json (Ever platform catalog: id, name, description ≤ 80, icon path, order, status, https URL per environment), icons/, schema/platforms.schema.json, CI validation (≤ 24 entries, icons ≤ 16 KB) |
9. Names written into a Work Repository (added by APW-05)
| Name | Owner | Consumers |
|---|---|---|
Workflow file .github/workflows/ever-works-build.yml — the only workflow APW-02's Actions hygiene keeps enabled; header line # ever-works-build generator=<n> inputs=sha256:<hex> | APW-05 | APW-02, 04, 08 |
Branch ever-works/build-workflow — the pull request branch for workflow changes | APW-05 | APW-02, 08 |
Repository Actions secret prefix EW_ (EW_<ENV NAME> for build.args[].fromEnv); the platform removes only EW_ secrets it wrote | APW-05 | APW-07 |
Image ghcr.io/<owner>/<repo>/ever-works-app (lower-cased); tags sha-<40 hex>, branch-<slug>, pr-<number>, buildcache; never latest; Deployments use the digest | APW-05 | APW-04, 06, 13 |
Actions secret EW_VERIFY__PROMPTED — per-verification-run JSON of already-set prompted values, deleted when the run ends | APW-05 | APW-04, 07 |
Workflow artifact ever-works-build-result (ever-works-build-result.json, ≤ 8 KB, 7-day retention) — untrusted until the digest is confirmed against the registry | APW-05 | APW-04 |
6A. Notification catalogue (added 2026-09-17 — resolution R-34)
Notifications were ad hoc: CONTRACTS.md mentioned "notification" nowhere, only APW-06's plan carried catalogue
rows, and notification copy had no locale keys. The rows below are normative — an epic that adds a notification
adds its row here and its keys to all 21 locale files in the same PR. Channels are the existing ones
(NotificationService, in-app inbox, e-mail); operator rows page the on-call channel named in the private
operations repository and bypass quiet hours; every other row respects them. dedupeKey collapses repeats so a
flapping signal cannot spam.
| Id | Owner | Audience | Urgency | Dedupe key | Copy / i18n keys |
|---|---|---|---|---|---|
app_sync_conflict | APW-02 | member | normal | work:<id>:conflict | Upstream conflict needs a decision (existing keys) |
app_build_failed | APW-05 | member | normal | work:<id>:build:<sha> | "The build of your change failed" |
app_provision_needs_input | APW-04 | member | normal | provisioning:<id>:question | "The App Provisioner needs an answer" |
app_deploy_failed | APW-06 | member | normal | work:<id>:deploy:<n> | "Your App Work did not come up" (APW-06 plan §9.4) |
app_unhealthy | APW-06 | member | normal | work:<id>:health | "Your App Work is not responding" |
app_recovered | APW-06 | member | low | work:<id>:recovered | "Your App Work is healthy again" |
app_cluster_unreachable | APW-06 | member | high | work:<id>:cluster | "We can't reach your cluster" |
app_pull_token_expiry | APW-05 | member | low | work:<id>:token | "Your registry pull token expires soon" (FR-51) |
app_backup_overdue | APW-07 | member | normal | dependency:<id>:backup | "A dependency backup is overdue" |
app_tier_usage_80 | APW-10 | member | normal | work:<id>:usage:<window> | "You have used 80% of your allowance" (FR-36) |
app_tier_quarantined | APW-10 | member + operator | high | work:<id>:quarantine | Owner copy + operator page (FR-39) |
app_tier_egress | APW-10 | operator | high | work:<id>:egress | Operator page — egress threshold crossed |
app_attestation_expiry | APW-10 | member | normal | attestation:<id> | "Your hosting attestation expires soon" |
app_upstream_pr_decision | APW-09 | member | normal | pr:<id>:decision | "A pull request is waiting for your decision" |
app_mail_relay_suspended | APW-07 | operator | high | relay:<workId> | Operator page — bounces or complaints crossed the limit |
7A. Quotas and caps (added 2026-09-17 — resolution R-31)
Each row is a cap with an environment override, a refusal code in §12 and user copy; member and org are the
two scopes. Defaults are deliberately generous — they exist to stop runaway loops and abuse, not to shape normal
use, and raising one is an operator action (no redeploy). Every refusal is a 4xx with copy; nothing is dropped
silently and nothing is deleted to make room.
| Cap | Default (member) | Default (organization) | Override | Owner |
|---|---|---|---|---|
| Active App Works | 25 | 200 | EVER_WORKS_APPS_MAX_ACTIVE | APW-01 |
| Creates per day | 20 | 100 | EVER_WORKS_APPS_MAX_CREATES_PER_DAY | APW-01 |
| Private copies (each ≤ 500 MB, FR-20) | 10 | 50 | EVER_WORKS_APPS_MAX_PRIVATE_COPIES | APW-01 |
Sync now calls per hour | 20 | 100 | EVER_WORKS_APP_SYNC_MAX_PER_HOUR | APW-02 |
| Push builds per day | 50 | 300 | EVER_WORKS_APP_BUILDS_MAX_PER_DAY | APW-05 |
| Runner-minute warning band per day | 600 (warn) | 3 000 (warn) | EVER_WORKS_APP_BUILD_MINUTES_WARN | APW-05 |
| Concurrent provisions | 2 | 10 | EVER_WORKS_APP_PROVISION_MAX_ACTIVE | APW-04 |
| Deploys per day | 30 | 200 | EVER_WORKS_APP_DEPLOYS_MAX_PER_DAY | APW-06 |
| Upstream PRs open (per member) | 5 | — | existing FR limits (APW-09) | APW-09 |
| Upstream PR opens per day | 3 | 20 | EVER_WORKS_APP_UPSTREAM_PR_MAX_PER_DAY | APW-09 |
| Upstream PR opens per upstream per day (platform-wide) | 5 | 5 | EVER_WORKS_APP_UPSTREAM_PLATFORM_MAX_PER_DAY | APW-09 |
| Mail relay messages per App Work/day | 200 (FR-39) | 2 000 | EVER_WORKS_APP_MAIL_MAX_PER_DAY | APW-07 |
| Managed tier App Works (paid) | existing EVER_WORKS_APPS_MAX_PER_USER = 3 | existing EVER_WORKS_APPS_MAX_SCOPE | — | APW-10 |
The relay is additionally limited to verified accounts, suspended automatically on a bounce/complaint
threshold (operator notification app_mail_relay_suspended), metered with a receipt, and covered by
EVER_WORKS_APP_MAIL_RELAY_ENABLED (R-30).
Spend is budgeted per Work, never per feature (added 2026-09-17, XC-19). Every agent Run (tokens) and every
managed Build (runner minutes) of an App Work books against that Work's own budget through the existing Work
budget mechanism, and the App Work's overview shows the cap and what remains. A run refused by the budget is
waiting, not failed, names the reset time, and opens no follow-up Task; the per-run caps above (APW-04,
APW-05) stay in force in addition, so a feature cap can never be used to escape the Work budget and a Work
budget can never be used to escape the per-run cap. Provisioning, builds, evolves and upstream-PR preparation all
book into the same budget.
Upstream PRs are bounded three ways (added 2026-09-17, XC-22). A member's own allowance (the table row
above, plus APW-09's per-day limits), a platform-wide per-upstream ceiling that refuses with
platformCapReached even when the member's allowance remains, and two opt-outs the platform honours: a
maintainer's declaration that the repository does not want automated contributions (maintainerOptOut) and an
operator deny list (deniedUpstream, which also stops polling for existing rows). None of the three is
configurable by a member.
10. Threat register (added 2026-09-17 — resolution R-37)
The program's threat model is THREAT-MODEL.md: assets, actors, the trust-boundary table and
one row per threat carrying threat → control (FR / R-n / rule / LG) → owning task → verifying ACC → residual
risk → the wave it is accepted in. This section is the index; the file holds the analysis and the public-safe
wording (R-14 — generic descriptions, no addresses, no unpatched-finding detail; operational specifics live in the
private operations repository).
| Boundary | Primary threats | Index rows |
|---|---|---|
| B-1 — Untrusted repository content → the App Provisioner and the evolve agents | prompt injection → tool misuse (R-17, R-33) | T-01…T-04 |
| B-2 — App spec checks and upstream code executed on Fleet nodes, the user's CI and verification runners | secret exfiltration, workflow tampering (R-9), Fleet containment downgrade (R-33) | T-05…T-07, T-34 |
B-3 — The platform-written workflow, its EW_ Actions secrets and the ever-works-build-result artifact | credential theft by unreviewed PR code | T-08…T-12 |
| B-4 — User-supplied kubeconfigs dialled by the cluster-op worker | SSRF, credential capture | T-13…T-16 |
| B-5 — Tenant workloads on Ever Works Apps → the zone, other tenants and the platform | tenant escape, noisy neighbour (APW-10) | T-17…T-20 |
B-6 — Runtime-loaded catalogs (ever-works/templates, ever-works/platforms, Blueprint repositories) as a supply chain | supply-chain poisoning (R-29) | T-21…T-23 |
B-7 — Delegated Ever ID tokens + cross-origin launcher reads → GET /api/me/apps | token replay, over-broad scope (R-19, R-28) | T-24…T-25 |
B-8 — The non-production EVER_WORKS_E2E_FAKES switch → the Git provider base URL | fake provider reachable in production | T-26 |
| B-9 — Upstream repositories ← pull requests sent in the member's name | reputation damage, spam (R-18) | T-27…T-30 |
| B-10 — User-supplied external endpoints (S3, SMTP, webhook receiver, the app's own public URL) → platform HTTP clients | SSRF against platform clients | T-31 |
| B-11 — Platform credentials and tenant data at rest → any read surface (API, logs, Activity, backup export) | credential reuse, co-tenant read (R-25) | T-32…T-33 |
| B-12 — Non-human callers on human-only routes | automation spending or deleting on a person's behalf (R-32) | T-35 |
Threat ids are stable and never re-sequenced: T-34 stays inside B-2 and T-35 inside B-12 because they were
added after the first pass — a new threat appends to the next free number rather than renumbering a table. The
per-threat detail (control with its file:line, owning task, verifying ACC, residual risk, accepted-in wave) lives
in THREAT-MODEL.md §3–§4.
11. Operational signals (added 2026-09-17 — resolution R-37)
Every §5 job names the signal an operator watches, its stuck-state threshold and the alert that fires. Alerts are
owned by the epic that dispatches the job and carry a runbook link; the correlation id is written into the
Activity row's details and into every §6 event of the same App Work change, so merge → Build → Deployment → live
can be followed end to end.
| Signal | Source | Threshold / stuck state | Alert | Owner |
|---|---|---|---|---|
| Build lost / silent | work_builds + sweep | running with no update > 90 s, twice | app_build_lost_rate | APW-05 |
| Deploy lock stale | work_app_runtime_states | lock held > 15 min with no phase change | app_deploy_lock_stale | APW-06 |
| Health-poll backlog | app-health-poll | poll overruns its 500/min budget twice | app_health_poll_backlog | APW-06 |
| Provisioning parked | work_app_provisionings | parkedReason set > 8 h | app_provision_parked | APW-04 |
| Fork readiness stuck | work_upstream_states | preparing > readiness timeout × 3 | app_fork_readiness_stuck | APW-02 |
| Upstream sync rate-limited streak | APW-02 sync dispatcher | ≥ 3 consecutive secondary_rate_limited | app_upstream_sync_rate_limited_streak | APW-02 |
| Upstream PR polling denied | upstream_pull_requests | ≥ 5 consecutive poll failures | app_upstream_pr_poll_failing | APW-09 |
| Deletion pending | WorkAppRuntimeState | deletionRequestedAt > 60 min | app_deletion_pending | APW-01 |
| Tier heartbeat stale | apps-tier-self-check | no successful self-check > 6 h | apps_tier_heartbeat_stale | APW-10 |
| Queue lag | job-runtime provider | oldest queued job > 5 min (any app-* queue) | app_job_queue_lag | all |
| Activity/notification delivery failure | ActivityLogService, mail | ≥ 10 failures in 10 min | app_activity_delivery_failing | all |
Each wave names one dashboard built from these signals, and every new job added after this date adds its row. Telemetry stays counters-and-ids only (APW-12 plan §9.1 is the precedent): no e-mail, subject, token, code, repository name or App Work content in a metric label.
GitHub API budget (added 2026-09-17). Upstream polling, readiness checks, Actions hygiene and run discovery
all spend a member's own GitHub rate limit, so the program budgets it rather than discovering the ceiling in
production: each App Work's polling has a floor interval and a per-tick batch cap (APW-02's */10 dispatcher and
APW-09's poll are the two loops that must state theirs), a 403/429 with a rate-limit body is a retry with
backoff and never a failure of the App Work, secondary_rate_limited pauses the family rather than hammering it,
and the app_upstream_sync_rate_limited_streak signal above is the alarm when one installation keeps hitting the
ceiling. A job that would exceed the budget defers to the next tick instead of consuming another member's quota.
Whose connection a background job uses (added 2026-09-17). A job acting on an App Work uses the Work
owner's stored Git connection; a job acting on a person's behalf outside their own Work (an upstream pull
request) uses that member's own connection through GitFacadeService.getMemberAccountToken and never a
platform PAT or an installation token — the APW-09 rule in §2A, now stated program-wide. On a shared App Work
(the owner plus invited members) the actor's own connection is used for anything the actor initiates, the owner's
for scheduled work, and a job that finds neither stops and notifies rather than falling back to a platform
credential: what a member can do through a background job is exactly what their own connection authorises.
12. Error codes (added 2026-09-17)
Error codes are public contract (Constitution X) and the web translates them into i18n keys, so they follow one
style: snake_case, { status: 'error', code, message, details? } bodies, one code per situation shared across
epics, and a camelCase i18n leaf per code. The existing apps/api/src literals are snake_case, so the camelCase
codes in the epic plans are the departure, not the convention. Codes are append-only — a code is never reused
for a different situation and never removed; a renamed code keeps its old value as an alias in the web map.
| Code | Status | Situation | Owner | i18n leaf |
|---|---|---|---|---|
app_works_disabled | 404 | App Works off at the API (§7/R-6) | APW-01 | appWorksDisabled |
not_app_work | 404 | the Work exists but is not kind app (one code, all epics) | APW-01 | notAppWork |
app_runtime_unavailable | 503 | runtime target unreachable | APW-06 | appRuntimeUnavailable |
not_found | 404 | the App Work does not exist or is another account's (R-36) | APW-01 | notFound |
sync_in_progress | 409 | a sync is already running | APW-02 | syncInProgress |
retry_limit_reached | 429 | readiness retry cap (§7A) | APW-02 | retryLimitReached |
provisioning_unavailable | 422 | no runtime that can provision | APW-04 | provisioningUnavailable |
build_not_found | 404 | unknown build id | APW-05 | buildNotFound |
rebuild_rate_limited | 429 | rebuild cap (§7A) | APW-05 | rebuildRateLimited |
secure_storage_unavailable | 503 | encrypted env store unavailable | APW-07 | secureStorageUnavailable |
confirmation_mismatch | 400 | typed confirmSlug does not match (R-32) | APW-07 | confirmationMismatch |
active_proposal | 409 | an open upstream-PR proposal exists | APW-09 | activeProposal |
rate_limited | 429 | any per-route throttle, details.scope names which (APW-09 §5) | APW-09 | rateLimited |
quota_exceeded | 429 | a §7A cap, details.cap names which | all | quotaExceeded |
switch_off | 503 | an R-30 operator switch is off | all | switchOff |
apps_tier_open_refused | 409 | the tier gate refuses this deployment | APW-10 | appsTierOpenRefused |
quota_ceiling_exceeded | 429 | tenant quota ceiling | APW-10 | quotaCeilingExceeded |
ever_id_disabled | 404 | Ever ID plugin off (APW-12 §5.2) | APW-12 | everIdDisabled |
transaction_invalid | 400 | bad/expired/replayed Ever ID transaction | APW-12 | transactionInvalid |
token_in_query | 400 | a token in a query parameter (FR-17) | APW-12 | tokenInQuery |
ever_id_signed_out | 401 | the session was ended by a sign-out notice (S6) | APW-12 | signedOutByProvider |
The existing snake_case literals already on develop (for example gh_repo_access_denied,
backup_not_found) keep their values; the epic plans' camelCase codes (provisioningUnavailable,
notAppWork, everIdDisabled, …) are aligned to the snake_case column above in each owner's own file, with the
camelCase form kept only as the web i18n leaf.