@wtfalch/foundry 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/bootstrap/declare.js +4 -1
  2. package/dist/bootstrap/versions.d.ts +2 -0
  3. package/dist/index.d.ts +9 -1
  4. package/dist/index.js +3 -0
  5. package/dist/migrations/0065_native_build_runs.sql +32 -0
  6. package/dist/migrations/0066_org_kosmos.sql +10 -0
  7. package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
  8. package/dist/migrations/0068_platform_registry.sql +17 -0
  9. package/dist/modules/sign-in.js +1 -1
  10. package/dist/native-build.d.ts +50 -0
  11. package/dist/native-build.js +72 -0
  12. package/dist/org.d.ts +22 -3
  13. package/dist/org.js +25 -2
  14. package/dist/plan/agora.d.ts +1 -1
  15. package/dist/plan/agora.js +4 -0
  16. package/dist/plan/archon-blue-green.d.ts +5 -0
  17. package/dist/plan/archon-blue-green.js +2 -0
  18. package/dist/plan/blue-green-apply.d.ts +5 -0
  19. package/dist/plan/blue-green-apply.js +197 -22
  20. package/dist/plan/blue-green.d.ts +80 -9
  21. package/dist/plan/blue-green.js +115 -14
  22. package/dist/plan/boule.d.ts +1 -1
  23. package/dist/plan/boule.js +2 -0
  24. package/dist/plan/broker-entries.d.ts +9 -0
  25. package/dist/plan/broker-entries.js +29 -0
  26. package/dist/plan/deploy-health.js +9 -2
  27. package/dist/plan/desktop-callback.js +4 -1
  28. package/dist/plan/diff.d.ts +5 -0
  29. package/dist/plan/diff.js +184 -63
  30. package/dist/plan/edge-access.d.ts +2 -3
  31. package/dist/plan/edge-access.js +8 -0
  32. package/dist/plan/executor.d.ts +111 -19
  33. package/dist/plan/executor.js +578 -72
  34. package/dist/plan/index.d.ts +2 -0
  35. package/dist/plan/index.js +1 -0
  36. package/dist/plan/jobs-scheduler-http.d.ts +2 -0
  37. package/dist/plan/jobs-scheduler-http.js +11 -0
  38. package/dist/plan/kosmos.d.ts +56 -0
  39. package/dist/plan/kosmos.js +84 -0
  40. package/dist/plan/locked-hostnames.js +5 -2
  41. package/dist/plan/org-names.d.ts +3 -3
  42. package/dist/plan/org-names.js +3 -1
  43. package/dist/plan/org-postgres-migrate.js +1 -0
  44. package/dist/plan/own-limits.d.ts +12 -0
  45. package/dist/plan/own-limits.js +2 -0
  46. package/dist/plan/plan-apply.d.ts +51 -1
  47. package/dist/plan/plan-apply.js +298 -25
  48. package/dist/plan/platform-diff.d.ts +6 -0
  49. package/dist/plan/platform-diff.js +74 -0
  50. package/dist/plan/platform-org.d.ts +2 -0
  51. package/dist/plan/platform-providers.d.ts +46 -1
  52. package/dist/plan/platform-providers.js +162 -1
  53. package/dist/plan/platform-registry-keys.d.ts +58 -0
  54. package/dist/plan/platform-registry-keys.js +69 -0
  55. package/dist/plan/platform-registry.d.ts +67 -0
  56. package/dist/plan/platform-registry.js +298 -0
  57. package/dist/plan/post-logout-uris.d.ts +1 -1
  58. package/dist/plan/post-logout-uris.js +5 -2
  59. package/dist/plan/providers-readers.js +20 -1
  60. package/dist/plan/release-host.d.ts +31 -2
  61. package/dist/plan/release-host.js +114 -6
  62. package/dist/plan/stage-copy-host.d.ts +104 -0
  63. package/dist/plan/stage-copy-host.js +259 -0
  64. package/dist/plan/stage-copy.js +44 -0
  65. package/dist/plan/stage-env.d.ts +9 -0
  66. package/dist/plan/stage-env.js +38 -0
  67. package/dist/plan/stage-guards.d.ts +57 -1
  68. package/dist/plan/stage-guards.js +106 -0
  69. package/dist/plan/stage-isolation.js +56 -0
  70. package/dist/plan/stage-services.d.ts +93 -0
  71. package/dist/plan/stage-services.js +153 -0
  72. package/dist/plan/stage-signin.d.ts +152 -0
  73. package/dist/plan/stage-signin.js +133 -0
  74. package/dist/plan/stage-sink.d.ts +2 -0
  75. package/dist/plan/stage-sink.js +6 -1
  76. package/dist/platform.d.ts +7 -1
  77. package/dist/platform.js +1 -0
  78. package/dist/release/decide.d.ts +12 -0
  79. package/dist/release/decide.js +37 -2
  80. package/dist/release/migration-compatibility.d.ts +66 -0
  81. package/dist/release/migration-compatibility.js +340 -0
  82. package/dist/release/run.d.ts +71 -1
  83. package/dist/release/run.js +92 -8
  84. package/dist/release/state.d.ts +14 -1
  85. package/dist/release/state.js +43 -0
  86. package/dist/release/types.d.ts +20 -0
  87. package/dist/release/types.js +2 -0
  88. package/dist/requested-changes.d.ts +11 -2
  89. package/dist/requested-changes.js +13 -1
  90. package/dist/slug.d.ts +1 -1
  91. package/dist/slug.js +11 -2
  92. package/dist/store/postgres.js +21 -0
  93. package/dist/store/schema.d.ts +87 -0
  94. package/dist/store/schema.js +10 -0
  95. package/package.json +3 -2
  96. package/versions.digests.json +1 -0
  97. package/versions.json +1 -0
@@ -53,6 +53,7 @@ const SERVICES_ENABLED_BY_DEFAULT = new Set([
53
53
  'archon',
54
54
  'edge',
55
55
  'hc',
56
+ 'registry',
56
57
  ]);
57
58
  /**
58
59
  * The services every company deploys its own copy of. Their platform entries
@@ -153,7 +154,9 @@ export function declaredPlatformFor(apex, serverIpv4) {
153
154
  ? DEFAULT_MAIL_IMAGE_TAG
154
155
  : key === 'hc'
155
156
  ? DEFAULT_HC_IMAGE_TAG
156
- : DEFAULT_GIT_REF,
157
+ : key === 'registry'
158
+ ? VERSIONS.registry
159
+ : DEFAULT_GIT_REF,
157
160
  };
158
161
  }
159
162
  return {
@@ -12,6 +12,8 @@ export interface PinnedVersions {
12
12
  cloudflared: string;
13
13
  stalwart: string;
14
14
  hc: string;
15
+ /** The platform's image registry (`registry:3`, Docker's `distribution`). */
16
+ registry: string;
15
17
  /** The mail relay's base image (an exact Alpine release); Postfix comes from its package repository. */
16
18
  alpine: string;
17
19
  /** A static busybox, copied into the MAS image so its health check has an HTTP client. */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, HostingGithub, BlueGreenConfig, OrgBuiltinApp, BouleApp, OrgRefusal, OrgRefusalCode, WorkerAppConfig, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryAuthRoom, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceDatabase, OrgServiceSchema, EntryWorkspace, ServicesConfig, SignInConfig, VersionPin, VersionPinPart, } from './org.js';
1
+ export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, HostingGithub, BlueGreenConfig, OrgBuiltinApp, BouleApp, KosmosApp, OrgRefusal, OrgRefusalCode, WorkerAppConfig, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryAuthRoom, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceDatabase, OrgServiceSchema, EntryWorkspace, ServicesConfig, SignInConfig, VersionPin, VersionPinPart, } from './org.js';
2
2
  export { APP_ENTRY_KEY, APPS_DOMAIN, DEFAULT_GATE_LIMIT, MAX_WORKSPACE_TESTERS, isLiveEntry, normalizeTesters, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, orgServiceDatabase, orgServiceDatabases, SFU_SLOT_MAX, VERSION_PIN_PARTS, isMainEntry, mainEntry, companyGithubFor, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, normalizeHostingRepos, validateMail, validateOwners, validateServices, validateCustomers, validateSignIn, validateVersionPin, validateSite, } from './org.js';
3
3
  export { refuseOrg } from './refuse-org.js';
4
4
  export { workerAppHasBuild } from './worker-app-config.js';
@@ -15,6 +15,8 @@ export type { SlugRejectionReason, SlugRejection } from './slug.js';
15
15
  export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
16
16
  export type { Checkpoint, LastApplied, AppliedState, DestroyRecord, DestroyStepRecord, } from './state.js';
17
17
  export type { PlanStep, UnmanagedItem, PlanNote, Plan, VersionChangeItem } from './plan.js';
18
+ export type { NativeBuildDispatchDeps } from './native-build.js';
19
+ export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild, } from './native-build.js';
18
20
  export type { AppliedVersion, AppliedVersionsStore, DeclaredOrgStore, HeldLease, MutationLease, PlatformStore, RequestedChangeStore, StaleUpdate, UpdateOptions, } from './store.js';
19
21
  export { StaleLeaseError } from './store.js';
20
22
  export type { PlanApply, DestroyProgress } from './apply.js';
@@ -23,6 +25,8 @@ export { createPostgresAuditLog } from './store/audit.js';
23
25
  export type { Color, ReleaseAction, ReleaseSignal, ReleaseState, WindowState, } from './release/types.js';
24
26
  export type { ReleaseStore } from './release/state.js';
25
27
  export { readReleaseState, requestReleaseAction } from './release/state.js';
28
+ export type { DeclaredClassRemoval, MigrationCompatibilityInput, MigrationCompatibilityProblem, MigrationCompatibilityResult, } from './release/migration-compatibility.js';
29
+ export { checkMigrationCompatibility } from './release/migration-compatibility.js';
26
30
  export { createPostgresReleaseStore } from './store/postgres.js';
27
31
  export type { FoundryStoreDb } from './store/postgres.js';
28
32
  export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryReleaseRequests, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
@@ -36,6 +40,8 @@ export { STAGE_COPY_EXCLUDED_TABLES, StageCopyFailedError, StageCopyRefusedError
36
40
  export type { StageCopyInput, StageCopyPort, StageCopyProduction, StageCopyRunRecord, UndoStageCopyInput, } from './plan/index.js';
37
41
  export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execRunner, } from './plan/index.js';
38
42
  export type { Runner, StageCopyPortDeps } from './plan/index.js';
43
+ export type { PlatformRegistryDeps, PlatformRegistryRuntime } from './plan/index.js';
44
+ export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './plan/index.js';
39
45
  export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
40
46
  export type { OrgPostgresMigrationConfirmation } from './plan/index.js';
41
47
  export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
@@ -55,6 +61,8 @@ export type { PlanOwnSfuOptions, SfuAppDeps, SfuAppInput } from './plan/index.js
55
61
  export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuCompose, } from './plan/index.js';
56
62
  export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, KeysRegistrarPort, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, LoginHostDiscoveryPort, } from './plan/index.js';
57
63
  export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
64
+ export type { StageProbePort, ReleasePreviewMembers } from './plan/stage-guards.js';
65
+ export type { StageCopyHostConfig } from './plan/stage-copy-host.js';
58
66
  export { NO_DOMAIN_ZONE, entryMachinePaths, ownHostnames, privatePageGateKey, workspaceHost, } from './plan/index.js';
59
67
  export type { PrivatePageScope } from './plan/index.js';
60
68
  export type { HostedLabelRefusal } from './hosted-label.js';
package/dist/index.js CHANGED
@@ -10,10 +10,12 @@ export const PINNED_VERSIONS = VERSIONS;
10
10
  export { buildJsonSchema, parseBuildJson, BuildJsonError } from './build-json.js';
11
11
  export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
12
12
  export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
13
+ export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild, } from './native-build.js';
13
14
  export { StaleLeaseError } from './store.js';
14
15
  export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
15
16
  export { createPostgresAuditLog } from './store/audit.js';
16
17
  export { readReleaseState, requestReleaseAction } from './release/state.js';
18
+ export { checkMigrationCompatibility } from './release/migration-compatibility.js';
17
19
  export { createPostgresReleaseStore } from './store/postgres.js';
18
20
  export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryReleaseRequests, foundryRequestedChanges, } from './store/schema.js';
19
21
  export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
@@ -22,6 +24,7 @@ export { DumpError, dumpNow } from './plan/index.js';
22
24
  export { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_IMAGE_REF, stageMailEnv, stageSinkAppName, } from './plan/index.js';
23
25
  export { STAGE_COPY_EXCLUDED_TABLES, StageCopyFailedError, StageCopyRefusedError, stageCopy, tableExcluded, undoStageCopy, } from './plan/index.js';
24
26
  export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execRunner, } from './plan/index.js';
27
+ export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './plan/index.js';
25
28
  export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
26
29
  export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
27
30
  export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
@@ -0,0 +1,32 @@
1
+ -- @wtfalch/foundry: a company's native app settings and its native build runs (foundry#948, estate
2
+ -- `.plans/2026-10-22-app-shells.md`). Additive, like 0060_hosting_repos.sql and
3
+ -- 0055_release_requests.sql: a column with a default plus a new table, so a row written before this
4
+ -- reads back `[]` and an older reader ignores both. Safe on live rows when the container starts.
5
+ --
6
+ -- foundry#951: Boule owns the native settings, build runs and store keys now (estate#262) -- the
7
+ -- column and the table below are unused by this package's code from here on, but foundry#951 also
8
+ -- establishes that a migration only adds in a release where idle and live share a database, so
9
+ -- neither is dropped here. A later release drops both.
10
+
11
+ alter table foundry_declared_orgs
12
+ add column if not exists hosting_native_apps jsonb not null default '[]'::jsonb;
13
+
14
+ create table if not exists foundry_native_build_runs (
15
+ id bigserial primary key,
16
+ org_id text not null,
17
+ app text not null,
18
+ state text not null default 'queued',
19
+ log text,
20
+ created_at timestamptz not null default now(),
21
+ updated_at timestamptz not null default now()
22
+ );
23
+
24
+ alter table foundry_native_build_runs
25
+ drop constraint if exists foundry_native_build_runs_state_check;
26
+ alter table foundry_native_build_runs
27
+ add constraint foundry_native_build_runs_state_check
28
+ check (state in ('queued', 'building', 'uploaded', 'failed'));
29
+
30
+ -- Listing one app's runs, and (while foundry still checked it, before foundry#951) the `uploaded` look-up.
31
+ create index if not exists foundry_native_build_runs_app_idx
32
+ on foundry_native_build_runs (org_id, app, id desc);
@@ -0,0 +1,10 @@
1
+ -- @wtfalch/foundry: `kosmos_enabled` is `org.kosmos.enabled` (foundry#949): the company's own kosmos is set up
2
+ -- only when it asks. `kosmos_blue_green` is `org.kosmos.blueGreen` ({ steps?, stepHoldMinutes? }, as the other
3
+ -- two built-ins' own columns hold it) -- kept even though kosmos always runs blue/green once enabled, so tuning
4
+ -- its steps needs no new column later. Both default to off. Additive like 0059_org_builtin_blue_green.sql and
5
+ -- 0063_boule_apps.sql: `if not exists`, safe to run at container start on live rows.
6
+ -- Mirrors src/store/schema.ts's `declaredOrgs`.
7
+
8
+ alter table foundry_declared_orgs
9
+ add column if not exists kosmos_enabled boolean not null default false,
10
+ add column if not exists kosmos_blue_green jsonb;
@@ -0,0 +1,6 @@
1
+ -- @wtfalch/foundry: a company's Boule can request `kosmos` (turn `org.kosmos.enabled` on or off).
2
+ alter table foundry_requested_changes
3
+ drop constraint if exists foundry_requested_changes_field_check;
4
+ alter table foundry_requested_changes
5
+ add constraint foundry_requested_changes_field_check
6
+ check (field in ('calls', 'customers', 'storageLimit', 'aiLimit', 'domain', 'mailTier', 'app', 'build', 'bouleApps', 'kosmos'));
@@ -0,0 +1,17 @@
1
+ -- @wtfalch/foundry: the platform's image registry (`registry:3`), declared with the rest of the
2
+ -- platform. Mirrors `src/store/schema.ts`'s `foundryDeclaredPlatform` additions. Off for the
3
+ -- platform that already exists: foundry deploys nothing new until it is declared on. Backfills
4
+ -- `registry_hostname` the way `0047_platform_coolify.sql` backfilled `coolify_hostname`: the
5
+ -- column must not be empty (`src/platform.ts`'s `validatePlatform`).
6
+
7
+ alter table foundry_declared_platform
8
+ add column if not exists registry_enabled boolean not null default false,
9
+ add column if not exists registry_hostname text,
10
+ add column if not exists registry_git_ref text not null default '';
11
+
12
+ update foundry_declared_platform
13
+ set registry_hostname = 'registry.' || apex
14
+ where registry_hostname is null;
15
+
16
+ alter table foundry_declared_platform
17
+ alter column registry_hostname set not null;
@@ -55,7 +55,7 @@ export const signIn = {
55
55
  label: 'Who can make an account',
56
56
  kind: 'choice',
57
57
  required: true,
58
- default: 'open',
58
+ default: 'closed',
59
59
  choices: [
60
60
  {
61
61
  value: 'open',
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `wtfalch/shell`'s `build-app.yml`: the one GitHub Actions workflow that
3
+ * builds and signs a company's native app shell (estate
4
+ * `.plans/2026-10-22-app-shells.md`). Boule now owns the `NativeAppConfig`,
5
+ * the build runs and the company's store keys (estate#262, foundry#951);
6
+ * foundry keeps only two things, both in this file: dispatching the
7
+ * workflow, and the key Boule presents when it asks the host (Archon) to
8
+ * dispatch it.
9
+ *
10
+ * `dispatchNativeBuild` runs inside Archon's own `/api/native/builds`
11
+ * route, not foundry's apply loop. The key itself is minted by
12
+ * `plan/executor.ts`'s `coolifyBoule` (`ensureNativeBuildKey`), reusing the
13
+ * same `mintIfAbsent` path every other per-company Boule secret goes
14
+ * through (foundry#970) -- this file keeps only the env name and the byte
15
+ * length, not a second mint function (foundry#957 review note 2 and
16
+ * thermo note 1: the earlier `mintNativeBuildKey` was a third, buggy copy
17
+ * of that same check).
18
+ */
19
+ export declare const NATIVE_BUILD_REPO = "wtfalch/shell";
20
+ export declare const NATIVE_BUILD_WORKFLOW_FILE = "build-app.yml";
21
+ export declare const NATIVE_BUILD_WORKFLOW_REF = "main";
22
+ /** Every GitHub call this module makes is cut off after this long. */
23
+ export declare const NATIVE_BUILD_DISPATCH_TIMEOUT_MS = 20000;
24
+ /** A `build-app.yml` dispatch that failed. The text is fixed: it never holds the token or a response body. */
25
+ export declare class NativeBuildDispatchError extends Error {
26
+ readonly status: number | null;
27
+ constructor(status: number | null);
28
+ }
29
+ export interface NativeBuildDispatchDeps {
30
+ /** A GitHub token that may dispatch workflows in {@link NATIVE_BUILD_REPO}. */
31
+ token: string;
32
+ /** Global fetch by default; tests give their own. */
33
+ fetchImpl?: typeof fetch;
34
+ timeoutMs?: number;
35
+ }
36
+ /**
37
+ * Dispatches `build-app.yml` with exactly `{ runId, origin }` as its inputs:
38
+ * `runId` is the run the caller (Archon, behind its own `/api/native/builds`
39
+ * route) already made, `origin` is the company's Boule address the job
40
+ * reports its status back to and trades its OIDC token for keys at. Foundry
41
+ * keeps no run store of its own any more -- Boule does -- so this is the one
42
+ * thing left for foundry to do with a run: start it.
43
+ */
44
+ export declare function dispatchNativeBuild(deps: NativeBuildDispatchDeps, run: {
45
+ runId: string;
46
+ origin: string;
47
+ }): Promise<void>;
48
+ /** The env name Boule presents this key under, to Archon's native-build route. */
49
+ export declare const NATIVE_BUILD_KEY_ENV = "NATIVE_BUILD_KEY";
50
+ export declare const NATIVE_BUILD_KEY_BYTES = 32;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `wtfalch/shell`'s `build-app.yml`: the one GitHub Actions workflow that
3
+ * builds and signs a company's native app shell (estate
4
+ * `.plans/2026-10-22-app-shells.md`). Boule now owns the `NativeAppConfig`,
5
+ * the build runs and the company's store keys (estate#262, foundry#951);
6
+ * foundry keeps only two things, both in this file: dispatching the
7
+ * workflow, and the key Boule presents when it asks the host (Archon) to
8
+ * dispatch it.
9
+ *
10
+ * `dispatchNativeBuild` runs inside Archon's own `/api/native/builds`
11
+ * route, not foundry's apply loop. The key itself is minted by
12
+ * `plan/executor.ts`'s `coolifyBoule` (`ensureNativeBuildKey`), reusing the
13
+ * same `mintIfAbsent` path every other per-company Boule secret goes
14
+ * through (foundry#970) -- this file keeps only the env name and the byte
15
+ * length, not a second mint function (foundry#957 review note 2 and
16
+ * thermo note 1: the earlier `mintNativeBuildKey` was a third, buggy copy
17
+ * of that same check).
18
+ */
19
+ export const NATIVE_BUILD_REPO = 'wtfalch/shell';
20
+ export const NATIVE_BUILD_WORKFLOW_FILE = 'build-app.yml';
21
+ export const NATIVE_BUILD_WORKFLOW_REF = 'main';
22
+ /** Every GitHub call this module makes is cut off after this long. */
23
+ export const NATIVE_BUILD_DISPATCH_TIMEOUT_MS = 20_000;
24
+ /** A `build-app.yml` dispatch that failed. The text is fixed: it never holds the token or a response body. */
25
+ export class NativeBuildDispatchError extends Error {
26
+ status;
27
+ constructor(status) {
28
+ super(status === null
29
+ ? `dispatching ${NATIVE_BUILD_WORKFLOW_FILE} did not answer`
30
+ : `dispatching ${NATIVE_BUILD_WORKFLOW_FILE} failed with status ${status}`);
31
+ this.status = status;
32
+ this.name = 'NativeBuildDispatchError';
33
+ }
34
+ }
35
+ /**
36
+ * Dispatches `build-app.yml` with exactly `{ runId, origin }` as its inputs:
37
+ * `runId` is the run the caller (Archon, behind its own `/api/native/builds`
38
+ * route) already made, `origin` is the company's Boule address the job
39
+ * reports its status back to and trades its OIDC token for keys at. Foundry
40
+ * keeps no run store of its own any more -- Boule does -- so this is the one
41
+ * thing left for foundry to do with a run: start it.
42
+ */
43
+ export async function dispatchNativeBuild(deps, run) {
44
+ const doFetch = deps.fetchImpl ?? fetch;
45
+ const timeoutMs = deps.timeoutMs ?? NATIVE_BUILD_DISPATCH_TIMEOUT_MS;
46
+ let response;
47
+ try {
48
+ response = await doFetch(`https://api.github.com/repos/${NATIVE_BUILD_REPO}/actions/workflows/${NATIVE_BUILD_WORKFLOW_FILE}/dispatches`, {
49
+ method: 'POST',
50
+ headers: {
51
+ Authorization: `Bearer ${deps.token}`,
52
+ Accept: 'application/vnd.github+json',
53
+ 'X-GitHub-Api-Version': '2022-11-28',
54
+ 'Content-Type': 'application/json',
55
+ },
56
+ body: JSON.stringify({
57
+ ref: NATIVE_BUILD_WORKFLOW_REF,
58
+ inputs: { runId: run.runId, origin: run.origin },
59
+ }),
60
+ signal: AbortSignal.timeout(timeoutMs),
61
+ });
62
+ }
63
+ catch (error) {
64
+ console.error(`native build: dispatch did not answer (${error instanceof Error ? error.name : 'error'})`);
65
+ throw new NativeBuildDispatchError(null);
66
+ }
67
+ if (!response.ok)
68
+ throw new NativeBuildDispatchError(response.status);
69
+ }
70
+ /** The env name Boule presents this key under, to Archon's native-build route. */
71
+ export const NATIVE_BUILD_KEY_ENV = 'NATIVE_BUILD_KEY';
72
+ export const NATIVE_BUILD_KEY_BYTES = 32;
package/dist/org.d.ts CHANGED
@@ -301,7 +301,7 @@ export type AccessRule = {
301
301
  /** The key of the entry that is today's one app: the Coolify app named `<slug>`. */
302
302
  export declare const APP_ENTRY_KEY = "app";
303
303
  /** Why a validator refused. `code` is stable and never reworded; `message` is for people. */
304
- export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'hosting_github_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'hosted_name_invalid' | 'hosted_name_not_unique' | 'hosted_name_worker_app' | 'entry_workspace_invalid' | 'entry_workspace_unsupported' | 'entry_live_needs_workspace' | 'entry_live_commit_invalid' | 'gate_key_taken_by_workspace' | 'gate_key_taken_by_private_page' | 'private_page_invalid' | 'private_page_path_invalid' | 'private_page_duplicate' | 'private_page_limit' | 'private_page_needs_hosting' | 'private_page_roles_invalid' | 'private_page_people_invalid' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'blue_green_advance_invalid' | 'blue_green_min_requests_invalid' | 'blue_green_error_margin_invalid' | 'blue_green_staff_first_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'auth_room_invalid' | 'auth_room_needs_customers' | 'auth_room_not_unique' | 'client_name_taken' | 'workspaces_needs_customers' | 'workspaces_needs_server' | 'workspaces_roles_invalid' | 'workspaces_role_reserved' | 'workspaces_invite_path_invalid' | 'workspaces_env_clash' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_needs_own_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'mail_not_true' | 'mail_needs_hosting' | 'mail_env_clash' | 'jobs_not_true' | 'jobs_needs_hosting' | 'jobs_env_clash' | 'customer_grants_not_true' | 'customer_grants_needs_customers' | 'customer_grants_env_clash' | 'repo_use_invalid' | 'repo_use_needs_repo' | 'repo_use_env_clash' | 'audit_not_true' | 'audit_needs_hosting' | 'audit_needs_worker_database' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
304
+ export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'issuer_app_name_reserved_kosmos' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'hosting_github_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'hosted_name_invalid' | 'hosted_name_not_unique' | 'hosted_name_worker_app' | 'entry_workspace_invalid' | 'entry_workspace_unsupported' | 'entry_live_needs_workspace' | 'entry_live_commit_invalid' | 'gate_key_taken_by_workspace' | 'gate_key_taken_by_private_page' | 'private_page_invalid' | 'private_page_path_invalid' | 'private_page_duplicate' | 'private_page_limit' | 'private_page_needs_hosting' | 'private_page_roles_invalid' | 'private_page_people_invalid' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'blue_green_advance_invalid' | 'blue_green_min_requests_invalid' | 'blue_green_error_margin_invalid' | 'blue_green_staff_first_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'auth_room_invalid' | 'auth_room_needs_customers' | 'auth_room_not_unique' | 'client_name_taken' | 'workspaces_needs_customers' | 'workspaces_needs_server' | 'workspaces_roles_invalid' | 'workspaces_role_reserved' | 'workspaces_invite_path_invalid' | 'workspaces_env_clash' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_needs_own_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'mail_not_true' | 'mail_needs_hosting' | 'mail_env_clash' | 'jobs_not_true' | 'jobs_needs_hosting' | 'jobs_env_clash' | 'customer_grants_not_true' | 'customer_grants_needs_customers' | 'customer_grants_env_clash' | 'repo_use_invalid' | 'repo_use_needs_repo' | 'repo_use_env_clash' | 'audit_not_true' | 'audit_needs_hosting' | 'audit_needs_worker_database' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
305
305
  /**
306
306
  * One refusal: a stable `code`, the dotted `path` into {@link DeclaredOrg} it is about
307
307
  * (`hosting.entries.terminal.uses.auth.registration`) and the sentence for people.
@@ -484,6 +484,18 @@ export interface BouleApp extends OrgBuiltinApp {
484
484
  }
485
485
  /** True while the company's Boule runs apps (`org.boule.apps`). */
486
486
  export declare const appsOn: (org: Pick<DeclaredOrg, "boule">) => boolean;
487
+ /**
488
+ * The company's kosmos: its own creative workspace (canvas, libraries, photos, pages) that drives the
489
+ * frontend of its apps. Unlike Boule and Agora, which every company runs, kosmos is set up only when the
490
+ * company asks (William, 2026-10-22) -- absent, or `enabled` absent or `false`, plans nothing for it.
491
+ * `enabled: true` plans its ZITADEL client, its database and its live and idle copies; turning it back off
492
+ * parks the app but keeps the database, the same rule every other switched-off service follows.
493
+ */
494
+ export interface KosmosApp extends OrgBuiltinApp {
495
+ enabled?: boolean;
496
+ }
497
+ /** True while the company's kosmos is turned on (`org.kosmos.enabled`). */
498
+ export declare const kosmosOn: (org: Pick<DeclaredOrg, "kosmos">) => boolean;
487
499
  /** The traffic steps (percent to the new color) and the hold at each step. Both default in `plan/blue-green.ts`. */
488
500
  export interface BlueGreenConfig {
489
501
  /** Ascending, each 1..100, ending at 100. */
@@ -798,8 +810,13 @@ export declare function refuseHosting(hosting: HostingConfig, names?: HostingNam
798
810
  export declare function companyGithubFor(hosting: Pick<HostingConfig, 'github'>, repo: string | null): HostingGithub | undefined;
799
811
  /** `blueGreen`: steps ascending, each a whole number 1..100, the last 100; a hold of 1..1440 minutes. */
800
812
  export declare function validateBlueGreen(value: unknown, at: string): string | null;
801
- /** {@link refuseBlueGreen} for the `blueGreen` of the company's Boule and Agora; the same codes. */
802
- export declare function refuseBuiltinBlueGreen(org: Pick<DeclaredOrg, 'boule' | 'agora'>): OrgRefusal | null;
813
+ /**
814
+ * {@link refuseBlueGreen} for the `blueGreen` of the company's Boule, Agora and kosmos; the same codes.
815
+ * Also refuses an `issuerApps` entry named `kosmos` while kosmos is on -- the same reservation
816
+ * {@link refuseHosting}'s own loop already gives Agora and Boule, but kosmos is opt-in, so it is checked here
817
+ * instead, the same way {@link validateSite} reserves `cms` only while `site.enabled` is true.
818
+ */
819
+ export declare function refuseBuiltinBlueGreen(org: Pick<DeclaredOrg, 'boule' | 'agora' | 'kosmos' | 'hosting'>): OrgRefusal | null;
803
820
  /** The env names `uses.repo` writes; `env` may not name them. */
804
821
  export declare const REPO_ENTRY_ENV_NAMES: readonly ["GIT_REPO_URL", "GIT_SSH_KEY"];
805
822
  /** The env name `uses.customerGrants` writes; `env` may not name it. */
@@ -1226,6 +1243,8 @@ export interface DeclaredOrg {
1226
1243
  boule?: BouleApp;
1227
1244
  /** The company's Agora, a static app: its release moves on hard fails and time only. Absent means no blue/green. */
1228
1245
  agora?: OrgBuiltinApp;
1246
+ /** The company's kosmos. Absent or `enabled: false` means it is not set up (2026-10-22). */
1247
+ kosmos?: KosmosApp;
1229
1248
  /** Absent means `{ enabled: false, signUp: false }`. Optional so every existing literal and fixture compiles unchanged. */
1230
1249
  customers?: CustomersConfig;
1231
1250
  /** Parts of this company held on their own version. Absent or null follows the platform. Ignored for the owner company. */
package/dist/org.js CHANGED
@@ -33,6 +33,8 @@ export function normalizeTesters(testers) {
33
33
  }
34
34
  /** True while the company's Boule runs apps (`org.boule.apps`). */
35
35
  export const appsOn = (org) => org.boule?.apps === true;
36
+ /** True while the company's kosmos is turned on (`org.kosmos.enabled`). */
37
+ export const kosmosOn = (org) => org.kosmos?.enabled === true;
36
38
  /** The most volumes one entry may declare. */
37
39
  export const MAX_ENTRY_VOLUMES = 8;
38
40
  /** The env names `uses.auth.workspaces` writes; `env` may not name them. */
@@ -763,9 +765,14 @@ function refuseBlueGreen(value, at, path) {
763
765
  }
764
766
  return null;
765
767
  }
766
- /** {@link refuseBlueGreen} for the `blueGreen` of the company's Boule and Agora; the same codes. */
768
+ /**
769
+ * {@link refuseBlueGreen} for the `blueGreen` of the company's Boule, Agora and kosmos; the same codes.
770
+ * Also refuses an `issuerApps` entry named `kosmos` while kosmos is on -- the same reservation
771
+ * {@link refuseHosting}'s own loop already gives Agora and Boule, but kosmos is opt-in, so it is checked here
772
+ * instead, the same way {@link validateSite} reserves `cms` only while `site.enabled` is true.
773
+ */
767
774
  export function refuseBuiltinBlueGreen(org) {
768
- for (const app of ['boule', 'agora']) {
775
+ for (const app of ['boule', 'agora', 'kosmos']) {
769
776
  const value = org[app];
770
777
  if (value === undefined)
771
778
  continue;
@@ -781,6 +788,15 @@ export function refuseBuiltinBlueGreen(org) {
781
788
  ? 'boule.apps must be true or false'
782
789
  : `${app} has no apps switch: only the company's Boule runs apps`);
783
790
  }
791
+ const enabled = value.enabled;
792
+ if (enabled !== undefined && (app !== 'kosmos' || typeof enabled !== 'boolean')) {
793
+ return refusal('builtin_app_invalid', `${app}.enabled`, app === 'kosmos'
794
+ ? 'kosmos.enabled must be true or false'
795
+ : `${app} has no enabled switch: it is always on`);
796
+ }
797
+ }
798
+ if (kosmosOn(org) && org.hosting.issuerApps.some((app) => app.name === 'kosmos')) {
799
+ return refusal('issuer_app_name_reserved_kosmos', 'hosting.issuerApps.kosmos.name', "kosmos.enabled reserves the issuerApps name 'kosmos'");
784
800
  }
785
801
  return null;
786
802
  }
@@ -899,6 +915,7 @@ function validateEntryAuth(entry, hosting, domain) {
899
915
  if (entry.key === 'agora' ||
900
916
  entry.key === 'boule' ||
901
917
  entry.key === 'cms' ||
918
+ entry.key === 'kosmos' ||
902
919
  hosting.issuerApps.some((app) => app.name === entry.key)) {
903
920
  return refusal('client_name_taken', `hosting.entries.${entry.key}.key`, `${at} client name '${entry.key}' is already held by another ZITADEL client`);
904
921
  }
@@ -1881,6 +1898,8 @@ function orgServiceSchema(service, schema, legacyKey, split = true) {
1881
1898
  export const ORG_SERVICE_SCHEMAS = [
1882
1899
  orgServiceSchema('Boule', 'boule', () => 'coolify_boule_database'),
1883
1900
  orgServiceSchema('CMS', 'cms', () => 'coolify_cms_database'),
1901
+ // kosmos, like CMS, sits behind its own feature switch (`KosmosApp.enabled`); see `orgServiceSchemaEnabled`.
1902
+ orgServiceSchema('kosmos', 'kosmos', () => 'coolify_kosmos_database'),
1884
1903
  orgServiceSchema("chat's Synapse homeserver", 'chat_synapse', () => 'coolify_chat_synapse_database', false),
1885
1904
  orgServiceSchema("chat's matrix-authentication-service", 'chat_mas', () => 'coolify_chat_mas_database', false),
1886
1905
  orgServiceSchema("chat's own service", 'chat_service', () => 'coolify_chat_service_database'),
@@ -1914,6 +1933,10 @@ export function orgServiceDatabase(org, home) {
1914
1933
  database = underscored(`${org.slug}-cms-db`);
1915
1934
  runtimeRole = 'cms_rt';
1916
1935
  break;
1936
+ case 'kosmos':
1937
+ database = underscored(`${org.slug}-kosmos-db`);
1938
+ runtimeRole = 'kosmos_rt';
1939
+ break;
1917
1940
  case 'chat_service':
1918
1941
  database = 'chat';
1919
1942
  break;
@@ -1,4 +1,4 @@
1
- import type { DeclaredOrg, OrgId } from '../org.js';
1
+ import { type DeclaredOrg, type OrgId } from '../org.js';
2
2
  /**
3
3
  * #164: what both the planner (`diff.ts`) and the executor (`executor.ts`)
4
4
  * need to agree on for an org's Agora -- the hostname, the values the host
@@ -1,5 +1,7 @@
1
+ import { kosmosOn } from '../org.js';
1
2
  import { chatUpstreams } from './chat-matrix.js';
2
3
  import { FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY } from './core-files.js';
4
+ import { kosmosHostnameFor } from './kosmos.js';
3
5
  import { SIGN_IN_UPSTREAM_ENV, inAppHostApplied, signInUpstream, staleSignInUpstream, } from './sign-in.js';
4
6
  /**
5
7
  * #164: what both the planner (`diff.ts`) and the executor (`executor.ts`)
@@ -307,6 +309,8 @@ export function desiredAgoraEnv(org, recorded, rawHost) {
307
309
  chat: { enabled: org.chat.enabled },
308
310
  // Agora draws call buttons only when this is true.
309
311
  calls: { enabled: org.calls.enabled },
312
+ // foundry#949: kosmos is opt-in, so Agora is told both whether it is on and where it lives.
313
+ kosmos: { enabled: kosmosOn(org), url: `https://${kosmosHostnameFor(org)}` },
310
314
  // In-app sign-in: Agora reads this at runtime (one image serves every company). Set with `SIGNIN_UPSTREAM`
311
315
  // only, so Agora never shows the in-app screen before the login can serve it.
312
316
  ...(inAppHostApplied(org, recorded, agoraHostnameFor(org)) ? { signInInApp: true } : {}),
@@ -2,6 +2,7 @@ import { type BlueGreenApplyDeps } from './blue-green-apply.js';
2
2
  import { type BlueGreenSubject } from './blue-green.js';
3
3
  import type { DeployHealthRuntime } from './deploy-health.js';
4
4
  import type { PlatformTunnelPort } from './platform-providers.js';
5
+ import type { StageGuardDeps } from './stage-guards.js';
5
6
  export interface ArchonBlueGreenRun {
6
7
  subject: BlueGreenSubject;
7
8
  /** The owner company's id: the health token's held ref is `sharedSecretRef(ownerOrgId, ...)` like an entry's. */
@@ -42,6 +43,10 @@ export interface ArchonBlueGreenRun {
42
43
  sleep: (ms: number) => Promise<void>;
43
44
  logDir: string;
44
45
  isRunning: (status: string | undefined) => boolean;
46
+ /** Stage's probe port and Access gate, the same guards an entry's stage gets. Absent: the guards read "not run". */
47
+ stageGuards?: StageGuardDeps;
48
+ /** The guard names the plan counted (`desired.guards`); they go into the applied hash. */
49
+ guardInputs?: readonly string[];
45
50
  }
46
51
  /**
47
52
  * platform-archon on the blue/green path (`platform.archon.blueGreen`): the archon app keeps its name and is
@@ -70,6 +70,8 @@ export async function applyArchonBlueGreen(run) {
70
70
  generate: () => randomBytes(32).toString('hex'),
71
71
  secret: run.secret,
72
72
  }),
73
+ ...(run.stageGuards ? { stageGuards: run.stageGuards } : {}),
74
+ ...(run.guardInputs ? { guardInputs: run.guardInputs } : {}),
73
75
  actionsKey: async () => {
74
76
  const key = await mintActionsKey({
75
77
  id,
@@ -54,6 +54,11 @@ export interface BlueGreenApplyDeps {
54
54
  * guard unrecorded ("not run", never passed), so `stageGuardsReady` stays false and the copy does not start.
55
55
  */
56
56
  stageGuards?: StageGuardDeps;
57
+ /**
58
+ * The guard names the plan counted ({@link stageGuardInputs}, from `desired.guards`). They go into the
59
+ * applied structure hash, so the next plan sees the same hash until a guard becomes possible.
60
+ */
61
+ guardInputs?: readonly string[];
57
62
  /**
58
63
  * The company the stage mail sink belongs to (its name is `<slug>-stage-sink`). Absent: no sink is made and
59
64
  * stage holds no mail env at all, so the copy stays refused.