@taskforcehq/taskforce 0.3.330 → 0.3.332
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 +7 -2
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +2 -2
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.js +2 -2
- package/dist/components/features/planning/PlanningModule.js +15 -3
- package/dist/components/features/planning/planningMetadata.d.ts +10 -0
- package/dist/components/features/planning/planningMetadata.js +8 -0
- package/dist/components/views/StandaloneLayout.js +35 -1
- package/dist/components/views/panels/PlanningDrawer.d.ts +2 -1
- package/dist/components/views/panels/PlanningDrawer.js +12 -8
- package/dist/config/envSchema.js +7 -0
- package/dist/core/McpTokenService.d.ts +2 -0
- package/dist/core/McpTokenService.js +13 -5
- package/dist/core/Taskforce.d.ts +11 -3
- package/dist/core/Taskforce.js +33 -3
- package/dist/core/types.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +2 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +28 -9
- package/dist/mcp/httpProtocolRouting.d.ts +1 -0
- package/dist/mcp/httpProtocolRouting.js +8 -5
- package/dist/mcp/runtime.js +24 -5
- package/dist/mcp/taskPlanningRegistrar.js +2 -2
- package/dist/mcp/toolProfiles.js +1 -0
- package/dist/mcp/toolRegistry.js +8 -8
- package/dist/server/buildInfo.d.ts +1 -0
- package/dist/server/buildInfo.js +5 -1
- package/dist/server/cloudMcpCapacityTelemetry.d.ts +68 -0
- package/dist/server/cloudMcpCapacityTelemetry.js +221 -0
- package/dist/server/localWorkspaceSyncServerRuntime.js +13 -5
- package/dist/server/routes/admin.js +14 -2
- package/dist/server/routes/shared.d.ts +3 -0
- package/dist/server/routes/shared.js +280 -240
- package/dist/service/localServiceCli.d.ts +2 -2
- package/dist/service/localServiceCli.js +4 -3
- package/dist/services/executorPhaseAIssuerCore.d.ts +13 -0
- package/dist/services/executorPhaseAIssuerCore.js +107 -0
- package/dist/services/taskforceAgentMcpBridge.d.ts +1 -1
- package/dist/services/taskforceAgentMcpBridge.js +35 -5
- package/dist/shared/executorPhaseASchemaValidation.js +1 -1
- package/dist/storage/postgresAdapter.js +3 -1
- package/dist/storage/postgresRequestDiagnostics.d.ts +9 -1
- package/dist/storage/postgresRequestDiagnostics.js +15 -2
- package/dist/storage/postgresWorker.js +13 -3
- package/dist/storage/postgresWorkerProtocol.d.ts +6 -0
- package/dist/storage/workflowStore.d.ts +2 -0
- package/dist/storage/workflowStore.js +39 -2
- package/dist/sync/cloudSyncApi.js +4 -2
- package/dist/sync/collaborationSyncState.js +5 -0
- package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +3 -1
- package/dist/sync/coordinator/localWorkspaceSyncRunner.js +18 -0
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
- package/dist/sync/syncApplyHandlers.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.js +72 -15
- package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +7 -7
- package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +50 -13
- package/dist/sync/v3/localOutboxRecoveryPreparationService.js +8 -2
- package/dist/sync/v3/localTaskCreateOutboxService.d.ts +3 -1
- package/dist/sync/v3/localTaskCreateOutboxService.js +6 -1
- package/dist/sync/v3/localTaskDeleteOutboxService.js +1 -0
- package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskLifecycleOutboxService.js +5 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.js +6 -2
- package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskStatusOutboxService.js +20 -5
- package/dist/sync/v3/syncDurabilityStore.d.ts +2 -1
- package/dist/sync/v3/syncDurabilityStore.js +34 -6
- package/dist/sync/v3/workflowRuntimeMutationService.d.ts +5 -0
- package/dist/sync/v3/workflowRuntimeMutationService.js +124 -71
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +25 -3
- package/dist/sync/v3/workspaceSyncV3Feed.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3Feed.js +6 -1
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +49 -18
- package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +50 -5
- package/dist/ui/assets/{AiIdentityRosterCard-OcSz1PkK.js → AiIdentityRosterCard-CDMPtnbC.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-Cww85C4E.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-Dt9TKYSH.js} +1 -1
- package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-CKgapnxu.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-T-cEVcOZ.js} +2 -2
- package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-HD4MUjAZ.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-_V0zfMuh.js} +1 -1
- package/dist/ui/assets/PlanningModule-mTkBygsB.js +1 -0
- package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-Btiq06VO.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-C00NVUpn.js} +1 -1
- package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-Cp-L-4xh.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-Coq9-PUp.js} +2 -2
- package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-CuqRzMmM.js} +1 -1
- package/dist/ui/assets/index-DSQINwt9.js +7 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -1
- package/dist/ui/assets/PlanningModule-pRPjR7v5.js +0 -1
- package/dist/ui/assets/index-CTsDBaef.js +0 -7
|
@@ -21,6 +21,7 @@ import { handleModernMcpHttpRequest, isModernMcpHttpRequest, } from '../../mcp/h
|
|
|
21
21
|
import { resolveAppAccessDeniedResponse } from '../appAccess.js';
|
|
22
22
|
import { resolveCloudApplicationBaseUrl } from '../../config/applicationBaseUrl.js';
|
|
23
23
|
import { getRequestPerformanceDiagnostics, runWithRequestPerformanceDiagnostics, } from '../../storage/postgresRequestDiagnostics.js';
|
|
24
|
+
import { beginCloudMcpCapacityRequest, isCloudMcpCapacityTelemetryEnabled, TASKFORCE_CAPACITY_TRACE_HEADER, } from '../cloudMcpCapacityTelemetry.js';
|
|
24
25
|
// === SECTION: Error helpers ===
|
|
25
26
|
export function resolveErrorStatusCode(error, fallback = 400) {
|
|
26
27
|
if (error instanceof TaskforceRuleError)
|
|
@@ -324,7 +325,7 @@ function findDeniedCloudMcpToolName(body, scopes, resolveToolPolicy) {
|
|
|
324
325
|
return null;
|
|
325
326
|
}
|
|
326
327
|
export function makeHandleCloudMcpRequest(core, deps) {
|
|
327
|
-
const { ensureAppAccess, resolveAuthenticatedUserId, writeRateLimit, createMcpServer = createTaskforceMcpServer, resolveMcpToolPolicy = resolveProductionCloudMcpToolPolicy, acquireMcpRuntime, } = deps;
|
|
328
|
+
const { ensureAppAccess, resolveAuthenticatedUserId, writeRateLimit, createMcpServer = createTaskforceMcpServer, resolveMcpToolPolicy = resolveProductionCloudMcpToolPolicy, acquireMcpRuntime, capacityTelemetryEnabled = isCloudMcpCapacityTelemetryEnabled, beginCapacityRequest = beginCloudMcpCapacityRequest, } = deps;
|
|
328
329
|
const authenticateCloudMcpRequest = makeAuthenticateCloudMcpRequest(core, {
|
|
329
330
|
resolveAuthenticatedUserId,
|
|
330
331
|
writeMcpHttpError,
|
|
@@ -405,105 +406,209 @@ export function makeHandleCloudMcpRequest(core, deps) {
|
|
|
405
406
|
writeMcpHttpError(res, 400, 'The /mcp HTTP endpoint is only available in cloud runtime. In local runtime, use the local stdio MCP server (for example: `taskforce mcp <project-path>`).');
|
|
406
407
|
return;
|
|
407
408
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
catch (error) {
|
|
434
|
-
writeMcpHttpError(res, resolveErrorStatusCode(error, 400), 'Invalid JSON body.');
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
const authScopes = auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [];
|
|
438
|
-
const requestId = String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID();
|
|
439
|
-
res.setHeader('X-Request-ID', requestId);
|
|
440
|
-
const mcpMethod = !Array.isArray(body) && body !== null && typeof body === 'object'
|
|
441
|
-
? String(body.method || '')
|
|
442
|
-
: '';
|
|
443
|
-
const initializeRequest = isInitializeRequest(body)
|
|
444
|
-
|| mcpMethod === 'initialize';
|
|
445
|
-
const modernProtocolRequest = await isModernMcpHttpRequest(req, body);
|
|
446
|
-
const deniedToolName = findDeniedCloudMcpToolName(body, authScopes, resolveMcpToolPolicy);
|
|
447
|
-
if (deniedToolName) {
|
|
448
|
-
core.addMcpAuditLog({
|
|
449
|
-
workspaceId: auth.workspaceId,
|
|
450
|
-
actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
|
|
451
|
-
actorUserId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || 'anonymous',
|
|
452
|
-
tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
|
|
453
|
-
toolName: deniedToolName,
|
|
454
|
-
details: {
|
|
455
|
-
requestId,
|
|
456
|
-
ip: req.socket?.remoteAddress || null,
|
|
457
|
-
userAgent: requestHeader(req, 'user-agent', '') || null,
|
|
458
|
-
outcome: 'deny',
|
|
459
|
-
reasonCode: 'scope_denied',
|
|
460
|
-
scopes: authScopes
|
|
409
|
+
const capacityRequest = beginCapacityRequest();
|
|
410
|
+
const capacityTraceEnabled = requestHeader(req, TASKFORCE_CAPACITY_TRACE_HEADER, '').trim() === '1';
|
|
411
|
+
return runWithRequestPerformanceDiagnostics(capacityTelemetryEnabled(), async () => {
|
|
412
|
+
let capacitySettled = false;
|
|
413
|
+
const settleCapacity = (aborted = false) => {
|
|
414
|
+
if (capacitySettled)
|
|
415
|
+
return;
|
|
416
|
+
capacitySettled = true;
|
|
417
|
+
capacityRequest.finish({
|
|
418
|
+
statusCode: res.statusCode,
|
|
419
|
+
aborted,
|
|
420
|
+
postgres: getRequestPerformanceDiagnostics(),
|
|
421
|
+
});
|
|
422
|
+
};
|
|
423
|
+
const onFinish = () => settleCapacity(false);
|
|
424
|
+
const onClose = () => settleCapacity(!res.writableEnded);
|
|
425
|
+
res.once('finish', onFinish);
|
|
426
|
+
res.once('close', onClose);
|
|
427
|
+
try {
|
|
428
|
+
if (!ensureAppAccess(req, res))
|
|
429
|
+
return;
|
|
430
|
+
if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {
|
|
431
|
+
writeMcpHttpError(res, 405, 'Method not allowed.');
|
|
432
|
+
return;
|
|
461
433
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
434
|
+
const authStartedAtMs = Date.now();
|
|
435
|
+
const auth = authenticateCloudMcpRequest(req, res);
|
|
436
|
+
authMs = Date.now() - authStartedAtMs;
|
|
437
|
+
if (!auth)
|
|
438
|
+
return;
|
|
439
|
+
if (capacityTraceEnabled)
|
|
440
|
+
capacityRequest.setTraceHeaders(res);
|
|
441
|
+
if (req.method !== 'POST') {
|
|
442
|
+
writeMcpHttpError(res, 405, 'Method not allowed.');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!isJsonContentType(requestHeader(req, 'content-type', ''))) {
|
|
446
|
+
writeMcpHttpError(res, 415, 'Unsupported Media Type: Content-Type must be application/json');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
let body = undefined;
|
|
450
|
+
try {
|
|
451
|
+
const bodyParseStartedAtMs = Date.now();
|
|
452
|
+
body = await parseJsonBody(req);
|
|
453
|
+
bodyParseMs = Date.now() - bodyParseStartedAtMs;
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
writeMcpHttpError(res, resolveErrorStatusCode(error, 400), 'Invalid JSON body.');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const authScopes = auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [];
|
|
460
|
+
const requestId = String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID();
|
|
461
|
+
res.setHeader('X-Request-ID', requestId);
|
|
462
|
+
const mcpMethod = !Array.isArray(body) && body !== null && typeof body === 'object'
|
|
463
|
+
? String(body.method || '')
|
|
464
|
+
: '';
|
|
465
|
+
const initializeRequest = isInitializeRequest(body)
|
|
466
|
+
|| mcpMethod === 'initialize';
|
|
467
|
+
const modernProtocolRequest = await isModernMcpHttpRequest(req, body);
|
|
468
|
+
const writeToolNames = extractMcpWriteToolNames(body, resolveMcpToolPolicy);
|
|
469
|
+
const requestClass = initializeRequest
|
|
470
|
+
? 'initialize'
|
|
471
|
+
: mcpMethod === 'server/discover'
|
|
472
|
+
? 'discover'
|
|
473
|
+
: mcpMethod === 'tools/list'
|
|
474
|
+
? 'tools_list'
|
|
475
|
+
: writeToolNames.length > 0
|
|
476
|
+
? 'write'
|
|
477
|
+
: extractFirstMcpToolName(body)
|
|
478
|
+
? 'read'
|
|
479
|
+
: 'other';
|
|
480
|
+
capacityRequest.classify(requestClass);
|
|
481
|
+
const deniedToolName = findDeniedCloudMcpToolName(body, authScopes, resolveMcpToolPolicy);
|
|
482
|
+
if (deniedToolName) {
|
|
483
|
+
core.addMcpAuditLog({
|
|
484
|
+
workspaceId: auth.workspaceId,
|
|
485
|
+
actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
|
|
486
|
+
actorUserId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || 'anonymous',
|
|
487
|
+
tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
|
|
488
|
+
toolName: deniedToolName,
|
|
489
|
+
details: {
|
|
490
|
+
requestId,
|
|
491
|
+
ip: req.socket?.remoteAddress || null,
|
|
492
|
+
userAgent: requestHeader(req, 'user-agent', '') || null,
|
|
493
|
+
outcome: 'deny',
|
|
494
|
+
reasonCode: 'scope_denied',
|
|
495
|
+
scopes: authScopes
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
writeMcpHttpError(res, 403, 'Forbidden.');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const rateLimitStartedAtMs = Date.now();
|
|
502
|
+
const rateLimitAllowed = applyCloudMcpWriteRateLimit(req, res, auth, body);
|
|
503
|
+
rateLimitMs = Date.now() - rateLimitStartedAtMs;
|
|
504
|
+
if (!rateLimitAllowed)
|
|
505
|
+
return;
|
|
506
|
+
let runtime = null;
|
|
507
|
+
let runtimeLease = null;
|
|
508
|
+
let runtimeConstructionMs = 0;
|
|
509
|
+
let transportConnectMs = 0;
|
|
510
|
+
let requestHandlingMs = 0;
|
|
511
|
+
let cleanupMs = 0;
|
|
512
|
+
let outcome = 'success';
|
|
513
|
+
const toolTimingState = { value: null };
|
|
514
|
+
try {
|
|
515
|
+
const runtimeStartedAtMs = Date.now();
|
|
516
|
+
// Production intentionally isolates the MCP runtime per HTTP request. The runtime closes over
|
|
517
|
+
// workspace auth, AI identity, permissions, subscriptions, and mutable helper state;
|
|
518
|
+
// sharing it could leak state across tenants or callers. Keep the transport stateless
|
|
519
|
+
// too. The timing event below measures construction cost so caching is only revisited
|
|
520
|
+
// with evidence and must preserve these boundaries (including dynamic tool schemas). The optional
|
|
521
|
+
// lease seam exists only for transport conformance tests that must carry legacy bidirectional calls.
|
|
522
|
+
const runtimeOptions = {
|
|
523
|
+
projectRoot: core.getPaths().projectRoot,
|
|
524
|
+
tenantId: core.getTenantId(),
|
|
525
|
+
workspaceId: auth.workspaceId,
|
|
526
|
+
core,
|
|
527
|
+
serverName: 'taskforce-cloud',
|
|
528
|
+
logStartup: false,
|
|
529
|
+
runtimeMode: 'cloud',
|
|
530
|
+
toolProfile: auth.authenticatedViaToken?.toolProfile || auth.authenticatedViaOAuth?.toolProfile || 'default',
|
|
531
|
+
applicationBaseUrl: resolveCloudApplicationBaseUrl(),
|
|
532
|
+
auth: buildCloudMcpRuntimeAuth({
|
|
533
|
+
req,
|
|
534
|
+
auth,
|
|
535
|
+
requestHeader,
|
|
536
|
+
requestId,
|
|
537
|
+
authScopes,
|
|
538
|
+
}),
|
|
539
|
+
onToolCallTiming: (timing) => {
|
|
540
|
+
toolTimingState.value = timing;
|
|
541
|
+
if (!res.headersSent) {
|
|
542
|
+
if (capacityTraceEnabled) {
|
|
543
|
+
capacityRequest.setTraceHeaders(res, getRequestPerformanceDiagnostics());
|
|
544
|
+
}
|
|
545
|
+
setServerTimingHeader(res, [
|
|
546
|
+
{ name: 'auth', durationMs: authMs },
|
|
547
|
+
{ name: 'body-parse', durationMs: bodyParseMs },
|
|
548
|
+
{ name: 'rate-limit', durationMs: rateLimitMs },
|
|
549
|
+
{ name: 'runtime', durationMs: runtimeConstructionMs },
|
|
550
|
+
{ name: 'transport-connect', durationMs: transportConnectMs },
|
|
551
|
+
{ name: 'tool-envelope-prepare', durationMs: timing.preparationMs },
|
|
552
|
+
{ name: 'tool-envelope-validate', durationMs: timing.validationMs },
|
|
553
|
+
{ name: 'tool-handler', durationMs: timing.handlerMs },
|
|
554
|
+
...Object.entries(timing.subphases).map(([phase, durationMs]) => ({
|
|
555
|
+
name: `tool-${phase}`,
|
|
556
|
+
durationMs,
|
|
557
|
+
})),
|
|
558
|
+
]);
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
if (modernProtocolRequest) {
|
|
563
|
+
const requestHandlingStartedAtMs = Date.now();
|
|
564
|
+
await handleModernMcpHttpRequest({
|
|
565
|
+
req,
|
|
566
|
+
res,
|
|
567
|
+
parsedBody: body,
|
|
568
|
+
factory: ({ era }) => {
|
|
569
|
+
const constructionStartedAtMs = Date.now();
|
|
570
|
+
const modernRuntime = createStatelessTaskforceMcpServer({
|
|
571
|
+
...runtimeOptions,
|
|
572
|
+
protocolEra: era,
|
|
573
|
+
}, createMcpServer);
|
|
574
|
+
runtimeConstructionMs = Date.now() - constructionStartedAtMs;
|
|
575
|
+
if (!res.headersSent) {
|
|
576
|
+
setServerTimingHeader(res, [
|
|
577
|
+
{ name: 'auth', durationMs: authMs },
|
|
578
|
+
{ name: 'body-parse', durationMs: bodyParseMs },
|
|
579
|
+
{ name: 'rate-limit', durationMs: rateLimitMs },
|
|
580
|
+
{ name: 'runtime', durationMs: runtimeConstructionMs },
|
|
581
|
+
]);
|
|
582
|
+
}
|
|
583
|
+
return modernRuntime.server;
|
|
584
|
+
},
|
|
585
|
+
onerror: (error) => {
|
|
586
|
+
console.warn('[Taskforce MCP] Modern cloud request rejected:', {
|
|
587
|
+
requestId,
|
|
588
|
+
message: error.message,
|
|
589
|
+
});
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
requestHandlingMs = Date.now() - requestHandlingStartedAtMs;
|
|
593
|
+
recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
runtimeLease = acquireMcpRuntime
|
|
597
|
+
? await acquireMcpRuntime({ req, options: runtimeOptions })
|
|
598
|
+
: null;
|
|
599
|
+
runtime = runtimeLease?.runtime
|
|
600
|
+
|| createStatelessTaskforceMcpServer(runtimeOptions, createMcpServer);
|
|
601
|
+
runtimeConstructionMs = Date.now() - runtimeStartedAtMs;
|
|
602
|
+
const transport = runtimeLease?.transport || new NodeStreamableHTTPServerTransport({
|
|
603
|
+
sessionIdGenerator: undefined,
|
|
604
|
+
enableJsonResponse: true,
|
|
605
|
+
});
|
|
606
|
+
const transportStartedAtMs = Date.now();
|
|
607
|
+
if (!runtimeLease)
|
|
608
|
+
await runtime.server.connect(transport);
|
|
609
|
+
transportConnectMs = Date.now() - transportStartedAtMs;
|
|
610
|
+
// Set request-level phases before the SDK writes response headers. Tool calls replace this
|
|
611
|
+
// with the richer envelope/handler breakdown from onToolCallTiming above.
|
|
507
612
|
if (!res.headersSent) {
|
|
508
613
|
setServerTimingHeader(res, [
|
|
509
614
|
{ name: 'auth', durationMs: authMs },
|
|
@@ -511,155 +616,90 @@ export function makeHandleCloudMcpRequest(core, deps) {
|
|
|
511
616
|
{ name: 'rate-limit', durationMs: rateLimitMs },
|
|
512
617
|
{ name: 'runtime', durationMs: runtimeConstructionMs },
|
|
513
618
|
{ name: 'transport-connect', durationMs: transportConnectMs },
|
|
514
|
-
{ name: 'tool-envelope-prepare', durationMs: timing.preparationMs },
|
|
515
|
-
{ name: 'tool-envelope-validate', durationMs: timing.validationMs },
|
|
516
|
-
{ name: 'tool-handler', durationMs: timing.handlerMs },
|
|
517
|
-
...Object.entries(timing.subphases).map(([phase, durationMs]) => ({
|
|
518
|
-
name: `tool-${phase}`,
|
|
519
|
-
durationMs,
|
|
520
|
-
})),
|
|
521
619
|
]);
|
|
522
620
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
setServerTimingHeader(res, [
|
|
540
|
-
{ name: 'auth', durationMs: authMs },
|
|
541
|
-
{ name: 'body-parse', durationMs: bodyParseMs },
|
|
542
|
-
{ name: 'rate-limit', durationMs: rateLimitMs },
|
|
543
|
-
{ name: 'runtime', durationMs: runtimeConstructionMs },
|
|
544
|
-
]);
|
|
545
|
-
}
|
|
546
|
-
return modernRuntime.server;
|
|
547
|
-
},
|
|
548
|
-
onerror: (error) => {
|
|
549
|
-
console.warn('[Taskforce MCP] Modern cloud request rejected:', {
|
|
621
|
+
const requestHandlingStartedAtMs = Date.now();
|
|
622
|
+
await transport.handleRequest(req, res, body);
|
|
623
|
+
requestHandlingMs = Date.now() - requestHandlingStartedAtMs;
|
|
624
|
+
recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
outcome = 'failure';
|
|
628
|
+
console.error('[Taskforce MCP] Cloud transport request failed:', {
|
|
629
|
+
requestId,
|
|
630
|
+
kind: 'tool_transport_failure',
|
|
631
|
+
message: error instanceof Error ? error.message : String(error),
|
|
632
|
+
});
|
|
633
|
+
if (!res.headersSent) {
|
|
634
|
+
writeMcpHttpError(res, 500, 'MCP transport failure.', {
|
|
635
|
+
kind: 'tool_transport_failure',
|
|
636
|
+
retryable: true,
|
|
550
637
|
requestId,
|
|
551
|
-
message: error.message,
|
|
552
638
|
});
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
requestHandlingMs = Date.now() - requestHandlingStartedAtMs;
|
|
556
|
-
recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
runtimeLease = acquireMcpRuntime
|
|
560
|
-
? await acquireMcpRuntime({ req, options: runtimeOptions })
|
|
561
|
-
: null;
|
|
562
|
-
runtime = runtimeLease?.runtime
|
|
563
|
-
|| createStatelessTaskforceMcpServer(runtimeOptions, createMcpServer);
|
|
564
|
-
runtimeConstructionMs = Date.now() - runtimeStartedAtMs;
|
|
565
|
-
const transport = runtimeLease?.transport || new NodeStreamableHTTPServerTransport({
|
|
566
|
-
sessionIdGenerator: undefined,
|
|
567
|
-
enableJsonResponse: true,
|
|
568
|
-
});
|
|
569
|
-
const transportStartedAtMs = Date.now();
|
|
570
|
-
if (!runtimeLease)
|
|
571
|
-
await runtime.server.connect(transport);
|
|
572
|
-
transportConnectMs = Date.now() - transportStartedAtMs;
|
|
573
|
-
// Set request-level phases before the SDK writes response headers. Tool calls replace this
|
|
574
|
-
// with the richer envelope/handler breakdown from onToolCallTiming above.
|
|
575
|
-
if (!res.headersSent) {
|
|
576
|
-
setServerTimingHeader(res, [
|
|
577
|
-
{ name: 'auth', durationMs: authMs },
|
|
578
|
-
{ name: 'body-parse', durationMs: bodyParseMs },
|
|
579
|
-
{ name: 'rate-limit', durationMs: rateLimitMs },
|
|
580
|
-
{ name: 'runtime', durationMs: runtimeConstructionMs },
|
|
581
|
-
{ name: 'transport-connect', durationMs: transportConnectMs },
|
|
582
|
-
]);
|
|
583
|
-
}
|
|
584
|
-
if (!initializeRequest) {
|
|
585
|
-
// Stateless cloud sessions still require a valid MCP JSON-RPC request;
|
|
586
|
-
// let the SDK validate and respond rather than trying to track sessions server-side.
|
|
587
|
-
}
|
|
588
|
-
const requestHandlingStartedAtMs = Date.now();
|
|
589
|
-
await transport.handleRequest(req, res, body);
|
|
590
|
-
requestHandlingMs = Date.now() - requestHandlingStartedAtMs;
|
|
591
|
-
recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
|
|
592
|
-
}
|
|
593
|
-
catch (error) {
|
|
594
|
-
outcome = 'failure';
|
|
595
|
-
console.error('[Taskforce MCP] Cloud transport request failed:', {
|
|
596
|
-
requestId,
|
|
597
|
-
kind: 'tool_transport_failure',
|
|
598
|
-
message: error instanceof Error ? error.message : String(error),
|
|
599
|
-
});
|
|
600
|
-
if (!res.headersSent) {
|
|
601
|
-
writeMcpHttpError(res, 500, 'MCP transport failure.', {
|
|
602
|
-
kind: 'tool_transport_failure',
|
|
603
|
-
retryable: true,
|
|
604
|
-
requestId,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
finally {
|
|
609
|
-
if (runtimeLease) {
|
|
610
|
-
const cleanupStartedAtMs = Date.now();
|
|
611
|
-
try {
|
|
612
|
-
await runtimeLease.release();
|
|
613
|
-
}
|
|
614
|
-
catch (closeError) {
|
|
615
|
-
console.warn('[Taskforce MCP] Cloud test runtime lease cleanup failed:', closeError);
|
|
616
|
-
}
|
|
617
|
-
cleanupMs = Date.now() - cleanupStartedAtMs;
|
|
618
|
-
}
|
|
619
|
-
else if (runtime) {
|
|
620
|
-
const cleanupStartedAtMs = Date.now();
|
|
621
|
-
try {
|
|
622
|
-
await runtime.server.close();
|
|
639
|
+
}
|
|
623
640
|
}
|
|
624
|
-
|
|
625
|
-
|
|
641
|
+
finally {
|
|
642
|
+
if (runtimeLease) {
|
|
643
|
+
const cleanupStartedAtMs = Date.now();
|
|
644
|
+
try {
|
|
645
|
+
await runtimeLease.release();
|
|
646
|
+
}
|
|
647
|
+
catch (closeError) {
|
|
648
|
+
console.warn('[Taskforce MCP] Cloud test runtime lease cleanup failed:', closeError);
|
|
649
|
+
}
|
|
650
|
+
cleanupMs = Date.now() - cleanupStartedAtMs;
|
|
651
|
+
}
|
|
652
|
+
else if (runtime) {
|
|
653
|
+
const cleanupStartedAtMs = Date.now();
|
|
654
|
+
try {
|
|
655
|
+
await runtime.server.close();
|
|
656
|
+
}
|
|
657
|
+
catch (closeError) {
|
|
658
|
+
console.warn('[Taskforce MCP] Cloud transport cleanup failed:', closeError);
|
|
659
|
+
}
|
|
660
|
+
cleanupMs = Date.now() - cleanupStartedAtMs;
|
|
661
|
+
}
|
|
662
|
+
const totalDurationMs = Date.now() - requestStartedAtMs;
|
|
663
|
+
if (initializeRequest || mcpMethod === 'tools/list' || writeToolNames.length > 0 || totalDurationMs >= 250) {
|
|
664
|
+
const timingSnapshot = toolTimingState.value;
|
|
665
|
+
console.info(`[Taskforce MCP] ${JSON.stringify({
|
|
666
|
+
event: 'cloud_request_timing',
|
|
667
|
+
requestId,
|
|
668
|
+
requestKind: initializeRequest
|
|
669
|
+
? 'initialize'
|
|
670
|
+
: mcpMethod === 'tools/list'
|
|
671
|
+
? 'tools_list'
|
|
672
|
+
: (extractFirstMcpToolName(body) ? 'tool_call' : 'other'),
|
|
673
|
+
method: mcpMethod || null,
|
|
674
|
+
toolName: extractFirstMcpToolName(body),
|
|
675
|
+
workspaceId: auth.workspaceId,
|
|
676
|
+
databaseProvider: core.getDatabaseProvider(),
|
|
677
|
+
authMode: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
|
|
678
|
+
batchSize: Array.isArray(body) ? body.length : 1,
|
|
679
|
+
isWrite: writeToolNames.length > 0,
|
|
680
|
+
outcome,
|
|
681
|
+
httpStatus: res.statusCode,
|
|
682
|
+
responseBytes: timingSnapshot?.responseBytes ?? null,
|
|
683
|
+
totalDurationMs,
|
|
684
|
+
preRuntimeMs: Math.max(0, totalDurationMs - runtimeConstructionMs - transportConnectMs - requestHandlingMs - cleanupMs),
|
|
685
|
+
authMs,
|
|
686
|
+
bodyParseMs,
|
|
687
|
+
rateLimitMs,
|
|
688
|
+
runtimeConstructionMs,
|
|
689
|
+
transportConnectMs,
|
|
690
|
+
requestHandlingMs,
|
|
691
|
+
cleanupMs,
|
|
692
|
+
toolTiming: timingSnapshot,
|
|
693
|
+
})}`);
|
|
694
|
+
}
|
|
626
695
|
}
|
|
627
|
-
cleanupMs = Date.now() - cleanupStartedAtMs;
|
|
628
696
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
event: 'cloud_request_timing',
|
|
634
|
-
requestId,
|
|
635
|
-
requestKind: initializeRequest
|
|
636
|
-
? 'initialize'
|
|
637
|
-
: mcpMethod === 'tools/list'
|
|
638
|
-
? 'tools_list'
|
|
639
|
-
: (extractFirstMcpToolName(body) ? 'tool_call' : 'other'),
|
|
640
|
-
method: mcpMethod || null,
|
|
641
|
-
toolName: extractFirstMcpToolName(body),
|
|
642
|
-
workspaceId: auth.workspaceId,
|
|
643
|
-
databaseProvider: core.getDatabaseProvider(),
|
|
644
|
-
authMode: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
|
|
645
|
-
batchSize: Array.isArray(body) ? body.length : 1,
|
|
646
|
-
isWrite: writeToolNames.length > 0,
|
|
647
|
-
outcome,
|
|
648
|
-
httpStatus: res.statusCode,
|
|
649
|
-
responseBytes: timingSnapshot?.responseBytes ?? null,
|
|
650
|
-
totalDurationMs,
|
|
651
|
-
preRuntimeMs: Math.max(0, totalDurationMs - runtimeConstructionMs - transportConnectMs - requestHandlingMs - cleanupMs),
|
|
652
|
-
authMs,
|
|
653
|
-
bodyParseMs,
|
|
654
|
-
rateLimitMs,
|
|
655
|
-
runtimeConstructionMs,
|
|
656
|
-
transportConnectMs,
|
|
657
|
-
requestHandlingMs,
|
|
658
|
-
cleanupMs,
|
|
659
|
-
toolTiming: timingSnapshot,
|
|
660
|
-
})}`);
|
|
697
|
+
finally {
|
|
698
|
+
settleCapacity(Boolean(req.destroyed && !res.writableEnded));
|
|
699
|
+
res.off('finish', onFinish);
|
|
700
|
+
res.off('close', onClose);
|
|
661
701
|
}
|
|
662
|
-
}
|
|
702
|
+
});
|
|
663
703
|
};
|
|
664
704
|
}
|
|
665
705
|
// === SECTION: Request header helpers ===
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { StandaloneServerOptions, TaskforceStandaloneServer } from '../server/index.js';
|
|
2
2
|
import { inspectLocalServiceDescriptor } from '../server/localServiceLease.js';
|
|
3
3
|
import { acquireLocalServiceOwnershipForRuntime } from '../server/localServiceOwnershipBootstrap.js';
|
|
4
4
|
import { type SqliteWalHolder } from '../storage/sqliteWalHealth.js';
|
|
@@ -18,7 +18,7 @@ export interface LocalServiceCliOptions {
|
|
|
18
18
|
executablePath: string;
|
|
19
19
|
cliEntryPath: string;
|
|
20
20
|
execArgv?: readonly string[];
|
|
21
|
-
createServer?:
|
|
21
|
+
createServer?: (options: StandaloneServerOptions) => TaskforceStandaloneServer;
|
|
22
22
|
acquireOwnership?: typeof acquireLocalServiceOwnershipForRuntime;
|
|
23
23
|
inspectDescriptor?: typeof inspectLocalServiceDescriptor;
|
|
24
24
|
spawnDetached?: (executablePath: string, args: readonly string[], env: NodeJS.ProcessEnv) => void;
|
|
@@ -2,7 +2,6 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { applyLocalServiceCloudTarget, localServiceCloudTargetsEqual, parseLocalServiceCloudTarget, resolveLocalServiceCloudTarget, } from '../config/localServiceCloudTarget.js';
|
|
4
4
|
import { localServiceRuntimeContractsEqual, parseLocalServiceRuntimeContract, resolveLocalServiceRuntimeContract, } from '../config/localServiceRuntimeContract.js';
|
|
5
|
-
import { createStandaloneServer } from '../server/index.js';
|
|
6
5
|
import { inspectLocalServiceDescriptor, redactLocalServiceDescriptor, } from '../server/localServiceLease.js';
|
|
7
6
|
import { acquireLocalServiceOwnershipForRuntime, } from '../server/localServiceOwnershipBootstrap.js';
|
|
8
7
|
import { discoverOrStartLocalService, LOCAL_SERVICE_API_BASE_PATH, LOCAL_SERVICE_PROTOCOL_VERSION, resolveLocalServiceLeaseInput, spawnDetachedLocalService, waitForLocalServiceDescriptor, } from './localServiceClientLifecycle.js';
|
|
@@ -126,7 +125,9 @@ async function runForeground(command, options) {
|
|
|
126
125
|
});
|
|
127
126
|
};
|
|
128
127
|
try {
|
|
129
|
-
|
|
128
|
+
const createServer = options.createServer
|
|
129
|
+
|| (await import('../server/index.js')).createStandaloneServer;
|
|
130
|
+
server = createServer({
|
|
130
131
|
projectRoot: command.projectRoot,
|
|
131
132
|
port: 0,
|
|
132
133
|
localSyncProcessLock: ownership.syncProcessLock || undefined,
|
|
@@ -370,7 +371,7 @@ export async function runLocalServiceCli(argv, input) {
|
|
|
370
371
|
executablePath: input.executablePath,
|
|
371
372
|
cliEntryPath: input.cliEntryPath,
|
|
372
373
|
execArgv: input.execArgv || [],
|
|
373
|
-
createServer: input.createServer
|
|
374
|
+
createServer: input.createServer,
|
|
374
375
|
acquireOwnership: input.acquireOwnership || acquireLocalServiceOwnershipForRuntime,
|
|
375
376
|
inspectDescriptor: input.inspectDescriptor || inspectLocalServiceDescriptor,
|
|
376
377
|
spawnDetached: input.spawnDetached || spawnDetachedLocalService,
|
|
@@ -17,6 +17,17 @@ export interface ExecutorPhaseAOidcKey {
|
|
|
17
17
|
lastIssuedAt: string | null;
|
|
18
18
|
publishUntil: string | null;
|
|
19
19
|
}
|
|
20
|
+
export interface ExecutorPhaseAOidcVerificationKey {
|
|
21
|
+
keyId: string;
|
|
22
|
+
publicKey: KeyObject;
|
|
23
|
+
status: 'active' | 'retiring' | 'disabled';
|
|
24
|
+
validFrom: string;
|
|
25
|
+
validUntil: string;
|
|
26
|
+
}
|
|
27
|
+
declare const verifiedWorkloadJwtClaimsBrand: unique symbol;
|
|
28
|
+
export type VerifiedExecutorPhaseAWorkloadJwtClaims = ExecutorPhaseAWorkloadJwtClaims & {
|
|
29
|
+
readonly [verifiedWorkloadJwtClaimsBrand]: true;
|
|
30
|
+
};
|
|
20
31
|
export interface ExecutorPhaseAWorkloadJwtClaims {
|
|
21
32
|
iss: string;
|
|
22
33
|
aud: string;
|
|
@@ -103,6 +114,8 @@ export declare function buildExecutorPhaseAOidcDiscovery(issuer: string): {
|
|
|
103
114
|
};
|
|
104
115
|
export declare function buildExecutorPhaseAWorkloadJwtClaims(bundle: VerifiedExecutorPhaseAWorkloadGrantBundle, issuer: string, audience: string, tokenId: string, now: Date): ExecutorPhaseAWorkloadJwtClaims;
|
|
105
116
|
export declare function signExecutorPhaseAWorkloadJwt(claims: ExecutorPhaseAWorkloadJwtClaims, signer: ExecutorPhaseARs256Signer): Promise<string>;
|
|
117
|
+
export declare function verifyExecutorPhaseAWorkloadJwt(jwt: string, bundle: VerifiedExecutorPhaseAWorkloadGrantBundle, keys: readonly ExecutorPhaseAOidcVerificationKey[], now?: Date): VerifiedExecutorPhaseAWorkloadJwtClaims;
|
|
118
|
+
export declare function assertVerifiedExecutorPhaseAWorkloadJwtClaims(claims: VerifiedExecutorPhaseAWorkloadJwtClaims): void;
|
|
106
119
|
export declare class ExecutorPhaseAIssuerCore {
|
|
107
120
|
private readonly options;
|
|
108
121
|
constructor(options: {
|