@agenshield/daemon 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -5422,6 +5422,20 @@ function mapSearchResult(result) {
5422
5422
  function isTextMime(mime) {
5423
5423
  return mime.startsWith("text/") || mime === "application/json" || mime === "text/yaml" || mime === "text/toml";
5424
5424
  }
5425
+ var IMAGE_EXT_MAP = {
5426
+ ".png": "image/png",
5427
+ ".jpg": "image/jpeg",
5428
+ ".jpeg": "image/jpeg",
5429
+ ".gif": "image/gif",
5430
+ ".svg": "image/svg+xml",
5431
+ ".webp": "image/webp",
5432
+ ".ico": "image/x-icon"
5433
+ };
5434
+ function isImageExt(filePath) {
5435
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
5436
+ return IMAGE_EXT_MAP[ext] ?? null;
5437
+ }
5438
+ var MAX_IMAGE_SIZE = 5e5;
5425
5439
  function getMarketplaceDir() {
5426
5440
  return path11.join(os4.homedir(), CONFIG_DIR2, MARKETPLACE_DIR);
5427
5441
  }
@@ -5444,9 +5458,19 @@ async function downloadAndExtractZip(slug) {
5444
5458
  const filename = zipPath.split("/").pop() || "";
5445
5459
  if (filename.startsWith(".")) continue;
5446
5460
  const mimeType = guessContentType(zipPath);
5447
- if (!isTextMime(mimeType)) continue;
5448
- const content = await zipEntry.async("text");
5449
- files.push({ name: zipPath, type: mimeType, content });
5461
+ if (isTextMime(mimeType)) {
5462
+ const content = await zipEntry.async("text");
5463
+ files.push({ name: zipPath, type: mimeType, content });
5464
+ continue;
5465
+ }
5466
+ const imageMime = isImageExt(zipPath);
5467
+ if (imageMime) {
5468
+ const buf = await zipEntry.async("nodebuffer");
5469
+ if (buf.length <= MAX_IMAGE_SIZE) {
5470
+ const dataUri = `data:${imageMime};base64,${buf.toString("base64")}`;
5471
+ files.push({ name: zipPath, type: imageMime, content: dataUri });
5472
+ }
5473
+ }
5450
5474
  }
5451
5475
  return files;
5452
5476
  }
@@ -5528,6 +5552,34 @@ function getDownloadedSkillMeta(slug) {
5528
5552
  }
5529
5553
  return null;
5530
5554
  }
5555
+ function inlineImagesInMarkdown(markdown, files) {
5556
+ const imageMap = /* @__PURE__ */ new Map();
5557
+ for (const file of files) {
5558
+ const mime = isImageExt(file.name);
5559
+ if (mime && file.content.startsWith("data:")) {
5560
+ imageMap.set(file.name, file.content);
5561
+ const basename2 = file.name.split("/").pop() ?? "";
5562
+ if (basename2 && !imageMap.has(basename2)) {
5563
+ imageMap.set(basename2, file.content);
5564
+ }
5565
+ }
5566
+ }
5567
+ if (imageMap.size === 0) return markdown;
5568
+ return markdown.replace(
5569
+ /!\[([^\]]*)\]\(([^)]+)\)/g,
5570
+ (_match, alt, src) => {
5571
+ if (src.startsWith("http://") || src.startsWith("https://") || src.startsWith("data:")) {
5572
+ return _match;
5573
+ }
5574
+ const normalized = src.replace(/^\.\//, "");
5575
+ const dataUri = imageMap.get(src) ?? imageMap.get(normalized) ?? imageMap.get(normalized.split("/").pop() ?? "");
5576
+ if (dataUri) {
5577
+ return `![${alt}](${dataUri})`;
5578
+ }
5579
+ return _match;
5580
+ }
5581
+ );
5582
+ }
5531
5583
  async function searchMarketplace(query) {
5532
5584
  const cacheKey = `search:${query}`;
5533
5585
  const cached = getCached(cacheKey);
@@ -5612,6 +5664,9 @@ async function getMarketplaceSkill(slug) {
5612
5664
  }
5613
5665
  }
5614
5666
  const tags = tagsFromRecord(skill.tags);
5667
+ if (readme && files.length > 0) {
5668
+ readme = inlineImagesInMarkdown(readme, files);
5669
+ }
5615
5670
  const mapped = {
5616
5671
  name: skill.displayName,
5617
5672
  slug: skill.slug,
@@ -5749,1063 +5804,1203 @@ async function getCachedAnalysis2(skillName, publisher) {
5749
5804
  };
5750
5805
  }
5751
5806
 
5752
- // libs/shield-daemon/src/routes/skills.ts
5753
- async function skillsRoutes(app) {
5754
- app.get("/skills", async (_request, reply) => {
5755
- const approved = listApproved();
5756
- const quarantined = listQuarantined();
5757
- const downloaded = listDownloadedSkills();
5758
- const approvedNames = new Set(approved.map((a) => a.name));
5759
- const availableDownloads = downloaded.filter((d) => !approvedNames.has(d.slug));
5760
- const skillsDir2 = getSkillsDir();
5761
- const data = [
5762
- // Approved → active
5763
- ...approved.map((a) => ({
5764
- name: a.name,
5765
- source: "user",
5766
- status: "active",
5767
- path: path12.join(skillsDir2 ?? "", a.name),
5768
- publisher: a.publisher,
5769
- description: void 0
5770
- })),
5771
- // Quarantined
5772
- ...quarantined.map((q) => ({
5773
- name: q.name,
5774
- source: "quarantine",
5775
- status: "quarantined",
5776
- path: q.originalPath,
5777
- description: void 0
5778
- })),
5779
- // Downloaded (not installed) available
5780
- ...availableDownloads.map((d) => ({
5781
- name: d.slug,
5782
- source: "marketplace",
5783
- status: "downloaded",
5784
- description: d.description,
5785
- path: "",
5786
- publisher: d.author
5787
- }))
5788
- ];
5789
- return reply.send({ data });
5790
- });
5791
- app.get("/skills/quarantined", async (_request, reply) => {
5792
- const quarantined = listQuarantined();
5793
- return reply.send({ quarantined });
5794
- });
5795
- app.get(
5796
- "/skills/:name",
5797
- async (request, reply) => {
5798
- const { name } = request.params;
5799
- if (!name || typeof name !== "string") {
5800
- return reply.code(400).send({ error: "Skill name is required" });
5801
- }
5802
- const analysis = getCachedAnalysis(name);
5803
- const approved = listApproved();
5804
- const entry = approved.find((s) => s.name === name);
5805
- return reply.send({
5806
- success: true,
5807
- data: {
5808
- name,
5809
- analysis: analysis ?? null,
5810
- publisher: entry?.publisher ?? null
5811
- }
5812
- });
5807
+ // libs/shield-broker/dist/index.js
5808
+ import { exec } from "node:child_process";
5809
+ import { promisify } from "node:util";
5810
+ import * as net2 from "node:net";
5811
+ import { randomUUID as randomUUID3 } from "node:crypto";
5812
+ var MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
5813
+ var execAsync = promisify(exec);
5814
+ var BrokerClient = class {
5815
+ socketPath;
5816
+ httpHost;
5817
+ httpPort;
5818
+ timeout;
5819
+ preferSocket;
5820
+ constructor(options = {}) {
5821
+ this.socketPath = options.socketPath || "/var/run/agenshield/agenshield.sock";
5822
+ this.httpHost = options.httpHost || "localhost";
5823
+ this.httpPort = options.httpPort || 5201;
5824
+ this.timeout = options.timeout || 3e4;
5825
+ this.preferSocket = options.preferSocket ?? true;
5826
+ }
5827
+ /**
5828
+ * Make an HTTP request through the broker
5829
+ */
5830
+ async httpRequest(params, options) {
5831
+ return this.request("http_request", params, options);
5832
+ }
5833
+ /**
5834
+ * Read a file through the broker
5835
+ */
5836
+ async fileRead(params, options) {
5837
+ return this.request("file_read", params, options);
5838
+ }
5839
+ /**
5840
+ * Write a file through the broker
5841
+ */
5842
+ async fileWrite(params, options) {
5843
+ return this.request("file_write", params, {
5844
+ ...options,
5845
+ channel: "socket"
5846
+ // file_write only allowed via socket
5847
+ });
5848
+ }
5849
+ /**
5850
+ * List files through the broker
5851
+ */
5852
+ async fileList(params, options) {
5853
+ return this.request("file_list", params, options);
5854
+ }
5855
+ /**
5856
+ * Execute a command through the broker
5857
+ */
5858
+ async exec(params, options) {
5859
+ return this.request("exec", params, {
5860
+ ...options,
5861
+ channel: "socket"
5862
+ // exec only allowed via socket
5863
+ });
5864
+ }
5865
+ /**
5866
+ * Open a URL through the broker
5867
+ */
5868
+ async openUrl(params, options) {
5869
+ return this.request("open_url", params, options);
5870
+ }
5871
+ /**
5872
+ * Inject a secret through the broker
5873
+ */
5874
+ async secretInject(params, options) {
5875
+ return this.request("secret_inject", params, {
5876
+ ...options,
5877
+ channel: "socket"
5878
+ // secret_inject only allowed via socket
5879
+ });
5880
+ }
5881
+ /**
5882
+ * Ping the broker
5883
+ */
5884
+ async ping(echo, options) {
5885
+ return this.request("ping", { echo }, options);
5886
+ }
5887
+ /**
5888
+ * Install a skill through the broker
5889
+ * Socket-only operation due to privileged file operations
5890
+ */
5891
+ async skillInstall(params, options) {
5892
+ return this.request("skill_install", params, {
5893
+ ...options,
5894
+ channel: "socket"
5895
+ // skill_install only allowed via socket
5896
+ });
5897
+ }
5898
+ /**
5899
+ * Uninstall a skill through the broker
5900
+ * Socket-only operation due to privileged file operations
5901
+ */
5902
+ async skillUninstall(params, options) {
5903
+ return this.request("skill_uninstall", params, {
5904
+ ...options,
5905
+ channel: "socket"
5906
+ // skill_uninstall only allowed via socket
5907
+ });
5908
+ }
5909
+ /**
5910
+ * Check if the broker is available
5911
+ */
5912
+ async isAvailable() {
5913
+ try {
5914
+ await this.ping();
5915
+ return true;
5916
+ } catch {
5917
+ return false;
5813
5918
  }
5814
- );
5815
- app.post(
5816
- "/skills/:name/analyze",
5817
- async (request, reply) => {
5818
- const { name } = request.params;
5819
- const { content, metadata } = request.body ?? {};
5820
- if (!name || typeof name !== "string") {
5821
- return reply.code(400).send({ error: "Skill name is required" });
5822
- }
5823
- clearCachedAnalysis(name);
5824
- if (content) {
5825
- const analysis = analyzeSkill(name, content, metadata);
5826
- return reply.send({ success: true, data: { analysis } });
5827
- }
5828
- return reply.send({
5829
- success: true,
5830
- data: {
5831
- analysis: {
5832
- status: "pending",
5833
- analyzerId: "agenshield",
5834
- commands: []
5835
- }
5919
+ }
5920
+ /**
5921
+ * Make a request to the broker
5922
+ */
5923
+ async request(method, params, options) {
5924
+ const channel = options?.channel || (this.preferSocket ? "socket" : "http");
5925
+ const timeout = options?.timeout || this.timeout;
5926
+ if (channel === "socket") {
5927
+ try {
5928
+ return await this.socketRequest(method, params, timeout);
5929
+ } catch (error) {
5930
+ if (!options?.channel) {
5931
+ return await this.httpRequest_internal(method, params, timeout);
5836
5932
  }
5837
- });
5838
- }
5839
- );
5840
- app.post(
5841
- "/skills/:name/approve",
5842
- async (request, reply) => {
5843
- const { name } = request.params;
5844
- if (!name || typeof name !== "string") {
5845
- return reply.code(400).send({ error: "Skill name is required" });
5846
- }
5847
- const result = approveSkill(name);
5848
- if (!result.success) {
5849
- return reply.code(404).send({ error: result.error });
5850
- }
5851
- return reply.send({ success: true, message: `Skill "${name}" approved` });
5852
- }
5853
- );
5854
- app.delete(
5855
- "/skills/:name",
5856
- async (request, reply) => {
5857
- const { name } = request.params;
5858
- if (!name || typeof name !== "string") {
5859
- return reply.code(400).send({ error: "Skill name is required" });
5860
- }
5861
- const result = rejectSkill(name);
5862
- if (!result.success) {
5863
- return reply.code(404).send({ error: result.error });
5933
+ throw error;
5864
5934
  }
5865
- return reply.send({ success: true, message: `Skill "${name}" rejected and deleted` });
5935
+ } else {
5936
+ return await this.httpRequest_internal(method, params, timeout);
5866
5937
  }
5867
- );
5868
- app.post(
5869
- "/skills/:name/revoke",
5870
- async (request, reply) => {
5871
- const { name } = request.params;
5872
- if (!name || typeof name !== "string") {
5873
- return reply.code(400).send({ error: "Skill name is required" });
5874
- }
5875
- const result = revokeSkill(name);
5876
- if (!result.success) {
5877
- return reply.code(500).send({ error: result.error });
5878
- }
5879
- return reply.send({ success: true, message: `Skill "${name}" approval revoked` });
5880
- }
5881
- );
5882
- app.put(
5883
- "/skills/:name/toggle",
5884
- async (request, reply) => {
5885
- const { name } = request.params;
5886
- if (!name || typeof name !== "string") {
5887
- return reply.code(400).send({ error: "Skill name is required" });
5888
- }
5889
- const skillsDir2 = getSkillsDir();
5890
- if (!skillsDir2) {
5891
- return reply.code(500).send({ error: "Skills directory not configured" });
5892
- }
5893
- const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
5894
- const binDir = path12.join(agentHome, "bin");
5895
- const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "clawshield";
5896
- const skillDir = path12.join(skillsDir2, name);
5897
- const isInstalled = fs13.existsSync(skillDir);
5898
- if (isInstalled) {
5899
- try {
5900
- fs13.rmSync(skillDir, { recursive: true, force: true });
5901
- removeSkillWrapper(name, binDir);
5902
- removeSkillEntry(name);
5903
- removeSkillPolicy(name);
5904
- removeFromApprovedList(name);
5905
- console.log(`[Skills] Disabled marketplace skill: ${name}`);
5906
- return reply.send({ success: true, action: "disabled", name });
5907
- } catch (err) {
5908
- console.error("[Skills] Disable failed:", err.message);
5909
- return reply.code(500).send({ error: `Disable failed: ${err.message}` });
5910
- }
5911
- } else {
5912
- const meta = getDownloadedSkillMeta(name);
5913
- if (!meta) {
5914
- return reply.code(404).send({ error: "Skill not found in download cache" });
5915
- }
5916
- const files = getDownloadedSkillFiles(name);
5917
- if (files.length === 0) {
5918
- return reply.code(404).send({ error: "No files in download cache for this skill" });
5919
- }
5920
- try {
5921
- addToApprovedList(name, meta.author);
5922
- fs13.mkdirSync(skillDir, { recursive: true });
5923
- for (const file of files) {
5924
- const filePath = path12.join(skillDir, file.name);
5925
- fs13.mkdirSync(path12.dirname(filePath), { recursive: true });
5926
- fs13.writeFileSync(filePath, file.content, "utf-8");
5927
- }
5928
- try {
5929
- execSync8(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
5930
- execSync8(`chmod -R a+rX,go-w "${skillDir}"`, { stdio: "pipe" });
5931
- } catch {
5932
- }
5933
- createSkillWrapper(name, binDir);
5934
- addSkillEntry(name);
5935
- addSkillPolicy(name);
5936
- console.log(`[Skills] Enabled marketplace skill: ${name}`);
5937
- return reply.send({ success: true, action: "enabled", name });
5938
- } catch (err) {
5938
+ }
5939
+ /**
5940
+ * Make a request via Unix socket
5941
+ */
5942
+ async socketRequest(method, params, timeout) {
5943
+ return new Promise((resolve3, reject) => {
5944
+ const socket = net2.createConnection(this.socketPath);
5945
+ const id = randomUUID3();
5946
+ let responseData = "";
5947
+ let timeoutId;
5948
+ socket.on("connect", () => {
5949
+ const request = {
5950
+ jsonrpc: "2.0",
5951
+ id,
5952
+ method,
5953
+ params
5954
+ };
5955
+ socket.write(JSON.stringify(request) + "\n");
5956
+ timeoutId = setTimeout(() => {
5957
+ socket.destroy();
5958
+ reject(new Error("Request timeout"));
5959
+ }, timeout);
5960
+ });
5961
+ socket.on("data", (data) => {
5962
+ responseData += data.toString();
5963
+ const newlineIndex = responseData.indexOf("\n");
5964
+ if (newlineIndex !== -1) {
5965
+ clearTimeout(timeoutId);
5966
+ socket.end();
5939
5967
  try {
5940
- if (fs13.existsSync(skillDir)) {
5941
- fs13.rmSync(skillDir, { recursive: true, force: true });
5968
+ const response = JSON.parse(
5969
+ responseData.slice(0, newlineIndex)
5970
+ );
5971
+ if (response.error) {
5972
+ const error = new Error(response.error.message);
5973
+ error.code = response.error.code;
5974
+ reject(error);
5975
+ } else {
5976
+ resolve3(response.result);
5942
5977
  }
5943
- removeFromApprovedList(name);
5944
- } catch {
5978
+ } catch (error) {
5979
+ reject(new Error("Invalid response from broker"));
5945
5980
  }
5946
- console.error("[Skills] Enable failed:", err.message);
5947
- return reply.code(500).send({ error: `Enable failed: ${err.message}` });
5948
5981
  }
5982
+ });
5983
+ socket.on("error", (error) => {
5984
+ clearTimeout(timeoutId);
5985
+ reject(error);
5986
+ });
5987
+ });
5988
+ }
5989
+ /**
5990
+ * Make a request via HTTP
5991
+ */
5992
+ async httpRequest_internal(method, params, timeout) {
5993
+ const url = `http://${this.httpHost}:${this.httpPort}/rpc`;
5994
+ const id = randomUUID3();
5995
+ const request = {
5996
+ jsonrpc: "2.0",
5997
+ id,
5998
+ method,
5999
+ params
6000
+ };
6001
+ const controller = new AbortController();
6002
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
6003
+ try {
6004
+ const response = await fetch(url, {
6005
+ method: "POST",
6006
+ headers: { "Content-Type": "application/json" },
6007
+ body: JSON.stringify(request),
6008
+ signal: controller.signal
6009
+ });
6010
+ clearTimeout(timeoutId);
6011
+ if (!response.ok) {
6012
+ throw new Error(`HTTP error: ${response.status}`);
5949
6013
  }
5950
- }
5951
- );
5952
- app.post(
5953
- "/skills/install",
5954
- { preHandler: [requireAuth] },
5955
- async (request, reply) => {
5956
- const { name, files, publisher } = request.body ?? {};
5957
- if (!name || typeof name !== "string") {
5958
- return reply.code(400).send({ error: "Skill name is required" });
5959
- }
5960
- if (!Array.isArray(files) || files.length === 0) {
5961
- return reply.code(400).send({ error: "Files array is required" });
5962
- }
5963
- const combinedContent = files.map((f) => f.content).join("\n");
5964
- const skillMdFile = files.find((f) => f.name === "SKILL.md");
5965
- let metadata;
5966
- if (skillMdFile) {
5967
- const parsed = parseSkillMd(skillMdFile.content);
5968
- metadata = parsed?.metadata;
5969
- }
5970
- const analysis = analyzeSkill(name, combinedContent, metadata);
5971
- if (analysis.vulnerability?.level === "critical") {
5972
- return reply.code(400).send({ error: "Critical vulnerability detected", analysis });
5973
- }
5974
- const skillsDir2 = getSkillsDir();
5975
- if (!skillsDir2) {
5976
- return reply.code(500).send({ error: "Skills directory not configured" });
6014
+ const jsonResponse = await response.json();
6015
+ if (jsonResponse.error) {
6016
+ const error = new Error(jsonResponse.error.message);
6017
+ error.code = jsonResponse.error.code;
6018
+ throw error;
5977
6019
  }
5978
- const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
5979
- const binDir = path12.join(agentHome, "bin");
5980
- const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "clawshield";
5981
- const skillDir = path12.join(skillsDir2, name);
5982
- try {
5983
- addToApprovedList(name, publisher);
5984
- fs13.mkdirSync(skillDir, { recursive: true });
5985
- for (const file of files) {
5986
- const filePath = path12.join(skillDir, file.name);
5987
- fs13.mkdirSync(path12.dirname(filePath), { recursive: true });
5988
- fs13.writeFileSync(filePath, file.content, "utf-8");
5989
- }
5990
- try {
5991
- execSync8(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
5992
- execSync8(`chmod -R a+rX,go-w "${skillDir}"`, { stdio: "pipe" });
5993
- } catch {
5994
- }
5995
- createSkillWrapper(name, binDir);
5996
- addSkillPolicy(name);
5997
- return reply.send({ success: true, name, analysis });
5998
- } catch (err) {
5999
- try {
6000
- if (fs13.existsSync(skillDir)) {
6001
- fs13.rmSync(skillDir, { recursive: true, force: true });
6002
- }
6003
- removeFromApprovedList(name);
6004
- } catch {
6005
- }
6006
- console.error("[Skills] Install failed:", err.message);
6007
- return reply.code(500).send({
6008
- error: `Installation failed: ${err.message}`
6009
- });
6020
+ return jsonResponse.result;
6021
+ } catch (error) {
6022
+ clearTimeout(timeoutId);
6023
+ if (error.name === "AbortError") {
6024
+ throw new Error("Request timeout");
6010
6025
  }
6026
+ throw error;
6011
6027
  }
6012
- );
6013
- }
6028
+ }
6029
+ };
6014
6030
 
6015
- // libs/shield-daemon/src/routes/exec.ts
6016
- import * as fs14 from "node:fs";
6017
- import * as path13 from "node:path";
6018
- var ALLOWED_COMMANDS_PATH2 = "/opt/agenshield/config/allowed-commands.json";
6019
- var BIN_DIRS = [
6020
- "/usr/bin",
6021
- "/usr/local/bin",
6022
- "/opt/homebrew/bin",
6023
- "/usr/sbin",
6024
- "/usr/local/sbin"
6025
- ];
6026
- var binCache = null;
6027
- var BIN_CACHE_TTL = 6e4;
6028
- var VALID_NAME = /^[a-zA-Z0-9_-]+$/;
6029
- function loadConfig2() {
6030
- if (!fs14.existsSync(ALLOWED_COMMANDS_PATH2)) {
6031
- return { version: "1.0.0", commands: [] };
6031
+ // libs/shield-daemon/src/services/broker-bridge.ts
6032
+ var brokerClient = null;
6033
+ function getBrokerClient() {
6034
+ if (!brokerClient) {
6035
+ brokerClient = new BrokerClient({
6036
+ socketPath: process.env["AGENSHIELD_SOCKET"] || "/var/run/agenshield/agenshield.sock",
6037
+ httpHost: "localhost",
6038
+ httpPort: 5201,
6039
+ // Broker uses 5201, daemon uses 5200
6040
+ timeout: 6e4,
6041
+ // 60s timeout for file operations
6042
+ preferSocket: true
6043
+ });
6032
6044
  }
6045
+ return brokerClient;
6046
+ }
6047
+ async function isBrokerAvailable() {
6033
6048
  try {
6034
- const content = fs14.readFileSync(ALLOWED_COMMANDS_PATH2, "utf-8");
6035
- return JSON.parse(content);
6049
+ const client = getBrokerClient();
6050
+ return await client.isAvailable();
6036
6051
  } catch {
6037
- return { version: "1.0.0", commands: [] };
6052
+ return false;
6038
6053
  }
6039
6054
  }
6040
- function saveConfig2(config) {
6041
- const dir = path13.dirname(ALLOWED_COMMANDS_PATH2);
6042
- if (!fs14.existsSync(dir)) {
6043
- fs14.mkdirSync(dir, { recursive: true });
6044
- }
6045
- fs14.writeFileSync(ALLOWED_COMMANDS_PATH2, JSON.stringify(config, null, 2) + "\n", "utf-8");
6055
+ async function installSkillViaBroker(slug, files, options = {}) {
6056
+ const client = getBrokerClient();
6057
+ const brokerFiles = files.map((file) => ({
6058
+ name: file.name,
6059
+ content: file.content,
6060
+ base64: false
6061
+ }));
6062
+ const result = await client.skillInstall({
6063
+ slug,
6064
+ files: brokerFiles,
6065
+ createWrapper: options.createWrapper ?? true,
6066
+ agentHome: options.agentHome,
6067
+ socketGroup: options.socketGroup
6068
+ });
6069
+ return result;
6046
6070
  }
6047
- function scanSystemBins() {
6048
- const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
6049
- const allDirs = [.../* @__PURE__ */ new Set([...BIN_DIRS, ...pathDirs])];
6050
- const seen = /* @__PURE__ */ new Set();
6051
- const results = [];
6052
- for (const dir of allDirs) {
6053
- try {
6054
- if (!fs14.existsSync(dir)) continue;
6055
- const entries = fs14.readdirSync(dir);
6056
- for (const entry of entries) {
6057
- if (seen.has(entry)) continue;
6058
- const fullPath = path13.join(dir, entry);
6059
- try {
6060
- const stat = fs14.statSync(fullPath);
6061
- if (stat.isFile() && (stat.mode & 73) !== 0) {
6062
- seen.add(entry);
6063
- results.push({ name: entry, path: fullPath });
6064
- }
6065
- } catch {
6066
- }
6067
- }
6068
- } catch {
6071
+ async function uninstallSkillViaBroker(slug, options = {}) {
6072
+ const client = getBrokerClient();
6073
+ const result = await client.skillUninstall({
6074
+ slug,
6075
+ removeWrapper: options.removeWrapper ?? true,
6076
+ agentHome: options.agentHome
6077
+ });
6078
+ return result;
6079
+ }
6080
+
6081
+ // libs/shield-daemon/src/routes/skills.ts
6082
+ function findSkillMdRecursive(dir, depth = 0) {
6083
+ if (depth > 3) return null;
6084
+ try {
6085
+ for (const name of ["SKILL.md", "skill.md", "README.md", "readme.md"]) {
6086
+ const candidate = path12.join(dir, name);
6087
+ if (fs13.existsSync(candidate)) return candidate;
6088
+ }
6089
+ const entries = fs13.readdirSync(dir, { withFileTypes: true });
6090
+ for (const entry of entries) {
6091
+ if (!entry.isDirectory()) continue;
6092
+ const found = findSkillMdRecursive(path12.join(dir, entry.name), depth + 1);
6093
+ if (found) return found;
6069
6094
  }
6095
+ } catch {
6070
6096
  }
6071
- return results.sort((a, b) => a.name.localeCompare(b.name));
6097
+ return null;
6072
6098
  }
6073
- async function execRoutes(app) {
6074
- app.get("/exec/system-bins", async () => {
6075
- const now = Date.now();
6076
- if (binCache && now - binCache.cachedAt < BIN_CACHE_TTL) {
6077
- return { success: true, data: { bins: binCache.bins } };
6099
+ function readSkillDescription(skillDir) {
6100
+ try {
6101
+ const mdPath = findSkillMdRecursive(skillDir);
6102
+ if (!mdPath) return void 0;
6103
+ const content = fs13.readFileSync(mdPath, "utf-8");
6104
+ const parsed = parseSkillMd(content);
6105
+ return parsed?.metadata?.description ?? void 0;
6106
+ } catch {
6107
+ return void 0;
6108
+ }
6109
+ }
6110
+ async function skillsRoutes(app) {
6111
+ app.get("/skills", async (_request, reply) => {
6112
+ const approved = listApproved();
6113
+ const quarantined = listQuarantined();
6114
+ const downloaded = listDownloadedSkills();
6115
+ const skillsDir2 = getSkillsDir();
6116
+ const approvedNames = new Set(approved.map((a) => a.name));
6117
+ const availableDownloads = downloaded.filter((d) => !approvedNames.has(d.slug));
6118
+ const quarantinedNames = new Set(quarantined.map((q) => q.name));
6119
+ let onDiskNames = [];
6120
+ if (skillsDir2) {
6121
+ try {
6122
+ onDiskNames = fs13.readdirSync(skillsDir2, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
6123
+ } catch {
6124
+ }
6078
6125
  }
6079
- const bins = scanSystemBins();
6080
- binCache = { bins, cachedAt: now };
6081
- return { success: true, data: { bins } };
6126
+ const workspaceNames = onDiskNames.filter(
6127
+ (n) => !approvedNames.has(n) && !quarantinedNames.has(n)
6128
+ );
6129
+ const data = [
6130
+ // Approved → active (with descriptions from SKILL.md)
6131
+ ...approved.map((a) => ({
6132
+ name: a.name,
6133
+ source: "user",
6134
+ status: "active",
6135
+ path: path12.join(skillsDir2 ?? "", a.name),
6136
+ publisher: a.publisher,
6137
+ description: skillsDir2 ? readSkillDescription(path12.join(skillsDir2, a.name)) : void 0
6138
+ })),
6139
+ // Quarantined
6140
+ ...quarantined.map((q) => ({
6141
+ name: q.name,
6142
+ source: "quarantine",
6143
+ status: "quarantined",
6144
+ path: q.originalPath,
6145
+ description: void 0
6146
+ })),
6147
+ // Workspace: on disk but not approved or quarantined
6148
+ ...workspaceNames.map((name) => ({
6149
+ name,
6150
+ source: "workspace",
6151
+ status: "workspace",
6152
+ path: path12.join(skillsDir2 ?? "", name),
6153
+ description: skillsDir2 ? readSkillDescription(path12.join(skillsDir2, name)) : void 0
6154
+ })),
6155
+ // Downloaded (not installed) → available
6156
+ ...availableDownloads.map((d) => ({
6157
+ name: d.slug,
6158
+ source: "marketplace",
6159
+ status: "downloaded",
6160
+ description: d.description,
6161
+ path: "",
6162
+ publisher: d.author
6163
+ }))
6164
+ ];
6165
+ return reply.send({ data });
6082
6166
  });
6083
- app.get("/exec/allowed-commands", async () => {
6084
- const config = loadConfig2();
6085
- return {
6086
- success: true,
6087
- data: {
6088
- commands: config.commands
6089
- }
6090
- };
6167
+ app.get("/skills/quarantined", async (_request, reply) => {
6168
+ const quarantined = listQuarantined();
6169
+ return reply.send({ quarantined });
6091
6170
  });
6092
- app.post("/exec/allowed-commands", async (request) => {
6093
- const { name, paths, category } = request.body;
6094
- if (!name || !VALID_NAME.test(name)) {
6095
- return {
6096
- success: false,
6097
- error: {
6098
- code: "INVALID_NAME",
6099
- message: "Command name must be alphanumeric with hyphens/underscores only"
6171
+ app.get(
6172
+ "/skills/:name",
6173
+ async (request, reply) => {
6174
+ const { name } = request.params;
6175
+ if (!name || typeof name !== "string") {
6176
+ return reply.code(400).send({ error: "Skill name is required" });
6177
+ }
6178
+ const analysis = getCachedAnalysis(name);
6179
+ const approved = listApproved();
6180
+ const quarantined = listQuarantined();
6181
+ const entry = approved.find((s) => s.name === name);
6182
+ const qEntry = quarantined.find((q) => q.name === name);
6183
+ const skillsDir2 = getSkillsDir();
6184
+ let source = "user";
6185
+ let status = "active";
6186
+ let skillPath = "";
6187
+ if (qEntry) {
6188
+ source = "quarantine";
6189
+ status = "quarantined";
6190
+ skillPath = qEntry.originalPath;
6191
+ } else if (entry) {
6192
+ source = "user";
6193
+ status = "active";
6194
+ skillPath = skillsDir2 ? path12.join(skillsDir2, name) : "";
6195
+ } else if (skillsDir2) {
6196
+ source = "workspace";
6197
+ status = "workspace";
6198
+ skillPath = path12.join(skillsDir2, name);
6199
+ }
6200
+ let content = "";
6201
+ let metadata;
6202
+ const dirToRead = skillPath || (skillsDir2 ? path12.join(skillsDir2, name) : "");
6203
+ if (dirToRead) {
6204
+ try {
6205
+ const mdPath = findSkillMdRecursive(dirToRead);
6206
+ if (mdPath) {
6207
+ content = fs13.readFileSync(mdPath, "utf-8");
6208
+ const parsed = parseSkillMd(content);
6209
+ metadata = parsed?.metadata;
6210
+ }
6211
+ } catch {
6100
6212
  }
6101
- };
6102
- }
6103
- if (!paths || !Array.isArray(paths) || paths.length === 0) {
6104
- return {
6105
- success: false,
6106
- error: {
6107
- code: "INVALID_PATHS",
6108
- message: "At least one absolute path is required"
6213
+ }
6214
+ if (!content) {
6215
+ try {
6216
+ const localMeta = getDownloadedSkillMeta(name);
6217
+ if (localMeta) {
6218
+ const localFiles = getDownloadedSkillFiles(name);
6219
+ const readmeFile = localFiles.find((f) => /readme|skill\.md/i.test(f.name));
6220
+ if (readmeFile?.content) {
6221
+ content = readmeFile.content;
6222
+ if (!metadata) {
6223
+ const parsed = parseSkillMd(content);
6224
+ metadata = parsed?.metadata;
6225
+ }
6226
+ }
6227
+ }
6228
+ } catch {
6109
6229
  }
6110
- };
6111
- }
6112
- for (const p of paths) {
6113
- if (!path13.isAbsolute(p)) {
6114
- return {
6115
- success: false,
6116
- error: {
6117
- code: "INVALID_PATH",
6118
- message: `Path must be absolute: ${p}`
6230
+ }
6231
+ const description = dirToRead ? readSkillDescription(dirToRead) : void 0;
6232
+ if (content) {
6233
+ try {
6234
+ const cachedFiles = getDownloadedSkillFiles(name);
6235
+ if (cachedFiles.length > 0) {
6236
+ content = inlineImagesInMarkdown(content, cachedFiles);
6119
6237
  }
6120
- };
6238
+ } catch {
6239
+ }
6121
6240
  }
6122
- }
6123
- const config = loadConfig2();
6124
- const existing = config.commands.find((c) => c.name === name);
6125
- if (existing) {
6126
- return {
6127
- success: false,
6128
- error: {
6129
- code: "ALREADY_EXISTS",
6130
- message: `Command '${name}' already exists in dynamic allowlist`
6241
+ return reply.send({
6242
+ success: true,
6243
+ data: {
6244
+ name,
6245
+ source,
6246
+ status,
6247
+ path: skillPath,
6248
+ description,
6249
+ content,
6250
+ metadata: metadata ?? null,
6251
+ analysis: analysis ?? null,
6252
+ publisher: entry?.publisher ?? null
6131
6253
  }
6132
- };
6254
+ });
6133
6255
  }
6134
- const newCommand = {
6135
- name,
6136
- paths,
6137
- addedAt: (/* @__PURE__ */ new Date()).toISOString(),
6138
- addedBy: "admin",
6139
- ...category ? { category } : {}
6140
- };
6141
- config.commands.push(newCommand);
6142
- saveConfig2(config);
6143
- return {
6144
- success: true,
6145
- data: newCommand
6146
- };
6147
- });
6148
- app.delete("/exec/allowed-commands/:name", async (request) => {
6149
- const { name } = request.params;
6150
- const config = loadConfig2();
6151
- const index = config.commands.findIndex((c) => c.name === name);
6152
- if (index === -1) {
6153
- return {
6154
- success: false,
6155
- error: {
6156
- code: "NOT_FOUND",
6157
- message: `Command '${name}' not found in dynamic allowlist`
6256
+ );
6257
+ app.post(
6258
+ "/skills/:name/analyze",
6259
+ async (request, reply) => {
6260
+ const { name } = request.params;
6261
+ const { content, metadata } = request.body ?? {};
6262
+ if (!name || typeof name !== "string") {
6263
+ return reply.code(400).send({ error: "Skill name is required" });
6264
+ }
6265
+ clearCachedAnalysis(name);
6266
+ if (content) {
6267
+ const analysis = analyzeSkill(name, content, metadata);
6268
+ return reply.send({ success: true, data: { analysis } });
6269
+ }
6270
+ return reply.send({
6271
+ success: true,
6272
+ data: {
6273
+ analysis: {
6274
+ status: "pending",
6275
+ analyzerId: "agenshield",
6276
+ commands: []
6277
+ }
6158
6278
  }
6159
- };
6160
- }
6161
- config.commands.splice(index, 1);
6162
- saveConfig2(config);
6163
- return {
6164
- success: true,
6165
- data: { removed: name }
6166
- };
6167
- });
6168
- }
6169
-
6170
- // libs/shield-daemon/src/routes/discovery.ts
6171
- import { scanDiscovery } from "@agenshield/sandbox";
6172
- var CACHE_TTL = 6e4;
6173
- var cache2 = null;
6174
- async function discoveryRoutes(app) {
6175
- app.get("/discovery/scan", async (request) => {
6176
- const refresh = request.query.refresh === "true";
6177
- const scanSkills2 = request.query.scanSkills !== "false";
6178
- const now = Date.now();
6179
- if (!refresh && cache2 && now - cache2.cachedAt < CACHE_TTL) {
6180
- return { success: true, data: cache2.result };
6279
+ });
6181
6280
  }
6182
- const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || void 0;
6183
- const result = scanDiscovery({
6184
- agentHome,
6185
- workspaceDir: agentHome ? `${agentHome}/workspace` : void 0,
6186
- scanSkills: scanSkills2 && !!agentHome
6187
- });
6188
- cache2 = { result, cachedAt: Date.now() };
6189
- return { success: true, data: result };
6190
- });
6191
- }
6192
-
6193
- // libs/shield-daemon/src/routes/auth.ts
6194
- import {
6195
- UnlockRequestSchema,
6196
- SetupPasscodeRequestSchema,
6197
- ChangePasscodeRequestSchema
6198
- } from "@agenshield/ipc";
6199
- async function authRoutes(app) {
6200
- app.get("/auth/status", async () => {
6201
- const passcodeSet = await isPasscodeSet();
6202
- const protectionEnabled = isProtectionEnabled();
6203
- const allowAnonymousReadOnly = isAnonymousReadOnlyAllowed();
6204
- const lockoutStatus = isLockedOut();
6205
- return {
6206
- passcodeSet,
6207
- protectionEnabled,
6208
- allowAnonymousReadOnly,
6209
- lockedOut: lockoutStatus.locked,
6210
- lockedUntil: lockoutStatus.lockedUntil
6211
- };
6212
- });
6281
+ );
6213
6282
  app.post(
6214
- "/auth/unlock",
6283
+ "/skills/:name/approve",
6215
6284
  async (request, reply) => {
6216
- const parseResult = UnlockRequestSchema.safeParse(request.body);
6217
- if (!parseResult.success) {
6218
- reply.code(400);
6219
- return {
6220
- success: false,
6221
- error: "Invalid request: " + parseResult.error.message
6222
- };
6223
- }
6224
- const { passcode } = parseResult.data;
6225
- const lockoutStatus = isLockedOut();
6226
- if (lockoutStatus.locked) {
6227
- reply.code(429);
6228
- return {
6229
- success: false,
6230
- error: "Too many failed attempts. Try again later.",
6231
- remainingAttempts: 0
6232
- };
6233
- }
6234
- const passcodeSet = await isPasscodeSet();
6235
- if (!passcodeSet) {
6236
- reply.code(400);
6237
- return {
6238
- success: false,
6239
- error: "Passcode not configured. Use /auth/setup first."
6240
- };
6285
+ const { name } = request.params;
6286
+ if (!name || typeof name !== "string") {
6287
+ return reply.code(400).send({ error: "Skill name is required" });
6241
6288
  }
6242
- const valid = await checkPasscode(passcode);
6243
- if (!valid) {
6244
- const remainingAttempts = recordFailedAttempt();
6245
- reply.code(401);
6246
- return {
6247
- success: false,
6248
- error: "Invalid passcode",
6249
- remainingAttempts
6250
- };
6289
+ const result = approveSkill(name);
6290
+ if (!result.success) {
6291
+ return reply.code(404).send({ error: result.error });
6251
6292
  }
6252
- clearFailedAttempts();
6253
- const sessionManager = getSessionManager();
6254
- const session = sessionManager.createSession();
6255
- return {
6256
- success: true,
6257
- token: session.token,
6258
- expiresAt: session.expiresAt
6259
- };
6293
+ return reply.send({ success: true, message: `Skill "${name}" approved` });
6260
6294
  }
6261
6295
  );
6262
- app.post(
6263
- "/auth/lock",
6296
+ app.delete(
6297
+ "/skills/:name",
6264
6298
  async (request, reply) => {
6265
- const token = request.body?.token || extractToken(request);
6266
- if (!token) {
6267
- reply.code(400);
6268
- return { success: false };
6299
+ const { name } = request.params;
6300
+ if (!name || typeof name !== "string") {
6301
+ return reply.code(400).send({ error: "Skill name is required" });
6269
6302
  }
6270
- const sessionManager = getSessionManager();
6271
- const invalidated = sessionManager.invalidateSession(token);
6272
- return { success: invalidated };
6303
+ const result = rejectSkill(name);
6304
+ if (!result.success) {
6305
+ return reply.code(404).send({ error: result.error });
6306
+ }
6307
+ return reply.send({ success: true, message: `Skill "${name}" rejected and deleted` });
6273
6308
  }
6274
6309
  );
6275
6310
  app.post(
6276
- "/auth/setup",
6311
+ "/skills/:name/revoke",
6277
6312
  async (request, reply) => {
6278
- const parseResult = SetupPasscodeRequestSchema.safeParse(request.body);
6279
- if (!parseResult.success) {
6280
- reply.code(400);
6281
- return {
6282
- success: false,
6283
- error: "Invalid request: " + parseResult.error.message
6284
- };
6285
- }
6286
- const { passcode, enableProtection = true } = parseResult.data;
6287
- const alreadySet = await isPasscodeSet();
6288
- if (alreadySet) {
6289
- reply.code(409);
6290
- return {
6291
- success: false,
6292
- error: "Passcode already configured. Use /auth/change to update."
6293
- };
6313
+ const { name } = request.params;
6314
+ if (!name || typeof name !== "string") {
6315
+ return reply.code(400).send({ error: "Skill name is required" });
6294
6316
  }
6295
- await setPasscode(passcode);
6296
- if (enableProtection) {
6297
- setProtectionEnabled(true);
6317
+ const result = revokeSkill(name);
6318
+ if (!result.success) {
6319
+ return reply.code(500).send({ error: result.error });
6298
6320
  }
6299
- return { success: true };
6321
+ return reply.send({ success: true, message: `Skill "${name}" approval revoked` });
6300
6322
  }
6301
6323
  );
6302
- app.post(
6303
- "/auth/change",
6324
+ app.put(
6325
+ "/skills/:name/toggle",
6304
6326
  async (request, reply) => {
6305
- const parseResult = ChangePasscodeRequestSchema.safeParse(request.body);
6306
- if (!parseResult.success) {
6307
- reply.code(400);
6308
- return {
6309
- success: false,
6310
- error: "Invalid request: " + parseResult.error.message
6311
- };
6327
+ const { name } = request.params;
6328
+ if (!name || typeof name !== "string") {
6329
+ return reply.code(400).send({ error: "Skill name is required" });
6312
6330
  }
6313
- const { oldPasscode, newPasscode } = parseResult.data;
6314
- const passcodeSet = await isPasscodeSet();
6315
- if (!passcodeSet) {
6316
- reply.code(400);
6317
- return {
6318
- success: false,
6319
- error: "Passcode not configured. Use /auth/setup first."
6320
- };
6331
+ const skillsDir2 = getSkillsDir();
6332
+ if (!skillsDir2) {
6333
+ return reply.code(500).send({ error: "Skills directory not configured" });
6321
6334
  }
6322
- if (!isRunningAsRoot()) {
6323
- if (!oldPasscode) {
6324
- reply.code(400);
6325
- return {
6326
- success: false,
6327
- error: "Old passcode required"
6328
- };
6335
+ const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
6336
+ const binDir = path12.join(agentHome, "bin");
6337
+ const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "clawshield";
6338
+ const skillDir = path12.join(skillsDir2, name);
6339
+ const isInstalled = fs13.existsSync(skillDir);
6340
+ if (isInstalled) {
6341
+ try {
6342
+ const brokerAvailable = await isBrokerAvailable();
6343
+ if (brokerAvailable) {
6344
+ await uninstallSkillViaBroker(name, {
6345
+ removeWrapper: true,
6346
+ agentHome
6347
+ });
6348
+ } else {
6349
+ fs13.rmSync(skillDir, { recursive: true, force: true });
6350
+ removeSkillWrapper(name, binDir);
6351
+ }
6352
+ removeSkillEntry(name);
6353
+ removeSkillPolicy(name);
6354
+ removeFromApprovedList(name);
6355
+ console.log(`[Skills] Disabled marketplace skill: ${name}`);
6356
+ return reply.send({ success: true, action: "disabled", name });
6357
+ } catch (err) {
6358
+ console.error("[Skills] Disable failed:", err.message);
6359
+ return reply.code(500).send({ error: `Disable failed: ${err.message}` });
6329
6360
  }
6330
- const valid = await checkPasscode(oldPasscode);
6331
- if (!valid) {
6332
- const remainingAttempts = recordFailedAttempt();
6333
- reply.code(401);
6334
- return {
6335
- success: false,
6336
- error: "Invalid old passcode"
6337
- };
6361
+ } else {
6362
+ const meta = getDownloadedSkillMeta(name);
6363
+ if (!meta) {
6364
+ return reply.code(404).send({ error: "Skill not found in download cache" });
6365
+ }
6366
+ const files = getDownloadedSkillFiles(name);
6367
+ if (files.length === 0) {
6368
+ return reply.code(404).send({ error: "No files in download cache for this skill" });
6369
+ }
6370
+ try {
6371
+ addToApprovedList(name, meta.author);
6372
+ const brokerAvailable = await isBrokerAvailable();
6373
+ if (brokerAvailable) {
6374
+ const brokerResult = await installSkillViaBroker(
6375
+ name,
6376
+ files.map((f) => ({ name: f.name, content: f.content })),
6377
+ { createWrapper: true, agentHome, socketGroup }
6378
+ );
6379
+ if (!brokerResult.installed) {
6380
+ throw new Error("Broker failed to install skill files");
6381
+ }
6382
+ } else {
6383
+ fs13.mkdirSync(skillDir, { recursive: true });
6384
+ for (const file of files) {
6385
+ const filePath = path12.join(skillDir, file.name);
6386
+ fs13.mkdirSync(path12.dirname(filePath), { recursive: true });
6387
+ fs13.writeFileSync(filePath, file.content, "utf-8");
6388
+ }
6389
+ try {
6390
+ execSync8(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
6391
+ execSync8(`chmod -R a+rX,go-w "${skillDir}"`, { stdio: "pipe" });
6392
+ } catch {
6393
+ }
6394
+ createSkillWrapper(name, binDir);
6395
+ }
6396
+ addSkillEntry(name);
6397
+ addSkillPolicy(name);
6398
+ console.log(`[Skills] Enabled marketplace skill: ${name}`);
6399
+ return reply.send({ success: true, action: "enabled", name });
6400
+ } catch (err) {
6401
+ try {
6402
+ if (fs13.existsSync(skillDir)) {
6403
+ fs13.rmSync(skillDir, { recursive: true, force: true });
6404
+ }
6405
+ removeFromApprovedList(name);
6406
+ } catch {
6407
+ }
6408
+ console.error("[Skills] Enable failed:", err.message);
6409
+ return reply.code(500).send({ error: `Enable failed: ${err.message}` });
6338
6410
  }
6339
- clearFailedAttempts();
6340
6411
  }
6341
- await setPasscode(newPasscode);
6342
- const sessionManager = getSessionManager();
6343
- sessionManager.clearAllSessions();
6344
- return { success: true };
6345
6412
  }
6346
6413
  );
6347
6414
  app.post(
6348
- "/auth/refresh",
6415
+ "/skills/install",
6416
+ { preHandler: [requireAuth] },
6349
6417
  async (request, reply) => {
6350
- const token = extractToken(request);
6351
- if (!token) {
6352
- reply.code(401);
6353
- return {
6354
- success: false,
6355
- error: "No token provided"
6356
- };
6418
+ const { name, files, publisher } = request.body ?? {};
6419
+ if (!name || typeof name !== "string") {
6420
+ return reply.code(400).send({ error: "Skill name is required" });
6357
6421
  }
6358
- const sessionManager = getSessionManager();
6359
- const refreshed = sessionManager.refreshSession(token);
6360
- if (!refreshed) {
6361
- reply.code(401);
6362
- return {
6363
- success: false,
6364
- error: "Invalid or expired token"
6365
- };
6422
+ if (!Array.isArray(files) || files.length === 0) {
6423
+ return reply.code(400).send({ error: "Files array is required" });
6366
6424
  }
6367
- return {
6368
- success: true,
6369
- token: refreshed.token,
6370
- expiresAt: refreshed.expiresAt
6371
- };
6372
- }
6373
- );
6374
- app.post(
6375
- "/auth/enable",
6376
- async (request, reply) => {
6377
- const passcodeSet = await isPasscodeSet();
6378
- if (!passcodeSet) {
6379
- reply.code(400);
6380
- return {
6381
- success: false,
6382
- error: "Passcode not configured. Use /auth/setup first."
6383
- };
6425
+ const combinedContent = files.map((f) => f.content).join("\n");
6426
+ const skillMdFile = files.find((f) => f.name === "SKILL.md");
6427
+ let metadata;
6428
+ if (skillMdFile) {
6429
+ const parsed = parseSkillMd(skillMdFile.content);
6430
+ metadata = parsed?.metadata;
6384
6431
  }
6385
- setProtectionEnabled(true);
6386
- return { success: true };
6387
- }
6388
- );
6389
- app.post(
6390
- "/auth/disable",
6391
- async (request, reply) => {
6392
- if (!isRunningAsRoot()) {
6393
- const token = extractToken(request);
6394
- if (!token) {
6395
- reply.code(401);
6396
- return {
6397
- success: false,
6398
- error: "Authentication required to disable protection"
6399
- };
6400
- }
6401
- const sessionManager = getSessionManager();
6402
- const session = sessionManager.validateSession(token);
6403
- if (!session) {
6404
- reply.code(401);
6405
- return {
6406
- success: false,
6407
- error: "Invalid or expired token"
6408
- };
6409
- }
6432
+ const analysis = analyzeSkill(name, combinedContent, metadata);
6433
+ if (analysis.vulnerability?.level === "critical") {
6434
+ return reply.code(400).send({ error: "Critical vulnerability detected", analysis });
6410
6435
  }
6411
- setProtectionEnabled(false);
6412
- return { success: true };
6413
- }
6414
- );
6415
- app.post(
6416
- "/auth/anonymous-readonly",
6417
- async (request, reply) => {
6418
- if (!isRunningAsRoot()) {
6419
- const token = extractToken(request);
6420
- if (!token) {
6421
- reply.code(401);
6422
- return {
6423
- success: false,
6424
- error: "Authentication required to change anonymous access"
6425
- };
6436
+ const skillsDir2 = getSkillsDir();
6437
+ if (!skillsDir2) {
6438
+ return reply.code(500).send({ error: "Skills directory not configured" });
6439
+ }
6440
+ const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent";
6441
+ const binDir = path12.join(agentHome, "bin");
6442
+ const socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "clawshield";
6443
+ const skillDir = path12.join(skillsDir2, name);
6444
+ try {
6445
+ addToApprovedList(name, publisher);
6446
+ fs13.mkdirSync(skillDir, { recursive: true });
6447
+ for (const file of files) {
6448
+ const filePath = path12.join(skillDir, file.name);
6449
+ fs13.mkdirSync(path12.dirname(filePath), { recursive: true });
6450
+ fs13.writeFileSync(filePath, file.content, "utf-8");
6426
6451
  }
6427
- const sessionManager = getSessionManager();
6428
- const session = sessionManager.validateSession(token);
6429
- if (!session) {
6430
- reply.code(401);
6431
- return {
6432
- success: false,
6433
- error: "Invalid or expired token"
6434
- };
6452
+ try {
6453
+ execSync8(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
6454
+ execSync8(`chmod -R a+rX,go-w "${skillDir}"`, { stdio: "pipe" });
6455
+ } catch {
6435
6456
  }
6457
+ createSkillWrapper(name, binDir);
6458
+ addSkillPolicy(name);
6459
+ return reply.send({ success: true, name, analysis });
6460
+ } catch (err) {
6461
+ try {
6462
+ if (fs13.existsSync(skillDir)) {
6463
+ fs13.rmSync(skillDir, { recursive: true, force: true });
6464
+ }
6465
+ removeFromApprovedList(name);
6466
+ } catch {
6467
+ }
6468
+ console.error("[Skills] Install failed:", err.message);
6469
+ return reply.code(500).send({
6470
+ error: `Installation failed: ${err.message}`
6471
+ });
6436
6472
  }
6437
- const { allowed } = request.body;
6438
- if (typeof allowed !== "boolean") {
6439
- reply.code(400);
6440
- return {
6441
- success: false,
6442
- error: 'Invalid request: "allowed" must be a boolean'
6443
- };
6444
- }
6445
- setAnonymousReadOnly(allowed);
6446
- return { success: true, allowAnonymousReadOnly: allowed };
6447
6473
  }
6448
6474
  );
6449
6475
  }
6450
6476
 
6451
- // libs/shield-daemon/src/routes/secrets.ts
6452
- import { isSecretEnvVar } from "@agenshield/sandbox";
6453
- import crypto4 from "node:crypto";
6454
- function maskValue(value) {
6455
- if (value.length <= 4) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
6456
- return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + value.slice(-4);
6477
+ // libs/shield-daemon/src/routes/exec.ts
6478
+ import * as fs14 from "node:fs";
6479
+ import * as path13 from "node:path";
6480
+ var ALLOWED_COMMANDS_PATH2 = "/opt/agenshield/config/allowed-commands.json";
6481
+ var BIN_DIRS = [
6482
+ "/usr/bin",
6483
+ "/usr/local/bin",
6484
+ "/opt/homebrew/bin",
6485
+ "/usr/sbin",
6486
+ "/usr/local/sbin"
6487
+ ];
6488
+ var binCache = null;
6489
+ var BIN_CACHE_TTL = 6e4;
6490
+ var VALID_NAME = /^[a-zA-Z0-9_-]+$/;
6491
+ function loadConfig2() {
6492
+ if (!fs14.existsSync(ALLOWED_COMMANDS_PATH2)) {
6493
+ return { version: "1.0.0", commands: [] };
6494
+ }
6495
+ try {
6496
+ const content = fs14.readFileSync(ALLOWED_COMMANDS_PATH2, "utf-8");
6497
+ return JSON.parse(content);
6498
+ } catch {
6499
+ return { version: "1.0.0", commands: [] };
6500
+ }
6457
6501
  }
6458
- function toMasked(secret) {
6459
- return {
6460
- id: secret.id,
6461
- name: secret.name,
6462
- policyIds: secret.policyIds,
6463
- maskedValue: maskValue(secret.value),
6464
- createdAt: secret.createdAt
6465
- };
6502
+ function saveConfig2(config) {
6503
+ const dir = path13.dirname(ALLOWED_COMMANDS_PATH2);
6504
+ if (!fs14.existsSync(dir)) {
6505
+ fs14.mkdirSync(dir, { recursive: true });
6506
+ }
6507
+ fs14.writeFileSync(ALLOWED_COMMANDS_PATH2, JSON.stringify(config, null, 2) + "\n", "utf-8");
6466
6508
  }
6467
- async function secretsRoutes(app) {
6468
- app.get("/secrets", async () => {
6469
- const vault = getVault();
6470
- const secrets = await vault.get("secrets") ?? [];
6471
- return { data: secrets.map(toMasked) };
6472
- });
6473
- app.get("/secrets/env", async () => {
6474
- const names = /* @__PURE__ */ new Set();
6475
- for (const key of Object.keys(process.env)) {
6476
- if (process.env[key] && isSecretEnvVar(key)) {
6477
- names.add(key);
6509
+ function scanSystemBins() {
6510
+ const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
6511
+ const allDirs = [.../* @__PURE__ */ new Set([...BIN_DIRS, ...pathDirs])];
6512
+ const seen = /* @__PURE__ */ new Set();
6513
+ const results = [];
6514
+ for (const dir of allDirs) {
6515
+ try {
6516
+ if (!fs14.existsSync(dir)) continue;
6517
+ const entries = fs14.readdirSync(dir);
6518
+ for (const entry of entries) {
6519
+ if (seen.has(entry)) continue;
6520
+ const fullPath = path13.join(dir, entry);
6521
+ try {
6522
+ const stat = fs14.statSync(fullPath);
6523
+ if (stat.isFile() && (stat.mode & 73) !== 0) {
6524
+ seen.add(entry);
6525
+ results.push({ name: entry, path: fullPath });
6526
+ }
6527
+ } catch {
6528
+ }
6478
6529
  }
6530
+ } catch {
6479
6531
  }
6480
- const userSecrets = process.env["AGENSHIELD_USER_SECRETS"];
6481
- if (userSecrets) {
6482
- for (const name of userSecrets.split(",").filter(Boolean)) {
6483
- names.add(name);
6484
- }
6532
+ }
6533
+ return results.sort((a, b) => a.name.localeCompare(b.name));
6534
+ }
6535
+ async function execRoutes(app) {
6536
+ app.get("/exec/system-bins", async () => {
6537
+ const now = Date.now();
6538
+ if (binCache && now - binCache.cachedAt < BIN_CACHE_TTL) {
6539
+ return { success: true, data: { bins: binCache.bins } };
6485
6540
  }
6486
- return { data: Array.from(names).sort((a, b) => a.localeCompare(b)) };
6541
+ const bins = scanSystemBins();
6542
+ binCache = { bins, cachedAt: now };
6543
+ return { success: true, data: { bins } };
6487
6544
  });
6488
- app.post(
6489
- "/secrets",
6490
- async (request) => {
6491
- const { name, value, policyIds } = request.body;
6492
- if (!name?.trim() || !value?.trim()) {
6493
- return { success: false, error: "Name and value are required" };
6545
+ app.get("/exec/allowed-commands", async () => {
6546
+ const config = loadConfig2();
6547
+ return {
6548
+ success: true,
6549
+ data: {
6550
+ commands: config.commands
6494
6551
  }
6495
- const vault = getVault();
6496
- const secrets = await vault.get("secrets") ?? [];
6497
- const newSecret = {
6498
- id: crypto4.randomUUID(),
6499
- name: name.trim(),
6500
- value,
6501
- policyIds: policyIds ?? [],
6502
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
6552
+ };
6553
+ });
6554
+ app.post("/exec/allowed-commands", async (request) => {
6555
+ const { name, paths, category } = request.body;
6556
+ if (!name || !VALID_NAME.test(name)) {
6557
+ return {
6558
+ success: false,
6559
+ error: {
6560
+ code: "INVALID_NAME",
6561
+ message: "Command name must be alphanumeric with hyphens/underscores only"
6562
+ }
6503
6563
  };
6504
- secrets.push(newSecret);
6505
- await vault.set("secrets", secrets);
6506
- return { data: toMasked(newSecret) };
6507
6564
  }
6508
- );
6509
- app.patch(
6510
- "/secrets/:id",
6511
- async (request) => {
6512
- const { id } = request.params;
6513
- const { policyIds } = request.body;
6514
- const vault = getVault();
6515
- const secrets = await vault.get("secrets") ?? [];
6516
- const idx = secrets.findIndex((s) => s.id === id);
6517
- if (idx === -1) return { success: false, error: "Secret not found" };
6518
- secrets[idx].policyIds = policyIds ?? [];
6519
- await vault.set("secrets", secrets);
6520
- return { data: toMasked(secrets[idx]) };
6565
+ if (!paths || !Array.isArray(paths) || paths.length === 0) {
6566
+ return {
6567
+ success: false,
6568
+ error: {
6569
+ code: "INVALID_PATHS",
6570
+ message: "At least one absolute path is required"
6571
+ }
6572
+ };
6521
6573
  }
6522
- );
6523
- app.delete(
6524
- "/secrets/:id",
6525
- async (request) => {
6526
- const { id } = request.params;
6527
- const vault = getVault();
6528
- const secrets = await vault.get("secrets") ?? [];
6529
- const filtered = secrets.filter((s) => s.id !== id);
6530
- if (filtered.length === secrets.length) {
6531
- return { success: false, error: "Secret not found" };
6574
+ for (const p of paths) {
6575
+ if (!path13.isAbsolute(p)) {
6576
+ return {
6577
+ success: false,
6578
+ error: {
6579
+ code: "INVALID_PATH",
6580
+ message: `Path must be absolute: ${p}`
6581
+ }
6582
+ };
6532
6583
  }
6533
- await vault.set("secrets", filtered);
6534
- return { deleted: true };
6535
6584
  }
6536
- );
6585
+ const config = loadConfig2();
6586
+ const existing = config.commands.find((c) => c.name === name);
6587
+ if (existing) {
6588
+ return {
6589
+ success: false,
6590
+ error: {
6591
+ code: "ALREADY_EXISTS",
6592
+ message: `Command '${name}' already exists in dynamic allowlist`
6593
+ }
6594
+ };
6595
+ }
6596
+ const newCommand = {
6597
+ name,
6598
+ paths,
6599
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
6600
+ addedBy: "admin",
6601
+ ...category ? { category } : {}
6602
+ };
6603
+ config.commands.push(newCommand);
6604
+ saveConfig2(config);
6605
+ return {
6606
+ success: true,
6607
+ data: newCommand
6608
+ };
6609
+ });
6610
+ app.delete("/exec/allowed-commands/:name", async (request) => {
6611
+ const { name } = request.params;
6612
+ const config = loadConfig2();
6613
+ const index = config.commands.findIndex((c) => c.name === name);
6614
+ if (index === -1) {
6615
+ return {
6616
+ success: false,
6617
+ error: {
6618
+ code: "NOT_FOUND",
6619
+ message: `Command '${name}' not found in dynamic allowlist`
6620
+ }
6621
+ };
6622
+ }
6623
+ config.commands.splice(index, 1);
6624
+ saveConfig2(config);
6625
+ return {
6626
+ success: true,
6627
+ data: { removed: name }
6628
+ };
6629
+ });
6537
6630
  }
6538
6631
 
6539
- // libs/shield-daemon/src/routes/marketplace.ts
6540
- import * as fs15 from "node:fs";
6541
- import * as path14 from "node:path";
6542
-
6543
- // libs/shield-broker/dist/index.js
6544
- import { exec } from "node:child_process";
6545
- import { promisify } from "node:util";
6546
- import * as net2 from "node:net";
6547
- import { randomUUID as randomUUID3 } from "node:crypto";
6548
- var MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
6549
- var execAsync = promisify(exec);
6550
- var BrokerClient = class {
6551
- socketPath;
6552
- httpHost;
6553
- httpPort;
6554
- timeout;
6555
- preferSocket;
6556
- constructor(options = {}) {
6557
- this.socketPath = options.socketPath || "/var/run/agenshield/agenshield.sock";
6558
- this.httpHost = options.httpHost || "localhost";
6559
- this.httpPort = options.httpPort || 5201;
6560
- this.timeout = options.timeout || 3e4;
6561
- this.preferSocket = options.preferSocket ?? true;
6562
- }
6563
- /**
6564
- * Make an HTTP request through the broker
6565
- */
6566
- async httpRequest(params, options) {
6567
- return this.request("http_request", params, options);
6568
- }
6569
- /**
6570
- * Read a file through the broker
6571
- */
6572
- async fileRead(params, options) {
6573
- return this.request("file_read", params, options);
6574
- }
6575
- /**
6576
- * Write a file through the broker
6577
- */
6578
- async fileWrite(params, options) {
6579
- return this.request("file_write", params, {
6580
- ...options,
6581
- channel: "socket"
6582
- // file_write only allowed via socket
6583
- });
6584
- }
6585
- /**
6586
- * List files through the broker
6587
- */
6588
- async fileList(params, options) {
6589
- return this.request("file_list", params, options);
6590
- }
6591
- /**
6592
- * Execute a command through the broker
6593
- */
6594
- async exec(params, options) {
6595
- return this.request("exec", params, {
6596
- ...options,
6597
- channel: "socket"
6598
- // exec only allowed via socket
6599
- });
6600
- }
6601
- /**
6602
- * Open a URL through the broker
6603
- */
6604
- async openUrl(params, options) {
6605
- return this.request("open_url", params, options);
6606
- }
6607
- /**
6608
- * Inject a secret through the broker
6609
- */
6610
- async secretInject(params, options) {
6611
- return this.request("secret_inject", params, {
6612
- ...options,
6613
- channel: "socket"
6614
- // secret_inject only allowed via socket
6615
- });
6616
- }
6617
- /**
6618
- * Ping the broker
6619
- */
6620
- async ping(echo, options) {
6621
- return this.request("ping", { echo }, options);
6622
- }
6623
- /**
6624
- * Install a skill through the broker
6625
- * Socket-only operation due to privileged file operations
6626
- */
6627
- async skillInstall(params, options) {
6628
- return this.request("skill_install", params, {
6629
- ...options,
6630
- channel: "socket"
6631
- // skill_install only allowed via socket
6632
- });
6633
- }
6634
- /**
6635
- * Uninstall a skill through the broker
6636
- * Socket-only operation due to privileged file operations
6637
- */
6638
- async skillUninstall(params, options) {
6639
- return this.request("skill_uninstall", params, {
6640
- ...options,
6641
- channel: "socket"
6642
- // skill_uninstall only allowed via socket
6632
+ // libs/shield-daemon/src/routes/discovery.ts
6633
+ import { scanDiscovery } from "@agenshield/sandbox";
6634
+ var CACHE_TTL = 6e4;
6635
+ var cache2 = null;
6636
+ async function discoveryRoutes(app) {
6637
+ app.get("/discovery/scan", async (request) => {
6638
+ const refresh = request.query.refresh === "true";
6639
+ const scanSkills2 = request.query.scanSkills !== "false";
6640
+ const now = Date.now();
6641
+ if (!refresh && cache2 && now - cache2.cachedAt < CACHE_TTL) {
6642
+ return { success: true, data: cache2.result };
6643
+ }
6644
+ const agentHome = process.env["AGENSHIELD_AGENT_HOME"] || void 0;
6645
+ const result = scanDiscovery({
6646
+ agentHome,
6647
+ workspaceDir: agentHome ? `${agentHome}/workspace` : void 0,
6648
+ scanSkills: scanSkills2 && !!agentHome
6643
6649
  });
6644
- }
6645
- /**
6646
- * Check if the broker is available
6647
- */
6648
- async isAvailable() {
6649
- try {
6650
- await this.ping();
6651
- return true;
6652
- } catch {
6653
- return false;
6650
+ cache2 = { result, cachedAt: Date.now() };
6651
+ return { success: true, data: result };
6652
+ });
6653
+ }
6654
+
6655
+ // libs/shield-daemon/src/routes/auth.ts
6656
+ import {
6657
+ UnlockRequestSchema,
6658
+ SetupPasscodeRequestSchema,
6659
+ ChangePasscodeRequestSchema
6660
+ } from "@agenshield/ipc";
6661
+ async function authRoutes(app) {
6662
+ app.get("/auth/status", async () => {
6663
+ const passcodeSet = await isPasscodeSet();
6664
+ const protectionEnabled = isProtectionEnabled();
6665
+ const allowAnonymousReadOnly = isAnonymousReadOnlyAllowed();
6666
+ const lockoutStatus = isLockedOut();
6667
+ return {
6668
+ passcodeSet,
6669
+ protectionEnabled,
6670
+ allowAnonymousReadOnly,
6671
+ lockedOut: lockoutStatus.locked,
6672
+ lockedUntil: lockoutStatus.lockedUntil
6673
+ };
6674
+ });
6675
+ app.post(
6676
+ "/auth/unlock",
6677
+ async (request, reply) => {
6678
+ const parseResult = UnlockRequestSchema.safeParse(request.body);
6679
+ if (!parseResult.success) {
6680
+ reply.code(400);
6681
+ return {
6682
+ success: false,
6683
+ error: "Invalid request: " + parseResult.error.message
6684
+ };
6685
+ }
6686
+ const { passcode } = parseResult.data;
6687
+ const lockoutStatus = isLockedOut();
6688
+ if (lockoutStatus.locked) {
6689
+ reply.code(429);
6690
+ return {
6691
+ success: false,
6692
+ error: "Too many failed attempts. Try again later.",
6693
+ remainingAttempts: 0
6694
+ };
6695
+ }
6696
+ const passcodeSet = await isPasscodeSet();
6697
+ if (!passcodeSet) {
6698
+ reply.code(400);
6699
+ return {
6700
+ success: false,
6701
+ error: "Passcode not configured. Use /auth/setup first."
6702
+ };
6703
+ }
6704
+ const valid = await checkPasscode(passcode);
6705
+ if (!valid) {
6706
+ const remainingAttempts = recordFailedAttempt();
6707
+ reply.code(401);
6708
+ return {
6709
+ success: false,
6710
+ error: "Invalid passcode",
6711
+ remainingAttempts
6712
+ };
6713
+ }
6714
+ clearFailedAttempts();
6715
+ const sessionManager = getSessionManager();
6716
+ const session = sessionManager.createSession();
6717
+ return {
6718
+ success: true,
6719
+ token: session.token,
6720
+ expiresAt: session.expiresAt
6721
+ };
6722
+ }
6723
+ );
6724
+ app.post(
6725
+ "/auth/lock",
6726
+ async (request, reply) => {
6727
+ const token = request.body?.token || extractToken(request);
6728
+ if (!token) {
6729
+ reply.code(400);
6730
+ return { success: false };
6731
+ }
6732
+ const sessionManager = getSessionManager();
6733
+ const invalidated = sessionManager.invalidateSession(token);
6734
+ return { success: invalidated };
6735
+ }
6736
+ );
6737
+ app.post(
6738
+ "/auth/setup",
6739
+ async (request, reply) => {
6740
+ const parseResult = SetupPasscodeRequestSchema.safeParse(request.body);
6741
+ if (!parseResult.success) {
6742
+ reply.code(400);
6743
+ return {
6744
+ success: false,
6745
+ error: "Invalid request: " + parseResult.error.message
6746
+ };
6747
+ }
6748
+ const { passcode, enableProtection = true } = parseResult.data;
6749
+ const alreadySet = await isPasscodeSet();
6750
+ if (alreadySet) {
6751
+ reply.code(409);
6752
+ return {
6753
+ success: false,
6754
+ error: "Passcode already configured. Use /auth/change to update."
6755
+ };
6756
+ }
6757
+ await setPasscode(passcode);
6758
+ if (enableProtection) {
6759
+ setProtectionEnabled(true);
6760
+ }
6761
+ return { success: true };
6654
6762
  }
6655
- }
6656
- /**
6657
- * Make a request to the broker
6658
- */
6659
- async request(method, params, options) {
6660
- const channel = options?.channel || (this.preferSocket ? "socket" : "http");
6661
- const timeout = options?.timeout || this.timeout;
6662
- if (channel === "socket") {
6663
- try {
6664
- return await this.socketRequest(method, params, timeout);
6665
- } catch (error) {
6666
- if (!options?.channel) {
6667
- return await this.httpRequest_internal(method, params, timeout);
6763
+ );
6764
+ app.post(
6765
+ "/auth/change",
6766
+ async (request, reply) => {
6767
+ const parseResult = ChangePasscodeRequestSchema.safeParse(request.body);
6768
+ if (!parseResult.success) {
6769
+ reply.code(400);
6770
+ return {
6771
+ success: false,
6772
+ error: "Invalid request: " + parseResult.error.message
6773
+ };
6774
+ }
6775
+ const { oldPasscode, newPasscode } = parseResult.data;
6776
+ const passcodeSet = await isPasscodeSet();
6777
+ if (!passcodeSet) {
6778
+ reply.code(400);
6779
+ return {
6780
+ success: false,
6781
+ error: "Passcode not configured. Use /auth/setup first."
6782
+ };
6783
+ }
6784
+ if (!isRunningAsRoot()) {
6785
+ if (!oldPasscode) {
6786
+ reply.code(400);
6787
+ return {
6788
+ success: false,
6789
+ error: "Old passcode required"
6790
+ };
6668
6791
  }
6669
- throw error;
6792
+ const valid = await checkPasscode(oldPasscode);
6793
+ if (!valid) {
6794
+ const remainingAttempts = recordFailedAttempt();
6795
+ reply.code(401);
6796
+ return {
6797
+ success: false,
6798
+ error: "Invalid old passcode"
6799
+ };
6800
+ }
6801
+ clearFailedAttempts();
6670
6802
  }
6671
- } else {
6672
- return await this.httpRequest_internal(method, params, timeout);
6803
+ await setPasscode(newPasscode);
6804
+ const sessionManager = getSessionManager();
6805
+ sessionManager.clearAllSessions();
6806
+ return { success: true };
6673
6807
  }
6674
- }
6675
- /**
6676
- * Make a request via Unix socket
6677
- */
6678
- async socketRequest(method, params, timeout) {
6679
- return new Promise((resolve3, reject) => {
6680
- const socket = net2.createConnection(this.socketPath);
6681
- const id = randomUUID3();
6682
- let responseData = "";
6683
- let timeoutId;
6684
- socket.on("connect", () => {
6685
- const request = {
6686
- jsonrpc: "2.0",
6687
- id,
6688
- method,
6689
- params
6808
+ );
6809
+ app.post(
6810
+ "/auth/refresh",
6811
+ async (request, reply) => {
6812
+ const token = extractToken(request);
6813
+ if (!token) {
6814
+ reply.code(401);
6815
+ return {
6816
+ success: false,
6817
+ error: "No token provided"
6690
6818
  };
6691
- socket.write(JSON.stringify(request) + "\n");
6692
- timeoutId = setTimeout(() => {
6693
- socket.destroy();
6694
- reject(new Error("Request timeout"));
6695
- }, timeout);
6696
- });
6697
- socket.on("data", (data) => {
6698
- responseData += data.toString();
6699
- const newlineIndex = responseData.indexOf("\n");
6700
- if (newlineIndex !== -1) {
6701
- clearTimeout(timeoutId);
6702
- socket.end();
6703
- try {
6704
- const response = JSON.parse(
6705
- responseData.slice(0, newlineIndex)
6706
- );
6707
- if (response.error) {
6708
- const error = new Error(response.error.message);
6709
- error.code = response.error.code;
6710
- reject(error);
6711
- } else {
6712
- resolve3(response.result);
6713
- }
6714
- } catch (error) {
6715
- reject(new Error("Invalid response from broker"));
6716
- }
6819
+ }
6820
+ const sessionManager = getSessionManager();
6821
+ const refreshed = sessionManager.refreshSession(token);
6822
+ if (!refreshed) {
6823
+ reply.code(401);
6824
+ return {
6825
+ success: false,
6826
+ error: "Invalid or expired token"
6827
+ };
6828
+ }
6829
+ return {
6830
+ success: true,
6831
+ token: refreshed.token,
6832
+ expiresAt: refreshed.expiresAt
6833
+ };
6834
+ }
6835
+ );
6836
+ app.post(
6837
+ "/auth/enable",
6838
+ async (request, reply) => {
6839
+ const passcodeSet = await isPasscodeSet();
6840
+ if (!passcodeSet) {
6841
+ reply.code(400);
6842
+ return {
6843
+ success: false,
6844
+ error: "Passcode not configured. Use /auth/setup first."
6845
+ };
6846
+ }
6847
+ setProtectionEnabled(true);
6848
+ return { success: true };
6849
+ }
6850
+ );
6851
+ app.post(
6852
+ "/auth/disable",
6853
+ async (request, reply) => {
6854
+ if (!isRunningAsRoot()) {
6855
+ const token = extractToken(request);
6856
+ if (!token) {
6857
+ reply.code(401);
6858
+ return {
6859
+ success: false,
6860
+ error: "Authentication required to disable protection"
6861
+ };
6862
+ }
6863
+ const sessionManager = getSessionManager();
6864
+ const session = sessionManager.validateSession(token);
6865
+ if (!session) {
6866
+ reply.code(401);
6867
+ return {
6868
+ success: false,
6869
+ error: "Invalid or expired token"
6870
+ };
6717
6871
  }
6718
- });
6719
- socket.on("error", (error) => {
6720
- clearTimeout(timeoutId);
6721
- reject(error);
6722
- });
6723
- });
6724
- }
6725
- /**
6726
- * Make a request via HTTP
6727
- */
6728
- async httpRequest_internal(method, params, timeout) {
6729
- const url = `http://${this.httpHost}:${this.httpPort}/rpc`;
6730
- const id = randomUUID3();
6731
- const request = {
6732
- jsonrpc: "2.0",
6733
- id,
6734
- method,
6735
- params
6736
- };
6737
- const controller = new AbortController();
6738
- const timeoutId = setTimeout(() => controller.abort(), timeout);
6739
- try {
6740
- const response = await fetch(url, {
6741
- method: "POST",
6742
- headers: { "Content-Type": "application/json" },
6743
- body: JSON.stringify(request),
6744
- signal: controller.signal
6745
- });
6746
- clearTimeout(timeoutId);
6747
- if (!response.ok) {
6748
- throw new Error(`HTTP error: ${response.status}`);
6749
6872
  }
6750
- const jsonResponse = await response.json();
6751
- if (jsonResponse.error) {
6752
- const error = new Error(jsonResponse.error.message);
6753
- error.code = jsonResponse.error.code;
6754
- throw error;
6873
+ setProtectionEnabled(false);
6874
+ return { success: true };
6875
+ }
6876
+ );
6877
+ app.post(
6878
+ "/auth/anonymous-readonly",
6879
+ async (request, reply) => {
6880
+ if (!isRunningAsRoot()) {
6881
+ const token = extractToken(request);
6882
+ if (!token) {
6883
+ reply.code(401);
6884
+ return {
6885
+ success: false,
6886
+ error: "Authentication required to change anonymous access"
6887
+ };
6888
+ }
6889
+ const sessionManager = getSessionManager();
6890
+ const session = sessionManager.validateSession(token);
6891
+ if (!session) {
6892
+ reply.code(401);
6893
+ return {
6894
+ success: false,
6895
+ error: "Invalid or expired token"
6896
+ };
6897
+ }
6898
+ }
6899
+ const { allowed } = request.body;
6900
+ if (typeof allowed !== "boolean") {
6901
+ reply.code(400);
6902
+ return {
6903
+ success: false,
6904
+ error: 'Invalid request: "allowed" must be a boolean'
6905
+ };
6906
+ }
6907
+ setAnonymousReadOnly(allowed);
6908
+ return { success: true, allowAnonymousReadOnly: allowed };
6909
+ }
6910
+ );
6911
+ }
6912
+
6913
+ // libs/shield-daemon/src/routes/secrets.ts
6914
+ import { isSecretEnvVar } from "@agenshield/sandbox";
6915
+ import crypto4 from "node:crypto";
6916
+ function maskValue(value) {
6917
+ if (value.length <= 4) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
6918
+ return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + value.slice(-4);
6919
+ }
6920
+ function toMasked(secret) {
6921
+ return {
6922
+ id: secret.id,
6923
+ name: secret.name,
6924
+ policyIds: secret.policyIds,
6925
+ maskedValue: maskValue(secret.value),
6926
+ createdAt: secret.createdAt
6927
+ };
6928
+ }
6929
+ async function secretsRoutes(app) {
6930
+ app.get("/secrets", async () => {
6931
+ const vault = getVault();
6932
+ const secrets = await vault.get("secrets") ?? [];
6933
+ return { data: secrets.map(toMasked) };
6934
+ });
6935
+ app.get("/secrets/env", async () => {
6936
+ const names = /* @__PURE__ */ new Set();
6937
+ for (const key of Object.keys(process.env)) {
6938
+ if (process.env[key] && isSecretEnvVar(key)) {
6939
+ names.add(key);
6755
6940
  }
6756
- return jsonResponse.result;
6757
- } catch (error) {
6758
- clearTimeout(timeoutId);
6759
- if (error.name === "AbortError") {
6760
- throw new Error("Request timeout");
6941
+ }
6942
+ const userSecrets = process.env["AGENSHIELD_USER_SECRETS"];
6943
+ if (userSecrets) {
6944
+ for (const name of userSecrets.split(",").filter(Boolean)) {
6945
+ names.add(name);
6761
6946
  }
6762
- throw error;
6763
6947
  }
6764
- }
6765
- };
6766
-
6767
- // libs/shield-daemon/src/services/broker-bridge.ts
6768
- var brokerClient = null;
6769
- function getBrokerClient() {
6770
- if (!brokerClient) {
6771
- brokerClient = new BrokerClient({
6772
- socketPath: process.env["AGENSHIELD_SOCKET"] || "/var/run/agenshield/agenshield.sock",
6773
- httpHost: "localhost",
6774
- httpPort: 5201,
6775
- // Broker uses 5201, daemon uses 5200
6776
- timeout: 6e4,
6777
- // 60s timeout for file operations
6778
- preferSocket: true
6779
- });
6780
- }
6781
- return brokerClient;
6782
- }
6783
- async function isBrokerAvailable() {
6784
- try {
6785
- const client = getBrokerClient();
6786
- return await client.isAvailable();
6787
- } catch {
6788
- return false;
6789
- }
6790
- }
6791
- async function installSkillViaBroker(slug, files, options = {}) {
6792
- const client = getBrokerClient();
6793
- const brokerFiles = files.map((file) => ({
6794
- name: file.name,
6795
- content: file.content,
6796
- base64: false
6797
- }));
6798
- const result = await client.skillInstall({
6799
- slug,
6800
- files: brokerFiles,
6801
- createWrapper: options.createWrapper ?? true,
6802
- agentHome: options.agentHome,
6803
- socketGroup: options.socketGroup
6948
+ return { data: Array.from(names).sort((a, b) => a.localeCompare(b)) };
6804
6949
  });
6805
- return result;
6950
+ app.post(
6951
+ "/secrets",
6952
+ async (request) => {
6953
+ const { name, value, policyIds } = request.body;
6954
+ if (!name?.trim() || !value?.trim()) {
6955
+ return { success: false, error: "Name and value are required" };
6956
+ }
6957
+ const vault = getVault();
6958
+ const secrets = await vault.get("secrets") ?? [];
6959
+ const newSecret = {
6960
+ id: crypto4.randomUUID(),
6961
+ name: name.trim(),
6962
+ value,
6963
+ policyIds: policyIds ?? [],
6964
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6965
+ };
6966
+ secrets.push(newSecret);
6967
+ await vault.set("secrets", secrets);
6968
+ return { data: toMasked(newSecret) };
6969
+ }
6970
+ );
6971
+ app.patch(
6972
+ "/secrets/:id",
6973
+ async (request) => {
6974
+ const { id } = request.params;
6975
+ const { policyIds } = request.body;
6976
+ const vault = getVault();
6977
+ const secrets = await vault.get("secrets") ?? [];
6978
+ const idx = secrets.findIndex((s) => s.id === id);
6979
+ if (idx === -1) return { success: false, error: "Secret not found" };
6980
+ secrets[idx].policyIds = policyIds ?? [];
6981
+ await vault.set("secrets", secrets);
6982
+ return { data: toMasked(secrets[idx]) };
6983
+ }
6984
+ );
6985
+ app.delete(
6986
+ "/secrets/:id",
6987
+ async (request) => {
6988
+ const { id } = request.params;
6989
+ const vault = getVault();
6990
+ const secrets = await vault.get("secrets") ?? [];
6991
+ const filtered = secrets.filter((s) => s.id !== id);
6992
+ if (filtered.length === secrets.length) {
6993
+ return { success: false, error: "Secret not found" };
6994
+ }
6995
+ await vault.set("secrets", filtered);
6996
+ return { deleted: true };
6997
+ }
6998
+ );
6806
6999
  }
6807
7000
 
6808
7001
  // libs/shield-daemon/src/routes/marketplace.ts
7002
+ import * as fs15 from "node:fs";
7003
+ import * as path14 from "node:path";
6809
7004
  async function marketplaceRoutes(app) {
6810
7005
  app.get(
6811
7006
  "/marketplace/search",
@@ -6841,6 +7036,10 @@ async function marketplaceRoutes(app) {
6841
7036
  if (localMeta) {
6842
7037
  const localFiles = getDownloadedSkillFiles(slug);
6843
7038
  const readmeFile = localFiles.find((f) => /readme|skill\.md/i.test(f.name));
7039
+ let readme = readmeFile?.content;
7040
+ if (readme) {
7041
+ readme = inlineImagesInMarkdown(readme, localFiles);
7042
+ }
6844
7043
  const skill2 = {
6845
7044
  name: localMeta.name,
6846
7045
  slug: localMeta.slug,
@@ -6850,7 +7049,7 @@ async function marketplaceRoutes(app) {
6850
7049
  installs: 0,
6851
7050
  // Not stored locally
6852
7051
  tags: localMeta.tags,
6853
- readme: readmeFile?.content,
7052
+ readme,
6854
7053
  files: localFiles
6855
7054
  };
6856
7055
  if (localMeta.analysis) {
@@ -7099,6 +7298,120 @@ async function fsRoutes(app) {
7099
7298
  });
7100
7299
  }
7101
7300
 
7301
+ // libs/shield-daemon/src/services/activity-log.ts
7302
+ import * as fs17 from "node:fs";
7303
+ import * as path16 from "node:path";
7304
+ var ACTIVITY_FILE = "activity.jsonl";
7305
+ var MAX_SIZE_BYTES = 100 * 1024 * 1024;
7306
+ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7307
+ var PRUNE_INTERVAL = 1e3;
7308
+ var instance = null;
7309
+ function getActivityLog() {
7310
+ if (!instance) {
7311
+ instance = new ActivityLog();
7312
+ }
7313
+ return instance;
7314
+ }
7315
+ var ActivityLog = class {
7316
+ filePath;
7317
+ writeCount = 0;
7318
+ unsubscribe;
7319
+ constructor() {
7320
+ this.filePath = path16.join(getConfigDir(), ACTIVITY_FILE);
7321
+ }
7322
+ /** Read historical events from the JSONL file, newest first */
7323
+ getHistory(limit = 500) {
7324
+ if (!fs17.existsSync(this.filePath)) return [];
7325
+ const content = fs17.readFileSync(this.filePath, "utf-8");
7326
+ const lines = content.split("\n").filter(Boolean);
7327
+ const events = [];
7328
+ for (const line of lines) {
7329
+ try {
7330
+ const evt = JSON.parse(line);
7331
+ if (evt.type === "heartbeat") continue;
7332
+ events.push(evt);
7333
+ } catch {
7334
+ }
7335
+ }
7336
+ events.reverse();
7337
+ return events.slice(0, limit);
7338
+ }
7339
+ start() {
7340
+ this.pruneOldEntries();
7341
+ this.unsubscribe = daemonEvents.subscribe((event) => {
7342
+ this.append(event);
7343
+ });
7344
+ }
7345
+ stop() {
7346
+ this.unsubscribe?.();
7347
+ }
7348
+ append(event) {
7349
+ const line = JSON.stringify(event) + "\n";
7350
+ fs17.appendFileSync(this.filePath, line, "utf-8");
7351
+ this.writeCount++;
7352
+ if (this.writeCount % PRUNE_INTERVAL === 0) {
7353
+ this.rotate();
7354
+ }
7355
+ }
7356
+ rotate() {
7357
+ try {
7358
+ const stat = fs17.statSync(this.filePath);
7359
+ if (stat.size > MAX_SIZE_BYTES) {
7360
+ this.truncateBySize();
7361
+ }
7362
+ } catch {
7363
+ }
7364
+ this.pruneOldEntries();
7365
+ }
7366
+ /** Keep newest half of lines when file exceeds size limit */
7367
+ truncateBySize() {
7368
+ const content = fs17.readFileSync(this.filePath, "utf-8");
7369
+ const lines = content.split("\n").filter(Boolean);
7370
+ const keep = lines.slice(Math.floor(lines.length / 2));
7371
+ fs17.writeFileSync(this.filePath, keep.join("\n") + "\n", "utf-8");
7372
+ }
7373
+ /** Remove entries older than 24 hours */
7374
+ pruneOldEntries() {
7375
+ if (!fs17.existsSync(this.filePath)) return;
7376
+ const content = fs17.readFileSync(this.filePath, "utf-8");
7377
+ const lines = content.split("\n").filter(Boolean);
7378
+ const cutoff = Date.now() - MAX_AGE_MS;
7379
+ const kept = lines.filter((line) => {
7380
+ try {
7381
+ const evt = JSON.parse(line);
7382
+ return new Date(evt.timestamp).getTime() >= cutoff;
7383
+ } catch {
7384
+ return false;
7385
+ }
7386
+ });
7387
+ if (kept.length < lines.length) {
7388
+ fs17.writeFileSync(this.filePath, kept.join("\n") + "\n", "utf-8");
7389
+ }
7390
+ }
7391
+ };
7392
+
7393
+ // libs/shield-daemon/src/routes/activity.ts
7394
+ async function activityRoutes(app) {
7395
+ app.get(
7396
+ "/activity",
7397
+ async (request) => {
7398
+ const authenticated = isAuthenticated(request);
7399
+ const raw = Number(request.query.limit) || 500;
7400
+ const limit = Math.min(Math.max(raw, 1), 1e4);
7401
+ const events = getActivityLog().getHistory(limit);
7402
+ if (authenticated) {
7403
+ return { data: events };
7404
+ }
7405
+ const stripped = events.map((e) => ({
7406
+ type: e.type,
7407
+ timestamp: e.timestamp,
7408
+ data: {}
7409
+ }));
7410
+ return { data: stripped };
7411
+ }
7412
+ );
7413
+ }
7414
+
7102
7415
  // libs/shield-daemon/src/routes/rpc.ts
7103
7416
  function globToRegex(pattern) {
7104
7417
  const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/{{GLOBSTAR}}/g, ".*");
@@ -7297,7 +7610,7 @@ async function registerRoutes(app) {
7297
7610
  });
7298
7611
  app.addHook("onResponse", (request, reply, done) => {
7299
7612
  if (!request.url.startsWith("/sse") && !request.url.startsWith("/rpc") && !request.url.includes(".") && !request.url.endsWith("/health")) {
7300
- if (request.method === "GET" && request.url.startsWith("/api/status") && reply.statusCode === 200) {
7613
+ if (request.method === "GET" && (request.url.startsWith("/api/status") || request.url.startsWith("/api/activity")) && reply.statusCode === 200) {
7301
7614
  done();
7302
7615
  return;
7303
7616
  }
@@ -7350,28 +7663,29 @@ async function registerRoutes(app) {
7350
7663
  await api.register(secretsRoutes);
7351
7664
  await api.register(marketplaceRoutes);
7352
7665
  await api.register(fsRoutes);
7666
+ await api.register(activityRoutes);
7353
7667
  },
7354
7668
  { prefix: API_PREFIX }
7355
7669
  );
7356
7670
  }
7357
7671
 
7358
7672
  // libs/shield-daemon/src/static.ts
7359
- import * as fs17 from "node:fs";
7360
- import * as path16 from "node:path";
7673
+ import * as fs18 from "node:fs";
7674
+ import * as path17 from "node:path";
7361
7675
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7362
7676
  var __filename = fileURLToPath2(import.meta.url);
7363
- var __dirname = path16.dirname(__filename);
7677
+ var __dirname = path17.dirname(__filename);
7364
7678
  function getUiAssetsPath() {
7365
- const pkgRootPath = path16.join(__dirname, "..", "ui-assets");
7366
- if (fs17.existsSync(pkgRootPath)) {
7679
+ const pkgRootPath = path17.join(__dirname, "..", "ui-assets");
7680
+ if (fs18.existsSync(pkgRootPath)) {
7367
7681
  return pkgRootPath;
7368
7682
  }
7369
- const bundledPath = path16.join(__dirname, "ui-assets");
7370
- if (fs17.existsSync(bundledPath)) {
7683
+ const bundledPath = path17.join(__dirname, "ui-assets");
7684
+ if (fs18.existsSync(bundledPath)) {
7371
7685
  return bundledPath;
7372
7686
  }
7373
- const devPath = path16.join(__dirname, "..", "..", "..", "dist", "apps", "shield-ui");
7374
- if (fs17.existsSync(devPath)) {
7687
+ const devPath = path17.join(__dirname, "..", "..", "..", "dist", "apps", "shield-ui");
7688
+ if (fs18.existsSync(devPath)) {
7375
7689
  return devPath;
7376
7690
  }
7377
7691
  return null;
@@ -7430,74 +7744,6 @@ function stopSecurityWatcher() {
7430
7744
  }
7431
7745
  }
7432
7746
 
7433
- // libs/shield-daemon/src/services/activity-log.ts
7434
- import * as fs18 from "node:fs";
7435
- import * as path17 from "node:path";
7436
- var ACTIVITY_FILE = "activity.jsonl";
7437
- var MAX_SIZE_BYTES = 100 * 1024 * 1024;
7438
- var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7439
- var PRUNE_INTERVAL = 1e3;
7440
- var ActivityLog = class {
7441
- filePath;
7442
- writeCount = 0;
7443
- unsubscribe;
7444
- constructor() {
7445
- this.filePath = path17.join(getConfigDir(), ACTIVITY_FILE);
7446
- }
7447
- start() {
7448
- this.pruneOldEntries();
7449
- this.unsubscribe = daemonEvents.subscribe((event) => {
7450
- this.append(event);
7451
- });
7452
- }
7453
- stop() {
7454
- this.unsubscribe?.();
7455
- }
7456
- append(event) {
7457
- const line = JSON.stringify(event) + "\n";
7458
- fs18.appendFileSync(this.filePath, line, "utf-8");
7459
- this.writeCount++;
7460
- if (this.writeCount % PRUNE_INTERVAL === 0) {
7461
- this.rotate();
7462
- }
7463
- }
7464
- rotate() {
7465
- try {
7466
- const stat = fs18.statSync(this.filePath);
7467
- if (stat.size > MAX_SIZE_BYTES) {
7468
- this.truncateBySize();
7469
- }
7470
- } catch {
7471
- }
7472
- this.pruneOldEntries();
7473
- }
7474
- /** Keep newest half of lines when file exceeds size limit */
7475
- truncateBySize() {
7476
- const content = fs18.readFileSync(this.filePath, "utf-8");
7477
- const lines = content.split("\n").filter(Boolean);
7478
- const keep = lines.slice(Math.floor(lines.length / 2));
7479
- fs18.writeFileSync(this.filePath, keep.join("\n") + "\n", "utf-8");
7480
- }
7481
- /** Remove entries older than 24 hours */
7482
- pruneOldEntries() {
7483
- if (!fs18.existsSync(this.filePath)) return;
7484
- const content = fs18.readFileSync(this.filePath, "utf-8");
7485
- const lines = content.split("\n").filter(Boolean);
7486
- const cutoff = Date.now() - MAX_AGE_MS;
7487
- const kept = lines.filter((line) => {
7488
- try {
7489
- const evt = JSON.parse(line);
7490
- return new Date(evt.timestamp).getTime() >= cutoff;
7491
- } catch {
7492
- return false;
7493
- }
7494
- });
7495
- if (kept.length < lines.length) {
7496
- fs18.writeFileSync(this.filePath, kept.join("\n") + "\n", "utf-8");
7497
- }
7498
- }
7499
- };
7500
-
7501
7747
  // libs/shield-daemon/src/server.ts
7502
7748
  async function createServer(config) {
7503
7749
  const app = Fastify({
@@ -7536,7 +7782,7 @@ async function startServer(config) {
7536
7782
  onQuarantined: (info) => emitSkillQuarantined(info.name, info.reason),
7537
7783
  onApproved: (name) => emitSkillApproved(name)
7538
7784
  }, 3e4);
7539
- const activityLog = new ActivityLog();
7785
+ const activityLog = getActivityLog();
7540
7786
  activityLog.start();
7541
7787
  try {
7542
7788
  const vault = getVault();