@opengeni/api-router 2.2.0-canary.0 → 2.3.2-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/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +5 -2
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/organization-user-setup.d.ts +57 -0
- package/dist/{chunk-3TP54PPX.js → chunk-IBV7Z6F4.js} +3545 -2009
- package/dist/chunk-IBV7Z6F4.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-app-home.d.ts +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/routes/automations.d.ts +13 -0
- package/dist/routes/insights.d.ts +2 -1
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/package.json +18 -18
- package/src/app.ts +56 -5
- package/src/auth/managed-auth.ts +29 -34
- package/src/auth/managed-email.ts +174 -0
- package/src/auth/organization-user-setup.ts +217 -0
- package/src/http/auth.ts +15 -0
- package/src/http/sse.ts +62 -13
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/mcp/company-brain-governed-writes.ts +4 -4
- package/src/mcp/company-profile-agent-admin.ts +11 -18
- package/src/mcp/remember.ts +4 -4
- package/src/mcp/server.ts +50 -4
- package/src/mcp/session-view.ts +8 -2
- package/src/routes/automations.ts +3 -3
- package/src/routes/documents.ts +2 -0
- package/src/routes/insights.ts +61 -19
- package/src/routes/managed-onboarding.ts +317 -0
- package/src/routes/organization-memberships.ts +212 -155
- package/src/routes/pr-review-github.ts +844 -0
- package/src/routes/pr-review.ts +20 -0
- package/src/routes/rigs.ts +37 -4
- package/src/routes/sessions.ts +101 -0
- package/src/sandbox/channel-a.ts +34 -7
- package/src/sandbox/viewer.ts +47 -11
- package/dist/chunk-3TP54PPX.js.map +0 -1
package/src/routes/pr-review.ts
CHANGED
|
@@ -44,9 +44,11 @@ import {
|
|
|
44
44
|
PrReviewProviderRepositoryError,
|
|
45
45
|
verifyPrReviewProviderRepository,
|
|
46
46
|
} from "../integrations/pr-review-provider";
|
|
47
|
+
import { registerPrReviewGitHubRoutes } from "./pr-review-github";
|
|
47
48
|
|
|
48
49
|
export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
49
50
|
const { db, settings } = deps;
|
|
51
|
+
registerPrReviewGitHubRoutes(app, deps);
|
|
50
52
|
|
|
51
53
|
app.get("/v1/workspaces/:workspaceId/pr-review/registrations", async (c) => {
|
|
52
54
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -144,6 +146,24 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
144
146
|
throw new HTTPException(404, {
|
|
145
147
|
message: "PR Review app registration not found",
|
|
146
148
|
});
|
|
149
|
+
if (existing.credentialKind === "managed_github_app") {
|
|
150
|
+
if (
|
|
151
|
+
payload.accessToken !== undefined ||
|
|
152
|
+
payload.privateKey !== undefined ||
|
|
153
|
+
payload.webhookSecret !== undefined ||
|
|
154
|
+
payload.webhookUsername !== undefined ||
|
|
155
|
+
payload.accessTokenExpiresAt !== undefined
|
|
156
|
+
) {
|
|
157
|
+
throw new HTTPException(422, {
|
|
158
|
+
message: "OpenGeni Lens credentials are managed by the deployment",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (payload.status === "active") {
|
|
162
|
+
throw new HTTPException(409, {
|
|
163
|
+
message: "Reconnect OpenGeni Lens to reactivate and resync its repositories",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
147
167
|
if (payload.accessToken && existing.credentialKind !== "provider_token") {
|
|
148
168
|
throw new HTTPException(422, {
|
|
149
169
|
message: "GitHub App registrations accept private keys, not access tokens",
|
package/src/routes/rigs.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { Hono } from "hono";
|
|
13
13
|
import type { Context } from "hono";
|
|
14
14
|
import { HTTPException } from "hono/http-exception";
|
|
15
|
+
import type { ZodType } from "zod";
|
|
15
16
|
import {
|
|
16
17
|
requireAccessGrant,
|
|
17
18
|
requireAccessGrantAuthorization,
|
|
@@ -32,6 +33,38 @@ import {
|
|
|
32
33
|
updateRigForApi,
|
|
33
34
|
} from "@opengeni/core";
|
|
34
35
|
import { boundedLimit } from "../http/common";
|
|
36
|
+
import { ApiHttpError } from "../http/api-error";
|
|
37
|
+
|
|
38
|
+
async function parseRigRequest<T>(c: Context, schema: ZodType<T>, label: string): Promise<T> {
|
|
39
|
+
const body: unknown = await c.req.json().catch(() => null);
|
|
40
|
+
const bodyRecord = body && typeof body === "object" ? (body as Record<string, unknown>) : null;
|
|
41
|
+
const payloadRecord =
|
|
42
|
+
bodyRecord?.payload && typeof bodyRecord.payload === "object"
|
|
43
|
+
? (bodyRecord.payload as Record<string, unknown>)
|
|
44
|
+
: null;
|
|
45
|
+
const imageOverrideUnsupported = Boolean(
|
|
46
|
+
(bodyRecord && Object.hasOwn(bodyRecord, "image")) ||
|
|
47
|
+
(payloadRecord && Object.hasOwn(payloadRecord, "image")),
|
|
48
|
+
);
|
|
49
|
+
const parsed = schema.safeParse(body);
|
|
50
|
+
if (parsed.success && !imageOverrideUnsupported) return parsed.data;
|
|
51
|
+
throw new ApiHttpError(422, {
|
|
52
|
+
code: "validation_failed",
|
|
53
|
+
message: imageOverrideUnsupported
|
|
54
|
+
? "Rig base-image overrides are not supported; Rigs use the deployment-managed platform sandbox."
|
|
55
|
+
: `Invalid ${label}.`,
|
|
56
|
+
retryable: false,
|
|
57
|
+
details: {
|
|
58
|
+
...(imageOverrideUnsupported ? { code: "RIG_IMAGE_OVERRIDE_UNSUPPORTED" } : {}),
|
|
59
|
+
fields: parsed.success
|
|
60
|
+
? []
|
|
61
|
+
: parsed.error.issues.slice(0, 16).map((issue) => ({
|
|
62
|
+
path: issue.path,
|
|
63
|
+
code: issue.code,
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
35
68
|
|
|
36
69
|
export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
37
70
|
const { db, workflowClient } = deps;
|
|
@@ -150,7 +183,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
150
183
|
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
151
184
|
const grant = authorization.grant;
|
|
152
185
|
requirePermission(grant, "rigs:manage");
|
|
153
|
-
const payload =
|
|
186
|
+
const payload = await parseRigRequest(c, CreateRigRequest, "Rig create request");
|
|
154
187
|
const allowOrganization =
|
|
155
188
|
payload.scope === "organization" &&
|
|
156
189
|
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
@@ -189,7 +222,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
189
222
|
message: "missing permission: account:admin",
|
|
190
223
|
});
|
|
191
224
|
}
|
|
192
|
-
const payload =
|
|
225
|
+
const payload = await parseRigRequest(c, UpdateRigRequest, "Rig update request");
|
|
193
226
|
return c.json(await updateRigForApi({ db }, grant, rig, payload, { allowOrganization }));
|
|
194
227
|
});
|
|
195
228
|
|
|
@@ -221,7 +254,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
221
254
|
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/versions", async (c) => {
|
|
222
255
|
const workspaceId = c.req.param("workspaceId");
|
|
223
256
|
const { grant, rig } = await requireRigMutation(c, workspaceId, "rigs:manage");
|
|
224
|
-
const payload =
|
|
257
|
+
const payload = await parseRigRequest(c, RigDefinitionEditPayload, "Rig version request");
|
|
225
258
|
const version = await createRigVersionForApi({ db }, grant, rig, payload);
|
|
226
259
|
const started = await tryStartInitialVersionVerification(rig.workspaceId, version.id);
|
|
227
260
|
if (!started) c.header("OpenGeni-Rig-Verification", "deferred");
|
|
@@ -256,7 +289,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
256
289
|
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/changes", async (c) => {
|
|
257
290
|
const workspaceId = c.req.param("workspaceId");
|
|
258
291
|
const { grant, rig } = await requireRigMutation(c, workspaceId, "rigs:use");
|
|
259
|
-
const request =
|
|
292
|
+
const request = await parseRigRequest(c, ProposeRigChangeRequest, "Rig change request");
|
|
260
293
|
const change = await proposeRigChangeForApi({ db }, grant, rig, request);
|
|
261
294
|
const verification = await startChangeVerification(rig.workspaceId, change.id);
|
|
262
295
|
if (!verification.started) c.header("OpenGeni-Rig-Verification", "deferred");
|
package/src/routes/sessions.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
UpdateSessionGoalRequest,
|
|
62
62
|
UpdateSessionMcpApprovalPolicyRequest,
|
|
63
63
|
UpdateSessionRequest,
|
|
64
|
+
UpdateSessionVariableSetsRequest,
|
|
64
65
|
UpdateSessionVisibilityRequest,
|
|
65
66
|
UpdateSessionToolPolicyRequest,
|
|
66
67
|
ViewerHeartbeatRequest,
|
|
@@ -79,6 +80,7 @@ import {
|
|
|
79
80
|
type TerminalPtyExitedPayload,
|
|
80
81
|
type TerminalPtyOutputDeltaPayload,
|
|
81
82
|
type TerminalPtyStartedPayload,
|
|
83
|
+
type VariableSet,
|
|
82
84
|
} from "@opengeni/contracts";
|
|
83
85
|
import { streamTokenDegraded } from "@opengeni/config";
|
|
84
86
|
import {
|
|
@@ -121,6 +123,7 @@ import {
|
|
|
121
123
|
setSessionCodexPinInTransaction,
|
|
122
124
|
withSessionCodexCapacityMutation,
|
|
123
125
|
setSessionChannel,
|
|
126
|
+
updateSessionVariableSets,
|
|
124
127
|
ChannelNotFoundError,
|
|
125
128
|
setSessionAttention,
|
|
126
129
|
setSessionArchive,
|
|
@@ -150,6 +153,7 @@ import {
|
|
|
150
153
|
SessionRealtimeConflictError,
|
|
151
154
|
SessionToolPolicyVersionConflictError,
|
|
152
155
|
SessionContextBusyError,
|
|
156
|
+
SessionVariableSetSelectionUnavailableError,
|
|
153
157
|
workspaceControlRequestLockTimeoutMs,
|
|
154
158
|
SessionTenancyAccessError,
|
|
155
159
|
SessionTenancyConflictError,
|
|
@@ -264,6 +268,7 @@ import {
|
|
|
264
268
|
workspaceSessionToolPolicyDefaultServerIds,
|
|
265
269
|
workspaceSessionToolPolicyServerIds,
|
|
266
270
|
relayConfigFromSettings,
|
|
271
|
+
validateVariableSetAttachment,
|
|
267
272
|
} from "@opengeni/core";
|
|
268
273
|
import { assertSessionExists, boundedLimit } from "../http/common";
|
|
269
274
|
import { sseSessionStream } from "../http/sse";
|
|
@@ -1769,6 +1774,91 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1769
1774
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
1770
1775
|
});
|
|
1771
1776
|
|
|
1777
|
+
// Replace the complete ordered Variable Set selection. The DB mutation
|
|
1778
|
+
// serializes with turn claim, rejects live/shared sandbox use, and requests a
|
|
1779
|
+
// cold rotation before the new environment can be materialized.
|
|
1780
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/variable-sets", async (c) => {
|
|
1781
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1782
|
+
const sessionId = c.req.param("sessionId");
|
|
1783
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1784
|
+
try {
|
|
1785
|
+
await requireSessionAuthorization(deps, grant, {
|
|
1786
|
+
sessionId,
|
|
1787
|
+
operation: "session.variable_sets.write",
|
|
1788
|
+
surface: "http",
|
|
1789
|
+
});
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
throw sessionAuthorizationHttpError(error);
|
|
1792
|
+
}
|
|
1793
|
+
// Detach still requires attach authority: it changes which protected
|
|
1794
|
+
// resources the session may materialize. Non-empty selections additionally
|
|
1795
|
+
// require use authority through validateVariableSetAttachment.
|
|
1796
|
+
requirePermission(grant, "variable-sets:attach");
|
|
1797
|
+
const parsed = UpdateSessionVariableSetsRequest.safeParse(await c.req.json().catch(() => null));
|
|
1798
|
+
if (!parsed.success) {
|
|
1799
|
+
throw new ApiHttpError(422, {
|
|
1800
|
+
code: "validation_failed",
|
|
1801
|
+
message: "Invalid session Variable Set request.",
|
|
1802
|
+
retryable: false,
|
|
1803
|
+
details: { fields: zodErrorFields(parsed.error) },
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
const variableSets: VariableSet[] = [];
|
|
1807
|
+
for (const variableSetId of parsed.data.variableSetIds) {
|
|
1808
|
+
variableSets.push(
|
|
1809
|
+
await validateVariableSetAttachment({ settings, db }, grant, workspaceId, variableSetId),
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
const result = await updateSessionVariableSets(db, {
|
|
1813
|
+
accountId: grant.accountId,
|
|
1814
|
+
workspaceId,
|
|
1815
|
+
sessionId,
|
|
1816
|
+
subjectId: grant.subjectId,
|
|
1817
|
+
variableSets: variableSets.map((variableSet) => ({
|
|
1818
|
+
id: variableSet.id,
|
|
1819
|
+
name: variableSet.name,
|
|
1820
|
+
scope: variableSet.scope,
|
|
1821
|
+
})),
|
|
1822
|
+
});
|
|
1823
|
+
if (result.status === "not_found") {
|
|
1824
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1825
|
+
}
|
|
1826
|
+
if (result.status === "invalid_variable_sets") {
|
|
1827
|
+
throw new ApiHttpError(422, {
|
|
1828
|
+
code: "validation_failed",
|
|
1829
|
+
message: "One or more selected Variable Sets are no longer available.",
|
|
1830
|
+
retryable: false,
|
|
1831
|
+
outcomeUnknown: false,
|
|
1832
|
+
details: { variableSetIds: result.variableSetIds },
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
if (result.status === "blocked") {
|
|
1836
|
+
const messages = {
|
|
1837
|
+
turn_in_flight:
|
|
1838
|
+
"Variable Sets can be changed only when the session has no accepted, queued, claimed, or pending work.",
|
|
1839
|
+
shared_sandbox_group:
|
|
1840
|
+
"Variable Sets cannot be changed while this session shares a sandbox; fork it into a separate session first.",
|
|
1841
|
+
live_sandbox_holders:
|
|
1842
|
+
"Close active terminal, desktop, and sandbox operations before changing Variable Sets.",
|
|
1843
|
+
} as const;
|
|
1844
|
+
throw new HTTPException(409, { message: messages[result.reason] });
|
|
1845
|
+
}
|
|
1846
|
+
if (result.status === "updated") {
|
|
1847
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, [result.event]);
|
|
1848
|
+
}
|
|
1849
|
+
const session = await getSessionForSubject(
|
|
1850
|
+
db,
|
|
1851
|
+
workspaceId,
|
|
1852
|
+
sessionId,
|
|
1853
|
+
grant.subjectId,
|
|
1854
|
+
relatedSessionAccessFor(c),
|
|
1855
|
+
);
|
|
1856
|
+
if (!session) {
|
|
1857
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1858
|
+
}
|
|
1859
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1772
1862
|
// Manual rename. A user-set title is permanent: the db write is
|
|
1773
1863
|
// unconditional (source='user'), so it always pins the session over later
|
|
1774
1864
|
// agent writes. Returns the refreshed session, mirroring GET detail.
|
|
@@ -4125,6 +4215,7 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
4125
4215
|
if (suffix === "/visibility" && verb === "PUT") return "session.visibility.write";
|
|
4126
4216
|
if (suffix === "/forks" && verb === "POST") return "session.fork.create";
|
|
4127
4217
|
if (suffix === "/channel" && verb === "PUT") return "session.channel.write";
|
|
4218
|
+
if (suffix === "/variable-sets" && verb === "PUT") return "session.variable_sets.write";
|
|
4128
4219
|
if (suffix === "/tool-policy" && verb === "PUT") return "session.tool_policy.write";
|
|
4129
4220
|
if (/^\/mcp-servers\/[^/]+\/approval-policy$/.test(suffix) && verb === "PATCH") {
|
|
4130
4221
|
return "session.mcp.approval_policy.write";
|
|
@@ -4584,6 +4675,16 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
|
|
|
4584
4675
|
// insert's FK rejection surfaces as the same 422 an unknown id gets.
|
|
4585
4676
|
return c.json({ code: "SESSION_CREATE_REJECTED", message: error.message }, 422);
|
|
4586
4677
|
}
|
|
4678
|
+
if (error instanceof SessionVariableSetSelectionUnavailableError) {
|
|
4679
|
+
return c.json(
|
|
4680
|
+
{
|
|
4681
|
+
code: "SESSION_CREATE_REJECTED",
|
|
4682
|
+
message: error.message,
|
|
4683
|
+
details: { variableSetIds: error.variableSetIds },
|
|
4684
|
+
},
|
|
4685
|
+
422,
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4587
4688
|
if (error instanceof SessionSpawnDeniedError) {
|
|
4588
4689
|
return c.json(
|
|
4589
4690
|
sessionSpawnDenialEnvelope(error),
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
getSandboxSessionEnvelope,
|
|
33
33
|
getLiveEnrollmentConnection,
|
|
34
34
|
getSandbox,
|
|
35
|
+
getScheduledScopedRigVersionMetadata,
|
|
35
36
|
touchLeaseHolder,
|
|
36
37
|
loadWorkspaceEnvironmentForRun,
|
|
37
38
|
markWarmLeaseInstanceLost,
|
|
@@ -539,19 +540,45 @@ async function withChannelAOperation<T>(
|
|
|
539
540
|
|
|
540
541
|
// The STABLE run-environment used by both a cloud home and a machine home.
|
|
541
542
|
// It also carries the per-session Codemode pointer selected below.
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
543
|
+
const workspaceEnvironmentValues: Record<string, string> = {};
|
|
544
|
+
const rigVersion =
|
|
545
|
+
session.rigId && session.rigVersionId
|
|
546
|
+
? await getScheduledScopedRigVersionMetadata(
|
|
547
|
+
db,
|
|
548
|
+
{
|
|
549
|
+
accountId,
|
|
550
|
+
workspaceId,
|
|
551
|
+
subjectId: ctx.subjectId ?? "session-attach",
|
|
552
|
+
},
|
|
553
|
+
session.rigId,
|
|
554
|
+
session.rigVersionId,
|
|
555
|
+
)
|
|
556
|
+
: null;
|
|
557
|
+
for (const variableSetId of rigVersion?.version.defaultVariableSetIds ?? []) {
|
|
558
|
+
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(db, settings, {
|
|
559
|
+
accountId,
|
|
560
|
+
workspaceId,
|
|
561
|
+
variableSetId,
|
|
562
|
+
authority: { kind: "session_attach", sessionId: session.id, subjectId: ctx.subjectId },
|
|
563
|
+
});
|
|
564
|
+
Object.assign(workspaceEnvironmentValues, workspaceEnvironment?.values ?? {});
|
|
565
|
+
}
|
|
566
|
+
for (const variableSetId of session.variableSetIds) {
|
|
567
|
+
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(db, settings, {
|
|
568
|
+
accountId,
|
|
569
|
+
workspaceId,
|
|
570
|
+
variableSetId,
|
|
571
|
+
authority: { kind: "session_attach", sessionId: session.id, subjectId: ctx.subjectId },
|
|
572
|
+
});
|
|
573
|
+
Object.assign(workspaceEnvironmentValues, workspaceEnvironment?.values ?? {});
|
|
574
|
+
}
|
|
548
575
|
const settingsForSession =
|
|
549
576
|
session.sandboxBackend !== settings.sandboxBackend
|
|
550
577
|
? { ...settings, sandboxBackend: session.sandboxBackend }
|
|
551
578
|
: settings;
|
|
552
579
|
const environment = stableSandboxEnvironmentForRun(
|
|
553
580
|
settingsForSession,
|
|
554
|
-
|
|
581
|
+
workspaceEnvironmentValues,
|
|
555
582
|
{ workspaceId },
|
|
556
583
|
);
|
|
557
584
|
if (hasGitCredentialRepositorySelection(session.resources)) {
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
acquireLease,
|
|
36
36
|
getLiveEnrollmentConnection,
|
|
37
37
|
getSandbox,
|
|
38
|
+
getScheduledScopedRigVersionMetadata,
|
|
38
39
|
getSandboxSessionEnvelope,
|
|
39
40
|
heartbeatLeaseHolder,
|
|
40
41
|
loadWorkspaceEnvironmentForRun,
|
|
@@ -149,16 +150,51 @@ export async function sessionAttachEnvironment(
|
|
|
149
150
|
* the materialization audit fact. Null records the legacy service sentinel. */
|
|
150
151
|
attachSubjectId: string | null,
|
|
151
152
|
): Promise<Record<string, string>> {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
const workspaceEnvironmentValues: Record<string, string> = {};
|
|
154
|
+
const rigVersion =
|
|
155
|
+
session.rigId && session.rigVersionId
|
|
156
|
+
? await getScheduledScopedRigVersionMetadata(
|
|
157
|
+
services.db,
|
|
158
|
+
{
|
|
159
|
+
accountId: session.accountId,
|
|
160
|
+
workspaceId,
|
|
161
|
+
subjectId: attachSubjectId ?? "session-attach",
|
|
162
|
+
},
|
|
163
|
+
session.rigId,
|
|
164
|
+
session.rigVersionId,
|
|
165
|
+
)
|
|
166
|
+
: null;
|
|
167
|
+
for (const variableSetId of rigVersion?.version.defaultVariableSetIds ?? []) {
|
|
168
|
+
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
|
|
169
|
+
services.db,
|
|
170
|
+
services.settings,
|
|
171
|
+
{
|
|
172
|
+
accountId: session.accountId,
|
|
173
|
+
workspaceId,
|
|
174
|
+
variableSetId,
|
|
175
|
+
authority: { kind: "session_attach", sessionId: session.id, subjectId: attachSubjectId },
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
Object.assign(workspaceEnvironmentValues, workspaceEnvironment?.values ?? {});
|
|
179
|
+
}
|
|
180
|
+
// Older persisted/test projections can omit the plural field. Preserve the
|
|
181
|
+
// legacy final alias as the single explicit selection until every caller is
|
|
182
|
+
// guaranteed to have crossed the plural contract boundary.
|
|
183
|
+
const explicitVariableSetIds =
|
|
184
|
+
session.variableSetIds ?? (session.variableSetId ? [session.variableSetId] : []);
|
|
185
|
+
for (const variableSetId of explicitVariableSetIds) {
|
|
186
|
+
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
|
|
187
|
+
services.db,
|
|
188
|
+
services.settings,
|
|
189
|
+
{
|
|
190
|
+
accountId: session.accountId,
|
|
191
|
+
workspaceId,
|
|
192
|
+
variableSetId,
|
|
193
|
+
authority: { kind: "session_attach", sessionId: session.id, subjectId: attachSubjectId },
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
Object.assign(workspaceEnvironmentValues, workspaceEnvironment?.values ?? {});
|
|
197
|
+
}
|
|
162
198
|
// Build the env with the SESSION's backend, not the deployment default: the
|
|
163
199
|
// stable base is backend-aware (HOME = the descriptor workspaceRoot, and the
|
|
164
200
|
// git token-file/askpass pointers derive from HOME), the box is established
|
|
@@ -173,7 +209,7 @@ export async function sessionAttachEnvironment(
|
|
|
173
209
|
: services.settings;
|
|
174
210
|
const environment = stableSandboxEnvironmentForRun(
|
|
175
211
|
settingsForSession,
|
|
176
|
-
|
|
212
|
+
workspaceEnvironmentValues,
|
|
177
213
|
{ workspaceId },
|
|
178
214
|
);
|
|
179
215
|
if (hasGitCredentialRepositorySelection(session.resources)) {
|