@agi-cli/server 0.1.140 → 0.1.141

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/server",
3
- "version": "0.1.140",
3
+ "version": "0.1.141",
4
4
  "description": "HTTP API server for AGI CLI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -29,8 +29,8 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@agi-cli/sdk": "0.1.140",
33
- "@agi-cli/database": "0.1.140",
32
+ "@agi-cli/sdk": "0.1.141",
33
+ "@agi-cli/database": "0.1.141",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
@@ -1,4 +1,6 @@
1
1
  export type AGIEventType =
2
+ | 'tool.approval.required'
3
+ | 'tool.approval.resolved'
2
4
  | 'solforge.payment.required'
3
5
  | 'solforge.payment.signing'
4
6
  | 'solforge.payment.complete'
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import { registerTerminalsRoutes } from './routes/terminals.ts';
16
16
  import { registerSessionFilesRoutes } from './routes/session-files.ts';
17
17
  import { registerBranchRoutes } from './routes/branch.ts';
18
18
  import { registerResearchRoutes } from './routes/research.ts';
19
+ import { registerSessionApprovalRoute } from './routes/session-approval.ts';
19
20
  import { registerSolforgeRoutes } from './routes/solforge.ts';
20
21
  import type { AgentConfigEntry } from './runtime/agent/registry.ts';
21
22
 
@@ -59,6 +60,7 @@ function initApp() {
59
60
  registerRootRoutes(app);
60
61
  registerOpenApiRoute(app);
61
62
  registerSessionsRoutes(app);
63
+ registerSessionApprovalRoute(app);
62
64
  registerSessionMessagesRoutes(app);
63
65
  registerSessionStreamRoute(app);
64
66
  registerAskRoutes(app);
@@ -128,6 +130,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
128
130
  registerRootRoutes(honoApp);
129
131
  registerOpenApiRoute(honoApp);
130
132
  registerSessionsRoutes(honoApp);
133
+ registerSessionApprovalRoute(honoApp);
131
134
  registerSessionMessagesRoutes(honoApp);
132
135
  registerSessionStreamRoute(honoApp);
133
136
  registerAskRoutes(honoApp);
@@ -225,6 +228,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
225
228
  registerRootRoutes(honoApp);
226
229
  registerOpenApiRoute(honoApp);
227
230
  registerSessionsRoutes(honoApp);
231
+ registerSessionApprovalRoute(honoApp);
228
232
  registerSessionMessagesRoutes(honoApp);
229
233
  registerSessionStreamRoute(honoApp);
230
234
  registerAskRoutes(honoApp);
@@ -11,6 +11,7 @@ export function registerDefaultsRoute(app: Hono) {
11
11
  agent?: string;
12
12
  provider?: string;
13
13
  model?: string;
14
+ toolApproval?: 'auto' | 'dangerous' | 'all';
14
15
  scope?: 'global' | 'local';
15
16
  }>();
16
17
 
@@ -19,11 +20,13 @@ export function registerDefaultsRoute(app: Hono) {
19
20
  agent: string;
20
21
  provider: string;
21
22
  model: string;
23
+ toolApproval: 'auto' | 'dangerous' | 'all';
22
24
  }> = {};
23
25
 
24
26
  if (body.agent) updates.agent = body.agent;
25
27
  if (body.provider) updates.provider = body.provider;
26
28
  if (body.model) updates.model = body.model;
29
+ if (body.toolApproval) updates.toolApproval = body.toolApproval;
27
30
 
28
31
  await setConfig(scope, updates, projectRoot);
29
32
 
@@ -52,6 +52,11 @@ export function registerMainConfigRoute(app: Hono) {
52
52
  embeddedConfig?.defaults?.model,
53
53
  cfg.defaults.model,
54
54
  ),
55
+ toolApproval: getDefault(
56
+ undefined,
57
+ embeddedConfig?.defaults?.toolApproval,
58
+ cfg.defaults.toolApproval,
59
+ ) as 'auto' | 'dangerous' | 'all',
55
60
  };
56
61
 
57
62
  return c.json({
@@ -0,0 +1,53 @@
1
+ import type { Hono } from 'hono';
2
+ import {
3
+ resolveApproval,
4
+ getPendingApprovalsForSession,
5
+ } from '../runtime/tools/approval.ts';
6
+
7
+ export function registerSessionApprovalRoute(app: Hono) {
8
+ app.post('/v1/sessions/:id/approval', async (c) => {
9
+ const sessionId = c.req.param('id');
10
+ const body = await c.req.json<{
11
+ callId: string;
12
+ approved: boolean;
13
+ }>();
14
+
15
+ if (!body.callId) {
16
+ return c.json({ ok: false, error: 'callId is required' }, 400);
17
+ }
18
+
19
+ if (typeof body.approved !== 'boolean') {
20
+ return c.json({ ok: false, error: 'approved must be a boolean' }, 400);
21
+ }
22
+
23
+ console.log('[approval-route] Received approval request', {
24
+ sessionId,
25
+ callId: body.callId,
26
+ approved: body.approved,
27
+ });
28
+
29
+ const result = resolveApproval(body.callId, body.approved);
30
+
31
+ if (!result.ok) {
32
+ return c.json(result, 404);
33
+ }
34
+
35
+ return c.json({ ok: true, callId: body.callId, approved: body.approved });
36
+ });
37
+
38
+ app.get('/v1/sessions/:id/approval/pending', async (c) => {
39
+ const sessionId = c.req.param('id');
40
+ const pending = getPendingApprovalsForSession(sessionId);
41
+
42
+ return c.json({
43
+ ok: true,
44
+ pending: pending.map((p) => ({
45
+ callId: p.callId,
46
+ toolName: p.toolName,
47
+ args: p.args,
48
+ messageId: p.messageId,
49
+ createdAt: p.createdAt,
50
+ })),
51
+ });
52
+ });
53
+ }
@@ -171,6 +171,9 @@ export async function dispatchAssistantMessage(
171
171
  );
172
172
  }
173
173
 
174
+ // Read tool approval mode from config
175
+ const toolApprovalMode = cfg.defaults.toolApproval ?? 'auto';
176
+
174
177
  enqueueAssistantRun(
175
178
  {
176
179
  sessionId,
@@ -184,6 +187,7 @@ export async function dispatchAssistantMessage(
184
187
  reasoningText,
185
188
  isCompactCommand: isCompact,
186
189
  compactionContext,
190
+ toolApprovalMode,
187
191
  },
188
192
  runSessionLoop,
189
193
  );
@@ -6,6 +6,7 @@ import { resolveOpenRouterModel } from './openrouter.ts';
6
6
  import { resolveSolforgeModel } from './solforge.ts';
7
7
  import { getZaiInstance, getZaiCodingInstance } from './zai.ts';
8
8
  import { resolveOpencodeModel } from './opencode.ts';
9
+ import { getMoonshotInstance } from './moonshot.ts';
9
10
 
10
11
  export type ProviderName = ProviderId;
11
12
 
@@ -42,5 +43,8 @@ export async function resolveModel(
42
43
  if (provider === 'zai-coding') {
43
44
  return getZaiCodingInstance(cfg, model);
44
45
  }
46
+ if (provider === 'moonshot') {
47
+ return getMoonshotInstance(cfg, model);
48
+ }
45
49
  throw new Error(`Unsupported provider: ${provider}`);
46
50
  }
@@ -0,0 +1,8 @@
1
+ import type { AGIConfig } from '@agi-cli/sdk';
2
+ import { getAuth, createMoonshotModel } from '@agi-cli/sdk';
3
+
4
+ export async function getMoonshotInstance(cfg: AGIConfig, model: string) {
5
+ const auth = await getAuth('moonshot', cfg.projectRoot);
6
+ const apiKey = auth?.type === 'api' ? auth.key : undefined;
7
+ return createMoonshotModel(model, { apiKey });
8
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ProviderName } from '../provider/index.ts';
2
2
  import { publish } from '../../events/bus.ts';
3
+ import type { ToolApprovalMode } from '../tools/approval.ts';
3
4
 
4
5
  export type RunOpts = {
5
6
  sessionId: string;
@@ -14,6 +15,7 @@ export type RunOpts = {
14
15
  abortSignal?: AbortSignal;
15
16
  isCompactCommand?: boolean;
16
17
  compactionContext?: string;
18
+ toolApprovalMode?: ToolApprovalMode;
17
19
  };
18
20
 
19
21
  export type QueuedMessage = {
@@ -47,7 +47,7 @@ export function createStepFinishHandler(
47
47
  try {
48
48
  await updateSessionTokensIncrementalFn(
49
49
  step.usage,
50
- step.experimental_providerMetadata,
50
+ step.providerMetadata,
51
51
  opts,
52
52
  db,
53
53
  );
@@ -56,7 +56,7 @@ export function createStepFinishHandler(
56
56
  try {
57
57
  await updateMessageTokensIncrementalFn(
58
58
  step.usage,
59
- step.experimental_providerMetadata,
59
+ step.providerMetadata,
60
60
  opts,
61
61
  db,
62
62
  );
@@ -4,7 +4,7 @@ export type StepFinishEvent = {
4
4
  usage?: UsageData;
5
5
  finishReason?: string;
6
6
  response?: unknown;
7
- experimental_providerMetadata?: ProviderMetadata;
7
+ providerMetadata?: ProviderMetadata;
8
8
  };
9
9
 
10
10
  export type FinishEvent = {
@@ -0,0 +1,179 @@
1
+ import { publish } from '../../events/bus.ts';
2
+
3
+ export type ToolApprovalMode = 'auto' | 'dangerous' | 'all';
4
+
5
+ export const DANGEROUS_TOOLS = new Set([
6
+ 'bash',
7
+ 'write',
8
+ 'apply_patch',
9
+ 'terminal',
10
+ 'edit',
11
+ 'git_commit',
12
+ 'git_push',
13
+ ]);
14
+
15
+ export const SAFE_TOOLS = new Set([
16
+ 'finish',
17
+ 'progress_update',
18
+ 'update_todos',
19
+ ]);
20
+
21
+ export interface PendingApproval {
22
+ callId: string;
23
+ toolName: string;
24
+ args: unknown;
25
+ sessionId: string;
26
+ messageId: string;
27
+ resolve: (approved: boolean) => void;
28
+ createdAt: number;
29
+ }
30
+
31
+ const pendingApprovals = new Map<string, PendingApproval>();
32
+
33
+ export function requiresApproval(
34
+ toolName: string,
35
+ mode: ToolApprovalMode,
36
+ ): boolean {
37
+ if (SAFE_TOOLS.has(toolName)) return false;
38
+ if (mode === 'auto') return false;
39
+ if (mode === 'all') return true;
40
+ if (mode === 'dangerous') return DANGEROUS_TOOLS.has(toolName);
41
+ return false;
42
+ }
43
+
44
+ export async function requestApproval(
45
+ sessionId: string,
46
+ messageId: string,
47
+ callId: string,
48
+ toolName: string,
49
+ args: unknown,
50
+ timeoutMs = 120000,
51
+ ): Promise<boolean> {
52
+ console.log('[approval] requestApproval called', {
53
+ sessionId,
54
+ messageId,
55
+ callId,
56
+ toolName,
57
+ });
58
+ return new Promise((resolve) => {
59
+ const approval: PendingApproval = {
60
+ callId,
61
+ toolName,
62
+ args,
63
+ sessionId,
64
+ messageId,
65
+ resolve,
66
+ createdAt: Date.now(),
67
+ };
68
+
69
+ pendingApprovals.set(callId, approval);
70
+ console.log(
71
+ '[approval] Added to pendingApprovals, count:',
72
+ pendingApprovals.size,
73
+ );
74
+
75
+ publish({
76
+ type: 'tool.approval.required',
77
+ sessionId,
78
+ payload: {
79
+ callId,
80
+ toolName,
81
+ args,
82
+ messageId,
83
+ },
84
+ });
85
+
86
+ setTimeout(() => {
87
+ if (pendingApprovals.has(callId)) {
88
+ pendingApprovals.delete(callId);
89
+ resolve(false);
90
+ publish({
91
+ type: 'tool.approval.resolved',
92
+ sessionId,
93
+ payload: {
94
+ callId,
95
+ toolName,
96
+ approved: false,
97
+ reason: 'timeout',
98
+ },
99
+ });
100
+ }
101
+ }, timeoutMs);
102
+ });
103
+ }
104
+
105
+ export function resolveApproval(
106
+ callId: string,
107
+ approved: boolean,
108
+ ): { ok: boolean; error?: string } {
109
+ console.log('[approval] resolveApproval called', {
110
+ callId,
111
+ approved,
112
+ pendingCount: pendingApprovals.size,
113
+ pendingIds: [...pendingApprovals.keys()],
114
+ });
115
+ const approval = pendingApprovals.get(callId);
116
+ if (!approval) {
117
+ console.log('[approval] No pending approval found for callId:', callId);
118
+ return { ok: false, error: 'No pending approval found for this callId' };
119
+ }
120
+
121
+ pendingApprovals.delete(callId);
122
+ approval.resolve(approved);
123
+
124
+ publish({
125
+ type: 'tool.approval.resolved',
126
+ sessionId: approval.sessionId,
127
+ payload: {
128
+ callId,
129
+ toolName: approval.toolName,
130
+ approved,
131
+ reason: approved ? 'user_approved' : 'user_rejected',
132
+ },
133
+ });
134
+
135
+ return { ok: true };
136
+ }
137
+
138
+ export function getPendingApproval(
139
+ callId: string,
140
+ ): PendingApproval | undefined {
141
+ return pendingApprovals.get(callId);
142
+ }
143
+
144
+ export function updateApprovalArgs(callId: string, args: unknown): boolean {
145
+ const approval = pendingApprovals.get(callId);
146
+ if (!approval) return false;
147
+
148
+ approval.args = args;
149
+
150
+ publish({
151
+ type: 'tool.approval.updated',
152
+ sessionId: approval.sessionId,
153
+ payload: {
154
+ callId,
155
+ toolName: approval.toolName,
156
+ args,
157
+ messageId: approval.messageId,
158
+ },
159
+ });
160
+
161
+ return true;
162
+ }
163
+
164
+ export function getPendingApprovalsForSession(
165
+ sessionId: string,
166
+ ): PendingApproval[] {
167
+ return Array.from(pendingApprovals.values()).filter(
168
+ (a) => a.sessionId === sessionId,
169
+ );
170
+ }
171
+
172
+ export function clearPendingApprovalsForSession(sessionId: string): void {
173
+ for (const [callId, approval] of pendingApprovals) {
174
+ if (approval.sessionId === sessionId) {
175
+ approval.resolve(false);
176
+ pendingApprovals.delete(callId);
177
+ }
178
+ }
179
+ }
@@ -1,6 +1,7 @@
1
1
  import { eq } from 'drizzle-orm';
2
2
  import type { DB } from '@agi-cli/database';
3
3
  import { messageParts } from '@agi-cli/database/schema';
4
+ import type { ToolApprovalMode } from './approval.ts';
4
5
  import { publish } from '../../events/bus.ts';
5
6
 
6
7
  export type StepExecutionState = {
@@ -24,6 +25,7 @@ export type ToolAdapterContext = {
24
25
  stepExecution?: {
25
26
  states: Map<number, StepExecutionState>;
26
27
  };
28
+ toolApprovalMode?: ToolApprovalMode;
27
29
  };
28
30
 
29
31
  export function extractFinishText(input: unknown): string | undefined {
@@ -32,6 +32,7 @@ export async function setupToolContext(
32
32
  model: opts.model,
33
33
  projectRoot: opts.projectRoot,
34
34
  stepExecution: { states: new Map() },
35
+ toolApprovalMode: opts.toolApprovalMode,
35
36
  onFirstToolCall: () => {
36
37
  if (firstToolSeen) return;
37
38
  firstToolSeen = true;
@@ -13,6 +13,10 @@ import {
13
13
  toClaudeCodeName,
14
14
  requiresClaudeCodeNaming,
15
15
  } from '../runtime/tools/mapping.ts';
16
+ import {
17
+ requiresApproval,
18
+ requestApproval,
19
+ } from '../runtime/tools/approval.ts';
16
20
 
17
21
  export type { ToolAdapterContext } from '../runtime/tools/context.ts';
18
22
 
@@ -33,6 +37,7 @@ type PendingCallMeta = {
33
37
  startTs: number;
34
38
  stepIndex?: number;
35
39
  args?: unknown;
40
+ approvalPromise?: Promise<boolean>;
36
41
  };
37
42
 
38
43
  function getPendingQueue(
@@ -294,6 +299,19 @@ export function adaptTools(
294
299
  toolCallId: callId,
295
300
  });
296
301
  } catch {}
302
+ // Start approval request with full args
303
+ if (
304
+ ctx.toolApprovalMode &&
305
+ requiresApproval(name, ctx.toolApprovalMode)
306
+ ) {
307
+ meta.approvalPromise = requestApproval(
308
+ ctx.sessionId,
309
+ ctx.messageId,
310
+ callId,
311
+ name,
312
+ args,
313
+ );
314
+ }
297
315
  if (typeof base.onInputAvailable === 'function') {
298
316
  // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
299
317
  await base.onInputAvailable(options as any);
@@ -324,6 +342,16 @@ export function adaptTools(
324
342
 
325
343
  const executeWithGuards = async (): Promise<ToolExecuteReturn> => {
326
344
  try {
345
+ // Await approval if it was requested in onInputAvailable
346
+ if (meta?.approvalPromise) {
347
+ const approved = await meta.approvalPromise;
348
+ if (!approved) {
349
+ return {
350
+ ok: false,
351
+ error: 'Tool execution rejected by user',
352
+ } as ToolExecuteReturn;
353
+ }
354
+ }
327
355
  // Handle session-relative paths and cwd tools
328
356
  let res: ToolExecuteReturn | { cwd: string } | null | undefined;
329
357
  const cwd = getCwd(ctx.sessionId);
package/sst-env.d.ts CHANGED
@@ -2,9 +2,7 @@
2
2
  /* tslint:disable */
3
3
  /* eslint-disable */
4
4
  /* deno-fmt-ignore-file */
5
- /* biome-ignore-all lint: auto-generated */
6
5
 
7
6
  /// <reference path="../../sst-env.d.ts" />
8
7
 
9
8
  import 'sst';
10
- export {};