@agent-native/core 0.209.1-nightly-20261015153252 → 0.209.1

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.
Files changed (37) hide show
  1. package/dist/a2a/index.d.ts +1 -1
  2. package/dist/a2a/index.js +1 -1
  3. package/dist/a2a/server.d.ts +10 -0
  4. package/dist/a2a/server.js +96 -0
  5. package/dist/agent/production-agent.d.ts +2 -0
  6. package/dist/agent/production-agent.js +43 -6
  7. package/dist/agent/submission-deadline.d.ts +1 -0
  8. package/dist/agent/submission-deadline.js +27 -0
  9. package/dist/agent/thread-data-builder.d.ts +2 -1
  10. package/dist/agent/thread-data-builder.js +37 -6
  11. package/dist/authorization/check-action.js +4 -3
  12. package/dist/chat-threads/store.d.ts +1 -0
  13. package/dist/chat-threads/store.js +34 -2
  14. package/dist/client/agent-chat.d.ts +3 -0
  15. package/dist/client/agent-chat.js +9 -0
  16. package/dist/client/chat/agentkit-agent-native.js +126 -3
  17. package/dist/client/error-format.d.ts +2 -0
  18. package/dist/client/error-format.js +10 -0
  19. package/dist/collab/routes.d.ts +1 -1
  20. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  21. package/dist/org/index.d.ts +1 -1
  22. package/dist/org/index.js +1 -1
  23. package/dist/org/workspace-app-access-outcome.d.ts +45 -0
  24. package/dist/org/workspace-app-access-outcome.js +80 -0
  25. package/dist/org/workspace-app-access-request.d.ts +2 -2
  26. package/dist/org/workspace-app-access-request.js +2 -2
  27. package/dist/org/workspace-app-access.d.ts +8 -3
  28. package/dist/org/workspace-app-access.js +122 -30
  29. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  30. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  31. package/dist/server/action-routes.js +10 -7
  32. package/dist/server/agent-chat-plugin.js +28 -2
  33. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  34. package/dist/server/auth.js +3 -3
  35. package/dist/server/realtime-token.d.ts +1 -1
  36. package/dist/triggers/actions/manage-automation.d.ts +3 -3
  37. package/package.json +4 -4
@@ -1,4 +1,4 @@
1
- export { mountA2A, verifyA2AToken } from "./server.js";
1
+ export { diagnoseA2ATokenFailure, mountA2A, verifyA2AToken, type A2ATokenFailureCause, } from "./server.js";
2
2
  export type { A2ATokenPayload } from "./server.js";
3
3
  export { generateAgentCard } from "./agent-card.js";
4
4
  export { A2A_AGENT_ACTIVITY_KIND, A2A_AGENT_ACTIVITY_VERSION, MAX_A2A_ACTIVITY_REASONING_CHARS, MAX_A2A_ACTIVITY_REASONING_SEGMENTS, MAX_A2A_ACTIVITY_RESPONSE_CHARS, MAX_A2A_ACTIVITY_TOOL_CALLS, MAX_A2A_ACTIVITY_TOTAL_CHARS, MAX_A2A_ACTIVITY_TOOL_ID_CHARS, MAX_A2A_ACTIVITY_TOOL_INPUT_CHARS, MAX_A2A_ACTIVITY_TOOL_NAME_CHARS, MAX_A2A_ACTIVITY_TOOL_PAYLOAD_CHARS, MAX_A2A_ACTIVITY_TOOL_RESULT_CHARS, applyA2AAgentActivityEvent, buildA2AAgentActivityPart, buildA2AAgentActivitySnapshot, createA2AAgentActivityState, parseA2AAgentActivityPart, } from "./activity.js";
package/dist/a2a/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { mountA2A, verifyA2AToken } from "./server.js";
1
+ export { diagnoseA2ATokenFailure, mountA2A, verifyA2AToken, } from "./server.js";
2
2
  export { generateAgentCard } from "./agent-card.js";
3
3
  export { A2A_AGENT_ACTIVITY_KIND, A2A_AGENT_ACTIVITY_VERSION, MAX_A2A_ACTIVITY_REASONING_CHARS, MAX_A2A_ACTIVITY_REASONING_SEGMENTS, MAX_A2A_ACTIVITY_RESPONSE_CHARS, MAX_A2A_ACTIVITY_TOOL_CALLS, MAX_A2A_ACTIVITY_TOTAL_CHARS, MAX_A2A_ACTIVITY_TOOL_ID_CHARS, MAX_A2A_ACTIVITY_TOOL_INPUT_CHARS, MAX_A2A_ACTIVITY_TOOL_NAME_CHARS, MAX_A2A_ACTIVITY_TOOL_PAYLOAD_CHARS, MAX_A2A_ACTIVITY_TOOL_RESULT_CHARS, applyA2AAgentActivityEvent, buildA2AAgentActivityPart, buildA2AAgentActivitySnapshot, createA2AAgentActivityState, parseA2AAgentActivityPart, } from "./activity.js";
4
4
  export { A2AClient, A2AJsonRpcResponseError, A2AMissingJsonRpcResponseError, A2ANoJsonRpcInterfaceError, A2AProtocolError, assertCredentialedA2AUrl, callAction, callAgent, clearA2ACardCache, getGlobalA2ASecret, signA2AOrganizationToken, signA2AToken, } from "./client.js";
@@ -48,5 +48,15 @@ export declare function verifyA2AToken(token: string, event?: any, audienceOptio
48
48
  /** Only use this explicitly configured credential; never fall back. */
49
49
  verificationSecret?: string;
50
50
  }): Promise<A2ATokenPayload>;
51
+ export type A2ATokenFailureCause = "malformed" | "secret-missing" | "audience-unresolvable" | "audience" | "issuer" | "expired" | "signature" | "invalid-claims" | "mcp-credential" | "missing-subject" | "org-binding" | "org-lookup-failed" | "undetermined";
52
+ /**
53
+ * Names why a token that `verifyA2AToken` resolved no identity for was
54
+ * refused. `verifyA2AToken` collapses a wrong audience, a wrong secret and an
55
+ * unbound organization into one empty identity; this re-checks the same inputs,
56
+ * against the deploy secret and then the organization's, to tell them apart.
57
+ * Diagnostic only: it never grants anything, and its answer must not be used as
58
+ * an authorization result.
59
+ */
60
+ export declare function diagnoseA2ATokenFailure(token: string, event?: any): Promise<A2ATokenFailureCause>;
51
61
  export declare function mountA2A(nitroApp: any, config: A2AConfig, routePrefix?: string): void;
52
62
  export declare function filterPublicAgentCardSkills(config: A2AConfig): AgentSkill[];
@@ -159,6 +159,102 @@ function isDirectReadSkill(skill) {
159
159
  export async function verifyA2AToken(token, event, audienceOptions) {
160
160
  return (await verifyA2ATokenInternal(token, event, audienceOptions)).payload;
161
161
  }
162
+ async function checkAgainstSecret(token, secret, options) {
163
+ try {
164
+ const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(secret), options);
165
+ return { payload };
166
+ }
167
+ catch (error) {
168
+ const { code, claim } = error;
169
+ if (code === "ERR_JWT_EXPIRED")
170
+ return { cause: "expired" };
171
+ if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") {
172
+ return { cause: "signature" };
173
+ }
174
+ if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED") {
175
+ if (claim === "aud")
176
+ return { cause: "audience" };
177
+ if (claim === "iss")
178
+ return { cause: "issuer" };
179
+ }
180
+ return { cause: "invalid-claims" };
181
+ }
182
+ }
183
+ async function diagnoseVerifiedPayload(payload) {
184
+ if (isMcpCredential(payload))
185
+ return "mcp-credential";
186
+ if (typeof payload.sub !== "string" || !payload.sub.trim()) {
187
+ return "missing-subject";
188
+ }
189
+ try {
190
+ const organization = await verifyA2AOrganizationIdentity(payload);
191
+ return organization === null ? "org-binding" : "undetermined";
192
+ }
193
+ catch {
194
+ // coercion-ok: an unreadable organization registry is the typed "org-lookup-failed" cause.
195
+ return "org-lookup-failed";
196
+ }
197
+ }
198
+ /**
199
+ * Names why a token that `verifyA2AToken` resolved no identity for was
200
+ * refused. `verifyA2AToken` collapses a wrong audience, a wrong secret and an
201
+ * unbound organization into one empty identity; this re-checks the same inputs,
202
+ * against the deploy secret and then the organization's, to tell them apart.
203
+ * Diagnostic only: it never grants anything, and its answer must not be used as
204
+ * an authorization result.
205
+ */
206
+ export async function diagnoseA2ATokenFailure(token, event) {
207
+ let claims;
208
+ try {
209
+ claims = jose.decodeJwt(token);
210
+ }
211
+ catch {
212
+ // coercion-ok: an undecodable token is the typed "malformed" cause.
213
+ return "malformed";
214
+ }
215
+ const secret = readDeployCredentialEnv("A2A_SECRET")?.trim();
216
+ const options = {};
217
+ if (typeof claims.aud !== "undefined") {
218
+ const audience = expectedJwtAudience(event);
219
+ if (!audience)
220
+ return "audience-unresolvable";
221
+ options.audience = audience;
222
+ }
223
+ if (typeof claims.iss === "string" && claims.iss.length > 0) {
224
+ options.issuer = claims.iss;
225
+ }
226
+ // The verifier goes on to the organization's own secret when the deploy secret
227
+ // is unset or does not verify the token, so neither is the final answer yet.
228
+ let deployCause = "secret-missing";
229
+ if (secret) {
230
+ const deploy = await checkAgainstSecret(token, secret, options);
231
+ if ("payload" in deploy)
232
+ return diagnoseVerifiedPayload(deploy.payload);
233
+ deployCause = deploy.cause;
234
+ }
235
+ const orgDomain = typeof claims.org_domain === "string" ? claims.org_domain.trim() : "";
236
+ if ((deployCause !== "signature" && deployCause !== "secret-missing") ||
237
+ !orgDomain) {
238
+ return deployCause;
239
+ }
240
+ let organization;
241
+ try {
242
+ const { resolveA2AOrganizationCredentialsByDomain } = await import("../org/context.js");
243
+ organization = await resolveA2AOrganizationCredentialsByDomain(orgDomain);
244
+ }
245
+ catch {
246
+ // coercion-ok: an unreadable organization registry is the typed "org-lookup-failed" cause.
247
+ return "org-lookup-failed";
248
+ }
249
+ if (!organization)
250
+ return deployCause;
251
+ const orgCheck = await checkAgainstSecret(token, organization.secret, options);
252
+ if ("cause" in orgCheck)
253
+ return orgCheck.cause;
254
+ return organizationPrincipalClaims(orgCheck.payload, organization)
255
+ ? "undetermined"
256
+ : "org-binding";
257
+ }
162
258
  async function verifyA2ATokenInternal(token, event, audienceOptions) {
163
259
  // Step 1: Peek at JWT claims WITHOUT verification to get org_domain.
164
260
  // This is safe because we only use org_domain to look up the secret,
@@ -329,6 +329,7 @@ export interface ProductionAgentOptions {
329
329
  attachments?: AgentChatAttachment[];
330
330
  queuedMessageId?: string;
331
331
  queuedMessageClaimId?: string;
332
+ submissionExpiresAt?: unknown;
332
333
  }) => void | Promise<void>;
333
334
  /**
334
335
  * The turn was refused before a run started (no usable model credential).
@@ -342,6 +343,7 @@ export interface ProductionAgentOptions {
342
343
  attachments?: AgentChatAttachment[];
343
344
  queuedMessageId?: string;
344
345
  agentKitMessageId?: string;
346
+ submissionExpiresAt?: unknown;
345
347
  retryContext: RefusedTurnRetryContext;
346
348
  failure: {
347
349
  code: string;
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS, MAX_AGENT_REQUEST_ATTACHMENTS, } from "@agent-native/agentkit/protocol";
2
+ import { AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY, AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY, MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS, MAX_AGENT_REQUEST_ATTACHMENTS, } from "@agent-native/agentkit/protocol";
3
3
  import Ajv from "ajv";
4
4
  import Ajv2020 from "ajv/dist/2020.js";
5
5
  import { defineEventHandler, getHeader, setResponseHeader, setResponseStatus, getMethod, } from "h3";
@@ -83,6 +83,7 @@ import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, AgentTurnIniti
83
83
  import { buildCurrentTimeUserContext } from "./runtime-context.js";
84
84
  import { claimSetupResume, setupResumeRefusedRunId, } from "./setup-resume-claim.js";
85
85
  import { getStrictToolInputValidator } from "./strict-tool-input.js";
86
+ import { normalizeSubmissionDeadlineMetadata } from "./submission-deadline.js";
86
87
  import { consumeAgentToolApproval, createAgentToolApproval, isAgentToolAlwaysAllowed, resolveAgentToolApprovalTurnId, } from "./tool-approval-store.js";
87
88
  import { findCompletedJournalEntry, isUnavailableToolCallInput, } from "./tool-call-journal.js";
88
89
  import { redactSensitiveFields, sanitizeToolErrorText, sanitizeToolErrorValue, } from "./tool-error-redaction.js";
@@ -1024,6 +1025,42 @@ export function createPlanModeActionRegistry(actions) {
1024
1025
  }
1025
1026
  return filtered;
1026
1027
  }
1028
+ function submissionExpiryMetadata(metadata) {
1029
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
1030
+ return undefined;
1031
+ }
1032
+ const custom = metadata.custom;
1033
+ if (!custom ||
1034
+ typeof custom !== "object" ||
1035
+ Array.isArray(custom) ||
1036
+ !Object.prototype.hasOwnProperty.call(custom, AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY)) {
1037
+ return undefined;
1038
+ }
1039
+ return {
1040
+ submissionExpiresAt: custom[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY],
1041
+ };
1042
+ }
1043
+ function mergeQueuedPromotionMetadata(admittedMetadata, optionMetadata) {
1044
+ const admitted = queuedPromotionRecord(admittedMetadata) ?? {};
1045
+ const options = queuedPromotionRecord(optionMetadata) ?? {};
1046
+ const admittedCustom = queuedPromotionRecord(admitted.custom) ?? {};
1047
+ const optionCustom = queuedPromotionRecord(options.custom) ?? {};
1048
+ const custom = { ...admittedCustom, ...optionCustom };
1049
+ delete custom[AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY];
1050
+ delete custom[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY];
1051
+ if (Object.prototype.hasOwnProperty.call(admittedCustom, AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY)) {
1052
+ custom[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY] =
1053
+ admittedCustom[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY];
1054
+ }
1055
+ const metadata = { ...admitted, ...options };
1056
+ if (Object.keys(custom).length > 0) {
1057
+ metadata.custom = custom;
1058
+ }
1059
+ else {
1060
+ delete metadata.custom;
1061
+ }
1062
+ return metadata;
1063
+ }
1027
1064
  export async function resolveAgentOwnerEmail(options, event) {
1028
1065
  let ownerEmail = null;
1029
1066
  if (options.resolveOwnerEmail) {
@@ -7955,7 +7992,7 @@ export function createProductionAgentHandler(options) {
7955
7992
  : typeof parentId === "string" && parentId.trim()
7956
7993
  ? parentId.trim()
7957
7994
  : undefined;
7958
- let requestMetadata = body.metadata;
7995
+ let requestMetadata = normalizeSubmissionDeadlineMetadata(body.metadata);
7959
7996
  const continuedRunId = (requested) => internalContinuation === true &&
7960
7997
  typeof requested === "string" &&
7961
7998
  requested.trim().length <= 200
@@ -8248,10 +8285,7 @@ export function createProductionAgentHandler(options) {
8248
8285
  if (admittedQueuedMessage) {
8249
8286
  // A claim admits the persisted queue payload, not extra fields in this request.
8250
8287
  const promotionOptions = admittedQueuedMessage.options ?? {};
8251
- const promotionMetadata = {
8252
- ...(admittedQueuedMessage.metadata ?? {}),
8253
- ...(queuedPromotionRecord(promotionOptions.metadata) ?? {}),
8254
- };
8288
+ const promotionMetadata = mergeQueuedPromotionMetadata(admittedQueuedMessage.metadata, promotionOptions.metadata);
8255
8289
  requestMessage = admittedQueuedMessage.text;
8256
8290
  agentKitMessageId = admittedQueuedMessage.id;
8257
8291
  requestDisplayMessage = admittedQueuedMessage.text;
@@ -8330,6 +8364,7 @@ export function createProductionAgentHandler(options) {
8330
8364
  ? requestDisplayMessage
8331
8365
  : requestMessage,
8332
8366
  attachments: requestAttachments,
8367
+ ...submissionExpiryMetadata(requestMetadata),
8333
8368
  ...(typeof queuedMessageId === "string" && queuedMessageId.trim()
8334
8369
  ? { queuedMessageId: queuedMessageId.trim() }
8335
8370
  : {}),
@@ -8715,6 +8750,7 @@ export function createProductionAgentHandler(options) {
8715
8750
  ? requestDisplayMessage
8716
8751
  : requestMessage,
8717
8752
  attachments: requestAttachments,
8753
+ ...submissionExpiryMetadata(requestMetadata),
8718
8754
  ...(typeof queuedMessageId === "string" && queuedMessageId.trim()
8719
8755
  ? { queuedMessageId: queuedMessageId.trim() }
8720
8756
  : {}),
@@ -9549,6 +9585,7 @@ export function createProductionAgentHandler(options) {
9549
9585
  message: messageToPersist,
9550
9586
  ...(agentKitMessageId ? { agentKitMessageId } : {}),
9551
9587
  attachments: requestAttachments,
9588
+ ...submissionExpiryMetadata(requestMetadata),
9552
9589
  ...(typeof queuedMessageId === "string" && queuedMessageId.trim()
9553
9590
  ? { queuedMessageId: queuedMessageId.trim() }
9554
9591
  : {}),
@@ -0,0 +1 @@
1
+ export declare function normalizeSubmissionDeadlineMetadata(metadata: unknown, serverReceivedAt?: number): Record<string, unknown> | undefined;
@@ -0,0 +1,27 @@
1
+ import { AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY, AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY, } from "@agent-native/agentkit/protocol";
2
+ export function normalizeSubmissionDeadlineMetadata(metadata, serverReceivedAt = Date.now()) {
3
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
4
+ return undefined;
5
+ }
6
+ const record = metadata;
7
+ const custom = record.custom;
8
+ if (!custom || typeof custom !== "object" || Array.isArray(custom)) {
9
+ return record;
10
+ }
11
+ const customRecord = custom;
12
+ if (!Object.prototype.hasOwnProperty.call(customRecord, AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY)) {
13
+ return record;
14
+ }
15
+ const ttlMs = customRecord[AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY];
16
+ const expiresAt = typeof ttlMs === "number" &&
17
+ Number.isFinite(ttlMs) &&
18
+ ttlMs >= 0 &&
19
+ Number.isFinite(serverReceivedAt) &&
20
+ Number.isFinite(serverReceivedAt + ttlMs)
21
+ ? serverReceivedAt + ttlMs
22
+ : Number.NaN;
23
+ const normalizedCustom = { ...customRecord };
24
+ delete normalizedCustom[AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY];
25
+ normalizedCustom[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY] = expiresAt;
26
+ return { ...record, custom: normalizedCustom };
27
+ }
@@ -128,7 +128,7 @@ export declare function applySubmittedUserMessage(repo: any, userMessage: UserMe
128
128
  status: "submitted" | "already_submitted";
129
129
  repo: any;
130
130
  } | {
131
- status: "already_claimed" | "claim_expired";
131
+ status: "already_claimed" | "claim_expired" | "submission_expired";
132
132
  };
133
133
  /**
134
134
  * Whether a stored user message is the prompt this run answers, rather than
@@ -150,6 +150,7 @@ export declare function buildUserMessage(opts: {
150
150
  turnId?: string;
151
151
  agentKitMessageId?: string;
152
152
  queuedMessageId?: string;
153
+ submissionExpiresAt?: unknown;
153
154
  createdAt?: Date;
154
155
  /** The turn was refused before a run started; its retry reads this back. */
155
156
  refusedRetry?: RefusedTurnRetryContext;
@@ -1,4 +1,4 @@
1
- import { parseAgentSuggestion, } from "@agent-native/agentkit/protocol";
1
+ import { AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY, parseAgentSuggestion, } from "@agent-native/agentkit/protocol";
2
2
  import { formatChatErrorText, normalizeChatError, } from "../client/error-format.js";
3
3
  import { isCredentialGapCodeAgentEvent, normalizeCodeAgentTranscript, } from "../code-agents/transcript-normalizer.js";
4
4
  import { stripAgentChatContextFromMessage } from "../shared/agent-chat-context.js";
@@ -1252,11 +1252,8 @@ export function claimQueuedMessage(repo, messageId) {
1252
1252
  return pruneClaimedQueuedMessages(normalized);
1253
1253
  }
1254
1254
  export function applySubmittedUserMessage(repo, userMessage, queuedMessage) {
1255
- if (!queuedMessage) {
1256
- return { status: "submitted", repo: upsertUserMessage(repo, userMessage) };
1257
- }
1258
1255
  const userCustom = userMessage.metadata.custom;
1259
- const wasSubmitted = Array.isArray(repo?.messages)
1256
+ const wasSubmitted = queuedMessage && Array.isArray(repo?.messages)
1260
1257
  ? repo.messages.some((entry) => {
1261
1258
  const outer = entry;
1262
1259
  const message = outer?.message ?? outer;
@@ -1274,12 +1271,41 @@ export function applySubmittedUserMessage(repo, userMessage, queuedMessage) {
1274
1271
  userCustom?.submittedRunId);
1275
1272
  })
1276
1273
  : false;
1277
- if (wasSubmitted) {
1274
+ if (wasSubmitted && queuedMessage) {
1278
1275
  return {
1279
1276
  status: "already_submitted",
1280
1277
  repo: claimQueuedMessage(repo, queuedMessage.id),
1281
1278
  };
1282
1279
  }
1280
+ const submittedRunId = userCustom?.submittedRunId;
1281
+ const wasDirectlySubmitted = !queuedMessage &&
1282
+ typeof submittedRunId === "string" &&
1283
+ submittedRunId.trim().length > 0 &&
1284
+ Array.isArray(repo?.messages) &&
1285
+ repo.messages.some((entry) => {
1286
+ const message = getStoredMessage(entry);
1287
+ const custom = message?.metadata?.custom;
1288
+ return (message?.role === "user" &&
1289
+ custom &&
1290
+ typeof custom === "object" &&
1291
+ !Array.isArray(custom) &&
1292
+ custom.submittedRunId === submittedRunId);
1293
+ });
1294
+ if (wasDirectlySubmitted) {
1295
+ return { status: "already_submitted", repo };
1296
+ }
1297
+ if (userCustom &&
1298
+ Object.prototype.hasOwnProperty.call(userCustom, AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY)) {
1299
+ const submissionExpiresAt = userCustom[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY];
1300
+ if (typeof submissionExpiresAt !== "number" ||
1301
+ !Number.isFinite(submissionExpiresAt) ||
1302
+ submissionExpiresAt <= Date.now()) {
1303
+ return { status: "submission_expired" };
1304
+ }
1305
+ }
1306
+ if (!queuedMessage) {
1307
+ return { status: "submitted", repo: upsertUserMessage(repo, userMessage) };
1308
+ }
1283
1309
  if (hasClaimedQueuedMessage(repo, queuedMessage.id)) {
1284
1310
  return { status: "already_claimed" };
1285
1311
  }
@@ -3136,6 +3162,11 @@ export function buildUserMessage(opts) {
3136
3162
  ...(opts.queuedMessageId
3137
3163
  ? { agentNativeQueuedMessageId: opts.queuedMessageId }
3138
3164
  : {}),
3165
+ ...(Object.prototype.hasOwnProperty.call(opts, "submissionExpiresAt")
3166
+ ? {
3167
+ [AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY]: opts.submissionExpiresAt,
3168
+ }
3169
+ : {}),
3139
3170
  ...(opts.refusedRetry ? { [RUN_NOT_STARTED_METADATA_KEY]: true } : {}),
3140
3171
  },
3141
3172
  },
@@ -1,6 +1,7 @@
1
1
  import { getDbExec } from "../db/client.js";
2
2
  import { getAppPermissionOverrides, getRegisteredAppRoles, resolveAppRole, } from "../org/app-roles.js";
3
- import { isWorkspaceAppAccessAllowed, WORKSPACE_APP_ACCESS_UNAVAILABLE, WORKSPACE_APP_ACCESS_UNAVAILABLE_MESSAGE, } from "../org/workspace-app-access.js";
3
+ import { isWorkspaceAppAccessUnavailable, WORKSPACE_APP_ACCESS_UNAVAILABLE_MESSAGE, } from "../org/workspace-app-access-outcome.js";
4
+ import { checkWorkspaceAppAccess } from "../org/workspace-app-access.js";
4
5
  import { resolveWorkspaceAccessAppId } from "../org/workspace-app-identity.js";
5
6
  import { ForbiddenError, resolveAccess } from "../sharing/access.js";
6
7
  import { ROLE_RANK } from "../sharing/schema.js";
@@ -66,12 +67,12 @@ export async function checkAction(config, args, ctx, target) {
66
67
  }
67
68
  const workspaceAppId = resolveWorkspaceAccessAppId(appId);
68
69
  const appAllowed = workspaceAppId
69
- ? await isWorkspaceAppAccessAllowed(workspaceAppId, {
70
+ ? await checkWorkspaceAppAccess(workspaceAppId, {
70
71
  email: identity.userEmail,
71
72
  orgId: identity.orgId,
72
73
  })
73
74
  : true;
74
- if (appAllowed === WORKSPACE_APP_ACCESS_UNAVAILABLE) {
75
+ if (isWorkspaceAppAccessUnavailable(appAllowed)) {
75
76
  throw Object.assign(new Error(WORKSPACE_APP_ACCESS_UNAVAILABLE_MESSAGE), {
76
77
  statusCode: 503,
77
78
  });
@@ -255,6 +255,7 @@ export interface ThreadQueuedMessageMutationResult {
255
255
  message?: QueuedMessage;
256
256
  claimedMessage?: QueuedMessage;
257
257
  claimBusy?: boolean;
258
+ submissionExpired?: boolean;
258
259
  promotionBusy?: boolean;
259
260
  released?: boolean;
260
261
  }
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
- import { parseQueueMessageInput, } from "@agent-native/agentkit/protocol";
2
+ import { AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY, parseQueueMessageInput, } from "@agent-native/agentkit/protocol";
3
+ import { normalizeSubmissionDeadlineMetadata } from "../agent/submission-deadline.js";
3
4
  import { extractThreadMeta, mergeThreadDataForClientSave, normalizeThreadRepository, normalizeThreadTitle, } from "../agent/thread-data-builder.js";
4
5
  import { failedRunIdsWithError, isRenderedRunNotStartedNotice, projectRootAssistantMessages, threadMessageRecord, } from "../agent/thread-message-projection.js";
5
6
  import { getDbExec } from "../db/client.js";
@@ -1259,9 +1260,30 @@ export async function setThreadEngineMeta(threadId, meta) {
1259
1260
  });
1260
1261
  }
1261
1262
  const QUEUED_MESSAGE_CLAIM_TTL_MS = 60_000;
1263
+ function queuedSubmissionExpired(message, now) {
1264
+ const custom = message.metadata?.custom;
1265
+ if (!custom ||
1266
+ typeof custom !== "object" ||
1267
+ Array.isArray(custom) ||
1268
+ !Object.prototype.hasOwnProperty.call(custom, AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY)) {
1269
+ return false;
1270
+ }
1271
+ const expiresAt = custom[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY];
1272
+ return (typeof expiresAt !== "number" ||
1273
+ !Number.isFinite(expiresAt) ||
1274
+ expiresAt <= now);
1275
+ }
1262
1276
  /** Applies a queue operation to the latest durable thread state on every CAS retry. */
1263
1277
  export async function mutateThreadQueuedMessages(threadId, mutation) {
1264
1278
  if (mutation.type === "append") {
1279
+ const metadata = normalizeSubmissionDeadlineMetadata(mutation.message.metadata, Date.now());
1280
+ mutation = {
1281
+ type: "append",
1282
+ message: {
1283
+ ...mutation.message,
1284
+ ...(metadata ? { metadata } : {}),
1285
+ },
1286
+ };
1265
1287
  try {
1266
1288
  assertNoInlineImageBytes({ type: "file", metadata: mutation.message.metadata }, "queuedMessage.metadata");
1267
1289
  }
@@ -1304,6 +1326,10 @@ export async function mutateThreadQueuedMessages(threadId, mutation) {
1304
1326
  let response = {};
1305
1327
  switch (mutation.type) {
1306
1328
  case "append": {
1329
+ if (queuedSubmissionExpired(mutation.message, Date.now())) {
1330
+ response = { submissionExpired: true };
1331
+ break;
1332
+ }
1307
1333
  const existing = current.find((message) => message.id === mutation.message.id);
1308
1334
  const { promotionClaim: _claim, ...existingMessage } = existing ?? {};
1309
1335
  if (existing &&
@@ -1320,7 +1346,8 @@ export async function mutateThreadQueuedMessages(threadId, mutation) {
1320
1346
  const index = current.findIndex((message) => message.id === mutation.messageId);
1321
1347
  const message = current[index];
1322
1348
  if (message?.promotionClaim &&
1323
- message.promotionClaim.expiresAt > Date.now()) {
1349
+ message.promotionClaim.expiresAt > Date.now() &&
1350
+ !queuedSubmissionExpired(message, Date.now())) {
1324
1351
  response = { promotionBusy: true };
1325
1352
  break;
1326
1353
  }
@@ -1351,6 +1378,11 @@ export async function mutateThreadQueuedMessages(threadId, mutation) {
1351
1378
  }
1352
1379
  const message = current[index];
1353
1380
  const now = Date.now();
1381
+ if (queuedSubmissionExpired(message, now)) {
1382
+ queuedMessages = current.filter((candidate) => candidate.id !== mutation.messageId);
1383
+ response = { submissionExpired: true };
1384
+ break;
1385
+ }
1354
1386
  if (message.promotionClaim &&
1355
1387
  message.promotionClaim.id !== mutation.claimId &&
1356
1388
  message.promotionClaim.expiresAt > now) {
@@ -42,6 +42,8 @@ export interface AgentChatMessage {
42
42
  * {@link AGENT_CHAT_SUBMIT_RESULT_EVENT}. Auto-generated if omitted.
43
43
  */
44
44
  submitMessageId?: string;
45
+ /** Drop a queued submit instead of handing it to chat after this time. */
46
+ submissionExpiresAt?: number;
45
47
  usageLabel?: string;
46
48
  approvedToolCalls?: string[];
47
49
  }
@@ -176,6 +178,7 @@ export interface ParsedSubmitChat {
176
178
  attachments?: AgentChatAttachment[];
177
179
  requestMode?: AgentChatRequestMode;
178
180
  submitMessageId?: string;
181
+ submissionExpiresAt?: number;
179
182
  usageLabel?: string;
180
183
  approvedToolCalls?: string[];
181
184
  }
@@ -747,11 +747,17 @@ export function parseSubmitChatMessage(event) {
747
747
  ].filter((image) => typeof image === "string" && image.trim().length > 0);
748
748
  const images = imageSources.length > 0 ? [...new Set(imageSources)] : undefined;
749
749
  const hasActionScope = Object.prototype.hasOwnProperty.call(raw, "actionScope");
750
+ const hasSubmissionExpiresAt = Object.prototype.hasOwnProperty.call(raw, "submissionExpiresAt");
750
751
  const actionScope = hasActionScope
751
752
  ? tryNormalizeAgentActionScope(raw.actionScope)
752
753
  : undefined;
753
754
  if (hasActionScope && !actionScope)
754
755
  return null;
756
+ if (hasSubmissionExpiresAt &&
757
+ (typeof raw.submissionExpiresAt !== "number" ||
758
+ !Number.isFinite(raw.submissionExpiresAt))) {
759
+ return null;
760
+ }
755
761
  return {
756
762
  message,
757
763
  context: typeof raw.context === "string" ? raw.context : undefined,
@@ -771,6 +777,9 @@ export function parseSubmitChatMessage(event) {
771
777
  attachments: parseSubmitChatAttachments(raw.attachments),
772
778
  requestMode: normalizeAgentChatRequestMode(raw.requestMode ?? raw.mode),
773
779
  submitMessageId: typeof raw.submitMessageId === "string" ? raw.submitMessageId : undefined,
780
+ ...(hasSubmissionExpiresAt
781
+ ? { submissionExpiresAt: raw.submissionExpiresAt }
782
+ : {}),
774
783
  usageLabel: nonEmptyString(raw.usageLabel),
775
784
  approvedToolCalls: parseSubmitChatApprovedToolCalls(raw.approvedToolCalls),
776
785
  };