@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
@@ -49,6 +49,8 @@ export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execR
49
49
  export type { Runner, StageCopyPortDeps } from './stage-copy-port.js';
50
50
  export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './platform-sfu.js';
51
51
  export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
52
+ export type { PlatformRegistryDeps, PlatformRegistryRuntime } from './platform-registry.js';
53
+ export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './platform-registry-keys.js';
52
54
  export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './mail-copy.js';
53
55
  export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
54
56
  export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './providers-readers.js';
@@ -30,6 +30,7 @@ export { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_IMAGE_REF, STAGE_SINK_SMTP_POR
30
30
  export { STAGE_COPY_EXCLUDED_TABLES, StageCopyFailedError, StageCopyRefusedError, stageCopy, tableExcluded, undoStageCopy, } from './stage-copy.js';
31
31
  export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execRunner, } from './stage-copy-port.js';
32
32
  export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
33
+ export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './platform-registry-keys.js';
33
34
  export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
34
35
  export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeLoginHostResolver, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
35
36
  export { PlatformBrokerPort } from './broker-entries.js';
@@ -26,4 +26,6 @@ export declare class HttpJobsSchedulerClient implements JobsSchedulerPort {
26
26
  secret?: string;
27
27
  syncKey?: string;
28
28
  }>;
29
+ /** `DELETE /admin/projects/<name>`. A refusal throws with the status only. */
30
+ deleteProject(name: string): Promise<void>;
29
31
  }
@@ -50,4 +50,15 @@ export class HttpJobsSchedulerClient {
50
50
  syncKey: typeof fields.syncKey === 'string' ? fields.syncKey : undefined,
51
51
  };
52
52
  }
53
+ /** `DELETE /admin/projects/<name>`. A refusal throws with the status only. */
54
+ async deleteProject(name) {
55
+ const response = await this.fetchImpl(`${this.baseUrl}/admin/projects/${encodeURIComponent(name)}`, {
56
+ method: 'DELETE',
57
+ headers: { authorization: `Bearer ${this.adminToken}` },
58
+ signal: AbortSignal.timeout(ADMIN_CALL_TIMEOUT_MS),
59
+ });
60
+ if (response.status === 200 || response.status === 204)
61
+ return;
62
+ throw Object.assign(new Error(`jobs scheduler: DELETE /admin/projects/<name> answered ${response.status}`), { status: response.status });
63
+ }
53
64
  }
@@ -0,0 +1,56 @@
1
+ import type { DeclaredOrg } from '../org.js';
2
+ /**
3
+ * foundry#949: what both the planner (`diff.ts`) and the executor
4
+ * (`executor.ts`) need to agree on for an org's kosmos -- the hostname, the
5
+ * recorded ids and the env vars those produce -- so the plan can compare the
6
+ * live app with exactly what the executor would write. The same split
7
+ * `boule.ts` and `agora.ts` already follow, trimmed to what kosmos needs for
8
+ * its first release: a ZITADEL client, a database and sign-in env. Kosmos has
9
+ * no mail, no owner seeding and no Archon credential -- it is not Boule.
10
+ */
11
+ /**
12
+ * The fixed `kosmos` ZITADEL web client's `clientId`, recorded by
13
+ * `executor.ts`'s `zitadelApp` and read by `coolifyKosmos` for
14
+ * `AUTH_CLIENT_ID`. Public, not a secret, so a flat scope id like
15
+ * `boule_client_id`.
16
+ */
17
+ export declare const KOSMOS_CLIENT_ID_SCOPE_KEY = "kosmos_client_id";
18
+ /**
19
+ * The kosmos database's Coolify uuid. It has no `system:kind:key` slot of its
20
+ * own (the step's one auto-recorded `resourceId` is the app), so it is a flat
21
+ * scope id, like the Boule and cms databases.
22
+ */
23
+ export declare const KOSMOS_DATABASE_SCOPE_KEY = "coolify_kosmos_database";
24
+ /**
25
+ * Recorded once `coolifyKosmos` has actually called `deploy` on this org's
26
+ * kosmos app -- the same marker {@link BOULE_DEPLOYED_SCOPE_KEY} (`boule.ts`)
27
+ * and {@link AGORA_DEPLOYED_SCOPE_KEY} (`agora.ts`) are, and for the same
28
+ * reason: Coolify defaults `instant_deploy` to false, so a freshly created
29
+ * app sat built but never started.
30
+ */
31
+ export declare const KOSMOS_DEPLOYED_SCOPE_KEY = "coolify_kosmos_deployed";
32
+ /**
33
+ * `kosmos.<domain>` when the org has a domain, `<slug>-kosmos.oikos.host`
34
+ * otherwise -- the same tier rule `bouleHostnameFor`/`agoraHostnameFor`
35
+ * follow; `../slug.ts`'s `KOSMOS_SUFFIX` reserves the collision.
36
+ */
37
+ export declare function kosmosHostnameFor(org: DeclaredOrg): string;
38
+ /** The env names `desiredKosmosEnv` can write, so the live reader knows which to read back. */
39
+ export declare const KOSMOS_MANAGED_ENV_KEYS: readonly ["APP_URL", "AUTH_ISSUER", "AUTH_ORGANIZATION_ID", "AUTH_CLIENT_ID"];
40
+ /**
41
+ * The env vars an org's kosmos app should carry, and every input still missing
42
+ * to build them. Callers that write must refuse on a non-empty `missing`: an
43
+ * kosmos with a blank client id fails every sign-in. `issuer` is the host's
44
+ * issuer address (`AgoraHostValues.issuer`), the same one Agora and Boule sign
45
+ * in at.
46
+ *
47
+ * Not here, and written by the executor instead: the database URLs
48
+ * (`DATABASE_URL_OWNER`, `DATABASE_URL`, minted once) and `AUTH_COOKIE_SECRET`
49
+ * (generated once, never rotated) -- the same split `desiredBouleEnv` keeps.
50
+ */
51
+ export declare function desiredKosmosEnv(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, issuer: string | null): {
52
+ env: Record<string, string>;
53
+ missing: string[];
54
+ };
55
+ /** The desired env keys the live app lacks or holds a different value for. */
56
+ export declare function kosmosEnvDrift(desired: Readonly<Record<string, string>>, live: Readonly<Record<string, string>>): string[];
@@ -0,0 +1,84 @@
1
+ import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
2
+ /**
3
+ * foundry#949: what both the planner (`diff.ts`) and the executor
4
+ * (`executor.ts`) need to agree on for an org's kosmos -- the hostname, the
5
+ * recorded ids and the env vars those produce -- so the plan can compare the
6
+ * live app with exactly what the executor would write. The same split
7
+ * `boule.ts` and `agora.ts` already follow, trimmed to what kosmos needs for
8
+ * its first release: a ZITADEL client, a database and sign-in env. Kosmos has
9
+ * no mail, no owner seeding and no Archon credential -- it is not Boule.
10
+ */
11
+ /**
12
+ * The fixed `kosmos` ZITADEL web client's `clientId`, recorded by
13
+ * `executor.ts`'s `zitadelApp` and read by `coolifyKosmos` for
14
+ * `AUTH_CLIENT_ID`. Public, not a secret, so a flat scope id like
15
+ * `boule_client_id`.
16
+ */
17
+ export const KOSMOS_CLIENT_ID_SCOPE_KEY = 'kosmos_client_id';
18
+ /**
19
+ * The kosmos database's Coolify uuid. It has no `system:kind:key` slot of its
20
+ * own (the step's one auto-recorded `resourceId` is the app), so it is a flat
21
+ * scope id, like the Boule and cms databases.
22
+ */
23
+ export const KOSMOS_DATABASE_SCOPE_KEY = 'coolify_kosmos_database';
24
+ /**
25
+ * Recorded once `coolifyKosmos` has actually called `deploy` on this org's
26
+ * kosmos app -- the same marker {@link BOULE_DEPLOYED_SCOPE_KEY} (`boule.ts`)
27
+ * and {@link AGORA_DEPLOYED_SCOPE_KEY} (`agora.ts`) are, and for the same
28
+ * reason: Coolify defaults `instant_deploy` to false, so a freshly created
29
+ * app sat built but never started.
30
+ */
31
+ export const KOSMOS_DEPLOYED_SCOPE_KEY = 'coolify_kosmos_deployed';
32
+ /**
33
+ * `kosmos.<domain>` when the org has a domain, `<slug>-kosmos.oikos.host`
34
+ * otherwise -- the same tier rule `bouleHostnameFor`/`agoraHostnameFor`
35
+ * follow; `../slug.ts`'s `KOSMOS_SUFFIX` reserves the collision.
36
+ */
37
+ export function kosmosHostnameFor(org) {
38
+ return org.domain ? `kosmos.${org.domain}` : `${org.slug}-kosmos.oikos.host`;
39
+ }
40
+ /** The env names `desiredKosmosEnv` can write, so the live reader knows which to read back. */
41
+ export const KOSMOS_MANAGED_ENV_KEYS = [
42
+ 'APP_URL',
43
+ 'AUTH_ISSUER',
44
+ 'AUTH_ORGANIZATION_ID',
45
+ 'AUTH_CLIENT_ID',
46
+ ];
47
+ /**
48
+ * The env vars an org's kosmos app should carry, and every input still missing
49
+ * to build them. Callers that write must refuse on a non-empty `missing`: an
50
+ * kosmos with a blank client id fails every sign-in. `issuer` is the host's
51
+ * issuer address (`AgoraHostValues.issuer`), the same one Agora and Boule sign
52
+ * in at.
53
+ *
54
+ * Not here, and written by the executor instead: the database URLs
55
+ * (`DATABASE_URL_OWNER`, `DATABASE_URL`, minted once) and `AUTH_COOKIE_SECRET`
56
+ * (generated once, never rotated) -- the same split `desiredBouleEnv` keeps.
57
+ */
58
+ export function desiredKosmosEnv(org, recorded, issuer) {
59
+ const zitadelOrgId = recorded[ZITADEL_ORG_SCOPE_KEY];
60
+ const clientId = recorded[KOSMOS_CLIENT_ID_SCOPE_KEY];
61
+ const missing = [];
62
+ if (!issuer)
63
+ missing.push('issuer');
64
+ if (!zitadelOrgId)
65
+ missing.push('zitadelOrgId');
66
+ if (!clientId)
67
+ missing.push('clientId');
68
+ const env = {
69
+ APP_URL: `https://${kosmosHostnameFor(org)}`,
70
+ };
71
+ if (issuer)
72
+ env.AUTH_ISSUER = issuer;
73
+ if (zitadelOrgId)
74
+ env.AUTH_ORGANIZATION_ID = zitadelOrgId;
75
+ if (clientId)
76
+ env.AUTH_CLIENT_ID = clientId;
77
+ return { env, missing };
78
+ }
79
+ /** The desired env keys the live app lacks or holds a different value for. */
80
+ export function kosmosEnvDrift(desired, live) {
81
+ return Object.entries(desired)
82
+ .filter(([key, value]) => live[key] !== value)
83
+ .map(([key]) => key);
84
+ }
@@ -1,8 +1,9 @@
1
- import { ownsServices } from '../org.js';
1
+ import { kosmosOn, ownsServices } from '../org.js';
2
2
  import { OWN_SERVICE_LABELS } from '../service-labels.js';
3
3
  import { agoraHostnameFor } from './agora.js';
4
4
  import { bouleHostnameFor } from './boule.js';
5
5
  import { cmsStudioHostnameFor, siteHostnameFor } from './hostnames.js';
6
+ import { kosmosHostnameFor } from './kosmos.js';
6
7
  import { loginHostnameFor } from './login-host.js';
7
8
  import { ownHostnames } from './own-hostnames.js';
8
9
  export { OWN_SERVICE_LABELS };
@@ -31,6 +32,8 @@ export function lockedHostnames(org, platformHostnames) {
31
32
  }
32
33
  locked.add(agoraHostnameFor(org));
33
34
  locked.add(bouleHostnameFor(org));
35
+ if (kosmosOn(org))
36
+ locked.add(kosmosHostnameFor(org));
34
37
  if (org.site.enabled)
35
38
  locked.add(cmsStudioHostnameFor(org));
36
39
  if (org.calls.enabled && org.calls.usesDomain)
@@ -58,6 +61,6 @@ export function hostingNames(org, platformHostnames) {
58
61
  locked: lockedHostnames(org, platformHostnames),
59
62
  slug: org.slug,
60
63
  siteOwnsApex: org.site.enabled && org.site.usesDomain && org.domain !== null,
61
- builtinBlueGreen: org.boule?.blueGreen !== undefined || org.agora?.blueGreen !== undefined,
64
+ builtinBlueGreen: org.boule?.blueGreen !== undefined || org.agora?.blueGreen !== undefined || kosmosOn(org),
62
65
  };
63
66
  }
@@ -6,13 +6,13 @@ export type OrgNameSource = 'own' | 'hosting';
6
6
  * hosting entries: Agora, Boule, the tunnel and its connector, and chat,
7
7
  * calls and the cms site while they are on.
8
8
  */
9
- export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site'>): string[];
9
+ export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site' | 'kosmos'>): string[];
10
10
  /**
11
11
  * Every container name, network alias, database, tunnel and bucket name that
12
12
  * foundry derives for an org: its own-service names when it owns services,
13
13
  * and its hosting-entry and database names.
14
14
  */
15
- export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>): {
15
+ export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>): {
16
16
  name: string;
17
17
  source: OrgNameSource;
18
18
  }[];
@@ -26,5 +26,5 @@ export interface NameClash {
26
26
  source: OrgNameSource;
27
27
  }
28
28
  /** Every name `org` derives that another org or the platform also derives. */
29
- export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>[]): NameClash[];
29
+ export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>[]): NameClash[];
30
30
  export declare function nameClashMessage(slug: string, clash: NameClash): string;
@@ -1,4 +1,4 @@
1
- import { ownsServices } from '../org.js';
1
+ import { kosmosOn, ownsServices } from '../org.js';
2
2
  import { entryHasDatabase } from '../worker-app-config.js';
3
3
  import { chatInternalNames, chatRekeyedInternalNames } from './chat-matrix.js';
4
4
  import { workerAppHostname, workspaceHost, workspaceInboxHost } from './hostnames.js';
@@ -20,6 +20,8 @@ export function companyServiceNames(org) {
20
20
  `${s}-tunnel`,
21
21
  `${s}-cloudflared`,
22
22
  ];
23
+ if (kosmosOn(org))
24
+ names.push(`${s}-kosmos`, `${s}-kosmos-db`);
23
25
  if (org.chat?.enabled) {
24
26
  names.push(...Object.values(chatInternalNames(s)), ...Object.values(chatRekeyedInternalNames(s)));
25
27
  }
@@ -64,6 +64,7 @@ export function parseMigrationConfirmation(raw) {
64
64
  const MIGRATION_ORDER = [
65
65
  'app',
66
66
  'cms',
67
+ 'kosmos',
67
68
  'boule',
68
69
  'chat_service',
69
70
  'calls_service',
@@ -107,6 +107,12 @@ export declare const LIMITS: {
107
107
  autoDeploy: false | "declared";
108
108
  capabilities: CapabilityTier;
109
109
  };
110
+ readonly kosmos: {
111
+ memory: string;
112
+ dbMemory: string;
113
+ autoDeploy: false | "declared";
114
+ capabilities: CapabilityTier;
115
+ };
110
116
  readonly companyOther: {
111
117
  memory: string;
112
118
  dbMemory: string;
@@ -167,6 +173,12 @@ export declare const LIMITS: {
167
173
  autoDeploy: false | "declared";
168
174
  capabilities: CapabilityTier;
169
175
  };
176
+ readonly platformRegistry: {
177
+ memory: string;
178
+ dbMemory: string;
179
+ autoDeploy: false | "declared";
180
+ capabilities: CapabilityTier;
181
+ };
170
182
  readonly platformMail: {
171
183
  memory: string;
172
184
  dbMemory: string;
@@ -29,6 +29,7 @@ export const LIMITS = {
29
29
  calls: profile('1g', '512m'),
30
30
  agora: profile('1g', '512m'),
31
31
  boule: profile('512m'),
32
+ kosmos: profile('512m'),
32
33
  companyOther: profile('1g', '512m'),
33
34
  platformArchon: profile('1g'),
34
35
  platformAi: profile('1g'),
@@ -39,6 +40,7 @@ export const LIMITS = {
39
40
  platformFiles: profile('1g'),
40
41
  platformEdge: profile('512m'),
41
42
  platformHc: profile('512m'),
43
+ platformRegistry: profile('256m'),
42
44
  platformMail: profile('1g'),
43
45
  platformAuth: profile('1g'),
44
46
  platformJobs: profile('1g'),
@@ -4,7 +4,7 @@ import { type AuditSealingOptions } from '../audit/tick.js';
4
4
  import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
5
5
  import type { Plan, PlanStep } from '../plan.js';
6
6
  import type { DeclaredPlatform } from '../platform.js';
7
- import type { ReleaseStore } from '../release/state.js';
7
+ import { type ReleaseStore } from '../release/state.js';
8
8
  import type { JobRunner } from '../runner/types.js';
9
9
  import type { AppliedState } from '../state.js';
10
10
  import { type AppliedVersionsStore, type DeclaredOrgStore, type PlatformStore } from '../store.js';
@@ -224,11 +224,21 @@ export interface StepExecutor {
224
224
  }>>;
225
225
  /** Whether the company's stage mail sink app (by Coolify id) is stopped or gone, read live. Optional: an executor without it reads the sink as up. */
226
226
  stageSinkDown?(appId: string): Promise<boolean>;
227
+ /** Which stage-guard ports the host has; they decide the guard names in the applied structure hash. Optional: neither. */
228
+ stageGuardPorts?(): {
229
+ probes: boolean;
230
+ access: boolean;
231
+ };
227
232
  /**
228
233
  * The release runner's outside calls for one blue/green entry (`plan/release-host.ts`); `null` while its blue
229
234
  * copy is not recorded. Optional: an executor without it runs no release.
230
235
  */
231
236
  releaseHost?(org: DeclaredOrg, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
237
+ /**
238
+ * One release tick for one subject: the recorded stage gate's list follows the people holding `releases:preview`,
239
+ * owners staying. Never throws. Optional: an executor without it leaves every gate as it is.
240
+ */
241
+ syncStageGateMembers?(org: DeclaredOrg, subject: BlueGreenSubject, context: ReleaseContext): Promise<void>;
232
242
  /** Whether the executor's held keys can store a Worker app database's CA and certificate. Absent: true. */
233
243
  workerAppDbKeysPuttable?(): boolean;
234
244
  /** Whether the host supplies the backup bucket a Worker app database dumps into. Absent: true. */
@@ -305,6 +315,8 @@ export interface PlatformDriftedSteps {
305
315
  authApps?: boolean;
306
316
  /** The host sets it. */
307
317
  edge?: boolean;
318
+ /** A usable, reachable Coolify server has no registry login for the current host and passwords. */
319
+ registryLogins?: boolean;
308
320
  /** #380 */
309
321
  files?: boolean;
310
322
  backupStorage?: boolean;
@@ -340,6 +352,8 @@ export interface PlatformStepExecutor {
340
352
  backupExecutorMissing?: boolean;
341
353
  /** #540: see `PlanPlatformOptions.blocked.hc` (`platform-diff.ts`). */
342
354
  hc?: string;
355
+ /** See `PlanPlatformOptions.blocked.registry` (`platform-diff.ts`). */
356
+ registry?: string;
343
357
  /** #540: see `PlanPlatformOptions.blocked.heartbeatsExecutorMissing` (`platform-diff.ts`). */
344
358
  heartbeatsExecutorMissing?: boolean;
345
359
  /** foundry#542: see `PlanPlatformOptions.blocked.uptime` (`platform-diff.ts`). */
@@ -373,8 +387,12 @@ export interface PlatformStepExecutor {
373
387
  servers?: BlueGreenServer[];
374
388
  blocked?: string;
375
389
  }>;
390
+ /** The stage guards platform-archon can make now, by name; they go into its applied structure hash. */
391
+ archonStageGuardNames?(platform: DeclaredPlatform, recorded: Readonly<Record<string, string>>, owner: PlatformOwnerInput): string[];
376
392
  /** The release runner's outside calls for platform-archon (`plan/release-host.ts`); `null` while its app is not recorded. */
377
393
  releaseHost?(platform: DeclaredPlatform, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
394
+ /** As {@link FoundryStepExecutor.syncStageGateMembers}, for platform-archon's own stage gate (orgId `platform`). */
395
+ syncStageGateMembers?(platform: DeclaredPlatform, subject: BlueGreenSubject, context: ReleaseContext): Promise<void>;
378
396
  execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
379
397
  resourceId?: string;
380
398
  } | undefined>;
@@ -633,6 +651,14 @@ export interface FoundryPlanApplyOptions {
633
651
  now?: () => Date;
634
652
  /** #441: how long a mutation lease lasts before another caller may take it over. Defaults to 30 minutes. */
635
653
  leaseTtlMs?: number;
654
+ /**
655
+ * A stage copy runs without the org's lease and takes it for a moment each time it records something. When an
656
+ * apply or a tick holds the lease it waits `waitMs` and tries again, up to `tries` times. Defaults to 30 s and 10.
657
+ */
658
+ copyLeaseRetry?: {
659
+ tries: number;
660
+ waitMs: number;
661
+ };
636
662
  /**
637
663
  * #441 (part b): a durable, DB-row-backed record of `apply`, `destroy`,
638
664
  * `adopt` and `release` for one org, held under the same mutation lease
@@ -730,6 +756,7 @@ export declare class FoundryPlanApply implements PlanApply {
730
756
  private readonly onlySteps;
731
757
  private readonly now;
732
758
  private readonly leaseTtlMs;
759
+ private readonly copyLeaseRetry;
733
760
  private readonly appliedVersions;
734
761
  private readonly backupGate;
735
762
  private readonly platformDeps;
@@ -900,6 +927,25 @@ export declare class FoundryPlanApply implements PlanApply {
900
927
  target: string;
901
928
  error?: string;
902
929
  }>>;
930
+ /**
931
+ * Phase 2 for one org: takes the copy lock of each stage copy request under the org's lease, then runs the
932
+ * copies after the lease is released. While a copy runs, every other tick and apply for the org goes on, a
933
+ * hard-fail rollback included; the copy records through short leases ({@link copyContext}).
934
+ */
935
+ private runOrgStageCopies;
936
+ /** Phase 2 for platform-archon; see {@link runOrgStageCopies}. */
937
+ private runPlatformStageCopies;
938
+ /**
939
+ * Runs one claimed copy with no lease held, then records its outcome and consumes the request. A lost lock
940
+ * throws (`StaleLeaseError`) and leaves the request open.
941
+ */
942
+ private runClaimedCopy;
943
+ /**
944
+ * The recorded ids a copy reads and writes, without the org's lease. It reads a snapshot taken when the lock was
945
+ * claimed; each `persist` takes the lease for a moment, re-reads the stored ids and writes only the keys the
946
+ * copy changed, so a tick that ran in between loses nothing. The lease may be busy: it waits and tries again.
947
+ */
948
+ private copyContext;
903
949
  /**
904
950
  * The recurring audit sealing run (foundry#929): seals every ledger registered by an `audit:ledger` step with the
905
951
  * platform signer and has the timestamp authority stamp the new checkpoints. Foundry has no scheduler, so the host
@@ -907,6 +953,10 @@ export declare class FoundryPlanApply implements PlanApply {
907
953
  * log line say what happened and why when nothing did. It writes nothing to the org's records.
908
954
  */
909
955
  runAuditSealTick(): Promise<SealResult[]>;
956
+ /**
957
+ * One org's release run. `defer`: stage copy requests stay open and `deferred` says whether there were any.
958
+ * `claim`: the copies this run decided on, each with its lock taken, for {@link runClaimedCopy}.
959
+ */
910
960
  private runOrgReleaseTick;
911
961
  /** An own org with calls on: the host's answer about its slot's ports. A failure or a slow answer is "not open". */
912
962
  private readSfuPortsOpen;