Aller au contenu principal

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 are APW-03/catalog.md.

  • C1 — Strict means strict reporting. An unknown key inside spec is an error (unknown_field), except keys starting with x-; no writer ever deletes it (the envelope's preservation rule stands). Two optional keys join spec: kind: app (repeats the root kind) and appSpecVersion (integer, default 1); a newer appSpecVersion turns unknown-key errors into warnings.
  • C2 — Outline clarifications. jobs[].component defaults to domains.primaryComponent, and an http job needs a web component. http.body string leaves take {{…}} placeholders — the outline's generatedCredential: admin is illustrative; write {{env.ADMIN_PASSWORD}} over a generated env entry. The outline's generate/validate line lists option names, not a consistent pair (a base64 generator of 32 bytes yields 44 characters; generate and validate must agree). A secret: true entry cannot carry value. Env names starting EVER_WORKS_ are reserved for platform-injected values. volumes with replicas > 1 is an error (aligned with APW-06). license.sourceOfferUrl (https URL) is added. The object storage bucket output is deps.objectStorage.bucket.<name>. http.authScheme (APW-13) applies to jobs and cron alike.
  • C3 — One license attestation record. Owner APW-03: WorkAppSpecState.attestation and POST /api/works/:id/app-license/attest. APW-06 reads it through AppLicenseService.getHostingEligibility and WorkAppRuntimeState keeps no license attestation of its own. The Source-link condition is shared: a network-source-offer obligation and (relation link or 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".

IdTopicResolutionEpics to align
R-1Shared types folderpackages/contracts/src/apps/ (one folder for every App Works shared type). No src/app-works/.all
R-2Activity namingaction = 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-3License attestationOne 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-4First write into the Work RepositoryA 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-5Managed tier deploy pathOn 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-6Kind switchWeb 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-7Capability flagsWorkCapabilities 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-8Upstream tabOne 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-9Checks in the user's CIAPW-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-10Provisioner verification hooksAccepted 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-11Keypair formatsenv[].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 targetThe 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-13Zero-config build strategybuild.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-14Public wording of existing defectsExisting, 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-15Delete an App WorkDeleting 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-16Public 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-17Safety 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-18Upstream PR approvalsThe 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-19Ever ID auth methodAuthenticatedUser.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-20Tier stop namingAPW-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-21Upstream sync conflictsAPW-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-22Test locationsapps/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-23Fixture repository branchesAPW-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-24Sandboxed runtime vs sandboxed buildsA 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-25Workspace 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-26Additive-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-27Deploy shapes are a familyAn 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-28Ever ID provider, domain and integration postureOwner 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-29Catalog repositories are real, and namedOwner 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-30Operator kill switchesEvery 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-31Quotas and capsEvery 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-32Human-only actionsEvery 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-33Fleet run containment is authoritativeA 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-34Activity completenessEvery 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-35Account and organization deletionDeleting 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-36Another account's App Work answers 404WorkOwnershipService.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-37Threat register and operational signals are deliverablesThe 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-38Merge order and prerequisite chainThe 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-39Migration timestampsProgram 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-40Non-production lane hooksEvery 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-41Upstream preparation's base, report and acknowledgementThree 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 RepositoryRole value website, whose UI label is literally "Work Repository" (packages/contracts/src/domain/work-capabilities.ts:40-49). data is 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 -app suffix alongside -website for the Work Repository, chosen by template type.

NameOwnerWhat it holdsConsumers
Work.kind = 'app' + WORK_KIND_CAPABILITIES.appAPW-01kind, capabilitiesall
Work.sourceRepository.type values app_link, app_fork, app_private_copy; sourceRepository.upstream {owner, repo, defaultBranch}APW-01how the Work Repository relates to the upstreamAPW-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 inputAPW-01APW-03, 04
WorkUpstreamState (table work_upstream_states)APW-02fork readiness, last synced upstream sha, ahead/behind, last sync result, conflict Task id, Actions hygiene stateAPW-01, 08, 09
WorkAppSpecState (table work_app_spec_states)APW-03applied 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 pathsAPW-04…07, 11
WorkBuild (table work_builds)APW-05commit sha, trigger, build plugin id, status, image digest, logs URL, duration, cost receipt refAPW-06, 08, 13
WorkDeployment extended: buildId, componentStatuses, smokeResultAPW-06link a Deployment to its Build and record per-component + smoke outcomeAPW-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, SUPERSEDEDAPW-06target, render facts and rollback outcome of an App DeploymentAPW-08, 11, 13
WorkAppRuntimeState (table work_app_runtime_states) — added by APW-06APW-06deploy 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 snapshotAPW-08, 11, 13
WorkAppProvisioning (table work_app_provisionings) — added by APW-04APW-04one 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 WorkAPW-01, 02, 06, 13
WorkAppEnvValue (table work_app_env_values)APW-07encrypted value per env name, origin (generated/prompted/derived/user), generated-atAPW-05, 06
WorkAppDependency (table work_app_dependencies)APW-07dependency kind, provider plugin id, status, encrypted connection outputs, backup policyAPW-06
UpstreamPullRequest (table upstream_pull_requests)APW-09Work, Task, upstream repo, PR number/url/state, approval id, disclosure textAPW-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-09APW-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-08APW-09, 11, 13
Task.branchGuardRefusal (column tasks.branchGuardRefusal, text NULL; migration 1792110100000-AddTaskBranchGuardRefusal.ts, added 2026-09-25 by APW-08 T17)APW-08the 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 refusedweb
Goal.workId (column goals.workId, uuid NULL) and Mission.outputMode (ideas · tasks · both), Mission.taskOutput, Mission.taskOutputNoticeAt — added by APW-08APW-08APW-13
AppLauncherPreference (table app_launcher_preferences)APW-11per user (and optional organization): item key, visible, order, pinnedAPW-12
ExternalIdentity (table external_identities)APW-12issuer, subject, user id, linked at, last loginAPW-11
Work.appLauncherExposed (column works.appLauncherExposed, boolean NULL = kind default: app on, others off) — added by APW-11APW-11Work-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.appsTierQuotaProfileAPW-10launch-gate evidence, open/closed transitions, quarantine, signals, quota profiles, idempotent usage windowsAPW-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 defaultAPW-04per-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 firstAPW-12sessions 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 shownAPW-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 existsAPW-05supply-chain evidence and egress blocking for the managed in-zone builderAPW-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 classificationAPW-05the state a Build's verdict (secretsSyncedAt <= startedAt, buildInputsHash == currentInputsHash) and run discovery both read; platform-written, never user-writableAPW-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 / dataOwnerRetention
work_builds (one row per run attempt)APW-0512 months, then the 50 newest per App Work are kept and the rest pruned
work_app_provisionings evidenceAPW-0412 months
upstream_pull_requests (polled for months)APW-09kept while open; closed rows pruned 12 months after their last change
apps_tier_state_events (append-only)APW-1024 months, then archived per quota profile
apps_tier_usage_windowsAPW-1024 months
Activity rows of the app_* familiesallthe existing Activity retention applies unchanged — no epic shortens it
app_launcher_preferencesAPW-11kept while the account exists; removed by R-35
external_identitiesAPW-12kept 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)​

NameOwnerConsumers
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.tsAPW-03all
AppSpecService.getEffectiveSpec(workId, commitSha?) (returns invalid for a commit whose spec has errors), validateDraft(workId, text), initialize(workId, branch)APW-03APW-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-03APW-05, 06, 07, 08
AppLicenseService.getHostingEligibility(workId), previewUpstream(workId, owner, repo, sha), recordEvidence(workId, commitSha, findings); pure scanLicenseHeaders(files)APW-03APW-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-03APW-04, 08
AppSourceCatalogAdapter — APW-03's binding of APW-01's APP_SOURCE_CATALOG_PORTAPW-03APW-01
APP_DEPENDENCY_OUTPUTS — output names per dependency kind that from: deps.<kind>.<output> may reference (proposed list: APW-03 schema.md §11)APW-07APW-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 onlyAPW-05 / APW-07—
Activity actionType app_provision (added by APW-04) — dotted §6 names in actionAPW-04—
Activity actionType app_tier (APW-10), app_launcher (APW-11) — dotted §6 names in actionAPW-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-07APW-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-08APW-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-09APW-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-01APW-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-03APW-01, 04, 05, 08

3. Capability interfaces (Constitution I–II)​

Interface / changeOwnerConsumers
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-02APW-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 semanticsAPW-02APW-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 AppSourceCatalogAdapterAPW-01APW-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 thereAPW-01APW-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-01APW-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 AppSourceInitializerServiceAPW-02APW-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-01APW-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 ownAPW-08APW-02
IGitProviderPlugin.createPullRequest cross-repository head (headOwner, headRepo, maintainerCanModify) and GitPullRequest.headRepoFullNameAPW-09APW-08
New capability build — IBuildPlugin (prepareRepository, startBuild, getBuild, cancelBuild, getLogsUrl); category build in everworks.pluginAPW-05APW-04, 06, 08
New capability app-dependency — IAppDependencyProvider (supports(kind, target), provision, getOutputs, deprovision, backupStatus); AppDependencyContext.ephemeral; deprovision option stopWorkloadsAPW-07APW-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 wholeAPW-06APW-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-06APW-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-06APW-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 valuesAPW-05APW-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 workerAPW-06APW-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 setAPW-07APW-04, 05, 06
IPipelinePlugin.enforcesRuntimeNetworking?: boolean (added by APW-04) — true only for pipelines that enforce runtimeEnvironment.networkingMode = 'limited' as a sandbox policyAPW-04APW-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-04APW-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 specAPW-04APW-01, 02, 05, 06
Identity provider plugin (identity-provider capability) for Ever ID relying-party sign-inAPW-12APW-11
IGitProviderPlugin additions (added by APW-08, optional): GitPullRequestStatus.mergeCommitSha?; isAncestorCommit?(owner, repo, ancestorSha, descendantSha, token) → boolean | nullAPW-08APW-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 statusAPW-05APW-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-09APW-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-03APW-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-02APW-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.tsAPW-12APW-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 varAPW-10APW-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 activeAPW-10APW-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-10APW-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 onlyAPW-05APW-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-05APW-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-05APW-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-08workspace 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 pushAPW-08APW-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 queueAPW-07APW-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-10APW-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_ENFORCEDAPW-10APW-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 readyAPW-07APW-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 secretsAPW-02APW-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 = 50APW-08APW-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 unboundAPW-11APW-06

4. HTTP API​

RouteOwner
POST /api/works with kind: 'app', repositoryUrl, repositoryMode: link | fork | private-copy, targetOwnerAPW-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/validateAPW-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 ProvisionerAPW-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/buildsAPW-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 aboveAPW-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/rotateAPW-07
GET /api/works/:id/app-dependenciesAPW-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/missionsAPW-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-requestsAPW-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/dismissAPW-09
GET /api/me/apps, PUT /api/me/apps/preferences; delegated read with scope apps:readAPW-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/:idAPW-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):

  1. Minimum role — owner, manager, editor, viewer or platform admin, against the existing WorkMemberRole, with platform-admin routes answering 404 rather than 403 for everyone else. A route may require a higher role than its neighbours but never silently: the epic's FR says which role, and 403 is reserved for a member who lacks it (R-36 covers the not-a-member case).
  2. Human-only — yes when the action spends money, deletes data, publishes outside the platform, changes a security posture or accepts a legal obligation (R-32), enforced with @HumanOnly(); no otherwise. This is the list the MCP whitelist and the chat tool registry read, so a human-only route is never whitelisted.
  3. Surface parity — OpenAPI (with @ApiOperation and DTOs), the MCP tool name, the CLI command, the chat tool name, and/or not exposed with a reason. MCP tool schemas are generated from OpenAPI operations through the existing whitelist, so a route without @ApiOperation is invisible there.
  4. Throttle — the @Throttle values 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.

RouteOwnerWhy human-only
POST /api/works/:id/delete — the delete_stored_data setAPW-01deletes data (typed confirm_slug, R-15)
POST /api/works/:id/app-license/attestAPW-03accepts a legal obligation (R-3)
POST /api/works/:id/provision (202)APW-04spends tokens
POST /api/works/:id/builds (202)APW-05spends runner minutes
PUT /api/works/:id/builds/pull-tokenAPW-05writes a registry credential
POST /api/deploy/works/:id — the managed-target branchAPW-06spends compute
POST /api/works/:id/app-lifecycle — remove + deleteDataAPW-06deletes data
PUT /api/works/:id/app-targetAPW-06changes a security posture
POST /api/works/:id/app-target/kubeconfigAPW-06stores a cluster credential
PUT /api/works/:id/app-envAPW-07writes secrets
POST /api/works/:id/app-env/:name/rotateAPW-07invalidates a live credential
PUT /api/works/:id/app-dependencies/:kind (202)APW-07writes provider credentials
POST /api/works/:id/app-dependencies/:kind/provision (202)APW-07provisions paid resources
DELETE /api/works/:id/app-dependencies/:kindAPW-07deletes data
POST /api/works/:id/app-runs/allow-containmentAPW-08changes a security posture (R-33)
POST /api/works/:id/upstream-pull-requestsAPW-09publishes outside the platform
POST …/:prId/signed (202)APW-09signs a CLA/DCO on the member's behalf
POST …/:prId/withdrawAPW-09public act on a third-party repository
POST …/:prId/address-review (202)APW-09publishes 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 idOwnerTrigger
app-fork-readinessAPW-02fork requested; Try again, stale re-dispatch, setup PR merged
app-upstream-syncAPW-02Schedule (spec.upstreamSync.schedule) or manual
app-upstream-sync-dispatcherAPW-02schedule */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-evaluateAPW-03spec applied, upstream synced; also license registry changed, manual re-check, header evidence recorded (APW-03)
app-spec-evaluateAPW-03push 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-applyAPW-03Blueprint apply or upgrade requested (added by APW-03)
apps-catalog-refreshAPW-03schedule 23 * * * * — catalog + registry refresh, upgrade notices, license re-classification fan-out (added by APW-03)
app-provisionAPW-04App Work created without a Blueprint, or manual
app-provision-sweepAPW-04schedule, every 15 minutes — expired verification targets, stale leases, 8 h deadline, question reminder/expiry, queue promotion (added by APW-04)
app-build-watchAPW-05Build started (webhook-driven, polling fallback)
app-build-prepareAPW-05app.spec.applied touching build or build-phase env, build-phase env change, Rebuild, verification request (added by APW-05)
app-build-sweepAPW-05schedule */2 * * * * — Builds silent > 90 s, lost Builds, unconfirmed digests after a pull token is saved (added by APW-05)
app-deployAPW-06Build succeeded on the deploy branch, or manual
app-smokeAPW-06Deployment reported ready
app-cluster-opAPW-06status refresh, logs, pause/resume/remove, cancel, job run, connection check, ingress/DNS reconcile, App Work deletion, verification targets (added by APW-06)
app-health-pollAPW-06schedule, every minute (added by APW-06)
app-preview-gcAPW-06schedule, every 5 minutes — Wave 3 (added by APW-06)
app-dependency-provisionAPW-07spec applied with new dependencies
upstream-pr-statusAPW-09open Upstream pull request (polling; upstream sends no webhooks)
upstream-pr-openAPW-09approval of an Upstream pull request decided by its author (added by APW-09)
upstream-pr-pushAPW-09approval of a review follow-up push decided by its author (added by APW-09)
app-change-deliveryAPW-08schedule 1-59/2 * * * * — follow merged App Work Task changes to Build, Deployment and live; follow-up Tasks (added by APW-08)
apps-tier-self-checkAPW-10manual (operator) or schedule 23 */6 * * * (added by APW-10)
apps-tier-gate-watchAPW-10schedule */5 * * * * — tier transitions, attestation notices, quarantine mirroring, signal import (added by APW-10)
apps-tier-metering-importAPW-10schedule 7 * * * * (added by APW-10)
apps-tier-daily-receiptsAPW-10schedule 15 0 * * * UTC (added by APW-10)

6. Activity event types (prefix app.)​

EventsOwner
app.source.linked, app.source.forked, app.source.copied, app.source.failedAPW-01
app.fork.ready, app.fork.timeout, app.actions.disabled, app.upstream.synced, app.upstream.behind, app.upstream.conflictAPW-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.changedAPW-03
Added by APW-03: app.blueprint.applied, app.blueprint.apply_failed, app.blueprint.upgrade_available, app.license.attested, app.license.attestation_requiredAPW-03
app.provision.started, app.provision.proposed, app.provision.needs_input, app.provision.succeeded, app.provision.failedAPW-04
Added by APW-04: app.provision.attempted (one per verification attempt: n, verdict, failed step, target kind), app.provision.blueprint_suggestedAPW-04
app.build.queued, app.build.started, app.build.succeeded, app.build.failed, app.build.cancelledAPW-05
app.deploy.started, app.deploy.succeeded, app.deploy.failed, app.job.succeeded, app.job.failed, app.smoke.passed, app.smoke.failedAPW-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.unreachableAPW-06
app.env.changed (names only), app.env.rotated, app.dependency.provisioned, app.dependency.failedAPW-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_openedAPW-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 recordAPW-09
app.upstream_pr.proposed, app.upstream_pr.approved, app.upstream_pr.opened, app.upstream_pr.merged, app.upstream_pr.closedAPW-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.suggestedAPW-09
app.launcher.exposed, app.launcher.hiddenAPW-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​

NameKindDefaultOwner
works-appflagoff in production until Wave 1 acceptance is greenAPW-01
EVER_WORKS_APP_WORKS_ENABLEDenvfalse — 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_MSenvunset — honoured only when NODE_ENV ≠ production, clamped 5 000–900 000 (added by APW-02, FR-18a)APW-02
EVER_WORKS_APPS_CATALOG_REPOenvever-works/templates — the listing repository, created 2026-09-17; the earlier drafts called it ever-works/apps, so an installation carrying that value keeps workingAPW-03
EVER_WORKS_APPS_CATALOG_REFenvmain (pin a SHA/tag in production)APW-03
EVER_WORKS_APPS_CATALOG_TOKENenv (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_ENABLEDenvfalse — only APW-10's launch gate may flip itAPW-10
EVER_WORKS_APPS_DOMAINenvdefaults 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_USERenv3APW-06
EVER_WORKS_APPS_DNS_ZONE_IDenvunset — no managed subdomains without it (added by APW-06); on the shared default this is the platform domain's own zoneAPW-06
EVER_WORKS_APPS_DNS_API_TOKENenv (secret)unset (added by APW-06)APW-06
EVER_WORKS_APPS_CLUSTER_WORKER_ISOLATEDenvfalse — production refuses App Work cluster jobs until the operator declares the isolated worker (added by APW-06)APW-06
EVER_WORKS_APPS_CLUSTER_PRIVATE_ALLOWLISTenvempty — CIDRs exempt from the public-address rule for self-hosted installations and e2e (added by APW-06)APW-06
works-app-previewsflagoff — Wave 3 (added by APW-06)APW-06
EVER_WORKS_APP_PROVISION_TOKEN_CAPenv3000000 — per-provisioning token cap, clamped 500000–10000000 (added by APW-04)APW-04
EVER_WORKS_APP_PROVISION_RUNNER_MINUTE_CAPenv240 — per-provisioning runner-minute cap, clamped 60–600 (added by APW-04)APW-04
app-launcherflagoffAPW-11
EVER_WORKS_APP_LAUNCHER_ENABLEDenvfalse — 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_IDenvever-works/platforms / main (pin in production) / production / ever-works (added by APW-11)APW-11
EVER_WORKS_APP_LAUNCHER_ORIGINSenvunset — ≤ 50 exact https origins for P2 delegated reads, no credentials (added by APW-11)APW-11
EVER_WORKS_APPS_MAX_SCOPEenvverified-blueprints (any allowed in Wave 3) (added by APW-10)APW-10
EVER_WORKS_APPS_CONTROL_KUBECONFIGenv (secret)unset — control-namespace-only credential for the ever-works-apps plugin (added by APW-10)APW-10
EVER_WORKS_APPS_CONTROL_NAMESPACEenvever-works-apps-control (added by APW-10)APW-10
EVER_WORKS_APPS_GATE_MAX_AGE_HOURSenv24 — may be lowered, never raised above 24 (added by APW-10)APW-10
EVER_WORKS_APPS_CONTROLLER_MIN_VERSIONenvunset = no minimum (added by APW-10)APW-10
EVER_WORKS_E2E_FAKESenvunset — 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_URLenvunset — 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-idflagoffAPW-12
EVER_ID_ISSUER_URLenvunset — 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_SECRETenv / secretunset — 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_ISSUERSenvunset — 1–3 issuers during a planned provider move (R-28 reversibility); defaults to [issuerUrl] (added by APW-12)APW-12
EVER_ID_API_AUDIENCEenvever-works — the audience a delegated apps:read token must carry (added by APW-12, APW-11 §2.4)APW-12
EVER_ID_SIGN_UP_ALLOWEDenvtrue — a private installation may default it off (added by APW-12)APW-12
EVER_ID_CLOCK_SKEW_SECONDSenv60, clamped 0–120 (added by APW-12)APW-12
EVER_ID_ENABLEDenvunset — 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_LOGINflagoff — the Ever Teams and Ever Gauzy adoption flags, evaluated strictly as 'true' (added by APW-12, R-28)APW-12
EVER_WORKS_APP_SYNC_ENABLEDenvtrue — R-30 kill switch; false pauses APW-02 sync and hygiene writes to user forksAPW-02
EVER_WORKS_APP_PROVISION_ENABLEDenvtrue — R-30 kill switch; false pauses new App Provisioner runs (a parked run keeps its place)APW-04
EVER_WORKS_APP_BUILDS_ENABLEDenvtrue — R-30 kill switch; false pauses workflow writes, secret sync and new BuildsAPW-05
EVER_WORKS_APP_DEPS_ENABLEDenvtrue — R-30 kill switch; false pauses dependency provisioningAPW-07
EVER_WORKS_APP_AUTO_DEPLOY_ENABLEDenvtrue — R-30 kill switch; false pauses auto-delivery (APW-08) and auto-deploy (APW-06); manual deploys stay availableAPW-06 / APW-08
EVER_WORKS_APP_UPSTREAM_PRS_ENABLEDenvtrue — R-30 kill switch; false refuses new upstream PR opens and pushes (Wave 2's only switch)APW-09
EVER_WORKS_APP_CHANGES_ENABLEDenvtrue — 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 parksAPW-08
APP_WORKS_CLOUD_PUSH_ENABLEDenvfalse — 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_ENABLEDenvtrue — R-30 kill switch; false stops the relay accepting new messages (queued ones are refused, not dropped)APW-07
allowBuildValuesOnPullRequestsplugin settingfalse — 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
verificationPromptedValuesRequireApprovalplugin settingtrue — 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_ACCOUNTenv1000 — per-account daily cap on the platform mail relay (added by APW-07, XC-21)APW-07
EVER_WORKS_APP_RELAY_DAILY_LIMIT_PER_ORGANIZATIONenv5000 — per-organization daily cap on the same relay (added by APW-07, XC-21)APW-07
EVER_WORKS_APP_RELAY_SUSPEND_BOUNCE_RATEenv5% — 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_REFenvunset — the ref the agent-template catalog reads; APW-04 T33 pins it per environment (added by APW-04)APW-04
EVER_WORKS_APPS_LOCAL_WORKERenvfalse — 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_URLenvunset — 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_SEEDenvunset — 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_ALLOWLISTenvempty — comma-separated CIDRs exempt from the public-address rule for dependency endpoints (added by APW-07)APW-07
EVER_WORKS_DB_PROVISION_IT_URLenv (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)​

RepositoryOwnerContent
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-03manifest.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-13one 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-13the acceptance fixture application (source only, no .works/); its Blueprint is ever-works/app-fixture-hello-template
<e2e-upstream-org>/* (test organization, placeholder name)APW-13per-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-provisionerAPW-04agent template
ever-works/build-on-open-source-app-mission-template (the seed repository APW-08 creates and targets)APW-08the 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-contributionAPW-04 / APW-08 / APW-09Skills — 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-08Mission 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-11platforms.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-11platforms.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)​

NameOwnerConsumers
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-05APW-02, 04, 08
Branch ever-works/build-workflow — the pull request branch for workflow changesAPW-05APW-02, 08
Repository Actions secret prefix EW_ (EW_<ENV NAME> for build.args[].fromEnv); the platform removes only EW_ secrets it wroteAPW-05APW-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 digestAPW-05APW-04, 06, 13
Actions secret EW_VERIFY__PROMPTED — per-verification-run JSON of already-set prompted values, deleted when the run endsAPW-05APW-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 registryAPW-05APW-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.

IdOwnerAudienceUrgencyDedupe keyCopy / i18n keys
app_sync_conflictAPW-02membernormalwork:<id>:conflictUpstream conflict needs a decision (existing keys)
app_build_failedAPW-05membernormalwork:<id>:build:<sha>"The build of your change failed"
app_provision_needs_inputAPW-04membernormalprovisioning:<id>:question"The App Provisioner needs an answer"
app_deploy_failedAPW-06membernormalwork:<id>:deploy:<n>"Your App Work did not come up" (APW-06 plan §9.4)
app_unhealthyAPW-06membernormalwork:<id>:health"Your App Work is not responding"
app_recoveredAPW-06memberlowwork:<id>:recovered"Your App Work is healthy again"
app_cluster_unreachableAPW-06memberhighwork:<id>:cluster"We can't reach your cluster"
app_pull_token_expiryAPW-05memberlowwork:<id>:token"Your registry pull token expires soon" (FR-51)
app_backup_overdueAPW-07membernormaldependency:<id>:backup"A dependency backup is overdue"
app_tier_usage_80APW-10membernormalwork:<id>:usage:<window>"You have used 80% of your allowance" (FR-36)
app_tier_quarantinedAPW-10member + operatorhighwork:<id>:quarantineOwner copy + operator page (FR-39)
app_tier_egressAPW-10operatorhighwork:<id>:egressOperator page — egress threshold crossed
app_attestation_expiryAPW-10membernormalattestation:<id>"Your hosting attestation expires soon"
app_upstream_pr_decisionAPW-09membernormalpr:<id>:decision"A pull request is waiting for your decision"
app_mail_relay_suspendedAPW-07operatorhighrelay:<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.

CapDefault (member)Default (organization)OverrideOwner
Active App Works25200EVER_WORKS_APPS_MAX_ACTIVEAPW-01
Creates per day20100EVER_WORKS_APPS_MAX_CREATES_PER_DAYAPW-01
Private copies (each ≤ 500 MB, FR-20)1050EVER_WORKS_APPS_MAX_PRIVATE_COPIESAPW-01
Sync now calls per hour20100EVER_WORKS_APP_SYNC_MAX_PER_HOURAPW-02
Push builds per day50300EVER_WORKS_APP_BUILDS_MAX_PER_DAYAPW-05
Runner-minute warning band per day600 (warn)3 000 (warn)EVER_WORKS_APP_BUILD_MINUTES_WARNAPW-05
Concurrent provisions210EVER_WORKS_APP_PROVISION_MAX_ACTIVEAPW-04
Deploys per day30200EVER_WORKS_APP_DEPLOYS_MAX_PER_DAYAPW-06
Upstream PRs open (per member)5—existing FR limits (APW-09)APW-09
Upstream PR opens per day320EVER_WORKS_APP_UPSTREAM_PR_MAX_PER_DAYAPW-09
Upstream PR opens per upstream per day (platform-wide)55EVER_WORKS_APP_UPSTREAM_PLATFORM_MAX_PER_DAYAPW-09
Mail relay messages per App Work/day200 (FR-39)2 000EVER_WORKS_APP_MAIL_MAX_PER_DAYAPW-07
Managed tier App Works (paid)existing EVER_WORKS_APPS_MAX_PER_USER = 3existing 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).

BoundaryPrimary threatsIndex rows
B-1 — Untrusted repository content → the App Provisioner and the evolve agentsprompt 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 runnerssecret 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 artifactcredential theft by unreviewed PR codeT-08…T-12
B-4 — User-supplied kubeconfigs dialled by the cluster-op workerSSRF, credential captureT-13…T-16
B-5 — Tenant workloads on Ever Works Apps → the zone, other tenants and the platformtenant 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 chainsupply-chain poisoning (R-29)T-21…T-23
B-7 — Delegated Ever ID tokens + cross-origin launcher reads → GET /api/me/appstoken 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 URLfake provider reachable in productionT-26
B-9 — Upstream repositories ← pull requests sent in the member's namereputation 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 clientsSSRF against platform clientsT-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 routesautomation 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.

SignalSourceThreshold / stuck stateAlertOwner
Build lost / silentwork_builds + sweeprunning with no update > 90 s, twiceapp_build_lost_rateAPW-05
Deploy lock stalework_app_runtime_stateslock held > 15 min with no phase changeapp_deploy_lock_staleAPW-06
Health-poll backlogapp-health-pollpoll overruns its 500/min budget twiceapp_health_poll_backlogAPW-06
Provisioning parkedwork_app_provisioningsparkedReason set > 8 happ_provision_parkedAPW-04
Fork readiness stuckwork_upstream_statespreparing > readiness timeout × 3app_fork_readiness_stuckAPW-02
Upstream sync rate-limited streakAPW-02 sync dispatcher≥ 3 consecutive secondary_rate_limitedapp_upstream_sync_rate_limited_streakAPW-02
Upstream PR polling deniedupstream_pull_requests≥ 5 consecutive poll failuresapp_upstream_pr_poll_failingAPW-09
Deletion pendingWorkAppRuntimeStatedeletionRequestedAt > 60 minapp_deletion_pendingAPW-01
Tier heartbeat staleapps-tier-self-checkno successful self-check > 6 happs_tier_heartbeat_staleAPW-10
Queue lagjob-runtime provideroldest queued job > 5 min (any app-* queue)app_job_queue_lagall
Activity/notification delivery failureActivityLogService, mail≥ 10 failures in 10 minapp_activity_delivery_failingall

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.

CodeStatusSituationOwneri18n leaf
app_works_disabled404App Works off at the API (§7/R-6)APW-01appWorksDisabled
not_app_work404the Work exists but is not kind app (one code, all epics)APW-01notAppWork
app_runtime_unavailable503runtime target unreachableAPW-06appRuntimeUnavailable
not_found404the App Work does not exist or is another account's (R-36)APW-01notFound
sync_in_progress409a sync is already runningAPW-02syncInProgress
retry_limit_reached429readiness retry cap (§7A)APW-02retryLimitReached
provisioning_unavailable422no runtime that can provisionAPW-04provisioningUnavailable
build_not_found404unknown build idAPW-05buildNotFound
rebuild_rate_limited429rebuild cap (§7A)APW-05rebuildRateLimited
secure_storage_unavailable503encrypted env store unavailableAPW-07secureStorageUnavailable
confirmation_mismatch400typed confirmSlug does not match (R-32)APW-07confirmationMismatch
active_proposal409an open upstream-PR proposal existsAPW-09activeProposal
rate_limited429any per-route throttle, details.scope names which (APW-09 §5)APW-09rateLimited
quota_exceeded429a §7A cap, details.cap names whichallquotaExceeded
switch_off503an R-30 operator switch is offallswitchOff
apps_tier_open_refused409the tier gate refuses this deploymentAPW-10appsTierOpenRefused
quota_ceiling_exceeded429tenant quota ceilingAPW-10quotaCeilingExceeded
ever_id_disabled404Ever ID plugin off (APW-12 §5.2)APW-12everIdDisabled
transaction_invalid400bad/expired/replayed Ever ID transactionAPW-12transactionInvalid
token_in_query400a token in a query parameter (FR-17)APW-12tokenInQuery
ever_id_signed_out401the session was ended by a sign-out notice (S6)APW-12signedOutByProvider

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.