@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/sandbox/channel-a.ts
CHANGED
|
@@ -30,25 +30,30 @@ import type { Session } from "@opengeni/contracts";
|
|
|
30
30
|
import {
|
|
31
31
|
acquireLease,
|
|
32
32
|
getSandboxSessionEnvelope,
|
|
33
|
-
|
|
33
|
+
getLiveEnrollmentConnection,
|
|
34
34
|
getSandbox,
|
|
35
|
+
touchLeaseHolder,
|
|
35
36
|
loadWorkspaceEnvironmentForRun,
|
|
36
37
|
markWarmLeaseInstanceLost,
|
|
37
38
|
readActiveSandbox,
|
|
38
39
|
readLease,
|
|
39
40
|
releaseLeaseHolder,
|
|
41
|
+
SandboxImageConflictError,
|
|
40
42
|
SandboxProviderReadLockUnavailableError,
|
|
43
|
+
SandboxRigConflictError,
|
|
41
44
|
withSandboxProviderReadLock,
|
|
42
45
|
type Database,
|
|
43
46
|
type LeaseSnapshot,
|
|
44
47
|
} from "@opengeni/db";
|
|
45
48
|
import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
|
|
46
49
|
import {
|
|
50
|
+
recordTenancyCompatibilityLaneUse,
|
|
47
51
|
sandboxLeaseTelemetryKey,
|
|
48
52
|
sandboxOperationMetricObserver,
|
|
49
53
|
type Observability,
|
|
50
54
|
} from "@opengeni/observability";
|
|
51
55
|
import { HTTPException } from "hono/http-exception";
|
|
56
|
+
import { ApiHttpError } from "../http/api-error";
|
|
52
57
|
|
|
53
58
|
import {
|
|
54
59
|
buildSelfhostedBackendSession,
|
|
@@ -59,6 +64,8 @@ import {
|
|
|
59
64
|
NatsOpStreamTransport,
|
|
60
65
|
SandboxResumeIdentityMismatchError,
|
|
61
66
|
SandboxResumeIdentityUnavailableError,
|
|
67
|
+
RoutingWorkspaceRootChangedError,
|
|
68
|
+
SelfhostedWorkspaceRootChangedError,
|
|
62
69
|
ChannelAConflictError,
|
|
63
70
|
ChannelANotFoundError,
|
|
64
71
|
ChannelAUnsupportedError,
|
|
@@ -66,14 +73,23 @@ import {
|
|
|
66
73
|
ChannelAValidationError,
|
|
67
74
|
BrowserControlRequestError,
|
|
68
75
|
BrowserControlTransportError,
|
|
76
|
+
SelfhostedControlError,
|
|
77
|
+
agentErrorToControlError,
|
|
69
78
|
codemodeTokenFileFromEnvironment,
|
|
79
|
+
offlineAgentError,
|
|
80
|
+
resolveConnectedMachineWorkspaceRoot,
|
|
70
81
|
withCodemodeTokenSession,
|
|
71
82
|
withRunCredentialsSession,
|
|
72
83
|
type ChannelASession,
|
|
73
84
|
type EstablishedSandboxSession,
|
|
74
85
|
type RoutingSandboxSession,
|
|
75
86
|
} from "@opengeni/runtime/sandbox";
|
|
76
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
providerSettingsForSessionSandboxRuntime,
|
|
89
|
+
relayConfigFromSettings,
|
|
90
|
+
resolveSessionSandboxRuntime,
|
|
91
|
+
wrapChannelABoxWithRouting,
|
|
92
|
+
} from "@opengeni/core";
|
|
77
93
|
import { establishApiSandboxSpawner } from "./rematerialize";
|
|
78
94
|
|
|
79
95
|
export type ChannelAServices = {
|
|
@@ -87,6 +103,7 @@ export type ChannelAOperation =
|
|
|
87
103
|
| "fs.list"
|
|
88
104
|
| "fs.list-batch"
|
|
89
105
|
| "fs.read"
|
|
106
|
+
| "artifact.publish"
|
|
90
107
|
| "fs.write"
|
|
91
108
|
| "fs.delete"
|
|
92
109
|
| "fs.move"
|
|
@@ -522,12 +539,12 @@ async function withChannelAOperation<T>(
|
|
|
522
539
|
|
|
523
540
|
// The STABLE run-environment used by both a cloud home and a machine home.
|
|
524
541
|
// It also carries the per-session Codemode pointer selected below.
|
|
525
|
-
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
|
|
526
|
-
|
|
527
|
-
settings,
|
|
542
|
+
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(db, settings, {
|
|
543
|
+
accountId,
|
|
528
544
|
workspaceId,
|
|
529
|
-
session.environmentId,
|
|
530
|
-
|
|
545
|
+
variableSetId: session.environmentId,
|
|
546
|
+
authority: { kind: "session_attach", sessionId: session.id, subjectId: ctx.subjectId },
|
|
547
|
+
});
|
|
531
548
|
const settingsForSession =
|
|
532
549
|
session.sandboxBackend !== settings.sandboxBackend
|
|
533
550
|
? { ...settings, sandboxBackend: session.sandboxBackend }
|
|
@@ -566,12 +583,20 @@ async function withChannelAOperation<T>(
|
|
|
566
583
|
codemodeTokenFileFromEnvironment(environment, session.id),
|
|
567
584
|
)
|
|
568
585
|
: credentialSession;
|
|
586
|
+
const fileSystemRoot = await routingSession.fileSystemRoot();
|
|
569
587
|
const service = new SandboxChannelAService({
|
|
570
588
|
session: scopedSession as ChannelASession,
|
|
589
|
+
workspaceRoot: fileSystemRoot,
|
|
571
590
|
leaseEpoch: lease?.leaseEpoch ?? session.activeEpoch,
|
|
572
591
|
emit,
|
|
573
592
|
});
|
|
574
|
-
const result = await fn({
|
|
593
|
+
const result = await fn({
|
|
594
|
+
service,
|
|
595
|
+
lease,
|
|
596
|
+
homeSession,
|
|
597
|
+
routingSession,
|
|
598
|
+
requestId,
|
|
599
|
+
});
|
|
575
600
|
// The direct request has accepted the result in memory. Finalize every
|
|
576
601
|
// Connected Machine backend the routing proxy reached so a mid-request
|
|
577
602
|
// route transition cannot leave completed output retained until TTL.
|
|
@@ -588,27 +613,76 @@ async function withChannelAOperation<T>(
|
|
|
588
613
|
ctx.waitSignal?.throwIfAborted();
|
|
589
614
|
const pointer = await readActiveSandbox(db, workspaceId, session.id);
|
|
590
615
|
if (!pointer?.activeSandboxId) {
|
|
616
|
+
// A machine-home session with no selected machine uses the deployment's
|
|
617
|
+
// managed group box, exactly like the worker turn path. Keep the durable
|
|
618
|
+
// home label honest; only this request's effective backend changes.
|
|
619
|
+
if (settings.sandboxBackend !== "none" && settings.sandboxBackend !== "selfhosted") {
|
|
620
|
+
return await withChannelAOperation(
|
|
621
|
+
services,
|
|
622
|
+
{
|
|
623
|
+
...ctx,
|
|
624
|
+
session: {
|
|
625
|
+
...session,
|
|
626
|
+
sandboxBackend: settings.sandboxBackend,
|
|
627
|
+
sandboxOs: "linux",
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
readOnly,
|
|
631
|
+
fn,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
591
634
|
throw new HTTPException(409, {
|
|
592
635
|
message: "machine-home session has no active Connected Machine",
|
|
593
636
|
});
|
|
594
637
|
}
|
|
595
|
-
const sandbox = await getSandbox(
|
|
638
|
+
const sandbox = await getSandbox(
|
|
639
|
+
db,
|
|
640
|
+
{ accountId: ctx.accountId, workspaceId, subjectId: ctx.subjectId },
|
|
641
|
+
pointer.activeSandboxId,
|
|
642
|
+
);
|
|
596
643
|
if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
597
644
|
throw new HTTPException(409, {
|
|
598
645
|
message: "machine-home session points to an unavailable Connected Machine",
|
|
599
646
|
});
|
|
600
647
|
}
|
|
601
|
-
const
|
|
648
|
+
const originWorkspaceId = sandbox.workspaceId;
|
|
649
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
650
|
+
db,
|
|
651
|
+
{ accountId, workspaceId, subjectId: ctx.subjectId },
|
|
652
|
+
sandbox.enrollmentId,
|
|
653
|
+
);
|
|
654
|
+
if (!enrollment?.connectionInstanceId) {
|
|
655
|
+
// Preserve causal machine liveness through Channel-A. Generic callers
|
|
656
|
+
// retain the established 409 mapping below; Browser/Computer callers can
|
|
657
|
+
// project the same bounded `agent_offline` contract as control RPC.
|
|
658
|
+
throw agentErrorToControlError(
|
|
659
|
+
offlineAgentError("Connected Machine has no live runner connection", true),
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (!enrollment.workspaceRoot) {
|
|
663
|
+
throw new HTTPException(409, {
|
|
664
|
+
message:
|
|
665
|
+
"Connected Machine has not reported an absolute workspace root; reconnect it with a current agent",
|
|
666
|
+
});
|
|
667
|
+
}
|
|
602
668
|
const built = await buildSelfhostedBackendSession({
|
|
603
|
-
workspaceId,
|
|
669
|
+
workspaceId: originWorkspaceId,
|
|
604
670
|
agentId: sandbox.enrollmentId,
|
|
671
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
672
|
+
workspaceRoot: resolveConnectedMachineWorkspaceRoot(
|
|
673
|
+
enrollment.workspaceRoot,
|
|
674
|
+
pointer.workingDir,
|
|
675
|
+
),
|
|
605
676
|
relay: relayConfigFromSettings(settings),
|
|
606
677
|
controlRpcFactory: () => new NatsControlRpc(async () => bus.getRequestConnection()),
|
|
607
678
|
epoch: pointer.activeEpoch,
|
|
608
679
|
environment,
|
|
609
|
-
workingDir: pointer.workingDir,
|
|
610
680
|
timeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
|
|
611
681
|
execTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
|
|
682
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
683
|
+
operationResourcePolicySupported:
|
|
684
|
+
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
685
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
612
686
|
...(settings.agentOpStreamEnabled === true &&
|
|
613
687
|
enrollment?.opStream === true &&
|
|
614
688
|
bus.getOpStreamConnection
|
|
@@ -640,6 +714,7 @@ async function withChannelAOperation<T>(
|
|
|
640
714
|
accountId,
|
|
641
715
|
workspaceId,
|
|
642
716
|
sessionId: session.id,
|
|
717
|
+
resourceSubjectId: ctx.subjectId,
|
|
643
718
|
pinnedSelfhosted: {
|
|
644
719
|
sandboxId: sandbox.id,
|
|
645
720
|
epoch: pointer.activeEpoch,
|
|
@@ -665,6 +740,12 @@ async function withChannelAOperation<T>(
|
|
|
665
740
|
}
|
|
666
741
|
}
|
|
667
742
|
|
|
743
|
+
// One session has one logical runtime across turns and every API-direct
|
|
744
|
+
// surface. Without this, Terminal/Files/Browser/Computer/viewers could rearm
|
|
745
|
+
// a stale deployment image after the worker had resolved a newer Pack/Rig or
|
|
746
|
+
// deployment image for the same durable sandbox group.
|
|
747
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(db, settings, session);
|
|
748
|
+
|
|
668
749
|
const release = async (): Promise<void> => {
|
|
669
750
|
await releaseLeaseHolder(db, {
|
|
670
751
|
accountId,
|
|
@@ -693,6 +774,8 @@ async function withChannelAOperation<T>(
|
|
|
693
774
|
subjectId: session.id,
|
|
694
775
|
backend: session.sandboxBackend,
|
|
695
776
|
os: session.sandboxOs,
|
|
777
|
+
image: sandboxRuntime.image,
|
|
778
|
+
rigVersionId: session.rigVersionId,
|
|
696
779
|
leaseTtlMs,
|
|
697
780
|
warmingLeaseTtlMs: settings.sandboxWarmingTimeoutMs,
|
|
698
781
|
captureWaitMs: sandboxLifecycleTransitionWaitMs(settings),
|
|
@@ -716,6 +799,56 @@ async function withChannelAOperation<T>(
|
|
|
716
799
|
});
|
|
717
800
|
}
|
|
718
801
|
} catch (error) {
|
|
802
|
+
let mappedError: unknown = error;
|
|
803
|
+
if (error instanceof SandboxImageConflictError) {
|
|
804
|
+
// A durable Browser/Computer holder can outlive its provider container.
|
|
805
|
+
// Runtime drift is checked before a new direct holder is admitted, so a
|
|
806
|
+
// dead old container would otherwise strand the lease behind a permanent
|
|
807
|
+
// conflict. Probe the exact old identity without replacement; only an
|
|
808
|
+
// authoritative provider-missing result may retire it and unblock the
|
|
809
|
+
// next request's cold successor election.
|
|
810
|
+
const live = await readLease(db, workspaceId, sandboxGroupId).catch(() => null);
|
|
811
|
+
if (live?.liveness === "warm" && live.instanceId !== null && live.resumeState !== null) {
|
|
812
|
+
let probe: EstablishedSandboxSession | undefined;
|
|
813
|
+
try {
|
|
814
|
+
probe = await establishSandboxSessionFromEnvelope(
|
|
815
|
+
sandboxRuntime.settings,
|
|
816
|
+
live.resumeState,
|
|
817
|
+
{
|
|
818
|
+
sessionId: session.id,
|
|
819
|
+
recovery: "resume-only",
|
|
820
|
+
backendOverride: session.sandboxBackend,
|
|
821
|
+
environment,
|
|
822
|
+
},
|
|
823
|
+
);
|
|
824
|
+
} catch (probeError) {
|
|
825
|
+
if (isProviderSandboxNotFoundError(session.sandboxBackend, probeError)) {
|
|
826
|
+
const marked = await markWarmLeaseInstanceLost(db, {
|
|
827
|
+
accountId,
|
|
828
|
+
workspaceId,
|
|
829
|
+
sandboxGroupId,
|
|
830
|
+
expectedEpoch: live.leaseEpoch,
|
|
831
|
+
expectedInstanceId: live.instanceId,
|
|
832
|
+
}).catch(() => null);
|
|
833
|
+
if (marked?.status === "marked") {
|
|
834
|
+
await appendAndPublishEvents(db, bus, workspaceId, session.id, [
|
|
835
|
+
{
|
|
836
|
+
type: "sandbox.box.lost",
|
|
837
|
+
payload: { sandboxId: live.instanceId },
|
|
838
|
+
},
|
|
839
|
+
]).catch(() => undefined);
|
|
840
|
+
mappedError = new ApiHttpError(409, {
|
|
841
|
+
code: "conflict",
|
|
842
|
+
message: "sandbox instance was lost; retry to restore it",
|
|
843
|
+
retryable: true,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} finally {
|
|
848
|
+
await dropEstablishedHandle(probe);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
719
852
|
// Release is idempotent. If acquisition committed before the request was
|
|
720
853
|
// cancelled, this removes that exact direct holder; a transient DB failure
|
|
721
854
|
// must not overwrite the original structural error (holder TTL is the final
|
|
@@ -727,12 +860,28 @@ async function withChannelAOperation<T>(
|
|
|
727
860
|
backend: session.sandboxBackend,
|
|
728
861
|
operation,
|
|
729
862
|
durationMs: performance.now() - operationStartedAt,
|
|
730
|
-
error,
|
|
863
|
+
error: mappedError,
|
|
731
864
|
waitSignal: ctx.waitSignal,
|
|
732
865
|
});
|
|
733
|
-
throw mapChannelAError(
|
|
866
|
+
throw mapChannelAError(mappedError, ctx.waitSignal);
|
|
734
867
|
}
|
|
735
868
|
|
|
869
|
+
// Keep the exact direct-request owner visible for the full operation. A
|
|
870
|
+
// yielded command is also tracked by a non-TTL process holder, but the
|
|
871
|
+
// retained-process reconciler must not poll that provider session while this
|
|
872
|
+
// request is still its active owner. Stale direct holders remain crash-
|
|
873
|
+
// recoverable through the ordinary holder TTL reaper.
|
|
874
|
+
const directHolderHeartbeat = setInterval(() => {
|
|
875
|
+
void touchLeaseHolder(db, {
|
|
876
|
+
accountId,
|
|
877
|
+
workspaceId,
|
|
878
|
+
sandboxGroupId,
|
|
879
|
+
kind: "direct",
|
|
880
|
+
holderId,
|
|
881
|
+
}).catch(() => undefined);
|
|
882
|
+
}, 10_000);
|
|
883
|
+
directHolderHeartbeat.unref?.();
|
|
884
|
+
|
|
736
885
|
let established: EstablishedSandboxSession | undefined;
|
|
737
886
|
let leaseSnapshot: LeaseSnapshot = acquired.lease;
|
|
738
887
|
let establishedCacheKey: string | null = null;
|
|
@@ -747,7 +896,7 @@ async function withChannelAOperation<T>(
|
|
|
747
896
|
): Promise<{ established: EstablishedSandboxSession; cacheKey: string }> => {
|
|
748
897
|
const cacheKey = establishedHandleCacheKey(workspaceId, session.id, live);
|
|
749
898
|
const establish = () =>
|
|
750
|
-
establishSandboxSessionFromEnvelope(settings, live.resumeState, {
|
|
899
|
+
establishSandboxSessionFromEnvelope(sandboxRuntime.settings, live.resumeState, {
|
|
751
900
|
sessionId: session.id,
|
|
752
901
|
recovery: "resume-only",
|
|
753
902
|
backendOverride: session.sandboxBackend,
|
|
@@ -781,8 +930,10 @@ async function withChannelAOperation<T>(
|
|
|
781
930
|
},
|
|
782
931
|
]);
|
|
783
932
|
}
|
|
784
|
-
throw new
|
|
933
|
+
throw new ApiHttpError(409, {
|
|
934
|
+
code: "conflict",
|
|
785
935
|
message: `sandbox instance was lost; retry to restore it`,
|
|
936
|
+
retryable: true,
|
|
786
937
|
});
|
|
787
938
|
}
|
|
788
939
|
};
|
|
@@ -802,9 +953,13 @@ async function withChannelAOperation<T>(
|
|
|
802
953
|
// resume_state is the lease's authoritative box descriptor; the session
|
|
803
954
|
// `_sandbox` envelope is only the per-session fallback.
|
|
804
955
|
try {
|
|
956
|
+
const providerSettings = await providerSettingsForSessionSandboxRuntime(
|
|
957
|
+
sandboxRuntime,
|
|
958
|
+
session.sandboxBackend,
|
|
959
|
+
);
|
|
805
960
|
const result = await establishApiSandboxSpawner({
|
|
806
961
|
db,
|
|
807
|
-
settings,
|
|
962
|
+
settings: providerSettings,
|
|
808
963
|
accountId,
|
|
809
964
|
workspaceId,
|
|
810
965
|
sandboxGroupId,
|
|
@@ -863,6 +1018,7 @@ async function withChannelAOperation<T>(
|
|
|
863
1018
|
accountId,
|
|
864
1019
|
workspaceId,
|
|
865
1020
|
sessionId: session.id,
|
|
1021
|
+
resourceSubjectId: ctx.subjectId,
|
|
866
1022
|
homeLease: {
|
|
867
1023
|
sandboxGroupId,
|
|
868
1024
|
leaseEpoch: leaseSnapshot.leaseEpoch,
|
|
@@ -990,6 +1146,7 @@ async function withChannelAOperation<T>(
|
|
|
990
1146
|
});
|
|
991
1147
|
throw mapChannelAError(error, ctx.waitSignal);
|
|
992
1148
|
} finally {
|
|
1149
|
+
clearInterval(directHolderHeartbeat);
|
|
993
1150
|
await release();
|
|
994
1151
|
await dropEstablishedHandle(established);
|
|
995
1152
|
}
|
|
@@ -1009,8 +1166,25 @@ export function mapChannelAError(error: unknown, waitSignal?: AbortSignal): unkn
|
|
|
1009
1166
|
error instanceof SandboxResumeIdentityUnavailableError
|
|
1010
1167
|
)
|
|
1011
1168
|
return new HTTPException(409, { message: error.message });
|
|
1169
|
+
if (
|
|
1170
|
+
error instanceof RoutingWorkspaceRootChangedError ||
|
|
1171
|
+
error instanceof SelfhostedWorkspaceRootChangedError
|
|
1172
|
+
)
|
|
1173
|
+
return new ApiHttpError(409, {
|
|
1174
|
+
code: "conflict",
|
|
1175
|
+
message: error.message,
|
|
1176
|
+
retryable: true,
|
|
1177
|
+
});
|
|
1012
1178
|
if (error instanceof SandboxProviderReadLockUnavailableError)
|
|
1013
1179
|
return new HTTPException(503, { message: error.message });
|
|
1180
|
+
if (error instanceof SandboxImageConflictError || error instanceof SandboxRigConflictError)
|
|
1181
|
+
return new ApiHttpError(409, {
|
|
1182
|
+
code: "conflict",
|
|
1183
|
+
message: "sandbox runtime changed while this session still has active operations; retry",
|
|
1184
|
+
retryable: true,
|
|
1185
|
+
});
|
|
1186
|
+
if (error instanceof SelfhostedControlError && error.agentOffline)
|
|
1187
|
+
return new HTTPException(409, { message: error.message, cause: error });
|
|
1014
1188
|
if (error instanceof ChannelAUnavailableError)
|
|
1015
1189
|
return new HTTPException(503, { message: error.message });
|
|
1016
1190
|
if (error instanceof ChannelAValidationError)
|
|
@@ -1021,9 +1195,31 @@ export function mapChannelAError(error: unknown, waitSignal?: AbortSignal): unkn
|
|
|
1021
1195
|
return new HTTPException(409, { message: error.message });
|
|
1022
1196
|
if (error instanceof ChannelAUnsupportedError)
|
|
1023
1197
|
return new HTTPException(409, { message: error.message });
|
|
1198
|
+
const authorityFence = workspaceMutationAuthorityFenceCode(error);
|
|
1199
|
+
// A revoked grant is an authorization outcome, not a server fault; an
|
|
1200
|
+
// unattributed pre-0277 writer is a conflict the caller resolves by starting
|
|
1201
|
+
// fresh work. Both must be visible instead of surfacing as a 500.
|
|
1202
|
+
if (authorityFence === "authority_revoked")
|
|
1203
|
+
return new HTTPException(403, { message: (error as Error).message });
|
|
1204
|
+
if (authorityFence === "authority_unattributed")
|
|
1205
|
+
return new HTTPException(409, { message: (error as Error).message });
|
|
1024
1206
|
return error;
|
|
1025
1207
|
}
|
|
1026
1208
|
|
|
1209
|
+
/** Structural check: the fence type lives in `@opengeni/db` and is raised deep
|
|
1210
|
+
* inside admission, so match it the same way the worker does. */
|
|
1211
|
+
function workspaceMutationAuthorityFenceCode(error: unknown): string | null {
|
|
1212
|
+
if (
|
|
1213
|
+
!(error instanceof Error) ||
|
|
1214
|
+
(error.name !== "SandboxWorkspaceMutationFencedError" &&
|
|
1215
|
+
error.name !== "SandboxRetainedProcessPromotionFencedError")
|
|
1216
|
+
) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
const code = (error as Error & { code?: unknown }).code;
|
|
1220
|
+
return typeof code === "string" ? code : null;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1027
1223
|
export function isChannelARequestCancellation(error: unknown, waitSignal?: AbortSignal): boolean {
|
|
1028
1224
|
if (waitSignal?.aborted !== true) return false;
|
|
1029
1225
|
const isSignalReason = waitSignal.reason !== undefined && error === waitSignal.reason;
|
|
@@ -1053,7 +1249,14 @@ export function channelAOperationFailureDiagnostic(
|
|
|
1053
1249
|
errorCode: "sandbox_channel_a_provider_busy",
|
|
1054
1250
|
};
|
|
1055
1251
|
}
|
|
1056
|
-
if (error instanceof
|
|
1252
|
+
if (error instanceof SandboxImageConflictError || error instanceof SandboxRigConflictError) {
|
|
1253
|
+
return {
|
|
1254
|
+
reason: "lifecycle_conflict",
|
|
1255
|
+
status: 409,
|
|
1256
|
+
errorCode: "sandbox_channel_a_lifecycle_conflict",
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (error instanceof ChannelAUnavailableError || error instanceof BrowserControlTransportError) {
|
|
1057
1260
|
return {
|
|
1058
1261
|
reason: "provider_unavailable",
|
|
1059
1262
|
status: 503,
|
|
@@ -1063,6 +1266,8 @@ export function channelAOperationFailureDiagnostic(
|
|
|
1063
1266
|
if (
|
|
1064
1267
|
error instanceof SandboxResumeIdentityMismatchError ||
|
|
1065
1268
|
error instanceof SandboxResumeIdentityUnavailableError ||
|
|
1269
|
+
error instanceof RoutingWorkspaceRootChangedError ||
|
|
1270
|
+
error instanceof SelfhostedWorkspaceRootChangedError ||
|
|
1066
1271
|
(error instanceof HTTPException && error.status === 409)
|
|
1067
1272
|
) {
|
|
1068
1273
|
return {
|
|
@@ -1091,6 +1296,16 @@ export function channelAOperationFailureDiagnostic(
|
|
|
1091
1296
|
errorCode: "sandbox_channel_a_operation_failed",
|
|
1092
1297
|
};
|
|
1093
1298
|
}
|
|
1299
|
+
// An authority fence is a deliberate rejection, not a fault: keep it out of
|
|
1300
|
+
// the unexpected-failure signal that operators page on.
|
|
1301
|
+
const authorityFence = workspaceMutationAuthorityFenceCode(error);
|
|
1302
|
+
if (authorityFence === "authority_revoked" || authorityFence === "authority_unattributed") {
|
|
1303
|
+
return {
|
|
1304
|
+
reason: "request_rejected",
|
|
1305
|
+
status: authorityFence === "authority_revoked" ? 403 : 409,
|
|
1306
|
+
errorCode: "sandbox_channel_a_operation_failed",
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1094
1309
|
return {
|
|
1095
1310
|
reason: "unexpected",
|
|
1096
1311
|
status: 500,
|
|
@@ -1098,7 +1313,8 @@ export function channelAOperationFailureDiagnostic(
|
|
|
1098
1313
|
};
|
|
1099
1314
|
}
|
|
1100
1315
|
|
|
1101
|
-
|
|
1316
|
+
/** Exported for the telemetry contract tests; not a route surface. */
|
|
1317
|
+
export function observeChannelAOperationFailure(
|
|
1102
1318
|
services: ChannelAServices,
|
|
1103
1319
|
input: {
|
|
1104
1320
|
workspaceId: string;
|
|
@@ -1111,6 +1327,13 @@ function observeChannelAOperationFailure(
|
|
|
1111
1327
|
},
|
|
1112
1328
|
): void {
|
|
1113
1329
|
if (!services.observability) return;
|
|
1330
|
+
// A writer with no recorded authority is a compatibility lane, not a fault:
|
|
1331
|
+
// count the lane itself so an operator can see whether it is still live,
|
|
1332
|
+
// separately from the structural failure signal operators page on below.
|
|
1333
|
+
// Lane name only - never the workspace, sandbox group, backend, or operation.
|
|
1334
|
+
if (workspaceMutationAuthorityFenceCode(input.error) === "authority_unattributed") {
|
|
1335
|
+
recordTenancyCompatibilityLaneUse(services.observability, "workspace_writer_unattributed");
|
|
1336
|
+
}
|
|
1114
1337
|
const diagnostic = channelAOperationFailureDiagnostic(input.error, input.waitSignal);
|
|
1115
1338
|
const attributes = {
|
|
1116
1339
|
sandboxLeaseKey: sandboxLeaseTelemetryKey(input.workspaceId, input.sandboxGroupId),
|
|
@@ -172,7 +172,7 @@ export async function startDeviceEnrollment(
|
|
|
172
172
|
throw lastError instanceof Error ? lastError : new Error("failed to start device enrollment");
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
const base = input.verificationOrigin.replace(
|
|
175
|
+
const base = input.verificationOrigin.replace(/\/+$/, "");
|
|
176
176
|
const verificationUri = `${base}/device`;
|
|
177
177
|
const verificationUriComplete = `${verificationUri}?user_code=${encodeURIComponent(request.userCode)}`;
|
|
178
178
|
return {
|
|
@@ -193,12 +193,18 @@ export async function approveDeviceEnrollment(
|
|
|
193
193
|
input: {
|
|
194
194
|
accountId: string;
|
|
195
195
|
workspaceId: string;
|
|
196
|
+
scope?: "organization" | "workspace" | "user";
|
|
197
|
+
allowOrganization?: boolean;
|
|
196
198
|
userCode: string;
|
|
197
199
|
allowScreenControl: boolean;
|
|
198
200
|
approvedBySubjectId: string;
|
|
199
201
|
approvedBySubjectLabel?: string | null;
|
|
200
202
|
},
|
|
201
|
-
): Promise<{
|
|
203
|
+
): Promise<{
|
|
204
|
+
enrollmentId: string;
|
|
205
|
+
sandboxId: string;
|
|
206
|
+
allowScreenControl: boolean;
|
|
207
|
+
} | null> {
|
|
202
208
|
const { db } = services;
|
|
203
209
|
const pending = await getPendingDeviceEnrollmentRequestByUserCode(
|
|
204
210
|
db,
|
|
@@ -213,6 +219,8 @@ export async function approveDeviceEnrollment(
|
|
|
213
219
|
const result = await approveDeviceEnrollmentRequest(db, {
|
|
214
220
|
accountId: input.accountId,
|
|
215
221
|
workspaceId: input.workspaceId,
|
|
222
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
223
|
+
allowOrganization: input.allowOrganization === true,
|
|
216
224
|
requestId: pending.id,
|
|
217
225
|
allowScreenControl: input.allowScreenControl,
|
|
218
226
|
approvedBySubjectId: input.approvedBySubjectId,
|
|
@@ -291,7 +299,11 @@ export async function denyDeviceEnrollment(
|
|
|
291
299
|
* mirror poll's "disabled" handling. The token value is NEVER logged. */
|
|
292
300
|
export async function mintEnrollToken(
|
|
293
301
|
services: EnrollmentServices,
|
|
294
|
-
input: {
|
|
302
|
+
input: {
|
|
303
|
+
accountId: string;
|
|
304
|
+
workspaceId: string;
|
|
305
|
+
allowScreenControl: boolean;
|
|
306
|
+
},
|
|
295
307
|
): Promise<MintEnrollTokenResponse | null> {
|
|
296
308
|
const { settings } = services;
|
|
297
309
|
const secret = resolveEnrollmentSigningSecret(settings);
|