@wtfalch/foundry 0.78.1 → 0.79.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.
- package/dist/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/mcp-resources/client.d.ts +16 -0
- package/dist/mcp-resources/client.js +69 -0
- package/dist/mcp-resources/index.d.ts +3 -0
- package/dist/mcp-resources/index.js +3 -0
- package/dist/mcp-resources/schema.d.ts +1707 -0
- package/dist/mcp-resources/schema.js +253 -0
- package/dist/mcp-resources/validation.d.ts +21 -0
- package/dist/mcp-resources/validation.js +97 -0
- package/dist/modules/ai.js +14 -3
- package/dist/modules/customer-grants.js +1 -1
- package/dist/modules/email.d.ts +2 -1
- package/dist/modules/email.js +3 -2
- package/dist/modules/files.js +4 -1
- package/dist/modules/index.js +4 -1
- package/dist/modules/jobs.js +1 -1
- package/dist/modules/mcp-resources.d.ts +2 -0
- package/dist/modules/mcp-resources.js +61 -0
- package/dist/modules/outputs.js +1 -0
- package/dist/modules/repository.d.ts +2 -0
- package/dist/modules/repository.js +3 -1
- package/dist/modules/workspaces.js +1 -1
- package/dist/org.d.ts +3 -0
- package/dist/org.js +10 -7
- package/dist/plan/ai-apps-minter.d.ts +79 -0
- package/dist/plan/ai-apps-minter.js +263 -0
- package/dist/plan/ai-boule-key.d.ts +1 -1
- package/dist/plan/ai-boule-key.js +10 -3
- package/dist/plan/broker-entries.js +4 -4
- package/dist/plan/diff.d.ts +6 -0
- package/dist/plan/diff.js +33 -13
- package/dist/plan/entry-ai-key.d.ts +2 -0
- package/dist/plan/entry-ai-key.js +4 -2
- package/dist/plan/entry-customer-grants.d.ts +1 -1
- package/dist/plan/entry-customer-grants.js +4 -2
- package/dist/plan/entry-files-key.d.ts +10 -0
- package/dist/plan/entry-files-key.js +6 -1
- package/dist/plan/entry-jobs-boule-key.js +1 -1
- package/dist/plan/entry-jobs.js +1 -1
- package/dist/plan/entry-repo-key.js +2 -2
- package/dist/plan/entry-workspaces.d.ts +5 -0
- package/dist/plan/entry-workspaces.js +3 -1
- package/dist/plan/executor.d.ts +27 -0
- package/dist/plan/executor.js +125 -4
- package/dist/plan/files-apps-minter.d.ts +131 -0
- package/dist/plan/files-apps-minter.js +336 -0
- package/dist/plan/files-boule-key.js +2 -2
- package/dist/plan/files-space-alert.js +3 -3
- package/dist/plan/jobs-apps-key.d.ts +29 -0
- package/dist/plan/jobs-apps-key.js +228 -0
- package/dist/plan/jobs-project-settings.js +2 -0
- package/dist/plan/jobs-scheduler-http.d.ts +7 -0
- package/dist/plan/jobs-scheduler-http.js +32 -1
- package/dist/plan/own-jobs-route.d.ts +1 -1
- package/dist/plan/own-jobs-route.js +1 -1
- package/dist/plan/plan-apply.d.ts +6 -0
- package/dist/plan/plan-apply.js +45 -3
- package/dist/plan/platform-diff.d.ts +1 -1
- package/dist/plan/platform-diff.js +3 -2
- package/dist/release/state.d.ts +1 -1
- package/dist/store/schema.d.ts +2 -2
- package/dist/worker-app-config.js +11 -2
- package/package.json +5 -1
package/dist/plan/diff.js
CHANGED
|
@@ -3,6 +3,7 @@ import { NATIVE_BUILD_KEY_ENV } from '../native-build.js';
|
|
|
3
3
|
import { APP_ENTRY_KEY, MAIL_APP_KEY_ENV, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, appsOn, companyGithubFor, heldEnvRef, kosmosOn, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownerNameProblem, ownsServices, sendsMail, signsInBoth, signsInCustomers, } from '../org.js';
|
|
4
4
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
5
5
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, chatCallsApplied, chatHasUpstreams, desiredAgoraEnv, entryClientIdScopeKey, withRecordedMessagingProject, } from './agora.js';
|
|
6
|
+
import { planAiAppsMinter } from './ai-apps-minter.js';
|
|
6
7
|
import { AI_OFFERS_SCOPE_KEY, aiKeyPermissions, aiOffers, planAiBouleKey } from './ai-boule-key.js';
|
|
7
8
|
import { planAnalyticsBoule } from './analytics-boule.js';
|
|
8
9
|
import { analyticsEnvDrift, analyticsWanted, desiredAnalyticsEnv } from './analytics.js';
|
|
@@ -25,17 +26,19 @@ import { planEntryAudit } from './entry-audit.js';
|
|
|
25
26
|
import { authEventsAppliedScopeKey, authEventsBlocked, authEventsMarker, } from './entry-auth-events.js';
|
|
26
27
|
import { CUSTOMER_GRANTS_ENV, CUSTOMER_GRANTS_WRITTEN_SCOPE_KEY, customerGrantsEntries, customerGrantsShapeMatches, customerGrantsToDrop, wantedCustomerGrants, } from './entry-customer-grants.js';
|
|
27
28
|
import { planEdgeReservedLabels, planEdgeSlug, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
28
|
-
import { planFilesKeys } from './entry-files-key.js';
|
|
29
|
+
import { planFilesKeys, usesServerFiles } from './entry-files-key.js';
|
|
29
30
|
import { planEntryJobsBouleKeys } from './entry-jobs-boule-key.js';
|
|
30
31
|
import { planEntryJobs } from './entry-jobs.js';
|
|
31
32
|
import { planEntryRepoKeys } from './entry-repo-key.js';
|
|
32
33
|
import { planEntryReportingKeys } from './entry-reporting-key.js';
|
|
33
|
-
import { BROKER_WORKSPACES_DATABASE_ENV, workspacesDatabaseRef } from './entry-workspaces.js';
|
|
34
|
+
import { BROKER_WORKSPACES_DATABASE_ENV, serverWorkspaces, workspacesDatabaseRef, } from './entry-workspaces.js';
|
|
35
|
+
import { planFilesAppsMinter } from './files-apps-minter.js';
|
|
34
36
|
import { planFilesBouleKey } from './files-boule-key.js';
|
|
35
37
|
import { planFilesSpaceAlerts } from './files-space-alert.js';
|
|
36
38
|
import { gateIdRecordKey, hasWildcardGate, planGates } from './gates.js';
|
|
37
39
|
import { githubSourceResourceKey, planGithubSource } from './github-source.js';
|
|
38
40
|
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
41
|
+
import { planJobsAppsKey } from './jobs-apps-key.js';
|
|
39
42
|
import { planJobsProjectSettings } from './jobs-project-settings.js';
|
|
40
43
|
import { planKeysRegistrar } from './keys-registrar.js';
|
|
41
44
|
import { desiredKosmosEnv, kosmosEnvDrift } from './kosmos.js';
|
|
@@ -457,13 +460,13 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
457
460
|
if (filesOrganisation.step)
|
|
458
461
|
plannedSteps.push(filesOrganisation.step);
|
|
459
462
|
const aiBouleReads = options.aiBouleReads ?? false;
|
|
460
|
-
const aiOrganisation = planAiOrganisation(org, byKey, claimed, platformReadiness.aiReady, platformReadiness.missingKeys.ai, platformReadiness.noKeyReader, recordedIds, aiBouleReads);
|
|
463
|
+
const aiOrganisation = planAiOrganisation(org, byKey, claimed, platformReadiness.aiReady, platformReadiness.missingKeys.ai, platformReadiness.noKeyReader, recordedIds, aiBouleReads, options.aiAppsKeys ?? false);
|
|
461
464
|
if (aiOrganisation.step)
|
|
462
465
|
plannedSteps.push(aiOrganisation.step);
|
|
463
466
|
const aiBudget = planAiBudget(org, byKey, claimed, platformReadiness.aiReady, platformReadiness.missingKeys.ai, platformReadiness.noKeyReader);
|
|
464
467
|
if (aiBudget.step)
|
|
465
468
|
plannedSteps.push(aiBudget.step);
|
|
466
|
-
const aiKey = planAiKey(org, recordedIds, platformReadiness.aiReady, aiBouleReads);
|
|
469
|
+
const aiKey = planAiKey(org, recordedIds, platformReadiness.aiReady, aiBouleReads, options.aiAppsKeys ?? false);
|
|
467
470
|
if (aiKey.step)
|
|
468
471
|
plannedSteps.push(aiKey.step);
|
|
469
472
|
// Before `planSite`'s own step (#139): `cmsSite`'s executor reads this
|
|
@@ -557,7 +560,7 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
557
560
|
plannedSteps.push(...app.steps);
|
|
558
561
|
// The broker's workspaces database, before any broker entry that needs it: made once, when none is held or set.
|
|
559
562
|
const workspacesDatabasePlanned = declaredOrg.hosting.enabled &&
|
|
560
|
-
declaredOrg.hosting.entries.some((e) => e
|
|
563
|
+
declaredOrg.hosting.entries.some((e) => serverWorkspaces(e)) &&
|
|
561
564
|
(options.broker ?? false) &&
|
|
562
565
|
!(options.brokerWorkspacesDatabase ?? false) &&
|
|
563
566
|
(options.provisionWorkspacesDatabase ?? false);
|
|
@@ -753,6 +756,9 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
753
756
|
// foundry#1121: Boule's files key, after Boule and after the org's own files key it is minted below.
|
|
754
757
|
const filesBouleKey = planFilesBouleKey(org, recordedIds, plannedSteps, platformReadiness, (options.now ?? Date.now)());
|
|
755
758
|
plannedSteps.push(...filesBouleKey.steps);
|
|
759
|
+
// Files for Boule's own apps: one key that mints each app's files key, while apps are on.
|
|
760
|
+
const filesAppsMinter = planFilesAppsMinter(org, recordedIds, plannedSteps, platformReadiness, (options.now ?? Date.now)());
|
|
761
|
+
plannedSteps.push(...filesAppsMinter.steps);
|
|
756
762
|
const filesSpaceAlerts = planFilesSpaceAlerts(org, recordedIds, platformReadiness.filesReady && !filesOrganisation.blocked, options.filesSpaceAlerts ?? false);
|
|
757
763
|
plannedSteps.push(...filesSpaceAlerts.steps);
|
|
758
764
|
const serviceAlertMail = planServiceAlertMail(org, platformReadiness, options.serviceAlertMail ?? false, options.ownMailKeyHeld ?? false, options.serviceAlertMailCurrent);
|
|
@@ -760,6 +766,8 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
760
766
|
// foundry#1122: Boule's ai key, after Boule and after the org's own ai key it is minted below.
|
|
761
767
|
const aiBouleKey = planAiBouleKey(org, recordedIds, plannedSteps, platformReadiness, (options.now ?? Date.now)(), aiBouleReads);
|
|
762
768
|
plannedSteps.push(...aiBouleKey.steps);
|
|
769
|
+
const aiAppsMinter = planAiAppsMinter(org, recordedIds, plannedSteps, platformReadiness, (options.now ?? Date.now)(), options.aiAppsKeys ?? false, options.aiAppsRootKeyHeld ?? false, options.aiAppsMinterHeld ?? true);
|
|
770
|
+
plannedSteps.push(...aiAppsMinter.steps);
|
|
763
771
|
// foundry#949: the company's kosmos, only once it turns it on. Its fixed ZITADEL client first, the same
|
|
764
772
|
// reason Boule's and Agora's do: `coolifyKosmos` reads this step's recorded client id.
|
|
765
773
|
const kosmosClient = planKosmosClient(org, byKey, claimed);
|
|
@@ -809,6 +817,8 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
809
817
|
// A company on shared services registers on the scheduler the host supplies (the owner company's); a company
|
|
810
818
|
// that runs its own services never does, only on its own.
|
|
811
819
|
const schedulerUsable = (jobsAvailable && !ownsServices(org)) || platformReadiness.jobsReady;
|
|
820
|
+
const jobsApps = planJobsAppsKey(org, recordedIds, plannedSteps, platformReadiness, schedulerUsable, options.jobsAppProjects ?? false, (options.now ?? Date.now)());
|
|
821
|
+
plannedSteps.push(...jobsApps.steps);
|
|
812
822
|
// A `uses.jobs` entry registers with the scheduler after its app, whose env the step writes.
|
|
813
823
|
const entryJobs = planEntryJobs(org, recordedIds, plannedSteps, platformReadiness, schedulerUsable);
|
|
814
824
|
plannedSteps.push(...entryJobs.steps);
|
|
@@ -1095,6 +1105,8 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
1095
1105
|
unmanaged.push(...workspaces.unmanaged);
|
|
1096
1106
|
unmanaged.push(...filesKeys.blocked);
|
|
1097
1107
|
unmanaged.push(...filesBouleKey.blocked);
|
|
1108
|
+
unmanaged.push(...filesAppsMinter.blocked);
|
|
1109
|
+
unmanaged.push(...aiAppsMinter.blocked);
|
|
1098
1110
|
unmanaged.push(...filesSpaceAlerts.blocked);
|
|
1099
1111
|
unmanaged.push(...serviceAlertMail.blocked);
|
|
1100
1112
|
unmanaged.push(...entryAiKeys.blocked);
|
|
@@ -1118,6 +1130,7 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
1118
1130
|
unmanaged.push(...appsAdmin.blocked);
|
|
1119
1131
|
unmanaged.push(...appsDbRoute.blocked);
|
|
1120
1132
|
unmanaged.push(...appsZones.blocked);
|
|
1133
|
+
unmanaged.push(...jobsApps.blocked);
|
|
1121
1134
|
unmanaged.push(...entryJobs.blocked);
|
|
1122
1135
|
unmanaged.push(...jobsSettings.blocked);
|
|
1123
1136
|
unmanaged.push(...entryJobsBouleKeys.blocked);
|
|
@@ -2624,7 +2637,7 @@ ownFilesPending = false) {
|
|
|
2624
2637
|
blocked: `${org.slug}'s Files organisation is blocked, not planned: ${notReadyReason('files', 'files management key', missingKeyRef, noKeyReader)}`,
|
|
2625
2638
|
};
|
|
2626
2639
|
}
|
|
2627
|
-
const shareHost = org.domain && org.hosting.entries.some((e) => e.uses?.files?.shareHost)
|
|
2640
|
+
const shareHost = org.domain && org.hosting.entries.some((e) => usesServerFiles(e) && e.uses?.files?.shareHost)
|
|
2628
2641
|
? `share.${org.domain}`
|
|
2629
2642
|
: null;
|
|
2630
2643
|
return {
|
|
@@ -2673,15 +2686,17 @@ ownFilesPending = false) {
|
|
|
2673
2686
|
* #177: blocked, not planned, while `aiReady` is false -- the platform's own
|
|
2674
2687
|
* AI host (#172) is off or not yet applied.
|
|
2675
2688
|
*/
|
|
2676
|
-
function planAiOrganisation(org, byKey, claimed, aiReady, missingKeyRef, noKeyReader = false, recordedIds = {}, bouleReads = false) {
|
|
2689
|
+
function planAiOrganisation(org, byKey, claimed, aiReady, missingKeyRef, noKeyReader = false, recordedIds = {}, bouleReads = false, appsKeys = false) {
|
|
2677
2690
|
const key = resourceKey('ai', 'organisation', org.id);
|
|
2678
2691
|
const live = byKey.get(key);
|
|
2679
|
-
const offers = aiOffers(bouleReads);
|
|
2692
|
+
const offers = aiOffers(bouleReads, appsKeys);
|
|
2680
2693
|
if (live) {
|
|
2681
2694
|
claimed.add(key);
|
|
2682
2695
|
// foundry#1122: once ai offers Boule's reads, an organisation offered fewer is offered them (both calls
|
|
2683
2696
|
// are repeatable). Without the host's word nothing changes.
|
|
2684
|
-
if (!bouleReads
|
|
2697
|
+
if ((!bouleReads && !appsKeys) ||
|
|
2698
|
+
!aiReady ||
|
|
2699
|
+
recordedIds[AI_OFFERS_SCOPE_KEY] === JSON.stringify(offers)) {
|
|
2685
2700
|
return { step: null, blocked: null };
|
|
2686
2701
|
}
|
|
2687
2702
|
return {
|
|
@@ -2804,14 +2819,15 @@ function planAiBudget(org, byKey, claimed, aiReady, missingKeyRef, noKeyReader =
|
|
|
2804
2819
|
* organisation to mint a key inside yet, and {@link planAiOrganisation}
|
|
2805
2820
|
* already reports the platform's AI host as missing once.
|
|
2806
2821
|
*/
|
|
2807
|
-
function planAiKey(org, recordedIds, aiReady, bouleReads = false) {
|
|
2822
|
+
function planAiKey(org, recordedIds, aiReady, bouleReads = false, appsKeys = false) {
|
|
2808
2823
|
const key = resourceKey('ai', 'key', org.id);
|
|
2809
|
-
const permissions = aiOffers(bouleReads);
|
|
2824
|
+
const permissions = aiOffers(bouleReads, appsKeys);
|
|
2810
2825
|
// foundry#1122: a delivered key that holds fewer than the organisation now offers is minted again with them,
|
|
2811
2826
|
// so Boule's key can be minted below it. Without the host's word a delivered key plans nothing.
|
|
2812
2827
|
const widen = Boolean(recordedIds[key]) &&
|
|
2813
|
-
bouleReads &&
|
|
2814
|
-
aiKeyPermissions(recordedIds) !== JSON.stringify(permissions)
|
|
2828
|
+
(bouleReads || appsKeys) &&
|
|
2829
|
+
(aiKeyPermissions(recordedIds) !== JSON.stringify(permissions) ||
|
|
2830
|
+
(appsKeys && recordedIds.ai_key_purpose !== 'company_root'));
|
|
2815
2831
|
if (recordedIds[key] && !widen)
|
|
2816
2832
|
return { step: null, blocked: null }; // delivered -- a re-run plans nothing.
|
|
2817
2833
|
if (!aiReady)
|
|
@@ -2838,6 +2854,7 @@ function planAiKey(org, recordedIds, aiReady, bouleReads = false) {
|
|
|
2838
2854
|
name: `${org.slug}-app`,
|
|
2839
2855
|
expiresDays: 365,
|
|
2840
2856
|
permissions,
|
|
2857
|
+
...(appsKeys ? { protectedRoot: true } : {}),
|
|
2841
2858
|
...(widen ? { mode: 'widen' } : {}),
|
|
2842
2859
|
},
|
|
2843
2860
|
},
|
|
@@ -4809,6 +4826,9 @@ export function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired, b
|
|
|
4809
4826
|
// An entry both sign in to is registered (`registered-app.ts`), not an `AUTH_APPS` entry.
|
|
4810
4827
|
if (!auth || signsInBoth(auth))
|
|
4811
4828
|
continue;
|
|
4829
|
+
// Boule's registrar owns this copy's workspaces key; AUTH_APPS must not replace it.
|
|
4830
|
+
if (entry.workerApp && auth.workspaces)
|
|
4831
|
+
continue;
|
|
4812
4832
|
const at = `${org.slug}'s ${entry.key} broker entry`;
|
|
4813
4833
|
const key = `coolify:broker-entry:${entry.key}`;
|
|
4814
4834
|
const warnings = recordedIds[brokerEntryWarningsScopeKey(entry.key)];
|
|
@@ -9,6 +9,8 @@ import type { OrgPlatformReadiness } from './platform-org.js';
|
|
|
9
9
|
export declare const AI_ENTRY_KEY_DAYS = 365;
|
|
10
10
|
/** A key with fewer days left than this is renewed. */
|
|
11
11
|
export declare const AI_ENTRY_RENEW_WINDOW_DAYS = 30;
|
|
12
|
+
/** Worker app keys are delivered by Boule; the server plan must never mint or write their env. */
|
|
13
|
+
export declare const usesServerAi: (entry: Pick<HostingEntry, "uses" | "workerApp">) => boolean;
|
|
12
14
|
/** Flat scope ids (no `:`) per entry. Entry keys hold neither `:` nor `.`. */
|
|
13
15
|
export declare const entryAiUndeliveredScopeKey: (entryKey: string) => string;
|
|
14
16
|
/** The ISO time the entry's live key expires. */
|
|
@@ -9,6 +9,8 @@ import { appStepKey } from './platform-diff.js';
|
|
|
9
9
|
export const AI_ENTRY_KEY_DAYS = 365;
|
|
10
10
|
/** A key with fewer days left than this is renewed. */
|
|
11
11
|
export const AI_ENTRY_RENEW_WINDOW_DAYS = 30;
|
|
12
|
+
/** Worker app keys are delivered by Boule; the server plan must never mint or write their env. */
|
|
13
|
+
export const usesServerAi = (entry) => Boolean(entry.uses?.ai) && !entry.workerApp;
|
|
12
14
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
13
15
|
/** Flat scope ids (no `:`) per entry. Entry keys hold neither `:` nor `.`. */
|
|
14
16
|
export const entryAiUndeliveredScopeKey = (entryKey) => `ai_key_undelivered_${entryKey}`;
|
|
@@ -74,7 +76,7 @@ export function planEntryAiKeys(org, recordedIds, plannedSteps, readiness, now)
|
|
|
74
76
|
});
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
|
-
const declared = new Set(org.hosting.entries.filter(
|
|
79
|
+
const declared = new Set(org.hosting.entries.filter(usesServerAi).map((e) => e.key));
|
|
78
80
|
const keyPrefix = `ai:key:${org.id}:`;
|
|
79
81
|
// A key whose entry is gone, or no longer declares `uses.ai`, is revoked.
|
|
80
82
|
if (readiness.aiReady) {
|
|
@@ -100,7 +102,7 @@ export function planEntryAiKeys(org, recordedIds, plannedSteps, readiness, now)
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
for (const entry of org.hosting.entries) {
|
|
103
|
-
if (!entry
|
|
105
|
+
if (!usesServerAi(entry))
|
|
104
106
|
continue;
|
|
105
107
|
const at = `${org.slug}'s ${entry.key} ai key`;
|
|
106
108
|
const stepKey = appStepKey(org, entry.key);
|
|
@@ -39,7 +39,7 @@ export declare function ensureCustomerGrantsCredential(input: {
|
|
|
39
39
|
persist: () => Promise<void>;
|
|
40
40
|
secret: (value: string) => void;
|
|
41
41
|
}): Promise<string>;
|
|
42
|
-
/**
|
|
42
|
+
/** Server entries: Boule provisions Worker credentials privately, never through the static server manifest. */
|
|
43
43
|
export declare const customerGrantsEntries: (org: Pick<DeclaredOrg, "hosting">) => HostingEntry[];
|
|
44
44
|
/** Entry keys that were given a credential and no longer declare the flag (or are gone). */
|
|
45
45
|
export declare function customerGrantsToDrop(org: Pick<DeclaredOrg, 'hosting'>, recorded: Readonly<Record<string, string>>): string[];
|
|
@@ -49,8 +49,10 @@ export async function ensureCustomerGrantsCredential(input) {
|
|
|
49
49
|
await input.persist();
|
|
50
50
|
return credential;
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
53
|
-
export const customerGrantsEntries = (org) => org.hosting.enabled
|
|
52
|
+
/** Server entries: Boule provisions Worker credentials privately, never through the static server manifest. */
|
|
53
|
+
export const customerGrantsEntries = (org) => org.hosting.enabled
|
|
54
|
+
? org.hosting.entries.filter((e) => e.uses?.customerGrants && !e.workerApp)
|
|
55
|
+
: [];
|
|
54
56
|
/** Entry keys that were given a credential and no longer declare the flag (or are gone). */
|
|
55
57
|
export function customerGrantsToDrop(org, recorded) {
|
|
56
58
|
const wanted = new Set(customerGrantsEntries(org).map((e) => e.key));
|
|
@@ -7,6 +7,16 @@ import type { OrgPlatformReadiness } from './platform-org.js';
|
|
|
7
7
|
* it. A secret goes to `deliverSecret` (the org key) or into the entry's env
|
|
8
8
|
* (an entry key) and nowhere else.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* An entry foundry mints a files key for: a server entry with `uses.files`. A Worker app's files keys come from
|
|
12
|
+
* its Boule (`files-apps-minter.ts`), so foundry plans nothing for one.
|
|
13
|
+
*/
|
|
14
|
+
export declare const usesServerFiles: (entry: {
|
|
15
|
+
uses?: {
|
|
16
|
+
files?: unknown;
|
|
17
|
+
};
|
|
18
|
+
workerApp?: unknown;
|
|
19
|
+
}) => boolean;
|
|
10
20
|
/** How long a key lives. files refuses more than 365 and anything past its minter's own expiry. */
|
|
11
21
|
export declare const FILES_KEY_DAYS = 365;
|
|
12
22
|
/** A key with fewer days left than this is renewed. */
|
|
@@ -7,6 +7,11 @@ import { appStepKey } from './platform-diff.js';
|
|
|
7
7
|
* it. A secret goes to `deliverSecret` (the org key) or into the entry's env
|
|
8
8
|
* (an entry key) and nowhere else.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* An entry foundry mints a files key for: a server entry with `uses.files`. A Worker app's files keys come from
|
|
12
|
+
* its Boule (`files-apps-minter.ts`), so foundry plans nothing for one.
|
|
13
|
+
*/
|
|
14
|
+
export const usesServerFiles = (entry) => Boolean(entry.uses?.files) && !entry.workerApp;
|
|
10
15
|
/** How long a key lives. files refuses more than 365 and anything past its minter's own expiry. */
|
|
11
16
|
export const FILES_KEY_DAYS = 365;
|
|
12
17
|
/** A key with fewer days left than this is renewed. */
|
|
@@ -211,7 +216,7 @@ export function planFilesKeys(org, recordedIds, plannedSteps, readiness, now) {
|
|
|
211
216
|
key: `files:agora-key:${org.id}`,
|
|
212
217
|
});
|
|
213
218
|
}
|
|
214
|
-
const entries = org.hosting.entries.filter(
|
|
219
|
+
const entries = org.hosting.entries.filter(usesServerFiles);
|
|
215
220
|
const declared = new Set(entries.map((e) => e.key));
|
|
216
221
|
const rootKeyId = recordedIds[`files:key:${org.id}`];
|
|
217
222
|
// A key whose entry is gone, or no longer declares `uses.files`, is revoked:
|
|
@@ -40,7 +40,7 @@ export const JOBS_BOULE_KEY_GRANT = {
|
|
|
40
40
|
principal: 'boule',
|
|
41
41
|
scopes: ['jobs:read', 'jobs:control'],
|
|
42
42
|
};
|
|
43
|
-
const usesJobs = (entry) => entry.uses?.jobs
|
|
43
|
+
const usesJobs = (entry) => !entry.workerApp && Boolean(entry.uses?.jobs);
|
|
44
44
|
/**
|
|
45
45
|
* Mint or deliver one step per `uses.jobs` entry whose key is not in Boule's env at the scheduler's current
|
|
46
46
|
* address; retire one per entry that no longer declares it and still has a key recorded. Blocked, not planned
|
package/dist/plan/entry-jobs.js
CHANGED
|
@@ -40,7 +40,7 @@ export function planEntryJobs(org, recordedIds, plannedSteps, readiness, schedul
|
|
|
40
40
|
return { steps, blocked };
|
|
41
41
|
const appPlanned = (stepKey) => plannedSteps.some((p) => p.action.system === 'coolify' && p.action.kind === 'app' && p.action.key === stepKey);
|
|
42
42
|
for (const entry of org.hosting.entries) {
|
|
43
|
-
if (!entry.uses?.jobs)
|
|
43
|
+
if (!entry.uses?.jobs || entry.workerApp)
|
|
44
44
|
continue;
|
|
45
45
|
const stepKey = appStepKey(org, entry.key);
|
|
46
46
|
const key = `jobs:project:${stepKey}`;
|
|
@@ -66,7 +66,7 @@ export function repoKeyTarget(org, repo) {
|
|
|
66
66
|
* has no app, so it counts as none: a key it had is removed, and made again when it goes live.
|
|
67
67
|
*/
|
|
68
68
|
export const repoKeyEntries = (org) => org.hosting.enabled
|
|
69
|
-
? org.hosting.entries.filter((e) => e.uses?.repo && e.repo !== null && e.live !== false)
|
|
69
|
+
? org.hosting.entries.filter((e) => e.uses?.repo && !e.workerApp && e.repo !== null && e.live !== false)
|
|
70
70
|
: [];
|
|
71
71
|
/** Entry keys that were given a key and no longer declare `uses.repo` (or are gone, or hosting is off). */
|
|
72
72
|
export function repoKeysToRemove(org, recorded) {
|
|
@@ -98,7 +98,7 @@ export function planEntryRepoKeys(org, recorded, plannedSteps, readiness) {
|
|
|
98
98
|
const appPlanned = (stepKey) => plannedSteps.some((p) => p.action.system === 'coolify' && p.action.kind === 'app' && p.action.key === stepKey);
|
|
99
99
|
for (const entry of org.hosting.enabled ? org.hosting.entries : []) {
|
|
100
100
|
const use = entry.uses?.repo;
|
|
101
|
-
if (!use)
|
|
101
|
+
if (!use || entry.workerApp)
|
|
102
102
|
continue;
|
|
103
103
|
const stepKey = appStepKey(org, entry.key);
|
|
104
104
|
const key = `github:deploy-key:${stepKey}`;
|
|
@@ -4,6 +4,11 @@ import { type DeclaredOrg, type HostingEntry } from '../org.js';
|
|
|
4
4
|
* entry, holds it, and gives it to two places: the app's env (`WORKSPACES_API_KEY`) and, as a sha256, the
|
|
5
5
|
* broker entry (`workspacesKey`). The broker never sees the key and the plan never holds it.
|
|
6
6
|
*/
|
|
7
|
+
/** Worker copies register through Boule, whose registrar answer owns the per-copy key. */
|
|
8
|
+
export declare const serverWorkspaces: (entry: HostingEntry) => {
|
|
9
|
+
roles: string[];
|
|
10
|
+
invitePath: string;
|
|
11
|
+
} | undefined;
|
|
7
12
|
/** The marker that the entry's key was put in the held-key store: its ref, never the key. */
|
|
8
13
|
export declare const entryWorkspacesMintedScopeKey: (entryKey: string) => string;
|
|
9
14
|
/** The held-key ref of an entry's workspaces key. At most 123 characters: a UUID and an entry key of up to 63. */
|
|
@@ -8,6 +8,8 @@ import { orgTarget, platformTarget } from './own-services.js';
|
|
|
8
8
|
* entry, holds it, and gives it to two places: the app's env (`WORKSPACES_API_KEY`) and, as a sha256, the
|
|
9
9
|
* broker entry (`workspacesKey`). The broker never sees the key and the plan never holds it.
|
|
10
10
|
*/
|
|
11
|
+
/** Worker copies register through Boule, whose registrar answer owns the per-copy key. */
|
|
12
|
+
export const serverWorkspaces = (entry) => entry.workerApp ? undefined : entry.uses?.auth?.workspaces;
|
|
11
13
|
/** The marker that the entry's key was put in the held-key store: its ref, never the key. */
|
|
12
14
|
export const entryWorkspacesMintedScopeKey = (entryKey) => `workspaces_key_minted.${entryKey}`;
|
|
13
15
|
/** The held-key ref of an entry's workspaces key. At most 123 characters: a UUID and an entry key of up to 63. */
|
|
@@ -26,7 +28,7 @@ export const workspacesUrl = (org) => `https://${loginHostnameFor(org)}/api`;
|
|
|
26
28
|
* broker entry either, so the link never points at a private workspace address.
|
|
27
29
|
*/
|
|
28
30
|
export function wantedWorkspaces(org, entry) {
|
|
29
|
-
const workspaces = entry
|
|
31
|
+
const workspaces = serverWorkspaces(entry);
|
|
30
32
|
const host = entryHostnames(org, entry)[0];
|
|
31
33
|
if (!workspaces || !host)
|
|
32
34
|
return null;
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -1538,6 +1538,16 @@ export interface AiOrganisationPort {
|
|
|
1538
1538
|
id: string;
|
|
1539
1539
|
secret: string;
|
|
1540
1540
|
}>;
|
|
1541
|
+
/** Trusted infrastructure-root issuance route; ordinary operator app/user issuance remains ordinary. */
|
|
1542
|
+
issueOrganisationRootKey?(organisationId: string, input: {
|
|
1543
|
+
name: string;
|
|
1544
|
+
expiresDays: number;
|
|
1545
|
+
permissions: string[];
|
|
1546
|
+
}): Promise<{
|
|
1547
|
+
organisationId: string;
|
|
1548
|
+
id: string;
|
|
1549
|
+
secret: string;
|
|
1550
|
+
}>;
|
|
1541
1551
|
/**
|
|
1542
1552
|
* F6a: mints an entry's key below the org's own key, matching ai's
|
|
1543
1553
|
* `POST /v1/keys` (wtfalch/ai#162, SDK `mintKey`), which the management key
|
|
@@ -1569,6 +1579,12 @@ export interface AiOrganisationPort {
|
|
|
1569
1579
|
secret: string;
|
|
1570
1580
|
expiresAt: string;
|
|
1571
1581
|
}>;
|
|
1582
|
+
/** Ordinary descendant revocation authenticated by explicit company parent authority, never the platform token. */
|
|
1583
|
+
revokeDescendantKey?(input: {
|
|
1584
|
+
organisationId: string;
|
|
1585
|
+
credential: string;
|
|
1586
|
+
keyId: string;
|
|
1587
|
+
}): Promise<unknown>;
|
|
1572
1588
|
/**
|
|
1573
1589
|
* F6a: registers `address` as the org's webhook, matching `PUT /v1/webhooks`
|
|
1574
1590
|
* (`setWebhookAddress`). `secret` is present only when ai created the
|
|
@@ -1735,10 +1751,18 @@ export interface JobsSchedulerPort {
|
|
|
1735
1751
|
mintProjectKey?(project: string, grant: {
|
|
1736
1752
|
principal: string;
|
|
1737
1753
|
scopes: readonly string[];
|
|
1754
|
+
expiresAt?: string;
|
|
1738
1755
|
}): Promise<{
|
|
1739
1756
|
id: string;
|
|
1740
1757
|
key: string;
|
|
1741
1758
|
}>;
|
|
1759
|
+
/** Names only, used to recover an ambiguous one-time provisioning mint without deleting runtime projects. */
|
|
1760
|
+
listProjectKeys?(project: string): Promise<{
|
|
1761
|
+
id: string;
|
|
1762
|
+
principal: string;
|
|
1763
|
+
scopes: string[];
|
|
1764
|
+
revoked: boolean;
|
|
1765
|
+
}[]>;
|
|
1742
1766
|
/** Revokes one project key (`DELETE /admin/projects/<name>/keys/<id>`); an unknown key throws with `status: 404`. */
|
|
1743
1767
|
revokeProjectKey?(project: string, keyId: string): Promise<void>;
|
|
1744
1768
|
}
|
|
@@ -4253,6 +4277,7 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
4253
4277
|
* foundry#1122, `plan/ai-boule-key.ts`: Boule's ai key, minted below the org's own ai key under rule 1, held,
|
|
4254
4278
|
* and written to Boule's `AI_URL`, `AI_ORGANISATION_ID` and `AI_API_KEY`. See `ai-boule-key-apply.ts`.
|
|
4255
4279
|
*/
|
|
4280
|
+
private aiAppsMinter;
|
|
4256
4281
|
private aiBouleKey;
|
|
4257
4282
|
/**
|
|
4258
4283
|
* REVOKE SEAM (ai#157): revokes through `revokeKey` when the port has one.
|
|
@@ -4323,6 +4348,8 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
4323
4348
|
* `files-boule-key-apply.ts`.
|
|
4324
4349
|
*/
|
|
4325
4350
|
private filesBouleKey;
|
|
4351
|
+
/** Files for Boule's own apps (`files-apps-minter.ts`): the key Boule mints each app's files key with. */
|
|
4352
|
+
private filesAppsMinter;
|
|
4326
4353
|
/**
|
|
4327
4354
|
* Revokes one entry key. When the call fails the id joins the entry's list
|
|
4328
4355
|
* of keys left live, which the plan shows, and the step goes on: the app
|
package/dist/plan/executor.js
CHANGED
|
@@ -9,6 +9,7 @@ import { NATIVE_BUILD_KEY_ENV } from '../native-build.js';
|
|
|
9
9
|
import { AI_ENTRY_PERMISSIONS, APPS_DOMAIN, APP_ENTRY_KEY, FILES_ENTRY_PERMISSIONS, FILES_SETTINGS_ENV_NAMES, FULL_COMMIT_SHA, MAIL_APP_KEY_ENV, MAIL_SETTINGS_ENV_NAMES, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, appsOn, companyGithubFor, filesCollectionEnvName, heldEnvRef, isLiveEntry, isMainEntry, kosmosOn, mailAppKeyOf, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownsServices, sendsMail, sharedSecretRef, signsInBoth, signsInCustomers, } from '../org.js';
|
|
10
10
|
import { adoptedKeysUnreadable, parseAdoptSnapshot } from './adopt.js';
|
|
11
11
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, DESKTOP_AUTH_PORT, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatCallsApplied, chatHasUpstreams, chatIntrospectionAppName, desiredAgoraEnv, desktopAuthInternalName, entryClientIdScopeKey, } from './agora.js';
|
|
12
|
+
import { ensureAiAppsMinter } from './ai-apps-minter.js';
|
|
12
13
|
import { ensureAiBouleKey } from './ai-boule-key-apply.js';
|
|
13
14
|
import { AI_KEY_PERMISSIONS_SCOPE_KEY, AI_KEY_PREVIOUS_SCOPE_KEY, AI_OFFERS_SCOPE_KEY, parseIds as parseAiIds, } from './ai-boule-key.js';
|
|
14
15
|
import { ANALYTICS_BOULE_APPLIED_SCOPE_KEY, ANALYTICS_BOULE_ENV_NAMES, analyticsBouleMarker, } from './analytics-boule.js';
|
|
@@ -50,12 +51,13 @@ import { applyEntryAudit } from './entry-audit.js';
|
|
|
50
51
|
import { AUTH_EVENTS_ENV, authEventsAppliedScopeKey, authEventsMarker, authEventsWanted, ensureAuthEventsSecret, heldAuthEventsSecret, } from './entry-auth-events.js';
|
|
51
52
|
import { CUSTOMER_GRANTS_CREDENTIAL_ENV, CUSTOMER_GRANTS_ENV, CUSTOMER_GRANTS_WRITTEN_SCOPE_KEY, customerGrantsEntries, customerGrantsJson, customerGrantsMintedScopeKey, customerGrantsRef, customerGrantsToDrop, ensureCustomerGrantsCredential, wantedCustomerGrants, } from './entry-customer-grants.js';
|
|
52
53
|
import { BOULE_EDGE_ENTRY_KEY, EDGE_ENTRY_KEY_DAYS, EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY, EDGE_SLUG_APPLIED_SCOPE_KEY, ENTRY_EDGE_KEY_DELIVERY_ATTEMPTS, bouleEdgeKeyWanted, edgeKeyAppRecordKey, edgeKeyDomain, entryEdgeMintedAtScopeKey, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
53
|
-
import { FILES_AGORA_KEY_SCOPE_KEY, FILES_AGORA_UNDELIVERED_SCOPE_KEY, FILES_KEY_DAYS, FILES_KEY_RENEW_WINDOW_DAYS, FILES_ORG_OWNER_SCOPE_KEY, FILES_ROOT_EXPIRES_SCOPE_KEY, FILES_ROOT_PERMISSIONS_SCOPE_KEY, FILES_ROOT_PREVIOUS_SCOPE_KEY, FILES_ROOT_UNDELIVERED_SCOPE_KEY, FILES_ROOT_UNREVOKED_SCOPE_KEY, bouleKeyBelow, entryFilesAppliedScopeKey, entryFilesCollectionsScopeKey, entryFilesExpiresScopeKey, entryFilesMinterScopeKey, entryFilesPendingExpiresScopeKey, entryFilesRetiredScopeKey, entryFilesUndeliveredScopeKey, entryFilesUnrevokedScopeKey, entryFilesWrittenScopeKey, filesOwnerUserId, knownCollectionNames, parseIdList as parseFilesIdList, } from './entry-files-key.js';
|
|
54
|
+
import { FILES_AGORA_KEY_SCOPE_KEY, FILES_AGORA_UNDELIVERED_SCOPE_KEY, FILES_KEY_DAYS, FILES_KEY_RENEW_WINDOW_DAYS, FILES_ORG_OWNER_SCOPE_KEY, FILES_ROOT_EXPIRES_SCOPE_KEY, FILES_ROOT_PERMISSIONS_SCOPE_KEY, FILES_ROOT_PREVIOUS_SCOPE_KEY, FILES_ROOT_UNDELIVERED_SCOPE_KEY, FILES_ROOT_UNREVOKED_SCOPE_KEY, bouleKeyBelow, entryFilesAppliedScopeKey, entryFilesCollectionsScopeKey, entryFilesExpiresScopeKey, entryFilesMinterScopeKey, entryFilesPendingExpiresScopeKey, entryFilesRetiredScopeKey, entryFilesUndeliveredScopeKey, entryFilesUnrevokedScopeKey, entryFilesWrittenScopeKey, filesOwnerUserId, knownCollectionNames, parseIdList as parseFilesIdList, usesServerFiles, } from './entry-files-key.js';
|
|
54
55
|
import { ensureJobsBouleKey, retireJobsBouleKey } from './entry-jobs-boule-key-apply.js';
|
|
55
56
|
import { entryJobsAppliedScopeKey, entryJobsRefs, entryJobsUndeliveredScopeKey, entryJobsWantMarker, } from './entry-jobs.js';
|
|
56
57
|
import { ensureRepoKey, removeRepoKey } from './entry-repo-key-apply.js';
|
|
57
58
|
import { ENTRY_REPORTING_KEY_DELIVERY_ATTEMPTS, entryReportingAppliedScopeKey, entryReportingRetiredScopeKey, entryReportingUndeliveredScopeKey, stageReportingKeyName, } from './entry-reporting-key.js';
|
|
58
59
|
import { BROKER_WORKSPACES_DATABASE_ENV, ensureWorkspacesKey, heldWorkspacesDatabase, heldWorkspacesKey, workspacesDatabaseRef, workspacesKeyHash, workspacesKeyRef, } from './entry-workspaces.js';
|
|
60
|
+
import { ensureFilesAppsMinter, retireFilesAppsMinter, } from './files-apps-minter.js';
|
|
59
61
|
import { ensureFilesBouleKey, retireFilesBouleKey } from './files-boule-key-apply.js';
|
|
60
62
|
import { applyFilesSpaceAlert, } from './files-space-alert.js';
|
|
61
63
|
import { GATE_CLAIM_NAME, gateHostnamesScopeKey, gateIdRecordKey, isBehindGate } from './gates.js';
|
|
@@ -63,6 +65,7 @@ import { GITHUB_SOURCE_INSTALLATION_SCOPE_KEY, GITHUB_SOURCE_TIMEOUT_MS, ensureG
|
|
|
63
65
|
import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, wgetProbe, } from './health-check-settings.js';
|
|
64
66
|
import { PROXY_LABEL_TOKEN_REF } from './held-key-refs.js';
|
|
65
67
|
import { appHostname, cmsStudioHostnameFor, coolifyTunnelDomains, entryHostnames, siteHostnameFor, tunnelDnsHostnames, tunnelIngressHostnames, workspaceHost, } from './hostnames.js';
|
|
68
|
+
import { applyJobsAppsKey, jobsAppsRef } from './jobs-apps-key.js';
|
|
66
69
|
import { applyJobsProjectSettings, } from './jobs-project-settings.js';
|
|
67
70
|
import { KEYS_REGISTRAR_DELIVERY_ATTEMPTS, KEYS_REGISTRAR_KEY_DAYS, KEYS_REGISTRAR_KEY_ENV, KEYS_REGISTRAR_MINTED_AT_SCOPE_KEY, KEYS_REGISTRAR_MINT_STARTED_SCOPE_KEY, KEYS_REGISTRAR_SCOPE_KEY, KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY, KEYS_REGISTRAR_URL_APPLIED_SCOPE_KEY, KEYS_REGISTRAR_URL_ENV, } from './keys-registrar.js';
|
|
68
71
|
import { KOSMOS_CLIENT_ID_SCOPE_KEY, KOSMOS_DATABASE_SCOPE_KEY, KOSMOS_DEPLOYED_SCOPE_KEY, desiredKosmosEnv, kosmosEnvDrift, kosmosHostnameFor, } from './kosmos.js';
|
|
@@ -1452,6 +1455,10 @@ export class FoundryStepExecutor {
|
|
|
1452
1455
|
return this.filesOrganisation(action, org, context);
|
|
1453
1456
|
case 'files:boule-key':
|
|
1454
1457
|
return this.filesBouleKey(action, org, context);
|
|
1458
|
+
case 'ai:apps-minter':
|
|
1459
|
+
return this.aiAppsMinter(action, org, context);
|
|
1460
|
+
case 'files:apps-minter':
|
|
1461
|
+
return this.filesAppsMinter(action, org, context);
|
|
1455
1462
|
case 'coolify:service-alert-mail': {
|
|
1456
1463
|
await applyServiceAlertMail({
|
|
1457
1464
|
want: action.desired,
|
|
@@ -1523,6 +1530,29 @@ export class FoundryStepExecutor {
|
|
|
1523
1530
|
return this.edgeSlug(action, org, context);
|
|
1524
1531
|
case 'jobs:project':
|
|
1525
1532
|
return this.jobsProject(action, org, context);
|
|
1533
|
+
case 'jobs:apps-key': {
|
|
1534
|
+
const held = this.ports.heldKeys;
|
|
1535
|
+
if (!held?.put)
|
|
1536
|
+
throw new Error('jobs:apps-key: held keys cannot put');
|
|
1537
|
+
const desired = action.desired;
|
|
1538
|
+
if (desired.mode === 'ensure' && org.boule?.apps !== true)
|
|
1539
|
+
throw new Error('jobs:apps-key: apps are off now');
|
|
1540
|
+
if (desired.mode === 'retire' && org.boule?.apps === true)
|
|
1541
|
+
throw new Error('jobs:apps-key: apps are on again');
|
|
1542
|
+
await applyJobsAppsKey({
|
|
1543
|
+
scheduler: await this.jobsPort(org),
|
|
1544
|
+
heldKeys: { resolve: held.resolve.bind(held), put: held.put.bind(held) },
|
|
1545
|
+
coolify: this.ports.coolify,
|
|
1546
|
+
org,
|
|
1547
|
+
bouleAppId: context.recordedIds()[`coolify:boule:${org.id}`],
|
|
1548
|
+
recorded: () => context.recordedIds(),
|
|
1549
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
1550
|
+
forget: (key) => forgetScopeId(context, key),
|
|
1551
|
+
persist: () => context.persistScopeIds(),
|
|
1552
|
+
secret: (value) => void this.handledSecrets.add(value),
|
|
1553
|
+
}, desired);
|
|
1554
|
+
return {};
|
|
1555
|
+
}
|
|
1526
1556
|
case 'jobs:boule-key':
|
|
1527
1557
|
return this.jobsBouleKey(action, org, context);
|
|
1528
1558
|
case 'github:deploy-key':
|
|
@@ -5533,6 +5563,9 @@ export class FoundryStepExecutor {
|
|
|
5533
5563
|
if (!entry?.uses?.auth) {
|
|
5534
5564
|
throw new Error(`coolify:broker-entry for ${org.slug}: no uses.auth entry for ${action.key}`);
|
|
5535
5565
|
}
|
|
5566
|
+
if (entry.workerApp && entry.uses.auth.workspaces) {
|
|
5567
|
+
throw new Error(`coolify:broker-entry for ${org.slug}: Worker workspaces are registered through Boule, not AUTH_APPS`);
|
|
5568
|
+
}
|
|
5536
5569
|
const wanted = wantedBrokerEntry(org, entry, recorded);
|
|
5537
5570
|
if ('missing' in wanted) {
|
|
5538
5571
|
throw new Error(`coolify:broker-entry for ${org.slug}: ${wanted.missing.map((m) => m.what).join(', ')} not recorded yet`);
|
|
@@ -11377,7 +11410,12 @@ export class FoundryStepExecutor {
|
|
|
11377
11410
|
*/
|
|
11378
11411
|
async aiKey(action, org, context) {
|
|
11379
11412
|
const ai = await this.aiPort();
|
|
11380
|
-
const
|
|
11413
|
+
const issue = action.desired.protectedRoot
|
|
11414
|
+
? ai.issueOrganisationRootKey?.bind(ai)
|
|
11415
|
+
: ai.issueOrganisationKey.bind(ai);
|
|
11416
|
+
if (!issue)
|
|
11417
|
+
throw new Error(`ai:key for ${org.slug}: the released infrastructure-root adapter is required`);
|
|
11418
|
+
const issued = await issue(org.id, {
|
|
11381
11419
|
name: action.desired.name,
|
|
11382
11420
|
expiresDays: action.desired.expiresDays,
|
|
11383
11421
|
permissions: action.desired.permissions ?? [],
|
|
@@ -11397,6 +11435,7 @@ export class FoundryStepExecutor {
|
|
|
11397
11435
|
// foundry#1122: what the key holds, so a narrower one is minted again (`planAiKey`). A key it
|
|
11398
11436
|
// replaced stays live until it expires (no revoke route yet, ai#157); the plan lists it.
|
|
11399
11437
|
context.recordScopeId(AI_KEY_PERMISSIONS_SCOPE_KEY, JSON.stringify(action.desired.permissions ?? []));
|
|
11438
|
+
context.recordScopeId('ai_key_purpose', action.desired.protectedRoot ? 'company_root' : 'ordinary');
|
|
11400
11439
|
const replaced = context.recordedIds()[`ai:key:${org.id}`];
|
|
11401
11440
|
if (replaced && replaced !== issued.id) {
|
|
11402
11441
|
const previous = parseAiIds(context.recordedIds()[AI_KEY_PREVIOUS_SCOPE_KEY]);
|
|
@@ -11565,6 +11604,32 @@ export class FoundryStepExecutor {
|
|
|
11565
11604
|
* foundry#1122, `plan/ai-boule-key.ts`: Boule's ai key, minted below the org's own ai key under rule 1, held,
|
|
11566
11605
|
* and written to Boule's `AI_URL`, `AI_ORGANISATION_ID` and `AI_API_KEY`. See `ai-boule-key-apply.ts`.
|
|
11567
11606
|
*/
|
|
11607
|
+
async aiAppsMinter(action, org, context) {
|
|
11608
|
+
const ai = await this.aiPort();
|
|
11609
|
+
const held = this.ports.heldKeys;
|
|
11610
|
+
if (!ai.revokeDescendantKey || !held?.put)
|
|
11611
|
+
throw new Error(`ai:apps-minter for ${org.slug}: scoped lifecycle and held-key adapters are required`);
|
|
11612
|
+
const step = action.desired;
|
|
11613
|
+
if (step.mode === 'retire' && org.boule?.apps === true)
|
|
11614
|
+
throw new Error(`ai:apps-minter for ${org.slug}: apps are on again; nothing was revoked`);
|
|
11615
|
+
await ensureAiAppsMinter({
|
|
11616
|
+
ai: {
|
|
11617
|
+
mintNestedKey: ai.mintNestedKey.bind(ai),
|
|
11618
|
+
revokeDescendantKey: ai.revokeDescendantKey.bind(ai),
|
|
11619
|
+
},
|
|
11620
|
+
heldKeys: { resolve: held.resolve.bind(held), put: held.put.bind(held) },
|
|
11621
|
+
coolify: this.ports.coolify,
|
|
11622
|
+
org,
|
|
11623
|
+
bouleAppId: context.recordedIds()[`coolify:boule:${org.id}`],
|
|
11624
|
+
recorded: () => context.recordedIds(),
|
|
11625
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
11626
|
+
forget: (key) => forgetScopeId(context, key),
|
|
11627
|
+
persist: () => context.persistScopeIds(),
|
|
11628
|
+
secret: (value) => void this.handledSecrets.add(value),
|
|
11629
|
+
now: () => context.now?.() ?? Date.now(),
|
|
11630
|
+
}, step);
|
|
11631
|
+
return {};
|
|
11632
|
+
}
|
|
11568
11633
|
async aiBouleKey(action, org, context) {
|
|
11569
11634
|
const at = `ai:boule-key for ${org.slug}`;
|
|
11570
11635
|
const ai = await this.aiPort();
|
|
@@ -11963,7 +12028,7 @@ export class FoundryStepExecutor {
|
|
|
11963
12028
|
// Every entry must already hold a key below the new org key; otherwise
|
|
11964
12029
|
// an app would lose its key. The next plan asks again.
|
|
11965
12030
|
const ready = org.hosting.entries
|
|
11966
|
-
.filter(
|
|
12031
|
+
.filter(usesServerFiles)
|
|
11967
12032
|
.every((e) => recorded[`files:key:${org.id}:${e.key}`] &&
|
|
11968
12033
|
recorded[entryFilesMinterScopeKey(e.key)] === rootId &&
|
|
11969
12034
|
!recorded[entryFilesUndeliveredScopeKey(e.key)]);
|
|
@@ -12213,7 +12278,7 @@ export class FoundryStepExecutor {
|
|
|
12213
12278
|
now: () => context.now?.() ?? Date.now(),
|
|
12214
12279
|
};
|
|
12215
12280
|
if (desired.mode === 'retire') {
|
|
12216
|
-
if (org.hosting.enabled && org.hosting.entries.some(
|
|
12281
|
+
if (org.hosting.enabled && org.hosting.entries.some(usesServerFiles)) {
|
|
12217
12282
|
throw new Error(`${at}: an entry declares uses.files again; nothing was revoked`);
|
|
12218
12283
|
}
|
|
12219
12284
|
await retireFilesBouleKey(deps);
|
|
@@ -12224,6 +12289,47 @@ export class FoundryStepExecutor {
|
|
|
12224
12289
|
await ensureFilesBouleKey(deps, action.desired);
|
|
12225
12290
|
return {};
|
|
12226
12291
|
}
|
|
12292
|
+
/** Files for Boule's own apps (`files-apps-minter.ts`): the key Boule mints each app's files key with. */
|
|
12293
|
+
async filesAppsMinter(action, org, context) {
|
|
12294
|
+
const at = `files:apps-minter for ${org.slug}`;
|
|
12295
|
+
const files = await this.filesPort();
|
|
12296
|
+
const held = this.ports.heldKeys;
|
|
12297
|
+
const put = held?.put?.bind(held);
|
|
12298
|
+
const desired = action.desired;
|
|
12299
|
+
const deps = {
|
|
12300
|
+
files: {
|
|
12301
|
+
issueOrganisationKey: files.issueOrganisationKey.bind(files),
|
|
12302
|
+
revokeOrganisationKey: files.revokeOrganisationKey.bind(files),
|
|
12303
|
+
},
|
|
12304
|
+
heldKeys: {
|
|
12305
|
+
resolve: async (ref) => (held ? held.resolve(ref) : null),
|
|
12306
|
+
put: async (ref, value) => {
|
|
12307
|
+
if (!put)
|
|
12308
|
+
throw new Error('the held keys cannot put');
|
|
12309
|
+
await put(ref, value);
|
|
12310
|
+
},
|
|
12311
|
+
},
|
|
12312
|
+
coolify: this.ports.coolify,
|
|
12313
|
+
org: { id: org.id, slug: org.slug },
|
|
12314
|
+
bouleAppId: context.recordedIds()[`coolify:boule:${org.id}`],
|
|
12315
|
+
recorded: () => context.recordedIds(),
|
|
12316
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
12317
|
+
forget: (key) => forgetScopeId(context, key),
|
|
12318
|
+
persist: () => context.persistScopeIds(),
|
|
12319
|
+
secret: (value) => void this.handledSecrets.add(value),
|
|
12320
|
+
};
|
|
12321
|
+
if (desired.mode === 'retire') {
|
|
12322
|
+
if (org.boule?.apps === true) {
|
|
12323
|
+
throw new Error(`${at}: apps are on again; nothing was revoked`);
|
|
12324
|
+
}
|
|
12325
|
+
await retireFilesAppsMinter(deps);
|
|
12326
|
+
return {};
|
|
12327
|
+
}
|
|
12328
|
+
if (!held || !put)
|
|
12329
|
+
throw new Error(`${at}: the held keys cannot hold the key`);
|
|
12330
|
+
await ensureFilesAppsMinter(deps, action.desired);
|
|
12331
|
+
return {};
|
|
12332
|
+
}
|
|
12227
12333
|
/**
|
|
12228
12334
|
* Revokes one entry key. When the call fails the id joins the entry's list
|
|
12229
12335
|
* of keys left live, which the plan shows, and the step goes on: the app
|
|
@@ -12468,6 +12574,21 @@ export class FoundryStepExecutor {
|
|
|
12468
12574
|
await port.deleteOrganisation(stageOrgId);
|
|
12469
12575
|
return;
|
|
12470
12576
|
}
|
|
12577
|
+
case 'jobs:apps-projects': {
|
|
12578
|
+
const scheduler = await this.jobsPort(org);
|
|
12579
|
+
if (!scheduler.deleteProject)
|
|
12580
|
+
throw new Error('jobs:apps-projects: project removal is not wired');
|
|
12581
|
+
try {
|
|
12582
|
+
await withTimeout(scheduler.deleteProject(target.id), 30_000);
|
|
12583
|
+
}
|
|
12584
|
+
catch (error) {
|
|
12585
|
+
if (!isNotFoundError(error))
|
|
12586
|
+
throw new Error('jobs:apps-projects: the project could not be removed');
|
|
12587
|
+
}
|
|
12588
|
+
if (this.ports.heldKeys?.put)
|
|
12589
|
+
await withTimeout(this.ports.heldKeys.put(jobsAppsRef(org.id), ''), 30_000);
|
|
12590
|
+
return;
|
|
12591
|
+
}
|
|
12471
12592
|
case 'jobs:bg-keys':
|
|
12472
12593
|
// A jobs project's keys go with the project.
|
|
12473
12594
|
return;
|