salmon-loop 0.2.3 → 0.2.16

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.
Files changed (234) hide show
  1. package/dist/cli/argv/headless-detection.js +27 -0
  2. package/dist/cli/chat-flow.js +11 -0
  3. package/dist/cli/chat.js +161 -24
  4. package/dist/cli/commands/chat.js +30 -24
  5. package/dist/cli/commands/context.js +15 -3
  6. package/dist/cli/commands/flow-mode.js +63 -0
  7. package/dist/cli/commands/help-format.js +12 -0
  8. package/dist/cli/commands/registry.js +6 -7
  9. package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
  10. package/dist/cli/commands/run/config-resolution.js +30 -24
  11. package/dist/cli/commands/run/early-errors.js +23 -0
  12. package/dist/cli/commands/run/handler.js +131 -44
  13. package/dist/cli/commands/run/headless-error-writer.js +8 -0
  14. package/dist/cli/commands/run/loop-params.js +3 -0
  15. package/dist/cli/commands/run/mode.js +2 -5
  16. package/dist/cli/commands/run/parse-options.js +18 -2
  17. package/dist/cli/commands/run/persist-session.js +10 -1
  18. package/dist/cli/commands/run/preflight.js +10 -0
  19. package/dist/cli/commands/run/reporter-factory.js +4 -0
  20. package/dist/cli/commands/run/runtime-llm.js +38 -11
  21. package/dist/cli/commands/run/runtime-options.js +2 -2
  22. package/dist/cli/commands/run/validate-options.js +0 -5
  23. package/dist/cli/commands/run/verbose.js +2 -7
  24. package/dist/cli/commands/serve.js +117 -90
  25. package/dist/cli/commands/tool-names.js +78 -78
  26. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
  27. package/dist/cli/headless/json-protocol.js +37 -0
  28. package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
  29. package/dist/cli/headless/protocol-metadata.js +22 -0
  30. package/dist/cli/headless/stream-json-protocol.js +34 -1
  31. package/dist/cli/index.js +6 -4
  32. package/dist/cli/locales/en.js +32 -6
  33. package/dist/cli/program-bootstrap.js +14 -4
  34. package/dist/cli/program-commands.js +9 -1
  35. package/dist/cli/program-options.js +1 -0
  36. package/dist/cli/reporters/anthropic-stream.js +7 -1
  37. package/dist/cli/reporters/json.js +4 -0
  38. package/dist/cli/reporters/stream-json.js +17 -2
  39. package/dist/cli/run-cli.js +5 -3
  40. package/dist/cli/slash/runtime.js +30 -15
  41. package/dist/cli/ui/components/CommandInput.js +7 -3
  42. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  43. package/dist/cli/utils/command-option-source.js +13 -0
  44. package/dist/cli/utils/output-format.js +6 -0
  45. package/dist/cli/utils/resolve-cli-config.js +98 -0
  46. package/dist/cli/utils/verbose-level.js +8 -0
  47. package/dist/cli/utils/verify-resolver.js +8 -4
  48. package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
  49. package/dist/core/adapters/fs/file-adapter.js +6 -0
  50. package/dist/core/adapters/fs/filesystem.js +2 -1
  51. package/dist/core/adapters/git/git-adapter.js +78 -1
  52. package/dist/core/benchmark/patch-artifact.js +124 -0
  53. package/dist/core/benchmark/swe-bench.js +25 -0
  54. package/dist/core/config/load.js +39 -18
  55. package/dist/core/config/merge.js +27 -0
  56. package/dist/core/config/paths.js +24 -5
  57. package/dist/core/config/resolve-llm.js +12 -0
  58. package/dist/core/config/resolve.js +7 -5
  59. package/dist/core/config/resolvers/server.js +0 -6
  60. package/dist/core/config/validate.js +94 -21
  61. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  62. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  63. package/dist/core/context/keywords.js +18 -4
  64. package/dist/core/context/service-deps.js +2 -2
  65. package/dist/core/context/service.js +8 -0
  66. package/dist/core/context/steps/context-gather.js +38 -0
  67. package/dist/core/context/summarization/summarizer.js +55 -12
  68. package/dist/core/context/targeting/target-resolver.js +4 -4
  69. package/dist/core/extensions/index.js +23 -5
  70. package/dist/core/extensions/paths.js +31 -0
  71. package/dist/core/extensions/schemas.js +8 -5
  72. package/dist/core/facades/cli-chat.js +6 -2
  73. package/dist/core/facades/cli-command-chat.js +2 -1
  74. package/dist/core/facades/cli-command-tool-names.js +2 -0
  75. package/dist/core/facades/cli-context.js +1 -0
  76. package/dist/core/facades/cli-observability.js +1 -1
  77. package/dist/core/facades/cli-run-handler.js +4 -2
  78. package/dist/core/facades/cli-run-persist-session.js +1 -0
  79. package/dist/core/facades/cli-serve.js +2 -4
  80. package/dist/core/facades/cli-utils-worktree.js +1 -1
  81. package/dist/core/failure/diagnostics.js +53 -1
  82. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  83. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  84. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  85. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  86. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  87. package/dist/core/grizzco/engine/transaction/transaction-runner.js +173 -7
  88. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  89. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  90. package/dist/core/grizzco/steps/answer.js +13 -14
  91. package/dist/core/grizzco/steps/autopilot.js +396 -0
  92. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  93. package/dist/core/grizzco/steps/explore.js +37 -21
  94. package/dist/core/grizzco/steps/generateReview.js +2 -5
  95. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  96. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  97. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  98. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  99. package/dist/core/grizzco/steps/patch.js +105 -146
  100. package/dist/core/grizzco/steps/plan.js +101 -25
  101. package/dist/core/grizzco/steps/preflight.js +5 -3
  102. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  103. package/dist/core/grizzco/steps/research.js +39 -36
  104. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  105. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  106. package/dist/core/grizzco/steps/verify.js +13 -21
  107. package/dist/core/intent/chat-intent.js +0 -4
  108. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  109. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  110. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  111. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  112. package/dist/core/llm/ai-sdk/request-params.js +74 -1
  113. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  114. package/dist/core/llm/ai-sdk.js +112 -27
  115. package/dist/core/llm/capabilities.js +12 -0
  116. package/dist/core/llm/contracts/repair.js +36 -30
  117. package/dist/core/llm/errors.js +83 -2
  118. package/dist/core/llm/message-composition.js +7 -22
  119. package/dist/core/llm/phase-router.js +29 -10
  120. package/dist/core/llm/redact.js +28 -3
  121. package/dist/core/llm/registry.js +2 -0
  122. package/dist/core/llm/request-augmentation.js +55 -0
  123. package/dist/core/llm/request-envelope.js +334 -0
  124. package/dist/core/llm/shared-request-assembly.js +35 -0
  125. package/dist/core/llm/stream-utils.js +13 -4
  126. package/dist/core/llm/utils.js +18 -29
  127. package/dist/core/memory/relevant-retrieval.js +144 -0
  128. package/dist/core/observability/logger.js +11 -2
  129. package/dist/core/patch/diff.js +1 -0
  130. package/dist/core/prompts/registry.js +39 -2
  131. package/dist/core/prompts/runtime.js +50 -12
  132. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  133. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  134. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  135. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  136. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  137. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  138. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  139. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  140. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  141. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  142. package/dist/core/protocols/a2a/agent-card.js +3 -2
  143. package/dist/core/protocols/a2a/sdk/executor.js +8 -6
  144. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  145. package/dist/core/protocols/acp/formal-agent.js +221 -55
  146. package/dist/core/protocols/acp/handlers.js +5 -1
  147. package/dist/core/protocols/acp/permission-provider.js +21 -1
  148. package/dist/core/protocols/shared/execution-request.js +24 -0
  149. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  150. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  151. package/dist/core/public-capabilities/projections.js +29 -0
  152. package/dist/core/public-capabilities/registry.js +26 -0
  153. package/dist/core/public-capabilities/types.js +2 -0
  154. package/dist/core/runtime/agent-server-runtime.js +47 -43
  155. package/dist/core/runtime/execution-profile.js +67 -0
  156. package/dist/core/session/artifact-state.js +160 -0
  157. package/dist/core/session/compaction/index.js +183 -0
  158. package/dist/core/session/compaction/microcompact.js +78 -0
  159. package/dist/core/session/compaction/tracking.js +48 -0
  160. package/dist/core/session/compaction/types.js +11 -0
  161. package/dist/core/session/compression.js +12 -4
  162. package/dist/core/session/manager.js +247 -10
  163. package/dist/core/session/pruning-strategy.js +55 -9
  164. package/dist/core/session/replacement-preview-provider.js +24 -0
  165. package/dist/core/session/replacement-state.js +131 -0
  166. package/dist/core/session/resume-repair/pipeline.js +79 -0
  167. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  168. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  169. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  170. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  171. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  172. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  173. package/dist/core/session/resume-repair/types.js +2 -0
  174. package/dist/core/session/summary-sync.js +164 -13
  175. package/dist/core/session/token-tracker.js +6 -0
  176. package/dist/core/skills/audit.js +34 -0
  177. package/dist/core/skills/bridge.js +84 -7
  178. package/dist/core/skills/discovery.js +94 -0
  179. package/dist/core/skills/feature-flags.js +52 -0
  180. package/dist/core/skills/index.js +1 -1
  181. package/dist/core/skills/loader.js +195 -20
  182. package/dist/core/skills/parser.js +296 -24
  183. package/dist/core/skills/permissions.js +117 -0
  184. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  185. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  186. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  187. package/dist/core/strata/layers/worktree.js +70 -13
  188. package/dist/core/strata/runtime/synchronizer.js +29 -2
  189. package/dist/core/streaming/stream-assembler.js +75 -31
  190. package/dist/core/sub-agent/context-snapshot.js +156 -0
  191. package/dist/core/sub-agent/core/loop.js +1 -1
  192. package/dist/core/sub-agent/core/manager.js +119 -20
  193. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  194. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  195. package/dist/core/sub-agent/registry-defaults.js +4 -0
  196. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  197. package/dist/core/sub-agent/types.js +134 -5
  198. package/dist/core/tools/audit.js +13 -4
  199. package/dist/core/tools/builtin/ast-grep.js +1 -1
  200. package/dist/core/tools/builtin/ast.js +1 -1
  201. package/dist/core/tools/builtin/benchmark.js +360 -0
  202. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  203. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  204. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  205. package/dist/core/tools/builtin/fs.js +256 -23
  206. package/dist/core/tools/builtin/git.js +2 -2
  207. package/dist/core/tools/builtin/index.js +51 -2
  208. package/dist/core/tools/builtin/interaction.js +8 -1
  209. package/dist/core/tools/builtin/plan.js +37 -15
  210. package/dist/core/tools/builtin/shell.js +1 -1
  211. package/dist/core/tools/loader.js +39 -16
  212. package/dist/core/tools/mapper.js +17 -3
  213. package/dist/core/tools/parallel/scheduler.js +35 -4
  214. package/dist/core/tools/permissions/permission-rules.js +5 -10
  215. package/dist/core/tools/policy.js +6 -1
  216. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  217. package/dist/core/tools/router.js +24 -6
  218. package/dist/core/tools/session.js +458 -48
  219. package/dist/core/tools/tool-visibility.js +62 -0
  220. package/dist/core/tools/types.js +9 -1
  221. package/dist/core/types/execution.js +4 -0
  222. package/dist/core/types/flow-mode.js +8 -0
  223. package/dist/core/utils/path.js +52 -0
  224. package/dist/core/verification/runner.js +4 -1
  225. package/dist/interfaces/cli/task-runner.js +4 -3
  226. package/dist/languages/typescript/index.js +4 -1
  227. package/dist/locales/en.js +87 -2
  228. package/dist/utils/eol.js +1 -1
  229. package/package.json +15 -8
  230. package/scripts/fix-es-abstract-compat.js +77 -0
  231. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  232. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  233. package/dist/core/runtime/sidecar-paths.js +0 -47
  234. 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
- const runner = new MicroTaskRunner({
47
- debugLabel: `SkillRunner:${skill.id}`,
48
- maxRounds: 10,
49
- strategy: (engine) => {
50
- // Computation phase: assemble a prompt without any "!..." lines.
51
- const promptLines = (skill.instructions || '')
52
- .split('\n')
53
- .filter((line) => !line.trim().startsWith('!'));
54
- const basePrompt = SkillParser.substituteVariables(promptLines.join('\n').trim(), inputs);
55
- const transcript = formatShellTranscript(data.shell_outputs);
56
- data.prompt = `${basePrompt}${transcript}`.trim();
57
- SkillStrategyDSL(engine);
58
- return engine;
59
- },
60
- resolveData: async (_ctx, key) => {
61
- if (!key.startsWith('sh:')) {
62
- return undefined;
63
- }
64
- const command = key.slice(3);
65
- const callId = `slash-sh-${buildStableId([skill.id, command])}`;
66
- const envelope = {
67
- id: callId,
68
- phase: Phase.SLASH,
69
- toolName: 'shell.exec',
70
- args: { command },
71
- ctx: {
72
- ...toolCtx,
73
- // Ensure ToolPolicy sees worktree isolation for process execution.
74
- worktreeRoot: toolCtx.worktreeRoot ?? toolCtx.repoRoot,
75
- },
76
- };
77
- let result = await toolRouter.call(envelope);
78
- if (result.status === 'denied' && result.error?.code === 'AUTH_REQUIRED') {
79
- await toolRouter.waitForAuthorization(callId, signal);
80
- result = await toolRouter.call(envelope);
81
- }
82
- if (result.status !== 'ok') {
83
- const msg = result.error?.message || 'shell.exec failed';
84
- throw new Error(msg);
85
- }
86
- const output = result.output;
87
- const combined = [output.stdout, output.stderr].filter(Boolean).join('\n').trim();
88
- data.shell_outputs[command] = combined;
89
- data[key] = combined;
90
- return combined;
91
- },
92
- });
93
- const decided = await runner.decide(ctx);
94
- const plan = decided.plan;
95
- const inject = plan.actions.find((a) => a.type === 'INJECT_PROMPT');
96
- return {
97
- traceId: `skill-${skill.id}-${Date.now()}`,
98
- skillId: skill.id,
99
- inputs,
100
- dynamicCommands: Object.entries(data.shell_outputs).map(([cmd, output]) => ({
101
- cmd,
102
- output: String(output),
103
- })),
104
- injectedPrompt: String(inject?.params?.prompt ?? ''),
105
- status: plan.shouldAbort ? 'FAILURE' : 'SUCCESS',
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 { rm, mkdir, symlink } from '../../../adapters/fs/node-fs.js';
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
- if ((err && typeof err === 'object' && 'code' in err
53
- ? err.code
54
- : undefined) !== 'EEXIST') {
55
- getLogger().warn(`Failed to link ${depPath}: ${err instanceof Error ? err.message : String(err)}`);
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().debug(`Dependency link already exists: ${depPath}`);
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
- if (isPathWithinDirectory(tmpdir(), workPath, { allowEqual: false }))
32
- return true;
33
- const parityRoot = resolveParityWorktreeRoot(baseRepoPath);
34
- if (isPathWithinDirectory(parityRoot, workPath, { allowEqual: false }))
35
- return true;
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
  *
@@ -101,17 +154,17 @@ export class WorkspaceManager {
101
154
  if (environmentMode === 'parity') {
102
155
  const parityRoot = worktreeRootImpl.normalize(resolveParityWorktreeRoot(options.repoPath));
103
156
  if (!isPathWithinDirectory(parityRoot, normalizedWorktreePath, { allowEqual: false })) {
104
- throw new Error('Worktree path must be under parity worktree root');
157
+ throw new Error(text.errors.worktreePathMustBeUnderParityRoot);
105
158
  }
106
159
  }
107
160
  else {
108
161
  const tmpDir = worktreeRootImpl.normalize(tmpdir());
109
162
  if (!isPathWithinDirectory(tmpDir, normalizedWorktreePath, { allowEqual: false })) {
110
- throw new Error('Worktree path must be in system temp directory');
163
+ throw new Error(text.errors.worktreePathMustBeInTempDir);
111
164
  }
112
165
  }
113
166
  if (isPathWithinDirectory(options.repoPath, worktreePath, { allowEqual: true })) {
114
- throw new Error('Worktree path must not be inside repo path');
167
+ throw new Error(text.errors.worktreePathMustNotBeInsideRepo);
115
168
  }
116
169
  // Use GitAdapter for worktree creation
117
170
  await git.query(['worktree', 'add', '--quiet', '--detach', worktreePath, baseRef.trim()]);
@@ -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('Worktree path not in managed roots, refusing to delete');
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
- if (entryStat.isSymbolicLink()) {
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
  }