agentsmesh 0.19.1 → 0.21.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 CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b1efca1: Harden install pipeline against third-party supply-chain attacks.
8
+ - **Strip elevated artifacts from non-local sources by default.** `hooks.yaml`, `permissions.yaml`, and `mcp.json` are now removed from any pack installed from a `github:`, `gitlab:`, or `git+...` source unless you opt in. These three files control your agent's tool settings (shell hooks, granted permissions, MCP launch specs) and a malicious pack shipping any of them could otherwise execute arbitrary local commands the next time the matching event fires. Opt in per-artifact with `--accept-hooks`, `--accept-permissions`, `--accept-mcp`, or all three with `--accept-elevated`. Local sources remain trusted as before.
9
+ - **Skill supporting-file traversal no longer follows symlinks.** A pack containing `skills/foo/keys -> /Users/victim/.ssh` previously pulled external bytes (private keys, etc.) into the canonical skill content. Skill traversal now uses the existing `readDirRecursiveNoSymlinks` helper, mirroring the hardening already applied to install-manifest hashing.
10
+ - **Redact credentials from remote-fetch error output.** `oauth2:<token>@`, `x-access-token:<token>@`, and any other userinfo-bearing URLs are now masked (`https://***@host/...`) in console warnings and thrown error messages so GitHub PATs and GitLab tokens never leak into CI logs, terminal scrollback, or log shippers.
11
+ - **Gate `git+file://` sources behind `AGENTSMESH_ALLOW_LOCAL_GIT=1`.** On shared/multi-tenant hosts a `git+file:///tmp/world-writable-repo` `extends:` clause could silently consume a repo planted by another user; combined with downstream elevated-artifact emission this was a local priv-esc vector. Set `AGENTSMESH_ALLOW_LOCAL_GIT=1` to enable for closed-network development.
12
+ - **Allowlist tar entry types on GitHub tarball extraction.** Previously only `Link` and `SymbolicLink` were rejected (denylist). Now only `File` and `Directory` entries extract; FIFOs, devices, hardlinks, and any future/exotic tar variant are rejected by default.
13
+
14
+ These changes apply to `agentsmesh install`, `agentsmesh refresh`, and any `extends:` resolution against a non-local source. They are behavior changes for users who were silently inheriting hooks/permissions/mcp from a remote pack — re-run with the matching `--accept-*` flag (or `--accept-elevated`) to preserve previous behavior intentionally.
15
+
16
+ ## 0.20.0
17
+
18
+ ### Minor Changes
19
+
20
+ - 6739c63: feat(refresh): add `agentsmesh refresh` to re-fetch and re-apply installed packs
21
+
22
+ A new top-level CLI command and MCP tool for keeping installed packs in
23
+ sync with their declared sources. Branch pins re-resolve to the current
24
+ tip; tag pins re-resolve in case the tag moved; SHA pins stay put. Per-pack
25
+ atomic via the existing `materializePack` swap — a failure or interruption
26
+ leaves the affected pack at its pre-refresh state.
27
+
28
+ ```
29
+ agentsmesh refresh # refresh every installed pack
30
+ agentsmesh refresh my-pack,other-pack # refresh just these
31
+ agentsmesh refresh --dry-run # preview without writing
32
+ agentsmesh refresh --force # skip the drift consent prompt
33
+ agentsmesh refresh --json # JSON output (implies --force)
34
+ agentsmesh refresh --global # global scope
35
+ ```
36
+
37
+ **MCP parity:** new `mcp__agentsmesh__refresh` tool with the same
38
+ `{ names?, dry_run?, global? }` input shape MCP install/uninstall use.
39
+ `force: true` is implicit (no TTY). Errors map to two new codes —
40
+ `REFRESH_RESOLVE_FAILED` and `REFRESH_APPLY_FAILED` — plus the existing
41
+ `LOCK_HELD` and `IO_ERROR`.
42
+
43
+ **Drift handling:** modified pack files trigger a consolidated consent
44
+ prompt with a 5-minute timeout (default no). `--force` bypasses the prompt
45
+ and overwrites local edits. The prompt is collapsed across packs so a bulk
46
+ refresh asks once, not N times.
47
+
48
+ **Schema additions** (`installs.yaml`, all optional, backwards-compatible):
49
+ - `original_ref?: string` — the user's original ref expression (e.g.
50
+ `main`, `v1.2.3`) captured at install time. Used by refresh to
51
+ re-resolve branch pins. Absent on installs predating this release;
52
+ refresh becomes a deterministic no-op for those rows.
53
+ - `refreshed_at?: string` — ISO-8601 timestamp of the last successful
54
+ refresh. Surfaces in `installs list` under the "LAST TOUCHED" column
55
+ (falls back to `installed_at` when absent).
56
+
57
+ **Behavior changes that could affect existing consumers:**
58
+ - `installs.yaml` rows written by this release include `original_ref`.
59
+ Pre-existing rows continue to parse and behave identically.
60
+ - `--json` on `agentsmesh refresh` implies `--force` (CI/MCP can't
61
+ answer the consent prompt). Documented on the website CLI reference.
62
+ - `installs list` column header was "INSTALLED AT", now "LAST TOUCHED",
63
+ showing `refreshed_at` when present and `installed_at` otherwise.
64
+
65
+ **Architecture notes:**
66
+ - `installAsPack` gains an optional `forceFreshMaterialize` flag,
67
+ threaded through five layers (`install-flags → run-install →
68
+ run-install-locked → single-pack-install → run-install-execute →
69
+ installAsPack`). Default is false; install's existing flow is
70
+ untouched. Refresh sets the flag to bypass the
71
+ merge-into-existing-pack branch and force atomic replacement via
72
+ `materializePack`.
73
+ - Source-URL parsing is now shared between install and refresh via the
74
+ new pure `parseSourceUrl` helper (`src/install/source/parse-source-url.ts`).
75
+
76
+ **Refresh does NOT switch refs.** To move a pack to a different ref,
77
+ re-run `agentsmesh install <source>@<new-ref>` — install silently
78
+ overwrites an existing pack of the same name.
79
+
80
+ **Refresh vs `install --sync`** are orthogonal. `--sync` replays missing
81
+ installs from `installs.yaml` (fresh clone). `refresh` updates existing
82
+ installs against their declared sources.
83
+
84
+ Verified end-to-end against 64 community packs from the install
85
+ compatibility log spanning Anthropic skill-packs, marketplaces (`--all`),
86
+ canonical mixed packs, flat collections, root SKILL.md, root CLAUDE.md,
87
+ manual `--path`/`--as`/`--target` combinations, and packs using `pick`
88
+ selectors. Full unit/integration/e2e suite green (7700+ tests).
89
+
3
90
  ## 0.19.1
4
91
 
5
92
  ### Patch Changes
package/README.md CHANGED
@@ -233,8 +233,10 @@ agentsmesh check [--global]
233
233
  agentsmesh merge [--global]
234
234
  agentsmesh matrix [--global] [--targets <csv>] [--verbose]
235
235
  agentsmesh install <source> [--sync] [--path <dir>] [--target <id>] [--as <kind>] [--name <id>] [--extends] [--all] [--dry-run] [--global] [--force]
236
+ [--accept-hooks|--accept-permissions|--accept-mcp|--accept-elevated]
236
237
  agentsmesh uninstall <name>[,<name>...] [--all] [--keep-pack] [--keep-generated] [--dry-run] [--global] [--force]
237
238
  agentsmesh installs list [--global]
239
+ agentsmesh refresh [<name>[,<name>...]] [--dry-run] [--force] [--json] [--global]
238
240
  agentsmesh plugin add|list|remove|info [--version <v>] [--id <id>]
239
241
  agentsmesh target scaffold <id> [--name <displayName>] [--force]
240
242
  ```
@@ -314,6 +316,34 @@ agentsmesh uninstall <name> --dry-run # preview; no writes
314
316
 
315
317
  Each install writes `.agentsmesh-install-manifest.json` next to the pack with per-file sha256 hashes; uninstall compares current contents against that manifest and prompts before deleting locally-modified files. `--force` accepts the documented defaults (bulk = accept all, broken-link = leave-with-warnings, modified = delete-anyway). `.agentsmesh/.install.lock` serialises install/uninstall so concurrent runs on the same project fail fast rather than racing on disk.
316
318
 
319
+ ### Refreshing packs
320
+
321
+ `agentsmesh refresh` re-fetches every installed pack from its recorded source/ref
322
+ and re-applies it. Branch pins (`@main`) advance to the current tip; tag pins
323
+ re-resolve in case the tag moved; SHA pins stay put (re-fetch with the same content).
324
+
325
+ ```bash
326
+ agentsmesh refresh # refresh every installed pack
327
+ agentsmesh refresh my-pack,other-pack # refresh just these
328
+ agentsmesh refresh --dry-run # preview without writing
329
+ agentsmesh refresh --force # skip the drift prompt
330
+ ```
331
+
332
+ Each pack is refreshed atomically — a failure or interruption leaves the
333
+ affected pack at its pre-refresh state. Local edits to pack files trigger
334
+ a consolidated consent prompt (5-minute timeout) unless `--force` is set.
335
+
336
+ **refresh does NOT switch refs.** To move a pack to a different ref, just install
337
+ with the new ref — install silently overwrites an existing pack of the same name:
338
+
339
+ ```bash
340
+ agentsmesh install github:org/repo@v2.0.0
341
+ ```
342
+
343
+ **refresh vs `install --sync`.** `--sync` replays missing installs from
344
+ `installs.yaml` (e.g. after a fresh clone). `refresh` updates existing
345
+ installs against their declared sources. They are orthogonal.
346
+
317
347
  ### What to commit and what to gitignore
318
348
 
319
349
  `agentsmesh init` writes a `.gitignore` that follows the recommended convention. The defaults are deliberate:
@@ -457,7 +487,7 @@ See the [full feature matrix docs](https://samplexbro.github.io/agentsmesh/refer
457
487
 
458
488
  - **[Getting Started](https://samplexbro.github.io/agentsmesh/getting-started/installation/)** — installation, quick start
459
489
  - **[Canonical Config](https://samplexbro.github.io/agentsmesh/canonical-config/)** — rules, commands, agents, skills, MCP, hooks, ignore, permissions
460
- - **[CLI Reference](https://samplexbro.github.io/agentsmesh/cli/)** — `init`, `generate`, `import`, `convert`, `install`, `uninstall`, `installs`, `diff`, `lint`, `watch`, `check`, `merge`, `matrix`, `plugin`, `target`
490
+ - **[CLI Reference](https://samplexbro.github.io/agentsmesh/cli/)** — `init`, `generate`, `import`, `convert`, `install`, `uninstall`, `installs`, `refresh`, `diff`, `lint`, `watch`, `check`, `merge`, `matrix`, `plugin`, `target`
461
491
  - **[Configuration](https://samplexbro.github.io/agentsmesh/configuration/agentsmesh-yaml/)** — `agentsmesh.yaml`, local overrides, extends, collaboration, conversions
462
492
  - **[Guides](https://samplexbro.github.io/agentsmesh/guides/existing-project/)** — adopting in existing projects · multi-tool teams · sharing config · CI drift detection · community packs · **building plugins**
463
493
  - **[Reference](https://samplexbro.github.io/agentsmesh/reference/generation-pipeline/)** — supported tools matrix · generation pipeline · managed embedding
package/dist/canonical.js CHANGED
@@ -185,6 +185,33 @@ async function readDirRecursive(dir, visited, branchSegments) {
185
185
  );
186
186
  }
187
187
  }
188
+ async function readDirRecursiveNoSymlinks(dir, branchSegments) {
189
+ const currentBranchSegments = branchSegments ?? [basename(dir)];
190
+ try {
191
+ const entries = await readdir(dir, { withFileTypes: true });
192
+ const files = [];
193
+ for (const ent of entries) {
194
+ if (ent.isSymbolicLink()) continue;
195
+ const full = join(dir, ent.name);
196
+ if (ent.isDirectory()) {
197
+ const nextSegments = [...currentBranchSegments, ent.name];
198
+ if (shouldSkipRecursiveBranch(nextSegments)) continue;
199
+ files.push(...await readDirRecursiveNoSymlinks(full, nextSegments));
200
+ } else if (ent.isFile()) {
201
+ files.push(full);
202
+ }
203
+ }
204
+ return files;
205
+ } catch (err) {
206
+ const e = err;
207
+ if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "EACCES") return [];
208
+ throw new FileSystemError(
209
+ dir,
210
+ `Failed to read directory ${dir}: ${e.message}. Check permissions.`,
211
+ { cause: err, errnoCode: e.code }
212
+ );
213
+ }
214
+ }
188
215
  var MAX_RECURSIVE_DEPTH, MAX_SEGMENT_REPETITIONS;
189
216
  var init_fs_traverse = __esm({
190
217
  "src/utils/filesystem/fs-traverse.ts"() {
@@ -1023,7 +1050,7 @@ ${legacy}`, "");
1023
1050
  }
1024
1051
  return result.trim();
1025
1052
  }
1026
- var ROOT_INSTRUCTION_BODY_V1, ROOT_INSTRUCTION_BODY_V2, ROOT_INSTRUCTION_BODY_V3, ROOT_INSTRUCTION_BODY_V4, ROOT_INSTRUCTION_BODY_V5, ROOT_INSTRUCTION_BODY_V6, ROOT_INSTRUCTION_BODY_V7, ROOT_INSTRUCTION_BODY, LEGACY_AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH, LEGACY_AGENTSMESH_ROOT_INSTRUCTION_SECTION, AGENTSMESH_CONTRACT_WITH_V1_BODY, AGENTSMESH_CONTRACT_WITH_V2_BODY, AGENTSMESH_CONTRACT_WITH_V3_BODY, AGENTSMESH_CONTRACT_WITH_V4_BODY, AGENTSMESH_CONTRACT_WITH_V5_BODY, AGENTSMESH_CONTRACT_WITH_V6_BODY, AGENTSMESH_CONTRACT_WITH_V7_BODY, AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH, LEGACY_FORMS;
1053
+ var ROOT_INSTRUCTION_BODY_V1, ROOT_INSTRUCTION_BODY_V2, ROOT_INSTRUCTION_BODY_V3, ROOT_INSTRUCTION_BODY_V4, ROOT_INSTRUCTION_BODY_V5, ROOT_INSTRUCTION_BODY_V6, ROOT_INSTRUCTION_BODY_V7, ROOT_INSTRUCTION_BODY_V8, ROOT_INSTRUCTION_BODY, LEGACY_AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH, LEGACY_AGENTSMESH_ROOT_INSTRUCTION_SECTION, AGENTSMESH_CONTRACT_WITH_V1_BODY, AGENTSMESH_CONTRACT_WITH_V2_BODY, AGENTSMESH_CONTRACT_WITH_V3_BODY, AGENTSMESH_CONTRACT_WITH_V4_BODY, AGENTSMESH_CONTRACT_WITH_V5_BODY, AGENTSMESH_CONTRACT_WITH_V6_BODY, AGENTSMESH_CONTRACT_WITH_V7_BODY, AGENTSMESH_CONTRACT_WITH_V8_BODY, AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH, LEGACY_FORMS;
1027
1054
  var init_root_instruction_paragraph = __esm({
1028
1055
  "src/targets/projection/root-instruction-paragraph.ts"() {
1029
1056
  init_managed_blocks();
@@ -1034,7 +1061,8 @@ var init_root_instruction_paragraph = __esm({
1034
1061
  ROOT_INSTRUCTION_BODY_V5 = "Use Claude-style Markdown in `.agentsmesh`: `agents/*.md`, `commands/*.md`, and `skills/*/SKILL.md`; keep rules in `rules/*.md`, hooks in `hooks.yaml`, MCP in `mcp.json`, permissions in `permissions.yaml`, and ignore patterns in `ignore`, then run `agentsmesh generate`.";
1035
1062
  ROOT_INSTRUCTION_BODY_V6 = "Create canonical files in `.agentsmesh`: `rules/_root.md` and `rules/*.md` are Markdown rules; `commands/*.md`, `agents/*.md`, and `skills/*/SKILL.md` plus supporting files use Claude-style frontmatter Markdown; `mcp.json` is MCP JSON; `hooks.yaml` and `permissions.yaml` are YAML; `ignore` is gitignore-style text. Then run `agentsmesh generate`.";
1036
1063
  ROOT_INSTRUCTION_BODY_V7 = "`.agentsmesh` is the only folder you edit or add these files in: `rules/_root.md` and `rules/*.md` are Markdown rules; `commands/*.md`, `agents/*.md`, and `skills/*/SKILL.md` plus supporting files use Claude-style frontmatter Markdown; `mcp.json` is MCP JSON; `hooks.yaml` and `permissions.yaml` are YAML; `ignore` is gitignore-style text. Do not edit generated tool files; run `agentsmesh generate`.";
1037
- ROOT_INSTRUCTION_BODY = "`agentsmesh.yaml` selects targets/features (`agentsmesh.local.yaml` overrides locally), and `.agentsmesh` is the only place to add or edit canonical items: `rules/_root.md`, `rules/*.md`, `commands/*.md`, `agents/*.md`, `skills/*/SKILL.md` plus supporting files, `mcp.json`, `hooks.yaml`, `permissions.yaml`, and `ignore`; if missing run `agentsmesh init`, use `agentsmesh import --from <tool>` for native configs, `agentsmesh install <source>` or `install --sync` for reusable packs, then run `agentsmesh generate`. Use `diff`, `lint`, `check`, `watch`, `matrix`, and `merge` as needed; never edit generated tool files.";
1064
+ ROOT_INSTRUCTION_BODY_V8 = "`agentsmesh.yaml` selects targets/features (`agentsmesh.local.yaml` overrides locally), and `.agentsmesh` is the only place to add or edit canonical items: `rules/_root.md`, `rules/*.md`, `commands/*.md`, `agents/*.md`, `skills/*/SKILL.md` plus supporting files, `mcp.json`, `hooks.yaml`, `permissions.yaml`, and `ignore`; if missing run `agentsmesh init`, use `agentsmesh import --from <tool>` for native configs, `agentsmesh install <source>` or `install --sync` for reusable packs, then run `agentsmesh generate`. Use `diff`, `lint`, `check`, `watch`, `matrix`, and `merge` as needed; never edit generated tool files.";
1065
+ ROOT_INSTRUCTION_BODY = "`agentsmesh.yaml` selects targets/features (`agentsmesh.local.yaml` overrides locally), and `.agentsmesh` is the only place to add or edit canonical items: `rules/_root.md`, `rules/*.md`, `commands/*.md`, `agents/*.md`, `skills/*/SKILL.md` plus supporting files, `mcp.json`, `hooks.yaml`, `permissions.yaml`, and `ignore`; if missing run `agentsmesh init`, use `agentsmesh import --from <tool>` for native configs, `agentsmesh install <source>` or `install --sync` for reusable packs, then run `agentsmesh generate`. Use `diff`, `lint`, `check`, `watch`, `matrix`, `merge`, and `refresh` as needed; never edit generated tool files.";
1038
1066
  LEGACY_AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH = ROOT_INSTRUCTION_BODY_V1;
1039
1067
  LEGACY_AGENTSMESH_ROOT_INSTRUCTION_SECTION = `## Project-Specific Rules
1040
1068
 
@@ -1060,12 +1088,16 @@ ${ROOT_INSTRUCTION_BODY_V6}`;
1060
1088
  AGENTSMESH_CONTRACT_WITH_V7_BODY = `## AgentsMesh Generation Contract
1061
1089
 
1062
1090
  ${ROOT_INSTRUCTION_BODY_V7}`;
1091
+ AGENTSMESH_CONTRACT_WITH_V8_BODY = `## AgentsMesh Generation Contract
1092
+
1093
+ ${ROOT_INSTRUCTION_BODY_V8}`;
1063
1094
  AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH = `${ROOT_CONTRACT_START}
1064
1095
  ## AgentsMesh Generation Contract
1065
1096
 
1066
1097
  ${ROOT_INSTRUCTION_BODY}
1067
1098
  ${ROOT_CONTRACT_END}`;
1068
1099
  LEGACY_FORMS = [
1100
+ AGENTSMESH_CONTRACT_WITH_V8_BODY,
1069
1101
  AGENTSMESH_CONTRACT_WITH_V7_BODY,
1070
1102
  AGENTSMESH_CONTRACT_WITH_V6_BODY,
1071
1103
  AGENTSMESH_CONTRACT_WITH_V5_BODY,
@@ -18726,6 +18758,19 @@ init_fs();
18726
18758
 
18727
18759
  // src/config/remote/git-remote.ts
18728
18760
  init_fs();
18761
+
18762
+ // src/utils/output/redact-url-secrets.ts
18763
+ var URL_WITH_CREDENTIALS = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s"'<>]+)@([^\s"'<>]+)/g;
18764
+ function redactUrlSecrets(message) {
18765
+ return message.replace(
18766
+ URL_WITH_CREDENTIALS,
18767
+ (_full, scheme, _userinfo, rest) => {
18768
+ return `${scheme}***@${rest}`;
18769
+ }
18770
+ );
18771
+ }
18772
+
18773
+ // src/config/remote/git-remote.ts
18729
18774
  var execFileAsync = promisify(execFile);
18730
18775
  var REPO_DIRNAME = "repo";
18731
18776
  function ensureNotFlag(value, kind) {
@@ -18759,12 +18804,13 @@ async function fetchGitRemoteExtend(parsed, extendName, options, cacheDir, build
18759
18804
  await rm(stagedRoot, { recursive: true, force: true });
18760
18805
  const allowFallback = options.allowOfflineFallback !== false;
18761
18806
  if (allowFallback && await hasCachedRepo(cacheRepoDir)) {
18807
+ const rawMsg = err instanceof Error ? err.message : String(err);
18762
18808
  console.warn(
18763
- `[agentsmesh] Remote fetch failed for ${extendName}; using cached version. Error: ${err instanceof Error ? err.message : String(err)}`
18809
+ `[agentsmesh] Remote fetch failed for ${extendName}; using cached version. Error: ${redactUrlSecrets(rawMsg)}`
18764
18810
  );
18765
18811
  return readCachedRepo(cacheRepoDir);
18766
18812
  }
18767
- throw err;
18813
+ throw err instanceof Error ? Object.assign(new Error(redactUrlSecrets(err.message)), { cause: err.cause }) : err;
18768
18814
  }
18769
18815
  }
18770
18816
  async function readCachedRepo(repoDir) {
@@ -18925,13 +18971,14 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
18925
18971
  if (allowFallback && await exists(extractDir)) {
18926
18972
  const topDir2 = await findExtractTopDir(extractDir);
18927
18973
  if (topDir2) {
18974
+ const rawMsg = err instanceof Error ? err.message : String(err);
18928
18975
  console.warn(
18929
- `[agentsmesh] Network failed for ${extendName}; using cached version. Error: ${err instanceof Error ? err.message : String(err)}`
18976
+ `[agentsmesh] Network failed for ${extendName}; using cached version. Error: ${redactUrlSecrets(rawMsg)}`
18930
18977
  );
18931
18978
  return { resolvedPath: join(extractDir, topDir2), version: tag };
18932
18979
  }
18933
18980
  }
18934
- throw err;
18981
+ throw err instanceof Error ? Object.assign(new Error(redactUrlSecrets(err.message)), { cause: err.cause }) : err;
18935
18982
  }
18936
18983
  await rm(extractDir, { recursive: true, force: true });
18937
18984
  await mkdir(extractDir, { recursive: true });
@@ -18942,12 +18989,14 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
18942
18989
  file: tarPath,
18943
18990
  cwd: extractDir,
18944
18991
  strict: true,
18992
+ // Allowlist entry types instead of denylist: only `File` and `Directory`
18993
+ // can be extracted. Hardlinks (`Link`), symlinks (`SymbolicLink`), FIFOs,
18994
+ // character/block devices, and any future/exotic tar entry type are
18995
+ // rejected. A denylist would silently let an unknown variant through.
18945
18996
  filter: (entryPath, entry) => {
18946
18997
  if (isZipSlipPath(entryPath)) return false;
18947
- if (entry && "type" in entry && (entry.type === "Link" || entry.type === "SymbolicLink")) {
18948
- return false;
18949
- }
18950
- return true;
18998
+ const type = entry && "type" in entry ? entry.type : void 0;
18999
+ return type === "File" || type === "Directory";
18951
19000
  }
18952
19001
  });
18953
19002
  } finally {
@@ -19045,8 +19094,11 @@ function parseGitSource(source) {
19045
19094
  return null;
19046
19095
  }
19047
19096
  const allowInsecure = process.env.AGENTSMESH_ALLOW_INSECURE_GIT === "1" || process.env.AGENTSMESH_ALLOW_INSECURE_GIT === "true";
19048
- const allowedProtocols = allowInsecure ? ["https:", "http:", "ssh:", "file:"] : ["https:", "ssh:", "file:"];
19049
- if (!allowedProtocols.includes(parsedUrl.protocol)) {
19097
+ const allowLocalGit = process.env.AGENTSMESH_ALLOW_LOCAL_GIT === "1" || process.env.AGENTSMESH_ALLOW_LOCAL_GIT === "true";
19098
+ const allowed = ["https:", "ssh:"];
19099
+ if (allowInsecure) allowed.push("http:");
19100
+ if (allowLocalGit) allowed.push("file:");
19101
+ if (!allowed.includes(parsedUrl.protocol)) {
19050
19102
  return null;
19051
19103
  }
19052
19104
  return { url, ref };
@@ -19571,6 +19623,7 @@ async function parseAgents(agentsDir, opts = {}) {
19571
19623
 
19572
19624
  // src/canonical/features/skills.ts
19573
19625
  init_fs();
19626
+ init_fs_traverse();
19574
19627
  init_markdown();
19575
19628
  init_boilerplate_filter();
19576
19629
  async function readContent(path) {
@@ -19589,7 +19642,7 @@ function sanitizeSkillName(raw) {
19589
19642
  return raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
19590
19643
  }
19591
19644
  async function listSupportingFiles(skillDir) {
19592
- const files = await readDirRecursive(skillDir);
19645
+ const files = await readDirRecursiveNoSymlinks(skillDir);
19593
19646
  const result = [];
19594
19647
  for (const absPath of files) {
19595
19648
  const raw = absPath.slice(skillDir.length + 1);