@playcraft/cli 0.1.85-beta → 0.1.86
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/commands/tools-request.js +24 -0
- package/dist/commands/tools.js +2 -0
- package/dist/utils/agent-api-client.js +57 -18
- package/dist/utils/tool-operation-journal.js +3 -3
- package/dist/workspace-runtime/codex/model-broker.js +82 -269
- package/package.json +3 -4
- package/templates/preview-runtime/v1/manifest.json +1 -1
- package/templates/preview-runtime/v1/runtime.mjs +1 -1
- package/dist/workspace-runtime/codex/capture-isolation.js +0 -10
- package/dist/workspace-runtime/codex/model-capture.js +0 -675
|
@@ -1,41 +1,23 @@
|
|
|
1
|
-
import { createHash, randomBytes,
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { Readable, Transform } from 'node:stream';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
5
5
|
import { basename, dirname } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { workspaceContextFromRoot } from '../workspaces/registry.js';
|
|
8
|
-
import { PLATFORM_ROUTES, modelResponsesPath, platformEndpoint, workspaceExecutionTraceOriginSchema
|
|
8
|
+
import { PLATFORM_ROUTES, modelResponsesPath, platformEndpoint, workspaceExecutionTraceOriginSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
9
9
|
import { WorkspaceGenerationProvenanceBroker } from './generation-provenance-broker.js';
|
|
10
10
|
import { WorkspaceRemixReferencesBroker } from './remix-references-broker.js';
|
|
11
11
|
import { WorkspaceExecutionContextBroker } from './execution-context-broker.js';
|
|
12
12
|
import { WorkspaceSkillDiscoveryBroker } from './skill-discovery-broker.js';
|
|
13
|
-
import { WorkspaceModelCapture, captureSubmittedResponse, } from './model-capture.js';
|
|
14
13
|
const credentialSchema = z.object({
|
|
15
|
-
token: z.string().min(1),
|
|
16
|
-
|
|
17
|
-
configId: z.string().uuid(),
|
|
18
|
-
model: z.string().min(1).max(128),
|
|
14
|
+
token: z.string().min(1), expiresAt: z.string().datetime(),
|
|
15
|
+
configId: z.string().uuid(), model: z.string().min(1).max(128),
|
|
19
16
|
supportsReasoning: z.boolean().optional(),
|
|
20
17
|
claims: z.object({
|
|
21
|
-
environment: z.string(),
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
runtimeId: z.string(),
|
|
25
|
-
generation: z.number().int(),
|
|
26
|
-
runtimeEpoch: z.number().int(),
|
|
27
|
-
traceOrigin: z
|
|
28
|
-
.custom((value) => workspaceExecutionTraceOriginSchema.safeParse(value).success)
|
|
29
|
-
.optional(),
|
|
30
|
-
capturePolicy: z
|
|
31
|
-
.object({
|
|
32
|
-
enabled: z.literal(true),
|
|
33
|
-
policyVersion: z.literal('workspace-model-capture-v1'),
|
|
34
|
-
maxRequestBytes: z.number().int().positive(),
|
|
35
|
-
maxResponseBytes: z.number().int().positive(),
|
|
36
|
-
expiresAt: z.string().datetime(),
|
|
37
|
-
})
|
|
38
|
-
.optional(),
|
|
18
|
+
environment: z.string(), userId: z.string(), workspaceId: z.string(), runtimeId: z.string(),
|
|
19
|
+
generation: z.number().int(), runtimeEpoch: z.number().int(),
|
|
20
|
+
traceOrigin: z.custom(value => workspaceExecutionTraceOriginSchema.safeParse(value).success).optional(),
|
|
39
21
|
}),
|
|
40
22
|
});
|
|
41
23
|
const MAX_MODEL_REQUEST_BYTES = 128 * 1024 * 1024;
|
|
@@ -53,14 +35,12 @@ export class WorkspaceModelBroker {
|
|
|
53
35
|
references;
|
|
54
36
|
executionContext;
|
|
55
37
|
skills;
|
|
56
|
-
capture;
|
|
57
38
|
constructor(state) {
|
|
58
39
|
this.state = state;
|
|
59
40
|
this.generation = new WorkspaceGenerationProvenanceBroker(state);
|
|
60
41
|
this.references = new WorkspaceRemixReferencesBroker(state);
|
|
61
42
|
this.executionContext = new WorkspaceExecutionContextBroker(state);
|
|
62
43
|
this.skills = new WorkspaceSkillDiscoveryBroker(state);
|
|
63
|
-
this.capture = new WorkspaceModelCapture(state);
|
|
64
44
|
}
|
|
65
45
|
async start() {
|
|
66
46
|
if (this.server)
|
|
@@ -77,7 +57,6 @@ export class WorkspaceModelBroker {
|
|
|
77
57
|
if (!address || typeof address === 'string')
|
|
78
58
|
throw new Error('model_broker_unavailable');
|
|
79
59
|
this.baseUrl = `http://127.0.0.1:${address.port}`;
|
|
80
|
-
await this.capture.start();
|
|
81
60
|
}
|
|
82
61
|
async threadConfig(cwd, modelConfigId, sessionId, reasoningEffort) {
|
|
83
62
|
const { activate, ...settings } = await this.prepareThreadConfig(cwd, modelConfigId, sessionId, reasoningEffort);
|
|
@@ -90,44 +69,24 @@ export class WorkspaceModelBroker {
|
|
|
90
69
|
const { workspaceId } = await workspaceContextFromRoot(this.state, cwd);
|
|
91
70
|
if (modelConfigId !== undefined && !z.string().uuid().safeParse(modelConfigId).success)
|
|
92
71
|
throw new Error('model_config_denied');
|
|
93
|
-
const routeKey = sessionId
|
|
94
|
-
? `${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`
|
|
95
|
-
: workspaceId;
|
|
72
|
+
const routeKey = sessionId ? `${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}` : workspaceId;
|
|
96
73
|
let route = this.routes.get(routeKey);
|
|
97
|
-
const selected = modelConfigId ??
|
|
98
|
-
(route?.epoch === this.state.runtimeEpoch ? route.selected?.entry.modelConfigId : undefined);
|
|
74
|
+
const selected = modelConfigId ?? (route?.epoch === this.state.runtimeEpoch ? route.selected?.entry.modelConfigId : undefined);
|
|
99
75
|
const cacheKey = `${workspaceId}:${selected ?? 'default'}:${reasoningEffort ?? 'default'}:${sessionId ?? ''}:${clientRequestId ?? ''}`;
|
|
100
|
-
let entry = this.entries.get(cacheKey) ??
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
item.modelConfigId === selected &&
|
|
104
|
-
item.sessionId === sessionId &&
|
|
105
|
-
item.clientRequestId === clientRequestId &&
|
|
106
|
-
item.reasoningEffort === reasoningEffort);
|
|
76
|
+
let entry = this.entries.get(cacheKey) ?? [...this.entries.values()].find(item => item.workspaceId === workspaceId && item.epoch === this.state.runtimeEpoch && item.modelConfigId === selected
|
|
77
|
+
&& item.sessionId === sessionId && item.clientRequestId === clientRequestId
|
|
78
|
+
&& item.reasoningEffort === reasoningEffort);
|
|
107
79
|
if (!entry || entry.epoch !== this.state.runtimeEpoch) {
|
|
108
|
-
entry = {
|
|
109
|
-
workspaceId,
|
|
110
|
-
epoch: this.state.runtimeEpoch,
|
|
111
|
-
cacheKey,
|
|
112
|
-
modelConfigId: selected,
|
|
113
|
-
reasoningEffort,
|
|
114
|
-
sessionId,
|
|
115
|
-
clientRequestId,
|
|
116
|
-
};
|
|
80
|
+
entry = { workspaceId, epoch: this.state.runtimeEpoch, cacheKey, modelConfigId: selected, reasoningEffort, sessionId, clientRequestId };
|
|
117
81
|
this.entries.set(cacheKey, entry);
|
|
118
82
|
}
|
|
119
83
|
// Resolve current model metadata and permission at every native turn boundary.
|
|
120
84
|
const credential = await this.credential(entry, true);
|
|
121
85
|
const boundaryId = sessionId && clientRequestId ? await this.generation.prepare() : undefined;
|
|
122
86
|
if (!route || route.epoch !== this.state.runtimeEpoch) {
|
|
123
|
-
route = {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
capability: randomBytes(32).toString('base64url'),
|
|
127
|
-
generationCapability: randomBytes(32).toString('base64url'),
|
|
128
|
-
referencesCapability: randomBytes(32).toString('base64url'),
|
|
129
|
-
skillsCapability: randomBytes(32).toString('base64url'),
|
|
130
|
-
};
|
|
87
|
+
route = { workspaceId, epoch: this.state.runtimeEpoch, capability: randomBytes(32).toString('base64url'),
|
|
88
|
+
generationCapability: randomBytes(32).toString('base64url'), referencesCapability: randomBytes(32).toString('base64url'),
|
|
89
|
+
skillsCapability: randomBytes(32).toString('base64url') };
|
|
131
90
|
this.routes.set(routeKey, route);
|
|
132
91
|
}
|
|
133
92
|
// Each session pins its prepared model; shared credential renewal cannot change an active turn.
|
|
@@ -136,67 +95,41 @@ export class WorkspaceModelBroker {
|
|
|
136
95
|
if (!this.valid(entry) || this.routes.get(routeKey) !== route)
|
|
137
96
|
throw new Error('model_access_denied');
|
|
138
97
|
route.lastAdmissionReason = undefined;
|
|
139
|
-
route.selected = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
...(sessionId && clientRequestId
|
|
143
|
-
? {
|
|
144
|
-
feedback: {
|
|
145
|
-
sessionId,
|
|
146
|
-
clientRequestId,
|
|
147
|
-
cwd,
|
|
148
|
-
boundaryId,
|
|
149
|
-
readOnly,
|
|
150
|
-
...(originCwd ? { originCwd } : {}),
|
|
151
|
-
},
|
|
152
|
-
}
|
|
153
|
-
: {}),
|
|
154
|
-
};
|
|
98
|
+
route.selected = { entry, model: credential.model,
|
|
99
|
+
...(sessionId && clientRequestId ? { feedback: { sessionId, clientRequestId, cwd, boundaryId, readOnly,
|
|
100
|
+
...(originCwd ? { originCwd } : {}) } } : {}) };
|
|
155
101
|
},
|
|
156
102
|
model: credential.model,
|
|
157
103
|
modelConfigId: credential.configId,
|
|
158
104
|
modelProvider: 'playcraft_workspace',
|
|
159
105
|
config: {
|
|
160
|
-
...(sessionId
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
PLAYCRAFT_SKILLS_CAPABILITY: route.skillsCapability,
|
|
169
|
-
},
|
|
170
|
-
}
|
|
171
|
-
: {}),
|
|
106
|
+
...(sessionId ? { 'shell_environment_policy.set': {
|
|
107
|
+
PLAYCRAFT_AGENT_GENERATION_ENDPOINT: `${this.baseUrl}/${routeKey}/generation-provenance`,
|
|
108
|
+
PLAYCRAFT_AGENT_GENERATION_CAPABILITY: route.generationCapability,
|
|
109
|
+
PLAYCRAFT_REMIX_REFERENCES_ENDPOINT: `${this.baseUrl}/${routeKey}/remix-references`,
|
|
110
|
+
PLAYCRAFT_REMIX_REFERENCES_CAPABILITY: route.referencesCapability,
|
|
111
|
+
PLAYCRAFT_SKILLS_ENDPOINT: `${this.baseUrl}/${routeKey}/skill-discovery`,
|
|
112
|
+
PLAYCRAFT_SKILLS_CAPABILITY: route.skillsCapability,
|
|
113
|
+
} } : {}),
|
|
172
114
|
'model_providers.playcraft_workspace': {
|
|
173
|
-
name: 'PlayCraft Workspace',
|
|
174
|
-
|
|
175
|
-
wire_api: 'responses',
|
|
176
|
-
requires_openai_auth: false,
|
|
115
|
+
name: 'PlayCraft Workspace', base_url: `${this.baseUrl}/${routeKey}`,
|
|
116
|
+
wire_api: 'responses', requires_openai_auth: false,
|
|
177
117
|
http_headers: { Authorization: `Bearer ${route.capability}` },
|
|
178
118
|
},
|
|
179
119
|
},
|
|
180
120
|
};
|
|
181
121
|
}
|
|
182
122
|
valid(entry) {
|
|
183
|
-
return Boolean(this.server &&
|
|
184
|
-
|
|
185
|
-
this.
|
|
186
|
-
this.state.journal.getWorkspace(entry.workspaceId) &&
|
|
187
|
-
this.entries.get(entry.cacheKey) === entry);
|
|
123
|
+
return Boolean(this.server && !this.state.journal.destroyed()
|
|
124
|
+
&& this.state.runtimeEpoch === entry.epoch && this.state.journal.getWorkspace(entry.workspaceId)
|
|
125
|
+
&& this.entries.get(entry.cacheKey) === entry);
|
|
188
126
|
}
|
|
189
127
|
/** Called immediately before the native turn/start write, within this broker's process clock. */
|
|
190
128
|
markCodexDispatched(cwd, sessionId, clientRequestId) {
|
|
191
|
-
const workspaceId = /^(basic|remix-)/.test(basename(cwd))
|
|
192
|
-
? basename(dirname(cwd))
|
|
193
|
-
: basename(cwd);
|
|
129
|
+
const workspaceId = /^(basic|remix-)/.test(basename(cwd)) ? basename(dirname(cwd)) : basename(cwd);
|
|
194
130
|
const route = this.routes.get(`${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`);
|
|
195
131
|
const entry = route?.selected?.entry;
|
|
196
|
-
if (!entry ||
|
|
197
|
-
!this.valid(entry) ||
|
|
198
|
-
entry.sessionId !== sessionId ||
|
|
199
|
-
entry.clientRequestId !== clientRequestId)
|
|
132
|
+
if (!entry || !this.valid(entry) || entry.sessionId !== sessionId || entry.clientRequestId !== clientRequestId)
|
|
200
133
|
return;
|
|
201
134
|
entry.codexDispatchedAt = new Date().toISOString();
|
|
202
135
|
entry.codexDispatchMark = performance.now();
|
|
@@ -204,9 +137,7 @@ export class WorkspaceModelBroker {
|
|
|
204
137
|
async credential(entry, refresh = false) {
|
|
205
138
|
if (!this.valid(entry))
|
|
206
139
|
throw new Error('model_access_denied');
|
|
207
|
-
if (!refresh &&
|
|
208
|
-
entry.credential &&
|
|
209
|
-
Date.parse(entry.credential.expiresAt) > Date.now() + 60_000)
|
|
140
|
+
if (!refresh && entry.credential && Date.parse(entry.credential.expiresAt) > Date.now() + 60_000)
|
|
210
141
|
return entry.credential;
|
|
211
142
|
let pending = this.pending.get(entry);
|
|
212
143
|
if (!pending) {
|
|
@@ -219,48 +150,26 @@ export class WorkspaceModelBroker {
|
|
|
219
150
|
const config = this.state.config;
|
|
220
151
|
const endpoint = platformEndpoint(config.publicBaseUrl, PLATFORM_ROUTES.modelCredentials);
|
|
221
152
|
const response = await fetch(endpoint, {
|
|
222
|
-
method: 'POST',
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
authorization: `Bearer ${config.controlToken}`,
|
|
227
|
-
'content-type': 'application/json',
|
|
228
|
-
'playcraft-env': config.environment,
|
|
229
|
-
...(this.capture.supported ? { 'x-playcraft-model-capture-protocol': '1' } : {}),
|
|
230
|
-
},
|
|
231
|
-
body: JSON.stringify({
|
|
232
|
-
runtimeId: config.runtimeId,
|
|
233
|
-
generation: config.generation,
|
|
234
|
-
runtimeEpoch: entry.epoch,
|
|
235
|
-
workspaceId: entry.workspaceId,
|
|
153
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
|
|
154
|
+
headers: { authorization: `Bearer ${config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': config.environment },
|
|
155
|
+
body: JSON.stringify({ runtimeId: config.runtimeId, generation: config.generation,
|
|
156
|
+
runtimeEpoch: entry.epoch, workspaceId: entry.workspaceId,
|
|
236
157
|
...(entry.sessionId ? { sessionId: entry.sessionId } : {}),
|
|
237
158
|
...(entry.clientRequestId ? { clientRequestId: entry.clientRequestId } : {}),
|
|
238
159
|
...(entry.modelConfigId ? { modelConfigId: entry.modelConfigId } : {}),
|
|
239
|
-
...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort } : {}),
|
|
240
|
-
}),
|
|
160
|
+
...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort } : {}) }),
|
|
241
161
|
});
|
|
242
162
|
if (!response.ok) {
|
|
243
|
-
const error =
|
|
244
|
-
throw new Error([
|
|
245
|
-
'model_config_required',
|
|
246
|
-
'model_config_denied',
|
|
247
|
-
'reasoning_effort_denied',
|
|
248
|
-
'reasoning_effort_unsupported',
|
|
249
|
-
].includes(error.reason ?? '')
|
|
250
|
-
? error.reason
|
|
251
|
-
: 'model_credential_unavailable');
|
|
163
|
+
const error = await response.json().catch(() => ({}));
|
|
164
|
+
throw new Error(['model_config_required', 'model_config_denied', 'reasoning_effort_denied', 'reasoning_effort_unsupported'].includes(error.reason ?? '') ? error.reason : 'model_credential_unavailable');
|
|
252
165
|
}
|
|
253
166
|
const credential = credentialSchema.parse(await response.json());
|
|
254
167
|
const claims = credential.claims;
|
|
255
|
-
if (!this.valid(entry) ||
|
|
256
|
-
claims.
|
|
257
|
-
claims.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
claims.generation !== config.generation ||
|
|
261
|
-
claims.runtimeEpoch !== entry.epoch ||
|
|
262
|
-
(entry.modelConfigId !== undefined && credential.configId !== entry.modelConfigId) ||
|
|
263
|
-
Date.parse(credential.expiresAt) <= Date.now() + 1000)
|
|
168
|
+
if (!this.valid(entry) || claims.workspaceId !== entry.workspaceId || claims.runtimeId !== config.runtimeId
|
|
169
|
+
|| claims.userId !== config.userId || claims.environment !== config.environment
|
|
170
|
+
|| claims.generation !== config.generation || claims.runtimeEpoch !== entry.epoch
|
|
171
|
+
|| (entry.modelConfigId !== undefined && credential.configId !== entry.modelConfigId)
|
|
172
|
+
|| Date.parse(credential.expiresAt) <= Date.now() + 1000)
|
|
264
173
|
throw new Error('model_credential_scope_mismatch');
|
|
265
174
|
entry.modelConfigId = credential.configId;
|
|
266
175
|
entry.credential = credential;
|
|
@@ -273,9 +182,6 @@ export class WorkspaceModelBroker {
|
|
|
273
182
|
const abort = new AbortController();
|
|
274
183
|
this.requests.add(abort);
|
|
275
184
|
let activeRoute;
|
|
276
|
-
let capturedRequest;
|
|
277
|
-
let capturedResponse;
|
|
278
|
-
let requestEnded = false;
|
|
279
185
|
const cancel = () => abort.abort();
|
|
280
186
|
response.once('close', cancel);
|
|
281
187
|
try {
|
|
@@ -288,21 +194,10 @@ export class WorkspaceModelBroker {
|
|
|
288
194
|
const contextRequest = match?.[2] === 'execution-context';
|
|
289
195
|
const referencesRequest = match?.[2] === 'remix-references';
|
|
290
196
|
const skillsRequest = match?.[2]?.startsWith('skill-discovery/');
|
|
291
|
-
const expected = Buffer.from(route
|
|
292
|
-
? `
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
? route.referencesCapability
|
|
296
|
-
: skillsRequest
|
|
297
|
-
? route.skillsCapability
|
|
298
|
-
: route.capability}`
|
|
299
|
-
: '');
|
|
300
|
-
if (request.method !== 'POST' ||
|
|
301
|
-
!entry ||
|
|
302
|
-
!expected.length ||
|
|
303
|
-
presented.length !== expected.length ||
|
|
304
|
-
!timingSafeEqual(presented, expected) ||
|
|
305
|
-
!this.valid(entry)) {
|
|
197
|
+
const expected = Buffer.from(route ? `Bearer ${generationRequest || contextRequest ? route.generationCapability
|
|
198
|
+
: referencesRequest ? route.referencesCapability : skillsRequest ? route.skillsCapability : route.capability}` : '');
|
|
199
|
+
if (request.method !== 'POST' || !entry || !expected.length || presented.length !== expected.length
|
|
200
|
+
|| !timingSafeEqual(presented, expected) || !this.valid(entry)) {
|
|
306
201
|
response.writeHead(403);
|
|
307
202
|
response.end();
|
|
308
203
|
return;
|
|
@@ -345,23 +240,12 @@ export class WorkspaceModelBroker {
|
|
|
345
240
|
return;
|
|
346
241
|
}
|
|
347
242
|
const rejectOversizedBody = () => {
|
|
348
|
-
response.writeHead(413, {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
});
|
|
352
|
-
response.end(JSON.stringify({
|
|
353
|
-
error: {
|
|
354
|
-
message: 'model_request_too_large',
|
|
355
|
-
type: 'invalid_request_error',
|
|
356
|
-
code: 'model_request_too_large',
|
|
357
|
-
maxBytes: MAX_MODEL_REQUEST_BYTES,
|
|
358
|
-
},
|
|
359
|
-
}));
|
|
243
|
+
response.writeHead(413, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
244
|
+
response.end(JSON.stringify({ error: { message: 'model_request_too_large',
|
|
245
|
+
type: 'invalid_request_error', code: 'model_request_too_large', maxBytes: MAX_MODEL_REQUEST_BYTES } }));
|
|
360
246
|
};
|
|
361
247
|
const declaredLength = request.headers['content-length'];
|
|
362
|
-
if (declaredLength !== undefined &&
|
|
363
|
-
/^\d+$/.test(declaredLength) &&
|
|
364
|
-
Number(declaredLength) > MAX_MODEL_REQUEST_BYTES) {
|
|
248
|
+
if (declaredLength !== undefined && /^\d+$/.test(declaredLength) && Number(declaredLength) > MAX_MODEL_REQUEST_BYTES) {
|
|
365
249
|
rejectOversizedBody();
|
|
366
250
|
return;
|
|
367
251
|
}
|
|
@@ -372,21 +256,10 @@ export class WorkspaceModelBroker {
|
|
|
372
256
|
const credentialMark = performance.now();
|
|
373
257
|
const credential = await this.credential(entry);
|
|
374
258
|
const credentialWaitMs = elapsed(credentialMark);
|
|
375
|
-
const modelCallId = randomUUID();
|
|
376
|
-
const candidateCaptureOrigin = credential.claims.traceOrigin;
|
|
377
|
-
const captured = candidateCaptureOrigin?.sessionId === entry.sessionId &&
|
|
378
|
-
candidateCaptureOrigin?.clientRequestId === entry.clientRequestId
|
|
379
|
-
? this.capture.begin(modelCallId, credential)
|
|
380
|
-
: undefined;
|
|
381
|
-
capturedRequest = captured?.artifact('codex_request', request.headers['content-type'] ?? 'application/json');
|
|
382
|
-
capturedResponse = captured?.artifact('codex_response');
|
|
383
|
-
if (capturedResponse)
|
|
384
|
-
captureSubmittedResponse(response, capturedResponse);
|
|
385
259
|
const requestReadMark = performance.now();
|
|
386
260
|
const chunks = [];
|
|
387
261
|
let size = 0;
|
|
388
262
|
for await (const chunk of request) {
|
|
389
|
-
capturedRequest?.append(chunk);
|
|
390
263
|
size += chunk.length;
|
|
391
264
|
if (size > MAX_MODEL_REQUEST_BYTES) {
|
|
392
265
|
rejectOversizedBody();
|
|
@@ -395,8 +268,6 @@ export class WorkspaceModelBroker {
|
|
|
395
268
|
chunks.push(Buffer.from(chunk));
|
|
396
269
|
}
|
|
397
270
|
const requestReadMs = elapsed(requestReadMark);
|
|
398
|
-
capturedRequest?.finish({ sourceEnd: 'eof', protocolTerminal: 'not_applicable' });
|
|
399
|
-
requestEnded = true;
|
|
400
271
|
if (!this.valid(entry))
|
|
401
272
|
throw new Error('model_access_denied');
|
|
402
273
|
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
@@ -417,12 +288,9 @@ export class WorkspaceModelBroker {
|
|
|
417
288
|
const reasoning = body.reasoning;
|
|
418
289
|
const modelName = selected.model.split('/').pop() ?? selected.model;
|
|
419
290
|
const effort = entry.reasoningEffort === 'max' && /^o\d|gpt-[5-9]/i.test(modelName)
|
|
420
|
-
? 'xhigh'
|
|
421
|
-
: entry.reasoningEffort;
|
|
291
|
+
? 'xhigh' : entry.reasoningEffort;
|
|
422
292
|
body.reasoning = {
|
|
423
|
-
...(reasoning && typeof reasoning === 'object' && !Array.isArray(reasoning)
|
|
424
|
-
? reasoning
|
|
425
|
-
: {}),
|
|
293
|
+
...(reasoning && typeof reasoning === 'object' && !Array.isArray(reasoning) ? reasoning : {}),
|
|
426
294
|
effort,
|
|
427
295
|
};
|
|
428
296
|
}
|
|
@@ -431,58 +299,26 @@ export class WorkspaceModelBroker {
|
|
|
431
299
|
const dispatchedMark = performance.now();
|
|
432
300
|
const beforeDispatchMs = elapsed(receivedMark);
|
|
433
301
|
const candidateOrigin = credential.claims.traceOrigin;
|
|
434
|
-
const origin = candidateOrigin?.sessionId === entry.sessionId &&
|
|
435
|
-
candidateOrigin
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const broker = {
|
|
439
|
-
receivedAt,
|
|
440
|
-
dispatchedAt,
|
|
441
|
-
credentialWaitMs,
|
|
442
|
-
requestReadMs,
|
|
443
|
-
beforeDispatchMs,
|
|
444
|
-
};
|
|
302
|
+
const origin = candidateOrigin?.sessionId === entry.sessionId && candidateOrigin?.clientRequestId === entry.clientRequestId
|
|
303
|
+
? candidateOrigin : undefined;
|
|
304
|
+
const broker = { receivedAt, dispatchedAt,
|
|
305
|
+
credentialWaitMs, requestReadMs, beforeDispatchMs };
|
|
445
306
|
let attemptId;
|
|
446
307
|
let outcome = 'error';
|
|
447
308
|
try {
|
|
448
309
|
const upstream = await fetch(platformEndpoint(this.state.config.publicBaseUrl, modelResponsesPath(credential.configId)), {
|
|
449
|
-
method: 'POST',
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
sessionId: origin.sessionId,
|
|
462
|
-
clientRequestId: origin.clientRequestId,
|
|
463
|
-
...(entry.codexDispatchedAt
|
|
464
|
-
? { codexDispatchedAt: entry.codexDispatchedAt }
|
|
465
|
-
: {}),
|
|
466
|
-
brokerReceivedAt: receivedAt,
|
|
467
|
-
brokerDispatchedAt: dispatchedAt,
|
|
468
|
-
credentialWaitMs,
|
|
469
|
-
requestReadMs,
|
|
470
|
-
beforeDispatchMs,
|
|
471
|
-
...(entry.codexDispatchMark === undefined
|
|
472
|
-
? {}
|
|
473
|
-
: {
|
|
474
|
-
codexDispatchToBrokerMs: Math.max(0, Math.round(receivedMark - entry.codexDispatchMark)),
|
|
475
|
-
}),
|
|
476
|
-
}),
|
|
477
|
-
}
|
|
478
|
-
: {}),
|
|
479
|
-
...(selected?.feedback
|
|
480
|
-
? {
|
|
481
|
-
'x-playcraft-cloud-session-id': selected.feedback.sessionId,
|
|
482
|
-
'x-playcraft-client-request-id': selected.feedback.clientRequestId,
|
|
483
|
-
}
|
|
484
|
-
: {}),
|
|
485
|
-
},
|
|
310
|
+
method: 'POST', redirect: 'error', signal: abort.signal,
|
|
311
|
+
headers: { authorization: `Bearer ${credential.token}`, 'content-type': 'application/json',
|
|
312
|
+
...(origin ? { 'x-playcraft-model-journey': JSON.stringify({ version: 1,
|
|
313
|
+
runtimeOperationId: origin.runtimeOperationId, sessionId: origin.sessionId, clientRequestId: origin.clientRequestId,
|
|
314
|
+
...(entry.codexDispatchedAt ? { codexDispatchedAt: entry.codexDispatchedAt } : {}),
|
|
315
|
+
brokerReceivedAt: receivedAt, brokerDispatchedAt: dispatchedAt, credentialWaitMs, requestReadMs, beforeDispatchMs,
|
|
316
|
+
...(entry.codexDispatchMark === undefined ? {} : { codexDispatchToBrokerMs: Math.max(0, Math.round(receivedMark - entry.codexDispatchMark)) }),
|
|
317
|
+
}) } : {}),
|
|
318
|
+
...(selected?.feedback ? {
|
|
319
|
+
'x-playcraft-cloud-session-id': selected.feedback.sessionId,
|
|
320
|
+
'x-playcraft-client-request-id': selected.feedback.clientRequestId,
|
|
321
|
+
} : {}) },
|
|
486
322
|
body: JSON.stringify(body),
|
|
487
323
|
});
|
|
488
324
|
broker.responseHeadersAt = new Date().toISOString();
|
|
@@ -507,7 +343,6 @@ export class WorkspaceModelBroker {
|
|
|
507
343
|
if (upstream.body)
|
|
508
344
|
await pipeline(Readable.fromWeb(upstream.body), new Transform({
|
|
509
345
|
transform(chunk, _encoding, callback) {
|
|
510
|
-
capturedResponse?.upstreamObserved(chunk.length);
|
|
511
346
|
if (chunk.length && broker.firstBodyByteAt === undefined) {
|
|
512
347
|
broker.firstBodyByteAt = new Date().toISOString();
|
|
513
348
|
broker.proxyFirstBodyByteMs = elapsed(dispatchedMark);
|
|
@@ -529,37 +364,21 @@ export class WorkspaceModelBroker {
|
|
|
529
364
|
broker.proxyResponseDurationMs = elapsed(dispatchedMark);
|
|
530
365
|
if (origin && attemptId) {
|
|
531
366
|
// Telemetry is sent after transfer and cannot delay or change the Codex model response.
|
|
532
|
-
void this.reportObservation(credential.token, {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
runtimeOperationId: origin.runtimeOperationId,
|
|
536
|
-
sessionId: origin.sessionId,
|
|
537
|
-
clientRequestId: origin.clientRequestId,
|
|
538
|
-
broker,
|
|
539
|
-
outcome,
|
|
540
|
-
});
|
|
367
|
+
void this.reportObservation(credential.token, { version: 1, attemptId,
|
|
368
|
+
runtimeOperationId: origin.runtimeOperationId, sessionId: origin.sessionId,
|
|
369
|
+
clientRequestId: origin.clientRequestId, broker, outcome });
|
|
541
370
|
}
|
|
542
371
|
}
|
|
543
372
|
}
|
|
544
373
|
catch {
|
|
545
374
|
if (!response.headersSent) {
|
|
546
|
-
response.writeHead(503, {
|
|
547
|
-
|
|
548
|
-
'cache-control': 'no-store',
|
|
549
|
-
});
|
|
550
|
-
response.end(JSON.stringify({
|
|
551
|
-
error: { message: 'model_credential_unavailable', type: 'server_error' },
|
|
552
|
-
}));
|
|
375
|
+
response.writeHead(503, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
376
|
+
response.end(JSON.stringify({ error: { message: 'model_credential_unavailable', type: 'server_error' } }));
|
|
553
377
|
}
|
|
554
378
|
else
|
|
555
379
|
response.destroy();
|
|
556
380
|
}
|
|
557
381
|
finally {
|
|
558
|
-
if (!requestEnded)
|
|
559
|
-
capturedRequest?.finish({
|
|
560
|
-
sourceEnd: abort.signal.aborted ? 'aborted' : 'io_error',
|
|
561
|
-
protocolTerminal: 'unknown',
|
|
562
|
-
});
|
|
563
382
|
response.off('close', cancel);
|
|
564
383
|
this.requests.delete(abort);
|
|
565
384
|
if (activeRoute) {
|
|
@@ -573,17 +392,12 @@ export class WorkspaceModelBroker {
|
|
|
573
392
|
async reportObservation(token, input) {
|
|
574
393
|
try {
|
|
575
394
|
const response = await fetch(platformEndpoint(this.state.config.publicBaseUrl, '/api/llm-proxy/runtime-observations'), {
|
|
576
|
-
method: 'POST',
|
|
577
|
-
|
|
578
|
-
signal: AbortSignal.timeout(3_000),
|
|
579
|
-
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
580
|
-
body: JSON.stringify(input),
|
|
395
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(3_000),
|
|
396
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, body: JSON.stringify(input),
|
|
581
397
|
});
|
|
582
398
|
await response.body?.cancel();
|
|
583
399
|
}
|
|
584
|
-
catch {
|
|
585
|
-
/* Missing observations remain unknown; never replay a model request for telemetry. */
|
|
586
|
-
}
|
|
400
|
+
catch { /* Missing observations remain unknown; never replay a model request for telemetry. */ }
|
|
587
401
|
}
|
|
588
402
|
cancelSession(cwd, sessionId) {
|
|
589
403
|
const routeKey = `${/^(basic|remix-)/.test(basename(cwd)) ? basename(dirname(cwd)) : basename(cwd)}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`;
|
|
@@ -610,8 +424,7 @@ export class WorkspaceModelBroker {
|
|
|
610
424
|
request.abort();
|
|
611
425
|
if (server) {
|
|
612
426
|
server.closeAllConnections();
|
|
613
|
-
await new Promise(
|
|
427
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
614
428
|
}
|
|
615
|
-
await this.capture.close();
|
|
616
429
|
}
|
|
617
430
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.86",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"lint": "pnpm -w run lint:eslint packages/cli",
|
|
26
25
|
"dev": "tsc -w",
|
|
27
26
|
"build": "node scripts/generate-asset-skills.mjs && node scripts/generate-video-skills.mjs && node scripts/generate-parameter-skill.mjs && node scripts/generate-level-editor-skill.mjs && node scripts/generate-preview-runtime.mjs && tsc",
|
|
28
27
|
"start": "node dist/index.js",
|
|
@@ -58,8 +57,8 @@
|
|
|
58
57
|
"@gltf-transform/core": "^4.3.0",
|
|
59
58
|
"@gltf-transform/extensions": "^4.3.0",
|
|
60
59
|
"@gltf-transform/functions": "^4.3.0",
|
|
61
|
-
"@playcraft/build": "0.1.
|
|
62
|
-
"@playcraft/common": "0.1.
|
|
60
|
+
"@playcraft/build": "0.1.86",
|
|
61
|
+
"@playcraft/common": "0.1.86",
|
|
63
62
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
64
63
|
"chokidar": "^4.0.3",
|
|
65
64
|
"commander": "^13.1.0",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"release-guard.mjs": "728ac088f8d885a50a1c9bcdddad724fa80afd06e66a6dd3658756034e563055",
|
|
9
9
|
"runtime.d.mts": "a90c69a8b72105e8dc8fe0b3bcd272f038ed255f7daf5ec6c1aefc293a1e84b2",
|
|
10
10
|
"runtime.d.ts": "a90c69a8b72105e8dc8fe0b3bcd272f038ed255f7daf5ec6c1aefc293a1e84b2",
|
|
11
|
-
"runtime.mjs": "
|
|
11
|
+
"runtime.mjs": "50dc53ff94effedf10903df708c1ecdb51bd8145a5a6ec1eac9449cad7424a24",
|
|
12
12
|
"types/adapters.d.ts": "f5e70c4bb9043de1c590eada14266f613286360c809faf26680b0bbbd1cc7b2c",
|
|
13
13
|
"types/host.d.ts": "9ea867d5485a66aba7da86ea7e5583bca6b9a7a89618afd0eb70ace58e06faf9",
|
|
14
14
|
"types/index.d.ts": "210d24a767f260bc8944d917b50dae1357cba31b8b5d499bea7bcc99b1051e6c",
|
|
@@ -257,7 +257,7 @@ var package_default = {
|
|
|
257
257
|
test: "vitest run",
|
|
258
258
|
"test:watch": "vitest",
|
|
259
259
|
playground: 'pnpm --filter "@playcraft/iframe-bridge-playground-*" --parallel dev',
|
|
260
|
-
lint: "
|
|
260
|
+
lint: "echo 'No eslint config yet'",
|
|
261
261
|
release: "node ../../scripts/release-npm.mjs --packages=iframe-bridge --skip-install --skip-tests --skip-git-push"
|
|
262
262
|
},
|
|
263
263
|
devDependencies: {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** Matches the root-owned sandbox launcher: control 1500, development children 1501. */
|
|
2
|
-
export function isCaptureIsolationAvailable(input = {
|
|
3
|
-
platform: process.platform,
|
|
4
|
-
uid: process.getuid?.(),
|
|
5
|
-
launcher: process.env.PLAYCRAFT_LAUNCHER_SOCKET,
|
|
6
|
-
}) {
|
|
7
|
-
return (input.platform === 'linux' &&
|
|
8
|
-
input.uid === 1500 &&
|
|
9
|
-
input.launcher === '/run/playcraft/runtime/launcher.sock');
|
|
10
|
-
}
|