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,37 +1,225 @@
1
- import { getLogger } from '../observability/logger.js';
1
+ import path from 'node:path';
2
+ import { parse as parseYaml } from 'yaml';
3
+ import { z } from 'zod';
4
+ import { text } from '../../locales/index.js';
5
+ import { tryGetLogger } from '../observability/logger.js';
6
+ /**
7
+ * Safe logger accessor that never throws when the logger is not yet initialized.
8
+ *
9
+ * Falls back to a no-op stub so that parser code can run in test environments
10
+ * or early startup paths where the global logger has not been set.
11
+ */
12
+ function safeLogger() {
13
+ return (tryGetLogger() ?? {
14
+ error: (..._args) => { },
15
+ warn: (..._args) => { },
16
+ info: (..._args) => { },
17
+ debug: (..._args) => { },
18
+ audit: (..._args) => { },
19
+ });
20
+ }
21
+ /**
22
+ * Naming convention regex for AgentSkills spec compliance.
23
+ *
24
+ * AgentSkills spec: "unicode lowercase alphanumeric characters (a-z) and
25
+ * hyphens (-)". We accept Unicode lowercase letters (\p{Ll}) and Unicode
26
+ * digits (\p{N}) in addition to ASCII, using the `u` flag for Unicode
27
+ * property escapes.
28
+ */
29
+ const SKILL_NAME_REGEX = /^[\p{Ll}\p{N}](?:[\p{Ll}\p{N}-]*[\p{Ll}\p{N}])?$/u;
30
+ /** Shared field definitions defined by the Agent Skills specification. */
31
+ const sharedFields = {
32
+ license: z.string().optional(),
33
+ compatibility: z.string().max(500).optional(),
34
+ metadata: z.record(z.string(), z.string()).optional(),
35
+ // AgentSkills spec: "Space-delimited list of pre-approved tools" (Experimental).
36
+ // YAML bare key (`allowed-tools:`) parses as null — normalize to undefined so
37
+ // that Zod's `.optional()` accepts it as "not declared".
38
+ 'allowed-tools': z.preprocess((val) => {
39
+ if (val === null || val === undefined)
40
+ return undefined;
41
+ return val;
42
+ }, z.string().optional()),
43
+ };
44
+ /**
45
+ * Strict Zod schema for SKILL.md frontmatter validation.
46
+ *
47
+ * Enforces AgentSkills naming conventions and type correctness:
48
+ * - `name`: 1-64 chars, lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens
49
+ * - `description`: 1-1024 chars, non-empty
50
+ * @see Requirements 5.1, 5.2, 5.4, 5.5
51
+ */
52
+ export const SkillFrontmatterSchema = z
53
+ .object({
54
+ name: z
55
+ .string()
56
+ .min(1)
57
+ .max(64)
58
+ .regex(SKILL_NAME_REGEX, 'name must be unicode lowercase alphanumeric + hyphens per AgentSkills spec')
59
+ .refine((s) => !s.includes('--'), 'name must not contain consecutive hyphens'),
60
+ description: z.string().min(1).max(1024),
61
+ ...sharedFields,
62
+ })
63
+ .strict();
64
+ /**
65
+ * Maximum allowed length for a single extracted command (in characters).
66
+ *
67
+ * This limit mitigates denial-of-service via extremely long command strings and
68
+ * reduces the blast radius of any injection that bypasses pattern checks.
69
+ * The value (4096) aligns with common OS `ARG_MAX` per-argument limits.
70
+ *
71
+ * @security Guard — enforced inside {@link SkillParser.extractCommands}.
72
+ */
73
+ export const COMMAND_MAX_LENGTH = 4096;
74
+ /** Control character range that must not appear in commands (C0 subset excluding tab, LF, CR). */
75
+ // eslint-disable-next-line no-control-regex
76
+ const CONTROL_CHAR_PATTERN = /[\x00-\x08\x0e-\x1f]/;
77
+ /**
78
+ * Default deny-list of dangerous shell patterns.
79
+ *
80
+ * Each regex targets a well-known destructive or injection-prone idiom:
81
+ *
82
+ * | Pattern | Threat |
83
+ * |-----------------------|-------------------------------------------------|
84
+ * | `rm -rf /` | Recursive root deletion |
85
+ * | `curl … \| sh` | Remote code execution via piped download |
86
+ * | `\beval\b` | Arbitrary code evaluation in shell |
87
+ * | `\bexec\b.*<` | Process replacement with redirected input |
88
+ *
89
+ * The list is intentionally conservative — it catches blatant patterns but does
90
+ * NOT attempt full shell-syntax analysis. Callers may supply their own list via
91
+ * the `dangerousPatterns` parameter of {@link SkillParser.extractCommands}.
92
+ *
93
+ * @security Guard — applied after length and control-character checks.
94
+ */
95
+ export const DEFAULT_DANGEROUS_PATTERNS = [
96
+ /rm\s+-rf\s+\//,
97
+ /curl\s.*\|\s*sh/,
98
+ /\beval\b/,
99
+ /\bexec\b.*</,
100
+ ];
2
101
  export class SkillParser {
102
+ /**
103
+ * Parse a SKILL.md file with YAML frontmatter validation.
104
+ *
105
+ * Uses the `yaml` library for robust YAML parsing and Zod schema for
106
+ * field validation and type coercion. Throws on missing or invalid
107
+ * frontmatter instead of silently falling back.
108
+ *
109
+ * @param content - Raw SKILL.md file content
110
+ * @param filePath - Absolute or relative path to the SKILL.md file
111
+ * @throws Error if frontmatter is missing, YAML is malformed, or schema validation fails
112
+ * @see Requirements 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 1.9, 1.10
113
+ */
3
114
  static parse(content, filePath) {
4
- // COMPLIANCE: Lightweight parsing instead of heavy gray-matter
5
- const yamlRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
115
+ // Accept frontmatter with or without a body after the closing ---.
116
+ // The body capture is optional to avoid rejecting minimal SKILL.md files
117
+ // that contain only frontmatter (no trailing newline or instruction text).
118
+ const yamlRegex = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/;
6
119
  const match = content.match(yamlRegex);
7
120
  if (!match) {
8
- getLogger().error(`Failed to parse skill at ${filePath}: Missing frontmatter`);
9
- return {
10
- id: filePath,
11
- path: filePath,
12
- metadata: {},
13
- rawContent: content,
14
- instructions: content.trim(),
15
- };
121
+ const msg = text.skills.missingFrontmatter(filePath);
122
+ safeLogger().error(msg);
123
+ throw new Error(msg);
16
124
  }
17
125
  const yamlRaw = match[1];
18
- const instructions = match[2];
19
- const data = {};
20
- // Simple key-value parser for basic frontmatter
21
- yamlRaw.split('\n').forEach((line) => {
22
- const [key, ...value] = line.split(':');
23
- if (key && value) {
24
- data[key.trim()] = value.join(':').trim();
25
- }
26
- });
126
+ const instructions = match[2] ?? '';
127
+ let parsed;
128
+ try {
129
+ parsed = parseYaml(yamlRaw);
130
+ }
131
+ catch (error) {
132
+ const reason = error instanceof Error ? error.message : String(error);
133
+ const msg = text.skills.yamlParseError(filePath, reason);
134
+ safeLogger().error(msg);
135
+ throw new Error(msg);
136
+ }
137
+ if (parsed == null || typeof parsed !== 'object') {
138
+ const msg = text.skills.missingFrontmatter(filePath);
139
+ safeLogger().error(msg);
140
+ throw new Error(msg);
141
+ }
142
+ const result = SkillFrontmatterSchema.safeParse(parsed);
143
+ if (!result.success) {
144
+ const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
145
+ const msg = text.skills.invalidFrontmatter(filePath, issues);
146
+ safeLogger().error(msg);
147
+ throw new Error(msg);
148
+ }
149
+ const data = result.data;
150
+ const parentDir = path.basename(path.dirname(filePath));
151
+ if (parentDir && parentDir !== '.' && parentDir !== data.name) {
152
+ const msg = text.skills.nameDirMismatch(filePath, parentDir, data.name);
153
+ safeLogger().error(msg);
154
+ throw new Error(msg);
155
+ }
27
156
  return {
28
- id: data.name || filePath,
157
+ id: data.name,
29
158
  path: filePath,
30
159
  metadata: data,
31
160
  rawContent: content,
32
161
  instructions: instructions.trim(),
33
162
  };
34
163
  }
164
+ /**
165
+ * Parse only the YAML frontmatter of a SKILL.md file (Tier 1 catalog loading).
166
+ *
167
+ * Extracts name, description, and optional conditional paths without reading
168
+ * the full instruction body. This keeps startup context cost at approximately
169
+ * 50-100 tokens per skill.
170
+ *
171
+ * @param content - Raw SKILL.md file content
172
+ * @param filePath - Absolute or relative path to the SKILL.md file
173
+ * @param scope - Discovery scope for the catalog entry
174
+ * @returns A lightweight {@link SkillCatalogEntry} or throws on invalid frontmatter
175
+ * @see Requirements 1.1, 1.2, 1.3, 1.5, 1.6, 6.1, 6.3
176
+ */
177
+ static parseFrontmatterOnly(content, filePath, scope) {
178
+ const yamlRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
179
+ const match = content.match(yamlRegex);
180
+ if (!match) {
181
+ const msg = text.skills.missingFrontmatter(filePath);
182
+ safeLogger().error(msg);
183
+ throw new Error(msg);
184
+ }
185
+ const yamlRaw = match[1];
186
+ let parsed;
187
+ try {
188
+ parsed = parseYaml(yamlRaw);
189
+ }
190
+ catch (error) {
191
+ const reason = error instanceof Error ? error.message : String(error);
192
+ const msg = text.skills.yamlParseError(filePath, reason);
193
+ safeLogger().error(msg);
194
+ throw new Error(msg);
195
+ }
196
+ if (parsed == null || typeof parsed !== 'object') {
197
+ const msg = text.skills.missingFrontmatter(filePath);
198
+ safeLogger().error(msg);
199
+ throw new Error(msg);
200
+ }
201
+ const result = SkillFrontmatterSchema.safeParse(parsed);
202
+ if (!result.success) {
203
+ const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
204
+ const msg = text.skills.invalidFrontmatter(filePath, issues);
205
+ safeLogger().error(msg);
206
+ throw new Error(msg);
207
+ }
208
+ const data = result.data;
209
+ const parentDir = path.basename(path.dirname(filePath));
210
+ if (parentDir && parentDir !== '.' && parentDir !== data.name) {
211
+ const msg = text.skills.nameDirMismatch(filePath, parentDir, data.name);
212
+ safeLogger().error(msg);
213
+ throw new Error(msg);
214
+ }
215
+ return {
216
+ id: data.name,
217
+ name: data.name,
218
+ description: data.description,
219
+ location: filePath,
220
+ scope,
221
+ };
222
+ }
35
223
  static substituteVariables(template, args) {
36
224
  let result = template;
37
225
  // Sort keys by length descending to prevent shorter keys from replacing parts of longer keys
@@ -56,11 +244,95 @@ export class SkillParser {
56
244
  }
57
245
  return result;
58
246
  }
59
- static extractCommands(instructions) {
60
- // Matches !sh command or !command
247
+ /**
248
+ * Extract shell commands from skill instruction markdown.
249
+ *
250
+ * ## Regex: `/^!(?:sh\s+)?(.*)$/gm`
251
+ *
252
+ * ### What it matches
253
+ * Lines that begin with `!` are treated as command directives. Two forms are
254
+ * recognised:
255
+ *
256
+ * - `!sh <command>` — explicit shell prefix (the `sh ` prefix is consumed,
257
+ * only `<command>` is captured).
258
+ * - `!<command>` — shorthand without the `sh` keyword.
259
+ *
260
+ * The `m` (multiline) flag makes `^` / `$` match per-line, so every command
261
+ * line in a multi-line instruction block is extracted independently.
262
+ *
263
+ * ### What it intentionally excludes
264
+ * - Lines that do NOT start with `!` (regular markdown prose).
265
+ * - The `!` prefix itself and the optional `sh ` token — only the payload
266
+ * after them is captured in group 1.
267
+ * - Empty captures are filtered out after extraction (`trim().length > 0`).
268
+ *
269
+ * ### Security implications
270
+ *
271
+ * 1. **Greedy `(.*)` capture** — the capture group accepts any character
272
+ * (except newline) without restriction. This means the regex alone does
273
+ * NOT prevent shell metacharacters, variable expansion, pipes, or
274
+ * subshell invocations from appearing in the captured command.
275
+ *
276
+ * 2. **Multiline mode (`m` flag)** — each line is evaluated independently.
277
+ * An attacker cannot splice two lines into a single command via the regex
278
+ * itself, but embedded newlines within a single logical line (e.g. via
279
+ * `\n` literals in a YAML value) would not be caught by `^…$` anchors.
280
+ * The control-character filter below mitigates this.
281
+ *
282
+ * 3. **No shell-syntax parsing** — the regex performs plain text extraction;
283
+ * it has no awareness of quoting, escaping, or shell grammar. Security
284
+ * therefore relies on the downstream guard chain, NOT on the regex.
285
+ *
286
+ * ### Downstream security guards (defense-in-depth)
287
+ *
288
+ * The following guards are applied sequentially after extraction to mitigate
289
+ * the risks above:
290
+ *
291
+ * | Guard | Constant / Pattern | Purpose |
292
+ * |--------------------------|-----------------------------|--------------------------------------------|
293
+ * | Max length | {@link COMMAND_MAX_LENGTH} | Caps command size to 4096 chars |
294
+ * | Control-char rejection | `CONTROL_CHAR_PATTERN` | Blocks C0 control chars (except tab/LF/CR) |
295
+ * | Dangerous-pattern filter | {@link DEFAULT_DANGEROUS_PATTERNS} | Rejects known destructive idioms |
296
+ * | Audit logging | `SKILL_COMMANDS_EXTRACTED` | Logs all surviving commands for review |
297
+ *
298
+ * @param instructions - Raw skill instruction text (may contain markdown).
299
+ * @param dangerousPatterns - Optional override for the dangerous-pattern
300
+ * deny-list. Defaults to {@link DEFAULT_DANGEROUS_PATTERNS}.
301
+ * @returns Array of sanitised command strings ready for governed execution
302
+ * via ToolRouter.
303
+ *
304
+ * @security Requirement 8.4 — command extraction regex documented with
305
+ * security implications.
306
+ */
307
+ static extractCommands(instructions, dangerousPatterns = DEFAULT_DANGEROUS_PATTERNS) {
61
308
  const commandRegex = /^!(?:sh\s+)?(.*)$/gm;
62
309
  const matches = instructions.matchAll(commandRegex);
63
- return Array.from(matches, (m) => m[1].trim()).filter((cmd) => cmd.length > 0);
310
+ const raw = Array.from(matches, (m) => m[1].trim()).filter((cmd) => cmd.length > 0);
311
+ const logger = tryGetLogger();
312
+ const safe = raw.filter((cmd) => {
313
+ if (cmd.length > COMMAND_MAX_LENGTH) {
314
+ logger?.warn(`Skill command rejected: exceeds max length (${cmd.length} > ${COMMAND_MAX_LENGTH})`);
315
+ return false;
316
+ }
317
+ if (CONTROL_CHAR_PATTERN.test(cmd)) {
318
+ logger?.warn('Skill command rejected: contains control characters');
319
+ return false;
320
+ }
321
+ const matched = dangerousPatterns.find((p) => p.test(cmd));
322
+ if (matched) {
323
+ logger?.warn(`Skill command rejected: matches dangerous pattern ${matched}`);
324
+ return false;
325
+ }
326
+ return true;
327
+ });
328
+ // Audit: log all commands that will be executed
329
+ if (safe.length > 0) {
330
+ logger?.audit('SKILL_COMMANDS_EXTRACTED', {
331
+ commandCount: safe.length,
332
+ commands: safe,
333
+ }, { source: 'skill-parser', severity: 'low', scope: 'session' });
334
+ }
335
+ return safe;
64
336
  }
65
337
  }
66
338
  //# sourceMappingURL=parser.js.map
@@ -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;