@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.
Files changed (64) hide show
  1. package/dist/catalogue/copy-en.js +5 -0
  2. package/dist/catalogue/integrations.js +5 -0
  3. package/dist/mcp-resources/client.d.ts +16 -0
  4. package/dist/mcp-resources/client.js +69 -0
  5. package/dist/mcp-resources/index.d.ts +3 -0
  6. package/dist/mcp-resources/index.js +3 -0
  7. package/dist/mcp-resources/schema.d.ts +1707 -0
  8. package/dist/mcp-resources/schema.js +253 -0
  9. package/dist/mcp-resources/validation.d.ts +21 -0
  10. package/dist/mcp-resources/validation.js +97 -0
  11. package/dist/modules/ai.js +14 -3
  12. package/dist/modules/customer-grants.js +1 -1
  13. package/dist/modules/email.d.ts +2 -1
  14. package/dist/modules/email.js +3 -2
  15. package/dist/modules/files.js +4 -1
  16. package/dist/modules/index.js +4 -1
  17. package/dist/modules/jobs.js +1 -1
  18. package/dist/modules/mcp-resources.d.ts +2 -0
  19. package/dist/modules/mcp-resources.js +61 -0
  20. package/dist/modules/outputs.js +1 -0
  21. package/dist/modules/repository.d.ts +2 -0
  22. package/dist/modules/repository.js +3 -1
  23. package/dist/modules/workspaces.js +1 -1
  24. package/dist/org.d.ts +3 -0
  25. package/dist/org.js +10 -7
  26. package/dist/plan/ai-apps-minter.d.ts +79 -0
  27. package/dist/plan/ai-apps-minter.js +263 -0
  28. package/dist/plan/ai-boule-key.d.ts +1 -1
  29. package/dist/plan/ai-boule-key.js +10 -3
  30. package/dist/plan/broker-entries.js +4 -4
  31. package/dist/plan/diff.d.ts +6 -0
  32. package/dist/plan/diff.js +33 -13
  33. package/dist/plan/entry-ai-key.d.ts +2 -0
  34. package/dist/plan/entry-ai-key.js +4 -2
  35. package/dist/plan/entry-customer-grants.d.ts +1 -1
  36. package/dist/plan/entry-customer-grants.js +4 -2
  37. package/dist/plan/entry-files-key.d.ts +10 -0
  38. package/dist/plan/entry-files-key.js +6 -1
  39. package/dist/plan/entry-jobs-boule-key.js +1 -1
  40. package/dist/plan/entry-jobs.js +1 -1
  41. package/dist/plan/entry-repo-key.js +2 -2
  42. package/dist/plan/entry-workspaces.d.ts +5 -0
  43. package/dist/plan/entry-workspaces.js +3 -1
  44. package/dist/plan/executor.d.ts +27 -0
  45. package/dist/plan/executor.js +125 -4
  46. package/dist/plan/files-apps-minter.d.ts +131 -0
  47. package/dist/plan/files-apps-minter.js +336 -0
  48. package/dist/plan/files-boule-key.js +2 -2
  49. package/dist/plan/files-space-alert.js +3 -3
  50. package/dist/plan/jobs-apps-key.d.ts +29 -0
  51. package/dist/plan/jobs-apps-key.js +228 -0
  52. package/dist/plan/jobs-project-settings.js +2 -0
  53. package/dist/plan/jobs-scheduler-http.d.ts +7 -0
  54. package/dist/plan/jobs-scheduler-http.js +32 -1
  55. package/dist/plan/own-jobs-route.d.ts +1 -1
  56. package/dist/plan/own-jobs-route.js +1 -1
  57. package/dist/plan/plan-apply.d.ts +6 -0
  58. package/dist/plan/plan-apply.js +45 -3
  59. package/dist/plan/platform-diff.d.ts +1 -1
  60. package/dist/plan/platform-diff.js +3 -2
  61. package/dist/release/state.d.ts +1 -1
  62. package/dist/store/schema.d.ts +2 -2
  63. package/dist/worker-app-config.js +11 -2
  64. package/package.json +5 -1
@@ -0,0 +1,253 @@
1
+ import { z } from 'zod';
2
+ export const MCP_RESOURCE_CONTRACT_VERSION = 1;
3
+ const opaqueId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/);
4
+ const revision = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
5
+ const epoch = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
6
+ const exactIdentifier = z
7
+ .string()
8
+ .min(1)
9
+ .max(512)
10
+ .regex(/^[^\s\p{Cc}<>]+$/u);
11
+ const text = (max) => z
12
+ .string()
13
+ .refine((value) => value.trim().length > 0)
14
+ .min(1)
15
+ .max(max)
16
+ .regex(/^[^\p{Cc}<>]+$/u);
17
+ const httpsUrl = z
18
+ .string()
19
+ .max(2048)
20
+ .refine((value) => {
21
+ try {
22
+ const url = new URL(value);
23
+ return (url.protocol === 'https:' &&
24
+ !value.includes('?') &&
25
+ !value.includes('#') &&
26
+ !url.username &&
27
+ !url.password &&
28
+ !url.search &&
29
+ !url.hash &&
30
+ url.href === value);
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ });
36
+ // Auth may identify an issuer by its exact origin without a trailing slash.
37
+ // Accept that spelling without transforming it into a different issuer identifier.
38
+ const httpsIssuer = httpsUrl.or(z
39
+ .string()
40
+ .max(2048)
41
+ .refine((value) => {
42
+ try {
43
+ const url = new URL(value);
44
+ return url.protocol === 'https:' && url.origin === value;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }));
50
+ export const McpResourceNamespaceSchema = z.strictObject({
51
+ providerTenantId: opaqueId,
52
+ appId: opaqueId,
53
+ copyId: opaqueId,
54
+ });
55
+ export const McpResourceKeySchema = McpResourceNamespaceSchema.extend({ resourceId: opaqueId });
56
+ export const McpResourceScopeSchema = z.strictObject({
57
+ id: z
58
+ .string()
59
+ .min(1)
60
+ .max(128)
61
+ .regex(/^[\x21\x23-\x5b\x5d-\x7e]+$/),
62
+ title: text(120).optional(),
63
+ description: text(1000),
64
+ });
65
+ export const McpResourceBrandingSchema = z.strictObject({
66
+ displayName: text(120),
67
+ description: text(1000).optional(),
68
+ locale: z
69
+ .string()
70
+ .min(2)
71
+ .max(35)
72
+ .regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/)
73
+ .optional(),
74
+ });
75
+ export const McpResourceAdmissionSchema = z.discriminatedUnion('mode', [
76
+ z.strictObject({ mode: z.literal('unrestricted') }),
77
+ z.strictObject({
78
+ mode: z.literal('application-approval'),
79
+ policyId: opaqueId,
80
+ requiredEvidence: z
81
+ .array(z.enum(['provider', 'authentication-method', 'authentication-time', 'groups']))
82
+ .max(4)
83
+ .refine((items) => new Set(items).size === items.length),
84
+ returnPath: z
85
+ .string()
86
+ .max(200)
87
+ .regex(/^\/[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/),
88
+ }),
89
+ ]);
90
+ export const McpResourceDesiredSchema = z
91
+ .strictObject({
92
+ state: z.enum(['active', 'suspended', 'retired']),
93
+ configurationRevision: revision,
94
+ policyRevision: revision,
95
+ denyEpoch: epoch,
96
+ scopes: z
97
+ .array(McpResourceScopeSchema)
98
+ .max(128)
99
+ .refine((scopes) => new Set(scopes.map((scope) => scope.id)).size === scopes.length),
100
+ admission: McpResourceAdmissionSchema,
101
+ branding: McpResourceBrandingSchema.optional(),
102
+ })
103
+ .refine((desired) => desired.state !== 'active' || desired.scopes.length > 0);
104
+ export const McpResourceIdentitySchema = z.strictObject({
105
+ registrationId: exactIdentifier,
106
+ resourceUri: httpsUrl,
107
+ audience: exactIdentifier,
108
+ issuer: httpsIssuer,
109
+ });
110
+ export const McpResourcePhaseSchema = z.enum([
111
+ 'registration',
112
+ 'discovery',
113
+ 'registry',
114
+ 'runtime',
115
+ 'revocation',
116
+ 'cleanup',
117
+ 'complete',
118
+ ]);
119
+ export const McpResourceObservedSchema = z.strictObject({
120
+ state: z.enum(['absent', 'applying', 'active', 'suspended', 'retiring', 'retired']),
121
+ configurationRevision: revision.nullable(),
122
+ policyRevision: revision.nullable(),
123
+ denyEpoch: epoch,
124
+ phase: McpResourcePhaseSchema,
125
+ revocation: z.enum(['not-required', 'pending', 'complete']),
126
+ cleanup: z.enum(['not-required', 'pending', 'complete']),
127
+ });
128
+ export const McpResourceSnapshotSchema = z.strictObject({
129
+ version: z.literal(MCP_RESOURCE_CONTRACT_VERSION),
130
+ key: McpResourceKeySchema,
131
+ generation: revision,
132
+ identity: McpResourceIdentitySchema.nullable(),
133
+ desired: McpResourceDesiredSchema,
134
+ observed: McpResourceObservedSchema,
135
+ });
136
+ export const McpResourceApplyRequestSchema = z.strictObject({
137
+ version: z.literal(MCP_RESOURCE_CONTRACT_VERSION),
138
+ key: McpResourceKeySchema,
139
+ operationId: opaqueId,
140
+ expectedGeneration: revision.nullable(),
141
+ desired: McpResourceDesiredSchema,
142
+ });
143
+ export const McpResourceReadRequestSchema = z.strictObject({
144
+ version: z.literal(MCP_RESOURCE_CONTRACT_VERSION),
145
+ key: McpResourceKeySchema,
146
+ });
147
+ export const McpResourceListRequestSchema = z.strictObject({
148
+ version: z.literal(MCP_RESOURCE_CONTRACT_VERSION),
149
+ namespace: McpResourceNamespaceSchema,
150
+ cursor: opaqueId.optional(),
151
+ });
152
+ export const McpResourceOperationRequestSchema = McpResourceReadRequestSchema.extend({
153
+ operationId: opaqueId,
154
+ });
155
+ export const McpResourceFailureSchema = z.strictObject({
156
+ code: z.enum([
157
+ 'forbidden',
158
+ 'invalid-request',
159
+ 'generation-mismatch',
160
+ 'idempotency-mismatch',
161
+ 'retired',
162
+ 'revision-regression',
163
+ 'backend-unsupported',
164
+ 'backend-unavailable',
165
+ 'provider-failure',
166
+ 'invalid-response',
167
+ 'transport-failure',
168
+ ]),
169
+ phase: McpResourcePhaseSchema,
170
+ });
171
+ export const McpResourceRecoverySchema = z.discriminatedUnion('action', [
172
+ z.strictObject({
173
+ action: z.literal('poll-operation'),
174
+ afterMs: z.number().int().min(0).max(86_400_000),
175
+ }),
176
+ z.strictObject({
177
+ action: z.literal('retry-same-operation'),
178
+ afterMs: z.number().int().min(0).max(86_400_000),
179
+ }),
180
+ z.strictObject({ action: z.literal('read-resource') }),
181
+ z.strictObject({ action: z.literal('operator-intervention') }),
182
+ z.strictObject({ action: z.literal('none') }),
183
+ ]);
184
+ const outcome = { version: z.literal(MCP_RESOURCE_CONTRACT_VERSION) };
185
+ const operation = { ...outcome, operationId: opaqueId };
186
+ const problem = { failure: McpResourceFailureSchema, recovery: McpResourceRecoverySchema };
187
+ export const McpResourceApplyResultSchema = z.discriminatedUnion('outcome', [
188
+ z.strictObject({
189
+ ...operation,
190
+ outcome: z.literal('applied'),
191
+ snapshot: McpResourceSnapshotSchema,
192
+ }),
193
+ z.strictObject({
194
+ ...operation,
195
+ outcome: z.literal('pending'),
196
+ snapshot: McpResourceSnapshotSchema,
197
+ recovery: McpResourceRecoverySchema,
198
+ }),
199
+ z.strictObject({
200
+ ...operation,
201
+ outcome: z.literal('partial'),
202
+ snapshot: McpResourceSnapshotSchema,
203
+ ...problem,
204
+ }),
205
+ z.strictObject({
206
+ ...operation,
207
+ outcome: z.literal('conflict'),
208
+ snapshot: McpResourceSnapshotSchema.nullable(),
209
+ ...problem,
210
+ }),
211
+ z.strictObject({ ...operation, outcome: z.literal('refused'), ...problem }),
212
+ z.strictObject({ ...operation, outcome: z.literal('unavailable'), ...problem }),
213
+ ]);
214
+ export const McpResourceReadResultSchema = z.discriminatedUnion('outcome', [
215
+ z.strictObject({ ...outcome, outcome: z.literal('found'), snapshot: McpResourceSnapshotSchema }),
216
+ z.strictObject({ ...outcome, outcome: z.literal('not-found') }),
217
+ z.strictObject({ ...outcome, outcome: z.literal('refused'), ...problem }),
218
+ z.strictObject({ ...outcome, outcome: z.literal('unavailable'), ...problem }),
219
+ ]);
220
+ export const McpResourceListResultSchema = z.discriminatedUnion('outcome', [
221
+ z.strictObject({
222
+ ...outcome,
223
+ outcome: z.literal('listed'),
224
+ resources: z.array(McpResourceSnapshotSchema).max(100),
225
+ nextCursor: opaqueId.nullable(),
226
+ }),
227
+ z.strictObject({ ...outcome, outcome: z.literal('refused'), ...problem }),
228
+ z.strictObject({ ...outcome, outcome: z.literal('unavailable'), ...problem }),
229
+ ]);
230
+ export const McpResourceOperationResultSchema = z.union([
231
+ McpResourceApplyResultSchema,
232
+ z.strictObject({ ...operation, outcome: z.literal('not-found') }),
233
+ ]);
234
+ export const McpResourceIntegrationSchema = z.strictObject({
235
+ version: z.literal(MCP_RESOURCE_CONTRACT_VERSION),
236
+ pathPrefix: z
237
+ .string()
238
+ .max(160)
239
+ .regex(/^\/[a-z0-9_-]+(?:\/[a-z0-9_-]+)*$/),
240
+ });
241
+ export const MCP_RESOURCE_ENV = {
242
+ managementUrl: 'MCP_MANAGEMENT_URL',
243
+ managementCredential: 'MCP_MANAGEMENT_CREDENTIAL',
244
+ namespace: 'MCP_RESOURCE_NAMESPACE',
245
+ pathPrefix: 'MCP_RESOURCE_PATH_PREFIX',
246
+ };
247
+ /** Non-secret bootstrap. The management credential is delivered separately as a server secret. */
248
+ export const McpResourceBootstrapSchema = z.strictObject({
249
+ version: z.literal(MCP_RESOURCE_CONTRACT_VERSION),
250
+ managementUrl: httpsUrl,
251
+ namespace: McpResourceNamespaceSchema,
252
+ pathPrefix: McpResourceIntegrationSchema.shape.pathPrefix,
253
+ });
@@ -0,0 +1,21 @@
1
+ import { type McpResourceApplyRequest, type McpResourceDesired, type McpResourceFailure, type McpResourceKey, type McpResourceNamespace, type McpResourceSnapshot } from './schema.js';
2
+ export type McpResourceManagementAction = 'apply' | 'read' | 'list' | 'get-operation';
3
+ /** Created by the server from authenticated installation authority, never from the request body. */
4
+ export interface McpResourceManagementAuthority {
5
+ namespace: McpResourceNamespace;
6
+ actions: readonly McpResourceManagementAction[];
7
+ }
8
+ export declare function sameMcpResourceNamespace(a: McpResourceNamespace, b: McpResourceNamespace): boolean;
9
+ export declare function sameMcpResourceKey(a: McpResourceKey, b: McpResourceKey): boolean;
10
+ export declare function admitsMcpResourceManagement(authority: McpResourceManagementAuthority, action: McpResourceManagementAction, target: McpResourceNamespace): boolean;
11
+ /** Check the stored operation receipt BEFORE this guard: a replay recovers its original result. */
12
+ export declare function checkMcpResourceApply(request: McpResourceApplyRequest, current: McpResourceSnapshot | null): McpResourceFailure | null;
13
+ export declare function sameMcpResourceDesired(a: McpResourceDesired, b: McpResourceDesired): boolean;
14
+ /** Canonical non-secret payload for a durable (key, apply, operationId) idempotency receipt. */
15
+ export declare function mcpResourceApplyFingerprint(request: McpResourceApplyRequest): string;
16
+ /** Exact canonical runtime path; the hosting installation selects the immutable origin separately. */
17
+ export declare function mcpResourcePath(pathPrefix: string, resourceId: string): string;
18
+ export declare function mcpResourceUrls(origin: string, pathPrefix: string, resourceId: string): {
19
+ resourceUri: string;
20
+ protectedResourceMetadataUri: string;
21
+ };
@@ -0,0 +1,97 @@
1
+ import { McpResourceApplyRequestSchema, McpResourceIntegrationSchema, McpResourceKeySchema, McpResourceNamespaceSchema, } from './schema.js';
2
+ export function sameMcpResourceNamespace(a, b) {
3
+ return a.providerTenantId === b.providerTenantId && a.appId === b.appId && a.copyId === b.copyId;
4
+ }
5
+ export function sameMcpResourceKey(a, b) {
6
+ return sameMcpResourceNamespace(a, b) && a.resourceId === b.resourceId;
7
+ }
8
+ export function admitsMcpResourceManagement(authority, action, target) {
9
+ return (McpResourceNamespaceSchema.safeParse(authority.namespace).success &&
10
+ (McpResourceNamespaceSchema.safeParse(target).success ||
11
+ McpResourceKeySchema.safeParse(target).success) &&
12
+ sameMcpResourceNamespace(authority.namespace, target) &&
13
+ authority.actions.includes(action));
14
+ }
15
+ /** Check the stored operation receipt BEFORE this guard: a replay recovers its original result. */
16
+ export function checkMcpResourceApply(request, current) {
17
+ if (!McpResourceApplyRequestSchema.safeParse(request).success) {
18
+ return { code: 'invalid-request', phase: 'registration' };
19
+ }
20
+ if (current && !sameMcpResourceKey(request.key, current.key)) {
21
+ return { code: 'forbidden', phase: 'registration' };
22
+ }
23
+ if (request.expectedGeneration !== (current?.generation ?? null)) {
24
+ return { code: 'generation-mismatch', phase: 'registration' };
25
+ }
26
+ if (current?.desired.state === 'retired' || current?.observed.state === 'retired') {
27
+ return { code: 'retired', phase: 'registration' };
28
+ }
29
+ if (current &&
30
+ (request.desired.denyEpoch < current.desired.denyEpoch ||
31
+ request.desired.configurationRevision < current.desired.configurationRevision ||
32
+ request.desired.policyRevision < current.desired.policyRevision))
33
+ return { code: 'revision-regression', phase: 'registration' };
34
+ if (current) {
35
+ const before = current.desired;
36
+ const after = request.desired;
37
+ const sameDefinition = JSON.stringify(ordered([before.scopes, before.admission, before.branding])) ===
38
+ JSON.stringify(ordered([after.scopes, after.admission, after.branding]));
39
+ const admissionChanged = JSON.stringify(ordered(before.admission)) !== JSON.stringify(ordered(after.admission));
40
+ const restrictive = (after.state !== 'active' && after.state !== before.state) ||
41
+ before.scopes.some((scope) => !after.scopes.some((next) => next.id === scope.id)) ||
42
+ (admissionChanged && after.admission.mode === 'application-approval');
43
+ if ((!sameDefinition && after.configurationRevision <= before.configurationRevision) ||
44
+ (admissionChanged && after.policyRevision <= before.policyRevision) ||
45
+ (restrictive &&
46
+ (after.denyEpoch <= before.denyEpoch || after.policyRevision <= before.policyRevision))) {
47
+ return { code: 'revision-regression', phase: 'registration' };
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ const ordered = (value) => {
53
+ if (Array.isArray(value))
54
+ return value.map(ordered);
55
+ if (value !== null && typeof value === 'object') {
56
+ return Object.fromEntries(Object.entries(value)
57
+ .sort(([a], [b]) => a.localeCompare(b))
58
+ .map(([key, item]) => [key, ordered(item)]));
59
+ }
60
+ return value;
61
+ };
62
+ export function sameMcpResourceDesired(a, b) {
63
+ return JSON.stringify(ordered(a)) === JSON.stringify(ordered(b));
64
+ }
65
+ /** Canonical non-secret payload for a durable (key, apply, operationId) idempotency receipt. */
66
+ export function mcpResourceApplyFingerprint(request) {
67
+ const checked = McpResourceApplyRequestSchema.safeParse(request);
68
+ if (!checked.success)
69
+ throw new Error('Invalid MCP resource apply request');
70
+ const { operationId: _operationId, ...payload } = checked.data;
71
+ return JSON.stringify(ordered(payload));
72
+ }
73
+ /** Exact canonical runtime path; the hosting installation selects the immutable origin separately. */
74
+ export function mcpResourcePath(pathPrefix, resourceId) {
75
+ const integration = McpResourceIntegrationSchema.safeParse({ version: 1, pathPrefix });
76
+ const key = McpResourceKeySchema.shape.resourceId.safeParse(resourceId);
77
+ if (!integration.success || !key.success)
78
+ throw new Error('Invalid MCP resource path');
79
+ return `${integration.data.pathPrefix}/${key.data}`;
80
+ }
81
+ export function mcpResourceUrls(origin, pathPrefix, resourceId) {
82
+ let url;
83
+ try {
84
+ url = new URL(origin);
85
+ }
86
+ catch {
87
+ throw new Error('Invalid MCP resource origin');
88
+ }
89
+ if (url.protocol !== 'https:' || url.origin !== origin || url.username || url.password) {
90
+ throw new Error('Invalid MCP resource origin');
91
+ }
92
+ const path = mcpResourcePath(pathPrefix, resourceId);
93
+ return {
94
+ resourceUri: `${origin}${path}`,
95
+ protectedResourceMetadataUri: `${origin}/.well-known/oauth-protected-resource${path}`,
96
+ };
97
+ }
@@ -1,5 +1,5 @@
1
1
  import { AI_ENTRY_PERMISSIONS, AI_WEBHOOK_PATH } from '../entry-rules.js';
2
- import { list, setUse, text } from './entry-parts.js';
2
+ import { list, problem, setUse, text } from './entry-parts.js';
3
3
  /**
4
4
  * Plain words for every permission an app's AI key may hold. A `Record` over the permission list, so a
5
5
  * permission added to {@link AI_ENTRY_PERMISSIONS} fails the typecheck until it has words here.
@@ -22,7 +22,7 @@ export const ai = {
22
22
  line: 'The app uses AI to do work for people, such as writing a summary or reading a document.',
23
23
  category: 'AI',
24
24
  icon: 'sparkles',
25
- runtimes: ['server'],
25
+ runtimes: ['server', 'worker'],
26
26
  requires: [],
27
27
  availability: 'ready',
28
28
  fields: [
@@ -52,14 +52,25 @@ export const ai = {
52
52
  ],
53
53
  adds: [{ name: 'AI tasks', hint: "Run on your company's own AI account." }],
54
54
  },
55
+ check(values, _modules, ctx) {
56
+ if (ctx.runtime !== 'worker')
57
+ return null;
58
+ if (text(values, 'webhookPath'))
59
+ return problem('webhookPath', 'Worker apps cannot receive an organisation-wide AI callback. Leave this empty.');
60
+ if ((list(values, 'permissions') ?? []).includes('ai.runs:create'))
61
+ return problem('permissions', 'Worker apps use AI tasks that answer at once. Longer work needs an app-specific callback or a scoped result reader.');
62
+ return null;
63
+ },
55
64
  toEntry(entry, values) {
56
65
  if (values === null)
57
66
  return setUse(entry, 'ai', undefined);
58
67
  const next = { offerings: list(values, 'offerings') ?? [] };
59
68
  const permissions = list(values, 'permissions');
60
69
  const webhookPath = text(values, 'webhookPath');
61
- if (permissions)
70
+ if (permissions?.length)
62
71
  next.permissions = permissions;
72
+ else if (entry.workerApp)
73
+ next.permissions = ['ai.offerings:invoke'];
63
74
  if (webhookPath !== undefined)
64
75
  next.webhookPath = webhookPath;
65
76
  return setUse(entry, 'ai', next);
@@ -7,7 +7,7 @@ export const customerGrants = {
7
7
  line: 'Your team gives each customer the permissions they need in the app, and we keep them in step.',
8
8
  category: 'People',
9
9
  icon: 'shield',
10
- runtimes: ['server'],
10
+ runtimes: ['server', 'worker'],
11
11
  requires: ['sign-in'],
12
12
  availability: 'ready',
13
13
  fields: [],
@@ -3,6 +3,7 @@ import type { AppModule } from './types.js';
3
3
  * `uses.mail`: the app sends mail from the company's mail domain. With every setting at its default the entry
4
4
  * holds `uses.mail: true`, as before; otherwise an {@link EntryMail} holding only the settings that differ.
5
5
  * The sender name and reply-to go to the app's env (`entrySettingsEnv`); the bounce settings are for the
6
- * company's mail service.
6
+ * company's mail service. A Worker app may declare it too: foundry delivers nothing to a Worker (it has no
7
+ * Coolify app), and Boule's managed install gives its live copy the company's mail token and sender.
7
8
  */
8
9
  export declare const email: AppModule;
@@ -4,7 +4,8 @@ import { problem, setUse, text } from './entry-parts.js';
4
4
  * `uses.mail`: the app sends mail from the company's mail domain. With every setting at its default the entry
5
5
  * holds `uses.mail: true`, as before; otherwise an {@link EntryMail} holding only the settings that differ.
6
6
  * The sender name and reply-to go to the app's env (`entrySettingsEnv`); the bounce settings are for the
7
- * company's mail service.
7
+ * company's mail service. A Worker app may declare it too: foundry delivers nothing to a Worker (it has no
8
+ * Coolify app), and Boule's managed install gives its live copy the company's mail token and sender.
8
9
  */
9
10
  export const email = {
10
11
  view: {
@@ -13,7 +14,7 @@ export const email = {
13
14
  line: "The app sends email from your company's address, such as receipts and notices.",
14
15
  category: 'Messages',
15
16
  icon: 'mail',
16
- runtimes: ['server'],
17
+ runtimes: ['server', 'worker'],
17
18
  requires: [],
18
19
  availability: 'ready',
19
20
  fields: [
@@ -34,7 +34,8 @@ export const files = {
34
34
  line: 'The app keeps files people upload, such as documents and pictures.',
35
35
  category: 'Data',
36
36
  icon: 'folder',
37
- runtimes: ['server'],
37
+ // A Worker app's keys come from its Boule (`files-apps-minter.ts`), not from foundry.
38
+ runtimes: ['server', 'worker'],
38
39
  requires: [],
39
40
  availability: 'ready',
40
41
  fields: [
@@ -73,6 +74,7 @@ export const files = {
73
74
  label: 'Email the owners when this much of the space is used',
74
75
  help: `In percent, from ${PERCENT_MIN} to ${PERCENT_MAX}.`,
75
76
  kind: 'number',
77
+ runtimes: ['server'],
76
78
  default: FILES_SPACE_ALERT_DEFAULT,
77
79
  min: PERCENT_MIN,
78
80
  max: PERCENT_MAX,
@@ -82,6 +84,7 @@ export const files = {
82
84
  label: 'Share links on your own address',
83
85
  help: 'Links to shared files start with share. and your domain. Needs your own domain.',
84
86
  kind: 'switch',
87
+ runtimes: ['server'],
85
88
  advanced: true,
86
89
  default: false,
87
90
  },
@@ -9,6 +9,7 @@ import { email } from './email.js';
9
9
  import { clone, isProblem } from './entry-parts.js';
10
10
  import { files } from './files.js';
11
11
  import { jobs } from './jobs.js';
12
+ import { mcpResources } from './mcp-resources.js';
12
13
  import { organisations } from './organisations.js';
13
14
  import { SECRET_OUTPUTS, referenceProblem } from './outputs.js';
14
15
  import { reporting } from './reporting.js';
@@ -24,6 +25,7 @@ export { databaseAddressesOf } from './database.js';
24
25
  export const MODULES = [
25
26
  signIn,
26
27
  assistants,
28
+ mcpResources,
27
29
  workspaces,
28
30
  database,
29
31
  signedRecords,
@@ -165,7 +167,8 @@ export function checkModules(modules, ctx) {
165
167
  }
166
168
  for (const field of view.fields) {
167
169
  if (field.runtimes && !field.runtimes.includes(ctx.runtime)) {
168
- if (isEmpty(values[field.key]))
170
+ // A default the module fills in is not a setting the app made: only what was given counts.
171
+ if (isEmpty(modules[id]?.[field.key]))
169
172
  continue;
170
173
  return refuse(field.key, `"${field.label}" can only be set for ${field.runtimes.map((r) => RUNTIME_WORDS[r]).join(' or ')}. This app is ${RUNTIME_WORDS[ctx.runtime]}.`);
171
174
  }
@@ -12,7 +12,7 @@ export const jobs = {
12
12
  line: 'The app runs tasks on a timer or later, such as a nightly report or a reminder.',
13
13
  category: 'In the background',
14
14
  icon: 'clock',
15
- runtimes: ['server'],
15
+ runtimes: ['server', 'worker'],
16
16
  requires: [],
17
17
  availability: 'ready',
18
18
  fields: [
@@ -0,0 +1,2 @@
1
+ import type { AppModule } from './types.js';
2
+ export declare const mcpResources: AppModule;
@@ -0,0 +1,61 @@
1
+ import { McpResourceIntegrationSchema } from '../mcp-resources/schema.js';
2
+ import { problem, setAuthPart } from './entry-parts.js';
3
+ export const mcpResources = {
4
+ view: {
5
+ id: 'mcp-resources',
6
+ name: 'Configurable MCP resources',
7
+ line: 'Your app manages independent assistant connections configured by its customers.',
8
+ category: 'AI',
9
+ icon: 'bot',
10
+ runtimes: ['server', 'worker'],
11
+ requires: ['sign-in'],
12
+ availability: 'ready',
13
+ fields: [
14
+ {
15
+ key: 'version',
16
+ label: 'Integration version',
17
+ kind: 'number',
18
+ default: 1,
19
+ min: 1,
20
+ max: 1,
21
+ hidden: true,
22
+ },
23
+ {
24
+ key: 'pathPrefix',
25
+ label: 'Where configured assistants connect',
26
+ kind: 'path',
27
+ default: '/mcp/resources',
28
+ max: 160,
29
+ pattern: '^/[a-z0-9_-]+(?:/[a-z0-9_-]+)*$',
30
+ help: 'Each configured connection gets an independent path below this address.',
31
+ },
32
+ ],
33
+ adds: [
34
+ {
35
+ name: 'Resource management integration',
36
+ hint: 'Provisioning requires compatible platform and identity services.',
37
+ },
38
+ ],
39
+ },
40
+ check(values, modules) {
41
+ if (modules['sign-in']?.users !== 'customers' || modules['sign-in']?.team === true) {
42
+ return problem(null, 'Configurable MCP resources needs Sign-in for customers only.');
43
+ }
44
+ if (!McpResourceIntegrationSchema.safeParse(values).success) {
45
+ return problem(null, 'Configurable MCP resources needs version 1 and an absolute path prefix of lowercase letters, digits, dashes and underscores.');
46
+ }
47
+ return null;
48
+ },
49
+ toEntry(entry, values) {
50
+ if (values === null)
51
+ return setAuthPart(entry, 'mcpResources', undefined);
52
+ const checked = McpResourceIntegrationSchema.safeParse(values);
53
+ if (!checked.success || !entry.uses?.auth)
54
+ return problem(null, 'Invalid configurable MCP integration.');
55
+ return setAuthPart(entry, 'mcpResources', checked.data);
56
+ },
57
+ fromEntry(entry) {
58
+ const integration = entry.uses?.auth?.mcpResources;
59
+ return integration ? { ...integration } : null;
60
+ },
61
+ };
@@ -5,6 +5,7 @@
5
5
  export const OUTPUTS = {
6
6
  'sign-in': ['issuer', 'client-id'],
7
7
  assistants: ['mcp-url'],
8
+ 'mcp-resources': [],
8
9
  workspaces: [],
9
10
  database: ['address', 'owner-address'],
10
11
  'signed-records': ['registrar-url'],
@@ -2,5 +2,7 @@ import type { AppModule } from './types.js';
2
2
  /**
3
3
  * `uses.repo`: the app gets a key for its own repository. The entry's `repo` names the repository (it comes
4
4
  * from the install, not from this module); `access` says whether the app may only read or also push.
5
+ * Worker apps keep entry.repo null: Boule binds their HTTPS credential to the trusted installed source
6
+ * and access, and handles renewal; foundry never creates a server SSH deploy key for a Worker.
5
7
  */
6
8
  export declare const repository: AppModule;
@@ -2,6 +2,8 @@ import { setUse } from './entry-parts.js';
2
2
  /**
3
3
  * `uses.repo`: the app gets a key for its own repository. The entry's `repo` names the repository (it comes
4
4
  * from the install, not from this module); `access` says whether the app may only read or also push.
5
+ * Worker apps keep entry.repo null: Boule binds their HTTPS credential to the trusted installed source
6
+ * and access, and handles renewal; foundry never creates a server SSH deploy key for a Worker.
5
7
  */
6
8
  export const repository = {
7
9
  view: {
@@ -10,7 +12,7 @@ export const repository = {
10
12
  line: 'The app reads or updates the files in its repository, such as the pages it keeps there.',
11
13
  category: 'Data',
12
14
  icon: 'git-branch',
13
- runtimes: ['server'],
15
+ runtimes: ['server', 'worker'],
14
16
  requires: [],
15
17
  availability: 'ready',
16
18
  fields: [
@@ -8,7 +8,7 @@ export const workspaces = {
8
8
  line: 'Your customers make a shared space, invite the people they work with and give each one a role.',
9
9
  category: 'People',
10
10
  icon: 'users',
11
- runtimes: ['server'],
11
+ runtimes: ['server', 'worker'],
12
12
  requires: ['sign-in'],
13
13
  availability: 'ready',
14
14
  fields: [
package/dist/org.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type McpResourceIntegration } from './mcp-resources/schema.js';
1
2
  import { type WorkerAppConfig } from './worker-app-config.js';
2
3
  export type { WorkerAppConfig } from './worker-app-config.js';
3
4
  export { AI_ENTRY_PERMISSIONS, FILES_ENTRY_PERMISSIONS, MCP_PATH, ONBOARDING_PATH_MAX_LENGTH, ROOM_STEPS_MAX, ROOM_STEPS_MIN, ROOM_TEXT_MAX, WORKSPACE_ROLE, WORKSPACE_ROLES_MAX, } from './entry-rules.js';
@@ -640,6 +641,8 @@ export interface EntryAuth {
640
641
  };
641
642
  managePath?: string;
642
643
  };
644
+ /** Integration bootstrap only. Customer MCP definitions are runtime application data. */
645
+ mcpResources?: McpResourceIntegration;
643
646
  /**
644
647
  * How the customer proves who they are. `password` is the default.
645
648
  * `email-code` has the sign-in page mail a one-time code instead of asking