@nimbus-dev/connectors 0.7.0 → 0.9.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/connectors/apple/src/adapters.ts +56 -1
- package/connectors/apple/src/caldav-core.ts +7 -0
- package/connectors/apple/src/calendar-tools.ts +34 -0
- package/connectors/apple/src/tools.ts +2 -1
- package/connectors/aws/src/tools.ts +110 -0
- package/connectors/azure/src/tools.ts +70 -0
- package/connectors/circleci/src/tools.ts +63 -1
- package/connectors/gcp/src/tools.ts +74 -16
- package/connectors/github/src/tools.ts +298 -6
- package/connectors/gitlab/src/gitlab-files.ts +213 -0
- package/connectors/gitlab/src/tools.ts +226 -0
- package/connectors/iac/nimbus.extension.json +1 -1
- package/connectors/iac/src/tools.ts +808 -105
- package/connectors/kubernetes/nimbus.extension.json +1 -1
- package/connectors/kubernetes/src/manifest-scan.ts +602 -0
- package/connectors/kubernetes/src/tools.ts +366 -17
- package/connectors/snowflake/src/server.ts +69 -2
- package/package.json +1 -1
- package/shared/audit-chain.ts +5 -2
- package/shared/commit-files.ts +138 -0
- package/shared/consent-kit.ts +156 -31
- package/shared/github-files.ts +195 -0
- package/shared/github-rest.ts +12 -0
- package/shared/k8s-deployment-view.ts +68 -0
- package/shared/prepare-tool-ids.ts +30 -0
- package/shared/private-kubeconfig.ts +111 -0
- package/shared/private-scratch.ts +68 -0
- package/shared/run-cli-json.ts +17 -1
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
assertCombinedSize,
|
|
4
|
+
assertCommitFiles,
|
|
5
|
+
COMMIT_MAX_COMBINED_BYTES,
|
|
6
|
+
COMMIT_MAX_FILES,
|
|
7
|
+
commitFilePathSchema,
|
|
8
|
+
commitFilesSchema,
|
|
9
|
+
fullShaSchema,
|
|
10
|
+
notOverwritable,
|
|
11
|
+
} from "../../../shared/commit-files.ts";
|
|
2
12
|
import {
|
|
3
13
|
type ConsentServer,
|
|
14
|
+
createPrepareToolRegistrar,
|
|
4
15
|
createWriteToolRegistrar,
|
|
5
16
|
type WriteToolConfig,
|
|
6
17
|
} from "../../../shared/consent-kit.ts";
|
|
@@ -21,6 +32,14 @@ import {
|
|
|
21
32
|
type RestFetchResult,
|
|
22
33
|
toRestFetchResult,
|
|
23
34
|
} from "../../../shared/rest-tool-kit.ts";
|
|
35
|
+
import {
|
|
36
|
+
assertRegularTargets,
|
|
37
|
+
branchHead,
|
|
38
|
+
type GitlabFileRead,
|
|
39
|
+
type GitlabGet,
|
|
40
|
+
readGitlabFilesAt,
|
|
41
|
+
refCommit,
|
|
42
|
+
} from "./gitlab-files.ts";
|
|
24
43
|
|
|
25
44
|
/**
|
|
26
45
|
* A project reference that reaches a URL path: a numeric id, or `/`-separated segments of
|
|
@@ -163,6 +182,9 @@ export const GITLAB_TOOL_NAMES = [
|
|
|
163
182
|
"gitlab_job_log_tail",
|
|
164
183
|
"gitlab_pipeline_retry",
|
|
165
184
|
"gitlab_pipeline_cancel",
|
|
185
|
+
"gitlab_files_get",
|
|
186
|
+
"gitlab_commit_prepare",
|
|
187
|
+
"gitlab_commit_create",
|
|
166
188
|
] as const;
|
|
167
189
|
|
|
168
190
|
export function registerGitlabTools(
|
|
@@ -182,6 +204,16 @@ export function registerGitlabTools(
|
|
|
182
204
|
scopeKinds: ["repo"],
|
|
183
205
|
});
|
|
184
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Prepare tools (shared/prepare-tool-ids.ts) read the state a write is bound to, for the gateway to
|
|
209
|
+
* call BEFORE the write's approval prompt; they are neither reads nor writes to the kit.
|
|
210
|
+
*/
|
|
211
|
+
const registerPrepareTool = createPrepareToolRegistrar(server, {
|
|
212
|
+
connector: "gitlab",
|
|
213
|
+
scopeEnv: "NIMBUS_MCP_GITLAB_WRITE_SCOPE",
|
|
214
|
+
scopeKinds: ["repo"],
|
|
215
|
+
});
|
|
216
|
+
|
|
185
217
|
/**
|
|
186
218
|
* Standard GitLab tool, read or write: `token → glFetch(buildUrl[, buildInit]) →
|
|
187
219
|
* mcpJsonResultIfOk("GitLab", res)`. `buildUrl` returns the relative path (or absolute URL) and
|
|
@@ -707,4 +739,198 @@ export function registerGitlabTools(
|
|
|
707
739
|
"Cancel a pipeline.",
|
|
708
740
|
"cancel",
|
|
709
741
|
);
|
|
742
|
+
|
|
743
|
+
// -------------------------------------------------------------------------------------------
|
|
744
|
+
// Multi-file commit: gitlab_files_get (read), gitlab_commit_prepare (prepare), gitlab_commit_create
|
|
745
|
+
// -------------------------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
/** Bound GETs: the token is read at call time, before the first request. */
|
|
748
|
+
const gitlabGet = (): GitlabGet => {
|
|
749
|
+
const token = requireProcessEnv("GITLAB_PAT");
|
|
750
|
+
return (path) => glFetch(token, path);
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const READ_LIMIT = COMMIT_MAX_COMBINED_BYTES;
|
|
754
|
+
|
|
755
|
+
/** What a read tool reports for one file; `content` only for text. */
|
|
756
|
+
function fileView(f: GitlabFileRead): Record<string, unknown> {
|
|
757
|
+
if (!f.exists) return { path: f.path, exists: false };
|
|
758
|
+
const base = {
|
|
759
|
+
path: f.path,
|
|
760
|
+
exists: true,
|
|
761
|
+
lastCommitId: f.lastCommitId,
|
|
762
|
+
executable: f.executable,
|
|
763
|
+
size: f.size,
|
|
764
|
+
};
|
|
765
|
+
return f.kind === "text" ? { ...base, content: f.content } : { ...base, withheld: f.kind };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
reg(
|
|
769
|
+
"gitlab_files_get",
|
|
770
|
+
'Read up to 50 files at a branch, tag or commit: {ref, headSha, files:[{path, exists, lastCommitId, executable, size, content}]}. A binary or over-192-KiB file is reported with `withheld` instead of content, and the content of all the files together is capped at 192 KiB: the first file that does not fit, and every file after it, is reported with `withheld: "budget"` and no content.',
|
|
771
|
+
projectPathArg.extend({
|
|
772
|
+
ref: gitlabRefSchema,
|
|
773
|
+
paths: z.array(commitFilePathSchema).min(1).max(50),
|
|
774
|
+
}),
|
|
775
|
+
async (parsed) => {
|
|
776
|
+
const get = gitlabGet();
|
|
777
|
+
const project = encodeURIComponent(parsed.projectPath);
|
|
778
|
+
const sha = await refCommit(get, project, parsed.ref);
|
|
779
|
+
const files = await readGitlabFilesAt(
|
|
780
|
+
get,
|
|
781
|
+
project,
|
|
782
|
+
sha,
|
|
783
|
+
parsed.paths,
|
|
784
|
+
READ_LIMIT,
|
|
785
|
+
READ_LIMIT,
|
|
786
|
+
);
|
|
787
|
+
return jsonResult({ ref: parsed.ref, headSha: sha, files: files.map(fileView) });
|
|
788
|
+
},
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
const gitlabCommitInputSchema = projectPathArg.extend({
|
|
792
|
+
branch: gitlabRefSchema,
|
|
793
|
+
files: commitFilesSchema,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const repoScopeOf = (p: { projectPath: string }): { kind: string; value: string } => ({
|
|
797
|
+
kind: "repo",
|
|
798
|
+
value: p.projectPath,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
registerPrepareTool(
|
|
802
|
+
"gitlab_commit_prepare",
|
|
803
|
+
{ scopeTargetOf: repoScopeOf },
|
|
804
|
+
`Read the state a gitlab_commit_create is bound to: the branch head and, for each file, whether it exists, its last_commit_id, whether it is executable and its current content. Refuses (no truncation) a commit that touches more than ${String(COMMIT_MAX_FILES)} files or whose old and new content together exceed 192 KiB, and any existing file that is not a text file. Takes the commit's own projectPath, branch and files.`,
|
|
805
|
+
gitlabCommitInputSchema,
|
|
806
|
+
async (parsed) => {
|
|
807
|
+
assertCommitFiles(parsed.files);
|
|
808
|
+
const get = gitlabGet();
|
|
809
|
+
const project = encodeURIComponent(parsed.projectPath);
|
|
810
|
+
const head = await branchHead(get, project, parsed.branch);
|
|
811
|
+
const read = await readGitlabFilesAt(
|
|
812
|
+
get,
|
|
813
|
+
project,
|
|
814
|
+
head,
|
|
815
|
+
parsed.files.map((f) => f.path),
|
|
816
|
+
READ_LIMIT,
|
|
817
|
+
);
|
|
818
|
+
// The files API reports no mode, so a symlink looks like a text file: read the tree entries and
|
|
819
|
+
// refuse a symlink, a submodule or a directory before anything is written.
|
|
820
|
+
await assertRegularTargets(get, project, head, read);
|
|
821
|
+
let oldBytes = 0;
|
|
822
|
+
const files = read.map((f) => {
|
|
823
|
+
if (!f.exists) return { path: f.path, exists: false };
|
|
824
|
+
if (f.kind !== "text") throw notOverwritable(f.path);
|
|
825
|
+
oldBytes += f.size;
|
|
826
|
+
return {
|
|
827
|
+
path: f.path,
|
|
828
|
+
exists: true,
|
|
829
|
+
lastCommitId: f.lastCommitId,
|
|
830
|
+
executable: f.executable,
|
|
831
|
+
oldContent: f.content,
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
assertCombinedSize(oldBytes, parsed.files);
|
|
835
|
+
return jsonResult({ headSha: head, files });
|
|
836
|
+
},
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
registerWriteTool(
|
|
840
|
+
"gitlab_commit_create",
|
|
841
|
+
{
|
|
842
|
+
mutates: "gitlab.commit.create",
|
|
843
|
+
// A commit moves a branch; the inverse needs the head and each file's last commit, captured first.
|
|
844
|
+
recoverable: false,
|
|
845
|
+
capturePreState: async (parsed) => {
|
|
846
|
+
const get = gitlabGet();
|
|
847
|
+
const project = encodeURIComponent(parsed.projectPath);
|
|
848
|
+
const head = await branchHead(get, project, parsed.branch);
|
|
849
|
+
const read = await readGitlabFilesAt(
|
|
850
|
+
get,
|
|
851
|
+
project,
|
|
852
|
+
head,
|
|
853
|
+
parsed.files.map((f) => f.path),
|
|
854
|
+
READ_LIMIT,
|
|
855
|
+
);
|
|
856
|
+
return {
|
|
857
|
+
branch: parsed.branch,
|
|
858
|
+
headSha: head,
|
|
859
|
+
files: read.map((f) => ({
|
|
860
|
+
path: f.path,
|
|
861
|
+
lastCommitId: f.exists ? f.lastCommitId : null,
|
|
862
|
+
})),
|
|
863
|
+
};
|
|
864
|
+
},
|
|
865
|
+
scopeTargetOf: repoScopeOf,
|
|
866
|
+
},
|
|
867
|
+
"Commit one or more whole text files to a branch as ONE commit. The branch head is checked against expectedHeadSha (the head the owner approved) before the write, but GitLab has no head-conditional commit, so that check is NOT atomic with the write. What protects the files is last_commit_id: each update carries the file's last_commit_id read at the approved head, and GitLab refuses it if that file changed since. A commit to OTHER files that lands between the check and the write is not detected before the write; movedHead: true in the result reports it afterwards (the new commit's parent is not the approved head). Never forced.",
|
|
868
|
+
gitlabCommitInputSchema.extend({
|
|
869
|
+
message: z.string().min(1).max(5000),
|
|
870
|
+
expectedHeadSha: fullShaSchema,
|
|
871
|
+
}),
|
|
872
|
+
async (parsed) => {
|
|
873
|
+
assertCommitFiles(parsed.files);
|
|
874
|
+
const get = gitlabGet();
|
|
875
|
+
const token = requireProcessEnv("GITLAB_PAT");
|
|
876
|
+
const project = encodeURIComponent(parsed.projectPath);
|
|
877
|
+
const expected = parsed.expectedHeadSha.toLowerCase();
|
|
878
|
+
// 1. The head AGAIN: the approval was for this commit, not for whatever the branch became.
|
|
879
|
+
const head = await branchHead(get, project, parsed.branch);
|
|
880
|
+
if (head !== expected) {
|
|
881
|
+
throw new Error(`branch head is ${head}, not the approved ${expected}`);
|
|
882
|
+
}
|
|
883
|
+
// 2. Each existing file AT that head: its last_commit_id goes on the update action.
|
|
884
|
+
const read = await readGitlabFilesAt(
|
|
885
|
+
get,
|
|
886
|
+
project,
|
|
887
|
+
head,
|
|
888
|
+
parsed.files.map((f) => f.path),
|
|
889
|
+
READ_LIMIT,
|
|
890
|
+
);
|
|
891
|
+
// The files API reports no mode, so a symlink looks like a text file: read the tree entries and
|
|
892
|
+
// refuse a symlink, a submodule or a directory before anything is written.
|
|
893
|
+
await assertRegularTargets(get, project, head, read);
|
|
894
|
+
let oldBytes = 0;
|
|
895
|
+
const actions = parsed.files.map((f, i) => {
|
|
896
|
+
const existing = read[i];
|
|
897
|
+
if (existing === undefined || !existing.exists) {
|
|
898
|
+
return { action: "create", file_path: f.path, content: f.content, encoding: "text" };
|
|
899
|
+
}
|
|
900
|
+
if (existing.kind !== "text") throw notOverwritable(f.path);
|
|
901
|
+
oldBytes += existing.size;
|
|
902
|
+
return {
|
|
903
|
+
action: "update",
|
|
904
|
+
file_path: f.path,
|
|
905
|
+
content: f.content,
|
|
906
|
+
encoding: "text",
|
|
907
|
+
last_commit_id: existing.lastCommitId,
|
|
908
|
+
};
|
|
909
|
+
});
|
|
910
|
+
assertCombinedSize(oldBytes, parsed.files);
|
|
911
|
+
// 3. One commit, no force, no start_branch.
|
|
912
|
+
const res = await glFetch(
|
|
913
|
+
token,
|
|
914
|
+
`/projects/${project}/repository/commits`,
|
|
915
|
+
jsonBody("POST", { branch: parsed.branch, commit_message: parsed.message, actions }),
|
|
916
|
+
);
|
|
917
|
+
if (!res.ok) {
|
|
918
|
+
throw new Error(`GitLab ${String(res.status)}: ${res.text.slice(0, 300)}`);
|
|
919
|
+
}
|
|
920
|
+
const made = (res.json ?? {}) as { id?: unknown; parent_ids?: unknown; web_url?: unknown };
|
|
921
|
+
// The head can move between step 1 and this POST (the API has no head-conditional commit):
|
|
922
|
+
// say so when the new commit's parent is not the head that was approved.
|
|
923
|
+
const movedHead = !(
|
|
924
|
+
Array.isArray(made.parent_ids) && made.parent_ids.map(String).includes(expected)
|
|
925
|
+
);
|
|
926
|
+
return jsonResult({
|
|
927
|
+
ok: true,
|
|
928
|
+
commitId: made.id,
|
|
929
|
+
branch: parsed.branch,
|
|
930
|
+
parent: expected,
|
|
931
|
+
webUrl: made.web_url,
|
|
932
|
+
movedHead,
|
|
933
|
+
});
|
|
934
|
+
},
|
|
935
|
+
);
|
|
710
936
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "com.nimbus.iac",
|
|
3
3
|
"displayName": "Infrastructure-as-Code",
|
|
4
4
|
"version": "0.1.0",
|
|
5
|
-
"description": "Terraform /
|
|
5
|
+
"description": "Terraform / CloudFormation / Pulumi CLI helpers. Applies are plan-bound: a Terraform apply runs only a saved plan file whose SHA-256 the owner approved, a CloudFormation deploy executes only a reviewed change set made for the exact template, and Pulumi up is refused. The connector itself opens no sockets; CLI subprocesses' network access is sandbox-bounded. Workspace paths are tool-call scoped (workingDirectory argument), not baked into the manifest. Plans are saved under NIMBUS_IAC_PLAN_DIR. Mutations require Gateway HITL.",
|
|
6
6
|
"author": "Nimbus",
|
|
7
7
|
"entrypoint": "dist/server.js",
|
|
8
8
|
"runtime": "bun",
|