@agenshield/daemon 0.7.0 → 0.7.2

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/main.js CHANGED
@@ -1044,10 +1044,7 @@ PLISTEOF`);
1044
1044
  }
1045
1045
  async function startOpenClawServices() {
1046
1046
  try {
1047
- try {
1048
- await execAsync3(`sudo launchctl kickstart system/${OPENCLAW_GATEWAY_LABEL}`);
1049
- } catch {
1050
- }
1047
+ await execAsync3(`sudo launchctl kickstart system/${OPENCLAW_GATEWAY_LABEL}`);
1051
1048
  return {
1052
1049
  success: true,
1053
1050
  message: "OpenClaw gateway started"
@@ -1062,10 +1059,7 @@ async function startOpenClawServices() {
1062
1059
  }
1063
1060
  async function stopOpenClawServices() {
1064
1061
  try {
1065
- try {
1066
- await execAsync3(`sudo launchctl kill SIGTERM system/${OPENCLAW_GATEWAY_LABEL}`);
1067
- } catch {
1068
- }
1062
+ await execAsync3(`sudo launchctl kill SIGTERM system/${OPENCLAW_GATEWAY_LABEL}`);
1069
1063
  return {
1070
1064
  success: true,
1071
1065
  message: "OpenClaw gateway stopped"
@@ -1080,10 +1074,7 @@ async function stopOpenClawServices() {
1080
1074
  }
1081
1075
  async function restartOpenClawServices() {
1082
1076
  try {
1083
- try {
1084
- await execAsync3(`sudo launchctl kickstart -k system/${OPENCLAW_GATEWAY_LABEL}`);
1085
- } catch {
1086
- }
1077
+ await execAsync3(`sudo launchctl kickstart -k system/${OPENCLAW_GATEWAY_LABEL}`);
1087
1078
  return {
1088
1079
  success: true,
1089
1080
  message: "OpenClaw gateway restarted"
@@ -1166,15 +1157,29 @@ function getOpenClawStatusSync() {
1166
1157
  async function getOpenClawDashboardUrl() {
1167
1158
  try {
1168
1159
  const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
1169
- const agentUsername = path22.basename(agentHome);
1170
- const { stdout, stderr } = await execAsync3(
1171
- `sudo -H -u ${agentUsername} ${OPENCLAW_LAUNCHER_PATH} dashboard`,
1172
- { timeout: 15e3, maxBuffer: 10 * 1024 * 1024 }
1173
- );
1174
- const url = stdout.trim();
1175
- if (!url) {
1176
- return { success: false, error: stderr.trim() || "No URL returned from openclaw dashboard" };
1160
+ const configPath = path22.join(agentHome, ".openclaw", "openclaw.json");
1161
+ let raw;
1162
+ try {
1163
+ raw = await fs32.readFile(configPath, "utf-8");
1164
+ } catch (err) {
1165
+ if (err.code === "EACCES") {
1166
+ const agentUsername = path22.basename(agentHome);
1167
+ const { stdout } = await execAsync3(
1168
+ `sudo -H -u ${agentUsername} cat "${configPath}"`,
1169
+ { cwd: "/" }
1170
+ );
1171
+ raw = stdout;
1172
+ } else {
1173
+ return { success: false, error: `Cannot read openclaw.json: ${err.message}` };
1174
+ }
1175
+ }
1176
+ const config = JSON.parse(raw);
1177
+ const port = config.gateway?.port;
1178
+ const token = config.gateway?.auth?.token;
1179
+ if (!port || !token) {
1180
+ return { success: false, error: "Gateway port or auth token not found in openclaw.json" };
1177
1181
  }
1182
+ const url = `http://127.0.0.1:${port}/?token=${token}`;
1178
1183
  return { success: true, url };
1179
1184
  } catch (error) {
1180
1185
  return { success: false, error: `Failed to get dashboard URL: ${error.message}` };
@@ -1288,6 +1293,41 @@ var init_crypto = __esm({
1288
1293
  }
1289
1294
  });
1290
1295
 
1296
+ // libs/shield-daemon/src/vault/installation-key.ts
1297
+ import * as crypto2 from "node:crypto";
1298
+ async function getInstallationKey() {
1299
+ if (cachedKey) return cachedKey;
1300
+ const vault = getVault();
1301
+ const contents = await vault.load();
1302
+ if (contents.installationKey) {
1303
+ cachedKey = contents.installationKey;
1304
+ return cachedKey;
1305
+ }
1306
+ const key = crypto2.randomBytes(32).toString("hex");
1307
+ await vault.set("installationKey", key);
1308
+ cachedKey = key;
1309
+ console.log("[InstallationKey] Generated new installation key");
1310
+ return key;
1311
+ }
1312
+ async function getInstallationTag() {
1313
+ const key = await getInstallationKey();
1314
+ return `${INSTALLATION_KEY_PREFIX}${key}`;
1315
+ }
1316
+ function hasValidInstallationTagSync(tags) {
1317
+ if (!cachedKey) return false;
1318
+ const fullTag = `${INSTALLATION_KEY_PREFIX}${cachedKey}`;
1319
+ return tags.some((tag) => tag === fullTag);
1320
+ }
1321
+ var INSTALLATION_KEY_PREFIX, cachedKey;
1322
+ var init_installation_key = __esm({
1323
+ "libs/shield-daemon/src/vault/installation-key.ts"() {
1324
+ "use strict";
1325
+ init_vault();
1326
+ INSTALLATION_KEY_PREFIX = "agenshield-";
1327
+ cachedKey = null;
1328
+ }
1329
+ });
1330
+
1291
1331
  // libs/shield-daemon/src/vault/index.ts
1292
1332
  import * as fs4 from "node:fs";
1293
1333
  import * as path4 from "node:path";
@@ -1305,6 +1345,7 @@ var init_vault = __esm({
1305
1345
  init_crypto();
1306
1346
  init_paths();
1307
1347
  init_crypto();
1348
+ init_installation_key();
1308
1349
  Vault = class {
1309
1350
  key;
1310
1351
  vaultPath;
@@ -1761,7 +1802,7 @@ var init_secret_sync = __esm({
1761
1802
  });
1762
1803
 
1763
1804
  // libs/shield-daemon/src/main.ts
1764
- import * as fs23 from "node:fs";
1805
+ import * as fs24 from "node:fs";
1765
1806
 
1766
1807
  // libs/shield-daemon/src/config/index.ts
1767
1808
  init_paths();
@@ -1769,7 +1810,7 @@ init_defaults();
1769
1810
  init_loader();
1770
1811
 
1771
1812
  // libs/shield-daemon/src/server.ts
1772
- import * as fs21 from "node:fs";
1813
+ import * as fs23 from "node:fs";
1773
1814
  import Fastify from "fastify";
1774
1815
  import cors from "@fastify/cors";
1775
1816
  import fastifyStatic from "@fastify/static";
@@ -1842,7 +1883,7 @@ init_state();
1842
1883
  init_vault();
1843
1884
 
1844
1885
  // libs/shield-daemon/src/auth/session.ts
1845
- import * as crypto2 from "node:crypto";
1886
+ import * as crypto3 from "node:crypto";
1846
1887
  import { DEFAULT_AUTH_CONFIG } from "@agenshield/ipc";
1847
1888
  var SessionManager = class {
1848
1889
  sessions = /* @__PURE__ */ new Map();
@@ -1856,7 +1897,7 @@ var SessionManager = class {
1856
1897
  * Generate a secure random token
1857
1898
  */
1858
1899
  generateToken() {
1859
- return crypto2.randomBytes(32).toString("base64url");
1900
+ return crypto3.randomBytes(32).toString("base64url");
1860
1901
  }
1861
1902
  /**
1862
1903
  * Create a new session
@@ -2141,11 +2182,24 @@ function getOpenClawConfigPath() {
2141
2182
  const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
2142
2183
  return path8.join(agentHome, ".openclaw", "openclaw.json");
2143
2184
  }
2144
- function readConfig() {
2185
+ function readOpenClawConfig() {
2145
2186
  const configPath = getOpenClawConfigPath();
2146
2187
  try {
2147
2188
  if (fs8.existsSync(configPath)) {
2148
- return JSON.parse(fs8.readFileSync(configPath, "utf-8"));
2189
+ try {
2190
+ return JSON.parse(fs8.readFileSync(configPath, "utf-8"));
2191
+ } catch (err) {
2192
+ if (err.code === "EACCES") {
2193
+ const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
2194
+ const agentUsername = path8.basename(agentHome);
2195
+ const raw = execSync6(
2196
+ `sudo -H -u ${agentUsername} cat "${configPath}"`,
2197
+ { encoding: "utf-8", cwd: "/", stdio: ["pipe", "pipe", "pipe"] }
2198
+ );
2199
+ return JSON.parse(raw);
2200
+ }
2201
+ throw err;
2202
+ }
2149
2203
  }
2150
2204
  } catch {
2151
2205
  console.warn("[OpenClawConfig] Failed to read openclaw.json, starting fresh");
@@ -2163,7 +2217,7 @@ function writeConfig(config) {
2163
2217
  const agentUsername = path8.basename(agentHome);
2164
2218
  execSync6(
2165
2219
  `sudo -H -u ${agentUsername} tee "${configPath}" > /dev/null`,
2166
- { input: JSON.stringify(config, null, 2), stdio: ["pipe", "pipe", "pipe"] }
2220
+ { input: JSON.stringify(config, null, 2), stdio: ["pipe", "pipe", "pipe"], cwd: "/" }
2167
2221
  );
2168
2222
  } else {
2169
2223
  throw err;
@@ -2171,27 +2225,30 @@ function writeConfig(config) {
2171
2225
  }
2172
2226
  }
2173
2227
  function addSkillEntry(slug) {
2174
- const config = readConfig();
2228
+ const config = readOpenClawConfig();
2175
2229
  if (!config.skills) {
2176
2230
  config.skills = {};
2177
2231
  }
2178
2232
  if (!config.skills.entries) {
2179
2233
  config.skills.entries = {};
2180
2234
  }
2181
- config.skills.entries[slug] = { enabled: true };
2235
+ const existing = config.skills.entries[slug] ?? {};
2236
+ config.skills.entries[slug] = { ...existing, enabled: true };
2182
2237
  writeConfig(config);
2183
2238
  console.log(`[OpenClawConfig] Added skill entry: ${slug}`);
2184
2239
  }
2185
2240
  function removeSkillEntry(slug) {
2186
- const config = readConfig();
2241
+ const config = readOpenClawConfig();
2187
2242
  if (config.skills?.entries?.[slug]) {
2188
- delete config.skills.entries[slug];
2243
+ const existing = config.skills.entries[slug];
2244
+ delete existing.env;
2245
+ config.skills.entries[slug] = { ...existing, enabled: false };
2189
2246
  writeConfig(config);
2190
- console.log(`[OpenClawConfig] Removed skill entry: ${slug}`);
2247
+ console.log(`[OpenClawConfig] Disabled skill entry: ${slug}`);
2191
2248
  }
2192
2249
  }
2193
2250
  function syncOpenClawFromPolicies(policies) {
2194
- const config = readConfig();
2251
+ const config = readOpenClawConfig();
2195
2252
  if (!config.skills) config.skills = {};
2196
2253
  const allowBundled = [];
2197
2254
  for (const p of policies) {
@@ -2410,7 +2467,7 @@ function generatePolicyMarkdown(policies, knownSkills) {
2410
2467
  // libs/shield-daemon/src/watchers/skills.ts
2411
2468
  import * as fs11 from "node:fs";
2412
2469
  import * as path11 from "node:path";
2413
- import * as crypto3 from "node:crypto";
2470
+ import * as crypto4 from "node:crypto";
2414
2471
  import { parseSkillMd } from "@agenshield/sandbox";
2415
2472
 
2416
2473
  // libs/shield-daemon/src/services/marketplace.ts
@@ -2439,35 +2496,35 @@ var ANALYSIS_TIMEOUT = 4 * 6e4;
2439
2496
  var SEARCH_CACHE_TTL = 6e4;
2440
2497
  var DETAIL_CACHE_TTL = 5 * 6e4;
2441
2498
  var SHORT_TIMEOUT = 1e4;
2442
- async function convexAction(path21, args, timeout) {
2499
+ async function convexAction(path24, args, timeout) {
2443
2500
  const res = await fetch(`${CONVEX_BASE}/api/action`, {
2444
2501
  method: "POST",
2445
2502
  signal: AbortSignal.timeout(timeout),
2446
2503
  headers: { "Content-Type": "application/json" },
2447
- body: JSON.stringify({ path: path21, args, format: "json" })
2504
+ body: JSON.stringify({ path: path24, args, format: "json" })
2448
2505
  });
2449
2506
  if (!res.ok) {
2450
- throw new Error(`Convex action ${path21} returned ${res.status}`);
2507
+ throw new Error(`Convex action ${path24} returned ${res.status}`);
2451
2508
  }
2452
2509
  const body = await res.json();
2453
2510
  if (body.status === "error") {
2454
- throw new Error(`Convex action ${path21}: ${body.errorMessage ?? "unknown error"}`);
2511
+ throw new Error(`Convex action ${path24}: ${body.errorMessage ?? "unknown error"}`);
2455
2512
  }
2456
2513
  return body.value;
2457
2514
  }
2458
- async function convexQuery(path21, args, timeout) {
2515
+ async function convexQuery(path24, args, timeout) {
2459
2516
  const res = await fetch(`${CONVEX_BASE}/api/query`, {
2460
2517
  method: "POST",
2461
2518
  signal: AbortSignal.timeout(timeout),
2462
2519
  headers: { "Content-Type": "application/json" },
2463
- body: JSON.stringify({ path: path21, args, format: "json" })
2520
+ body: JSON.stringify({ path: path24, args, format: "json" })
2464
2521
  });
2465
2522
  if (!res.ok) {
2466
- throw new Error(`Convex query ${path21} returned ${res.status}`);
2523
+ throw new Error(`Convex query ${path24} returned ${res.status}`);
2467
2524
  }
2468
2525
  const body = await res.json();
2469
2526
  if (body.status === "error") {
2470
- throw new Error(`Convex query ${path21}: ${body.errorMessage ?? "unknown error"}`);
2527
+ throw new Error(`Convex query ${path24}: ${body.errorMessage ?? "unknown error"}`);
2471
2528
  }
2472
2529
  return body.value;
2473
2530
  }
@@ -2599,6 +2656,17 @@ function updateDownloadedAnalysis(slug, analysis) {
2599
2656
  } catch {
2600
2657
  }
2601
2658
  }
2659
+ function markDownloadedAsInstalled(slug) {
2660
+ const metaPath = path9.join(getMarketplaceDir(), slug, "metadata.json");
2661
+ try {
2662
+ if (!fs9.existsSync(metaPath)) return;
2663
+ const meta = JSON.parse(fs9.readFileSync(metaPath, "utf-8"));
2664
+ if (meta.wasInstalled) return;
2665
+ meta.wasInstalled = true;
2666
+ fs9.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
2667
+ } catch {
2668
+ }
2669
+ }
2602
2670
  function listDownloadedSkills() {
2603
2671
  const baseDir = getMarketplaceDir();
2604
2672
  if (!fs9.existsSync(baseDir)) return [];
@@ -2619,7 +2687,8 @@ function listDownloadedSkills() {
2619
2687
  tags: meta.tags ?? [],
2620
2688
  hasAnalysis: !!meta.analysis,
2621
2689
  source: meta.source,
2622
- analysis: meta.analysis
2690
+ analysis: meta.analysis,
2691
+ wasInstalled: meta.wasInstalled ?? false
2623
2692
  });
2624
2693
  } catch {
2625
2694
  }
@@ -2669,9 +2738,9 @@ function inlineImagesInMarkdown(markdown, files) {
2669
2738
  const mime = isImageExt(file.name);
2670
2739
  if (mime && file.content.startsWith("data:")) {
2671
2740
  imageMap.set(file.name, file.content);
2672
- const basename3 = file.name.split("/").pop() ?? "";
2673
- if (basename3 && !imageMap.has(basename3)) {
2674
- imageMap.set(basename3, file.content);
2741
+ const basename6 = file.name.split("/").pop() ?? "";
2742
+ if (basename6 && !imageMap.has(basename6)) {
2743
+ imageMap.set(basename6, file.content);
2675
2744
  }
2676
2745
  }
2677
2746
  }
@@ -3197,10 +3266,10 @@ function emitSecurityWarning(warning) {
3197
3266
  function emitSecurityCritical(issue) {
3198
3267
  daemonEvents.broadcast("security:critical", { message: issue });
3199
3268
  }
3200
- function emitApiRequest(method, path21, statusCode, duration, requestBody, responseBody) {
3269
+ function emitApiRequest(method, path24, statusCode, duration, requestBody, responseBody) {
3201
3270
  daemonEvents.broadcast("api:request", {
3202
3271
  method,
3203
- path: path21,
3272
+ path: path24,
3204
3273
  statusCode,
3205
3274
  duration,
3206
3275
  ...requestBody !== void 0 && { requestBody },
@@ -3216,6 +3285,9 @@ function emitSkillUntrustedDetected(name, reason) {
3216
3285
  function emitSkillApproved(skillName) {
3217
3286
  daemonEvents.broadcast("skills:approved", { name: skillName });
3218
3287
  }
3288
+ function emitExecDenied(command, reason) {
3289
+ daemonEvents.broadcast("exec:denied", { command, reason });
3290
+ }
3219
3291
  function emitAgenCoAuthRequired(authUrl, integration) {
3220
3292
  daemonEvents.broadcast("agenco:auth_required", { authUrl, integration });
3221
3293
  }
@@ -3255,6 +3327,56 @@ function emitEvent(type, data) {
3255
3327
 
3256
3328
  // libs/shield-daemon/src/watchers/skills.ts
3257
3329
  init_paths();
3330
+
3331
+ // libs/shield-daemon/src/services/skill-tag-injector.ts
3332
+ init_installation_key();
3333
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
3334
+ var FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
3335
+ async function injectInstallationTag(content) {
3336
+ const tag = await getInstallationTag();
3337
+ const match = content.match(FRONTMATTER_RE);
3338
+ if (!match) {
3339
+ return `---
3340
+ tags:
3341
+ - ${tag}
3342
+ ---
3343
+ ${content}`;
3344
+ }
3345
+ try {
3346
+ const metadata = parseYaml(match[1]);
3347
+ if (!metadata || typeof metadata !== "object") {
3348
+ return content;
3349
+ }
3350
+ if (!Array.isArray(metadata.tags)) {
3351
+ metadata.tags = [];
3352
+ }
3353
+ metadata.tags = metadata.tags.filter(
3354
+ (t) => typeof t !== "string" || !t.startsWith("agenshield-")
3355
+ );
3356
+ metadata.tags.push(tag);
3357
+ return `---
3358
+ ${stringifyYaml(metadata).trimEnd()}
3359
+ ---
3360
+ ${match[2]}`;
3361
+ } catch {
3362
+ return content;
3363
+ }
3364
+ }
3365
+ function extractTagsFromSkillMd(content) {
3366
+ const match = content.match(FRONTMATTER_RE);
3367
+ if (!match) return [];
3368
+ try {
3369
+ const metadata = parseYaml(match[1]);
3370
+ if (!metadata || typeof metadata !== "object") return [];
3371
+ if (!Array.isArray(metadata.tags)) return [];
3372
+ return metadata.tags.filter((t) => typeof t === "string");
3373
+ } catch {
3374
+ return [];
3375
+ }
3376
+ }
3377
+
3378
+ // libs/shield-daemon/src/watchers/skills.ts
3379
+ init_installation_key();
3258
3380
  function getApprovedSkillsPath() {
3259
3381
  return path11.join(getSystemConfigDir(), "approved-skills.json");
3260
3382
  }
@@ -3393,7 +3515,7 @@ function computeSkillHash(skillDir) {
3393
3515
  const files = readSkillFiles(skillDir);
3394
3516
  if (files.length === 0) return null;
3395
3517
  files.sort((a, b) => a.name.localeCompare(b.name));
3396
- const hash = crypto3.createHash("sha256");
3518
+ const hash = crypto4.createHash("sha256");
3397
3519
  for (const file of files) {
3398
3520
  hash.update(file.name);
3399
3521
  hash.update(file.content);
@@ -3422,10 +3544,33 @@ function scanSkills() {
3422
3544
  const approvedEntry = approvedMap.get(skillName);
3423
3545
  if (!approvedEntry) {
3424
3546
  const fullPath = path11.join(skillsDir, skillName);
3425
- const slug = moveToMarketplace(skillName, fullPath);
3426
- if (slug) {
3427
- if (callbacks.onUntrustedDetected) {
3428
- callbacks.onUntrustedDetected({ name: skillName, reason: "Skill not in approved list" });
3547
+ let autoApproved = false;
3548
+ for (const mdName of ["SKILL.md", "skill.md"]) {
3549
+ const mdPath = path11.join(fullPath, mdName);
3550
+ try {
3551
+ if (fs11.existsSync(mdPath)) {
3552
+ const content = fs11.readFileSync(mdPath, "utf-8");
3553
+ const tags = extractTagsFromSkillMd(content);
3554
+ if (hasValidInstallationTagSync(tags)) {
3555
+ console.log(`[SkillsWatcher] Auto-approving skill with valid installation tag: ${skillName}`);
3556
+ const hash = computeSkillHash(fullPath);
3557
+ addToApprovedList(skillName, void 0, hash ?? void 0);
3558
+ if (callbacks.onApproved) {
3559
+ callbacks.onApproved(skillName);
3560
+ }
3561
+ autoApproved = true;
3562
+ break;
3563
+ }
3564
+ }
3565
+ } catch {
3566
+ }
3567
+ }
3568
+ if (!autoApproved) {
3569
+ const slug = moveToMarketplace(skillName, fullPath);
3570
+ if (slug) {
3571
+ if (callbacks.onUntrustedDetected) {
3572
+ callbacks.onUntrustedDetected({ name: skillName, reason: "Skill not in approved list" });
3573
+ }
3429
3574
  }
3430
3575
  }
3431
3576
  } else if (approvedEntry.hash) {
@@ -3455,26 +3600,16 @@ function scanSkills() {
3455
3600
  }
3456
3601
  function detectOpenClawMismatches() {
3457
3602
  try {
3458
- const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
3459
- const configPath = path11.join(agentHome, ".openclaw", "openclaw.json");
3460
- if (!fs11.existsSync(configPath)) return;
3461
- const raw = fs11.readFileSync(configPath, "utf-8");
3462
- const config = JSON.parse(raw);
3463
- if (!config.skills?.entries) return;
3464
3603
  const approved = loadApprovedSkills();
3465
3604
  const approvedNames = new Set(approved.map((a) => a.name));
3466
- const entries = Object.keys(config.skills.entries);
3467
- let changed = false;
3468
- for (const name of entries) {
3605
+ const config = readOpenClawConfig();
3606
+ if (!config.skills?.entries) return;
3607
+ for (const name of Object.keys(config.skills.entries)) {
3469
3608
  if (!approvedNames.has(name)) {
3470
- delete config.skills.entries[name];
3471
- changed = true;
3472
- console.log(`[SkillsWatcher] Removed stale openclaw.json entry: ${name}`);
3609
+ removeSkillEntry(name);
3610
+ console.log(`[SkillsWatcher] Disabled stale openclaw.json entry: ${name}`);
3473
3611
  }
3474
3612
  }
3475
- if (changed) {
3476
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
3477
- }
3478
3613
  } catch {
3479
3614
  }
3480
3615
  }
@@ -3541,7 +3676,7 @@ function approveSkill(skillName) {
3541
3676
  const cachedFiles = getDownloadedSkillFiles(slug);
3542
3677
  let hash;
3543
3678
  if (cachedFiles.length > 0) {
3544
- const h = crypto3.createHash("sha256");
3679
+ const h = crypto4.createHash("sha256");
3545
3680
  const sorted = [...cachedFiles].sort((a, b) => a.name.localeCompare(b.name));
3546
3681
  for (const file of sorted) {
3547
3682
  h.update(file.name);
@@ -3610,14 +3745,15 @@ function listApproved() {
3610
3745
  function getSkillsDir() {
3611
3746
  return skillsDir;
3612
3747
  }
3613
- function addToApprovedList(skillName, publisher, hash) {
3748
+ function addToApprovedList(skillName, publisher, hash, slug) {
3614
3749
  const approved = loadApprovedSkills();
3615
3750
  if (!approved.some((s) => s.name === skillName)) {
3616
3751
  approved.push({
3617
3752
  name: skillName,
3618
3753
  approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
3619
3754
  ...publisher ? { publisher } : {},
3620
- ...hash ? { hash } : {}
3755
+ ...hash ? { hash } : {},
3756
+ ...slug ? { slug } : {}
3621
3757
  });
3622
3758
  saveApprovedSkills(approved);
3623
3759
  }
@@ -3864,7 +4000,7 @@ async function securityRoutes(app) {
3864
4000
  // libs/shield-daemon/src/auth/passcode.ts
3865
4001
  init_vault();
3866
4002
  init_state();
3867
- import * as crypto4 from "node:crypto";
4003
+ import * as crypto5 from "node:crypto";
3868
4004
  import { DEFAULT_AUTH_CONFIG as DEFAULT_AUTH_CONFIG2 } from "@agenshield/ipc";
3869
4005
  var ITERATIONS = 1e5;
3870
4006
  var KEY_LENGTH = 64;
@@ -3872,8 +4008,8 @@ var DIGEST = "sha512";
3872
4008
  var SALT_LENGTH = 16;
3873
4009
  function hashPasscode(passcode) {
3874
4010
  return new Promise((resolve3, reject) => {
3875
- const salt = crypto4.randomBytes(SALT_LENGTH);
3876
- crypto4.pbkdf2(passcode, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
4011
+ const salt = crypto5.randomBytes(SALT_LENGTH);
4012
+ crypto5.pbkdf2(passcode, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
3877
4013
  if (err) {
3878
4014
  reject(err);
3879
4015
  return;
@@ -3893,12 +4029,12 @@ function verifyPasscode(passcode, storedHash) {
3893
4029
  const iterations = parseInt(parts[0], 10);
3894
4030
  const salt = Buffer.from(parts[1], "base64");
3895
4031
  const hash = Buffer.from(parts[2], "base64");
3896
- crypto4.pbkdf2(passcode, salt, iterations, hash.length, DIGEST, (err, derivedKey) => {
4032
+ crypto5.pbkdf2(passcode, salt, iterations, hash.length, DIGEST, (err, derivedKey) => {
3897
4033
  if (err) {
3898
4034
  reject(err);
3899
4035
  return;
3900
4036
  }
3901
- resolve3(crypto4.timingSafeEqual(hash, derivedKey));
4037
+ resolve3(crypto5.timingSafeEqual(hash, derivedKey));
3902
4038
  });
3903
4039
  });
3904
4040
  }
@@ -4030,16 +4166,16 @@ var PROTECTED_ROUTES = [
4030
4166
  { method: "POST", path: "/api/skills/install" },
4031
4167
  { method: "GET", path: "/api/openclaw/dashboard-url" }
4032
4168
  ];
4033
- function isProtectedRoute(method, path21) {
4169
+ function isProtectedRoute(method, path24) {
4034
4170
  return PROTECTED_ROUTES.some(
4035
- (route) => route.method === method && path21.startsWith(route.path)
4171
+ (route) => route.method === method && path24.startsWith(route.path)
4036
4172
  );
4037
4173
  }
4038
4174
  function createAuthHook() {
4039
4175
  return async (request2, reply) => {
4040
4176
  const method = request2.method;
4041
- const path21 = request2.url.split("?")[0];
4042
- if (!isProtectedRoute(method, path21)) {
4177
+ const path24 = request2.url.split("?")[0];
4178
+ if (!isProtectedRoute(method, path24)) {
4043
4179
  return;
4044
4180
  }
4045
4181
  if (!isAuthenticated(request2)) {
@@ -4070,6 +4206,12 @@ data: ${data}
4070
4206
 
4071
4207
  `;
4072
4208
  }
4209
+ var ALWAYS_FULL_PREFIXES = ["skills:", "exec:", "interceptor:", "security:", "wrappers:", "process:", "config:"];
4210
+ function shouldSendFull(event, authenticated) {
4211
+ if (authenticated) return true;
4212
+ if (event.type === "heartbeat" || event.type === "daemon:status") return true;
4213
+ return ALWAYS_FULL_PREFIXES.some((p) => event.type.startsWith(p));
4214
+ }
4073
4215
  async function sseRoutes(app) {
4074
4216
  app.get("/sse/events", async (request2, reply) => {
4075
4217
  const authenticated = isAuthenticated(request2);
@@ -4095,7 +4237,7 @@ async function sseRoutes(app) {
4095
4237
  reply.raw.write(authenticated ? formatSSE(statusEvent) : formatStrippedSSE(statusEvent));
4096
4238
  const unsubscribe = daemonEvents.subscribe((event) => {
4097
4239
  try {
4098
- if (event.type === "heartbeat" || event.type === "daemon:status" || event.type.startsWith("skills:") || authenticated) {
4240
+ if (shouldSendFull(event, authenticated)) {
4099
4241
  reply.raw.write(formatSSE(event));
4100
4242
  } else {
4101
4243
  reply.raw.write(formatStrippedSSE(event));
@@ -4143,7 +4285,7 @@ async function sseRoutes(app) {
4143
4285
  const unsubscribe = daemonEvents.subscribe((event) => {
4144
4286
  if (event.type.startsWith(filter) || event.type === "heartbeat" || event.type === "daemon:status") {
4145
4287
  try {
4146
- if (event.type === "heartbeat" || event.type === "daemon:status" || authenticated) {
4288
+ if (shouldSendFull(event, authenticated)) {
4147
4289
  reply.raw.write(formatSSE(event));
4148
4290
  } else {
4149
4291
  reply.raw.write(formatStrippedSSE(event));
@@ -7538,19 +7680,50 @@ async function agencoRoutes(app) {
7538
7680
  }
7539
7681
 
7540
7682
  // libs/shield-daemon/src/routes/skills.ts
7541
- import * as fs16 from "node:fs";
7542
- import * as path16 from "node:path";
7683
+ import * as fs17 from "node:fs";
7684
+ import * as path17 from "node:path";
7543
7685
  import { execSync as execSync9 } from "node:child_process";
7544
- import { parseSkillMd as parseSkillMd2 } from "@agenshield/sandbox";
7686
+ import { parseSkillMd as parseSkillMd3, stripEnvFromSkillMd as stripEnvFromSkillMd2 } from "@agenshield/sandbox";
7545
7687
 
7546
7688
  // libs/shield-daemon/src/services/skill-lifecycle.ts
7547
7689
  import * as fs14 from "node:fs";
7548
7690
  import * as path14 from "node:path";
7549
7691
  import { execSync as execSync8 } from "node:child_process";
7550
- function createSkillWrapper(name, binDir) {
7551
- if (!fs14.existsSync(binDir)) {
7552
- fs14.mkdirSync(binDir, { recursive: true });
7692
+ function sudoMkdir(dir, agentUsername) {
7693
+ try {
7694
+ fs14.mkdirSync(dir, { recursive: true });
7695
+ } catch (err) {
7696
+ if (err.code === "EACCES") {
7697
+ execSync8(`sudo -H -u ${agentUsername} /bin/mkdir -p "${dir}"`, { cwd: "/", stdio: "pipe" });
7698
+ } else {
7699
+ throw err;
7700
+ }
7701
+ }
7702
+ }
7703
+ function sudoWriteFile(filePath, content, agentUsername, mode) {
7704
+ try {
7705
+ fs14.writeFileSync(filePath, content, { mode });
7706
+ } catch (err) {
7707
+ if (err.code === "EACCES") {
7708
+ execSync8(
7709
+ `sudo -H -u ${agentUsername} tee "${filePath}" > /dev/null`,
7710
+ { input: content, cwd: "/", stdio: ["pipe", "pipe", "pipe"] }
7711
+ );
7712
+ if (mode) {
7713
+ try {
7714
+ execSync8(`sudo -H -u ${agentUsername} chmod ${mode.toString(8)} "${filePath}"`, { cwd: "/", stdio: "pipe" });
7715
+ } catch {
7716
+ }
7717
+ }
7718
+ } else {
7719
+ throw err;
7720
+ }
7553
7721
  }
7722
+ }
7723
+ function createSkillWrapper(name, binDir) {
7724
+ const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
7725
+ const agentUsername = path14.basename(agentHome);
7726
+ sudoMkdir(binDir, agentUsername);
7554
7727
  const wrapperPath = path14.join(binDir, name);
7555
7728
  const wrapperContent = `#!/bin/bash
7556
7729
  # ${name} skill wrapper - policy-enforced execution
@@ -7558,7 +7731,7 @@ function createSkillWrapper(name, binDir) {
7558
7731
  if ! /bin/pwd > /dev/null 2>&1; then cd ~ 2>/dev/null || cd /; fi
7559
7732
  exec /opt/agenshield/bin/shield-client skill run "${name}" "$@"
7560
7733
  `;
7561
- fs14.writeFileSync(wrapperPath, wrapperContent, { mode: 493 });
7734
+ sudoWriteFile(wrapperPath, wrapperContent, agentUsername, 493);
7562
7735
  const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "ash_default";
7563
7736
  try {
7564
7737
  execSync8(`chown root:${socketGroup} "${wrapperPath}"`, { stdio: "pipe" });
@@ -7603,8 +7776,8 @@ function removeSkillPolicy(name) {
7603
7776
  }
7604
7777
 
7605
7778
  // libs/shield-daemon/src/routes/marketplace.ts
7606
- import * as fs15 from "node:fs";
7607
- import * as path15 from "node:path";
7779
+ import * as fs16 from "node:fs";
7780
+ import * as path16 from "node:path";
7608
7781
 
7609
7782
  // libs/shield-broker/dist/index.js
7610
7783
  import { exec as exec4 } from "node:child_process";
@@ -7880,6 +8053,176 @@ async function uninstallSkillViaBroker(slug, options = {}) {
7880
8053
  return result;
7881
8054
  }
7882
8055
 
8056
+ // libs/shield-daemon/src/routes/marketplace.ts
8057
+ import { stripEnvFromSkillMd } from "@agenshield/sandbox";
8058
+
8059
+ // libs/shield-daemon/src/services/skill-deps.ts
8060
+ import * as fs15 from "node:fs";
8061
+ import * as path15 from "node:path";
8062
+ import { parseSkillMd as parseSkillMd2, extractSkillInfo } from "@agenshield/sandbox";
8063
+ import { execWithProgress as execWithProgress3 } from "@agenshield/sandbox";
8064
+ var SUPPORTED_KINDS = /* @__PURE__ */ new Set(["brew", "npm", "pip"]);
8065
+ var SAFE_PACKAGE_RE = /^[a-zA-Z0-9@/_.\-]+$/;
8066
+ var SYSTEM_PATH = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
8067
+ function findSkillMdRecursive(dir, depth = 0) {
8068
+ if (depth > 3) return null;
8069
+ try {
8070
+ for (const name of ["SKILL.md", "skill.md", "README.md", "readme.md"]) {
8071
+ const candidate = path15.join(dir, name);
8072
+ if (fs15.existsSync(candidate)) return candidate;
8073
+ }
8074
+ const entries = fs15.readdirSync(dir, { withFileTypes: true });
8075
+ for (const entry of entries) {
8076
+ if (!entry.isDirectory()) continue;
8077
+ const found = findSkillMdRecursive(path15.join(dir, entry.name), depth + 1);
8078
+ if (found) return found;
8079
+ }
8080
+ } catch {
8081
+ }
8082
+ return null;
8083
+ }
8084
+ async function executeSkillInstallSteps(options) {
8085
+ const { slug, skillDir, agentHome, agentUsername, onLog } = options;
8086
+ const installed = [];
8087
+ const errors = [];
8088
+ const skillMdPath = findSkillMdRecursive(skillDir);
8089
+ if (!skillMdPath) {
8090
+ return { success: true, installed, errors };
8091
+ }
8092
+ let content;
8093
+ try {
8094
+ content = fs15.readFileSync(skillMdPath, "utf-8");
8095
+ } catch {
8096
+ return { success: true, installed, errors };
8097
+ }
8098
+ const parsed = parseSkillMd2(content);
8099
+ if (!parsed) {
8100
+ return { success: true, installed, errors };
8101
+ }
8102
+ const info = extractSkillInfo(parsed.metadata);
8103
+ const installSteps = info.installSteps;
8104
+ if (!Array.isArray(installSteps) || installSteps.length === 0) {
8105
+ return { success: true, installed, errors };
8106
+ }
8107
+ onLog(`Found ${installSteps.length} dependency install step(s) for ${slug}`);
8108
+ for (const step of installSteps) {
8109
+ const kind = step.kind;
8110
+ const stepId = step.id || kind;
8111
+ if (!SUPPORTED_KINDS.has(kind)) {
8112
+ errors.push(`Unsupported install kind "${kind}" (step: ${stepId})`);
8113
+ continue;
8114
+ }
8115
+ try {
8116
+ switch (kind) {
8117
+ case "brew": {
8118
+ const formula = step.formula;
8119
+ if (!formula) {
8120
+ errors.push(`Brew step "${stepId}" missing formula`);
8121
+ break;
8122
+ }
8123
+ if (!SAFE_PACKAGE_RE.test(formula)) {
8124
+ errors.push(`Unsafe brew formula name: ${formula}`);
8125
+ break;
8126
+ }
8127
+ onLog(`Installing brew formula: ${formula}`);
8128
+ const brewCmd = [
8129
+ `export HOME="${agentHome}"`,
8130
+ `export PATH="${agentHome}/homebrew/bin:${agentHome}/bin:${SYSTEM_PATH}:$PATH"`,
8131
+ `brew install ${formula}`
8132
+ ].join(" && ");
8133
+ await execWithProgress3(
8134
+ `sudo -H -u ${agentUsername} /bin/bash --norc --noprofile -c '${brewCmd}'`,
8135
+ onLog,
8136
+ { timeout: 12e4, cwd: "/" }
8137
+ );
8138
+ installed.push(formula);
8139
+ break;
8140
+ }
8141
+ case "npm": {
8142
+ const pkg = step["package"];
8143
+ if (!pkg) {
8144
+ errors.push(`npm step "${stepId}" missing package`);
8145
+ break;
8146
+ }
8147
+ if (!SAFE_PACKAGE_RE.test(pkg)) {
8148
+ errors.push(`Unsafe npm package name: ${pkg}`);
8149
+ break;
8150
+ }
8151
+ onLog(`Installing npm package: ${pkg}`);
8152
+ const npmCmd = [
8153
+ `export HOME="${agentHome}"`,
8154
+ `export PATH="${agentHome}/bin:${SYSTEM_PATH}:$PATH"`,
8155
+ `source "${agentHome}/.nvm/nvm.sh" 2>/dev/null || true`,
8156
+ `npm install -g ${pkg}`
8157
+ ].join(" && ");
8158
+ await execWithProgress3(
8159
+ `sudo -H -u ${agentUsername} /bin/bash --norc --noprofile -c '${npmCmd}'`,
8160
+ onLog,
8161
+ { timeout: 6e4, cwd: "/" }
8162
+ );
8163
+ installed.push(pkg);
8164
+ break;
8165
+ }
8166
+ case "pip": {
8167
+ const pkg = step["package"];
8168
+ if (!pkg) {
8169
+ errors.push(`pip step "${stepId}" missing package`);
8170
+ break;
8171
+ }
8172
+ if (!SAFE_PACKAGE_RE.test(pkg)) {
8173
+ errors.push(`Unsafe pip package name: ${pkg}`);
8174
+ break;
8175
+ }
8176
+ onLog(`Installing pip package: ${pkg}`);
8177
+ const pipCmd = [
8178
+ `export HOME="${agentHome}"`,
8179
+ `export PATH="${agentHome}/bin:${SYSTEM_PATH}:$PATH"`,
8180
+ `pip install ${pkg}`
8181
+ ].join(" && ");
8182
+ await execWithProgress3(
8183
+ `sudo -H -u ${agentUsername} /bin/bash --norc --noprofile -c '${pipCmd}'`,
8184
+ onLog,
8185
+ { timeout: 6e4, cwd: "/" }
8186
+ );
8187
+ installed.push(pkg);
8188
+ break;
8189
+ }
8190
+ }
8191
+ } catch (err) {
8192
+ const msg = `Failed to install ${kind} dep (step: ${stepId}): ${err.message}`;
8193
+ onLog(msg);
8194
+ errors.push(msg);
8195
+ }
8196
+ }
8197
+ const requiredBins = info.bins;
8198
+ if (requiredBins.length > 0) {
8199
+ onLog(`Verifying required binaries: ${requiredBins.join(", ")}`);
8200
+ for (const bin of requiredBins) {
8201
+ try {
8202
+ const checkCmd = [
8203
+ `export HOME="${agentHome}"`,
8204
+ `export PATH="${agentHome}/homebrew/bin:${agentHome}/bin:${SYSTEM_PATH}:$PATH"`,
8205
+ `source "${agentHome}/.nvm/nvm.sh" 2>/dev/null || true`,
8206
+ `which ${bin}`
8207
+ ].join(" && ");
8208
+ await execWithProgress3(
8209
+ `sudo -H -u ${agentUsername} /bin/bash --norc --noprofile -c '${checkCmd}'`,
8210
+ () => {
8211
+ },
8212
+ { timeout: 5e3, cwd: "/" }
8213
+ );
8214
+ } catch {
8215
+ errors.push(`Required binary "${bin}" not found in agent PATH after install`);
8216
+ }
8217
+ }
8218
+ }
8219
+ return {
8220
+ success: errors.length === 0,
8221
+ installed,
8222
+ errors
8223
+ };
8224
+ }
8225
+
7883
8226
  // libs/shield-daemon/src/routes/marketplace.ts
7884
8227
  var installInProgress = /* @__PURE__ */ new Set();
7885
8228
  function isInstallInProgress(slug) {
@@ -8134,7 +8477,7 @@ async function marketplaceRoutes(app) {
8134
8477
  error: "Critical vulnerability detected",
8135
8478
  analysis: analysisResult
8136
8479
  });
8137
- return { success: false, name: slug, analysis: analysisResult, logs };
8480
+ return { success: false, name: slug, analysis: analysisResult, logs, depsSuccess: void 0 };
8138
8481
  }
8139
8482
  emitSkillInstallProgress(slug, "download", "Downloading skill files");
8140
8483
  const skill = await getMarketplaceSkill(slug);
@@ -8144,7 +8487,7 @@ async function marketplaceRoutes(app) {
8144
8487
  name: slug,
8145
8488
  error: "No files available for installation"
8146
8489
  });
8147
- return { success: false, name: slug, analysis: analysisResult, logs };
8490
+ return { success: false, name: slug, analysis: analysisResult, logs, depsSuccess: void 0 };
8148
8491
  }
8149
8492
  const publisher = skill.author;
8150
8493
  logs.push("Downloaded skill files");
@@ -8153,18 +8496,27 @@ async function marketplaceRoutes(app) {
8153
8496
  throw new Error("Skills directory not configured");
8154
8497
  }
8155
8498
  const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
8156
- const binDir = path15.join(agentHome, "bin");
8499
+ const agentUsername = path16.basename(agentHome);
8500
+ const binDir = path16.join(agentHome, "bin");
8157
8501
  const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "ash_default";
8158
- skillDir = path15.join(skillsDir2, slug);
8502
+ skillDir = path16.join(skillsDir2, slug);
8159
8503
  emitSkillInstallProgress(slug, "approve", "Pre-approving skill");
8160
- addToApprovedList(slug, publisher);
8504
+ addToApprovedList(slug, publisher, void 0, slug);
8161
8505
  logs.push("Skill pre-approved");
8162
8506
  emitSkillInstallProgress(slug, "copy", "Writing skill files");
8507
+ const taggedFiles = await Promise.all(files.map(async (f) => {
8508
+ let content = f.content;
8509
+ if (/SKILL\.md$/i.test(f.name)) {
8510
+ content = stripEnvFromSkillMd(content);
8511
+ content = await injectInstallationTag(content);
8512
+ }
8513
+ return { ...f, content };
8514
+ }));
8163
8515
  const brokerAvailable = await isBrokerAvailable();
8164
8516
  if (brokerAvailable) {
8165
8517
  const brokerResult = await installSkillViaBroker(
8166
8518
  slug,
8167
- files.map((f) => ({ name: f.name, content: f.content })),
8519
+ taggedFiles.map((f) => ({ name: f.name, content: f.content })),
8168
8520
  { createWrapper: true, agentHome, socketGroup }
8169
8521
  );
8170
8522
  if (!brokerResult.installed) {
@@ -8183,18 +8535,65 @@ async function marketplaceRoutes(app) {
8183
8535
  }
8184
8536
  } else {
8185
8537
  console.log(`[Marketplace] Broker unavailable, installing ${slug} directly`);
8186
- fs15.mkdirSync(skillDir, { recursive: true });
8187
- for (const file of files) {
8188
- const filePath = path15.join(skillDir, file.name);
8189
- const fileDir = path15.dirname(filePath);
8538
+ sudoMkdir(skillDir, agentUsername);
8539
+ for (const file of taggedFiles) {
8540
+ const filePath = path16.join(skillDir, file.name);
8541
+ const fileDir = path16.dirname(filePath);
8190
8542
  if (fileDir !== skillDir) {
8191
- fs15.mkdirSync(fileDir, { recursive: true });
8543
+ sudoMkdir(fileDir, agentUsername);
8192
8544
  }
8193
- fs15.writeFileSync(filePath, file.content, "utf-8");
8545
+ sudoWriteFile(filePath, file.content, agentUsername);
8194
8546
  }
8195
8547
  createSkillWrapper(slug, binDir);
8196
- logs.push(`Files written directly: ${files.length} files`);
8197
- logs.push(`Wrapper created: ${path15.join(binDir, slug)}`);
8548
+ logs.push(`Files written directly: ${taggedFiles.length} files`);
8549
+ logs.push(`Wrapper created: ${path16.join(binDir, slug)}`);
8550
+ }
8551
+ let depsSuccess = true;
8552
+ emitSkillInstallProgress(slug, "deps", "Installing skill dependencies");
8553
+ try {
8554
+ let depsLineCount = 0;
8555
+ let depsLastEmit = Date.now();
8556
+ let depsLastLine = "";
8557
+ const DEPS_DEBOUNCE_MS = 3e3;
8558
+ const depsOnLog = (msg) => {
8559
+ depsLineCount++;
8560
+ depsLastLine = msg;
8561
+ if (/^(Installing|Found|Verifying)\s/.test(msg)) {
8562
+ emitSkillInstallProgress(slug, "deps", msg);
8563
+ depsLastEmit = Date.now();
8564
+ return;
8565
+ }
8566
+ const now = Date.now();
8567
+ if (now - depsLastEmit >= DEPS_DEBOUNCE_MS) {
8568
+ emitSkillInstallProgress(slug, "deps", `Installing... (${depsLineCount} lines)`);
8569
+ depsLastEmit = now;
8570
+ }
8571
+ };
8572
+ const depsResult = await executeSkillInstallSteps({
8573
+ slug,
8574
+ skillDir,
8575
+ agentHome,
8576
+ agentUsername,
8577
+ onLog: depsOnLog
8578
+ });
8579
+ if (depsLineCount > 0) {
8580
+ emitSkillInstallProgress(slug, "deps", `Dependency install complete (${depsLineCount} lines processed)`);
8581
+ }
8582
+ if (depsResult.installed.length > 0) {
8583
+ logs.push(`Dependencies installed: ${depsResult.installed.join(", ")}`);
8584
+ }
8585
+ if (depsResult.errors.length > 0) {
8586
+ depsSuccess = false;
8587
+ for (const err of depsResult.errors) {
8588
+ emitSkillInstallProgress(slug, "warning", `Dependency warning: ${err}`);
8589
+ logs.push(`Dependency warning: ${err}`);
8590
+ }
8591
+ }
8592
+ } catch (err) {
8593
+ depsSuccess = false;
8594
+ const msg = `Dependency installation failed: ${err.message}`;
8595
+ emitSkillInstallProgress(slug, "warning", msg);
8596
+ logs.push(msg);
8198
8597
  }
8199
8598
  addSkillEntry(slug);
8200
8599
  addSkillPolicy(slug);
@@ -8205,14 +8604,19 @@ async function marketplaceRoutes(app) {
8205
8604
  updateApprovedHash(slug, hash);
8206
8605
  logs.push("Integrity hash recorded");
8207
8606
  }
8607
+ try {
8608
+ markDownloadedAsInstalled(slug);
8609
+ } catch {
8610
+ }
8208
8611
  installInProgress.delete(slug);
8209
- daemonEvents.broadcast("skills:installed", { name: slug, analysis: analysisResult });
8612
+ const depsWarnings = depsSuccess ? void 0 : logs.filter((l) => l.startsWith("Dependency"));
8613
+ daemonEvents.broadcast("skills:installed", { name: slug, analysis: analysisResult, depsWarnings });
8210
8614
  logs.push("Installation complete");
8211
- return { success: true, name: slug, analysis: analysisResult, logs };
8615
+ return { success: true, name: slug, analysis: analysisResult, logs, depsSuccess };
8212
8616
  } catch (err) {
8213
8617
  try {
8214
- if (skillDir && fs15.existsSync(skillDir)) {
8215
- fs15.rmSync(skillDir, { recursive: true, force: true });
8618
+ if (skillDir && fs16.existsSync(skillDir)) {
8619
+ fs16.rmSync(skillDir, { recursive: true, force: true });
8216
8620
  }
8217
8621
  removeFromApprovedList(slug);
8218
8622
  } catch {
@@ -8221,7 +8625,7 @@ async function marketplaceRoutes(app) {
8221
8625
  installInProgress.delete(slug);
8222
8626
  daemonEvents.broadcast("skills:install_failed", { name: slug, error: errorMsg });
8223
8627
  console.error("[Marketplace] Install failed:", errorMsg);
8224
- return { success: false, name: slug, analysis: analysisResult, logs: [...logs, `Error: ${errorMsg}`] };
8628
+ return { success: false, name: slug, analysis: analysisResult, logs: [...logs, `Error: ${errorMsg}`], depsSuccess: void 0 };
8225
8629
  } finally {
8226
8630
  installInProgress.delete(slug);
8227
8631
  }
@@ -8245,17 +8649,17 @@ async function marketplaceRoutes(app) {
8245
8649
  }
8246
8650
 
8247
8651
  // libs/shield-daemon/src/routes/skills.ts
8248
- function findSkillMdRecursive(dir, depth = 0) {
8652
+ function findSkillMdRecursive2(dir, depth = 0) {
8249
8653
  if (depth > 3) return null;
8250
8654
  try {
8251
8655
  for (const name of ["SKILL.md", "skill.md", "README.md", "readme.md"]) {
8252
- const candidate = path16.join(dir, name);
8253
- if (fs16.existsSync(candidate)) return candidate;
8656
+ const candidate = path17.join(dir, name);
8657
+ if (fs17.existsSync(candidate)) return candidate;
8254
8658
  }
8255
- const entries = fs16.readdirSync(dir, { withFileTypes: true });
8659
+ const entries = fs17.readdirSync(dir, { withFileTypes: true });
8256
8660
  for (const entry of entries) {
8257
8661
  if (!entry.isDirectory()) continue;
8258
- const found = findSkillMdRecursive(path16.join(dir, entry.name), depth + 1);
8662
+ const found = findSkillMdRecursive2(path17.join(dir, entry.name), depth + 1);
8259
8663
  if (found) return found;
8260
8664
  }
8261
8665
  } catch {
@@ -8264,10 +8668,10 @@ function findSkillMdRecursive(dir, depth = 0) {
8264
8668
  }
8265
8669
  function readSkillMetadata(skillDir) {
8266
8670
  try {
8267
- const mdPath = findSkillMdRecursive(skillDir);
8671
+ const mdPath = findSkillMdRecursive2(skillDir);
8268
8672
  if (!mdPath) return {};
8269
- const content = fs16.readFileSync(mdPath, "utf-8");
8270
- const parsed = parseSkillMd2(content);
8673
+ const content = fs17.readFileSync(mdPath, "utf-8");
8674
+ const parsed = parseSkillMd3(content);
8271
8675
  const meta = parsed?.metadata;
8272
8676
  return {
8273
8677
  description: meta?.description,
@@ -8325,7 +8729,7 @@ async function skillsRoutes(app) {
8325
8729
  let onDiskNames = [];
8326
8730
  if (skillsDir2) {
8327
8731
  try {
8328
- onDiskNames = fs16.readdirSync(skillsDir2, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
8732
+ onDiskNames = fs17.readdirSync(skillsDir2, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
8329
8733
  } catch {
8330
8734
  }
8331
8735
  }
@@ -8335,14 +8739,14 @@ async function skillsRoutes(app) {
8335
8739
  const data = [
8336
8740
  // Approved → active (with metadata from SKILL.md + cached analysis)
8337
8741
  ...approved.map((a) => {
8338
- const meta = skillsDir2 ? readSkillMetadata(path16.join(skillsDir2, a.name)) : {};
8742
+ const meta = skillsDir2 ? readSkillMetadata(path17.join(skillsDir2, a.name)) : {};
8339
8743
  const cached = getCachedAnalysis2(a.name);
8340
8744
  const dlMeta = getDownloadedSkillMeta(a.name);
8341
8745
  return {
8342
8746
  name: a.name,
8343
8747
  source: "user",
8344
8748
  status: "active",
8345
- path: path16.join(skillsDir2 ?? "", a.name),
8749
+ path: path17.join(skillsDir2 ?? "", a.name),
8346
8750
  publisher: a.publisher,
8347
8751
  description: meta.description,
8348
8752
  version: meta.version,
@@ -8369,19 +8773,42 @@ async function skillsRoutes(app) {
8369
8773
  }),
8370
8774
  // Workspace: on disk but not approved or untrusted
8371
8775
  ...workspaceNames.map((name) => {
8372
- const meta = skillsDir2 ? readSkillMetadata(path16.join(skillsDir2, name)) : {};
8776
+ const meta = skillsDir2 ? readSkillMetadata(path17.join(skillsDir2, name)) : {};
8373
8777
  return {
8374
8778
  name,
8375
8779
  source: "workspace",
8376
8780
  status: "workspace",
8377
- path: path16.join(skillsDir2 ?? "", name),
8781
+ path: path17.join(skillsDir2 ?? "", name),
8378
8782
  description: meta.description,
8379
8783
  version: meta.version,
8380
8784
  author: meta.author,
8381
8785
  tags: meta.tags,
8382
8786
  analysis: buildAnalysisSummary(name, getCachedAnalysis2(name))
8383
8787
  };
8384
- })
8788
+ }),
8789
+ // Disabled: previously installed marketplace skills that are no longer active
8790
+ ...(() => {
8791
+ const allKnown = /* @__PURE__ */ new Set([
8792
+ ...approvedNames,
8793
+ ...untrustedNames,
8794
+ ...workspaceNames
8795
+ ]);
8796
+ return listDownloadedSkills().filter((d) => d.wasInstalled && !allKnown.has(d.slug) && !allKnown.has(d.name)).map((d) => {
8797
+ const cached = getCachedAnalysis2(d.slug) || getCachedAnalysis2(d.name);
8798
+ return {
8799
+ name: d.slug,
8800
+ source: "marketplace",
8801
+ status: "disabled",
8802
+ path: "",
8803
+ publisher: d.author,
8804
+ description: d.description,
8805
+ version: d.version,
8806
+ author: d.author,
8807
+ tags: d.tags,
8808
+ analysis: buildAnalysisSummary(d.slug, d.analysis || cached)
8809
+ };
8810
+ });
8811
+ })()
8385
8812
  ];
8386
8813
  return reply.send({ data });
8387
8814
  });
@@ -8405,7 +8832,7 @@ async function skillsRoutes(app) {
8405
8832
  let isWorkspace = false;
8406
8833
  if (!entry && !uEntry && skillsDir2) {
8407
8834
  try {
8408
- isWorkspace = fs16.existsSync(path16.join(skillsDir2, name));
8835
+ isWorkspace = fs17.existsSync(path17.join(skillsDir2, name));
8409
8836
  } catch {
8410
8837
  }
8411
8838
  }
@@ -8423,13 +8850,13 @@ async function skillsRoutes(app) {
8423
8850
  analysis: buildFullAnalysis(name, dlMeta?.analysis || getCachedAnalysis2(name))
8424
8851
  };
8425
8852
  } else if (entry) {
8426
- const meta = skillsDir2 ? readSkillMetadata(path16.join(skillsDir2, name)) : {};
8853
+ const meta = skillsDir2 ? readSkillMetadata(path17.join(skillsDir2, name)) : {};
8427
8854
  const cached = getCachedAnalysis2(name);
8428
8855
  summary = {
8429
8856
  name,
8430
8857
  source: "user",
8431
8858
  status: "active",
8432
- path: skillsDir2 ? path16.join(skillsDir2, name) : "",
8859
+ path: skillsDir2 ? path17.join(skillsDir2, name) : "",
8433
8860
  publisher: entry.publisher,
8434
8861
  description: meta.description,
8435
8862
  version: meta.version,
@@ -8438,12 +8865,12 @@ async function skillsRoutes(app) {
8438
8865
  analysis: buildFullAnalysis(name, dlMeta?.analysis || cached)
8439
8866
  };
8440
8867
  } else if (isWorkspace) {
8441
- const meta = skillsDir2 ? readSkillMetadata(path16.join(skillsDir2, name)) : {};
8868
+ const meta = skillsDir2 ? readSkillMetadata(path17.join(skillsDir2, name)) : {};
8442
8869
  summary = {
8443
8870
  name,
8444
8871
  source: "workspace",
8445
8872
  status: "workspace",
8446
- path: skillsDir2 ? path16.join(skillsDir2, name) : "",
8873
+ path: skillsDir2 ? path17.join(skillsDir2, name) : "",
8447
8874
  description: meta.description,
8448
8875
  version: meta.version,
8449
8876
  author: meta.author,
@@ -8455,7 +8882,7 @@ async function skillsRoutes(app) {
8455
8882
  summary = {
8456
8883
  name: dlMeta.slug ?? name,
8457
8884
  source: "marketplace",
8458
- status: "downloaded",
8885
+ status: dlMeta.wasInstalled ? "disabled" : "downloaded",
8459
8886
  description: dlMeta.description,
8460
8887
  path: "",
8461
8888
  publisher: dlMeta.author,
@@ -8468,11 +8895,11 @@ async function skillsRoutes(app) {
8468
8895
  return reply.code(404).send({ error: `Skill "${name}" not found` });
8469
8896
  }
8470
8897
  let content = "";
8471
- const dirToRead = summary.path || (skillsDir2 ? path16.join(skillsDir2, name) : "");
8898
+ const dirToRead = summary.path || (skillsDir2 ? path17.join(skillsDir2, name) : "");
8472
8899
  if (dirToRead) {
8473
8900
  try {
8474
- const mdPath = findSkillMdRecursive(dirToRead);
8475
- if (mdPath) content = fs16.readFileSync(mdPath, "utf-8");
8901
+ const mdPath = findSkillMdRecursive2(dirToRead);
8902
+ if (mdPath) content = fs17.readFileSync(mdPath, "utf-8");
8476
8903
  } catch {
8477
8904
  }
8478
8905
  }
@@ -8510,14 +8937,14 @@ async function skillsRoutes(app) {
8510
8937
  if (!content) {
8511
8938
  const skillsDir2 = getSkillsDir();
8512
8939
  const possibleDirs = [
8513
- skillsDir2 ? path16.join(skillsDir2, name) : null
8940
+ skillsDir2 ? path17.join(skillsDir2, name) : null
8514
8941
  ].filter(Boolean);
8515
8942
  for (const dir of possibleDirs) {
8516
8943
  try {
8517
- const mdPath = findSkillMdRecursive(dir);
8944
+ const mdPath = findSkillMdRecursive2(dir);
8518
8945
  if (mdPath) {
8519
- content = fs16.readFileSync(mdPath, "utf-8");
8520
- const parsed = parseSkillMd2(content);
8946
+ content = fs17.readFileSync(mdPath, "utf-8");
8947
+ const parsed = parseSkillMd3(content);
8521
8948
  if (parsed?.metadata && !metadata) {
8522
8949
  metadata = parsed.metadata;
8523
8950
  }
@@ -8532,7 +8959,7 @@ async function skillsRoutes(app) {
8532
8959
  const skillFile = localFiles.find((f) => /skill\.md/i.test(f.name));
8533
8960
  if (skillFile?.content) {
8534
8961
  content = skillFile.content;
8535
- const parsed = parseSkillMd2(content);
8962
+ const parsed = parseSkillMd3(content);
8536
8963
  if (parsed?.metadata && !metadata) {
8537
8964
  metadata = parsed.metadata;
8538
8965
  }
@@ -8547,7 +8974,7 @@ async function skillsRoutes(app) {
8547
8974
  const skillFile = freshFiles.find((f) => /skill\.md/i.test(f.name));
8548
8975
  if (skillFile?.content) {
8549
8976
  content = skillFile.content;
8550
- const parsed = parseSkillMd2(content);
8977
+ const parsed = parseSkillMd3(content);
8551
8978
  if (parsed?.metadata && !metadata) {
8552
8979
  metadata = parsed.metadata;
8553
8980
  }
@@ -8701,10 +9128,10 @@ async function skillsRoutes(app) {
8701
9128
  return reply.code(500).send({ error: "Skills directory not configured" });
8702
9129
  }
8703
9130
  const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
8704
- const binDir = path16.join(agentHome, "bin");
9131
+ const binDir = path17.join(agentHome, "bin");
8705
9132
  const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "ash_default";
8706
- const skillDir = path16.join(skillsDir2, name);
8707
- const isInstalled = fs16.existsSync(skillDir);
9133
+ const skillDir = path17.join(skillsDir2, name);
9134
+ const isInstalled = fs17.existsSync(skillDir);
8708
9135
  if (isInstalled) {
8709
9136
  try {
8710
9137
  const brokerAvailable = await isBrokerAvailable();
@@ -8714,7 +9141,7 @@ async function skillsRoutes(app) {
8714
9141
  agentHome
8715
9142
  });
8716
9143
  } else {
8717
- fs16.rmSync(skillDir, { recursive: true, force: true });
9144
+ fs17.rmSync(skillDir, { recursive: true, force: true });
8718
9145
  removeSkillWrapper(name, binDir);
8719
9146
  }
8720
9147
  removeSkillEntry(name);
@@ -8722,7 +9149,7 @@ async function skillsRoutes(app) {
8722
9149
  syncOpenClawFromPolicies(loadConfig().policies);
8723
9150
  removeFromApprovedList(name);
8724
9151
  try {
8725
- deleteDownloadedSkill(name);
9152
+ markDownloadedAsInstalled(name);
8726
9153
  } catch {
8727
9154
  }
8728
9155
  console.log(`[Skills] Disabled marketplace skill: ${name}`);
@@ -8742,12 +9169,20 @@ async function skillsRoutes(app) {
8742
9169
  return reply.code(404).send({ error: "No files in download cache for this skill" });
8743
9170
  }
8744
9171
  try {
8745
- addToApprovedList(name, meta.author);
9172
+ addToApprovedList(name, meta.author, void 0, meta.slug);
9173
+ const taggedFiles = await Promise.all(files.map(async (f) => {
9174
+ let content = f.content;
9175
+ if (/SKILL\.md$/i.test(f.name)) {
9176
+ content = stripEnvFromSkillMd2(content);
9177
+ content = await injectInstallationTag(content);
9178
+ }
9179
+ return { name: f.name, content, type: f.type };
9180
+ }));
8746
9181
  const brokerAvailable = await isBrokerAvailable();
8747
9182
  if (brokerAvailable) {
8748
9183
  const brokerResult = await installSkillViaBroker(
8749
9184
  name,
8750
- files.map((f) => ({ name: f.name, content: f.content })),
9185
+ taggedFiles.map((f) => ({ name: f.name, content: f.content })),
8751
9186
  { createWrapper: true, agentHome, socketGroup }
8752
9187
  );
8753
9188
  if (!brokerResult.installed) {
@@ -8759,11 +9194,11 @@ async function skillsRoutes(app) {
8759
9194
  }
8760
9195
  }
8761
9196
  } else {
8762
- fs16.mkdirSync(skillDir, { recursive: true });
8763
- for (const file of files) {
8764
- const filePath = path16.join(skillDir, file.name);
8765
- fs16.mkdirSync(path16.dirname(filePath), { recursive: true });
8766
- fs16.writeFileSync(filePath, file.content, "utf-8");
9197
+ fs17.mkdirSync(skillDir, { recursive: true });
9198
+ for (const file of taggedFiles) {
9199
+ const filePath = path17.join(skillDir, file.name);
9200
+ fs17.mkdirSync(path17.dirname(filePath), { recursive: true });
9201
+ fs17.writeFileSync(filePath, file.content, "utf-8");
8767
9202
  }
8768
9203
  try {
8769
9204
  execSync9(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
@@ -8777,12 +9212,16 @@ async function skillsRoutes(app) {
8777
9212
  syncOpenClawFromPolicies(loadConfig().policies);
8778
9213
  const hash = computeSkillHash(skillDir);
8779
9214
  if (hash) updateApprovedHash(name, hash);
9215
+ try {
9216
+ markDownloadedAsInstalled(name);
9217
+ } catch {
9218
+ }
8780
9219
  console.log(`[Skills] Enabled marketplace skill: ${name}`);
8781
9220
  return reply.send({ success: true, action: "enabled", name });
8782
9221
  } catch (err) {
8783
9222
  try {
8784
- if (fs16.existsSync(skillDir)) {
8785
- fs16.rmSync(skillDir, { recursive: true, force: true });
9223
+ if (fs17.existsSync(skillDir)) {
9224
+ fs17.rmSync(skillDir, { recursive: true, force: true });
8786
9225
  }
8787
9226
  removeFromApprovedList(name);
8788
9227
  } catch {
@@ -8808,7 +9247,7 @@ async function skillsRoutes(app) {
8808
9247
  const skillMdFile = files.find((f) => f.name === "SKILL.md");
8809
9248
  let metadata;
8810
9249
  if (skillMdFile) {
8811
- const parsed = parseSkillMd2(skillMdFile.content);
9250
+ const parsed = parseSkillMd3(skillMdFile.content);
8812
9251
  metadata = parsed?.metadata;
8813
9252
  }
8814
9253
  const analysis = analyzeSkill(name, combinedContent, metadata);
@@ -8820,16 +9259,25 @@ async function skillsRoutes(app) {
8820
9259
  return reply.code(500).send({ error: "Skills directory not configured" });
8821
9260
  }
8822
9261
  const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
8823
- const binDir = path16.join(agentHome, "bin");
9262
+ const agentUsername = path17.basename(agentHome);
9263
+ const binDir = path17.join(agentHome, "bin");
8824
9264
  const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "ash_default";
8825
- const skillDir = path16.join(skillsDir2, name);
9265
+ const skillDir = path17.join(skillsDir2, name);
8826
9266
  try {
8827
9267
  addToApprovedList(name, publisher);
8828
- fs16.mkdirSync(skillDir, { recursive: true });
8829
- for (const file of files) {
8830
- const filePath = path16.join(skillDir, file.name);
8831
- fs16.mkdirSync(path16.dirname(filePath), { recursive: true });
8832
- fs16.writeFileSync(filePath, file.content, "utf-8");
9268
+ const taggedFiles = await Promise.all(files.map(async (f) => {
9269
+ let content = f.content;
9270
+ if (/SKILL\.md$/i.test(f.name)) {
9271
+ content = stripEnvFromSkillMd2(content);
9272
+ content = await injectInstallationTag(content);
9273
+ }
9274
+ return { name: f.name, content };
9275
+ }));
9276
+ sudoMkdir(skillDir, agentUsername);
9277
+ for (const file of taggedFiles) {
9278
+ const filePath = path17.join(skillDir, file.name);
9279
+ sudoMkdir(path17.dirname(filePath), agentUsername);
9280
+ sudoWriteFile(filePath, file.content, agentUsername);
8833
9281
  }
8834
9282
  try {
8835
9283
  execSync9(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
@@ -8843,8 +9291,8 @@ async function skillsRoutes(app) {
8843
9291
  return reply.send({ success: true, name, analysis });
8844
9292
  } catch (err) {
8845
9293
  try {
8846
- if (fs16.existsSync(skillDir)) {
8847
- fs16.rmSync(skillDir, { recursive: true, force: true });
9294
+ if (fs17.existsSync(skillDir)) {
9295
+ fs17.rmSync(skillDir, { recursive: true, force: true });
8848
9296
  }
8849
9297
  removeFromApprovedList(name);
8850
9298
  } catch {
@@ -8878,15 +9326,23 @@ async function skillsRoutes(app) {
8878
9326
  }
8879
9327
  try {
8880
9328
  const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
8881
- const binDir = path16.join(agentHome, "bin");
9329
+ const binDir = path17.join(agentHome, "bin");
8882
9330
  const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "ash_default";
8883
- const skillDir = path16.join(getSkillsDir(), name);
8884
- addToApprovedList(name, meta.author);
9331
+ const skillDir = path17.join(getSkillsDir(), name);
9332
+ addToApprovedList(name, meta.author, void 0, meta.slug);
9333
+ const taggedFiles = await Promise.all(files.map(async (f) => {
9334
+ let content = f.content;
9335
+ if (/SKILL\.md$/i.test(f.name)) {
9336
+ content = stripEnvFromSkillMd2(content);
9337
+ content = await injectInstallationTag(content);
9338
+ }
9339
+ return { name: f.name, content, type: f.type };
9340
+ }));
8885
9341
  const brokerAvailable = await isBrokerAvailable();
8886
9342
  if (brokerAvailable) {
8887
9343
  const brokerResult = await installSkillViaBroker(
8888
9344
  name,
8889
- files.map((f) => ({ name: f.name, content: f.content })),
9345
+ taggedFiles.map((f) => ({ name: f.name, content: f.content })),
8890
9346
  { createWrapper: true, agentHome, socketGroup }
8891
9347
  );
8892
9348
  if (!brokerResult.installed) {
@@ -8898,11 +9354,11 @@ async function skillsRoutes(app) {
8898
9354
  }
8899
9355
  }
8900
9356
  } else {
8901
- fs16.mkdirSync(skillDir, { recursive: true });
8902
- for (const file of files) {
8903
- const filePath = path16.join(skillDir, file.name);
8904
- fs16.mkdirSync(path16.dirname(filePath), { recursive: true });
8905
- fs16.writeFileSync(filePath, file.content, "utf-8");
9357
+ fs17.mkdirSync(skillDir, { recursive: true });
9358
+ for (const file of taggedFiles) {
9359
+ const filePath = path17.join(skillDir, file.name);
9360
+ fs17.mkdirSync(path17.dirname(filePath), { recursive: true });
9361
+ fs17.writeFileSync(filePath, file.content, "utf-8");
8906
9362
  }
8907
9363
  try {
8908
9364
  execSync9(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
@@ -8914,8 +9370,12 @@ async function skillsRoutes(app) {
8914
9370
  addSkillEntry(name);
8915
9371
  addSkillPolicy(name);
8916
9372
  syncOpenClawFromPolicies(loadConfig().policies);
8917
- const unblockHash = computeSkillHash(path16.join(getSkillsDir(), name));
9373
+ const unblockHash = computeSkillHash(path17.join(getSkillsDir(), name));
8918
9374
  if (unblockHash) updateApprovedHash(name, unblockHash);
9375
+ try {
9376
+ markDownloadedAsInstalled(name);
9377
+ } catch {
9378
+ }
8919
9379
  console.log(`[Skills] Unblocked and installed skill: ${name}`);
8920
9380
  return reply.send({ success: true, message: `Skill "${name}" approved and installed` });
8921
9381
  } catch (err) {
@@ -8945,7 +9405,7 @@ async function skillsRoutes(app) {
8945
9405
  const skillMdFile = files.find((f) => /skill\.md/i.test(f.name));
8946
9406
  if (skillMdFile) {
8947
9407
  try {
8948
- const parsed = parseSkillMd2(skillMdFile.content);
9408
+ const parsed = parseSkillMd3(skillMdFile.content);
8949
9409
  if (parsed?.metadata) {
8950
9410
  parsedDescription = parsedDescription || parsed.metadata.description;
8951
9411
  parsedVersion = parsedVersion || parsed.metadata.version;
@@ -8974,10 +9434,10 @@ async function skillsRoutes(app) {
8974
9434
 
8975
9435
  // libs/shield-daemon/src/routes/exec.ts
8976
9436
  init_paths();
8977
- import * as fs17 from "node:fs";
8978
- import * as path17 from "node:path";
9437
+ import * as fs18 from "node:fs";
9438
+ import * as path18 from "node:path";
8979
9439
  function getAllowedCommandsPath2() {
8980
- return path17.join(getSystemConfigDir(), "allowed-commands.json");
9440
+ return path18.join(getSystemConfigDir(), "allowed-commands.json");
8981
9441
  }
8982
9442
  var BIN_DIRS = [
8983
9443
  "/usr/bin",
@@ -8990,22 +9450,22 @@ var binCache = null;
8990
9450
  var BIN_CACHE_TTL = 6e4;
8991
9451
  var VALID_NAME = /^[a-zA-Z0-9_-]+$/;
8992
9452
  function loadConfig2() {
8993
- if (!fs17.existsSync(getAllowedCommandsPath2())) {
9453
+ if (!fs18.existsSync(getAllowedCommandsPath2())) {
8994
9454
  return { version: "1.0.0", commands: [] };
8995
9455
  }
8996
9456
  try {
8997
- const content = fs17.readFileSync(getAllowedCommandsPath2(), "utf-8");
9457
+ const content = fs18.readFileSync(getAllowedCommandsPath2(), "utf-8");
8998
9458
  return JSON.parse(content);
8999
9459
  } catch {
9000
9460
  return { version: "1.0.0", commands: [] };
9001
9461
  }
9002
9462
  }
9003
9463
  function saveConfig2(config) {
9004
- const dir = path17.dirname(getAllowedCommandsPath2());
9005
- if (!fs17.existsSync(dir)) {
9006
- fs17.mkdirSync(dir, { recursive: true });
9464
+ const dir = path18.dirname(getAllowedCommandsPath2());
9465
+ if (!fs18.existsSync(dir)) {
9466
+ fs18.mkdirSync(dir, { recursive: true });
9007
9467
  }
9008
- fs17.writeFileSync(getAllowedCommandsPath2(), JSON.stringify(config, null, 2) + "\n", "utf-8");
9468
+ fs18.writeFileSync(getAllowedCommandsPath2(), JSON.stringify(config, null, 2) + "\n", "utf-8");
9009
9469
  }
9010
9470
  function scanSystemBins() {
9011
9471
  const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
@@ -9014,13 +9474,13 @@ function scanSystemBins() {
9014
9474
  const results = [];
9015
9475
  for (const dir of allDirs) {
9016
9476
  try {
9017
- if (!fs17.existsSync(dir)) continue;
9018
- const entries = fs17.readdirSync(dir);
9477
+ if (!fs18.existsSync(dir)) continue;
9478
+ const entries = fs18.readdirSync(dir);
9019
9479
  for (const entry of entries) {
9020
9480
  if (seen.has(entry)) continue;
9021
- const fullPath = path17.join(dir, entry);
9481
+ const fullPath = path18.join(dir, entry);
9022
9482
  try {
9023
- const stat = fs17.statSync(fullPath);
9483
+ const stat = fs18.statSync(fullPath);
9024
9484
  if (stat.isFile() && (stat.mode & 73) !== 0) {
9025
9485
  seen.add(entry);
9026
9486
  results.push({ name: entry, path: fullPath });
@@ -9073,7 +9533,7 @@ async function execRoutes(app) {
9073
9533
  };
9074
9534
  }
9075
9535
  for (const p of paths) {
9076
- if (!path17.isAbsolute(p)) {
9536
+ if (!path18.isAbsolute(p)) {
9077
9537
  return {
9078
9538
  success: false,
9079
9539
  error: {
@@ -9415,7 +9875,7 @@ async function authRoutes(app) {
9415
9875
  init_vault();
9416
9876
  import { isSecretEnvVar } from "@agenshield/sandbox";
9417
9877
  init_secret_sync();
9418
- import crypto5 from "node:crypto";
9878
+ import crypto6 from "node:crypto";
9419
9879
  function maskValue(value) {
9420
9880
  if (value.length <= 4) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
9421
9881
  return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + value.slice(-4);
@@ -9510,7 +9970,7 @@ async function secretsRoutes(app) {
9510
9970
  const secrets = await vault.get("secrets") ?? [];
9511
9971
  const resolvedScope = scope ?? (policyIds?.length > 0 ? "policed" : "global");
9512
9972
  const newSecret = {
9513
- id: crypto5.randomUUID(),
9973
+ id: crypto6.randomUUID(),
9514
9974
  name: name.trim(),
9515
9975
  value,
9516
9976
  policyIds: resolvedScope === "standalone" ? [] : policyIds ?? [],
@@ -9569,18 +10029,18 @@ async function secretsRoutes(app) {
9569
10029
  }
9570
10030
 
9571
10031
  // libs/shield-daemon/src/routes/fs.ts
9572
- import * as fs18 from "node:fs";
9573
- import * as path18 from "node:path";
10032
+ import * as fs19 from "node:fs";
10033
+ import * as path19 from "node:path";
9574
10034
  import * as os6 from "node:os";
9575
10035
  var MAX_ENTRIES = 200;
9576
10036
  async function fsRoutes(app) {
9577
10037
  app.get("/fs/browse", async (request2) => {
9578
10038
  const dirPath = request2.query.path || os6.homedir();
9579
10039
  const showHidden = request2.query.showHidden === "true";
9580
- const resolvedPath = path18.resolve(dirPath);
10040
+ const resolvedPath = path19.resolve(dirPath);
9581
10041
  let dirents;
9582
10042
  try {
9583
- dirents = fs18.readdirSync(resolvedPath, { withFileTypes: true });
10043
+ dirents = fs19.readdirSync(resolvedPath, { withFileTypes: true });
9584
10044
  } catch {
9585
10045
  return { success: true, data: { entries: [] } };
9586
10046
  }
@@ -9589,7 +10049,7 @@ async function fsRoutes(app) {
9589
10049
  if (!showHidden && dirent.name.startsWith(".")) continue;
9590
10050
  entries.push({
9591
10051
  name: dirent.name,
9592
- path: path18.join(resolvedPath, dirent.name),
10052
+ path: path19.join(resolvedPath, dirent.name),
9593
10053
  type: dirent.isDirectory() ? "directory" : "file"
9594
10054
  });
9595
10055
  if (entries.length >= MAX_ENTRIES) break;
@@ -9603,8 +10063,8 @@ async function fsRoutes(app) {
9603
10063
  }
9604
10064
 
9605
10065
  // libs/shield-daemon/src/services/activity-log.ts
9606
- import * as fs19 from "node:fs";
9607
- import * as path19 from "node:path";
10066
+ import * as fs20 from "node:fs";
10067
+ import * as path20 from "node:path";
9608
10068
  init_paths();
9609
10069
  var ACTIVITY_FILE = "activity.jsonl";
9610
10070
  var MAX_SIZE_BYTES = 100 * 1024 * 1024;
@@ -9622,12 +10082,12 @@ var ActivityLog = class {
9622
10082
  writeCount = 0;
9623
10083
  unsubscribe;
9624
10084
  constructor() {
9625
- this.filePath = path19.join(getConfigDir(), ACTIVITY_FILE);
10085
+ this.filePath = path20.join(getConfigDir(), ACTIVITY_FILE);
9626
10086
  }
9627
10087
  /** Read historical events from the JSONL file, newest first */
9628
10088
  getHistory(limit = 500) {
9629
- if (!fs19.existsSync(this.filePath)) return [];
9630
- const content = fs19.readFileSync(this.filePath, "utf-8");
10089
+ if (!fs20.existsSync(this.filePath)) return [];
10090
+ const content = fs20.readFileSync(this.filePath, "utf-8");
9631
10091
  const lines = content.split("\n").filter(Boolean);
9632
10092
  const events = [];
9633
10093
  for (const line of lines) {
@@ -9652,7 +10112,7 @@ var ActivityLog = class {
9652
10112
  }
9653
10113
  append(event) {
9654
10114
  const line = JSON.stringify(event) + "\n";
9655
- fs19.appendFileSync(this.filePath, line, "utf-8");
10115
+ fs20.appendFileSync(this.filePath, line, "utf-8");
9656
10116
  this.writeCount++;
9657
10117
  if (this.writeCount % PRUNE_INTERVAL === 0) {
9658
10118
  this.rotate();
@@ -9660,7 +10120,7 @@ var ActivityLog = class {
9660
10120
  }
9661
10121
  rotate() {
9662
10122
  try {
9663
- const stat = fs19.statSync(this.filePath);
10123
+ const stat = fs20.statSync(this.filePath);
9664
10124
  if (stat.size > MAX_SIZE_BYTES) {
9665
10125
  this.truncateBySize();
9666
10126
  }
@@ -9670,15 +10130,15 @@ var ActivityLog = class {
9670
10130
  }
9671
10131
  /** Keep newest half of lines when file exceeds size limit */
9672
10132
  truncateBySize() {
9673
- const content = fs19.readFileSync(this.filePath, "utf-8");
10133
+ const content = fs20.readFileSync(this.filePath, "utf-8");
9674
10134
  const lines = content.split("\n").filter(Boolean);
9675
10135
  const keep = lines.slice(Math.floor(lines.length / 2));
9676
- fs19.writeFileSync(this.filePath, keep.join("\n") + "\n", "utf-8");
10136
+ fs20.writeFileSync(this.filePath, keep.join("\n") + "\n", "utf-8");
9677
10137
  }
9678
10138
  /** Remove entries older than 24 hours */
9679
10139
  pruneOldEntries() {
9680
- if (!fs19.existsSync(this.filePath)) return;
9681
- const content = fs19.readFileSync(this.filePath, "utf-8");
10140
+ if (!fs20.existsSync(this.filePath)) return;
10141
+ const content = fs20.readFileSync(this.filePath, "utf-8");
9682
10142
  const lines = content.split("\n").filter(Boolean);
9683
10143
  const cutoff = Date.now() - MAX_AGE_MS;
9684
10144
  const kept = lines.filter((line) => {
@@ -9690,7 +10150,7 @@ var ActivityLog = class {
9690
10150
  }
9691
10151
  });
9692
10152
  if (kept.length < lines.length) {
9693
- fs19.writeFileSync(this.filePath, kept.join("\n") + "\n", "utf-8");
10153
+ fs20.writeFileSync(this.filePath, kept.join("\n") + "\n", "utf-8");
9694
10154
  }
9695
10155
  }
9696
10156
  };
@@ -9811,7 +10271,7 @@ async function openclawRoutes(app) {
9811
10271
  }
9812
10272
 
9813
10273
  // libs/shield-daemon/src/routes/rpc.ts
9814
- import * as crypto6 from "node:crypto";
10274
+ import * as crypto7 from "node:crypto";
9815
10275
  import * as nodefs from "node:fs";
9816
10276
 
9817
10277
  // libs/shield-daemon/src/policy/url-matcher.ts
@@ -9831,11 +10291,11 @@ function normalizeUrlTarget(url) {
9831
10291
  const trimmed = url.trim();
9832
10292
  try {
9833
10293
  const parsed = new URL(trimmed);
9834
- let path21 = parsed.pathname;
9835
- if (path21.length > 1) {
9836
- path21 = path21.replace(/\/+$/, "");
10294
+ let path24 = parsed.pathname;
10295
+ if (path24.length > 1) {
10296
+ path24 = path24.replace(/\/+$/, "");
9837
10297
  }
9838
- return `${parsed.protocol}//${parsed.host}${path21}${parsed.search}`;
10298
+ return `${parsed.protocol}//${parsed.host}${path24}${parsed.search}`;
9839
10299
  } catch {
9840
10300
  return trimmed.replace(/\/+$/, "");
9841
10301
  }
@@ -9925,7 +10385,7 @@ function checkUrlPolicy(policies, url) {
9925
10385
  // libs/shield-daemon/src/proxy/server.ts
9926
10386
  import * as http from "node:http";
9927
10387
  import * as net from "node:net";
9928
- function createPerRunProxy(urlPolicies, onActivity, logger) {
10388
+ function createPerRunProxy(urlPolicies, onActivity, logger, onBlock) {
9929
10389
  const server = http.createServer((req, res) => {
9930
10390
  onActivity();
9931
10391
  const url = req.url;
@@ -9937,6 +10397,7 @@ function createPerRunProxy(urlPolicies, onActivity, logger) {
9937
10397
  const allowed = checkUrlPolicy(urlPolicies, url);
9938
10398
  if (!allowed) {
9939
10399
  logger(`BLOCKED HTTP ${req.method} ${url}`);
10400
+ onBlock?.(req.method || "GET", url, "http");
9940
10401
  res.writeHead(403, { "Content-Type": "text/plain" });
9941
10402
  res.end("Blocked by AgenShield URL policy");
9942
10403
  return;
@@ -9980,6 +10441,7 @@ function createPerRunProxy(urlPolicies, onActivity, logger) {
9980
10441
  const allowed = checkUrlPolicy(urlPolicies, `https://${hostname2}`);
9981
10442
  if (!allowed) {
9982
10443
  logger(`BLOCKED CONNECT ${hostname2}:${port}`);
10444
+ onBlock?.("CONNECT", `${hostname2}:${port}`, "https");
9983
10445
  clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
9984
10446
  clientSocket.destroy();
9985
10447
  return;
@@ -10042,7 +10504,16 @@ var ProxyPool = class {
10042
10504
  const logger = (msg) => {
10043
10505
  console.log(`[proxy:${execId.slice(0, 8)}] ${msg}`);
10044
10506
  };
10045
- const server = createPerRunProxy(urlPolicies, onActivity, logger);
10507
+ const onBlock = (method, target, protocol) => {
10508
+ emitInterceptorEvent({
10509
+ type: "denied",
10510
+ operation: "http_request",
10511
+ target: protocol === "https" ? `https://${target}` : target,
10512
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10513
+ error: `Blocked by URL policy (${method})`
10514
+ });
10515
+ };
10516
+ const server = createPerRunProxy(urlPolicies, onActivity, logger, onBlock);
10046
10517
  const port = await new Promise((resolve3, reject) => {
10047
10518
  server.listen(0, "127.0.0.1", () => {
10048
10519
  const addr = server.address();
@@ -10121,16 +10592,23 @@ function matchCommandPattern(pattern, target) {
10121
10592
  const firstSpace = target.indexOf(" ");
10122
10593
  const cmd = firstSpace >= 0 ? target.slice(0, firstSpace) : target;
10123
10594
  if (cmd.startsWith("/")) {
10124
- const basename3 = cmd.split("/").pop() || cmd;
10125
- normalizedTarget = firstSpace >= 0 ? basename3 + target.slice(firstSpace) : basename3;
10595
+ const basename6 = cmd.split("/").pop() || cmd;
10596
+ normalizedTarget = firstSpace >= 0 ? basename6 + target.slice(firstSpace) : basename6;
10126
10597
  }
10127
10598
  if (trimmed.endsWith(":*")) {
10128
- const prefix = trimmed.slice(0, -2);
10599
+ let prefix = trimmed.slice(0, -2);
10600
+ if (prefix.includes("/")) {
10601
+ prefix = prefix.split("/").pop() || prefix;
10602
+ }
10129
10603
  const lowerTarget = normalizedTarget.toLowerCase();
10130
10604
  const lowerPrefix = prefix.toLowerCase();
10131
10605
  return lowerTarget === lowerPrefix || lowerTarget.startsWith(lowerPrefix + " ");
10132
10606
  }
10133
- return normalizedTarget.toLowerCase() === trimmed.toLowerCase();
10607
+ let normalizedPattern = trimmed;
10608
+ if (trimmed.includes("/")) {
10609
+ normalizedPattern = trimmed.split("/").pop() || trimmed;
10610
+ }
10611
+ return normalizedTarget.toLowerCase() === normalizedPattern.toLowerCase();
10134
10612
  }
10135
10613
  function operationToTarget(operation) {
10136
10614
  switch (operation) {
@@ -10173,8 +10651,8 @@ function determineNetworkAccess(_config, matchedPolicy, target) {
10173
10651
  if (matchedPolicy?.networkAccess) return matchedPolicy.networkAccess;
10174
10652
  const cleanTarget = target.startsWith("fork:") ? target.slice(5) : target;
10175
10653
  const cmdPart = cleanTarget.split(" ")[0] || "";
10176
- const basename3 = cmdPart.includes("/") ? cmdPart.split("/").pop() : cmdPart;
10177
- if (!NETWORK_COMMANDS.has(basename3.toLowerCase())) return "none";
10654
+ const basename6 = cmdPart.includes("/") ? cmdPart.split("/").pop() : cmdPart;
10655
+ if (!NETWORK_COMMANDS.has(basename6.toLowerCase())) return "none";
10178
10656
  return "proxy";
10179
10657
  }
10180
10658
  async function buildSandboxConfig(config, matchedPolicy, _context, target) {
@@ -10223,7 +10701,7 @@ async function buildSandboxConfig(config, matchedPolicy, _context, target) {
10223
10701
  console.log(`[sandbox] direct network access (no proxy)`);
10224
10702
  sandbox.networkAllowed = true;
10225
10703
  } else if (networkMode === "proxy") {
10226
- const execId = crypto6.randomUUID();
10704
+ const execId = crypto7.randomUUID();
10227
10705
  const commandBasename = extractCommandBasename(target || "");
10228
10706
  const urlPolicies = filterUrlPoliciesForCommand(config.policies || [], commandBasename);
10229
10707
  const pool = getProxyPool();
@@ -10288,7 +10766,11 @@ async function evaluatePolicyCheck(operation, target, context) {
10288
10766
  } else if (targetType === "command") {
10289
10767
  matches = matchCommandPattern(pattern, effectiveTarget);
10290
10768
  } else {
10291
- const regex = globToRegex(pattern);
10769
+ let fsPattern = pattern;
10770
+ if (targetType === "filesystem" && fsPattern.endsWith("/")) {
10771
+ fsPattern = fsPattern + "**";
10772
+ }
10773
+ const regex = globToRegex(fsPattern);
10292
10774
  matches = regex.test(effectiveTarget);
10293
10775
  }
10294
10776
  console.log("[policy_check] pattern:", pattern, "-> base:", targetType === "url" ? normalizeUrlBase(pattern) : pattern, "| target:", effectiveTarget, "| matches:", matches);
@@ -10354,12 +10836,29 @@ async function handleHttpRequest(params) {
10354
10836
  body: responseBody
10355
10837
  };
10356
10838
  }
10839
+ async function handlePolicyCheck(params) {
10840
+ const operation = String(params["operation"] ?? "");
10841
+ const target = String(params["target"] ?? "");
10842
+ const context = params["context"];
10843
+ const result = await evaluatePolicyCheck(operation, target, context);
10844
+ if (!result.allowed) {
10845
+ if (operation === "exec") {
10846
+ emitExecDenied(target, result.reason || "Denied by policy");
10847
+ } else {
10848
+ emitInterceptorEvent({
10849
+ type: "denied",
10850
+ operation,
10851
+ target,
10852
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10853
+ policyId: result.policyId,
10854
+ error: result.reason || "Denied by policy"
10855
+ });
10856
+ }
10857
+ }
10858
+ return result;
10859
+ }
10357
10860
  var handlers = {
10358
- policy_check: (params) => evaluatePolicyCheck(
10359
- String(params["operation"] ?? ""),
10360
- String(params["target"] ?? ""),
10361
- params["context"]
10362
- ),
10861
+ policy_check: (params) => handlePolicyCheck(params),
10363
10862
  events_batch: (params) => handleEventsBatch(params),
10364
10863
  http_request: (params) => handleHttpRequest(params),
10365
10864
  ping: () => ({ status: "ok" })
@@ -10475,22 +10974,22 @@ async function registerRoutes(app) {
10475
10974
  }
10476
10975
 
10477
10976
  // libs/shield-daemon/src/static.ts
10478
- import * as fs20 from "node:fs";
10479
- import * as path20 from "node:path";
10977
+ import * as fs21 from "node:fs";
10978
+ import * as path21 from "node:path";
10480
10979
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10481
10980
  var __filename = fileURLToPath2(import.meta.url);
10482
- var __dirname = path20.dirname(__filename);
10981
+ var __dirname = path21.dirname(__filename);
10483
10982
  function getUiAssetsPath() {
10484
- const pkgRootPath = path20.join(__dirname, "..", "ui-assets");
10485
- if (fs20.existsSync(pkgRootPath)) {
10983
+ const pkgRootPath = path21.join(__dirname, "..", "ui-assets");
10984
+ if (fs21.existsSync(pkgRootPath)) {
10486
10985
  return pkgRootPath;
10487
10986
  }
10488
- const bundledPath = path20.join(__dirname, "ui-assets");
10489
- if (fs20.existsSync(bundledPath)) {
10987
+ const bundledPath = path21.join(__dirname, "ui-assets");
10988
+ if (fs21.existsSync(bundledPath)) {
10490
10989
  return bundledPath;
10491
10990
  }
10492
- const devPath = path20.join(__dirname, "..", "..", "..", "dist", "apps", "shield-ui");
10493
- if (fs20.existsSync(devPath)) {
10991
+ const devPath = path21.join(__dirname, "..", "..", "..", "dist", "apps", "shield-ui");
10992
+ if (fs21.existsSync(devPath)) {
10494
10993
  return devPath;
10495
10994
  }
10496
10995
  return null;
@@ -10580,10 +11079,16 @@ async function startServer(config) {
10580
11079
  startSecurityWatcher(1e4);
10581
11080
  const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
10582
11081
  const skillsDir2 = `${agentHome}/.openclaw/skills`;
10583
- if (!fs21.existsSync(skillsDir2)) {
10584
- fs21.mkdirSync(skillsDir2, { recursive: true, mode: 493 });
11082
+ if (!fs23.existsSync(skillsDir2)) {
11083
+ fs23.mkdirSync(skillsDir2, { recursive: true, mode: 493 });
10585
11084
  console.log(`[Daemon] Created skills directory: ${skillsDir2}`);
10586
11085
  }
11086
+ try {
11087
+ await getInstallationKey();
11088
+ console.log("[Daemon] Installation key ready");
11089
+ } catch (err) {
11090
+ console.warn("[Daemon] Failed to initialize installation key:", err.message);
11091
+ }
10587
11092
  startSkillsWatcher(skillsDir2, {
10588
11093
  onUntrustedDetected: (info) => emitSkillUntrustedDetected(info.name, info.reason),
10589
11094
  onApproved: (name) => emitSkillApproved(name)
@@ -10632,10 +11137,10 @@ async function main() {
10632
11137
  ensureConfigDir();
10633
11138
  const config = loadConfig();
10634
11139
  const pidPath = getPidPath();
10635
- fs23.writeFileSync(pidPath, process.pid.toString(), "utf-8");
11140
+ fs24.writeFileSync(pidPath, process.pid.toString(), "utf-8");
10636
11141
  const cleanup = () => {
10637
- if (fs23.existsSync(pidPath)) {
10638
- fs23.unlinkSync(pidPath);
11142
+ if (fs24.existsSync(pidPath)) {
11143
+ fs24.unlinkSync(pidPath);
10639
11144
  }
10640
11145
  process.exit(0);
10641
11146
  };