gitlab-mcp 1.0.0 → 1.2.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/LICENSE +21 -0
- package/dist/config/env.d.ts +56 -0
- package/dist/config/env.js +163 -0
- package/dist/config/env.js.map +1 -0
- package/dist/http-app.d.ts +45 -0
- package/dist/http-app.js +550 -0
- package/dist/http-app.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +65 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth-context.d.ts +9 -0
- package/dist/lib/auth-context.js +9 -0
- package/dist/lib/auth-context.js.map +1 -0
- package/dist/lib/gitlab-client.d.ts +331 -0
- package/dist/lib/gitlab-client.js +1025 -0
- package/dist/lib/gitlab-client.js.map +1 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +13 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/network.d.ts +3 -0
- package/dist/lib/network.js +38 -0
- package/dist/lib/network.js.map +1 -0
- package/dist/lib/oauth.d.ts +29 -0
- package/dist/lib/oauth.js +220 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/lib/output.d.ts +14 -0
- package/dist/lib/output.js +38 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/policy.d.ts +25 -0
- package/dist/lib/policy.js +48 -0
- package/dist/lib/policy.js.map +1 -0
- package/dist/lib/request-runtime.d.ts +26 -0
- package/dist/lib/request-runtime.js +323 -0
- package/dist/lib/request-runtime.js.map +1 -0
- package/dist/lib/sanitize.d.ts +1 -0
- package/dist/lib/sanitize.js +21 -0
- package/dist/lib/sanitize.js.map +1 -0
- package/dist/lib/session-capacity.d.ts +8 -0
- package/dist/lib/session-capacity.js +7 -0
- package/dist/lib/session-capacity.js.map +1 -0
- package/dist/server/build-server.d.ts +3 -0
- package/dist/server/build-server.js +13 -0
- package/dist/server/build-server.js.map +1 -0
- package/dist/tools/gitlab.d.ts +9 -0
- package/dist/tools/gitlab.js +2576 -0
- package/dist/tools/gitlab.js.map +1 -0
- package/dist/tools/health.d.ts +2 -0
- package/dist/tools/health.js +21 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/mr-code-context.d.ts +38 -0
- package/dist/tools/mr-code-context.js +330 -0
- package/dist/tools/mr-code-context.js.map +1 -0
- package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
- package/dist/types/context.js +2 -0
- package/dist/types/context.js.map +1 -0
- package/docs/configuration.md +6 -6
- package/docs/mcp-integration-testing-best-practices.md +981 -0
- package/package.json +21 -1
- package/.dockerignore +0 -7
- package/.editorconfig +0 -9
- package/.env.example +0 -75
- package/.github/workflows/nodejs.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -31
- package/.husky/pre-commit +0 -1
- package/.nvmrc +0 -1
- package/.prettierrc.json +0 -6
- package/Dockerfile +0 -20
- package/docker-compose.yml +0 -10
- package/eslint.config.js +0 -23
- package/scripts/get-oauth-token.example.sh +0 -15
- package/src/config/env.ts +0 -171
- package/src/http.ts +0 -605
- package/src/index.ts +0 -77
- package/src/lib/auth-context.ts +0 -19
- package/src/lib/gitlab-client.ts +0 -1810
- package/src/lib/logger.ts +0 -17
- package/src/lib/network.ts +0 -45
- package/src/lib/oauth.ts +0 -287
- package/src/lib/output.ts +0 -51
- package/src/lib/policy.ts +0 -78
- package/src/lib/request-runtime.ts +0 -376
- package/src/lib/sanitize.ts +0 -25
- package/src/server/build-server.ts +0 -17
- package/src/tools/gitlab.ts +0 -3128
- package/src/tools/health.ts +0 -27
- package/src/tools/mr-code-context.ts +0 -473
- package/tests/auth-context.test.ts +0 -102
- package/tests/gitlab-client.test.ts +0 -674
- package/tests/graphql-guard.test.ts +0 -121
- package/tests/integration/agent-loop.integration.test.ts +0 -552
- package/tests/integration/server.integration.test.ts +0 -543
- package/tests/mr-code-context.test.ts +0 -600
- package/tests/oauth.test.ts +0 -43
- package/tests/output.test.ts +0 -186
- package/tests/policy.test.ts +0 -324
- package/tests/request-runtime.test.ts +0 -252
- package/tests/sanitize.test.ts +0 -123
- package/tests/upload-reference.test.ts +0 -84
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -12
package/tests/output.test.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { OutputFormatter } from "../src/lib/output.js";
|
|
4
|
-
|
|
5
|
-
describe("OutputFormatter", () => {
|
|
6
|
-
describe("json mode", () => {
|
|
7
|
-
it("formats objects with 2-space indentation", () => {
|
|
8
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
9
|
-
const result = formatter.format({ hello: "world", count: 42 });
|
|
10
|
-
|
|
11
|
-
expect(result.text).toBe(JSON.stringify({ hello: "world", count: 42 }, null, 2));
|
|
12
|
-
expect(result.truncated).toBe(false);
|
|
13
|
-
expect(result.bytes).toBeGreaterThan(0);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("formats arrays", () => {
|
|
17
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
18
|
-
const result = formatter.format([1, 2, 3]);
|
|
19
|
-
|
|
20
|
-
expect(result.text).toBe(JSON.stringify([1, 2, 3], null, 2));
|
|
21
|
-
expect(result.truncated).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("formats null", () => {
|
|
25
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
26
|
-
const result = formatter.format(null);
|
|
27
|
-
|
|
28
|
-
expect(result.text).toBe("null");
|
|
29
|
-
expect(result.truncated).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("formats primitive strings", () => {
|
|
33
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
34
|
-
const result = formatter.format("hello");
|
|
35
|
-
|
|
36
|
-
expect(result.text).toBe('"hello"');
|
|
37
|
-
expect(result.truncated).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("formats numbers", () => {
|
|
41
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
42
|
-
const result = formatter.format(42);
|
|
43
|
-
|
|
44
|
-
expect(result.text).toBe("42");
|
|
45
|
-
expect(result.truncated).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("formats booleans", () => {
|
|
49
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
50
|
-
const result = formatter.format(true);
|
|
51
|
-
|
|
52
|
-
expect(result.text).toBe("true");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("compact-json mode", () => {
|
|
57
|
-
it("formats without whitespace", () => {
|
|
58
|
-
const formatter = new OutputFormatter({ responseMode: "compact-json", maxBytes: 10_000 });
|
|
59
|
-
const result = formatter.format({ hello: "world", nested: { a: 1 } });
|
|
60
|
-
|
|
61
|
-
expect(result.text).toBe('{"hello":"world","nested":{"a":1}}');
|
|
62
|
-
expect(result.truncated).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("formats arrays compactly", () => {
|
|
66
|
-
const formatter = new OutputFormatter({ responseMode: "compact-json", maxBytes: 10_000 });
|
|
67
|
-
const result = formatter.format([1, 2, 3]);
|
|
68
|
-
|
|
69
|
-
expect(result.text).toBe("[1,2,3]");
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("yaml mode", () => {
|
|
74
|
-
it("formats objects as YAML", () => {
|
|
75
|
-
const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
|
|
76
|
-
const result = formatter.format({ name: "test", value: 123 });
|
|
77
|
-
|
|
78
|
-
expect(result.text).toContain("name: test");
|
|
79
|
-
expect(result.text).toContain("value: 123");
|
|
80
|
-
expect(result.truncated).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("formats nested objects as YAML", () => {
|
|
84
|
-
const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
|
|
85
|
-
const result = formatter.format({ outer: { inner: "value" } });
|
|
86
|
-
|
|
87
|
-
expect(result.text).toContain("outer:");
|
|
88
|
-
expect(result.text).toContain("inner: value");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("formats arrays as YAML", () => {
|
|
92
|
-
const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
|
|
93
|
-
const result = formatter.format({ items: ["a", "b", "c"] });
|
|
94
|
-
|
|
95
|
-
expect(result.text).toContain("items:");
|
|
96
|
-
expect(result.text).toContain("- a");
|
|
97
|
-
expect(result.text).toContain("- b");
|
|
98
|
-
expect(result.text).toContain("- c");
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("truncation", () => {
|
|
103
|
-
it("truncates output exceeding maxBytes", () => {
|
|
104
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 20 });
|
|
105
|
-
const result = formatter.format({ key: "this is a very long value that exceeds the limit" });
|
|
106
|
-
|
|
107
|
-
expect(result.truncated).toBe(true);
|
|
108
|
-
expect(result.text).toContain("[truncated");
|
|
109
|
-
expect(result.bytes).toBeGreaterThan(20);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("does not truncate when output fits within maxBytes", () => {
|
|
113
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 100_000 });
|
|
114
|
-
const result = formatter.format({ small: "data" });
|
|
115
|
-
|
|
116
|
-
expect(result.truncated).toBe(false);
|
|
117
|
-
expect(result.text).not.toContain("[truncated");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("reports correct original byte count even when truncated", () => {
|
|
121
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10 });
|
|
122
|
-
const data = { message: "hello world this is a longer message" };
|
|
123
|
-
const result = formatter.format(data);
|
|
124
|
-
|
|
125
|
-
const expectedFull = JSON.stringify(data, null, 2);
|
|
126
|
-
const expectedBytes = Buffer.byteLength(expectedFull, "utf8");
|
|
127
|
-
|
|
128
|
-
expect(result.truncated).toBe(true);
|
|
129
|
-
expect(result.bytes).toBe(expectedBytes);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("handles exact boundary correctly", () => {
|
|
133
|
-
const data = { a: 1 };
|
|
134
|
-
const serialized = JSON.stringify(data, null, 2);
|
|
135
|
-
const exactBytes = Buffer.byteLength(serialized, "utf8");
|
|
136
|
-
|
|
137
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: exactBytes });
|
|
138
|
-
const result = formatter.format(data);
|
|
139
|
-
|
|
140
|
-
expect(result.truncated).toBe(false);
|
|
141
|
-
expect(result.bytes).toBe(exactBytes);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("handles multi-byte characters in truncation", () => {
|
|
145
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 30 });
|
|
146
|
-
const result = formatter.format({ emoji: "Hello 🌍🌍🌍🌍🌍" });
|
|
147
|
-
|
|
148
|
-
expect(result.truncated).toBe(true);
|
|
149
|
-
expect(result.text).toContain("[truncated");
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe("edge cases", () => {
|
|
154
|
-
it("handles empty object", () => {
|
|
155
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
156
|
-
const result = formatter.format({});
|
|
157
|
-
|
|
158
|
-
expect(result.text).toBe("{}");
|
|
159
|
-
expect(result.truncated).toBe(false);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("handles empty array", () => {
|
|
163
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
164
|
-
const result = formatter.format([]);
|
|
165
|
-
|
|
166
|
-
expect(result.text).toBe("[]");
|
|
167
|
-
expect(result.truncated).toBe(false);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("throws on undefined because JSON.stringify returns undefined", () => {
|
|
171
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
172
|
-
// JSON.stringify(undefined) returns the JS value undefined, which is
|
|
173
|
-
// not a valid argument for Buffer.byteLength.
|
|
174
|
-
expect(() => formatter.format(undefined)).toThrow();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("handles deeply nested objects", () => {
|
|
178
|
-
const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
|
|
179
|
-
const deep = { a: { b: { c: { d: { e: "deep" } } } } };
|
|
180
|
-
const result = formatter.format(deep);
|
|
181
|
-
|
|
182
|
-
expect(result.text).toContain('"deep"');
|
|
183
|
-
expect(result.truncated).toBe(false);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
});
|
package/tests/policy.test.ts
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { ToolPolicyEngine, type ToolPolicyMeta } from "../src/lib/policy.js";
|
|
4
|
-
|
|
5
|
-
const defaultFeatures = {
|
|
6
|
-
wiki: true,
|
|
7
|
-
milestone: true,
|
|
8
|
-
pipeline: true,
|
|
9
|
-
release: true
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
describe("ToolPolicyEngine", () => {
|
|
13
|
-
describe("filterTools", () => {
|
|
14
|
-
it("blocks mutating tools in readonly mode", () => {
|
|
15
|
-
const engine = new ToolPolicyEngine({
|
|
16
|
-
readOnlyMode: true,
|
|
17
|
-
allowedTools: [],
|
|
18
|
-
enabledFeatures: defaultFeatures
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
expect(
|
|
22
|
-
engine.filterTools([
|
|
23
|
-
{ name: "readonly", mutating: false },
|
|
24
|
-
{ name: "mutate", mutating: true }
|
|
25
|
-
])
|
|
26
|
-
).toEqual([{ name: "readonly", mutating: false }]);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("allows all tools when no restrictions are set", () => {
|
|
30
|
-
const engine = new ToolPolicyEngine({
|
|
31
|
-
readOnlyMode: false,
|
|
32
|
-
allowedTools: [],
|
|
33
|
-
enabledFeatures: defaultFeatures
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const tools: ToolPolicyMeta[] = [
|
|
37
|
-
{ name: "tool_a", mutating: false },
|
|
38
|
-
{ name: "tool_b", mutating: true },
|
|
39
|
-
{ name: "tool_c", mutating: false }
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
expect(engine.filterTools(tools)).toEqual(tools);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("applies allowlist and deny regex", () => {
|
|
46
|
-
const engine = new ToolPolicyEngine({
|
|
47
|
-
readOnlyMode: false,
|
|
48
|
-
allowedTools: ["gitlab_get_project", "gitlab_list_projects"],
|
|
49
|
-
deniedToolsRegex: /^gitlab_list_/,
|
|
50
|
-
enabledFeatures: defaultFeatures
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
expect(
|
|
54
|
-
engine.filterTools([
|
|
55
|
-
{ name: "gitlab_get_project", mutating: false },
|
|
56
|
-
{ name: "gitlab_list_projects", mutating: false },
|
|
57
|
-
{ name: "gitlab_create_issue", mutating: true }
|
|
58
|
-
])
|
|
59
|
-
).toEqual([{ name: "gitlab_get_project", mutating: false }]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("supports allowlist names without gitlab_ prefix", () => {
|
|
63
|
-
const engine = new ToolPolicyEngine({
|
|
64
|
-
readOnlyMode: false,
|
|
65
|
-
allowedTools: ["get_project"],
|
|
66
|
-
enabledFeatures: defaultFeatures
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
expect(
|
|
70
|
-
engine.filterTools([
|
|
71
|
-
{ name: "gitlab_get_project", mutating: false },
|
|
72
|
-
{ name: "gitlab_list_projects", mutating: false }
|
|
73
|
-
])
|
|
74
|
-
).toEqual([{ name: "gitlab_get_project", mutating: false }]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("supports allowlist names with gitlab_ prefix", () => {
|
|
78
|
-
const engine = new ToolPolicyEngine({
|
|
79
|
-
readOnlyMode: false,
|
|
80
|
-
allowedTools: ["gitlab_get_project"],
|
|
81
|
-
enabledFeatures: defaultFeatures
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
expect(
|
|
85
|
-
engine.filterTools([
|
|
86
|
-
{ name: "gitlab_get_project", mutating: false },
|
|
87
|
-
{ name: "gitlab_list_projects", mutating: false }
|
|
88
|
-
])
|
|
89
|
-
).toEqual([{ name: "gitlab_get_project", mutating: false }]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("respects feature flags", () => {
|
|
93
|
-
const engine = new ToolPolicyEngine({
|
|
94
|
-
readOnlyMode: false,
|
|
95
|
-
allowedTools: [],
|
|
96
|
-
enabledFeatures: {
|
|
97
|
-
wiki: false,
|
|
98
|
-
milestone: false,
|
|
99
|
-
pipeline: true,
|
|
100
|
-
release: true
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
expect(
|
|
105
|
-
engine.filterTools([
|
|
106
|
-
{ name: "wiki", mutating: false, requiresFeature: "wiki" },
|
|
107
|
-
{ name: "pipeline", mutating: false, requiresFeature: "pipeline" }
|
|
108
|
-
])
|
|
109
|
-
).toEqual([{ name: "pipeline", mutating: false, requiresFeature: "pipeline" }]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("allows tools without requiresFeature even when features are disabled", () => {
|
|
113
|
-
const engine = new ToolPolicyEngine({
|
|
114
|
-
readOnlyMode: false,
|
|
115
|
-
allowedTools: [],
|
|
116
|
-
enabledFeatures: {
|
|
117
|
-
wiki: false,
|
|
118
|
-
milestone: false,
|
|
119
|
-
pipeline: false,
|
|
120
|
-
release: false
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
expect(
|
|
125
|
-
engine.filterTools([
|
|
126
|
-
{ name: "generic_tool", mutating: false },
|
|
127
|
-
{ name: "wiki_tool", mutating: false, requiresFeature: "wiki" }
|
|
128
|
-
])
|
|
129
|
-
).toEqual([{ name: "generic_tool", mutating: false }]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("applies deniedToolsRegex without allowlist", () => {
|
|
133
|
-
const engine = new ToolPolicyEngine({
|
|
134
|
-
readOnlyMode: false,
|
|
135
|
-
allowedTools: [],
|
|
136
|
-
deniedToolsRegex: /^gitlab_delete_/,
|
|
137
|
-
enabledFeatures: defaultFeatures
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
expect(
|
|
141
|
-
engine.filterTools([
|
|
142
|
-
{ name: "gitlab_get_project", mutating: false },
|
|
143
|
-
{ name: "gitlab_delete_project", mutating: true },
|
|
144
|
-
{ name: "gitlab_delete_issue", mutating: true }
|
|
145
|
-
])
|
|
146
|
-
).toEqual([{ name: "gitlab_get_project", mutating: false }]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("handles empty tools array", () => {
|
|
150
|
-
const engine = new ToolPolicyEngine({
|
|
151
|
-
readOnlyMode: false,
|
|
152
|
-
allowedTools: [],
|
|
153
|
-
enabledFeatures: defaultFeatures
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
expect(engine.filterTools([])).toEqual([]);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("combines readOnly mode with feature flags", () => {
|
|
160
|
-
const engine = new ToolPolicyEngine({
|
|
161
|
-
readOnlyMode: true,
|
|
162
|
-
allowedTools: [],
|
|
163
|
-
enabledFeatures: {
|
|
164
|
-
wiki: true,
|
|
165
|
-
milestone: true,
|
|
166
|
-
pipeline: true,
|
|
167
|
-
release: true
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
expect(
|
|
172
|
-
engine.filterTools([
|
|
173
|
-
{ name: "read_wiki", mutating: false, requiresFeature: "wiki" },
|
|
174
|
-
{ name: "create_wiki", mutating: true, requiresFeature: "wiki" },
|
|
175
|
-
{ name: "get_project", mutating: false }
|
|
176
|
-
])
|
|
177
|
-
).toEqual([
|
|
178
|
-
{ name: "read_wiki", mutating: false, requiresFeature: "wiki" },
|
|
179
|
-
{ name: "get_project", mutating: false }
|
|
180
|
-
]);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("handles allowlist with whitespace-padded names", () => {
|
|
184
|
-
const engine = new ToolPolicyEngine({
|
|
185
|
-
readOnlyMode: false,
|
|
186
|
-
allowedTools: [" get_project "],
|
|
187
|
-
enabledFeatures: defaultFeatures
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
expect(
|
|
191
|
-
engine.filterTools([
|
|
192
|
-
{ name: "gitlab_get_project", mutating: false },
|
|
193
|
-
{ name: "gitlab_list_projects", mutating: false }
|
|
194
|
-
])
|
|
195
|
-
).toEqual([{ name: "gitlab_get_project", mutating: false }]);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("handles allowlist with empty strings", () => {
|
|
199
|
-
const engine = new ToolPolicyEngine({
|
|
200
|
-
readOnlyMode: false,
|
|
201
|
-
allowedTools: ["", " ", "get_project"],
|
|
202
|
-
enabledFeatures: defaultFeatures
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
expect(
|
|
206
|
-
engine.filterTools([
|
|
207
|
-
{ name: "gitlab_get_project", mutating: false },
|
|
208
|
-
{ name: "gitlab_list_projects", mutating: false }
|
|
209
|
-
])
|
|
210
|
-
).toEqual([{ name: "gitlab_get_project", mutating: false }]);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("respects release feature flag", () => {
|
|
214
|
-
const engine = new ToolPolicyEngine({
|
|
215
|
-
readOnlyMode: false,
|
|
216
|
-
allowedTools: [],
|
|
217
|
-
enabledFeatures: {
|
|
218
|
-
wiki: true,
|
|
219
|
-
milestone: true,
|
|
220
|
-
pipeline: true,
|
|
221
|
-
release: false
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
expect(
|
|
226
|
-
engine.filterTools([
|
|
227
|
-
{ name: "list_releases", mutating: false, requiresFeature: "release" },
|
|
228
|
-
{ name: "list_pipelines", mutating: false, requiresFeature: "pipeline" }
|
|
229
|
-
])
|
|
230
|
-
).toEqual([{ name: "list_pipelines", mutating: false, requiresFeature: "pipeline" }]);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe("assertCanExecute", () => {
|
|
235
|
-
it("does not throw for enabled tools", () => {
|
|
236
|
-
const engine = new ToolPolicyEngine({
|
|
237
|
-
readOnlyMode: false,
|
|
238
|
-
allowedTools: [],
|
|
239
|
-
enabledFeatures: defaultFeatures
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
expect(() => {
|
|
243
|
-
engine.assertCanExecute({ name: "any_tool", mutating: false });
|
|
244
|
-
}).not.toThrow();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("throws for disabled tools in readonly mode", () => {
|
|
248
|
-
const engine = new ToolPolicyEngine({
|
|
249
|
-
readOnlyMode: true,
|
|
250
|
-
allowedTools: [],
|
|
251
|
-
enabledFeatures: defaultFeatures
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
expect(() => {
|
|
255
|
-
engine.assertCanExecute({ name: "create_issue", mutating: true });
|
|
256
|
-
}).toThrow("disabled by policy");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("throws for tools not in allowlist", () => {
|
|
260
|
-
const engine = new ToolPolicyEngine({
|
|
261
|
-
readOnlyMode: false,
|
|
262
|
-
allowedTools: ["gitlab_get_project"],
|
|
263
|
-
enabledFeatures: defaultFeatures
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
expect(() => {
|
|
267
|
-
engine.assertCanExecute({ name: "gitlab_list_projects", mutating: false });
|
|
268
|
-
}).toThrow("disabled by policy");
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("throws for tools matching denied regex", () => {
|
|
272
|
-
const engine = new ToolPolicyEngine({
|
|
273
|
-
readOnlyMode: false,
|
|
274
|
-
allowedTools: [],
|
|
275
|
-
deniedToolsRegex: /^gitlab_delete_/,
|
|
276
|
-
enabledFeatures: defaultFeatures
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
expect(() => {
|
|
280
|
-
engine.assertCanExecute({ name: "gitlab_delete_issue", mutating: true });
|
|
281
|
-
}).toThrow("disabled by policy");
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("throws for tools requiring disabled features", () => {
|
|
285
|
-
const engine = new ToolPolicyEngine({
|
|
286
|
-
readOnlyMode: false,
|
|
287
|
-
allowedTools: [],
|
|
288
|
-
enabledFeatures: {
|
|
289
|
-
wiki: false,
|
|
290
|
-
milestone: true,
|
|
291
|
-
pipeline: true,
|
|
292
|
-
release: true
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
expect(() => {
|
|
297
|
-
engine.assertCanExecute({ name: "wiki_tool", mutating: false, requiresFeature: "wiki" });
|
|
298
|
-
}).toThrow("disabled by policy");
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
describe("isToolEnabled", () => {
|
|
303
|
-
it("returns true for unrestricted tools", () => {
|
|
304
|
-
const engine = new ToolPolicyEngine({
|
|
305
|
-
readOnlyMode: false,
|
|
306
|
-
allowedTools: [],
|
|
307
|
-
enabledFeatures: defaultFeatures
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
expect(engine.isToolEnabled({ name: "any_tool", mutating: false })).toBe(true);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("returns false for mutating tools in readonly mode", () => {
|
|
314
|
-
const engine = new ToolPolicyEngine({
|
|
315
|
-
readOnlyMode: true,
|
|
316
|
-
allowedTools: [],
|
|
317
|
-
enabledFeatures: defaultFeatures
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
expect(engine.isToolEnabled({ name: "create_something", mutating: true })).toBe(false);
|
|
321
|
-
expect(engine.isToolEnabled({ name: "read_something", mutating: false })).toBe(true);
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
});
|