salmon-loop 0.2.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/argv/headless-detection.js +27 -0
- package/dist/cli/chat-flow.js +11 -0
- package/dist/cli/chat.js +160 -24
- package/dist/cli/commands/chat.js +14 -7
- package/dist/cli/commands/flow-mode.js +63 -0
- package/dist/cli/commands/registry.js +2 -0
- package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
- package/dist/cli/commands/run/early-errors.js +23 -0
- package/dist/cli/commands/run/handler.js +115 -27
- package/dist/cli/commands/run/headless-error-writer.js +8 -0
- package/dist/cli/commands/run/loop-params.js +2 -0
- package/dist/cli/commands/run/mode.js +2 -5
- package/dist/cli/commands/run/parse-options.js +16 -0
- package/dist/cli/commands/run/persist-session.js +10 -1
- package/dist/cli/commands/run/preflight.js +10 -0
- package/dist/cli/commands/run/reporter-factory.js +4 -0
- package/dist/cli/commands/run/runtime-llm.js +38 -11
- package/dist/cli/commands/run/runtime-options.js +2 -2
- package/dist/cli/commands/serve.js +97 -77
- package/dist/cli/commands/tool-names.js +78 -78
- package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
- package/dist/cli/headless/json-protocol.js +37 -0
- package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
- package/dist/cli/headless/protocol-metadata.js +22 -0
- package/dist/cli/headless/stream-json-protocol.js +34 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/locales/en.js +30 -6
- package/dist/cli/program-bootstrap.js +10 -5
- package/dist/cli/program-commands.js +5 -1
- package/dist/cli/reporters/anthropic-stream.js +7 -1
- package/dist/cli/reporters/json.js +4 -0
- package/dist/cli/reporters/stream-json.js +17 -2
- package/dist/cli/run-cli.js +5 -3
- package/dist/cli/slash/runtime.js +27 -12
- package/dist/cli/ui/components/CommandInput.js +7 -3
- package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
- package/dist/cli/utils/command-option-source.js +13 -0
- package/dist/cli/utils/verify-resolver.js +8 -4
- package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
- package/dist/core/adapters/fs/file-adapter.js +6 -0
- package/dist/core/adapters/fs/filesystem.js +2 -1
- package/dist/core/adapters/git/git-adapter.js +78 -1
- package/dist/core/backends/salmon-loop/task-executor.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +124 -0
- package/dist/core/benchmark/swe-bench.js +25 -0
- package/dist/core/config/load.js +18 -11
- package/dist/core/config/resolve-llm.js +12 -0
- package/dist/core/config/resolvers/server.js +0 -6
- package/dist/core/config/validate.js +73 -21
- package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
- package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
- package/dist/core/context/keywords.js +18 -4
- package/dist/core/context/service-deps.js +2 -2
- package/dist/core/context/service.js +8 -0
- package/dist/core/context/steps/context-gather.js +38 -0
- package/dist/core/context/summarization/summarizer.js +55 -12
- package/dist/core/context/targeting/target-resolver.js +4 -4
- package/dist/core/extensions/index.js +23 -5
- package/dist/core/extensions/merge.js +14 -0
- package/dist/core/extensions/paths.js +31 -0
- package/dist/core/extensions/schemas.js +8 -5
- package/dist/core/facades/cli-chat.js +6 -2
- package/dist/core/facades/cli-command-chat.js +1 -0
- package/dist/core/facades/cli-command-tool-names.js +2 -0
- package/dist/core/facades/cli-observability.js +1 -1
- package/dist/core/facades/cli-program-bootstrap.js +1 -0
- package/dist/core/facades/cli-run-handler.js +4 -2
- package/dist/core/facades/cli-run-persist-session.js +1 -0
- package/dist/core/facades/cli-serve.js +4 -4
- package/dist/core/facades/cli-utils-worktree.js +1 -1
- package/dist/core/failure/diagnostics.js +53 -1
- package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
- package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
- package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
- package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
- package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
- package/dist/core/grizzco/steps/answer.js +13 -14
- package/dist/core/grizzco/steps/autopilot.js +396 -0
- package/dist/core/grizzco/steps/cache-sharing.js +29 -0
- package/dist/core/grizzco/steps/explore.js +37 -21
- package/dist/core/grizzco/steps/generateReview.js +2 -5
- package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
- package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
- package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
- package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
- package/dist/core/grizzco/steps/patch.js +105 -146
- package/dist/core/grizzco/steps/plan.js +101 -25
- package/dist/core/grizzco/steps/preflight.js +5 -6
- package/dist/core/grizzco/steps/request-assembly.js +78 -0
- package/dist/core/grizzco/steps/research.js +39 -36
- package/dist/core/grizzco/steps/tool-runtime.js +47 -0
- package/dist/core/grizzco/steps/verify-shared.js +23 -0
- package/dist/core/grizzco/steps/verify.js +13 -21
- package/dist/core/interaction/orchestration/facade.js +1 -1
- package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
- package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
- package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
- package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
- package/dist/core/llm/ai-sdk/request-params.js +113 -1
- package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
- package/dist/core/llm/ai-sdk.js +112 -27
- package/dist/core/llm/capabilities.js +12 -0
- package/dist/core/llm/contracts/repair.js +36 -30
- package/dist/core/llm/errors.js +83 -2
- package/dist/core/llm/message-composition.js +7 -22
- package/dist/core/llm/phase-router.js +29 -10
- package/dist/core/llm/redact.js +28 -3
- package/dist/core/llm/registry.js +2 -0
- package/dist/core/llm/request-augmentation.js +55 -0
- package/dist/core/llm/request-envelope.js +334 -0
- package/dist/core/llm/shared-request-assembly.js +35 -0
- package/dist/core/llm/stream-utils.js +13 -4
- package/dist/core/llm/utils.js +18 -29
- package/dist/core/memory/relevant-retrieval.js +144 -0
- package/dist/core/observability/logger.js +11 -2
- package/dist/core/patch/diff.js +1 -0
- package/dist/core/prompts/registry.js +39 -2
- package/dist/core/prompts/runtime.js +50 -12
- package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
- package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
- package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
- package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
- package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
- package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
- package/dist/core/prompts/templates/system/main_system.hbs +4 -16
- package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
- package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
- package/dist/core/prompts/templates/system/research_system.hbs +2 -0
- package/dist/core/protocols/a2a/agent-card.js +5 -3
- package/dist/core/protocols/a2a/sdk/executor.js +2 -1
- package/dist/core/protocols/a2a/sdk/server.js +0 -1
- package/dist/core/protocols/acp/formal-agent.js +300 -58
- package/dist/core/protocols/acp/handlers.js +5 -1
- package/dist/core/protocols/acp/permission-provider.js +1 -1
- package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
- package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
- package/dist/core/public-capabilities/projections.js +29 -0
- package/dist/core/public-capabilities/registry.js +26 -0
- package/dist/core/public-capabilities/types.js +2 -0
- package/dist/core/runtime/agent-server-runtime.js +47 -43
- package/dist/core/runtime/execution-profile.js +67 -0
- package/dist/core/session/artifact-state.js +160 -0
- package/dist/core/session/compaction/index.js +183 -0
- package/dist/core/session/compaction/microcompact.js +78 -0
- package/dist/core/session/compaction/tracking.js +48 -0
- package/dist/core/session/compaction/types.js +11 -0
- package/dist/core/session/compression.js +8 -0
- package/dist/core/session/manager.js +244 -8
- package/dist/core/session/pruning-strategy.js +55 -9
- package/dist/core/session/replacement-preview-provider.js +24 -0
- package/dist/core/session/replacement-state.js +131 -0
- package/dist/core/session/resume-repair/pipeline.js +79 -0
- package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
- package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
- package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
- package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
- package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
- package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
- package/dist/core/session/resume-repair/types.js +2 -0
- package/dist/core/session/summary-sync.js +164 -13
- package/dist/core/session/token-tracker.js +6 -0
- package/dist/core/skills/audit.js +34 -0
- package/dist/core/skills/bridge.js +84 -7
- package/dist/core/skills/discovery.js +94 -0
- package/dist/core/skills/feature-flags.js +52 -0
- package/dist/core/skills/index.js +1 -1
- package/dist/core/skills/loader.js +195 -20
- package/dist/core/skills/parser.js +296 -24
- package/dist/core/skills/permissions.js +117 -0
- package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
- package/dist/core/skills/runtime/SkillRunner.js +240 -61
- package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
- package/dist/core/strata/layers/worktree.js +67 -10
- package/dist/core/strata/runtime/synchronizer.js +29 -2
- package/dist/core/streaming/stream-assembler.js +75 -31
- package/dist/core/sub-agent/context-snapshot.js +156 -0
- package/dist/core/sub-agent/core/loop.js +1 -1
- package/dist/core/sub-agent/core/manager.js +119 -20
- package/dist/core/sub-agent/dispatch-policy.js +29 -0
- package/dist/core/sub-agent/prefix-consistency.js +48 -0
- package/dist/core/sub-agent/registry-defaults.js +4 -0
- package/dist/core/sub-agent/tools/task-spawn.js +79 -2
- package/dist/core/sub-agent/types.js +134 -5
- package/dist/core/tools/audit.js +13 -4
- package/dist/core/tools/builtin/ast-grep.js +1 -1
- package/dist/core/tools/builtin/ast.js +1 -1
- package/dist/core/tools/builtin/benchmark.js +360 -0
- package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
- package/dist/core/tools/builtin/code-search/executor.js +6 -1
- package/dist/core/tools/builtin/code-search/spec.js +26 -2
- package/dist/core/tools/builtin/fs.js +256 -23
- package/dist/core/tools/builtin/git.js +2 -2
- package/dist/core/tools/builtin/index.js +51 -2
- package/dist/core/tools/builtin/interaction.js +8 -1
- package/dist/core/tools/builtin/plan.js +37 -15
- package/dist/core/tools/builtin/shell.js +1 -1
- package/dist/core/tools/loader.js +39 -16
- package/dist/core/tools/mapper.js +17 -3
- package/dist/core/tools/mcp/client.js +2 -1
- package/dist/core/tools/parallel/scheduler.js +35 -4
- package/dist/core/tools/permissions/permission-rules.js +5 -10
- package/dist/core/tools/policy.js +6 -1
- package/dist/core/tools/recoverable-tool-errors.js +10 -0
- package/dist/core/tools/router.js +24 -6
- package/dist/core/tools/session.js +458 -48
- package/dist/core/tools/tool-visibility.js +62 -0
- package/dist/core/tools/types.js +9 -1
- package/dist/core/types/execution.js +4 -0
- package/dist/core/types/flow-mode.js +8 -0
- package/dist/core/utils/path.js +52 -0
- package/dist/core/verification/runner.js +4 -1
- package/dist/core/version.js +17 -0
- package/dist/languages/typescript/index.js +4 -1
- package/dist/locales/en.js +35 -2
- package/dist/utils/eol.js +1 -1
- package/package.json +14 -7
- package/scripts/fix-es-abstract-compat.js +77 -0
- package/dist/core/runtime/fastify-server-bundle.js +0 -26
- package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
- package/dist/core/runtime/sidecar-paths.js +0 -47
- package/dist/core/runtime/sidecar-route-catalog.js +0 -103
|
@@ -1,37 +1,225 @@
|
|
|
1
|
-
import
|
|
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
|
-
//
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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;
|