@opengeni/api-router 0.30.3 → 2.2.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-WCXHI3FF.js.map +0 -1
package/src/routes/sessions.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AcknowledgeStreamRequest,
|
|
3
3
|
ApplySessionGoalRevisionRequest,
|
|
4
|
+
ListSessionGoalRevisionsQuery,
|
|
5
|
+
RejectSessionGoalRevisionRequest,
|
|
6
|
+
RollbackSessionGoalRevisionRequest,
|
|
4
7
|
ActivateCodexRealtimeConnectionRequest,
|
|
5
8
|
AttachViewerRequest,
|
|
6
9
|
BeginSessionRealtimeRequest,
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
FsMoveRequest,
|
|
20
23
|
FsReadRequest,
|
|
21
24
|
FsWriteRequest,
|
|
25
|
+
ForkSessionRequest,
|
|
22
26
|
HumanInputRequestStatus,
|
|
23
27
|
GitDiffRequest,
|
|
24
28
|
GitReadBatchRequest,
|
|
@@ -30,6 +34,7 @@ import {
|
|
|
30
34
|
PtyOpenRequest,
|
|
31
35
|
PtyResizeRequest,
|
|
32
36
|
PtyWriteRequest,
|
|
37
|
+
PublishSandboxFileArtifactRequest,
|
|
33
38
|
RenewSessionRealtimeRequest,
|
|
34
39
|
SyncSessionRealtimeLedgerRequest,
|
|
35
40
|
SessionControlRequest,
|
|
@@ -45,14 +50,18 @@ import {
|
|
|
45
50
|
compactSessionEventResult,
|
|
46
51
|
sessionEventLatestClassToSemanticClass,
|
|
47
52
|
SaveComposerDraftRequest,
|
|
53
|
+
SubmitComposerDraftRequest,
|
|
48
54
|
SteerSessionQueueItemRequest,
|
|
49
55
|
SteerSessionMessageRequest,
|
|
50
56
|
TerminalExecRequest,
|
|
51
57
|
UpdateSessionChannelRequest,
|
|
58
|
+
UpdateSessionAttentionRequest,
|
|
59
|
+
UpdateSessionArchiveRequest,
|
|
52
60
|
UpdateSessionPinRequest,
|
|
53
61
|
UpdateSessionGoalRequest,
|
|
54
62
|
UpdateSessionMcpApprovalPolicyRequest,
|
|
55
63
|
UpdateSessionRequest,
|
|
64
|
+
UpdateSessionVisibilityRequest,
|
|
56
65
|
UpdateSessionToolPolicyRequest,
|
|
57
66
|
ViewerHeartbeatRequest,
|
|
58
67
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
@@ -73,15 +82,18 @@ import {
|
|
|
73
82
|
} from "@opengeni/contracts";
|
|
74
83
|
import { streamTokenDegraded } from "@opengeni/config";
|
|
75
84
|
import {
|
|
85
|
+
recordAuditEvent,
|
|
76
86
|
acceptSessionApprovalDecision,
|
|
77
87
|
acceptSessionHumanInputResponse,
|
|
78
88
|
clearSessionGoal,
|
|
79
89
|
clearSessionContext,
|
|
80
90
|
getOpenPtySession,
|
|
81
91
|
getEnrollment,
|
|
92
|
+
getLiveEnrollmentConnection,
|
|
82
93
|
getRetainedProcess,
|
|
83
94
|
getSandbox,
|
|
84
95
|
getSession,
|
|
96
|
+
deleteSessionTreeIfQuiescent,
|
|
85
97
|
getSessionEvent,
|
|
86
98
|
getSessionForSubject,
|
|
87
99
|
getSessionGoal,
|
|
@@ -93,7 +105,9 @@ import {
|
|
|
93
105
|
insertPtySession,
|
|
94
106
|
listSessionEventPage,
|
|
95
107
|
listSessionHumanInputRequests,
|
|
108
|
+
listSessionGoalRevisionPage,
|
|
96
109
|
listSessionGoalRevisions,
|
|
110
|
+
rejectSessionGoalRevisionWithEvent,
|
|
97
111
|
listSessionIdsInGroup,
|
|
98
112
|
listSessionDiscoverySummaries,
|
|
99
113
|
listSessionDiscoveryAncestorPaths,
|
|
@@ -108,13 +122,16 @@ import {
|
|
|
108
122
|
withSessionCodexCapacityMutation,
|
|
109
123
|
setSessionChannel,
|
|
110
124
|
ChannelNotFoundError,
|
|
125
|
+
setSessionAttention,
|
|
126
|
+
setSessionArchive,
|
|
111
127
|
setSessionPin,
|
|
128
|
+
SessionAttentionVersionConflictError,
|
|
129
|
+
SessionArchiveVersionConflictError,
|
|
112
130
|
SessionPinVersionConflictError,
|
|
113
131
|
SessionPinAccessError,
|
|
114
132
|
SessionListAccessError,
|
|
115
133
|
SessionListCursorError,
|
|
116
134
|
SessionListCursorExpiredError,
|
|
117
|
-
SessionListSnapshotLimitError,
|
|
118
135
|
decodeSessionListCursor,
|
|
119
136
|
revokeViewer,
|
|
120
137
|
setSessionGoalStatusWithEvent,
|
|
@@ -133,6 +150,11 @@ import {
|
|
|
133
150
|
SessionRealtimeConflictError,
|
|
134
151
|
SessionToolPolicyVersionConflictError,
|
|
135
152
|
SessionContextBusyError,
|
|
153
|
+
workspaceControlRequestLockTimeoutMs,
|
|
154
|
+
SessionTenancyAccessError,
|
|
155
|
+
SessionTenancyConflictError,
|
|
156
|
+
SessionTenancyInvalidRequestError,
|
|
157
|
+
SessionTenancyNotActivatedError,
|
|
136
158
|
HumanInputResponseValidationError,
|
|
137
159
|
latestWorkspaceCapture,
|
|
138
160
|
sessionLatestWorkspaceCapture,
|
|
@@ -149,6 +171,11 @@ import {
|
|
|
149
171
|
type SessionDiscoveryCursor,
|
|
150
172
|
type SessionDiscoveryAncestor,
|
|
151
173
|
} from "@opengeni/db";
|
|
174
|
+
import {
|
|
175
|
+
backgroundCommandActivityForSessions,
|
|
176
|
+
listSessionBackgroundCommands,
|
|
177
|
+
requestSessionBackgroundCommandCancellation,
|
|
178
|
+
} from "@opengeni/db/session-background-commands";
|
|
152
179
|
import {
|
|
153
180
|
appendAndPublishEvents,
|
|
154
181
|
boundSessionEventHttpPage,
|
|
@@ -173,6 +200,7 @@ import {
|
|
|
173
200
|
NatsControlRpc,
|
|
174
201
|
negotiateCapabilities,
|
|
175
202
|
negotiateSelfhostedCapabilities,
|
|
203
|
+
selectBackend,
|
|
176
204
|
SelfhostedSession,
|
|
177
205
|
} from "@opengeni/runtime/sandbox";
|
|
178
206
|
import type { Context, Hono, MiddlewareHandler } from "hono";
|
|
@@ -181,6 +209,8 @@ import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
|
181
209
|
import {
|
|
182
210
|
hasPermission,
|
|
183
211
|
requireAccessGrant,
|
|
212
|
+
requireAccessGrantAuthorization,
|
|
213
|
+
requireFreshAccessGrant,
|
|
184
214
|
requirePermission,
|
|
185
215
|
requireSessionAuthorization,
|
|
186
216
|
requireSessionAuthorizationListScope,
|
|
@@ -188,6 +218,8 @@ import {
|
|
|
188
218
|
SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
189
219
|
SessionAuthorizationDeniedError,
|
|
190
220
|
SessionAuthorizationUnavailableError,
|
|
221
|
+
SessionTenancyManagedHumanRequiredError,
|
|
222
|
+
SessionTenancyPersistenceOutcomeUnknownError,
|
|
191
223
|
type ResolvedSessionAuthorization,
|
|
192
224
|
} from "@opengeni/core";
|
|
193
225
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
@@ -207,12 +239,15 @@ import {
|
|
|
207
239
|
import { buildSessionCodexRealtimeBroker, CodexRealtimeBrokerError } from "../codex-realtime";
|
|
208
240
|
import {
|
|
209
241
|
acceptSessionUserMessage,
|
|
242
|
+
acceptSessionUserMessageWithOutcome,
|
|
210
243
|
controlHumanSessionWorkstream,
|
|
211
244
|
createSessionForRequest,
|
|
212
245
|
deleteHumanQueuePrompt,
|
|
213
246
|
editHumanQueuePrompt,
|
|
214
247
|
getActorNewSessionDraft,
|
|
248
|
+
getManagedHumanSessionCreateCapabilities,
|
|
215
249
|
getHumanComposerDraft,
|
|
250
|
+
forkManagedHumanSession,
|
|
216
251
|
moveHumanQueuePrompt,
|
|
217
252
|
readSessionLineage,
|
|
218
253
|
saveHumanComposerDraft,
|
|
@@ -221,6 +256,7 @@ import {
|
|
|
221
256
|
sessionSpawnDenialEnvelope,
|
|
222
257
|
steerHumanQueuePrompt,
|
|
223
258
|
updateSessionMcpApprovalPolicy,
|
|
259
|
+
updateManagedHumanSessionVisibility,
|
|
224
260
|
updateSessionToolPolicy,
|
|
225
261
|
updateSessionTitle,
|
|
226
262
|
workflowIdForSession,
|
|
@@ -236,11 +272,27 @@ import {
|
|
|
236
272
|
serveWorkspaceCaptureFile,
|
|
237
273
|
WorkspaceCaptureManifestCache,
|
|
238
274
|
} from "./workspace-capture";
|
|
275
|
+
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
276
|
+
import { ApiHttpError } from "../http/api-error";
|
|
239
277
|
|
|
240
278
|
type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
|
|
241
279
|
|
|
242
280
|
const VIEWER_LIFECYCLE_PERMISSIONS = ["stream:view", "terminal:attach", "files:write"] as const;
|
|
243
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Bounded responder kind recorded on a child's requires-action resolution
|
|
284
|
+
* notice for its parent: a key principal is `api`, a signed agent attempt is
|
|
285
|
+
* `agent_attempt`, and every other HTTP caller is a human. Never a subject id.
|
|
286
|
+
*/
|
|
287
|
+
function childRequiresActionRespondedByKindForGrant(
|
|
288
|
+
grant: Pick<AccessGrant, "principalKind">,
|
|
289
|
+
): "human" | "api" | "agent_attempt" | "system" {
|
|
290
|
+
if (grant.principalKind === "api_key" || grant.principalKind === "configured_key") return "api";
|
|
291
|
+
if (grant.principalKind === "agent_attempt") return "agent_attempt";
|
|
292
|
+
if (grant.principalKind === "service") return "system";
|
|
293
|
+
return "human";
|
|
294
|
+
}
|
|
295
|
+
|
|
244
296
|
function requireViewerLifecyclePermission(grant: AccessGrant): void {
|
|
245
297
|
if (
|
|
246
298
|
VIEWER_LIFECYCLE_PERMISSIONS.some((permission) => hasPermission(grant.permissions, permission))
|
|
@@ -406,6 +458,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
406
458
|
if (!operation) {
|
|
407
459
|
throw sessionAuthorizationHttpError(new SessionAuthorizationUnavailableError());
|
|
408
460
|
}
|
|
461
|
+
if (operation === "session.visibility.write" || operation === "session.fork.create") {
|
|
462
|
+
// These product mutations perform their target-free managed-cookie,
|
|
463
|
+
// permission, and activation gates inside core before resolving the
|
|
464
|
+
// target exactly once. Visibility changes remain owner-only; a shared
|
|
465
|
+
// source may be forked by any currently authorized workspace member.
|
|
466
|
+
// Running the generic middleware here would both create a pre-gate
|
|
467
|
+
// existence oracle and double-call the host.
|
|
468
|
+
await next();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
409
471
|
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
410
472
|
try {
|
|
411
473
|
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
@@ -434,9 +496,26 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
434
496
|
: {}),
|
|
435
497
|
};
|
|
436
498
|
|
|
499
|
+
app.get("/v1/workspaces/:workspaceId/session-tenancy/capabilities", async (c) => {
|
|
500
|
+
const workspaceId = c.req.param("workspaceId");
|
|
501
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
502
|
+
c,
|
|
503
|
+
deps,
|
|
504
|
+
workspaceId,
|
|
505
|
+
"sessions:create",
|
|
506
|
+
);
|
|
507
|
+
return c.json(await getManagedHumanSessionCreateCapabilities(deps, authorization, workspaceId));
|
|
508
|
+
});
|
|
509
|
+
|
|
437
510
|
app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
438
511
|
const workspaceId = c.req.param("workspaceId");
|
|
439
|
-
const
|
|
512
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
513
|
+
c,
|
|
514
|
+
deps,
|
|
515
|
+
workspaceId,
|
|
516
|
+
"sessions:create",
|
|
517
|
+
);
|
|
518
|
+
const grant = authorization.grant;
|
|
440
519
|
let payload: unknown;
|
|
441
520
|
try {
|
|
442
521
|
payload = await c.req.json();
|
|
@@ -451,7 +530,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
451
530
|
}
|
|
452
531
|
let session: Session;
|
|
453
532
|
try {
|
|
454
|
-
session = await createSessionForRequest(deps, grant, workspaceId, payload);
|
|
533
|
+
session = await createSessionForRequest(deps, grant, workspaceId, payload, authorization);
|
|
455
534
|
} catch (error) {
|
|
456
535
|
return sessionCreateErrorResponse(c, error);
|
|
457
536
|
}
|
|
@@ -469,7 +548,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
469
548
|
|
|
470
549
|
app.put("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
|
|
471
550
|
const workspaceId = c.req.param("workspaceId");
|
|
472
|
-
const
|
|
551
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
552
|
+
c,
|
|
553
|
+
deps,
|
|
554
|
+
workspaceId,
|
|
555
|
+
"sessions:create",
|
|
556
|
+
);
|
|
557
|
+
const grant = authorization.grant;
|
|
473
558
|
let payload: unknown;
|
|
474
559
|
try {
|
|
475
560
|
payload = await c.req.json();
|
|
@@ -489,6 +574,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
489
574
|
grant,
|
|
490
575
|
workspaceId,
|
|
491
576
|
payload,
|
|
577
|
+
authorization.canonicalManagedHumanSession,
|
|
492
578
|
),
|
|
493
579
|
);
|
|
494
580
|
} catch (error) {
|
|
@@ -517,7 +603,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
517
603
|
|
|
518
604
|
app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
519
605
|
const workspaceId = c.req.param("workspaceId");
|
|
520
|
-
const
|
|
606
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
607
|
+
c,
|
|
608
|
+
deps,
|
|
609
|
+
workspaceId,
|
|
610
|
+
"sessions:read",
|
|
611
|
+
);
|
|
612
|
+
const grant = authorization.grant;
|
|
521
613
|
let authorizationScope;
|
|
522
614
|
try {
|
|
523
615
|
authorizationScope = await requireSessionAuthorizationListScope(deps, grant, "http");
|
|
@@ -535,8 +627,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
535
627
|
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
536
628
|
...(query.search ? { search: query.search } : {}),
|
|
537
629
|
...(query.pinsOnly ? { pinsOnly: true } : {}),
|
|
630
|
+
...(query.archivedOnly ? { archivedOnly: true } : {}),
|
|
538
631
|
...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
|
|
539
632
|
...(authorizationScope ? { authorizationScope } : {}),
|
|
633
|
+
// A managed human's own personal workspace has no membership row, so
|
|
634
|
+
// the list's removal fence must fall back to the organization-membership
|
|
635
|
+
// pointer — for the canonical managed-cookie session that owns it only.
|
|
636
|
+
personalWorkspaceOwnerException: authorization.canonicalManagedHumanSession,
|
|
540
637
|
});
|
|
541
638
|
} catch (error) {
|
|
542
639
|
if (error instanceof SessionListAccessError) {
|
|
@@ -552,10 +649,6 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
552
649
|
if (error instanceof SessionListCursorError) {
|
|
553
650
|
throw new HTTPException(400, { message: error.message });
|
|
554
651
|
}
|
|
555
|
-
if (error instanceof SessionListSnapshotLimitError) {
|
|
556
|
-
c.header("Retry-After", "5");
|
|
557
|
-
throw new HTTPException(429, { message: error.message });
|
|
558
|
-
}
|
|
559
652
|
throw error;
|
|
560
653
|
}
|
|
561
654
|
// The page body carries this fact directly. Preserve the historical array
|
|
@@ -563,12 +656,19 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
563
656
|
// to raw HTTP consumers without changing that response shape.
|
|
564
657
|
c.header("x-opengeni-pinned-truncated", page.pinnedTruncated === true ? "true" : "false");
|
|
565
658
|
const policy = await loadEffectivePolicyContext(deps, workspaceId, grant.subjectId);
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
659
|
+
const commandActivity = await backgroundCommandActivityForSessions(db, {
|
|
660
|
+
accountId: grant.accountId,
|
|
661
|
+
workspaceId,
|
|
662
|
+
sessionIds: [...page.pinned, ...page.sessions].map((session) => session.id),
|
|
663
|
+
});
|
|
664
|
+
const decorate = (session: Session): Session => {
|
|
665
|
+
const activity = commandActivity.get(session.id);
|
|
666
|
+
return sessionWithEffectiveToolPolicy(
|
|
667
|
+
{ ...session, ...(activity ? { backgroundCommandActivity: activity } : {}) },
|
|
569
668
|
policy.workspaceServerIds,
|
|
570
669
|
policy.workspaceDefaultServerIds,
|
|
571
670
|
);
|
|
671
|
+
};
|
|
572
672
|
if (pageView) {
|
|
573
673
|
return c.json({
|
|
574
674
|
...page,
|
|
@@ -681,6 +781,99 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
681
781
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
682
782
|
});
|
|
683
783
|
|
|
784
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/background-commands", async (c) => {
|
|
785
|
+
const workspaceId = c.req.param("workspaceId");
|
|
786
|
+
const sessionId = c.req.param("sessionId");
|
|
787
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
788
|
+
const commands = await listSessionBackgroundCommands(db, {
|
|
789
|
+
accountId: grant.accountId,
|
|
790
|
+
workspaceId,
|
|
791
|
+
sessionId,
|
|
792
|
+
});
|
|
793
|
+
return c.json({ commands });
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
app.delete(
|
|
797
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/background-commands/:commandId",
|
|
798
|
+
async (c) => {
|
|
799
|
+
const workspaceId = c.req.param("workspaceId");
|
|
800
|
+
const sessionId = c.req.param("sessionId");
|
|
801
|
+
const commandId = c.req.param("commandId");
|
|
802
|
+
if (!z.string().uuid().safeParse(commandId).success) {
|
|
803
|
+
throw new HTTPException(404, { message: "background command not found" });
|
|
804
|
+
}
|
|
805
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
806
|
+
const result = await requestSessionBackgroundCommandCancellation(db, {
|
|
807
|
+
accountId: grant.accountId,
|
|
808
|
+
workspaceId,
|
|
809
|
+
sessionId,
|
|
810
|
+
commandId,
|
|
811
|
+
subjectId: grant.subjectId,
|
|
812
|
+
});
|
|
813
|
+
if (!result.command) {
|
|
814
|
+
throw new HTTPException(404, { message: "background command not found" });
|
|
815
|
+
}
|
|
816
|
+
return c.json(result);
|
|
817
|
+
},
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/visibility", async (c) => {
|
|
821
|
+
const workspaceId = c.req.param("workspaceId");
|
|
822
|
+
const sessionId = c.req.param("sessionId");
|
|
823
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
824
|
+
const parsed = UpdateSessionVisibilityRequest.safeParse(await c.req.json().catch(() => null));
|
|
825
|
+
if (!parsed.success) {
|
|
826
|
+
throw new ApiHttpError(422, {
|
|
827
|
+
code: "validation_failed",
|
|
828
|
+
message: "Invalid session visibility request.",
|
|
829
|
+
retryable: false,
|
|
830
|
+
details: { fields: zodErrorFields(parsed.error) },
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
return c.json(
|
|
835
|
+
await updateManagedHumanSessionVisibility(
|
|
836
|
+
deps,
|
|
837
|
+
authorization,
|
|
838
|
+
workspaceId,
|
|
839
|
+
sessionId,
|
|
840
|
+
parsed.data,
|
|
841
|
+
"http",
|
|
842
|
+
),
|
|
843
|
+
);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
throw sessionTenancyHttpError(error);
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/forks", async (c) => {
|
|
850
|
+
const workspaceId = c.req.param("workspaceId");
|
|
851
|
+
const sessionId = c.req.param("sessionId");
|
|
852
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
853
|
+
const parsed = ForkSessionRequest.safeParse(await c.req.json().catch(() => null));
|
|
854
|
+
if (!parsed.success) {
|
|
855
|
+
throw new ApiHttpError(422, {
|
|
856
|
+
code: "validation_failed",
|
|
857
|
+
message: "Invalid session fork request.",
|
|
858
|
+
retryable: false,
|
|
859
|
+
details: { fields: zodErrorFields(parsed.error) },
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const response = await forkManagedHumanSession(
|
|
864
|
+
deps,
|
|
865
|
+
authorization,
|
|
866
|
+
workspaceId,
|
|
867
|
+
sessionId,
|
|
868
|
+
parsed.data,
|
|
869
|
+
"http",
|
|
870
|
+
);
|
|
871
|
+
return c.json(response, response.replay ? 200 : 201);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
throw sessionTenancyHttpError(error);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
684
877
|
const publishRealtimeMutation = async (
|
|
685
878
|
accountId: string,
|
|
686
879
|
workspaceId: string,
|
|
@@ -1299,6 +1492,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1299
1492
|
realtimeId,
|
|
1300
1493
|
ownerSubjectId: grant.subjectId,
|
|
1301
1494
|
...parsed.data,
|
|
1495
|
+
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
1302
1496
|
}),
|
|
1303
1497
|
);
|
|
1304
1498
|
await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
|
|
@@ -1315,7 +1509,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1315
1509
|
// (not session control) and returns 404 for a foreign/inaccessible session.
|
|
1316
1510
|
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/pin", async (c) => {
|
|
1317
1511
|
const workspaceId = c.req.param("workspaceId");
|
|
1318
|
-
const
|
|
1512
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
1513
|
+
c,
|
|
1514
|
+
deps,
|
|
1515
|
+
workspaceId,
|
|
1516
|
+
"sessions:read",
|
|
1517
|
+
);
|
|
1518
|
+
const grant = authorization.grant;
|
|
1319
1519
|
const sessionId = c.req.param("sessionId");
|
|
1320
1520
|
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
1321
1521
|
throw new HTTPException(404, { message: "session not found" });
|
|
@@ -1329,6 +1529,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1329
1529
|
workspaceId,
|
|
1330
1530
|
subjectId: grant.subjectId,
|
|
1331
1531
|
sessionId,
|
|
1532
|
+
// Same owner-only personal-workspace fallback as the list above.
|
|
1533
|
+
personalWorkspaceOwnerException: authorization.canonicalManagedHumanSession,
|
|
1332
1534
|
...parsed.data,
|
|
1333
1535
|
});
|
|
1334
1536
|
if (!session) {
|
|
@@ -1359,6 +1561,94 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1359
1561
|
}
|
|
1360
1562
|
});
|
|
1361
1563
|
|
|
1564
|
+
// Personal follow-up state. Session reads stay side-effect free; clients use
|
|
1565
|
+
// this explicit mutation after a deliberate read/unread action. The managed
|
|
1566
|
+
// web console treats an exact foreground chat event frontier as that action,
|
|
1567
|
+
// while the actively-working label remains independent of acknowledgment.
|
|
1568
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/attention", async (c) => {
|
|
1569
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1570
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1571
|
+
const sessionId = c.req.param("sessionId");
|
|
1572
|
+
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
1573
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1574
|
+
}
|
|
1575
|
+
const parsed = UpdateSessionAttentionRequest.safeParse(await c.req.json().catch(() => null));
|
|
1576
|
+
if (!parsed.success) {
|
|
1577
|
+
throw new HTTPException(400, { message: "invalid session attention request" });
|
|
1578
|
+
}
|
|
1579
|
+
try {
|
|
1580
|
+
const session = await setSessionAttention(db, {
|
|
1581
|
+
workspaceId,
|
|
1582
|
+
subjectId: grant.subjectId,
|
|
1583
|
+
sessionId,
|
|
1584
|
+
...parsed.data,
|
|
1585
|
+
});
|
|
1586
|
+
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
1587
|
+
return c.json(
|
|
1588
|
+
await withEffectivePolicy(
|
|
1589
|
+
deps,
|
|
1590
|
+
workspaceId,
|
|
1591
|
+
grant.subjectId,
|
|
1592
|
+
projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)),
|
|
1593
|
+
),
|
|
1594
|
+
);
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
if (error instanceof SessionPinAccessError) {
|
|
1597
|
+
throw new HTTPException(403, { message: error.message });
|
|
1598
|
+
}
|
|
1599
|
+
if (error instanceof SessionAttentionVersionConflictError) {
|
|
1600
|
+
return c.json(
|
|
1601
|
+
{
|
|
1602
|
+
message: "session attention changed in another client",
|
|
1603
|
+
current: error.current,
|
|
1604
|
+
},
|
|
1605
|
+
409,
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
throw error;
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// Personal chat organization: archived roots leave the ordinary rail only
|
|
1613
|
+
// for this member and remain recoverable through the archived list view.
|
|
1614
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/archive", async (c) => {
|
|
1615
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1616
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1617
|
+
const sessionId = c.req.param("sessionId");
|
|
1618
|
+
const parsed = UpdateSessionArchiveRequest.safeParse(await c.req.json().catch(() => null));
|
|
1619
|
+
if (!parsed.success) {
|
|
1620
|
+
throw new HTTPException(400, { message: "invalid session archive request" });
|
|
1621
|
+
}
|
|
1622
|
+
try {
|
|
1623
|
+
const session = await setSessionArchive(db, {
|
|
1624
|
+
workspaceId,
|
|
1625
|
+
subjectId: grant.subjectId,
|
|
1626
|
+
sessionId,
|
|
1627
|
+
...parsed.data,
|
|
1628
|
+
});
|
|
1629
|
+
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
1630
|
+
return c.json(
|
|
1631
|
+
await withEffectivePolicy(
|
|
1632
|
+
deps,
|
|
1633
|
+
workspaceId,
|
|
1634
|
+
grant.subjectId,
|
|
1635
|
+
projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)),
|
|
1636
|
+
),
|
|
1637
|
+
);
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
if (error instanceof SessionPinAccessError) {
|
|
1640
|
+
throw new HTTPException(403, { message: error.message });
|
|
1641
|
+
}
|
|
1642
|
+
if (error instanceof SessionArchiveVersionConflictError) {
|
|
1643
|
+
return c.json(
|
|
1644
|
+
{ message: "session archive changed in another client", current: error.current },
|
|
1645
|
+
409,
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
throw error;
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1362
1652
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/lineage", async (c) => {
|
|
1363
1653
|
const workspaceId = c.req.param("workspaceId");
|
|
1364
1654
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
@@ -1505,6 +1795,49 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1505
1795
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
1506
1796
|
});
|
|
1507
1797
|
|
|
1798
|
+
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
1799
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1800
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1801
|
+
const deleted = await deleteSessionTreeIfQuiescent(db, {
|
|
1802
|
+
workspaceId,
|
|
1803
|
+
subjectId: grant.subjectId,
|
|
1804
|
+
sessionId: c.req.param("sessionId"),
|
|
1805
|
+
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
1806
|
+
});
|
|
1807
|
+
switch (deleted.status) {
|
|
1808
|
+
case "deleted":
|
|
1809
|
+
return c.json({ deletedSessionCount: deleted.deletedSessionCount });
|
|
1810
|
+
case "not_found":
|
|
1811
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1812
|
+
case "not_root":
|
|
1813
|
+
throw new HTTPException(409, {
|
|
1814
|
+
message: "delete the root session to remove the complete workstream",
|
|
1815
|
+
});
|
|
1816
|
+
case "active_sessions":
|
|
1817
|
+
throw new HTTPException(409, {
|
|
1818
|
+
message: "cancel the workstream and wait for active turns to finish before deleting it",
|
|
1819
|
+
});
|
|
1820
|
+
case "active_video_generations":
|
|
1821
|
+
throw new HTTPException(409, {
|
|
1822
|
+
message: "wait for active video generations to finish before deleting this workstream",
|
|
1823
|
+
});
|
|
1824
|
+
case "active_background_commands":
|
|
1825
|
+
throw new HTTPException(409, {
|
|
1826
|
+
message: "pause or cancel this workstream's background commands before deleting it",
|
|
1827
|
+
});
|
|
1828
|
+
case "live_sandboxes":
|
|
1829
|
+
throw new HTTPException(409, {
|
|
1830
|
+
message:
|
|
1831
|
+
"wait for the workstream's sandbox activity to finish draining before deleting it",
|
|
1832
|
+
});
|
|
1833
|
+
case "externally_referenced":
|
|
1834
|
+
throw new HTTPException(409, {
|
|
1835
|
+
message:
|
|
1836
|
+
"this workstream has durable workspace outputs or independent forks; archive it instead",
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1508
1841
|
app.patch(
|
|
1509
1842
|
"/v1/workspaces/:workspaceId/sessions/:sessionId/mcp-servers/:serverId/approval-policy",
|
|
1510
1843
|
async (c) => {
|
|
@@ -1576,6 +1909,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1576
1909
|
return c.json(await listSessionGoalRevisions(db, workspaceId, sessionId));
|
|
1577
1910
|
});
|
|
1578
1911
|
|
|
1912
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/page", async (c) => {
|
|
1913
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1914
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1915
|
+
const sessionId = c.req.param("sessionId");
|
|
1916
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
1917
|
+
const parsedQuery = ListSessionGoalRevisionsQuery.safeParse({
|
|
1918
|
+
limit: c.req.query("limit"),
|
|
1919
|
+
before: c.req.query("before"),
|
|
1920
|
+
});
|
|
1921
|
+
if (!parsedQuery.success) {
|
|
1922
|
+
throw new HTTPException(400, { message: "invalid goal revision page query" });
|
|
1923
|
+
}
|
|
1924
|
+
const query = parsedQuery.data;
|
|
1925
|
+
try {
|
|
1926
|
+
return c.json(
|
|
1927
|
+
await listSessionGoalRevisionPage(db, workspaceId, sessionId, {
|
|
1928
|
+
limit: query.limit,
|
|
1929
|
+
...(query.before ? { before: query.before } : {}),
|
|
1930
|
+
}),
|
|
1931
|
+
);
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
if (error instanceof SessionControlConflictError) {
|
|
1934
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
1935
|
+
}
|
|
1936
|
+
throw error;
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1579
1940
|
app.post(
|
|
1580
1941
|
"/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/apply",
|
|
1581
1942
|
async (c) => {
|
|
@@ -1607,6 +1968,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1607
1968
|
sessionId,
|
|
1608
1969
|
text: revision.text,
|
|
1609
1970
|
successCriteria: revision.successCriteria,
|
|
1971
|
+
rootConstraints: revision.rootConstraints,
|
|
1610
1972
|
mutationPolicy: revision.mutationPolicy,
|
|
1611
1973
|
expectedObjectiveRevision: payload.expectedObjectiveRevision,
|
|
1612
1974
|
expectedGoalId: revision.goalId,
|
|
@@ -1639,6 +2001,88 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1639
2001
|
},
|
|
1640
2002
|
);
|
|
1641
2003
|
|
|
2004
|
+
app.post(
|
|
2005
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/reject",
|
|
2006
|
+
async (c) => {
|
|
2007
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2008
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2009
|
+
const sessionId = c.req.param("sessionId");
|
|
2010
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
2011
|
+
const payload = RejectSessionGoalRevisionRequest.parse(await c.req.json());
|
|
2012
|
+
try {
|
|
2013
|
+
const result = await rejectSessionGoalRevisionWithEvent(db, {
|
|
2014
|
+
accountId: grant.accountId,
|
|
2015
|
+
workspaceId,
|
|
2016
|
+
sessionId,
|
|
2017
|
+
revisionId: c.req.param("revisionId"),
|
|
2018
|
+
expectedObjectiveRevision: payload.expectedObjectiveRevision,
|
|
2019
|
+
rationale: payload.rationale,
|
|
2020
|
+
});
|
|
2021
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
|
|
2022
|
+
return c.json({ revision: result.revision, replay: result.replay });
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
if (error instanceof SessionControlConflictError) {
|
|
2025
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
2026
|
+
}
|
|
2027
|
+
throw error;
|
|
2028
|
+
}
|
|
2029
|
+
},
|
|
2030
|
+
);
|
|
2031
|
+
|
|
2032
|
+
app.post(
|
|
2033
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/rollback",
|
|
2034
|
+
async (c) => {
|
|
2035
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2036
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2037
|
+
const sessionId = c.req.param("sessionId");
|
|
2038
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
2039
|
+
const payload = RollbackSessionGoalRevisionRequest.parse(await c.req.json());
|
|
2040
|
+
const revision = await getSessionGoalRevision(
|
|
2041
|
+
db,
|
|
2042
|
+
workspaceId,
|
|
2043
|
+
sessionId,
|
|
2044
|
+
c.req.param("revisionId"),
|
|
2045
|
+
);
|
|
2046
|
+
if (!revision || revision.disposition !== "applied") {
|
|
2047
|
+
throw new HTTPException(404, { message: "applied goal revision not found" });
|
|
2048
|
+
}
|
|
2049
|
+
try {
|
|
2050
|
+
const { goal, workflowWakeRevision, events } = await upsertSessionGoalWithEvent(db, {
|
|
2051
|
+
accountId: grant.accountId,
|
|
2052
|
+
workspaceId,
|
|
2053
|
+
sessionId,
|
|
2054
|
+
text: revision.text,
|
|
2055
|
+
successCriteria: revision.successCriteria,
|
|
2056
|
+
rootConstraints: revision.rootConstraints,
|
|
2057
|
+
mutationPolicy: revision.mutationPolicy,
|
|
2058
|
+
expectedObjectiveRevision: payload.expectedObjectiveRevision,
|
|
2059
|
+
expectedGoalId: revision.goalId,
|
|
2060
|
+
changeKind: "replacement",
|
|
2061
|
+
changeRationale: payload.rationale,
|
|
2062
|
+
rollbackOfRevisionId: revision.id,
|
|
2063
|
+
createdBy: "api",
|
|
2064
|
+
actor: "api",
|
|
2065
|
+
});
|
|
2066
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, events);
|
|
2067
|
+
if (workflowWakeRevision !== null) {
|
|
2068
|
+
await workflowClient.wakeSessionWorkflow({
|
|
2069
|
+
accountId: grant.accountId,
|
|
2070
|
+
workspaceId,
|
|
2071
|
+
sessionId,
|
|
2072
|
+
workflowId: workflowIdForSession(sessionId),
|
|
2073
|
+
wakeRevision: workflowWakeRevision,
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
return c.json((await getSessionGoalWithContinuation(db, workspaceId, sessionId)) ?? goal);
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
if (error instanceof SessionControlConflictError) {
|
|
2079
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
2080
|
+
}
|
|
2081
|
+
throw error;
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
);
|
|
2085
|
+
|
|
1642
2086
|
app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
1643
2087
|
const workspaceId = c.req.param("workspaceId");
|
|
1644
2088
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
@@ -1660,6 +2104,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1660
2104
|
payload.successCriteria !== undefined
|
|
1661
2105
|
? payload.successCriteria
|
|
1662
2106
|
: existing.successCriteria,
|
|
2107
|
+
rootConstraints:
|
|
2108
|
+
payload.rootConstraints !== undefined
|
|
2109
|
+
? payload.rootConstraints
|
|
2110
|
+
: existing.rootConstraints,
|
|
1663
2111
|
maxAutoContinuations: existing.maxAutoContinuations,
|
|
1664
2112
|
mutationPolicy: payload.mutationPolicy ?? existing.mutationPolicy,
|
|
1665
2113
|
expectedObjectiveRevision: payload.expectedObjectiveRevision,
|
|
@@ -2021,19 +2469,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2021
2469
|
c.req.raw.signal,
|
|
2022
2470
|
{
|
|
2023
2471
|
observability: deps.observability,
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
},
|
|
2035
|
-
}
|
|
2036
|
-
: {}),
|
|
2472
|
+
reauthorizeAfterMs:
|
|
2473
|
+
authorization?.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
2474
|
+
reauthorize: async () => {
|
|
2475
|
+
const freshGrant = await requireFreshAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
2476
|
+
await requireSessionAuthorization(deps, freshGrant, {
|
|
2477
|
+
sessionId,
|
|
2478
|
+
operation: "session.stream.read",
|
|
2479
|
+
surface: "stream",
|
|
2480
|
+
});
|
|
2481
|
+
},
|
|
2037
2482
|
},
|
|
2038
2483
|
);
|
|
2039
2484
|
});
|
|
@@ -2250,7 +2695,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2250
2695
|
|
|
2251
2696
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/steer", async (c) => {
|
|
2252
2697
|
const workspaceId = c.req.param("workspaceId");
|
|
2253
|
-
const
|
|
2698
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
2699
|
+
c,
|
|
2700
|
+
deps,
|
|
2701
|
+
workspaceId,
|
|
2702
|
+
"sessions:control",
|
|
2703
|
+
);
|
|
2704
|
+
const grant = authorization.grant;
|
|
2254
2705
|
const sessionId = c.req.param("sessionId");
|
|
2255
2706
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2256
2707
|
const payload = parseSteerSessionAdmission(await c.req.json().catch(() => null));
|
|
@@ -2263,6 +2714,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2263
2714
|
reasoningEffort: payload.reasoningEffort ?? null,
|
|
2264
2715
|
latencyMode: payload.latencyMode ?? null,
|
|
2265
2716
|
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
2717
|
+
connectionAuthorities: payload.connectionAuthorities,
|
|
2718
|
+
...(payload.personalResourceAttachment
|
|
2719
|
+
? { personalResourceAttachment: payload.personalResourceAttachment }
|
|
2720
|
+
: {}),
|
|
2721
|
+
authorization,
|
|
2266
2722
|
delivery: "steer",
|
|
2267
2723
|
origin: "human",
|
|
2268
2724
|
...(payload.controlEtag !== undefined ? { controlEtag: payload.controlEtag } : {}),
|
|
@@ -2274,9 +2730,69 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2274
2730
|
return c.json(result, 202);
|
|
2275
2731
|
});
|
|
2276
2732
|
|
|
2733
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/composer-draft/submit", async (c) => {
|
|
2734
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2735
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
2736
|
+
c,
|
|
2737
|
+
deps,
|
|
2738
|
+
workspaceId,
|
|
2739
|
+
"sessions:control",
|
|
2740
|
+
);
|
|
2741
|
+
const grant = authorization.grant;
|
|
2742
|
+
const sessionId = c.req.param("sessionId");
|
|
2743
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
2744
|
+
const payload = SubmitComposerDraftRequest.parse(await c.req.json().catch(() => null));
|
|
2745
|
+
let result: Awaited<ReturnType<typeof acceptSessionUserMessageWithOutcome>>;
|
|
2746
|
+
try {
|
|
2747
|
+
result = await acceptSessionUserMessageWithOutcome(deps, grant, workspaceId, sessionId, {
|
|
2748
|
+
text: payload.text,
|
|
2749
|
+
annotations: payload.annotations,
|
|
2750
|
+
modelContext: payload.modelContext ?? null,
|
|
2751
|
+
resources: payload.resources,
|
|
2752
|
+
model: payload.model,
|
|
2753
|
+
reasoningEffort: payload.reasoningEffort,
|
|
2754
|
+
latencyMode: payload.latencyMode,
|
|
2755
|
+
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
2756
|
+
connectionAuthorities: payload.connectionAuthorities,
|
|
2757
|
+
...(payload.personalResourceAttachment
|
|
2758
|
+
? { personalResourceAttachment: payload.personalResourceAttachment }
|
|
2759
|
+
: {}),
|
|
2760
|
+
authorization,
|
|
2761
|
+
delivery: payload.delivery,
|
|
2762
|
+
origin: "human",
|
|
2763
|
+
expectedDraftRevision: payload.expectedDraftRevision,
|
|
2764
|
+
clientEventId: payload.clientEventId,
|
|
2765
|
+
...(payload.controlEtag ? { controlEtag: payload.controlEtag } : {}),
|
|
2766
|
+
});
|
|
2767
|
+
} catch (error) {
|
|
2768
|
+
return commandConflictResponse(c, error);
|
|
2769
|
+
}
|
|
2770
|
+
if (!result.draft) {
|
|
2771
|
+
throw new Error("Accepted composer draft submission did not return its next draft");
|
|
2772
|
+
}
|
|
2773
|
+
return c.json(
|
|
2774
|
+
{
|
|
2775
|
+
accepted: result.accepted,
|
|
2776
|
+
turn: result.turn,
|
|
2777
|
+
draft: result.draft,
|
|
2778
|
+
receipt: result.receipt,
|
|
2779
|
+
routing: result.routing,
|
|
2780
|
+
interruptionCount: result.interruptionCount,
|
|
2781
|
+
replay: result.replay,
|
|
2782
|
+
},
|
|
2783
|
+
202,
|
|
2784
|
+
);
|
|
2785
|
+
});
|
|
2786
|
+
|
|
2277
2787
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
|
|
2278
2788
|
const workspaceId = c.req.param("workspaceId");
|
|
2279
|
-
const
|
|
2789
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
2790
|
+
c,
|
|
2791
|
+
deps,
|
|
2792
|
+
workspaceId,
|
|
2793
|
+
"sessions:control",
|
|
2794
|
+
);
|
|
2795
|
+
const grant = authorization.grant;
|
|
2280
2796
|
const sessionId = c.req.param("sessionId");
|
|
2281
2797
|
const event = parseSessionEventAdmission(await c.req.json().catch(() => null));
|
|
2282
2798
|
const refinedOperation =
|
|
@@ -2306,6 +2822,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2306
2822
|
reasoningEffort: event.payload.reasoningEffort ?? null,
|
|
2307
2823
|
latencyMode: event.payload.latencyMode ?? null,
|
|
2308
2824
|
mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
|
|
2825
|
+
connectionAuthorities: event.payload.connectionAuthorities,
|
|
2826
|
+
...(event.payload.personalResourceAttachment
|
|
2827
|
+
? { personalResourceAttachment: event.payload.personalResourceAttachment }
|
|
2828
|
+
: {}),
|
|
2829
|
+
authorization,
|
|
2309
2830
|
...(event.payload.controlEtag !== undefined
|
|
2310
2831
|
? { controlEtag: event.payload.controlEtag }
|
|
2311
2832
|
: {}),
|
|
@@ -2323,6 +2844,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2323
2844
|
workspaceId,
|
|
2324
2845
|
sessionId,
|
|
2325
2846
|
subjectId: grant.subjectId,
|
|
2847
|
+
respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
|
|
2326
2848
|
payload: event.payload,
|
|
2327
2849
|
clientEventId: event.clientEventId ?? null,
|
|
2328
2850
|
});
|
|
@@ -2354,6 +2876,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2354
2876
|
requestId: event.payload.requestId,
|
|
2355
2877
|
response: event.payload.response,
|
|
2356
2878
|
respondedBy: grant.subjectId,
|
|
2879
|
+
respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
|
|
2357
2880
|
clientEventId: event.clientEventId ?? null,
|
|
2358
2881
|
});
|
|
2359
2882
|
} catch (error) {
|
|
@@ -2481,7 +3004,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2481
3004
|
// would mint its relay terminal/desktop cells. Resolve the active target once
|
|
2482
3005
|
// and use the selfhosted liveness probe when it is a connected machine.
|
|
2483
3006
|
const activeSandbox = session.activeSandboxId
|
|
2484
|
-
? await getSandbox(
|
|
3007
|
+
? await getSandbox(
|
|
3008
|
+
db,
|
|
3009
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
3010
|
+
session.activeSandboxId,
|
|
3011
|
+
)
|
|
2485
3012
|
: null;
|
|
2486
3013
|
const selfhostedActive = activeSandbox?.kind === "selfhosted";
|
|
2487
3014
|
const lease = await readGroupLease(
|
|
@@ -2529,12 +3056,19 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2529
3056
|
|
|
2530
3057
|
let capabilities;
|
|
2531
3058
|
if (selfhostedActive && activeSandbox.enrollmentId) {
|
|
2532
|
-
const enrollment = await
|
|
3059
|
+
const [enrollment, liveConnection] = await Promise.all([
|
|
3060
|
+
getEnrollment(db, grant, activeSandbox.enrollmentId),
|
|
3061
|
+
getLiveEnrollmentConnection(db, grant, activeSandbox.enrollmentId),
|
|
3062
|
+
]);
|
|
2533
3063
|
let probeResponded = false;
|
|
2534
|
-
if (
|
|
3064
|
+
if (liveConnection?.connectionInstanceId) {
|
|
2535
3065
|
const machine = new SelfhostedSession({
|
|
2536
|
-
workspaceId,
|
|
2537
|
-
agentId:
|
|
3066
|
+
workspaceId: activeSandbox.workspaceId,
|
|
3067
|
+
agentId: liveConnection.id,
|
|
3068
|
+
connectionInstanceId: liveConnection.connectionInstanceId,
|
|
3069
|
+
// Capability negotiation only pings, so a pre-root agent may still
|
|
3070
|
+
// report upgrade guidance without exposing a false filesystem root.
|
|
3071
|
+
workspaceRoot: liveConnection.workspaceRoot ?? "/",
|
|
2538
3072
|
controlRpc: new NatsControlRpc(async () => bus.getRequestConnection()),
|
|
2539
3073
|
relay: relayConfigFromSettings(settings),
|
|
2540
3074
|
epoch: session.activeEpoch,
|
|
@@ -2597,6 +3131,20 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2597
3131
|
// place (resolveActiveDesktopTransport), covering BOTH swap directions.
|
|
2598
3132
|
let responseCapabilities = {
|
|
2599
3133
|
...capabilities,
|
|
3134
|
+
FileSystem: {
|
|
3135
|
+
...capabilities.FileSystem,
|
|
3136
|
+
root: selectBackend(
|
|
3137
|
+
(selfhostedActive
|
|
3138
|
+
? "selfhosted"
|
|
3139
|
+
: activeSandbox?.kind === "modal"
|
|
3140
|
+
? "modal"
|
|
3141
|
+
: session.sandboxBackend === "selfhosted" &&
|
|
3142
|
+
settings.sandboxBackend !== "selfhosted" &&
|
|
3143
|
+
settings.sandboxBackend !== "none"
|
|
3144
|
+
? settings.sandboxBackend
|
|
3145
|
+
: session.sandboxBackend) as SandboxBackend,
|
|
3146
|
+
).workspaceRoot,
|
|
3147
|
+
},
|
|
2600
3148
|
Git: {
|
|
2601
3149
|
...capabilities.Git,
|
|
2602
3150
|
repos: capabilities.Git.available ? repositoryRoots : [],
|
|
@@ -2723,7 +3271,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2723
3271
|
// attachViewer (it warms the Modal group box — the wrong target). Synthesize a
|
|
2724
3272
|
// result shaped like ViewerAttachResult and mint relay cells directly.
|
|
2725
3273
|
const activeSandbox = session.activeSandboxId
|
|
2726
|
-
? await getSandbox(
|
|
3274
|
+
? await getSandbox(
|
|
3275
|
+
db,
|
|
3276
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
3277
|
+
session.activeSandboxId,
|
|
3278
|
+
)
|
|
2727
3279
|
: null;
|
|
2728
3280
|
const selfhostedActive = activeSandbox?.kind === "selfhosted";
|
|
2729
3281
|
|
|
@@ -2753,6 +3305,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2753
3305
|
stream = await mintDesktopStream(viewerServices, {
|
|
2754
3306
|
accountId: grant.accountId,
|
|
2755
3307
|
workspaceId,
|
|
3308
|
+
resourceSubjectId: grant.subjectId,
|
|
3309
|
+
resourceSubjectDelegated: grant.metadata?.delegated === true,
|
|
2756
3310
|
session,
|
|
2757
3311
|
viewerId,
|
|
2758
3312
|
// No Modal lease for selfhosted-active; the mint routes to the relay.
|
|
@@ -2762,6 +3316,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2762
3316
|
terminal = await mintTerminalStream(viewerServices, {
|
|
2763
3317
|
accountId: grant.accountId,
|
|
2764
3318
|
workspaceId,
|
|
3319
|
+
resourceSubjectId: grant.subjectId,
|
|
3320
|
+
resourceSubjectDelegated: grant.metadata?.delegated === true,
|
|
2765
3321
|
session,
|
|
2766
3322
|
viewerId,
|
|
2767
3323
|
// No Modal lease for selfhosted-active; the mint routes to the relay.
|
|
@@ -2773,6 +3329,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2773
3329
|
accountId: grant.accountId,
|
|
2774
3330
|
workspaceId,
|
|
2775
3331
|
session,
|
|
3332
|
+
viewerSubjectId: grant.subjectId,
|
|
2776
3333
|
waitSignal: c.req.raw.signal,
|
|
2777
3334
|
...(parsed.data.viewerId ? { viewerId: parsed.data.viewerId } : {}),
|
|
2778
3335
|
});
|
|
@@ -2800,6 +3357,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2800
3357
|
stream = await mintDesktopStream(viewerServices, {
|
|
2801
3358
|
accountId: grant.accountId,
|
|
2802
3359
|
workspaceId,
|
|
3360
|
+
resourceSubjectId: grant.subjectId,
|
|
3361
|
+
resourceSubjectDelegated: grant.metadata?.delegated === true,
|
|
2803
3362
|
session,
|
|
2804
3363
|
viewerId: result.viewerId,
|
|
2805
3364
|
lease,
|
|
@@ -2809,6 +3368,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2809
3368
|
terminal = await mintTerminalStream(viewerServices, {
|
|
2810
3369
|
accountId: grant.accountId,
|
|
2811
3370
|
workspaceId,
|
|
3371
|
+
resourceSubjectId: grant.subjectId,
|
|
3372
|
+
resourceSubjectDelegated: grant.metadata?.delegated === true,
|
|
2812
3373
|
session,
|
|
2813
3374
|
viewerId: result.viewerId,
|
|
2814
3375
|
lease,
|
|
@@ -2951,6 +3512,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2951
3512
|
// (rides files:read); Terminal exec + PTY ride terminal:attach.
|
|
2952
3513
|
|
|
2953
3514
|
type ChannelARouteCtx = ChannelAContext & {
|
|
3515
|
+
grant: AccessGrant;
|
|
2954
3516
|
waitSignal: AbortSignal;
|
|
2955
3517
|
operation: ChannelAOperation;
|
|
2956
3518
|
};
|
|
@@ -2971,6 +3533,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2971
3533
|
throw new HTTPException(404, { message: "session not found" });
|
|
2972
3534
|
}
|
|
2973
3535
|
return {
|
|
3536
|
+
grant,
|
|
2974
3537
|
accountId: grant.accountId,
|
|
2975
3538
|
workspaceId,
|
|
2976
3539
|
session,
|
|
@@ -3020,6 +3583,21 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3020
3583
|
return c.json(out);
|
|
3021
3584
|
});
|
|
3022
3585
|
|
|
3586
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/publish", async (c) => {
|
|
3587
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3588
|
+
await requireAccessGrant(c, deps, workspaceId, "files:upload");
|
|
3589
|
+
const ctx = await channelAPreamble(c, "files:read", "artifact.publish");
|
|
3590
|
+
const request = await parseChannelABody(c, PublishSandboxFileArtifactRequest);
|
|
3591
|
+
return c.json(
|
|
3592
|
+
await publishSandboxFileArtifact(deps, {
|
|
3593
|
+
grant: ctx.grant,
|
|
3594
|
+
session: ctx.session,
|
|
3595
|
+
path: request.path,
|
|
3596
|
+
signal: ctx.waitSignal,
|
|
3597
|
+
}),
|
|
3598
|
+
);
|
|
3599
|
+
});
|
|
3600
|
+
|
|
3023
3601
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/write", async (c) => {
|
|
3024
3602
|
const ctx = await channelAPreamble(c, "files:write", "fs.write");
|
|
3025
3603
|
const req = await parseChannelABody(c, FsWriteRequest);
|
|
@@ -3142,7 +3720,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3142
3720
|
// the live/wake path — the feature degrades to today's behavior, never worse.
|
|
3143
3721
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/workspace/capture", async (c) => {
|
|
3144
3722
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
3145
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
3723
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
3146
3724
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
3147
3725
|
const lookup = await sessionLatestWorkspaceCapture(db, workspaceId, sessionId);
|
|
3148
3726
|
if (!lookup.sessionExists) {
|
|
@@ -3153,13 +3731,27 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3153
3731
|
return c.json({ available: false });
|
|
3154
3732
|
}
|
|
3155
3733
|
return c.json(
|
|
3156
|
-
await serveWorkspaceCapture(
|
|
3734
|
+
await serveWorkspaceCapture(
|
|
3735
|
+
lookup.capture,
|
|
3736
|
+
objectStorage,
|
|
3737
|
+
workspaceCaptureManifestCache,
|
|
3738
|
+
(fact) =>
|
|
3739
|
+
recordAuditEvent(db, {
|
|
3740
|
+
accountId: grant.accountId,
|
|
3741
|
+
workspaceId,
|
|
3742
|
+
subjectId: grant.subjectId,
|
|
3743
|
+
action: "file.signed_url.issued",
|
|
3744
|
+
targetType: "workspace_capture",
|
|
3745
|
+
targetId: sessionId,
|
|
3746
|
+
metadata: { sessionId, ...fact },
|
|
3747
|
+
}),
|
|
3748
|
+
),
|
|
3157
3749
|
);
|
|
3158
3750
|
});
|
|
3159
3751
|
|
|
3160
3752
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/workspace/capture/file", async (c) => {
|
|
3161
3753
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
3162
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
3754
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
3163
3755
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
3164
3756
|
const path = c.req.query("path");
|
|
3165
3757
|
if (!path) {
|
|
@@ -3188,7 +3780,21 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3188
3780
|
} else {
|
|
3189
3781
|
row = await latestWorkspaceCapture(db, workspaceId, sessionId);
|
|
3190
3782
|
}
|
|
3191
|
-
return c.json(
|
|
3783
|
+
return c.json(
|
|
3784
|
+
await serveWorkspaceCaptureFile(row, path, objectStorage, (fact) =>
|
|
3785
|
+
recordAuditEvent(db, {
|
|
3786
|
+
accountId: grant.accountId,
|
|
3787
|
+
workspaceId,
|
|
3788
|
+
subjectId: grant.subjectId,
|
|
3789
|
+
action: "file.signed_url.issued",
|
|
3790
|
+
targetType: "workspace_capture",
|
|
3791
|
+
targetId: sessionId,
|
|
3792
|
+
// The captured PATH is workspace-file identity (like a filename),
|
|
3793
|
+
// not content; the signed URL and object key are never recorded.
|
|
3794
|
+
metadata: { sessionId, path, ...fact },
|
|
3795
|
+
}),
|
|
3796
|
+
),
|
|
3797
|
+
);
|
|
3192
3798
|
});
|
|
3193
3799
|
|
|
3194
3800
|
// ── Terminal: synchronous exec ────────────────────────────────────────────
|
|
@@ -3510,15 +4116,24 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3510
4116
|
if (suffix === "") {
|
|
3511
4117
|
if (verb === "GET") return "session.read";
|
|
3512
4118
|
if (verb === "PATCH") return "session.title.write";
|
|
4119
|
+
if (verb === "DELETE") return "session.delete";
|
|
3513
4120
|
return null;
|
|
3514
4121
|
}
|
|
3515
4122
|
if (suffix === "/pin" && verb === "PUT") return "session.pin.write";
|
|
4123
|
+
if (suffix === "/attention" && verb === "PUT") return "session.attention.write";
|
|
4124
|
+
if (suffix === "/archive" && verb === "PUT") return "session.archive.write";
|
|
4125
|
+
if (suffix === "/visibility" && verb === "PUT") return "session.visibility.write";
|
|
4126
|
+
if (suffix === "/forks" && verb === "POST") return "session.fork.create";
|
|
3516
4127
|
if (suffix === "/channel" && verb === "PUT") return "session.channel.write";
|
|
3517
4128
|
if (suffix === "/tool-policy" && verb === "PUT") return "session.tool_policy.write";
|
|
3518
4129
|
if (/^\/mcp-servers\/[^/]+\/approval-policy$/.test(suffix) && verb === "PATCH") {
|
|
3519
4130
|
return "session.mcp.approval_policy.write";
|
|
3520
4131
|
}
|
|
3521
4132
|
if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
|
|
4133
|
+
if (suffix === "/background-commands" && verb === "GET") return "session.read";
|
|
4134
|
+
if (/^\/background-commands\/[^/]+$/.test(suffix) && verb === "DELETE") {
|
|
4135
|
+
return "session.control";
|
|
4136
|
+
}
|
|
3522
4137
|
if (suffix === "/codex-account" && verb === "POST") {
|
|
3523
4138
|
return "session.codex_account.write";
|
|
3524
4139
|
}
|
|
@@ -3554,9 +4169,13 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3554
4169
|
: null;
|
|
3555
4170
|
}
|
|
3556
4171
|
if (suffix === "/goal/revisions" && verb === "GET") return "session.goal.read";
|
|
4172
|
+
if (suffix === "/goal/revisions/page" && verb === "GET") return "session.goal.read";
|
|
3557
4173
|
if (/^\/goal\/revisions\/[^/]+\/apply$/.test(suffix) && verb === "POST") {
|
|
3558
4174
|
return "session.goal.write";
|
|
3559
4175
|
}
|
|
4176
|
+
if (/^\/goal\/revisions\/[^/]+\/(reject|rollback)$/.test(suffix) && verb === "POST") {
|
|
4177
|
+
return "session.goal.write";
|
|
4178
|
+
}
|
|
3560
4179
|
if (suffix === "/context/clear" || suffix === "/context/compact") {
|
|
3561
4180
|
return verb === "POST" ? "session.context.write" : null;
|
|
3562
4181
|
}
|
|
@@ -3574,6 +4193,9 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3574
4193
|
if (verb === "PUT") return "session.composer.write";
|
|
3575
4194
|
return null;
|
|
3576
4195
|
}
|
|
4196
|
+
// Same HTTP gate as POST /events. Delivery `steer` is re-authorized inside
|
|
4197
|
+
// acceptSessionUserMessageWithOutcome after the body is parsed.
|
|
4198
|
+
if (suffix === "/composer-draft/submit" && verb === "POST") return "session.append";
|
|
3577
4199
|
if (suffix === "/control" && verb === "POST") return "session.control";
|
|
3578
4200
|
if (suffix === "/steer" && verb === "POST") return "session.steer";
|
|
3579
4201
|
if (suffix === "/human-input-requests" && verb === "GET") {
|
|
@@ -3593,6 +4215,7 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3593
4215
|
if (suffix === "/fs/list" || suffix === "/fs/list-batch" || suffix === "/fs/read") {
|
|
3594
4216
|
return verb === "POST" ? "session.files.read" : null;
|
|
3595
4217
|
}
|
|
4218
|
+
if (suffix === "/artifacts/publish" && verb === "POST") return "session.files.write";
|
|
3596
4219
|
if (["/fs/write", "/fs/delete", "/fs/move", "/fs/mkdir"].includes(suffix)) {
|
|
3597
4220
|
return verb === "POST" ? "session.files.write" : null;
|
|
3598
4221
|
}
|
|
@@ -3621,6 +4244,70 @@ function sessionAuthorizationHttpError(error: unknown): HTTPException {
|
|
|
3621
4244
|
throw error;
|
|
3622
4245
|
}
|
|
3623
4246
|
|
|
4247
|
+
export function sessionTenancyHttpError(error: unknown): Error {
|
|
4248
|
+
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
4249
|
+
return new ApiHttpError(403, {
|
|
4250
|
+
code: "forbidden",
|
|
4251
|
+
message: "Session tenancy mutations require the owning managed-human session.",
|
|
4252
|
+
retryable: false,
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
if (error instanceof SessionTenancyNotActivatedError) {
|
|
4256
|
+
return new ApiHttpError(409, {
|
|
4257
|
+
code: "conflict",
|
|
4258
|
+
message: "Session tenancy is not activated for this organization.",
|
|
4259
|
+
retryable: false,
|
|
4260
|
+
details: { reason: "not_activated" },
|
|
4261
|
+
});
|
|
4262
|
+
}
|
|
4263
|
+
if (error instanceof SessionTenancyConflictError) {
|
|
4264
|
+
return new ApiHttpError(409, {
|
|
4265
|
+
code: error.reason === "operation_reuse" ? "idempotency_conflict" : "conflict",
|
|
4266
|
+
message:
|
|
4267
|
+
error.reason === "not_quiescent"
|
|
4268
|
+
? "The session must be fully quiescent before this change."
|
|
4269
|
+
: error.reason === "authority_epoch"
|
|
4270
|
+
? "The session authority changed before this operation committed."
|
|
4271
|
+
: "The idempotency key was already used with different input.",
|
|
4272
|
+
retryable: false,
|
|
4273
|
+
details: {
|
|
4274
|
+
reason: error.reason,
|
|
4275
|
+
...(error.blocker ? { blocker: error.blocker } : {}),
|
|
4276
|
+
},
|
|
4277
|
+
});
|
|
4278
|
+
}
|
|
4279
|
+
if (
|
|
4280
|
+
error instanceof SessionTenancyAccessError ||
|
|
4281
|
+
error instanceof SessionAuthorizationDeniedError
|
|
4282
|
+
) {
|
|
4283
|
+
return new ApiHttpError(404, {
|
|
4284
|
+
code: "not_found",
|
|
4285
|
+
message: "Session not found.",
|
|
4286
|
+
retryable: false,
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
if (error instanceof SessionTenancyInvalidRequestError) {
|
|
4290
|
+
return new ApiHttpError(422, {
|
|
4291
|
+
code: "validation_failed",
|
|
4292
|
+
message: "Invalid session tenancy request.",
|
|
4293
|
+
retryable: false,
|
|
4294
|
+
});
|
|
4295
|
+
}
|
|
4296
|
+
if (error instanceof SessionTenancyPersistenceOutcomeUnknownError) {
|
|
4297
|
+
return new ApiHttpError(503, {
|
|
4298
|
+
code: "upstream_unavailable",
|
|
4299
|
+
message:
|
|
4300
|
+
"The session tenancy mutation outcome is unknown. Retry with the same idempotency key.",
|
|
4301
|
+
retryable: true,
|
|
4302
|
+
outcomeUnknown: true,
|
|
4303
|
+
});
|
|
4304
|
+
}
|
|
4305
|
+
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
4306
|
+
return sessionAuthorizationHttpError(error);
|
|
4307
|
+
}
|
|
4308
|
+
return error instanceof Error ? error : new Error("Unknown session tenancy error");
|
|
4309
|
+
}
|
|
4310
|
+
|
|
3624
4311
|
function eventEnumValue<T extends string>(
|
|
3625
4312
|
raw: string | undefined,
|
|
3626
4313
|
schema: { safeParse(value: unknown): { success: boolean; data?: T } },
|
|
@@ -3682,6 +4369,7 @@ function sessionListQuery(
|
|
|
3682
4369
|
cursor: ReturnType<typeof decodeSessionListCursor> | undefined;
|
|
3683
4370
|
search: string | undefined;
|
|
3684
4371
|
pinsOnly: boolean;
|
|
4372
|
+
archivedOnly: boolean;
|
|
3685
4373
|
} {
|
|
3686
4374
|
const parentSessionId = query.parentSessionId;
|
|
3687
4375
|
// "null" = roots only; a uuid = children of that session; anything else is
|
|
@@ -3713,6 +4401,10 @@ function sessionListQuery(
|
|
|
3713
4401
|
});
|
|
3714
4402
|
}
|
|
3715
4403
|
const pinsOnly = query.pinsOnly === "true";
|
|
4404
|
+
if (query.archivedOnly !== undefined && query.archivedOnly !== "true") {
|
|
4405
|
+
throw new HTTPException(400, { message: 'archivedOnly must be the literal "true"' });
|
|
4406
|
+
}
|
|
4407
|
+
const archivedOnly = query.archivedOnly === "true";
|
|
3716
4408
|
if (pinsOnly && !allowCursor) {
|
|
3717
4409
|
throw new HTTPException(400, { message: 'pinsOnly requires view="page"' });
|
|
3718
4410
|
}
|
|
@@ -3721,6 +4413,9 @@ function sessionListQuery(
|
|
|
3721
4413
|
message: "pinsOnly cannot be combined with cursor, parentSessionId, or search",
|
|
3722
4414
|
});
|
|
3723
4415
|
}
|
|
4416
|
+
if (pinsOnly && archivedOnly) {
|
|
4417
|
+
throw new HTTPException(400, { message: "pinsOnly cannot be combined with archivedOnly" });
|
|
4418
|
+
}
|
|
3724
4419
|
return {
|
|
3725
4420
|
limit: query.limit,
|
|
3726
4421
|
parentSessionId:
|
|
@@ -3732,6 +4427,7 @@ function sessionListQuery(
|
|
|
3732
4427
|
cursor,
|
|
3733
4428
|
search: search || undefined,
|
|
3734
4429
|
pinsOnly,
|
|
4430
|
+
archivedOnly,
|
|
3735
4431
|
};
|
|
3736
4432
|
}
|
|
3737
4433
|
|
|
@@ -3865,6 +4561,24 @@ function optionalEventSequence(raw: string | undefined): number | undefined {
|
|
|
3865
4561
|
|
|
3866
4562
|
/** Stable, value-free JSON errors for only the create-session boundary. */
|
|
3867
4563
|
export function sessionCreateErrorResponse(c: Context, error: unknown): Response {
|
|
4564
|
+
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
4565
|
+
return c.json(
|
|
4566
|
+
{
|
|
4567
|
+
code: "SESSION_CREATE_FORBIDDEN",
|
|
4568
|
+
message: "Only managed-account users can create an Only-me session.",
|
|
4569
|
+
},
|
|
4570
|
+
403,
|
|
4571
|
+
);
|
|
4572
|
+
}
|
|
4573
|
+
if (error instanceof SessionTenancyNotActivatedError) {
|
|
4574
|
+
return c.json(
|
|
4575
|
+
{
|
|
4576
|
+
code: "SESSION_TENANCY_NOT_ACTIVATED",
|
|
4577
|
+
message: "Private sessions are not enabled for this organization.",
|
|
4578
|
+
},
|
|
4579
|
+
409,
|
|
4580
|
+
);
|
|
4581
|
+
}
|
|
3868
4582
|
if (error instanceof ChannelNotFoundError) {
|
|
3869
4583
|
// Covers the create-vs-channel-delete race the pre-validation cannot: the
|
|
3870
4584
|
// insert's FK rejection surfaces as the same 422 an unknown id gets.
|
|
@@ -3885,6 +4599,20 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
|
|
|
3885
4599
|
422,
|
|
3886
4600
|
);
|
|
3887
4601
|
}
|
|
4602
|
+
if (
|
|
4603
|
+
error instanceof HTTPException &&
|
|
4604
|
+
error.status === 409 &&
|
|
4605
|
+
error.cause instanceof NewSessionDraftConflictError
|
|
4606
|
+
) {
|
|
4607
|
+
return c.json(
|
|
4608
|
+
{
|
|
4609
|
+
code: "NEW_SESSION_DRAFT_CONFLICT",
|
|
4610
|
+
message: error.cause.message,
|
|
4611
|
+
currentRevision: error.cause.currentRevision,
|
|
4612
|
+
},
|
|
4613
|
+
409,
|
|
4614
|
+
);
|
|
4615
|
+
}
|
|
3888
4616
|
if (error instanceof HTTPException && error.status === 422) {
|
|
3889
4617
|
return c.json(
|
|
3890
4618
|
{
|
|
@@ -3894,6 +4622,15 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
|
|
|
3894
4622
|
422,
|
|
3895
4623
|
);
|
|
3896
4624
|
}
|
|
4625
|
+
if (error instanceof HTTPException && error.status === 409) {
|
|
4626
|
+
return c.json(
|
|
4627
|
+
{
|
|
4628
|
+
code: "SESSION_CREATE_CONFLICT",
|
|
4629
|
+
message: error.message,
|
|
4630
|
+
},
|
|
4631
|
+
409,
|
|
4632
|
+
);
|
|
4633
|
+
}
|
|
3897
4634
|
throw error;
|
|
3898
4635
|
}
|
|
3899
4636
|
|