@agent-native/core 0.101.10 → 0.101.11
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +23 -1
- package/corpus/core/src/a2a/handlers.ts +65 -1
- package/corpus/core/src/a2a/server.ts +124 -0
- package/corpus/core/src/a2a/task-store.ts +253 -0
- package/corpus/core/src/a2a/types.ts +26 -0
- package/corpus/core/src/agent/production-agent.ts +5 -2
- package/corpus/core/src/mcp/builtin-tools.ts +25 -1
- package/corpus/core/src/scripts/call-agent.ts +22 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
- package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
- package/corpus/templates/content/app/global.css +53 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/content/app/i18n-data.ts +163 -0
- package/corpus/templates/content/app/root.tsx +2 -0
- package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/shared/document-export.ts +296 -25
- package/corpus/templates/content/shared/inline-math.ts +128 -0
- package/corpus/templates/content/shared/math-rendering.ts +50 -0
- package/corpus/templates/content/shared/nfm.ts +17 -14
- package/corpus/templates/content/vite.config.ts +0 -1
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
- package/corpus/templates/mail/AGENTS.md +21 -19
- package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
- package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
- package/corpus/templates/mail/server/handlers/media.ts +84 -61
- package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
- package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
- package/corpus/templates/mail/server/plugins/auth.ts +9 -1
- package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
- package/dist/a2a/client.d.ts +5 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +18 -1
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +57 -8
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +87 -1
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/task-store.d.ts +27 -0
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +216 -0
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +24 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +5 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +22 -3
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/scripts/call-agent.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +19 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +71 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
|
@@ -24,6 +24,23 @@ async function ensureTable(): Promise<void> {
|
|
|
24
24
|
updated_at ${intType()} NOT NULL
|
|
25
25
|
)
|
|
26
26
|
`;
|
|
27
|
+
const createApprovalsSql = `
|
|
28
|
+
CREATE TABLE IF NOT EXISTS a2a_approvals (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
task_id TEXT NOT NULL UNIQUE,
|
|
31
|
+
owner_email TEXT NOT NULL,
|
|
32
|
+
org_id TEXT,
|
|
33
|
+
tool_name TEXT NOT NULL,
|
|
34
|
+
tool_input TEXT NOT NULL,
|
|
35
|
+
approval_key TEXT NOT NULL,
|
|
36
|
+
call_id TEXT NOT NULL,
|
|
37
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
38
|
+
result TEXT,
|
|
39
|
+
expires_at ${intType()} NOT NULL,
|
|
40
|
+
created_at ${intType()} NOT NULL,
|
|
41
|
+
updated_at ${intType()} NOT NULL
|
|
42
|
+
)
|
|
43
|
+
`;
|
|
27
44
|
|
|
28
45
|
if (isPostgres()) {
|
|
29
46
|
// PG-guard: probe information_schema before issuing DDL to avoid ACCESS
|
|
@@ -40,6 +57,7 @@ async function ensureTable(): Promise<void> {
|
|
|
40
57
|
"owner_email",
|
|
41
58
|
`ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS owner_email TEXT`,
|
|
42
59
|
);
|
|
60
|
+
await ensureTableExists("a2a_approvals", createApprovalsSql);
|
|
43
61
|
return;
|
|
44
62
|
}
|
|
45
63
|
|
|
@@ -58,6 +76,7 @@ async function ensureTable(): Promise<void> {
|
|
|
58
76
|
} catch {
|
|
59
77
|
// Column already exists — expected on every restart after first run.
|
|
60
78
|
}
|
|
79
|
+
await client.execute(createApprovalsSql);
|
|
61
80
|
})().catch((err) => {
|
|
62
81
|
// Retry init on the next call after a failed startup.
|
|
63
82
|
_initPromise = undefined;
|
|
@@ -67,6 +86,240 @@ async function ensureTable(): Promise<void> {
|
|
|
67
86
|
return _initPromise;
|
|
68
87
|
}
|
|
69
88
|
|
|
89
|
+
export interface A2AApprovalRecord {
|
|
90
|
+
id: string;
|
|
91
|
+
taskId: string;
|
|
92
|
+
ownerEmail: string;
|
|
93
|
+
orgId: string | null;
|
|
94
|
+
tool: string;
|
|
95
|
+
input: unknown;
|
|
96
|
+
approvalKey: string;
|
|
97
|
+
callId: string;
|
|
98
|
+
status: "pending" | "processing" | "completed" | "failed";
|
|
99
|
+
result: string | null;
|
|
100
|
+
expiresAt: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function withDbTransaction<T>(
|
|
104
|
+
client: ReturnType<typeof getDbExec>,
|
|
105
|
+
fn: (tx: ReturnType<typeof getDbExec>) => Promise<T>,
|
|
106
|
+
): Promise<T> {
|
|
107
|
+
if (client.transaction) return client.transaction(fn);
|
|
108
|
+
await client.execute(isPostgres() ? "BEGIN" : "BEGIN IMMEDIATE");
|
|
109
|
+
try {
|
|
110
|
+
const result = await fn(client);
|
|
111
|
+
await client.execute("COMMIT");
|
|
112
|
+
return result;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
await client.execute("ROLLBACK").catch(() => {});
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function approvalFromRow(row: any): A2AApprovalRecord {
|
|
120
|
+
return {
|
|
121
|
+
id: String(row.id),
|
|
122
|
+
taskId: String(row.task_id),
|
|
123
|
+
ownerEmail: String(row.owner_email),
|
|
124
|
+
orgId: row.org_id ? String(row.org_id) : null,
|
|
125
|
+
tool: String(row.tool_name),
|
|
126
|
+
input: JSON.parse(String(row.tool_input)),
|
|
127
|
+
approvalKey: String(row.approval_key),
|
|
128
|
+
callId: String(row.call_id),
|
|
129
|
+
status: row.status,
|
|
130
|
+
result: row.result ? String(row.result) : null,
|
|
131
|
+
expiresAt: Number(row.expires_at),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function createA2AApproval(input: {
|
|
136
|
+
taskId: string;
|
|
137
|
+
ownerEmail: string;
|
|
138
|
+
orgId?: string | null;
|
|
139
|
+
tool: string;
|
|
140
|
+
toolInput: unknown;
|
|
141
|
+
approvalKey: string;
|
|
142
|
+
callId: string;
|
|
143
|
+
ttlMs?: number;
|
|
144
|
+
}): Promise<A2AApprovalRecord> {
|
|
145
|
+
await ensureTable();
|
|
146
|
+
const client = getDbExec();
|
|
147
|
+
const id = crypto.randomUUID();
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const expiresAt = now + (input.ttlMs ?? 15 * 60_000);
|
|
150
|
+
await client.execute({
|
|
151
|
+
sql: `INSERT INTO a2a_approvals (id, task_id, owner_email, org_id, tool_name, tool_input, approval_key, call_id, status, expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)`,
|
|
152
|
+
args: [
|
|
153
|
+
id,
|
|
154
|
+
input.taskId,
|
|
155
|
+
input.ownerEmail,
|
|
156
|
+
input.orgId ?? null,
|
|
157
|
+
input.tool,
|
|
158
|
+
JSON.stringify(input.toolInput ?? {}),
|
|
159
|
+
input.approvalKey,
|
|
160
|
+
input.callId,
|
|
161
|
+
expiresAt,
|
|
162
|
+
now,
|
|
163
|
+
now,
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
id,
|
|
168
|
+
taskId: input.taskId,
|
|
169
|
+
ownerEmail: input.ownerEmail,
|
|
170
|
+
orgId: input.orgId ?? null,
|
|
171
|
+
tool: input.tool,
|
|
172
|
+
input: input.toolInput ?? {},
|
|
173
|
+
approvalKey: input.approvalKey,
|
|
174
|
+
callId: input.callId,
|
|
175
|
+
status: "pending",
|
|
176
|
+
result: null,
|
|
177
|
+
expiresAt,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function getA2AApprovalForOwner(
|
|
182
|
+
id: string,
|
|
183
|
+
ownerEmail: string,
|
|
184
|
+
orgId?: string | null,
|
|
185
|
+
): Promise<A2AApprovalRecord | null> {
|
|
186
|
+
await ensureTable();
|
|
187
|
+
const { rows } = await getDbExec().execute({
|
|
188
|
+
sql: `SELECT * FROM a2a_approvals WHERE id = ? AND owner_email = ? AND (org_id IS NULL OR org_id = ?)`,
|
|
189
|
+
args: [id, ownerEmail, orgId ?? null],
|
|
190
|
+
});
|
|
191
|
+
return rows[0] ? approvalFromRow(rows[0]) : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function claimA2AApproval(
|
|
195
|
+
id: string,
|
|
196
|
+
ownerEmail: string,
|
|
197
|
+
orgId?: string | null,
|
|
198
|
+
): Promise<A2AApprovalRecord | null> {
|
|
199
|
+
await ensureTable();
|
|
200
|
+
const client = getDbExec();
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
class ClaimRejected extends Error {}
|
|
203
|
+
try {
|
|
204
|
+
return await withDbTransaction(client, async (tx) => {
|
|
205
|
+
const approvalUpdate = await tx.execute({
|
|
206
|
+
sql: `UPDATE a2a_approvals SET status = 'processing', updated_at = ? WHERE id = ? AND owner_email = ? AND status = 'pending' AND expires_at > ? AND (org_id IS NULL OR org_id = ?)`,
|
|
207
|
+
args: [now, id, ownerEmail, now, orgId ?? null],
|
|
208
|
+
});
|
|
209
|
+
if (getAffectedRowCount(approvalUpdate) === 0) throw new ClaimRejected();
|
|
210
|
+
const { rows } = await tx.execute({
|
|
211
|
+
sql: `SELECT * FROM a2a_approvals WHERE id = ? AND owner_email = ? AND status = 'processing'`,
|
|
212
|
+
args: [id, ownerEmail],
|
|
213
|
+
});
|
|
214
|
+
if (!rows[0]) throw new ClaimRejected();
|
|
215
|
+
const approval = approvalFromRow(rows[0]);
|
|
216
|
+
const timestamp = new Date(now).toISOString();
|
|
217
|
+
const runningMessage: Message = {
|
|
218
|
+
role: "agent",
|
|
219
|
+
parts: [{ type: "text", text: "Approved action is running." }],
|
|
220
|
+
};
|
|
221
|
+
const taskUpdate = await tx.execute({
|
|
222
|
+
sql: `UPDATE a2a_tasks SET status_state = 'working', status_message = ?, status_timestamp = ?, updated_at = ? WHERE id = ? AND owner_email = ? AND status_state = 'input-required'`,
|
|
223
|
+
args: [
|
|
224
|
+
JSON.stringify(runningMessage),
|
|
225
|
+
timestamp,
|
|
226
|
+
now,
|
|
227
|
+
approval.taskId,
|
|
228
|
+
ownerEmail,
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
if (getAffectedRowCount(taskUpdate) === 0) throw new ClaimRejected();
|
|
232
|
+
return approval;
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error instanceof ClaimRejected) return null;
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function settleA2AApproval(
|
|
241
|
+
id: string,
|
|
242
|
+
status: "completed" | "failed",
|
|
243
|
+
resultText: string,
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
await ensureTable();
|
|
246
|
+
const client = getDbExec();
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const message: Message = {
|
|
249
|
+
role: "agent",
|
|
250
|
+
parts: [{ type: "text", text: resultText }],
|
|
251
|
+
};
|
|
252
|
+
await withDbTransaction(client, async (tx) => {
|
|
253
|
+
const { rows } = await tx.execute({
|
|
254
|
+
sql: `SELECT task_id FROM a2a_approvals WHERE id = ? AND status = 'processing'`,
|
|
255
|
+
args: [id],
|
|
256
|
+
});
|
|
257
|
+
if (!rows[0]) throw new Error("Approval is not processing");
|
|
258
|
+
const taskId = String((rows[0] as any).task_id);
|
|
259
|
+
const { rows: taskRows } = await tx.execute({
|
|
260
|
+
sql: `SELECT history FROM a2a_tasks WHERE id = ? AND status_state = 'working'`,
|
|
261
|
+
args: [taskId],
|
|
262
|
+
});
|
|
263
|
+
if (!taskRows[0]) {
|
|
264
|
+
throw new Error("Approval task is no longer awaiting settlement");
|
|
265
|
+
}
|
|
266
|
+
const history = JSON.parse(String((taskRows[0] as any).history));
|
|
267
|
+
history.push(message);
|
|
268
|
+
const timestamp = new Date(now).toISOString();
|
|
269
|
+
const taskUpdate = await tx.execute({
|
|
270
|
+
sql: `UPDATE a2a_tasks SET status_state = ?, status_message = ?, status_timestamp = ?, history = ?, updated_at = ? WHERE id = ? AND status_state = 'working'`,
|
|
271
|
+
args: [
|
|
272
|
+
status,
|
|
273
|
+
JSON.stringify(message),
|
|
274
|
+
timestamp,
|
|
275
|
+
JSON.stringify(history),
|
|
276
|
+
now,
|
|
277
|
+
taskId,
|
|
278
|
+
],
|
|
279
|
+
});
|
|
280
|
+
if (getAffectedRowCount(taskUpdate) === 0) {
|
|
281
|
+
throw new Error("Approval task settlement lost its state claim");
|
|
282
|
+
}
|
|
283
|
+
const approvalUpdate = await tx.execute({
|
|
284
|
+
sql: `UPDATE a2a_approvals SET status = ?, result = ?, updated_at = ? WHERE id = ? AND status = 'processing'`,
|
|
285
|
+
args: [status, resultText, now, id],
|
|
286
|
+
});
|
|
287
|
+
if (getAffectedRowCount(approvalUpdate) === 0) {
|
|
288
|
+
throw new Error("Approval settlement lost its state claim");
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function pauseProcessingA2ATask(
|
|
294
|
+
id: string,
|
|
295
|
+
message: Message,
|
|
296
|
+
): Promise<Task | null> {
|
|
297
|
+
await ensureTable();
|
|
298
|
+
const client = getDbExec();
|
|
299
|
+
const { rows } = await client.execute({
|
|
300
|
+
sql: `SELECT * FROM a2a_tasks WHERE id = ? AND status_state = 'processing'`,
|
|
301
|
+
args: [id],
|
|
302
|
+
});
|
|
303
|
+
if (!rows[0]) return null;
|
|
304
|
+
const task = taskFromRow(rows[0]);
|
|
305
|
+
task.history?.push(message);
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
const timestamp = new Date(now).toISOString();
|
|
308
|
+
const result = await client.execute({
|
|
309
|
+
sql: `UPDATE a2a_tasks SET status_state = 'input-required', status_message = ?, status_timestamp = ?, history = ?, updated_at = ? WHERE id = ? AND status_state = 'processing'`,
|
|
310
|
+
args: [
|
|
311
|
+
JSON.stringify(message),
|
|
312
|
+
timestamp,
|
|
313
|
+
JSON.stringify(task.history ?? []),
|
|
314
|
+
now,
|
|
315
|
+
id,
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
if (getAffectedRowCount(result) === 0) return null;
|
|
319
|
+
task.status = { state: "input-required", message, timestamp };
|
|
320
|
+
return task;
|
|
321
|
+
}
|
|
322
|
+
|
|
70
323
|
function taskFromRow(row: any): Task & { ownerEmail?: string | null } {
|
|
71
324
|
return {
|
|
72
325
|
id: row.id as string,
|
|
@@ -126,6 +126,12 @@ export interface JsonRpcResponse {
|
|
|
126
126
|
error?: JsonRpcError;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** One exact downstream action explicitly authorized in the caller's chat. */
|
|
130
|
+
export interface A2AApprovedAction {
|
|
131
|
+
tool: string;
|
|
132
|
+
input: unknown;
|
|
133
|
+
}
|
|
134
|
+
|
|
129
135
|
// --- Framework config ---
|
|
130
136
|
|
|
131
137
|
export interface A2AHandlerContext {
|
|
@@ -135,12 +141,27 @@ export interface A2AHandlerContext {
|
|
|
135
141
|
metadata?: Record<string, unknown>;
|
|
136
142
|
/** Current H3 event when the handler is running inside an HTTP request. */
|
|
137
143
|
event?: unknown;
|
|
144
|
+
/** Exact one-time action grants from a JWT-authenticated caller. */
|
|
145
|
+
approvedActions?: A2AApprovedAction[];
|
|
138
146
|
writeArtifact: (name: string, content: string, mimeType?: string) => string;
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
export interface A2AHandlerResult {
|
|
142
150
|
message: Message;
|
|
143
151
|
artifacts?: Artifact[];
|
|
152
|
+
/** Optional non-terminal state requested by the handler. */
|
|
153
|
+
taskState?: Extract<TaskState, "input-required">;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface A2AApprovalExecution {
|
|
157
|
+
id: string;
|
|
158
|
+
taskId: string;
|
|
159
|
+
ownerEmail: string;
|
|
160
|
+
orgId?: string | null;
|
|
161
|
+
tool: string;
|
|
162
|
+
input: unknown;
|
|
163
|
+
approvalKey: string;
|
|
164
|
+
callId: string;
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
export type A2AHandler = (
|
|
@@ -160,4 +181,9 @@ export interface A2AConfig {
|
|
|
160
181
|
streaming?: boolean;
|
|
161
182
|
/** Route async A2A work through the app's durable background worker when available. */
|
|
162
183
|
durableBackgroundRuns?: boolean;
|
|
184
|
+
/** Execute a persisted, human-approved A2A tool call. */
|
|
185
|
+
executeApproval?: (approval: A2AApprovalExecution) => Promise<{
|
|
186
|
+
status: "completed" | "failed";
|
|
187
|
+
output: string;
|
|
188
|
+
}>;
|
|
163
189
|
}
|
|
@@ -2747,7 +2747,7 @@ function stableStringify(value: unknown): string {
|
|
|
2747
2747
|
.join(",")}}`;
|
|
2748
2748
|
}
|
|
2749
2749
|
|
|
2750
|
-
function toolCallCacheKey(toolName: string, input: unknown): string {
|
|
2750
|
+
export function toolCallCacheKey(toolName: string, input: unknown): string {
|
|
2751
2751
|
return `${toolName}:${stableStringify(normalizeToolCallInputForHistory(input))}`;
|
|
2752
2752
|
}
|
|
2753
2753
|
|
|
@@ -4180,7 +4180,10 @@ export async function runAgentLoop(opts: {
|
|
|
4180
4180
|
// instead of executing. The action's side effect never happens until a
|
|
4181
4181
|
// human re-issues the turn approving this call's stable key.
|
|
4182
4182
|
const approvalKey = toolCallCacheKey(toolCall.name, toolCall.input);
|
|
4183
|
-
|
|
4183
|
+
// Consume a grant on its first exact match. A second identical call in
|
|
4184
|
+
// the same continuation must request its own human approval.
|
|
4185
|
+
const wasApproved = approvedToolCallKeys.delete(approvalKey);
|
|
4186
|
+
if (actionEntry.needsApproval && !wasApproved) {
|
|
4184
4187
|
let mustApprove = false;
|
|
4185
4188
|
try {
|
|
4186
4189
|
mustApprove =
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* `mountMCP` — the Node bits are dynamically imported inside `run()`.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import type { Task } from "../a2a/types.js";
|
|
34
|
+
import type { A2AApprovedAction, Task } from "../a2a/types.js";
|
|
35
35
|
import type { ActionEntry } from "../agent/production-agent.js";
|
|
36
36
|
import type { ActionTool } from "../agent/types.js";
|
|
37
37
|
import { getConfiguredAppBasePath } from "../server/app-base-path.js";
|
|
@@ -404,6 +404,7 @@ async function submitAskAppA2ATask(
|
|
|
404
404
|
route: AskAppRoute,
|
|
405
405
|
message: string,
|
|
406
406
|
maxWaitMs: number,
|
|
407
|
+
approvedActions?: A2AApprovedAction[],
|
|
407
408
|
): Promise<AskAppTaskResult> {
|
|
408
409
|
const deadline = maxWaitMs > 0 ? Date.now() + maxWaitMs : undefined;
|
|
409
410
|
const { client, metadata } = await createA2AClientForAskApp(
|
|
@@ -419,6 +420,7 @@ async function submitAskAppA2ATask(
|
|
|
419
420
|
{
|
|
420
421
|
async: true,
|
|
421
422
|
metadata,
|
|
423
|
+
...(approvedActions?.length ? { approvedActions } : {}),
|
|
422
424
|
},
|
|
423
425
|
);
|
|
424
426
|
const finalOrRunning = await waitForA2ATask(client, task, deadline);
|
|
@@ -890,6 +892,7 @@ async function routeAskOverA2A(
|
|
|
890
892
|
durable?: boolean;
|
|
891
893
|
maxWaitMs?: number;
|
|
892
894
|
requestOrigin?: string;
|
|
895
|
+
approvedActions?: A2AApprovedAction[];
|
|
893
896
|
},
|
|
894
897
|
): Promise<
|
|
895
898
|
{ app: string; routedVia: "a2a"; response: string } | AskAppTaskResult
|
|
@@ -904,6 +907,7 @@ async function routeAskOverA2A(
|
|
|
904
907
|
},
|
|
905
908
|
message,
|
|
906
909
|
options.maxWaitMs ?? ASK_APP_DEFAULT_INLINE_WAIT_MS,
|
|
910
|
+
options.approvedActions,
|
|
907
911
|
);
|
|
908
912
|
}
|
|
909
913
|
const { callAgent } = await import("../a2a/client.js");
|
|
@@ -919,6 +923,7 @@ async function routeAskOverA2A(
|
|
|
919
923
|
orgDomain: auth.orgDomain,
|
|
920
924
|
orgSecret: auth.orgSecret,
|
|
921
925
|
requestOrigin: options?.requestOrigin,
|
|
926
|
+
approvedActions: options?.approvedActions,
|
|
922
927
|
// Bound the wait — cross-app A2A polls async by default.
|
|
923
928
|
timeoutMs: 5 * 60_000,
|
|
924
929
|
});
|
|
@@ -1012,6 +1017,19 @@ function askAppTool(
|
|
|
1012
1017
|
description:
|
|
1013
1018
|
"Maximum time to wait inline before returning a taskId. Hosted MCP clamps this to 25000ms.",
|
|
1014
1019
|
},
|
|
1020
|
+
approvedActions: {
|
|
1021
|
+
type: "array",
|
|
1022
|
+
description:
|
|
1023
|
+
"Exact downstream tool calls the user explicitly authorized in this chat. Never infer authorization or include a different action.",
|
|
1024
|
+
items: {
|
|
1025
|
+
type: "object",
|
|
1026
|
+
properties: {
|
|
1027
|
+
tool: { type: "string" },
|
|
1028
|
+
input: { type: "object", additionalProperties: true },
|
|
1029
|
+
},
|
|
1030
|
+
required: ["tool", "input"],
|
|
1031
|
+
},
|
|
1032
|
+
} as any,
|
|
1015
1033
|
},
|
|
1016
1034
|
["message"],
|
|
1017
1035
|
),
|
|
@@ -1024,6 +1042,9 @@ function askAppTool(
|
|
|
1024
1042
|
const maxWaitMs = isExplicitAsyncAsk(args.async)
|
|
1025
1043
|
? 0
|
|
1026
1044
|
: boundedAskAppWaitMs(args.maxWaitMs);
|
|
1045
|
+
const approvedActions = Array.isArray(args.approvedActions)
|
|
1046
|
+
? (args.approvedActions as A2AApprovedAction[])
|
|
1047
|
+
: undefined;
|
|
1027
1048
|
|
|
1028
1049
|
// Cross-app: the caller named a *different* workspace app. Route the
|
|
1029
1050
|
// message to THAT app's agent over A2A (its `/_agent-native/a2a`
|
|
@@ -1040,6 +1061,7 @@ function askAppTool(
|
|
|
1040
1061
|
durable: useDurableA2A,
|
|
1041
1062
|
maxWaitMs,
|
|
1042
1063
|
requestOrigin: targetApp.origin,
|
|
1064
|
+
approvedActions,
|
|
1043
1065
|
},
|
|
1044
1066
|
);
|
|
1045
1067
|
} catch (err: any) {
|
|
@@ -1074,6 +1096,7 @@ function askAppTool(
|
|
|
1074
1096
|
durable: useDurableA2A,
|
|
1075
1097
|
maxWaitMs,
|
|
1076
1098
|
requestOrigin: dirMatch.url,
|
|
1099
|
+
approvedActions,
|
|
1077
1100
|
},
|
|
1078
1101
|
);
|
|
1079
1102
|
} catch (err: any) {
|
|
@@ -1120,6 +1143,7 @@ function askAppTool(
|
|
|
1120
1143
|
},
|
|
1121
1144
|
message,
|
|
1122
1145
|
maxWaitMs,
|
|
1146
|
+
approvedActions,
|
|
1123
1147
|
);
|
|
1124
1148
|
}
|
|
1125
1149
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
shouldPreferGlobalA2ASecret,
|
|
7
7
|
signA2AToken,
|
|
8
8
|
} from "../a2a/client.js";
|
|
9
|
-
import type { Task } from "../a2a/types.js";
|
|
9
|
+
import type { A2AApprovedAction, Task } from "../a2a/types.js";
|
|
10
10
|
import {
|
|
11
11
|
formatLlmCredentialErrorMessage,
|
|
12
12
|
isLlmCredentialError,
|
|
@@ -112,17 +112,34 @@ export const tool: ActionTool = {
|
|
|
112
112
|
type: "string",
|
|
113
113
|
description: "The message/question to send to the other agent",
|
|
114
114
|
},
|
|
115
|
+
approvedActions: {
|
|
116
|
+
type: "array",
|
|
117
|
+
description:
|
|
118
|
+
"Exact downstream tool calls the current user explicitly authorized in this chat. Never infer authorization or include a broader/different action.",
|
|
119
|
+
items: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
tool: { type: "string" },
|
|
123
|
+
input: { type: "object", additionalProperties: true },
|
|
124
|
+
},
|
|
125
|
+
required: ["tool", "input"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
115
128
|
},
|
|
116
129
|
required: ["agent", "message"],
|
|
117
130
|
},
|
|
118
131
|
};
|
|
119
132
|
|
|
120
133
|
export async function run(
|
|
121
|
-
args: Record<string,
|
|
134
|
+
args: Record<string, unknown>,
|
|
122
135
|
context?: ActionRunContext,
|
|
123
136
|
selfAppId?: string,
|
|
124
137
|
): Promise<string> {
|
|
125
|
-
const
|
|
138
|
+
const agentIdOrName = String(args.agent ?? "");
|
|
139
|
+
const message = String(args.message ?? "");
|
|
140
|
+
const approvedActions = Array.isArray(args.approvedActions)
|
|
141
|
+
? (args.approvedActions as A2AApprovedAction[])
|
|
142
|
+
: undefined;
|
|
126
143
|
|
|
127
144
|
if (!agentIdOrName) return "Error: --agent is required";
|
|
128
145
|
if (!message) return "Error: --message is required";
|
|
@@ -320,6 +337,7 @@ export async function run(
|
|
|
320
337
|
userEmail: callerEmail,
|
|
321
338
|
orgDomain: callerOrgDomain,
|
|
322
339
|
orgSecret: callerOrgSecret,
|
|
340
|
+
approvedActions,
|
|
323
341
|
onUpdate: onRemotePollUpdate,
|
|
324
342
|
...(callTimeoutMs
|
|
325
343
|
? {
|
|
@@ -408,6 +426,7 @@ export async function run(
|
|
|
408
426
|
userEmail: email,
|
|
409
427
|
orgDomain: domain,
|
|
410
428
|
orgSecret,
|
|
429
|
+
approvedActions,
|
|
411
430
|
});
|
|
412
431
|
const sanitized =
|
|
413
432
|
formatDownstreamLlmCredentialFailure(agent.name, response) ?? response;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import nodePath from "node:path";
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -19,7 +20,10 @@ import {
|
|
|
19
20
|
isTrustedLocalRuntime,
|
|
20
21
|
} from "../a2a/auth-policy.js";
|
|
21
22
|
import { applyAgentTextEventToBuffer } from "../a2a/response-text.js";
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
createA2AApproval,
|
|
25
|
+
updateTaskStatusMessage,
|
|
26
|
+
} from "../a2a/task-store.js";
|
|
23
27
|
import type { ActionHttpConfig } from "../action.js";
|
|
24
28
|
import {
|
|
25
29
|
canUpdateAgentAppModelDefaultSettings,
|
|
@@ -52,6 +56,7 @@ import {
|
|
|
52
56
|
createProductionAgentHandler,
|
|
53
57
|
actionsToEngineTools,
|
|
54
58
|
executeAgentToolCall,
|
|
59
|
+
toolCallCacheKey,
|
|
55
60
|
getActiveRunForThreadAsync,
|
|
56
61
|
abortRunDurably,
|
|
57
62
|
subscribeToRun,
|
|
@@ -1171,6 +1176,25 @@ export function createAgentChatPlugin(
|
|
|
1171
1176
|
publicSkillsOnly: true,
|
|
1172
1177
|
streaming: true,
|
|
1173
1178
|
durableBackgroundRuns: options?.durableBackgroundRuns,
|
|
1179
|
+
executeApproval: async (approval) => {
|
|
1180
|
+
const result = await executeAgentToolCall({
|
|
1181
|
+
actions: mcpFullActions ?? allScripts,
|
|
1182
|
+
name: approval.tool,
|
|
1183
|
+
input: approval.input,
|
|
1184
|
+
callId: approval.callId,
|
|
1185
|
+
ownerEmail: approval.ownerEmail,
|
|
1186
|
+
orgId: approval.orgId ?? null,
|
|
1187
|
+
approvedToolCalls: [approval.approvalKey],
|
|
1188
|
+
});
|
|
1189
|
+
if (result.status === "approval_required") {
|
|
1190
|
+
return {
|
|
1191
|
+
status: "failed" as const,
|
|
1192
|
+
output:
|
|
1193
|
+
"The approved action was unexpectedly gated again and did not run.",
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
return { status: result.status, output: result.output };
|
|
1197
|
+
},
|
|
1174
1198
|
handler: async function* (message, context) {
|
|
1175
1199
|
// Resolve the caller's identity for user-scoped data access.
|
|
1176
1200
|
// Priority: A2A-JWT verified email (set by the A2A handler in
|
|
@@ -1495,6 +1519,9 @@ export function createAgentChatPlugin(
|
|
|
1495
1519
|
// scope when a processor hop or alternate runner is involved.
|
|
1496
1520
|
ownerEmail: userEmail,
|
|
1497
1521
|
orgId: getRequestOrgId() ?? null,
|
|
1522
|
+
approvedToolCalls: context.approvedActions?.map((approved) =>
|
|
1523
|
+
toolCallCacheKey(approved.tool, approved.input),
|
|
1524
|
+
),
|
|
1498
1525
|
executionMode: "act",
|
|
1499
1526
|
send: (event) => {
|
|
1500
1527
|
a2aEvents.push(event);
|
|
@@ -1549,6 +1576,62 @@ export function createAgentChatPlugin(
|
|
|
1549
1576
|
},
|
|
1550
1577
|
);
|
|
1551
1578
|
|
|
1579
|
+
const approval = [...a2aEvents]
|
|
1580
|
+
.reverse()
|
|
1581
|
+
.find(
|
|
1582
|
+
(
|
|
1583
|
+
event,
|
|
1584
|
+
): event is Extract<
|
|
1585
|
+
AgentChatEvent,
|
|
1586
|
+
{ type: "approval_required" }
|
|
1587
|
+
> => event.type === "approval_required",
|
|
1588
|
+
);
|
|
1589
|
+
if (approval) {
|
|
1590
|
+
const pending = await createA2AApproval({
|
|
1591
|
+
taskId: context.taskId,
|
|
1592
|
+
ownerEmail: userEmail,
|
|
1593
|
+
orgId: getRequestOrgId() ?? null,
|
|
1594
|
+
tool: approval.tool,
|
|
1595
|
+
toolInput: approval.input,
|
|
1596
|
+
approvalKey: approval.approvalKey,
|
|
1597
|
+
callId: approval.toolCallId ?? crypto.randomUUID(),
|
|
1598
|
+
});
|
|
1599
|
+
const baseUrl = resolveArtifactBaseUrl(context.event);
|
|
1600
|
+
const approvalPath = `/_agent-native/a2a/approvals/${encodeURIComponent(pending.id)}`;
|
|
1601
|
+
const approvalUrl = baseUrl
|
|
1602
|
+
? `${baseUrl}${approvalPath}`
|
|
1603
|
+
: approvalPath;
|
|
1604
|
+
yield {
|
|
1605
|
+
role: "agent" as const,
|
|
1606
|
+
metadata: {
|
|
1607
|
+
agentNativeTaskState: "input-required",
|
|
1608
|
+
agentNativeApproval: {
|
|
1609
|
+
id: pending.id,
|
|
1610
|
+
tool: approval.tool,
|
|
1611
|
+
url: approvalUrl,
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
parts: [
|
|
1615
|
+
{
|
|
1616
|
+
type: "text" as const,
|
|
1617
|
+
text:
|
|
1618
|
+
`Human approval is required to run ${approval.tool}. ` +
|
|
1619
|
+
`Open ${approvalUrl} to review and approve this one-time action.`,
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
type: "data" as const,
|
|
1623
|
+
data: {
|
|
1624
|
+
kind: "agent-native/approval-required",
|
|
1625
|
+
approvalId: pending.id,
|
|
1626
|
+
tool: approval.tool,
|
|
1627
|
+
approvalUrl,
|
|
1628
|
+
},
|
|
1629
|
+
},
|
|
1630
|
+
],
|
|
1631
|
+
};
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1552
1635
|
const { responseText, finalText } = assembleA2AFinalResponse(
|
|
1553
1636
|
a2aEvents,
|
|
1554
1637
|
a2aToolResults,
|
|
@@ -5621,6 +5704,8 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
5621
5704
|
"/_agent-native/agent-model-defaults",
|
|
5622
5705
|
"/_agent-native/mcp",
|
|
5623
5706
|
"/mcp",
|
|
5707
|
+
"/.well-known/agent-card.json",
|
|
5708
|
+
"/_agent-native/a2a",
|
|
5624
5709
|
],
|
|
5625
5710
|
});
|
|
5626
5711
|
};
|
|
@@ -147,6 +147,31 @@ for await (const update of client.stream({
|
|
|
147
147
|
}
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
### Carrying explicit chat authorization
|
|
151
|
+
|
|
152
|
+
When the authenticated caller has an exact consequential action that the user
|
|
153
|
+
explicitly authorized in the originating chat, pass the tool name and complete
|
|
154
|
+
input as `approvedActions`. The receiver accepts these grants only from a
|
|
155
|
+
JWT-verified user identity, converts each one to the same content-addressed key
|
|
156
|
+
as its local approval gate, and consumes it once:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await client.send(message, {
|
|
160
|
+
async: true,
|
|
161
|
+
approvedActions: [
|
|
162
|
+
{
|
|
163
|
+
tool: "send-email",
|
|
164
|
+
input: { to, subject, body, attachments },
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Never infer authorization from request prose or broaden the input. A changed
|
|
171
|
+
recipient, body, attachment, or tool produces a different key and follows the
|
|
172
|
+
receiver's normal approval-required path. Static API keys and unsigned callers
|
|
173
|
+
cannot carry these grants.
|
|
174
|
+
|
|
150
175
|
## JSON-RPC Methods
|
|
151
176
|
|
|
152
177
|
| Method | Purpose | Auth required |
|