salmon-loop 0.2.13 → 0.3.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/argv/headless-detection.js +27 -0
- package/dist/cli/chat-flow.js +11 -0
- package/dist/cli/chat.js +160 -24
- package/dist/cli/commands/chat.js +14 -7
- package/dist/cli/commands/flow-mode.js +63 -0
- package/dist/cli/commands/registry.js +2 -0
- package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
- package/dist/cli/commands/run/early-errors.js +23 -0
- package/dist/cli/commands/run/handler.js +115 -27
- package/dist/cli/commands/run/headless-error-writer.js +8 -0
- package/dist/cli/commands/run/loop-params.js +2 -0
- package/dist/cli/commands/run/mode.js +2 -5
- package/dist/cli/commands/run/parse-options.js +16 -0
- package/dist/cli/commands/run/persist-session.js +10 -1
- package/dist/cli/commands/run/preflight.js +10 -0
- package/dist/cli/commands/run/reporter-factory.js +4 -0
- package/dist/cli/commands/run/runtime-llm.js +38 -11
- package/dist/cli/commands/run/runtime-options.js +2 -2
- package/dist/cli/commands/serve.js +97 -77
- package/dist/cli/commands/tool-names.js +78 -78
- package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
- package/dist/cli/headless/json-protocol.js +37 -0
- package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
- package/dist/cli/headless/protocol-metadata.js +22 -0
- package/dist/cli/headless/stream-json-protocol.js +34 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/locales/en.js +30 -6
- package/dist/cli/program-bootstrap.js +10 -5
- package/dist/cli/program-commands.js +5 -1
- package/dist/cli/reporters/anthropic-stream.js +7 -1
- package/dist/cli/reporters/json.js +4 -0
- package/dist/cli/reporters/stream-json.js +17 -2
- package/dist/cli/run-cli.js +5 -3
- package/dist/cli/slash/runtime.js +27 -12
- package/dist/cli/ui/components/CommandInput.js +7 -3
- package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
- package/dist/cli/utils/command-option-source.js +13 -0
- package/dist/cli/utils/verify-resolver.js +8 -4
- package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
- package/dist/core/adapters/fs/file-adapter.js +6 -0
- package/dist/core/adapters/fs/filesystem.js +2 -1
- package/dist/core/adapters/git/git-adapter.js +78 -1
- package/dist/core/backends/salmon-loop/task-executor.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +124 -0
- package/dist/core/benchmark/swe-bench.js +25 -0
- package/dist/core/config/load.js +18 -11
- package/dist/core/config/resolve-llm.js +12 -0
- package/dist/core/config/resolvers/server.js +0 -6
- package/dist/core/config/validate.js +73 -21
- package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
- package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
- package/dist/core/context/keywords.js +18 -4
- package/dist/core/context/service-deps.js +2 -2
- package/dist/core/context/service.js +8 -0
- package/dist/core/context/steps/context-gather.js +38 -0
- package/dist/core/context/summarization/summarizer.js +55 -12
- package/dist/core/context/targeting/target-resolver.js +4 -4
- package/dist/core/extensions/index.js +23 -5
- package/dist/core/extensions/merge.js +14 -0
- package/dist/core/extensions/paths.js +31 -0
- package/dist/core/extensions/schemas.js +8 -5
- package/dist/core/facades/cli-chat.js +6 -2
- package/dist/core/facades/cli-command-chat.js +1 -0
- package/dist/core/facades/cli-command-tool-names.js +2 -0
- package/dist/core/facades/cli-observability.js +1 -1
- package/dist/core/facades/cli-program-bootstrap.js +1 -0
- package/dist/core/facades/cli-run-handler.js +4 -2
- package/dist/core/facades/cli-run-persist-session.js +1 -0
- package/dist/core/facades/cli-serve.js +4 -4
- package/dist/core/facades/cli-utils-worktree.js +1 -1
- package/dist/core/failure/diagnostics.js +53 -1
- package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
- package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
- package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
- package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
- package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
- package/dist/core/grizzco/steps/answer.js +13 -14
- package/dist/core/grizzco/steps/autopilot.js +396 -0
- package/dist/core/grizzco/steps/cache-sharing.js +29 -0
- package/dist/core/grizzco/steps/explore.js +37 -21
- package/dist/core/grizzco/steps/generateReview.js +2 -5
- package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
- package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
- package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
- package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
- package/dist/core/grizzco/steps/patch.js +105 -146
- package/dist/core/grizzco/steps/plan.js +101 -25
- package/dist/core/grizzco/steps/preflight.js +5 -6
- package/dist/core/grizzco/steps/request-assembly.js +78 -0
- package/dist/core/grizzco/steps/research.js +39 -36
- package/dist/core/grizzco/steps/tool-runtime.js +47 -0
- package/dist/core/grizzco/steps/verify-shared.js +23 -0
- package/dist/core/grizzco/steps/verify.js +13 -21
- package/dist/core/interaction/orchestration/facade.js +1 -1
- package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
- package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
- package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
- package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
- package/dist/core/llm/ai-sdk/request-params.js +113 -1
- package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
- package/dist/core/llm/ai-sdk.js +112 -27
- package/dist/core/llm/capabilities.js +12 -0
- package/dist/core/llm/contracts/repair.js +36 -30
- package/dist/core/llm/errors.js +83 -2
- package/dist/core/llm/message-composition.js +7 -22
- package/dist/core/llm/phase-router.js +29 -10
- package/dist/core/llm/redact.js +28 -3
- package/dist/core/llm/registry.js +2 -0
- package/dist/core/llm/request-augmentation.js +55 -0
- package/dist/core/llm/request-envelope.js +334 -0
- package/dist/core/llm/shared-request-assembly.js +35 -0
- package/dist/core/llm/stream-utils.js +13 -4
- package/dist/core/llm/utils.js +18 -29
- package/dist/core/memory/relevant-retrieval.js +144 -0
- package/dist/core/observability/logger.js +11 -2
- package/dist/core/patch/diff.js +1 -0
- package/dist/core/prompts/registry.js +39 -2
- package/dist/core/prompts/runtime.js +50 -12
- package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
- package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
- package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
- package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
- package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
- package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
- package/dist/core/prompts/templates/system/main_system.hbs +4 -16
- package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
- package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
- package/dist/core/prompts/templates/system/research_system.hbs +2 -0
- package/dist/core/protocols/a2a/agent-card.js +5 -3
- package/dist/core/protocols/a2a/sdk/executor.js +2 -1
- package/dist/core/protocols/a2a/sdk/server.js +0 -1
- package/dist/core/protocols/acp/formal-agent.js +300 -58
- package/dist/core/protocols/acp/handlers.js +5 -1
- package/dist/core/protocols/acp/permission-provider.js +1 -1
- package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
- package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
- package/dist/core/public-capabilities/projections.js +29 -0
- package/dist/core/public-capabilities/registry.js +26 -0
- package/dist/core/public-capabilities/types.js +2 -0
- package/dist/core/runtime/agent-server-runtime.js +47 -43
- package/dist/core/runtime/execution-profile.js +67 -0
- package/dist/core/session/artifact-state.js +160 -0
- package/dist/core/session/compaction/index.js +183 -0
- package/dist/core/session/compaction/microcompact.js +78 -0
- package/dist/core/session/compaction/tracking.js +48 -0
- package/dist/core/session/compaction/types.js +11 -0
- package/dist/core/session/compression.js +8 -0
- package/dist/core/session/manager.js +244 -8
- package/dist/core/session/pruning-strategy.js +55 -9
- package/dist/core/session/replacement-preview-provider.js +24 -0
- package/dist/core/session/replacement-state.js +131 -0
- package/dist/core/session/resume-repair/pipeline.js +79 -0
- package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
- package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
- package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
- package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
- package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
- package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
- package/dist/core/session/resume-repair/types.js +2 -0
- package/dist/core/session/summary-sync.js +164 -13
- package/dist/core/session/token-tracker.js +6 -0
- package/dist/core/skills/audit.js +34 -0
- package/dist/core/skills/bridge.js +84 -7
- package/dist/core/skills/discovery.js +94 -0
- package/dist/core/skills/feature-flags.js +52 -0
- package/dist/core/skills/index.js +1 -1
- package/dist/core/skills/loader.js +195 -20
- package/dist/core/skills/parser.js +296 -24
- package/dist/core/skills/permissions.js +117 -0
- package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
- package/dist/core/skills/runtime/SkillRunner.js +240 -61
- package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
- package/dist/core/strata/layers/worktree.js +67 -10
- package/dist/core/strata/runtime/synchronizer.js +29 -2
- package/dist/core/streaming/stream-assembler.js +75 -31
- package/dist/core/sub-agent/context-snapshot.js +156 -0
- package/dist/core/sub-agent/core/loop.js +1 -1
- package/dist/core/sub-agent/core/manager.js +119 -20
- package/dist/core/sub-agent/dispatch-policy.js +29 -0
- package/dist/core/sub-agent/prefix-consistency.js +48 -0
- package/dist/core/sub-agent/registry-defaults.js +4 -0
- package/dist/core/sub-agent/tools/task-spawn.js +79 -2
- package/dist/core/sub-agent/types.js +134 -5
- package/dist/core/tools/audit.js +13 -4
- package/dist/core/tools/builtin/ast-grep.js +1 -1
- package/dist/core/tools/builtin/ast.js +1 -1
- package/dist/core/tools/builtin/benchmark.js +360 -0
- package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
- package/dist/core/tools/builtin/code-search/executor.js +6 -1
- package/dist/core/tools/builtin/code-search/spec.js +26 -2
- package/dist/core/tools/builtin/fs.js +256 -23
- package/dist/core/tools/builtin/git.js +2 -2
- package/dist/core/tools/builtin/index.js +51 -2
- package/dist/core/tools/builtin/interaction.js +8 -1
- package/dist/core/tools/builtin/plan.js +37 -15
- package/dist/core/tools/builtin/shell.js +1 -1
- package/dist/core/tools/loader.js +39 -16
- package/dist/core/tools/mapper.js +17 -3
- package/dist/core/tools/mcp/client.js +2 -1
- package/dist/core/tools/parallel/scheduler.js +35 -4
- package/dist/core/tools/permissions/permission-rules.js +5 -10
- package/dist/core/tools/policy.js +6 -1
- package/dist/core/tools/recoverable-tool-errors.js +10 -0
- package/dist/core/tools/router.js +24 -6
- package/dist/core/tools/session.js +458 -48
- package/dist/core/tools/tool-visibility.js +62 -0
- package/dist/core/tools/types.js +9 -1
- package/dist/core/types/execution.js +4 -0
- package/dist/core/types/flow-mode.js +8 -0
- package/dist/core/utils/path.js +52 -0
- package/dist/core/verification/runner.js +4 -1
- package/dist/core/version.js +17 -0
- package/dist/languages/typescript/index.js +4 -1
- package/dist/locales/en.js +35 -2
- package/dist/utils/eol.js +1 -1
- package/package.json +14 -7
- package/scripts/fix-es-abstract-compat.js +77 -0
- package/dist/core/runtime/fastify-server-bundle.js +0 -26
- package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
- package/dist/core/runtime/sidecar-paths.js +0 -47
- package/dist/core/runtime/sidecar-route-catalog.js +0 -103
|
@@ -1,8 +1,103 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
2
|
import { MicroTaskRunner } from '../../grizzco/dsl/MicroTaskRunner.js';
|
|
3
|
+
import { tryGetLogger } from '../../observability/logger.js';
|
|
3
4
|
import { Phase } from '../../types/index.js';
|
|
5
|
+
import { emitSkillAuditEvent, generateSkillTraceId, hashSkillArgs } from '../audit.js';
|
|
4
6
|
import { SkillParser } from '../parser.js';
|
|
5
7
|
import { SkillStrategyDSL } from '../strategy.js';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the effective allowed-tools set for a skill.
|
|
10
|
+
*
|
|
11
|
+
* Uses only the AgentSkills spec field (`allowed-tools`, space-delimited string)
|
|
12
|
+
* and returns a single `Set<string>`. Returns `null` when the field is not
|
|
13
|
+
* declared, meaning the skill places no tool restrictions.
|
|
14
|
+
*
|
|
15
|
+
* Distinguishes three states:
|
|
16
|
+
* - Field not declared → `null` (no restriction)
|
|
17
|
+
* - Field declared but empty (`""`) → empty `Set` (deny all tools)
|
|
18
|
+
* - Field declared with values → `Set` containing those tool names
|
|
19
|
+
*
|
|
20
|
+
* @see https://agentskills.io/specification — allowed-tools field
|
|
21
|
+
*/
|
|
22
|
+
function resolveAllowedTools(skill) {
|
|
23
|
+
const specField = skill.metadata?.['allowed-tools'];
|
|
24
|
+
if (specField === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
if (!specField.trim())
|
|
27
|
+
return new Set();
|
|
28
|
+
return new Set(specField.split(/\s+/).filter(Boolean));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Match a tool name against an allowed-tools pattern.
|
|
32
|
+
*
|
|
33
|
+
* Supports two modes:
|
|
34
|
+
* - Exact match: pattern contains no `*` → strict string equality
|
|
35
|
+
* - Glob match: pattern contains `*` → each `*` matches zero or more characters
|
|
36
|
+
*
|
|
37
|
+
* Case-sensitive. Only `*` is treated as special; no `?` or `[...]` ranges.
|
|
38
|
+
*
|
|
39
|
+
* Uses an iterative segment-matching algorithm (no regex) to avoid ReDoS risk.
|
|
40
|
+
*
|
|
41
|
+
* @param pattern - An allowed-tools entry (e.g. "shell.*", "code.search")
|
|
42
|
+
* @param toolName - The tool name to check (e.g. "shell.exec")
|
|
43
|
+
* @returns true if toolName matches the pattern
|
|
44
|
+
*/
|
|
45
|
+
export function matchAllowedTool(pattern, toolName) {
|
|
46
|
+
// Split pattern on '*' into literal segments
|
|
47
|
+
const segments = pattern.split('*');
|
|
48
|
+
// No wildcard → exact match
|
|
49
|
+
if (segments.length === 1) {
|
|
50
|
+
return pattern === toolName;
|
|
51
|
+
}
|
|
52
|
+
const first = segments[0];
|
|
53
|
+
const last = segments[segments.length - 1];
|
|
54
|
+
// toolName must start with the first segment
|
|
55
|
+
if (!toolName.startsWith(first)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// toolName must end with the last segment
|
|
59
|
+
if (!toolName.endsWith(last)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
// Guard: the tool name must be long enough to contain all literal segments
|
|
63
|
+
// without overlap between the first/last anchors
|
|
64
|
+
let pos = first.length;
|
|
65
|
+
const endBound = toolName.length - last.length;
|
|
66
|
+
// Match each middle segment in order (greedy left-to-right scan)
|
|
67
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
68
|
+
const seg = segments[i];
|
|
69
|
+
if (seg === '') {
|
|
70
|
+
// Consecutive '*' — matches any amount of characters, skip
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const idx = toolName.indexOf(seg, pos);
|
|
74
|
+
if (idx === -1 || idx + seg.length > endBound) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
pos = idx + seg.length;
|
|
78
|
+
}
|
|
79
|
+
return pos <= endBound;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check whether a tool name is permitted by the allowed-tools set.
|
|
83
|
+
*
|
|
84
|
+
* @param toolName - The tool name to check
|
|
85
|
+
* @param allowedTools - The resolved allowed-tools set, or null for no restriction
|
|
86
|
+
* @returns true if the tool is permitted
|
|
87
|
+
*/
|
|
88
|
+
export function isToolPermitted(toolName, allowedTools) {
|
|
89
|
+
// null means no restriction — all tools permitted
|
|
90
|
+
if (allowedTools === null) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
// Iterate entries; return true if any pattern matches
|
|
94
|
+
for (const pattern of allowedTools) {
|
|
95
|
+
if (matchAllowedTool(pattern, toolName)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
6
101
|
function buildStableId(parts) {
|
|
7
102
|
return crypto.createHash('sha256').update(parts.join('\n')).digest('hex').slice(0, 16);
|
|
8
103
|
}
|
|
@@ -29,8 +124,24 @@ function formatShellTranscript(shellOutputs) {
|
|
|
29
124
|
*/
|
|
30
125
|
export async function executeSkill(options) {
|
|
31
126
|
const { skill, argsText, toolRouter, toolCtx, signal } = options;
|
|
127
|
+
const route = options.route ?? 'slash-governed';
|
|
32
128
|
const inputs = { args: argsText ?? '' };
|
|
33
129
|
const rawCommands = SkillParser.extractCommands(skill.instructions || '');
|
|
130
|
+
const traceId = generateSkillTraceId(skill.id);
|
|
131
|
+
const argsHash = hashSkillArgs(argsText);
|
|
132
|
+
const startedAt = Date.now();
|
|
133
|
+
const allowedTools = resolveAllowedTools(skill);
|
|
134
|
+
// Emit SKILL_EXECUTION_START before execution
|
|
135
|
+
emitSkillAuditEvent({
|
|
136
|
+
type: 'SKILL_EXECUTION_START',
|
|
137
|
+
skillId: skill.id,
|
|
138
|
+
route,
|
|
139
|
+
runnerClass: 'MicroTaskRunner',
|
|
140
|
+
commandCount: rawCommands.length,
|
|
141
|
+
authorizationMode: 'blocking',
|
|
142
|
+
argsHash,
|
|
143
|
+
traceId,
|
|
144
|
+
});
|
|
34
145
|
const requiredShKeys = rawCommands.map((cmd) => `sh:${SkillParser.substituteVariables(cmd, inputs)}`);
|
|
35
146
|
const data = {
|
|
36
147
|
skill,
|
|
@@ -43,66 +154,134 @@ export async function executeSkill(options) {
|
|
|
43
154
|
skillId: skill.id,
|
|
44
155
|
path: skill.path,
|
|
45
156
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
...
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
try {
|
|
158
|
+
const runner = new MicroTaskRunner({
|
|
159
|
+
debugLabel: `SkillRunner:${skill.id}`,
|
|
160
|
+
maxRounds: 10,
|
|
161
|
+
strategy: (engine) => {
|
|
162
|
+
// Computation phase: assemble a prompt without any "!..." lines.
|
|
163
|
+
const promptLines = (skill.instructions || '')
|
|
164
|
+
.split('\n')
|
|
165
|
+
.filter((line) => !line.trim().startsWith('!'));
|
|
166
|
+
const basePrompt = SkillParser.substituteVariables(promptLines.join('\n').trim(), inputs);
|
|
167
|
+
const transcript = formatShellTranscript(data.shell_outputs);
|
|
168
|
+
data.prompt = `${basePrompt}${transcript}`.trim();
|
|
169
|
+
SkillStrategyDSL(engine);
|
|
170
|
+
return engine;
|
|
171
|
+
},
|
|
172
|
+
resolveData: async (_ctx, key) => {
|
|
173
|
+
if (!key.startsWith('sh:')) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const command = key.slice(3);
|
|
177
|
+
const toolName = 'shell.exec';
|
|
178
|
+
// Enforce allowed-tools constraint from skill frontmatter.
|
|
179
|
+
// When declared, only pre-approved tools may be invoked.
|
|
180
|
+
// Uses isToolPermitted for glob pattern matching support.
|
|
181
|
+
if (!isToolPermitted(toolName, allowedTools)) {
|
|
182
|
+
const logger = tryGetLogger();
|
|
183
|
+
logger?.warn(`Skill "${skill.id}" attempted to use tool "${toolName}" which is not in allowed-tools: [${[...(allowedTools ?? [])].join(', ')}]`);
|
|
184
|
+
emitSkillAuditEvent({
|
|
185
|
+
type: 'SKILL_EXECUTION_DENIED',
|
|
186
|
+
skillId: skill.id,
|
|
187
|
+
route,
|
|
188
|
+
runnerClass: 'MicroTaskRunner',
|
|
189
|
+
commandCount: rawCommands.length,
|
|
190
|
+
authorizationMode: 'blocking',
|
|
191
|
+
argsHash,
|
|
192
|
+
traceId,
|
|
193
|
+
denyReason: 'ALLOWED_TOOLS_VIOLATION',
|
|
194
|
+
denySource: `skill-frontmatter:allowed-tools`,
|
|
195
|
+
durationMs: Date.now() - startedAt,
|
|
196
|
+
});
|
|
197
|
+
throw new Error(`Tool "${toolName}" is not permitted by skill "${skill.id}" allowed-tools policy`);
|
|
198
|
+
}
|
|
199
|
+
const callId = `slash-sh-${buildStableId([skill.id, command])}`;
|
|
200
|
+
const envelope = {
|
|
201
|
+
id: callId,
|
|
202
|
+
phase: Phase.SLASH,
|
|
203
|
+
toolName: 'shell.exec',
|
|
204
|
+
args: { command },
|
|
205
|
+
ctx: {
|
|
206
|
+
...toolCtx,
|
|
207
|
+
// Ensure ToolPolicy sees worktree isolation for process execution.
|
|
208
|
+
worktreeRoot: toolCtx.worktreeRoot ?? toolCtx.repoRoot,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
let result = await toolRouter.call(envelope);
|
|
212
|
+
if (result.status === 'denied' && result.error?.code === 'AUTH_REQUIRED') {
|
|
213
|
+
await toolRouter.waitForAuthorization(callId, signal);
|
|
214
|
+
result = await toolRouter.call(envelope);
|
|
215
|
+
}
|
|
216
|
+
if (result.status !== 'ok') {
|
|
217
|
+
const msg = result.error?.message || 'shell.exec failed';
|
|
218
|
+
// Emit SKILL_EXECUTION_DENIED for command-level denial
|
|
219
|
+
if (result.status === 'denied') {
|
|
220
|
+
emitSkillAuditEvent({
|
|
221
|
+
type: 'SKILL_EXECUTION_DENIED',
|
|
222
|
+
skillId: skill.id,
|
|
223
|
+
route,
|
|
224
|
+
runnerClass: 'MicroTaskRunner',
|
|
225
|
+
commandCount: rawCommands.length,
|
|
226
|
+
authorizationMode: 'blocking',
|
|
227
|
+
argsHash,
|
|
228
|
+
traceId,
|
|
229
|
+
denyReason: result.error?.code || 'unknown',
|
|
230
|
+
denySource: result.meta?.authorization?.source || 'policy',
|
|
231
|
+
durationMs: Date.now() - startedAt,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
throw new Error(msg);
|
|
235
|
+
}
|
|
236
|
+
const output = result.output;
|
|
237
|
+
const combined = [output.stdout, output.stderr].filter(Boolean).join('\n').trim();
|
|
238
|
+
data.shell_outputs[command] = combined;
|
|
239
|
+
data[key] = combined;
|
|
240
|
+
return combined;
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
const decided = await runner.decide(ctx);
|
|
244
|
+
const plan = decided.plan;
|
|
245
|
+
const inject = plan.actions.find((a) => a.type === 'INJECT_PROMPT');
|
|
246
|
+
const status = plan.shouldAbort ? 'FAILURE' : 'SUCCESS';
|
|
247
|
+
// Emit SKILL_EXECUTION_END after successful execution
|
|
248
|
+
emitSkillAuditEvent({
|
|
249
|
+
type: 'SKILL_EXECUTION_END',
|
|
250
|
+
skillId: skill.id,
|
|
251
|
+
route,
|
|
252
|
+
runnerClass: 'MicroTaskRunner',
|
|
253
|
+
commandCount: rawCommands.length,
|
|
254
|
+
authorizationMode: 'blocking',
|
|
255
|
+
argsHash,
|
|
256
|
+
traceId,
|
|
257
|
+
durationMs: Date.now() - startedAt,
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
traceId,
|
|
261
|
+
skillId: skill.id,
|
|
262
|
+
inputs,
|
|
263
|
+
dynamicCommands: Object.entries(data.shell_outputs).map(([cmd, output]) => ({
|
|
264
|
+
cmd,
|
|
265
|
+
output: String(output),
|
|
266
|
+
})),
|
|
267
|
+
injectedPrompt: String(inject?.params?.prompt ?? ''),
|
|
268
|
+
status,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
// Emit SKILL_EXECUTION_END on failure (non-denial errors)
|
|
273
|
+
emitSkillAuditEvent({
|
|
274
|
+
type: 'SKILL_EXECUTION_END',
|
|
275
|
+
skillId: skill.id,
|
|
276
|
+
route,
|
|
277
|
+
runnerClass: 'MicroTaskRunner',
|
|
278
|
+
commandCount: rawCommands.length,
|
|
279
|
+
authorizationMode: 'blocking',
|
|
280
|
+
argsHash,
|
|
281
|
+
traceId,
|
|
282
|
+
durationMs: Date.now() - startedAt,
|
|
283
|
+
});
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
107
286
|
}
|
|
108
287
|
//# sourceMappingURL=SkillRunner.js.map
|
|
@@ -5,16 +5,41 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { existsSync } from '../../../adapters/fs/node-fs.js';
|
|
8
|
-
import {
|
|
8
|
+
import { lstat, mkdir, realpath, rm, symlink } from '../../../adapters/fs/node-fs.js';
|
|
9
9
|
import { getLogger } from '../../../observability/logger.js';
|
|
10
10
|
import { spawnCommand } from '../../../runtime/process-runner.js';
|
|
11
|
-
import { normalizePath } from '../../../utils/path.js';
|
|
11
|
+
import { arePathsEquivalent, normalizePath } from '../../../utils/path.js';
|
|
12
12
|
import { getPlatformShellInvocation } from '../../../utils/platform-shell.js';
|
|
13
13
|
import { copyDir, linkDirLinux } from './copy-backend.js';
|
|
14
14
|
import { getEnvInjection } from './env.js';
|
|
15
15
|
import { isEnvironmentError } from './error-classifier.js';
|
|
16
16
|
import { enforceReadOnly, restoreWrite, acquireLock, releaseLock } from './readonly-lock.js';
|
|
17
17
|
import { determineStrategy, planDependencyPaths, detectDependencyPaths } from './strategy.js';
|
|
18
|
+
const DEPENDENCY_LINK_CONFLICT_CODES = new Set([
|
|
19
|
+
'EEXIST',
|
|
20
|
+
'EISDIR',
|
|
21
|
+
'ENOTEMPTY',
|
|
22
|
+
'ENOTDIR',
|
|
23
|
+
'EPERM',
|
|
24
|
+
]);
|
|
25
|
+
function getErrorCode(error) {
|
|
26
|
+
return error && typeof error === 'object' && 'code' in error
|
|
27
|
+
? error.code
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
async function pointsToExpectedDependency(sourcePath, targetPath) {
|
|
31
|
+
try {
|
|
32
|
+
await lstat(targetPath);
|
|
33
|
+
const [resolvedSourcePath, resolvedTargetPath] = await Promise.all([
|
|
34
|
+
realpath(sourcePath),
|
|
35
|
+
realpath(targetPath),
|
|
36
|
+
]);
|
|
37
|
+
return arePathsEquivalent(resolvedSourcePath, resolvedTargetPath);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
18
43
|
/**
|
|
19
44
|
* ShadowDriver Class
|
|
20
45
|
*/
|
|
@@ -49,13 +74,18 @@ export class ShadowDriver {
|
|
|
49
74
|
getLogger().debug(`Linked dependency: ${depPath}`);
|
|
50
75
|
}
|
|
51
76
|
catch (err) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
const errorCode = getErrorCode(err);
|
|
78
|
+
if (errorCode && DEPENDENCY_LINK_CONFLICT_CODES.has(errorCode)) {
|
|
79
|
+
const alreadyProjected = await pointsToExpectedDependency(sourcePath, targetDepPath);
|
|
80
|
+
if (alreadyProjected) {
|
|
81
|
+
getLogger().debug(`Dependency projection already matches source: ${depPath}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Dependency projection path conflict for ${depPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
85
|
}
|
|
57
86
|
else {
|
|
58
|
-
getLogger().
|
|
87
|
+
getLogger().warn(`Failed to link ${depPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
59
89
|
}
|
|
60
90
|
}
|
|
61
91
|
}
|
|
@@ -2,10 +2,11 @@ import { randomBytes } from 'crypto';
|
|
|
2
2
|
import { tmpdir } from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { text } from '../../../locales/index.js';
|
|
5
|
-
import { access, realpath, rm } from '../../adapters/fs/node-fs.js';
|
|
5
|
+
import { access, readdir, realpath, rm } from '../../adapters/fs/node-fs.js';
|
|
6
6
|
import { GitAdapter } from '../../adapters/git/git-adapter.js';
|
|
7
7
|
import { getLogger } from '../../observability/logger.js';
|
|
8
8
|
import { isPathWithinDirectory, normalizePath } from '../../utils/path.js';
|
|
9
|
+
import { detectDependencyPaths } from './shadow-driver/strategy.js';
|
|
9
10
|
function resolveEnvironmentMode(options) {
|
|
10
11
|
return options.environmentMode === 'parity' ? 'parity' : 'strict';
|
|
11
12
|
}
|
|
@@ -28,12 +29,11 @@ function resolveParityWorktreeRoot(repoPath) {
|
|
|
28
29
|
return normalizePath(impl.join(impl.dirname(impl.resolve(normalizedRepoPath)), '.salmonloop', 'worktrees'));
|
|
29
30
|
}
|
|
30
31
|
function isManagedWorktreePath(baseRepoPath, workPath) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return false;
|
|
32
|
+
const comparableWorkPath = normalizePathForCompare(workPath);
|
|
33
|
+
const managedRoots = [tmpdir(), resolveParityWorktreeRoot(baseRepoPath)];
|
|
34
|
+
return managedRoots.some((root) => isPathWithinDirectory(normalizePathForCompare(root), comparableWorkPath, {
|
|
35
|
+
allowEqual: false,
|
|
36
|
+
}));
|
|
37
37
|
}
|
|
38
38
|
function normalizePathForCompare(value) {
|
|
39
39
|
const normalized = path.normalize(value).replace(/\\/g, '/');
|
|
@@ -67,6 +67,59 @@ async function resolveWorktreeMatchPath(worktreePaths, targetPath) {
|
|
|
67
67
|
}
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
|
+
async function removeProjectedWorktreeEntries(workPath) {
|
|
71
|
+
let worktreeRealPath;
|
|
72
|
+
try {
|
|
73
|
+
worktreeRealPath = await realpath(workPath);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(`Failed to resolve worktree path before git cleanup (${workPath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
let entries = [];
|
|
79
|
+
try {
|
|
80
|
+
entries = (await readdir(workPath, { withFileTypes: true }));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new Error(`Failed to enumerate worktree entries before git cleanup (${workPath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
84
|
+
}
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const name = entry?.name;
|
|
87
|
+
if (!name || name === '.git')
|
|
88
|
+
continue;
|
|
89
|
+
const entryPath = path.join(workPath, name);
|
|
90
|
+
const entryRealPath = await tryRealpath(entryPath);
|
|
91
|
+
if (!entryRealPath)
|
|
92
|
+
continue;
|
|
93
|
+
if (isPathWithinDirectory(worktreeRealPath, entryRealPath, { allowEqual: false })) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
await rm(entryPath, {
|
|
97
|
+
recursive: true,
|
|
98
|
+
force: true,
|
|
99
|
+
maxRetries: 3,
|
|
100
|
+
retryDelay: 100,
|
|
101
|
+
});
|
|
102
|
+
getLogger().debug(`Removed projected worktree entry before git cleanup: ${entryPath}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function pruneWorktreeDependencyRoots(baseRepoPath, worktreePath) {
|
|
106
|
+
const dependencyPaths = await detectDependencyPaths(baseRepoPath);
|
|
107
|
+
for (const dependencyPath of dependencyPaths) {
|
|
108
|
+
const dependencyRoot = path.join(worktreePath, dependencyPath);
|
|
109
|
+
try {
|
|
110
|
+
await rm(dependencyRoot, {
|
|
111
|
+
recursive: true,
|
|
112
|
+
force: true,
|
|
113
|
+
maxRetries: 3,
|
|
114
|
+
retryDelay: 100,
|
|
115
|
+
});
|
|
116
|
+
getLogger().debug(`Pruned disposable dependency root before worktree cleanup: ${dependencyRoot}`);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
getLogger().debug(`Failed to prune dependency root before worktree cleanup (${dependencyRoot}): ${error instanceof Error ? error.message : String(error)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
70
123
|
/**
|
|
71
124
|
* WorkspaceManager - Manages execution workspace for different checkpoint strategies
|
|
72
125
|
*
|
|
@@ -163,9 +216,14 @@ export class WorkspaceManager {
|
|
|
163
216
|
});
|
|
164
217
|
return;
|
|
165
218
|
}
|
|
219
|
+
// CRITICAL SAFETY: refuse any destructive cleanup outside managed worktree roots.
|
|
220
|
+
if (!isManagedWorktreePath(workspace.baseRepoPath, workspace.workPath)) {
|
|
221
|
+
throw new Error(text.errors.worktreePathNotInManagedRoots);
|
|
222
|
+
}
|
|
166
223
|
const git = new GitAdapter(workspace.baseRepoPath);
|
|
167
224
|
let removed = false;
|
|
168
225
|
try {
|
|
226
|
+
await pruneWorktreeDependencyRoots(workspace.baseRepoPath, workspace.workPath);
|
|
169
227
|
const list = await git.query(['worktree', 'list', '--porcelain']);
|
|
170
228
|
const worktreePaths = list
|
|
171
229
|
.split('\n')
|
|
@@ -175,6 +233,8 @@ export class WorkspaceManager {
|
|
|
175
233
|
.filter(Boolean);
|
|
176
234
|
const matchPath = await resolveWorktreeMatchPath(worktreePaths, workspace.workPath);
|
|
177
235
|
if (matchPath) {
|
|
236
|
+
// CRITICAL SAFETY: if projection inspection fails, do not risk git traversing external roots.
|
|
237
|
+
await removeProjectedWorktreeEntries(workspace.workPath);
|
|
178
238
|
await git.query(['worktree', 'remove', '--force', matchPath]);
|
|
179
239
|
removed = true;
|
|
180
240
|
const directoryStillExists = await (async () => {
|
|
@@ -224,9 +284,6 @@ export class WorkspaceManager {
|
|
|
224
284
|
getLogger().debug(`git worktree remove failed, falling back to filesystem removal: ${msg}`);
|
|
225
285
|
}
|
|
226
286
|
if (!removed) {
|
|
227
|
-
if (!isManagedWorktreePath(workspace.baseRepoPath, workspace.workPath)) {
|
|
228
|
-
throw new Error(text.errors.worktreePathNotInManagedRoots);
|
|
229
|
-
}
|
|
230
287
|
await rm(workspace.workPath, {
|
|
231
288
|
recursive: true,
|
|
232
289
|
force: true,
|
|
@@ -3,11 +3,12 @@ import { tmpdir } from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { text } from '../../../locales/index.js';
|
|
5
5
|
import { TextNormalizer } from '../../../utils/eol.js';
|
|
6
|
-
import { copyFile, lstat, mkdir, readFile, readdir, rm, stat, unlink, writeFile, } from '../../adapters/fs/node-fs.js';
|
|
6
|
+
import { copyFile, lstat, mkdir, readFile, readdir, realpath, rm, stat, unlink, writeFile, } from '../../adapters/fs/node-fs.js';
|
|
7
7
|
import { GitAdapter } from '../../adapters/git/git-adapter.js';
|
|
8
8
|
import { logIgnoredError } from '../../observability/ignored-error.js';
|
|
9
9
|
import { getLogger } from '../../observability/logger.js';
|
|
10
10
|
import { getMonitor } from '../../observability/monitor.js';
|
|
11
|
+
import { isCanonicalPathWithinDirectory } from '../../utils/path.js';
|
|
11
12
|
import { detectDependencyPaths } from '../layers/shadow-driver/strategy.js';
|
|
12
13
|
const SECURITY_BLOCKLIST = [
|
|
13
14
|
/^\.git(\/|\\)/i,
|
|
@@ -51,6 +52,27 @@ export class WorkspaceSynchronizer {
|
|
|
51
52
|
normalizePath(value) {
|
|
52
53
|
return value.replace(/\\/g, '/');
|
|
53
54
|
}
|
|
55
|
+
async tryRealPath(value) {
|
|
56
|
+
try {
|
|
57
|
+
return await realpath(value);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async isProjectedDependencyRoot(repoRealPath, candidatePath, entryStat) {
|
|
64
|
+
if (entryStat?.isSymbolicLink()) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (!repoRealPath) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const candidateRealPath = await this.tryRealPath(candidatePath);
|
|
71
|
+
if (!candidateRealPath) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return !isCanonicalPathWithinDirectory(repoRealPath, candidateRealPath, { allowEqual: true });
|
|
75
|
+
}
|
|
54
76
|
isRenameOrCopyStatus(xy) {
|
|
55
77
|
const x = xy.charAt(0);
|
|
56
78
|
const y = xy.charAt(1);
|
|
@@ -120,6 +142,7 @@ export class WorkspaceSynchronizer {
|
|
|
120
142
|
...detectedDependencyPaths,
|
|
121
143
|
]);
|
|
122
144
|
const symlinkedRoots = new Set();
|
|
145
|
+
const repoRealPath = await this.tryRealPath(repoPath);
|
|
123
146
|
for (const candidate of candidates) {
|
|
124
147
|
const normalizedCandidate = this.sanitizeRelativePath(candidate);
|
|
125
148
|
if (!normalizedCandidate || normalizedCandidate.includes('/')) {
|
|
@@ -128,7 +151,11 @@ export class WorkspaceSynchronizer {
|
|
|
128
151
|
const candidatePath = path.join(repoPath, ...normalizedCandidate.split('/'));
|
|
129
152
|
try {
|
|
130
153
|
const entryStat = await lstat(candidatePath);
|
|
131
|
-
|
|
154
|
+
const isProjectedRoot = await this.isProjectedDependencyRoot(repoRealPath, candidatePath, entryStat);
|
|
155
|
+
if (isProjectedRoot) {
|
|
156
|
+
if (!entryStat.isSymbolicLink()) {
|
|
157
|
+
getLogger().debug(`[checkpoint] Treating dependency root as projected via realpath escape: ${normalizedCandidate}`);
|
|
158
|
+
}
|
|
132
159
|
symlinkedRoots.add(normalizedCandidate);
|
|
133
160
|
}
|
|
134
161
|
}
|