@agent-native/core 0.101.10 → 0.101.12
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 +12 -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/coding-tools/run-code.ts +65 -12
- package/corpus/core/src/extensions/actions.ts +141 -7
- 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/analytics/.agents/skills/dashboard-management/SKILL.md +59 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +5 -2
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +13 -4
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +63 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +30 -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/coding-tools/run-code.js +64 -11
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +129 -7
- package/dist/extensions/actions.js.map +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 +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- 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/transcribe-voice.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 =
|
|
@@ -740,8 +740,18 @@ function handleBridgeRequest(
|
|
|
740
740
|
|
|
741
741
|
// Enforce allowlist.
|
|
742
742
|
const entry = actions[toolName];
|
|
743
|
+
// Unknown/mistyped tool: report "not registered" (404) before the
|
|
744
|
+
// access-control branch below. Otherwise an undefined `entry` falls into the
|
|
745
|
+
// allowlist 403 and returns a misleading access error for a tool that simply
|
|
746
|
+
// does not exist. (Bridge-allowlisted tools always have an entry, so this
|
|
747
|
+
// cannot mask a legitimate allowlisted call.)
|
|
748
|
+
if (!entry) {
|
|
749
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
750
|
+
res.end(JSON.stringify({ error: `Tool "${toolName}" is not registered.` }));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
743
753
|
const isReadOnlyAction =
|
|
744
|
-
entry
|
|
754
|
+
entry.readOnly === true &&
|
|
745
755
|
entry.agentTool !== false &&
|
|
746
756
|
entry.toolCallable !== false;
|
|
747
757
|
if (
|
|
@@ -749,21 +759,24 @@ function handleBridgeRequest(
|
|
|
749
759
|
!extraTools.has(toolName) &&
|
|
750
760
|
!isReadOnlyAction
|
|
751
761
|
) {
|
|
762
|
+
// A registered, agent-exposed action that isn't read-only is a mutation.
|
|
763
|
+
// (Unknown tools already returned 404 above, so `entry` is defined here.)
|
|
764
|
+
// Point the caller at the native tool path instead of leaving them to guess
|
|
765
|
+
// (the common trap: retrying create-extension/update-extension through
|
|
766
|
+
// appAction, which cannot work).
|
|
767
|
+
const isMutatingAction =
|
|
768
|
+
entry.agentTool !== false && entry.readOnly !== true;
|
|
752
769
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
753
770
|
res.end(
|
|
754
771
|
JSON.stringify({
|
|
755
|
-
error:
|
|
772
|
+
error: isMutatingAction
|
|
773
|
+
? `Tool "${toolName}" is a mutating action and cannot be called from run-code (appAction only exposes read-only actions). Call "${toolName}" directly as a native tool. For large content bodies, stage the content and pass "contentFromAttachment" instead of an inline string rather than routing it through run-code.`
|
|
774
|
+
: `Tool "${toolName}" is not an agent-exposed read-only action or sandbox bridge allowlisted tool.`,
|
|
756
775
|
}),
|
|
757
776
|
);
|
|
758
777
|
return;
|
|
759
778
|
}
|
|
760
779
|
|
|
761
|
-
if (!entry) {
|
|
762
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
763
|
-
res.end(JSON.stringify({ error: `Tool "${toolName}" is not registered.` }));
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
780
|
const toolArgs = parsed.args ?? {};
|
|
768
781
|
usedTools.add(toolName);
|
|
769
782
|
// Run the tool with the parent request context so auth/org/owner resolution
|
|
@@ -1534,12 +1547,52 @@ async function webRead(url, init = {}) {
|
|
|
1534
1547
|
/**
|
|
1535
1548
|
* Read a Resources-backed workspace file by path. Returns the file content as
|
|
1536
1549
|
* a string, or null if not found.
|
|
1537
|
-
*
|
|
1550
|
+
*
|
|
1551
|
+
* By default this returns the WHOLE file: the underlying store caps a single
|
|
1552
|
+
* read at 100k chars, so this auto-pages across chunks and concatenates them,
|
|
1553
|
+
* so callers never get a silently truncated body. Pass an explicit \`offset\`
|
|
1554
|
+
* or \`maxChars\` to take manual control of a single page instead.
|
|
1538
1555
|
*/
|
|
1539
1556
|
async function workspaceRead(path, opts = {}) {
|
|
1540
|
-
|
|
1541
|
-
if (
|
|
1542
|
-
|
|
1557
|
+
// Explicit paging requested → single read, caller owns the window.
|
|
1558
|
+
if (opts.offset !== undefined || opts.maxChars !== undefined) {
|
|
1559
|
+
const parsed = await workspaceReadMeta(path, opts);
|
|
1560
|
+
if (parsed && parsed.ok === false) return null;
|
|
1561
|
+
return parsed && typeof parsed.content === "string" ? parsed.content : null;
|
|
1562
|
+
}
|
|
1563
|
+
// Default → assemble the full file by paging until the store reports no more.
|
|
1564
|
+
let offset = 0;
|
|
1565
|
+
let out = "";
|
|
1566
|
+
let found = false;
|
|
1567
|
+
let complete = false;
|
|
1568
|
+
// Bounded loop (files cap at 2 MB / 100k-char reads) so a misbehaving store
|
|
1569
|
+
// can never spin forever.
|
|
1570
|
+
for (let i = 0; i < 512; i++) {
|
|
1571
|
+
const parsed = await workspaceReadMeta(path, { offset, maxChars: 100000 });
|
|
1572
|
+
// A failed/missing page aborts. If it failed before the first page we never
|
|
1573
|
+
// found the file (return null below); if it failed part-way through a
|
|
1574
|
+
// truncated read, complete stays false and we return null rather than a
|
|
1575
|
+
// silently truncated body.
|
|
1576
|
+
if (!parsed || parsed.ok === false) break;
|
|
1577
|
+
found = true;
|
|
1578
|
+
const chunk = typeof parsed.content === "string" ? parsed.content : "";
|
|
1579
|
+
out += chunk;
|
|
1580
|
+
if (!parsed.truncated) {
|
|
1581
|
+
complete = true;
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
const next =
|
|
1585
|
+
typeof parsed.nextOffset === "number"
|
|
1586
|
+
? parsed.nextOffset
|
|
1587
|
+
: offset + chunk.length;
|
|
1588
|
+
if (next <= offset) break; // no forward progress; stop rather than loop
|
|
1589
|
+
offset = next;
|
|
1590
|
+
}
|
|
1591
|
+
if (!found) return null;
|
|
1592
|
+
// Only hand back a body we know is whole. A truncated read followed by a
|
|
1593
|
+
// failed/stalled page would otherwise corrupt a large clone with a partial
|
|
1594
|
+
// prefix and no error signal.
|
|
1595
|
+
return complete ? out : null;
|
|
1543
1596
|
}
|
|
1544
1597
|
|
|
1545
1598
|
/**
|