@arbium/enterprise-agent-connect 0.2.0 → 0.2.1-dev.7b4e802
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/README.md +10 -3
- package/dist/connectors/adestra/connector.json +336 -0
- package/dist/connectors/adobe-admin-console/connector.json +212 -0
- package/dist/connectors/argo-cd/connector.json +210 -0
- package/dist/connectors/automox/connector.json +376 -0
- package/dist/connectors/churnzero/connector.json +238 -0
- package/dist/connectors/confluence/connector.json +168 -0
- package/dist/connectors/cvent/connector.json +287 -0
- package/dist/connectors/datadog/connector.json +188 -0
- package/dist/connectors/drata/connector.json +447 -0
- package/dist/connectors/druva/connector.json +258 -0
- package/dist/connectors/forsta-decipher/connector.json +164 -0
- package/dist/connectors/gitlab/connector.json +263 -0
- package/dist/connectors/grafana/connector.json +162 -0
- package/dist/connectors/http/index.d.ts +5 -1
- package/dist/connectors/http/index.js +62 -8
- package/dist/connectors/http/index.js.map +1 -1
- package/dist/connectors/http/openapi.d.ts +62 -0
- package/dist/connectors/http/openapi.js +342 -0
- package/dist/connectors/http/openapi.js.map +1 -0
- package/dist/connectors/intercom/connector.json +249 -0
- package/dist/connectors/lusha/connector.json +59 -0
- package/dist/connectors/lusha/index.d.ts +8 -0
- package/dist/connectors/lusha/index.js +7 -0
- package/dist/connectors/lusha/index.js.map +1 -0
- package/dist/connectors/lusha/tools.d.ts +2 -0
- package/dist/connectors/lusha/tools.js +113 -0
- package/dist/connectors/lusha/tools.js.map +1 -0
- package/dist/connectors/lusha/vendor.d.ts +4 -0
- package/dist/connectors/lusha/vendor.js +72 -0
- package/dist/connectors/lusha/vendor.js.map +1 -0
- package/dist/connectors/odata/index.js +12 -5
- package/dist/connectors/odata/index.js.map +1 -1
- package/dist/connectors/pagerduty/connector.json +247 -0
- package/dist/connectors/qualys/connector.json +271 -0
- package/dist/connectors/safebase/connector.json +221 -0
- package/dist/connectors/soap/index.js +4 -3
- package/dist/connectors/soap/index.js.map +1 -1
- package/dist/connectors/sonarqube/connector.json +165 -0
- package/dist/connectors/sprout-social/connector.json +158 -0
- package/dist/connectors/synthesia/connector.json +113 -0
- package/dist/connectors/wordpress/connector.json +203 -0
- package/dist/connectors/zephyr/connector.json +192 -0
- package/dist/packages/connector-kit/src/connector.d.ts +45 -7
- package/dist/packages/connector-kit/src/connector.js +113 -10
- package/dist/packages/connector-kit/src/connector.js.map +1 -1
- package/dist/packages/connector-kit/src/oauth.d.ts +2 -0
- package/dist/packages/connector-kit/src/oauth.js +12 -4
- package/dist/packages/connector-kit/src/oauth.js.map +1 -1
- package/dist/packages/server/migrations/0008_internal_apps.sql +16 -0
- package/dist/packages/server/migrations/rollback/to-legacy.sql +2 -0
- package/dist/packages/server/src/api.d.ts +2490 -277
- package/dist/packages/server/src/api.js +82 -1
- package/dist/packages/server/src/api.js.map +1 -1
- package/dist/packages/server/src/app.js +26 -1
- package/dist/packages/server/src/app.js.map +1 -1
- package/dist/packages/server/src/builtin.d.ts +3 -0
- package/dist/packages/server/src/builtin.js +32 -0
- package/dist/packages/server/src/builtin.js.map +1 -1
- package/dist/packages/server/src/index.js +27 -18
- package/dist/packages/server/src/index.js.map +1 -1
- package/dist/packages/server/src/service.d.ts +1113 -0
- package/dist/packages/server/src/service.js +257 -19
- package/dist/packages/server/src/service.js.map +1 -1
- package/dist/packages/server/src/store.d.ts +41 -1
- package/dist/packages/server/src/store.js +79 -0
- package/dist/packages/server/src/store.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "gitlab",
|
|
3
|
+
"name": "GitLab",
|
|
4
|
+
"description": "Let agents read merge requests, pipelines, job logs and commits in the GitLab projects you choose, and comment on merge requests. For review prep, failed pipeline triage, release checks and status reports.",
|
|
5
|
+
"logo": "/brand/registry/gitlab.svg",
|
|
6
|
+
"identity": "A GitLab project or group access token. It acts as a bot member of that project or group, with the role you give it, and reaches nothing outside it.",
|
|
7
|
+
"accountKey": "host",
|
|
8
|
+
"connectionKind": "guided",
|
|
9
|
+
"setup": [
|
|
10
|
+
{
|
|
11
|
+
"title": "Create an access token",
|
|
12
|
+
"body": "Create the token on the group that holds the projects agents should see, or on one project. Give it the Reporter role and the read_api scope. If agents may comment on merge requests, keep the Reporter role and choose the api scope instead: Reporter can comment but cannot push code, merge or run pipelines. GitLab shows the token only once.",
|
|
13
|
+
"link": {
|
|
14
|
+
"label": "GitLab: project access tokens",
|
|
15
|
+
"href": "https://docs.gitlab.com/user/project/settings/project_access_tokens/"
|
|
16
|
+
},
|
|
17
|
+
"click": [
|
|
18
|
+
"In GitLab, open the group (or the project), then select Settings > Access tokens > Add new token.",
|
|
19
|
+
"Token name: AI agents. Set an expiration date.",
|
|
20
|
+
"Role: Reporter. Scopes: read_api (api if agents may comment).",
|
|
21
|
+
"Select Create group access token (or Create project access token) and copy the token."
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"title": "Find your GitLab address",
|
|
26
|
+
"body": "Enter the host name of your GitLab, such as gitlab.com or gitlab.example.com, without https://. You can test the token with the command below: it shows the token's bot user and changes nothing.",
|
|
27
|
+
"click": ["Copy the host name from your browser's address bar while you are in GitLab."],
|
|
28
|
+
"copy": [
|
|
29
|
+
"GITLAB_HOST=gitlab.example.com",
|
|
30
|
+
"curl -s --header \"PRIVATE-TOKEN: $GITLAB_TOKEN\" \"https://$GITLAB_HOST/api/v4/user\""
|
|
31
|
+
],
|
|
32
|
+
"shell": "bash"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"afterConnect": [],
|
|
36
|
+
"fields": [
|
|
37
|
+
{
|
|
38
|
+
"id": "host",
|
|
39
|
+
"label": "GitLab address",
|
|
40
|
+
"help": "The host name only, such as gitlab.com or gitlab.example.com.",
|
|
41
|
+
"placeholder": "gitlab.example.com",
|
|
42
|
+
"secret": false
|
|
43
|
+
},
|
|
44
|
+
{ "id": "token", "label": "Access token", "secret": true }
|
|
45
|
+
],
|
|
46
|
+
"capabilities": [
|
|
47
|
+
{
|
|
48
|
+
"id": "read",
|
|
49
|
+
"label": "Read merge requests and pipelines",
|
|
50
|
+
"description": "List and read merge requests with their changes and comments, list and read pipelines and their jobs, read a job's log, and list commits.",
|
|
51
|
+
"required": true,
|
|
52
|
+
"note": "Reaches every project the token can see unless you keep a key to chosen projects. Read-only.",
|
|
53
|
+
"scopes": ["read_api"],
|
|
54
|
+
"permissions": [],
|
|
55
|
+
"tools": [
|
|
56
|
+
"gitlab_list_merge_requests",
|
|
57
|
+
"gitlab_get_merge_request",
|
|
58
|
+
"gitlab_list_merge_request_changes",
|
|
59
|
+
"gitlab_list_merge_request_comments",
|
|
60
|
+
"gitlab_list_pipelines",
|
|
61
|
+
"gitlab_get_pipeline",
|
|
62
|
+
"gitlab_list_pipeline_jobs",
|
|
63
|
+
"gitlab_get_job_log",
|
|
64
|
+
"gitlab_list_commits"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "comment",
|
|
69
|
+
"label": "Comment on merge requests",
|
|
70
|
+
"description": "Add a comment to a merge request, such as a review brief. It can be marked internal, so only project members see it.",
|
|
71
|
+
"note": "Needs a token with the api scope. Keep its role at Reporter.",
|
|
72
|
+
"scopes": ["api"],
|
|
73
|
+
"permissions": [],
|
|
74
|
+
"tools": ["gitlab_add_merge_request_comment"]
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"limits": [
|
|
78
|
+
{
|
|
79
|
+
"id": "projects",
|
|
80
|
+
"label": "Projects",
|
|
81
|
+
"help": "Optional: only these projects, by their full path as in the project's address, such as platform/payments-api. Agents must then name projects by that path. Leave empty for every project the token can see.",
|
|
82
|
+
"kind": "list",
|
|
83
|
+
"required": false,
|
|
84
|
+
"pattern": "^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)+$"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"verify": { "method": "GET", "path": "/user", "label": "GitLab token user", "show": "username" },
|
|
88
|
+
"revoke": "In GitLab, open the group or project, go to Settings > Access tokens and revoke the token. Agents lose access to GitLab at once.",
|
|
89
|
+
"http": {
|
|
90
|
+
"baseUrl": "https://{host}/api/v4",
|
|
91
|
+
"auth": { "type": "header", "header": "PRIVATE-TOKEN", "value": "token" },
|
|
92
|
+
"tools": [
|
|
93
|
+
{
|
|
94
|
+
"name": "gitlab_list_merge_requests",
|
|
95
|
+
"description": "List a project's merge requests, newest first. Pass next from the answer as page for the next page.",
|
|
96
|
+
"method": "GET",
|
|
97
|
+
"path": "/projects/{project}/merge_requests",
|
|
98
|
+
"params": {
|
|
99
|
+
"project": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "The project's full path, such as platform/payments-api, or its numeric id",
|
|
102
|
+
"in": "path",
|
|
103
|
+
"limit": "projects"
|
|
104
|
+
},
|
|
105
|
+
"state": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"enum": ["opened", "closed", "locked", "merged", "all"],
|
|
108
|
+
"description": "Only merge requests in this state",
|
|
109
|
+
"in": "query",
|
|
110
|
+
"default": "opened"
|
|
111
|
+
},
|
|
112
|
+
"target_branch": { "type": "string", "description": "Only those merging into this branch", "in": "query", "required": false },
|
|
113
|
+
"source_branch": { "type": "string", "description": "Only those from this branch", "in": "query", "required": false },
|
|
114
|
+
"labels": { "type": "string", "description": "Comma-separated labels, all of which must be on the merge request", "in": "query", "required": false },
|
|
115
|
+
"author_username": { "type": "string", "description": "Only those opened by this username", "in": "query", "required": false },
|
|
116
|
+
"reviewer_username": { "type": "string", "description": "Only those with this reviewer", "in": "query", "required": false },
|
|
117
|
+
"search": { "type": "string", "description": "Words in the title or description", "in": "query", "required": false },
|
|
118
|
+
"updated_after": { "type": "string", "description": "Only those updated after this time, ISO 8601, such as 2026-10-01T00:00:00Z", "in": "query", "required": false },
|
|
119
|
+
"per_page": { "type": "integer", "description": "How many per page, at most 100", "in": "query", "default": 20 },
|
|
120
|
+
"page": { "type": "integer", "description": "The next value of the previous answer", "in": "query", "required": false }
|
|
121
|
+
},
|
|
122
|
+
"paging": { "param": "page", "header": "X-Next-Page" },
|
|
123
|
+
"result": {
|
|
124
|
+
"fields": ["iid", "title", "state", "draft", "author.username", "reviewers.username", "source_branch", "target_branch", "labels", "detailed_merge_status", "has_conflicts", "updated_at", "web_url"]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "gitlab_get_merge_request",
|
|
129
|
+
"description": "Read one merge request: description, branches, approvals state, merge status and its latest pipeline.",
|
|
130
|
+
"method": "GET",
|
|
131
|
+
"path": "/projects/{project}/merge_requests/{iid}",
|
|
132
|
+
"params": {
|
|
133
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
134
|
+
"iid": { "type": "integer", "description": "The merge request number within the project (its iid)", "in": "path" }
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "gitlab_list_merge_request_changes",
|
|
139
|
+
"description": "List the files a merge request changes, with each file's diff. Pass next from the answer as page for the next page.",
|
|
140
|
+
"method": "GET",
|
|
141
|
+
"path": "/projects/{project}/merge_requests/{iid}/diffs",
|
|
142
|
+
"params": {
|
|
143
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
144
|
+
"iid": { "type": "integer", "description": "The merge request number within the project (its iid)", "in": "path" },
|
|
145
|
+
"per_page": { "type": "integer", "description": "How many files per page, at most 100", "in": "query", "default": 20 },
|
|
146
|
+
"page": { "type": "integer", "description": "The next value of the previous answer", "in": "query", "required": false }
|
|
147
|
+
},
|
|
148
|
+
"paging": { "param": "page", "header": "X-Next-Page" }
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "gitlab_list_merge_request_comments",
|
|
152
|
+
"description": "List the comments on a merge request, newest first, including system notes such as approvals. Pass next from the answer as page for the next page.",
|
|
153
|
+
"method": "GET",
|
|
154
|
+
"path": "/projects/{project}/merge_requests/{iid}/notes",
|
|
155
|
+
"params": {
|
|
156
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
157
|
+
"iid": { "type": "integer", "description": "The merge request number within the project (its iid)", "in": "path" },
|
|
158
|
+
"per_page": { "type": "integer", "description": "How many per page, at most 100", "in": "query", "default": 20 },
|
|
159
|
+
"page": { "type": "integer", "description": "The next value of the previous answer", "in": "query", "required": false }
|
|
160
|
+
},
|
|
161
|
+
"paging": { "param": "page", "header": "X-Next-Page" },
|
|
162
|
+
"result": { "fields": ["id", "author.username", "body", "system", "internal", "created_at"] }
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "gitlab_list_pipelines",
|
|
166
|
+
"description": "List a project's pipelines, newest first. Pass next from the answer as page for the next page.",
|
|
167
|
+
"method": "GET",
|
|
168
|
+
"path": "/projects/{project}/pipelines",
|
|
169
|
+
"params": {
|
|
170
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
171
|
+
"status": {
|
|
172
|
+
"type": "string",
|
|
173
|
+
"enum": ["created", "waiting_for_resource", "preparing", "pending", "running", "success", "failed", "canceled", "skipped", "manual", "scheduled"],
|
|
174
|
+
"description": "Only pipelines in this status",
|
|
175
|
+
"in": "query",
|
|
176
|
+
"required": false
|
|
177
|
+
},
|
|
178
|
+
"ref": { "type": "string", "description": "Only pipelines for this branch or tag", "in": "query", "required": false },
|
|
179
|
+
"sha": { "type": "string", "description": "Only pipelines for this commit", "in": "query", "required": false },
|
|
180
|
+
"updated_after": { "type": "string", "description": "Only those updated after this time, ISO 8601", "in": "query", "required": false },
|
|
181
|
+
"per_page": { "type": "integer", "description": "How many per page, at most 100", "in": "query", "default": 20 },
|
|
182
|
+
"page": { "type": "integer", "description": "The next value of the previous answer", "in": "query", "required": false }
|
|
183
|
+
},
|
|
184
|
+
"paging": { "param": "page", "header": "X-Next-Page" },
|
|
185
|
+
"result": { "fields": ["id", "status", "ref", "sha", "source", "created_at", "updated_at", "web_url"] }
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "gitlab_get_pipeline",
|
|
189
|
+
"description": "Read one pipeline: status, branch, commit, who started it, timing and the reason it failed, if GitLab gives one.",
|
|
190
|
+
"method": "GET",
|
|
191
|
+
"path": "/projects/{project}/pipelines/{pipeline_id}",
|
|
192
|
+
"params": {
|
|
193
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
194
|
+
"pipeline_id": { "type": "integer", "description": "The pipeline id", "in": "path" }
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "gitlab_list_pipeline_jobs",
|
|
199
|
+
"description": "List a pipeline's jobs with their stage, status and failure reason. Pass next from the answer as page for the next page.",
|
|
200
|
+
"method": "GET",
|
|
201
|
+
"path": "/projects/{project}/pipelines/{pipeline_id}/jobs",
|
|
202
|
+
"params": {
|
|
203
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
204
|
+
"pipeline_id": { "type": "integer", "description": "The pipeline id", "in": "path" },
|
|
205
|
+
"scope": {
|
|
206
|
+
"type": "array",
|
|
207
|
+
"items": "string",
|
|
208
|
+
"enum": ["created", "pending", "running", "failed", "success", "canceled", "skipped", "waiting_for_resource", "manual"],
|
|
209
|
+
"description": "Only jobs in these statuses",
|
|
210
|
+
"in": "query",
|
|
211
|
+
"name": "scope[]",
|
|
212
|
+
"required": false
|
|
213
|
+
},
|
|
214
|
+
"include_retried": { "type": "boolean", "description": "Include jobs that were retried", "in": "query", "required": false },
|
|
215
|
+
"per_page": { "type": "integer", "description": "How many per page, at most 100", "in": "query", "default": 50 },
|
|
216
|
+
"page": { "type": "integer", "description": "The next value of the previous answer", "in": "query", "required": false }
|
|
217
|
+
},
|
|
218
|
+
"paging": { "param": "page", "header": "X-Next-Page" },
|
|
219
|
+
"result": { "fields": ["id", "name", "stage", "status", "failure_reason", "allow_failure", "duration", "started_at", "finished_at", "web_url"] }
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"name": "gitlab_get_job_log",
|
|
223
|
+
"description": "Read a job's log as text, to see why it failed. Logs larger than 1 MB cannot be read.",
|
|
224
|
+
"method": "GET",
|
|
225
|
+
"path": "/projects/{project}/jobs/{job_id}/trace",
|
|
226
|
+
"params": {
|
|
227
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
228
|
+
"job_id": { "type": "integer", "description": "The job id", "in": "path" }
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"name": "gitlab_list_commits",
|
|
233
|
+
"description": "List commits on a branch, newest first, optionally only within dates or by one author. Pass next from the answer as page for the next page.",
|
|
234
|
+
"method": "GET",
|
|
235
|
+
"path": "/projects/{project}/repository/commits",
|
|
236
|
+
"params": {
|
|
237
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
238
|
+
"ref_name": { "type": "string", "description": "The branch or tag; the default branch when left out", "in": "query", "required": false },
|
|
239
|
+
"since": { "type": "string", "description": "Only commits after this time, ISO 8601", "in": "query", "required": false },
|
|
240
|
+
"until": { "type": "string", "description": "Only commits before this time, ISO 8601", "in": "query", "required": false },
|
|
241
|
+
"author": { "type": "string", "description": "Only commits by this author name or email", "in": "query", "required": false },
|
|
242
|
+
"per_page": { "type": "integer", "description": "How many per page, at most 100", "in": "query", "default": 20 },
|
|
243
|
+
"page": { "type": "integer", "description": "The next value of the previous answer", "in": "query", "required": false }
|
|
244
|
+
},
|
|
245
|
+
"paging": { "param": "page", "header": "X-Next-Page" },
|
|
246
|
+
"result": { "fields": ["short_id", "title", "author_name", "author_email", "committed_date", "web_url"] }
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"name": "gitlab_add_merge_request_comment",
|
|
250
|
+
"description": "Add a comment to a merge request. Markdown is allowed.",
|
|
251
|
+
"method": "POST",
|
|
252
|
+
"path": "/projects/{project}/merge_requests/{iid}/notes",
|
|
253
|
+
"params": {
|
|
254
|
+
"project": { "type": "string", "description": "The project's full path or numeric id", "in": "path", "limit": "projects" },
|
|
255
|
+
"iid": { "type": "integer", "description": "The merge request number within the project (its iid)", "in": "path" },
|
|
256
|
+
"body": { "type": "string", "description": "The comment, in Markdown", "in": "body" },
|
|
257
|
+
"internal": { "type": "boolean", "description": "Only project members can see it", "in": "body", "required": false }
|
|
258
|
+
},
|
|
259
|
+
"result": { "fields": ["id", "body", "internal", "created_at"] }
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "grafana",
|
|
3
|
+
"name": "Grafana",
|
|
4
|
+
"description": "Let agents find and read Grafana dashboards and alert rules, and read alert state changes and annotations around an incident. For first response to production alerts.",
|
|
5
|
+
"logo": "/brand/registry/grafana.svg",
|
|
6
|
+
"identity": "A Grafana service account token. The service account has the Viewer role, so it reads what a viewer in your organization may read.",
|
|
7
|
+
"accountKey": "host",
|
|
8
|
+
"connectionKind": "guided",
|
|
9
|
+
"setup": [
|
|
10
|
+
{
|
|
11
|
+
"title": "Create a service account with the Viewer role",
|
|
12
|
+
"body": "You need a Grafana organization administrator for this. Viewer lets agents read dashboards, alert rules and annotations and change nothing.",
|
|
13
|
+
"link": {
|
|
14
|
+
"label": "Grafana: service accounts",
|
|
15
|
+
"href": "https://grafana.com/docs/grafana/latest/administration/service-accounts/"
|
|
16
|
+
},
|
|
17
|
+
"click": [
|
|
18
|
+
"In Grafana, select Administration > Users and access > Service accounts > Add service account.",
|
|
19
|
+
"Display name: AI agents. Select Create.",
|
|
20
|
+
"On the AI agents service account, set its role to Viewer with the role picker."
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"title": "Add a token",
|
|
25
|
+
"body": "Grafana shows the token only once. You can test it with the command below: it shows your organization's name and changes nothing.",
|
|
26
|
+
"click": [
|
|
27
|
+
"On the AI agents service account, select Add service account token.",
|
|
28
|
+
"Name: AI agents. Set an expiration date, then select Generate token and copy it."
|
|
29
|
+
],
|
|
30
|
+
"copy": [
|
|
31
|
+
"GRAFANA_HOST=grafana.example.com",
|
|
32
|
+
"curl -s -H \"Authorization: Bearer $GRAFANA_TOKEN\" \"https://$GRAFANA_HOST/api/org/\""
|
|
33
|
+
],
|
|
34
|
+
"shell": "bash"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"afterConnect": [],
|
|
38
|
+
"fields": [
|
|
39
|
+
{
|
|
40
|
+
"id": "host",
|
|
41
|
+
"label": "Grafana address",
|
|
42
|
+
"help": "The host name of your Grafana, such as grafana.example.com or yourstack.grafana.net, without https://.",
|
|
43
|
+
"placeholder": "grafana.example.com",
|
|
44
|
+
"secret": false
|
|
45
|
+
},
|
|
46
|
+
{ "id": "token", "label": "Service account token", "secret": true }
|
|
47
|
+
],
|
|
48
|
+
"capabilities": [
|
|
49
|
+
{
|
|
50
|
+
"id": "read",
|
|
51
|
+
"label": "Read dashboards and alert rules",
|
|
52
|
+
"description": "Search dashboards and folders, read a dashboard's panels and queries, and list and read alert rules.",
|
|
53
|
+
"required": true,
|
|
54
|
+
"note": "Read-only. Reaches every dashboard and alert rule a viewer can see unless you keep a key to chosen folders. Reading alert rules with the Viewer role needs Grafana 11 or later.",
|
|
55
|
+
"scopes": [],
|
|
56
|
+
"permissions": ["Viewer"],
|
|
57
|
+
"tools": ["grafana_search", "grafana_get_dashboard", "grafana_list_alert_rules", "grafana_get_alert_rule"]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "read_annotations",
|
|
61
|
+
"label": "Read annotations",
|
|
62
|
+
"description": "Read annotations in a time window, such as alert state changes, deployments and notes people added to graphs.",
|
|
63
|
+
"note": "Annotations are read across the organization, whatever folders a key is kept to.",
|
|
64
|
+
"scopes": [],
|
|
65
|
+
"permissions": ["Viewer"],
|
|
66
|
+
"tools": ["grafana_list_annotations"]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"limits": [
|
|
70
|
+
{
|
|
71
|
+
"id": "folders",
|
|
72
|
+
"label": "Folders",
|
|
73
|
+
"help": "Optional: only dashboards and alert rules in these folders, by folder UID (in the folder's address after /dashboards/f/). Leave empty for every folder.",
|
|
74
|
+
"kind": "list",
|
|
75
|
+
"required": false,
|
|
76
|
+
"pattern": "^[A-Za-z0-9_-]{1,40}$"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"verify": { "method": "GET", "path": "/api/org/", "label": "Grafana organization", "show": "name" },
|
|
80
|
+
"revoke": "In Grafana, go to Administration > Users and access > Service accounts, select AI agents and delete its token, or the service account. Agents lose access to Grafana at once.",
|
|
81
|
+
"http": {
|
|
82
|
+
"baseUrl": "https://{host}",
|
|
83
|
+
"auth": { "type": "bearer", "token": "token" },
|
|
84
|
+
"tools": [
|
|
85
|
+
{
|
|
86
|
+
"name": "grafana_search",
|
|
87
|
+
"description": "Search dashboards or folders by title, tag or folder. Pass page to read further pages, from 1.",
|
|
88
|
+
"method": "GET",
|
|
89
|
+
"path": "/api/search",
|
|
90
|
+
"params": {
|
|
91
|
+
"query": { "type": "string", "description": "Words in the title", "in": "query", "required": false },
|
|
92
|
+
"tag": { "type": "array", "items": "string", "description": "Only those with all these tags", "in": "query", "required": false },
|
|
93
|
+
"type": { "type": "string", "enum": ["dash-db", "dash-folder"], "description": "Dashboards or folders", "in": "query", "default": "dash-db" },
|
|
94
|
+
"folder_uids": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": "string",
|
|
97
|
+
"description": "Only in these folders, by UID",
|
|
98
|
+
"in": "query",
|
|
99
|
+
"name": "folderUIDs",
|
|
100
|
+
"required": false,
|
|
101
|
+
"limit": "folders"
|
|
102
|
+
},
|
|
103
|
+
"limit": { "type": "integer", "description": "Results per page", "in": "query", "default": 50 },
|
|
104
|
+
"page": { "type": "integer", "description": "The page, from 1", "in": "query", "default": 1 }
|
|
105
|
+
},
|
|
106
|
+
"result": { "fields": ["uid", "title", "type", "tags", "folderUid", "folderTitle", "url"] }
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "grafana_get_dashboard",
|
|
110
|
+
"description": "Read a dashboard by UID: its panels with their titles, queries and data sources, its variables and its folder.",
|
|
111
|
+
"method": "GET",
|
|
112
|
+
"path": "/api/dashboards/uid/{uid}",
|
|
113
|
+
"params": { "uid": { "type": "string", "description": "The dashboard UID", "in": "path" } },
|
|
114
|
+
"answerLimits": [{ "field": "meta.folderUid", "limit": "folders" }],
|
|
115
|
+
"result": {
|
|
116
|
+
"fields": [
|
|
117
|
+
"dashboard.uid",
|
|
118
|
+
"dashboard.title",
|
|
119
|
+
"dashboard.tags",
|
|
120
|
+
"dashboard.panels",
|
|
121
|
+
"dashboard.templating",
|
|
122
|
+
"meta.folderUid",
|
|
123
|
+
"meta.folderTitle",
|
|
124
|
+
"meta.url",
|
|
125
|
+
"meta.updated"
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "grafana_list_alert_rules",
|
|
131
|
+
"description": "List every alert rule with its folder, group, condition and labels. Under a key kept to some folders, this answers only when every rule is in them; read one rule instead.",
|
|
132
|
+
"method": "GET",
|
|
133
|
+
"path": "/api/v1/provisioning/alert-rules",
|
|
134
|
+
"answerLimits": [{ "field": "folderUID", "limit": "folders" }],
|
|
135
|
+
"result": { "fields": ["uid", "title", "folderUID", "ruleGroup", "condition", "for", "labels", "annotations", "isPaused"] }
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "grafana_get_alert_rule",
|
|
139
|
+
"description": "Read one alert rule by UID: its queries, condition, labels, annotations such as the runbook, and how long it must hold before firing.",
|
|
140
|
+
"method": "GET",
|
|
141
|
+
"path": "/api/v1/provisioning/alert-rules/{uid}",
|
|
142
|
+
"params": { "uid": { "type": "string", "description": "The alert rule UID", "in": "path" } },
|
|
143
|
+
"answerLimits": [{ "field": "folderUID", "limit": "folders" }]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "grafana_list_annotations",
|
|
147
|
+
"description": "List annotations in a time window, newest first: alert state changes (type alert) or notes people added (type annotation), optionally on one dashboard or with tags.",
|
|
148
|
+
"method": "GET",
|
|
149
|
+
"path": "/api/annotations",
|
|
150
|
+
"params": {
|
|
151
|
+
"from": { "type": "integer", "description": "Start of the window, milliseconds since 1970", "in": "query", "required": false },
|
|
152
|
+
"to": { "type": "integer", "description": "End of the window, milliseconds since 1970", "in": "query", "required": false },
|
|
153
|
+
"type": { "type": "string", "enum": ["alert", "annotation"], "description": "Alert state changes or notes", "in": "query", "required": false },
|
|
154
|
+
"dashboard_uid": { "type": "string", "description": "Only on this dashboard", "in": "query", "name": "dashboardUID", "required": false },
|
|
155
|
+
"tags": { "type": "array", "items": "string", "description": "Only those with all these tags", "in": "query", "required": false },
|
|
156
|
+
"limit": { "type": "integer", "description": "At most this many", "in": "query", "default": 100 }
|
|
157
|
+
},
|
|
158
|
+
"result": { "fields": ["id", "time", "timeEnd", "text", "tags", "dashboardUID", "panelId", "alertName", "newState", "prevState"] }
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type Toolset } from "../../packages/connector-kit/src/common.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type Connector, type HttpApp, type HttpAuth } from "../../packages/connector-kit/src/connector.js";
|
|
4
4
|
import { type Fetch, type Limits, type Vendor, type VendorConnection } from "../../packages/connector-kit/src/types.js";
|
|
5
5
|
export declare const MAX_RESPONSE_BYTES: number;
|
|
6
6
|
export declare const MAX_BODY_BYTES: number;
|
|
@@ -11,6 +11,10 @@ export declare function formValues(connector: Connector, fields: Record<string,
|
|
|
11
11
|
config: Record<string, string>;
|
|
12
12
|
secret: Record<string, string>;
|
|
13
13
|
};
|
|
14
|
+
export declare function connectionAddress(connector: Connector, address: string, allowedHosts: string[] | undefined, tokenUrl: string | null, fetchImpl: Fetch, types?: RegExp, passed?: string): (c: Pick<VendorConnection, "config">) => {
|
|
15
|
+
url: import("node:url").URL;
|
|
16
|
+
safe: typeof fetch;
|
|
17
|
+
};
|
|
14
18
|
export declare function httpVendor(connector: Connector, fetchImpl?: Fetch): Vendor;
|
|
15
19
|
export declare function answerWithin(data: unknown, path: string, values: string[]): boolean;
|
|
16
20
|
export declare function paramLimits(limits: Limits, params: Record<string, {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
1
3
|
import { z } from "zod";
|
|
2
4
|
import { audited, Denied, json, listLimit, VendorFailed, } from "../../packages/connector-kit/src/common.js";
|
|
5
|
+
import { checkHost, fillAddress, } from "../../packages/connector-kit/src/connector.js";
|
|
3
6
|
import { clientCredentials, vendorError } from "../../packages/connector-kit/src/oauth.js";
|
|
4
7
|
import { ConnectError, } from "../../packages/connector-kit/src/types.js";
|
|
5
8
|
// The generic module behind every connector.json with an `http` block: an app described as
|
|
@@ -8,6 +11,20 @@ export const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
|
8
11
|
export const MAX_BODY_BYTES = 256 * 1024;
|
|
9
12
|
export const MAX_TARGET_BYTES = 8 * 1024;
|
|
10
13
|
const TIMEOUT_MS = 30_000;
|
|
14
|
+
// Cloud metadata services (169.254.169.254, fd00:ec2::254) hand out the server's own credentials,
|
|
15
|
+
// and no app is served from a link-local address, so an address that resolves there is refused.
|
|
16
|
+
// ponytail: checked at lookup, so a DNS answer that changes before the connect is not caught.
|
|
17
|
+
// IPv4-mapped IPv6 comes back from URL as hex: ::ffff:a9fe:a9fe is 169.254.169.254.
|
|
18
|
+
const LINK_LOCAL = /^(169\.254\.|::ffff:(169\.254\.|a9fe:)|fe[89ab][0-9a-f]:|fd00:ec2::254$)/i;
|
|
19
|
+
async function refuseLinkLocal(hostname) {
|
|
20
|
+
const host = hostname.replace(/^\[|\]$/g, "");
|
|
21
|
+
// A name that does not resolve is left to the call itself to fail on.
|
|
22
|
+
const addresses = isIP(host)
|
|
23
|
+
? [host]
|
|
24
|
+
: await lookup(host, { all: true }).then((all) => all.map((a) => a.address), () => []);
|
|
25
|
+
if (addresses.some((a) => LINK_LOCAL.test(a)))
|
|
26
|
+
throw new ConnectError(`${hostname} is a link-local address, which no app is reached at`);
|
|
27
|
+
}
|
|
11
28
|
// fetch that reaches only `allowed` origins, follows no redirect, gives up after TIMEOUT_MS and
|
|
12
29
|
// reads at most MAX_RESPONSE_BYTES of JSON or text (or what `types` matches). Its refusals are
|
|
13
30
|
// ConnectError.
|
|
@@ -18,6 +35,7 @@ passed = "JSON or text") {
|
|
|
18
35
|
const url = new URL(input instanceof Request ? input.url : input);
|
|
19
36
|
if (!allowed.includes(url.origin))
|
|
20
37
|
throw new ConnectError(`${url.origin} is not this app's address`);
|
|
38
|
+
await refuseLinkLocal(url.hostname);
|
|
21
39
|
const res = await fetchImpl(url, {
|
|
22
40
|
...init,
|
|
23
41
|
redirect: "manual",
|
|
@@ -82,13 +100,35 @@ export function formValues(connector, fields) {
|
|
|
82
100
|
}
|
|
83
101
|
return { config, secret };
|
|
84
102
|
}
|
|
103
|
+
// An app's address filled in for one connection (its {field}s), and the guarded fetch reaching
|
|
104
|
+
// only that origin (and the token service's), kept per origin so a token cache survives calls.
|
|
105
|
+
export function connectionAddress(connector, address, allowedHosts, tokenUrl, fetchImpl, types, passed) {
|
|
106
|
+
const label = (id) => connector.fields.find((f) => f.id === id)?.label ?? id;
|
|
107
|
+
const fetches = new Map();
|
|
108
|
+
return (c) => {
|
|
109
|
+
const url = new URL(fillAddress(address, c.config, label));
|
|
110
|
+
checkHost(url, allowedHosts);
|
|
111
|
+
let safe = fetches.get(url.origin);
|
|
112
|
+
if (!safe) {
|
|
113
|
+
const origins = [url.origin, ...(tokenUrl ? [new URL(tokenUrl).origin] : [])];
|
|
114
|
+
safe = guardedFetch(origins, fetchImpl, types, passed);
|
|
115
|
+
fetches.set(url.origin, safe);
|
|
116
|
+
}
|
|
117
|
+
return { url, safe };
|
|
118
|
+
};
|
|
119
|
+
}
|
|
85
120
|
export function httpVendor(connector, fetchImpl = fetch) {
|
|
86
121
|
const http = connector.http;
|
|
87
|
-
const base = new URL(http.baseUrl);
|
|
88
|
-
const prefix = base.pathname.replace(/\/+$/, "");
|
|
89
122
|
const auth = http.auth;
|
|
90
|
-
const
|
|
91
|
-
const
|
|
123
|
+
const addressOf = connectionAddress(connector, http.baseUrl, http.allowedHosts, auth.type === "oauth2" ? auth.tokenUrl : null, fetchImpl);
|
|
124
|
+
const clients = new Map();
|
|
125
|
+
const oauthFor = (origin, safe) => {
|
|
126
|
+
if (auth.type !== "oauth2")
|
|
127
|
+
return null;
|
|
128
|
+
if (!clients.has(origin))
|
|
129
|
+
clients.set(origin, clientCredentials(connector.name, safe));
|
|
130
|
+
return clients.get(origin);
|
|
131
|
+
};
|
|
92
132
|
// Lower-case, so a configured Accept replaces the default rather than joining it.
|
|
93
133
|
const fixed = Object.fromEntries(Object.entries(http.headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
94
134
|
const oauthCreds = (c) => auth.type === "oauth2"
|
|
@@ -97,9 +137,22 @@ export function httpVendor(connector, fetchImpl = fetch) {
|
|
|
97
137
|
clientId: c.config[auth.clientId],
|
|
98
138
|
clientSecret: c.secret[auth.clientSecret],
|
|
99
139
|
scope: auth.scope,
|
|
140
|
+
clientAuth: auth.clientAuth,
|
|
141
|
+
audience: auth.audience,
|
|
100
142
|
}
|
|
101
143
|
: null;
|
|
144
|
+
const fieldHeaders = (c) => Object.fromEntries(Object.entries(http.fieldHeaders).flatMap(([name, id]) => {
|
|
145
|
+
const value = c.secret[id] ?? c.config[id];
|
|
146
|
+
if (!value)
|
|
147
|
+
return [];
|
|
148
|
+
if (!/^[\x20-\x7E]*$/.test(value))
|
|
149
|
+
throw new ConnectError(`${name} can only hold plain letters, digits and symbols`);
|
|
150
|
+
return [[name.toLowerCase(), value]];
|
|
151
|
+
}));
|
|
102
152
|
const call = async (c, request) => {
|
|
153
|
+
const { url: base, safe } = addressOf(c);
|
|
154
|
+
const prefix = base.pathname.replace(/\/+$/, "");
|
|
155
|
+
const oauth = oauthFor(base.origin, safe);
|
|
103
156
|
// The path is the tool's, already encoded; it must stay under baseUrl's path.
|
|
104
157
|
const url = new URL(`${base.origin}${prefix}${request.path}`);
|
|
105
158
|
if (url.origin !== base.origin || !`${url.pathname}/`.startsWith(`${prefix}/`))
|
|
@@ -111,13 +164,14 @@ export function httpVendor(connector, fetchImpl = fetch) {
|
|
|
111
164
|
path: `${url.pathname}${url.search}`,
|
|
112
165
|
body: request.body,
|
|
113
166
|
};
|
|
167
|
+
const headers = { ...fixed, ...fieldHeaders(c) };
|
|
114
168
|
if (oauth)
|
|
115
|
-
return oauth.call(oauthCreds(c), base.origin, req,
|
|
169
|
+
return oauth.call(oauthCreds(c), base.origin, req, headers);
|
|
116
170
|
const res = await safe(`${base.origin}${req.path}`, {
|
|
117
171
|
method: req.method,
|
|
118
172
|
headers: {
|
|
119
173
|
accept: "application/json",
|
|
120
|
-
...
|
|
174
|
+
...headers,
|
|
121
175
|
...authHeaders(auth, c),
|
|
122
176
|
...(req.body === undefined ? {} : { "content-type": "application/json" }),
|
|
123
177
|
},
|
|
@@ -133,9 +187,9 @@ export function httpVendor(connector, fetchImpl = fetch) {
|
|
|
133
187
|
return {
|
|
134
188
|
async connect(fields) {
|
|
135
189
|
const { config, secret } = formValues(connector, fields);
|
|
190
|
+
const { url: base, safe } = addressOf({ config });
|
|
136
191
|
const conn = { config: { host: base.host, ...config }, secret };
|
|
137
|
-
|
|
138
|
-
await oauth.signIn(oauthCreds(conn));
|
|
192
|
+
await oauthFor(base.origin, safe)?.signIn(oauthCreds(conn));
|
|
139
193
|
return { ...conn, accountLabel: conn.config[connector.accountKey] ?? base.host };
|
|
140
194
|
},
|
|
141
195
|
call,
|