agentsmesh 0.20.0 → 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,18 @@
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
+
3
16
  ## 0.20.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -233,6 +233,7 @@ 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]
238
239
  agentsmesh refresh [<name>[,<name>...]] [--dry-run] [--force] [--json] [--global]
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"() {
@@ -18731,6 +18758,19 @@ init_fs();
18731
18758
 
18732
18759
  // src/config/remote/git-remote.ts
18733
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
18734
18774
  var execFileAsync = promisify(execFile);
18735
18775
  var REPO_DIRNAME = "repo";
18736
18776
  function ensureNotFlag(value, kind) {
@@ -18764,12 +18804,13 @@ async function fetchGitRemoteExtend(parsed, extendName, options, cacheDir, build
18764
18804
  await rm(stagedRoot, { recursive: true, force: true });
18765
18805
  const allowFallback = options.allowOfflineFallback !== false;
18766
18806
  if (allowFallback && await hasCachedRepo(cacheRepoDir)) {
18807
+ const rawMsg = err instanceof Error ? err.message : String(err);
18767
18808
  console.warn(
18768
- `[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)}`
18769
18810
  );
18770
18811
  return readCachedRepo(cacheRepoDir);
18771
18812
  }
18772
- throw err;
18813
+ throw err instanceof Error ? Object.assign(new Error(redactUrlSecrets(err.message)), { cause: err.cause }) : err;
18773
18814
  }
18774
18815
  }
18775
18816
  async function readCachedRepo(repoDir) {
@@ -18930,13 +18971,14 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
18930
18971
  if (allowFallback && await exists(extractDir)) {
18931
18972
  const topDir2 = await findExtractTopDir(extractDir);
18932
18973
  if (topDir2) {
18974
+ const rawMsg = err instanceof Error ? err.message : String(err);
18933
18975
  console.warn(
18934
- `[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)}`
18935
18977
  );
18936
18978
  return { resolvedPath: join(extractDir, topDir2), version: tag };
18937
18979
  }
18938
18980
  }
18939
- throw err;
18981
+ throw err instanceof Error ? Object.assign(new Error(redactUrlSecrets(err.message)), { cause: err.cause }) : err;
18940
18982
  }
18941
18983
  await rm(extractDir, { recursive: true, force: true });
18942
18984
  await mkdir(extractDir, { recursive: true });
@@ -18947,12 +18989,14 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
18947
18989
  file: tarPath,
18948
18990
  cwd: extractDir,
18949
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.
18950
18996
  filter: (entryPath, entry) => {
18951
18997
  if (isZipSlipPath(entryPath)) return false;
18952
- if (entry && "type" in entry && (entry.type === "Link" || entry.type === "SymbolicLink")) {
18953
- return false;
18954
- }
18955
- return true;
18998
+ const type = entry && "type" in entry ? entry.type : void 0;
18999
+ return type === "File" || type === "Directory";
18956
19000
  }
18957
19001
  });
18958
19002
  } finally {
@@ -19050,8 +19094,11 @@ function parseGitSource(source) {
19050
19094
  return null;
19051
19095
  }
19052
19096
  const allowInsecure = process.env.AGENTSMESH_ALLOW_INSECURE_GIT === "1" || process.env.AGENTSMESH_ALLOW_INSECURE_GIT === "true";
19053
- const allowedProtocols = allowInsecure ? ["https:", "http:", "ssh:", "file:"] : ["https:", "ssh:", "file:"];
19054
- 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)) {
19055
19102
  return null;
19056
19103
  }
19057
19104
  return { url, ref };
@@ -19576,6 +19623,7 @@ async function parseAgents(agentsDir, opts = {}) {
19576
19623
 
19577
19624
  // src/canonical/features/skills.ts
19578
19625
  init_fs();
19626
+ init_fs_traverse();
19579
19627
  init_markdown();
19580
19628
  init_boilerplate_filter();
19581
19629
  async function readContent(path) {
@@ -19594,7 +19642,7 @@ function sanitizeSkillName(raw) {
19594
19642
  return raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
19595
19643
  }
19596
19644
  async function listSupportingFiles(skillDir) {
19597
- const files = await readDirRecursive(skillDir);
19645
+ const files = await readDirRecursiveNoSymlinks(skillDir);
19598
19646
  const result = [];
19599
19647
  for (const absPath of files) {
19600
19648
  const raw = absPath.slice(skillDir.length + 1);