@opengeni/api-router 0.5.7 → 0.9.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.d.ts +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-QOYQBYHM.js} +4302 -926
- package/dist/chunk-QOYQBYHM.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/app.ts +83 -18
- package/src/codex-redemption-security.ts +96 -0
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +828 -146
- package/src/mcp/session-view.ts +721 -201
- package/src/mcp/toolspace.ts +482 -132
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +883 -28
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/files.ts +153 -0
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +789 -81
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +66 -13
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +8 -2
- package/dist/chunk-HBEJMWD3.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Workbench v2 — capture READ serving
|
|
1
|
+
// Workbench v2 — capture READ serving.
|
|
2
2
|
//
|
|
3
3
|
// The two GET capture routes in `sessions.ts` are thin: grant-first (files:read),
|
|
4
4
|
// load the row (DB, RLS-scoped), then delegate the response SHAPING to the two
|
|
@@ -46,13 +46,24 @@ function signedUrl(signed: { url: string; expiresAt: Date }): { url: string; exp
|
|
|
46
46
|
// manifest key, the blob is gone (GC'd), or the bytes fail to parse/validate — a
|
|
47
47
|
// malformed capture is treated as "no capture available" (the list route degrades
|
|
48
48
|
// to {available:false}, the file route to 404). Capture reads must NEVER be worse
|
|
49
|
-
// than the status-quo live/wake fallback
|
|
49
|
+
// than the status-quo live/wake fallback, so a poison row can
|
|
50
50
|
// never 500 the workbench; it degrades and logs.
|
|
51
51
|
async function loadManifest(
|
|
52
52
|
row: WorkspaceCaptureRow,
|
|
53
53
|
storage: CaptureStoragePort,
|
|
54
|
-
): Promise<{
|
|
54
|
+
): Promise<{
|
|
55
|
+
manifest: WorkspaceCaptureManifest;
|
|
56
|
+
byteLength: number;
|
|
57
|
+
stats: WorkspaceCaptureStats;
|
|
58
|
+
} | null> {
|
|
55
59
|
if (!row.manifestKey) return null;
|
|
60
|
+
const stats = WorkspaceCaptureStats.safeParse(row.stats);
|
|
61
|
+
if (!stats.success) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`workspace capture read — row stats failed schema validation (session=${row.sessionId} rev=${row.revision})`,
|
|
64
|
+
);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
56
67
|
const blob = await storage.getObjectBytes(row.manifestKey);
|
|
57
68
|
if (!blob) return null;
|
|
58
69
|
let json: unknown;
|
|
@@ -71,7 +82,39 @@ async function loadManifest(
|
|
|
71
82
|
);
|
|
72
83
|
return null;
|
|
73
84
|
}
|
|
74
|
-
|
|
85
|
+
const manifest = parsed.data;
|
|
86
|
+
const servedStats = stats.data;
|
|
87
|
+
const statsMatch =
|
|
88
|
+
manifest.stats.repoCount === servedStats.repoCount &&
|
|
89
|
+
manifest.stats.fileCount === servedStats.fileCount &&
|
|
90
|
+
manifest.stats.additions === servedStats.additions &&
|
|
91
|
+
manifest.stats.deletions === servedStats.deletions &&
|
|
92
|
+
manifest.stats.totalBytes === servedStats.totalBytes &&
|
|
93
|
+
manifest.stats.tooLargeCount === servedStats.tooLargeCount &&
|
|
94
|
+
manifest.stats.binaryCount === servedStats.binaryCount &&
|
|
95
|
+
manifest.stats.treeEntryCount === servedStats.treeEntryCount &&
|
|
96
|
+
manifest.stats.treeTruncated === servedStats.treeTruncated &&
|
|
97
|
+
manifest.stats.durationMs === servedStats.durationMs &&
|
|
98
|
+
(manifest.stats.fingerprint ?? null) === (servedStats.fingerprint ?? null);
|
|
99
|
+
if (
|
|
100
|
+
manifest.revision !== row.revision ||
|
|
101
|
+
manifest.capturedAt !== row.capturedAt ||
|
|
102
|
+
manifest.turnId !== row.turnId ||
|
|
103
|
+
manifest.leaseEpoch !== row.leaseEpoch ||
|
|
104
|
+
!statsMatch ||
|
|
105
|
+
manifest.repos.length !== manifest.stats.repoCount ||
|
|
106
|
+
manifest.files.length !== manifest.stats.fileCount ||
|
|
107
|
+
manifest.treeTruncated !== manifest.stats.treeTruncated
|
|
108
|
+
) {
|
|
109
|
+
// A valid blob under the wrong row/key is still poison: row metadata drives
|
|
110
|
+
// cache identity and revision pinning in the client. Never combine two
|
|
111
|
+
// different captures into one apparently authoritative response.
|
|
112
|
+
console.warn(
|
|
113
|
+
`workspace capture read — manifest identity did not match row (session=${row.sessionId} rev=${row.revision})`,
|
|
114
|
+
);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return { manifest, byteLength: blob.bytes.byteLength, stats: servedStats };
|
|
75
118
|
}
|
|
76
119
|
|
|
77
120
|
/**
|
|
@@ -105,15 +148,12 @@ export async function serveWorkspaceCapture(
|
|
|
105
148
|
});
|
|
106
149
|
}
|
|
107
150
|
if (row.state !== "available" || !row.manifestKey) return { available: false };
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
115
|
-
return { available: false };
|
|
116
|
-
}
|
|
151
|
+
// Validate every manifest before serving it, including the rare >2MB signed
|
|
152
|
+
// path. Previously that branch signed arbitrary bytes merely because they
|
|
153
|
+
// exceeded the inline cap, allowing a poison/mis-keyed blob to bypass both the
|
|
154
|
+
// schema and row-identity checks.
|
|
155
|
+
const loaded = await loadManifest(row, storage);
|
|
156
|
+
if (!loaded) return { available: false };
|
|
117
157
|
const meta = {
|
|
118
158
|
available: true as const,
|
|
119
159
|
revision: row.revision,
|
|
@@ -121,34 +161,12 @@ export async function serveWorkspaceCapture(
|
|
|
121
161
|
turnId: row.turnId,
|
|
122
162
|
leaseEpoch: row.leaseEpoch,
|
|
123
163
|
sizeBytes: row.sizeBytes ?? 0,
|
|
124
|
-
stats: stats
|
|
164
|
+
stats: loaded.stats,
|
|
125
165
|
};
|
|
126
|
-
|
|
127
|
-
if (!blob) {
|
|
128
|
-
// Manifest raced GC between the row read and the blob fetch — degrade to the
|
|
129
|
-
// cold-fallback state rather than 500.
|
|
130
|
-
return { available: false };
|
|
131
|
-
}
|
|
132
|
-
if (blob.bytes.byteLength <= CAPTURE_INLINE_MANIFEST_MAX_BYTES) {
|
|
133
|
-
let json: unknown;
|
|
134
|
-
try {
|
|
135
|
-
json = JSON.parse(new TextDecoder().decode(blob.bytes));
|
|
136
|
-
} catch {
|
|
137
|
-
console.warn(
|
|
138
|
-
`workspace capture read — manifest blob is not valid JSON (session=${row.sessionId} rev=${row.revision})`,
|
|
139
|
-
);
|
|
140
|
-
return { available: false };
|
|
141
|
-
}
|
|
142
|
-
const manifest = WorkspaceCaptureManifest.safeParse(json);
|
|
143
|
-
if (!manifest.success) {
|
|
144
|
-
console.warn(
|
|
145
|
-
`workspace capture read — manifest failed schema validation (session=${row.sessionId} rev=${row.revision})`,
|
|
146
|
-
);
|
|
147
|
-
return { available: false };
|
|
148
|
-
}
|
|
166
|
+
if (loaded.byteLength <= CAPTURE_INLINE_MANIFEST_MAX_BYTES) {
|
|
149
167
|
return GetWorkspaceCaptureResponse.parse({
|
|
150
168
|
...meta,
|
|
151
|
-
manifest: manifest
|
|
169
|
+
manifest: loaded.manifest,
|
|
152
170
|
manifestUrl: null,
|
|
153
171
|
});
|
|
154
172
|
}
|
package/src/routes/workspaces.ts
CHANGED
|
@@ -7,9 +7,12 @@ import {
|
|
|
7
7
|
UpdateWorkspaceModelPolicyRequest,
|
|
8
8
|
UpdateWorkspaceRequest,
|
|
9
9
|
UpdateWorkspaceSettingsRequest,
|
|
10
|
+
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
11
|
+
WorkspaceModelCatalogResponse,
|
|
10
12
|
WorkspaceInferenceControlRequest,
|
|
11
13
|
Workspace,
|
|
12
14
|
WorkspaceMember,
|
|
15
|
+
workspaceControlUtf8Bytes,
|
|
13
16
|
type AccessContext,
|
|
14
17
|
type Permission,
|
|
15
18
|
} from "@opengeni/contracts";
|
|
@@ -33,7 +36,9 @@ import {
|
|
|
33
36
|
updateWorkspace,
|
|
34
37
|
updateWorkspaceSettings,
|
|
35
38
|
upsertWorkspaceModelPolicy,
|
|
39
|
+
workspaceCodexSubscriptionActive,
|
|
36
40
|
} from "@opengeni/db";
|
|
41
|
+
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
37
42
|
import type { Hono } from "hono";
|
|
38
43
|
import { HTTPException } from "hono/http-exception";
|
|
39
44
|
import { hasPermission, requireAccessContext, requireAccessGrant } from "@opengeni/core";
|
|
@@ -47,6 +52,18 @@ import {
|
|
|
47
52
|
} from "@opengeni/core";
|
|
48
53
|
import { boundedLimit } from "../http/common";
|
|
49
54
|
import { sseWorkspaceControlStream } from "../http/sse";
|
|
55
|
+
import { buildWorkspaceModelCatalog } from "../model-catalog";
|
|
56
|
+
import { canonicalizeConfiguredModelId, type Settings } from "@opengeni/config";
|
|
57
|
+
|
|
58
|
+
export function canonicalWorkspacePolicyModelIds(
|
|
59
|
+
settings: Settings,
|
|
60
|
+
modelIds: string[] | null | undefined,
|
|
61
|
+
): string[] | null {
|
|
62
|
+
if (modelIds === null || modelIds === undefined) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return [...new Set(modelIds.map((modelId) => canonicalizeConfiguredModelId(settings, modelId)))];
|
|
66
|
+
}
|
|
50
67
|
|
|
51
68
|
export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
52
69
|
app.get("/v1/access/me", async (c) => {
|
|
@@ -140,7 +157,27 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
140
157
|
|
|
141
158
|
// Per-workspace model/provider availability policy (the HARD blocker over
|
|
142
159
|
// which providers/models may serve a turn at all). Absent row reads as
|
|
143
|
-
// unrestricted {null, null}.
|
|
160
|
+
// unrestricted {null, null}. No Azure AD credential resolver is wired here,
|
|
161
|
+
// so bearer/federated definitions intentionally fail closed as not ready.
|
|
162
|
+
app.get("/v1/workspaces/:workspaceId/model-catalog", async (c) => {
|
|
163
|
+
const workspaceId = c.req.param("workspaceId");
|
|
164
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
165
|
+
const [policy, codexSubscriptionActive] = await Promise.all([
|
|
166
|
+
getWorkspaceModelPolicy(deps.db, workspaceId),
|
|
167
|
+
workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
|
|
168
|
+
]);
|
|
169
|
+
c.header("cache-control", "private, no-store");
|
|
170
|
+
return c.json(
|
|
171
|
+
WorkspaceModelCatalogResponse.parse(
|
|
172
|
+
buildWorkspaceModelCatalog({
|
|
173
|
+
settings: deps.settings,
|
|
174
|
+
policy,
|
|
175
|
+
codexSubscriptionActive,
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
144
181
|
app.get("/v1/workspaces/:workspaceId/model-policy", async (c) => {
|
|
145
182
|
const workspaceId = c.req.param("workspaceId");
|
|
146
183
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
@@ -163,7 +200,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
163
200
|
accountId: grant.accountId,
|
|
164
201
|
workspaceId,
|
|
165
202
|
allowedProviders: payload.allowedProviders ?? null,
|
|
166
|
-
allowedModels: payload.allowedModels
|
|
203
|
+
allowedModels: canonicalWorkspacePolicyModelIds(deps.settings, payload.allowedModels),
|
|
167
204
|
});
|
|
168
205
|
return c.json(policy);
|
|
169
206
|
});
|
|
@@ -171,12 +208,18 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
171
208
|
app.post("/v1/workspaces/:workspaceId/inference-control", async (c) => {
|
|
172
209
|
const workspaceId = c.req.param("workspaceId");
|
|
173
210
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
174
|
-
|
|
211
|
+
if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
|
|
212
|
+
throw new HTTPException(400, { message: "workspace-control actor is too large" });
|
|
213
|
+
}
|
|
214
|
+
const parsed = WorkspaceInferenceControlRequest.safeParse(await c.req.json().catch(() => null));
|
|
215
|
+
if (!parsed.success) {
|
|
216
|
+
throw new HTTPException(400, { message: "invalid workspace inference-control request" });
|
|
217
|
+
}
|
|
175
218
|
return c.json(
|
|
176
219
|
await controlHumanWorkspace(
|
|
177
220
|
{ db: deps.db, bus: deps.bus, workflowClient: deps.workflowClient },
|
|
178
221
|
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
179
|
-
|
|
222
|
+
parsed.data,
|
|
180
223
|
),
|
|
181
224
|
);
|
|
182
225
|
});
|
|
@@ -185,21 +228,31 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
185
228
|
const workspaceId = c.req.param("workspaceId");
|
|
186
229
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
187
230
|
const after = Math.max(0, Number.parseInt(c.req.query("after") ?? "0", 10) || 0);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
231
|
+
const limit = boundedLimit(c.req.query("limit"));
|
|
232
|
+
const fetched = await listWorkspaceControlEvents(deps.db, workspaceId, after, limit + 1);
|
|
233
|
+
const countHasMore = fetched.length > limit;
|
|
234
|
+
const page = boundWorkspaceControlHttpPage(fetched.slice(0, limit));
|
|
235
|
+
const truncated = countHasMore || page.truncated;
|
|
236
|
+
c.header("X-OpenGeni-Page-Bytes", String(page.bytes));
|
|
237
|
+
c.header("X-OpenGeni-Page-Truncated", String(truncated));
|
|
238
|
+
if (page.nextSequence !== null) {
|
|
239
|
+
c.header("X-OpenGeni-Next-After", String(page.nextSequence));
|
|
240
|
+
}
|
|
241
|
+
return c.json(page.events);
|
|
196
242
|
});
|
|
197
243
|
|
|
198
244
|
app.get("/v1/workspaces/:workspaceId/control-events/stream", async (c) => {
|
|
199
245
|
const workspaceId = c.req.param("workspaceId");
|
|
200
246
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
201
247
|
const after = Math.max(0, Number.parseInt(c.req.query("after") ?? "0", 10) || 0);
|
|
202
|
-
return await sseWorkspaceControlStream(
|
|
248
|
+
return await sseWorkspaceControlStream(
|
|
249
|
+
deps.db,
|
|
250
|
+
deps.bus,
|
|
251
|
+
workspaceId,
|
|
252
|
+
after,
|
|
253
|
+
c.req.raw.signal,
|
|
254
|
+
{ observability: deps.observability },
|
|
255
|
+
);
|
|
203
256
|
});
|
|
204
257
|
|
|
205
258
|
app.put("/v1/workspaces/:workspaceId/default-rig", async (c) => {
|
package/src/sandbox/access.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// apps/api/src/sandbox/access.ts — the API-tier sandbox access seam.
|
|
2
2
|
//
|
|
3
3
|
// This is the foundation of the API-DIRECT control plane
|
|
4
|
-
// (docs/
|
|
4
|
+
// (docs/connected-machines.md): the apps/api process constructs its OWN
|
|
5
5
|
// sandbox client and resumes boxes by id IN-PROCESS, so non-turn ops (viewer
|
|
6
6
|
// attach, FS/git reads, tunnel URL mint) never touch Temporal or a worker.
|
|
7
7
|
//
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// apps/api/src/sandbox/auth-callout.ts — the NATS AUTH-CALLOUT responder (the
|
|
2
|
-
// bring-your-own-compute M-AUTH tenancy boundary;
|
|
2
|
+
// bring-your-own-compute M-AUTH tenancy boundary; NATS Accounts per
|
|
3
3
|
// workspace + §17 the isolation smoke + §19 the NATS-Accounts-misconfig leak risk).
|
|
4
4
|
//
|
|
5
5
|
// THE BOUNDARY THIS CLOSES: an external agent connects to NATS presenting its
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -50,7 +50,11 @@ import {
|
|
|
50
50
|
ChannelAConflictError,
|
|
51
51
|
ChannelANotFoundError,
|
|
52
52
|
ChannelAUnsupportedError,
|
|
53
|
+
ChannelAUnavailableError,
|
|
53
54
|
ChannelAValidationError,
|
|
55
|
+
toolspaceTokenFileFromEnvironment,
|
|
56
|
+
withToolspaceTokenSession,
|
|
57
|
+
withRunCredentialsSession,
|
|
54
58
|
type ChannelASession,
|
|
55
59
|
type EstablishedSandboxSession,
|
|
56
60
|
} from "@opengeni/runtime/sandbox";
|
|
@@ -293,9 +297,16 @@ export async function withChannelA<T>(
|
|
|
293
297
|
established,
|
|
294
298
|
).session
|
|
295
299
|
: established.session;
|
|
300
|
+
const credentialSession = withRunCredentialsSession(routedSession as object, session.id);
|
|
301
|
+
const scopedSession = environment.OPENGENI_TOOLSPACE_TOKEN_FILE
|
|
302
|
+
? withToolspaceTokenSession(
|
|
303
|
+
credentialSession,
|
|
304
|
+
toolspaceTokenFileFromEnvironment(environment, session.id),
|
|
305
|
+
)
|
|
306
|
+
: credentialSession;
|
|
296
307
|
|
|
297
308
|
const service = new SandboxChannelAService({
|
|
298
|
-
session:
|
|
309
|
+
session: scopedSession as ChannelASession,
|
|
299
310
|
leaseEpoch: leaseSnapshot.leaseEpoch,
|
|
300
311
|
emit,
|
|
301
312
|
});
|
|
@@ -313,6 +324,8 @@ export async function withChannelA<T>(
|
|
|
313
324
|
* already-HTTPException unchanged. */
|
|
314
325
|
export function mapChannelAError(error: unknown): unknown {
|
|
315
326
|
if (error instanceof HTTPException) return error;
|
|
327
|
+
if (error instanceof ChannelAUnavailableError)
|
|
328
|
+
return new HTTPException(503, { message: error.message });
|
|
316
329
|
if (error instanceof ChannelAValidationError)
|
|
317
330
|
return new HTTPException(400, { message: error.message });
|
|
318
331
|
if (error instanceof ChannelANotFoundError)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// apps/api/src/sandbox/enrollment.ts — the API-DIRECT enrollment device-flow seam
|
|
2
|
-
// (M5 of the bring-your-own-compute mega-PR;
|
|
2
|
+
// (M5 of the bring-your-own-compute mega-PR; enrollment + §18 LOUD
|
|
3
3
|
// consent). This is the service layer the routes (routes/enrollments.ts) call — it
|
|
4
4
|
// mirrors the channel-a.ts / viewer.ts split (a thin route over a focused service).
|
|
5
5
|
//
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// placeholder for the per-workspace NATS Account creds [infra-deferred]);
|
|
21
21
|
// denied/expired/disabled → the typed state.
|
|
22
22
|
//
|
|
23
|
-
// SECURITY
|
|
23
|
+
// SECURITY: device_code/user_code are CSPRNG-unguessable + short-TTL +
|
|
24
24
|
// single-use; approve is strictly workspace-gated (the route asserts the grant); the
|
|
25
25
|
// signing secret value is NEVER logged. Rate-limiting of start/poll is enforced at
|
|
26
26
|
// the route. The consent record (who/when/what) lives on the request row.
|
|
@@ -78,7 +78,7 @@ export const DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
|
78
78
|
// enrollment status on every (re)connect (auth-callout.ts) — a revoked machine is
|
|
79
79
|
// denied regardless of bearer life — exactly as the long-lived relay token relies on.
|
|
80
80
|
export const ENROLLMENT_BEARER_TTL_SECONDS = 30 * 24 * 3600;
|
|
81
|
-
// The relay PRODUCER token (the `ogr_` token; M8b
|
|
81
|
+
// The relay PRODUCER token (the `ogr_` token; M8b) is ENROLLMENT-scoped,
|
|
82
82
|
// NOT per-stream: the agent presents it on every channel registration for the life
|
|
83
83
|
// of its run, and the producer side has no per-viewer epoch fence (that is the
|
|
84
84
|
// VIEWER's `ogs_` token's job). So it is long-lived — 30 days — re-minted on every
|
|
@@ -492,7 +492,7 @@ async function buildEnrollmentCredentials(
|
|
|
492
492
|
// Hand the agent the canonical `/stream` dial base, NOT the raw configured URL.
|
|
493
493
|
// The agent's relay producer appends only its routing query and assumes the base
|
|
494
494
|
// already carries the relay's `/stream` route; a path-less base 400s the dial and
|
|
495
|
-
// makes the terminal/desktop streams unreachable
|
|
495
|
+
// makes the terminal/desktop streams unreachable.
|
|
496
496
|
relayUrl: relayDialBaseFromSettings(settings),
|
|
497
497
|
relayToken,
|
|
498
498
|
// M-AUTH closes the placeholder: there is NO per-machine NATS Account creds
|
package/src/sandbox/machines.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// apps/api/src/sandbox/machines.ts — the M10 Machines-DASHBOARD service (
|
|
1
|
+
// apps/api/src/sandbox/machines.ts — the M10 Machines-DASHBOARD service (design
|
|
2
2
|
// §10.7). Builds the `MachinesResponse` the dashboard renders: the workspace's
|
|
3
3
|
// enrolled selfhosted machines, each enriched with
|
|
4
4
|
// * STATE — the M3 liveness (online/reconnecting/offline) overlaid with the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// apps/api/src/sandbox/metrics-ingestion.ts — the M10 metrics INGESTION consumer
|
|
2
|
-
//
|
|
2
|
+
// + the connect-Hello DISPLAY-REFRESH consumer. The
|
|
3
3
|
// enrolled agent piggybacks a `MetricsSample` on its ~5s heartbeat (an
|
|
4
4
|
// `AgentEvent` published one-way on `agent.<ws>.<id>.events`) and publishes a
|
|
5
5
|
// `Hello` (its live self-description) on `agent.<ws>.<id>.hello` on every connect
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -141,7 +141,7 @@ export async function sessionAttachEnvironment(
|
|
|
141
141
|
// the same env from runSettings.sandboxBackend = the session's backend. An
|
|
142
142
|
// attach env keyed off the deployment default would cold-create e.g. an e2b
|
|
143
143
|
// session's box with /workspace-rooted values while its turn declares
|
|
144
|
-
// /home/user
|
|
144
|
+
// /home/user ones — the same guard-killed first turn all over again.
|
|
145
145
|
const settingsForSession =
|
|
146
146
|
session.sandboxBackend !== services.settings.sandboxBackend
|
|
147
147
|
? { ...services.settings, sandboxBackend: session.sandboxBackend }
|
|
@@ -648,7 +648,13 @@ export async function mintDesktopStream(
|
|
|
648
648
|
// Idempotent display stack (flock-guarded; a no-op when already up). A box
|
|
649
649
|
// that genuinely can't run the stack degrades to transport:null, not a throw.
|
|
650
650
|
try {
|
|
651
|
-
await ensureDisplayStack(established.session
|
|
651
|
+
await ensureDisplayStack(established.session, {
|
|
652
|
+
telemetryContext: {
|
|
653
|
+
callerKind: "viewer",
|
|
654
|
+
...(lease.instanceId ? { sandboxId: lease.instanceId } : {}),
|
|
655
|
+
leaseEpoch: lease.leaseEpoch,
|
|
656
|
+
},
|
|
657
|
+
});
|
|
652
658
|
} catch (error) {
|
|
653
659
|
if (error instanceof DisplayStackUnsupportedError) {
|
|
654
660
|
return null;
|