salmon-loop 0.3.2 → 0.5.0
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/authorization/provider.js +2 -10
- 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/config.js +2 -2
- package/dist/cli/commands/mode.js +2 -2
- package/dist/cli/commands/parallel.js +1 -1
- package/dist/cli/commands/run/handler.js +9 -4
- package/dist/cli/commands/run/loop-params.js +2 -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/run/runtime-options.js +3 -1
- package/dist/cli/config.js +0 -8
- package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
- package/dist/cli/locales/en.js +2 -2
- package/dist/cli/reporters/standard.js +12 -3
- package/dist/cli/reporters/stream-json.js +2 -1
- package/dist/cli/slash/runtime.js +2 -2
- package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
- package/dist/cli/ui/hooks/useLoopState.js +1 -1
- package/dist/core/adapters/fs/file-adapter.js +3 -1
- package/dist/core/adapters/git/git-adapter.js +6 -3
- package/dist/core/adapters/git/git-runner.js +5 -2
- package/dist/core/adapters/git/lock-manager.js +7 -4
- package/dist/core/ast/parser.js +18 -9
- package/dist/core/checkpoint-domain/manifest-store.js +21 -13
- package/dist/core/checkpoint-domain/service.js +3 -1
- package/dist/core/config/limits.js +1 -1
- package/dist/core/config/model-pricing.js +61 -0
- package/dist/core/config/schema.js +738 -0
- package/dist/core/config/validate.js +11 -922
- package/dist/core/context/ast/skeleton-extractor.js +225 -0
- package/dist/core/context/ast/source-outline.js +24 -1
- package/dist/core/context/budget/dynamic-adjuster.js +20 -5
- package/dist/core/context/builder.js +7 -3
- package/dist/core/context/cache/store-factory.js +3 -1
- package/dist/core/context/dependencies.js +2 -1
- package/dist/core/context/effectiveness/persistence.js +50 -0
- package/dist/core/context/effectiveness/tracker.js +24 -0
- package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
- package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
- package/dist/core/context/gatherers/ast-gatherer.js +34 -40
- package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
- package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
- package/dist/core/context/gatherers/knowledge-gatherer.js +21 -2
- package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
- package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
- package/dist/core/context/service.js +12 -2
- package/dist/core/context/steps/context-gather.js +14 -3
- package/dist/core/context/steps/context-targets.js +1 -0
- package/dist/core/context/targeting/target-resolver.js +29 -11
- package/dist/core/context/token/cache.js +5 -2
- package/dist/core/context/token/encoding-registry.js +7 -6
- package/dist/core/context/truncation/strategies/json.js +5 -2
- package/dist/core/context/truncation/type-detector.js +3 -1
- 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 +8 -2
- package/dist/core/extensions/schemas.js +21 -0
- package/dist/core/facades/cli-authorization-provider.js +1 -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/feedback/parsers.js +290 -1
- package/dist/core/grizzco/dsl/llm-strategy.js +4 -3
- package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +30 -13
- package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +49 -24
- package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
- package/dist/core/grizzco/execution/RejectionManager.js +7 -5
- package/dist/core/grizzco/runtime/apply-back-runtime.js +5 -2
- package/dist/core/grizzco/services/implementations/default/GitConfigService.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/autopilot.js +21 -32
- package/dist/core/grizzco/steps/display-report.js +4 -11
- package/dist/core/grizzco/steps/explore.js +14 -4
- package/dist/core/grizzco/steps/generateReview.js +3 -1
- 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/research.js +3 -1
- package/dist/core/grizzco/steps/tool-runtime.js +3 -0
- package/dist/core/grizzco/steps/verify.js +7 -1
- package/dist/core/grizzco/validation/AstValidationService.js +3 -1
- package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
- package/dist/core/history/input-history.js +3 -1
- package/dist/core/intent/chat-intent.js +3 -1
- package/dist/core/llm/ai-sdk/message-mapper.js +37 -26
- package/dist/core/llm/ai-sdk/request-params.js +2 -6
- package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
- package/dist/core/llm/ai-sdk/retry-classifier.js +17 -7
- package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
- package/dist/core/llm/contracts/repair.js +16 -8
- package/dist/core/llm/errors.js +18 -14
- package/dist/core/llm/output-policy.js +8 -0
- package/dist/core/llm/redact.js +1 -3
- package/dist/core/llm/retry-utils.js +8 -2
- package/dist/core/llm/stream-utils.js +5 -3
- package/dist/core/llm/sub-agent-factory.js +51 -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/resource-context-provider.js +3 -1
- package/dist/core/mcp/bridge/tool-bridge.js +5 -14
- package/dist/core/mcp/catalog/discovery.js +3 -1
- package/dist/core/mcp/client/connection-manager.js +7 -4
- package/dist/core/mcp/client/transport-factory.js +7 -3
- 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/audit-file.js +2 -1
- package/dist/core/observability/audit-trail.js +3 -1
- package/dist/core/observability/authorization-decisions.js +13 -12
- package/dist/core/observability/error-mapping.js +2 -1
- package/dist/core/observability/logger.js +2 -1
- package/dist/core/observability/monitor.js +24 -0
- package/dist/core/observability/run-outcome-reporter.js +1 -0
- package/dist/core/observability/token-usage.js +5 -4
- package/dist/core/permission-gate/default-gate.js +5 -8
- package/dist/core/plan/storage.js +7 -4
- package/dist/core/plugin/loader.js +8 -5
- package/dist/core/prompts/registry.js +12 -30
- package/dist/core/prompts/runtime.js +3 -1
- package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
- package/dist/core/protocols/a2a/sdk/executor.js +3 -1
- package/dist/core/protocols/a2a/sdk/server.js +5 -4
- package/dist/core/protocols/acp/acp-command-runner.js +7 -6
- package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
- package/dist/core/protocols/acp/formal-agent.js +13 -6
- package/dist/core/protocols/acp/permission-provider.js +3 -2
- package/dist/core/protocols/acp/stdio-server.js +6 -6
- package/dist/core/reflection/engine.js +114 -14
- package/dist/core/runtime/agent-server-runtime.js +3 -2
- package/dist/core/runtime/batch-runner.js +81 -0
- package/dist/core/runtime/initialize.js +71 -6
- package/dist/core/runtime/loop-finalize.js +3 -0
- package/dist/core/runtime/loop-session-runner.js +5 -0
- package/dist/core/runtime/loop.js +4 -0
- package/dist/core/runtime/paths.js +9 -6
- package/dist/core/runtime/spawn-interactive.js +5 -4
- package/dist/core/security/redaction.js +3 -2
- package/dist/core/session/compaction/index.js +4 -3
- package/dist/core/session/compression.js +3 -1
- package/dist/core/session/manager.js +26 -38
- package/dist/core/session/pruning-strategy.js +2 -1
- package/dist/core/session/token-tracker.js +27 -9
- package/dist/core/skills/parser.js +3 -2
- package/dist/core/skills/permissions.js +2 -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/checkpoint/manager.js +16 -10
- package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
- package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
- package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
- package/dist/core/strata/interaction/file-system-provider.js +2 -1
- package/dist/core/strata/layers/file-state-resolver.js +9 -7
- package/dist/core/strata/layers/immutable-git-layer.js +3 -1
- package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
- package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
- package/dist/core/strata/layers/worktree.js +9 -10
- package/dist/core/strata/runtime/environment.js +2 -1
- package/dist/core/strata/runtime/synchronizer.js +28 -26
- package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
- package/dist/core/structured-output/json-extract.js +3 -1
- package/dist/core/structured-output/json-schema-validator.js +1 -13
- package/dist/core/sub-agent/artifacts/store.js +2 -1
- 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 +343 -117
- package/dist/core/sub-agent/registry-defaults.js +12 -0
- package/dist/core/sub-agent/registry.js +8 -0
- package/dist/core/sub-agent/summary.js +96 -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 +52 -7
- package/dist/core/sub-agent/tools/team.js +92 -0
- package/dist/core/sub-agent/types.js +11 -2
- package/dist/core/target-runtime/profile.js +3 -1
- package/dist/core/tools/audit.js +3 -2
- package/dist/core/tools/budget.js +7 -12
- package/dist/core/tools/builtin/ast.js +144 -0
- package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
- package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
- package/dist/core/tools/builtin/code-search/executor.js +46 -43
- package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
- package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
- package/dist/core/tools/builtin/fs.js +90 -7
- package/dist/core/tools/builtin/git.js +242 -0
- package/dist/core/tools/builtin/glob.js +79 -0
- package/dist/core/tools/builtin/index.js +53 -111
- package/dist/core/tools/builtin/interaction.js +13 -15
- package/dist/core/tools/builtin/knowledge.js +146 -4
- package/dist/core/tools/builtin/proposal.js +14 -3
- package/dist/core/tools/builtin/verify.js +35 -3
- 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 +69 -115
- package/dist/core/tools/plugins/loader.js +4 -3
- package/dist/core/tools/router.js +112 -58
- package/dist/core/tools/session.js +64 -102
- 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/types/batch.js +2 -0
- package/dist/core/utils/error.js +79 -0
- package/dist/core/utils/sanitizer.js +5 -2
- package/dist/core/utils/serialize.js +66 -0
- package/dist/core/utils/zod.js +29 -0
- package/dist/core/verification/detect-runner.js +86 -0
- package/dist/core/verification/runner.js +76 -0
- package/dist/core/version.js +3 -1
- package/dist/core/workspace/capabilities.js +3 -2
- package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
- package/dist/languages/python/index.js +154 -0
- package/dist/locales/en.js +8 -1
- package/package.json +2 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { text } from '../../../locales/index.js';
|
|
2
|
+
import { getLogger } from '../../observability/logger.js';
|
|
2
3
|
import { normalizeDiff, validateDiff } from '../../patch/diff.js';
|
|
3
4
|
import { ArtifactStore } from '../../sub-agent/artifacts/store.js';
|
|
4
5
|
import { normalizeRepoRelativePath } from '../../utils/path.js';
|
|
6
|
+
import { isRecord } from '../../utils/serialize.js';
|
|
5
7
|
const DEFAULT_TOOL_ALIASES = {
|
|
6
8
|
bash: 'Bash',
|
|
7
9
|
read: 'Read',
|
|
@@ -218,57 +220,66 @@ function compilePathMatcher(specifier) {
|
|
|
218
220
|
matches: (repoRelativePath) => re.test(normalizeRepoRelativePath(repoRelativePath)),
|
|
219
221
|
};
|
|
220
222
|
}
|
|
223
|
+
const TOOL_CATEGORY = {
|
|
224
|
+
Bash: 'bash',
|
|
225
|
+
bash: 'bash',
|
|
226
|
+
'shell.exec': 'bash',
|
|
227
|
+
'test.run': 'bash',
|
|
228
|
+
Edit: 'edit',
|
|
229
|
+
edit: 'edit',
|
|
230
|
+
'proposal.apply': 'edit',
|
|
231
|
+
Read: 'path',
|
|
232
|
+
read: 'path',
|
|
233
|
+
LS: 'path',
|
|
234
|
+
ls: 'path',
|
|
235
|
+
'fs.read': 'path',
|
|
236
|
+
'code.read': 'path',
|
|
237
|
+
'git.cat': 'path',
|
|
238
|
+
'fs.list': 'path',
|
|
239
|
+
'fs.list_directory': 'path',
|
|
240
|
+
'fs.list_files': 'path',
|
|
241
|
+
'artifact.read': 'path',
|
|
242
|
+
};
|
|
243
|
+
function resolveToolCategory(tool) {
|
|
244
|
+
if (TOOL_CATEGORY[tool])
|
|
245
|
+
return TOOL_CATEGORY[tool];
|
|
246
|
+
if (isAliasToolName(tool)) {
|
|
247
|
+
const alias = DEFAULT_TOOL_ALIASES[tool.toLowerCase()];
|
|
248
|
+
return TOOL_CATEGORY[alias];
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
221
252
|
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) {
|
|
253
|
+
const { tool, raw, specifier } = parsed;
|
|
254
|
+
const category = resolveToolCategory(tool);
|
|
255
|
+
if (category === 'bash') {
|
|
245
256
|
return {
|
|
246
257
|
effect,
|
|
247
258
|
tool,
|
|
248
|
-
raw
|
|
259
|
+
raw,
|
|
249
260
|
specifier,
|
|
250
261
|
compiled: { kind: 'bash', matcher: compileBashMatcher(specifier) },
|
|
251
262
|
};
|
|
252
263
|
}
|
|
253
|
-
if (
|
|
264
|
+
if (category === 'edit') {
|
|
254
265
|
return {
|
|
255
266
|
effect,
|
|
256
267
|
tool,
|
|
257
|
-
raw
|
|
268
|
+
raw,
|
|
258
269
|
specifier,
|
|
259
270
|
compiled: { kind: 'edit', matcher: compilePathMatcher(specifier) },
|
|
260
271
|
};
|
|
261
272
|
}
|
|
262
|
-
if (
|
|
273
|
+
if (category === 'path') {
|
|
263
274
|
return {
|
|
264
275
|
effect,
|
|
265
276
|
tool,
|
|
266
|
-
raw
|
|
277
|
+
raw,
|
|
267
278
|
specifier,
|
|
268
279
|
compiled: { kind: 'path', matcher: compilePathMatcher(specifier) },
|
|
269
280
|
};
|
|
270
281
|
}
|
|
271
|
-
return { effect, tool, raw
|
|
282
|
+
return { effect, tool, raw, specifier, compiled: { kind: 'tool_any' } };
|
|
272
283
|
}
|
|
273
284
|
function buildVisibleToolNamesFromAllow(allowRules) {
|
|
274
285
|
const visible = new Set();
|
|
@@ -353,85 +364,42 @@ async function loadProposalChangedFiles(handle) {
|
|
|
353
364
|
const meta = validateDiff(normalized);
|
|
354
365
|
return meta.changedFiles ?? [];
|
|
355
366
|
}
|
|
356
|
-
catch {
|
|
367
|
+
catch (error) {
|
|
368
|
+
getLogger().warn(`[PermissionRules] Failed to load proposal changed files: ${error instanceof Error ? error.message : String(error)}`);
|
|
357
369
|
return null;
|
|
358
370
|
}
|
|
359
371
|
}
|
|
360
|
-
function
|
|
372
|
+
function matchRule(rule, toolName, args) {
|
|
361
373
|
if (!toolMatchesRuleTool(toolName, rule.tool))
|
|
362
374
|
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';
|
|
375
|
+
switch (rule.compiled.kind) {
|
|
376
|
+
case 'tool_any':
|
|
377
|
+
return true;
|
|
378
|
+
case 'bash': {
|
|
379
|
+
const cmd = extractCommandArg(toolName, args);
|
|
380
|
+
return cmd ? rule.compiled.matcher.matches(cmd) : false;
|
|
381
|
+
}
|
|
382
|
+
case 'path': {
|
|
383
|
+
const p = extractPrimaryPathArg(toolName, args);
|
|
384
|
+
return p ? rule.compiled.matcher.matches(p) : false;
|
|
385
|
+
}
|
|
386
|
+
case 'edit':
|
|
387
|
+
// Edit rules require async file-level matching; sync check is a gate.
|
|
388
|
+
return toolName === 'proposal.apply';
|
|
380
389
|
}
|
|
381
|
-
return false;
|
|
382
390
|
}
|
|
383
|
-
async function
|
|
391
|
+
async function matchEditRule(rule, args, filePredicate) {
|
|
384
392
|
if (rule.compiled.kind !== 'edit')
|
|
385
393
|
return false;
|
|
386
|
-
|
|
387
|
-
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
394
|
+
if (!isRecord(args))
|
|
388
395
|
return false;
|
|
389
396
|
const handle = args.handle;
|
|
390
397
|
if (typeof handle !== 'string' || !handle.trim())
|
|
391
398
|
return false;
|
|
392
399
|
const changedFiles = await loadProposalChangedFiles(handle);
|
|
393
|
-
if (!changedFiles)
|
|
394
|
-
return false;
|
|
395
|
-
if (changedFiles.length === 0)
|
|
400
|
+
if (!changedFiles || changedFiles.length === 0)
|
|
396
401
|
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;
|
|
402
|
+
return filePredicate(changedFiles, rule.compiled.matcher);
|
|
435
403
|
}
|
|
436
404
|
export async function decidePermissionForToolCall(options) {
|
|
437
405
|
const rules = options.rules;
|
|
@@ -442,17 +410,10 @@ export async function decidePermissionForToolCall(options) {
|
|
|
442
410
|
}
|
|
443
411
|
// Deny rules win.
|
|
444
412
|
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)) {
|
|
413
|
+
const isDeny = rule.compiled.kind === 'edit'
|
|
414
|
+
? await matchEditRule(rule, options.args, (files, matcher) => files.some((p) => matcher.matches(p)))
|
|
415
|
+
: matchRule(rule, options.toolName, options.args);
|
|
416
|
+
if (isDeny) {
|
|
456
417
|
return {
|
|
457
418
|
kind: 'deny',
|
|
458
419
|
reason: text.tools.permissionRuleDenied(rule.raw),
|
|
@@ -461,17 +422,10 @@ export async function decidePermissionForToolCall(options) {
|
|
|
461
422
|
}
|
|
462
423
|
}
|
|
463
424
|
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)) {
|
|
425
|
+
const isAllow = rule.compiled.kind === 'edit'
|
|
426
|
+
? await matchEditRule(rule, options.args, (files, matcher) => files.every((p) => matcher.matches(p)))
|
|
427
|
+
: matchRule(rule, options.toolName, options.args);
|
|
428
|
+
if (isAllow) {
|
|
475
429
|
return {
|
|
476
430
|
kind: 'allow',
|
|
477
431
|
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)) {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { z } from 'zod';
|
|
4
|
+
import { readFile } from '../adapters/fs/node-fs.js';
|
|
5
|
+
import { AstParser } from '../ast/parser.js';
|
|
3
6
|
import { LIMITS } from '../config/limits.js';
|
|
4
7
|
import { getLogger } from '../observability/logger.js';
|
|
8
|
+
import { isRecord } from '../utils/serialize.js';
|
|
9
|
+
import { unwrapZodSchema } from '../utils/zod.js';
|
|
5
10
|
import { decidePermissionForToolCall } from './permissions/permission-rules.js';
|
|
6
11
|
export class ToolRouter {
|
|
7
12
|
registry;
|
|
@@ -14,32 +19,7 @@ export class ToolRouter {
|
|
|
14
19
|
authorizationMode;
|
|
15
20
|
permissionRules;
|
|
16
21
|
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;
|
|
22
|
+
return unwrapZodSchema(schema);
|
|
43
23
|
}
|
|
44
24
|
buildInputHint(spec) {
|
|
45
25
|
if (!spec.inputSchema)
|
|
@@ -115,7 +95,7 @@ export class ToolRouter {
|
|
|
115
95
|
const message = hint
|
|
116
96
|
? `${inputCheck.message} (${hint})`
|
|
117
97
|
: inputCheck.message || 'Invalid input';
|
|
118
|
-
throw { code: 'INVALID_INPUT'
|
|
98
|
+
throw Object.assign(new Error(message), { code: 'INVALID_INPUT' });
|
|
119
99
|
}
|
|
120
100
|
const normalizedArgs = inputCheck.value ?? envelope.args;
|
|
121
101
|
const normalizedEnvelope = normalizedArgs === envelope.args ? envelope : { ...envelope, args: normalizedArgs };
|
|
@@ -133,7 +113,7 @@ export class ToolRouter {
|
|
|
133
113
|
ctx: normalizedEnvelope.ctx,
|
|
134
114
|
});
|
|
135
115
|
if (permissionDecision.kind === 'deny') {
|
|
136
|
-
const result = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason, {
|
|
116
|
+
const result = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason ?? 'Permission denied', {
|
|
137
117
|
authorization: {
|
|
138
118
|
outcome: 'deny',
|
|
139
119
|
reason: permissionDecision.reason,
|
|
@@ -178,7 +158,9 @@ export class ToolRouter {
|
|
|
178
158
|
},
|
|
179
159
|
});
|
|
180
160
|
// Provide a stable token for challenge-response UIs.
|
|
181
|
-
result.error
|
|
161
|
+
if (isRecord(result.error)) {
|
|
162
|
+
result.error.confirmToken = auth.challenge;
|
|
163
|
+
}
|
|
182
164
|
this.audit.onEnd(result);
|
|
183
165
|
return result;
|
|
184
166
|
}
|
|
@@ -199,7 +181,7 @@ export class ToolRouter {
|
|
|
199
181
|
// 5. Output Validation & Sanitize: Result validation and sensitive summary
|
|
200
182
|
const sanitized = this.sanitizer.sanitizeOutput(spec, rawOutput);
|
|
201
183
|
if (!sanitized.ok) {
|
|
202
|
-
throw { code: 'INVALID_OUTPUT'
|
|
184
|
+
throw Object.assign(new Error(sanitized.message), { code: 'INVALID_OUTPUT' });
|
|
203
185
|
}
|
|
204
186
|
// 6. Return Standard Result (ok)
|
|
205
187
|
const durationMs = Date.now() - startedAt;
|
|
@@ -213,6 +195,12 @@ export class ToolRouter {
|
|
|
213
195
|
outputSummary: sanitized.summary,
|
|
214
196
|
durationMs,
|
|
215
197
|
};
|
|
198
|
+
// Per-edit syntax guard: check for syntax errors after file writes
|
|
199
|
+
const syntaxWarnings = await checkPostEditSyntax(spec, normalizedEnvelope.args, rawOutput, normalizedEnvelope.ctx);
|
|
200
|
+
if (syntaxWarnings.length > 0) {
|
|
201
|
+
result.warnings = syntaxWarnings;
|
|
202
|
+
result.meta = { ...result.meta, syntaxWarning: true };
|
|
203
|
+
}
|
|
216
204
|
this.audit.onEnd(result);
|
|
217
205
|
return result;
|
|
218
206
|
}
|
|
@@ -222,28 +210,18 @@ export class ToolRouter {
|
|
|
222
210
|
let errorMeta;
|
|
223
211
|
if (e instanceof Error) {
|
|
224
212
|
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
213
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
214
|
+
const errObj = isRecord(e) ? e : null;
|
|
215
|
+
if (errObj) {
|
|
216
|
+
if (typeof errObj.message === 'string')
|
|
217
|
+
errorMessage = errObj.message;
|
|
218
|
+
if (typeof errObj.code === 'string')
|
|
219
|
+
errorCode = errObj.code;
|
|
220
|
+
if ('interrupt' in errObj) {
|
|
221
|
+
errorMeta = { ...(errorMeta ?? {}), interrupt: errObj.interrupt };
|
|
238
222
|
}
|
|
239
|
-
if ('
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
if ('interrupt' in e) {
|
|
243
|
-
errorMeta = { ...(errorMeta ?? {}), interrupt: e.interrupt };
|
|
244
|
-
}
|
|
245
|
-
if ('inputRequired' in e) {
|
|
246
|
-
errorMeta = { ...(errorMeta ?? {}), inputRequired: e.inputRequired };
|
|
223
|
+
if ('inputRequired' in errObj) {
|
|
224
|
+
errorMeta = { ...(errorMeta ?? {}), inputRequired: errObj.inputRequired };
|
|
247
225
|
}
|
|
248
226
|
}
|
|
249
227
|
const result = this.createErrorResult(envelope, startedAt, errorCode === 'TIMEOUT' ? 'timeout' : 'error', errorCode, errorMessage, errorMeta);
|
|
@@ -291,7 +269,7 @@ export class ToolRouter {
|
|
|
291
269
|
ctx: normalizedEnvelope.ctx,
|
|
292
270
|
});
|
|
293
271
|
if (permissionDecision.kind === 'deny') {
|
|
294
|
-
const toolResult = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason, {
|
|
272
|
+
const toolResult = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason ?? 'Permission denied', {
|
|
295
273
|
authorization: {
|
|
296
274
|
outcome: 'deny',
|
|
297
275
|
reason: permissionDecision.reason,
|
|
@@ -315,7 +293,7 @@ export class ToolRouter {
|
|
|
315
293
|
source: spec.source,
|
|
316
294
|
phase: normalizedEnvelope.phase,
|
|
317
295
|
riskLevel: spec.riskLevel,
|
|
318
|
-
sideEffects:
|
|
296
|
+
sideEffects: spec.sideEffects,
|
|
319
297
|
argsSummary,
|
|
320
298
|
argsHash,
|
|
321
299
|
repoRoot: normalizedEnvelope.ctx.repoRoot,
|
|
@@ -333,7 +311,9 @@ export class ToolRouter {
|
|
|
333
311
|
source: 'user',
|
|
334
312
|
},
|
|
335
313
|
});
|
|
336
|
-
toolResult.error
|
|
314
|
+
if (isRecord(toolResult.error)) {
|
|
315
|
+
toolResult.error.confirmToken = deferred.challenge;
|
|
316
|
+
}
|
|
337
317
|
return {
|
|
338
318
|
kind: 'pending',
|
|
339
319
|
message: deferred.message,
|
|
@@ -408,7 +388,8 @@ export class ToolRouter {
|
|
|
408
388
|
return raw;
|
|
409
389
|
return `${raw.slice(0, maxLength)}...`;
|
|
410
390
|
}
|
|
411
|
-
catch {
|
|
391
|
+
catch (error) {
|
|
392
|
+
getLogger().debug(`[ToolRouter] Failed to summarize args: ${error instanceof Error ? error.message : String(error)}`);
|
|
412
393
|
return '[Unserializable]';
|
|
413
394
|
}
|
|
414
395
|
}
|
|
@@ -421,7 +402,8 @@ export class ToolRouter {
|
|
|
421
402
|
// Truncation to 16 hex was insufficient collision resistance for security use.
|
|
422
403
|
return crypto.createHash('sha256').update(raw).digest('hex');
|
|
423
404
|
}
|
|
424
|
-
catch {
|
|
405
|
+
catch (error) {
|
|
406
|
+
getLogger().debug(`[ToolRouter] Failed to hash args: ${error instanceof Error ? error.message : String(error)}`);
|
|
425
407
|
return undefined;
|
|
426
408
|
}
|
|
427
409
|
}
|
|
@@ -450,7 +432,7 @@ export class ToolRouter {
|
|
|
450
432
|
source: spec.source,
|
|
451
433
|
phase: envelope.phase,
|
|
452
434
|
riskLevel: spec.riskLevel,
|
|
453
|
-
sideEffects: (spec.sideEffects
|
|
435
|
+
sideEffects: (spec.sideEffects ?? []),
|
|
454
436
|
argsSummary,
|
|
455
437
|
argsHash,
|
|
456
438
|
repoRoot: envelope.ctx.repoRoot,
|
|
@@ -511,7 +493,7 @@ export class ToolRouter {
|
|
|
511
493
|
}
|
|
512
494
|
async getAuthorizationArgsSummary(envelope, spec) {
|
|
513
495
|
const fallback = this.summarizeArgs(envelope.args);
|
|
514
|
-
const summarize = spec
|
|
496
|
+
const summarize = spec.summarizeArgsForAuthorization;
|
|
515
497
|
if (typeof summarize !== 'function')
|
|
516
498
|
return fallback;
|
|
517
499
|
// Best-effort only. Avoid hanging authorization prompts on slow IO.
|
|
@@ -524,9 +506,81 @@ export class ToolRouter {
|
|
|
524
506
|
]);
|
|
525
507
|
return typeof result === 'string' && result.trim() ? result : fallback;
|
|
526
508
|
}
|
|
527
|
-
catch {
|
|
509
|
+
catch (error) {
|
|
510
|
+
getLogger().debug(`[ToolRouter] Failed to get authorization args summary: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
511
|
return fallback;
|
|
529
512
|
}
|
|
530
513
|
}
|
|
531
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Per-edit syntax guard: after a file write, parse the file with tree-sitter
|
|
517
|
+
* and return syntax error warnings. Non-blocking — returns empty array on
|
|
518
|
+
* any failure (missing grammar, parse error, etc.).
|
|
519
|
+
*/
|
|
520
|
+
async function checkPostEditSyntax(spec, args, rawOutput, ctx) {
|
|
521
|
+
if (spec.name !== 'fs.write_file' && spec.name !== 'fs.edit_file')
|
|
522
|
+
return [];
|
|
523
|
+
if (!isRecord(rawOutput) || typeof rawOutput.path !== 'string')
|
|
524
|
+
return [];
|
|
525
|
+
const filePath = rawOutput.path;
|
|
526
|
+
let content;
|
|
527
|
+
if (spec.name === 'fs.write_file') {
|
|
528
|
+
if (!isRecord(args) || typeof args.content !== 'string')
|
|
529
|
+
return [];
|
|
530
|
+
content = args.content;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// fs.edit_file — read post-edit content from disk
|
|
534
|
+
try {
|
|
535
|
+
const absolutePath = path.resolve(ctx.repoRoot, filePath);
|
|
536
|
+
content = await readFile(absolutePath, 'utf-8');
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
getLogger().debug(`[ToolRouter] Failed to read file for post-edit syntax check: ${error instanceof Error ? error.message : String(error)}`);
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Detect language from extension
|
|
544
|
+
const ext = path.extname(filePath).toLowerCase().replace('.', '');
|
|
545
|
+
if (!ext)
|
|
546
|
+
return [];
|
|
547
|
+
// Only check for languages that have tree-sitter support
|
|
548
|
+
const plugin = ctx.languagePlugins?.getByExtension(`.${ext}`);
|
|
549
|
+
if (!plugin)
|
|
550
|
+
return [];
|
|
551
|
+
try {
|
|
552
|
+
const tree = await AstParser.parse(content, plugin.meta.id);
|
|
553
|
+
if (!tree?.rootNode)
|
|
554
|
+
return [];
|
|
555
|
+
const errors = collectSyntaxErrors(tree.rootNode);
|
|
556
|
+
if (errors.length === 0)
|
|
557
|
+
return [];
|
|
558
|
+
return [
|
|
559
|
+
`Syntax warning in ${filePath}: ${errors.length} error(s) detected — ` +
|
|
560
|
+
errors
|
|
561
|
+
.slice(0, 3)
|
|
562
|
+
.map((e) => `line ${e.line}: ${e.text}`)
|
|
563
|
+
.join('; '),
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
// Tree-sitter parse failed (no grammar, etc.) — silently skip
|
|
568
|
+
getLogger().debug(`[ToolRouter] Post-edit syntax check parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function collectSyntaxErrors(node, errors = [], depth = 0) {
|
|
573
|
+
if (depth > 50)
|
|
574
|
+
return errors; // prevent stack overflow
|
|
575
|
+
if (node.type === 'ERROR' || node.isMissing) {
|
|
576
|
+
errors.push({
|
|
577
|
+
line: (node.startPosition?.row ?? 0) + 1,
|
|
578
|
+
text: node.text?.slice(0, 80) ?? node.type,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
for (const child of node.children ?? []) {
|
|
582
|
+
collectSyntaxErrors(child, errors, depth + 1);
|
|
583
|
+
}
|
|
584
|
+
return errors;
|
|
585
|
+
}
|
|
532
586
|
//# sourceMappingURL=router.js.map
|