agentsmesh 0.14.0 → 0.16.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/CHANGELOG.md +32 -0
- package/README.md +27 -2
- package/dist/canonical.d.ts +2 -2
- package/dist/canonical.js +305 -104
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +190 -188
- package/dist/engine.d.ts +2 -2
- package/dist/engine.js +343 -111
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +343 -111
- package/dist/index.js.map +1 -1
- package/dist/{schema-o4oXUVBP.d.ts → schema-CD2qcmDL.d.ts} +1 -0
- package/dist/{target-descriptor--Nw5i4v3.d.ts → target-descriptor-CvB1qzPn.d.ts} +1 -1
- package/dist/targets.d.ts +3 -3
- package/dist/targets.js +129 -98
- package/dist/targets.js.map +1 -1
- package/package.json +1 -1
- package/schemas/agentsmesh.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0b33c0d: Security and robustness hardening across MCP write tools, hook script generation, remote fetch, and canonical name validation. Some changes are behaviorally breaking; pre-1.0 minor bump per project policy.
|
|
8
|
+
|
|
9
|
+
**Hardening (no contract change)**
|
|
10
|
+
- MCP `add_mcp_server` / `update_mcp_server` / `update_hooks` / `update_permissions` now reject obviously malicious payloads at the schema layer (shell metacharacters in `command`, embedded newlines in matchers, env keys outside `[A-Za-z_][A-Za-z0-9_]*`, non-`http(s)` URLs, args arrays over 100, unknown server fields, permission patterns outside `Tool` / `Tool(matcher)`).
|
|
11
|
+
- Generated Copilot and Cline hook wrappers strip CR/LF from event/matcher/command before embedding them in the `# agentsmesh-*:` comment header so a multi-line YAML scalar cannot break out of the comment into executable shell.
|
|
12
|
+
- Generated `.sh` / `.bash` / `.zsh` files are now written with mode `0o755` so hooks emitted to disk are exec'able by the runner without a manual `chmod +x`.
|
|
13
|
+
- GitHub tarball downloads are capped at 500 MiB and aborted mid-stream when the running byte total exceeds the cap (Content-Length is also pre-checked).
|
|
14
|
+
- Git refs and clone URLs that begin with `-` are rejected to block `--upload-pack=…` style option injection.
|
|
15
|
+
- `MCP` non-`McpError` fallbacks redact absolute filesystem paths from raw `Error.message` strings; `IO_ERROR` envelopes carry the underlying `errno` in `details`.
|
|
16
|
+
- `parseAgents` / `parseCommands` / `parseRules` / `parseSkills` reject canonical filenames that are Windows reserved devices (CON, AUX, NUL, COM1–9, LPT1–9), contain `<>:|?*`, or end in `.`/space; nested basename collisions (e.g. `agents/foo.md` and `agents/sub/foo.md`) now error instead of silently last-write-wins.
|
|
17
|
+
- `loadAllPlugins` now collects all per-plugin failures and rethrows as a single combined error when any entry has `strict: true` or `AGENTSMESH_STRICT_PLUGINS=1` is set in the environment. Previous warn-and-skip behavior remains the default.
|
|
18
|
+
|
|
19
|
+
**Breaking**
|
|
20
|
+
- Generated hook wrappers run under `set -eu` instead of `set -e`. A canonical `command` that references an unset shell variable (`echo $VAR` where `$VAR` is never exported) will now abort the hook. Use `${VAR:-default}` syntax when an unset value is intentional.
|
|
21
|
+
- `AGENTSMESH_CACHE` must now be an absolute path that is not the filesystem root (`/` or a Windows drive root). Relative paths and roots throw at startup. Previously the value was used verbatim.
|
|
22
|
+
- MCP `create_rule` / `create_command` / `create_agent` and the canonical handlers reject names containing `/`. The `NAME_RE` validator was tightened from `[a-zA-Z0-9_/-]*` to `[a-zA-Z0-9_-]*` — names must be flat identifiers.
|
|
23
|
+
- Canonical files named after Windows reserved devices or with reserved characters now throw `CanonicalNameError` at parse time on every host (previously silent failure on Windows, success on POSIX).
|
|
24
|
+
|
|
25
|
+
**Internal**
|
|
26
|
+
- `src/mcp/register.ts`, `src/utils/filesystem/fs.ts`, `src/mcp/handlers/orchestrate.ts`, and `src/cli/commands/target-scaffold/templates.ts` were each split under the project's 200-line file budget. Public API surface (`./engine`, `./canonical`, `./targets`) is unchanged.
|
|
27
|
+
- New `executableModeFor(path)` helper in `src/utils/filesystem/fs.ts` infers the executable bit from the path extension; `writeFileAtomic` accepts an optional `{ mode }` override.
|
|
28
|
+
|
|
29
|
+
## 0.15.0
|
|
30
|
+
|
|
31
|
+
### Minor Changes
|
|
32
|
+
|
|
33
|
+
- 1edb936: Homebrew tap and standalone-binary distribution. `brew tap samplexbro/agentsmesh && brew install agentsmesh` installs from a Homebrew formula auto-rendered against the published npm tarball; `curl -fsSL https://github.com/sampleXbro/agentsmesh/releases/latest/download/install.sh | sh` downloads a Node-free Bun-compiled binary for macOS (arm64/x64) and Linux (arm64/x64), verifies SHA256, installs to `~/.agentsmesh/bin`, and adds it to PATH for zsh, bash, and fish. The release workflow builds binaries for every supported platform on each `master` push, attaches them to a GitHub Release alongside `SHA256SUMS`, and pushes the formula to the tap repo; `workflow_dispatch` re-runs rebuild assets against an existing tag. Installer fails closed on missing/unverifiable checksums and rejects shell-unsafe `AGENTSMESH_INSTALL` paths.
|
|
34
|
+
|
|
3
35
|
## 0.14.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -24,6 +24,31 @@ AI coding assistants now ship with their own configuration formats — `CLAUDE.m
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
### npm (recommended for Node.js projects)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g agentsmesh
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Homebrew (macOS / Linux)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
brew tap samplexbro/agentsmesh
|
|
39
|
+
brew install agentsmesh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Standalone binary (no Node.js required)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
curl -fsSL https://github.com/sampleXbro/agentsmesh/releases/latest/download/install.sh | sh
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or download a binary directly from [GitHub Releases](https://github.com/sampleXbro/agentsmesh/releases/latest).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
27
52
|
## Before / After
|
|
28
53
|
|
|
29
54
|
**Before — fragmented assistant-native config in one repo:**
|
|
@@ -64,7 +89,7 @@ The native files above are still emitted — AgentsMesh writes them for you from
|
|
|
64
89
|
|
|
65
90
|
## 60-second quickstart
|
|
66
91
|
|
|
67
|
-
|
|
92
|
+
Works on Linux, macOS, and Windows. The `npx` commands below require Node.js 20+; see [Install](#install) for alternatives that don't need Node.js.
|
|
68
93
|
|
|
69
94
|
```bash
|
|
70
95
|
npx agentsmesh init # scaffold .agentsmesh/ + agentsmesh.yaml
|
|
@@ -234,7 +259,7 @@ agentsmesh generate # plugin targets run alongside built-ins
|
|
|
234
259
|
agentsmesh generate --global # global mode works for plugins too
|
|
235
260
|
```
|
|
236
261
|
|
|
237
|
-
Plugins have full parity with built-in targets: project + global layouts, feature conversions, scoped settings, per-feature lint hooks, and hook post-processing. [Build a plugin →](https://samplexbro.github.io/agentsmesh/guides/building-plugins/)
|
|
262
|
+
Plugins have full parity with built-in targets: project + global layouts, feature conversions, scoped settings, per-feature lint hooks, and hook post-processing. By default a failed plugin import logs a warning and is skipped; set `strict: true` on the plugin entry or run `AGENTSMESH_STRICT_PLUGINS=1 agentsmesh generate` to fail the build instead — useful in CI where a missing target is a real regression. [Build a plugin →](https://samplexbro.github.io/agentsmesh/guides/building-plugins/)
|
|
238
263
|
|
|
239
264
|
### Team-safe collaboration & CI drift detection
|
|
240
265
|
|
package/dist/canonical.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { b as CanonicalFiles, V as ValidatedConfig } from './schema-
|
|
2
|
-
export { C as CanonicalAgent, a as CanonicalCommand, c as CanonicalRule, d as CanonicalSkill, H as HookEntry, e as Hooks, I as IgnorePatterns, M as McpConfig, f as McpServer, P as Permissions, S as SkillSupportingFile, g as StdioMcpServer, U as UrlMcpServer } from './schema-
|
|
1
|
+
import { b as CanonicalFiles, V as ValidatedConfig } from './schema-CD2qcmDL.js';
|
|
2
|
+
export { C as CanonicalAgent, a as CanonicalCommand, c as CanonicalRule, d as CanonicalSkill, H as HookEntry, e as Hooks, I as IgnorePatterns, M as McpConfig, f as McpServer, P as Permissions, S as SkillSupportingFile, g as StdioMcpServer, U as UrlMcpServer } from './schema-CD2qcmDL.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/canonical.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { access, readdir, realpath, stat, readFile, rm, mkdir, writeFile, rename, lstat, unlink } from 'fs/promises';
|
|
2
1
|
import { join, basename, dirname, resolve, relative, posix, win32, extname } from 'path';
|
|
2
|
+
import { access, readdir, realpath, stat, readFile, rm, mkdir, writeFile, rename, lstat, unlink, chmod } from 'fs/promises';
|
|
3
3
|
import { constants, existsSync, realpathSync, statSync } from 'fs';
|
|
4
4
|
import { parse, stringify } from 'yaml';
|
|
5
5
|
import { parse as parse$1 } from 'smol-toml';
|
|
@@ -79,6 +79,104 @@ function shouldNormalizeLineEndings(path) {
|
|
|
79
79
|
function normalizeLineEndings(content) {
|
|
80
80
|
return content.replace(/\r\n?/g, "\n");
|
|
81
81
|
}
|
|
82
|
+
function executableModeFor(path) {
|
|
83
|
+
return EXECUTABLE_SCRIPT_EXTENSIONS.has(extname(path).toLowerCase()) ? 493 : void 0;
|
|
84
|
+
}
|
|
85
|
+
var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES, EXECUTABLE_SCRIPT_EXTENSIONS;
|
|
86
|
+
var init_fs_text_encoding = __esm({
|
|
87
|
+
"src/utils/filesystem/fs-text-encoding.ts"() {
|
|
88
|
+
UTF8_BOM = "\uFEFF";
|
|
89
|
+
TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
90
|
+
".md",
|
|
91
|
+
".mdc",
|
|
92
|
+
".mdx",
|
|
93
|
+
".markdown",
|
|
94
|
+
".txt",
|
|
95
|
+
".json",
|
|
96
|
+
".jsonc",
|
|
97
|
+
".yaml",
|
|
98
|
+
".yml",
|
|
99
|
+
".toml",
|
|
100
|
+
".ini",
|
|
101
|
+
".sh",
|
|
102
|
+
".bash",
|
|
103
|
+
".zsh",
|
|
104
|
+
".ps1",
|
|
105
|
+
".js",
|
|
106
|
+
".mjs",
|
|
107
|
+
".cjs",
|
|
108
|
+
".ts",
|
|
109
|
+
".tsx",
|
|
110
|
+
".html",
|
|
111
|
+
".css"
|
|
112
|
+
]);
|
|
113
|
+
TEXT_DOTFILES = /* @__PURE__ */ new Set([
|
|
114
|
+
".gitignore",
|
|
115
|
+
".cursorignore",
|
|
116
|
+
".cursorindexingignore",
|
|
117
|
+
".aiignore",
|
|
118
|
+
".agentignore",
|
|
119
|
+
".clineignore",
|
|
120
|
+
".geminiignore",
|
|
121
|
+
".codeiumignore",
|
|
122
|
+
".continueignore",
|
|
123
|
+
".copilotignore",
|
|
124
|
+
".windsurfignore",
|
|
125
|
+
".junieignore",
|
|
126
|
+
".kiroignore",
|
|
127
|
+
".rooignore",
|
|
128
|
+
".antigravityignore"
|
|
129
|
+
]);
|
|
130
|
+
EXECUTABLE_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zsh"]);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
async function readDirRecursive(dir, visited) {
|
|
134
|
+
let canonicalDir;
|
|
135
|
+
try {
|
|
136
|
+
canonicalDir = await realpath(dir);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const e = err;
|
|
139
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "ELOOP") return [];
|
|
140
|
+
throw new FileSystemError(
|
|
141
|
+
dir,
|
|
142
|
+
`Failed to read directory ${dir}: ${e.message}. Check permissions.`,
|
|
143
|
+
{ cause: err, errnoCode: e.code }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const seen = visited ?? /* @__PURE__ */ new Set();
|
|
147
|
+
if (seen.has(canonicalDir)) return [];
|
|
148
|
+
seen.add(canonicalDir);
|
|
149
|
+
try {
|
|
150
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
151
|
+
const files = [];
|
|
152
|
+
for (const ent of entries) {
|
|
153
|
+
const full = join(dir, ent.name);
|
|
154
|
+
const walkChild = ent.isDirectory() || ent.isSymbolicLink() && await stat(full).then(
|
|
155
|
+
(s) => s.isDirectory(),
|
|
156
|
+
() => false
|
|
157
|
+
);
|
|
158
|
+
if (walkChild) {
|
|
159
|
+
files.push(...await readDirRecursive(full, seen));
|
|
160
|
+
} else {
|
|
161
|
+
files.push(full);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return files;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const e = err;
|
|
167
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "EACCES") return [];
|
|
168
|
+
throw new FileSystemError(
|
|
169
|
+
dir,
|
|
170
|
+
`Failed to read directory ${dir}: ${e.message}. Check permissions.`,
|
|
171
|
+
{ cause: err, errnoCode: e.code }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
var init_fs_traverse = __esm({
|
|
176
|
+
"src/utils/filesystem/fs-traverse.ts"() {
|
|
177
|
+
init_errors();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
82
180
|
async function readFileSafe(path) {
|
|
83
181
|
try {
|
|
84
182
|
const data = await readFile(path, "utf-8");
|
|
@@ -93,7 +191,7 @@ async function readFileSafe(path) {
|
|
|
93
191
|
);
|
|
94
192
|
}
|
|
95
193
|
}
|
|
96
|
-
async function writeFileAtomic(path, content) {
|
|
194
|
+
async function writeFileAtomic(path, content, options) {
|
|
97
195
|
const dir = dirname(path);
|
|
98
196
|
await mkdir(dir, { recursive: true });
|
|
99
197
|
try {
|
|
@@ -117,6 +215,7 @@ async function writeFileAtomic(path, content) {
|
|
|
117
215
|
}
|
|
118
216
|
const tmpPath = `${path}.tmp`;
|
|
119
217
|
const payload = shouldNormalizeLineEndings(path) ? normalizeLineEndings(content) : content;
|
|
218
|
+
const mode = executableModeFor(path);
|
|
120
219
|
try {
|
|
121
220
|
try {
|
|
122
221
|
const tmpInfo = await lstat(tmpPath);
|
|
@@ -126,8 +225,16 @@ async function writeFileAtomic(path, content) {
|
|
|
126
225
|
} catch (tmpErr) {
|
|
127
226
|
if (tmpErr.code !== "ENOENT") throw tmpErr;
|
|
128
227
|
}
|
|
129
|
-
|
|
228
|
+
const writeOpts = {
|
|
229
|
+
encoding: "utf-8",
|
|
230
|
+
flag: "w"
|
|
231
|
+
};
|
|
232
|
+
if (mode !== void 0) writeOpts.mode = mode;
|
|
233
|
+
await writeFile(tmpPath, payload, writeOpts);
|
|
130
234
|
await rename(tmpPath, path);
|
|
235
|
+
if (mode !== void 0) {
|
|
236
|
+
await chmod(path, mode);
|
|
237
|
+
}
|
|
131
238
|
} catch (err) {
|
|
132
239
|
await rm(tmpPath, { force: true }).catch(() => {
|
|
133
240
|
});
|
|
@@ -150,94 +257,12 @@ async function exists(path) {
|
|
|
150
257
|
async function mkdirp(path) {
|
|
151
258
|
await mkdir(path, { recursive: true });
|
|
152
259
|
}
|
|
153
|
-
async function readDirRecursive(dir, visited) {
|
|
154
|
-
let canonicalDir;
|
|
155
|
-
try {
|
|
156
|
-
canonicalDir = await realpath(dir);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
const e = err;
|
|
159
|
-
if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "ELOOP") return [];
|
|
160
|
-
throw new FileSystemError(
|
|
161
|
-
dir,
|
|
162
|
-
`Failed to read directory ${dir}: ${e.message}. Check permissions.`,
|
|
163
|
-
{ cause: err, errnoCode: e.code }
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
const seen = visited ?? /* @__PURE__ */ new Set();
|
|
167
|
-
if (seen.has(canonicalDir)) return [];
|
|
168
|
-
seen.add(canonicalDir);
|
|
169
|
-
try {
|
|
170
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
171
|
-
const files = [];
|
|
172
|
-
for (const ent of entries) {
|
|
173
|
-
const full = join(dir, ent.name);
|
|
174
|
-
const walkChild = ent.isDirectory() || ent.isSymbolicLink() && await stat(full).then(
|
|
175
|
-
(s) => s.isDirectory(),
|
|
176
|
-
() => false
|
|
177
|
-
);
|
|
178
|
-
if (walkChild) {
|
|
179
|
-
files.push(...await readDirRecursive(full, seen));
|
|
180
|
-
} else {
|
|
181
|
-
files.push(full);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return files;
|
|
185
|
-
} catch (err) {
|
|
186
|
-
const e = err;
|
|
187
|
-
if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "EACCES") return [];
|
|
188
|
-
throw new FileSystemError(
|
|
189
|
-
dir,
|
|
190
|
-
`Failed to read directory ${dir}: ${e.message}. Check permissions.`,
|
|
191
|
-
{ cause: err, errnoCode: e.code }
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES;
|
|
196
260
|
var init_fs = __esm({
|
|
197
261
|
"src/utils/filesystem/fs.ts"() {
|
|
198
262
|
init_errors();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
".mdc",
|
|
203
|
-
".mdx",
|
|
204
|
-
".markdown",
|
|
205
|
-
".txt",
|
|
206
|
-
".json",
|
|
207
|
-
".jsonc",
|
|
208
|
-
".yaml",
|
|
209
|
-
".yml",
|
|
210
|
-
".toml",
|
|
211
|
-
".ini",
|
|
212
|
-
".sh",
|
|
213
|
-
".bash",
|
|
214
|
-
".zsh",
|
|
215
|
-
".ps1",
|
|
216
|
-
".js",
|
|
217
|
-
".mjs",
|
|
218
|
-
".cjs",
|
|
219
|
-
".ts",
|
|
220
|
-
".tsx",
|
|
221
|
-
".html",
|
|
222
|
-
".css"
|
|
223
|
-
]);
|
|
224
|
-
TEXT_DOTFILES = /* @__PURE__ */ new Set([
|
|
225
|
-
".gitignore",
|
|
226
|
-
".cursorignore",
|
|
227
|
-
".cursorindexingignore",
|
|
228
|
-
".aiignore",
|
|
229
|
-
".agentignore",
|
|
230
|
-
".clineignore",
|
|
231
|
-
".geminiignore",
|
|
232
|
-
".codeiumignore",
|
|
233
|
-
".continueignore",
|
|
234
|
-
".copilotignore",
|
|
235
|
-
".windsurfignore",
|
|
236
|
-
".junieignore",
|
|
237
|
-
".kiroignore",
|
|
238
|
-
".rooignore",
|
|
239
|
-
".antigravityignore"
|
|
240
|
-
]);
|
|
263
|
+
init_fs_text_encoding();
|
|
264
|
+
init_fs_traverse();
|
|
265
|
+
init_fs_text_encoding();
|
|
241
266
|
}
|
|
242
267
|
});
|
|
243
268
|
function parseFrontmatter(content) {
|
|
@@ -3998,13 +4023,16 @@ function generateAgents4(canonical) {
|
|
|
3998
4023
|
function safeEventName(event) {
|
|
3999
4024
|
return event.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
|
|
4000
4025
|
}
|
|
4026
|
+
function safeShellLine(value) {
|
|
4027
|
+
return value.replace(/[\r\n]+/g, " ");
|
|
4028
|
+
}
|
|
4001
4029
|
function buildHookScript(event, command, matcher) {
|
|
4002
4030
|
return [
|
|
4003
4031
|
"#!/usr/bin/env bash",
|
|
4004
|
-
`# agentsmesh-event: ${event}`,
|
|
4005
|
-
`# agentsmesh-matcher: ${matcher}`,
|
|
4006
|
-
`# agentsmesh-command: ${command}`,
|
|
4007
|
-
"set -
|
|
4032
|
+
`# agentsmesh-event: ${safeShellLine(event)}`,
|
|
4033
|
+
`# agentsmesh-matcher: ${safeShellLine(matcher)}`,
|
|
4034
|
+
`# agentsmesh-command: ${safeShellLine(command)}`,
|
|
4035
|
+
"set -eu",
|
|
4008
4036
|
command,
|
|
4009
4037
|
""
|
|
4010
4038
|
].join("\n");
|
|
@@ -6461,7 +6489,7 @@ function extractMatcher(comment) {
|
|
|
6461
6489
|
function extractWrapperCommand(content) {
|
|
6462
6490
|
const metadataMatch = content.match(/^# agentsmesh-command:\s*(.+)$/m);
|
|
6463
6491
|
if (metadataMatch?.[1]) return metadataMatch[1].trim();
|
|
6464
|
-
return content.replace(/^#!.*\n/, "").replace(/^#.*\n/gm, "").replace(/^HOOK_DIR=.*\n/gm, "").replace(/^set -e
|
|
6492
|
+
return content.replace(/^#!.*\n/, "").replace(/^#.*\n/gm, "").replace(/^HOOK_DIR=.*\n/gm, "").replace(/^set -e[u]?\n?/m, "").trim();
|
|
6465
6493
|
}
|
|
6466
6494
|
async function importHooks(projectRoot, results) {
|
|
6467
6495
|
const hooksDir = join(projectRoot, COPILOT_HOOKS_DIR);
|
|
@@ -6783,12 +6811,15 @@ async function buildAssetOutput(projectRoot, command) {
|
|
|
6783
6811
|
function wrapperPath(event, index) {
|
|
6784
6812
|
return `${COPILOT_HOOKS_DIR}/scripts/${safePhaseName(event)}-${index}.sh`;
|
|
6785
6813
|
}
|
|
6814
|
+
function safeShellLine2(value) {
|
|
6815
|
+
return value.replace(/[\r\n]+/g, " ");
|
|
6816
|
+
}
|
|
6786
6817
|
function buildWrapper(command, matcher) {
|
|
6787
6818
|
return [
|
|
6788
6819
|
"#!/usr/bin/env bash",
|
|
6789
|
-
`# agentsmesh-matcher: ${matcher}`,
|
|
6790
|
-
`# agentsmesh-command: ${command}`,
|
|
6791
|
-
"set -
|
|
6820
|
+
`# agentsmesh-matcher: ${safeShellLine2(matcher)}`,
|
|
6821
|
+
`# agentsmesh-command: ${safeShellLine2(command)}`,
|
|
6822
|
+
"set -eu",
|
|
6792
6823
|
command,
|
|
6793
6824
|
""
|
|
6794
6825
|
].join("\n");
|
|
@@ -6812,8 +6843,8 @@ async function addHookScriptAssets(projectRoot, canonical, outputs) {
|
|
|
6812
6843
|
}
|
|
6813
6844
|
}
|
|
6814
6845
|
const wrapper = buildWrapper(command, entry.matcher).replace(
|
|
6815
|
-
"set -
|
|
6816
|
-
'set -
|
|
6846
|
+
"set -eu\n",
|
|
6847
|
+
'set -eu\nHOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\n'
|
|
6817
6848
|
);
|
|
6818
6849
|
wrapperOutputs.push({ path: scriptPath, content: wrapper });
|
|
6819
6850
|
index++;
|
|
@@ -14417,6 +14448,13 @@ init_fs();
|
|
|
14417
14448
|
init_fs();
|
|
14418
14449
|
var execFileAsync = promisify(execFile);
|
|
14419
14450
|
var REPO_DIRNAME = "repo";
|
|
14451
|
+
function ensureNotFlag(value, kind) {
|
|
14452
|
+
if (value.startsWith("-")) {
|
|
14453
|
+
throw new Error(
|
|
14454
|
+
`agentsmesh refuses ${kind} starting with "-" (option-injection guard): ${value}`
|
|
14455
|
+
);
|
|
14456
|
+
}
|
|
14457
|
+
}
|
|
14420
14458
|
async function fetchGitRemoteExtend(parsed, extendName, options, cacheDir, buildCacheKey2) {
|
|
14421
14459
|
const provider = "cloneUrl" in parsed ? "gitlab" : "git";
|
|
14422
14460
|
const identifier = "cloneUrl" in parsed ? `${parsed.namespace}/${parsed.project}` : parsed.url;
|
|
@@ -14474,9 +14512,11 @@ function resolveCloneUrl(parsed) {
|
|
|
14474
14512
|
return parsed.url;
|
|
14475
14513
|
}
|
|
14476
14514
|
async function cloneRepo(cloneUrl, repoDir) {
|
|
14515
|
+
ensureNotFlag(cloneUrl, "clone-url");
|
|
14477
14516
|
await runGit(["clone", cloneUrl, repoDir]);
|
|
14478
14517
|
}
|
|
14479
14518
|
async function checkoutRef(repoDir, ref) {
|
|
14519
|
+
ensureNotFlag(ref, "ref");
|
|
14480
14520
|
await runGit(["checkout", ref], repoDir);
|
|
14481
14521
|
}
|
|
14482
14522
|
async function getHeadSha(repoDir) {
|
|
@@ -14495,6 +14535,46 @@ async function runGit(args, cwd) {
|
|
|
14495
14535
|
|
|
14496
14536
|
// src/config/remote/github-remote.ts
|
|
14497
14537
|
init_fs();
|
|
14538
|
+
var MAX_TARBALL_BYTES = 500 * 1024 * 1024;
|
|
14539
|
+
async function readBoundedResponse(res, maxBytes) {
|
|
14540
|
+
const lenHeader = typeof res.headers?.get === "function" ? res.headers.get("content-length") : null;
|
|
14541
|
+
if (lenHeader !== null) {
|
|
14542
|
+
const declared = Number(lenHeader);
|
|
14543
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
14544
|
+
throw new Error(`remote response declared ${declared} bytes; exceeds cap of ${maxBytes}`);
|
|
14545
|
+
}
|
|
14546
|
+
}
|
|
14547
|
+
const stream = res.body;
|
|
14548
|
+
if (!stream) {
|
|
14549
|
+
const buf = await res.arrayBuffer();
|
|
14550
|
+
if (buf.byteLength > maxBytes) {
|
|
14551
|
+
throw new Error(`remote response is ${buf.byteLength} bytes; exceeds cap of ${maxBytes}`);
|
|
14552
|
+
}
|
|
14553
|
+
return new Uint8Array(buf);
|
|
14554
|
+
}
|
|
14555
|
+
const reader = stream.getReader();
|
|
14556
|
+
const chunks = [];
|
|
14557
|
+
let total = 0;
|
|
14558
|
+
for (; ; ) {
|
|
14559
|
+
const { done, value } = await reader.read();
|
|
14560
|
+
if (done) break;
|
|
14561
|
+
if (!value) continue;
|
|
14562
|
+
total += value.byteLength;
|
|
14563
|
+
if (total > maxBytes) {
|
|
14564
|
+
await reader.cancel().catch(() => {
|
|
14565
|
+
});
|
|
14566
|
+
throw new Error(`remote response exceeded cap of ${maxBytes} bytes during streaming`);
|
|
14567
|
+
}
|
|
14568
|
+
chunks.push(value);
|
|
14569
|
+
}
|
|
14570
|
+
const out2 = new Uint8Array(total);
|
|
14571
|
+
let offset = 0;
|
|
14572
|
+
for (const c2 of chunks) {
|
|
14573
|
+
out2.set(c2, offset);
|
|
14574
|
+
offset += c2.byteLength;
|
|
14575
|
+
}
|
|
14576
|
+
return out2;
|
|
14577
|
+
}
|
|
14498
14578
|
async function resolveLatestTag(org, repo, token) {
|
|
14499
14579
|
const url = `https://api.github.com/repos/${org}/${repo}/releases/latest`;
|
|
14500
14580
|
const headers = {
|
|
@@ -14534,11 +14614,11 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
|
|
|
14534
14614
|
const tarballUrl = `https://github.com/${parsed.org}/${parsed.repo}/tarball/${tag}`;
|
|
14535
14615
|
const headers = {};
|
|
14536
14616
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
14537
|
-
let
|
|
14617
|
+
let tarballBytes;
|
|
14538
14618
|
try {
|
|
14539
14619
|
const res = await globalThis.fetch(tarballUrl, { headers, redirect: "follow" });
|
|
14540
14620
|
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
14541
|
-
|
|
14621
|
+
tarballBytes = await readBoundedResponse(res, MAX_TARBALL_BYTES);
|
|
14542
14622
|
} catch (err) {
|
|
14543
14623
|
const allowFallback = options.allowOfflineFallback !== false;
|
|
14544
14624
|
if (allowFallback && await exists(extractDir)) {
|
|
@@ -14555,7 +14635,7 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
|
|
|
14555
14635
|
await rm(extractDir, { recursive: true, force: true });
|
|
14556
14636
|
await mkdir(extractDir, { recursive: true });
|
|
14557
14637
|
const tarPath = join(extractDir, "archive.tar.gz");
|
|
14558
|
-
await writeFile(tarPath,
|
|
14638
|
+
await writeFile(tarPath, tarballBytes);
|
|
14559
14639
|
try {
|
|
14560
14640
|
await tar.extract({
|
|
14561
14641
|
file: tarPath,
|
|
@@ -14727,7 +14807,20 @@ function buildCacheKey(provider, identifier, ref) {
|
|
|
14727
14807
|
}
|
|
14728
14808
|
function getCacheDir() {
|
|
14729
14809
|
const env = process.env.AGENTSMESH_CACHE;
|
|
14730
|
-
if (env)
|
|
14810
|
+
if (env) {
|
|
14811
|
+
const trimmed = env.trim();
|
|
14812
|
+
if (!trimmed) {
|
|
14813
|
+
return join(homedir(), ".agentsmesh", "cache");
|
|
14814
|
+
}
|
|
14815
|
+
const isAbs = /^([A-Za-z]:[\\/]|\/)/.test(trimmed);
|
|
14816
|
+
if (!isAbs) {
|
|
14817
|
+
throw new Error(`AGENTSMESH_CACHE must be an absolute path (got: "${trimmed}").`);
|
|
14818
|
+
}
|
|
14819
|
+
if (trimmed === "/" || /^[A-Za-z]:[\\/]?$/.test(trimmed)) {
|
|
14820
|
+
throw new Error(`AGENTSMESH_CACHE must not be the filesystem root (got: "${trimmed}").`);
|
|
14821
|
+
}
|
|
14822
|
+
return trimmed;
|
|
14823
|
+
}
|
|
14731
14824
|
return join(homedir(), ".agentsmesh", "cache");
|
|
14732
14825
|
}
|
|
14733
14826
|
async function fetchRemoteExtend(source, extendName, options = {}) {
|
|
@@ -14819,6 +14912,98 @@ async function resolveExtendPaths(config, configDir, options = {}) {
|
|
|
14819
14912
|
// src/canonical/features/rules.ts
|
|
14820
14913
|
init_fs();
|
|
14821
14914
|
init_markdown();
|
|
14915
|
+
|
|
14916
|
+
// src/utils/filesystem/windows-path-safety.ts
|
|
14917
|
+
var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
14918
|
+
"CON",
|
|
14919
|
+
"PRN",
|
|
14920
|
+
"AUX",
|
|
14921
|
+
"NUL",
|
|
14922
|
+
"COM1",
|
|
14923
|
+
"COM2",
|
|
14924
|
+
"COM3",
|
|
14925
|
+
"COM4",
|
|
14926
|
+
"COM5",
|
|
14927
|
+
"COM6",
|
|
14928
|
+
"COM7",
|
|
14929
|
+
"COM8",
|
|
14930
|
+
"COM9",
|
|
14931
|
+
"LPT1",
|
|
14932
|
+
"LPT2",
|
|
14933
|
+
"LPT3",
|
|
14934
|
+
"LPT4",
|
|
14935
|
+
"LPT5",
|
|
14936
|
+
"LPT6",
|
|
14937
|
+
"LPT7",
|
|
14938
|
+
"LPT8",
|
|
14939
|
+
"LPT9"
|
|
14940
|
+
]);
|
|
14941
|
+
var WINDOWS_ILLEGAL_CHARS = new RegExp('[<>:"|?*\\u0000-\\u001F]');
|
|
14942
|
+
function segmentReservedName(segment) {
|
|
14943
|
+
const stem = segment.replace(/\.[^.]*$/, "").toUpperCase();
|
|
14944
|
+
return WINDOWS_RESERVED_NAMES.has(stem);
|
|
14945
|
+
}
|
|
14946
|
+
function findWindowsPathIssues(path) {
|
|
14947
|
+
const issues = [];
|
|
14948
|
+
const segments = path.split(/[\\/]/);
|
|
14949
|
+
for (const segment of segments) {
|
|
14950
|
+
if (segment === "" || segment === "." || segment === "..") continue;
|
|
14951
|
+
if (WINDOWS_ILLEGAL_CHARS.test(segment)) {
|
|
14952
|
+
issues.push({ segment, reason: "illegal-character" });
|
|
14953
|
+
continue;
|
|
14954
|
+
}
|
|
14955
|
+
if (/[. ]$/.test(segment)) {
|
|
14956
|
+
issues.push({ segment, reason: "trailing-dot-or-space" });
|
|
14957
|
+
continue;
|
|
14958
|
+
}
|
|
14959
|
+
if (segmentReservedName(segment)) {
|
|
14960
|
+
issues.push({ segment, reason: "reserved-name" });
|
|
14961
|
+
}
|
|
14962
|
+
}
|
|
14963
|
+
return issues;
|
|
14964
|
+
}
|
|
14965
|
+
|
|
14966
|
+
// src/canonical/features/validate-name.ts
|
|
14967
|
+
var CanonicalNameError = class extends Error {
|
|
14968
|
+
feature;
|
|
14969
|
+
name;
|
|
14970
|
+
constructor(feature, name, message) {
|
|
14971
|
+
super(message);
|
|
14972
|
+
this.feature = feature;
|
|
14973
|
+
this.name = name;
|
|
14974
|
+
}
|
|
14975
|
+
};
|
|
14976
|
+
function assertCanonicalName(feature, name) {
|
|
14977
|
+
const issues = findWindowsPathIssues(name);
|
|
14978
|
+
if (issues.length === 0) return;
|
|
14979
|
+
const reasons = issues.map((i) => `${i.segment} (${i.reason})`).join(", ");
|
|
14980
|
+
throw new CanonicalNameError(
|
|
14981
|
+
feature,
|
|
14982
|
+
name,
|
|
14983
|
+
`canonical ${feature} name "${name}" is not portable to Windows: ${reasons}. Rename the file.`
|
|
14984
|
+
);
|
|
14985
|
+
}
|
|
14986
|
+
function assertNoBasenameCollisions(feature, paths, stripExt) {
|
|
14987
|
+
const seen = /* @__PURE__ */ new Map();
|
|
14988
|
+
for (const p of paths) {
|
|
14989
|
+
const fwdIdx = p.lastIndexOf("/");
|
|
14990
|
+
const bckIdx = p.lastIndexOf("\\");
|
|
14991
|
+
const idx = Math.max(fwdIdx, bckIdx);
|
|
14992
|
+
const base = idx === -1 ? p : p.slice(idx + 1);
|
|
14993
|
+
const slug = base.endsWith(stripExt) ? base.slice(0, -stripExt.length) : base;
|
|
14994
|
+
const prior = seen.get(slug);
|
|
14995
|
+
if (prior !== void 0 && prior !== p) {
|
|
14996
|
+
throw new CanonicalNameError(
|
|
14997
|
+
feature,
|
|
14998
|
+
slug,
|
|
14999
|
+
`canonical ${feature} files collide on slug "${slug}": ${prior} vs ${p}. Rename one.`
|
|
15000
|
+
);
|
|
15001
|
+
}
|
|
15002
|
+
seen.set(slug, p);
|
|
15003
|
+
}
|
|
15004
|
+
}
|
|
15005
|
+
|
|
15006
|
+
// src/canonical/features/rules.ts
|
|
14822
15007
|
var VALID_TRIGGERS = ["always_on", "model_decision", "glob", "manual"];
|
|
14823
15008
|
function toStrArray(v) {
|
|
14824
15009
|
if (Array.isArray(v)) return v.filter((x) => typeof x === "string");
|
|
@@ -14838,6 +15023,7 @@ async function parseRules(rulesDir) {
|
|
|
14838
15023
|
if (!content) continue;
|
|
14839
15024
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
14840
15025
|
const name = basename(path, ".md");
|
|
15026
|
+
assertCanonicalName("rule", name);
|
|
14841
15027
|
const rootFromFilename = name === "_root";
|
|
14842
15028
|
const rootFromFm = frontmatter.root === true;
|
|
14843
15029
|
const triggerRaw = frontmatter.trigger;
|
|
@@ -14879,12 +15065,14 @@ function toToolsArray(v) {
|
|
|
14879
15065
|
async function parseCommands(commandsDir) {
|
|
14880
15066
|
const files = await readDirRecursive(commandsDir);
|
|
14881
15067
|
const mdFiles = files.filter((f) => f.endsWith(".md") && !basename(f).startsWith("_"));
|
|
15068
|
+
assertNoBasenameCollisions("command", mdFiles, ".md");
|
|
14882
15069
|
const commands = [];
|
|
14883
15070
|
for (const path of mdFiles) {
|
|
14884
15071
|
const content = await readFileSafe(path);
|
|
14885
15072
|
if (!content) continue;
|
|
14886
15073
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
14887
15074
|
const name = basename(path, ".md");
|
|
15075
|
+
assertCanonicalName("command", name);
|
|
14888
15076
|
const fromCamel = toToolsArray(frontmatter.allowedTools);
|
|
14889
15077
|
const fromKebab = toToolsArray(frontmatter["allowed-tools"]);
|
|
14890
15078
|
const allowedTools = fromCamel.length > 0 ? fromCamel : fromKebab;
|
|
@@ -14932,12 +15120,14 @@ function toHooks(v) {
|
|
|
14932
15120
|
async function parseAgents(agentsDir) {
|
|
14933
15121
|
const files = await readDirRecursive(agentsDir);
|
|
14934
15122
|
const mdFiles = files.filter((f) => f.endsWith(".md") && !basename(f).startsWith("_"));
|
|
15123
|
+
assertNoBasenameCollisions("agent", mdFiles, ".md");
|
|
14935
15124
|
const agents = [];
|
|
14936
15125
|
for (const path of mdFiles) {
|
|
14937
15126
|
const content = await readFileSafe(path);
|
|
14938
15127
|
if (!content) continue;
|
|
14939
15128
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
14940
15129
|
const name = basename(path, ".md");
|
|
15130
|
+
assertCanonicalName("agent", name);
|
|
14941
15131
|
const toolsCamel = toStrArray2(frontmatter.tools);
|
|
14942
15132
|
const toolsKebab = toStrArray2(frontmatter["tools"]);
|
|
14943
15133
|
const tools = toolsCamel.length > 0 ? toolsCamel : toolsKebab;
|
|
@@ -15005,9 +15195,11 @@ async function parseSkillDirectory(skillDir) {
|
|
|
15005
15195
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
15006
15196
|
const supportingFiles = await listSupportingFiles(skillDir);
|
|
15007
15197
|
const fmName = typeof frontmatter.name === "string" ? sanitizeSkillName(frontmatter.name) : "";
|
|
15198
|
+
const name = fmName || basename(skillDir);
|
|
15199
|
+
assertCanonicalName("skill", name);
|
|
15008
15200
|
return {
|
|
15009
15201
|
source: skillPath,
|
|
15010
|
-
name
|
|
15202
|
+
name,
|
|
15011
15203
|
description: typeof frontmatter.description === "string" ? frontmatter.description : "",
|
|
15012
15204
|
body,
|
|
15013
15205
|
supportingFiles
|
|
@@ -15024,6 +15216,7 @@ async function parseSkills(skillsDir) {
|
|
|
15024
15216
|
for (const ent of entries) {
|
|
15025
15217
|
if (!ent.isDirectory()) continue;
|
|
15026
15218
|
if (ent.name.startsWith("_")) continue;
|
|
15219
|
+
assertCanonicalName("skill", ent.name);
|
|
15027
15220
|
const skillDir = join(skillsDir, ent.name);
|
|
15028
15221
|
const skillPath = join(skillDir, SKILL_FILE);
|
|
15029
15222
|
const content = await readFileSafe(skillPath);
|
|
@@ -15842,7 +16035,15 @@ var conversionsSchema = z.object({
|
|
|
15842
16035
|
var pluginEntrySchema = z.object({
|
|
15843
16036
|
id: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
15844
16037
|
source: z.string(),
|
|
15845
|
-
version: z.string().optional()
|
|
16038
|
+
version: z.string().optional(),
|
|
16039
|
+
/**
|
|
16040
|
+
* When true, a failure to import or validate this plugin throws and
|
|
16041
|
+
* aborts the run. Default `false` keeps the lenient behavior of logging
|
|
16042
|
+
* a warning and continuing. Use strict mode in CI when missing
|
|
16043
|
+
* descriptors should fail the build instead of silently shrinking the
|
|
16044
|
+
* generation matrix.
|
|
16045
|
+
*/
|
|
16046
|
+
strict: z.boolean().optional()
|
|
15846
16047
|
}).strict();
|
|
15847
16048
|
var configSchema = z.object({
|
|
15848
16049
|
version: z.literal(1),
|