@solongate/proxy 0.42.1 → 0.43.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/dist/index.js CHANGED
@@ -2745,12 +2745,56 @@ var PolicyRuleSchema = z.object({
2745
2745
  createdAt: z.string().datetime(),
2746
2746
  updatedAt: z.string().datetime()
2747
2747
  });
2748
+ var GroupPolicyRuleSchema = z.object({
2749
+ toolPattern: z.string().min(1),
2750
+ permission: z.enum(["READ", "WRITE", "EXECUTE", "NETWORK"]).optional(),
2751
+ effect: z.enum(["ALLOW", "DENY"]),
2752
+ pathConstraints: z.object({
2753
+ allowed: z.array(z.string()).optional(),
2754
+ denied: z.array(z.string()).optional(),
2755
+ rootDirectory: z.string().optional(),
2756
+ allowSymlinks: z.boolean().optional()
2757
+ }).optional(),
2758
+ commandConstraints: z.object({
2759
+ allowed: z.array(z.string()).optional(),
2760
+ denied: z.array(z.string()).optional()
2761
+ }).optional(),
2762
+ urlConstraints: z.object({
2763
+ allowed: z.array(z.string()).optional(),
2764
+ denied: z.array(z.string()).optional()
2765
+ }).optional()
2766
+ });
2767
+ var AgentRelationshipConfigSchema = z.object({
2768
+ source: z.string().min(1),
2769
+ target: z.string().min(1),
2770
+ type: z.enum(["peer", "delegation", "supervisor"]),
2771
+ trustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]).optional(),
2772
+ allowedTools: z.array(z.string()).optional(),
2773
+ deniedTools: z.array(z.string()).optional(),
2774
+ allowedPermissions: z.array(z.string()).optional(),
2775
+ maxDelegationDepth: z.number().int().min(0).max(10).optional()
2776
+ });
2777
+ var DelegationConfigSchema = z.object({
2778
+ chain: z.array(z.string()).min(2),
2779
+ effectiveTools: z.array(z.string()).optional(),
2780
+ effectivePermissions: z.array(z.string()).optional()
2781
+ });
2782
+ var AgentTrustMapSchema = z.object({
2783
+ groups: z.record(z.object({
2784
+ description: z.string().optional(),
2785
+ members: z.array(z.string()),
2786
+ rules: z.array(GroupPolicyRuleSchema)
2787
+ })).optional(),
2788
+ relationships: z.array(AgentRelationshipConfigSchema).optional(),
2789
+ delegations: z.array(DelegationConfigSchema).optional()
2790
+ });
2748
2791
  var PolicySetSchema = z.object({
2749
2792
  id: z.string().min(1).max(256),
2750
2793
  name: z.string().min(1).max(256),
2751
2794
  description: z.string().max(2048),
2752
2795
  version: z.number().int().min(0),
2753
2796
  rules: z.array(PolicyRuleSchema),
2797
+ agentTrustMap: AgentTrustMapSchema.optional(),
2754
2798
  createdAt: z.string().datetime(),
2755
2799
  updatedAt: z.string().datetime()
2756
2800
  });
@@ -5409,6 +5453,132 @@ var SolonGate = class {
5409
5453
  }
5410
5454
  };
5411
5455
 
5456
+ // src/agent-trust.ts
5457
+ function matchGlob(value, pattern) {
5458
+ if (pattern === "*") return true;
5459
+ if (!pattern.includes("*")) return value === pattern;
5460
+ const regex = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
5461
+ return regex.test(value);
5462
+ }
5463
+ var AgentTrustEvaluator = class {
5464
+ localConfig = null;
5465
+ cloudRulesCache = /* @__PURE__ */ new Map();
5466
+ lastFetchTime = /* @__PURE__ */ new Map();
5467
+ fetchIntervalMs = 6e4;
5468
+ apiKey;
5469
+ apiUrl;
5470
+ constructor(apiKey, apiUrl) {
5471
+ this.apiKey = apiKey;
5472
+ this.apiUrl = apiUrl;
5473
+ }
5474
+ /** Load local trust map from policy.json's agentTrustMap section */
5475
+ loadLocal(config) {
5476
+ this.localConfig = config ?? null;
5477
+ }
5478
+ /** Fetch cloud rules for a specific agent */
5479
+ async fetchCloudRules(agentId) {
5480
+ if (!this.apiKey || !this.apiUrl) return;
5481
+ const lastFetch = this.lastFetchTime.get(agentId) ?? 0;
5482
+ if (Date.now() - lastFetch < this.fetchIntervalMs) return;
5483
+ try {
5484
+ const res = await fetch(`${this.apiUrl}/api/v1/trust-map/rules?agentId=${encodeURIComponent(agentId)}`, {
5485
+ headers: {
5486
+ "Authorization": `Bearer ${this.apiKey}`,
5487
+ "Content-Type": "application/json"
5488
+ },
5489
+ signal: AbortSignal.timeout(5e3)
5490
+ });
5491
+ if (res.ok) {
5492
+ const data = await res.json();
5493
+ this.cloudRulesCache.set(agentId, data);
5494
+ this.lastFetchTime.set(agentId, Date.now());
5495
+ }
5496
+ } catch {
5497
+ }
5498
+ }
5499
+ /**
5500
+ * Evaluate whether agentId is allowed to call toolName with the given permission.
5501
+ * DENY wins: if either local or cloud rules deny, the tool call is blocked.
5502
+ */
5503
+ evaluate(agentId, toolName, permission, subAgentId) {
5504
+ const effectiveId = subAgentId || agentId;
5505
+ const localDecision = this.evaluateLocal(effectiveId, toolName, permission);
5506
+ if (!localDecision.allowed) return localDecision;
5507
+ const cloudDecision = this.evaluateCloud(effectiveId, toolName, permission);
5508
+ if (!cloudDecision.allowed) return cloudDecision;
5509
+ return { allowed: true, reason: "Passed agent trust checks" };
5510
+ }
5511
+ evaluateLocal(agentId, toolName, permission) {
5512
+ if (!this.localConfig) return { allowed: true, reason: "No local agent trust map" };
5513
+ for (const [groupName, group] of Object.entries(this.localConfig.groups ?? {})) {
5514
+ if (group.members.includes(agentId)) {
5515
+ for (const rule of group.rules) {
5516
+ if (matchGlob(toolName, rule.toolPattern)) {
5517
+ if (rule.permission && rule.permission !== permission) continue;
5518
+ if (rule.effect === "DENY") {
5519
+ return { allowed: false, reason: `Blocked by group "${groupName}": ${rule.toolPattern}` };
5520
+ }
5521
+ }
5522
+ }
5523
+ }
5524
+ }
5525
+ for (const rel of this.localConfig.relationships ?? []) {
5526
+ if (rel.target === agentId) {
5527
+ if (rel.deniedTools?.some((p) => matchGlob(toolName, p))) {
5528
+ return { allowed: false, reason: `Blocked by relationship: ${rel.source} \u2192 ${rel.target}` };
5529
+ }
5530
+ if (rel.allowedTools && rel.allowedTools.length > 0) {
5531
+ if (!rel.allowedTools.some((p) => matchGlob(toolName, p))) {
5532
+ return { allowed: false, reason: `Tool not in allowed list: ${rel.source} \u2192 ${rel.target}` };
5533
+ }
5534
+ }
5535
+ }
5536
+ }
5537
+ for (const del of this.localConfig.delegations ?? []) {
5538
+ if (del.chain.includes(agentId)) {
5539
+ if (del.effectiveTools && del.effectiveTools.length > 0) {
5540
+ if (!del.effectiveTools.some((p) => matchGlob(toolName, p))) {
5541
+ return { allowed: false, reason: `Tool not in delegation chain effective tools` };
5542
+ }
5543
+ }
5544
+ }
5545
+ }
5546
+ return { allowed: true, reason: "Passed local checks" };
5547
+ }
5548
+ evaluateCloud(agentId, toolName, permission) {
5549
+ const rules = this.cloudRulesCache.get(agentId);
5550
+ if (!rules) return { allowed: true, reason: "No cloud rules cached" };
5551
+ for (const group of rules.groups) {
5552
+ for (const rule of group.policyRules) {
5553
+ if (matchGlob(toolName, rule.toolPattern)) {
5554
+ if (rule.permission && rule.permission !== permission) continue;
5555
+ if (rule.effect === "DENY") {
5556
+ return { allowed: false, reason: `Blocked by cloud group "${group.name}": ${rule.toolPattern}` };
5557
+ }
5558
+ }
5559
+ }
5560
+ }
5561
+ for (const rel of rules.relationships) {
5562
+ if (rel.deniedTools.some((p) => matchGlob(toolName, p))) {
5563
+ return { allowed: false, reason: `Blocked by cloud relationship: ${rel.sourceAgentId} \u2192 ${agentId}` };
5564
+ }
5565
+ if (rel.allowedTools.length > 0) {
5566
+ if (!rel.allowedTools.some((p) => matchGlob(toolName, p))) {
5567
+ return { allowed: false, reason: `Tool not in cloud allowed list: ${rel.sourceAgentId} \u2192 ${agentId}` };
5568
+ }
5569
+ }
5570
+ }
5571
+ for (const del of rules.delegations) {
5572
+ if (del.chain.includes(agentId) && del.effectiveTools.length > 0) {
5573
+ if (!del.effectiveTools.some((p) => matchGlob(toolName, p))) {
5574
+ return { allowed: false, reason: `Tool not in cloud delegation chain` };
5575
+ }
5576
+ }
5577
+ }
5578
+ return { allowed: true, reason: "Passed cloud checks" };
5579
+ }
5580
+ };
5581
+
5412
5582
  // src/proxy.ts
5413
5583
  init_config();
5414
5584
 
@@ -5940,6 +6110,8 @@ var SolonGateProxy = class {
5940
6110
  httpAgentInfo = /* @__PURE__ */ new Map();
5941
6111
  /** Per-request sub-agent info from HTTP headers (transient, overwritten per request) */
5942
6112
  httpSubAgent = null;
6113
+ /** Agent trust map evaluator for group/relationship/delegation rules */
6114
+ agentTrustEvaluator = null;
5943
6115
  constructor(config) {
5944
6116
  this.config = config;
5945
6117
  this.guardConfig = config.advancedDetection ? { ...DEFAULT_INPUT_GUARD_CONFIG, advancedDetection: config.advancedDetection } : DEFAULT_INPUT_GUARD_CONFIG;
@@ -5962,6 +6134,13 @@ var SolonGateProxy = class {
5962
6134
  for (const w of warnings) {
5963
6135
  log2("WARNING:", w);
5964
6136
  }
6137
+ if (config.policy.agentTrustMap || config.apiKey && !config.apiKey.startsWith("sg_test_")) {
6138
+ this.agentTrustEvaluator = new AgentTrustEvaluator(config.apiKey, config.apiUrl);
6139
+ if (config.policy.agentTrustMap) {
6140
+ this.agentTrustEvaluator.loadLocal(config.policy.agentTrustMap);
6141
+ log2("Agent trust map loaded from policy.json");
6142
+ }
6143
+ }
5965
6144
  }
5966
6145
  /** Normalize well-known MCP client names to display-friendly agent identities */
5967
6146
  normalizeAgentName(raw) {
@@ -6193,6 +6372,10 @@ var SolonGateProxy = class {
6193
6372
  } else if (this.config.agentName) {
6194
6373
  log2(`Agent identity from --agent-name flag: ${this.agentName} (clientInfo: ${clientVersion?.name || "none"})`);
6195
6374
  }
6375
+ if (this.agentTrustEvaluator && this.agentId) {
6376
+ this.agentTrustEvaluator.fetchCloudRules(this.agentId).catch(() => {
6377
+ });
6378
+ }
6196
6379
  }
6197
6380
  };
6198
6381
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -6212,6 +6395,36 @@ var SolonGateProxy = class {
6212
6395
  };
6213
6396
  }
6214
6397
  log2(`Tool call: ${name}`);
6398
+ if (this.agentTrustEvaluator) {
6399
+ const trustDecision = this.agentTrustEvaluator.evaluate(
6400
+ this.agentId ?? "unknown",
6401
+ name,
6402
+ guessPermission(name),
6403
+ subAgent?.subAgentId
6404
+ );
6405
+ if (!trustDecision.allowed) {
6406
+ log2(`DENY: ${name} \u2014 ${trustDecision.reason}`);
6407
+ const apiUrl = this.config.apiUrl || "https://api.solongate.com";
6408
+ if (this.config.apiKey && !this.config.apiKey.startsWith("sg_test_")) {
6409
+ sendAuditLog(this.config.apiKey, apiUrl, {
6410
+ tool: name,
6411
+ arguments: args ?? {},
6412
+ decision: "DENY",
6413
+ reason: trustDecision.reason,
6414
+ permission: guessPermission(name),
6415
+ evaluationTimeMs: 0,
6416
+ agent_id: this.agentId ?? void 0,
6417
+ agent_name: this.agentName ?? void 0,
6418
+ sub_agent_id: subAgent?.subAgentId,
6419
+ sub_agent_name: subAgent?.subAgentName
6420
+ });
6421
+ }
6422
+ return {
6423
+ content: [{ type: "text", text: `Blocked by agent trust policy: ${trustDecision.reason}` }],
6424
+ isError: true
6425
+ };
6426
+ }
6427
+ }
6215
6428
  let piResult;
6216
6429
  if (args && typeof args === "object") {
6217
6430
  const argsCheck = this.config.advancedDetection ? await sanitizeInputAsync("tool.arguments", args, this.guardConfig) : sanitizeInput("tool.arguments", args);
package/dist/lib.js CHANGED
@@ -692,12 +692,56 @@ var PolicyRuleSchema = z.object({
692
692
  createdAt: z.string().datetime(),
693
693
  updatedAt: z.string().datetime()
694
694
  });
695
+ var GroupPolicyRuleSchema = z.object({
696
+ toolPattern: z.string().min(1),
697
+ permission: z.enum(["READ", "WRITE", "EXECUTE", "NETWORK"]).optional(),
698
+ effect: z.enum(["ALLOW", "DENY"]),
699
+ pathConstraints: z.object({
700
+ allowed: z.array(z.string()).optional(),
701
+ denied: z.array(z.string()).optional(),
702
+ rootDirectory: z.string().optional(),
703
+ allowSymlinks: z.boolean().optional()
704
+ }).optional(),
705
+ commandConstraints: z.object({
706
+ allowed: z.array(z.string()).optional(),
707
+ denied: z.array(z.string()).optional()
708
+ }).optional(),
709
+ urlConstraints: z.object({
710
+ allowed: z.array(z.string()).optional(),
711
+ denied: z.array(z.string()).optional()
712
+ }).optional()
713
+ });
714
+ var AgentRelationshipConfigSchema = z.object({
715
+ source: z.string().min(1),
716
+ target: z.string().min(1),
717
+ type: z.enum(["peer", "delegation", "supervisor"]),
718
+ trustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]).optional(),
719
+ allowedTools: z.array(z.string()).optional(),
720
+ deniedTools: z.array(z.string()).optional(),
721
+ allowedPermissions: z.array(z.string()).optional(),
722
+ maxDelegationDepth: z.number().int().min(0).max(10).optional()
723
+ });
724
+ var DelegationConfigSchema = z.object({
725
+ chain: z.array(z.string()).min(2),
726
+ effectiveTools: z.array(z.string()).optional(),
727
+ effectivePermissions: z.array(z.string()).optional()
728
+ });
729
+ var AgentTrustMapSchema = z.object({
730
+ groups: z.record(z.object({
731
+ description: z.string().optional(),
732
+ members: z.array(z.string()),
733
+ rules: z.array(GroupPolicyRuleSchema)
734
+ })).optional(),
735
+ relationships: z.array(AgentRelationshipConfigSchema).optional(),
736
+ delegations: z.array(DelegationConfigSchema).optional()
737
+ });
695
738
  var PolicySetSchema = z.object({
696
739
  id: z.string().min(1).max(256),
697
740
  name: z.string().min(1).max(256),
698
741
  description: z.string().max(2048),
699
742
  version: z.number().int().min(0),
700
743
  rules: z.array(PolicyRuleSchema),
744
+ agentTrustMap: AgentTrustMapSchema.optional(),
701
745
  createdAt: z.string().datetime(),
702
746
  updatedAt: z.string().datetime()
703
747
  });
@@ -3629,6 +3673,132 @@ import { createServer as createHttpServer } from "http";
3629
3673
  import { resolve as resolve2, join } from "path";
3630
3674
  import { existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync2, appendFileSync } from "fs";
3631
3675
 
3676
+ // src/agent-trust.ts
3677
+ function matchGlob(value, pattern) {
3678
+ if (pattern === "*") return true;
3679
+ if (!pattern.includes("*")) return value === pattern;
3680
+ const regex = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
3681
+ return regex.test(value);
3682
+ }
3683
+ var AgentTrustEvaluator = class {
3684
+ localConfig = null;
3685
+ cloudRulesCache = /* @__PURE__ */ new Map();
3686
+ lastFetchTime = /* @__PURE__ */ new Map();
3687
+ fetchIntervalMs = 6e4;
3688
+ apiKey;
3689
+ apiUrl;
3690
+ constructor(apiKey, apiUrl) {
3691
+ this.apiKey = apiKey;
3692
+ this.apiUrl = apiUrl;
3693
+ }
3694
+ /** Load local trust map from policy.json's agentTrustMap section */
3695
+ loadLocal(config) {
3696
+ this.localConfig = config ?? null;
3697
+ }
3698
+ /** Fetch cloud rules for a specific agent */
3699
+ async fetchCloudRules(agentId) {
3700
+ if (!this.apiKey || !this.apiUrl) return;
3701
+ const lastFetch = this.lastFetchTime.get(agentId) ?? 0;
3702
+ if (Date.now() - lastFetch < this.fetchIntervalMs) return;
3703
+ try {
3704
+ const res = await fetch(`${this.apiUrl}/api/v1/trust-map/rules?agentId=${encodeURIComponent(agentId)}`, {
3705
+ headers: {
3706
+ "Authorization": `Bearer ${this.apiKey}`,
3707
+ "Content-Type": "application/json"
3708
+ },
3709
+ signal: AbortSignal.timeout(5e3)
3710
+ });
3711
+ if (res.ok) {
3712
+ const data = await res.json();
3713
+ this.cloudRulesCache.set(agentId, data);
3714
+ this.lastFetchTime.set(agentId, Date.now());
3715
+ }
3716
+ } catch {
3717
+ }
3718
+ }
3719
+ /**
3720
+ * Evaluate whether agentId is allowed to call toolName with the given permission.
3721
+ * DENY wins: if either local or cloud rules deny, the tool call is blocked.
3722
+ */
3723
+ evaluate(agentId, toolName, permission, subAgentId) {
3724
+ const effectiveId = subAgentId || agentId;
3725
+ const localDecision = this.evaluateLocal(effectiveId, toolName, permission);
3726
+ if (!localDecision.allowed) return localDecision;
3727
+ const cloudDecision = this.evaluateCloud(effectiveId, toolName, permission);
3728
+ if (!cloudDecision.allowed) return cloudDecision;
3729
+ return { allowed: true, reason: "Passed agent trust checks" };
3730
+ }
3731
+ evaluateLocal(agentId, toolName, permission) {
3732
+ if (!this.localConfig) return { allowed: true, reason: "No local agent trust map" };
3733
+ for (const [groupName, group] of Object.entries(this.localConfig.groups ?? {})) {
3734
+ if (group.members.includes(agentId)) {
3735
+ for (const rule of group.rules) {
3736
+ if (matchGlob(toolName, rule.toolPattern)) {
3737
+ if (rule.permission && rule.permission !== permission) continue;
3738
+ if (rule.effect === "DENY") {
3739
+ return { allowed: false, reason: `Blocked by group "${groupName}": ${rule.toolPattern}` };
3740
+ }
3741
+ }
3742
+ }
3743
+ }
3744
+ }
3745
+ for (const rel of this.localConfig.relationships ?? []) {
3746
+ if (rel.target === agentId) {
3747
+ if (rel.deniedTools?.some((p) => matchGlob(toolName, p))) {
3748
+ return { allowed: false, reason: `Blocked by relationship: ${rel.source} \u2192 ${rel.target}` };
3749
+ }
3750
+ if (rel.allowedTools && rel.allowedTools.length > 0) {
3751
+ if (!rel.allowedTools.some((p) => matchGlob(toolName, p))) {
3752
+ return { allowed: false, reason: `Tool not in allowed list: ${rel.source} \u2192 ${rel.target}` };
3753
+ }
3754
+ }
3755
+ }
3756
+ }
3757
+ for (const del of this.localConfig.delegations ?? []) {
3758
+ if (del.chain.includes(agentId)) {
3759
+ if (del.effectiveTools && del.effectiveTools.length > 0) {
3760
+ if (!del.effectiveTools.some((p) => matchGlob(toolName, p))) {
3761
+ return { allowed: false, reason: `Tool not in delegation chain effective tools` };
3762
+ }
3763
+ }
3764
+ }
3765
+ }
3766
+ return { allowed: true, reason: "Passed local checks" };
3767
+ }
3768
+ evaluateCloud(agentId, toolName, permission) {
3769
+ const rules = this.cloudRulesCache.get(agentId);
3770
+ if (!rules) return { allowed: true, reason: "No cloud rules cached" };
3771
+ for (const group of rules.groups) {
3772
+ for (const rule of group.policyRules) {
3773
+ if (matchGlob(toolName, rule.toolPattern)) {
3774
+ if (rule.permission && rule.permission !== permission) continue;
3775
+ if (rule.effect === "DENY") {
3776
+ return { allowed: false, reason: `Blocked by cloud group "${group.name}": ${rule.toolPattern}` };
3777
+ }
3778
+ }
3779
+ }
3780
+ }
3781
+ for (const rel of rules.relationships) {
3782
+ if (rel.deniedTools.some((p) => matchGlob(toolName, p))) {
3783
+ return { allowed: false, reason: `Blocked by cloud relationship: ${rel.sourceAgentId} \u2192 ${agentId}` };
3784
+ }
3785
+ if (rel.allowedTools.length > 0) {
3786
+ if (!rel.allowedTools.some((p) => matchGlob(toolName, p))) {
3787
+ return { allowed: false, reason: `Tool not in cloud allowed list: ${rel.sourceAgentId} \u2192 ${agentId}` };
3788
+ }
3789
+ }
3790
+ }
3791
+ for (const del of rules.delegations) {
3792
+ if (del.chain.includes(agentId) && del.effectiveTools.length > 0) {
3793
+ if (!del.effectiveTools.some((p) => matchGlob(toolName, p))) {
3794
+ return { allowed: false, reason: `Tool not in cloud delegation chain` };
3795
+ }
3796
+ }
3797
+ }
3798
+ return { allowed: true, reason: "Passed cloud checks" };
3799
+ }
3800
+ };
3801
+
3632
3802
  // src/config.ts
3633
3803
  import { readFileSync, existsSync } from "fs";
3634
3804
  import { appendFile } from "fs/promises";
@@ -4266,6 +4436,8 @@ var SolonGateProxy = class {
4266
4436
  httpAgentInfo = /* @__PURE__ */ new Map();
4267
4437
  /** Per-request sub-agent info from HTTP headers (transient, overwritten per request) */
4268
4438
  httpSubAgent = null;
4439
+ /** Agent trust map evaluator for group/relationship/delegation rules */
4440
+ agentTrustEvaluator = null;
4269
4441
  constructor(config) {
4270
4442
  this.config = config;
4271
4443
  this.guardConfig = config.advancedDetection ? { ...DEFAULT_INPUT_GUARD_CONFIG, advancedDetection: config.advancedDetection } : DEFAULT_INPUT_GUARD_CONFIG;
@@ -4288,6 +4460,13 @@ var SolonGateProxy = class {
4288
4460
  for (const w of warnings) {
4289
4461
  log2("WARNING:", w);
4290
4462
  }
4463
+ if (config.policy.agentTrustMap || config.apiKey && !config.apiKey.startsWith("sg_test_")) {
4464
+ this.agentTrustEvaluator = new AgentTrustEvaluator(config.apiKey, config.apiUrl);
4465
+ if (config.policy.agentTrustMap) {
4466
+ this.agentTrustEvaluator.loadLocal(config.policy.agentTrustMap);
4467
+ log2("Agent trust map loaded from policy.json");
4468
+ }
4469
+ }
4291
4470
  }
4292
4471
  /** Normalize well-known MCP client names to display-friendly agent identities */
4293
4472
  normalizeAgentName(raw) {
@@ -4519,6 +4698,10 @@ var SolonGateProxy = class {
4519
4698
  } else if (this.config.agentName) {
4520
4699
  log2(`Agent identity from --agent-name flag: ${this.agentName} (clientInfo: ${clientVersion?.name || "none"})`);
4521
4700
  }
4701
+ if (this.agentTrustEvaluator && this.agentId) {
4702
+ this.agentTrustEvaluator.fetchCloudRules(this.agentId).catch(() => {
4703
+ });
4704
+ }
4522
4705
  }
4523
4706
  };
4524
4707
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -4538,6 +4721,36 @@ var SolonGateProxy = class {
4538
4721
  };
4539
4722
  }
4540
4723
  log2(`Tool call: ${name}`);
4724
+ if (this.agentTrustEvaluator) {
4725
+ const trustDecision = this.agentTrustEvaluator.evaluate(
4726
+ this.agentId ?? "unknown",
4727
+ name,
4728
+ guessPermission(name),
4729
+ subAgent?.subAgentId
4730
+ );
4731
+ if (!trustDecision.allowed) {
4732
+ log2(`DENY: ${name} \u2014 ${trustDecision.reason}`);
4733
+ const apiUrl = this.config.apiUrl || "https://api.solongate.com";
4734
+ if (this.config.apiKey && !this.config.apiKey.startsWith("sg_test_")) {
4735
+ sendAuditLog(this.config.apiKey, apiUrl, {
4736
+ tool: name,
4737
+ arguments: args ?? {},
4738
+ decision: "DENY",
4739
+ reason: trustDecision.reason,
4740
+ permission: guessPermission(name),
4741
+ evaluationTimeMs: 0,
4742
+ agent_id: this.agentId ?? void 0,
4743
+ agent_name: this.agentName ?? void 0,
4744
+ sub_agent_id: subAgent?.subAgentId,
4745
+ sub_agent_name: subAgent?.subAgentName
4746
+ });
4747
+ }
4748
+ return {
4749
+ content: [{ type: "text", text: `Blocked by agent trust policy: ${trustDecision.reason}` }],
4750
+ isError: true
4751
+ };
4752
+ }
4753
+ }
4541
4754
  let piResult;
4542
4755
  if (args && typeof args === "object") {
4543
4756
  const argsCheck = this.config.advancedDetection ? await sanitizeInputAsync("tool.arguments", args, this.guardConfig) : sanitizeInput("tool.arguments", args);
package/hooks/audit.mjs CHANGED
@@ -21,6 +21,14 @@ function loadEnvKey(dir) {
21
21
  } catch { return {}; }
22
22
  }
23
23
 
24
+ function guessPermission(toolName) {
25
+ const name = (toolName || '').toLowerCase();
26
+ if (name.includes('exec') || name.includes('shell') || name.includes('run') || name.includes('eval') || name === 'bash') return 'EXECUTE';
27
+ if (name.includes('fetch') || name.includes('http') || name.includes('request') || name.includes('curl') || name.includes('network') || name.includes('download') || name.includes('upload') || name === 'websearch') return 'NETWORK';
28
+ if (name.includes('write') || name.includes('create') || name.includes('delete') || name.includes('update') || name.includes('set') || name.includes('edit') || name.includes('remove') || name.includes('insert')) return 'WRITE';
29
+ return 'READ';
30
+ }
31
+
24
32
  const dotenv = loadEnvKey(process.cwd());
25
33
  const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
26
34
  const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
@@ -102,6 +110,7 @@ process.stdin.on('end', async () => {
102
110
  arguments: argsSummary,
103
111
  decision: hasError ? 'DENY' : 'ALLOW',
104
112
  reason: guardDenied ? 'blocked by policy guard' : hasError ? 'tool returned error' : 'allowed',
113
+ permission: guessPermission(toolName),
105
114
  source: `${AGENT_ID}-hook`,
106
115
  evaluationTimeMs: 0,
107
116
  agent_id: AGENT_ID,
package/hooks/guard.mjs CHANGED
@@ -35,6 +35,14 @@ function loadEnvKey(dir) {
35
35
  } catch { return {}; }
36
36
  }
37
37
 
38
+ function guessPermission(toolName) {
39
+ const name = (toolName || '').toLowerCase();
40
+ if (name.includes('exec') || name.includes('shell') || name.includes('run') || name.includes('eval') || name === 'bash') return 'EXECUTE';
41
+ if (name.includes('fetch') || name.includes('http') || name.includes('request') || name.includes('curl') || name.includes('network') || name.includes('download') || name.includes('upload') || name === 'websearch') return 'NETWORK';
42
+ if (name.includes('write') || name.includes('create') || name.includes('delete') || name.includes('update') || name.includes('set') || name.includes('edit') || name.includes('remove') || name.includes('insert')) return 'WRITE';
43
+ return 'READ';
44
+ }
45
+
38
46
  const hookCwdEarly = process.cwd();
39
47
  const dotenv = loadEnvKey(hookCwdEarly);
40
48
  const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
@@ -358,6 +366,66 @@ function evaluate(policy, args) {
358
366
  return null;
359
367
  }
360
368
 
369
+ // ── Agent Trust Map Evaluation ──
370
+ function matchTrustGlob(value, pattern) {
371
+ if (pattern === '*') return true;
372
+ if (!pattern.includes('*')) return value === pattern;
373
+ const regex = new RegExp('^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
374
+ return regex.test(value);
375
+ }
376
+
377
+ function evaluateAgentTrust(policy, agentId, toolName, permission) {
378
+ const trustMap = policy && policy.agentTrustMap;
379
+ if (!trustMap) return null;
380
+
381
+ // 1. Group rules
382
+ if (trustMap.groups) {
383
+ for (const [groupName, group] of Object.entries(trustMap.groups)) {
384
+ if (group.members && group.members.includes(agentId)) {
385
+ for (const rule of (group.rules || [])) {
386
+ if (matchTrustGlob(toolName, rule.toolPattern)) {
387
+ if (rule.permission && rule.permission !== permission) continue;
388
+ if (rule.effect === 'DENY') {
389
+ return 'Blocked by agent group "' + groupName + '": ' + rule.toolPattern;
390
+ }
391
+ }
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ // 2. Relationship rules (where this agent is the target)
398
+ if (trustMap.relationships) {
399
+ for (const rel of trustMap.relationships) {
400
+ if (rel.target === agentId) {
401
+ if (rel.deniedTools && rel.deniedTools.some(p => matchTrustGlob(toolName, p))) {
402
+ return 'Blocked by relationship: ' + rel.source + ' → ' + rel.target;
403
+ }
404
+ if (rel.allowedTools && rel.allowedTools.length > 0) {
405
+ if (!rel.allowedTools.some(p => matchTrustGlob(toolName, p))) {
406
+ return 'Tool not in allowed list: ' + rel.source + ' → ' + rel.target;
407
+ }
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ // 3. Delegation chains
414
+ if (trustMap.delegations) {
415
+ for (const del of trustMap.delegations) {
416
+ if (del.chain && del.chain.includes(agentId)) {
417
+ if (del.effectiveTools && del.effectiveTools.length > 0) {
418
+ if (!del.effectiveTools.some(p => matchTrustGlob(toolName, p))) {
419
+ return 'Tool not in delegation chain effective tools';
420
+ }
421
+ }
422
+ }
423
+ }
424
+ }
425
+
426
+ return null;
427
+ }
428
+
361
429
  // ── Main ──
362
430
  let input = '';
363
431
  process.stdin.on('data', c => input += c);
@@ -386,6 +454,7 @@ process.stdin.on('end', async () => {
386
454
  session_id: raw.session_id || raw.sessionId || raw.conversation_id || '',
387
455
  };
388
456
  const args = data.tool_input;
457
+ const toolName = data.tool_name || '';
389
458
 
390
459
  // ── Self-protection: block access to hook files and settings ──
391
460
  // Hardcoded, no bypass possible — runs before policy/PI config
@@ -407,6 +476,7 @@ process.stdin.on('end', async () => {
407
476
  body: JSON.stringify({
408
477
  tool: data.tool_name || '', arguments: args,
409
478
  decision: 'DENY', reason,
479
+ permission: guessPermission(data.tool_name || ''),
410
480
  source: `${AGENT_ID}-guard`,
411
481
  agent_id: AGENT_ID, agent_name: AGENT_NAME,
412
482
  }),
@@ -988,7 +1058,7 @@ process.stdin.on('end', async () => {
988
1058
  }
989
1059
 
990
1060
  // ── Per-tool config: check if PI scanning is disabled for this tool ──
991
- const toolName = data.tool_name || '';
1061
+ // toolName already defined above (before self-protection)
992
1062
  if (piCfg.piToolConfig && typeof piCfg.piToolConfig === 'object') {
993
1063
  if (piCfg.piToolConfig[toolName] === false) {
994
1064
  // PI scanning explicitly disabled for this tool — skip detection
@@ -1054,6 +1124,7 @@ process.stdin.on('end', async () => {
1054
1124
  arguments: args,
1055
1125
  decision: isLogOnly ? 'ALLOW' : 'DENY',
1056
1126
  reason: msg,
1127
+ permission: guessPermission(toolName),
1057
1128
  source: `${AGENT_ID}-guard`,
1058
1129
  agent_id: AGENT_ID, agent_name: AGENT_NAME,
1059
1130
  pi_detected: true,
@@ -1115,6 +1186,7 @@ process.stdin.on('end', async () => {
1115
1186
  arguments: args,
1116
1187
  decision: 'ALLOW',
1117
1188
  reason: 'Prompt injection detected but below threshold (trust: ' + (piResult.trustScore * 100).toFixed(0) + '%)',
1189
+ permission: guessPermission(toolName),
1118
1190
  source: `${AGENT_ID}-guard`,
1119
1191
  agent_id: AGENT_ID, agent_name: AGENT_NAME,
1120
1192
  pi_detected: true,
@@ -1130,8 +1202,91 @@ process.stdin.on('end', async () => {
1130
1202
  allowTool(); // No policy = allow all
1131
1203
  }
1132
1204
 
1205
+ // ── Fetch cloud trust-map rules and merge into policy.agentTrustMap ──
1206
+ if (API_KEY && API_KEY.startsWith('sg_live_') && AGENT_ID) {
1207
+ const tmCacheFile = join(resolve('.solongate'), '.trust-rules-cache.json');
1208
+ let tmCached = null;
1209
+ try {
1210
+ if (existsSync(tmCacheFile)) {
1211
+ const cached = JSON.parse(readFileSync(tmCacheFile, 'utf-8'));
1212
+ if (cached._ts && Date.now() - cached._ts < 60000 && cached._agentId === AGENT_ID) {
1213
+ tmCached = cached;
1214
+ }
1215
+ }
1216
+ } catch {}
1217
+ if (!tmCached) {
1218
+ try {
1219
+ const tmRes = await fetch(API_URL + '/api/v1/trust-map/rules?agentId=' + encodeURIComponent(AGENT_ID), {
1220
+ headers: { 'Authorization': 'Bearer ' + API_KEY },
1221
+ signal: AbortSignal.timeout(5000),
1222
+ });
1223
+ if (tmRes.ok) {
1224
+ tmCached = await tmRes.json();
1225
+ tmCached._ts = Date.now();
1226
+ tmCached._agentId = AGENT_ID;
1227
+ try { writeFileSync(tmCacheFile, JSON.stringify(tmCached)); } catch {}
1228
+ }
1229
+ } catch {}
1230
+ }
1231
+ if (tmCached) {
1232
+ if (!policy) policy = {};
1233
+ if (!policy.agentTrustMap) policy.agentTrustMap = {};
1234
+ const tm = policy.agentTrustMap;
1235
+
1236
+ // Merge cloud groups into local groups
1237
+ if (tmCached.groups && tmCached.groups.length > 0) {
1238
+ if (!tm.groups) tm.groups = {};
1239
+ for (const g of tmCached.groups) {
1240
+ const key = 'cloud_' + g.id;
1241
+ if (!tm.groups[key]) {
1242
+ tm.groups[key] = {
1243
+ members: [AGENT_ID],
1244
+ rules: (g.policyRules || []).map(r => ({
1245
+ toolPattern: r.toolPattern || '*',
1246
+ permission: r.permission,
1247
+ effect: r.effect || 'DENY',
1248
+ })),
1249
+ };
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ // Merge cloud relationships into local relationships
1255
+ if (tmCached.relationships && tmCached.relationships.length > 0) {
1256
+ if (!tm.relationships) tm.relationships = [];
1257
+ for (const r of tmCached.relationships) {
1258
+ tm.relationships.push({
1259
+ source: r.sourceAgentId,
1260
+ target: AGENT_ID,
1261
+ type: r.relationshipType || 'peer',
1262
+ allowedTools: r.allowedTools || [],
1263
+ deniedTools: r.deniedTools || [],
1264
+ });
1265
+ }
1266
+ }
1267
+
1268
+ // Merge cloud delegations into local delegations
1269
+ if (tmCached.delegations && tmCached.delegations.length > 0) {
1270
+ if (!tm.delegations) tm.delegations = [];
1271
+ for (const d of tmCached.delegations) {
1272
+ tm.delegations.push({
1273
+ chain: d.chain || [],
1274
+ effectiveTools: d.effectiveTools || [],
1275
+ effectivePermissions: d.effectivePermissions || [],
1276
+ });
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+
1133
1282
  let reason = evaluate(policy, args);
1134
1283
 
1284
+ // ── Agent Trust Map evaluation ──
1285
+ if (!reason) {
1286
+ const trustReason = evaluateAgentTrust(policy, AGENT_ID, toolName, guessPermission(toolName));
1287
+ if (trustReason) reason = trustReason;
1288
+ }
1289
+
1135
1290
  // ── AI Judge: semantic intent analysis (runs when policy ALLOWs) ──
1136
1291
  if (!reason) {
1137
1292
  let GROQ_KEY = process.env.GROQ_API_KEY || dotenv.GROQ_API_KEY || '';
@@ -1302,6 +1457,7 @@ Respond with ONLY valid JSON: {"decision": "ALLOW" or "DENY", "reason": "brief e
1302
1457
  const logEntry = {
1303
1458
  tool: toolName, arguments: args,
1304
1459
  decision: 'DENY', reason,
1460
+ permission: guessPermission(toolName),
1305
1461
  source: `${AGENT_ID}-guard`,
1306
1462
  agent_id: AGENT_ID, agent_name: AGENT_NAME,
1307
1463
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.42.1",
3
+ "version": "0.43.0",
4
4
  "description": "AI tool security proxy — protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
5
5
  "type": "module",
6
6
  "bin": {