@opengeni/core 3.0.1 → 3.1.2
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 +4 -4
- package/dist/access/index.d.ts +131 -7
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +1 -1
- package/dist/application/connect-operation.d.ts +6 -0
- package/dist/application/external-workspace-members.d.ts +13 -1
- package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
- package/dist/application/modal-native-original-configuration.d.ts +79 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +29 -2
- package/dist/application/organization-privacy.d.ts +57 -0
- package/dist/application/prepared-mcp-connection.d.ts +31 -0
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +3 -0
- package/dist/application/session-tenancy.d.ts +13 -4
- package/dist/billing/agent-run-admission.d.ts +21 -0
- package/dist/billing/limits.d.ts +27 -1
- package/dist/billing/model-call-admission.d.ts +34 -0
- package/dist/billing/model-usage-settlement.d.ts +112 -0
- package/dist/canonical-human-identities.js +1 -1
- package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
- package/dist/chunk-7T2SZ7BI.js.map +1 -0
- package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
- package/dist/chunk-OMJFIIWK.js.map +1 -0
- package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
- package/dist/chunk-QI2M2B2K.js.map +1 -0
- package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
- package/dist/chunk-TTWTXQB2.js.map +1 -0
- package/dist/codex-model-availability.d.ts +55 -0
- package/dist/default-session-model.d.ts +192 -0
- package/dist/dependencies.d.ts +41 -7
- 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 +8 -1
- package/dist/domain/capabilities.d.ts +112 -8
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +62 -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 +3 -1
- package/dist/domain/editable-artifacts/types.d.ts +1 -1
- package/dist/domain/encryption-key.d.ts +12 -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 +21 -3
- package/dist/domain/knowledge-files.d.ts +1 -1
- 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 -76
- package/dist/domain/mcp-account-bindings.d.ts +17 -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/personal-connection-delegations.d.ts +73 -12
- package/dist/domain/pr-review.d.ts +121 -6
- package/dist/domain/realtime-voice-billing.d.ts +103 -0
- package/dist/domain/resources.d.ts +17 -1
- package/dist/domain/scheduled-task-access.d.ts +265 -0
- package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
- package/dist/domain/scheduled-task-audience.d.ts +70 -0
- package/dist/domain/scheduled-tasks.d.ts +61 -18
- package/dist/domain/session-connection-accounts.d.ts +53 -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 +102 -4
- package/dist/domain/sessions.d.ts +179 -15
- package/dist/domain/site-audience.d.ts +52 -0
- package/dist/domain/skills.d.ts +29 -1
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
- package/dist/domain/vaults/index.d.ts +7 -0
- package/dist/domain/vaults/mapping.d.ts +25 -0
- package/dist/domain/vaults/provider-registry.d.ts +72 -0
- package/dist/domain/vaults/provider-sync.d.ts +176 -0
- package/dist/domain/vaults/provider-writes.d.ts +57 -0
- package/dist/domain/vaults/secret-resolver.d.ts +34 -0
- package/dist/domain/vaults/types.d.ts +24 -0
- package/dist/domain/vaults/vault-service.d.ts +35 -0
- package/dist/domain/voice-input-billing.d.ts +77 -0
- package/dist/domain/web-search-billing.d.ts +51 -0
- package/dist/domain/workspace-credits.d.ts +7 -0
- package/dist/editable-artifact-live/application.d.ts +2 -1
- package/dist/editable-artifact-live/server.d.ts +5 -1
- package/dist/editable-artifact-live/ticket.d.ts +2 -1
- package/dist/editable-artifact-live/types.d.ts +9 -0
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -2
- package/dist/goal-admission.d.ts +627 -0
- package/dist/host-session-usage-limit.d.ts +15 -0
- package/dist/index.d.ts +43 -8
- package/dist/index.js +22682 -11743
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +110 -7
- package/dist/model-call.d.ts +58 -0
- package/dist/model-catalog.d.ts +29 -3
- package/dist/model-provider-credentials.d.ts +43 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +1 -13
- package/dist/sandbox/fleet.d.ts +58 -4
- package/dist/sandbox/routing.d.ts +42 -2
- package/dist/sandbox/runtime-settings.d.ts +27 -17
- package/dist/session-authorization.d.ts +10 -0
- package/dist/session-usage-limit-metrics.d.ts +60 -0
- package/dist/session-usage-limits.d.ts +89 -0
- package/dist/site-agent-clamp.d.ts +71 -0
- package/dist/site-session-origin.d.ts +39 -2
- package/dist/transcription.d.ts +120 -3
- package/dist/turn-surface.d.ts +20 -0
- package/dist/user-presence.d.ts +32 -0
- package/dist/workflow-wake-contract.d.ts +13 -1
- package/package.json +15 -22
- package/src/access/index.ts +961 -38
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +9 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +3 -2
- package/src/application/connect-authority.ts +3 -3
- package/src/application/connect-operation.ts +37 -3
- package/src/application/external-continuation.ts +20 -3
- package/src/application/external-link-work-admission.ts +3 -0
- package/src/application/external-workspace-members.ts +51 -3
- package/src/application/modal-native-live-declaration-join.ts +196 -0
- package/src/application/modal-native-original-configuration.ts +261 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +172 -55
- package/src/application/organization-privacy.ts +161 -0
- package/src/application/prepared-mcp-connection.ts +133 -0
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +73 -1
- package/src/application/session-mcp-credential-rotation.ts +61 -18
- package/src/application/session-tenancy.ts +262 -44
- package/src/application/user-resource-grants.ts +1 -29
- package/src/billing/agent-run-admission.ts +117 -0
- package/src/billing/limits.ts +138 -14
- package/src/billing/model-call-admission.ts +118 -0
- package/src/billing/model-usage-settlement.ts +472 -0
- package/src/codex-model-availability.ts +264 -0
- package/src/default-session-model.ts +692 -0
- package/src/dependencies.ts +48 -6
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +76 -2
- package/src/domain/capabilities.ts +830 -181
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +572 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +44 -6
- 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 +20 -0
- package/src/domain/editable-artifacts/types.ts +1 -1
- package/src/domain/encryption-key.ts +26 -0
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/github-action-policies.ts +1 -1
- package/src/domain/github-skill-source.ts +3 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +167 -43
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -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 +617 -5
- package/src/domain/knowledge-search.ts +240 -23
- package/src/domain/mcp-account-bindings.ts +169 -0
- package/src/domain/mcp-account-routes.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/personal-connection-delegations.ts +399 -257
- package/src/domain/pr-review.ts +24 -8
- package/src/domain/realtime-voice-billing.ts +425 -0
- package/src/domain/resources.ts +50 -4
- package/src/domain/scheduled-task-access.ts +1089 -0
- package/src/domain/scheduled-task-account-audit.ts +90 -0
- package/src/domain/scheduled-task-audience.ts +249 -0
- package/src/domain/scheduled-tasks.ts +1313 -374
- package/src/domain/session-connection-accounts.ts +239 -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 +60 -0
- package/src/domain/session-tool-policy.ts +489 -14
- package/src/domain/sessions.ts +2254 -717
- package/src/domain/site-audience.ts +130 -0
- package/src/domain/skills.ts +9 -0
- package/src/domain/slack-bot.ts +141 -4
- package/src/domain/slack-publication-secret-safety.ts +1 -1
- package/src/domain/vaults/index.ts +19 -0
- package/src/domain/vaults/mapping.ts +190 -0
- package/src/domain/vaults/provider-registry.ts +201 -0
- package/src/domain/vaults/provider-sync.ts +531 -0
- package/src/domain/vaults/provider-writes.ts +288 -0
- package/src/domain/vaults/secret-resolver.ts +464 -0
- package/src/domain/vaults/types.ts +26 -0
- package/src/domain/vaults/vault-service.ts +1316 -0
- package/src/domain/voice-input-billing.ts +330 -0
- package/src/domain/web-search-billing.ts +169 -0
- package/src/domain/workspace-credits.ts +13 -0
- package/src/editable-artifact-live/application.ts +8 -1
- package/src/editable-artifact-live/server.ts +154 -6
- package/src/editable-artifact-live/ticket.ts +86 -1
- package/src/editable-artifact-live/types.ts +10 -0
- package/src/goal-admission.ts +220 -0
- package/src/host-session-usage-limit.ts +25 -0
- package/src/index.ts +75 -11
- package/src/managed-session.ts +137 -6
- package/src/model-call.ts +79 -0
- package/src/model-catalog.ts +345 -91
- package/src/model-provider-credentials.ts +394 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +23 -111
- package/src/sandbox/fleet.ts +253 -28
- package/src/sandbox/routing.ts +204 -43
- package/src/sandbox/runtime-settings.ts +159 -85
- package/src/session-authorization.ts +172 -23
- package/src/session-usage-limit-metrics.ts +95 -0
- package/src/session-usage-limits.ts +415 -0
- package/src/site-agent-clamp.ts +226 -0
- package/src/site-session-origin.ts +84 -5
- package/src/transcription.ts +123 -4
- package/src/turn-surface.ts +47 -0
- package/src/user-presence.ts +134 -0
- package/src/workflow-wake-contract.ts +14 -1
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/chunk-OF65T3PM.js.map +0 -1
- package/dist/chunk-VCZFFEPB.js.map +0 -1
- package/dist/chunk-ZYCNLYGH.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/environments.d.ts +0 -33
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/environments.ts +0 -120
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- package/src/remote-mcp-credentials.ts +0 -361
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
import {
|
|
2
|
+
getSessionTurnClaudeProviderAccountAuthoritySnapshot,
|
|
3
|
+
getScheduledTaskClaudeProviderAccountAuthoritySnapshot,
|
|
4
|
+
} from "@opengeni/db";
|
|
5
|
+
import {
|
|
6
|
+
SCHEDULED_SLACK_BOT_POSTING_TOOLS,
|
|
2
7
|
scheduledTaskKnowledgeSource,
|
|
3
8
|
requireScheduledTaskKnowledgeSource,
|
|
9
|
+
scheduledOccurrencePayloadUtf8Bytes,
|
|
10
|
+
mergeResourceRefs,
|
|
11
|
+
SCHEDULED_TASK_OCCURRENCE_PAYLOAD_MAX_BYTES,
|
|
12
|
+
SCHEDULED_TASK_OCCURRENCE_PAYLOAD_INGRESS_HEADROOM_BYTES,
|
|
4
13
|
} from "@opengeni/contracts";
|
|
5
14
|
import {
|
|
6
15
|
allowedFirstPartyMcpToolsForSession,
|
|
16
|
+
resolveSessionFirstPartyMcpTools,
|
|
7
17
|
resolveFirstPartyMcpToolPolicy,
|
|
18
|
+
type FirstPartyMcpToolPolicySettings,
|
|
8
19
|
type Settings,
|
|
9
20
|
} from "@opengeni/config";
|
|
21
|
+
import { WORKSPACE_XAI_PROVIDER_ACCOUNT_AUTHORITY_SNAPSHOT_V1 } from "@opengeni/contracts";
|
|
22
|
+
import type { VaultPolicy } from "@opengeni/contracts";
|
|
23
|
+
import {
|
|
24
|
+
ATLASSIAN_NATIVE_RETIRED_MESSAGE,
|
|
25
|
+
isRetiredNativeAtlassianSource,
|
|
26
|
+
isRetiredNativeAtlassianTask,
|
|
27
|
+
} from "@opengeni/contracts/atlassian-native-retirement";
|
|
10
28
|
import type {
|
|
11
29
|
AccessGrant,
|
|
12
|
-
McpPersonalConnectionDelegation,
|
|
13
30
|
KnowledgeSourceSyncAction,
|
|
31
|
+
McpConnectionAccountSelection,
|
|
14
32
|
Permission,
|
|
15
33
|
ScheduledTask,
|
|
16
34
|
ScheduledTaskAgentConfig,
|
|
17
35
|
Session,
|
|
36
|
+
ToolRef,
|
|
18
37
|
SessionAuthorizationPort,
|
|
19
38
|
SessionAuthorizationSurface,
|
|
20
39
|
CreateScheduledTaskRequest as CreateScheduledTaskPayload,
|
|
21
40
|
UpdateScheduledTaskRequest as UpdateScheduledTaskPayload,
|
|
22
41
|
XaiProviderAccountAuthoritySnapshotV1,
|
|
42
|
+
ClaudeProviderAccountAuthoritySnapshotV1,
|
|
23
43
|
} from "@opengeni/contracts";
|
|
24
44
|
import {
|
|
25
45
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
@@ -29,9 +49,11 @@ import {
|
|
|
29
49
|
SessionAgentAccess,
|
|
30
50
|
SessionScopeSubjectId,
|
|
31
51
|
SessionMemoryScope,
|
|
52
|
+
readTurnExecutionPolicyV1,
|
|
32
53
|
} from "@opengeni/contracts";
|
|
33
54
|
import {
|
|
34
55
|
createScheduledTask,
|
|
56
|
+
scheduledTaskMutationOwnerMatches,
|
|
35
57
|
getAgentLearningSettings,
|
|
36
58
|
saveAgentLearningSettings,
|
|
37
59
|
type KnowledgeContext,
|
|
@@ -45,33 +67,49 @@ import {
|
|
|
45
67
|
getRig,
|
|
46
68
|
getScheduledTask,
|
|
47
69
|
getScheduledTaskIncludingDeletedForUpdate,
|
|
48
|
-
getScheduledTaskPersonalConnectionDelegations,
|
|
49
70
|
getScheduledTaskXaiProviderAccountAuthoritySnapshot,
|
|
50
71
|
getSandbox,
|
|
51
72
|
getSessionTurnXaiProviderAccountAuthoritySnapshot,
|
|
52
73
|
getSession,
|
|
74
|
+
getSessionTurnForAttempt,
|
|
53
75
|
getSessionAuthorityProjection,
|
|
76
|
+
getWorkspaceDefaultRigId,
|
|
54
77
|
withSessionRlsActorContext,
|
|
55
78
|
nestedPostgresSqlState,
|
|
56
79
|
requireWorkspace,
|
|
57
80
|
scopedKnowledgeScopeKey,
|
|
58
81
|
updateScheduledTask,
|
|
82
|
+
ScheduledTaskHeadChangedError,
|
|
59
83
|
withWorkspaceSubjectRls,
|
|
60
|
-
resolveXaiProviderAccountAuthoritySnapshotForAcceptance,
|
|
61
84
|
type Database,
|
|
62
85
|
type ScheduledTaskCreatorPolicy,
|
|
86
|
+
type SessionCommandActor,
|
|
63
87
|
type TemporalScheduleCleanupClaim,
|
|
64
88
|
type UpdateScheduledTaskInput,
|
|
65
89
|
} from "@opengeni/db";
|
|
66
90
|
import { HTTPException } from "hono/http-exception";
|
|
91
|
+
import { GOOGLE_DRIVE_PUBLICATION_SERVER_ID } from "@opengeni/contracts/google-drive";
|
|
92
|
+
import { PERSONAL_GITHUB_CONNECTION_SURFACE_ID } from "@opengeni/contracts/personal-github";
|
|
93
|
+
import {
|
|
94
|
+
currentSiteSessionOrigin,
|
|
95
|
+
SITE_ORIGIN_METADATA_KEY,
|
|
96
|
+
siteOriginsMatch,
|
|
97
|
+
siteSessionOriginFromMetadata,
|
|
98
|
+
withSiteOriginMetadata,
|
|
99
|
+
} from "../site-session-origin";
|
|
100
|
+
import { assertNotSiteClampedAgent } from "../site-agent-clamp";
|
|
101
|
+
import { personalGitHubRepositoryResources } from "./resources";
|
|
67
102
|
import { knowledgeContextForAccess } from "./knowledge";
|
|
68
103
|
import { fileOwnerContextForAccess, fileOwnerContextForAgent } from "./file-owner";
|
|
69
104
|
import { isDeepStrictEqual } from "node:util";
|
|
70
|
-
import { hasPermission, requirePermission, type AccessGrantAuthorization } from "../access";
|
|
71
105
|
import {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
hasPermission,
|
|
107
|
+
requireExplicitPermissionDelegation,
|
|
108
|
+
isDeveloperSetupAuthorization,
|
|
109
|
+
isDeveloperSetupGrant,
|
|
110
|
+
requirePermission,
|
|
111
|
+
type AccessGrantAuthorization,
|
|
112
|
+
} from "../access";
|
|
75
113
|
import {
|
|
76
114
|
requireSessionAuthorization,
|
|
77
115
|
SessionAuthorizationDeniedError,
|
|
@@ -79,32 +117,46 @@ import {
|
|
|
79
117
|
} from "../session-authorization";
|
|
80
118
|
import type { SessionWorkflowClient } from "../dependencies";
|
|
81
119
|
import type { ObjectStorageDependency } from "../dependencies";
|
|
120
|
+
import {
|
|
121
|
+
resolveScheduledTaskVisibility,
|
|
122
|
+
scheduledTaskViewerForGrant,
|
|
123
|
+
} from "./scheduled-task-audience";
|
|
82
124
|
import { lockActiveCustomModelForAdmission, workspaceCustomModelReference } from "../model-catalog";
|
|
83
125
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
126
|
+
import {
|
|
127
|
+
resolveSessionToolPolicy,
|
|
128
|
+
workspaceSessionToolPolicyDefaultServerIds,
|
|
129
|
+
} from "./session-tool-policy";
|
|
84
130
|
import { prepareExternalLinkTaskAdmission } from "../application/external-link-work-admission";
|
|
85
|
-
import {
|
|
131
|
+
import { variableSetsRemovedError } from "./encryption-key";
|
|
86
132
|
import {
|
|
87
|
-
|
|
133
|
+
freezeConnectionAccounts,
|
|
88
134
|
personalConnectionDelegationSourceForGrant,
|
|
89
135
|
} from "./personal-connection-delegations";
|
|
90
136
|
import {
|
|
91
137
|
assertWorkspaceModelPolicyAllows,
|
|
92
138
|
canonicalConfiguredModel,
|
|
93
139
|
creationInitiatorForGrant,
|
|
140
|
+
modelUnavailableHttpException,
|
|
141
|
+
settingsWithSessionMcpServerMetadata,
|
|
94
142
|
} from "./sessions";
|
|
95
143
|
import {
|
|
96
144
|
hasReservedOpenGeniSlackBotSessionMetadata,
|
|
97
|
-
scheduledSlackBotConnectionId,
|
|
98
145
|
validateOpenGeniSlackBotConnectionSelection,
|
|
146
|
+
validateScheduledTaskSlackChannel,
|
|
147
|
+
type ScheduledTaskSlackChannelVerifier,
|
|
99
148
|
} from "./slack-bot";
|
|
100
149
|
import {
|
|
101
150
|
normalizeResources,
|
|
102
|
-
personalGitHubRepositoryResources,
|
|
103
151
|
validateFileResources,
|
|
104
152
|
validateGitHubRepositorySelection,
|
|
105
153
|
validateToolRefs,
|
|
106
|
-
|
|
154
|
+
scheduledTaskRunTools,
|
|
107
155
|
} from "./resources";
|
|
156
|
+
import {
|
|
157
|
+
recordScheduledTaskAccountChoiceChanges,
|
|
158
|
+
scheduledTaskAccountChoiceChanges,
|
|
159
|
+
} from "./scheduled-task-account-audit";
|
|
108
160
|
|
|
109
161
|
/**
|
|
110
162
|
* Whether a raw scheduled-task payload explicitly set agentConfig.tools.
|
|
@@ -114,6 +166,14 @@ import {
|
|
|
114
166
|
* (enabled capability MCP servers)", an explicit list (even empty) is taken
|
|
115
167
|
* verbatim.
|
|
116
168
|
*/
|
|
169
|
+
|
|
170
|
+
/** Vault IDs are compared as lowercase text in SQL. */
|
|
171
|
+
function scheduledVaultPolicy(policy: VaultPolicy): VaultPolicy {
|
|
172
|
+
return policy.mode === "all"
|
|
173
|
+
? { mode: "all" }
|
|
174
|
+
: { mode: "selected", vaultIds: [...new Set(policy.vaultIds.map((id) => id.toLowerCase()))] };
|
|
175
|
+
}
|
|
176
|
+
|
|
117
177
|
export function scheduledTaskToolsProvided(rawPayload: unknown): boolean {
|
|
118
178
|
if (!rawPayload || typeof rawPayload !== "object") {
|
|
119
179
|
return false;
|
|
@@ -126,11 +186,31 @@ export function scheduledTaskToolsProvided(rawPayload: unknown): boolean {
|
|
|
126
186
|
);
|
|
127
187
|
}
|
|
128
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Only the message survives. The destination chat owns execution: its
|
|
191
|
+
* connectors, and the owner's current accounts resolved at each run. An
|
|
192
|
+
* existing-chat schedule never saves account choices of its own.
|
|
193
|
+
*/
|
|
194
|
+
function scheduledSessionMessageConfig(config: ScheduledTaskAgentConfig): ScheduledTaskAgentConfig {
|
|
195
|
+
return {
|
|
196
|
+
prompt: config.prompt,
|
|
197
|
+
resources: config.resources,
|
|
198
|
+
tools: [],
|
|
199
|
+
metadata: config.metadata,
|
|
200
|
+
...(config.approvalTimeoutSeconds !== undefined
|
|
201
|
+
? { approvalTimeoutSeconds: config.approvalTimeoutSeconds }
|
|
202
|
+
: {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
129
206
|
function workspaceCustomModelCommitGuard(input: {
|
|
130
207
|
settings: Settings;
|
|
131
208
|
accountId: string;
|
|
132
209
|
workspaceId: string;
|
|
133
210
|
modelId: string;
|
|
211
|
+
claudeAuthoritySnapshot?: ClaudeProviderAccountAuthoritySnapshotV1;
|
|
212
|
+
/** The task an update re-admits (Claude's core route reads its accepted authority). */
|
|
213
|
+
scheduledTaskId?: string;
|
|
134
214
|
}): ((tx: Database) => Promise<void>) | undefined {
|
|
135
215
|
const reference = workspaceCustomModelReference(input.settings, input.modelId);
|
|
136
216
|
if (!reference) return undefined;
|
|
@@ -139,35 +219,142 @@ function workspaceCustomModelCommitGuard(input: {
|
|
|
139
219
|
accountId: input.accountId,
|
|
140
220
|
workspaceId: input.workspaceId,
|
|
141
221
|
reference,
|
|
222
|
+
claudeAuthority: input.claudeAuthoritySnapshot
|
|
223
|
+
? {
|
|
224
|
+
authoritySnapshot: input.claudeAuthoritySnapshot,
|
|
225
|
+
...(input.scheduledTaskId ? { scheduledTaskId: input.scheduledTaskId } : {}),
|
|
226
|
+
}
|
|
227
|
+
: null,
|
|
142
228
|
});
|
|
143
229
|
if (!active) {
|
|
144
|
-
throw
|
|
145
|
-
message: `model is not available: ${input.modelId}`,
|
|
146
|
-
});
|
|
230
|
+
throw modelUnavailableHttpException(input.modelId);
|
|
147
231
|
}
|
|
148
232
|
};
|
|
149
233
|
}
|
|
150
234
|
|
|
235
|
+
/** Resolve the target's current tool policy before choosing its owner's accounts. */
|
|
236
|
+
export async function scheduledConnectionTools(
|
|
237
|
+
db: Database,
|
|
238
|
+
workspaceId: string,
|
|
239
|
+
settings: Settings,
|
|
240
|
+
target: Session | null,
|
|
241
|
+
taskTools: ToolRef[],
|
|
242
|
+
subjectId?: string,
|
|
243
|
+
): Promise<ToolRef[]> {
|
|
244
|
+
if (!target) return [...taskTools, { kind: "mcp", id: "opengeni" }];
|
|
245
|
+
return resolveSessionToolPolicy({
|
|
246
|
+
toolPolicy: target.toolPolicy,
|
|
247
|
+
sessionTools: target.tools,
|
|
248
|
+
availableMcpServerIds: [
|
|
249
|
+
"opengeni",
|
|
250
|
+
...settings.mcpServers.map(({ id }) => id),
|
|
251
|
+
...target.mcpServers.map(({ id }) => id),
|
|
252
|
+
],
|
|
253
|
+
defaultMcpServerIds: await workspaceSessionToolPolicyDefaultServerIds(
|
|
254
|
+
db,
|
|
255
|
+
workspaceId,
|
|
256
|
+
settings,
|
|
257
|
+
subjectId,
|
|
258
|
+
),
|
|
259
|
+
}).toolRefs;
|
|
260
|
+
}
|
|
261
|
+
|
|
151
262
|
export function scheduledConnectionSurfaceEligibility(
|
|
152
263
|
settings: Settings,
|
|
153
|
-
target:
|
|
264
|
+
target:
|
|
265
|
+
| (Pick<Session, "firstPartyMcpTools" | "firstPartyMcpPermissions"> &
|
|
266
|
+
Partial<Pick<Session, "toolPolicy" | "agent">>)
|
|
267
|
+
| null,
|
|
268
|
+
workspaceSettings: unknown = {},
|
|
154
269
|
): { googleDrivePublicationEnabled: boolean; atlassianEnabled: boolean } {
|
|
155
|
-
const tools = target?.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
270
|
+
const tools = target?.toolPolicy
|
|
271
|
+
? resolveSessionFirstPartyMcpTools(
|
|
272
|
+
settings,
|
|
273
|
+
{ ...target, toolPolicy: target.toolPolicy },
|
|
274
|
+
workspaceSettings,
|
|
275
|
+
)
|
|
276
|
+
: allowedFirstPartyMcpToolsForSession(settings, target?.firstPartyMcpTools);
|
|
277
|
+
const permissions = target?.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS;
|
|
159
278
|
return {
|
|
160
279
|
googleDrivePublicationEnabled:
|
|
161
280
|
tools.includes("editable_artifact_export") &&
|
|
162
281
|
tools.includes("editable_artifact_export_status") &&
|
|
163
282
|
permissions.includes("artifacts:read") &&
|
|
164
283
|
permissions.includes("artifacts:publish"),
|
|
165
|
-
atlassianEnabled:
|
|
166
|
-
tools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
167
|
-
permissions.includes("connections:read"),
|
|
284
|
+
atlassianEnabled: false,
|
|
168
285
|
};
|
|
169
286
|
}
|
|
170
287
|
|
|
288
|
+
/**
|
|
289
|
+
* An organization/workspace API key or the deployment's configured key is a
|
|
290
|
+
* machine principal. It is never a person, so it can never be a schedule's
|
|
291
|
+
* execution owner (a human revision authorizer): its schedules are ownerless
|
|
292
|
+
* and run under service authority, exactly like a delegated service's.
|
|
293
|
+
*/
|
|
294
|
+
function isScheduledTaskMachinePrincipal(grant: AccessGrant): boolean {
|
|
295
|
+
return grant.principalKind === "api_key" || grant.principalKind === "configured_key";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Only a person pins connector accounts: an authenticated human acting
|
|
300
|
+
* directly, through the schedule editor or their own API call. Agents, API
|
|
301
|
+
* keys, services and delegated bearers never pin; schedules they save follow
|
|
302
|
+
* the owner's current accounts at each run.
|
|
303
|
+
*/
|
|
304
|
+
function requireScheduledTaskPinAuthor(grant: AccessGrant): void {
|
|
305
|
+
if (
|
|
306
|
+
isScheduledTaskMachinePrincipal(grant) ||
|
|
307
|
+
personalConnectionDelegationSourceForGrant(grant).kind !== "subject"
|
|
308
|
+
) {
|
|
309
|
+
throw new HTTPException(403, {
|
|
310
|
+
message:
|
|
311
|
+
"Only a person can pin connector accounts on a schedule. Schedules saved by agents, API keys or services use the owner's current accounts.",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Stable, duplicate-free pin order so equal choices store identically. */
|
|
317
|
+
function normalizedScheduledTaskPins(
|
|
318
|
+
pins: readonly McpConnectionAccountSelection[],
|
|
319
|
+
): McpConnectionAccountSelection[] {
|
|
320
|
+
const unique = new Map(
|
|
321
|
+
pins.map((pin) => [`${pin.serverId}\u0000${pin.connectionId}`, pin] as const),
|
|
322
|
+
);
|
|
323
|
+
return [...unique.entries()]
|
|
324
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
325
|
+
.map(([, pin]) => ({ serverId: pin.serverId, connectionId: pin.connectionId }));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Store pins and the do-not-run switch only when they say something. */
|
|
329
|
+
function withScheduledTaskPins(
|
|
330
|
+
config: ScheduledTaskAgentConfig,
|
|
331
|
+
pins: readonly McpConnectionAccountSelection[] | undefined,
|
|
332
|
+
required: boolean,
|
|
333
|
+
): ScheduledTaskAgentConfig {
|
|
334
|
+
const {
|
|
335
|
+
pinnedAccounts: _pinnedAccounts,
|
|
336
|
+
pinnedAccountsRequired: _pinnedAccountsRequired,
|
|
337
|
+
...rest
|
|
338
|
+
} = config;
|
|
339
|
+
const normalized = normalizedScheduledTaskPins(pins ?? []);
|
|
340
|
+
return normalized.length > 0
|
|
341
|
+
? { ...rest, pinnedAccounts: normalized, ...(required ? { pinnedAccountsRequired: true } : {}) }
|
|
342
|
+
: rest;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Service attribution cannot become a personal schedule execution owner. */
|
|
346
|
+
function scheduledTaskInitiatorForGrant(grant: AccessGrant) {
|
|
347
|
+
const creator = creationInitiatorForGrant(grant);
|
|
348
|
+
if (
|
|
349
|
+
creator.initiator?.kind === "subject" &&
|
|
350
|
+
(personalConnectionDelegationSourceForGrant(grant).kind === "none" ||
|
|
351
|
+
isScheduledTaskMachinePrincipal(grant))
|
|
352
|
+
) {
|
|
353
|
+
return { ...creator, initiator: { ...creator.initiator, kind: "service" as const } };
|
|
354
|
+
}
|
|
355
|
+
return creator;
|
|
356
|
+
}
|
|
357
|
+
|
|
171
358
|
export async function createValidatedScheduledTask(input: {
|
|
172
359
|
settings: Settings;
|
|
173
360
|
db: Database;
|
|
@@ -179,12 +366,43 @@ export async function createValidatedScheduledTask(input: {
|
|
|
179
366
|
// scheduledTaskToolsProvided). Absent tools get the workspace's enabled
|
|
180
367
|
// capability MCP servers, mirroring session creation.
|
|
181
368
|
toolsProvided?: boolean;
|
|
182
|
-
// Set for pack-installation-inherited attachments that were already
|
|
183
|
-
// authorized with variable-sets:use when the pack was enabled.
|
|
184
|
-
variableSetPreauthorized?: boolean;
|
|
185
369
|
sessionAuthorization?: SessionAuthorizationPort | null | undefined;
|
|
186
370
|
authorizationSurface?: SessionAuthorizationSurface | undefined;
|
|
371
|
+
/** Proves the bot may post in a newly chosen task Slack channel. */
|
|
372
|
+
verifySlackChannel?: ScheduledTaskSlackChannelVerifier | undefined;
|
|
187
373
|
}): Promise<ScheduledTask> {
|
|
374
|
+
// An agent started from a Site page acts with the page's reach: no schedules.
|
|
375
|
+
await assertNotSiteClampedAgent(input.db, input.grant, "schedules");
|
|
376
|
+
// Variable Sets were replaced by vaults (RM-1).
|
|
377
|
+
if (input.payload.variableSetId) throw variableSetsRemovedError("variableSetId");
|
|
378
|
+
if (input.payload.runMode === "existing_session") {
|
|
379
|
+
const target = await validateScheduledTaskTarget({
|
|
380
|
+
...input,
|
|
381
|
+
targetSessionId: input.payload.targetSessionId,
|
|
382
|
+
runMode: "existing_session",
|
|
383
|
+
variableSetId: input.payload.variableSetId,
|
|
384
|
+
rigId: input.payload.rigId,
|
|
385
|
+
agentConfig: input.payload.agentConfig,
|
|
386
|
+
});
|
|
387
|
+
if (input.payload.agentConfig.knowledgeSource)
|
|
388
|
+
throw new HTTPException(422, {
|
|
389
|
+
message: "Source ingestion requires a separate scheduled agent",
|
|
390
|
+
});
|
|
391
|
+
if (input.payload.rigId && input.payload.rigId !== target?.rigId)
|
|
392
|
+
throw new HTTPException(422, {
|
|
393
|
+
message:
|
|
394
|
+
"Existing-chat schedules use the chat's environment. Change the chat's attachments separately.",
|
|
395
|
+
});
|
|
396
|
+
input = {
|
|
397
|
+
...input,
|
|
398
|
+
payload: {
|
|
399
|
+
...input.payload,
|
|
400
|
+
agentConfig: scheduledSessionMessageConfig(input.payload.agentConfig),
|
|
401
|
+
variableSetId: null,
|
|
402
|
+
rigId: null,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
188
406
|
const learning = "agentLearning" in input.payload ? input.payload.agentLearning : undefined;
|
|
189
407
|
const learningContext =
|
|
190
408
|
learning && input.authorization
|
|
@@ -201,13 +419,8 @@ export async function createValidatedScheduledTask(input: {
|
|
|
201
419
|
}
|
|
202
420
|
if (learningContext?.actor.kind === "human" && learning)
|
|
203
421
|
learningContext.actor.settingsScopes = [learning.scope];
|
|
204
|
-
//
|
|
205
|
-
// callers can still invoke the shared validator with the pre-action shape.
|
|
422
|
+
// Internal callers can omit the action; agent turns are the default.
|
|
206
423
|
const action = input.payload.action ?? ({ kind: "agent_turn" } as const);
|
|
207
|
-
const hostSelections =
|
|
208
|
-
"selectedHostMcpDelegations" in input.payload
|
|
209
|
-
? input.payload.selectedHostMcpDelegations
|
|
210
|
-
: undefined;
|
|
211
424
|
const knowledgeAction = input.payload.agentConfig.knowledgeSource ?? null;
|
|
212
425
|
if (knowledgeAction) {
|
|
213
426
|
await validateKnowledgeSourceSyncAction({
|
|
@@ -216,10 +429,45 @@ export async function createValidatedScheduledTask(input: {
|
|
|
216
429
|
action: knowledgeAction,
|
|
217
430
|
});
|
|
218
431
|
}
|
|
219
|
-
const
|
|
432
|
+
const validatedAgentConfig: ScheduledTaskAgentConfig = await validateScheduledTaskAgentConfig({
|
|
220
433
|
...input,
|
|
221
434
|
workspaceId: input.grant.workspaceId,
|
|
222
435
|
});
|
|
436
|
+
// A schedule created from a Site page records which Site. Each run's
|
|
437
|
+
// session copies this metadata, so runs record it too and act with the
|
|
438
|
+
// page's reach (the Site agent clamp). Callers never supply it themselves.
|
|
439
|
+
const agentConfig: ScheduledTaskAgentConfig = {
|
|
440
|
+
...validatedAgentConfig,
|
|
441
|
+
metadata: withSiteOriginMetadata(
|
|
442
|
+
validatedAgentConfig.metadata ?? {},
|
|
443
|
+
currentSiteSessionOrigin(),
|
|
444
|
+
),
|
|
445
|
+
};
|
|
446
|
+
// Connector accounts are never saved from what the schedule would resolve
|
|
447
|
+
// now. Only accounts a person explicitly pins are stored, and only on a
|
|
448
|
+
// schedule that starts its own session; everything else follows the owner's
|
|
449
|
+
// current accounts at each run.
|
|
450
|
+
const requestedPins =
|
|
451
|
+
"pinnedAccounts" in input.payload ? input.payload.pinnedAccounts : undefined;
|
|
452
|
+
const requestedPinsRequired =
|
|
453
|
+
"pinnedAccountsRequired" in input.payload && input.payload.pinnedAccountsRequired === true;
|
|
454
|
+
if ((requestedPins?.length ?? 0) > 0 || requestedPinsRequired) {
|
|
455
|
+
if (input.payload.runMode === "existing_session" || knowledgeAction)
|
|
456
|
+
throw new HTTPException(422, {
|
|
457
|
+
message:
|
|
458
|
+
"Pin connector accounts only on a schedule that starts its own session. An existing-chat schedule uses that chat's connectors with the owner's current accounts.",
|
|
459
|
+
});
|
|
460
|
+
requireScheduledTaskPinAuthor(input.grant);
|
|
461
|
+
}
|
|
462
|
+
await validateScheduledTaskSlackChannel({
|
|
463
|
+
grant: input.grant,
|
|
464
|
+
authorization: input.authorization,
|
|
465
|
+
previous: null,
|
|
466
|
+
next: agentConfig,
|
|
467
|
+
runMode: input.payload.runMode,
|
|
468
|
+
reusableSessionCanPost: null,
|
|
469
|
+
verifySlackChannel: input.verifySlackChannel,
|
|
470
|
+
});
|
|
223
471
|
if (knowledgeAction && input.payload.overlapPolicy === "allow_concurrent")
|
|
224
472
|
throw new HTTPException(422, { message: "Source tasks require skip or buffer_one overlap" });
|
|
225
473
|
const id = crypto.randomUUID();
|
|
@@ -233,6 +481,7 @@ export async function createValidatedScheduledTask(input: {
|
|
|
233
481
|
runMode: input.payload.runMode,
|
|
234
482
|
variableSetId: input.payload.variableSetId,
|
|
235
483
|
rigId: input.payload.rigId,
|
|
484
|
+
// An omitted Sandbox Environment adopts the target session's own one below.
|
|
236
485
|
agentConfig,
|
|
237
486
|
});
|
|
238
487
|
if (
|
|
@@ -263,17 +512,12 @@ export async function createValidatedScheduledTask(input: {
|
|
|
263
512
|
grant: input.grant,
|
|
264
513
|
runMode: input.payload.runMode,
|
|
265
514
|
agentConfig,
|
|
515
|
+
audience:
|
|
516
|
+
"visibility" in input.payload && input.payload.visibility === "private"
|
|
517
|
+
? "user_private"
|
|
518
|
+
: "workspace_shared",
|
|
266
519
|
});
|
|
267
520
|
}
|
|
268
|
-
if (input.payload.variableSetId) {
|
|
269
|
-
await validateVariableSetAttachment(
|
|
270
|
-
{ settings: input.settings, db: input.db },
|
|
271
|
-
input.grant,
|
|
272
|
-
input.grant.workspaceId,
|
|
273
|
-
input.payload.variableSetId,
|
|
274
|
-
{ preauthorized: input.variableSetPreauthorized ?? false },
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
521
|
// The rig is stored on the task and resolved to its ACTIVE version per fire
|
|
278
522
|
// (at dispatch), so validate only that the id names a rig in the workspace —
|
|
279
523
|
// NOT that it has an active version now (that is a fire-time concern). RLS
|
|
@@ -289,6 +533,17 @@ export async function createValidatedScheduledTask(input: {
|
|
|
289
533
|
input.payload.rigId,
|
|
290
534
|
);
|
|
291
535
|
}
|
|
536
|
+
const rigId = knowledgeAction
|
|
537
|
+
? (input.payload.rigId ?? null)
|
|
538
|
+
: await resolveScheduledTaskCreateRigId({
|
|
539
|
+
settings: input.settings,
|
|
540
|
+
db: input.db,
|
|
541
|
+
grant: input.grant,
|
|
542
|
+
requestedRigId: input.payload.rigId,
|
|
543
|
+
runMode: input.payload.runMode,
|
|
544
|
+
target,
|
|
545
|
+
agentConfig,
|
|
546
|
+
});
|
|
292
547
|
const runtimeSettings = knowledgeAction
|
|
293
548
|
? null
|
|
294
549
|
: await settingsWithEnabledCapabilityMcpServers(
|
|
@@ -297,33 +552,63 @@ export async function createValidatedScheduledTask(input: {
|
|
|
297
552
|
input.settings,
|
|
298
553
|
{ subjectId: input.grant.subjectId },
|
|
299
554
|
);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
555
|
+
// Existing-session runs use the target's persisted MCP configuration, with
|
|
556
|
+
// session definitions taking precedence over deployment servers of the same
|
|
557
|
+
// ID. Metadata selects the destination; captured grants still supply authority.
|
|
558
|
+
const effectiveRuntimeSettings =
|
|
559
|
+
runtimeSettings && target
|
|
560
|
+
? settingsWithSessionMcpServerMetadata(runtimeSettings, target.mcpServers)
|
|
561
|
+
: runtimeSettings;
|
|
562
|
+
// Prove the creator may use the schedule's connectors and repositories, and
|
|
563
|
+
// that every pinned account resolves for the owner. Nothing it resolves is
|
|
564
|
+
// saved: each run resolves the owner's current accounts again, and a
|
|
565
|
+
// schedule that follows the workspace defaults resolves them again too.
|
|
566
|
+
if (!knowledgeAction && effectiveRuntimeSettings) {
|
|
567
|
+
const creationWorkspaceSettings = (await requireWorkspace(input.db, input.grant.workspaceId))
|
|
568
|
+
.settings;
|
|
569
|
+
await freezeConnectionAccounts({
|
|
570
|
+
db: input.db,
|
|
571
|
+
accountId: input.grant.accountId,
|
|
572
|
+
workspaceId: input.grant.workspaceId,
|
|
573
|
+
settings: effectiveRuntimeSettings,
|
|
574
|
+
tools: await scheduledConnectionTools(
|
|
575
|
+
input.db,
|
|
576
|
+
input.grant.workspaceId,
|
|
577
|
+
effectiveRuntimeSettings,
|
|
578
|
+
target,
|
|
579
|
+
scheduledTaskRunTools({
|
|
580
|
+
agentConfig,
|
|
581
|
+
settings: input.settings,
|
|
582
|
+
runtimeSettings: effectiveRuntimeSettings,
|
|
583
|
+
workspaceDefaults: resolveWorkspaceSessionToolDefaults(creationWorkspaceSettings),
|
|
584
|
+
}),
|
|
585
|
+
input.grant.subjectId,
|
|
586
|
+
),
|
|
587
|
+
resources: mergeResourceRefs(target?.resources ?? [], agentConfig.resources),
|
|
588
|
+
source: personalConnectionDelegationSourceForGrant(input.grant),
|
|
589
|
+
authoritySelections: requestedPins ?? [],
|
|
590
|
+
...scheduledConnectionSurfaceEligibility(
|
|
591
|
+
effectiveRuntimeSettings,
|
|
592
|
+
target,
|
|
593
|
+
creationWorkspaceSettings,
|
|
594
|
+
),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
const storedAgentConfig = knowledgeAction
|
|
598
|
+
? agentConfig
|
|
599
|
+
: withScheduledTaskPins(agentConfig, requestedPins, requestedPinsRequired);
|
|
600
|
+
const creationInitiator = scheduledTaskInitiatorForGrant(input.grant);
|
|
315
601
|
const captureLinkAuthority = prepareExternalLinkTaskAdmission(
|
|
316
602
|
input.authorization,
|
|
317
603
|
creationInitiator.actor,
|
|
318
604
|
);
|
|
319
|
-
const creatorPolicy =
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
: null;
|
|
605
|
+
const creatorPolicy = await frozenScheduledTaskCreatorPolicy({
|
|
606
|
+
db: input.db,
|
|
607
|
+
settings: input.settings,
|
|
608
|
+
grant: input.grant,
|
|
609
|
+
authorization: input.authorization,
|
|
610
|
+
actor: creationInitiator.actor ?? null,
|
|
611
|
+
});
|
|
327
612
|
const xaiProviderAccountAuthoritySnapshot: XaiProviderAccountAuthoritySnapshotV1 =
|
|
328
613
|
creationInitiator.actor
|
|
329
614
|
? await getSessionTurnXaiProviderAccountAuthoritySnapshot(
|
|
@@ -332,10 +617,19 @@ export async function createValidatedScheduledTask(input: {
|
|
|
332
617
|
creationInitiator.actor.sessionId,
|
|
333
618
|
creationInitiator.actor.turnId,
|
|
334
619
|
)
|
|
335
|
-
:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
620
|
+
: // SuperGrok's v1 value is never computed from live state after its
|
|
621
|
+
// cutover: new work writes the column default (design 5.3, "v1
|
|
622
|
+
// columns after a provider's cutover").
|
|
623
|
+
WORKSPACE_XAI_PROVIDER_ACCOUNT_AUTHORITY_SNAPSHOT_V1;
|
|
624
|
+
const claudeProviderAccountAuthoritySnapshot: ClaudeProviderAccountAuthoritySnapshotV1 =
|
|
625
|
+
creationInitiator.actor
|
|
626
|
+
? await getSessionTurnClaudeProviderAccountAuthoritySnapshot(
|
|
627
|
+
input.db,
|
|
628
|
+
input.grant.workspaceId,
|
|
629
|
+
creationInitiator.actor.sessionId,
|
|
630
|
+
creationInitiator.actor.turnId,
|
|
631
|
+
)
|
|
632
|
+
: { version: 1 as const, scope: "workspace" as const };
|
|
339
633
|
const beforeCreateCommit =
|
|
340
634
|
input.payload.runMode !== "existing_session"
|
|
341
635
|
? workspaceCustomModelCommitGuard({
|
|
@@ -343,8 +637,28 @@ export async function createValidatedScheduledTask(input: {
|
|
|
343
637
|
accountId: input.grant.accountId,
|
|
344
638
|
workspaceId: input.grant.workspaceId,
|
|
345
639
|
modelId: agentConfig.model ?? input.settings.openaiModel,
|
|
640
|
+
claudeAuthoritySnapshot: claudeProviderAccountAuthoritySnapshot,
|
|
346
641
|
})
|
|
347
642
|
: undefined;
|
|
643
|
+
const visibility = await resolveScheduledTaskVisibility({
|
|
644
|
+
db: input.db,
|
|
645
|
+
settings: input.settings,
|
|
646
|
+
grant: input.grant,
|
|
647
|
+
requested: "visibility" in input.payload ? input.payload.visibility : undefined,
|
|
648
|
+
current: null,
|
|
649
|
+
targetSessionId: target?.id ?? null,
|
|
650
|
+
personalKnowledge: agentConfig.knowledgeSource?.destination.kind === "personal",
|
|
651
|
+
ownerSubjectId:
|
|
652
|
+
action.kind === "agent_turn"
|
|
653
|
+
? (await scheduledTaskViewerForGrant(input.db, input.grant, input.settings)).subjectId
|
|
654
|
+
: null,
|
|
655
|
+
});
|
|
656
|
+
await requirePersonalMachineAudience({
|
|
657
|
+
db: input.db,
|
|
658
|
+
grant: input.grant,
|
|
659
|
+
agentConfig,
|
|
660
|
+
audience: visibility,
|
|
661
|
+
});
|
|
348
662
|
return await withScheduledTaskAuthorityWriteErrors(() =>
|
|
349
663
|
input.db.transaction(async (transaction) => {
|
|
350
664
|
const task = await createScheduledTask(transaction, {
|
|
@@ -353,44 +667,36 @@ export async function createValidatedScheduledTask(input: {
|
|
|
353
667
|
workspaceId: input.grant.workspaceId,
|
|
354
668
|
name: trimmedScheduledTaskName(input.payload.name),
|
|
355
669
|
status: input.payload.status,
|
|
670
|
+
visibility,
|
|
356
671
|
schedule: input.payload.schedule,
|
|
357
672
|
temporalScheduleId: scheduledTaskTemporalScheduleId(id),
|
|
358
673
|
runMode: input.payload.runMode,
|
|
359
674
|
overlapPolicy: input.payload.overlapPolicy,
|
|
360
675
|
action,
|
|
361
|
-
agentConfig,
|
|
676
|
+
agentConfig: storedAgentConfig,
|
|
362
677
|
...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
|
|
363
678
|
...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
|
|
364
679
|
createdByActor: creationInitiator.actor ?? null,
|
|
365
680
|
...(captureLinkAuthority ? { captureLinkAuthority } : {}),
|
|
366
|
-
personalConnectionDelegations,
|
|
367
681
|
xaiProviderAccountAuthoritySnapshot,
|
|
682
|
+
claudeProviderAccountAuthoritySnapshot,
|
|
368
683
|
creatorPolicy,
|
|
369
684
|
targetSessionId: target?.id ?? null,
|
|
370
685
|
variableSetId: input.payload.variableSetId ?? null,
|
|
371
|
-
|
|
686
|
+
...("vaultPolicy" in input.payload && input.payload.vaultPolicy
|
|
687
|
+
? { vaultPolicy: scheduledVaultPolicy(input.payload.vaultPolicy) }
|
|
688
|
+
: {}),
|
|
689
|
+
rigId,
|
|
372
690
|
metadata: input.payload.metadata,
|
|
373
|
-
...(hostSelections?.length && runtimeSettings
|
|
374
|
-
? {
|
|
375
|
-
captureHostAuthority: prepareHostMcpTaskAdmission({
|
|
376
|
-
settings: runtimeSettings,
|
|
377
|
-
tools: target?.tools ?? agentConfig.tools,
|
|
378
|
-
grant: input.grant,
|
|
379
|
-
...(input.authorization ? { authorization: input.authorization } : {}),
|
|
380
|
-
selections: hostSelections,
|
|
381
|
-
}),
|
|
382
|
-
}
|
|
383
|
-
: hostSelections === undefined && creationInitiator.actor && runtimeSettings
|
|
384
|
-
? {
|
|
385
|
-
captureHostAuthority: prepareInheritedHostMcpTaskAdmission(
|
|
386
|
-
runtimeSettings,
|
|
387
|
-
target?.tools ?? agentConfig.tools,
|
|
388
|
-
creationInitiator.actor,
|
|
389
|
-
),
|
|
390
|
-
}
|
|
391
|
-
: {}),
|
|
392
691
|
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
393
692
|
});
|
|
693
|
+
await recordScheduledTaskAccountChoiceChanges(transaction, {
|
|
694
|
+
grant: input.grant,
|
|
695
|
+
taskId: task.id,
|
|
696
|
+
operation: "create",
|
|
697
|
+
before: null,
|
|
698
|
+
after: task.agentConfig,
|
|
699
|
+
});
|
|
394
700
|
if (learning && learningContext)
|
|
395
701
|
await saveAgentLearningSettings(transaction, learningContext, {
|
|
396
702
|
scope: learning.scope,
|
|
@@ -405,7 +711,8 @@ export async function createValidatedScheduledTask(input: {
|
|
|
405
711
|
}
|
|
406
712
|
|
|
407
713
|
/**
|
|
408
|
-
* Freeze
|
|
714
|
+
* Freeze authenticated credential restrictions for every creation lane, and
|
|
715
|
+
* the creating session's boundary onto an agent-created task so the
|
|
409
716
|
* sessions generated for it inherit exactly what the creator could see and
|
|
410
717
|
* do, never the deployment default. Tools are the session's effective
|
|
411
718
|
* model-visible selection under the deployment ceiling; permissions are the
|
|
@@ -415,25 +722,86 @@ export async function createValidatedScheduledTask(input: {
|
|
|
415
722
|
* projection when it exposes those facts; each absent fact is stored as null
|
|
416
723
|
* so a generated session keeps its own default for that key.
|
|
417
724
|
*/
|
|
418
|
-
async function frozenScheduledTaskCreatorPolicy(input: {
|
|
725
|
+
export async function frozenScheduledTaskCreatorPolicy(input: {
|
|
419
726
|
db: Database;
|
|
420
727
|
settings: Settings;
|
|
421
728
|
grant: AccessGrant;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
729
|
+
authorization?: AccessGrantAuthorization | undefined;
|
|
730
|
+
actor: Extract<SessionCommandActor, { type: "agent_attempt" }> | null;
|
|
731
|
+
}): Promise<ScheduledTaskCreatorPolicy | null> {
|
|
732
|
+
let restricted =
|
|
733
|
+
(input.authorization?.grant === input.grant &&
|
|
734
|
+
isDeveloperSetupAuthorization(input.authorization)) ||
|
|
735
|
+
isDeveloperSetupGrant(input.grant);
|
|
736
|
+
if (!input.actor) {
|
|
737
|
+
if (input.grant.permissionMode === "explicit") {
|
|
738
|
+
const permissions = DEFAULT_FIRST_PARTY_MCP_PERMISSIONS.filter((permission) =>
|
|
739
|
+
hasPermission(input.grant.permissions, permission, "explicit"),
|
|
740
|
+
);
|
|
741
|
+
if (permissions.length === 0) {
|
|
742
|
+
throw new HTTPException(403, {
|
|
743
|
+
message:
|
|
744
|
+
"the organization key holds no first-party MCP permission it could delegate to scheduled runs",
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
firstPartyMcpTools: null,
|
|
749
|
+
firstPartyMcpPermissions: permissions,
|
|
750
|
+
sessionPolicy: null,
|
|
751
|
+
...(restricted ? { credentialRestriction: "developer_setup" as const } : {}),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
// A restriction is not an agent tool/permission selection. Keep the exact
|
|
755
|
+
// first-party and session defaults of ordinary API/service/asUser tasks.
|
|
756
|
+
return restricted
|
|
757
|
+
? {
|
|
758
|
+
firstPartyMcpTools: null,
|
|
759
|
+
firstPartyMcpPermissions: null,
|
|
760
|
+
sessionPolicy: null,
|
|
761
|
+
credentialRestriction: "developer_setup",
|
|
762
|
+
}
|
|
763
|
+
: null;
|
|
764
|
+
}
|
|
765
|
+
const session = await getSession(input.db, input.grant.workspaceId, input.actor.sessionId);
|
|
425
766
|
if (!session) {
|
|
426
767
|
throw new HTTPException(403, {
|
|
427
768
|
message: "the calling agent session is not available in this workspace",
|
|
428
769
|
});
|
|
429
770
|
}
|
|
430
|
-
const
|
|
771
|
+
const turn = await getSessionTurnForAttempt(
|
|
772
|
+
input.db,
|
|
773
|
+
input.grant.workspaceId,
|
|
774
|
+
input.actor.sessionId,
|
|
775
|
+
input.actor.attemptId,
|
|
776
|
+
);
|
|
777
|
+
if (!turn || turn.id !== input.actor.turnId) {
|
|
778
|
+
throw new HTTPException(403, { message: "the calling agent attempt is not available" });
|
|
779
|
+
}
|
|
780
|
+
// These are server-frozen policies, not task/agentConfig metadata. The DB
|
|
781
|
+
// insert also verifies this exact actor under its ownership locks.
|
|
782
|
+
const turnPolicy = readTurnExecutionPolicyV1(turn.metadata);
|
|
783
|
+
const sessionPolicy = readTurnExecutionPolicyV1(session.metadata);
|
|
784
|
+
restricted ||= Boolean(
|
|
785
|
+
(turnPolicy.kind === "valid" &&
|
|
786
|
+
turnPolicy.policy.credentialRestriction === "developer_setup") ||
|
|
787
|
+
(sessionPolicy.kind === "valid" &&
|
|
788
|
+
sessionPolicy.policy.credentialRestriction === "developer_setup"),
|
|
789
|
+
);
|
|
790
|
+
const currentSelection = resolveSessionFirstPartyMcpTools(
|
|
431
791
|
input.settings,
|
|
432
|
-
session
|
|
792
|
+
session,
|
|
793
|
+
(await requireWorkspace(input.db, input.grant.workspaceId)).settings,
|
|
433
794
|
);
|
|
795
|
+
const signedSelection = input.grant.metadata?.["firstPartyMcpTools"];
|
|
796
|
+
const firstPartyMcpTools = Array.isArray(signedSelection)
|
|
797
|
+
? currentSelection.filter((name) => signedSelection.includes(name))
|
|
798
|
+
: currentSelection;
|
|
434
799
|
const firstPartyMcpPermissions = (
|
|
435
800
|
session.firstPartyMcpPermissions ?? [...DEFAULT_FIRST_PARTY_MCP_PERMISSIONS]
|
|
436
|
-
).filter((permission) =>
|
|
801
|
+
).filter((permission) =>
|
|
802
|
+
hasPermission(input.grant.permissions, permission, input.grant.permissionMode),
|
|
803
|
+
);
|
|
804
|
+
requireExplicitPermissionDelegation(input.grant, firstPartyMcpPermissions);
|
|
437
805
|
if (firstPartyMcpPermissions.length === 0) {
|
|
438
806
|
throw new HTTPException(403, {
|
|
439
807
|
message:
|
|
@@ -449,6 +817,7 @@ async function frozenScheduledTaskCreatorPolicy(input: {
|
|
|
449
817
|
return {
|
|
450
818
|
firstPartyMcpTools,
|
|
451
819
|
firstPartyMcpPermissions,
|
|
820
|
+
...(restricted ? { credentialRestriction: "developer_setup" as const } : {}),
|
|
452
821
|
sessionPolicy: {
|
|
453
822
|
agentAccess: agentAccess.success ? agentAccess.data : null,
|
|
454
823
|
scopeSubjectId: scopeSubjectId.success ? scopeSubjectId.data : null,
|
|
@@ -487,6 +856,10 @@ export async function withScheduledTaskAuthorityWriteErrors<T>(run: () => Promis
|
|
|
487
856
|
try {
|
|
488
857
|
return await run();
|
|
489
858
|
} catch (error) {
|
|
859
|
+
if (error instanceof ScheduledTaskHeadChangedError)
|
|
860
|
+
throw new HTTPException(409, {
|
|
861
|
+
message: "Scheduled task changed. Reload it before saving.",
|
|
862
|
+
});
|
|
490
863
|
if (nestedPostgresSqlState(error) === "40001")
|
|
491
864
|
throw new HTTPException(409, {
|
|
492
865
|
message: "Scheduled task learning settings changed. Reload the task before saving.",
|
|
@@ -506,7 +879,7 @@ export async function withScheduledTaskAuthorityWriteErrors<T>(run: () => Promis
|
|
|
506
879
|
/** API/MCP-facing update that maps authority-write denials to 409. */
|
|
507
880
|
export async function updateScheduledTaskForApi(
|
|
508
881
|
db: Database,
|
|
509
|
-
|
|
882
|
+
grant: AccessGrant,
|
|
510
883
|
taskId: string,
|
|
511
884
|
update: UpdateScheduledTaskInput,
|
|
512
885
|
learning?: {
|
|
@@ -515,6 +888,15 @@ export async function updateScheduledTaskForApi(
|
|
|
515
888
|
restoreState: ScheduledTaskRestoreState;
|
|
516
889
|
},
|
|
517
890
|
): Promise<ScheduledTask> {
|
|
891
|
+
const workspaceId = grant.workspaceId;
|
|
892
|
+
const beforeUpdateCommit = update.beforeUpdateCommit;
|
|
893
|
+
update = {
|
|
894
|
+
...update,
|
|
895
|
+
beforeUpdateCommit: async (tx) => {
|
|
896
|
+
await assertScheduledTaskMutationOwner(tx, grant, taskId);
|
|
897
|
+
await beforeUpdateCommit?.(tx);
|
|
898
|
+
},
|
|
899
|
+
};
|
|
518
900
|
if (learning) {
|
|
519
901
|
const context = await knowledgeContextForAccess(
|
|
520
902
|
{ db },
|
|
@@ -581,6 +963,116 @@ export async function updateScheduledTaskForApi(
|
|
|
581
963
|
);
|
|
582
964
|
}
|
|
583
965
|
|
|
966
|
+
export async function assertScheduledTaskMutationOwner(
|
|
967
|
+
db: Database,
|
|
968
|
+
grant: AccessGrant,
|
|
969
|
+
taskId: string,
|
|
970
|
+
): Promise<void> {
|
|
971
|
+
await assertNotSiteClampedAgent(db, grant, "schedules");
|
|
972
|
+
await assertSiteMayReachScheduledTask(db, grant, taskId);
|
|
973
|
+
const writer = scheduledTaskInitiatorForGrant(grant);
|
|
974
|
+
const matches = await scheduledTaskMutationOwnerMatches(db, {
|
|
975
|
+
workspaceId: grant.workspaceId,
|
|
976
|
+
taskId,
|
|
977
|
+
...(writer.initiator ? { createdBy: writer.initiator } : {}),
|
|
978
|
+
...(writer.context ? { createdByContext: writer.context } : {}),
|
|
979
|
+
createdByActor: writer.actor ?? null,
|
|
980
|
+
});
|
|
981
|
+
if (!matches)
|
|
982
|
+
throw new HTTPException(403, {
|
|
983
|
+
message: "Only the schedule owner can change or run it. Copy it to create your own schedule.",
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* A request from a Site page (a tool the page called) may change or run only
|
|
989
|
+
* a schedule that same Site created. Refused as a missing schedule.
|
|
990
|
+
*/
|
|
991
|
+
async function assertSiteMayReachScheduledTask(
|
|
992
|
+
db: Database,
|
|
993
|
+
grant: AccessGrant,
|
|
994
|
+
taskId: string,
|
|
995
|
+
): Promise<void> {
|
|
996
|
+
const origin = currentSiteSessionOrigin();
|
|
997
|
+
if (!origin) return;
|
|
998
|
+
const task = await getScheduledTask(db, grant.workspaceId, taskId);
|
|
999
|
+
if (!siteOriginsMatch(origin, siteSessionOriginFromMetadata(task?.agentConfig.metadata)))
|
|
1000
|
+
throw new HTTPException(404, { message: "scheduled task not found" });
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* An admin's pause of a schedule they do not own. Pausing is reversible and
|
|
1005
|
+
* changes nothing else; only the owner can change, run or resume it again.
|
|
1006
|
+
*/
|
|
1007
|
+
export async function pauseScheduledTaskAsAdmin(
|
|
1008
|
+
db: Database,
|
|
1009
|
+
workspaceId: string,
|
|
1010
|
+
taskId: string,
|
|
1011
|
+
): Promise<ScheduledTask> {
|
|
1012
|
+
return await withScheduledTaskAuthorityWriteErrors(() =>
|
|
1013
|
+
updateScheduledTask(db, workspaceId, taskId, { status: "paused" }),
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export const SCHEDULED_TASK_PAUSED_TRIGGER_MESSAGE =
|
|
1018
|
+
"This schedule is paused, so it cannot run now. Resume it first, then trigger it.";
|
|
1019
|
+
|
|
1020
|
+
export async function triggerScheduledTaskForGrant(
|
|
1021
|
+
db: Database,
|
|
1022
|
+
grant: AccessGrant,
|
|
1023
|
+
workflowClient: SessionWorkflowClient,
|
|
1024
|
+
input: Parameters<SessionWorkflowClient["triggerScheduledTask"]>[0],
|
|
1025
|
+
): Promise<void> {
|
|
1026
|
+
await db.transaction(async (tx) => {
|
|
1027
|
+
await assertScheduledTaskMutationOwner(tx, grant, input.task.id);
|
|
1028
|
+
if (isRetiredNativeAtlassianTask(input.task))
|
|
1029
|
+
throw new HTTPException(410, { message: ATLASSIAN_NATIVE_RETIRED_MESSAGE });
|
|
1030
|
+
// The worker never runs a paused schedule. Refuse here so the caller is
|
|
1031
|
+
// told, instead of accepting a trigger that would silently do nothing.
|
|
1032
|
+
if (input.task.status === "paused")
|
|
1033
|
+
throw new HTTPException(409, { message: SCHEDULED_TASK_PAUSED_TRIGGER_MESSAGE });
|
|
1034
|
+
const actor = creationInitiatorForGrant(grant).actor ?? null;
|
|
1035
|
+
const restriction = await scheduledTaskCredentialRestrictionForGrant(tx, grant, actor);
|
|
1036
|
+
// A caller cannot supply or clear the trusted ceiling. Ownerless and
|
|
1037
|
+
// same-human schedules can be triggered by a different credential than
|
|
1038
|
+
// their creator; restrict that accepted run, not the durable task.
|
|
1039
|
+
const { credentialRestriction: _untrustedRestriction, ...trigger } = input;
|
|
1040
|
+
await workflowClient.triggerScheduledTask({
|
|
1041
|
+
...trigger,
|
|
1042
|
+
...(restriction ? { credentialRestriction: restriction } : {}),
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function scheduledTaskCredentialRestrictionForGrant(
|
|
1048
|
+
db: Database,
|
|
1049
|
+
grant: AccessGrant,
|
|
1050
|
+
actor: Extract<SessionCommandActor, { type: "agent_attempt" }> | null,
|
|
1051
|
+
): Promise<"developer_setup" | undefined> {
|
|
1052
|
+
if (isDeveloperSetupGrant(grant)) return "developer_setup";
|
|
1053
|
+
if (!actor) return undefined;
|
|
1054
|
+
const session = await getSession(db, grant.workspaceId, actor.sessionId);
|
|
1055
|
+
const turn = await getSessionTurnForAttempt(
|
|
1056
|
+
db,
|
|
1057
|
+
grant.workspaceId,
|
|
1058
|
+
actor.sessionId,
|
|
1059
|
+
actor.attemptId,
|
|
1060
|
+
);
|
|
1061
|
+
if (!session || !turn || turn.id !== actor.turnId) {
|
|
1062
|
+
throw new HTTPException(403, { message: "the calling agent attempt is not available" });
|
|
1063
|
+
}
|
|
1064
|
+
const policies = [
|
|
1065
|
+
readTurnExecutionPolicyV1(turn.metadata),
|
|
1066
|
+
readTurnExecutionPolicyV1(session.metadata),
|
|
1067
|
+
];
|
|
1068
|
+
return policies.some(
|
|
1069
|
+
(policy) =>
|
|
1070
|
+
policy.kind === "valid" && policy.policy.credentialRestriction === "developer_setup",
|
|
1071
|
+
)
|
|
1072
|
+
? "developer_setup"
|
|
1073
|
+
: undefined;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
584
1076
|
export async function validateScheduledTaskTarget(input: {
|
|
585
1077
|
db: Database;
|
|
586
1078
|
sessionAuthorization?: SessionAuthorizationPort | null | undefined;
|
|
@@ -592,6 +1084,8 @@ export async function validateScheduledTaskTarget(input: {
|
|
|
592
1084
|
rigId: string | null | undefined;
|
|
593
1085
|
agentConfig: ScheduledTaskAgentConfig;
|
|
594
1086
|
missingTargetStatus?: 404 | 422;
|
|
1087
|
+
/** Leaving a cancelled chat still requires control, but not revivability. */
|
|
1088
|
+
departingTarget?: boolean;
|
|
595
1089
|
}): Promise<Session | null> {
|
|
596
1090
|
if (input.runMode !== "existing_session") {
|
|
597
1091
|
if (input.targetSessionId) {
|
|
@@ -643,54 +1137,58 @@ export async function validateScheduledTaskTarget(input: {
|
|
|
643
1137
|
if (!session || session.accountId !== input.grant.accountId) {
|
|
644
1138
|
throw new HTTPException(404, { message: "target session not found" });
|
|
645
1139
|
}
|
|
646
|
-
if (
|
|
647
|
-
input.agentConfig.bundledSkillIds !== undefined &&
|
|
648
|
-
!isDeepStrictEqual(input.agentConfig.bundledSkillIds, session.bundledSkillIds)
|
|
649
|
-
) {
|
|
650
|
-
throw new HTTPException(422, {
|
|
651
|
-
message: "An existing-session schedule cannot change that session's bundled Skill selection",
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
if (session.status === "cancelled") {
|
|
1140
|
+
if (session.status === "cancelled" && !input.departingTarget) {
|
|
655
1141
|
throw new HTTPException(409, {
|
|
656
1142
|
message: "target session is cancelled; choose a revivable session",
|
|
657
1143
|
});
|
|
658
1144
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
message: "target session variableSet attachment does not match the scheduled task",
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
if ((session.rigId ?? null) !== (input.rigId ?? null)) {
|
|
665
|
-
throw new HTTPException(422, {
|
|
666
|
-
message: "target session rig does not match the scheduled task",
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
if (
|
|
670
|
-
input.agentConfig.sandboxBackend !== undefined &&
|
|
671
|
-
input.agentConfig.sandboxBackend !== session.sandboxBackend
|
|
672
|
-
) {
|
|
673
|
-
throw new HTTPException(422, {
|
|
674
|
-
message: "target session sandbox backend does not match the scheduled task",
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
if (
|
|
678
|
-
scheduledSlackBotConnectionId(session.metadata) !==
|
|
679
|
-
(input.agentConfig.slackBotConnectionId ?? null)
|
|
680
|
-
) {
|
|
681
|
-
throw new HTTPException(422, {
|
|
682
|
-
message: "target session OpenGeni Slack bot binding does not match the scheduled task",
|
|
683
|
-
});
|
|
684
|
-
}
|
|
1145
|
+
// The target owns execution settings. Admission freezes and revalidates its
|
|
1146
|
+
// current policy under the scheduled owner; duplicated task fields confer no access.
|
|
685
1147
|
return session;
|
|
686
1148
|
}
|
|
687
1149
|
|
|
1150
|
+
export const PERSONAL_MACHINE_SCHEDULE_AUDIENCE_MESSAGE =
|
|
1151
|
+
"Your personal machine can only run schedules that only you can see. Set Who can see it to Only me, or pick a shared machine.";
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* A schedule's audience is resolved after its machine is validated, and can be
|
|
1155
|
+
* changed without touching the machine. Re-check the pairing on the final
|
|
1156
|
+
* audience so a personal machine never ends up on a schedule others can see.
|
|
1157
|
+
*/
|
|
1158
|
+
async function requirePersonalMachineAudience(input: {
|
|
1159
|
+
db: Database;
|
|
1160
|
+
grant: AccessGrant;
|
|
1161
|
+
agentConfig: ScheduledTaskAgentConfig;
|
|
1162
|
+
audience: "user_private" | "workspace_shared";
|
|
1163
|
+
}): Promise<void> {
|
|
1164
|
+
const target = input.agentConfig.machineTarget;
|
|
1165
|
+
if (!target || input.audience === "user_private") return;
|
|
1166
|
+
const sandbox = await getSandbox(
|
|
1167
|
+
input.db,
|
|
1168
|
+
{
|
|
1169
|
+
accountId: input.grant.accountId,
|
|
1170
|
+
workspaceId: input.grant.workspaceId,
|
|
1171
|
+
subjectId: input.grant.subjectId,
|
|
1172
|
+
},
|
|
1173
|
+
target.targetSandboxId,
|
|
1174
|
+
);
|
|
1175
|
+
if (sandbox?.scope === "user") {
|
|
1176
|
+
throw new HTTPException(422, { message: PERSONAL_MACHINE_SCHEDULE_AUDIENCE_MESSAGE });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
688
1180
|
export async function validateScheduledTaskMachineTarget(input: {
|
|
689
1181
|
settings: Settings;
|
|
690
1182
|
db: Database;
|
|
691
1183
|
grant: AccessGrant;
|
|
692
1184
|
runMode: ScheduledTask["runMode"];
|
|
693
1185
|
agentConfig: ScheduledTaskAgentConfig;
|
|
1186
|
+
/**
|
|
1187
|
+
* Who can see the schedule. A personal machine runs only an Only me schedule
|
|
1188
|
+
* of its owner; the owner check is the scoped lookup itself, which returns a
|
|
1189
|
+
* personal machine to nobody else.
|
|
1190
|
+
*/
|
|
1191
|
+
audience: "user_private" | "workspace_shared";
|
|
694
1192
|
requireOnline?: boolean;
|
|
695
1193
|
}): Promise<{
|
|
696
1194
|
sandboxId: string;
|
|
@@ -731,11 +1229,8 @@ export async function validateScheduledTaskMachineTarget(input: {
|
|
|
731
1229
|
message: "the selected Connected Machine is unavailable",
|
|
732
1230
|
});
|
|
733
1231
|
}
|
|
734
|
-
if (sandbox.scope === "user") {
|
|
735
|
-
throw new HTTPException(422, {
|
|
736
|
-
message:
|
|
737
|
-
"personal Connected Machines cannot run unattended schedules; select a workspace or organization machine",
|
|
738
|
-
});
|
|
1232
|
+
if (sandbox.scope === "user" && input.audience !== "user_private") {
|
|
1233
|
+
throw new HTTPException(422, { message: PERSONAL_MACHINE_SCHEDULE_AUDIENCE_MESSAGE });
|
|
739
1234
|
}
|
|
740
1235
|
const enrollment = input.requireOnline
|
|
741
1236
|
? await getLiveEnrollmentConnection(input.db, access, sandbox.enrollmentId)
|
|
@@ -786,7 +1281,7 @@ export function scheduledTaskAuthorityUpdateForGrant(
|
|
|
786
1281
|
| "authorityUpdatedByContext"
|
|
787
1282
|
| "authorityUpdatedByActor"
|
|
788
1283
|
> {
|
|
789
|
-
const writer =
|
|
1284
|
+
const writer = scheduledTaskInitiatorForGrant(grant);
|
|
790
1285
|
return {
|
|
791
1286
|
refreshPersonalResourceAuthority: true,
|
|
792
1287
|
...(writer.initiator ? { authorityUpdatedBy: writer.initiator } : {}),
|
|
@@ -795,20 +1290,131 @@ export function scheduledTaskAuthorityUpdateForGrant(
|
|
|
795
1290
|
};
|
|
796
1291
|
}
|
|
797
1292
|
|
|
1293
|
+
/**
|
|
1294
|
+
* The Sandbox Environment a new task stores. An explicit id or null is the
|
|
1295
|
+
* caller's choice. An omitted value is resolved once, here, and frozen on the
|
|
1296
|
+
* task, the way session create resolves an omitted rigId:
|
|
1297
|
+
* - an existing-session task keeps its target session's own environment;
|
|
1298
|
+
* - a Connected Machine task stores none: environment setup and Variable Set
|
|
1299
|
+
* injection never reach a machine, so a binding would only add a fire-time
|
|
1300
|
+
* failure mode;
|
|
1301
|
+
* - otherwise the workspace default, when the creator can see it and it has an
|
|
1302
|
+
* active version. A stale default degrades to none, like session create.
|
|
1303
|
+
* The default's own Variable Sets must be attachable by the creator, exactly
|
|
1304
|
+
* as session create requires, so a workspace default never gives a task
|
|
1305
|
+
* secrets its creator could not attach. Later changes to the workspace default
|
|
1306
|
+
* do not move an existing task.
|
|
1307
|
+
*/
|
|
1308
|
+
async function resolveScheduledTaskCreateRigId(input: {
|
|
1309
|
+
settings: Settings;
|
|
1310
|
+
db: Database;
|
|
1311
|
+
grant: AccessGrant;
|
|
1312
|
+
requestedRigId: string | null | undefined;
|
|
1313
|
+
runMode: ScheduledTask["runMode"];
|
|
1314
|
+
target: Session | null;
|
|
1315
|
+
agentConfig: ScheduledTaskAgentConfig;
|
|
1316
|
+
}): Promise<string | null> {
|
|
1317
|
+
if (input.requestedRigId !== undefined) return input.requestedRigId;
|
|
1318
|
+
if (input.runMode === "existing_session") return input.target?.rigId ?? null;
|
|
1319
|
+
if (input.agentConfig.machineTarget) return null;
|
|
1320
|
+
const defaultRigId = await getWorkspaceDefaultRigId(input.db, input.grant.workspaceId);
|
|
1321
|
+
if (!defaultRigId) return null;
|
|
1322
|
+
const rig = await getRig(
|
|
1323
|
+
input.db,
|
|
1324
|
+
{
|
|
1325
|
+
accountId: input.grant.accountId,
|
|
1326
|
+
workspaceId: input.grant.workspaceId,
|
|
1327
|
+
subjectId: input.grant.subjectId,
|
|
1328
|
+
},
|
|
1329
|
+
defaultRigId,
|
|
1330
|
+
);
|
|
1331
|
+
if (!rig?.activeVersion) return null;
|
|
1332
|
+
return rig.id;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
type ScheduledTaskRig = NonNullable<Awaited<ReturnType<typeof getRig>>>;
|
|
1336
|
+
|
|
798
1337
|
// Validate a scheduled task's rig reference: it must name a rig in the
|
|
799
1338
|
// workspace. A missing/cross-workspace id is a 422 (RLS-invisible == missing).
|
|
800
1339
|
async function requireScheduledTaskRig(
|
|
801
1340
|
db: Database,
|
|
802
1341
|
access: { accountId: string; workspaceId: string; subjectId: string },
|
|
803
1342
|
rigId: string,
|
|
804
|
-
): Promise<
|
|
1343
|
+
): Promise<ScheduledTaskRig> {
|
|
805
1344
|
const rig = await getRig(db, access, rigId);
|
|
806
1345
|
if (!rig) {
|
|
807
1346
|
throw new HTTPException(422, { message: `unknown rigId: ${rigId}` });
|
|
808
1347
|
}
|
|
1348
|
+
return rig;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* The validated update for a schedule edit, including its audience: a schedule
|
|
1353
|
+
* that ends up posting into an Only me chat is Only me, and only its owner can
|
|
1354
|
+
* change who sees it (the update itself is owner-only).
|
|
1355
|
+
*/
|
|
1356
|
+
export async function validatedScheduledTaskUpdate(
|
|
1357
|
+
input: Parameters<typeof validatedScheduledTaskUpdateWithoutAudience>[0],
|
|
1358
|
+
): Promise<UpdateScheduledTaskInput> {
|
|
1359
|
+
const update = await validatedScheduledTaskUpdateWithoutAudience(input);
|
|
1360
|
+
const runMode = update.runMode ?? input.existing.runMode;
|
|
1361
|
+
const targetSessionId =
|
|
1362
|
+
runMode === "existing_session"
|
|
1363
|
+
? (update.targetSessionId ?? input.existing.targetSessionId)
|
|
1364
|
+
: runMode === "reusable_session" && input.existing.runMode === "reusable_session"
|
|
1365
|
+
? input.existing.reusableSessionId
|
|
1366
|
+
: null;
|
|
1367
|
+
const agentConfig = update.agentConfig ?? input.existing.agentConfig;
|
|
1368
|
+
const visibility = await resolveScheduledTaskVisibility({
|
|
1369
|
+
db: input.db,
|
|
1370
|
+
settings: input.settings,
|
|
1371
|
+
grant: input.grant,
|
|
1372
|
+
requested: input.payload.visibility,
|
|
1373
|
+
current: input.existing,
|
|
1374
|
+
targetSessionId,
|
|
1375
|
+
personalKnowledge: agentConfig.knowledgeSource?.destination.kind === "personal",
|
|
1376
|
+
ownerSubjectId: input.existing.ownerSubjectId,
|
|
1377
|
+
});
|
|
1378
|
+
await requirePersonalMachineAudience({
|
|
1379
|
+
db: input.db,
|
|
1380
|
+
grant: input.grant,
|
|
1381
|
+
agentConfig,
|
|
1382
|
+
audience: visibility,
|
|
1383
|
+
});
|
|
1384
|
+
const current = input.existing.visibility === "private" ? "user_private" : "workspace_shared";
|
|
1385
|
+
const audited = withScheduledTaskAccountChoiceAudit(input.grant, input.existing, update);
|
|
1386
|
+
return visibility === current ? audited : { ...audited, visibility };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Record the edit's account and connector choice changes in the transaction
|
|
1391
|
+
* that commits it. Every update writer (form, API, agent tool, access
|
|
1392
|
+
* refresh) commits through the validated update.
|
|
1393
|
+
*/
|
|
1394
|
+
function withScheduledTaskAccountChoiceAudit(
|
|
1395
|
+
grant: AccessGrant,
|
|
1396
|
+
existing: ScheduledTask,
|
|
1397
|
+
update: UpdateScheduledTaskInput,
|
|
1398
|
+
): UpdateScheduledTaskInput {
|
|
1399
|
+
const next = update.agentConfig;
|
|
1400
|
+
if (!next || !scheduledTaskAccountChoiceChanges(existing.agentConfig, next)) return update;
|
|
1401
|
+
const prior = update.beforeUpdateCommit;
|
|
1402
|
+
return {
|
|
1403
|
+
...update,
|
|
1404
|
+
beforeUpdateCommit: async (tx) => {
|
|
1405
|
+
await prior?.(tx);
|
|
1406
|
+
await recordScheduledTaskAccountChoiceChanges(tx, {
|
|
1407
|
+
grant,
|
|
1408
|
+
taskId: existing.id,
|
|
1409
|
+
operation: "update",
|
|
1410
|
+
before: existing.agentConfig,
|
|
1411
|
+
after: next,
|
|
1412
|
+
});
|
|
1413
|
+
},
|
|
1414
|
+
};
|
|
809
1415
|
}
|
|
810
1416
|
|
|
811
|
-
|
|
1417
|
+
async function validatedScheduledTaskUpdateWithoutAudience(input: {
|
|
812
1418
|
settings: Settings;
|
|
813
1419
|
db: Database;
|
|
814
1420
|
objectStorage: ObjectStorageDependency;
|
|
@@ -820,7 +1426,124 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
820
1426
|
toolsProvided?: boolean;
|
|
821
1427
|
sessionAuthorization?: SessionAuthorizationPort | null | undefined;
|
|
822
1428
|
authorizationSurface?: SessionAuthorizationSurface | undefined;
|
|
1429
|
+
/** Proves the bot may post in a newly chosen task Slack channel. */
|
|
1430
|
+
verifySlackChannel?: ScheduledTaskSlackChannelVerifier | undefined;
|
|
823
1431
|
}): Promise<UpdateScheduledTaskInput> {
|
|
1432
|
+
const requestedPrompt =
|
|
1433
|
+
input.payload.prompt ??
|
|
1434
|
+
input.payload.agentConfigPatch?.prompt ??
|
|
1435
|
+
input.payload.agentConfig?.prompt;
|
|
1436
|
+
if (requestedPrompt !== undefined && !requestedPrompt.trim())
|
|
1437
|
+
throw new HTTPException(422, { message: "scheduled task prompt is required" });
|
|
1438
|
+
const retainedPrompt =
|
|
1439
|
+
input.payload.agentConfig === undefined
|
|
1440
|
+
? (requestedPrompt ?? input.existing.agentConfig.prompt)
|
|
1441
|
+
: undefined;
|
|
1442
|
+
if (
|
|
1443
|
+
input.payload.expectedExecutionDigest !== undefined &&
|
|
1444
|
+
input.payload.expectedExecutionDigest !== input.existing.executionDigest
|
|
1445
|
+
)
|
|
1446
|
+
throw new HTTPException(409, {
|
|
1447
|
+
message: "Scheduled task changed. Reload it before saving.",
|
|
1448
|
+
});
|
|
1449
|
+
if (input.payload.prompt !== undefined)
|
|
1450
|
+
input = {
|
|
1451
|
+
...input,
|
|
1452
|
+
payload: {
|
|
1453
|
+
...input.payload,
|
|
1454
|
+
agentConfigPatch: {
|
|
1455
|
+
...input.payload.agentConfigPatch,
|
|
1456
|
+
prompt: input.payload.prompt,
|
|
1457
|
+
},
|
|
1458
|
+
},
|
|
1459
|
+
};
|
|
1460
|
+
if (input.payload.targetSessionId && input.payload.runMode === undefined)
|
|
1461
|
+
input = {
|
|
1462
|
+
...input,
|
|
1463
|
+
payload: { ...input.payload, runMode: "existing_session" },
|
|
1464
|
+
};
|
|
1465
|
+
const requestedMode = input.payload.runMode ?? input.existing.runMode;
|
|
1466
|
+
if (requestedMode === "existing_session") {
|
|
1467
|
+
const config = input.payload.agentConfig ?? input.existing.agentConfig;
|
|
1468
|
+
if (config.knowledgeSource)
|
|
1469
|
+
throw new HTTPException(422, {
|
|
1470
|
+
message: "Source ingestion requires a separate scheduled agent",
|
|
1471
|
+
});
|
|
1472
|
+
const target = await validateScheduledTaskTarget({
|
|
1473
|
+
...input,
|
|
1474
|
+
runMode: "existing_session",
|
|
1475
|
+
targetSessionId: input.payload.targetSessionId ?? input.existing.targetSessionId,
|
|
1476
|
+
variableSetId: null,
|
|
1477
|
+
rigId: null,
|
|
1478
|
+
agentConfig: scheduledSessionMessageConfig(config),
|
|
1479
|
+
});
|
|
1480
|
+
if (input.payload.rigId && input.payload.rigId !== target?.rigId)
|
|
1481
|
+
throw new HTTPException(422, {
|
|
1482
|
+
message:
|
|
1483
|
+
"Existing-chat schedules use the chat's environment. Change the chat's attachments separately.",
|
|
1484
|
+
});
|
|
1485
|
+
const retargeting =
|
|
1486
|
+
input.existing.runMode !== "existing_session" ||
|
|
1487
|
+
target?.id !== input.existing.targetSessionId;
|
|
1488
|
+
const previousTargetSessionId =
|
|
1489
|
+
input.existing.runMode === "existing_session"
|
|
1490
|
+
? input.existing.targetSessionId
|
|
1491
|
+
: input.existing.runMode === "reusable_session"
|
|
1492
|
+
? input.existing.reusableSessionId
|
|
1493
|
+
: null;
|
|
1494
|
+
const previousTarget =
|
|
1495
|
+
retargeting && previousTargetSessionId
|
|
1496
|
+
? await validateScheduledTaskTarget({
|
|
1497
|
+
...input,
|
|
1498
|
+
runMode: "existing_session",
|
|
1499
|
+
targetSessionId: previousTargetSessionId,
|
|
1500
|
+
departingTarget: true,
|
|
1501
|
+
variableSetId: null,
|
|
1502
|
+
rigId: null,
|
|
1503
|
+
agentConfig: scheduledSessionMessageConfig(input.existing.agentConfig),
|
|
1504
|
+
})
|
|
1505
|
+
: null;
|
|
1506
|
+
// Variable Sets were replaced by vaults; only an environment change needs review.
|
|
1507
|
+
const removedVariableSetIds: string[] = [];
|
|
1508
|
+
const previousRigId = previousTarget ? previousTarget.rigId : input.existing.rigId;
|
|
1509
|
+
const changedRig = retargeting && previousRigId && previousRigId !== target?.rigId;
|
|
1510
|
+
if ((removedVariableSetIds.length || changedRig) && !input.payload.adoptSessionSettings) {
|
|
1511
|
+
const detail = {
|
|
1512
|
+
code: "scheduled_target_access_change",
|
|
1513
|
+
targetSessionId: target!.id,
|
|
1514
|
+
removedVariableSetIds,
|
|
1515
|
+
removedVariableSetCount: removedVariableSetIds.length,
|
|
1516
|
+
removedRigId: changedRig ? previousRigId : null,
|
|
1517
|
+
resolution:
|
|
1518
|
+
"Review the destination's access, then retry with adoptSessionSettings=true and the reviewed expectedExecutionDigest to use that chat's attachments.",
|
|
1519
|
+
};
|
|
1520
|
+
throw new HTTPException(409, {
|
|
1521
|
+
message:
|
|
1522
|
+
"The destination chat has different attachments. Review the access change before moving this schedule.",
|
|
1523
|
+
cause: detail,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
// Server-side normalization preserves exact message fields and authority choices;
|
|
1527
|
+
// callers never reconstruct an incomplete get projection to remove old settings.
|
|
1528
|
+
input = {
|
|
1529
|
+
...input,
|
|
1530
|
+
payload: {
|
|
1531
|
+
...input.payload,
|
|
1532
|
+
...(retargeting
|
|
1533
|
+
? {
|
|
1534
|
+
agentConfig: scheduledSessionMessageConfig({
|
|
1535
|
+
...config,
|
|
1536
|
+
...input.payload.agentConfigPatch,
|
|
1537
|
+
prompt: input.payload.agentConfigPatch?.prompt ?? config.prompt,
|
|
1538
|
+
}),
|
|
1539
|
+
agentConfigPatch: undefined,
|
|
1540
|
+
variableSetId: null,
|
|
1541
|
+
rigId: null,
|
|
1542
|
+
}
|
|
1543
|
+
: {}),
|
|
1544
|
+
},
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
824
1547
|
if (input.payload.agentLearning) {
|
|
825
1548
|
const context = input.authorization
|
|
826
1549
|
? await knowledgeContextForAccess(
|
|
@@ -834,8 +1557,15 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
834
1557
|
message: "Only an authenticated person can configure Agent learning",
|
|
835
1558
|
});
|
|
836
1559
|
}
|
|
837
|
-
const update: UpdateScheduledTaskInput = {
|
|
1560
|
+
const update: UpdateScheduledTaskInput = {
|
|
1561
|
+
expectedExecutionDigest: input.existing.executionDigest,
|
|
1562
|
+
};
|
|
838
1563
|
const requestedKnowledgeSource = input.payload.agentConfig?.knowledgeSource ?? null;
|
|
1564
|
+
if (
|
|
1565
|
+
isRetiredNativeAtlassianTask(input.existing) &&
|
|
1566
|
+
(input.payload.status === "active" || requestedKnowledgeSource)
|
|
1567
|
+
)
|
|
1568
|
+
throw new HTTPException(410, { message: ATLASSIAN_NATIVE_RETIRED_MESSAGE });
|
|
839
1569
|
const existingKnowledgeSource = scheduledTaskKnowledgeSource(input.existing);
|
|
840
1570
|
// Editing an ordinary source task's prompt/settings must not orphan its
|
|
841
1571
|
// connector binding. Deleting the task is the explicit source-disable path.
|
|
@@ -885,22 +1615,17 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
885
1615
|
});
|
|
886
1616
|
const existingTarget = input.existing.targetSessionId;
|
|
887
1617
|
const nextRunMode = input.payload.runMode ?? input.existing.runMode;
|
|
1618
|
+
const materializedReusable =
|
|
1619
|
+
input.existing.runMode === "reusable_session" &&
|
|
1620
|
+
nextRunMode === "reusable_session" &&
|
|
1621
|
+
input.existing.reusableSessionId !== null;
|
|
1622
|
+
let reusableTarget: Session | null = null;
|
|
888
1623
|
const nextTargetSessionId =
|
|
889
1624
|
input.payload.targetSessionId !== undefined
|
|
890
1625
|
? input.payload.targetSessionId
|
|
891
1626
|
: nextRunMode === "existing_session"
|
|
892
1627
|
? existingTarget
|
|
893
1628
|
: null;
|
|
894
|
-
if (
|
|
895
|
-
input.existing.runMode === "reusable_session" &&
|
|
896
|
-
input.existing.reusableSessionId &&
|
|
897
|
-
nextRunMode === "existing_session"
|
|
898
|
-
) {
|
|
899
|
-
throw new HTTPException(409, {
|
|
900
|
-
message:
|
|
901
|
-
"cannot target an existing session after this task created its reusable session; create a new task",
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
1629
|
if (input.payload.name !== undefined) {
|
|
905
1630
|
update.name = trimmedScheduledTaskName(input.payload.name);
|
|
906
1631
|
}
|
|
@@ -920,41 +1645,14 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
920
1645
|
if (input.payload.metadata !== undefined) {
|
|
921
1646
|
update.metadata = input.payload.metadata;
|
|
922
1647
|
}
|
|
923
|
-
if (input.payload.
|
|
924
|
-
|
|
925
|
-
if (
|
|
926
|
-
(input.existing.variableSetId ?? null) !== (nextVariableSetId ?? null) &&
|
|
927
|
-
input.existing.runMode === "reusable_session" &&
|
|
928
|
-
input.existing.reusableSessionId
|
|
929
|
-
) {
|
|
930
|
-
throw new HTTPException(409, {
|
|
931
|
-
message:
|
|
932
|
-
"cannot change variableSet of a task with a live reusable session; recreate the task",
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
if (nextVariableSetId === null) {
|
|
936
|
-
if (input.existing.variableSetId !== null) {
|
|
937
|
-
// Detaching is also an attachment change: it strips the secrets a
|
|
938
|
-
// task's instructions were designed around.
|
|
939
|
-
requirePermission(input.grant, "variable-sets:attach");
|
|
940
|
-
}
|
|
941
|
-
update.variableSetId = null;
|
|
942
|
-
} else {
|
|
943
|
-
await validateVariableSetAttachment(
|
|
944
|
-
{ settings: input.settings, db: input.db },
|
|
945
|
-
input.grant,
|
|
946
|
-
input.existing.workspaceId,
|
|
947
|
-
nextVariableSetId,
|
|
948
|
-
);
|
|
949
|
-
update.variableSetId = nextVariableSetId;
|
|
950
|
-
}
|
|
1648
|
+
if (input.payload.vaultPolicy !== undefined) {
|
|
1649
|
+
update.vaultPolicy = scheduledVaultPolicy(input.payload.vaultPolicy);
|
|
951
1650
|
}
|
|
1651
|
+
if (input.payload.variableSetId) throw variableSetsRemovedError("variableSetId");
|
|
1652
|
+
// Clearing a legacy selection stays allowed.
|
|
1653
|
+
if (input.payload.variableSetId === null) update.variableSetId = null;
|
|
952
1654
|
if (input.payload.rigId !== undefined) {
|
|
953
|
-
if (
|
|
954
|
-
input.existing.runMode === "reusable_session" &&
|
|
955
|
-
input.existing.reusableSessionId !== null &&
|
|
956
|
-
input.payload.rigId !== input.existing.rigId
|
|
957
|
-
) {
|
|
1655
|
+
if (materializedReusable && input.payload.rigId !== input.existing.rigId) {
|
|
958
1656
|
throw new HTTPException(409, {
|
|
959
1657
|
message: "A reusable-session task cannot change rigId after materialization; recreate it",
|
|
960
1658
|
});
|
|
@@ -972,17 +1670,44 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
972
1670
|
}
|
|
973
1671
|
update.rigId = input.payload.rigId;
|
|
974
1672
|
}
|
|
975
|
-
if (input.payload.agentConfig !== undefined) {
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1673
|
+
if (input.payload.agentConfig !== undefined || input.payload.agentConfigPatch !== undefined) {
|
|
1674
|
+
// A materialized reusable chat must still exist to take new instructions.
|
|
1675
|
+
if (materializedReusable) {
|
|
1676
|
+
reusableTarget = await getSession(
|
|
1677
|
+
input.db,
|
|
1678
|
+
input.existing.workspaceId,
|
|
1679
|
+
input.existing.reusableSessionId!,
|
|
1680
|
+
);
|
|
1681
|
+
if (!reusableTarget || reusableTarget.accountId !== input.existing.accountId)
|
|
1682
|
+
throw new HTTPException(409, { message: "Scheduled task chat is unavailable" });
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (input.payload.agentConfigPatch) {
|
|
1686
|
+
const patch = input.payload.agentConfigPatch;
|
|
1687
|
+
const model =
|
|
1688
|
+
patch.model === undefined ? undefined : canonicalConfiguredModel(input.settings, patch.model);
|
|
1689
|
+
if (model !== undefined && model !== null) {
|
|
1690
|
+
await assertWorkspaceModelPolicyAllows(
|
|
1691
|
+
input.db,
|
|
1692
|
+
input.settings,
|
|
1693
|
+
input.existing.workspaceId,
|
|
1694
|
+
model,
|
|
1695
|
+
);
|
|
985
1696
|
}
|
|
1697
|
+
// Validate only the newly supplied model settings, not a reconstructed
|
|
1698
|
+
// bounded input. Legacy stored text, resources and selections stay exact.
|
|
1699
|
+
update.agentConfig = {
|
|
1700
|
+
...input.existing.agentConfig,
|
|
1701
|
+
...(patch.prompt !== undefined ? { prompt: patch.prompt } : {}),
|
|
1702
|
+
...(model !== undefined && model !== null ? { model } : {}),
|
|
1703
|
+
...(patch.reasoningEffort !== undefined ? { reasoningEffort: patch.reasoningEffort } : {}),
|
|
1704
|
+
};
|
|
1705
|
+
// Validation performs asynchronous authority checks before persistence.
|
|
1706
|
+
// Reuse the locked-row CAS so that merging this snapshot cannot erase a
|
|
1707
|
+
// concurrent config edit. The caller receives 409, never an automatic retry.
|
|
1708
|
+
update.expectedExecutionDigest = input.existing.executionDigest;
|
|
1709
|
+
}
|
|
1710
|
+
if (input.payload.agentConfig !== undefined) {
|
|
986
1711
|
const nextAgentConfig = await validateScheduledTaskAgentConfig({
|
|
987
1712
|
settings: input.settings,
|
|
988
1713
|
db: input.db,
|
|
@@ -999,21 +1724,105 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
999
1724
|
: input.existing.reusableSessionId,
|
|
1000
1725
|
},
|
|
1001
1726
|
...(input.toolsProvided !== undefined ? { toolsProvided: input.toolsProvided } : {}),
|
|
1727
|
+
...(materializedReusable ? { retainedCreationConfig: input.existing.agentConfig } : {}),
|
|
1728
|
+
...(input.existing.agentConfig.toolPolicy
|
|
1729
|
+
? { existingToolPolicy: input.existing.agentConfig.toolPolicy }
|
|
1730
|
+
: {}),
|
|
1002
1731
|
});
|
|
1003
1732
|
if (
|
|
1004
|
-
|
|
1005
|
-
input.existing.runMode === "reusable_session" &&
|
|
1733
|
+
materializedReusable &&
|
|
1006
1734
|
(input.existing.agentConfig.slackBotConnectionId ?? null) !==
|
|
1007
1735
|
(nextAgentConfig.slackBotConnectionId ?? null)
|
|
1008
1736
|
) {
|
|
1009
1737
|
throw new HTTPException(409, {
|
|
1010
1738
|
message:
|
|
1011
|
-
"cannot change the
|
|
1739
|
+
"cannot change the Opengeni Slack bot connection of a task with a live reusable session; recreate the task",
|
|
1012
1740
|
});
|
|
1013
1741
|
}
|
|
1742
|
+
// Update text is exact, even when a full form submission reuses the saved
|
|
1743
|
+
// prompt while changing another setting. Creation keeps its trim convention.
|
|
1744
|
+
nextAgentConfig.prompt = retainedPrompt ?? input.payload.agentConfig.prompt;
|
|
1745
|
+
if (retainedPrompt !== undefined) {
|
|
1746
|
+
nextAgentConfig.resources = input.existing.agentConfig.resources;
|
|
1747
|
+
}
|
|
1748
|
+
// The Site a schedule was created from stays recorded; an update never
|
|
1749
|
+
// sets, changes or clears it.
|
|
1750
|
+
const recordedOrigin = input.existing.agentConfig.metadata?.[SITE_ORIGIN_METADATA_KEY];
|
|
1751
|
+
nextAgentConfig.metadata = {
|
|
1752
|
+
...withSiteOriginMetadata(nextAgentConfig.metadata ?? {}, null),
|
|
1753
|
+
...(recordedOrigin !== undefined ? { [SITE_ORIGIN_METADATA_KEY]: recordedOrigin } : {}),
|
|
1754
|
+
};
|
|
1014
1755
|
update.agentConfig = nextAgentConfig;
|
|
1015
1756
|
}
|
|
1016
|
-
|
|
1757
|
+
// Pins are the only saved account choice, a person's explicit one, and only
|
|
1758
|
+
// a schedule that starts its own session has them. A full config replacement
|
|
1759
|
+
// keeps them unless the request changes them; moving into an existing chat
|
|
1760
|
+
// drops them, because that chat's connectors and the owner's current
|
|
1761
|
+
// accounts apply.
|
|
1762
|
+
const pinsProvided =
|
|
1763
|
+
input.payload.pinnedAccounts !== undefined ||
|
|
1764
|
+
input.payload.pinnedAccountsRequired !== undefined;
|
|
1765
|
+
if (pinsProvided) {
|
|
1766
|
+
if (
|
|
1767
|
+
nextRunMode === "existing_session" &&
|
|
1768
|
+
((input.payload.pinnedAccounts?.length ?? 0) > 0 ||
|
|
1769
|
+
input.payload.pinnedAccountsRequired === true)
|
|
1770
|
+
)
|
|
1771
|
+
throw new HTTPException(422, {
|
|
1772
|
+
message:
|
|
1773
|
+
"Pin connector accounts only on a schedule that starts its own session. An existing-chat schedule uses that chat's connectors with the owner's current accounts.",
|
|
1774
|
+
});
|
|
1775
|
+
requireScheduledTaskPinAuthor(input.grant);
|
|
1776
|
+
}
|
|
1777
|
+
if (update.agentConfig || pinsProvided || nextRunMode === "existing_session") {
|
|
1778
|
+
const base = update.agentConfig ?? input.existing.agentConfig;
|
|
1779
|
+
const pins =
|
|
1780
|
+
nextRunMode === "existing_session"
|
|
1781
|
+
? []
|
|
1782
|
+
: (input.payload.pinnedAccounts ?? input.existing.agentConfig.pinnedAccounts ?? []);
|
|
1783
|
+
const required =
|
|
1784
|
+
input.payload.pinnedAccountsRequired ??
|
|
1785
|
+
input.existing.agentConfig.pinnedAccountsRequired === true;
|
|
1786
|
+
const next = withScheduledTaskPins(base, pins, required);
|
|
1787
|
+
if (update.agentConfig || !isDeepStrictEqual(next, input.existing.agentConfig))
|
|
1788
|
+
update.agentConfig = next;
|
|
1789
|
+
}
|
|
1790
|
+
const nextAgentConfig = {
|
|
1791
|
+
...(update.agentConfig ?? input.existing.agentConfig),
|
|
1792
|
+
};
|
|
1793
|
+
// Retargeting rewrites a narrow patch into a full config above. Check the
|
|
1794
|
+
// exact final message after restoring retained fields, regardless of path.
|
|
1795
|
+
// An unedited legacy prompt remains movable even when above today's limit.
|
|
1796
|
+
if (
|
|
1797
|
+
requestedPrompt !== undefined &&
|
|
1798
|
+
scheduledOccurrencePayloadUtf8Bytes(nextAgentConfig) >
|
|
1799
|
+
SCHEDULED_TASK_OCCURRENCE_PAYLOAD_MAX_BYTES -
|
|
1800
|
+
SCHEDULED_TASK_OCCURRENCE_PAYLOAD_INGRESS_HEADROOM_BYTES
|
|
1801
|
+
)
|
|
1802
|
+
throw new HTTPException(422, {
|
|
1803
|
+
message: "Updated scheduled message and attachments exceed the supported occurrence payload",
|
|
1804
|
+
});
|
|
1805
|
+
await validateScheduledTaskSlackChannel({
|
|
1806
|
+
grant: input.grant,
|
|
1807
|
+
authorization: input.authorization,
|
|
1808
|
+
previous: input.existing.agentConfig,
|
|
1809
|
+
next: nextAgentConfig,
|
|
1810
|
+
runMode: nextRunMode,
|
|
1811
|
+
reusableSessionCanPost:
|
|
1812
|
+
input.existing.runMode === "reusable_session" && input.existing.reusableSessionId !== null
|
|
1813
|
+
? async () => {
|
|
1814
|
+
const chat = await getSession(
|
|
1815
|
+
input.db,
|
|
1816
|
+
input.existing.workspaceId,
|
|
1817
|
+
input.existing.reusableSessionId!,
|
|
1818
|
+
);
|
|
1819
|
+
return SCHEDULED_SLACK_BOT_POSTING_TOOLS.every(
|
|
1820
|
+
(tool) => chat?.firstPartyMcpTools?.includes(tool) === true,
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
: null,
|
|
1824
|
+
verifySlackChannel: input.verifySlackChannel,
|
|
1825
|
+
});
|
|
1017
1826
|
const authorityTargetChanged =
|
|
1018
1827
|
nextRunMode !== input.existing.runMode ||
|
|
1019
1828
|
nextTargetSessionId !== input.existing.targetSessionId ||
|
|
@@ -1022,8 +1831,7 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
1022
1831
|
(input.payload.rigId !== undefined && input.payload.rigId !== input.existing.rigId);
|
|
1023
1832
|
const materialExecutionChange =
|
|
1024
1833
|
authorityTargetChanged ||
|
|
1025
|
-
|
|
1026
|
-
input.payload.connectionAuthorities !== undefined ||
|
|
1834
|
+
pinsProvided ||
|
|
1027
1835
|
!isDeepStrictEqual(nextAgentConfig, input.existing.agentConfig) ||
|
|
1028
1836
|
(input.payload.action !== undefined &&
|
|
1029
1837
|
!isDeepStrictEqual(input.payload.action, input.existing.action)) ||
|
|
@@ -1034,15 +1842,31 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
1034
1842
|
(input.payload.metadata !== undefined &&
|
|
1035
1843
|
!isDeepStrictEqual(input.payload.metadata, input.existing.metadata)) ||
|
|
1036
1844
|
(input.existing.status === "paused" && input.payload.status === "active");
|
|
1037
|
-
if (
|
|
1845
|
+
if (
|
|
1846
|
+
materialExecutionChange &&
|
|
1847
|
+
nextRunMode !== "existing_session" &&
|
|
1848
|
+
(!materializedReusable || nextAgentConfig.model !== input.existing.agentConfig.model)
|
|
1849
|
+
) {
|
|
1038
1850
|
const beforeUpdateCommit = workspaceCustomModelCommitGuard({
|
|
1039
1851
|
settings: input.settings,
|
|
1040
1852
|
accountId: input.existing.accountId,
|
|
1041
1853
|
workspaceId: input.existing.workspaceId,
|
|
1042
1854
|
modelId: nextAgentConfig.model ?? input.settings.openaiModel,
|
|
1855
|
+
claudeAuthoritySnapshot: await getScheduledTaskClaudeProviderAccountAuthoritySnapshot(
|
|
1856
|
+
input.db,
|
|
1857
|
+
input.existing.workspaceId,
|
|
1858
|
+
input.existing.id,
|
|
1859
|
+
),
|
|
1860
|
+
scheduledTaskId: input.existing.id,
|
|
1043
1861
|
});
|
|
1044
1862
|
if (beforeUpdateCommit) update.beforeUpdateCommit = beforeUpdateCommit;
|
|
1045
1863
|
}
|
|
1864
|
+
// A task accepted with a user-scoped SuperGrok v1 snapshot keeps its
|
|
1865
|
+
// causal human: the scheduled dispatch and the deployed admission fence
|
|
1866
|
+
// (`admit_scheduled_agent_run_execution`) still take the run's causal
|
|
1867
|
+
// subject from that stored snapshot, so a material edit by anyone else
|
|
1868
|
+
// would leave every later occurrence refused. Reading the stored value is
|
|
1869
|
+
// not deriving it from live state; it retires with that fence.
|
|
1046
1870
|
const existingXaiAuthority = await getScheduledTaskXaiProviderAccountAuthoritySnapshot(
|
|
1047
1871
|
input.db,
|
|
1048
1872
|
input.existing.workspaceId,
|
|
@@ -1058,67 +1882,16 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
1058
1882
|
message: "changing a user-scoped xAI scheduled task requires the same causal human",
|
|
1059
1883
|
});
|
|
1060
1884
|
}
|
|
1061
|
-
|
|
1062
|
-
input.db,
|
|
1063
|
-
input.existing.
|
|
1064
|
-
input.existing.id,
|
|
1065
|
-
);
|
|
1066
|
-
if (input.payload.connectionAuthorities === undefined) {
|
|
1067
|
-
if (
|
|
1068
|
-
materialExecutionChange &&
|
|
1069
|
-
existingDelegations.some((delegation) => delegation.connectionType === "github_personal")
|
|
1070
|
-
) {
|
|
1071
|
-
throw new HTTPException(409, {
|
|
1072
|
-
message:
|
|
1073
|
-
"material changes to a personal GitHub-authorized task require explicit connectionAuthorities",
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
if (
|
|
1077
|
-
existingDelegations.length > 0 &&
|
|
1078
|
-
!isDeepStrictEqual(nextAgentConfig.tools, input.existing.agentConfig.tools)
|
|
1079
|
-
) {
|
|
1080
|
-
throw new HTTPException(409, {
|
|
1081
|
-
message:
|
|
1082
|
-
"changing tools on a connection-authorized task requires explicit connectionAuthorities",
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
if (existingDelegations.length > 0) {
|
|
1086
|
-
if (
|
|
1087
|
-
materialExecutionChange &&
|
|
1088
|
-
existingDelegations.some(
|
|
1089
|
-
(delegation) => delegation.ownerSubjectId !== input.grant.subjectId,
|
|
1090
|
-
)
|
|
1091
|
-
) {
|
|
1092
|
-
throw new HTTPException(409, {
|
|
1093
|
-
message:
|
|
1094
|
-
"preserving connection authority requires the same causal human; provide a new explicit selection",
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
if (authorityTargetChanged) {
|
|
1098
|
-
update.cloneConnectionAuthorityFromRevision = input.existing.authorityRevision;
|
|
1099
|
-
} else {
|
|
1100
|
-
update.clonePersonalResourceAuthorityFromRevision = input.existing.authorityRevision;
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
} else if (input.payload.connectionAuthorities.length === 0) {
|
|
1104
|
-
if (personalGitHubRepositoryResources(nextAgentConfig.resources).length > 0) {
|
|
1105
|
-
throw new HTTPException(409, {
|
|
1106
|
-
message:
|
|
1107
|
-
"personal GitHub repository resources cannot be retained without connectionAuthorities",
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
update.personalConnectionDelegations = [];
|
|
1111
|
-
} else {
|
|
1885
|
+
if (materialExecutionChange) {
|
|
1886
|
+
await assertScheduledTaskMutationOwner(input.db, input.grant, input.existing.id);
|
|
1887
|
+
const ownerSubjectId = input.existing.ownerSubjectId;
|
|
1112
1888
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
1113
1889
|
input.db,
|
|
1114
|
-
// Same object as the subject just below, and as the freeze call further
|
|
1115
|
-
// down: the task was loaded with `getScheduledTask(db, grant.workspaceId,
|
|
1116
|
-
// ...)`, so this is `input.existing.workspaceId`, sourced consistently.
|
|
1117
1890
|
input.grant.workspaceId,
|
|
1118
1891
|
input.settings,
|
|
1119
|
-
{ subjectId:
|
|
1892
|
+
ownerSubjectId ? { subjectId: ownerSubjectId } : {},
|
|
1120
1893
|
);
|
|
1121
|
-
|
|
1894
|
+
let nextTarget = await validateScheduledTaskTarget({
|
|
1122
1895
|
db: input.db,
|
|
1123
1896
|
sessionAuthorization: input.sessionAuthorization,
|
|
1124
1897
|
authorizationSurface: input.authorizationSurface,
|
|
@@ -1132,26 +1905,120 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
1132
1905
|
rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
|
|
1133
1906
|
agentConfig: nextAgentConfig,
|
|
1134
1907
|
});
|
|
1135
|
-
|
|
1908
|
+
if (materializedReusable) {
|
|
1909
|
+
nextTarget =
|
|
1910
|
+
reusableTarget ??
|
|
1911
|
+
(await getSession(input.db, input.existing.workspaceId, input.existing.reusableSessionId!));
|
|
1912
|
+
if (!nextTarget || nextTarget.accountId !== input.existing.accountId)
|
|
1913
|
+
throw new HTTPException(409, { message: "Scheduled task chat is unavailable" });
|
|
1914
|
+
}
|
|
1915
|
+
const updateWorkspaceDefaults = resolveWorkspaceSessionToolDefaults(
|
|
1916
|
+
(await requireWorkspace(input.db, input.grant.workspaceId)).settings,
|
|
1917
|
+
);
|
|
1918
|
+
// Pins are judged against what a run gets: its own tools plus the
|
|
1919
|
+
// workspace defaults it follows.
|
|
1920
|
+
const runToolsFor = (config: ScheduledTaskAgentConfig) =>
|
|
1921
|
+
scheduledTaskRunTools({
|
|
1922
|
+
agentConfig: config,
|
|
1923
|
+
settings: input.settings,
|
|
1924
|
+
runtimeSettings,
|
|
1925
|
+
workspaceDefaults: updateWorkspaceDefaults,
|
|
1926
|
+
});
|
|
1927
|
+
const nextRunTools = runToolsFor(nextAgentConfig);
|
|
1928
|
+
const priorRunTools = runToolsFor(input.existing.agentConfig);
|
|
1929
|
+
const nextConnectionTools = await scheduledConnectionTools(
|
|
1930
|
+
input.db,
|
|
1931
|
+
input.grant.workspaceId,
|
|
1932
|
+
runtimeSettings,
|
|
1933
|
+
nextTarget,
|
|
1934
|
+
nextRunTools,
|
|
1935
|
+
ownerSubjectId ?? undefined,
|
|
1936
|
+
);
|
|
1937
|
+
const priorTargetSessionId =
|
|
1938
|
+
input.existing.runMode === "existing_session"
|
|
1939
|
+
? input.existing.targetSessionId
|
|
1940
|
+
: input.existing.runMode === "reusable_session"
|
|
1941
|
+
? input.existing.reusableSessionId
|
|
1942
|
+
: null;
|
|
1943
|
+
const priorTarget = priorTargetSessionId
|
|
1944
|
+
? priorTargetSessionId === nextTarget?.id
|
|
1945
|
+
? nextTarget
|
|
1946
|
+
: await getSession(input.db, input.existing.workspaceId, priorTargetSessionId)
|
|
1947
|
+
: null;
|
|
1948
|
+
const priorConnectionResources = mergeResourceRefs(
|
|
1949
|
+
priorTarget?.resources ?? [],
|
|
1950
|
+
input.existing.agentConfig.resources,
|
|
1951
|
+
);
|
|
1952
|
+
const nextConnectionResources = mergeResourceRefs(
|
|
1953
|
+
nextTarget?.resources ?? [],
|
|
1954
|
+
nextAgentConfig.resources,
|
|
1955
|
+
);
|
|
1956
|
+
const priorConnectionTools = await scheduledConnectionTools(
|
|
1957
|
+
input.db,
|
|
1958
|
+
input.grant.workspaceId,
|
|
1959
|
+
runtimeSettings,
|
|
1960
|
+
priorTarget,
|
|
1961
|
+
priorRunTools,
|
|
1962
|
+
ownerSubjectId ?? undefined,
|
|
1963
|
+
);
|
|
1964
|
+
const priorAccountSurfaceIds = new Set(
|
|
1965
|
+
[...priorRunTools, ...(priorTarget?.tools ?? []), ...priorConnectionTools]
|
|
1966
|
+
.filter((tool) => tool.kind === "mcp")
|
|
1967
|
+
.map((tool) => tool.id),
|
|
1968
|
+
);
|
|
1969
|
+
if (personalGitHubRepositoryResources(priorConnectionResources).length)
|
|
1970
|
+
priorAccountSurfaceIds.add(PERSONAL_GITHUB_CONNECTION_SURFACE_ID);
|
|
1971
|
+
// Compared like for like with the prior surfaces: a connector the task or
|
|
1972
|
+
// its chat still names keeps its pin even while the chat has it turned
|
|
1973
|
+
// off, so only an edit that removes the connector removes the pin.
|
|
1974
|
+
const nextAccountSurfaceIds = new Set(
|
|
1975
|
+
[...nextRunTools, ...(nextTarget?.tools ?? []), ...nextConnectionTools]
|
|
1976
|
+
.filter((tool) => tool.kind === "mcp")
|
|
1977
|
+
.map((tool) => tool.id),
|
|
1978
|
+
);
|
|
1979
|
+
const nextSurfaceEligibility = scheduledConnectionSurfaceEligibility(
|
|
1980
|
+
runtimeSettings,
|
|
1981
|
+
nextTarget,
|
|
1982
|
+
(await requireWorkspace(input.db, input.grant.workspaceId)).settings,
|
|
1983
|
+
);
|
|
1984
|
+
if (nextSurfaceEligibility.googleDrivePublicationEnabled)
|
|
1985
|
+
nextAccountSurfaceIds.add(GOOGLE_DRIVE_PUBLICATION_SERVER_ID);
|
|
1986
|
+
if (personalGitHubRepositoryResources(nextConnectionResources).length)
|
|
1987
|
+
nextAccountSurfaceIds.add(PERSONAL_GITHUB_CONNECTION_SURFACE_ID);
|
|
1988
|
+
// Removing a connector or repository also removes its pin. Pins the
|
|
1989
|
+
// request supplies are validated strictly; retained pins are kept even
|
|
1990
|
+
// when their account is gone, so an edit never fails on a stale pin (the
|
|
1991
|
+
// run goes without that connector and the owner is told).
|
|
1992
|
+
const keptPins = (nextAgentConfig.pinnedAccounts ?? []).filter(
|
|
1993
|
+
(pin) =>
|
|
1994
|
+
input.payload.pinnedAccounts !== undefined ||
|
|
1995
|
+
nextAccountSurfaceIds.has(pin.serverId) ||
|
|
1996
|
+
!priorAccountSurfaceIds.has(pin.serverId),
|
|
1997
|
+
);
|
|
1998
|
+
await freezeConnectionAccounts({
|
|
1136
1999
|
db: input.db,
|
|
1137
|
-
|
|
1138
|
-
// loaded with `getScheduledTask(db, grant.workspaceId, ...)`, so this is
|
|
1139
|
-
// the same workspace as `input.existing.workspaceId`; taking it from the
|
|
1140
|
-
// grant means the workspace can never drift from the `accountId` that
|
|
1141
|
-
// `personalConnectionDelegationSourceForGrant` reads off the same grant.
|
|
2000
|
+
accountId: input.grant.accountId,
|
|
1142
2001
|
workspaceId: input.grant.workspaceId,
|
|
1143
|
-
settings:
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
2002
|
+
settings: nextTarget
|
|
2003
|
+
? settingsWithSessionMcpServerMetadata(runtimeSettings, nextTarget.mcpServers)
|
|
2004
|
+
: runtimeSettings,
|
|
2005
|
+
tools: nextConnectionTools,
|
|
2006
|
+
resources: nextConnectionResources,
|
|
2007
|
+
source: ownerSubjectId
|
|
2008
|
+
? { kind: "subject", subjectId: ownerSubjectId, accountId: input.existing.accountId }
|
|
2009
|
+
: { kind: "none" },
|
|
2010
|
+
authoritySelections: input.payload.pinnedAccounts ?? [],
|
|
2011
|
+
...nextSurfaceEligibility,
|
|
1150
2012
|
});
|
|
2013
|
+
update.agentConfig = withScheduledTaskPins(
|
|
2014
|
+
nextAgentConfig,
|
|
2015
|
+
keptPins,
|
|
2016
|
+
nextAgentConfig.pinnedAccountsRequired === true,
|
|
2017
|
+
);
|
|
1151
2018
|
}
|
|
1152
2019
|
if (
|
|
1153
2020
|
!materialExecutionChange &&
|
|
1154
|
-
|
|
2021
|
+
!pinsProvided &&
|
|
1155
2022
|
update.clonePersonalResourceAuthorityFromRevision === undefined
|
|
1156
2023
|
) {
|
|
1157
2024
|
// Administrative lifecycle/name edits preserve the exact revision-bound
|
|
@@ -1169,6 +2036,7 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
1169
2036
|
authorizationSurface: input.authorizationSurface,
|
|
1170
2037
|
grant: input.grant,
|
|
1171
2038
|
targetSessionId: existingTarget,
|
|
2039
|
+
departingTarget: true,
|
|
1172
2040
|
runMode: "existing_session",
|
|
1173
2041
|
variableSetId: input.existing.variableSetId,
|
|
1174
2042
|
rigId: input.existing.rigId,
|
|
@@ -1189,13 +2057,24 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
1189
2057
|
rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
|
|
1190
2058
|
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
1191
2059
|
});
|
|
1192
|
-
if (
|
|
2060
|
+
if (
|
|
2061
|
+
!knowledgeSource &&
|
|
2062
|
+
!(
|
|
2063
|
+
materializedReusable &&
|
|
2064
|
+
isDeepStrictEqual(nextAgentConfig.machineTarget, input.existing.agentConfig.machineTarget) &&
|
|
2065
|
+
nextAgentConfig.sandboxBackend === input.existing.agentConfig.sandboxBackend
|
|
2066
|
+
)
|
|
2067
|
+
) {
|
|
1193
2068
|
await validateScheduledTaskMachineTarget({
|
|
1194
2069
|
settings: input.settings,
|
|
1195
2070
|
db: input.db,
|
|
1196
2071
|
grant: input.grant,
|
|
1197
2072
|
runMode: nextRunMode,
|
|
1198
2073
|
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
2074
|
+
audience:
|
|
2075
|
+
(input.payload.visibility ?? input.existing.visibility) === "private"
|
|
2076
|
+
? "user_private"
|
|
2077
|
+
: "workspace_shared",
|
|
1199
2078
|
});
|
|
1200
2079
|
}
|
|
1201
2080
|
if (
|
|
@@ -1209,28 +2088,9 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
1209
2088
|
Object.assign(update, scheduledTaskAuthorityUpdateForGrant(input.grant));
|
|
1210
2089
|
const linkCapture = prepareExternalLinkTaskAdmission(
|
|
1211
2090
|
input.authorization,
|
|
1212
|
-
|
|
2091
|
+
scheduledTaskInitiatorForGrant(input.grant).actor,
|
|
1213
2092
|
);
|
|
1214
2093
|
if (linkCapture) update.captureLinkAuthority = linkCapture;
|
|
1215
|
-
if (input.payload.selectedHostMcpDelegations !== undefined) {
|
|
1216
|
-
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
1217
|
-
input.db,
|
|
1218
|
-
input.grant.workspaceId,
|
|
1219
|
-
input.settings,
|
|
1220
|
-
{ subjectId: input.grant.subjectId },
|
|
1221
|
-
);
|
|
1222
|
-
const target =
|
|
1223
|
-
nextRunMode === "existing_session" && nextTargetSessionId
|
|
1224
|
-
? await getSession(input.db, input.grant.workspaceId, nextTargetSessionId)
|
|
1225
|
-
: null;
|
|
1226
|
-
update.captureHostAuthority = prepareHostMcpTaskAdmission({
|
|
1227
|
-
settings: runtimeSettings,
|
|
1228
|
-
tools: target?.tools ?? nextAgentConfig.tools,
|
|
1229
|
-
grant: input.grant,
|
|
1230
|
-
...(input.authorization ? { authorization: input.authorization } : {}),
|
|
1231
|
-
selections: input.payload.selectedHostMcpDelegations,
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
2094
|
if (update.clonePersonalResourceAuthorityFromRevision !== undefined) {
|
|
1235
2095
|
update.refreshPersonalResourceAuthority = false;
|
|
1236
2096
|
}
|
|
@@ -1251,7 +2111,6 @@ export async function requireScheduledTaskForApi(
|
|
|
1251
2111
|
|
|
1252
2112
|
export type ScheduledTaskRestoreState = {
|
|
1253
2113
|
task: ScheduledTask;
|
|
1254
|
-
personalConnectionDelegations: McpPersonalConnectionDelegation[];
|
|
1255
2114
|
learning?: {
|
|
1256
2115
|
context: KnowledgeContext;
|
|
1257
2116
|
scope: "workspace" | "personal";
|
|
@@ -1261,17 +2120,10 @@ export type ScheduledTaskRestoreState = {
|
|
|
1261
2120
|
};
|
|
1262
2121
|
|
|
1263
2122
|
export async function captureScheduledTaskRestoreState(
|
|
1264
|
-
|
|
2123
|
+
_db: Database,
|
|
1265
2124
|
task: ScheduledTask,
|
|
1266
2125
|
): Promise<ScheduledTaskRestoreState> {
|
|
1267
|
-
return {
|
|
1268
|
-
task,
|
|
1269
|
-
personalConnectionDelegations: await getScheduledTaskPersonalConnectionDelegations(
|
|
1270
|
-
db,
|
|
1271
|
-
task.workspaceId,
|
|
1272
|
-
task.id,
|
|
1273
|
-
),
|
|
1274
|
-
};
|
|
2126
|
+
return { task };
|
|
1275
2127
|
}
|
|
1276
2128
|
|
|
1277
2129
|
export async function restoreScheduledTask(
|
|
@@ -1287,7 +2139,6 @@ export async function restoreScheduledTask(
|
|
|
1287
2139
|
overlapPolicy: task.overlapPolicy,
|
|
1288
2140
|
action: task.action,
|
|
1289
2141
|
agentConfig: task.agentConfig,
|
|
1290
|
-
personalConnectionDelegations: previous.personalConnectionDelegations,
|
|
1291
2142
|
...(task.runMode === "existing_session"
|
|
1292
2143
|
? { targetSessionId: task.targetSessionId }
|
|
1293
2144
|
: { reusableSessionId: task.reusableSessionId }),
|
|
@@ -1303,6 +2154,8 @@ async function validateKnowledgeSourceSyncAction(input: {
|
|
|
1303
2154
|
grant: AccessGrant;
|
|
1304
2155
|
action: KnowledgeSourceSyncAction;
|
|
1305
2156
|
}): Promise<void> {
|
|
2157
|
+
if (isRetiredNativeAtlassianSource(input.action))
|
|
2158
|
+
throw new HTTPException(410, { message: ATLASSIAN_NATIVE_RETIRED_MESSAGE });
|
|
1306
2159
|
if (input.action.initiatingSubjectId !== input.grant.subjectId) {
|
|
1307
2160
|
throw new HTTPException(403, {
|
|
1308
2161
|
message: "knowledge source sync must preserve the exact initiating subject",
|
|
@@ -1377,6 +2230,7 @@ export async function deleteScheduledTaskLifecycle(input: {
|
|
|
1377
2230
|
workspaceId: string;
|
|
1378
2231
|
taskId: string;
|
|
1379
2232
|
subjectId: string;
|
|
2233
|
+
beforeDeleteCommit?: (tx: Database) => Promise<void>;
|
|
1380
2234
|
preflightConnectorAuthorization: (task: ScheduledTask) => Promise<void>;
|
|
1381
2235
|
cleanupConnectorAuthorization: (db: Database, task: ScheduledTask) => Promise<void>;
|
|
1382
2236
|
processCleanupClaims?: (claims: readonly TemporalScheduleCleanupClaim[]) => Promise<void>;
|
|
@@ -1396,6 +2250,7 @@ export async function deleteScheduledTaskLifecycle(input: {
|
|
|
1396
2250
|
input.taskId,
|
|
1397
2251
|
);
|
|
1398
2252
|
if (!task) throw new HTTPException(404, { message: "Scheduled task not found" });
|
|
2253
|
+
await input.beforeDeleteCommit?.(tx);
|
|
1399
2254
|
const wasLive = task.deletedAt === null;
|
|
1400
2255
|
if (
|
|
1401
2256
|
scheduledTaskKnowledgeSource(task) &&
|
|
@@ -1437,21 +2292,49 @@ export async function syncCreatedScheduledTask(input: {
|
|
|
1437
2292
|
task: ScheduledTask;
|
|
1438
2293
|
}): Promise<void> {
|
|
1439
2294
|
try {
|
|
1440
|
-
await input.workflowClient.syncScheduledTask({
|
|
2295
|
+
await input.workflowClient.syncScheduledTask({
|
|
2296
|
+
task: input.task,
|
|
2297
|
+
onFailure: async (db, error) => {
|
|
2298
|
+
let persistenceRestored = true;
|
|
2299
|
+
try {
|
|
2300
|
+
// The synchronizer keeps its lock through compensation and commit.
|
|
2301
|
+
// A savepoint also rolls back a failed compensation independently.
|
|
2302
|
+
await db.transaction(async (tx) => {
|
|
2303
|
+
await requireUnchangedScheduledTaskForCompensation(tx, input.task);
|
|
2304
|
+
await deleteScheduledTask(tx, input.task.workspaceId, input.task.id);
|
|
2305
|
+
});
|
|
2306
|
+
} catch {
|
|
2307
|
+
persistenceRestored = false;
|
|
2308
|
+
}
|
|
2309
|
+
return new ScheduledTaskSyncError(error, persistenceRestored);
|
|
2310
|
+
},
|
|
2311
|
+
});
|
|
1441
2312
|
} catch (error) {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
// it; this is intentionally not the user-requested deletion lifecycle.
|
|
1447
|
-
await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id);
|
|
1448
|
-
} catch {
|
|
1449
|
-
persistenceRestored = false;
|
|
1450
|
-
}
|
|
1451
|
-
throw new ScheduledTaskSyncError(error, persistenceRestored);
|
|
2313
|
+
if (error instanceof ScheduledTaskSyncError) throw error;
|
|
2314
|
+
// Lock admission or transaction commit failed. No committed restoration
|
|
2315
|
+
// was acknowledged; never compensate outside the synchronizer's lock.
|
|
2316
|
+
throw new ScheduledTaskSyncError(error, false);
|
|
1452
2317
|
}
|
|
1453
2318
|
}
|
|
1454
2319
|
|
|
2320
|
+
async function requireUnchangedScheduledTaskForCompensation(
|
|
2321
|
+
db: Database,
|
|
2322
|
+
expected: ScheduledTask,
|
|
2323
|
+
): Promise<void> {
|
|
2324
|
+
const current = await getScheduledTaskIncludingDeletedForUpdate(
|
|
2325
|
+
db,
|
|
2326
|
+
expected.workspaceId,
|
|
2327
|
+
expected.id,
|
|
2328
|
+
);
|
|
2329
|
+
if (!current) throw new ScheduledTaskHeadChangedError();
|
|
2330
|
+
const { deletedAt, ...task } = current;
|
|
2331
|
+
// The execution digest excludes names and lifecycle state. Compare the full
|
|
2332
|
+
// saved row under its write lock so a late sync failure cannot undo a newer
|
|
2333
|
+
// edit, pause, materialization or authority revision.
|
|
2334
|
+
if (deletedAt !== null || !isDeepStrictEqual(task, expected))
|
|
2335
|
+
throw new ScheduledTaskHeadChangedError();
|
|
2336
|
+
}
|
|
2337
|
+
|
|
1455
2338
|
export async function syncUpdatedScheduledTask(input: {
|
|
1456
2339
|
db: Database;
|
|
1457
2340
|
workflowClient: SessionWorkflowClient;
|
|
@@ -1459,33 +2342,42 @@ export async function syncUpdatedScheduledTask(input: {
|
|
|
1459
2342
|
task: ScheduledTask;
|
|
1460
2343
|
}): Promise<void> {
|
|
1461
2344
|
try {
|
|
1462
|
-
await input.workflowClient.syncScheduledTask({
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
2345
|
+
await input.workflowClient.syncScheduledTask({
|
|
2346
|
+
task: input.task,
|
|
2347
|
+
onFailure: async (db, error) => {
|
|
2348
|
+
let persistenceRestored = true;
|
|
2349
|
+
try {
|
|
2350
|
+
await db.transaction(async (tx) => {
|
|
2351
|
+
const learning = input.previous.learning;
|
|
2352
|
+
// Compensate only our exact settings version, while the updated task
|
|
2353
|
+
// still names that owner scope. A concurrent edit is never overwritten.
|
|
2354
|
+
if (learning)
|
|
2355
|
+
await saveAgentLearningSettings(tx, learning.context, {
|
|
2356
|
+
scope: learning.scope,
|
|
2357
|
+
source: { kind: "scheduled_task", id: input.task.id },
|
|
2358
|
+
operationId: crypto.randomUUID(),
|
|
2359
|
+
expectedVersion: learning.expectedVersion,
|
|
2360
|
+
settings: {
|
|
2361
|
+
knowledge: "inherit",
|
|
2362
|
+
instructions: "inherit",
|
|
2363
|
+
skills: "inherit",
|
|
2364
|
+
...learning.settings,
|
|
2365
|
+
},
|
|
2366
|
+
});
|
|
2367
|
+
// Keep the learning-owner → task lock order used by ordinary edits.
|
|
2368
|
+
// Any learning compensation above rolls back with a stale task head.
|
|
2369
|
+
await requireUnchangedScheduledTaskForCompensation(tx, input.task);
|
|
2370
|
+
await restoreScheduledTask(tx, input.previous);
|
|
1482
2371
|
});
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
2372
|
+
} catch {
|
|
2373
|
+
persistenceRestored = false;
|
|
2374
|
+
}
|
|
2375
|
+
return new ScheduledTaskSyncError(error, persistenceRestored);
|
|
2376
|
+
},
|
|
2377
|
+
});
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
if (error instanceof ScheduledTaskSyncError) throw error;
|
|
2380
|
+
throw new ScheduledTaskSyncError(error, false);
|
|
1489
2381
|
}
|
|
1490
2382
|
}
|
|
1491
2383
|
|
|
@@ -1551,20 +2443,46 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
1551
2443
|
};
|
|
1552
2444
|
workspaceId: string;
|
|
1553
2445
|
toolsProvided?: boolean;
|
|
2446
|
+
/** Only an already materialized reusable chat may retain unused creation defaults. */
|
|
2447
|
+
retainedCreationConfig?: ScheduledTaskAgentConfig;
|
|
2448
|
+
/**
|
|
2449
|
+
* The connector policy the schedule already follows. An edit that does not
|
|
2450
|
+
* name a policy keeps it, so replacing the configuration (for example with
|
|
2451
|
+
* the tools a read returned) never turns the defaults into a fixed list.
|
|
2452
|
+
*/
|
|
2453
|
+
existingToolPolicy?: ScheduledTaskAgentConfig["toolPolicy"];
|
|
1554
2454
|
}): Promise<ScheduledTaskAgentConfig> {
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
2455
|
+
const existingChat = input.payload.runMode === "existing_session";
|
|
2456
|
+
if (existingChat)
|
|
2457
|
+
input = {
|
|
2458
|
+
...input,
|
|
2459
|
+
payload: {
|
|
2460
|
+
...input.payload,
|
|
2461
|
+
agentConfig: scheduledSessionMessageConfig(input.payload.agentConfig),
|
|
2462
|
+
},
|
|
2463
|
+
toolsProvided: true,
|
|
2464
|
+
};
|
|
2465
|
+
const unchangedCreationField = (key: keyof ScheduledTaskAgentConfig) =>
|
|
2466
|
+
input.retainedCreationConfig !== undefined &&
|
|
2467
|
+
isDeepStrictEqual(input.payload.agentConfig[key], input.retainedCreationConfig[key]);
|
|
2468
|
+
const actor = scheduledTaskInitiatorForGrant(input.grant).actor;
|
|
2469
|
+
const parent =
|
|
2470
|
+
actor && !existingChat ? await getSession(input.db, input.workspaceId, actor.sessionId) : null;
|
|
2471
|
+
if (!existingChat && actor && (!parent || parent.accountId !== input.grant.accountId)) {
|
|
1558
2472
|
throw new HTTPException(403, {
|
|
1559
2473
|
message: "Scheduled Skill selection requires the creating agent's session",
|
|
1560
2474
|
});
|
|
1561
2475
|
}
|
|
1562
2476
|
let bundledSkillIds: ScheduledTaskAgentConfig["bundledSkillIds"];
|
|
1563
2477
|
try {
|
|
1564
|
-
bundledSkillIds =
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2478
|
+
bundledSkillIds = existingChat
|
|
2479
|
+
? undefined
|
|
2480
|
+
: unchangedCreationField("bundledSkillIds")
|
|
2481
|
+
? input.payload.agentConfig.bundledSkillIds
|
|
2482
|
+
: resolveBundledSkillSelection(
|
|
2483
|
+
input.payload.agentConfig.bundledSkillIds,
|
|
2484
|
+
parent?.bundledSkillIds,
|
|
2485
|
+
);
|
|
1568
2486
|
} catch (error) {
|
|
1569
2487
|
throw new HTTPException(422, {
|
|
1570
2488
|
message: error instanceof Error ? error.message : "Invalid bundled Skill selection",
|
|
@@ -1575,10 +2493,13 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
1575
2493
|
// session choke points (a `scheduled_tasks:manage` holder could otherwise set
|
|
1576
2494
|
// a model the host does not expose). An omitted model inherits the host
|
|
1577
2495
|
// default downstream, which is always configured.
|
|
1578
|
-
const model =
|
|
2496
|
+
const model = unchangedCreationField("model")
|
|
2497
|
+
? input.payload.agentConfig.model
|
|
2498
|
+
: canonicalConfiguredModel(input.settings, input.payload.agentConfig.model);
|
|
1579
2499
|
// Same policy vetting as the session choke points; an omitted model flows
|
|
1580
2500
|
// through session creation later, where the effective default is vetted.
|
|
1581
|
-
|
|
2501
|
+
if (!unchangedCreationField("model"))
|
|
2502
|
+
await assertWorkspaceModelPolicyAllows(input.db, input.settings, input.workspaceId, model);
|
|
1582
2503
|
const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
|
|
1583
2504
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
1584
2505
|
input.db,
|
|
@@ -1586,7 +2507,10 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
1586
2507
|
input.settings,
|
|
1587
2508
|
{ subjectId: input.grant.subjectId },
|
|
1588
2509
|
);
|
|
1589
|
-
const
|
|
2510
|
+
const retainTools = unchangedCreationField("tools");
|
|
2511
|
+
const requestedTools = retainTools
|
|
2512
|
+
? input.payload.agentConfig.tools
|
|
2513
|
+
: validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
|
|
1590
2514
|
const workspace = await requireWorkspace(input.db, input.workspaceId);
|
|
1591
2515
|
if (
|
|
1592
2516
|
parent?.tenancy?.visibility === "private" &&
|
|
@@ -1600,21 +2524,24 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
1600
2524
|
});
|
|
1601
2525
|
}
|
|
1602
2526
|
const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
|
|
1603
|
-
// A
|
|
1604
|
-
//
|
|
1605
|
-
// a
|
|
1606
|
-
//
|
|
1607
|
-
//
|
|
1608
|
-
//
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
)
|
|
2527
|
+
// A schedule that starts its own session and whose creator did not choose
|
|
2528
|
+
// tools follows the workspace's default connectors at each run, exactly
|
|
2529
|
+
// like a new chat: nothing about the defaults is copied into the schedule,
|
|
2530
|
+
// so a connector the workspace adds later reaches its next run. A request
|
|
2531
|
+
// may also say so explicitly (`toolPolicy`), with the defaults a person
|
|
2532
|
+
// turned off; its `tools` are then added on top. An edit keeps the policy
|
|
2533
|
+
// the schedule already follows. Only a new schedule created with a list of
|
|
2534
|
+
// tools and no policy gets exactly that list.
|
|
2535
|
+
// A knowledge sync reads one source and never follows the defaults.
|
|
2536
|
+
const toolPolicy =
|
|
2537
|
+
existingChat || input.payload.agentConfig.knowledgeSource
|
|
2538
|
+
? undefined
|
|
2539
|
+
: (input.payload.agentConfig.toolPolicy ??
|
|
2540
|
+
input.existingToolPolicy ??
|
|
2541
|
+
(retainTools || (input.toolsProvided ?? true)
|
|
2542
|
+
? undefined
|
|
2543
|
+
: { mode: "workspace_default" as const, excludedMcpServerIds: [] }));
|
|
2544
|
+
const tools = requestedTools;
|
|
1618
2545
|
const prompt = input.payload.agentConfig.prompt.trim();
|
|
1619
2546
|
if (!prompt) {
|
|
1620
2547
|
throw new HTTPException(422, { message: "scheduled task prompt is required" });
|
|
@@ -1675,7 +2602,7 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
1675
2602
|
);
|
|
1676
2603
|
}
|
|
1677
2604
|
const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
|
|
1678
|
-
if (requestedMaxDepth !== undefined) {
|
|
2605
|
+
if (requestedMaxDepth !== undefined && !unchangedCreationField("maxNestedAgentDepth")) {
|
|
1679
2606
|
const workspaceMaxDepth = workspace.settings.maxNestedAgentDepth;
|
|
1680
2607
|
const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
|
|
1681
2608
|
const inheritedMaxDepth =
|
|
@@ -1691,16 +2618,28 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
1691
2618
|
});
|
|
1692
2619
|
}
|
|
1693
2620
|
}
|
|
2621
|
+
const { toolPolicy: _requestedToolPolicy, ...requestedConfig } = input.payload.agentConfig;
|
|
1694
2622
|
const validated = {
|
|
1695
|
-
...
|
|
2623
|
+
...requestedConfig,
|
|
1696
2624
|
...(bundledSkillIds !== undefined ? { bundledSkillIds } : {}),
|
|
1697
2625
|
...(model === undefined || model === null ? {} : { model }),
|
|
1698
2626
|
prompt,
|
|
1699
2627
|
resources,
|
|
1700
2628
|
tools,
|
|
2629
|
+
...(toolPolicy ? { toolPolicy } : {}),
|
|
1701
2630
|
};
|
|
1702
|
-
|
|
1703
|
-
|
|
2631
|
+
// Incident preflight names the connectors it needs; judge it on what a run
|
|
2632
|
+
// would get now, including the defaults the schedule follows.
|
|
2633
|
+
validateIncidentTelemetryPreflightSelection(input.settings, {
|
|
2634
|
+
...validated,
|
|
2635
|
+
tools: scheduledTaskRunTools({
|
|
2636
|
+
agentConfig: validated,
|
|
2637
|
+
settings: input.settings,
|
|
2638
|
+
runtimeSettings,
|
|
2639
|
+
workspaceDefaults: workspaceSessionToolDefaults,
|
|
2640
|
+
}),
|
|
2641
|
+
});
|
|
2642
|
+
return existingChat ? scheduledSessionMessageConfig(validated) : validated;
|
|
1704
2643
|
}
|
|
1705
2644
|
|
|
1706
2645
|
/**
|
|
@@ -1710,7 +2649,7 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
1710
2649
|
* Mutable rig/variable-set metadata is revalidated again at dispatch.
|
|
1711
2650
|
*/
|
|
1712
2651
|
export function validateIncidentTelemetryPreflightSelection(
|
|
1713
|
-
settings:
|
|
2652
|
+
settings: FirstPartyMcpToolPolicySettings,
|
|
1714
2653
|
agentConfig: ScheduledTaskAgentConfig,
|
|
1715
2654
|
): void {
|
|
1716
2655
|
const executionClass = agentConfig.executionClass;
|
|
@@ -1732,7 +2671,7 @@ export function validateIncidentTelemetryPreflightSelection(
|
|
|
1732
2671
|
}
|
|
1733
2672
|
|
|
1734
2673
|
const selectedMcpServerIds = new Set(agentConfig.tools.map((tool) => tool.id));
|
|
1735
|
-
// Scheduled dispatch always attaches the first-party
|
|
2674
|
+
// Scheduled dispatch always attaches the first-party Opengeni MCP server.
|
|
1736
2675
|
selectedMcpServerIds.add("opengeni");
|
|
1737
2676
|
if (preflight.requiredMcpServerIds.some((id) => !selectedMcpServerIds.has(id))) {
|
|
1738
2677
|
throw new HTTPException(422, {
|