@opengeni/core 2.5.3 → 2.6.4-canary.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/access/index.d.ts +24 -0
- package/dist/billing/limits.d.ts +5 -0
- package/dist/canonical-human-identities.js +2 -2
- package/dist/{chunk-ZVZJTMSV.js → chunk-OF65T3PM.js} +2 -2
- package/dist/{chunk-YGOMUGYS.js → chunk-QO5GVFFO.js} +17 -8
- package/dist/{chunk-YGOMUGYS.js.map → chunk-QO5GVFFO.js.map} +1 -1
- package/dist/dependencies.d.ts +10 -1
- package/dist/domain/company-brain-governed-writes.d.ts +15 -6
- package/dist/domain/company-profile-agent-admin.d.ts +3 -2
- package/dist/domain/environments.d.ts +1 -1
- package/dist/domain/memory-slack-delivery.d.ts +4 -1
- package/dist/domain/personal-connection-delegations.d.ts +1 -0
- package/dist/domain/pr-review.d.ts +1 -1
- package/dist/domain/scheduled-tasks.d.ts +12 -0
- package/dist/domain/sessions.d.ts +37 -11
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1706 -667
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +18 -0
- package/dist/managed-auth-session-sets.js +3 -1
- package/dist/model-catalog.d.ts +89 -0
- package/dist/sandbox/fleet.d.ts +6 -4
- package/dist/sandbox/routing.d.ts +7 -2
- package/dist/sandbox/runtime-settings.d.ts +17 -1
- package/package.json +10 -10
- package/src/access/index.ts +140 -5
- package/src/application/user-resource-grants.ts +31 -2
- package/src/billing/limits.ts +57 -24
- package/src/dependencies.ts +15 -1
- package/src/domain/company-brain-governed-writes.ts +29 -13
- package/src/domain/company-profile-agent-admin.ts +3 -2
- package/src/domain/environments.ts +6 -34
- package/src/domain/memory-slack-delivery.ts +30 -0
- package/src/domain/personal-connection-delegations.ts +40 -7
- package/src/domain/remember.ts +5 -6
- package/src/domain/scheduled-tasks.ts +146 -1
- package/src/domain/sessions.ts +912 -358
- package/src/domain/workspace-members.ts +34 -2
- package/src/index.ts +1 -0
- package/src/managed-auth-session-sets.ts +38 -11
- package/src/model-catalog.ts +565 -0
- package/src/sandbox/fleet.ts +20 -17
- package/src/sandbox/routing.ts +18 -4
- package/src/sandbox/runtime-settings.ts +32 -0
- /package/dist/{chunk-ZVZJTMSV.js.map → chunk-OF65T3PM.js.map} +0 -0
package/src/dependencies.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { ManagedAuthSessionAdapter } from "./managed-auth-session-sets";
|
|
|
19
19
|
import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "./sandbox-types";
|
|
20
20
|
import type { TranscriptionSegmenter, TranscriptionService } from "./transcription";
|
|
21
21
|
import type { EditableArtifactApplicationPort } from "./editable-artifact-live";
|
|
22
|
+
import type { ResolvedCatalogSettings } from "./model-catalog";
|
|
22
23
|
import type {
|
|
23
24
|
EditableArtifactAgentApplication,
|
|
24
25
|
EditableArtifactDurableExportService,
|
|
@@ -129,6 +130,13 @@ export type ManagedEmailTransport = {
|
|
|
129
130
|
|
|
130
131
|
export type AppDependencies = {
|
|
131
132
|
settings: Settings;
|
|
133
|
+
/**
|
|
134
|
+
* Original deployment settings when `settings` is already overlaid with a
|
|
135
|
+
* deployment/workspace catalog snapshot. Model-bearing request adapters set
|
|
136
|
+
* this marker so core admission never feeds a synthetic reviewed provider
|
|
137
|
+
* back through deployment validation.
|
|
138
|
+
*/
|
|
139
|
+
catalogSourceSettings?: Settings;
|
|
132
140
|
db: Database;
|
|
133
141
|
/**
|
|
134
142
|
* Host-composed editable artifact engine. Standalone startup binds the same
|
|
@@ -225,6 +233,7 @@ export type AppDependencies = {
|
|
|
225
233
|
export type ObjectStorageDependency = ReturnType<typeof createObjectStorage>;
|
|
226
234
|
|
|
227
235
|
export type ApiRouteDeps = AppDependencies & {
|
|
236
|
+
resolveCatalogSettings: () => Promise<ResolvedCatalogSettings>;
|
|
228
237
|
managedEmailTransport: ManagedEmailTransport;
|
|
229
238
|
objectStorage: ObjectStorageDependency;
|
|
230
239
|
githubStateSecret: string;
|
|
@@ -244,7 +253,12 @@ export type ApiRouteDeps = AppDependencies & {
|
|
|
244
253
|
*/
|
|
245
254
|
export type AcceptSessionUserMessageDependencies = Pick<
|
|
246
255
|
AppDependencies,
|
|
247
|
-
|
|
256
|
+
| "settings"
|
|
257
|
+
| "catalogSourceSettings"
|
|
258
|
+
| "db"
|
|
259
|
+
| "bus"
|
|
260
|
+
| "sessionAuthorization"
|
|
261
|
+
| "schedulePromptPostCommit"
|
|
248
262
|
> & {
|
|
249
263
|
workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
|
|
250
264
|
objectStorage: ObjectStorageDependency;
|
|
@@ -45,9 +45,8 @@ export type CompanyBrainGovernedWriteRouterOptions = {
|
|
|
45
45
|
activate?: typeof activateGovernedLearningDecision;
|
|
46
46
|
/**
|
|
47
47
|
* Destinations the router may activate automatically from a final eligible
|
|
48
|
-
* decision. Defaults to
|
|
49
|
-
*
|
|
50
|
-
* decision receipt is recorded but the inactive draft stays for review.
|
|
48
|
+
* decision. Defaults to Skills and instruction policy. Both still pass
|
|
49
|
+
* through the evaluator and destination-owned activation lifecycle.
|
|
51
50
|
*/
|
|
52
51
|
automaticDestinations?: ReadonlyArray<GovernedLearningActivationDestination>;
|
|
53
52
|
/**
|
|
@@ -64,7 +63,27 @@ export type CompanyBrainGovernedWriteRouterOptions = {
|
|
|
64
63
|
};
|
|
65
64
|
|
|
66
65
|
export const DEFAULT_AUTOMATIC_LEARNING_DESTINATIONS: ReadonlyArray<GovernedLearningActivationDestination> =
|
|
67
|
-
["preference"];
|
|
66
|
+
["preference", "instruction_policy"];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Start a non-authoritative post-activation notification without making the
|
|
70
|
+
* durable activation receipt wait for its settlement. The callback is invoked
|
|
71
|
+
* synchronously so immediate enqueue work begins before the router returns;
|
|
72
|
+
* both synchronous throws and later promise rejections are contained.
|
|
73
|
+
*
|
|
74
|
+
* Exact retries intentionally dispatch again: the publication sink owns an
|
|
75
|
+
* activation-receipt idempotency key, while the activation receipt remains the
|
|
76
|
+
* caller-facing source of truth even if notification delivery stalls forever.
|
|
77
|
+
*/
|
|
78
|
+
export function dispatchBestEffortGovernedLearningNotification(
|
|
79
|
+
notify: () => Promise<unknown>,
|
|
80
|
+
): void {
|
|
81
|
+
try {
|
|
82
|
+
void notify().catch(() => undefined);
|
|
83
|
+
} catch {
|
|
84
|
+
// Notification is best-effort; the durable activation receipt already exists.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
|
|
69
88
|
/**
|
|
70
89
|
* Transport-neutral facade for explicit governed Company Brain proposals.
|
|
@@ -143,9 +162,8 @@ function classifyLearningFailure(
|
|
|
143
162
|
* snapshot. `suggest` records the content-free decision receipt only. Under
|
|
144
163
|
* `automatic`, a final `automaticEligible` receipt is handed to the activation
|
|
145
164
|
* controller, which revalidates current authority and applies the change only
|
|
146
|
-
* through the destination-owned lifecycle.
|
|
147
|
-
* activate automatically
|
|
148
|
-
* activation boundary. Evaluation or activation failure
|
|
165
|
+
* through the destination-owned lifecycle. Eligible Skills and instruction
|
|
166
|
+
* policies may activate automatically. Evaluation or activation failure
|
|
149
167
|
* never rolls back the durable proposal; it is reported as a bounded
|
|
150
168
|
* `learningFailure` and the proposal remains for human review.
|
|
151
169
|
*/
|
|
@@ -297,16 +315,14 @@ export function createCompanyBrainLearningPolicyRouter(
|
|
|
297
315
|
learningFailure: classifyLearningFailure("activation", error),
|
|
298
316
|
});
|
|
299
317
|
}
|
|
300
|
-
|
|
301
|
-
|
|
318
|
+
dispatchBestEffortGovernedLearningNotification(() =>
|
|
319
|
+
notifyActivation({
|
|
302
320
|
db: options.db,
|
|
303
321
|
receipt: activation,
|
|
304
322
|
sessionId: attempt.sessionId,
|
|
305
323
|
attemptId: attempt.attemptId,
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
// Notification is best-effort; the durable receipts already exist.
|
|
309
|
-
}
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
310
326
|
return CompanyBrainLearningPolicyRouteReceipt.parse({
|
|
311
327
|
operationId: request.operationId,
|
|
312
328
|
workspaceId: attempt.workspaceId,
|
|
@@ -21,8 +21,9 @@ export type CompanyProfileAgentAdminRouterOptions = {
|
|
|
21
21
|
/**
|
|
22
22
|
* Explicit organization administration is intentionally separate from derived
|
|
23
23
|
* workspace learning. The database capabilities own exact-attempt admission,
|
|
24
|
-
* current organization-owner authority,
|
|
25
|
-
* tenant isolation, CAS, and immutable
|
|
24
|
+
* current organization-owner authority, the separate organization policy,
|
|
25
|
+
* canonical review when required, tenant isolation, CAS, and immutable
|
|
26
|
+
* receipts.
|
|
26
27
|
*/
|
|
27
28
|
export function createCompanyProfileAgentAdminRouter(
|
|
28
29
|
options: CompanyProfileAgentAdminRouterOptions,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
variableSetVariableNameReservation,
|
|
4
|
+
type AccessGrant,
|
|
5
|
+
type VariableSet,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
3
7
|
import { getVariableSet, recordAuditEvent, type Database } from "@opengeni/db";
|
|
4
8
|
import { HTTPException } from "hono/http-exception";
|
|
5
9
|
import { requirePermission } from "../access";
|
|
@@ -11,40 +15,8 @@ export const MAX_VARIABLES_PER_ENVIRONMENT = 100;
|
|
|
11
15
|
// collectGitIdentityEnvironment) plus loader/startup-injection vectors. These
|
|
12
16
|
// can never be set as variable set variables, so the run-scoped
|
|
13
17
|
// git auth block and git identity always win without silent collisions.
|
|
14
|
-
const reservedExactNames = new Set([
|
|
15
|
-
"HOME",
|
|
16
|
-
"PATH",
|
|
17
|
-
"SHELL",
|
|
18
|
-
"USER",
|
|
19
|
-
"LOGNAME",
|
|
20
|
-
"TMPDIR",
|
|
21
|
-
"IFS",
|
|
22
|
-
"ENV",
|
|
23
|
-
"BASH_ENV",
|
|
24
|
-
"NODE_OPTIONS",
|
|
25
|
-
"PYTHONPATH",
|
|
26
|
-
"PYTHONSTARTUP",
|
|
27
|
-
"PERL5OPT",
|
|
28
|
-
"PERL5LIB",
|
|
29
|
-
"GH_TOKEN",
|
|
30
|
-
"GITHUB_TOKEN",
|
|
31
|
-
"GITLAB_TOKEN",
|
|
32
|
-
"AZURE_DEVOPS_EXT_PAT",
|
|
33
|
-
"GIT_ASKPASS",
|
|
34
|
-
"GIT_TERMINAL_PROMPT",
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
const reservedPrefixes = [
|
|
38
|
-
"OPENGENI_",
|
|
39
|
-
"GIT_CONFIG_",
|
|
40
|
-
"GIT_AUTHOR_",
|
|
41
|
-
"GIT_COMMITTER_",
|
|
42
|
-
"LD_",
|
|
43
|
-
"DYLD_",
|
|
44
|
-
];
|
|
45
|
-
|
|
46
18
|
export function assertAllowedVariableSetVariableName(name: string): void {
|
|
47
|
-
if (
|
|
19
|
+
if (variableSetVariableNameReservation(name)) {
|
|
48
20
|
throw new HTTPException(422, {
|
|
49
21
|
message: `reserved variable set variable name / reserved environment variable name: ${name}`,
|
|
50
22
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { MemorySlackPublicationDistribution } from "@opengeni/contracts";
|
|
2
2
|
import {
|
|
3
|
+
correctWorkspaceMemory,
|
|
3
4
|
enqueueMemorySlackPublication,
|
|
4
5
|
getCurrentMemorySlackPublicationConfiguration,
|
|
5
6
|
getWorkspaceMemorySlackPublicationSnapshot,
|
|
6
7
|
saveWorkspaceMemory,
|
|
7
8
|
withWorkspaceRls,
|
|
9
|
+
type CorrectWorkspaceMemoryInput,
|
|
10
|
+
type CorrectWorkspaceMemoryResult,
|
|
8
11
|
type Database,
|
|
9
12
|
type EnqueueMemorySlackPublicationResult,
|
|
10
13
|
type MemoryEmbedder,
|
|
@@ -46,6 +49,33 @@ export async function saveWorkspaceMemoryWithSlackPublication(
|
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
export async function correctWorkspaceMemoryWithSlackPublication(
|
|
53
|
+
db: Database,
|
|
54
|
+
input: CorrectWorkspaceMemoryInput,
|
|
55
|
+
publication: MemorySlackPublicationCommitRequest | null,
|
|
56
|
+
embedder?: MemoryEmbedder,
|
|
57
|
+
): Promise<
|
|
58
|
+
CorrectWorkspaceMemoryResult & { slackPublication: MemorySlackPublicationCommitResult }
|
|
59
|
+
> {
|
|
60
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
61
|
+
const result = await correctWorkspaceMemory(scopedDb, input, embedder);
|
|
62
|
+
if (!publication || result.action !== "superseded" || !result.replacement) {
|
|
63
|
+
return { ...result, slackPublication: { decision: null, enqueue: null } };
|
|
64
|
+
}
|
|
65
|
+
const replacement = await requiredSnapshot(scopedDb, input.workspaceId, result.replacement.id);
|
|
66
|
+
const slackPublication = await evaluateAndEnqueue(scopedDb, {
|
|
67
|
+
accountId: input.accountId,
|
|
68
|
+
workspaceId: input.workspaceId,
|
|
69
|
+
snapshot: replacement,
|
|
70
|
+
changeKind: "corrected",
|
|
71
|
+
relatedMemoryId: result.memory.id,
|
|
72
|
+
occurredAt: result.replacement.updatedAt,
|
|
73
|
+
publication,
|
|
74
|
+
});
|
|
75
|
+
return { ...result, slackPublication };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
async function publishSavedMemoryMutation(
|
|
50
80
|
db: Database,
|
|
51
81
|
input: SaveWorkspaceMemoryInput,
|
|
@@ -384,6 +384,7 @@ export function personalConnectionDelegationsFromVisibleConnections(input: {
|
|
|
384
384
|
export function personalConnectionDelegationsFromParent(input: {
|
|
385
385
|
servers: McpServerConfig[];
|
|
386
386
|
parentDelegations: McpPersonalConnectionDelegation[];
|
|
387
|
+
personalGitHubResources?: ResourceRef[];
|
|
387
388
|
targetSessionId?: string;
|
|
388
389
|
rejectActivatedConnections?: boolean;
|
|
389
390
|
}): McpPersonalConnectionDelegation[] {
|
|
@@ -416,19 +417,55 @@ export function personalConnectionDelegationsFromParent(input: {
|
|
|
416
417
|
childEligible(item) &&
|
|
417
418
|
(item.connectionType === "social" ||
|
|
418
419
|
item.connectionType === "atlassian" ||
|
|
420
|
+
item.connectionType === "github_personal" ||
|
|
419
421
|
item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID),
|
|
420
422
|
)
|
|
421
423
|
.map((item) => ({ ...item })),
|
|
422
424
|
];
|
|
425
|
+
const requestedGitHub = personalGitHubRepositoryResources(input.personalGitHubResources ?? []);
|
|
426
|
+
const inherited = projected.flatMap((delegation) => {
|
|
427
|
+
if (delegation.connectionType !== "github_personal") return [delegation];
|
|
428
|
+
if (requestedGitHub.length === 0) return [];
|
|
429
|
+
const snapshot = delegation.personalGitHubRepositorySelection;
|
|
430
|
+
if (!snapshot) return [];
|
|
431
|
+
const repositories = requestedGitHub.map((resource) => {
|
|
432
|
+
const parent = snapshot.repositories.find(
|
|
433
|
+
(candidate) =>
|
|
434
|
+
candidate.repositoryId === resource.repositoryId &&
|
|
435
|
+
candidate.canonicalUrl === resource.uri &&
|
|
436
|
+
candidate.ref === resource.ref,
|
|
437
|
+
);
|
|
438
|
+
if (
|
|
439
|
+
!parent ||
|
|
440
|
+
resource.credentialBindingId !== snapshot.credentialBindingId ||
|
|
441
|
+
(resource.access === "write" && parent.access !== "write")
|
|
442
|
+
) {
|
|
443
|
+
throw new Error("agent-created personal GitHub repository exceeds parent authority");
|
|
444
|
+
}
|
|
445
|
+
return { ...parent, access: resource.access };
|
|
446
|
+
});
|
|
447
|
+
return [
|
|
448
|
+
{
|
|
449
|
+
...delegation,
|
|
450
|
+
personalGitHubRepositorySelection: { ...snapshot, repositories },
|
|
451
|
+
},
|
|
452
|
+
];
|
|
453
|
+
});
|
|
454
|
+
if (
|
|
455
|
+
requestedGitHub.length > 0 &&
|
|
456
|
+
!inherited.some((delegation) => delegation.connectionType === "github_personal")
|
|
457
|
+
) {
|
|
458
|
+
throw new Error("agent-created personal GitHub repository authority is unavailable");
|
|
459
|
+
}
|
|
423
460
|
if (
|
|
424
461
|
input.rejectActivatedConnections &&
|
|
425
|
-
|
|
462
|
+
inherited.some((delegation) => delegation.userDelegation)
|
|
426
463
|
) {
|
|
427
464
|
throw new Error(
|
|
428
465
|
"scheduled connection authority is not available until task occurrence authority is activated",
|
|
429
466
|
);
|
|
430
467
|
}
|
|
431
|
-
return
|
|
468
|
+
return inherited;
|
|
432
469
|
}
|
|
433
470
|
|
|
434
471
|
/**
|
|
@@ -798,11 +835,6 @@ export async function freezePersonalConnectionDelegations(input: {
|
|
|
798
835
|
"agent-created work inherits connection authority from its exact parent turn",
|
|
799
836
|
);
|
|
800
837
|
}
|
|
801
|
-
if (personalGitHubResources.length > 0) {
|
|
802
|
-
throw new Error(
|
|
803
|
-
"agent-created personal GitHub repository authority is not activated in this delivery phase",
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
838
|
const inherited = personalConnectionDelegationsFromParent({
|
|
807
839
|
servers,
|
|
808
840
|
parentDelegations: await getSessionTurnPersonalConnectionDelegations(
|
|
@@ -811,6 +843,7 @@ export async function freezePersonalConnectionDelegations(input: {
|
|
|
811
843
|
input.source.sessionId,
|
|
812
844
|
input.source.turnId,
|
|
813
845
|
),
|
|
846
|
+
personalGitHubResources,
|
|
814
847
|
...(input.targetSessionId ? { targetSessionId: input.targetSessionId } : {}),
|
|
815
848
|
...(input.rejectUnselectedActivatedConnections !== undefined
|
|
816
849
|
? { rejectActivatedConnections: input.rejectUnselectedActivatedConnections }
|
package/src/domain/remember.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { createHash } from "node:crypto";
|
|
|
35
35
|
import {
|
|
36
36
|
createCompanyBrainLearningPolicyRouter,
|
|
37
37
|
derivedGovernedLearningOperationId,
|
|
38
|
+
dispatchBestEffortGovernedLearningNotification,
|
|
38
39
|
} from "./company-brain-governed-writes";
|
|
39
40
|
import { publishGovernedLearningEventToSlack } from "./governed-learning-slack-publication";
|
|
40
41
|
|
|
@@ -548,16 +549,14 @@ export function createRememberRouter(options: RememberRouterOptions): {
|
|
|
548
549
|
}
|
|
549
550
|
}
|
|
550
551
|
if (!activation) throw asRememberFailure(lastFailure);
|
|
551
|
-
|
|
552
|
-
|
|
552
|
+
dispatchBestEffortGovernedLearningNotification(() =>
|
|
553
|
+
notifyActivation({
|
|
553
554
|
db: options.db,
|
|
554
555
|
receipt: activation,
|
|
555
556
|
sessionId: attempt.sessionId,
|
|
556
557
|
attemptId: attempt.attemptId,
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
// Notification is best-effort; the durable receipts already exist.
|
|
560
|
-
}
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
561
560
|
return RememberConfirmReceipt.parse({
|
|
562
561
|
status: "activated",
|
|
563
562
|
operationId: request.operationId,
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
createScheduledTask,
|
|
23
23
|
deleteScheduledTask,
|
|
24
24
|
getConnectionMetadata,
|
|
25
|
+
getEnrollment,
|
|
26
|
+
getLiveEnrollmentConnection,
|
|
25
27
|
getKnowledgeSourceForSyncAuthority,
|
|
26
28
|
getNestedAgentDepthDeploymentPolicy,
|
|
27
29
|
getRig,
|
|
@@ -29,8 +31,11 @@ import {
|
|
|
29
31
|
getScheduledTaskIncludingDeletedForUpdate,
|
|
30
32
|
getScheduledTaskPersonalConnectionDelegations,
|
|
31
33
|
getScheduledTaskXaiProviderAccountAuthoritySnapshot,
|
|
34
|
+
getSandbox,
|
|
32
35
|
getSessionTurnXaiProviderAccountAuthoritySnapshot,
|
|
33
36
|
getSession,
|
|
37
|
+
lockActiveWorkspaceGatewayCustomModelForAdmission,
|
|
38
|
+
lockActiveWorkspaceOpenRouterCustomModelForAdmission,
|
|
34
39
|
nestedPostgresSqlState,
|
|
35
40
|
requireWorkspace,
|
|
36
41
|
scopedKnowledgeScopeKey,
|
|
@@ -51,6 +56,7 @@ import {
|
|
|
51
56
|
} from "../session-authorization";
|
|
52
57
|
import type { SessionWorkflowClient } from "../dependencies";
|
|
53
58
|
import type { ObjectStorageDependency } from "../dependencies";
|
|
59
|
+
import { workspaceCustomModelReference } from "../model-catalog";
|
|
54
60
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
55
61
|
import { validateVariableSetAttachment } from "./environments";
|
|
56
62
|
import {
|
|
@@ -96,6 +102,35 @@ export function scheduledTaskToolsProvided(rawPayload: unknown): boolean {
|
|
|
96
102
|
);
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
function workspaceCustomModelCommitGuard(input: {
|
|
106
|
+
settings: Settings;
|
|
107
|
+
accountId: string;
|
|
108
|
+
workspaceId: string;
|
|
109
|
+
modelId: string;
|
|
110
|
+
}): ((tx: Database) => Promise<void>) | undefined {
|
|
111
|
+
const reference = workspaceCustomModelReference(input.settings, input.modelId);
|
|
112
|
+
if (!reference) return undefined;
|
|
113
|
+
return async (tx: Database): Promise<void> => {
|
|
114
|
+
const active =
|
|
115
|
+
reference.providerKind === "openrouter"
|
|
116
|
+
? await lockActiveWorkspaceOpenRouterCustomModelForAdmission(tx, {
|
|
117
|
+
accountId: input.accountId,
|
|
118
|
+
workspaceId: input.workspaceId,
|
|
119
|
+
upstreamModelId: reference.upstreamModelId,
|
|
120
|
+
})
|
|
121
|
+
: await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
|
|
122
|
+
accountId: input.accountId,
|
|
123
|
+
workspaceId: input.workspaceId,
|
|
124
|
+
upstreamModelId: reference.upstreamModelId,
|
|
125
|
+
});
|
|
126
|
+
if (!active) {
|
|
127
|
+
throw new HTTPException(422, {
|
|
128
|
+
message: `model is not available: ${input.modelId}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
99
134
|
export function scheduledConnectionSurfaceEligibility(
|
|
100
135
|
settings: Settings,
|
|
101
136
|
target: Pick<Session, "firstPartyMcpTools" | "firstPartyMcpPermissions"> | null,
|
|
@@ -143,7 +178,7 @@ export async function createValidatedScheduledTask(input: {
|
|
|
143
178
|
action: knowledgeAction,
|
|
144
179
|
});
|
|
145
180
|
}
|
|
146
|
-
const agentConfig = knowledgeAction
|
|
181
|
+
const agentConfig: ScheduledTaskAgentConfig = knowledgeAction
|
|
147
182
|
? input.payload.agentConfig
|
|
148
183
|
: await validateScheduledTaskAgentConfig({
|
|
149
184
|
...input,
|
|
@@ -164,6 +199,15 @@ export async function createValidatedScheduledTask(input: {
|
|
|
164
199
|
rigId: input.payload.rigId,
|
|
165
200
|
agentConfig,
|
|
166
201
|
});
|
|
202
|
+
if (!knowledgeAction) {
|
|
203
|
+
await validateScheduledTaskMachineTarget({
|
|
204
|
+
settings: input.settings,
|
|
205
|
+
db: input.db,
|
|
206
|
+
grant: input.grant,
|
|
207
|
+
runMode: input.payload.runMode,
|
|
208
|
+
agentConfig,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
167
211
|
if (!knowledgeAction && input.payload.variableSetId) {
|
|
168
212
|
await validateVariableSetAttachment(
|
|
169
213
|
{ settings: input.settings, db: input.db },
|
|
@@ -223,6 +267,15 @@ export async function createValidatedScheduledTask(input: {
|
|
|
223
267
|
workspaceId: input.grant.workspaceId,
|
|
224
268
|
subjectId: input.grant.subjectId,
|
|
225
269
|
});
|
|
270
|
+
const beforeCreateCommit =
|
|
271
|
+
!knowledgeAction && input.payload.runMode !== "existing_session"
|
|
272
|
+
? workspaceCustomModelCommitGuard({
|
|
273
|
+
settings: input.settings,
|
|
274
|
+
accountId: input.grant.accountId,
|
|
275
|
+
workspaceId: input.grant.workspaceId,
|
|
276
|
+
modelId: agentConfig.model ?? input.settings.openaiModel,
|
|
277
|
+
})
|
|
278
|
+
: undefined;
|
|
226
279
|
return await withScheduledTaskAuthorityWriteErrors(() =>
|
|
227
280
|
createScheduledTask(input.db, {
|
|
228
281
|
id,
|
|
@@ -245,6 +298,7 @@ export async function createValidatedScheduledTask(input: {
|
|
|
245
298
|
variableSetId: input.payload.variableSetId ?? null,
|
|
246
299
|
rigId: input.payload.rigId ?? null,
|
|
247
300
|
metadata: input.payload.metadata,
|
|
301
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
248
302
|
}),
|
|
249
303
|
);
|
|
250
304
|
}
|
|
@@ -399,6 +453,81 @@ export async function validateScheduledTaskTarget(input: {
|
|
|
399
453
|
return session;
|
|
400
454
|
}
|
|
401
455
|
|
|
456
|
+
export async function validateScheduledTaskMachineTarget(input: {
|
|
457
|
+
settings: Settings;
|
|
458
|
+
db: Database;
|
|
459
|
+
grant: AccessGrant;
|
|
460
|
+
runMode: ScheduledTask["runMode"];
|
|
461
|
+
agentConfig: ScheduledTaskAgentConfig;
|
|
462
|
+
requireOnline?: boolean;
|
|
463
|
+
}): Promise<{
|
|
464
|
+
sandboxId: string;
|
|
465
|
+
enrollmentId: string;
|
|
466
|
+
sandboxOs: Session["sandboxOs"];
|
|
467
|
+
} | null> {
|
|
468
|
+
const machineTarget = input.agentConfig.machineTarget;
|
|
469
|
+
if (!machineTarget) {
|
|
470
|
+
if (
|
|
471
|
+
input.runMode !== "existing_session" &&
|
|
472
|
+
(input.agentConfig.sandboxBackend ?? input.settings.sandboxBackend) === "selfhosted"
|
|
473
|
+
) {
|
|
474
|
+
throw new HTTPException(422, {
|
|
475
|
+
message:
|
|
476
|
+
"self-hosted scheduled tasks require a Connected Machine; select a machine before saving",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
if (input.runMode === "existing_session") {
|
|
482
|
+
throw new HTTPException(422, {
|
|
483
|
+
message: "machineTarget cannot be used with an existing-session target",
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
if (!input.settings.sandboxOwnershipEnabled || !input.settings.sandboxSelfhostedEnabled) {
|
|
487
|
+
throw new HTTPException(422, {
|
|
488
|
+
message: "Connected Machines are not enabled for scheduled tasks in this deployment",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const access = {
|
|
492
|
+
accountId: input.grant.accountId,
|
|
493
|
+
workspaceId: input.grant.workspaceId,
|
|
494
|
+
subjectId: input.grant.subjectId,
|
|
495
|
+
};
|
|
496
|
+
const sandbox = await getSandbox(input.db, access, machineTarget.targetSandboxId);
|
|
497
|
+
if (!sandbox || sandbox.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
498
|
+
throw new HTTPException(422, {
|
|
499
|
+
message: "the selected Connected Machine is unavailable",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (sandbox.scope === "user") {
|
|
503
|
+
throw new HTTPException(422, {
|
|
504
|
+
message:
|
|
505
|
+
"personal Connected Machines cannot run unattended schedules; select a workspace or organization machine",
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const enrollment = input.requireOnline
|
|
509
|
+
? await getLiveEnrollmentConnection(input.db, access, sandbox.enrollmentId)
|
|
510
|
+
: await getEnrollment(input.db, access, sandbox.enrollmentId);
|
|
511
|
+
if (!enrollment || enrollment.status !== "active") {
|
|
512
|
+
throw new HTTPException(422, {
|
|
513
|
+
message: input.requireOnline
|
|
514
|
+
? "the selected Connected Machine is offline"
|
|
515
|
+
: "the selected Connected Machine is unavailable",
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (input.requireOnline && !enrollment.workspaceRoot) {
|
|
519
|
+
throw new HTTPException(422, {
|
|
520
|
+
message:
|
|
521
|
+
"the selected Connected Machine has not reported a workspace root; reconnect it with a current agent",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
sandboxId: sandbox.id,
|
|
526
|
+
enrollmentId: sandbox.enrollmentId,
|
|
527
|
+
sandboxOs: enrollment.os,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
402
531
|
export function scheduledTaskForGrant(task: ScheduledTask, grant: AccessGrant): ScheduledTask {
|
|
403
532
|
if (hasPermission(grant.permissions, "sessions:control") || task.targetSessionId === null) {
|
|
404
533
|
return task;
|
|
@@ -647,6 +776,15 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
647
776
|
(input.payload.metadata !== undefined &&
|
|
648
777
|
!isDeepStrictEqual(input.payload.metadata, input.existing.metadata)) ||
|
|
649
778
|
(input.existing.status === "paused" && input.payload.status === "active");
|
|
779
|
+
if (materialExecutionChange && nextRunMode !== "existing_session") {
|
|
780
|
+
const beforeUpdateCommit = workspaceCustomModelCommitGuard({
|
|
781
|
+
settings: input.settings,
|
|
782
|
+
accountId: input.existing.accountId,
|
|
783
|
+
workspaceId: input.existing.workspaceId,
|
|
784
|
+
modelId: nextAgentConfig.model ?? input.settings.openaiModel,
|
|
785
|
+
});
|
|
786
|
+
if (beforeUpdateCommit) update.beforeUpdateCommit = beforeUpdateCommit;
|
|
787
|
+
}
|
|
650
788
|
const existingXaiAuthority = await getScheduledTaskXaiProviderAccountAuthoritySnapshot(
|
|
651
789
|
input.db,
|
|
652
790
|
input.existing.workspaceId,
|
|
@@ -793,6 +931,13 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
793
931
|
rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
|
|
794
932
|
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
795
933
|
});
|
|
934
|
+
await validateScheduledTaskMachineTarget({
|
|
935
|
+
settings: input.settings,
|
|
936
|
+
db: input.db,
|
|
937
|
+
grant: input.grant,
|
|
938
|
+
runMode: nextRunMode,
|
|
939
|
+
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
940
|
+
});
|
|
796
941
|
if (
|
|
797
942
|
input.payload.targetSessionId !== undefined ||
|
|
798
943
|
input.existing.runMode === "existing_session" ||
|