@opengeni/core 0.28.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +182 -3
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +24 -0
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +31 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +34 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +39 -2
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +26 -13
- package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
- package/dist/application/session-tenancy.d.ts +19 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/billing/limits.d.ts +11 -0
- package/dist/canonical-human-identities.d.ts +7 -2
- package/dist/canonical-human-identities.js +17 -4
- package/dist/canonical-human-identities.js.map +1 -1
- package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
- package/dist/chunk-2MKMRGSY.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
- package/dist/chunk-GVO3N37G.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
- package/dist/chunk-LCQXNR62.js.map +1 -0
- package/dist/chunk-MM6N7CTO.js +387 -0
- package/dist/chunk-MM6N7CTO.js.map +1 -0
- package/dist/chunk-QO5GVFFO.js +235 -0
- package/dist/chunk-QO5GVFFO.js.map +1 -0
- package/dist/codex-model-availability.d.ts +14 -0
- package/dist/default-session-model.d.ts +139 -0
- package/dist/dependencies.d.ts +87 -4
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +58 -0
- package/dist/domain/capabilities.d.ts +22 -4
- package/dist/domain/company-profile-agent-admin.d.ts +25 -0
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +36 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +4 -3
- package/dist/domain/environments.d.ts +5 -9
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/file-owner.d.ts +8 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +28 -1
- package/dist/domain/knowledge-files.d.ts +20 -0
- package/dist/domain/knowledge-messages.d.ts +39 -0
- package/dist/domain/knowledge-messages.test.d.ts +1 -0
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -0
- package/dist/domain/knowledge-search.test.d.ts +1 -0
- package/dist/domain/knowledge.d.ts +9 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/personal-connection-delegations.d.ts +87 -4
- package/dist/domain/pr-review.d.ts +249 -0
- package/dist/domain/resources.d.ts +24 -3
- package/dist/domain/scheduled-task-access.d.ts +247 -0
- package/dist/domain/scheduled-tasks.d.ts +103 -6
- package/dist/domain/session-connection-accounts.d.ts +16 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +93 -4
- package/dist/domain/sessions.d.ts +275 -28
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +117 -0
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/editable-artifact-live/application.d.ts +12 -3
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +5 -2
- package/dist/editable-artifact-live/ticket.d.ts +2 -2
- package/dist/editable-artifact-live/types.d.ts +14 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -4
- package/dist/index.d.ts +49 -3
- package/dist/index.js +18826 -6847
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +131 -0
- package/dist/managed-auth-session-sets.js +51 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/managed-session.d.ts +98 -11
- package/dist/model-catalog.d.ts +117 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +22 -5
- package/dist/rigs/provider-images.d.ts +1 -1
- package/dist/sandbox/fleet.d.ts +59 -10
- package/dist/sandbox/routing.d.ts +22 -2
- package/dist/sandbox/runtime-settings.d.ts +56 -0
- package/dist/session-authorization.d.ts +38 -1
- package/dist/site-session-origin.d.ts +9 -0
- package/dist/transcription.d.ts +12 -1
- package/dist/turn-surface.d.ts +18 -0
- package/dist/user-presence.d.ts +32 -0
- package/package.json +18 -11
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +1386 -44
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +77 -0
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +80 -0
- package/src/application/external-continuation.ts +129 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +90 -0
- package/src/application/external-workspace-members.ts +232 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +222 -29
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +430 -73
- package/src/application/session-mcp-credential-rotation.ts +217 -0
- package/src/application/session-tenancy.ts +445 -0
- package/src/application/user-resource-grants.ts +154 -0
- package/src/billing/limits.ts +103 -24
- package/src/canonical-human-identities.ts +22 -3
- package/src/codex-model-availability.ts +136 -0
- package/src/default-session-model.ts +656 -0
- package/src/dependencies.ts +97 -5
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +270 -0
- package/src/domain/capabilities.ts +244 -151
- package/src/domain/company-profile-agent-admin.ts +56 -0
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +371 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +61 -15
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +32 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +25 -45
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/file-owner.ts +67 -0
- package/src/domain/github-action-policies.ts +169 -0
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/github-skill-source.ts +233 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +270 -169
- package/src/domain/knowledge-files.ts +46 -0
- package/src/domain/knowledge-messages.test.ts +91 -0
- package/src/domain/knowledge-messages.ts +106 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +561 -0
- package/src/domain/knowledge-search.ts +256 -0
- package/src/domain/knowledge.ts +133 -0
- package/src/domain/mcp-account-bindings.ts +171 -0
- package/src/domain/mcp-account-routes.ts +121 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/organization-membership-lifecycle.ts +10 -0
- package/src/domain/personal-connection-delegations.ts +758 -115
- package/src/domain/pr-review.ts +687 -0
- package/src/domain/resources.ts +182 -25
- package/src/domain/scheduled-task-access.ts +1052 -0
- package/src/domain/scheduled-tasks.ts +1952 -215
- package/src/domain/session-connection-accounts.ts +67 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +55 -0
- package/src/domain/session-tool-policy.ts +443 -19
- package/src/domain/sessions.ts +3417 -619
- package/src/domain/skill-imports.ts +137 -17
- package/src/domain/skill-search.ts +221 -0
- package/src/domain/skills.ts +104 -0
- package/src/domain/slack-bot.ts +128 -3
- package/src/domain/workspace-members.ts +34 -2
- package/src/editable-artifact-live/application.ts +29 -4
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +167 -13
- package/src/editable-artifact-live/ticket.ts +87 -6
- package/src/editable-artifact-live/types.ts +16 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +69 -4
- package/src/managed-auth-session-sets.ts +371 -0
- package/src/managed-session.ts +520 -2
- package/src/model-catalog.ts +873 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +179 -63
- package/src/rigs/provider-images.ts +2 -3
- package/src/sandbox/fleet.ts +471 -59
- package/src/sandbox/routing.ts +160 -33
- package/src/sandbox/runtime-settings.ts +281 -0
- package/src/session-authorization.ts +262 -78
- package/src/site-session-origin.ts +22 -0
- package/src/transcription.ts +13 -1
- package/src/turn-surface.ts +45 -0
- package/src/user-presence.ts +134 -0
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-IBOEYG6N.js +0 -34
- package/dist/chunk-IBOEYG6N.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/dist/domain/packs.d.ts +0 -55
- package/src/domain/durable-learning-slack-publication.ts +0 -202
- package/src/domain/packs.ts +0 -556
package/src/sandbox/fleet.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// apps/api/src/sandbox/fleet.ts — the FLEET service backing the fleet MCP tools
|
|
2
2
|
// (M7): list / attach / swap / run_on / provision over the heterogeneous fleet
|
|
3
|
-
// (the session's
|
|
3
|
+
// (the session's managed group box + the workspace's enrolled selfhosted machines).
|
|
4
4
|
//
|
|
5
5
|
// Each operation is workspace-scoped (the caller's grant) and, for the
|
|
6
6
|
// session-pointer mutations (attach/swap), session-scoped (the worker-signed
|
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
// single op WITHOUT touching the active pointer.
|
|
13
13
|
|
|
14
14
|
import type { Settings } from "@opengeni/config";
|
|
15
|
+
import type { SandboxBackend } from "@opengeni/contracts";
|
|
15
16
|
import {
|
|
17
|
+
authorizePersonalMachineForAttempt,
|
|
16
18
|
getEnrollment,
|
|
19
|
+
getLiveEnrollmentConnection,
|
|
17
20
|
getSandbox,
|
|
21
|
+
listEnrollments,
|
|
18
22
|
listSandboxes,
|
|
19
23
|
readActiveSandbox,
|
|
20
24
|
readLease,
|
|
25
|
+
resolvePersonalMachineConnectionForAttempt,
|
|
21
26
|
requireSession,
|
|
22
27
|
setActiveSandbox,
|
|
23
28
|
type Database,
|
|
@@ -30,14 +35,18 @@ import {
|
|
|
30
35
|
NatsOpStreamTransport,
|
|
31
36
|
selfhostedLiveness,
|
|
32
37
|
SelfhostedSession,
|
|
38
|
+
resolveConnectedMachineWorkspaceRoot,
|
|
33
39
|
swapTargetEstablishability,
|
|
34
40
|
type BackendUnresolvableCode,
|
|
35
41
|
type ControlRpc,
|
|
36
42
|
type NatsRequestConnection,
|
|
37
43
|
type SelfhostedRelayConfig,
|
|
38
44
|
type SelfhostedOpStreamDeps,
|
|
45
|
+
type SelfhostedOperationAdmission,
|
|
46
|
+
type SelfhostedOperationResourcePolicy,
|
|
39
47
|
} from "@opengeni/runtime/sandbox";
|
|
40
48
|
import { relayConfigFromSettings } from "./routing";
|
|
49
|
+
import { managedSessionGroupBackend, sessionGroupMachinePresentation } from "./runtime-settings";
|
|
41
50
|
|
|
42
51
|
export type FleetServices = {
|
|
43
52
|
db: Database;
|
|
@@ -57,11 +66,18 @@ export type FleetReadinessHold = {
|
|
|
57
66
|
export type FleetContext = {
|
|
58
67
|
accountId: string;
|
|
59
68
|
workspaceId: string;
|
|
69
|
+
subjectId?: string;
|
|
70
|
+
attemptAuthority?: {
|
|
71
|
+
turnId: string;
|
|
72
|
+
attemptId: string;
|
|
73
|
+
executionGeneration: number;
|
|
74
|
+
initiatingHumanSubjectId: string;
|
|
75
|
+
};
|
|
60
76
|
/** The calling session (the pointer the attach/swap mutates + whose group box
|
|
61
77
|
* is the default fleet member). */
|
|
62
78
|
sessionId: string;
|
|
63
|
-
/** The session's
|
|
64
|
-
sessionBackend:
|
|
79
|
+
/** The session's durable home-compute policy. */
|
|
80
|
+
sessionBackend: SandboxBackend;
|
|
65
81
|
/** The session's own group sandbox id (the lease group). */
|
|
66
82
|
sessionGroupId: string;
|
|
67
83
|
};
|
|
@@ -78,12 +94,20 @@ export type FleetContext = {
|
|
|
78
94
|
*/
|
|
79
95
|
export async function buildFleetContextForSession(
|
|
80
96
|
deps: { db: Database },
|
|
81
|
-
ctx: {
|
|
97
|
+
ctx: {
|
|
98
|
+
accountId: string;
|
|
99
|
+
workspaceId: string;
|
|
100
|
+
sessionId: string;
|
|
101
|
+
subjectId?: string;
|
|
102
|
+
attemptAuthority?: FleetContext["attemptAuthority"];
|
|
103
|
+
},
|
|
82
104
|
): Promise<FleetContext> {
|
|
83
105
|
const session = await requireSession(deps.db, ctx.workspaceId, ctx.sessionId);
|
|
84
106
|
return {
|
|
85
107
|
accountId: ctx.accountId,
|
|
86
108
|
workspaceId: ctx.workspaceId,
|
|
109
|
+
...(ctx.subjectId ? { subjectId: ctx.subjectId } : {}),
|
|
110
|
+
...(ctx.attemptAuthority ? { attemptAuthority: ctx.attemptAuthority } : {}),
|
|
87
111
|
sessionId: ctx.sessionId,
|
|
88
112
|
sessionBackend: session.sandboxBackend,
|
|
89
113
|
sessionGroupId: session.sandboxGroupId,
|
|
@@ -98,14 +122,15 @@ export type FleetOperationAvailability = "ready" | "wakeable" | "recovering" | "
|
|
|
98
122
|
* A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
|
|
99
123
|
* `sandboxes_list` response entry the dock renders). STABLE shape: the dock keys
|
|
100
124
|
* on `id`, renders `name`/`kind`/`liveness`, and marks `active`. The session's own
|
|
101
|
-
*
|
|
102
|
-
* null `enrollmentId`; an enrolled machine
|
|
125
|
+
* managed group box is a synthetic entry with `id: groupId`, its actual
|
|
126
|
+
* provisioned backend kind, and a null `enrollmentId`; an enrolled machine
|
|
127
|
+
* carries its sandbox + enrollment ids.
|
|
103
128
|
*/
|
|
104
129
|
export type FleetSandboxEntry = {
|
|
105
130
|
/** The sandbox id used as the attach/swap/run_on `target`. For the session's
|
|
106
131
|
* own group box this is the group id (a null active pointer == this box). */
|
|
107
132
|
id: string;
|
|
108
|
-
kind:
|
|
133
|
+
kind: Exclude<SandboxBackend, "none">;
|
|
109
134
|
name: string;
|
|
110
135
|
liveness: FleetLiveness;
|
|
111
136
|
/** True for the session's currently-active sandbox (the routing target). */
|
|
@@ -165,6 +190,7 @@ export type FleetSwapResult = {
|
|
|
165
190
|
reason?: string;
|
|
166
191
|
code?:
|
|
167
192
|
| BackendUnresolvableCode
|
|
193
|
+
| "invalid_working_directory"
|
|
168
194
|
| "concurrent_swap"
|
|
169
195
|
| "recovery_in_progress"
|
|
170
196
|
| "recovery_degraded"
|
|
@@ -191,13 +217,22 @@ async function probeEnrollment(
|
|
|
191
217
|
services: FleetServices,
|
|
192
218
|
workspaceId: string,
|
|
193
219
|
enrollment: EnrollmentRecord,
|
|
194
|
-
|
|
220
|
+
liveConnection: EnrollmentRecord | null = enrollment,
|
|
221
|
+
): Promise<{
|
|
222
|
+
liveness: FleetLiveness;
|
|
223
|
+
consented: boolean;
|
|
224
|
+
hasDisplay: boolean;
|
|
225
|
+
}> {
|
|
195
226
|
const { settings, bus } = services;
|
|
196
227
|
let probeResponded = false;
|
|
197
|
-
if (
|
|
228
|
+
if (liveConnection?.connectionInstanceId) {
|
|
198
229
|
const session = new SelfhostedSession({
|
|
199
230
|
workspaceId,
|
|
200
231
|
agentId: enrollment.id,
|
|
232
|
+
connectionInstanceId: liveConnection.connectionInstanceId,
|
|
233
|
+
// Ping does not touch the filesystem. Use the reported root when present;
|
|
234
|
+
// older agents can still answer liveness before they reconnect and report it.
|
|
235
|
+
workspaceRoot: enrollment.workspaceRoot ?? "/",
|
|
201
236
|
controlRpc: controlRpc(bus),
|
|
202
237
|
relay: relayConfigFromSettings(settings),
|
|
203
238
|
timeoutMs: PROBE_TIMEOUT_MS,
|
|
@@ -220,29 +255,45 @@ async function probeEnrollment(
|
|
|
220
255
|
},
|
|
221
256
|
probeResponded,
|
|
222
257
|
});
|
|
223
|
-
return {
|
|
258
|
+
return {
|
|
259
|
+
liveness: state.state,
|
|
260
|
+
consented: state.consented,
|
|
261
|
+
hasDisplay: state.hasDisplay,
|
|
262
|
+
};
|
|
224
263
|
}
|
|
225
264
|
|
|
226
265
|
/**
|
|
227
266
|
* List the fleet: the session's own group box when it has one (a synthetic
|
|
228
267
|
* entry) + the workspace's first-class selfhosted sandboxes (each probed for
|
|
229
268
|
* liveness), each with an `active` marker derived from the session's active
|
|
230
|
-
* pointer. A backend:none session
|
|
231
|
-
*
|
|
269
|
+
* pointer. A backend:none session and a machine-home session on a deployment
|
|
270
|
+
* without a managed provider have no synthetic group entry; a null pointer then
|
|
271
|
+
* means no compute is attached.
|
|
232
272
|
*/
|
|
233
273
|
export async function listFleet(
|
|
234
274
|
services: FleetServices,
|
|
235
275
|
ctx: FleetContext,
|
|
236
276
|
): Promise<FleetListResult> {
|
|
237
277
|
const { db } = services;
|
|
278
|
+
const resourceAccess = ctx.subjectId
|
|
279
|
+
? {
|
|
280
|
+
accountId: ctx.accountId,
|
|
281
|
+
workspaceId: ctx.workspaceId,
|
|
282
|
+
subjectId: ctx.subjectId,
|
|
283
|
+
}
|
|
284
|
+
: ctx.workspaceId;
|
|
238
285
|
const pointer = (await readActiveSandbox(db, ctx.workspaceId, ctx.sessionId)) ?? {
|
|
239
286
|
activeSandboxId: null,
|
|
240
287
|
activeEpoch: 0,
|
|
241
288
|
};
|
|
242
289
|
|
|
243
290
|
const entries: FleetSandboxEntry[] = [];
|
|
291
|
+
const groupBackend = managedSessionGroupBackend(
|
|
292
|
+
services.settings.sandboxBackend,
|
|
293
|
+
ctx.sessionBackend,
|
|
294
|
+
);
|
|
244
295
|
|
|
245
|
-
if (
|
|
296
|
+
if (groupBackend) {
|
|
246
297
|
// The session's own group box (the default/home sandbox; null active pointer ==
|
|
247
298
|
// this box). A session/group row is not provider existence. Online requires a
|
|
248
299
|
// warm lease, observed provider existence, and verified workspace readiness.
|
|
@@ -273,13 +324,12 @@ export async function listFleet(
|
|
|
273
324
|
? "unavailable"
|
|
274
325
|
: groupRecovering
|
|
275
326
|
? "recovering"
|
|
276
|
-
:
|
|
277
|
-
|
|
278
|
-
: "wakeable";
|
|
327
|
+
: "wakeable";
|
|
328
|
+
const presentation = sessionGroupMachinePresentation(groupBackend);
|
|
279
329
|
entries.push({
|
|
280
330
|
id: ctx.sessionGroupId,
|
|
281
|
-
kind:
|
|
282
|
-
name:
|
|
331
|
+
kind: presentation.kind,
|
|
332
|
+
name: presentation.name,
|
|
283
333
|
liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
|
|
284
334
|
active: groupActive,
|
|
285
335
|
isSessionGroup: true,
|
|
@@ -302,20 +352,65 @@ export async function listFleet(
|
|
|
302
352
|
|
|
303
353
|
// The workspace's first-class selfhosted sandboxes (enrolled machines). Probe
|
|
304
354
|
// each for liveness; a missing enrollment is offline.
|
|
305
|
-
const
|
|
355
|
+
const legacySandboxes = await listSandboxes(db, ctx.workspaceId);
|
|
356
|
+
const enrollments = await listEnrollments(
|
|
357
|
+
db,
|
|
358
|
+
ctx.subjectId
|
|
359
|
+
? {
|
|
360
|
+
accountId: ctx.accountId,
|
|
361
|
+
workspaceId: ctx.workspaceId,
|
|
362
|
+
subjectId: ctx.subjectId,
|
|
363
|
+
}
|
|
364
|
+
: ctx.workspaceId,
|
|
365
|
+
{ status: "active" },
|
|
366
|
+
);
|
|
367
|
+
const enrollmentById = new Map(enrollments.map((enrollment) => [enrollment.id, enrollment]));
|
|
368
|
+
const scopedSandboxes: SandboxRecord[] = enrollments.flatMap((enrollment) =>
|
|
369
|
+
enrollment.sandboxId
|
|
370
|
+
? [
|
|
371
|
+
{
|
|
372
|
+
id: enrollment.sandboxId,
|
|
373
|
+
accountId: enrollment.accountId,
|
|
374
|
+
workspaceId: enrollment.workspaceId,
|
|
375
|
+
kind: "selfhosted" as const,
|
|
376
|
+
name: enrollment.sandboxName ?? `${enrollment.os} machine`,
|
|
377
|
+
enrollmentId: enrollment.id,
|
|
378
|
+
createdAt: enrollment.createdAt,
|
|
379
|
+
updatedAt: enrollment.updatedAt,
|
|
380
|
+
},
|
|
381
|
+
]
|
|
382
|
+
: [],
|
|
383
|
+
);
|
|
384
|
+
const sandboxes = [
|
|
385
|
+
...legacySandboxes.filter(
|
|
386
|
+
(sandbox) => !scopedSandboxes.some((scoped) => scoped.id === sandbox.id),
|
|
387
|
+
),
|
|
388
|
+
...scopedSandboxes,
|
|
389
|
+
];
|
|
306
390
|
for (const sandbox of sandboxes) {
|
|
307
391
|
if (sandbox.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
308
392
|
continue;
|
|
309
393
|
}
|
|
310
|
-
const enrollment =
|
|
394
|
+
const enrollment =
|
|
395
|
+
enrollmentById.get(sandbox.enrollmentId) ??
|
|
396
|
+
(await getEnrollment(db, resourceAccess, sandbox.enrollmentId));
|
|
311
397
|
if (!enrollment || enrollment.status !== "active") {
|
|
312
398
|
// Revoked enrollments remain durable audit/history records, but they are
|
|
313
399
|
// intentionally absent from the normal attach/run picker.
|
|
314
400
|
continue;
|
|
315
401
|
}
|
|
402
|
+
const liveConnection = await getLiveEnrollmentConnection(
|
|
403
|
+
db,
|
|
404
|
+
resourceAccess,
|
|
405
|
+
sandbox.enrollmentId,
|
|
406
|
+
);
|
|
316
407
|
const probe = enrollment
|
|
317
|
-
? await probeEnrollment(services,
|
|
318
|
-
: {
|
|
408
|
+
? await probeEnrollment(services, sandbox.workspaceId, enrollment, liveConnection)
|
|
409
|
+
: {
|
|
410
|
+
liveness: "offline" as FleetLiveness,
|
|
411
|
+
consented: false,
|
|
412
|
+
hasDisplay: false,
|
|
413
|
+
};
|
|
319
414
|
entries.push({
|
|
320
415
|
id: sandbox.id,
|
|
321
416
|
kind: "selfhosted",
|
|
@@ -360,29 +455,29 @@ export async function listFleet(
|
|
|
360
455
|
};
|
|
361
456
|
}
|
|
362
457
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
458
|
+
type FleetResourceContext = Pick<FleetContext, "accountId" | "workspaceId" | "subjectId">;
|
|
459
|
+
|
|
460
|
+
type ResolvedNamedSandboxTarget =
|
|
461
|
+
| { ok: true; targetSandboxId: string; workspaceRoot?: string }
|
|
462
|
+
| { ok: false; reason: string; code: BackendUnresolvableCode };
|
|
463
|
+
|
|
464
|
+
/** Resolve one named sandbox, independent of any existing session pointer. */
|
|
465
|
+
async function resolveNamedSandboxTarget(
|
|
367
466
|
services: FleetServices,
|
|
368
|
-
ctx:
|
|
467
|
+
ctx: FleetResourceContext,
|
|
369
468
|
target: string,
|
|
370
|
-
): Promise<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
return { ok: true, targetSandboxId: null };
|
|
384
|
-
}
|
|
385
|
-
const sandbox = await getSandbox(services.db, ctx.workspaceId, target);
|
|
469
|
+
): Promise<ResolvedNamedSandboxTarget> {
|
|
470
|
+
const sandbox = await getSandbox(
|
|
471
|
+
services.db,
|
|
472
|
+
ctx.subjectId
|
|
473
|
+
? {
|
|
474
|
+
accountId: ctx.accountId,
|
|
475
|
+
workspaceId: ctx.workspaceId,
|
|
476
|
+
subjectId: ctx.subjectId,
|
|
477
|
+
}
|
|
478
|
+
: ctx.workspaceId,
|
|
479
|
+
target,
|
|
480
|
+
);
|
|
386
481
|
if (!sandbox) {
|
|
387
482
|
return {
|
|
388
483
|
ok: false,
|
|
@@ -402,8 +497,13 @@ async function resolveTarget(
|
|
|
402
497
|
isSessionGroup: false,
|
|
403
498
|
});
|
|
404
499
|
if (!establishable.ok) {
|
|
405
|
-
return {
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
reason: establishable.reason,
|
|
503
|
+
code: establishable.code,
|
|
504
|
+
};
|
|
406
505
|
}
|
|
506
|
+
let targetWorkspaceRoot: string | undefined;
|
|
407
507
|
if (sandbox.kind === "selfhosted") {
|
|
408
508
|
if (!sandbox.enrollmentId) {
|
|
409
509
|
return {
|
|
@@ -412,7 +512,17 @@ async function resolveTarget(
|
|
|
412
512
|
code: "offline_enrollment",
|
|
413
513
|
};
|
|
414
514
|
}
|
|
415
|
-
const enrollment = await
|
|
515
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
516
|
+
services.db,
|
|
517
|
+
ctx.subjectId
|
|
518
|
+
? {
|
|
519
|
+
accountId: ctx.accountId,
|
|
520
|
+
workspaceId: ctx.workspaceId,
|
|
521
|
+
subjectId: ctx.subjectId,
|
|
522
|
+
}
|
|
523
|
+
: sandbox.workspaceId,
|
|
524
|
+
sandbox.enrollmentId,
|
|
525
|
+
);
|
|
416
526
|
if (!enrollment) {
|
|
417
527
|
return {
|
|
418
528
|
ok: false,
|
|
@@ -420,7 +530,15 @@ async function resolveTarget(
|
|
|
420
530
|
code: "offline_enrollment",
|
|
421
531
|
};
|
|
422
532
|
}
|
|
423
|
-
|
|
533
|
+
if (!enrollment.workspaceRoot) {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
reason: `sandbox ${target} has not reported an absolute workspace root; reconnect it with a current agent`,
|
|
537
|
+
code: "workspace_root_unavailable",
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
targetWorkspaceRoot = enrollment.workspaceRoot;
|
|
541
|
+
const probe = await probeEnrollment(services, sandbox.workspaceId, enrollment, enrollment);
|
|
424
542
|
if (probe.liveness !== "online") {
|
|
425
543
|
return {
|
|
426
544
|
ok: false,
|
|
@@ -429,7 +547,86 @@ async function resolveTarget(
|
|
|
429
547
|
};
|
|
430
548
|
}
|
|
431
549
|
}
|
|
432
|
-
return {
|
|
550
|
+
return {
|
|
551
|
+
ok: true,
|
|
552
|
+
targetSandboxId: sandbox.id,
|
|
553
|
+
...(targetWorkspaceRoot ? { workspaceRoot: targetWorkspaceRoot } : {}),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export type CreateTimeSandboxTargetPreflight =
|
|
558
|
+
| { ok: true; targetSandboxId: string; workingDir: string | null }
|
|
559
|
+
| {
|
|
560
|
+
ok: false;
|
|
561
|
+
reason: string;
|
|
562
|
+
code: BackendUnresolvableCode | "invalid_working_directory";
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Validate a named create-time machine target before any session row exists.
|
|
567
|
+
* The caller still commits the pointer with `setActiveSandbox` inside the
|
|
568
|
+
* session-create transaction, which rechecks durable ownership/enrollment
|
|
569
|
+
* authority and closes the remove/revoke race without holding database locks
|
|
570
|
+
* across the liveness probe.
|
|
571
|
+
*/
|
|
572
|
+
export async function preflightCreateTimeSandboxTarget(
|
|
573
|
+
services: FleetServices,
|
|
574
|
+
ctx: FleetResourceContext,
|
|
575
|
+
target: string,
|
|
576
|
+
workingDir: string | null,
|
|
577
|
+
): Promise<CreateTimeSandboxTargetPreflight> {
|
|
578
|
+
const resolved = await resolveNamedSandboxTarget(services, ctx, target);
|
|
579
|
+
if (!resolved.ok) return resolved;
|
|
580
|
+
if (!resolved.workspaceRoot) {
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
targetSandboxId: resolved.targetSandboxId,
|
|
584
|
+
workingDir,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
return {
|
|
589
|
+
ok: true,
|
|
590
|
+
targetSandboxId: resolved.targetSandboxId,
|
|
591
|
+
workingDir:
|
|
592
|
+
resolveConnectedMachineWorkspaceRoot(resolved.workspaceRoot, workingDir) ??
|
|
593
|
+
resolved.workspaceRoot,
|
|
594
|
+
};
|
|
595
|
+
} catch (error) {
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
599
|
+
code: "invalid_working_directory",
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Resolve a swap target id → the value `setActiveSandbox` writes. The session's
|
|
605
|
+
* own group id maps to NULL (the default pointer); a first-class sandbox id is
|
|
606
|
+
* validated (workspace ownership + liveness) and written verbatim. */
|
|
607
|
+
async function resolveTarget(
|
|
608
|
+
services: FleetServices,
|
|
609
|
+
ctx: FleetContext,
|
|
610
|
+
target: string,
|
|
611
|
+
): Promise<
|
|
612
|
+
| { ok: true; targetSandboxId: string | null; workspaceRoot?: string }
|
|
613
|
+
| { ok: false; reason: string; code: BackendUnresolvableCode }
|
|
614
|
+
> {
|
|
615
|
+
// The session's own group box → the default pointer (null).
|
|
616
|
+
if (target === ctx.sessionGroupId || target === "session" || target === "default") {
|
|
617
|
+
if (!managedSessionGroupBackend(services.settings.sandboxBackend, ctx.sessionBackend)) {
|
|
618
|
+
return {
|
|
619
|
+
ok: false,
|
|
620
|
+
reason:
|
|
621
|
+
ctx.sessionBackend === "none"
|
|
622
|
+
? "this session has no home sandbox; attach a Connected Machine"
|
|
623
|
+
: "this deployment has no managed session sandbox; select an enrolled machine",
|
|
624
|
+
code: "unsupported_backend_context",
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return { ok: true, targetSandboxId: null };
|
|
628
|
+
}
|
|
629
|
+
return await resolveNamedSandboxTarget(services, ctx, target);
|
|
433
630
|
}
|
|
434
631
|
|
|
435
632
|
/**
|
|
@@ -482,6 +679,7 @@ export async function swapActiveSandbox(
|
|
|
482
679
|
const pointer = (await readActiveSandbox(services.db, ctx.workspaceId, ctx.sessionId)) ?? {
|
|
483
680
|
activeSandboxId: null,
|
|
484
681
|
activeEpoch: 0,
|
|
682
|
+
workingDir: null,
|
|
485
683
|
};
|
|
486
684
|
return {
|
|
487
685
|
swapped: false,
|
|
@@ -503,7 +701,27 @@ export async function swapActiveSandbox(
|
|
|
503
701
|
const pointer = (await readActiveSandbox(services.db, ctx.workspaceId, ctx.sessionId)) ?? {
|
|
504
702
|
activeSandboxId: null,
|
|
505
703
|
activeEpoch: 0,
|
|
704
|
+
workingDir: null,
|
|
506
705
|
};
|
|
706
|
+
let effectiveWorkingDir = workingDir;
|
|
707
|
+
if (resolved.workspaceRoot) {
|
|
708
|
+
try {
|
|
709
|
+
effectiveWorkingDir = resolveConnectedMachineWorkspaceRoot(
|
|
710
|
+
resolved.workspaceRoot,
|
|
711
|
+
workingDir === undefined && pointer.activeSandboxId === resolved.targetSandboxId
|
|
712
|
+
? pointer.workingDir
|
|
713
|
+
: workingDir,
|
|
714
|
+
);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
return {
|
|
717
|
+
swapped: false,
|
|
718
|
+
activeSandboxId: pointer.activeSandboxId,
|
|
719
|
+
activeEpoch: pointer.activeEpoch,
|
|
720
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
721
|
+
code: "invalid_working_directory",
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
}
|
|
507
725
|
// Even a same-target attach advances active_epoch. It is a repair/fence
|
|
508
726
|
// request, not a no-op acknowledgment: any cached stale route is invalidated
|
|
509
727
|
// only after target readiness has been proved above.
|
|
@@ -513,7 +731,13 @@ export async function swapActiveSandbox(
|
|
|
513
731
|
sessionId: ctx.sessionId,
|
|
514
732
|
targetSandboxId: resolved.targetSandboxId,
|
|
515
733
|
expectedEpoch: pointer.activeEpoch,
|
|
516
|
-
...(
|
|
734
|
+
...(ctx.subjectId ? { subjectId: ctx.subjectId } : {}),
|
|
735
|
+
...(ctx.attemptAuthority ? { personalMachineAttempt: ctx.attemptAuthority } : {}),
|
|
736
|
+
...(resolved.workspaceRoot !== undefined
|
|
737
|
+
? { workingDir: effectiveWorkingDir ?? resolved.workspaceRoot }
|
|
738
|
+
: workingDir !== undefined
|
|
739
|
+
? { workingDir }
|
|
740
|
+
: {}),
|
|
517
741
|
});
|
|
518
742
|
if (result.swapped && result.pointer) {
|
|
519
743
|
return {
|
|
@@ -569,6 +793,9 @@ export type RunOnResult = {
|
|
|
569
793
|
export type RunOnSelfhostedMachine = {
|
|
570
794
|
workspaceId: string;
|
|
571
795
|
agentId: string;
|
|
796
|
+
/** Exact live daemon pinned for this operation. */
|
|
797
|
+
connectionInstanceId: string;
|
|
798
|
+
workspaceRoot: string;
|
|
572
799
|
controlRpc: ControlRpc;
|
|
573
800
|
relay: SelfhostedRelayConfig;
|
|
574
801
|
/** Short request/reply deadline for read/write and other control operations. */
|
|
@@ -577,8 +804,50 @@ export type RunOnSelfhostedMachine = {
|
|
|
577
804
|
execTimeoutMs: number;
|
|
578
805
|
/** Streaming transport required when execTimeoutMs is 0 (unbounded). */
|
|
579
806
|
opStream?: SelfhostedOpStreamDeps;
|
|
807
|
+
operationResourcePolicy?: SelfhostedOperationResourcePolicy;
|
|
808
|
+
operationResourcePolicySupported?: boolean;
|
|
809
|
+
operationCpuQuotaSupported?: boolean;
|
|
810
|
+
/** Last-boundary authority/connection resolver invoked before every physical
|
|
811
|
+
* provider operation. */
|
|
812
|
+
resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
|
|
813
|
+
/**
|
|
814
|
+
* Exact-attempt values for this one child process. Never persisted on the
|
|
815
|
+
* enrollment or machine. The caller may supply these only after authenticating
|
|
816
|
+
* the active attempt that owns the one-off operation.
|
|
817
|
+
*/
|
|
818
|
+
transientExecEnvironment?: Readonly<Record<string, string>>;
|
|
580
819
|
};
|
|
581
820
|
|
|
821
|
+
export type RunOnOptions = {
|
|
822
|
+
transientExecEnvironment?: Readonly<Record<string, string>>;
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
function runOnOperationAdmission(
|
|
826
|
+
services: FleetServices,
|
|
827
|
+
enrollment: EnrollmentRecord | null,
|
|
828
|
+
): SelfhostedOperationAdmission | null {
|
|
829
|
+
if (!enrollment?.connectionInstanceId || !enrollment.workspaceRoot) return null;
|
|
830
|
+
const opStream =
|
|
831
|
+
services.settings.agentOpStreamEnabled === true &&
|
|
832
|
+
enrollment.opStream === true &&
|
|
833
|
+
services.bus?.getOpStreamConnection
|
|
834
|
+
? {
|
|
835
|
+
transport: new NatsOpStreamTransport(
|
|
836
|
+
async () => services.bus?.getOpStreamConnection?.() ?? null,
|
|
837
|
+
),
|
|
838
|
+
}
|
|
839
|
+
: undefined;
|
|
840
|
+
return {
|
|
841
|
+
workspaceId: enrollment.workspaceId,
|
|
842
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
843
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
844
|
+
...(opStream ? { opStream } : {}),
|
|
845
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
846
|
+
operationResourcePolicySupported: enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
847
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
582
851
|
/**
|
|
583
852
|
* Execute the one-off machine operation once the workspace/enrollment lookup has
|
|
584
853
|
* succeeded. Kept separate from {@link runOnSandbox} so the command/deadline
|
|
@@ -593,10 +862,29 @@ export async function executeRunOnSelfhostedMachine(
|
|
|
593
862
|
const session = new SelfhostedSession({
|
|
594
863
|
workspaceId: machine.workspaceId,
|
|
595
864
|
agentId: machine.agentId,
|
|
865
|
+
connectionInstanceId: machine.connectionInstanceId,
|
|
866
|
+
workspaceRoot: machine.workspaceRoot,
|
|
596
867
|
controlRpc: machine.controlRpc,
|
|
597
868
|
relay: machine.relay,
|
|
598
869
|
timeoutMs: machine.controlTimeoutMs,
|
|
599
870
|
execTimeoutMs: machine.execTimeoutMs,
|
|
871
|
+
...(machine.operationResourcePolicy !== undefined
|
|
872
|
+
? { operationResourcePolicy: machine.operationResourcePolicy }
|
|
873
|
+
: {}),
|
|
874
|
+
...(machine.operationResourcePolicySupported !== undefined
|
|
875
|
+
? {
|
|
876
|
+
operationResourcePolicySupported: machine.operationResourcePolicySupported,
|
|
877
|
+
}
|
|
878
|
+
: {}),
|
|
879
|
+
...(machine.operationCpuQuotaSupported !== undefined
|
|
880
|
+
? { operationCpuQuotaSupported: machine.operationCpuQuotaSupported }
|
|
881
|
+
: {}),
|
|
882
|
+
...(machine.resolveOperationAdmission !== undefined
|
|
883
|
+
? { resolveOperationAdmission: machine.resolveOperationAdmission }
|
|
884
|
+
: {}),
|
|
885
|
+
...(machine.transientExecEnvironment !== undefined
|
|
886
|
+
? { transientExecEnvironment: () => machine.transientExecEnvironment! }
|
|
887
|
+
: {}),
|
|
600
888
|
...(machine.opStream !== undefined ? { opStream: machine.opStream } : {}),
|
|
601
889
|
});
|
|
602
890
|
|
|
@@ -622,7 +910,9 @@ export async function executeRunOnSelfhostedMachine(
|
|
|
622
910
|
timedOut,
|
|
623
911
|
deadlineMs,
|
|
624
912
|
...(timedOut
|
|
625
|
-
? {
|
|
913
|
+
? {
|
|
914
|
+
reason: `command exceeded the ${deadlineMs} ms execution deadline`,
|
|
915
|
+
}
|
|
626
916
|
: !hasTerminalExit
|
|
627
917
|
? { reason: "machine returned no terminal exit code" }
|
|
628
918
|
: {}),
|
|
@@ -630,9 +920,17 @@ export async function executeRunOnSelfhostedMachine(
|
|
|
630
920
|
}
|
|
631
921
|
if (op.kind === "read") {
|
|
632
922
|
const bytes = await session.readFile({ path: op.path });
|
|
633
|
-
return {
|
|
923
|
+
return {
|
|
924
|
+
target,
|
|
925
|
+
kind: "read",
|
|
926
|
+
ok: true,
|
|
927
|
+
content: new TextDecoder().decode(bytes),
|
|
928
|
+
};
|
|
634
929
|
}
|
|
635
|
-
const bytesWritten = await session.writeFile({
|
|
930
|
+
const bytesWritten = await session.writeFile({
|
|
931
|
+
path: op.path,
|
|
932
|
+
content: op.content,
|
|
933
|
+
});
|
|
636
934
|
return { target, kind: "write", ok: true, bytesWritten };
|
|
637
935
|
} catch (error) {
|
|
638
936
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -667,8 +965,19 @@ export async function runOnSandbox(
|
|
|
667
965
|
ctx: FleetContext,
|
|
668
966
|
target: string,
|
|
669
967
|
op: RunOnOp,
|
|
968
|
+
options: RunOnOptions = {},
|
|
670
969
|
): Promise<RunOnResult> {
|
|
671
|
-
const sandbox = await getSandbox(
|
|
970
|
+
const sandbox = await getSandbox(
|
|
971
|
+
services.db,
|
|
972
|
+
ctx.subjectId
|
|
973
|
+
? {
|
|
974
|
+
accountId: ctx.accountId,
|
|
975
|
+
workspaceId: ctx.workspaceId,
|
|
976
|
+
subjectId: ctx.subjectId,
|
|
977
|
+
}
|
|
978
|
+
: ctx.workspaceId,
|
|
979
|
+
target,
|
|
980
|
+
);
|
|
672
981
|
if (!sandbox) {
|
|
673
982
|
return {
|
|
674
983
|
target,
|
|
@@ -686,8 +995,59 @@ export async function runOnSandbox(
|
|
|
686
995
|
reason: `run_on routes one-off ops to enrolled selfhosted machines; ${sandbox.kind} targets are reached via the active sandbox (swap to it first)`,
|
|
687
996
|
};
|
|
688
997
|
}
|
|
689
|
-
|
|
690
|
-
|
|
998
|
+
if (sandbox.scope === "user") {
|
|
999
|
+
if (!ctx.subjectId || !ctx.attemptAuthority) {
|
|
1000
|
+
return {
|
|
1001
|
+
target,
|
|
1002
|
+
targetName: sandbox.name,
|
|
1003
|
+
kind: op.kind,
|
|
1004
|
+
ok: false,
|
|
1005
|
+
reason: "personal Connected Machine use requires an exact admitted agent attempt",
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
try {
|
|
1009
|
+
const authorized = await authorizePersonalMachineForAttempt(services.db, {
|
|
1010
|
+
accountId: ctx.accountId,
|
|
1011
|
+
workspaceId: ctx.workspaceId,
|
|
1012
|
+
subjectId: ctx.subjectId,
|
|
1013
|
+
sessionId: ctx.sessionId,
|
|
1014
|
+
turnId: ctx.attemptAuthority.turnId,
|
|
1015
|
+
attemptId: ctx.attemptAuthority.attemptId,
|
|
1016
|
+
executionGeneration: ctx.attemptAuthority.executionGeneration,
|
|
1017
|
+
enrollmentId: sandbox.enrollmentId,
|
|
1018
|
+
requireActiveSandbox: false,
|
|
1019
|
+
});
|
|
1020
|
+
if (!authorized) {
|
|
1021
|
+
return {
|
|
1022
|
+
target,
|
|
1023
|
+
targetName: sandbox.name,
|
|
1024
|
+
kind: op.kind,
|
|
1025
|
+
ok: false,
|
|
1026
|
+
reason: "personal Connected Machine authority was not admitted for this attempt",
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
} catch {
|
|
1030
|
+
return {
|
|
1031
|
+
target,
|
|
1032
|
+
targetName: sandbox.name,
|
|
1033
|
+
kind: op.kind,
|
|
1034
|
+
ok: false,
|
|
1035
|
+
reason: "personal Connected Machine authority is not live for this attempt",
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
1040
|
+
services.db,
|
|
1041
|
+
ctx.subjectId
|
|
1042
|
+
? {
|
|
1043
|
+
accountId: ctx.accountId,
|
|
1044
|
+
workspaceId: ctx.workspaceId,
|
|
1045
|
+
subjectId: ctx.subjectId,
|
|
1046
|
+
}
|
|
1047
|
+
: sandbox.workspaceId,
|
|
1048
|
+
sandbox.enrollmentId,
|
|
1049
|
+
);
|
|
1050
|
+
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
691
1051
|
return {
|
|
692
1052
|
target,
|
|
693
1053
|
targetName: sandbox.name,
|
|
@@ -696,15 +1056,65 @@ export async function runOnSandbox(
|
|
|
696
1056
|
reason: `sandbox ${target} is not enrolled/active`,
|
|
697
1057
|
};
|
|
698
1058
|
}
|
|
1059
|
+
if (!enrollment.workspaceRoot) {
|
|
1060
|
+
return {
|
|
1061
|
+
target,
|
|
1062
|
+
targetName: sandbox.name,
|
|
1063
|
+
kind: op.kind,
|
|
1064
|
+
ok: false,
|
|
1065
|
+
reason: `sandbox ${target} has not reported an absolute workspace root; reconnect it with a current agent`,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const resolveOperationAdmission = async (): Promise<SelfhostedOperationAdmission | null> => {
|
|
1069
|
+
try {
|
|
1070
|
+
const current =
|
|
1071
|
+
sandbox.scope === "user" && ctx.subjectId && ctx.attemptAuthority
|
|
1072
|
+
? await resolvePersonalMachineConnectionForAttempt(services.db, {
|
|
1073
|
+
accountId: ctx.accountId,
|
|
1074
|
+
workspaceId: ctx.workspaceId,
|
|
1075
|
+
subjectId: ctx.subjectId,
|
|
1076
|
+
sessionId: ctx.sessionId,
|
|
1077
|
+
turnId: ctx.attemptAuthority.turnId,
|
|
1078
|
+
attemptId: ctx.attemptAuthority.attemptId,
|
|
1079
|
+
executionGeneration: ctx.attemptAuthority.executionGeneration,
|
|
1080
|
+
enrollmentId: sandbox.enrollmentId!,
|
|
1081
|
+
requireActiveSandbox: false,
|
|
1082
|
+
})
|
|
1083
|
+
: await getLiveEnrollmentConnection(
|
|
1084
|
+
services.db,
|
|
1085
|
+
ctx.subjectId
|
|
1086
|
+
? {
|
|
1087
|
+
accountId: ctx.accountId,
|
|
1088
|
+
workspaceId: ctx.workspaceId,
|
|
1089
|
+
subjectId: ctx.subjectId,
|
|
1090
|
+
}
|
|
1091
|
+
: sandbox.workspaceId,
|
|
1092
|
+
sandbox.enrollmentId!,
|
|
1093
|
+
);
|
|
1094
|
+
return runOnOperationAdmission(services, current);
|
|
1095
|
+
} catch {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
699
1099
|
|
|
700
1100
|
const result = await executeRunOnSelfhostedMachine(
|
|
701
1101
|
{
|
|
702
|
-
workspaceId:
|
|
1102
|
+
workspaceId: sandbox.workspaceId,
|
|
703
1103
|
agentId: sandbox.enrollmentId,
|
|
1104
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
1105
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
704
1106
|
controlRpc: controlRpc(services.bus),
|
|
705
1107
|
relay: relayConfigFromSettings(services.settings),
|
|
706
1108
|
controlTimeoutMs: services.settings.sandboxSelfhostedControlTimeoutMs,
|
|
707
1109
|
execTimeoutMs: services.settings.sandboxSelfhostedExecTimeoutMs,
|
|
1110
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
1111
|
+
operationResourcePolicySupported:
|
|
1112
|
+
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
1113
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
1114
|
+
resolveOperationAdmission,
|
|
1115
|
+
...(options.transientExecEnvironment !== undefined
|
|
1116
|
+
? { transientExecEnvironment: options.transientExecEnvironment }
|
|
1117
|
+
: {}),
|
|
708
1118
|
...(services.settings.agentOpStreamEnabled === true &&
|
|
709
1119
|
enrollment.opStream === true &&
|
|
710
1120
|
services.bus?.getOpStreamConnection
|
|
@@ -749,17 +1159,19 @@ export async function provisionSandbox(
|
|
|
749
1159
|
input: { kind: "selfhosted" | "modal"; name?: string },
|
|
750
1160
|
): Promise<ProvisionResult> {
|
|
751
1161
|
if (input.kind === "selfhosted") {
|
|
752
|
-
const base = (services.settings.publicBaseUrl ?? "https://
|
|
1162
|
+
const base = (services.settings.publicBaseUrl ?? "https://app.opengeni.ai").replace(/\/+$/, "");
|
|
1163
|
+
const unixEnvironment = `OPENGENI_API_URL=${base} OPENGENI_WORKSPACE_ID=${ctx.workspaceId}`;
|
|
1164
|
+
const windowsEnvironment = `$env:OPENGENI_API_URL='${base}'; $env:OPENGENI_WORKSPACE_ID='${ctx.workspaceId}';`;
|
|
753
1165
|
return {
|
|
754
1166
|
kind: "selfhosted",
|
|
755
1167
|
instructions:
|
|
756
|
-
"Share these
|
|
1168
|
+
"Share one of these deployment-specific commands with a human operator. It installs the Opengeni agent and starts `opengeni-agent connect` for this exact deployment and workspace; do not run a second bare `connect`. Complete the device-flow at the verification URL (the loud whole-machine + screen-control consent), and the machine then appears here as an attachable selfhosted sandbox. Existing connections to other Opengeni workspaces or deployments are preserved.",
|
|
757
1169
|
// Install from THIS control plane's origin (not a hardcoded public CDN): the
|
|
758
1170
|
// served install script is rewritten to pull the per-SHA agent baked into
|
|
759
1171
|
// this exact deployment (see apps/api/src/routes/install.ts), so a deployed
|
|
760
1172
|
// env is self-contained and a private/air-gapped one works with no public DNS.
|
|
761
|
-
installCommandUnix: `curl -fsSL ${base}/install.sh | sh`,
|
|
762
|
-
installCommandWindows:
|
|
1173
|
+
installCommandUnix: `curl -fsSL ${base}/install.sh | ${unixEnvironment} sh`,
|
|
1174
|
+
installCommandWindows: `${windowsEnvironment} irm ${base}/install.ps1 | iex`,
|
|
763
1175
|
verificationUri: `${base}/device`,
|
|
764
1176
|
note: "Whole-machine access requires explicit human consent in the device-flow web page; the agent cannot self-consent.",
|
|
765
1177
|
};
|