@opengeni/core 0.28.0 → 1.0.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/README.md +1 -1
- package/dist/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +182 -3
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +24 -0
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +31 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +34 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +39 -2
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +26 -13
- package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
- package/dist/application/session-tenancy.d.ts +19 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/billing/limits.d.ts +11 -0
- package/dist/canonical-human-identities.d.ts +7 -2
- package/dist/canonical-human-identities.js +17 -4
- package/dist/canonical-human-identities.js.map +1 -1
- package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
- package/dist/chunk-2MKMRGSY.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
- package/dist/chunk-GVO3N37G.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
- package/dist/chunk-LCQXNR62.js.map +1 -0
- package/dist/chunk-MM6N7CTO.js +387 -0
- package/dist/chunk-MM6N7CTO.js.map +1 -0
- package/dist/chunk-QO5GVFFO.js +235 -0
- package/dist/chunk-QO5GVFFO.js.map +1 -0
- package/dist/codex-model-availability.d.ts +14 -0
- package/dist/default-session-model.d.ts +139 -0
- package/dist/dependencies.d.ts +87 -4
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +58 -0
- package/dist/domain/capabilities.d.ts +22 -4
- package/dist/domain/company-profile-agent-admin.d.ts +25 -0
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +36 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +4 -3
- package/dist/domain/environments.d.ts +5 -9
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/file-owner.d.ts +8 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +28 -1
- package/dist/domain/knowledge-files.d.ts +20 -0
- package/dist/domain/knowledge-messages.d.ts +39 -0
- package/dist/domain/knowledge-messages.test.d.ts +1 -0
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -0
- package/dist/domain/knowledge-search.test.d.ts +1 -0
- package/dist/domain/knowledge.d.ts +9 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/personal-connection-delegations.d.ts +87 -4
- package/dist/domain/pr-review.d.ts +249 -0
- package/dist/domain/resources.d.ts +24 -3
- package/dist/domain/scheduled-task-access.d.ts +247 -0
- package/dist/domain/scheduled-tasks.d.ts +103 -6
- package/dist/domain/session-connection-accounts.d.ts +16 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +93 -4
- package/dist/domain/sessions.d.ts +275 -28
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +117 -0
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/editable-artifact-live/application.d.ts +12 -3
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +5 -2
- package/dist/editable-artifact-live/ticket.d.ts +2 -2
- package/dist/editable-artifact-live/types.d.ts +14 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -4
- package/dist/index.d.ts +49 -3
- package/dist/index.js +18826 -6847
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +131 -0
- package/dist/managed-auth-session-sets.js +51 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/managed-session.d.ts +98 -11
- package/dist/model-catalog.d.ts +117 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +22 -5
- package/dist/rigs/provider-images.d.ts +1 -1
- package/dist/sandbox/fleet.d.ts +59 -10
- package/dist/sandbox/routing.d.ts +22 -2
- package/dist/sandbox/runtime-settings.d.ts +56 -0
- package/dist/session-authorization.d.ts +38 -1
- package/dist/site-session-origin.d.ts +9 -0
- package/dist/transcription.d.ts +12 -1
- package/dist/turn-surface.d.ts +18 -0
- package/dist/user-presence.d.ts +32 -0
- package/package.json +18 -11
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +1386 -44
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +77 -0
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +80 -0
- package/src/application/external-continuation.ts +129 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +90 -0
- package/src/application/external-workspace-members.ts +232 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +222 -29
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +430 -73
- package/src/application/session-mcp-credential-rotation.ts +217 -0
- package/src/application/session-tenancy.ts +445 -0
- package/src/application/user-resource-grants.ts +154 -0
- package/src/billing/limits.ts +103 -24
- package/src/canonical-human-identities.ts +22 -3
- package/src/codex-model-availability.ts +136 -0
- package/src/default-session-model.ts +656 -0
- package/src/dependencies.ts +97 -5
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +270 -0
- package/src/domain/capabilities.ts +244 -151
- package/src/domain/company-profile-agent-admin.ts +56 -0
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +371 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +61 -15
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +32 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +25 -45
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/file-owner.ts +67 -0
- package/src/domain/github-action-policies.ts +169 -0
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/github-skill-source.ts +233 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +270 -169
- package/src/domain/knowledge-files.ts +46 -0
- package/src/domain/knowledge-messages.test.ts +91 -0
- package/src/domain/knowledge-messages.ts +106 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +561 -0
- package/src/domain/knowledge-search.ts +256 -0
- package/src/domain/knowledge.ts +133 -0
- package/src/domain/mcp-account-bindings.ts +171 -0
- package/src/domain/mcp-account-routes.ts +121 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/organization-membership-lifecycle.ts +10 -0
- package/src/domain/personal-connection-delegations.ts +758 -115
- package/src/domain/pr-review.ts +687 -0
- package/src/domain/resources.ts +182 -25
- package/src/domain/scheduled-task-access.ts +1052 -0
- package/src/domain/scheduled-tasks.ts +1952 -215
- package/src/domain/session-connection-accounts.ts +67 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +55 -0
- package/src/domain/session-tool-policy.ts +443 -19
- package/src/domain/sessions.ts +3417 -619
- package/src/domain/skill-imports.ts +137 -17
- package/src/domain/skill-search.ts +221 -0
- package/src/domain/skills.ts +104 -0
- package/src/domain/slack-bot.ts +128 -3
- package/src/domain/workspace-members.ts +34 -2
- package/src/editable-artifact-live/application.ts +29 -4
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +167 -13
- package/src/editable-artifact-live/ticket.ts +87 -6
- package/src/editable-artifact-live/types.ts +16 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +69 -4
- package/src/managed-auth-session-sets.ts +371 -0
- package/src/managed-session.ts +520 -2
- package/src/model-catalog.ts +873 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +179 -63
- package/src/rigs/provider-images.ts +2 -3
- package/src/sandbox/fleet.ts +471 -59
- package/src/sandbox/routing.ts +160 -33
- package/src/sandbox/runtime-settings.ts +281 -0
- package/src/session-authorization.ts +262 -78
- package/src/site-session-origin.ts +22 -0
- package/src/transcription.ts +13 -1
- package/src/turn-surface.ts +45 -0
- package/src/user-presence.ts +134 -0
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-IBOEYG6N.js +0 -34
- package/dist/chunk-IBOEYG6N.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/dist/domain/packs.d.ts +0 -55
- package/src/domain/durable-learning-slack-publication.ts +0 -202
- package/src/domain/packs.ts +0 -556
|
@@ -1,38 +1,105 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
getSessionTurnClaudeProviderAccountAuthoritySnapshot,
|
|
3
|
+
getScheduledTaskClaudeProviderAccountAuthoritySnapshot,
|
|
4
|
+
resolveClaudeProviderAccountAuthoritySnapshotForAcceptance,
|
|
5
|
+
} from "@opengeni/db";
|
|
6
|
+
import {
|
|
7
|
+
SCHEDULED_SLACK_BOT_POSTING_TOOLS,
|
|
8
|
+
scheduledTaskKnowledgeSource,
|
|
9
|
+
requireScheduledTaskKnowledgeSource,
|
|
10
|
+
scheduledOccurrencePayloadUtf8Bytes,
|
|
11
|
+
mergeResourceRefs,
|
|
12
|
+
SCHEDULED_TASK_OCCURRENCE_PAYLOAD_MAX_BYTES,
|
|
13
|
+
SCHEDULED_TASK_OCCURRENCE_PAYLOAD_INGRESS_HEADROOM_BYTES,
|
|
14
|
+
} from "@opengeni/contracts";
|
|
15
|
+
import {
|
|
16
|
+
allowedFirstPartyMcpToolsForSession,
|
|
17
|
+
resolveFirstPartyMcpToolPolicy,
|
|
18
|
+
type Settings,
|
|
19
|
+
} from "@opengeni/config";
|
|
20
|
+
import {
|
|
21
|
+
ATLASSIAN_NATIVE_RETIRED_MESSAGE,
|
|
22
|
+
isRetiredNativeAtlassianSource,
|
|
23
|
+
isRetiredNativeAtlassianTask,
|
|
24
|
+
} from "@opengeni/contracts/atlassian-native-retirement";
|
|
2
25
|
import type {
|
|
3
26
|
AccessGrant,
|
|
4
|
-
McpPersonalConnectionDelegation,
|
|
5
27
|
KnowledgeSourceSyncAction,
|
|
28
|
+
Permission,
|
|
6
29
|
ScheduledTask,
|
|
7
30
|
ScheduledTaskAgentConfig,
|
|
8
31
|
Session,
|
|
32
|
+
ToolRef,
|
|
9
33
|
SessionAuthorizationPort,
|
|
10
34
|
SessionAuthorizationSurface,
|
|
11
35
|
CreateScheduledTaskRequest as CreateScheduledTaskPayload,
|
|
12
36
|
UpdateScheduledTaskRequest as UpdateScheduledTaskPayload,
|
|
13
37
|
XaiProviderAccountAuthoritySnapshotV1,
|
|
38
|
+
ClaudeProviderAccountAuthoritySnapshotV1,
|
|
39
|
+
} from "@opengeni/contracts";
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
42
|
+
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
43
|
+
resolveWorkspaceSessionToolDefaults,
|
|
44
|
+
resolveBundledSkillSelection,
|
|
45
|
+
SessionAgentAccess,
|
|
46
|
+
SessionScopeSubjectId,
|
|
47
|
+
SessionMemoryScope,
|
|
48
|
+
readTurnExecutionPolicyV1,
|
|
14
49
|
} from "@opengeni/contracts";
|
|
15
|
-
import { OPENGENI_SLACK_BOT_SESSION_METADATA_KEY } from "@opengeni/contracts";
|
|
16
50
|
import {
|
|
17
51
|
createScheduledTask,
|
|
52
|
+
scheduledTaskMutationOwnerMatches,
|
|
53
|
+
getAgentLearningSettings,
|
|
54
|
+
saveAgentLearningSettings,
|
|
55
|
+
type KnowledgeContext,
|
|
56
|
+
dbSql,
|
|
18
57
|
deleteScheduledTask,
|
|
19
58
|
getConnectionMetadata,
|
|
59
|
+
getEnrollment,
|
|
60
|
+
getLiveEnrollmentConnection,
|
|
20
61
|
getKnowledgeSourceForSyncAuthority,
|
|
21
62
|
getNestedAgentDepthDeploymentPolicy,
|
|
22
63
|
getRig,
|
|
64
|
+
getScheduledScopedRigVersionMetadata,
|
|
23
65
|
getScheduledTask,
|
|
24
|
-
|
|
66
|
+
getScheduledTaskIncludingDeletedForUpdate,
|
|
67
|
+
getScheduledTaskXaiProviderAccountAuthoritySnapshot,
|
|
68
|
+
getSandbox,
|
|
25
69
|
getSessionTurnXaiProviderAccountAuthoritySnapshot,
|
|
26
70
|
getSession,
|
|
71
|
+
getSessionTurnForAttempt,
|
|
72
|
+
getSessionAuthorityProjection,
|
|
73
|
+
getWorkspaceDefaultRigId,
|
|
74
|
+
withSessionRlsActorContext,
|
|
75
|
+
nestedPostgresSqlState,
|
|
27
76
|
requireWorkspace,
|
|
28
77
|
scopedKnowledgeScopeKey,
|
|
29
78
|
updateScheduledTask,
|
|
79
|
+
ScheduledTaskHeadChangedError,
|
|
80
|
+
withWorkspaceSubjectRls,
|
|
30
81
|
resolveXaiProviderAccountAuthoritySnapshotForAcceptance,
|
|
31
82
|
type Database,
|
|
83
|
+
type ScheduledTaskCreatorPolicy,
|
|
84
|
+
type SessionCommandActor,
|
|
85
|
+
type TemporalScheduleCleanupClaim,
|
|
32
86
|
type UpdateScheduledTaskInput,
|
|
33
87
|
} from "@opengeni/db";
|
|
34
88
|
import { HTTPException } from "hono/http-exception";
|
|
35
|
-
import {
|
|
89
|
+
import { GOOGLE_DRIVE_PUBLICATION_SERVER_ID } from "@opengeni/contracts/google-drive";
|
|
90
|
+
import { PERSONAL_GITHUB_CONNECTION_SURFACE_ID } from "@opengeni/contracts/personal-github";
|
|
91
|
+
import { personalGitHubRepositoryResources } from "./resources";
|
|
92
|
+
import { knowledgeContextForAccess } from "./knowledge";
|
|
93
|
+
import { fileOwnerContextForAccess, fileOwnerContextForAgent } from "./file-owner";
|
|
94
|
+
import { isDeepStrictEqual } from "node:util";
|
|
95
|
+
import {
|
|
96
|
+
hasPermission,
|
|
97
|
+
requireExplicitPermissionDelegation,
|
|
98
|
+
isDeveloperSetupAuthorization,
|
|
99
|
+
isDeveloperSetupGrant,
|
|
100
|
+
requirePermission,
|
|
101
|
+
type AccessGrantAuthorization,
|
|
102
|
+
} from "../access";
|
|
36
103
|
import {
|
|
37
104
|
requireSessionAuthorization,
|
|
38
105
|
SessionAuthorizationDeniedError,
|
|
@@ -40,29 +107,37 @@ import {
|
|
|
40
107
|
} from "../session-authorization";
|
|
41
108
|
import type { SessionWorkflowClient } from "../dependencies";
|
|
42
109
|
import type { ObjectStorageDependency } from "../dependencies";
|
|
110
|
+
import { lockActiveCustomModelForAdmission, workspaceCustomModelReference } from "../model-catalog";
|
|
43
111
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
112
|
+
import {
|
|
113
|
+
resolveSessionToolPolicy,
|
|
114
|
+
workspaceSessionToolPolicyDefaultServerIds,
|
|
115
|
+
} from "./session-tool-policy";
|
|
116
|
+
import { prepareExternalLinkTaskAdmission } from "../application/external-link-work-admission";
|
|
44
117
|
import { validateVariableSetAttachment } from "./environments";
|
|
45
118
|
import {
|
|
46
|
-
|
|
119
|
+
freezeConnectionAccounts,
|
|
47
120
|
personalConnectionDelegationSourceForGrant,
|
|
48
|
-
personalConnectionDelegationsEqual,
|
|
49
121
|
} from "./personal-connection-delegations";
|
|
50
122
|
import {
|
|
51
123
|
assertWorkspaceModelPolicyAllows,
|
|
52
124
|
canonicalConfiguredModel,
|
|
53
125
|
creationInitiatorForGrant,
|
|
126
|
+
modelUnavailableHttpException,
|
|
127
|
+
settingsWithSessionMcpServerMetadata,
|
|
54
128
|
} from "./sessions";
|
|
55
129
|
import {
|
|
56
130
|
hasReservedOpenGeniSlackBotSessionMetadata,
|
|
57
|
-
scheduledSlackBotConnectionId,
|
|
58
131
|
validateOpenGeniSlackBotConnectionSelection,
|
|
132
|
+
validateScheduledTaskSlackChannel,
|
|
133
|
+
type ScheduledTaskSlackChannelVerifier,
|
|
59
134
|
} from "./slack-bot";
|
|
60
135
|
import {
|
|
61
136
|
normalizeResources,
|
|
62
137
|
validateFileResources,
|
|
63
138
|
validateGitHubRepositorySelection,
|
|
64
139
|
validateToolRefs,
|
|
65
|
-
|
|
140
|
+
withWorkspaceDefaultMcpTools,
|
|
66
141
|
} from "./resources";
|
|
67
142
|
|
|
68
143
|
/**
|
|
@@ -85,26 +160,186 @@ export function scheduledTaskToolsProvided(rawPayload: unknown): boolean {
|
|
|
85
160
|
);
|
|
86
161
|
}
|
|
87
162
|
|
|
163
|
+
/** Message data and captured account choices survive; the destination owns execution. */
|
|
164
|
+
function scheduledSessionMessageConfig(config: ScheduledTaskAgentConfig): ScheduledTaskAgentConfig {
|
|
165
|
+
return {
|
|
166
|
+
prompt: config.prompt,
|
|
167
|
+
resources: config.resources,
|
|
168
|
+
tools: [],
|
|
169
|
+
metadata: config.metadata,
|
|
170
|
+
...(config.approvalTimeoutSeconds !== undefined
|
|
171
|
+
? { approvalTimeoutSeconds: config.approvalTimeoutSeconds }
|
|
172
|
+
: {}),
|
|
173
|
+
...(config.connectionAccounts !== undefined
|
|
174
|
+
? { connectionAccounts: config.connectionAccounts }
|
|
175
|
+
: {}),
|
|
176
|
+
...(config.connectionAccountsFrozen ? { connectionAccountsFrozen: true } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function workspaceCustomModelCommitGuard(input: {
|
|
181
|
+
settings: Settings;
|
|
182
|
+
accountId: string;
|
|
183
|
+
workspaceId: string;
|
|
184
|
+
modelId: string;
|
|
185
|
+
claudeAuthoritySnapshot?: ClaudeProviderAccountAuthoritySnapshotV1;
|
|
186
|
+
}): ((tx: Database) => Promise<void>) | undefined {
|
|
187
|
+
const reference = workspaceCustomModelReference(input.settings, input.modelId);
|
|
188
|
+
if (!reference) return undefined;
|
|
189
|
+
return async (tx: Database): Promise<void> => {
|
|
190
|
+
const active = await lockActiveCustomModelForAdmission(tx, {
|
|
191
|
+
accountId: input.accountId,
|
|
192
|
+
workspaceId: input.workspaceId,
|
|
193
|
+
reference,
|
|
194
|
+
...(input.claudeAuthoritySnapshot
|
|
195
|
+
? { claudeAuthority: { authoritySnapshot: input.claudeAuthoritySnapshot } }
|
|
196
|
+
: {}),
|
|
197
|
+
});
|
|
198
|
+
if (!active) {
|
|
199
|
+
throw modelUnavailableHttpException(input.modelId);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Resolve the target's current tool policy before choosing its owner's accounts. */
|
|
205
|
+
export async function scheduledConnectionTools(
|
|
206
|
+
db: Database,
|
|
207
|
+
workspaceId: string,
|
|
208
|
+
settings: Settings,
|
|
209
|
+
target: Session | null,
|
|
210
|
+
taskTools: ToolRef[],
|
|
211
|
+
subjectId?: string,
|
|
212
|
+
): Promise<ToolRef[]> {
|
|
213
|
+
if (!target) return [...taskTools, { kind: "mcp", id: "opengeni" }];
|
|
214
|
+
return resolveSessionToolPolicy({
|
|
215
|
+
toolPolicy: target.toolPolicy,
|
|
216
|
+
sessionTools: target.tools,
|
|
217
|
+
availableMcpServerIds: [
|
|
218
|
+
"opengeni",
|
|
219
|
+
...settings.mcpServers.map(({ id }) => id),
|
|
220
|
+
...target.mcpServers.map(({ id }) => id),
|
|
221
|
+
],
|
|
222
|
+
defaultMcpServerIds: await workspaceSessionToolPolicyDefaultServerIds(
|
|
223
|
+
db,
|
|
224
|
+
workspaceId,
|
|
225
|
+
settings,
|
|
226
|
+
subjectId,
|
|
227
|
+
),
|
|
228
|
+
}).toolRefs;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function scheduledConnectionSurfaceEligibility(
|
|
232
|
+
settings: Settings,
|
|
233
|
+
target: Pick<Session, "firstPartyMcpTools" | "firstPartyMcpPermissions"> | null,
|
|
234
|
+
): { googleDrivePublicationEnabled: boolean; atlassianEnabled: boolean } {
|
|
235
|
+
const tools = target?.firstPartyMcpTools ?? resolveFirstPartyMcpToolPolicy(settings).default;
|
|
236
|
+
const permissions = target?.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS;
|
|
237
|
+
return {
|
|
238
|
+
googleDrivePublicationEnabled:
|
|
239
|
+
tools.includes("editable_artifact_export") &&
|
|
240
|
+
tools.includes("editable_artifact_export_status") &&
|
|
241
|
+
permissions.includes("artifacts:read") &&
|
|
242
|
+
permissions.includes("artifacts:publish"),
|
|
243
|
+
atlassianEnabled: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* An organization/workspace API key or the deployment's configured key is a
|
|
249
|
+
* machine principal. It is never a person, so it can never be a schedule's
|
|
250
|
+
* execution owner (a human revision authorizer): its schedules are ownerless
|
|
251
|
+
* and run under service authority, exactly like a delegated service's.
|
|
252
|
+
*/
|
|
253
|
+
function isScheduledTaskMachinePrincipal(grant: AccessGrant): boolean {
|
|
254
|
+
return grant.principalKind === "api_key" || grant.principalKind === "configured_key";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Service attribution cannot become a personal schedule execution owner. */
|
|
258
|
+
function scheduledTaskInitiatorForGrant(grant: AccessGrant) {
|
|
259
|
+
const creator = creationInitiatorForGrant(grant);
|
|
260
|
+
if (
|
|
261
|
+
creator.initiator?.kind === "subject" &&
|
|
262
|
+
(personalConnectionDelegationSourceForGrant(grant).kind === "none" ||
|
|
263
|
+
isScheduledTaskMachinePrincipal(grant))
|
|
264
|
+
) {
|
|
265
|
+
return { ...creator, initiator: { ...creator.initiator, kind: "service" as const } };
|
|
266
|
+
}
|
|
267
|
+
return creator;
|
|
268
|
+
}
|
|
269
|
+
|
|
88
270
|
export async function createValidatedScheduledTask(input: {
|
|
89
271
|
settings: Settings;
|
|
90
272
|
db: Database;
|
|
91
273
|
objectStorage: ObjectStorageDependency;
|
|
92
274
|
grant: AccessGrant;
|
|
275
|
+
authorization?: AccessGrantAuthorization;
|
|
93
276
|
payload: CreateScheduledTaskPayload;
|
|
94
277
|
// Whether the caller explicitly set agentConfig.tools (see
|
|
95
278
|
// scheduledTaskToolsProvided). Absent tools get the workspace's enabled
|
|
96
279
|
// capability MCP servers, mirroring session creation.
|
|
97
280
|
toolsProvided?: boolean;
|
|
98
|
-
// Set for pack-installation-inherited attachments that were already
|
|
99
|
-
// authorized with variable-sets:use when the pack was enabled.
|
|
100
|
-
variableSetPreauthorized?: boolean;
|
|
101
281
|
sessionAuthorization?: SessionAuthorizationPort | null | undefined;
|
|
102
282
|
authorizationSurface?: SessionAuthorizationSurface | undefined;
|
|
283
|
+
/** Proves the bot may post in a newly chosen task Slack channel. */
|
|
284
|
+
verifySlackChannel?: ScheduledTaskSlackChannelVerifier | undefined;
|
|
103
285
|
}): Promise<ScheduledTask> {
|
|
104
|
-
|
|
105
|
-
|
|
286
|
+
if (input.payload.runMode === "existing_session") {
|
|
287
|
+
const target = await validateScheduledTaskTarget({
|
|
288
|
+
...input,
|
|
289
|
+
targetSessionId: input.payload.targetSessionId,
|
|
290
|
+
runMode: "existing_session",
|
|
291
|
+
variableSetId: input.payload.variableSetId,
|
|
292
|
+
rigId: input.payload.rigId,
|
|
293
|
+
agentConfig: input.payload.agentConfig,
|
|
294
|
+
});
|
|
295
|
+
if (input.payload.agentConfig.knowledgeSource)
|
|
296
|
+
throw new HTTPException(422, {
|
|
297
|
+
message: "Source ingestion requires a separate scheduled agent",
|
|
298
|
+
});
|
|
299
|
+
if (input.payload.variableSetId && input.payload.variableSetId !== target?.variableSetId)
|
|
300
|
+
throw new HTTPException(422, {
|
|
301
|
+
message:
|
|
302
|
+
"Existing-chat schedules use the chat's Variable Sets. Change the chat's attachments separately.",
|
|
303
|
+
});
|
|
304
|
+
if (input.payload.rigId && input.payload.rigId !== target?.rigId)
|
|
305
|
+
throw new HTTPException(422, {
|
|
306
|
+
message:
|
|
307
|
+
"Existing-chat schedules use the chat's environment. Change the chat's attachments separately.",
|
|
308
|
+
});
|
|
309
|
+
await requireScheduledMessageVariableSetUse({
|
|
310
|
+
db: input.db,
|
|
311
|
+
grant: input.grant,
|
|
312
|
+
target,
|
|
313
|
+
});
|
|
314
|
+
input = {
|
|
315
|
+
...input,
|
|
316
|
+
payload: {
|
|
317
|
+
...input.payload,
|
|
318
|
+
agentConfig: scheduledSessionMessageConfig(input.payload.agentConfig),
|
|
319
|
+
variableSetId: null,
|
|
320
|
+
rigId: null,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const learning = "agentLearning" in input.payload ? input.payload.agentLearning : undefined;
|
|
325
|
+
const learningContext =
|
|
326
|
+
learning && input.authorization
|
|
327
|
+
? await knowledgeContextForAccess(
|
|
328
|
+
{ db: input.db },
|
|
329
|
+
input.authorization,
|
|
330
|
+
"scheduled_tasks:manage",
|
|
331
|
+
)
|
|
332
|
+
: null;
|
|
333
|
+
if (learning && learningContext?.actor.kind !== "human") {
|
|
334
|
+
throw new HTTPException(403, {
|
|
335
|
+
message: "Only an authenticated person can configure Agent learning",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (learningContext?.actor.kind === "human" && learning)
|
|
339
|
+
learningContext.actor.settingsScopes = [learning.scope];
|
|
340
|
+
// Internal callers can omit the action; agent turns are the default.
|
|
106
341
|
const action = input.payload.action ?? ({ kind: "agent_turn" } as const);
|
|
107
|
-
const knowledgeAction =
|
|
342
|
+
const knowledgeAction = input.payload.agentConfig.knowledgeSource ?? null;
|
|
108
343
|
if (knowledgeAction) {
|
|
109
344
|
await validateKnowledgeSourceSyncAction({
|
|
110
345
|
db: input.db,
|
|
@@ -112,58 +347,180 @@ export async function createValidatedScheduledTask(input: {
|
|
|
112
347
|
action: knowledgeAction,
|
|
113
348
|
});
|
|
114
349
|
}
|
|
115
|
-
const agentConfig =
|
|
116
|
-
|
|
117
|
-
:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
350
|
+
const agentConfig: ScheduledTaskAgentConfig = await validateScheduledTaskAgentConfig({
|
|
351
|
+
...input,
|
|
352
|
+
workspaceId: input.grant.workspaceId,
|
|
353
|
+
});
|
|
354
|
+
agentConfig.connectionAccounts = input.payload.connectionAccounts ?? [];
|
|
355
|
+
await validateScheduledTaskSlackChannel({
|
|
356
|
+
grant: input.grant,
|
|
357
|
+
authorization: input.authorization,
|
|
358
|
+
previous: null,
|
|
359
|
+
next: agentConfig,
|
|
360
|
+
runMode: input.payload.runMode,
|
|
361
|
+
reusableSessionCanPost: null,
|
|
362
|
+
verifySlackChannel: input.verifySlackChannel,
|
|
363
|
+
});
|
|
364
|
+
if (knowledgeAction && input.payload.overlapPolicy === "allow_concurrent")
|
|
365
|
+
throw new HTTPException(422, { message: "Source tasks require skip or buffer_one overlap" });
|
|
121
366
|
const id = crypto.randomUUID();
|
|
122
367
|
validateScheduledTaskSchedule(input.payload.schedule);
|
|
123
|
-
const target =
|
|
124
|
-
|
|
125
|
-
:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
368
|
+
const target = await validateScheduledTaskTarget({
|
|
369
|
+
db: input.db,
|
|
370
|
+
sessionAuthorization: input.sessionAuthorization,
|
|
371
|
+
authorizationSurface: input.authorizationSurface,
|
|
372
|
+
grant: input.grant,
|
|
373
|
+
targetSessionId: input.payload.targetSessionId,
|
|
374
|
+
runMode: input.payload.runMode,
|
|
375
|
+
variableSetId: input.payload.variableSetId,
|
|
376
|
+
rigId: input.payload.rigId,
|
|
377
|
+
// An omitted Sandbox Environment adopts the target session's own one below.
|
|
378
|
+
agentConfig,
|
|
379
|
+
});
|
|
380
|
+
if (
|
|
381
|
+
knowledgeAction?.destination.kind === "personal" &&
|
|
382
|
+
input.payload.runMode !== "new_session_per_run"
|
|
383
|
+
)
|
|
384
|
+
throw new HTTPException(422, {
|
|
385
|
+
message: "Personal source tasks create a private session for each run",
|
|
386
|
+
});
|
|
387
|
+
if (learning) {
|
|
388
|
+
const workspace = await requireWorkspace(input.db, input.grant.workspaceId);
|
|
389
|
+
const destination =
|
|
390
|
+
knowledgeAction?.destination.kind === "personal" ||
|
|
391
|
+
workspace.kind === "personal" ||
|
|
392
|
+
target?.tenancy?.visibility === "private" ||
|
|
393
|
+
target?.memoryScope === "user"
|
|
394
|
+
? "personal"
|
|
395
|
+
: "workspace";
|
|
396
|
+
if (learning.scope !== destination)
|
|
397
|
+
throw new HTTPException(422, {
|
|
398
|
+
message: `This task saves Knowledge in the ${destination} scope. Set its Agent learning override for that scope.`,
|
|
135
399
|
});
|
|
136
|
-
|
|
400
|
+
}
|
|
401
|
+
if (!knowledgeAction) {
|
|
402
|
+
agentConfig.connectionAccountsFrozen = true;
|
|
403
|
+
await validateScheduledTaskMachineTarget({
|
|
404
|
+
settings: input.settings,
|
|
405
|
+
db: input.db,
|
|
406
|
+
grant: input.grant,
|
|
407
|
+
runMode: input.payload.runMode,
|
|
408
|
+
agentConfig,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (input.payload.variableSetId) {
|
|
137
412
|
await validateVariableSetAttachment(
|
|
138
413
|
{ settings: input.settings, db: input.db },
|
|
139
414
|
input.grant,
|
|
140
415
|
input.grant.workspaceId,
|
|
141
416
|
input.payload.variableSetId,
|
|
142
|
-
{ preauthorized: input.variableSetPreauthorized ?? false },
|
|
143
417
|
);
|
|
144
418
|
}
|
|
145
419
|
// The rig is stored on the task and resolved to its ACTIVE version per fire
|
|
146
420
|
// (at dispatch), so validate only that the id names a rig in the workspace —
|
|
147
421
|
// NOT that it has an active version now (that is a fire-time concern). RLS
|
|
148
422
|
// makes a cross-workspace id indistinguishable from missing → both 422.
|
|
423
|
+
// A generated session binds the environment and its default Variable Sets,
|
|
424
|
+
// so an explicit choice needs the same attachment authority as session
|
|
425
|
+
// create; an existing-session task only matches its target's environment.
|
|
149
426
|
if (!knowledgeAction && input.payload.rigId) {
|
|
150
|
-
await requireScheduledTaskRig(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
427
|
+
const rig = await requireScheduledTaskRig(
|
|
428
|
+
input.db,
|
|
429
|
+
{
|
|
430
|
+
accountId: input.grant.accountId,
|
|
431
|
+
workspaceId: input.grant.workspaceId,
|
|
432
|
+
subjectId: input.grant.subjectId,
|
|
433
|
+
},
|
|
434
|
+
input.payload.rigId,
|
|
435
|
+
);
|
|
436
|
+
if (input.payload.runMode !== "existing_session") {
|
|
437
|
+
await requireScheduledTaskRigVariableSetAttachments({
|
|
438
|
+
settings: input.settings,
|
|
155
439
|
db: input.db,
|
|
440
|
+
grant: input.grant,
|
|
156
441
|
workspaceId: input.grant.workspaceId,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
442
|
+
rig,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const rigId = knowledgeAction
|
|
447
|
+
? (input.payload.rigId ?? null)
|
|
448
|
+
: await resolveScheduledTaskCreateRigId({
|
|
449
|
+
settings: input.settings,
|
|
450
|
+
db: input.db,
|
|
451
|
+
grant: input.grant,
|
|
452
|
+
requestedRigId: input.payload.rigId,
|
|
453
|
+
runMode: input.payload.runMode,
|
|
454
|
+
target,
|
|
455
|
+
agentConfig,
|
|
165
456
|
});
|
|
166
|
-
const
|
|
457
|
+
const runtimeSettings = knowledgeAction
|
|
458
|
+
? null
|
|
459
|
+
: await settingsWithEnabledCapabilityMcpServers(
|
|
460
|
+
input.db,
|
|
461
|
+
input.grant.workspaceId,
|
|
462
|
+
input.settings,
|
|
463
|
+
{ subjectId: input.grant.subjectId },
|
|
464
|
+
);
|
|
465
|
+
// Existing-session runs use the target's persisted MCP configuration, with
|
|
466
|
+
// session definitions taking precedence over deployment servers of the same
|
|
467
|
+
// ID. Metadata selects the destination; captured grants still supply authority.
|
|
468
|
+
const effectiveRuntimeSettings =
|
|
469
|
+
runtimeSettings && target
|
|
470
|
+
? settingsWithSessionMcpServerMetadata(runtimeSettings, target.mcpServers)
|
|
471
|
+
: runtimeSettings;
|
|
472
|
+
const acceptedConnections =
|
|
473
|
+
knowledgeAction || !effectiveRuntimeSettings
|
|
474
|
+
? { personalConnectionDelegations: [], mcpAccountBindings: [] }
|
|
475
|
+
: await freezeConnectionAccounts({
|
|
476
|
+
db: input.db,
|
|
477
|
+
accountId: input.grant.accountId,
|
|
478
|
+
workspaceId: input.grant.workspaceId,
|
|
479
|
+
settings: effectiveRuntimeSettings,
|
|
480
|
+
tools: await scheduledConnectionTools(
|
|
481
|
+
input.db,
|
|
482
|
+
input.grant.workspaceId,
|
|
483
|
+
effectiveRuntimeSettings,
|
|
484
|
+
target,
|
|
485
|
+
agentConfig.tools,
|
|
486
|
+
input.grant.subjectId,
|
|
487
|
+
),
|
|
488
|
+
resources: mergeResourceRefs(target?.resources ?? [], agentConfig.resources),
|
|
489
|
+
source: personalConnectionDelegationSourceForGrant(input.grant),
|
|
490
|
+
authoritySelections: input.payload.connectionAccounts,
|
|
491
|
+
...scheduledConnectionSurfaceEligibility(effectiveRuntimeSettings, target),
|
|
492
|
+
});
|
|
493
|
+
const { personalConnectionDelegations, mcpAccountBindings } = acceptedConnections;
|
|
494
|
+
if (!knowledgeAction) {
|
|
495
|
+
const boundRoutes = new Set((mcpAccountBindings ?? []).map((binding) => binding.serverId));
|
|
496
|
+
agentConfig.connectionAccounts = [
|
|
497
|
+
...(mcpAccountBindings ?? []).map(({ canonicalServerId, connectionId }) => ({
|
|
498
|
+
serverId: canonicalServerId,
|
|
499
|
+
connectionId,
|
|
500
|
+
})),
|
|
501
|
+
...personalConnectionDelegations
|
|
502
|
+
.filter(
|
|
503
|
+
(item) =>
|
|
504
|
+
!boundRoutes.has(item.serverId) &&
|
|
505
|
+
(!item.connectionType ||
|
|
506
|
+
item.connectionType === "mcp" ||
|
|
507
|
+
item.connectionType === "github_personal"),
|
|
508
|
+
)
|
|
509
|
+
.map(({ serverId, connectionId }) => ({ serverId, connectionId })),
|
|
510
|
+
];
|
|
511
|
+
}
|
|
512
|
+
const creationInitiator = scheduledTaskInitiatorForGrant(input.grant);
|
|
513
|
+
const captureLinkAuthority = prepareExternalLinkTaskAdmission(
|
|
514
|
+
input.authorization,
|
|
515
|
+
creationInitiator.actor,
|
|
516
|
+
);
|
|
517
|
+
const creatorPolicy = await frozenScheduledTaskCreatorPolicy({
|
|
518
|
+
db: input.db,
|
|
519
|
+
settings: input.settings,
|
|
520
|
+
grant: input.grant,
|
|
521
|
+
authorization: input.authorization,
|
|
522
|
+
actor: creationInitiator.actor ?? null,
|
|
523
|
+
});
|
|
167
524
|
const xaiProviderAccountAuthoritySnapshot: XaiProviderAccountAuthoritySnapshotV1 =
|
|
168
525
|
creationInitiator.actor
|
|
169
526
|
? await getSessionTurnXaiProviderAccountAuthoritySnapshot(
|
|
@@ -176,28 +533,385 @@ export async function createValidatedScheduledTask(input: {
|
|
|
176
533
|
workspaceId: input.grant.workspaceId,
|
|
177
534
|
subjectId: input.grant.subjectId,
|
|
178
535
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
536
|
+
const claudeProviderAccountAuthoritySnapshot: ClaudeProviderAccountAuthoritySnapshotV1 =
|
|
537
|
+
creationInitiator.actor
|
|
538
|
+
? await getSessionTurnClaudeProviderAccountAuthoritySnapshot(
|
|
539
|
+
input.db,
|
|
540
|
+
input.grant.workspaceId,
|
|
541
|
+
creationInitiator.actor.sessionId,
|
|
542
|
+
creationInitiator.actor.turnId,
|
|
543
|
+
)
|
|
544
|
+
: await resolveClaudeProviderAccountAuthoritySnapshotForAcceptance(input.db, {
|
|
545
|
+
workspaceId: input.grant.workspaceId,
|
|
546
|
+
subjectId: input.grant.subjectId,
|
|
547
|
+
});
|
|
548
|
+
const beforeCreateCommit =
|
|
549
|
+
input.payload.runMode !== "existing_session"
|
|
550
|
+
? workspaceCustomModelCommitGuard({
|
|
551
|
+
settings: input.settings,
|
|
552
|
+
accountId: input.grant.accountId,
|
|
553
|
+
workspaceId: input.grant.workspaceId,
|
|
554
|
+
modelId: agentConfig.model ?? input.settings.openaiModel,
|
|
555
|
+
claudeAuthoritySnapshot: claudeProviderAccountAuthoritySnapshot,
|
|
556
|
+
})
|
|
557
|
+
: undefined;
|
|
558
|
+
return await withScheduledTaskAuthorityWriteErrors(() =>
|
|
559
|
+
input.db.transaction(async (transaction) => {
|
|
560
|
+
const task = await createScheduledTask(transaction, {
|
|
561
|
+
id,
|
|
562
|
+
accountId: input.grant.accountId,
|
|
563
|
+
workspaceId: input.grant.workspaceId,
|
|
564
|
+
name: trimmedScheduledTaskName(input.payload.name),
|
|
565
|
+
status: input.payload.status,
|
|
566
|
+
schedule: input.payload.schedule,
|
|
567
|
+
temporalScheduleId: scheduledTaskTemporalScheduleId(id),
|
|
568
|
+
runMode: input.payload.runMode,
|
|
569
|
+
overlapPolicy: input.payload.overlapPolicy,
|
|
570
|
+
action,
|
|
571
|
+
agentConfig,
|
|
572
|
+
...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
|
|
573
|
+
...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
|
|
574
|
+
createdByActor: creationInitiator.actor ?? null,
|
|
575
|
+
...(captureLinkAuthority ? { captureLinkAuthority } : {}),
|
|
576
|
+
xaiProviderAccountAuthoritySnapshot,
|
|
577
|
+
claudeProviderAccountAuthoritySnapshot,
|
|
578
|
+
creatorPolicy,
|
|
579
|
+
targetSessionId: target?.id ?? null,
|
|
580
|
+
variableSetId: input.payload.variableSetId ?? null,
|
|
581
|
+
rigId,
|
|
582
|
+
metadata: input.payload.metadata,
|
|
583
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
584
|
+
});
|
|
585
|
+
if (learning && learningContext)
|
|
586
|
+
await saveAgentLearningSettings(transaction, learningContext, {
|
|
587
|
+
scope: learning.scope,
|
|
588
|
+
source: { kind: "scheduled_task", id: task.id },
|
|
589
|
+
operationId: crypto.randomUUID(),
|
|
590
|
+
expectedVersion: 0,
|
|
591
|
+
settings: learning.settings,
|
|
592
|
+
});
|
|
593
|
+
return task;
|
|
594
|
+
}),
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Freeze authenticated credential restrictions for every creation lane, and
|
|
600
|
+
* the creating session's boundary onto an agent-created task so the
|
|
601
|
+
* sessions generated for it inherit exactly what the creator could see and
|
|
602
|
+
* do, never the deployment default. Tools are the session's effective
|
|
603
|
+
* model-visible selection under the deployment ceiling; permissions are the
|
|
604
|
+
* session's effective first-party set intersected with what the calling
|
|
605
|
+
* grant actually holds (a narrowly delegated spawn token cannot hand a
|
|
606
|
+
* schedule more than itself). The session access policy is copied from the
|
|
607
|
+
* projection when it exposes those facts; each absent fact is stored as null
|
|
608
|
+
* so a generated session keeps its own default for that key.
|
|
609
|
+
*/
|
|
610
|
+
export async function frozenScheduledTaskCreatorPolicy(input: {
|
|
611
|
+
db: Database;
|
|
612
|
+
settings: Settings;
|
|
613
|
+
grant: AccessGrant;
|
|
614
|
+
authorization?: AccessGrantAuthorization | undefined;
|
|
615
|
+
actor: Extract<SessionCommandActor, { type: "agent_attempt" }> | null;
|
|
616
|
+
}): Promise<ScheduledTaskCreatorPolicy | null> {
|
|
617
|
+
let restricted =
|
|
618
|
+
(input.authorization?.grant === input.grant &&
|
|
619
|
+
isDeveloperSetupAuthorization(input.authorization)) ||
|
|
620
|
+
isDeveloperSetupGrant(input.grant);
|
|
621
|
+
if (!input.actor) {
|
|
622
|
+
if (input.grant.permissionMode === "explicit") {
|
|
623
|
+
const permissions = DEFAULT_FIRST_PARTY_MCP_PERMISSIONS.filter((permission) =>
|
|
624
|
+
hasPermission(input.grant.permissions, permission, "explicit"),
|
|
625
|
+
);
|
|
626
|
+
if (permissions.length === 0) {
|
|
627
|
+
throw new HTTPException(403, {
|
|
628
|
+
message:
|
|
629
|
+
"the organization key holds no first-party MCP permission it could delegate to scheduled runs",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
firstPartyMcpTools: null,
|
|
634
|
+
firstPartyMcpPermissions: permissions,
|
|
635
|
+
sessionPolicy: null,
|
|
636
|
+
...(restricted ? { credentialRestriction: "developer_setup" as const } : {}),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
// A restriction is not an agent tool/permission selection. Keep the exact
|
|
640
|
+
// first-party and session defaults of ordinary API/service/asUser tasks.
|
|
641
|
+
return restricted
|
|
642
|
+
? {
|
|
643
|
+
firstPartyMcpTools: null,
|
|
644
|
+
firstPartyMcpPermissions: null,
|
|
645
|
+
sessionPolicy: null,
|
|
646
|
+
credentialRestriction: "developer_setup",
|
|
647
|
+
}
|
|
648
|
+
: null;
|
|
649
|
+
}
|
|
650
|
+
const session = await getSession(input.db, input.grant.workspaceId, input.actor.sessionId);
|
|
651
|
+
if (!session) {
|
|
652
|
+
throw new HTTPException(403, {
|
|
653
|
+
message: "the calling agent session is not available in this workspace",
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const turn = await getSessionTurnForAttempt(
|
|
657
|
+
input.db,
|
|
658
|
+
input.grant.workspaceId,
|
|
659
|
+
input.actor.sessionId,
|
|
660
|
+
input.actor.attemptId,
|
|
661
|
+
);
|
|
662
|
+
if (!turn || turn.id !== input.actor.turnId) {
|
|
663
|
+
throw new HTTPException(403, { message: "the calling agent attempt is not available" });
|
|
664
|
+
}
|
|
665
|
+
// These are server-frozen policies, not task/agentConfig metadata. The DB
|
|
666
|
+
// insert also verifies this exact actor under its ownership locks.
|
|
667
|
+
const turnPolicy = readTurnExecutionPolicyV1(turn.metadata);
|
|
668
|
+
const sessionPolicy = readTurnExecutionPolicyV1(session.metadata);
|
|
669
|
+
restricted ||= Boolean(
|
|
670
|
+
(turnPolicy.kind === "valid" &&
|
|
671
|
+
turnPolicy.policy.credentialRestriction === "developer_setup") ||
|
|
672
|
+
(sessionPolicy.kind === "valid" &&
|
|
673
|
+
sessionPolicy.policy.credentialRestriction === "developer_setup"),
|
|
674
|
+
);
|
|
675
|
+
const firstPartyMcpTools = allowedFirstPartyMcpToolsForSession(
|
|
676
|
+
input.settings,
|
|
677
|
+
session.firstPartyMcpTools,
|
|
678
|
+
);
|
|
679
|
+
const firstPartyMcpPermissions = (
|
|
680
|
+
session.firstPartyMcpPermissions ?? [...DEFAULT_FIRST_PARTY_MCP_PERMISSIONS]
|
|
681
|
+
).filter((permission) =>
|
|
682
|
+
hasPermission(input.grant.permissions, permission, input.grant.permissionMode),
|
|
683
|
+
);
|
|
684
|
+
requireExplicitPermissionDelegation(input.grant, firstPartyMcpPermissions);
|
|
685
|
+
if (firstPartyMcpPermissions.length === 0) {
|
|
686
|
+
throw new HTTPException(403, {
|
|
687
|
+
message:
|
|
688
|
+
"the calling agent session holds no first-party MCP permission it could delegate to scheduled runs",
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
// The projection facts are validated through the access-scope contract so
|
|
692
|
+
// only a well-formed value is frozen; anything else stores null for that key.
|
|
693
|
+
const projection = session as unknown as Record<string, unknown>;
|
|
694
|
+
const agentAccess = SessionAgentAccess.safeParse(projection["agentAccess"]);
|
|
695
|
+
const scopeSubjectId = SessionScopeSubjectId.safeParse(projection["scopeSubjectId"]);
|
|
696
|
+
const memoryScope = SessionMemoryScope.safeParse(projection["memoryScope"]);
|
|
697
|
+
return {
|
|
698
|
+
firstPartyMcpTools,
|
|
699
|
+
firstPartyMcpPermissions,
|
|
700
|
+
...(restricted ? { credentialRestriction: "developer_setup" as const } : {}),
|
|
701
|
+
sessionPolicy: {
|
|
702
|
+
agentAccess: agentAccess.success ? agentAccess.data : null,
|
|
703
|
+
scopeSubjectId: scopeSubjectId.success ? scopeSubjectId.data : null,
|
|
704
|
+
memoryScope: memoryScope.success ? memoryScope.data : null,
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function nestedPostgresMessage(error: unknown): string | null {
|
|
710
|
+
const queue: unknown[] = [error];
|
|
711
|
+
const seen = new Set<unknown>();
|
|
712
|
+
while (queue.length > 0 && seen.size < 64) {
|
|
713
|
+
const current = queue.shift();
|
|
714
|
+
if (!current || typeof current !== "object" || seen.has(current)) continue;
|
|
715
|
+
seen.add(current);
|
|
716
|
+
const record = current as Record<string, unknown>;
|
|
717
|
+
if (typeof record.code === "string" && typeof record.message === "string") {
|
|
718
|
+
return record.message;
|
|
719
|
+
}
|
|
720
|
+
for (const key of ["cause", "errors", "error", "originalError"]) {
|
|
721
|
+
const nested = record[key];
|
|
722
|
+
if (Array.isArray(nested)) queue.push(...nested);
|
|
723
|
+
else if (nested !== undefined) queue.push(nested);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Scheduled-task authority writes fail closed inside SECURITY DEFINER seams
|
|
731
|
+
* (42501): a non-human writer delegating resources, an authorizer without
|
|
732
|
+
* active workspace membership, or a foreign human retaining another subject's
|
|
733
|
+
* grants. Those are caller-resolvable conflicts, not server faults.
|
|
734
|
+
*/
|
|
735
|
+
export async function withScheduledTaskAuthorityWriteErrors<T>(run: () => Promise<T>): Promise<T> {
|
|
736
|
+
try {
|
|
737
|
+
return await run();
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (error instanceof ScheduledTaskHeadChangedError)
|
|
740
|
+
throw new HTTPException(409, {
|
|
741
|
+
message: "Scheduled task changed. Reload it before saving.",
|
|
742
|
+
});
|
|
743
|
+
if (nestedPostgresSqlState(error) === "40001")
|
|
744
|
+
throw new HTTPException(409, {
|
|
745
|
+
message: "Scheduled task learning settings changed. Reload the task before saving.",
|
|
746
|
+
});
|
|
747
|
+
if (nestedPostgresSqlState(error) === "42501") {
|
|
748
|
+
const detail = nestedPostgresMessage(error);
|
|
749
|
+
throw new HTTPException(409, {
|
|
750
|
+
message: detail
|
|
751
|
+
? `scheduled task authority denied: ${detail}`
|
|
752
|
+
: "scheduled task authority denied",
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** API/MCP-facing update that maps authority-write denials to 409. */
|
|
760
|
+
export async function updateScheduledTaskForApi(
|
|
761
|
+
db: Database,
|
|
762
|
+
grant: AccessGrant,
|
|
763
|
+
taskId: string,
|
|
764
|
+
update: UpdateScheduledTaskInput,
|
|
765
|
+
learning?: {
|
|
766
|
+
authorization: AccessGrantAuthorization;
|
|
767
|
+
request: NonNullable<UpdateScheduledTaskPayload["agentLearning"]>;
|
|
768
|
+
restoreState: ScheduledTaskRestoreState;
|
|
769
|
+
},
|
|
770
|
+
): Promise<ScheduledTask> {
|
|
771
|
+
const workspaceId = grant.workspaceId;
|
|
772
|
+
const beforeUpdateCommit = update.beforeUpdateCommit;
|
|
773
|
+
update = {
|
|
774
|
+
...update,
|
|
775
|
+
beforeUpdateCommit: async (tx) => {
|
|
776
|
+
await assertScheduledTaskMutationOwner(tx, grant, taskId);
|
|
777
|
+
await beforeUpdateCommit?.(tx);
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
if (learning) {
|
|
781
|
+
const context = await knowledgeContextForAccess(
|
|
782
|
+
{ db },
|
|
783
|
+
learning.authorization,
|
|
784
|
+
"scheduled_tasks:manage",
|
|
785
|
+
);
|
|
786
|
+
if (context.actor.kind !== "human")
|
|
787
|
+
throw new HTTPException(403, {
|
|
788
|
+
message: "Only an authenticated person can configure Agent learning",
|
|
789
|
+
});
|
|
790
|
+
context.actor.settingsScopes = [learning.request.scope];
|
|
791
|
+
return withScheduledTaskAuthorityWriteErrors(() =>
|
|
792
|
+
db.transaction(async (tx) => {
|
|
793
|
+
const source = { kind: "scheduled_task" as const, id: taskId };
|
|
794
|
+
const baselineScope = learning.request.baselineScope ?? learning.request.scope;
|
|
795
|
+
// Lock policy owners in one order before comparing the draft baseline.
|
|
796
|
+
// A destination change must not race an edit in its previous owner layer.
|
|
797
|
+
const subjectId = context.actor.kind === "human" ? context.actor.subjectId : "";
|
|
798
|
+
const owners = [
|
|
799
|
+
...new Set(
|
|
800
|
+
[baselineScope, learning.request.scope].map((scope) =>
|
|
801
|
+
scope === "personal" ? `personal:${subjectId}` : `workspace:${workspaceId}`,
|
|
802
|
+
),
|
|
803
|
+
),
|
|
804
|
+
].sort();
|
|
805
|
+
for (const owner of owners)
|
|
806
|
+
await tx.execute(
|
|
807
|
+
dbSql`SELECT pg_advisory_xact_lock(hashtextextended(${`agent-learning:${context.accountId}:${owner}`},0))`,
|
|
808
|
+
);
|
|
809
|
+
const baseline = await getAgentLearningSettings(tx, context, baselineScope, source);
|
|
810
|
+
if (baseline.version !== learning.request.expectedVersion)
|
|
811
|
+
throw new HTTPException(409, {
|
|
812
|
+
message: "Scheduled task learning settings changed. Reload the task before saving.",
|
|
813
|
+
});
|
|
814
|
+
const task = await updateScheduledTask(tx, workspaceId, taskId, update);
|
|
815
|
+
const prior = await getAgentLearningSettings(tx, context, learning.request.scope, source);
|
|
816
|
+
const saved = await saveAgentLearningSettings(tx, context, {
|
|
817
|
+
scope: learning.request.scope,
|
|
818
|
+
operationId: learning.request.operationId,
|
|
819
|
+
expectedVersion:
|
|
820
|
+
baselineScope === learning.request.scope
|
|
821
|
+
? learning.request.expectedVersion
|
|
822
|
+
: prior.version,
|
|
823
|
+
source,
|
|
824
|
+
settings: {
|
|
825
|
+
knowledge: "inherit",
|
|
826
|
+
instructions: "inherit",
|
|
827
|
+
skills: "inherit",
|
|
828
|
+
...learning.request.settings,
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
learning.restoreState.learning = {
|
|
832
|
+
context,
|
|
833
|
+
scope: learning.request.scope,
|
|
834
|
+
settings: prior.settings,
|
|
835
|
+
expectedVersion: saved.version,
|
|
836
|
+
};
|
|
837
|
+
return task;
|
|
838
|
+
}),
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
return await withScheduledTaskAuthorityWriteErrors(() =>
|
|
842
|
+
updateScheduledTask(db, workspaceId, taskId, update),
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export async function assertScheduledTaskMutationOwner(
|
|
847
|
+
db: Database,
|
|
848
|
+
grant: AccessGrant,
|
|
849
|
+
taskId: string,
|
|
850
|
+
): Promise<void> {
|
|
851
|
+
const writer = scheduledTaskInitiatorForGrant(grant);
|
|
852
|
+
const matches = await scheduledTaskMutationOwnerMatches(db, {
|
|
853
|
+
workspaceId: grant.workspaceId,
|
|
854
|
+
taskId,
|
|
855
|
+
...(writer.initiator ? { createdBy: writer.initiator } : {}),
|
|
856
|
+
...(writer.context ? { createdByContext: writer.context } : {}),
|
|
857
|
+
createdByActor: writer.actor ?? null,
|
|
200
858
|
});
|
|
859
|
+
if (!matches)
|
|
860
|
+
throw new HTTPException(403, {
|
|
861
|
+
message: "Only the schedule owner can change or run it. Copy it to create your own schedule.",
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export async function triggerScheduledTaskForGrant(
|
|
866
|
+
db: Database,
|
|
867
|
+
grant: AccessGrant,
|
|
868
|
+
workflowClient: SessionWorkflowClient,
|
|
869
|
+
input: Parameters<SessionWorkflowClient["triggerScheduledTask"]>[0],
|
|
870
|
+
): Promise<void> {
|
|
871
|
+
await db.transaction(async (tx) => {
|
|
872
|
+
await assertScheduledTaskMutationOwner(tx, grant, input.task.id);
|
|
873
|
+
if (isRetiredNativeAtlassianTask(input.task))
|
|
874
|
+
throw new HTTPException(410, { message: ATLASSIAN_NATIVE_RETIRED_MESSAGE });
|
|
875
|
+
const actor = creationInitiatorForGrant(grant).actor ?? null;
|
|
876
|
+
const restriction = await scheduledTaskCredentialRestrictionForGrant(tx, grant, actor);
|
|
877
|
+
// A caller cannot supply or clear the trusted ceiling. Ownerless and
|
|
878
|
+
// same-human schedules can be triggered by a different credential than
|
|
879
|
+
// their creator; restrict that accepted run, not the durable task.
|
|
880
|
+
const { credentialRestriction: _untrustedRestriction, ...trigger } = input;
|
|
881
|
+
await workflowClient.triggerScheduledTask({
|
|
882
|
+
...trigger,
|
|
883
|
+
...(restriction ? { credentialRestriction: restriction } : {}),
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function scheduledTaskCredentialRestrictionForGrant(
|
|
889
|
+
db: Database,
|
|
890
|
+
grant: AccessGrant,
|
|
891
|
+
actor: Extract<SessionCommandActor, { type: "agent_attempt" }> | null,
|
|
892
|
+
): Promise<"developer_setup" | undefined> {
|
|
893
|
+
if (isDeveloperSetupGrant(grant)) return "developer_setup";
|
|
894
|
+
if (!actor) return undefined;
|
|
895
|
+
const session = await getSession(db, grant.workspaceId, actor.sessionId);
|
|
896
|
+
const turn = await getSessionTurnForAttempt(
|
|
897
|
+
db,
|
|
898
|
+
grant.workspaceId,
|
|
899
|
+
actor.sessionId,
|
|
900
|
+
actor.attemptId,
|
|
901
|
+
);
|
|
902
|
+
if (!session || !turn || turn.id !== actor.turnId) {
|
|
903
|
+
throw new HTTPException(403, { message: "the calling agent attempt is not available" });
|
|
904
|
+
}
|
|
905
|
+
const policies = [
|
|
906
|
+
readTurnExecutionPolicyV1(turn.metadata),
|
|
907
|
+
readTurnExecutionPolicyV1(session.metadata),
|
|
908
|
+
];
|
|
909
|
+
return policies.some(
|
|
910
|
+
(policy) =>
|
|
911
|
+
policy.kind === "valid" && policy.policy.credentialRestriction === "developer_setup",
|
|
912
|
+
)
|
|
913
|
+
? "developer_setup"
|
|
914
|
+
: undefined;
|
|
201
915
|
}
|
|
202
916
|
|
|
203
917
|
export async function validateScheduledTaskTarget(input: {
|
|
@@ -211,6 +925,8 @@ export async function validateScheduledTaskTarget(input: {
|
|
|
211
925
|
rigId: string | null | undefined;
|
|
212
926
|
agentConfig: ScheduledTaskAgentConfig;
|
|
213
927
|
missingTargetStatus?: 404 | 422;
|
|
928
|
+
/** Leaving a cancelled chat still requires control, but not revivability. */
|
|
929
|
+
departingTarget?: boolean;
|
|
214
930
|
}): Promise<Session | null> {
|
|
215
931
|
if (input.runMode !== "existing_session") {
|
|
216
932
|
if (input.targetSessionId) {
|
|
@@ -262,38 +978,89 @@ export async function validateScheduledTaskTarget(input: {
|
|
|
262
978
|
if (!session || session.accountId !== input.grant.accountId) {
|
|
263
979
|
throw new HTTPException(404, { message: "target session not found" });
|
|
264
980
|
}
|
|
265
|
-
if (session.status === "cancelled") {
|
|
981
|
+
if (session.status === "cancelled" && !input.departingTarget) {
|
|
266
982
|
throw new HTTPException(409, {
|
|
267
983
|
message: "target session is cancelled; choose a revivable session",
|
|
268
984
|
});
|
|
269
985
|
}
|
|
270
|
-
|
|
986
|
+
// The target owns execution settings. Admission freezes and revalidates its
|
|
987
|
+
// current policy under the scheduled owner; duplicated task fields confer no access.
|
|
988
|
+
return session;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export async function validateScheduledTaskMachineTarget(input: {
|
|
992
|
+
settings: Settings;
|
|
993
|
+
db: Database;
|
|
994
|
+
grant: AccessGrant;
|
|
995
|
+
runMode: ScheduledTask["runMode"];
|
|
996
|
+
agentConfig: ScheduledTaskAgentConfig;
|
|
997
|
+
requireOnline?: boolean;
|
|
998
|
+
}): Promise<{
|
|
999
|
+
sandboxId: string;
|
|
1000
|
+
enrollmentId: string;
|
|
1001
|
+
sandboxOs: Session["sandboxOs"];
|
|
1002
|
+
} | null> {
|
|
1003
|
+
const machineTarget = input.agentConfig.machineTarget;
|
|
1004
|
+
if (!machineTarget) {
|
|
1005
|
+
if (
|
|
1006
|
+
input.runMode !== "existing_session" &&
|
|
1007
|
+
(input.agentConfig.sandboxBackend ?? input.settings.sandboxBackend) === "selfhosted"
|
|
1008
|
+
) {
|
|
1009
|
+
throw new HTTPException(422, {
|
|
1010
|
+
message:
|
|
1011
|
+
"self-hosted scheduled tasks require a Connected Machine; select a machine before saving",
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
if (input.runMode === "existing_session") {
|
|
271
1017
|
throw new HTTPException(422, {
|
|
272
|
-
message: "
|
|
1018
|
+
message: "machineTarget cannot be used with an existing-session target",
|
|
273
1019
|
});
|
|
274
1020
|
}
|
|
275
|
-
if (input.
|
|
1021
|
+
if (!input.settings.sandboxOwnershipEnabled || !input.settings.sandboxSelfhostedEnabled) {
|
|
276
1022
|
throw new HTTPException(422, {
|
|
277
|
-
message: "
|
|
1023
|
+
message: "Connected Machines are not enabled for scheduled tasks in this deployment",
|
|
278
1024
|
});
|
|
279
1025
|
}
|
|
280
|
-
|
|
281
|
-
input.
|
|
282
|
-
input.
|
|
283
|
-
|
|
1026
|
+
const access = {
|
|
1027
|
+
accountId: input.grant.accountId,
|
|
1028
|
+
workspaceId: input.grant.workspaceId,
|
|
1029
|
+
subjectId: input.grant.subjectId,
|
|
1030
|
+
};
|
|
1031
|
+
const sandbox = await getSandbox(input.db, access, machineTarget.targetSandboxId);
|
|
1032
|
+
if (!sandbox || sandbox.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
284
1033
|
throw new HTTPException(422, {
|
|
285
|
-
message: "
|
|
1034
|
+
message: "the selected Connected Machine is unavailable",
|
|
286
1035
|
});
|
|
287
1036
|
}
|
|
288
|
-
if (
|
|
289
|
-
scheduledSlackBotConnectionId(session.metadata) !==
|
|
290
|
-
(input.agentConfig.slackBotConnectionId ?? null)
|
|
291
|
-
) {
|
|
1037
|
+
if (sandbox.scope === "user") {
|
|
292
1038
|
throw new HTTPException(422, {
|
|
293
|
-
message:
|
|
1039
|
+
message:
|
|
1040
|
+
"personal Connected Machines cannot run unattended schedules; select a workspace or organization machine",
|
|
294
1041
|
});
|
|
295
1042
|
}
|
|
296
|
-
|
|
1043
|
+
const enrollment = input.requireOnline
|
|
1044
|
+
? await getLiveEnrollmentConnection(input.db, access, sandbox.enrollmentId)
|
|
1045
|
+
: await getEnrollment(input.db, access, sandbox.enrollmentId);
|
|
1046
|
+
if (!enrollment || enrollment.status !== "active") {
|
|
1047
|
+
throw new HTTPException(422, {
|
|
1048
|
+
message: input.requireOnline
|
|
1049
|
+
? "the selected Connected Machine is offline"
|
|
1050
|
+
: "the selected Connected Machine is unavailable",
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
if (input.requireOnline && !enrollment.workspaceRoot) {
|
|
1054
|
+
throw new HTTPException(422, {
|
|
1055
|
+
message:
|
|
1056
|
+
"the selected Connected Machine has not reported a workspace root; reconnect it with a current agent",
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
sandboxId: sandbox.id,
|
|
1061
|
+
enrollmentId: sandbox.enrollmentId,
|
|
1062
|
+
sandboxOs: enrollment.os,
|
|
1063
|
+
};
|
|
297
1064
|
}
|
|
298
1065
|
|
|
299
1066
|
export function scheduledTaskForGrant(task: ScheduledTask, grant: AccessGrant): ScheduledTask {
|
|
@@ -313,17 +1080,152 @@ export function scheduledTaskRunForGrant<T extends { sessionId: string | null }>
|
|
|
313
1080
|
return { ...run, sessionId: null };
|
|
314
1081
|
}
|
|
315
1082
|
|
|
1083
|
+
export function scheduledTaskAuthorityUpdateForGrant(
|
|
1084
|
+
grant: AccessGrant,
|
|
1085
|
+
): Pick<
|
|
1086
|
+
UpdateScheduledTaskInput,
|
|
1087
|
+
| "refreshPersonalResourceAuthority"
|
|
1088
|
+
| "authorityUpdatedBy"
|
|
1089
|
+
| "authorityUpdatedByContext"
|
|
1090
|
+
| "authorityUpdatedByActor"
|
|
1091
|
+
> {
|
|
1092
|
+
const writer = scheduledTaskInitiatorForGrant(grant);
|
|
1093
|
+
return {
|
|
1094
|
+
refreshPersonalResourceAuthority: true,
|
|
1095
|
+
...(writer.initiator ? { authorityUpdatedBy: writer.initiator } : {}),
|
|
1096
|
+
...(writer.context ? { authorityUpdatedByContext: writer.context } : {}),
|
|
1097
|
+
authorityUpdatedByActor: writer.actor ?? null,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* The Sandbox Environment a new task stores. An explicit id or null is the
|
|
1103
|
+
* caller's choice. An omitted value is resolved once, here, and frozen on the
|
|
1104
|
+
* task, the way session create resolves an omitted rigId:
|
|
1105
|
+
* - an existing-session task keeps its target session's own environment;
|
|
1106
|
+
* - a Connected Machine task stores none: environment setup and Variable Set
|
|
1107
|
+
* injection never reach a machine, so a binding would only add a fire-time
|
|
1108
|
+
* failure mode;
|
|
1109
|
+
* - otherwise the workspace default, when the creator can see it and it has an
|
|
1110
|
+
* active version. A stale default degrades to none, like session create.
|
|
1111
|
+
* The default's own Variable Sets must be attachable by the creator, exactly
|
|
1112
|
+
* as session create requires, so a workspace default never gives a task
|
|
1113
|
+
* secrets its creator could not attach. Later changes to the workspace default
|
|
1114
|
+
* do not move an existing task.
|
|
1115
|
+
*/
|
|
1116
|
+
async function resolveScheduledTaskCreateRigId(input: {
|
|
1117
|
+
settings: Settings;
|
|
1118
|
+
db: Database;
|
|
1119
|
+
grant: AccessGrant;
|
|
1120
|
+
requestedRigId: string | null | undefined;
|
|
1121
|
+
runMode: ScheduledTask["runMode"];
|
|
1122
|
+
target: Session | null;
|
|
1123
|
+
agentConfig: ScheduledTaskAgentConfig;
|
|
1124
|
+
}): Promise<string | null> {
|
|
1125
|
+
if (input.requestedRigId !== undefined) return input.requestedRigId;
|
|
1126
|
+
if (input.runMode === "existing_session") return input.target?.rigId ?? null;
|
|
1127
|
+
if (input.agentConfig.machineTarget) return null;
|
|
1128
|
+
const defaultRigId = await getWorkspaceDefaultRigId(input.db, input.grant.workspaceId);
|
|
1129
|
+
if (!defaultRigId) return null;
|
|
1130
|
+
const rig = await getRig(
|
|
1131
|
+
input.db,
|
|
1132
|
+
{
|
|
1133
|
+
accountId: input.grant.accountId,
|
|
1134
|
+
workspaceId: input.grant.workspaceId,
|
|
1135
|
+
subjectId: input.grant.subjectId,
|
|
1136
|
+
},
|
|
1137
|
+
defaultRigId,
|
|
1138
|
+
);
|
|
1139
|
+
if (!rig?.activeVersion) return null;
|
|
1140
|
+
await requireScheduledTaskRigVariableSetAttachments({
|
|
1141
|
+
settings: input.settings,
|
|
1142
|
+
db: input.db,
|
|
1143
|
+
grant: input.grant,
|
|
1144
|
+
workspaceId: input.grant.workspaceId,
|
|
1145
|
+
rig,
|
|
1146
|
+
});
|
|
1147
|
+
return rig.id;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
type ScheduledTaskRig = NonNullable<Awaited<ReturnType<typeof getRig>>>;
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Binding a Sandbox Environment to generated sessions layers its active
|
|
1154
|
+
* version's default Variable Sets into every turn, so the task writer must be
|
|
1155
|
+
* allowed to attach each of them, exactly as session create requires. An
|
|
1156
|
+
* environment without an active version has nothing to check yet.
|
|
1157
|
+
*/
|
|
1158
|
+
async function requireScheduledTaskRigVariableSetAttachments(input: {
|
|
1159
|
+
settings: Settings;
|
|
1160
|
+
db: Database;
|
|
1161
|
+
grant: AccessGrant;
|
|
1162
|
+
workspaceId: string;
|
|
1163
|
+
rig: ScheduledTaskRig;
|
|
1164
|
+
}): Promise<void> {
|
|
1165
|
+
for (const variableSetId of new Set(input.rig.activeVersion?.defaultVariableSetIds ?? [])) {
|
|
1166
|
+
await validateVariableSetAttachment(
|
|
1167
|
+
{ settings: input.settings, db: input.db },
|
|
1168
|
+
input.grant,
|
|
1169
|
+
input.workspaceId,
|
|
1170
|
+
variableSetId,
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/** Message authority covers both direct sets and the environment's defaults. */
|
|
1176
|
+
async function requireScheduledMessageVariableSetUse(input: {
|
|
1177
|
+
db: Database;
|
|
1178
|
+
grant: AccessGrant;
|
|
1179
|
+
target?: Session | null;
|
|
1180
|
+
variableSetId?: string | null;
|
|
1181
|
+
rigId?: string | null;
|
|
1182
|
+
}): Promise<void> {
|
|
1183
|
+
if (hasPermission(input.grant.permissions, "variable-sets:use", input.grant.permissionMode))
|
|
1184
|
+
return;
|
|
1185
|
+
const directSets = input.target
|
|
1186
|
+
? Boolean(input.target.variableSetId || input.target.variableSetIds?.length)
|
|
1187
|
+
: Boolean(input.variableSetId);
|
|
1188
|
+
if (directSets) requirePermission(input.grant, "variable-sets:use");
|
|
1189
|
+
const rigId = input.target ? input.target.rigId : input.rigId;
|
|
1190
|
+
if (!rigId) return;
|
|
1191
|
+
const access = {
|
|
1192
|
+
accountId: input.grant.accountId,
|
|
1193
|
+
workspaceId: input.grant.workspaceId,
|
|
1194
|
+
subjectId: input.grant.subjectId,
|
|
1195
|
+
};
|
|
1196
|
+
let defaultVariableSetIds: string[];
|
|
1197
|
+
if (input.target) {
|
|
1198
|
+
// A chat rides its exact retained version. The rig's active version only
|
|
1199
|
+
// determines defaults for newly generated chats.
|
|
1200
|
+
if (!input.target.rigVersionId) return;
|
|
1201
|
+
const pinned = await getScheduledScopedRigVersionMetadata(
|
|
1202
|
+
input.db,
|
|
1203
|
+
access,
|
|
1204
|
+
rigId,
|
|
1205
|
+
input.target.rigVersionId,
|
|
1206
|
+
);
|
|
1207
|
+
if (!pinned)
|
|
1208
|
+
throw new HTTPException(409, { message: "Scheduled task chat environment is unavailable" });
|
|
1209
|
+
defaultVariableSetIds = pinned.version.defaultVariableSetIds;
|
|
1210
|
+
} else {
|
|
1211
|
+
const rig = await requireScheduledTaskRig(input.db, access, rigId);
|
|
1212
|
+
defaultVariableSetIds = rig.activeVersion?.defaultVariableSetIds ?? [];
|
|
1213
|
+
}
|
|
1214
|
+
if (defaultVariableSetIds.length) requirePermission(input.grant, "variable-sets:use");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
316
1217
|
// Validate a scheduled task's rig reference: it must name a rig in the
|
|
317
1218
|
// workspace. A missing/cross-workspace id is a 422 (RLS-invisible == missing).
|
|
318
1219
|
async function requireScheduledTaskRig(
|
|
319
1220
|
db: Database,
|
|
320
|
-
workspaceId: string,
|
|
1221
|
+
access: { accountId: string; workspaceId: string; subjectId: string },
|
|
321
1222
|
rigId: string,
|
|
322
|
-
): Promise<
|
|
323
|
-
const rig = await getRig(db,
|
|
1223
|
+
): Promise<ScheduledTaskRig> {
|
|
1224
|
+
const rig = await getRig(db, access, rigId);
|
|
324
1225
|
if (!rig) {
|
|
325
1226
|
throw new HTTPException(422, { message: `unknown rigId: ${rigId}` });
|
|
326
1227
|
}
|
|
1228
|
+
return rig;
|
|
327
1229
|
}
|
|
328
1230
|
|
|
329
1231
|
export async function validatedScheduledTaskUpdate(input: {
|
|
@@ -331,76 +1233,238 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
331
1233
|
db: Database;
|
|
332
1234
|
objectStorage: ObjectStorageDependency;
|
|
333
1235
|
grant: AccessGrant;
|
|
1236
|
+
authorization?: AccessGrantAuthorization;
|
|
334
1237
|
existing: ScheduledTask;
|
|
335
1238
|
payload: UpdateScheduledTaskPayload;
|
|
336
1239
|
/** See createValidatedScheduledTask; only consulted when agentConfig is updated. */
|
|
337
1240
|
toolsProvided?: boolean;
|
|
338
1241
|
sessionAuthorization?: SessionAuthorizationPort | null | undefined;
|
|
339
1242
|
authorizationSurface?: SessionAuthorizationSurface | undefined;
|
|
1243
|
+
/** Proves the bot may post in a newly chosen task Slack channel. */
|
|
1244
|
+
verifySlackChannel?: ScheduledTaskSlackChannelVerifier | undefined;
|
|
340
1245
|
}): Promise<UpdateScheduledTaskInput> {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
1246
|
+
const requestedPrompt =
|
|
1247
|
+
input.payload.prompt ??
|
|
1248
|
+
input.payload.agentConfigPatch?.prompt ??
|
|
1249
|
+
input.payload.agentConfig?.prompt;
|
|
1250
|
+
if (requestedPrompt !== undefined && !requestedPrompt.trim())
|
|
1251
|
+
throw new HTTPException(422, { message: "scheduled task prompt is required" });
|
|
1252
|
+
const retainedPrompt =
|
|
1253
|
+
input.payload.agentConfig === undefined
|
|
1254
|
+
? (requestedPrompt ?? input.existing.agentConfig.prompt)
|
|
1255
|
+
: undefined;
|
|
1256
|
+
if (
|
|
1257
|
+
input.payload.expectedExecutionDigest !== undefined &&
|
|
1258
|
+
input.payload.expectedExecutionDigest !== input.existing.executionDigest
|
|
1259
|
+
)
|
|
344
1260
|
throw new HTTPException(409, {
|
|
345
|
-
message: "
|
|
1261
|
+
message: "Scheduled task changed. Reload it before saving.",
|
|
1262
|
+
});
|
|
1263
|
+
if (input.payload.prompt !== undefined)
|
|
1264
|
+
input = {
|
|
1265
|
+
...input,
|
|
1266
|
+
payload: {
|
|
1267
|
+
...input.payload,
|
|
1268
|
+
agentConfigPatch: {
|
|
1269
|
+
...input.payload.agentConfigPatch,
|
|
1270
|
+
prompt: input.payload.prompt,
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
};
|
|
1274
|
+
if (input.payload.targetSessionId && input.payload.runMode === undefined)
|
|
1275
|
+
input = {
|
|
1276
|
+
...input,
|
|
1277
|
+
payload: { ...input.payload, runMode: "existing_session" },
|
|
1278
|
+
};
|
|
1279
|
+
const requestedMode = input.payload.runMode ?? input.existing.runMode;
|
|
1280
|
+
if (requestedMode === "existing_session") {
|
|
1281
|
+
const config = input.payload.agentConfig ?? input.existing.agentConfig;
|
|
1282
|
+
if (config.knowledgeSource)
|
|
1283
|
+
throw new HTTPException(422, {
|
|
1284
|
+
message: "Source ingestion requires a separate scheduled agent",
|
|
1285
|
+
});
|
|
1286
|
+
const target = await validateScheduledTaskTarget({
|
|
1287
|
+
...input,
|
|
1288
|
+
runMode: "existing_session",
|
|
1289
|
+
targetSessionId: input.payload.targetSessionId ?? input.existing.targetSessionId,
|
|
1290
|
+
variableSetId: null,
|
|
1291
|
+
rigId: null,
|
|
1292
|
+
agentConfig: scheduledSessionMessageConfig(config),
|
|
346
1293
|
});
|
|
347
|
-
}
|
|
348
|
-
if (existingKnowledge) {
|
|
349
1294
|
if (
|
|
350
|
-
input.payload.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
) {
|
|
1295
|
+
input.payload.variableSetId &&
|
|
1296
|
+
![target?.variableSetId, ...(target?.variableSetIds ?? [])].includes(
|
|
1297
|
+
input.payload.variableSetId,
|
|
1298
|
+
)
|
|
1299
|
+
)
|
|
356
1300
|
throw new HTTPException(422, {
|
|
357
|
-
message:
|
|
1301
|
+
message:
|
|
1302
|
+
"Existing-chat schedules use the chat's Variable Sets. Change the chat's attachments separately.",
|
|
358
1303
|
});
|
|
359
|
-
|
|
360
|
-
if (input.payload.overlapPolicy === "allow_concurrent") {
|
|
1304
|
+
if (input.payload.rigId && input.payload.rigId !== target?.rigId)
|
|
361
1305
|
throw new HTTPException(422, {
|
|
362
|
-
message:
|
|
1306
|
+
message:
|
|
1307
|
+
"Existing-chat schedules use the chat's environment. Change the chat's attachments separately.",
|
|
363
1308
|
});
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
1309
|
+
const retargeting =
|
|
1310
|
+
input.existing.runMode !== "existing_session" ||
|
|
1311
|
+
target?.id !== input.existing.targetSessionId;
|
|
1312
|
+
const previousTargetSessionId =
|
|
1313
|
+
input.existing.runMode === "existing_session"
|
|
1314
|
+
? input.existing.targetSessionId
|
|
1315
|
+
: input.existing.runMode === "reusable_session"
|
|
1316
|
+
? input.existing.reusableSessionId
|
|
1317
|
+
: null;
|
|
1318
|
+
const previousTarget =
|
|
1319
|
+
retargeting && previousTargetSessionId
|
|
1320
|
+
? await validateScheduledTaskTarget({
|
|
1321
|
+
...input,
|
|
1322
|
+
runMode: "existing_session",
|
|
1323
|
+
targetSessionId: previousTargetSessionId,
|
|
1324
|
+
departingTarget: true,
|
|
1325
|
+
variableSetId: null,
|
|
1326
|
+
rigId: null,
|
|
1327
|
+
agentConfig: scheduledSessionMessageConfig(input.existing.agentConfig),
|
|
1328
|
+
})
|
|
1329
|
+
: null;
|
|
1330
|
+
const previousVariableSets = previousTarget
|
|
1331
|
+
? [
|
|
1332
|
+
...new Set(
|
|
1333
|
+
[previousTarget.variableSetId, ...(previousTarget.variableSetIds ?? [])].filter(
|
|
1334
|
+
(id): id is string => Boolean(id),
|
|
1335
|
+
),
|
|
1336
|
+
),
|
|
1337
|
+
]
|
|
1338
|
+
: input.existing.variableSetId
|
|
1339
|
+
? [input.existing.variableSetId]
|
|
1340
|
+
: [];
|
|
1341
|
+
const targetVariableSets = new Set([target?.variableSetId, ...(target?.variableSetIds ?? [])]);
|
|
1342
|
+
const removedVariableSetIds = retargeting
|
|
1343
|
+
? previousVariableSets.filter((id) => !targetVariableSets.has(id))
|
|
1344
|
+
: [];
|
|
1345
|
+
const previousRigId = previousTarget ? previousTarget.rigId : input.existing.rigId;
|
|
1346
|
+
const changedRig = retargeting && previousRigId && previousRigId !== target?.rigId;
|
|
1347
|
+
if ((removedVariableSetIds.length || changedRig) && !input.payload.adoptSessionSettings) {
|
|
1348
|
+
const detail = {
|
|
1349
|
+
code: "scheduled_target_access_change",
|
|
1350
|
+
targetSessionId: target!.id,
|
|
1351
|
+
removedVariableSetIds,
|
|
1352
|
+
removedVariableSetCount: removedVariableSetIds.length,
|
|
1353
|
+
removedRigId: changedRig ? previousRigId : null,
|
|
1354
|
+
resolution:
|
|
1355
|
+
"Review the destination's access, then retry with adoptSessionSettings=true and the reviewed expectedExecutionDigest to use that chat's attachments.",
|
|
1356
|
+
};
|
|
1357
|
+
throw new HTTPException(409, {
|
|
1358
|
+
message:
|
|
1359
|
+
"The destination chat has different attachments. Review the access change before moving this schedule.",
|
|
1360
|
+
cause: detail,
|
|
370
1361
|
});
|
|
371
|
-
update.action = input.payload.action;
|
|
372
|
-
}
|
|
373
|
-
if (input.payload.name !== undefined)
|
|
374
|
-
update.name = trimmedScheduledTaskName(input.payload.name);
|
|
375
|
-
if (input.payload.status !== undefined) update.status = input.payload.status;
|
|
376
|
-
if (input.payload.schedule !== undefined) {
|
|
377
|
-
validateScheduledTaskSchedule(input.payload.schedule);
|
|
378
|
-
update.schedule = input.payload.schedule;
|
|
379
|
-
}
|
|
380
|
-
if (input.payload.overlapPolicy !== undefined) {
|
|
381
|
-
update.overlapPolicy = input.payload.overlapPolicy;
|
|
382
1362
|
}
|
|
383
|
-
if (input.payload.
|
|
384
|
-
|
|
1363
|
+
if (input.payload.agentConfig || input.payload.agentConfigPatch || retargeting)
|
|
1364
|
+
await requireScheduledMessageVariableSetUse({ db: input.db, grant: input.grant, target });
|
|
1365
|
+
// Server-side normalization preserves exact message fields and authority choices;
|
|
1366
|
+
// callers never reconstruct an incomplete get projection to remove old settings.
|
|
1367
|
+
input = {
|
|
1368
|
+
...input,
|
|
1369
|
+
payload: {
|
|
1370
|
+
...input.payload,
|
|
1371
|
+
...(retargeting
|
|
1372
|
+
? {
|
|
1373
|
+
agentConfig: scheduledSessionMessageConfig({
|
|
1374
|
+
...config,
|
|
1375
|
+
...input.payload.agentConfigPatch,
|
|
1376
|
+
prompt: input.payload.agentConfigPatch?.prompt ?? config.prompt,
|
|
1377
|
+
}),
|
|
1378
|
+
agentConfigPatch: undefined,
|
|
1379
|
+
variableSetId: null,
|
|
1380
|
+
rigId: null,
|
|
1381
|
+
}
|
|
1382
|
+
: {}),
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
if (input.payload.agentLearning) {
|
|
1387
|
+
const context = input.authorization
|
|
1388
|
+
? await knowledgeContextForAccess(
|
|
1389
|
+
{ db: input.db },
|
|
1390
|
+
input.authorization,
|
|
1391
|
+
"scheduled_tasks:manage",
|
|
1392
|
+
)
|
|
1393
|
+
: null;
|
|
1394
|
+
if (context?.actor.kind !== "human")
|
|
1395
|
+
throw new HTTPException(403, {
|
|
1396
|
+
message: "Only an authenticated person can configure Agent learning",
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
const update: UpdateScheduledTaskInput = {
|
|
1400
|
+
expectedExecutionDigest: input.existing.executionDigest,
|
|
1401
|
+
};
|
|
1402
|
+
const requestedKnowledgeSource = input.payload.agentConfig?.knowledgeSource ?? null;
|
|
1403
|
+
if (
|
|
1404
|
+
isRetiredNativeAtlassianTask(input.existing) &&
|
|
1405
|
+
(input.payload.status === "active" || requestedKnowledgeSource)
|
|
1406
|
+
)
|
|
1407
|
+
throw new HTTPException(410, { message: ATLASSIAN_NATIVE_RETIRED_MESSAGE });
|
|
1408
|
+
const existingKnowledgeSource = scheduledTaskKnowledgeSource(input.existing);
|
|
1409
|
+
// Editing an ordinary source task's prompt/settings must not orphan its
|
|
1410
|
+
// connector binding. Deleting the task is the explicit source-disable path.
|
|
1411
|
+
if (
|
|
1412
|
+
input.payload.agentConfig &&
|
|
1413
|
+
input.existing.agentConfig.knowledgeSource &&
|
|
1414
|
+
!input.payload.agentConfig.knowledgeSource
|
|
1415
|
+
) {
|
|
1416
|
+
input = {
|
|
1417
|
+
...input,
|
|
1418
|
+
payload: {
|
|
1419
|
+
...input.payload,
|
|
1420
|
+
agentConfig: {
|
|
1421
|
+
...input.payload.agentConfig,
|
|
1422
|
+
knowledgeSource: input.existing.agentConfig.knowledgeSource,
|
|
1423
|
+
},
|
|
1424
|
+
},
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
if (input.payload.action && input.payload.action.kind !== "agent_turn") {
|
|
1428
|
+
throw new HTTPException(422, { message: "Source ingestion uses an ordinary agent task" });
|
|
1429
|
+
}
|
|
1430
|
+
const knowledgeSource =
|
|
1431
|
+
input.payload.agentConfig?.knowledgeSource ?? scheduledTaskKnowledgeSource(input.existing);
|
|
1432
|
+
if (
|
|
1433
|
+
knowledgeSource &&
|
|
1434
|
+
requestedKnowledgeSource &&
|
|
1435
|
+
!isDeepStrictEqual(requestedKnowledgeSource, existingKnowledgeSource)
|
|
1436
|
+
) {
|
|
1437
|
+
await validateKnowledgeSourceSyncAction({
|
|
1438
|
+
db: input.db,
|
|
1439
|
+
grant: input.grant,
|
|
1440
|
+
action: knowledgeSource,
|
|
1441
|
+
});
|
|
385
1442
|
}
|
|
1443
|
+
if (
|
|
1444
|
+
knowledgeSource &&
|
|
1445
|
+
(input.payload.overlapPolicy ?? input.existing.overlapPolicy) === "allow_concurrent"
|
|
1446
|
+
)
|
|
1447
|
+
throw new HTTPException(422, { message: "Source tasks require skip or buffer_one overlap" });
|
|
1448
|
+
if (
|
|
1449
|
+
knowledgeSource?.destination.kind === "personal" &&
|
|
1450
|
+
(input.payload.runMode ?? input.existing.runMode) !== "new_session_per_run"
|
|
1451
|
+
)
|
|
1452
|
+
throw new HTTPException(422, {
|
|
1453
|
+
message: "Personal source tasks create a private session for each run",
|
|
1454
|
+
});
|
|
386
1455
|
const existingTarget = input.existing.targetSessionId;
|
|
387
1456
|
const nextRunMode = input.payload.runMode ?? input.existing.runMode;
|
|
1457
|
+
const materializedReusable =
|
|
1458
|
+
input.existing.runMode === "reusable_session" &&
|
|
1459
|
+
nextRunMode === "reusable_session" &&
|
|
1460
|
+
input.existing.reusableSessionId !== null;
|
|
1461
|
+
let reusableTarget: Session | null = null;
|
|
388
1462
|
const nextTargetSessionId =
|
|
389
1463
|
input.payload.targetSessionId !== undefined
|
|
390
1464
|
? input.payload.targetSessionId
|
|
391
1465
|
: nextRunMode === "existing_session"
|
|
392
1466
|
? existingTarget
|
|
393
1467
|
: null;
|
|
394
|
-
if (
|
|
395
|
-
input.existing.runMode === "reusable_session" &&
|
|
396
|
-
input.existing.reusableSessionId &&
|
|
397
|
-
nextRunMode === "existing_session"
|
|
398
|
-
) {
|
|
399
|
-
throw new HTTPException(409, {
|
|
400
|
-
message:
|
|
401
|
-
"cannot target an existing session after this task created its reusable session; create a new task",
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
1468
|
if (input.payload.name !== undefined) {
|
|
405
1469
|
update.name = trimmedScheduledTaskName(input.payload.name);
|
|
406
1470
|
}
|
|
@@ -424,8 +1488,7 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
424
1488
|
const nextVariableSetId = input.payload.variableSetId;
|
|
425
1489
|
if (
|
|
426
1490
|
(input.existing.variableSetId ?? null) !== (nextVariableSetId ?? null) &&
|
|
427
|
-
|
|
428
|
-
input.existing.reusableSessionId
|
|
1491
|
+
materializedReusable
|
|
429
1492
|
) {
|
|
430
1493
|
throw new HTTPException(409, {
|
|
431
1494
|
message:
|
|
@@ -433,10 +1496,10 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
433
1496
|
});
|
|
434
1497
|
}
|
|
435
1498
|
if (nextVariableSetId === null) {
|
|
436
|
-
if (input.existing.variableSetId !== null) {
|
|
1499
|
+
if (input.existing.variableSetId !== null && nextRunMode !== "existing_session") {
|
|
437
1500
|
// Detaching is also an attachment change: it strips the secrets a
|
|
438
1501
|
// task's instructions were designed around.
|
|
439
|
-
requirePermission(input.grant, "variable-sets:
|
|
1502
|
+
requirePermission(input.grant, "variable-sets:attach");
|
|
440
1503
|
}
|
|
441
1504
|
update.variableSetId = null;
|
|
442
1505
|
} else {
|
|
@@ -450,73 +1513,396 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
450
1513
|
}
|
|
451
1514
|
}
|
|
452
1515
|
if (input.payload.rigId !== undefined) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
1516
|
+
if (materializedReusable && input.payload.rigId !== input.existing.rigId) {
|
|
1517
|
+
throw new HTTPException(409, {
|
|
1518
|
+
message: "A reusable-session task cannot change rigId after materialization; recreate it",
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
456
1521
|
if (input.payload.rigId !== null) {
|
|
457
|
-
await requireScheduledTaskRig(
|
|
1522
|
+
await requireScheduledTaskRig(
|
|
1523
|
+
input.db,
|
|
1524
|
+
{
|
|
1525
|
+
accountId: input.existing.accountId,
|
|
1526
|
+
workspaceId: input.existing.workspaceId,
|
|
1527
|
+
subjectId: input.grant.subjectId,
|
|
1528
|
+
},
|
|
1529
|
+
input.payload.rigId,
|
|
1530
|
+
);
|
|
458
1531
|
}
|
|
459
1532
|
update.rigId = input.payload.rigId;
|
|
460
1533
|
}
|
|
461
|
-
|
|
1534
|
+
// An edit that newly binds an environment to generated sessions needs the
|
|
1535
|
+
// same Variable Set attachment authority as create: a changed rigId, or a
|
|
1536
|
+
// switch away from an existing-session target that keeps the environment
|
|
1537
|
+
// the task adopted from that session.
|
|
1538
|
+
const nextRigId = input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId;
|
|
1539
|
+
if (
|
|
1540
|
+
nextRigId !== null &&
|
|
1541
|
+
!knowledgeSource &&
|
|
1542
|
+
nextRunMode !== "existing_session" &&
|
|
1543
|
+
(nextRigId !== input.existing.rigId || input.existing.runMode === "existing_session")
|
|
1544
|
+
) {
|
|
1545
|
+
await requireScheduledTaskRigVariableSetAttachments({
|
|
1546
|
+
settings: input.settings,
|
|
1547
|
+
db: input.db,
|
|
1548
|
+
grant: input.grant,
|
|
1549
|
+
workspaceId: input.existing.workspaceId,
|
|
1550
|
+
rig: await requireScheduledTaskRig(
|
|
1551
|
+
input.db,
|
|
1552
|
+
{
|
|
1553
|
+
accountId: input.existing.accountId,
|
|
1554
|
+
workspaceId: input.existing.workspaceId,
|
|
1555
|
+
subjectId: input.grant.subjectId,
|
|
1556
|
+
},
|
|
1557
|
+
nextRigId,
|
|
1558
|
+
),
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
if (input.payload.agentConfig !== undefined || input.payload.agentConfigPatch !== undefined) {
|
|
462
1562
|
// Editing the instructions of a task that injects workspace secrets is
|
|
463
1563
|
// equivalent to attaching those secrets to new instructions, so it
|
|
464
1564
|
// requires variable-sets:use even though plain task edits do not.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1565
|
+
if (materializedReusable) {
|
|
1566
|
+
reusableTarget = await getSession(
|
|
1567
|
+
input.db,
|
|
1568
|
+
input.existing.workspaceId,
|
|
1569
|
+
input.existing.reusableSessionId!,
|
|
1570
|
+
);
|
|
1571
|
+
if (!reusableTarget || reusableTarget.accountId !== input.existing.accountId)
|
|
1572
|
+
throw new HTTPException(409, { message: "Scheduled task chat is unavailable" });
|
|
1573
|
+
}
|
|
1574
|
+
// Existing-chat targets were checked above. A materialized reusable chat
|
|
1575
|
+
// likewise owns its live attachments; obsolete creation defaults do not
|
|
1576
|
+
// determine whether editing its scheduled message needs secret-use access.
|
|
1577
|
+
if (nextRunMode !== "existing_session")
|
|
1578
|
+
await requireScheduledMessageVariableSetUse({
|
|
1579
|
+
db: input.db,
|
|
1580
|
+
grant: input.grant,
|
|
1581
|
+
target: reusableTarget,
|
|
1582
|
+
variableSetId:
|
|
1583
|
+
input.payload.variableSetId !== undefined
|
|
1584
|
+
? input.payload.variableSetId
|
|
1585
|
+
: input.existing.variableSetId,
|
|
1586
|
+
rigId: nextRigId,
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
if (input.payload.agentConfigPatch) {
|
|
1590
|
+
const patch = input.payload.agentConfigPatch;
|
|
1591
|
+
const model =
|
|
1592
|
+
patch.model === undefined ? undefined : canonicalConfiguredModel(input.settings, patch.model);
|
|
1593
|
+
if (model !== undefined && model !== null) {
|
|
1594
|
+
await assertWorkspaceModelPolicyAllows(
|
|
1595
|
+
input.db,
|
|
1596
|
+
input.settings,
|
|
1597
|
+
input.existing.workspaceId,
|
|
1598
|
+
model,
|
|
1599
|
+
);
|
|
471
1600
|
}
|
|
1601
|
+
// Validate only the newly supplied model settings, not a reconstructed
|
|
1602
|
+
// bounded input. Legacy stored text, resources and selections stay exact.
|
|
1603
|
+
update.agentConfig = {
|
|
1604
|
+
...input.existing.agentConfig,
|
|
1605
|
+
...(patch.prompt !== undefined ? { prompt: patch.prompt } : {}),
|
|
1606
|
+
...(model !== undefined && model !== null ? { model } : {}),
|
|
1607
|
+
...(patch.reasoningEffort !== undefined ? { reasoningEffort: patch.reasoningEffort } : {}),
|
|
1608
|
+
};
|
|
1609
|
+
// Validation performs asynchronous authority checks before persistence.
|
|
1610
|
+
// Reuse the locked-row CAS so that merging this snapshot cannot erase a
|
|
1611
|
+
// concurrent config edit. The caller receives 409, never an automatic retry.
|
|
1612
|
+
update.expectedExecutionDigest = input.existing.executionDigest;
|
|
1613
|
+
}
|
|
1614
|
+
if (input.payload.agentConfig !== undefined) {
|
|
472
1615
|
const nextAgentConfig = await validateScheduledTaskAgentConfig({
|
|
473
1616
|
settings: input.settings,
|
|
474
1617
|
db: input.db,
|
|
475
1618
|
objectStorage: input.objectStorage,
|
|
476
1619
|
grant: input.grant,
|
|
477
1620
|
workspaceId: input.existing.workspaceId,
|
|
478
|
-
|
|
1621
|
+
...(input.authorization ? { authorization: input.authorization } : {}),
|
|
1622
|
+
payload: {
|
|
1623
|
+
agentConfig: input.payload.agentConfig,
|
|
1624
|
+
runMode: nextRunMode,
|
|
1625
|
+
targetSessionId:
|
|
1626
|
+
nextRunMode === "existing_session"
|
|
1627
|
+
? nextTargetSessionId
|
|
1628
|
+
: input.existing.reusableSessionId,
|
|
1629
|
+
},
|
|
479
1630
|
...(input.toolsProvided !== undefined ? { toolsProvided: input.toolsProvided } : {}),
|
|
1631
|
+
...(materializedReusable ? { retainedCreationConfig: input.existing.agentConfig } : {}),
|
|
480
1632
|
});
|
|
481
1633
|
if (
|
|
482
|
-
|
|
483
|
-
input.existing.runMode === "reusable_session" &&
|
|
1634
|
+
materializedReusable &&
|
|
484
1635
|
(input.existing.agentConfig.slackBotConnectionId ?? null) !==
|
|
485
1636
|
(nextAgentConfig.slackBotConnectionId ?? null)
|
|
486
1637
|
) {
|
|
487
1638
|
throw new HTTPException(409, {
|
|
488
1639
|
message:
|
|
489
|
-
"cannot change the
|
|
1640
|
+
"cannot change the Opengeni Slack bot connection of a task with a live reusable session; recreate the task",
|
|
490
1641
|
});
|
|
491
1642
|
}
|
|
1643
|
+
// Update text is exact, even when a full form submission reuses the saved
|
|
1644
|
+
// prompt while changing another setting. Creation keeps its trim convention.
|
|
1645
|
+
nextAgentConfig.prompt = retainedPrompt ?? input.payload.agentConfig.prompt;
|
|
1646
|
+
if (retainedPrompt !== undefined) {
|
|
1647
|
+
nextAgentConfig.resources = input.existing.agentConfig.resources;
|
|
1648
|
+
}
|
|
492
1649
|
update.agentConfig = nextAgentConfig;
|
|
1650
|
+
}
|
|
1651
|
+
if (
|
|
1652
|
+
(update.agentConfig && !input.payload.agentConfigPatch) ||
|
|
1653
|
+
input.payload.connectionAccounts !== undefined
|
|
1654
|
+
) {
|
|
1655
|
+
update.agentConfig = {
|
|
1656
|
+
...(update.agentConfig ?? input.existing.agentConfig),
|
|
1657
|
+
connectionAccounts:
|
|
1658
|
+
input.payload.connectionAccounts ?? input.existing.agentConfig.connectionAccounts ?? [],
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
const nextAgentConfig = {
|
|
1662
|
+
...(update.agentConfig ?? input.existing.agentConfig),
|
|
1663
|
+
};
|
|
1664
|
+
// Retargeting rewrites a narrow patch into a full config above. Check the
|
|
1665
|
+
// exact final message after restoring retained fields, regardless of path.
|
|
1666
|
+
// An unedited legacy prompt remains movable even when above today's limit.
|
|
1667
|
+
if (
|
|
1668
|
+
requestedPrompt !== undefined &&
|
|
1669
|
+
scheduledOccurrencePayloadUtf8Bytes(nextAgentConfig) >
|
|
1670
|
+
SCHEDULED_TASK_OCCURRENCE_PAYLOAD_MAX_BYTES -
|
|
1671
|
+
SCHEDULED_TASK_OCCURRENCE_PAYLOAD_INGRESS_HEADROOM_BYTES
|
|
1672
|
+
)
|
|
1673
|
+
throw new HTTPException(422, {
|
|
1674
|
+
message: "Updated scheduled message and attachments exceed the supported occurrence payload",
|
|
1675
|
+
});
|
|
1676
|
+
await validateScheduledTaskSlackChannel({
|
|
1677
|
+
grant: input.grant,
|
|
1678
|
+
authorization: input.authorization,
|
|
1679
|
+
previous: input.existing.agentConfig,
|
|
1680
|
+
next: nextAgentConfig,
|
|
1681
|
+
runMode: nextRunMode,
|
|
1682
|
+
reusableSessionCanPost:
|
|
1683
|
+
input.existing.runMode === "reusable_session" && input.existing.reusableSessionId !== null
|
|
1684
|
+
? async () => {
|
|
1685
|
+
const chat = await getSession(
|
|
1686
|
+
input.db,
|
|
1687
|
+
input.existing.workspaceId,
|
|
1688
|
+
input.existing.reusableSessionId!,
|
|
1689
|
+
);
|
|
1690
|
+
return SCHEDULED_SLACK_BOT_POSTING_TOOLS.every(
|
|
1691
|
+
(tool) => chat?.firstPartyMcpTools?.includes(tool) === true,
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
: null,
|
|
1695
|
+
verifySlackChannel: input.verifySlackChannel,
|
|
1696
|
+
});
|
|
1697
|
+
const authorityTargetChanged =
|
|
1698
|
+
nextRunMode !== input.existing.runMode ||
|
|
1699
|
+
nextTargetSessionId !== input.existing.targetSessionId ||
|
|
1700
|
+
(input.payload.variableSetId !== undefined &&
|
|
1701
|
+
input.payload.variableSetId !== input.existing.variableSetId) ||
|
|
1702
|
+
(input.payload.rigId !== undefined && input.payload.rigId !== input.existing.rigId);
|
|
1703
|
+
const materialExecutionChange =
|
|
1704
|
+
authorityTargetChanged ||
|
|
1705
|
+
input.payload.connectionAccounts !== undefined ||
|
|
1706
|
+
!isDeepStrictEqual(nextAgentConfig, input.existing.agentConfig) ||
|
|
1707
|
+
(input.payload.action !== undefined &&
|
|
1708
|
+
!isDeepStrictEqual(input.payload.action, input.existing.action)) ||
|
|
1709
|
+
(input.payload.schedule !== undefined &&
|
|
1710
|
+
!isDeepStrictEqual(input.payload.schedule, input.existing.schedule)) ||
|
|
1711
|
+
(input.payload.overlapPolicy !== undefined &&
|
|
1712
|
+
input.payload.overlapPolicy !== input.existing.overlapPolicy) ||
|
|
1713
|
+
(input.payload.metadata !== undefined &&
|
|
1714
|
+
!isDeepStrictEqual(input.payload.metadata, input.existing.metadata)) ||
|
|
1715
|
+
(input.existing.status === "paused" && input.payload.status === "active");
|
|
1716
|
+
if (
|
|
1717
|
+
materialExecutionChange &&
|
|
1718
|
+
nextRunMode !== "existing_session" &&
|
|
1719
|
+
(!materializedReusable || nextAgentConfig.model !== input.existing.agentConfig.model)
|
|
1720
|
+
) {
|
|
1721
|
+
const beforeUpdateCommit = workspaceCustomModelCommitGuard({
|
|
1722
|
+
settings: input.settings,
|
|
1723
|
+
accountId: input.existing.accountId,
|
|
1724
|
+
workspaceId: input.existing.workspaceId,
|
|
1725
|
+
modelId: nextAgentConfig.model ?? input.settings.openaiModel,
|
|
1726
|
+
claudeAuthoritySnapshot: await getScheduledTaskClaudeProviderAccountAuthoritySnapshot(
|
|
1727
|
+
input.db,
|
|
1728
|
+
input.existing.workspaceId,
|
|
1729
|
+
input.existing.id,
|
|
1730
|
+
),
|
|
1731
|
+
});
|
|
1732
|
+
if (beforeUpdateCommit) update.beforeUpdateCommit = beforeUpdateCommit;
|
|
1733
|
+
}
|
|
1734
|
+
const existingXaiAuthority = await getScheduledTaskXaiProviderAccountAuthoritySnapshot(
|
|
1735
|
+
input.db,
|
|
1736
|
+
input.existing.workspaceId,
|
|
1737
|
+
input.existing.id,
|
|
1738
|
+
);
|
|
1739
|
+
if (
|
|
1740
|
+
existingXaiAuthority.scope === "user" &&
|
|
1741
|
+
materialExecutionChange &&
|
|
1742
|
+
(input.existing.createdBy.kind !== "subject" ||
|
|
1743
|
+
input.existing.createdBy.subjectId !== input.grant.subjectId)
|
|
1744
|
+
) {
|
|
1745
|
+
throw new HTTPException(409, {
|
|
1746
|
+
message: "changing a user-scoped xAI scheduled task requires the same causal human",
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
if (materialExecutionChange) {
|
|
1750
|
+
await assertScheduledTaskMutationOwner(input.db, input.grant, input.existing.id);
|
|
1751
|
+
const ownerSubjectId = input.existing.ownerSubjectId;
|
|
493
1752
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
494
1753
|
input.db,
|
|
495
|
-
input.
|
|
1754
|
+
input.grant.workspaceId,
|
|
496
1755
|
input.settings,
|
|
497
|
-
{ subjectId:
|
|
1756
|
+
ownerSubjectId ? { subjectId: ownerSubjectId } : {},
|
|
498
1757
|
);
|
|
499
|
-
|
|
1758
|
+
let nextTarget = await validateScheduledTaskTarget({
|
|
500
1759
|
db: input.db,
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1760
|
+
sessionAuthorization: input.sessionAuthorization,
|
|
1761
|
+
authorizationSurface: input.authorizationSurface,
|
|
1762
|
+
grant: input.grant,
|
|
1763
|
+
targetSessionId: nextTargetSessionId,
|
|
1764
|
+
runMode: nextRunMode,
|
|
1765
|
+
variableSetId:
|
|
1766
|
+
input.payload.variableSetId !== undefined
|
|
1767
|
+
? input.payload.variableSetId
|
|
1768
|
+
: input.existing.variableSetId,
|
|
1769
|
+
rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
|
|
1770
|
+
agentConfig: nextAgentConfig,
|
|
505
1771
|
});
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
input.existing.workspaceId,
|
|
510
|
-
|
|
1772
|
+
if (materializedReusable) {
|
|
1773
|
+
nextTarget =
|
|
1774
|
+
reusableTarget ??
|
|
1775
|
+
(await getSession(input.db, input.existing.workspaceId, input.existing.reusableSessionId!));
|
|
1776
|
+
if (!nextTarget || nextTarget.accountId !== input.existing.accountId)
|
|
1777
|
+
throw new HTTPException(409, { message: "Scheduled task chat is unavailable" });
|
|
1778
|
+
}
|
|
1779
|
+
const nextConnectionTools = await scheduledConnectionTools(
|
|
1780
|
+
input.db,
|
|
1781
|
+
input.grant.workspaceId,
|
|
1782
|
+
runtimeSettings,
|
|
1783
|
+
nextTarget,
|
|
1784
|
+
nextAgentConfig.tools,
|
|
1785
|
+
ownerSubjectId ?? undefined,
|
|
1786
|
+
);
|
|
1787
|
+
const priorTargetSessionId =
|
|
1788
|
+
input.existing.runMode === "existing_session"
|
|
1789
|
+
? input.existing.targetSessionId
|
|
1790
|
+
: input.existing.runMode === "reusable_session"
|
|
1791
|
+
? input.existing.reusableSessionId
|
|
1792
|
+
: null;
|
|
1793
|
+
const priorTarget = priorTargetSessionId
|
|
1794
|
+
? priorTargetSessionId === nextTarget?.id
|
|
1795
|
+
? nextTarget
|
|
1796
|
+
: await getSession(input.db, input.existing.workspaceId, priorTargetSessionId)
|
|
1797
|
+
: null;
|
|
1798
|
+
const priorConnectionResources = mergeResourceRefs(
|
|
1799
|
+
priorTarget?.resources ?? [],
|
|
1800
|
+
input.existing.agentConfig.resources,
|
|
1801
|
+
);
|
|
1802
|
+
const nextConnectionResources = mergeResourceRefs(
|
|
1803
|
+
nextTarget?.resources ?? [],
|
|
1804
|
+
nextAgentConfig.resources,
|
|
1805
|
+
);
|
|
1806
|
+
const priorConnectionTools = await scheduledConnectionTools(
|
|
1807
|
+
input.db,
|
|
1808
|
+
input.grant.workspaceId,
|
|
1809
|
+
runtimeSettings,
|
|
1810
|
+
priorTarget,
|
|
1811
|
+
input.existing.agentConfig.tools,
|
|
1812
|
+
ownerSubjectId ?? undefined,
|
|
1813
|
+
);
|
|
1814
|
+
const priorAccountSurfaceIds = new Set(
|
|
1815
|
+
[...input.existing.agentConfig.tools, ...(priorTarget?.tools ?? []), ...priorConnectionTools]
|
|
1816
|
+
.filter((tool) => tool.kind === "mcp")
|
|
1817
|
+
.map((tool) => tool.id),
|
|
1818
|
+
);
|
|
1819
|
+
if (personalGitHubRepositoryResources(priorConnectionResources).length)
|
|
1820
|
+
priorAccountSurfaceIds.add(PERSONAL_GITHUB_CONNECTION_SURFACE_ID);
|
|
1821
|
+
const nextMcpIds = new Set(
|
|
1822
|
+
nextConnectionTools.filter((tool) => tool.kind === "mcp").map((tool) => tool.id),
|
|
1823
|
+
);
|
|
1824
|
+
const nextAccountSurfaceIds = new Set(nextMcpIds);
|
|
1825
|
+
const nextSurfaceEligibility = scheduledConnectionSurfaceEligibility(
|
|
1826
|
+
runtimeSettings,
|
|
1827
|
+
nextTarget,
|
|
1828
|
+
);
|
|
1829
|
+
if (nextSurfaceEligibility.googleDrivePublicationEnabled)
|
|
1830
|
+
nextAccountSurfaceIds.add(GOOGLE_DRIVE_PUBLICATION_SERVER_ID);
|
|
1831
|
+
if (personalGitHubRepositoryResources(nextConnectionResources).length)
|
|
1832
|
+
nextAccountSurfaceIds.add(PERSONAL_GITHUB_CONNECTION_SURFACE_ID);
|
|
1833
|
+
const movingChat =
|
|
1834
|
+
nextRunMode !== input.existing.runMode ||
|
|
1835
|
+
nextTargetSessionId !== input.existing.targetSessionId;
|
|
1836
|
+
// Removing a selected tool or repository also removes its inherited account choice.
|
|
1837
|
+
// Keep explicit caller selections subject to normal validation, and retain
|
|
1838
|
+
// account choices for unchanged tools and repository selections.
|
|
1839
|
+
const authoritySelections = (nextAgentConfig.connectionAccounts ?? []).filter(
|
|
1840
|
+
(selection) =>
|
|
1841
|
+
input.payload.connectionAccounts !== undefined ||
|
|
1842
|
+
(movingChat
|
|
1843
|
+
? nextAccountSurfaceIds.has(selection.serverId)
|
|
1844
|
+
: !priorAccountSurfaceIds.has(selection.serverId) ||
|
|
1845
|
+
nextAccountSurfaceIds.has(selection.serverId)),
|
|
1846
|
+
);
|
|
1847
|
+
const acceptedConnections = await freezeConnectionAccounts({
|
|
1848
|
+
db: input.db,
|
|
1849
|
+
accountId: input.grant.accountId,
|
|
1850
|
+
workspaceId: input.grant.workspaceId,
|
|
1851
|
+
settings: nextTarget
|
|
1852
|
+
? settingsWithSessionMcpServerMetadata(runtimeSettings, nextTarget.mcpServers)
|
|
1853
|
+
: runtimeSettings,
|
|
1854
|
+
tools: nextConnectionTools,
|
|
1855
|
+
resources: nextConnectionResources,
|
|
1856
|
+
source: ownerSubjectId
|
|
1857
|
+
? { kind: "subject", subjectId: ownerSubjectId, accountId: input.existing.accountId }
|
|
1858
|
+
: { kind: "none" },
|
|
1859
|
+
authoritySelections,
|
|
1860
|
+
// An edit replaces an accepted account set exactly. Omitting a connector
|
|
1861
|
+
// from that set must not attach newly available accounts implicitly.
|
|
1862
|
+
authoritySelectionsFrozen: input.existing.agentConfig.connectionAccountsFrozen === true,
|
|
1863
|
+
...nextSurfaceEligibility,
|
|
1864
|
+
});
|
|
1865
|
+
// A model-only patch still revalidates authority above, but is not an
|
|
1866
|
+
// access refresh. Preserve exact existing selections, including legacy
|
|
1867
|
+
// absent fields, unless the caller also changed accounts or the target.
|
|
1868
|
+
if (
|
|
1869
|
+
!input.payload.agentConfigPatch ||
|
|
1870
|
+
input.payload.connectionAccounts !== undefined ||
|
|
1871
|
+
authorityTargetChanged
|
|
1872
|
+
) {
|
|
1873
|
+
const routeIds = new Set(
|
|
1874
|
+
(acceptedConnections.mcpAccountBindings ?? []).map((binding) => binding.serverId),
|
|
511
1875
|
);
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
1876
|
+
nextAgentConfig.connectionAccounts = [
|
|
1877
|
+
...(acceptedConnections.mcpAccountBindings ?? []).map(
|
|
1878
|
+
({ canonicalServerId, connectionId }) => ({
|
|
1879
|
+
serverId: canonicalServerId,
|
|
1880
|
+
connectionId,
|
|
1881
|
+
}),
|
|
1882
|
+
),
|
|
1883
|
+
...acceptedConnections.personalConnectionDelegations
|
|
1884
|
+
.filter(
|
|
1885
|
+
(item) =>
|
|
1886
|
+
!routeIds.has(item.serverId) &&
|
|
1887
|
+
(!item.connectionType ||
|
|
1888
|
+
item.connectionType === "mcp" ||
|
|
1889
|
+
item.connectionType === "github_personal"),
|
|
1890
|
+
)
|
|
1891
|
+
.map(({ serverId, connectionId }) => ({ serverId, connectionId })),
|
|
1892
|
+
];
|
|
1893
|
+
nextAgentConfig.connectionAccountsFrozen = true;
|
|
518
1894
|
}
|
|
519
|
-
update.
|
|
1895
|
+
update.agentConfig = nextAgentConfig;
|
|
1896
|
+
}
|
|
1897
|
+
if (
|
|
1898
|
+
!materialExecutionChange &&
|
|
1899
|
+
input.payload.connectionAccounts === undefined &&
|
|
1900
|
+
update.clonePersonalResourceAuthorityFromRevision === undefined
|
|
1901
|
+
) {
|
|
1902
|
+
// Administrative lifecycle/name edits preserve the exact revision-bound
|
|
1903
|
+
// causal human. They must not silently re-authorize retained Variable Set,
|
|
1904
|
+
// Rig, Connection, or xAI authority under the manager performing the edit.
|
|
1905
|
+
update.clonePersonalResourceAuthorityFromRevision = input.existing.authorityRevision;
|
|
520
1906
|
}
|
|
521
1907
|
if (
|
|
522
1908
|
existingTarget &&
|
|
@@ -528,6 +1914,7 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
528
1914
|
authorizationSurface: input.authorizationSurface,
|
|
529
1915
|
grant: input.grant,
|
|
530
1916
|
targetSessionId: existingTarget,
|
|
1917
|
+
departingTarget: true,
|
|
531
1918
|
runMode: "existing_session",
|
|
532
1919
|
variableSetId: input.existing.variableSetId,
|
|
533
1920
|
rigId: input.existing.rigId,
|
|
@@ -548,13 +1935,39 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
548
1935
|
rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
|
|
549
1936
|
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
550
1937
|
});
|
|
1938
|
+
if (
|
|
1939
|
+
!knowledgeSource &&
|
|
1940
|
+
!(
|
|
1941
|
+
materializedReusable &&
|
|
1942
|
+
isDeepStrictEqual(nextAgentConfig.machineTarget, input.existing.agentConfig.machineTarget) &&
|
|
1943
|
+
nextAgentConfig.sandboxBackend === input.existing.agentConfig.sandboxBackend
|
|
1944
|
+
)
|
|
1945
|
+
) {
|
|
1946
|
+
await validateScheduledTaskMachineTarget({
|
|
1947
|
+
settings: input.settings,
|
|
1948
|
+
db: input.db,
|
|
1949
|
+
grant: input.grant,
|
|
1950
|
+
runMode: nextRunMode,
|
|
1951
|
+
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
551
1954
|
if (
|
|
552
1955
|
input.payload.targetSessionId !== undefined ||
|
|
553
1956
|
input.existing.runMode === "existing_session" ||
|
|
554
|
-
nextRunMode === "existing_session"
|
|
1957
|
+
nextRunMode === "existing_session" ||
|
|
1958
|
+
(input.existing.runMode === "reusable_session" && nextRunMode !== "reusable_session")
|
|
555
1959
|
) {
|
|
556
1960
|
update.targetSessionId = nextTargetSessionId;
|
|
557
1961
|
}
|
|
1962
|
+
Object.assign(update, scheduledTaskAuthorityUpdateForGrant(input.grant));
|
|
1963
|
+
const linkCapture = prepareExternalLinkTaskAdmission(
|
|
1964
|
+
input.authorization,
|
|
1965
|
+
scheduledTaskInitiatorForGrant(input.grant).actor,
|
|
1966
|
+
);
|
|
1967
|
+
if (linkCapture) update.captureLinkAuthority = linkCapture;
|
|
1968
|
+
if (update.clonePersonalResourceAuthorityFromRevision !== undefined) {
|
|
1969
|
+
update.refreshPersonalResourceAuthority = false;
|
|
1970
|
+
}
|
|
558
1971
|
return update;
|
|
559
1972
|
}
|
|
560
1973
|
|
|
@@ -572,21 +1985,19 @@ export async function requireScheduledTaskForApi(
|
|
|
572
1985
|
|
|
573
1986
|
export type ScheduledTaskRestoreState = {
|
|
574
1987
|
task: ScheduledTask;
|
|
575
|
-
|
|
1988
|
+
learning?: {
|
|
1989
|
+
context: KnowledgeContext;
|
|
1990
|
+
scope: "workspace" | "personal";
|
|
1991
|
+
settings: import("@opengeni/contracts").AgentLearningOverridePatch;
|
|
1992
|
+
expectedVersion: number;
|
|
1993
|
+
};
|
|
576
1994
|
};
|
|
577
1995
|
|
|
578
1996
|
export async function captureScheduledTaskRestoreState(
|
|
579
|
-
|
|
1997
|
+
_db: Database,
|
|
580
1998
|
task: ScheduledTask,
|
|
581
1999
|
): Promise<ScheduledTaskRestoreState> {
|
|
582
|
-
return {
|
|
583
|
-
task,
|
|
584
|
-
personalConnectionDelegations: await getScheduledTaskPersonalConnectionDelegations(
|
|
585
|
-
db,
|
|
586
|
-
task.workspaceId,
|
|
587
|
-
task.id,
|
|
588
|
-
),
|
|
589
|
-
};
|
|
2000
|
+
return { task };
|
|
590
2001
|
}
|
|
591
2002
|
|
|
592
2003
|
export async function restoreScheduledTask(
|
|
@@ -602,13 +2013,13 @@ export async function restoreScheduledTask(
|
|
|
602
2013
|
overlapPolicy: task.overlapPolicy,
|
|
603
2014
|
action: task.action,
|
|
604
2015
|
agentConfig: task.agentConfig,
|
|
605
|
-
personalConnectionDelegations: previous.personalConnectionDelegations,
|
|
606
2016
|
...(task.runMode === "existing_session"
|
|
607
2017
|
? { targetSessionId: task.targetSessionId }
|
|
608
2018
|
: { reusableSessionId: task.reusableSessionId }),
|
|
609
2019
|
variableSetId: task.variableSetId,
|
|
610
2020
|
rigId: task.rigId,
|
|
611
2021
|
metadata: task.metadata,
|
|
2022
|
+
clonePersonalResourceAuthorityFromRevision: task.authorityRevision,
|
|
612
2023
|
});
|
|
613
2024
|
}
|
|
614
2025
|
|
|
@@ -617,6 +2028,8 @@ async function validateKnowledgeSourceSyncAction(input: {
|
|
|
617
2028
|
grant: AccessGrant;
|
|
618
2029
|
action: KnowledgeSourceSyncAction;
|
|
619
2030
|
}): Promise<void> {
|
|
2031
|
+
if (isRetiredNativeAtlassianSource(input.action))
|
|
2032
|
+
throw new HTTPException(410, { message: ATLASSIAN_NATIVE_RETIRED_MESSAGE });
|
|
620
2033
|
if (input.action.initiatingSubjectId !== input.grant.subjectId) {
|
|
621
2034
|
throw new HTTPException(403, {
|
|
622
2035
|
message: "knowledge source sync must preserve the exact initiating subject",
|
|
@@ -679,24 +2092,123 @@ export class ScheduledTaskSyncError extends Error {
|
|
|
679
2092
|
}
|
|
680
2093
|
}
|
|
681
2094
|
|
|
2095
|
+
/**
|
|
2096
|
+
* One deletion lifecycle shared by HTTP and first-party MCP adapters.
|
|
2097
|
+
* Connector authorization is proven before the one-way tombstone. The same
|
|
2098
|
+
* transaction detaches reclaimable task resources and persists both external
|
|
2099
|
+
* cleanup obligations; best-effort processing is only an acceleration of that
|
|
2100
|
+
* durable receipt.
|
|
2101
|
+
*/
|
|
2102
|
+
export async function deleteScheduledTaskLifecycle(input: {
|
|
2103
|
+
db: Database;
|
|
2104
|
+
workspaceId: string;
|
|
2105
|
+
taskId: string;
|
|
2106
|
+
subjectId: string;
|
|
2107
|
+
beforeDeleteCommit?: (tx: Database) => Promise<void>;
|
|
2108
|
+
preflightConnectorAuthorization: (task: ScheduledTask) => Promise<void>;
|
|
2109
|
+
cleanupConnectorAuthorization: (db: Database, task: ScheduledTask) => Promise<void>;
|
|
2110
|
+
processCleanupClaims?: (claims: readonly TemporalScheduleCleanupClaim[]) => Promise<void>;
|
|
2111
|
+
}): Promise<{
|
|
2112
|
+
task: ScheduledTask;
|
|
2113
|
+
changed: boolean;
|
|
2114
|
+
cleanup: TemporalScheduleCleanupClaim | null;
|
|
2115
|
+
}> {
|
|
2116
|
+
const result = await withWorkspaceSubjectRls(
|
|
2117
|
+
input.db,
|
|
2118
|
+
input.workspaceId,
|
|
2119
|
+
input.subjectId,
|
|
2120
|
+
async (tx) => {
|
|
2121
|
+
const task = await getScheduledTaskIncludingDeletedForUpdate(
|
|
2122
|
+
tx,
|
|
2123
|
+
input.workspaceId,
|
|
2124
|
+
input.taskId,
|
|
2125
|
+
);
|
|
2126
|
+
if (!task) throw new HTTPException(404, { message: "Scheduled task not found" });
|
|
2127
|
+
await input.beforeDeleteCommit?.(tx);
|
|
2128
|
+
const wasLive = task.deletedAt === null;
|
|
2129
|
+
if (
|
|
2130
|
+
scheduledTaskKnowledgeSource(task) &&
|
|
2131
|
+
(requireScheduledTaskKnowledgeSource(task).initiatingSubjectId !== input.subjectId ||
|
|
2132
|
+
requireScheduledTaskKnowledgeSource(task).connection.ownerSubjectId !== input.subjectId)
|
|
2133
|
+
) {
|
|
2134
|
+
throw new HTTPException(403, {
|
|
2135
|
+
message: "knowledge source schedule requires the exact initiating subject",
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
if (wasLive && scheduledTaskKnowledgeSource(task)) {
|
|
2139
|
+
await input.preflightConnectorAuthorization(task);
|
|
2140
|
+
}
|
|
2141
|
+
const deletion = scheduledTaskKnowledgeSource(task)
|
|
2142
|
+
? await (async () => {
|
|
2143
|
+
await input.cleanupConnectorAuthorization(tx, task);
|
|
2144
|
+
return await deleteScheduledTask(tx, input.workspaceId, task.id, {
|
|
2145
|
+
connectorCleanupSubjectId: input.subjectId,
|
|
2146
|
+
connectorCleanupCompleted: true,
|
|
2147
|
+
expectedAuthorityRevision: task.authorityRevision,
|
|
2148
|
+
expectedExecutionDigest: task.executionDigest,
|
|
2149
|
+
});
|
|
2150
|
+
})()
|
|
2151
|
+
: await deleteScheduledTask(tx, input.workspaceId, task.id);
|
|
2152
|
+
return { task, deletion };
|
|
2153
|
+
},
|
|
2154
|
+
);
|
|
2155
|
+
const { task, deletion } = result;
|
|
2156
|
+
const { cleanup, changed } = deletion;
|
|
2157
|
+
if (cleanup && input.processCleanupClaims) {
|
|
2158
|
+
await input.processCleanupClaims([cleanup]);
|
|
2159
|
+
}
|
|
2160
|
+
return { task, changed, cleanup };
|
|
2161
|
+
}
|
|
2162
|
+
|
|
682
2163
|
export async function syncCreatedScheduledTask(input: {
|
|
683
2164
|
db: Database;
|
|
684
2165
|
workflowClient: SessionWorkflowClient;
|
|
685
2166
|
task: ScheduledTask;
|
|
686
2167
|
}): Promise<void> {
|
|
687
2168
|
try {
|
|
688
|
-
await input.workflowClient.syncScheduledTask({
|
|
2169
|
+
await input.workflowClient.syncScheduledTask({
|
|
2170
|
+
task: input.task,
|
|
2171
|
+
onFailure: async (db, error) => {
|
|
2172
|
+
let persistenceRestored = true;
|
|
2173
|
+
try {
|
|
2174
|
+
// The synchronizer keeps its lock through compensation and commit.
|
|
2175
|
+
// A savepoint also rolls back a failed compensation independently.
|
|
2176
|
+
await db.transaction(async (tx) => {
|
|
2177
|
+
await requireUnchangedScheduledTaskForCompensation(tx, input.task);
|
|
2178
|
+
await deleteScheduledTask(tx, input.task.workspaceId, input.task.id);
|
|
2179
|
+
});
|
|
2180
|
+
} catch {
|
|
2181
|
+
persistenceRestored = false;
|
|
2182
|
+
}
|
|
2183
|
+
return new ScheduledTaskSyncError(error, persistenceRestored);
|
|
2184
|
+
},
|
|
2185
|
+
});
|
|
689
2186
|
} catch (error) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
persistenceRestored = false;
|
|
695
|
-
}
|
|
696
|
-
throw new ScheduledTaskSyncError(error, persistenceRestored);
|
|
2187
|
+
if (error instanceof ScheduledTaskSyncError) throw error;
|
|
2188
|
+
// Lock admission or transaction commit failed. No committed restoration
|
|
2189
|
+
// was acknowledged; never compensate outside the synchronizer's lock.
|
|
2190
|
+
throw new ScheduledTaskSyncError(error, false);
|
|
697
2191
|
}
|
|
698
2192
|
}
|
|
699
2193
|
|
|
2194
|
+
async function requireUnchangedScheduledTaskForCompensation(
|
|
2195
|
+
db: Database,
|
|
2196
|
+
expected: ScheduledTask,
|
|
2197
|
+
): Promise<void> {
|
|
2198
|
+
const current = await getScheduledTaskIncludingDeletedForUpdate(
|
|
2199
|
+
db,
|
|
2200
|
+
expected.workspaceId,
|
|
2201
|
+
expected.id,
|
|
2202
|
+
);
|
|
2203
|
+
if (!current) throw new ScheduledTaskHeadChangedError();
|
|
2204
|
+
const { deletedAt, ...task } = current;
|
|
2205
|
+
// The execution digest excludes names and lifecycle state. Compare the full
|
|
2206
|
+
// saved row under its write lock so a late sync failure cannot undo a newer
|
|
2207
|
+
// edit, pause, materialization or authority revision.
|
|
2208
|
+
if (deletedAt !== null || !isDeepStrictEqual(task, expected))
|
|
2209
|
+
throw new ScheduledTaskHeadChangedError();
|
|
2210
|
+
}
|
|
2211
|
+
|
|
700
2212
|
export async function syncUpdatedScheduledTask(input: {
|
|
701
2213
|
db: Database;
|
|
702
2214
|
workflowClient: SessionWorkflowClient;
|
|
@@ -704,15 +2216,42 @@ export async function syncUpdatedScheduledTask(input: {
|
|
|
704
2216
|
task: ScheduledTask;
|
|
705
2217
|
}): Promise<void> {
|
|
706
2218
|
try {
|
|
707
|
-
await input.workflowClient.syncScheduledTask({
|
|
2219
|
+
await input.workflowClient.syncScheduledTask({
|
|
2220
|
+
task: input.task,
|
|
2221
|
+
onFailure: async (db, error) => {
|
|
2222
|
+
let persistenceRestored = true;
|
|
2223
|
+
try {
|
|
2224
|
+
await db.transaction(async (tx) => {
|
|
2225
|
+
const learning = input.previous.learning;
|
|
2226
|
+
// Compensate only our exact settings version, while the updated task
|
|
2227
|
+
// still names that owner scope. A concurrent edit is never overwritten.
|
|
2228
|
+
if (learning)
|
|
2229
|
+
await saveAgentLearningSettings(tx, learning.context, {
|
|
2230
|
+
scope: learning.scope,
|
|
2231
|
+
source: { kind: "scheduled_task", id: input.task.id },
|
|
2232
|
+
operationId: crypto.randomUUID(),
|
|
2233
|
+
expectedVersion: learning.expectedVersion,
|
|
2234
|
+
settings: {
|
|
2235
|
+
knowledge: "inherit",
|
|
2236
|
+
instructions: "inherit",
|
|
2237
|
+
skills: "inherit",
|
|
2238
|
+
...learning.settings,
|
|
2239
|
+
},
|
|
2240
|
+
});
|
|
2241
|
+
// Keep the learning-owner → task lock order used by ordinary edits.
|
|
2242
|
+
// Any learning compensation above rolls back with a stale task head.
|
|
2243
|
+
await requireUnchangedScheduledTaskForCompensation(tx, input.task);
|
|
2244
|
+
await restoreScheduledTask(tx, input.previous);
|
|
2245
|
+
});
|
|
2246
|
+
} catch {
|
|
2247
|
+
persistenceRestored = false;
|
|
2248
|
+
}
|
|
2249
|
+
return new ScheduledTaskSyncError(error, persistenceRestored);
|
|
2250
|
+
},
|
|
2251
|
+
});
|
|
708
2252
|
} catch (error) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
await restoreScheduledTask(input.db, input.previous);
|
|
712
|
-
} catch {
|
|
713
|
-
persistenceRestored = false;
|
|
714
|
-
}
|
|
715
|
-
throw new ScheduledTaskSyncError(error, persistenceRestored);
|
|
2253
|
+
if (error instanceof ScheduledTaskSyncError) throw error;
|
|
2254
|
+
throw new ScheduledTaskSyncError(error, false);
|
|
716
2255
|
}
|
|
717
2256
|
}
|
|
718
2257
|
|
|
@@ -770,19 +2309,65 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
770
2309
|
db: Database;
|
|
771
2310
|
objectStorage: ObjectStorageDependency;
|
|
772
2311
|
grant: AccessGrant;
|
|
773
|
-
|
|
2312
|
+
authorization?: AccessGrantAuthorization;
|
|
2313
|
+
payload: {
|
|
2314
|
+
agentConfig: ScheduledTaskAgentConfig;
|
|
2315
|
+
runMode?: string;
|
|
2316
|
+
targetSessionId?: string | null | undefined;
|
|
2317
|
+
};
|
|
774
2318
|
workspaceId: string;
|
|
775
2319
|
toolsProvided?: boolean;
|
|
2320
|
+
/** Only an already materialized reusable chat may retain unused creation defaults. */
|
|
2321
|
+
retainedCreationConfig?: ScheduledTaskAgentConfig;
|
|
776
2322
|
}): Promise<ScheduledTaskAgentConfig> {
|
|
2323
|
+
const existingChat = input.payload.runMode === "existing_session";
|
|
2324
|
+
if (existingChat)
|
|
2325
|
+
input = {
|
|
2326
|
+
...input,
|
|
2327
|
+
payload: {
|
|
2328
|
+
...input.payload,
|
|
2329
|
+
agentConfig: scheduledSessionMessageConfig(input.payload.agentConfig),
|
|
2330
|
+
},
|
|
2331
|
+
toolsProvided: true,
|
|
2332
|
+
};
|
|
2333
|
+
const unchangedCreationField = (key: keyof ScheduledTaskAgentConfig) =>
|
|
2334
|
+
input.retainedCreationConfig !== undefined &&
|
|
2335
|
+
isDeepStrictEqual(input.payload.agentConfig[key], input.retainedCreationConfig[key]);
|
|
2336
|
+
const actor = scheduledTaskInitiatorForGrant(input.grant).actor;
|
|
2337
|
+
const parent =
|
|
2338
|
+
actor && !existingChat ? await getSession(input.db, input.workspaceId, actor.sessionId) : null;
|
|
2339
|
+
if (!existingChat && actor && (!parent || parent.accountId !== input.grant.accountId)) {
|
|
2340
|
+
throw new HTTPException(403, {
|
|
2341
|
+
message: "Scheduled Skill selection requires the creating agent's session",
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
let bundledSkillIds: ScheduledTaskAgentConfig["bundledSkillIds"];
|
|
2345
|
+
try {
|
|
2346
|
+
bundledSkillIds = existingChat
|
|
2347
|
+
? undefined
|
|
2348
|
+
: unchangedCreationField("bundledSkillIds")
|
|
2349
|
+
? input.payload.agentConfig.bundledSkillIds
|
|
2350
|
+
: resolveBundledSkillSelection(
|
|
2351
|
+
input.payload.agentConfig.bundledSkillIds,
|
|
2352
|
+
parent?.bundledSkillIds,
|
|
2353
|
+
);
|
|
2354
|
+
} catch (error) {
|
|
2355
|
+
throw new HTTPException(422, {
|
|
2356
|
+
message: error instanceof Error ? error.message : "Invalid bundled Skill selection",
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
777
2359
|
// Reject a curated-out model before touching the DB: a scheduled task is a
|
|
778
2360
|
// session the worker runs later, so it must pass the same allow-list as the
|
|
779
2361
|
// session choke points (a `scheduled_tasks:manage` holder could otherwise set
|
|
780
2362
|
// a model the host does not expose). An omitted model inherits the host
|
|
781
2363
|
// default downstream, which is always configured.
|
|
782
|
-
const model =
|
|
2364
|
+
const model = unchangedCreationField("model")
|
|
2365
|
+
? input.payload.agentConfig.model
|
|
2366
|
+
: canonicalConfiguredModel(input.settings, input.payload.agentConfig.model);
|
|
783
2367
|
// Same policy vetting as the session choke points; an omitted model flows
|
|
784
2368
|
// through session creation later, where the effective default is vetted.
|
|
785
|
-
|
|
2369
|
+
if (!unchangedCreationField("model"))
|
|
2370
|
+
await assertWorkspaceModelPolicyAllows(input.db, input.settings, input.workspaceId, model);
|
|
786
2371
|
const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
|
|
787
2372
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
788
2373
|
input.db,
|
|
@@ -790,16 +2375,38 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
790
2375
|
input.settings,
|
|
791
2376
|
{ subjectId: input.grant.subjectId },
|
|
792
2377
|
);
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
2378
|
+
const retainTools = unchangedCreationField("tools");
|
|
2379
|
+
const requestedTools = retainTools
|
|
2380
|
+
? input.payload.agentConfig.tools
|
|
2381
|
+
: validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
|
|
2382
|
+
const workspace = await requireWorkspace(input.db, input.workspaceId);
|
|
2383
|
+
if (
|
|
2384
|
+
parent?.tenancy?.visibility === "private" &&
|
|
2385
|
+
workspace.kind !== "personal" &&
|
|
2386
|
+
input.payload.runMode !== "existing_session" &&
|
|
2387
|
+
!input.payload.targetSessionId
|
|
2388
|
+
) {
|
|
2389
|
+
throw new HTTPException(422, {
|
|
2390
|
+
message:
|
|
2391
|
+
"Schedule this private chat as the existing-session target, or create the task in your Personal workspace.",
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
|
|
2395
|
+
// A task whose creator did not choose tools gets the workspace's exact
|
|
2396
|
+
// session defaults (or the deployment compatibility default), exactly like
|
|
2397
|
+
// a session created without a tools key. Scheduled runs are sessions too;
|
|
2398
|
+
|
|
2399
|
+
// kept falling into (a maintenance task that cannot reach its workspace's
|
|
2400
|
+
// notebook MCP cannot do its job).
|
|
799
2401
|
const tools =
|
|
800
|
-
(input.toolsProvided ?? true)
|
|
2402
|
+
retainTools || (input.toolsProvided ?? true)
|
|
801
2403
|
? requestedTools
|
|
802
|
-
:
|
|
2404
|
+
: withWorkspaceDefaultMcpTools(
|
|
2405
|
+
requestedTools,
|
|
2406
|
+
input.settings,
|
|
2407
|
+
runtimeSettings,
|
|
2408
|
+
workspaceSessionToolDefaults,
|
|
2409
|
+
);
|
|
803
2410
|
const prompt = input.payload.agentConfig.prompt.trim();
|
|
804
2411
|
if (!prompt) {
|
|
805
2412
|
throw new HTTPException(422, { message: "scheduled task prompt is required" });
|
|
@@ -813,7 +2420,44 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
813
2420
|
if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
|
|
814
2421
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
815
2422
|
}
|
|
816
|
-
|
|
2423
|
+
const fileActor =
|
|
2424
|
+
resources.some((resource) => resource.kind === "file") &&
|
|
2425
|
+
hasPermission(input.grant.permissions, "files:read")
|
|
2426
|
+
? input.authorization
|
|
2427
|
+
? await fileOwnerContextForAccess({ db: input.db }, input.authorization, "files:read")
|
|
2428
|
+
: input.grant.principalKind === "agent_attempt"
|
|
2429
|
+
? await fileOwnerContextForAgent({ db: input.db }, input.grant, "files:read")
|
|
2430
|
+
: { subjectId: input.grant.subjectId, privateFileOwnerSubjectId: null }
|
|
2431
|
+
: { subjectId: input.grant.subjectId, privateFileOwnerSubjectId: null };
|
|
2432
|
+
await withSessionRlsActorContext(fileActor, async () => {
|
|
2433
|
+
const target =
|
|
2434
|
+
input.payload.targetSessionId && input.payload.runMode !== "new_session_per_run"
|
|
2435
|
+
? await getSessionAuthorityProjection(
|
|
2436
|
+
input.db,
|
|
2437
|
+
input.workspaceId,
|
|
2438
|
+
input.payload.targetSessionId,
|
|
2439
|
+
)
|
|
2440
|
+
: null;
|
|
2441
|
+
const privateDestination =
|
|
2442
|
+
workspace.kind === "personal" ||
|
|
2443
|
+
(target?.visibility === "user_private" &&
|
|
2444
|
+
target.ownerSubjectId === fileActor.privateFileOwnerSubjectId) ||
|
|
2445
|
+
(target?.memoryScope === "user" &&
|
|
2446
|
+
target.scopeSubjectId === fileActor.privateFileOwnerSubjectId);
|
|
2447
|
+
await validateFileResources(
|
|
2448
|
+
input.db,
|
|
2449
|
+
input.grant.accountId,
|
|
2450
|
+
input.workspaceId,
|
|
2451
|
+
input.grant.subjectId,
|
|
2452
|
+
resources,
|
|
2453
|
+
{
|
|
2454
|
+
...fileActor,
|
|
2455
|
+
privateFileOwnerSubjectId: privateDestination
|
|
2456
|
+
? (fileActor.privateFileOwnerSubjectId ?? null)
|
|
2457
|
+
: null,
|
|
2458
|
+
},
|
|
2459
|
+
);
|
|
2460
|
+
});
|
|
817
2461
|
if (input.payload.agentConfig.slackBotConnectionId) {
|
|
818
2462
|
await validateOpenGeniSlackBotConnectionSelection(
|
|
819
2463
|
input.db,
|
|
@@ -823,8 +2467,7 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
823
2467
|
);
|
|
824
2468
|
}
|
|
825
2469
|
const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
|
|
826
|
-
if (requestedMaxDepth !== undefined) {
|
|
827
|
-
const workspace = await requireWorkspace(input.db, input.workspaceId);
|
|
2470
|
+
if (requestedMaxDepth !== undefined && !unchangedCreationField("maxNestedAgentDepth")) {
|
|
828
2471
|
const workspaceMaxDepth = workspace.settings.maxNestedAgentDepth;
|
|
829
2472
|
const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
|
|
830
2473
|
const inheritedMaxDepth =
|
|
@@ -840,13 +2483,107 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
840
2483
|
});
|
|
841
2484
|
}
|
|
842
2485
|
}
|
|
843
|
-
|
|
2486
|
+
const validated = {
|
|
844
2487
|
...input.payload.agentConfig,
|
|
2488
|
+
...(bundledSkillIds !== undefined ? { bundledSkillIds } : {}),
|
|
845
2489
|
...(model === undefined || model === null ? {} : { model }),
|
|
846
2490
|
prompt,
|
|
847
2491
|
resources,
|
|
848
2492
|
tools,
|
|
849
2493
|
};
|
|
2494
|
+
validateIncidentTelemetryPreflightSelection(input.settings, validated);
|
|
2495
|
+
return existingChat ? scheduledSessionMessageConfig(validated) : validated;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* Static incident-telemetry admission validates only already-selected task
|
|
2500
|
+
* authority. It never discovers a resource, reads a variable value, probes a
|
|
2501
|
+
* provider, or treats ambient worker credentials as responder capability.
|
|
2502
|
+
* Mutable rig/variable-set metadata is revalidated again at dispatch.
|
|
2503
|
+
*/
|
|
2504
|
+
export function validateIncidentTelemetryPreflightSelection(
|
|
2505
|
+
settings: Pick<Settings, "allowedFirstPartyMcpTools" | "defaultFirstPartyMcpTools">,
|
|
2506
|
+
agentConfig: ScheduledTaskAgentConfig,
|
|
2507
|
+
): void {
|
|
2508
|
+
const executionClass = agentConfig.executionClass;
|
|
2509
|
+
const preflight = agentConfig.incidentTelemetryPreflight;
|
|
2510
|
+
if (executionClass === undefined && preflight === undefined) return;
|
|
2511
|
+
if (executionClass !== "incident_telemetry" || !preflight) {
|
|
2512
|
+
throw new HTTPException(422, {
|
|
2513
|
+
message:
|
|
2514
|
+
"executionClass=incident_telemetry and incidentTelemetryPreflight must be configured together",
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
for (const required of preflight.requiredResources) {
|
|
2519
|
+
if (!agentConfig.resources.some((selected) => isDeepStrictEqual(selected, required))) {
|
|
2520
|
+
throw new HTTPException(422, {
|
|
2521
|
+
message: "incidentTelemetryPreflight.requiredResources must be exact selected resources",
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
const selectedMcpServerIds = new Set(agentConfig.tools.map((tool) => tool.id));
|
|
2527
|
+
// Scheduled dispatch always attaches the first-party OpenGeni MCP server.
|
|
2528
|
+
selectedMcpServerIds.add("opengeni");
|
|
2529
|
+
if (preflight.requiredMcpServerIds.some((id) => !selectedMcpServerIds.has(id))) {
|
|
2530
|
+
throw new HTTPException(422, {
|
|
2531
|
+
message: "incidentTelemetryPreflight.requiredMcpServerIds must be exact selected MCP servers",
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
const selectedFirstPartyTools = new Set(resolveFirstPartyMcpToolPolicy(settings).default);
|
|
2536
|
+
if (preflight.requiredFirstPartyMcpTools.some((tool) => !selectedFirstPartyTools.has(tool))) {
|
|
2537
|
+
throw new HTTPException(422, {
|
|
2538
|
+
message:
|
|
2539
|
+
"incidentTelemetryPreflight.requiredFirstPartyMcpTools must be present in the selected first-party tool policy",
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
const selectedFirstPartyPermissions = new Set<Permission>(DEFAULT_FIRST_PARTY_MCP_PERMISSIONS);
|
|
2544
|
+
if (
|
|
2545
|
+
(preflight.requiredFirstPartyMcpPermissions ?? []).some(
|
|
2546
|
+
(permission) => !selectedFirstPartyPermissions.has(permission),
|
|
2547
|
+
)
|
|
2548
|
+
) {
|
|
2549
|
+
throw new HTTPException(422, {
|
|
2550
|
+
message:
|
|
2551
|
+
"incidentTelemetryPreflight.requiredFirstPartyMcpPermissions must be present in the scheduled responder permission policy",
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const route = preflight.dataSource.route;
|
|
2556
|
+
if (route.kind === "mcp" && !selectedMcpServerIds.has(route.serverId)) {
|
|
2557
|
+
throw new HTTPException(422, {
|
|
2558
|
+
message: "incidentTelemetryPreflight.dataSource.route must use a selected MCP server",
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
if (route.kind === "first_party" && !selectedFirstPartyTools.has(route.tool)) {
|
|
2562
|
+
throw new HTTPException(422, {
|
|
2563
|
+
message: "incidentTelemetryPreflight.dataSource.route must use a selected first-party tool",
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
if (route.kind === "variable_set") {
|
|
2567
|
+
const declaredSets = new Set(preflight.requiredVariableSetNames);
|
|
2568
|
+
const declaredVariables = new Set(preflight.requiredVariableNames);
|
|
2569
|
+
if (
|
|
2570
|
+
!declaredSets.has(route.variableSetName) ||
|
|
2571
|
+
route.variableNames.some((name) => !declaredVariables.has(name))
|
|
2572
|
+
) {
|
|
2573
|
+
throw new HTTPException(422, {
|
|
2574
|
+
message:
|
|
2575
|
+
"incidentTelemetryPreflight.dataSource.route variable metadata must be declared as required",
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
if (route.kind === "rig_credential_hook") {
|
|
2580
|
+
if (!preflight.requiredRig?.credentialHookIds.includes(route.credentialHookId)) {
|
|
2581
|
+
throw new HTTPException(422, {
|
|
2582
|
+
message:
|
|
2583
|
+
"incidentTelemetryPreflight.dataSource.route rig hook must be declared as required",
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
850
2587
|
}
|
|
851
2588
|
|
|
852
2589
|
function validateScheduledTaskSchedule(schedule: ScheduledTask["schedule"]): void {
|