salmon-loop 0.3.1 → 0.4.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.
- package/dist/cli/authorization/non-interactive.js +9 -13
- package/dist/cli/chat.js +12 -6
- package/dist/cli/commands/allowlist.js +1 -1
- package/dist/cli/commands/chat.js +13 -13
- package/dist/cli/commands/parallel.js +1 -1
- package/dist/cli/commands/run/handler.js +6 -3
- package/dist/cli/commands/run/loop-params.js +1 -0
- package/dist/cli/commands/run/parse-options.js +14 -26
- package/dist/cli/commands/run/runtime-llm.js +15 -12
- package/dist/cli/commands/serve.js +14 -1
- package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
- package/dist/cli/reporters/standard.js +2 -3
- package/dist/cli/reporters/stream-json.js +2 -1
- package/dist/cli/slash/runtime.js +2 -2
- package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
- package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
- package/dist/cli/ui/hooks/useLoopState.js +1 -1
- package/dist/core/ast/parser.js +18 -9
- package/dist/core/config/schema.js +738 -0
- package/dist/core/config/validate.js +11 -922
- package/dist/core/context/gatherers/ast-gatherer.js +4 -12
- package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
- package/dist/core/context/gatherers/knowledge-gatherer.js +3 -0
- package/dist/core/context/service.js +39 -8
- package/dist/core/context/token/encoding-registry.js +7 -6
- package/dist/core/extensions/index.js +48 -3
- package/dist/core/extensions/load.js +3 -2
- package/dist/core/extensions/merge.js +5 -1
- package/dist/core/extensions/paths.js +6 -0
- package/dist/core/extensions/schemas.js +21 -0
- package/dist/core/facades/cli-command-chat.js +2 -0
- package/dist/core/facades/cli-run-handler.js +1 -0
- package/dist/core/facades/cli-utils-serialize.js +2 -0
- package/dist/core/grizzco/dsl/llm-strategy.js +3 -2
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -10
- package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +5 -4
- package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
- package/dist/core/grizzco/runtime/apply-back-runtime.js +2 -1
- package/dist/core/grizzco/services/registry.js +18 -0
- package/dist/core/grizzco/steps/audit.js +20 -10
- package/dist/core/grizzco/steps/display-report.js +4 -11
- package/dist/core/grizzco/steps/explore.js +9 -2
- package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
- package/dist/core/grizzco/steps/patch.js +1 -0
- package/dist/core/grizzco/steps/plan.js +58 -49
- package/dist/core/grizzco/steps/tool-runtime.js +3 -0
- package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
- package/dist/core/llm/ai-sdk/message-mapper.js +24 -18
- package/dist/core/llm/ai-sdk/request-params.js +1 -3
- package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
- package/dist/core/llm/ai-sdk/retry-classifier.js +6 -4
- package/dist/core/llm/contracts/repair.js +16 -8
- package/dist/core/llm/errors.js +13 -10
- package/dist/core/llm/output-policy.js +8 -0
- package/dist/core/llm/redact.js +1 -3
- package/dist/core/llm/sub-agent-factory.js +48 -0
- package/dist/core/llm/tool-calling-stub.js +48 -0
- package/dist/core/llm/utils.js +17 -6
- package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
- package/dist/core/mcp/bridge/tool-bridge.js +5 -14
- package/dist/core/mcp/client/connection-manager.js +3 -2
- package/dist/core/mcp/host/sampling-provider.js +1 -1
- package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
- package/dist/core/memory/relevant-retrieval.js +6 -4
- package/dist/core/observability/authorization-decisions.js +13 -12
- package/dist/core/observability/error-mapping.js +2 -1
- package/dist/core/observability/token-usage.js +5 -4
- package/dist/core/plugin/loader.js +5 -4
- package/dist/core/prompts/registry.js +11 -29
- package/dist/core/protocols/a2a/sdk/server.js +2 -3
- package/dist/core/protocols/acp/formal-agent.js +10 -4
- package/dist/core/protocols/acp/stdio-server.js +6 -6
- package/dist/core/runtime/agent-server-runtime.js +3 -2
- package/dist/core/runtime/initialize.js +70 -6
- package/dist/core/session/compaction/index.js +4 -3
- package/dist/core/session/manager.js +41 -47
- package/dist/core/session/token-tracker.js +18 -7
- package/dist/core/skills/parser.js +3 -2
- package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
- package/dist/core/skills/runtime/SkillRunner.js +5 -2
- package/dist/core/slash/steps/slash-execute.js +7 -5
- package/dist/core/slash/strategy.js +1 -1
- package/dist/core/strata/layers/worktree.js +7 -9
- package/dist/core/strata/runtime/synchronizer.js +10 -9
- package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
- package/dist/core/structured-output/json-schema-validator.js +1 -13
- package/dist/core/sub-agent/context-snapshot.js +12 -6
- package/dist/core/sub-agent/controller.js +70 -1
- package/dist/core/sub-agent/core/loop.js +25 -3
- package/dist/core/sub-agent/core/manager.js +319 -116
- package/dist/core/sub-agent/registry-defaults.js +12 -0
- package/dist/core/sub-agent/registry.js +8 -0
- package/dist/core/sub-agent/team.js +98 -0
- package/dist/core/sub-agent/tools/task-await.js +109 -0
- package/dist/core/sub-agent/tools/task-spawn.js +49 -7
- package/dist/core/sub-agent/tools/team.js +92 -0
- package/dist/core/sub-agent/types.js +11 -2
- package/dist/core/tools/budget.js +4 -11
- package/dist/core/tools/builtin/code-search/executor.js +46 -43
- package/dist/core/tools/builtin/fs.js +14 -6
- package/dist/core/tools/builtin/index.js +41 -107
- package/dist/core/tools/builtin/interaction.js +13 -15
- package/dist/core/tools/builtin/proposal.js +11 -2
- package/dist/core/tools/capability/executor.js +5 -5
- package/dist/core/tools/headless-payload.js +1 -3
- package/dist/core/tools/mapper.js +8 -42
- package/dist/core/tools/parallel/persistence.js +17 -5
- package/dist/core/tools/parallel/scheduler.js +23 -21
- package/dist/core/tools/permissions/permission-rules.js +66 -114
- package/dist/core/tools/plugins/loader.js +4 -3
- package/dist/core/tools/router.js +24 -53
- package/dist/core/tools/session.js +54 -97
- package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
- package/dist/core/tools/tool-visibility.js +2 -1
- package/dist/core/tools/types.js +10 -0
- package/dist/core/utils/error.js +79 -0
- package/dist/core/utils/serialize.js +63 -0
- package/dist/core/utils/zod.js +29 -0
- package/dist/core/workspace/capabilities.js +3 -2
- package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
- package/dist/locales/en.js +2 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { text } from '../../../locales/index.js';
|
|
|
2
2
|
import { normalizeDiff, validateDiff } from '../../patch/diff.js';
|
|
3
3
|
import { ArtifactStore } from '../../sub-agent/artifacts/store.js';
|
|
4
4
|
import { normalizeRepoRelativePath } from '../../utils/path.js';
|
|
5
|
+
import { isRecord } from '../../utils/serialize.js';
|
|
5
6
|
const DEFAULT_TOOL_ALIASES = {
|
|
6
7
|
bash: 'Bash',
|
|
7
8
|
read: 'Read',
|
|
@@ -218,57 +219,66 @@ function compilePathMatcher(specifier) {
|
|
|
218
219
|
matches: (repoRelativePath) => re.test(normalizeRepoRelativePath(repoRelativePath)),
|
|
219
220
|
};
|
|
220
221
|
}
|
|
222
|
+
const TOOL_CATEGORY = {
|
|
223
|
+
Bash: 'bash',
|
|
224
|
+
bash: 'bash',
|
|
225
|
+
'shell.exec': 'bash',
|
|
226
|
+
'test.run': 'bash',
|
|
227
|
+
Edit: 'edit',
|
|
228
|
+
edit: 'edit',
|
|
229
|
+
'proposal.apply': 'edit',
|
|
230
|
+
Read: 'path',
|
|
231
|
+
read: 'path',
|
|
232
|
+
LS: 'path',
|
|
233
|
+
ls: 'path',
|
|
234
|
+
'fs.read': 'path',
|
|
235
|
+
'code.read': 'path',
|
|
236
|
+
'git.cat': 'path',
|
|
237
|
+
'fs.list': 'path',
|
|
238
|
+
'fs.list_directory': 'path',
|
|
239
|
+
'fs.list_files': 'path',
|
|
240
|
+
'artifact.read': 'path',
|
|
241
|
+
};
|
|
242
|
+
function resolveToolCategory(tool) {
|
|
243
|
+
if (TOOL_CATEGORY[tool])
|
|
244
|
+
return TOOL_CATEGORY[tool];
|
|
245
|
+
if (isAliasToolName(tool)) {
|
|
246
|
+
const alias = DEFAULT_TOOL_ALIASES[tool.toLowerCase()];
|
|
247
|
+
return TOOL_CATEGORY[alias];
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
221
251
|
function compileRule(effect, parsed) {
|
|
222
|
-
const tool = parsed
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
const shouldTreatAsBash = tool === 'Bash' ||
|
|
226
|
-
tool === 'bash' ||
|
|
227
|
-
tool === 'shell.exec' ||
|
|
228
|
-
tool === 'test.run' ||
|
|
229
|
-
asAlias === 'Bash';
|
|
230
|
-
const shouldTreatAsEdit = tool === 'Edit' || tool === 'edit' || tool === 'proposal.apply' || asAlias === 'Edit';
|
|
231
|
-
const shouldTreatAsPath = tool === 'Read' ||
|
|
232
|
-
tool === 'read' ||
|
|
233
|
-
tool === 'LS' ||
|
|
234
|
-
tool === 'ls' ||
|
|
235
|
-
tool === 'fs.read' ||
|
|
236
|
-
tool === 'code.read' ||
|
|
237
|
-
tool === 'git.cat' ||
|
|
238
|
-
tool === 'fs.list' ||
|
|
239
|
-
tool === 'fs.list_directory' ||
|
|
240
|
-
tool === 'fs.list_files' ||
|
|
241
|
-
tool === 'artifact.read' ||
|
|
242
|
-
asAlias === 'Read' ||
|
|
243
|
-
asAlias === 'LS';
|
|
244
|
-
if (shouldTreatAsBash) {
|
|
252
|
+
const { tool, raw, specifier } = parsed;
|
|
253
|
+
const category = resolveToolCategory(tool);
|
|
254
|
+
if (category === 'bash') {
|
|
245
255
|
return {
|
|
246
256
|
effect,
|
|
247
257
|
tool,
|
|
248
|
-
raw
|
|
258
|
+
raw,
|
|
249
259
|
specifier,
|
|
250
260
|
compiled: { kind: 'bash', matcher: compileBashMatcher(specifier) },
|
|
251
261
|
};
|
|
252
262
|
}
|
|
253
|
-
if (
|
|
263
|
+
if (category === 'edit') {
|
|
254
264
|
return {
|
|
255
265
|
effect,
|
|
256
266
|
tool,
|
|
257
|
-
raw
|
|
267
|
+
raw,
|
|
258
268
|
specifier,
|
|
259
269
|
compiled: { kind: 'edit', matcher: compilePathMatcher(specifier) },
|
|
260
270
|
};
|
|
261
271
|
}
|
|
262
|
-
if (
|
|
272
|
+
if (category === 'path') {
|
|
263
273
|
return {
|
|
264
274
|
effect,
|
|
265
275
|
tool,
|
|
266
|
-
raw
|
|
276
|
+
raw,
|
|
267
277
|
specifier,
|
|
268
278
|
compiled: { kind: 'path', matcher: compilePathMatcher(specifier) },
|
|
269
279
|
};
|
|
270
280
|
}
|
|
271
|
-
return { effect, tool, raw
|
|
281
|
+
return { effect, tool, raw, specifier, compiled: { kind: 'tool_any' } };
|
|
272
282
|
}
|
|
273
283
|
function buildVisibleToolNamesFromAllow(allowRules) {
|
|
274
284
|
const visible = new Set();
|
|
@@ -357,81 +367,37 @@ async function loadProposalChangedFiles(handle) {
|
|
|
357
367
|
return null;
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
|
-
function
|
|
370
|
+
function matchRule(rule, toolName, args) {
|
|
361
371
|
if (!toolMatchesRuleTool(toolName, rule.tool))
|
|
362
372
|
return false;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return false;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (rule.compiled.kind === 'edit') {
|
|
378
|
-
// Edit allow rules are handled by an async path-aware matcher.
|
|
379
|
-
return toolName === 'proposal.apply';
|
|
373
|
+
switch (rule.compiled.kind) {
|
|
374
|
+
case 'tool_any':
|
|
375
|
+
return true;
|
|
376
|
+
case 'bash': {
|
|
377
|
+
const cmd = extractCommandArg(toolName, args);
|
|
378
|
+
return cmd ? rule.compiled.matcher.matches(cmd) : false;
|
|
379
|
+
}
|
|
380
|
+
case 'path': {
|
|
381
|
+
const p = extractPrimaryPathArg(toolName, args);
|
|
382
|
+
return p ? rule.compiled.matcher.matches(p) : false;
|
|
383
|
+
}
|
|
384
|
+
case 'edit':
|
|
385
|
+
// Edit rules require async file-level matching; sync check is a gate.
|
|
386
|
+
return toolName === 'proposal.apply';
|
|
380
387
|
}
|
|
381
|
-
return false;
|
|
382
388
|
}
|
|
383
|
-
async function
|
|
389
|
+
async function matchEditRule(rule, args, filePredicate) {
|
|
384
390
|
if (rule.compiled.kind !== 'edit')
|
|
385
391
|
return false;
|
|
386
|
-
|
|
387
|
-
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
392
|
+
if (!isRecord(args))
|
|
388
393
|
return false;
|
|
389
394
|
const handle = args.handle;
|
|
390
395
|
if (typeof handle !== 'string' || !handle.trim())
|
|
391
396
|
return false;
|
|
392
397
|
const changedFiles = await loadProposalChangedFiles(handle);
|
|
393
|
-
if (!changedFiles)
|
|
394
|
-
return false;
|
|
395
|
-
if (changedFiles.length === 0)
|
|
398
|
+
if (!changedFiles || changedFiles.length === 0)
|
|
396
399
|
return false;
|
|
397
|
-
return changedFiles.
|
|
398
|
-
}
|
|
399
|
-
async function matchDenyEditRule(rule, args) {
|
|
400
|
-
if (rule.compiled.kind !== 'edit')
|
|
401
|
-
return false;
|
|
402
|
-
const matcher = rule.compiled.matcher;
|
|
403
|
-
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
404
|
-
return false;
|
|
405
|
-
const handle = args.handle;
|
|
406
|
-
if (typeof handle !== 'string' || !handle.trim())
|
|
407
|
-
return false;
|
|
408
|
-
const changedFiles = await loadProposalChangedFiles(handle);
|
|
409
|
-
if (!changedFiles)
|
|
410
|
-
return false;
|
|
411
|
-
return changedFiles.some((p) => matcher.matches(p));
|
|
412
|
-
}
|
|
413
|
-
function matchDenyRule(rule, toolName, args) {
|
|
414
|
-
if (!toolMatchesRuleTool(toolName, rule.tool))
|
|
415
|
-
return false;
|
|
416
|
-
if (rule.compiled.kind === 'tool_any')
|
|
417
|
-
return true;
|
|
418
|
-
if (rule.compiled.kind === 'bash') {
|
|
419
|
-
const cmd = extractCommandArg(toolName, args);
|
|
420
|
-
if (!cmd)
|
|
421
|
-
return false;
|
|
422
|
-
return rule.compiled.matcher.matches(cmd);
|
|
423
|
-
}
|
|
424
|
-
if (rule.compiled.kind === 'path') {
|
|
425
|
-
const p = extractPrimaryPathArg(toolName, args);
|
|
426
|
-
if (!p)
|
|
427
|
-
return false;
|
|
428
|
-
return rule.compiled.matcher.matches(p);
|
|
429
|
-
}
|
|
430
|
-
if (rule.compiled.kind === 'edit') {
|
|
431
|
-
// Deny edit rules are handled by an async path-aware matcher.
|
|
432
|
-
return toolName === 'proposal.apply';
|
|
433
|
-
}
|
|
434
|
-
return false;
|
|
400
|
+
return filePredicate(changedFiles, rule.compiled.matcher);
|
|
435
401
|
}
|
|
436
402
|
export async function decidePermissionForToolCall(options) {
|
|
437
403
|
const rules = options.rules;
|
|
@@ -442,17 +408,10 @@ export async function decidePermissionForToolCall(options) {
|
|
|
442
408
|
}
|
|
443
409
|
// Deny rules win.
|
|
444
410
|
for (const rule of rules.deny) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
reason: text.tools.permissionRuleDenied(rule.raw),
|
|
450
|
-
rule: { effect: 'deny', raw: rule.raw, tool: rule.tool },
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
if (matchDenyRule(rule, options.toolName, options.args)) {
|
|
411
|
+
const isDeny = rule.compiled.kind === 'edit'
|
|
412
|
+
? await matchEditRule(rule, options.args, (files, matcher) => files.some((p) => matcher.matches(p)))
|
|
413
|
+
: matchRule(rule, options.toolName, options.args);
|
|
414
|
+
if (isDeny) {
|
|
456
415
|
return {
|
|
457
416
|
kind: 'deny',
|
|
458
417
|
reason: text.tools.permissionRuleDenied(rule.raw),
|
|
@@ -461,17 +420,10 @@ export async function decidePermissionForToolCall(options) {
|
|
|
461
420
|
}
|
|
462
421
|
}
|
|
463
422
|
for (const rule of rules.allow) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
reason: rule.raw,
|
|
469
|
-
rule: { effect: 'allow', raw: rule.raw, tool: rule.tool },
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (matchAllowRule(rule, options.toolName, options.args)) {
|
|
423
|
+
const isAllow = rule.compiled.kind === 'edit'
|
|
424
|
+
? await matchEditRule(rule, options.args, (files, matcher) => files.every((p) => matcher.matches(p)))
|
|
425
|
+
: matchRule(rule, options.toolName, options.args);
|
|
426
|
+
if (isAllow) {
|
|
475
427
|
return {
|
|
476
428
|
kind: 'allow',
|
|
477
429
|
reason: rule.raw,
|
|
@@ -3,6 +3,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
3
3
|
import { syncFs as fs } from '../../adapters/fs/node-fs.js';
|
|
4
4
|
import { getLogger } from '../../observability/logger.js';
|
|
5
5
|
import { Phase } from '../../types/runtime.js';
|
|
6
|
+
import { errorMessage } from '../../utils/error.js';
|
|
6
7
|
const FORBIDDEN_PHASES = new Set([
|
|
7
8
|
Phase.PLAN,
|
|
8
9
|
Phase.PATCH,
|
|
@@ -25,7 +26,7 @@ export async function registerPluginTools(registry, plugins) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
catch (error) {
|
|
28
|
-
getLogger().warn(`Plugin ${plugin.id} path ${entryPoint} is not accessible: ${
|
|
29
|
+
getLogger().warn(`Plugin ${plugin.id} path ${entryPoint} is not accessible: ${errorMessage(error)}`);
|
|
29
30
|
continue;
|
|
30
31
|
}
|
|
31
32
|
const moduleUrl = pathToFileURL(entryPoint).href;
|
|
@@ -35,7 +36,7 @@ export async function registerPluginTools(registry, plugins) {
|
|
|
35
36
|
manifest = (mod.default ?? mod);
|
|
36
37
|
}
|
|
37
38
|
catch (error) {
|
|
38
|
-
getLogger().error(`Failed to import plugin ${plugin.id} from ${entryPoint}: ${
|
|
39
|
+
getLogger().error(`Failed to import plugin ${plugin.id} from ${entryPoint}: ${errorMessage(error)}`);
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
41
42
|
const registerFn = manifest?.register;
|
|
@@ -52,7 +53,7 @@ export async function registerPluginTools(registry, plugins) {
|
|
|
52
53
|
tools = await registerFn();
|
|
53
54
|
}
|
|
54
55
|
catch (error) {
|
|
55
|
-
getLogger().error(`Plugin ${pluginId} register() failed: ${
|
|
56
|
+
getLogger().error(`Plugin ${pluginId} register() failed: ${errorMessage(error)}`);
|
|
56
57
|
continue;
|
|
57
58
|
}
|
|
58
59
|
if (!Array.isArray(tools)) {
|
|
@@ -2,6 +2,8 @@ import * as crypto from 'crypto';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { LIMITS } from '../config/limits.js';
|
|
4
4
|
import { getLogger } from '../observability/logger.js';
|
|
5
|
+
import { isRecord } from '../utils/serialize.js';
|
|
6
|
+
import { unwrapZodSchema } from '../utils/zod.js';
|
|
5
7
|
import { decidePermissionForToolCall } from './permissions/permission-rules.js';
|
|
6
8
|
export class ToolRouter {
|
|
7
9
|
registry;
|
|
@@ -14,32 +16,7 @@ export class ToolRouter {
|
|
|
14
16
|
authorizationMode;
|
|
15
17
|
permissionRules;
|
|
16
18
|
unwrapForHint(schema) {
|
|
17
|
-
|
|
18
|
-
for (let depth = 0; depth < 20; depth++) {
|
|
19
|
-
const ZodEffects = z.ZodEffects;
|
|
20
|
-
if (typeof ZodEffects === 'function' && current instanceof ZodEffects) {
|
|
21
|
-
current = current._def.schema;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (current instanceof z.ZodPipe) {
|
|
25
|
-
current = current._def.out;
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (current instanceof z.ZodOptional) {
|
|
29
|
-
current = current._def.innerType;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (current instanceof z.ZodNullable) {
|
|
33
|
-
current = current._def.innerType;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (current instanceof z.ZodDefault) {
|
|
37
|
-
current = current._def.innerType;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
return current;
|
|
19
|
+
return unwrapZodSchema(schema);
|
|
43
20
|
}
|
|
44
21
|
buildInputHint(spec) {
|
|
45
22
|
if (!spec.inputSchema)
|
|
@@ -115,7 +92,7 @@ export class ToolRouter {
|
|
|
115
92
|
const message = hint
|
|
116
93
|
? `${inputCheck.message} (${hint})`
|
|
117
94
|
: inputCheck.message || 'Invalid input';
|
|
118
|
-
throw { code: 'INVALID_INPUT'
|
|
95
|
+
throw Object.assign(new Error(message), { code: 'INVALID_INPUT' });
|
|
119
96
|
}
|
|
120
97
|
const normalizedArgs = inputCheck.value ?? envelope.args;
|
|
121
98
|
const normalizedEnvelope = normalizedArgs === envelope.args ? envelope : { ...envelope, args: normalizedArgs };
|
|
@@ -178,7 +155,9 @@ export class ToolRouter {
|
|
|
178
155
|
},
|
|
179
156
|
});
|
|
180
157
|
// Provide a stable token for challenge-response UIs.
|
|
181
|
-
result.error
|
|
158
|
+
if (isRecord(result.error)) {
|
|
159
|
+
result.error.confirmToken = auth.challenge;
|
|
160
|
+
}
|
|
182
161
|
this.audit.onEnd(result);
|
|
183
162
|
return result;
|
|
184
163
|
}
|
|
@@ -199,7 +178,7 @@ export class ToolRouter {
|
|
|
199
178
|
// 5. Output Validation & Sanitize: Result validation and sensitive summary
|
|
200
179
|
const sanitized = this.sanitizer.sanitizeOutput(spec, rawOutput);
|
|
201
180
|
if (!sanitized.ok) {
|
|
202
|
-
throw { code: 'INVALID_OUTPUT'
|
|
181
|
+
throw Object.assign(new Error(sanitized.message), { code: 'INVALID_OUTPUT' });
|
|
203
182
|
}
|
|
204
183
|
// 6. Return Standard Result (ok)
|
|
205
184
|
const durationMs = Date.now() - startedAt;
|
|
@@ -222,28 +201,18 @@ export class ToolRouter {
|
|
|
222
201
|
let errorMeta;
|
|
223
202
|
if (e instanceof Error) {
|
|
224
203
|
errorMessage = e.message;
|
|
225
|
-
if ('code' in e && typeof e.code === 'string') {
|
|
226
|
-
errorCode = e.code;
|
|
227
|
-
}
|
|
228
|
-
if ('interrupt' in e) {
|
|
229
|
-
errorMeta = { ...(errorMeta ?? {}), interrupt: e.interrupt };
|
|
230
|
-
}
|
|
231
|
-
if ('inputRequired' in e) {
|
|
232
|
-
errorMeta = { ...(errorMeta ?? {}), inputRequired: e.inputRequired };
|
|
233
|
-
}
|
|
234
204
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
errorCode =
|
|
205
|
+
const errObj = isRecord(e) ? e : null;
|
|
206
|
+
if (errObj) {
|
|
207
|
+
if (typeof errObj.message === 'string')
|
|
208
|
+
errorMessage = errObj.message;
|
|
209
|
+
if (typeof errObj.code === 'string')
|
|
210
|
+
errorCode = errObj.code;
|
|
211
|
+
if ('interrupt' in errObj) {
|
|
212
|
+
errorMeta = { ...(errorMeta ?? {}), interrupt: errObj.interrupt };
|
|
241
213
|
}
|
|
242
|
-
if ('
|
|
243
|
-
errorMeta = { ...(errorMeta ?? {}),
|
|
244
|
-
}
|
|
245
|
-
if ('inputRequired' in e) {
|
|
246
|
-
errorMeta = { ...(errorMeta ?? {}), inputRequired: e.inputRequired };
|
|
214
|
+
if ('inputRequired' in errObj) {
|
|
215
|
+
errorMeta = { ...(errorMeta ?? {}), inputRequired: errObj.inputRequired };
|
|
247
216
|
}
|
|
248
217
|
}
|
|
249
218
|
const result = this.createErrorResult(envelope, startedAt, errorCode === 'TIMEOUT' ? 'timeout' : 'error', errorCode, errorMessage, errorMeta);
|
|
@@ -315,7 +284,7 @@ export class ToolRouter {
|
|
|
315
284
|
source: spec.source,
|
|
316
285
|
phase: normalizedEnvelope.phase,
|
|
317
286
|
riskLevel: spec.riskLevel,
|
|
318
|
-
sideEffects:
|
|
287
|
+
sideEffects: spec.sideEffects,
|
|
319
288
|
argsSummary,
|
|
320
289
|
argsHash,
|
|
321
290
|
repoRoot: normalizedEnvelope.ctx.repoRoot,
|
|
@@ -333,7 +302,9 @@ export class ToolRouter {
|
|
|
333
302
|
source: 'user',
|
|
334
303
|
},
|
|
335
304
|
});
|
|
336
|
-
toolResult.error
|
|
305
|
+
if (isRecord(toolResult.error)) {
|
|
306
|
+
toolResult.error.confirmToken = deferred.challenge;
|
|
307
|
+
}
|
|
337
308
|
return {
|
|
338
309
|
kind: 'pending',
|
|
339
310
|
message: deferred.message,
|
|
@@ -450,7 +421,7 @@ export class ToolRouter {
|
|
|
450
421
|
source: spec.source,
|
|
451
422
|
phase: envelope.phase,
|
|
452
423
|
riskLevel: spec.riskLevel,
|
|
453
|
-
sideEffects: (spec.sideEffects
|
|
424
|
+
sideEffects: (spec.sideEffects ?? []),
|
|
454
425
|
argsSummary,
|
|
455
426
|
argsHash,
|
|
456
427
|
repoRoot: envelope.ctx.repoRoot,
|
|
@@ -511,7 +482,7 @@ export class ToolRouter {
|
|
|
511
482
|
}
|
|
512
483
|
async getAuthorizationArgsSummary(envelope, spec) {
|
|
513
484
|
const fallback = this.summarizeArgs(envelope.args);
|
|
514
|
-
const summarize = spec
|
|
485
|
+
const summarize = spec.summarizeArgsForAuthorization;
|
|
515
486
|
if (typeof summarize !== 'function')
|
|
516
487
|
return fallback;
|
|
517
488
|
// Best-effort only. Avoid hanging authorization prompts on slow IO.
|