@agent-native/core 0.222.0-nightly-20261029004912 → 0.222.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/agent/production-agent.js +22 -9
- package/dist/agent/run-store.d.ts +9 -1
- package/dist/agent/run-store.js +26 -3
- package/dist/cli/code-agent-mcp-config.js +6 -1
- package/dist/client/chat/agentkit-agent-native.js +1 -2
- package/dist/client/chat/agentkit-protocol.js +14 -0
- package/dist/client/chat/runtime.d.ts +5 -1
- package/dist/client/chat/runtime.js +23 -1
- package/dist/client/sse-event-processor.d.ts +10 -0
- package/dist/client/sse-event-processor.js +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +1 -1
- package/dist/connections/catalog.js +4 -11
- package/dist/mcp-client/config.d.ts +6 -0
- package/dist/mcp-client/manager.js +25 -2
- package/dist/mcp-client/routes.js +2 -0
- package/dist/mcp-client/workspace-connection-servers.d.ts +30 -0
- package/dist/mcp-client/workspace-connection-servers.js +119 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.js +2 -0
- package/dist/provider-api/agent-request-policy.d.ts +13 -0
- package/dist/provider-api/agent-request-policy.js +31 -0
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/corpus-jobs.js +7 -1
- package/dist/provider-api/index.d.ts +4 -1
- package/dist/provider-api/index.js +4 -1
- package/dist/provider-api/read-only-requests.js +11 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +4 -4
- package/dist/server/action-discovery.js +6 -0
- package/dist/server/agent-chat/process-run-failure.js +58 -2
- package/dist/server/agent-engine-api-key-route.d.ts +3 -3
- package/dist/server/workspace-provider-oauth.d.ts +1 -0
- package/dist/server/workspace-provider-oauth.js +10 -2
- package/dist/templates/chat/.agents/skills/actions/references/provider-apis.md +13 -0
- package/dist/templates/default/.agents/skills/actions/references/provider-apis.md +13 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +10 -4
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +287 -0
- package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +266 -92
- package/dist/templates/headless/.agents/skills/actions/references/provider-apis.md +13 -0
- package/dist/templates/workspace-core/.agents/skills/actions/references/provider-apis.md +13 -0
- package/dist/vite/action-types-plugin.js +4 -0
- package/dist/workspace-connections/actions/list-workspace-connection-scopes.d.ts +16 -0
- package/dist/workspace-connections/actions/list-workspace-connection-scopes.js +62 -0
- package/package.json +5 -4
- package/src/templates/chat/.agents/skills/actions/references/provider-apis.md +13 -0
- package/src/templates/default/.agents/skills/actions/references/provider-apis.md +13 -0
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +10 -4
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +287 -0
- package/src/templates/factory/.agents/skills/review-prs/SKILL.md +266 -92
- package/src/templates/headless/.agents/skills/actions/references/provider-apis.md +13 -0
- package/src/templates/workspace-core/.agents/skills/actions/references/provider-apis.md +13 -0
|
@@ -5417,6 +5417,7 @@ export async function runAgentLoop(opts) {
|
|
|
5417
5417
|
const blockedA2ATargets = new Map();
|
|
5418
5418
|
let finalGuardRetries = 0;
|
|
5419
5419
|
let emptyFinalResponseRetries = 0;
|
|
5420
|
+
let followUpOnlyTurns = 0;
|
|
5420
5421
|
let preserveReasoningEffortForEmptyResponseRetries = false;
|
|
5421
5422
|
let consecutiveMaxTokensContinuations = 0;
|
|
5422
5423
|
let truncatedToolCallRetries = 0;
|
|
@@ -6201,14 +6202,20 @@ export async function runAgentLoop(opts) {
|
|
|
6201
6202
|
else {
|
|
6202
6203
|
consecutiveMaxTokensContinuations = 0;
|
|
6203
6204
|
}
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6205
|
+
// Only a follow-up call with no reply asks again. A reply sent beside
|
|
6206
|
+
// its follow-ups is the final answer: clearing it to ask again leaves
|
|
6207
|
+
// the model recapping a reply the user no longer sees.
|
|
6208
|
+
// Past the cap, an empty turn takes the bounded empty-response retries
|
|
6209
|
+
// below instead of asking again until the loop's iteration limit.
|
|
6209
6210
|
if (followUpResults.length > 0 &&
|
|
6210
6211
|
hasEmptyFinalResponse &&
|
|
6211
|
-
terminalStopReason !== "max_tokens"
|
|
6212
|
+
terminalStopReason !== "max_tokens" &&
|
|
6213
|
+
followUpOnlyTurns < EMPTY_FINAL_RESPONSE_RETRY_LIMIT) {
|
|
6214
|
+
followUpOnlyTurns += 1;
|
|
6215
|
+
// Whitespace the model streamed before its follow-ups is not a reply,
|
|
6216
|
+
// and the next turn's answer would land after it.
|
|
6217
|
+
if (streamedAssistantText)
|
|
6218
|
+
await send({ type: "clear" });
|
|
6212
6219
|
flushUnstreamedAssistantText();
|
|
6213
6220
|
continue;
|
|
6214
6221
|
}
|
|
@@ -6306,7 +6313,7 @@ export async function runAgentLoop(opts) {
|
|
|
6306
6313
|
flushUnstreamedAssistantText();
|
|
6307
6314
|
if (followUpRunId &&
|
|
6308
6315
|
completedFollowUpSuggestions === undefined &&
|
|
6309
|
-
terminalStopReason === "end_turn") {
|
|
6316
|
+
(terminalStopReason === "end_turn" || followUpResults.length > 0)) {
|
|
6310
6317
|
completingFollowUpSuggestions = true;
|
|
6311
6318
|
followUpStartedAt = Date.now();
|
|
6312
6319
|
followUpCompletionContext = buildFollowUpCompletionMessages({
|
|
@@ -6328,6 +6335,7 @@ export async function runAgentLoop(opts) {
|
|
|
6328
6335
|
}
|
|
6329
6336
|
finalGuardRetries = 0;
|
|
6330
6337
|
emptyFinalResponseRetries = 0;
|
|
6338
|
+
followUpOnlyTurns = 0;
|
|
6331
6339
|
preserveReasoningEffortForEmptyResponseRetries = false;
|
|
6332
6340
|
consecutiveMaxTokensContinuations = 0;
|
|
6333
6341
|
const toolCallTruncatedByOutputCap = terminalStopReason === "max_tokens";
|
|
@@ -9374,6 +9382,8 @@ export async function chainServerDrivenContinuation(opts) {
|
|
|
9374
9382
|
// Approval selectors apply only to the request that consumes their one-shot grants.
|
|
9375
9383
|
delete continuationBody.approvedToolCalls;
|
|
9376
9384
|
delete continuationBody.approvedToolCallAskIds;
|
|
9385
|
+
// The initial admission consumed this one-time queued-message claim.
|
|
9386
|
+
delete continuationBody.queuedMessageClaimId;
|
|
9377
9387
|
delete continuationBody[AGENT_CHAT_BACKGROUND_RUN_FIELD];
|
|
9378
9388
|
const markRunsAbortedOrThrow = async (...runIds) => {
|
|
9379
9389
|
const results = await Promise.allSettled(runIds.map(async (targetRunId) => {
|
|
@@ -11528,12 +11538,15 @@ export function createProductionAgentHandler(options) {
|
|
|
11528
11538
|
let durableDispatchPayload;
|
|
11529
11539
|
if (dispatchToBackground) {
|
|
11530
11540
|
try {
|
|
11531
|
-
|
|
11541
|
+
const dispatchBody = {
|
|
11532
11542
|
...body,
|
|
11533
11543
|
...(Array.isArray(attachments) || requestAttachments.length > 0
|
|
11534
11544
|
? { attachments: requestAttachments }
|
|
11535
11545
|
: {}),
|
|
11536
|
-
}
|
|
11546
|
+
};
|
|
11547
|
+
// This one-time claim authorizes only the request that consumes it.
|
|
11548
|
+
delete dispatchBody.queuedMessageClaimId;
|
|
11549
|
+
durableDispatchPayload = serializeDurableDispatchPayload(dispatchBody);
|
|
11537
11550
|
}
|
|
11538
11551
|
catch (error) {
|
|
11539
11552
|
if (!(error instanceof DurableAttachmentReferenceRequiredError)) {
|
|
@@ -317,7 +317,15 @@ export declare const RUN_DIAG_STAGE: {
|
|
|
317
317
|
readonly staleRunReaped: "stale_run_reaped";
|
|
318
318
|
};
|
|
319
319
|
export type RunDiagStage = (typeof RUN_DIAG_STAGE)[keyof typeof RUN_DIAG_STAGE];
|
|
320
|
-
export
|
|
320
|
+
export interface RouteFailureDiagnosticEvidence {
|
|
321
|
+
format: "route_failure_v1";
|
|
322
|
+
errorMetadata?: "unreadable";
|
|
323
|
+
errorName?: string;
|
|
324
|
+
errorCode?: string;
|
|
325
|
+
httpStatus?: string;
|
|
326
|
+
causeName?: string;
|
|
327
|
+
}
|
|
328
|
+
export declare function recordRunDiagnostic(runId: string, stage: RunDiagStage, detail?: string, evidence?: RouteFailureDiagnosticEvidence): Promise<void>;
|
|
321
329
|
export declare function updateRunHeartbeat(runId: string): Promise<void>;
|
|
322
330
|
export declare function bumpRunProgress(runId: string): Promise<boolean>;
|
|
323
331
|
/**
|
package/dist/agent/run-store.js
CHANGED
|
@@ -2058,16 +2058,39 @@ export const RUN_DIAG_STAGE = {
|
|
|
2058
2058
|
runBoundaryReached: "run_boundary_reached",
|
|
2059
2059
|
staleRunReaped: "stale_run_reaped",
|
|
2060
2060
|
};
|
|
2061
|
-
export async function recordRunDiagnostic(runId, stage, detail) {
|
|
2061
|
+
export async function recordRunDiagnostic(runId, stage, detail, evidence) {
|
|
2062
2062
|
if (!runId)
|
|
2063
2063
|
return;
|
|
2064
2064
|
try {
|
|
2065
2065
|
await ensureRunTables();
|
|
2066
|
-
|
|
2066
|
+
// raw-sql-ok: diagnostic JSON is passed only as a bound SQL value.
|
|
2067
|
+
const payloadFields = {
|
|
2067
2068
|
stage,
|
|
2068
2069
|
...(detail ? { detail: detail.slice(0, 1500) } : {}),
|
|
2070
|
+
...(stage === RUN_DIAG_STAGE.routeThrew && evidence ? { evidence } : {}),
|
|
2069
2071
|
at: Date.now(),
|
|
2070
|
-
}
|
|
2072
|
+
};
|
|
2073
|
+
// raw-sql-ok: diagnostic JSON is passed only as a bound SQL value.
|
|
2074
|
+
let payload = JSON.stringify(payloadFields);
|
|
2075
|
+
if (payload.length > 2000 && typeof payloadFields.detail === "string") {
|
|
2076
|
+
const fullDetail = payloadFields.detail;
|
|
2077
|
+
let low = 0;
|
|
2078
|
+
let high = fullDetail.length;
|
|
2079
|
+
while (low < high) {
|
|
2080
|
+
const middle = Math.ceil((low + high) / 2);
|
|
2081
|
+
payloadFields.detail = fullDetail.slice(0, middle);
|
|
2082
|
+
if (JSON.stringify(payloadFields).length <= 2000)
|
|
2083
|
+
low = middle;
|
|
2084
|
+
else
|
|
2085
|
+
high = middle - 1;
|
|
2086
|
+
}
|
|
2087
|
+
if (low === 0)
|
|
2088
|
+
delete payloadFields.detail;
|
|
2089
|
+
else
|
|
2090
|
+
payloadFields.detail = fullDetail.slice(0, low);
|
|
2091
|
+
// raw-sql-ok: diagnostic JSON is passed only as a bound SQL value.
|
|
2092
|
+
payload = JSON.stringify(payloadFields);
|
|
2093
|
+
}
|
|
2071
2094
|
const isWorkerStage = stage === RUN_DIAG_STAGE.workerSetupStep ||
|
|
2072
2095
|
stage === RUN_DIAG_STAGE.workerStarted;
|
|
2073
2096
|
if (isWorkerStage) {
|
|
@@ -132,7 +132,12 @@ function codeAgentHttpMcpServers(config, environment) {
|
|
|
132
132
|
// its credential headers) silently replace another. Reject that instead.
|
|
133
133
|
const idsByKey = new Map();
|
|
134
134
|
return Object.entries(servers).flatMap(([serverId, server]) => {
|
|
135
|
-
|
|
135
|
+
// A workspace-connection server's bearer is resolved per request inside
|
|
136
|
+
// this process, so a CLI copy would have no credentials.
|
|
137
|
+
if (server.type !== "http" ||
|
|
138
|
+
!server.url ||
|
|
139
|
+
server.configurationError ||
|
|
140
|
+
server.workspaceConnection)
|
|
136
141
|
return [];
|
|
137
142
|
const key = codexConfigKey(serverId);
|
|
138
143
|
const existing = idsByKey.get(key);
|
|
@@ -978,8 +978,6 @@ function reconcileDurableMessages(messages, durable, events, runs, toolCalls) {
|
|
|
978
978
|
snapshotReply.id !== durableReply.id &&
|
|
979
979
|
eventMessageIds?.size === 1 &&
|
|
980
980
|
eventMessageIds.has(snapshotReply.id) &&
|
|
981
|
-
snapshotReply.parts.length > 0 &&
|
|
982
|
-
snapshotReply.parts.every((part) => part.type === "text") &&
|
|
983
981
|
snapshotText.length > 0 &&
|
|
984
982
|
durableText.length > 0 &&
|
|
985
983
|
snapshotText !== durableText &&
|
|
@@ -4221,6 +4219,7 @@ export function createAgentNativeAgentKitTransport(options = {}) {
|
|
|
4221
4219
|
switch (sourceEvent.type) {
|
|
4222
4220
|
case "message.created":
|
|
4223
4221
|
case "message.completed":
|
|
4222
|
+
case "message.replaced":
|
|
4224
4223
|
if (sourceEvent.message.role === "assistant") {
|
|
4225
4224
|
const messageId = foldedMessageIdFor(sourceEvent.message.id);
|
|
4226
4225
|
if (messageId && messageId !== sourceEvent.message.id) {
|
|
@@ -2026,6 +2026,20 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
|
|
|
2026
2026
|
},
|
|
2027
2027
|
...attachPendingWidgets(event.messageId),
|
|
2028
2028
|
];
|
|
2029
|
+
case "message-replace":
|
|
2030
|
+
run.activeMessageId = event.message.id;
|
|
2031
|
+
run.activeMessageRole = event.message.role;
|
|
2032
|
+
run.activeMessageCompleted = false;
|
|
2033
|
+
return [
|
|
2034
|
+
{
|
|
2035
|
+
type: "message.replaced",
|
|
2036
|
+
...(base.occurredAt === undefined
|
|
2037
|
+
? {}
|
|
2038
|
+
: { occurredAt: base.occurredAt }),
|
|
2039
|
+
...(base.metadata === undefined ? {} : { metadata: base.metadata }),
|
|
2040
|
+
message: runtimeMessageToProtocolMessage(event.message, textFormat),
|
|
2041
|
+
},
|
|
2042
|
+
];
|
|
2029
2043
|
case "message-done":
|
|
2030
2044
|
run.activeMessageId = event.message.id;
|
|
2031
2045
|
run.activeMessageRole = event.message.role;
|
|
@@ -307,6 +307,10 @@ export interface AgentChatRuntimeMessageDeltaEvent extends AgentChatRuntimeEvent
|
|
|
307
307
|
export interface AgentChatRuntimeMessageDoneEvent extends AgentChatRuntimeEventBase<"message-done"> {
|
|
308
308
|
readonly message: AgentChatRuntimeMessage;
|
|
309
309
|
}
|
|
310
|
+
/** Replaces a streaming message's content without finishing it. */
|
|
311
|
+
export interface AgentChatRuntimeMessageReplaceEvent extends AgentChatRuntimeEventBase<"message-replace"> {
|
|
312
|
+
readonly message: AgentChatRuntimeMessage;
|
|
313
|
+
}
|
|
310
314
|
export interface AgentChatRuntimeToolStartEvent extends AgentChatRuntimeEventBase<"tool-start"> {
|
|
311
315
|
readonly toolCall: AgentChatRuntimeToolCall;
|
|
312
316
|
}
|
|
@@ -546,7 +550,7 @@ export interface AgentChatRuntimeDoneEvent extends AgentChatRuntimeEventBase<"do
|
|
|
546
550
|
export interface AgentChatRuntimeCustomEvent extends AgentChatRuntimeEventBase {
|
|
547
551
|
readonly [key: string]: unknown;
|
|
548
552
|
}
|
|
549
|
-
export type AgentChatRuntimeKnownEvent = AgentChatRuntimeMessageStartEvent | AgentChatRuntimeMessageDeltaEvent | AgentChatRuntimeMessageDoneEvent | AgentChatRuntimeToolStartEvent | AgentChatRuntimeToolDeltaEvent | AgentChatRuntimeToolDoneEvent | AgentChatRuntimeApprovalRequestEvent | AgentChatRuntimeApprovalResolvedEvent | AgentChatRuntimeConnectionRequestEvent | AgentChatRuntimeStatusEvent | AgentChatRuntimeSuggestionsEvent | AgentChatRuntimeAnnotationEvent | AgentChatRuntimeWidgetEvent | AgentChatRuntimeParticipantEvent | AgentChatRuntimeInteractionEvent | AgentChatRuntimeActivityEvent | AgentChatRuntimeTaskEvent | AgentChatRuntimeTaskGroupEvent | AgentChatRuntimeUploadProgressEvent | AgentChatRuntimeClientEffectEvent | AgentChatRuntimeExtensionEvent | AgentChatRuntimeArtifactEvent | AgentChatRuntimeFileEvent | AgentChatRuntimeUsageEvent | AgentChatRuntimeErrorEvent | AgentChatRuntimeContinuationEvent | AgentChatRuntimeDoneEvent;
|
|
553
|
+
export type AgentChatRuntimeKnownEvent = AgentChatRuntimeMessageStartEvent | AgentChatRuntimeMessageDeltaEvent | AgentChatRuntimeMessageDoneEvent | AgentChatRuntimeMessageReplaceEvent | AgentChatRuntimeToolStartEvent | AgentChatRuntimeToolDeltaEvent | AgentChatRuntimeToolDoneEvent | AgentChatRuntimeApprovalRequestEvent | AgentChatRuntimeApprovalResolvedEvent | AgentChatRuntimeConnectionRequestEvent | AgentChatRuntimeStatusEvent | AgentChatRuntimeSuggestionsEvent | AgentChatRuntimeAnnotationEvent | AgentChatRuntimeWidgetEvent | AgentChatRuntimeParticipantEvent | AgentChatRuntimeInteractionEvent | AgentChatRuntimeActivityEvent | AgentChatRuntimeTaskEvent | AgentChatRuntimeTaskGroupEvent | AgentChatRuntimeUploadProgressEvent | AgentChatRuntimeClientEffectEvent | AgentChatRuntimeExtensionEvent | AgentChatRuntimeArtifactEvent | AgentChatRuntimeFileEvent | AgentChatRuntimeUsageEvent | AgentChatRuntimeErrorEvent | AgentChatRuntimeContinuationEvent | AgentChatRuntimeDoneEvent;
|
|
550
554
|
export type AgentChatRuntimeEvent<TCustomEvent extends AgentChatRuntimeCustomEvent = never> = AgentChatRuntimeKnownEvent | TCustomEvent;
|
|
551
555
|
export interface AgentChatRuntimeTurn<TEvent extends AgentChatRuntimeEventBase = AgentChatRuntimeKnownEvent> {
|
|
552
556
|
readonly id?: AgentChatRuntimeTurnId;
|
|
@@ -6,7 +6,7 @@ import { agentEngineStatusUrlForChatApi, requireAgentEngineConfiguredForDispatch
|
|
|
6
6
|
import { getOrCreateAnalyticsSessionId } from "../analytics-session.js";
|
|
7
7
|
import { agentChatStreamingUrl, agentNativePath } from "../api-path.js";
|
|
8
8
|
import { CHAT_REQUEST_TOO_LARGE_MESSAGE } from "../error-format.js";
|
|
9
|
-
import { appendMissingFinalResponseWarning, findApprovalToolCallIndex, } from "../sse-event-processor.js";
|
|
9
|
+
import { appendMissingFinalResponseWarning, clearAssistantDraftContent, findApprovalToolCallIndex, } from "../sse-event-processor.js";
|
|
10
10
|
import { runOutcomeForCode } from "./run-outcome.js";
|
|
11
11
|
import { acceptedPromptForMetadata, DEFAULT_TURN_SURFACE, } from "./turn-submitted.js";
|
|
12
12
|
export const AGENT_NATIVE_RUN_RESUME_STATE_METADATA_KEY = "agentNativeRunResumeState";
|
|
@@ -2417,6 +2417,28 @@ function mapAgentNativeEvent(raw, input) {
|
|
|
2417
2417
|
if (ev.type === "auto_continue") {
|
|
2418
2418
|
return [{ type: "continuation", ...base }];
|
|
2419
2419
|
}
|
|
2420
|
+
// Skipping this leaves the saved reply as a suffix of the live one, and
|
|
2421
|
+
// reload renders it as a second reply.
|
|
2422
|
+
if (ev.type === "clear") {
|
|
2423
|
+
clearAssistantDraftContent(input.message.content);
|
|
2424
|
+
if (!input.message.started)
|
|
2425
|
+
return [];
|
|
2426
|
+
const content = input.message.content
|
|
2427
|
+
.filter((part) => part.type === "text" || part.type === "reasoning")
|
|
2428
|
+
.map((part) => ({ ...part }));
|
|
2429
|
+
// Text the retry writes after a kept tool call is a new paragraph. The
|
|
2430
|
+
// replacement carries no tool calls, so an empty part marks that break.
|
|
2431
|
+
if (input.message.content.at(-1)?.type === "tool-call") {
|
|
2432
|
+
content.push({ type: "text", text: "" });
|
|
2433
|
+
}
|
|
2434
|
+
return [
|
|
2435
|
+
{
|
|
2436
|
+
type: "message-replace",
|
|
2437
|
+
...base,
|
|
2438
|
+
message: { id: input.messageId, role: "assistant", content },
|
|
2439
|
+
},
|
|
2440
|
+
];
|
|
2441
|
+
}
|
|
2420
2442
|
if (ev.type === "text" || ev.type === "thinking" || ev.type === "reasoning") {
|
|
2421
2443
|
const text = ev.text ?? "";
|
|
2422
2444
|
const type = ev.type === "text" ? "text" : "reasoning";
|
|
@@ -354,6 +354,16 @@ export declare function processEvent(ev: SSEEvent, content: ContentPart[], toolC
|
|
|
354
354
|
errorInfo?: AgentAutoContinueErrorInfo;
|
|
355
355
|
};
|
|
356
356
|
};
|
|
357
|
+
/**
|
|
358
|
+
* Drop the draft the server is about to re-emit — the narration since the last
|
|
359
|
+
* completed tool, not the whole turn. A `clear` arrives on a final-answer-guard
|
|
360
|
+
* retry or a continuation, both of which resume AFTER the last completed tool
|
|
361
|
+
* call, so anything before that boundary is settled multi-step narration the
|
|
362
|
+
* retry will never re-send. Wiping it read to users as "it deleted its reply
|
|
363
|
+
* and started over", and `thread-data-builder` replays this same scoping on
|
|
364
|
+
* rebuild, so the loss survived a reload.
|
|
365
|
+
*/
|
|
366
|
+
export declare function clearAssistantDraftContent(content: ContentPart[]): void;
|
|
357
367
|
/**
|
|
358
368
|
* Read and process SSE events from a ReadableStream response body.
|
|
359
369
|
* Yields Core-owned stream results for each meaningful event.
|
|
@@ -1891,7 +1891,7 @@ export function processEvent(ev, content, toolCallCounter, tabId, state, context
|
|
|
1891
1891
|
* and started over", and `thread-data-builder` replays this same scoping on
|
|
1892
1892
|
* rebuild, so the loss survived a reload.
|
|
1893
1893
|
*/
|
|
1894
|
-
function clearAssistantDraftContent(content) {
|
|
1894
|
+
export function clearAssistantDraftContent(content) {
|
|
1895
1895
|
for (let index = content.length - 1; index >= 0; index--) {
|
|
1896
1896
|
const part = content[index];
|
|
1897
1897
|
if (!part)
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
|
|
|
25
25
|
ok: boolean;
|
|
26
26
|
}>>;
|
|
27
27
|
export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
|
-
text?: undefined;
|
|
29
28
|
ok?: undefined;
|
|
29
|
+
text?: undefined;
|
|
30
30
|
error: string;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
@@ -13,9 +13,9 @@ export declare const postCollabPatch: import("h3").EventHandlerWithFetch<import(
|
|
|
13
13
|
ok: boolean;
|
|
14
14
|
}>>;
|
|
15
15
|
export declare const getCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
+
data?: undefined;
|
|
16
17
|
error: string;
|
|
17
18
|
docId?: undefined;
|
|
18
|
-
data?: undefined;
|
|
19
19
|
} | {
|
|
20
20
|
error?: undefined;
|
|
21
21
|
docId: string;
|
|
@@ -280,7 +280,7 @@ export declare const WORKSPACE_CONNECTION_PROVIDERS: readonly [{
|
|
|
280
280
|
readonly authorizationUrl: "https://app.hubspot.com/oauth/authorize";
|
|
281
281
|
readonly tokenUrl: "https://api.hubapi.com/oauth/v3/token";
|
|
282
282
|
readonly refreshUrl: "https://api.hubapi.com/oauth/v3/token";
|
|
283
|
-
readonly scopes: readonly [
|
|
283
|
+
readonly scopes: readonly [];
|
|
284
284
|
};
|
|
285
285
|
readonly capabilities: readonly ["search", "import", "crm"];
|
|
286
286
|
readonly recommendedTemplateUses: readonly ["analytics", "brain", "crm", "mail", "dispatch"];
|
|
@@ -245,17 +245,10 @@ export const WORKSPACE_CONNECTION_PROVIDERS = [
|
|
|
245
245
|
authorizationUrl: "https://app.hubspot.com/oauth/authorize",
|
|
246
246
|
tokenUrl: "https://api.hubapi.com/oauth/v3/token",
|
|
247
247
|
refreshUrl: "https://api.hubapi.com/oauth/v3/token",
|
|
248
|
-
scopes
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
"crm.objects.deals.read",
|
|
253
|
-
"crm.objects.tickets.read",
|
|
254
|
-
"crm.schemas.contacts.read",
|
|
255
|
-
"crm.schemas.companies.read",
|
|
256
|
-
"crm.schemas.deals.read",
|
|
257
|
-
"crm.schemas.tickets.read",
|
|
258
|
-
],
|
|
248
|
+
// HubSpot grants the scopes configured on the HubSpot app itself, so the
|
|
249
|
+
// app's config is the source of truth. Sending no scope list keeps the
|
|
250
|
+
// two from drifting; the granted scopes are recorded from the token.
|
|
251
|
+
scopes: [],
|
|
259
252
|
},
|
|
260
253
|
capabilities: ["search", "import", "crm"],
|
|
261
254
|
recommendedTemplateUses: ["analytics", "brain", "crm", "mail", "dispatch"],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { McpWorkspaceConnectionAuth } from "./workspace-connection-servers.js";
|
|
1
2
|
export interface McpStdioServerConfig {
|
|
2
3
|
type?: "stdio";
|
|
3
4
|
command: string;
|
|
@@ -21,6 +22,11 @@ export interface McpHttpServerConfig {
|
|
|
21
22
|
firstPartyAppId?: string;
|
|
22
23
|
firstPartyOrgId?: string;
|
|
23
24
|
description?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Authenticate each request with this owner's workspace connection instead
|
|
27
|
+
* of stored headers. Holds identifiers only; the token is never stored here.
|
|
28
|
+
*/
|
|
29
|
+
workspaceConnection?: McpWorkspaceConnectionAuth;
|
|
24
30
|
}
|
|
25
31
|
export type McpServerConfig = McpStdioServerConfig | McpHttpServerConfig;
|
|
26
32
|
export interface McpConfig {
|
|
@@ -4,6 +4,7 @@ import { plainJsonSnapshot, UnusableSchemaError, } from "../json-schema/unusable
|
|
|
4
4
|
import { assertUsableSchema } from "../json-schema/usable-schema.js";
|
|
5
5
|
import { formatMcpConnectError, httpStatusFromError } from "./errors.js";
|
|
6
6
|
import { isFirstPartyRemoteEndpointTrusted, parseMergedKey, } from "./remote-store.js";
|
|
7
|
+
import { isWorkspaceConnectionMcpServer, resolveWorkspaceConnectionMcpBearer, } from "./workspace-connection-servers.js";
|
|
7
8
|
export const MCP_TOOL_PREFIX = "mcp__";
|
|
8
9
|
/**
|
|
9
10
|
* Why a listed tool cannot be offered to the model, or null when it can. A
|
|
@@ -106,7 +107,9 @@ function sameServerConfig(a, b) {
|
|
|
106
107
|
errorIdentity(b.configurationError) &&
|
|
107
108
|
a.firstParty === b.firstParty &&
|
|
108
109
|
a.firstPartyAppId === b.firstPartyAppId &&
|
|
109
|
-
a.firstPartyOrgId === b.firstPartyOrgId
|
|
110
|
+
a.firstPartyOrgId === b.firstPartyOrgId &&
|
|
111
|
+
JSON.stringify(a.workspaceConnection ?? null) ===
|
|
112
|
+
JSON.stringify(b.workspaceConnection ?? null));
|
|
110
113
|
}
|
|
111
114
|
if (a.type !== "http" && b.type !== "http") {
|
|
112
115
|
return (a.command === b.command &&
|
|
@@ -334,7 +337,16 @@ export class McpClientManager {
|
|
|
334
337
|
const requestInit = {};
|
|
335
338
|
const staticHeaders = { ...(cfg.headers ?? {}) };
|
|
336
339
|
const transportOptions = { requestInit };
|
|
337
|
-
if (
|
|
340
|
+
if (isWorkspaceConnectionMcpServer(cfg)) {
|
|
341
|
+
removeAuthorizationHeaders(staticHeaders);
|
|
342
|
+
if (Object.keys(staticHeaders).length > 0) {
|
|
343
|
+
requestInit.headers = staticHeaders;
|
|
344
|
+
}
|
|
345
|
+
const auth = cfg.workspaceConnection;
|
|
346
|
+
const nativeFetch = fetch.bind(globalThis);
|
|
347
|
+
transportOptions.fetch = async (input, init) => nativeFetch(input, await buildWorkspaceConnectionRequestInit(staticHeaders, init, auth));
|
|
348
|
+
}
|
|
349
|
+
else if (this.shouldInjectFirstPartyIdentity(entry)) {
|
|
338
350
|
const baselineHeaders = { ...staticHeaders };
|
|
339
351
|
removeAuthorizationHeaders(baselineHeaders);
|
|
340
352
|
if (Object.keys(baselineHeaders).length > 0) {
|
|
@@ -857,6 +869,17 @@ function headersToRecord(value) {
|
|
|
857
869
|
}
|
|
858
870
|
return {};
|
|
859
871
|
}
|
|
872
|
+
async function buildWorkspaceConnectionRequestInit(staticHeaders, rawInit, auth) {
|
|
873
|
+
const init = rawInit && typeof rawInit === "object" ? rawInit : {};
|
|
874
|
+
const headers = {
|
|
875
|
+
...headersToRecord(staticHeaders),
|
|
876
|
+
...headersToRecord(init.headers),
|
|
877
|
+
};
|
|
878
|
+
removeAuthorizationHeaders(headers);
|
|
879
|
+
// Resolved per request so a refreshed token is used as soon as it exists.
|
|
880
|
+
headers.Authorization = `Bearer ${await resolveWorkspaceConnectionMcpBearer(auth)}`;
|
|
881
|
+
return { ...init, headers };
|
|
882
|
+
}
|
|
860
883
|
async function buildFirstPartyRequestInit(staticHeaders, rawInit, trust) {
|
|
861
884
|
const init = rawInit && typeof rawInit === "object" ? rawInit : {};
|
|
862
885
|
const headers = {
|
|
@@ -38,6 +38,7 @@ import { mountMcpOAuthRoutes } from "./oauth-routes.js";
|
|
|
38
38
|
import { normalizeMcpPrincipal, resolveMcpPrincipalForEvent, } from "./principal.js";
|
|
39
39
|
import { InvalidMcpHeaderConfigurationError, McpHeaderSecretUnavailableError, addRemoteServer, listRemoteServers, mergedConfigKey, removeRemoteServer, toHttpServerConfigAsync, validateRemoteUrl, } from "./remote-store.js";
|
|
40
40
|
import { isMcpToolAllowedForRequest } from "./visibility.js";
|
|
41
|
+
import { addWorkspaceConnectionMcpServers } from "./workspace-connection-servers.js";
|
|
41
42
|
import { loadWorkspaceMcpServers } from "./workspace-servers.js";
|
|
42
43
|
const getOrgContext = (...args) => import("../org/context.js").then(({ getOrgContext }) => getOrgContext(...args));
|
|
43
44
|
export { formatMcpConnectError } from "./errors.js";
|
|
@@ -219,6 +220,7 @@ export async function buildMergedConfig(rawPrincipal) {
|
|
|
219
220
|
for (const [mergedKey, cfg] of Object.entries(workspaceServers)) {
|
|
220
221
|
servers[mergedKey] = cfg;
|
|
221
222
|
}
|
|
223
|
+
await addWorkspaceConnectionMcpServers(servers, principal);
|
|
222
224
|
// Hub-consume: if this app is configured to consume from a remote hub
|
|
223
225
|
// (AGENT_NATIVE_MCP_HUB_URL + AGENT_NATIVE_MCP_HUB_TOKEN), pull its
|
|
224
226
|
// org-scope servers and merge. Hub entries use `hub_<orgId>_<name>` so
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ProviderApiId } from "../provider-api/index.js";
|
|
2
|
+
import type { McpHttpServerConfig, McpServerConfig } from "./config.js";
|
|
3
|
+
import type { McpPrincipal } from "./principal.js";
|
|
4
|
+
export interface McpWorkspaceConnectionAuth {
|
|
5
|
+
provider: ProviderApiId;
|
|
6
|
+
connectionId: string;
|
|
7
|
+
/** The connection owner, who is also the MCP principal. */
|
|
8
|
+
ownerEmail: string;
|
|
9
|
+
orgId: string | null;
|
|
10
|
+
appId: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Add one MCP server per provider for which the principal has a connected
|
|
14
|
+
* personal workspace connection granted to this app. Organization- and
|
|
15
|
+
* app-scoped connections never qualify, even ones the principal created: they
|
|
16
|
+
* may have been authorized as another provider user, and an MCP call must act
|
|
17
|
+
* as the caller. A server already configured for the same endpoint wins, so
|
|
18
|
+
* tools are not duplicated.
|
|
19
|
+
*/
|
|
20
|
+
export declare function addWorkspaceConnectionMcpServers(servers: Record<string, McpServerConfig>, principal: McpPrincipal): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the bearer for one MCP request from the owner's workspace
|
|
23
|
+
* connection, refreshing it through the connection's own refresh path. The
|
|
24
|
+
* token is sent to the provider's MCP endpoint only; it is never persisted on
|
|
25
|
+
* the server config or shown to the agent.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveWorkspaceConnectionMcpBearer(auth: McpWorkspaceConnectionAuth): Promise<string>;
|
|
28
|
+
export declare function isWorkspaceConnectionMcpServer(config: McpServerConfig): config is McpHttpServerConfig & {
|
|
29
|
+
workspaceConnection: McpWorkspaceConnectionAuth;
|
|
30
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote MCP servers that authenticate with a workspace connection the user
|
|
3
|
+
* already has, so one provider sign-in covers both its REST API and its MCP
|
|
4
|
+
* server. HubSpot is the only verified provider: a token from the HubSpot app
|
|
5
|
+
* behind the workspace connection is accepted by mcp.hubspot.com.
|
|
6
|
+
*/
|
|
7
|
+
import { getAppConfig } from "../app-config/index.js";
|
|
8
|
+
import { runWithRequestContext } from "../server/request-context.js";
|
|
9
|
+
import { normalizeMcpUrl } from "../shared/mcp-provider-hosts.js";
|
|
10
|
+
import { mergedConfigKey } from "./remote-store.js";
|
|
11
|
+
const WORKSPACE_CONNECTION_MCP_SERVERS = [["hubspot", "https://mcp.hubspot.com"]];
|
|
12
|
+
// Same order as the agent-chat runtime app id, so MCP sees the grants the
|
|
13
|
+
// app's own actions see.
|
|
14
|
+
function currentAppId() {
|
|
15
|
+
const app = getAppConfig().app;
|
|
16
|
+
const value = (app.id ?? app.template ?? app.workspaceId ?? app.packageName)
|
|
17
|
+
?.trim()
|
|
18
|
+
.toLowerCase();
|
|
19
|
+
return value || null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Add one MCP server per provider for which the principal has a connected
|
|
23
|
+
* personal workspace connection granted to this app. Organization- and
|
|
24
|
+
* app-scoped connections never qualify, even ones the principal created: they
|
|
25
|
+
* may have been authorized as another provider user, and an MCP call must act
|
|
26
|
+
* as the caller. A server already configured for the same endpoint wins, so
|
|
27
|
+
* tools are not duplicated.
|
|
28
|
+
*/
|
|
29
|
+
export async function addWorkspaceConnectionMcpServers(servers, principal) {
|
|
30
|
+
const appId = currentAppId();
|
|
31
|
+
if (!appId)
|
|
32
|
+
return;
|
|
33
|
+
const { resolveWorkspaceConnectionsForApp } = await import("../workspace-connections/store.js");
|
|
34
|
+
const caller = principal.userEmail.trim().toLowerCase();
|
|
35
|
+
for (const [provider, url] of WORKSPACE_CONNECTION_MCP_SERVERS) {
|
|
36
|
+
const alreadyConfigured = Object.values(servers).some((server) => server.type === "http" &&
|
|
37
|
+
normalizeMcpUrl(server.url) === normalizeMcpUrl(url));
|
|
38
|
+
if (alreadyConfigured)
|
|
39
|
+
continue;
|
|
40
|
+
const key = mergedConfigKey("user", {
|
|
41
|
+
id: `workspace_connection_${provider}`,
|
|
42
|
+
name: provider,
|
|
43
|
+
url,
|
|
44
|
+
createdAt: 0,
|
|
45
|
+
}, principal.userEmail);
|
|
46
|
+
const description = `${provider} through your ${provider} workspace connection`;
|
|
47
|
+
let connectionIds;
|
|
48
|
+
try {
|
|
49
|
+
const connections = await runWithRequestContext({
|
|
50
|
+
userEmail: principal.userEmail,
|
|
51
|
+
...(principal.orgId ? { orgId: principal.orgId } : {}),
|
|
52
|
+
}, () => resolveWorkspaceConnectionsForApp({ appId, provider }));
|
|
53
|
+
connectionIds = connections
|
|
54
|
+
.filter((connection) => connection.status === "connected" &&
|
|
55
|
+
connection.ownerEmail.trim().toLowerCase() === caller &&
|
|
56
|
+
connection.config.workspaceConnectionScope === "user")
|
|
57
|
+
.map((connection) => connection.id);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Keep other MCP servers working, but report this one as unreadable
|
|
61
|
+
// rather than as an absent connection.
|
|
62
|
+
servers[key] = {
|
|
63
|
+
type: "http",
|
|
64
|
+
url,
|
|
65
|
+
description,
|
|
66
|
+
configurationError: new Error(`The ${provider} workspace connection could not be read.`, { cause: error }),
|
|
67
|
+
};
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// The server has no account selector, so with two personal connections
|
|
71
|
+
// (e.g. a sandbox and a production portal) any pick could write to the
|
|
72
|
+
// wrong account.
|
|
73
|
+
if (connectionIds.length > 1) {
|
|
74
|
+
servers[key] = {
|
|
75
|
+
type: "http",
|
|
76
|
+
url,
|
|
77
|
+
description,
|
|
78
|
+
configurationError: new Error(`You have ${connectionIds.length} personal ${provider} connections for this app, so the ${provider} MCP server can't tell which account to use. Disconnect the extra ones, or add the ${provider} MCP server yourself.`),
|
|
79
|
+
};
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const connectionId = connectionIds[0];
|
|
83
|
+
if (!connectionId)
|
|
84
|
+
continue;
|
|
85
|
+
servers[key] = {
|
|
86
|
+
type: "http",
|
|
87
|
+
url,
|
|
88
|
+
description,
|
|
89
|
+
workspaceConnection: {
|
|
90
|
+
provider,
|
|
91
|
+
connectionId,
|
|
92
|
+
ownerEmail: principal.userEmail,
|
|
93
|
+
orgId: principal.orgId,
|
|
94
|
+
appId,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the bearer for one MCP request from the owner's workspace
|
|
101
|
+
* connection, refreshing it through the connection's own refresh path. The
|
|
102
|
+
* token is sent to the provider's MCP endpoint only; it is never persisted on
|
|
103
|
+
* the server config or shown to the agent.
|
|
104
|
+
*/
|
|
105
|
+
export async function resolveWorkspaceConnectionMcpBearer(auth) {
|
|
106
|
+
const { createProviderApiRuntime } = await import("../provider-api/index.js");
|
|
107
|
+
const runtime = createProviderApiRuntime({ appId: auth.appId });
|
|
108
|
+
const token = await runWithRequestContext({
|
|
109
|
+
userEmail: auth.ownerEmail,
|
|
110
|
+
...(auth.orgId ? { orgId: auth.orgId } : {}),
|
|
111
|
+
}, () => runtime.resolveOAuthAccessToken({
|
|
112
|
+
provider: auth.provider,
|
|
113
|
+
connectionId: auth.connectionId,
|
|
114
|
+
}));
|
|
115
|
+
return token.accessToken;
|
|
116
|
+
}
|
|
117
|
+
export function isWorkspaceConnectionMcpServer(config) {
|
|
118
|
+
return config.type === "http" && !!config.workspaceConnection;
|
|
119
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
2
2
|
error?: undefined;
|
|
3
|
-
ok?: undefined;
|
|
4
3
|
count: number;
|
|
5
4
|
updated?: undefined;
|
|
5
|
+
ok?: undefined;
|
|
6
6
|
} | {
|
|
7
7
|
error?: undefined;
|
|
8
|
-
ok?: undefined;
|
|
9
8
|
count?: undefined;
|
|
10
9
|
updated: number;
|
|
11
|
-
} | {
|
|
12
10
|
ok?: undefined;
|
|
11
|
+
} | {
|
|
13
12
|
count?: undefined;
|
|
14
13
|
updated?: undefined;
|
|
15
14
|
error: string;
|
|
15
|
+
ok?: undefined;
|
|
16
16
|
} | {
|
|
17
17
|
error?: undefined;
|
|
18
18
|
count?: undefined;
|