salmon-loop 0.2.13 → 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 (218) 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 +160 -24
  4. package/dist/cli/commands/chat.js +14 -7
  5. package/dist/cli/commands/flow-mode.js +63 -0
  6. package/dist/cli/commands/registry.js +2 -0
  7. package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
  8. package/dist/cli/commands/run/early-errors.js +23 -0
  9. package/dist/cli/commands/run/handler.js +115 -27
  10. package/dist/cli/commands/run/headless-error-writer.js +8 -0
  11. package/dist/cli/commands/run/loop-params.js +2 -0
  12. package/dist/cli/commands/run/mode.js +2 -5
  13. package/dist/cli/commands/run/parse-options.js +16 -0
  14. package/dist/cli/commands/run/persist-session.js +10 -1
  15. package/dist/cli/commands/run/preflight.js +10 -0
  16. package/dist/cli/commands/run/reporter-factory.js +4 -0
  17. package/dist/cli/commands/run/runtime-llm.js +38 -11
  18. package/dist/cli/commands/run/runtime-options.js +2 -2
  19. package/dist/cli/commands/serve.js +91 -71
  20. package/dist/cli/commands/tool-names.js +78 -78
  21. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
  22. package/dist/cli/headless/json-protocol.js +37 -0
  23. package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
  24. package/dist/cli/headless/protocol-metadata.js +22 -0
  25. package/dist/cli/headless/stream-json-protocol.js +34 -1
  26. package/dist/cli/index.js +6 -4
  27. package/dist/cli/locales/en.js +30 -6
  28. package/dist/cli/program-bootstrap.js +8 -3
  29. package/dist/cli/program-commands.js +5 -1
  30. package/dist/cli/reporters/anthropic-stream.js +7 -1
  31. package/dist/cli/reporters/json.js +4 -0
  32. package/dist/cli/reporters/stream-json.js +17 -2
  33. package/dist/cli/run-cli.js +5 -3
  34. package/dist/cli/slash/runtime.js +27 -12
  35. package/dist/cli/ui/components/CommandInput.js +7 -3
  36. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  37. package/dist/cli/utils/command-option-source.js +13 -0
  38. package/dist/cli/utils/verify-resolver.js +8 -4
  39. package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
  40. package/dist/core/adapters/fs/file-adapter.js +6 -0
  41. package/dist/core/adapters/fs/filesystem.js +2 -1
  42. package/dist/core/adapters/git/git-adapter.js +78 -1
  43. package/dist/core/benchmark/patch-artifact.js +124 -0
  44. package/dist/core/benchmark/swe-bench.js +25 -0
  45. package/dist/core/config/load.js +18 -11
  46. package/dist/core/config/resolve-llm.js +12 -0
  47. package/dist/core/config/resolvers/server.js +0 -6
  48. package/dist/core/config/validate.js +73 -21
  49. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  50. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  51. package/dist/core/context/keywords.js +18 -4
  52. package/dist/core/context/service-deps.js +2 -2
  53. package/dist/core/context/service.js +8 -0
  54. package/dist/core/context/steps/context-gather.js +38 -0
  55. package/dist/core/context/summarization/summarizer.js +55 -12
  56. package/dist/core/context/targeting/target-resolver.js +4 -4
  57. package/dist/core/extensions/index.js +23 -5
  58. package/dist/core/extensions/paths.js +31 -0
  59. package/dist/core/extensions/schemas.js +8 -5
  60. package/dist/core/facades/cli-chat.js +6 -2
  61. package/dist/core/facades/cli-command-chat.js +1 -0
  62. package/dist/core/facades/cli-command-tool-names.js +2 -0
  63. package/dist/core/facades/cli-observability.js +1 -1
  64. package/dist/core/facades/cli-run-handler.js +4 -2
  65. package/dist/core/facades/cli-run-persist-session.js +1 -0
  66. package/dist/core/facades/cli-serve.js +2 -4
  67. package/dist/core/facades/cli-utils-worktree.js +1 -1
  68. package/dist/core/failure/diagnostics.js +53 -1
  69. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  70. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  71. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  72. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  73. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  74. package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
  75. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  76. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  77. package/dist/core/grizzco/steps/answer.js +13 -14
  78. package/dist/core/grizzco/steps/autopilot.js +396 -0
  79. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  80. package/dist/core/grizzco/steps/explore.js +37 -21
  81. package/dist/core/grizzco/steps/generateReview.js +2 -5
  82. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  83. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  84. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  85. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  86. package/dist/core/grizzco/steps/patch.js +105 -146
  87. package/dist/core/grizzco/steps/plan.js +101 -25
  88. package/dist/core/grizzco/steps/preflight.js +5 -6
  89. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  90. package/dist/core/grizzco/steps/research.js +39 -36
  91. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  92. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  93. package/dist/core/grizzco/steps/verify.js +13 -21
  94. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  95. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  96. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  97. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  98. package/dist/core/llm/ai-sdk/request-params.js +73 -0
  99. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  100. package/dist/core/llm/ai-sdk.js +112 -27
  101. package/dist/core/llm/capabilities.js +12 -0
  102. package/dist/core/llm/contracts/repair.js +36 -30
  103. package/dist/core/llm/errors.js +83 -2
  104. package/dist/core/llm/message-composition.js +7 -22
  105. package/dist/core/llm/phase-router.js +29 -10
  106. package/dist/core/llm/redact.js +28 -3
  107. package/dist/core/llm/registry.js +2 -0
  108. package/dist/core/llm/request-augmentation.js +55 -0
  109. package/dist/core/llm/request-envelope.js +334 -0
  110. package/dist/core/llm/shared-request-assembly.js +35 -0
  111. package/dist/core/llm/stream-utils.js +13 -4
  112. package/dist/core/llm/utils.js +18 -29
  113. package/dist/core/memory/relevant-retrieval.js +144 -0
  114. package/dist/core/observability/logger.js +11 -2
  115. package/dist/core/patch/diff.js +1 -0
  116. package/dist/core/prompts/registry.js +39 -2
  117. package/dist/core/prompts/runtime.js +50 -12
  118. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  119. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  120. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  121. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  122. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  123. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  124. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  125. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  126. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  127. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  128. package/dist/core/protocols/a2a/agent-card.js +3 -2
  129. package/dist/core/protocols/a2a/sdk/executor.js +2 -1
  130. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  131. package/dist/core/protocols/acp/formal-agent.js +74 -51
  132. package/dist/core/protocols/acp/handlers.js +5 -1
  133. package/dist/core/protocols/acp/permission-provider.js +1 -1
  134. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  135. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  136. package/dist/core/public-capabilities/projections.js +29 -0
  137. package/dist/core/public-capabilities/registry.js +26 -0
  138. package/dist/core/public-capabilities/types.js +2 -0
  139. package/dist/core/runtime/agent-server-runtime.js +47 -43
  140. package/dist/core/runtime/execution-profile.js +67 -0
  141. package/dist/core/session/artifact-state.js +160 -0
  142. package/dist/core/session/compaction/index.js +183 -0
  143. package/dist/core/session/compaction/microcompact.js +78 -0
  144. package/dist/core/session/compaction/tracking.js +48 -0
  145. package/dist/core/session/compaction/types.js +11 -0
  146. package/dist/core/session/compression.js +8 -0
  147. package/dist/core/session/manager.js +244 -8
  148. package/dist/core/session/pruning-strategy.js +55 -9
  149. package/dist/core/session/replacement-preview-provider.js +24 -0
  150. package/dist/core/session/replacement-state.js +131 -0
  151. package/dist/core/session/resume-repair/pipeline.js +79 -0
  152. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  153. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  154. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  155. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  156. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  157. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  158. package/dist/core/session/resume-repair/types.js +2 -0
  159. package/dist/core/session/summary-sync.js +164 -13
  160. package/dist/core/session/token-tracker.js +6 -0
  161. package/dist/core/skills/audit.js +34 -0
  162. package/dist/core/skills/bridge.js +84 -7
  163. package/dist/core/skills/discovery.js +94 -0
  164. package/dist/core/skills/feature-flags.js +52 -0
  165. package/dist/core/skills/index.js +1 -1
  166. package/dist/core/skills/loader.js +195 -20
  167. package/dist/core/skills/parser.js +296 -24
  168. package/dist/core/skills/permissions.js +117 -0
  169. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  170. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  171. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  172. package/dist/core/strata/layers/worktree.js +67 -10
  173. package/dist/core/strata/runtime/synchronizer.js +29 -2
  174. package/dist/core/streaming/stream-assembler.js +75 -31
  175. package/dist/core/sub-agent/context-snapshot.js +156 -0
  176. package/dist/core/sub-agent/core/loop.js +1 -1
  177. package/dist/core/sub-agent/core/manager.js +119 -20
  178. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  179. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  180. package/dist/core/sub-agent/registry-defaults.js +4 -0
  181. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  182. package/dist/core/sub-agent/types.js +134 -5
  183. package/dist/core/tools/audit.js +13 -4
  184. package/dist/core/tools/builtin/ast-grep.js +1 -1
  185. package/dist/core/tools/builtin/ast.js +1 -1
  186. package/dist/core/tools/builtin/benchmark.js +360 -0
  187. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  188. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  189. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  190. package/dist/core/tools/builtin/fs.js +256 -23
  191. package/dist/core/tools/builtin/git.js +2 -2
  192. package/dist/core/tools/builtin/index.js +51 -2
  193. package/dist/core/tools/builtin/interaction.js +8 -1
  194. package/dist/core/tools/builtin/plan.js +37 -15
  195. package/dist/core/tools/builtin/shell.js +1 -1
  196. package/dist/core/tools/loader.js +39 -16
  197. package/dist/core/tools/mapper.js +17 -3
  198. package/dist/core/tools/parallel/scheduler.js +35 -4
  199. package/dist/core/tools/permissions/permission-rules.js +5 -10
  200. package/dist/core/tools/policy.js +6 -1
  201. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  202. package/dist/core/tools/router.js +24 -6
  203. package/dist/core/tools/session.js +458 -48
  204. package/dist/core/tools/tool-visibility.js +62 -0
  205. package/dist/core/tools/types.js +9 -1
  206. package/dist/core/types/execution.js +4 -0
  207. package/dist/core/types/flow-mode.js +8 -0
  208. package/dist/core/utils/path.js +52 -0
  209. package/dist/core/verification/runner.js +4 -1
  210. package/dist/languages/typescript/index.js +4 -1
  211. package/dist/locales/en.js +35 -2
  212. package/dist/utils/eol.js +1 -1
  213. package/package.json +13 -6
  214. package/scripts/fix-es-abstract-compat.js +77 -0
  215. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  216. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  217. package/dist/core/runtime/sidecar-paths.js +0 -47
  218. package/dist/core/runtime/sidecar-route-catalog.js +0 -103
@@ -0,0 +1,117 @@
1
+ import path from 'node:path';
2
+ import { text } from '../../locales/index.js';
3
+ import { syncFs as fs } from '../adapters/fs/node-fs.js';
4
+ import { tryGetLogger } from '../observability/logger.js';
5
+ /**
6
+ * Manages skill-level permission policies with exact and prefix matching.
7
+ *
8
+ * Persists policies to a JSON file for auditable provenance tracking.
9
+ * Aligns with the existing permission-rules pattern in the tools layer.
10
+ */
11
+ export class SkillPermissionManager {
12
+ policies = [];
13
+ filePath;
14
+ constructor(filePath) {
15
+ this.filePath = filePath;
16
+ this.load();
17
+ }
18
+ /**
19
+ * Check whether a skill id is allowed by any active policy.
20
+ *
21
+ * Evaluates all policies in order: exact matches are checked first,
22
+ * then prefix matches. Returns true if any policy matches.
23
+ */
24
+ isAllowed(skillId) {
25
+ for (const policy of this.policies) {
26
+ if (policy.kind === 'exact' && policy.pattern === skillId) {
27
+ return true;
28
+ }
29
+ if (policy.kind === 'prefix' && skillId.startsWith(policy.pattern)) {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ /**
36
+ * Grant a new permission policy. Deduplicates by pattern+kind.
37
+ * Persists the updated allowlist to disk.
38
+ */
39
+ grant(policy) {
40
+ const existing = this.policies.findIndex((p) => p.pattern === policy.pattern && p.kind === policy.kind);
41
+ if (existing >= 0) {
42
+ // Update provenance on re-grant
43
+ this.policies[existing] = policy;
44
+ }
45
+ else {
46
+ this.policies.push(policy);
47
+ }
48
+ const logger = tryGetLogger();
49
+ logger?.audit('SKILL_PERMISSION_GRANTED', {
50
+ pattern: policy.pattern,
51
+ kind: policy.kind,
52
+ grantedBy: policy.grantedBy,
53
+ grantedAt: policy.grantedAt,
54
+ }, { source: 'skill-permissions', severity: 'low', scope: 'session' });
55
+ this.save();
56
+ }
57
+ /**
58
+ * Revoke all policies matching a given skill id (exact match on pattern).
59
+ * Persists the updated allowlist to disk.
60
+ */
61
+ revoke(skillId) {
62
+ const before = this.policies.length;
63
+ this.policies = this.policies.filter((p) => p.pattern !== skillId);
64
+ if (this.policies.length < before) {
65
+ const logger = tryGetLogger();
66
+ logger?.audit('SKILL_PERMISSION_REVOKED', { skillId, removedCount: before - this.policies.length }, { source: 'skill-permissions', severity: 'low', scope: 'session' });
67
+ this.save();
68
+ }
69
+ }
70
+ /** Return a readonly snapshot of current policies. */
71
+ getPolicies() {
72
+ return [...this.policies];
73
+ }
74
+ /** Load policies from the persisted JSON file. */
75
+ load() {
76
+ try {
77
+ if (!fs.existsSync(this.filePath)) {
78
+ this.policies = [];
79
+ return;
80
+ }
81
+ const raw = fs.readFileSync(this.filePath, 'utf-8');
82
+ const data = JSON.parse(raw);
83
+ if (data.version === 1 && Array.isArray(data.policies)) {
84
+ this.policies = data.policies;
85
+ }
86
+ else {
87
+ const logger = tryGetLogger();
88
+ logger?.warn(text.skills.permissionFileInvalidFormat(this.filePath));
89
+ this.policies = [];
90
+ }
91
+ }
92
+ catch {
93
+ const logger = tryGetLogger();
94
+ logger?.warn(text.skills.permissionFileLoadError(this.filePath));
95
+ this.policies = [];
96
+ }
97
+ }
98
+ /** Persist current policies to the JSON file. */
99
+ save() {
100
+ try {
101
+ const dir = path.dirname(this.filePath);
102
+ if (!fs.existsSync(dir)) {
103
+ fs.mkdirSync(dir, { recursive: true });
104
+ }
105
+ const data = {
106
+ version: 1,
107
+ policies: this.policies,
108
+ };
109
+ fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
110
+ }
111
+ catch (err) {
112
+ const logger = tryGetLogger();
113
+ logger?.error(text.skills.permissionFileSaveError(this.filePath, err instanceof Error ? err.message : String(err)));
114
+ }
115
+ }
116
+ }
117
+ //# sourceMappingURL=permissions.js.map
@@ -5,15 +5,21 @@ import { getPlatformShellInvocation } from '../../utils/platform-shell.js';
5
5
  import { SkillParser } from '../parser.js';
6
6
  import { SkillStrategyDSL } from '../strategy.js';
7
7
  /**
8
- * MicroTaskRunner manages the execution loop of a single skill.
9
- * COMPLIANCE: DSL-Spec-V3
10
- * - Computation Layer: Handles variable substitution, command extraction, and prompt assembly.
11
- * - Action Layer: Executes Shell commands and final Tool injection.
8
+ * @deprecated Legacy MicroTaskRunner restricted to test context only.
9
+ *
10
+ * This runner calls execa directly, bypassing ToolRouter governance.
11
+ * Production code MUST use executeSkill() from SkillRunner.ts instead.
12
+ *
13
+ * A runtime guard throws if instantiated outside a test environment
14
+ * (process.env.NODE_ENV !== 'test').
12
15
  */
13
16
  export class MicroTaskRunner {
14
17
  skill;
15
18
  constructor(skill) {
16
19
  this.skill = skill;
20
+ if (process.env.NODE_ENV !== 'test') {
21
+ throw new Error(text.skills.legacyRunnerForbidden);
22
+ }
17
23
  }
18
24
  async execute(inputs, ctx) {
19
25
  const skill = this.skill;
@@ -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
  *
@@ -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,