@agenshield/daemon 0.7.1 → 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/index.js CHANGED
@@ -1295,6 +1295,41 @@ var init_crypto = __esm({
1295
1295
  }
1296
1296
  });
1297
1297
 
1298
+ // libs/shield-daemon/src/vault/installation-key.ts
1299
+ import * as crypto2 from "node:crypto";
1300
+ async function getInstallationKey() {
1301
+ if (cachedKey) return cachedKey;
1302
+ const vault = getVault();
1303
+ const contents = await vault.load();
1304
+ if (contents.installationKey) {
1305
+ cachedKey = contents.installationKey;
1306
+ return cachedKey;
1307
+ }
1308
+ const key = crypto2.randomBytes(32).toString("hex");
1309
+ await vault.set("installationKey", key);
1310
+ cachedKey = key;
1311
+ console.log("[InstallationKey] Generated new installation key");
1312
+ return key;
1313
+ }
1314
+ async function getInstallationTag() {
1315
+ const key = await getInstallationKey();
1316
+ return `${INSTALLATION_KEY_PREFIX}${key}`;
1317
+ }
1318
+ function hasValidInstallationTagSync(tags) {
1319
+ if (!cachedKey) return false;
1320
+ const fullTag = `${INSTALLATION_KEY_PREFIX}${cachedKey}`;
1321
+ return tags.some((tag) => tag === fullTag);
1322
+ }
1323
+ var INSTALLATION_KEY_PREFIX, cachedKey;
1324
+ var init_installation_key = __esm({
1325
+ "libs/shield-daemon/src/vault/installation-key.ts"() {
1326
+ "use strict";
1327
+ init_vault();
1328
+ INSTALLATION_KEY_PREFIX = "agenshield-";
1329
+ cachedKey = null;
1330
+ }
1331
+ });
1332
+
1298
1333
  // libs/shield-daemon/src/vault/index.ts
1299
1334
  import * as fs4 from "node:fs";
1300
1335
  import * as path4 from "node:path";
@@ -1315,6 +1350,7 @@ var init_vault = __esm({
1315
1350
  init_crypto();
1316
1351
  init_paths();
1317
1352
  init_crypto();
1353
+ init_installation_key();
1318
1354
  Vault = class {
1319
1355
  key;
1320
1356
  vaultPath;
@@ -1849,7 +1885,7 @@ init_state();
1849
1885
  init_vault();
1850
1886
 
1851
1887
  // libs/shield-daemon/src/auth/session.ts
1852
- import * as crypto2 from "node:crypto";
1888
+ import * as crypto3 from "node:crypto";
1853
1889
  import { DEFAULT_AUTH_CONFIG } from "@agenshield/ipc";
1854
1890
  var SessionManager = class {
1855
1891
  sessions = /* @__PURE__ */ new Map();
@@ -1863,7 +1899,7 @@ var SessionManager = class {
1863
1899
  * Generate a secure random token
1864
1900
  */
1865
1901
  generateToken() {
1866
- return crypto2.randomBytes(32).toString("base64url");
1902
+ return crypto3.randomBytes(32).toString("base64url");
1867
1903
  }
1868
1904
  /**
1869
1905
  * Create a new session
@@ -2440,7 +2476,7 @@ function generatePolicyMarkdown(policies, knownSkills) {
2440
2476
  // libs/shield-daemon/src/watchers/skills.ts
2441
2477
  import * as fs11 from "node:fs";
2442
2478
  import * as path11 from "node:path";
2443
- import * as crypto3 from "node:crypto";
2479
+ import * as crypto4 from "node:crypto";
2444
2480
  import { parseSkillMd } from "@agenshield/sandbox";
2445
2481
 
2446
2482
  // libs/shield-daemon/src/services/marketplace.ts
@@ -2629,6 +2665,17 @@ function updateDownloadedAnalysis(slug, analysis) {
2629
2665
  } catch {
2630
2666
  }
2631
2667
  }
2668
+ function markDownloadedAsInstalled(slug) {
2669
+ const metaPath = path9.join(getMarketplaceDir(), slug, "metadata.json");
2670
+ try {
2671
+ if (!fs9.existsSync(metaPath)) return;
2672
+ const meta = JSON.parse(fs9.readFileSync(metaPath, "utf-8"));
2673
+ if (meta.wasInstalled) return;
2674
+ meta.wasInstalled = true;
2675
+ fs9.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
2676
+ } catch {
2677
+ }
2678
+ }
2632
2679
  function listDownloadedSkills() {
2633
2680
  const baseDir = getMarketplaceDir();
2634
2681
  if (!fs9.existsSync(baseDir)) return [];
@@ -2649,7 +2696,8 @@ function listDownloadedSkills() {
2649
2696
  tags: meta.tags ?? [],
2650
2697
  hasAnalysis: !!meta.analysis,
2651
2698
  source: meta.source,
2652
- analysis: meta.analysis
2699
+ analysis: meta.analysis,
2700
+ wasInstalled: meta.wasInstalled ?? false
2653
2701
  });
2654
2702
  } catch {
2655
2703
  }
@@ -3246,6 +3294,9 @@ function emitSkillUntrustedDetected(name, reason) {
3246
3294
  function emitSkillApproved(skillName) {
3247
3295
  daemonEvents.broadcast("skills:approved", { name: skillName });
3248
3296
  }
3297
+ function emitExecDenied(command, reason) {
3298
+ daemonEvents.broadcast("exec:denied", { command, reason });
3299
+ }
3249
3300
  function emitAgenCoAuthRequired(authUrl, integration) {
3250
3301
  daemonEvents.broadcast("agenco:auth_required", { authUrl, integration });
3251
3302
  }
@@ -3285,6 +3336,56 @@ function emitEvent(type, data) {
3285
3336
 
3286
3337
  // libs/shield-daemon/src/watchers/skills.ts
3287
3338
  init_paths();
3339
+
3340
+ // libs/shield-daemon/src/services/skill-tag-injector.ts
3341
+ init_installation_key();
3342
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
3343
+ var FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
3344
+ async function injectInstallationTag(content) {
3345
+ const tag = await getInstallationTag();
3346
+ const match = content.match(FRONTMATTER_RE);
3347
+ if (!match) {
3348
+ return `---
3349
+ tags:
3350
+ - ${tag}
3351
+ ---
3352
+ ${content}`;
3353
+ }
3354
+ try {
3355
+ const metadata = parseYaml(match[1]);
3356
+ if (!metadata || typeof metadata !== "object") {
3357
+ return content;
3358
+ }
3359
+ if (!Array.isArray(metadata.tags)) {
3360
+ metadata.tags = [];
3361
+ }
3362
+ metadata.tags = metadata.tags.filter(
3363
+ (t) => typeof t !== "string" || !t.startsWith("agenshield-")
3364
+ );
3365
+ metadata.tags.push(tag);
3366
+ return `---
3367
+ ${stringifyYaml(metadata).trimEnd()}
3368
+ ---
3369
+ ${match[2]}`;
3370
+ } catch {
3371
+ return content;
3372
+ }
3373
+ }
3374
+ function extractTagsFromSkillMd(content) {
3375
+ const match = content.match(FRONTMATTER_RE);
3376
+ if (!match) return [];
3377
+ try {
3378
+ const metadata = parseYaml(match[1]);
3379
+ if (!metadata || typeof metadata !== "object") return [];
3380
+ if (!Array.isArray(metadata.tags)) return [];
3381
+ return metadata.tags.filter((t) => typeof t === "string");
3382
+ } catch {
3383
+ return [];
3384
+ }
3385
+ }
3386
+
3387
+ // libs/shield-daemon/src/watchers/skills.ts
3388
+ init_installation_key();
3288
3389
  function getApprovedSkillsPath() {
3289
3390
  return path11.join(getSystemConfigDir(), "approved-skills.json");
3290
3391
  }
@@ -3423,7 +3524,7 @@ function computeSkillHash(skillDir) {
3423
3524
  const files = readSkillFiles(skillDir);
3424
3525
  if (files.length === 0) return null;
3425
3526
  files.sort((a, b) => a.name.localeCompare(b.name));
3426
- const hash = crypto3.createHash("sha256");
3527
+ const hash = crypto4.createHash("sha256");
3427
3528
  for (const file of files) {
3428
3529
  hash.update(file.name);
3429
3530
  hash.update(file.content);
@@ -3452,10 +3553,33 @@ function scanSkills() {
3452
3553
  const approvedEntry = approvedMap.get(skillName);
3453
3554
  if (!approvedEntry) {
3454
3555
  const fullPath = path11.join(skillsDir, skillName);
3455
- const slug = moveToMarketplace(skillName, fullPath);
3456
- if (slug) {
3457
- if (callbacks.onUntrustedDetected) {
3458
- callbacks.onUntrustedDetected({ name: skillName, reason: "Skill not in approved list" });
3556
+ let autoApproved = false;
3557
+ for (const mdName of ["SKILL.md", "skill.md"]) {
3558
+ const mdPath = path11.join(fullPath, mdName);
3559
+ try {
3560
+ if (fs11.existsSync(mdPath)) {
3561
+ const content = fs11.readFileSync(mdPath, "utf-8");
3562
+ const tags = extractTagsFromSkillMd(content);
3563
+ if (hasValidInstallationTagSync(tags)) {
3564
+ console.log(`[SkillsWatcher] Auto-approving skill with valid installation tag: ${skillName}`);
3565
+ const hash = computeSkillHash(fullPath);
3566
+ addToApprovedList(skillName, void 0, hash ?? void 0);
3567
+ if (callbacks.onApproved) {
3568
+ callbacks.onApproved(skillName);
3569
+ }
3570
+ autoApproved = true;
3571
+ break;
3572
+ }
3573
+ }
3574
+ } catch {
3575
+ }
3576
+ }
3577
+ if (!autoApproved) {
3578
+ const slug = moveToMarketplace(skillName, fullPath);
3579
+ if (slug) {
3580
+ if (callbacks.onUntrustedDetected) {
3581
+ callbacks.onUntrustedDetected({ name: skillName, reason: "Skill not in approved list" });
3582
+ }
3459
3583
  }
3460
3584
  }
3461
3585
  } else if (approvedEntry.hash) {
@@ -3561,7 +3685,7 @@ function approveSkill(skillName) {
3561
3685
  const cachedFiles = getDownloadedSkillFiles(slug);
3562
3686
  let hash;
3563
3687
  if (cachedFiles.length > 0) {
3564
- const h = crypto3.createHash("sha256");
3688
+ const h = crypto4.createHash("sha256");
3565
3689
  const sorted = [...cachedFiles].sort((a, b) => a.name.localeCompare(b.name));
3566
3690
  for (const file of sorted) {
3567
3691
  h.update(file.name);
@@ -3633,14 +3757,15 @@ function triggerSkillsScan() {
3633
3757
  function getSkillsDir() {
3634
3758
  return skillsDir;
3635
3759
  }
3636
- function addToApprovedList(skillName, publisher, hash) {
3760
+ function addToApprovedList(skillName, publisher, hash, slug) {
3637
3761
  const approved = loadApprovedSkills();
3638
3762
  if (!approved.some((s) => s.name === skillName)) {
3639
3763
  approved.push({
3640
3764
  name: skillName,
3641
3765
  approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
3642
3766
  ...publisher ? { publisher } : {},
3643
- ...hash ? { hash } : {}
3767
+ ...hash ? { hash } : {},
3768
+ ...slug ? { slug } : {}
3644
3769
  });
3645
3770
  saveApprovedSkills(approved);
3646
3771
  }
@@ -3887,7 +4012,7 @@ async function securityRoutes(app) {
3887
4012
  // libs/shield-daemon/src/auth/passcode.ts
3888
4013
  init_vault();
3889
4014
  init_state();
3890
- import * as crypto4 from "node:crypto";
4015
+ import * as crypto5 from "node:crypto";
3891
4016
  import { DEFAULT_AUTH_CONFIG as DEFAULT_AUTH_CONFIG2 } from "@agenshield/ipc";
3892
4017
  var ITERATIONS = 1e5;
3893
4018
  var KEY_LENGTH = 64;
@@ -3895,8 +4020,8 @@ var DIGEST = "sha512";
3895
4020
  var SALT_LENGTH = 16;
3896
4021
  function hashPasscode(passcode) {
3897
4022
  return new Promise((resolve3, reject) => {
3898
- const salt = crypto4.randomBytes(SALT_LENGTH);
3899
- crypto4.pbkdf2(passcode, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
4023
+ const salt = crypto5.randomBytes(SALT_LENGTH);
4024
+ crypto5.pbkdf2(passcode, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
3900
4025
  if (err) {
3901
4026
  reject(err);
3902
4027
  return;
@@ -3916,12 +4041,12 @@ function verifyPasscode(passcode, storedHash) {
3916
4041
  const iterations = parseInt(parts[0], 10);
3917
4042
  const salt = Buffer.from(parts[1], "base64");
3918
4043
  const hash = Buffer.from(parts[2], "base64");
3919
- crypto4.pbkdf2(passcode, salt, iterations, hash.length, DIGEST, (err, derivedKey) => {
4044
+ crypto5.pbkdf2(passcode, salt, iterations, hash.length, DIGEST, (err, derivedKey) => {
3920
4045
  if (err) {
3921
4046
  reject(err);
3922
4047
  return;
3923
4048
  }
3924
- resolve3(crypto4.timingSafeEqual(hash, derivedKey));
4049
+ resolve3(crypto5.timingSafeEqual(hash, derivedKey));
3925
4050
  });
3926
4051
  });
3927
4052
  }
@@ -4093,6 +4218,12 @@ data: ${data}
4093
4218
 
4094
4219
  `;
4095
4220
  }
4221
+ var ALWAYS_FULL_PREFIXES = ["skills:", "exec:", "interceptor:", "security:", "wrappers:", "process:", "config:"];
4222
+ function shouldSendFull(event, authenticated) {
4223
+ if (authenticated) return true;
4224
+ if (event.type === "heartbeat" || event.type === "daemon:status") return true;
4225
+ return ALWAYS_FULL_PREFIXES.some((p) => event.type.startsWith(p));
4226
+ }
4096
4227
  async function sseRoutes(app) {
4097
4228
  app.get("/sse/events", async (request2, reply) => {
4098
4229
  const authenticated = isAuthenticated(request2);
@@ -4118,7 +4249,7 @@ async function sseRoutes(app) {
4118
4249
  reply.raw.write(authenticated ? formatSSE(statusEvent) : formatStrippedSSE(statusEvent));
4119
4250
  const unsubscribe = daemonEvents.subscribe((event) => {
4120
4251
  try {
4121
- if (event.type === "heartbeat" || event.type === "daemon:status" || event.type.startsWith("skills:") || authenticated) {
4252
+ if (shouldSendFull(event, authenticated)) {
4122
4253
  reply.raw.write(formatSSE(event));
4123
4254
  } else {
4124
4255
  reply.raw.write(formatStrippedSSE(event));
@@ -4166,7 +4297,7 @@ async function sseRoutes(app) {
4166
4297
  const unsubscribe = daemonEvents.subscribe((event) => {
4167
4298
  if (event.type.startsWith(filter) || event.type === "heartbeat" || event.type === "daemon:status") {
4168
4299
  try {
4169
- if (event.type === "heartbeat" || event.type === "daemon:status" || authenticated) {
4300
+ if (shouldSendFull(event, authenticated)) {
4170
4301
  reply.raw.write(formatSSE(event));
4171
4302
  } else {
4172
4303
  reply.raw.write(formatStrippedSSE(event));
@@ -7944,6 +8075,7 @@ import { parseSkillMd as parseSkillMd2, extractSkillInfo } from "@agenshield/san
7944
8075
  import { execWithProgress as execWithProgress3 } from "@agenshield/sandbox";
7945
8076
  var SUPPORTED_KINDS = /* @__PURE__ */ new Set(["brew", "npm", "pip"]);
7946
8077
  var SAFE_PACKAGE_RE = /^[a-zA-Z0-9@/_.\-]+$/;
8078
+ var SYSTEM_PATH = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
7947
8079
  function findSkillMdRecursive(dir, depth = 0) {
7948
8080
  if (depth > 3) return null;
7949
8081
  try {
@@ -8007,7 +8139,7 @@ async function executeSkillInstallSteps(options) {
8007
8139
  onLog(`Installing brew formula: ${formula}`);
8008
8140
  const brewCmd = [
8009
8141
  `export HOME="${agentHome}"`,
8010
- `export PATH="${agentHome}/homebrew/bin:${agentHome}/bin:$PATH"`,
8142
+ `export PATH="${agentHome}/homebrew/bin:${agentHome}/bin:${SYSTEM_PATH}:$PATH"`,
8011
8143
  `brew install ${formula}`
8012
8144
  ].join(" && ");
8013
8145
  await execWithProgress3(
@@ -8031,7 +8163,7 @@ async function executeSkillInstallSteps(options) {
8031
8163
  onLog(`Installing npm package: ${pkg}`);
8032
8164
  const npmCmd = [
8033
8165
  `export HOME="${agentHome}"`,
8034
- `export PATH="${agentHome}/bin:$PATH"`,
8166
+ `export PATH="${agentHome}/bin:${SYSTEM_PATH}:$PATH"`,
8035
8167
  `source "${agentHome}/.nvm/nvm.sh" 2>/dev/null || true`,
8036
8168
  `npm install -g ${pkg}`
8037
8169
  ].join(" && ");
@@ -8056,7 +8188,7 @@ async function executeSkillInstallSteps(options) {
8056
8188
  onLog(`Installing pip package: ${pkg}`);
8057
8189
  const pipCmd = [
8058
8190
  `export HOME="${agentHome}"`,
8059
- `export PATH="${agentHome}/bin:$PATH"`,
8191
+ `export PATH="${agentHome}/bin:${SYSTEM_PATH}:$PATH"`,
8060
8192
  `pip install ${pkg}`
8061
8193
  ].join(" && ");
8062
8194
  await execWithProgress3(
@@ -8081,7 +8213,7 @@ async function executeSkillInstallSteps(options) {
8081
8213
  try {
8082
8214
  const checkCmd = [
8083
8215
  `export HOME="${agentHome}"`,
8084
- `export PATH="${agentHome}/homebrew/bin:${agentHome}/bin:$PATH"`,
8216
+ `export PATH="${agentHome}/homebrew/bin:${agentHome}/bin:${SYSTEM_PATH}:$PATH"`,
8085
8217
  `source "${agentHome}/.nvm/nvm.sh" 2>/dev/null || true`,
8086
8218
  `which ${bin}`
8087
8219
  ].join(" && ");
@@ -8381,18 +8513,22 @@ async function marketplaceRoutes(app) {
8381
8513
  const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "ash_default";
8382
8514
  skillDir = path16.join(skillsDir2, slug);
8383
8515
  emitSkillInstallProgress(slug, "approve", "Pre-approving skill");
8384
- addToApprovedList(slug, publisher);
8516
+ addToApprovedList(slug, publisher, void 0, slug);
8385
8517
  logs.push("Skill pre-approved");
8386
8518
  emitSkillInstallProgress(slug, "copy", "Writing skill files");
8519
+ const taggedFiles = await Promise.all(files.map(async (f) => {
8520
+ let content = f.content;
8521
+ if (/SKILL\.md$/i.test(f.name)) {
8522
+ content = stripEnvFromSkillMd(content);
8523
+ content = await injectInstallationTag(content);
8524
+ }
8525
+ return { ...f, content };
8526
+ }));
8387
8527
  const brokerAvailable = await isBrokerAvailable();
8388
8528
  if (brokerAvailable) {
8389
- const sanitizedFiles = files.map((f) => ({
8390
- name: f.name,
8391
- content: /SKILL\.md$/i.test(f.name) ? stripEnvFromSkillMd(f.content) : f.content
8392
- }));
8393
8529
  const brokerResult = await installSkillViaBroker(
8394
8530
  slug,
8395
- sanitizedFiles,
8531
+ taggedFiles.map((f) => ({ name: f.name, content: f.content })),
8396
8532
  { createWrapper: true, agentHome, socketGroup }
8397
8533
  );
8398
8534
  if (!brokerResult.installed) {
@@ -8412,29 +8548,49 @@ async function marketplaceRoutes(app) {
8412
8548
  } else {
8413
8549
  console.log(`[Marketplace] Broker unavailable, installing ${slug} directly`);
8414
8550
  sudoMkdir(skillDir, agentUsername);
8415
- for (const file of files) {
8551
+ for (const file of taggedFiles) {
8416
8552
  const filePath = path16.join(skillDir, file.name);
8417
8553
  const fileDir = path16.dirname(filePath);
8418
8554
  if (fileDir !== skillDir) {
8419
8555
  sudoMkdir(fileDir, agentUsername);
8420
8556
  }
8421
- const content = /SKILL\.md$/i.test(file.name) ? stripEnvFromSkillMd(file.content) : file.content;
8422
- sudoWriteFile(filePath, content, agentUsername);
8557
+ sudoWriteFile(filePath, file.content, agentUsername);
8423
8558
  }
8424
8559
  createSkillWrapper(slug, binDir);
8425
- logs.push(`Files written directly: ${files.length} files`);
8560
+ logs.push(`Files written directly: ${taggedFiles.length} files`);
8426
8561
  logs.push(`Wrapper created: ${path16.join(binDir, slug)}`);
8427
8562
  }
8428
8563
  let depsSuccess = true;
8429
8564
  emitSkillInstallProgress(slug, "deps", "Installing skill dependencies");
8430
8565
  try {
8566
+ let depsLineCount = 0;
8567
+ let depsLastEmit = Date.now();
8568
+ let depsLastLine = "";
8569
+ const DEPS_DEBOUNCE_MS = 3e3;
8570
+ const depsOnLog = (msg) => {
8571
+ depsLineCount++;
8572
+ depsLastLine = msg;
8573
+ if (/^(Installing|Found|Verifying)\s/.test(msg)) {
8574
+ emitSkillInstallProgress(slug, "deps", msg);
8575
+ depsLastEmit = Date.now();
8576
+ return;
8577
+ }
8578
+ const now = Date.now();
8579
+ if (now - depsLastEmit >= DEPS_DEBOUNCE_MS) {
8580
+ emitSkillInstallProgress(slug, "deps", `Installing... (${depsLineCount} lines)`);
8581
+ depsLastEmit = now;
8582
+ }
8583
+ };
8431
8584
  const depsResult = await executeSkillInstallSteps({
8432
8585
  slug,
8433
8586
  skillDir,
8434
8587
  agentHome,
8435
8588
  agentUsername,
8436
- onLog: (msg) => emitSkillInstallProgress(slug, "deps", msg)
8589
+ onLog: depsOnLog
8437
8590
  });
8591
+ if (depsLineCount > 0) {
8592
+ emitSkillInstallProgress(slug, "deps", `Dependency install complete (${depsLineCount} lines processed)`);
8593
+ }
8438
8594
  if (depsResult.installed.length > 0) {
8439
8595
  logs.push(`Dependencies installed: ${depsResult.installed.join(", ")}`);
8440
8596
  }
@@ -8460,6 +8616,10 @@ async function marketplaceRoutes(app) {
8460
8616
  updateApprovedHash(slug, hash);
8461
8617
  logs.push("Integrity hash recorded");
8462
8618
  }
8619
+ try {
8620
+ markDownloadedAsInstalled(slug);
8621
+ } catch {
8622
+ }
8463
8623
  installInProgress.delete(slug);
8464
8624
  const depsWarnings = depsSuccess ? void 0 : logs.filter((l) => l.startsWith("Dependency"));
8465
8625
  daemonEvents.broadcast("skills:installed", { name: slug, analysis: analysisResult, depsWarnings });
@@ -8637,7 +8797,30 @@ async function skillsRoutes(app) {
8637
8797
  tags: meta.tags,
8638
8798
  analysis: buildAnalysisSummary(name, getCachedAnalysis2(name))
8639
8799
  };
8640
- })
8800
+ }),
8801
+ // Disabled: previously installed marketplace skills that are no longer active
8802
+ ...(() => {
8803
+ const allKnown = /* @__PURE__ */ new Set([
8804
+ ...approvedNames,
8805
+ ...untrustedNames,
8806
+ ...workspaceNames
8807
+ ]);
8808
+ return listDownloadedSkills().filter((d) => d.wasInstalled && !allKnown.has(d.slug) && !allKnown.has(d.name)).map((d) => {
8809
+ const cached = getCachedAnalysis2(d.slug) || getCachedAnalysis2(d.name);
8810
+ return {
8811
+ name: d.slug,
8812
+ source: "marketplace",
8813
+ status: "disabled",
8814
+ path: "",
8815
+ publisher: d.author,
8816
+ description: d.description,
8817
+ version: d.version,
8818
+ author: d.author,
8819
+ tags: d.tags,
8820
+ analysis: buildAnalysisSummary(d.slug, d.analysis || cached)
8821
+ };
8822
+ });
8823
+ })()
8641
8824
  ];
8642
8825
  return reply.send({ data });
8643
8826
  });
@@ -8711,7 +8894,7 @@ async function skillsRoutes(app) {
8711
8894
  summary = {
8712
8895
  name: dlMeta.slug ?? name,
8713
8896
  source: "marketplace",
8714
- status: "downloaded",
8897
+ status: dlMeta.wasInstalled ? "disabled" : "downloaded",
8715
8898
  description: dlMeta.description,
8716
8899
  path: "",
8717
8900
  publisher: dlMeta.author,
@@ -8978,7 +9161,7 @@ async function skillsRoutes(app) {
8978
9161
  syncOpenClawFromPolicies(loadConfig().policies);
8979
9162
  removeFromApprovedList(name);
8980
9163
  try {
8981
- deleteDownloadedSkill(name);
9164
+ markDownloadedAsInstalled(name);
8982
9165
  } catch {
8983
9166
  }
8984
9167
  console.log(`[Skills] Disabled marketplace skill: ${name}`);
@@ -8998,12 +9181,20 @@ async function skillsRoutes(app) {
8998
9181
  return reply.code(404).send({ error: "No files in download cache for this skill" });
8999
9182
  }
9000
9183
  try {
9001
- addToApprovedList(name, meta.author);
9184
+ addToApprovedList(name, meta.author, void 0, meta.slug);
9185
+ const taggedFiles = await Promise.all(files.map(async (f) => {
9186
+ let content = f.content;
9187
+ if (/SKILL\.md$/i.test(f.name)) {
9188
+ content = stripEnvFromSkillMd2(content);
9189
+ content = await injectInstallationTag(content);
9190
+ }
9191
+ return { name: f.name, content, type: f.type };
9192
+ }));
9002
9193
  const brokerAvailable = await isBrokerAvailable();
9003
9194
  if (brokerAvailable) {
9004
9195
  const brokerResult = await installSkillViaBroker(
9005
9196
  name,
9006
- files.map((f) => ({ name: f.name, content: f.content })),
9197
+ taggedFiles.map((f) => ({ name: f.name, content: f.content })),
9007
9198
  { createWrapper: true, agentHome, socketGroup }
9008
9199
  );
9009
9200
  if (!brokerResult.installed) {
@@ -9016,7 +9207,7 @@ async function skillsRoutes(app) {
9016
9207
  }
9017
9208
  } else {
9018
9209
  fs17.mkdirSync(skillDir, { recursive: true });
9019
- for (const file of files) {
9210
+ for (const file of taggedFiles) {
9020
9211
  const filePath = path17.join(skillDir, file.name);
9021
9212
  fs17.mkdirSync(path17.dirname(filePath), { recursive: true });
9022
9213
  fs17.writeFileSync(filePath, file.content, "utf-8");
@@ -9033,6 +9224,10 @@ async function skillsRoutes(app) {
9033
9224
  syncOpenClawFromPolicies(loadConfig().policies);
9034
9225
  const hash = computeSkillHash(skillDir);
9035
9226
  if (hash) updateApprovedHash(name, hash);
9227
+ try {
9228
+ markDownloadedAsInstalled(name);
9229
+ } catch {
9230
+ }
9036
9231
  console.log(`[Skills] Enabled marketplace skill: ${name}`);
9037
9232
  return reply.send({ success: true, action: "enabled", name });
9038
9233
  } catch (err) {
@@ -9082,12 +9277,19 @@ async function skillsRoutes(app) {
9082
9277
  const skillDir = path17.join(skillsDir2, name);
9083
9278
  try {
9084
9279
  addToApprovedList(name, publisher);
9280
+ const taggedFiles = await Promise.all(files.map(async (f) => {
9281
+ let content = f.content;
9282
+ if (/SKILL\.md$/i.test(f.name)) {
9283
+ content = stripEnvFromSkillMd2(content);
9284
+ content = await injectInstallationTag(content);
9285
+ }
9286
+ return { name: f.name, content };
9287
+ }));
9085
9288
  sudoMkdir(skillDir, agentUsername);
9086
- for (const file of files) {
9289
+ for (const file of taggedFiles) {
9087
9290
  const filePath = path17.join(skillDir, file.name);
9088
9291
  sudoMkdir(path17.dirname(filePath), agentUsername);
9089
- const content = /SKILL\.md$/i.test(file.name) ? stripEnvFromSkillMd2(file.content) : file.content;
9090
- sudoWriteFile(filePath, content, agentUsername);
9292
+ sudoWriteFile(filePath, file.content, agentUsername);
9091
9293
  }
9092
9294
  try {
9093
9295
  execSync9(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
@@ -9139,12 +9341,20 @@ async function skillsRoutes(app) {
9139
9341
  const binDir = path17.join(agentHome, "bin");
9140
9342
  const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "ash_default";
9141
9343
  const skillDir = path17.join(getSkillsDir(), name);
9142
- addToApprovedList(name, meta.author);
9344
+ addToApprovedList(name, meta.author, void 0, meta.slug);
9345
+ const taggedFiles = await Promise.all(files.map(async (f) => {
9346
+ let content = f.content;
9347
+ if (/SKILL\.md$/i.test(f.name)) {
9348
+ content = stripEnvFromSkillMd2(content);
9349
+ content = await injectInstallationTag(content);
9350
+ }
9351
+ return { name: f.name, content, type: f.type };
9352
+ }));
9143
9353
  const brokerAvailable = await isBrokerAvailable();
9144
9354
  if (brokerAvailable) {
9145
9355
  const brokerResult = await installSkillViaBroker(
9146
9356
  name,
9147
- files.map((f) => ({ name: f.name, content: f.content })),
9357
+ taggedFiles.map((f) => ({ name: f.name, content: f.content })),
9148
9358
  { createWrapper: true, agentHome, socketGroup }
9149
9359
  );
9150
9360
  if (!brokerResult.installed) {
@@ -9157,7 +9367,7 @@ async function skillsRoutes(app) {
9157
9367
  }
9158
9368
  } else {
9159
9369
  fs17.mkdirSync(skillDir, { recursive: true });
9160
- for (const file of files) {
9370
+ for (const file of taggedFiles) {
9161
9371
  const filePath = path17.join(skillDir, file.name);
9162
9372
  fs17.mkdirSync(path17.dirname(filePath), { recursive: true });
9163
9373
  fs17.writeFileSync(filePath, file.content, "utf-8");
@@ -9174,6 +9384,10 @@ async function skillsRoutes(app) {
9174
9384
  syncOpenClawFromPolicies(loadConfig().policies);
9175
9385
  const unblockHash = computeSkillHash(path17.join(getSkillsDir(), name));
9176
9386
  if (unblockHash) updateApprovedHash(name, unblockHash);
9387
+ try {
9388
+ markDownloadedAsInstalled(name);
9389
+ } catch {
9390
+ }
9177
9391
  console.log(`[Skills] Unblocked and installed skill: ${name}`);
9178
9392
  return reply.send({ success: true, message: `Skill "${name}" approved and installed` });
9179
9393
  } catch (err) {
@@ -9673,7 +9887,7 @@ async function authRoutes(app) {
9673
9887
  init_vault();
9674
9888
  import { isSecretEnvVar } from "@agenshield/sandbox";
9675
9889
  init_secret_sync();
9676
- import crypto5 from "node:crypto";
9890
+ import crypto6 from "node:crypto";
9677
9891
  function maskValue(value) {
9678
9892
  if (value.length <= 4) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
9679
9893
  return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + value.slice(-4);
@@ -9768,7 +9982,7 @@ async function secretsRoutes(app) {
9768
9982
  const secrets = await vault.get("secrets") ?? [];
9769
9983
  const resolvedScope = scope ?? (policyIds?.length > 0 ? "policed" : "global");
9770
9984
  const newSecret = {
9771
- id: crypto5.randomUUID(),
9985
+ id: crypto6.randomUUID(),
9772
9986
  name: name.trim(),
9773
9987
  value,
9774
9988
  policyIds: resolvedScope === "standalone" ? [] : policyIds ?? [],
@@ -10069,7 +10283,7 @@ async function openclawRoutes(app) {
10069
10283
  }
10070
10284
 
10071
10285
  // libs/shield-daemon/src/routes/rpc.ts
10072
- import * as crypto6 from "node:crypto";
10286
+ import * as crypto7 from "node:crypto";
10073
10287
  import * as nodefs from "node:fs";
10074
10288
 
10075
10289
  // libs/shield-daemon/src/policy/url-matcher.ts
@@ -10183,7 +10397,7 @@ function checkUrlPolicy(policies, url) {
10183
10397
  // libs/shield-daemon/src/proxy/server.ts
10184
10398
  import * as http from "node:http";
10185
10399
  import * as net from "node:net";
10186
- function createPerRunProxy(urlPolicies, onActivity, logger) {
10400
+ function createPerRunProxy(urlPolicies, onActivity, logger, onBlock) {
10187
10401
  const server = http.createServer((req, res) => {
10188
10402
  onActivity();
10189
10403
  const url = req.url;
@@ -10195,6 +10409,7 @@ function createPerRunProxy(urlPolicies, onActivity, logger) {
10195
10409
  const allowed = checkUrlPolicy(urlPolicies, url);
10196
10410
  if (!allowed) {
10197
10411
  logger(`BLOCKED HTTP ${req.method} ${url}`);
10412
+ onBlock?.(req.method || "GET", url, "http");
10198
10413
  res.writeHead(403, { "Content-Type": "text/plain" });
10199
10414
  res.end("Blocked by AgenShield URL policy");
10200
10415
  return;
@@ -10238,6 +10453,7 @@ function createPerRunProxy(urlPolicies, onActivity, logger) {
10238
10453
  const allowed = checkUrlPolicy(urlPolicies, `https://${hostname2}`);
10239
10454
  if (!allowed) {
10240
10455
  logger(`BLOCKED CONNECT ${hostname2}:${port}`);
10456
+ onBlock?.("CONNECT", `${hostname2}:${port}`, "https");
10241
10457
  clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
10242
10458
  clientSocket.destroy();
10243
10459
  return;
@@ -10300,7 +10516,16 @@ var ProxyPool = class {
10300
10516
  const logger = (msg) => {
10301
10517
  console.log(`[proxy:${execId.slice(0, 8)}] ${msg}`);
10302
10518
  };
10303
- const server = createPerRunProxy(urlPolicies, onActivity, logger);
10519
+ const onBlock = (method, target, protocol) => {
10520
+ emitInterceptorEvent({
10521
+ type: "denied",
10522
+ operation: "http_request",
10523
+ target: protocol === "https" ? `https://${target}` : target,
10524
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10525
+ error: `Blocked by URL policy (${method})`
10526
+ });
10527
+ };
10528
+ const server = createPerRunProxy(urlPolicies, onActivity, logger, onBlock);
10304
10529
  const port = await new Promise((resolve3, reject) => {
10305
10530
  server.listen(0, "127.0.0.1", () => {
10306
10531
  const addr = server.address();
@@ -10383,12 +10608,19 @@ function matchCommandPattern(pattern, target) {
10383
10608
  normalizedTarget = firstSpace >= 0 ? basename6 + target.slice(firstSpace) : basename6;
10384
10609
  }
10385
10610
  if (trimmed.endsWith(":*")) {
10386
- const prefix = trimmed.slice(0, -2);
10611
+ let prefix = trimmed.slice(0, -2);
10612
+ if (prefix.includes("/")) {
10613
+ prefix = prefix.split("/").pop() || prefix;
10614
+ }
10387
10615
  const lowerTarget = normalizedTarget.toLowerCase();
10388
10616
  const lowerPrefix = prefix.toLowerCase();
10389
10617
  return lowerTarget === lowerPrefix || lowerTarget.startsWith(lowerPrefix + " ");
10390
10618
  }
10391
- return normalizedTarget.toLowerCase() === trimmed.toLowerCase();
10619
+ let normalizedPattern = trimmed;
10620
+ if (trimmed.includes("/")) {
10621
+ normalizedPattern = trimmed.split("/").pop() || trimmed;
10622
+ }
10623
+ return normalizedTarget.toLowerCase() === normalizedPattern.toLowerCase();
10392
10624
  }
10393
10625
  function operationToTarget(operation) {
10394
10626
  switch (operation) {
@@ -10481,7 +10713,7 @@ async function buildSandboxConfig(config, matchedPolicy, _context, target) {
10481
10713
  console.log(`[sandbox] direct network access (no proxy)`);
10482
10714
  sandbox.networkAllowed = true;
10483
10715
  } else if (networkMode === "proxy") {
10484
- const execId = crypto6.randomUUID();
10716
+ const execId = crypto7.randomUUID();
10485
10717
  const commandBasename = extractCommandBasename(target || "");
10486
10718
  const urlPolicies = filterUrlPoliciesForCommand(config.policies || [], commandBasename);
10487
10719
  const pool = getProxyPool();
@@ -10546,7 +10778,11 @@ async function evaluatePolicyCheck(operation, target, context) {
10546
10778
  } else if (targetType === "command") {
10547
10779
  matches = matchCommandPattern(pattern, effectiveTarget);
10548
10780
  } else {
10549
- const regex = globToRegex(pattern);
10781
+ let fsPattern = pattern;
10782
+ if (targetType === "filesystem" && fsPattern.endsWith("/")) {
10783
+ fsPattern = fsPattern + "**";
10784
+ }
10785
+ const regex = globToRegex(fsPattern);
10550
10786
  matches = regex.test(effectiveTarget);
10551
10787
  }
10552
10788
  console.log("[policy_check] pattern:", pattern, "-> base:", targetType === "url" ? normalizeUrlBase(pattern) : pattern, "| target:", effectiveTarget, "| matches:", matches);
@@ -10612,12 +10848,29 @@ async function handleHttpRequest(params) {
10612
10848
  body: responseBody
10613
10849
  };
10614
10850
  }
10851
+ async function handlePolicyCheck(params) {
10852
+ const operation = String(params["operation"] ?? "");
10853
+ const target = String(params["target"] ?? "");
10854
+ const context = params["context"];
10855
+ const result = await evaluatePolicyCheck(operation, target, context);
10856
+ if (!result.allowed) {
10857
+ if (operation === "exec") {
10858
+ emitExecDenied(target, result.reason || "Denied by policy");
10859
+ } else {
10860
+ emitInterceptorEvent({
10861
+ type: "denied",
10862
+ operation,
10863
+ target,
10864
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10865
+ policyId: result.policyId,
10866
+ error: result.reason || "Denied by policy"
10867
+ });
10868
+ }
10869
+ }
10870
+ return result;
10871
+ }
10615
10872
  var handlers = {
10616
- policy_check: (params) => evaluatePolicyCheck(
10617
- String(params["operation"] ?? ""),
10618
- String(params["target"] ?? ""),
10619
- params["context"]
10620
- ),
10873
+ policy_check: (params) => handlePolicyCheck(params),
10621
10874
  events_batch: (params) => handleEventsBatch(params),
10622
10875
  http_request: (params) => handleHttpRequest(params),
10623
10876
  ping: () => ({ status: "ok" })
@@ -10842,6 +11095,12 @@ async function startServer(config) {
10842
11095
  fs23.mkdirSync(skillsDir2, { recursive: true, mode: 493 });
10843
11096
  console.log(`[Daemon] Created skills directory: ${skillsDir2}`);
10844
11097
  }
11098
+ try {
11099
+ await getInstallationKey();
11100
+ console.log("[Daemon] Installation key ready");
11101
+ } catch (err) {
11102
+ console.warn("[Daemon] Failed to initialize installation key:", err.message);
11103
+ }
10845
11104
  startSkillsWatcher(skillsDir2, {
10846
11105
  onUntrustedDetected: (info) => emitSkillUntrustedDetected(info.name, info.reason),
10847
11106
  onApproved: (name) => emitSkillApproved(name)