@tangle-network/agent-integrations 0.2.1 → 0.3.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
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { createHmac as createHmac3, randomUUID, timingSafeEqual as timingSafeEqual2 } from "crypto";
2
+ import { createHmac as createHmac3, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
3
3
 
4
4
  // src/connectors/types.ts
5
5
  var ResourceContention = class extends Error {
@@ -2640,6 +2640,434 @@ var slackEventsConnector = {
2640
2640
  }
2641
2641
  };
2642
2642
 
2643
+ // src/catalog.ts
2644
+ var riskRank = {
2645
+ read: 0,
2646
+ write: 1,
2647
+ destructive: 2
2648
+ };
2649
+ function integrationToolName(providerId, connectorId, actionId) {
2650
+ return `int_${encodeToolPart(providerId)}_${encodeToolPart(connectorId)}_${encodeToolPart(actionId)}`;
2651
+ }
2652
+ function parseIntegrationToolName(name) {
2653
+ const parts = name.split("_");
2654
+ if (parts.length !== 4 || parts[0] !== "int") {
2655
+ throw new Error(`Invalid integration tool name: ${name}`);
2656
+ }
2657
+ return {
2658
+ providerId: decodeToolPart(parts[1]),
2659
+ connectorId: decodeToolPart(parts[2]),
2660
+ actionId: decodeToolPart(parts[3])
2661
+ };
2662
+ }
2663
+ function buildIntegrationToolCatalog(connectors) {
2664
+ const tools = [];
2665
+ for (const connector of connectors) {
2666
+ for (const action of connector.actions) {
2667
+ const tags = unique([
2668
+ connector.id,
2669
+ connector.providerId,
2670
+ connector.title,
2671
+ connector.category,
2672
+ action.id,
2673
+ action.title,
2674
+ action.risk,
2675
+ action.dataClass,
2676
+ ...connector.scopes ?? [],
2677
+ ...action.requiredScopes ?? []
2678
+ ].flatMap(tokenize));
2679
+ tools.push({
2680
+ name: integrationToolName(connector.providerId, connector.id, action.id),
2681
+ title: `${connector.title}: ${action.title}`,
2682
+ description: action.description ?? `${action.risk} action ${action.id} on ${connector.title}`,
2683
+ providerId: connector.providerId,
2684
+ connectorId: connector.id,
2685
+ connectorTitle: connector.title,
2686
+ category: connector.category,
2687
+ action,
2688
+ risk: action.risk,
2689
+ dataClass: action.dataClass,
2690
+ requiredScopes: action.requiredScopes,
2691
+ inputSchema: action.inputSchema,
2692
+ outputSchema: action.outputSchema,
2693
+ tags
2694
+ });
2695
+ }
2696
+ }
2697
+ return tools;
2698
+ }
2699
+ function searchIntegrationTools(catalog, query, filters = {}) {
2700
+ const terms = tokenize(query);
2701
+ const filtered = catalog.filter((tool) => {
2702
+ if (filters.providerId && tool.providerId !== filters.providerId) return false;
2703
+ if (filters.connectorId && tool.connectorId !== filters.connectorId) return false;
2704
+ if (filters.category && tool.category !== filters.category) return false;
2705
+ if (filters.dataClass && tool.dataClass !== filters.dataClass) return false;
2706
+ if (filters.maxRisk && riskRank[tool.risk] > riskRank[filters.maxRisk]) return false;
2707
+ return true;
2708
+ });
2709
+ const scored = filtered.map((tool) => scoreTool(tool, terms));
2710
+ return scored.filter((result) => terms.length === 0 || result.score > 0).sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name)).slice(0, filters.limit ?? 20);
2711
+ }
2712
+ function toMcpTools(tools) {
2713
+ return tools.map((tool) => ({
2714
+ name: tool.name,
2715
+ description: `${tool.title}. ${tool.description}`,
2716
+ inputSchema: tool.inputSchema ?? {
2717
+ type: "object",
2718
+ additionalProperties: true,
2719
+ properties: {}
2720
+ }
2721
+ }));
2722
+ }
2723
+ function scoreTool(tool, terms) {
2724
+ if (terms.length === 0) return { tool, score: 1, matched: [] };
2725
+ const haystack = new Set(tool.tags);
2726
+ const matched = [];
2727
+ let score = 0;
2728
+ for (const term of terms) {
2729
+ if (haystack.has(term)) {
2730
+ matched.push(term);
2731
+ score += 4;
2732
+ continue;
2733
+ }
2734
+ if (tool.tags.some((tag) => tag.includes(term))) {
2735
+ matched.push(term);
2736
+ score += 1;
2737
+ }
2738
+ }
2739
+ if (tool.risk === "read") score += 0.25;
2740
+ return { tool, score, matched: unique(matched) };
2741
+ }
2742
+ function tokenize(value) {
2743
+ return value.toLowerCase().split(/[^a-z0-9]+/g).map((part) => part.trim()).filter(Boolean);
2744
+ }
2745
+ function encodeToolPart(value) {
2746
+ return Buffer.from(value, "utf8").toString("base64url");
2747
+ }
2748
+ function decodeToolPart(value) {
2749
+ return Buffer.from(value, "base64url").toString("utf8");
2750
+ }
2751
+ function unique(values) {
2752
+ return [...new Set(values)];
2753
+ }
2754
+
2755
+ // src/policy.ts
2756
+ import { randomUUID } from "crypto";
2757
+ var StaticIntegrationPolicyEngine = class {
2758
+ rules;
2759
+ defaultReadEffect;
2760
+ defaultWriteEffect;
2761
+ defaultDestructiveEffect;
2762
+ now;
2763
+ constructor(options = {}) {
2764
+ this.rules = options.rules ?? [];
2765
+ this.defaultReadEffect = options.defaultReadEffect ?? "allow";
2766
+ this.defaultWriteEffect = options.defaultWriteEffect ?? "require_approval";
2767
+ this.defaultDestructiveEffect = options.defaultDestructiveEffect ?? "deny";
2768
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2769
+ }
2770
+ decide(ctx) {
2771
+ const action = ctx.action;
2772
+ if (!action) return { decision: "deny", reason: "Integration action is missing from connector catalog." };
2773
+ const matched = this.rules.find((rule) => ruleMatches(rule, ctx));
2774
+ const effect = matched?.effect ?? this.defaultEffect(action.risk);
2775
+ const reason = matched?.reason ?? defaultReason(effect, action.risk);
2776
+ if (effect === "allow") return { decision: "allow", reason, metadata: matched ? { ruleId: matched.id } : void 0 };
2777
+ if (effect === "deny") return { decision: "deny", reason, metadata: matched ? { ruleId: matched.id } : void 0 };
2778
+ return {
2779
+ decision: "require_approval",
2780
+ reason,
2781
+ approval: buildApprovalRequest(ctx, reason, this.now()),
2782
+ metadata: matched ? { ruleId: matched.id } : void 0
2783
+ };
2784
+ }
2785
+ defaultEffect(risk) {
2786
+ if (risk === "read") return this.defaultReadEffect;
2787
+ if (risk === "write") return this.defaultWriteEffect;
2788
+ return this.defaultDestructiveEffect;
2789
+ }
2790
+ };
2791
+ function createDefaultIntegrationPolicyEngine(options = {}) {
2792
+ return new StaticIntegrationPolicyEngine(options);
2793
+ }
2794
+ function buildApprovalRequest(ctx, reason, requestedAt) {
2795
+ if (!ctx.action) {
2796
+ throw new Error("Cannot build approval request without an action descriptor.");
2797
+ }
2798
+ return {
2799
+ id: `approval_${randomUUID()}`,
2800
+ connectionId: ctx.connection.id,
2801
+ providerId: ctx.connection.providerId,
2802
+ connectorId: ctx.connection.connectorId,
2803
+ action: ctx.request.action,
2804
+ actor: { type: ctx.subject.type, id: ctx.subject.id },
2805
+ risk: ctx.action.risk,
2806
+ dataClass: ctx.action.dataClass,
2807
+ reason,
2808
+ requestedAt: requestedAt.toISOString(),
2809
+ inputPreview: previewInput(ctx.request.input)
2810
+ };
2811
+ }
2812
+ function redactApprovalRequest(request) {
2813
+ return {
2814
+ ...request,
2815
+ inputPreview: redactUnknown(request.inputPreview)
2816
+ };
2817
+ }
2818
+ function ruleMatches(rule, ctx) {
2819
+ if (!ctx.action) return false;
2820
+ if (rule.providerId && rule.providerId !== ctx.connection.providerId) return false;
2821
+ if (rule.connectorId && rule.connectorId !== ctx.connection.connectorId) return false;
2822
+ if (rule.action && rule.action !== ctx.request.action) return false;
2823
+ if (rule.risk && rule.risk !== ctx.action.risk) return false;
2824
+ if (rule.maxRisk && riskRank2(ctx.action.risk) > riskRank2(rule.maxRisk)) return false;
2825
+ if (rule.dataClass && rule.dataClass !== ctx.action.dataClass) return false;
2826
+ return true;
2827
+ }
2828
+ function riskRank2(risk) {
2829
+ if (risk === "read") return 0;
2830
+ if (risk === "write") return 1;
2831
+ return 2;
2832
+ }
2833
+ function defaultReason(effect, risk) {
2834
+ if (effect === "allow") return `${risk} integration action allowed by default policy.`;
2835
+ if (effect === "deny") return `${risk} integration action denied by default policy.`;
2836
+ return `${risk} integration action requires approval by default policy.`;
2837
+ }
2838
+ function previewInput(input) {
2839
+ return redactUnknown(input);
2840
+ }
2841
+ function redactUnknown(value) {
2842
+ if (Array.isArray(value)) return value.map(redactUnknown);
2843
+ if (!value || typeof value !== "object") return value;
2844
+ const out = {};
2845
+ for (const [key, child] of Object.entries(value)) {
2846
+ if (/token|secret|password|authorization|api[_-]?key|credential/i.test(key)) {
2847
+ out[key] = "[REDACTED]";
2848
+ } else {
2849
+ out[key] = redactUnknown(child);
2850
+ }
2851
+ }
2852
+ return out;
2853
+ }
2854
+
2855
+ // src/sandbox.ts
2856
+ function buildIntegrationInvocationEnvelope(input) {
2857
+ const parsed = parseIntegrationToolName(input.toolName);
2858
+ return {
2859
+ kind: "integration.invocation",
2860
+ capabilityToken: input.capabilityToken,
2861
+ toolName: input.toolName,
2862
+ action: parsed.actionId,
2863
+ input: input.args,
2864
+ idempotencyKey: input.idempotencyKey,
2865
+ dryRun: input.dryRun,
2866
+ metadata: input.metadata
2867
+ };
2868
+ }
2869
+ function invocationRequestFromEnvelope(envelope) {
2870
+ return {
2871
+ action: envelope.action,
2872
+ input: envelope.input,
2873
+ idempotencyKey: envelope.idempotencyKey,
2874
+ dryRun: envelope.dryRun,
2875
+ metadata: envelope.metadata
2876
+ };
2877
+ }
2878
+ function redactInvocationEnvelope(envelope) {
2879
+ return {
2880
+ ...envelope,
2881
+ capabilityToken: "[REDACTED]",
2882
+ input: redactUnknown2(envelope.input)
2883
+ };
2884
+ }
2885
+ function redactCapability(capability) {
2886
+ return {
2887
+ ...capability,
2888
+ metadata: redactUnknown2(capability.metadata)
2889
+ };
2890
+ }
2891
+ function normalizeIntegrationResult(result) {
2892
+ const output = result.output;
2893
+ if (!result.ok && output?.approvalRequired === true && output.approval) {
2894
+ return {
2895
+ status: "approval_required",
2896
+ action: result.action,
2897
+ approval: output.approval,
2898
+ metadata: result.metadata
2899
+ };
2900
+ }
2901
+ if (!result.ok) {
2902
+ return {
2903
+ status: "failed",
2904
+ action: result.action,
2905
+ error: String(result.output ?? result.warnings?.[0] ?? "integration action failed"),
2906
+ metadata: result.metadata
2907
+ };
2908
+ }
2909
+ return {
2910
+ status: "ok",
2911
+ action: result.action,
2912
+ output: result.output,
2913
+ metadata: result.metadata
2914
+ };
2915
+ }
2916
+ function redactUnknown2(value) {
2917
+ if (Array.isArray(value)) return value.map(redactUnknown2);
2918
+ if (!value || typeof value !== "object") return value;
2919
+ const out = {};
2920
+ for (const [key, child] of Object.entries(value)) {
2921
+ if (/token|secret|password|authorization|api[_-]?key|credential/i.test(key)) {
2922
+ out[key] = "[REDACTED]";
2923
+ } else {
2924
+ out[key] = redactUnknown2(child);
2925
+ }
2926
+ }
2927
+ return out;
2928
+ }
2929
+
2930
+ // src/adapter-provider.ts
2931
+ function createConnectorAdapterProvider(options) {
2932
+ const providerId = options.id ?? "first-party";
2933
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
2934
+ const adapters = /* @__PURE__ */ new Map();
2935
+ for (const adapter of options.adapters) {
2936
+ adapters.set(adapter.manifest.kind, adapter);
2937
+ }
2938
+ return {
2939
+ id: providerId,
2940
+ kind: options.kind ?? "first_party",
2941
+ listConnectors: () => [...adapters.values()].map((adapter) => manifestToConnector(providerId, adapter)),
2942
+ async invokeAction(connection, request) {
2943
+ const adapter = adapters.get(connection.connectorId);
2944
+ if (!adapter) {
2945
+ throw new IntegrationError(`Connector adapter ${connection.connectorId} not found.`, "connector_not_found");
2946
+ }
2947
+ const capability = adapter.manifest.capabilities.find((candidate) => candidate.name === request.action);
2948
+ if (!capability) {
2949
+ throw new IntegrationError(`Capability ${request.action} is not defined by ${connection.connectorId}.`, "action_not_found");
2950
+ }
2951
+ const source = await options.resolveDataSource(connection);
2952
+ const invocation = {
2953
+ source,
2954
+ capabilityName: request.action,
2955
+ args: toRecord(request.input),
2956
+ idempotencyKey: request.idempotencyKey ?? `idem_${connection.id}_${request.action}_${now().getTime()}`,
2957
+ expectedEtag: typeof request.metadata?.expectedEtag === "string" ? request.metadata.expectedEtag : void 0,
2958
+ callSessionId: typeof request.metadata?.callSessionId === "string" ? request.metadata.callSessionId : void 0
2959
+ };
2960
+ if (capability.class === "read") {
2961
+ if (!adapter.executeRead) {
2962
+ throw new IntegrationError(`Connector ${connection.connectorId} does not implement reads.`, "action_not_found");
2963
+ }
2964
+ const result = await adapter.executeRead(invocation);
2965
+ return readResultToAction(request, result);
2966
+ }
2967
+ if (capability.class === "mutation") {
2968
+ if (!adapter.executeMutation) {
2969
+ throw new IntegrationError(`Connector ${connection.connectorId} does not implement mutations.`, "action_not_found");
2970
+ }
2971
+ const result = await adapter.executeMutation(invocation);
2972
+ return mutationResultToAction(request, result);
2973
+ }
2974
+ throw new IntegrationError(`Capability ${request.action} is not invokable as an action.`, "action_not_found");
2975
+ }
2976
+ };
2977
+ }
2978
+ function manifestToConnector(providerId, adapter) {
2979
+ const manifest = adapter.manifest;
2980
+ return {
2981
+ id: manifest.kind,
2982
+ providerId,
2983
+ title: manifest.displayName,
2984
+ category: mapCategory(manifest.category),
2985
+ auth: mapAuth(manifest.auth.kind),
2986
+ scopes: manifest.auth.kind === "oauth2" ? manifest.auth.scopes : [],
2987
+ actions: manifest.capabilities.filter((capability) => capability.class === "read" || capability.class === "mutation").map((capability) => ({
2988
+ id: capability.name,
2989
+ title: titleFromName(capability.name),
2990
+ risk: capability.class === "read" ? "read" : capability.externalEffect ? "destructive" : "write",
2991
+ requiredScopes: capability.requiredScopes ?? [],
2992
+ dataClass: inferDataClass(manifest.category),
2993
+ description: capability.description,
2994
+ approvalRequired: capability.class === "mutation",
2995
+ inputSchema: capability.parameters
2996
+ }))
2997
+ };
2998
+ }
2999
+ function readResultToAction(request, result) {
3000
+ return {
3001
+ ok: true,
3002
+ action: request.action,
3003
+ output: result.data,
3004
+ metadata: {
3005
+ etag: result.etag,
3006
+ fetchedAt: result.fetchedAt
3007
+ }
3008
+ };
3009
+ }
3010
+ function mutationResultToAction(request, result) {
3011
+ if (result.status === "committed") {
3012
+ return {
3013
+ ok: true,
3014
+ action: request.action,
3015
+ output: result.data,
3016
+ metadata: {
3017
+ etagAfter: result.etagAfter,
3018
+ committedAt: result.committedAt,
3019
+ idempotentReplay: result.idempotentReplay
3020
+ }
3021
+ };
3022
+ }
3023
+ if (result.status === "conflict") {
3024
+ return {
3025
+ ok: false,
3026
+ action: request.action,
3027
+ output: {
3028
+ conflict: true,
3029
+ message: result.message,
3030
+ alternatives: result.alternatives,
3031
+ currentState: result.currentState
3032
+ }
3033
+ };
3034
+ }
3035
+ return {
3036
+ ok: false,
3037
+ action: request.action,
3038
+ output: {
3039
+ rateLimited: true,
3040
+ retryAfterMs: result.retryAfterMs,
3041
+ message: result.message
3042
+ }
3043
+ };
3044
+ }
3045
+ function mapAuth(kind) {
3046
+ if (kind === "oauth2") return "oauth2";
3047
+ if (kind === "api-key") return "api_key";
3048
+ if (kind === "none") return "none";
3049
+ return "custom";
3050
+ }
3051
+ function mapCategory(category) {
3052
+ if (category === "comms") return "chat";
3053
+ if (category === "spreadsheet") return "database";
3054
+ if (category === "doc") return "docs";
3055
+ if (category === "commerce") return "workflow";
3056
+ return category === "other" ? "other" : category;
3057
+ }
3058
+ function inferDataClass(category) {
3059
+ if (category === "commerce") return "sensitive";
3060
+ if (category === "webhook") return "internal";
3061
+ return "private";
3062
+ }
3063
+ function titleFromName(name) {
3064
+ return name.split(/[._-]/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
3065
+ }
3066
+ function toRecord(input) {
3067
+ if (input && typeof input === "object" && !Array.isArray(input)) return input;
3068
+ return {};
3069
+ }
3070
+
2643
3071
  // src/index.ts
2644
3072
  var IntegrationError = class extends Error {
2645
3073
  constructor(message, code) {
@@ -2671,6 +3099,7 @@ var IntegrationHub = class {
2671
3099
  store;
2672
3100
  capabilitySecret;
2673
3101
  guard;
3102
+ policy;
2674
3103
  now;
2675
3104
  constructor(options) {
2676
3105
  if (!options.capabilitySecret) {
@@ -2680,6 +3109,7 @@ var IntegrationHub = class {
2680
3109
  this.store = options.store;
2681
3110
  this.capabilitySecret = options.capabilitySecret;
2682
3111
  this.guard = options.guard;
3112
+ this.policy = options.policy;
2683
3113
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2684
3114
  }
2685
3115
  async listConnectors() {
@@ -2709,11 +3139,11 @@ var IntegrationHub = class {
2709
3139
  assertScopes(connection, request.scopes);
2710
3140
  const now = this.now();
2711
3141
  const capability = {
2712
- id: `cap_${randomUUID()}`,
3142
+ id: `cap_${randomUUID2()}`,
2713
3143
  subject: request.subject,
2714
3144
  connectionId: request.connectionId,
2715
- scopes: unique(request.scopes),
2716
- allowedActions: unique(request.allowedActions),
3145
+ scopes: unique2(request.scopes),
3146
+ allowedActions: unique2(request.allowedActions),
2717
3147
  issuedAt: now.toISOString(),
2718
3148
  expiresAt: new Date(now.getTime() + request.ttlMs).toISOString(),
2719
3149
  metadata: request.metadata
@@ -2741,6 +3171,25 @@ var IntegrationHub = class {
2741
3171
  assertScopes(connection, action.requiredScopes);
2742
3172
  assertScopes({ ...connection, grantedScopes: capability.scopes }, action.requiredScopes);
2743
3173
  const fullRequest = { ...request, connectionId: connection.id };
3174
+ if (this.policy) {
3175
+ const decision = await this.policy.decide({
3176
+ connection,
3177
+ request: fullRequest,
3178
+ action,
3179
+ subject: capability.subject
3180
+ });
3181
+ if (decision.decision === "deny") {
3182
+ throw new IntegrationError(decision.reason, "policy_denied");
3183
+ }
3184
+ if (decision.decision === "require_approval") {
3185
+ return {
3186
+ ok: false,
3187
+ action: request.action,
3188
+ output: { approvalRequired: true, approval: decision.approval },
3189
+ metadata: { policyDecision: decision.decision, reason: decision.reason, ...decision.metadata }
3190
+ };
3191
+ }
3192
+ }
2744
3193
  const proceed = () => Promise.resolve(provider.invokeAction(connection, fullRequest));
2745
3194
  if (this.guard) {
2746
3195
  return this.guard.invokeAction({ connection, request: fullRequest, action }, proceed);
@@ -2940,7 +3389,7 @@ function base64UrlEncode(value) {
2940
3389
  function base64UrlDecode(value) {
2941
3390
  return Buffer.from(value, "base64url").toString("utf8");
2942
3391
  }
2943
- function unique(values) {
3392
+ function unique2(values) {
2944
3393
  return [...new Set(values)];
2945
3394
  }
2946
3395
  export {
@@ -2951,9 +3400,15 @@ export {
2951
3400
  IntegrationError,
2952
3401
  IntegrationHub,
2953
3402
  ResourceContention,
3403
+ StaticIntegrationPolicyEngine,
2954
3404
  _resetPendingFlowsForTests,
2955
3405
  assertValidConnectorManifest,
3406
+ buildApprovalRequest,
3407
+ buildIntegrationInvocationEnvelope,
3408
+ buildIntegrationToolCatalog,
2956
3409
  consumePendingFlow,
3410
+ createConnectorAdapterProvider,
3411
+ createDefaultIntegrationPolicyEngine,
2957
3412
  createHttpIntegrationProvider,
2958
3413
  createMockIntegrationProvider,
2959
3414
  exchangeAuthorizationCode,
@@ -2961,17 +3416,27 @@ export {
2961
3416
  googleCalendar,
2962
3417
  googleSheets,
2963
3418
  hubspot,
3419
+ integrationToolName,
3420
+ invocationRequestFromEnvelope,
3421
+ manifestToConnector,
2964
3422
  microsoftCalendar,
3423
+ normalizeIntegrationResult,
2965
3424
  notionDatabase,
3425
+ parseIntegrationToolName,
2966
3426
  parseStripeSignatureHeader,
3427
+ redactApprovalRequest,
3428
+ redactCapability,
3429
+ redactInvocationEnvelope,
2967
3430
  refreshAccessToken,
2968
3431
  sanitizeConnection,
3432
+ searchIntegrationTools,
2969
3433
  signCapability,
2970
3434
  slack,
2971
3435
  slackEventsConnector,
2972
3436
  startOAuthFlow,
2973
3437
  stripePackConnector,
2974
3438
  stripeWebhookReceiverConnector,
3439
+ toMcpTools,
2975
3440
  twilioSmsConnector,
2976
3441
  validateConnectorManifest,
2977
3442
  verifyCapabilityToken,