@blogic-cz/agent-tools 0.18.1 → 1.0.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.
Files changed (68) hide show
  1. package/README.md +23 -6
  2. package/dist/az-tool/config.d.ts +60 -12
  3. package/dist/az-tool/config.d.ts.map +1 -1
  4. package/dist/az-tool/errors.d.ts +10 -1
  5. package/dist/az-tool/errors.d.ts.map +1 -1
  6. package/dist/az-tool/profile.d.ts +16 -0
  7. package/dist/az-tool/profile.d.ts.map +1 -0
  8. package/dist/az-tool/security.d.ts +20 -3
  9. package/dist/az-tool/security.d.ts.map +1 -1
  10. package/dist/az-tool/service.d.ts +9 -4
  11. package/dist/az-tool/service.d.ts.map +1 -1
  12. package/dist/az-tool/types.d.ts +10 -60
  13. package/dist/az-tool/types.d.ts.map +1 -1
  14. package/dist/{az-tool → azdo-tool}/build.d.ts +9 -9
  15. package/dist/azdo-tool/build.d.ts.map +1 -0
  16. package/dist/azdo-tool/config.d.ts +13 -0
  17. package/dist/azdo-tool/config.d.ts.map +1 -0
  18. package/dist/azdo-tool/errors.d.ts +43 -0
  19. package/dist/azdo-tool/errors.d.ts.map +1 -0
  20. package/dist/azdo-tool/extract-option-value.d.ts.map +1 -0
  21. package/dist/azdo-tool/index.d.ts +3 -0
  22. package/dist/azdo-tool/index.d.ts.map +1 -0
  23. package/dist/azdo-tool/security.d.ts +4 -0
  24. package/dist/azdo-tool/security.d.ts.map +1 -0
  25. package/dist/azdo-tool/service.d.ts +15 -0
  26. package/dist/azdo-tool/service.d.ts.map +1 -0
  27. package/dist/azdo-tool/transformers.d.ts.map +1 -0
  28. package/dist/azdo-tool/types.d.ts +64 -0
  29. package/dist/azdo-tool/types.d.ts.map +1 -0
  30. package/dist/config/index.d.ts +1 -1
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/loader.d.ts +1 -1
  33. package/dist/config/loader.d.ts.map +1 -1
  34. package/dist/config/types.d.ts +18 -1
  35. package/dist/config/types.d.ts.map +1 -1
  36. package/dist/credential-guard/index.d.ts.map +1 -1
  37. package/dist/shared/azure-credentials.d.ts +20 -0
  38. package/dist/shared/azure-credentials.d.ts.map +1 -0
  39. package/dist/shared/binary-preflight.d.ts +1 -1
  40. package/package.json +8 -2
  41. package/schemas/agent-tools.schema.json +42 -1
  42. package/src/az-tool/config.ts +135 -25
  43. package/src/az-tool/errors.ts +14 -1
  44. package/src/az-tool/index.ts +67 -134
  45. package/src/az-tool/profile.ts +48 -0
  46. package/src/az-tool/security.ts +248 -89
  47. package/src/az-tool/service.ts +137 -242
  48. package/src/az-tool/types.ts +11 -64
  49. package/src/{az-tool → azdo-tool}/build.ts +11 -11
  50. package/src/azdo-tool/config.ts +33 -0
  51. package/src/azdo-tool/errors.ts +41 -0
  52. package/src/azdo-tool/index.ts +222 -0
  53. package/src/azdo-tool/security.ts +157 -0
  54. package/src/azdo-tool/service.ts +322 -0
  55. package/src/azdo-tool/types.ts +67 -0
  56. package/src/config/index.ts +1 -0
  57. package/src/config/loader.ts +10 -1
  58. package/src/config/types.ts +19 -1
  59. package/src/credential-guard/index.ts +7 -2
  60. package/src/shared/azure-credentials.ts +50 -0
  61. package/src/shared/binary-preflight.ts +1 -1
  62. package/dist/az-tool/build.d.ts.map +0 -1
  63. package/dist/az-tool/extract-option-value.d.ts.map +0 -1
  64. package/dist/az-tool/transformers.d.ts.map +0 -1
  65. /package/dist/{az-tool → azdo-tool}/extract-option-value.d.ts +0 -0
  66. /package/dist/{az-tool → azdo-tool}/transformers.d.ts +0 -0
  67. /package/src/{az-tool → azdo-tool}/extract-option-value.ts +0 -0
  68. /package/src/{az-tool → azdo-tool}/transformers.ts +0 -0
@@ -1,50 +1,112 @@
1
1
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2
2
  import { Context, Effect, Layer, Option, Stream } from "effect";
3
3
 
4
- import type { InvokeParams } from "./types";
5
- import type { AzureConfig } from "#config/types";
4
+ import type { AzurePlatformConfig } from "#config/types";
6
5
 
7
- import { DIRECT_AZ_COMMANDS, STANDALONE_AZ_COMMANDS } from "./config";
8
- import { AzSecurityError, AzCommandError, AzTimeoutError, AzParseError } from "./errors";
9
- import { isCommandAllowed, isInvokeAllowed } from "./security";
10
- import { transformCmdOutput } from "./transformers";
6
+ import type { AzParseError } from "./errors";
7
+
8
+ import { AzCommandError, AzProfileError, AzSecurityError, AzTimeoutError } from "./errors";
9
+ import { isProductionProfile, selectAzProfileName } from "./profile";
10
+ import { isAzCommandAllowed } from "./security";
11
11
  import { ConfigService, getToolConfig } from "#config";
12
- import { renderCommandLine, tokenizeCommandLine } from "#shared/exec";
12
+ import { missingBinaryFromSpawnFailure } from "#shared/binary-preflight";
13
+ import { renderCommandLine } from "#shared/exec";
14
+
15
+ const DEFAULT_TIMEOUT_MS = 60000;
16
+
17
+ const hasOutputFlag = (argv: readonly string[]): boolean =>
18
+ argv.some((arg) => arg === "-o" || arg === "--output" || arg.startsWith("--output="));
19
+
20
+ export type AzCommandResult = {
21
+ readonly command: string;
22
+ readonly subscription: string | undefined;
23
+ readonly data: unknown;
24
+ };
13
25
 
14
26
  export class AzService extends Context.Service<
15
27
  AzService,
16
28
  {
17
29
  readonly runCommand: (
18
30
  cmd: string,
19
- project?: string,
20
- ) => Effect.Effect<string, AzSecurityError | AzCommandError | AzTimeoutError | AzParseError>;
21
- readonly runInvoke: (
22
- params: InvokeParams,
23
- ) => Effect.Effect<unknown, AzSecurityError | AzCommandError | AzTimeoutError | AzParseError>;
31
+ profile?: string,
32
+ ) => Effect.Effect<
33
+ AzCommandResult,
34
+ AzSecurityError | AzCommandError | AzTimeoutError | AzParseError | AzProfileError
35
+ >;
36
+ readonly renderCommand: (
37
+ cmd: string,
38
+ profile?: string,
39
+ ) => Effect.Effect<string, AzSecurityError | AzCommandError | AzProfileError>;
24
40
  }
25
41
  >()("@agent-tools/AzService") {
26
42
  static readonly layer = Layer.effect(
27
43
  AzService,
28
44
  Effect.gen(function* () {
29
45
  const config = yield* ConfigService;
30
- const azConfig = getToolConfig<AzureConfig>(config, "azure");
31
-
32
- if (!azConfig) {
33
- const noConfigError = new AzCommandError({
34
- message: "No Azure configuration found. Add an 'azure' section to agent-tools.json5.",
35
- command: "unknown",
36
- exitCode: -1,
37
- hint: "Create or update agent-tools.json5 with an 'azure' section containing organization and defaultProject",
46
+ const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
47
+
48
+ const resolveConfig = (profile?: string) =>
49
+ Effect.gen(function* () {
50
+ const azConfig = getToolConfig<AzurePlatformConfig>(config, "azurePlatform", profile);
51
+
52
+ if (!azConfig) {
53
+ return yield* new AzCommandError({
54
+ message:
55
+ "No Azure platform configuration found. Add an 'azurePlatform' section to agent-tools.json5.",
56
+ command: "unknown",
57
+ exitCode: -1,
58
+ hint: "azurePlatform pins the subscription every command runs against, so it is required.",
59
+ nextCommand:
60
+ "echo '{ azurePlatform: { default: { subscription: \"<subscription-id>\" } } }' > agent-tools.json5",
61
+ });
62
+ }
63
+
64
+ // A production subscription must be named, never inherited from
65
+ // auto-selection or the "default" key.
66
+ const profileName = selectAzProfileName(config?.azurePlatform, profile);
67
+ if (!profile && isProductionProfile(profileName, azConfig)) {
68
+ const label = profileName ?? "default";
69
+ return yield* new AzProfileError({
70
+ message: `Implicit production access blocked. Profile '${label}' is a production profile but --profile was not passed explicitly.`,
71
+ profile: label,
72
+ hint: `Pass --profile ${label} explicitly to confirm production access, or set production: false on that profile.`,
73
+ nextCommand: `agent-tools-az cmd --profile ${label} --cmd "group list"`,
74
+ });
75
+ }
76
+
77
+ return azConfig;
38
78
  });
39
- return {
40
- runCommand: (_cmd: string, _project?: string) => Effect.fail(noConfigError),
41
- runInvoke: (_params: InvokeParams) => Effect.fail(noConfigError),
42
- };
43
- }
44
79
 
45
- const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
80
+ /** Security gate plus profile scoping. Shared by dry-run rendering and execution. */
81
+ const buildArgv = (cmd: string, profile?: string) =>
82
+ Effect.gen(function* () {
83
+ const azConfig = yield* resolveConfig(profile);
84
+ const securityCheck = isAzCommandAllowed(cmd, {
85
+ allowedResourceGroups: azConfig.allowedResourceGroups,
86
+ });
87
+
88
+ if (!securityCheck.allowed || !securityCheck.argv) {
89
+ return yield* new AzSecurityError({
90
+ message: securityCheck.reason ?? "Command not allowed",
91
+ command: cmd,
92
+ hint:
93
+ securityCheck.hint ??
94
+ "Only read-only Azure platform commands are allowed. Use azdo-tool for Azure DevOps.",
95
+ });
96
+ }
97
+
98
+ const argv = [...securityCheck.argv, "--only-show-errors"];
99
+
100
+ if (!hasOutputFlag(securityCheck.argv)) {
101
+ argv.push("--output", "json");
102
+ }
103
+
104
+ argv.push("--subscription", azConfig.subscription);
46
105
 
47
- const runAzCommand = (argv: readonly string[], timeoutMs: number) =>
106
+ return { argv, azConfig };
107
+ });
108
+
109
+ const spawnAz = (argv: readonly string[], timeoutMs: number, renderedCommand: string) =>
48
110
  Effect.scoped(
49
111
  Effect.gen(function* () {
50
112
  const command = ChildProcess.make("az", argv, {
@@ -54,89 +116,52 @@ export class AzService extends Context.Service<
54
116
  const process = yield* executor.spawn(command);
55
117
 
56
118
  const stdoutChunk = yield* process.stdout.pipe(Stream.decodeText(), Stream.runCollect);
57
- const stdout = stdoutChunk.join("");
58
-
59
119
  const stderrChunk = yield* process.stderr.pipe(Stream.decodeText(), Stream.runCollect);
60
- const stderr = stderrChunk.join("");
61
-
62
120
  const exitCode = yield* process.exitCode;
63
121
 
64
- return { stdout, stderr, exitCode };
122
+ return { stdout: stdoutChunk.join(""), stderr: stderrChunk.join(""), exitCode };
65
123
  }),
66
124
  ).pipe(
67
125
  Effect.timeoutOption(timeoutMs),
68
- Effect.mapError(
69
- (platformError) =>
70
- new AzCommandError({
71
- message: `Command execution failed: ${platformError.message}`,
72
- command: renderCommandLine(["az", ...argv]),
73
- exitCode: -1,
74
- hint: "Check that the az CLI is installed and authenticated",
75
- nextCommand: "az login",
76
- retryable: true,
77
- }),
78
- ),
126
+ Effect.mapError((platformError) => {
127
+ const missing = missingBinaryFromSpawnFailure("az", String(platformError));
128
+ return new AzCommandError({
129
+ message: `Command execution failed: ${String(platformError)}`,
130
+ command: renderedCommand,
131
+ exitCode: -1,
132
+ stderr: String(platformError),
133
+ hint: missing?.hint ?? "Check that the az CLI is installed and authenticated",
134
+ nextCommand: "az login",
135
+ retryable: true,
136
+ });
137
+ }),
79
138
  );
80
139
 
81
- const resolveProject = (project?: string) => project ?? azConfig.defaultProject;
140
+ const renderCommand = Effect.fn("AzService.renderCommand")(function* (
141
+ cmd: string,
142
+ profile?: string,
143
+ ) {
144
+ const { argv } = yield* buildArgv(cmd, profile);
145
+ return renderCommandLine(["az", ...argv]);
146
+ });
82
147
 
83
148
  const runCommand = Effect.fn("AzService.runCommand")(function* (
84
149
  cmd: string,
85
- project?: string,
150
+ profile?: string,
86
151
  ) {
87
- const projectName = resolveProject(project);
88
-
89
- const securityCheck = isCommandAllowed(cmd);
90
- if (!securityCheck.allowed) {
91
- return yield* new AzSecurityError({
92
- message: securityCheck.reason ?? "Command not allowed",
93
- command: cmd,
94
- hint: "Only read-only az devops commands are allowed. Use build helpers for common operations.",
95
- });
96
- }
97
-
98
- const invokeParams = parseInvokeFromCommand(cmd);
99
- if (invokeParams) {
100
- const invokeResult = yield* runInvoke({
101
- ...invokeParams,
102
- project: projectName,
103
- });
104
- return JSON.stringify(invokeResult);
105
- }
106
-
107
- const cmdWords = cmd.trim().split(/\s+/);
108
- const firstWord = cmdWords[0]?.toLowerCase() ?? "";
109
- const isDirectCommand = DIRECT_AZ_COMMANDS.includes(
110
- firstWord as (typeof DIRECT_AZ_COMMANDS)[number],
111
- );
112
- const isStandaloneCommand = STANDALONE_AZ_COMMANDS.includes(
113
- firstWord as (typeof STANDALONE_AZ_COMMANDS)[number],
114
- );
115
-
116
- const cmdArgv = tokenizeCommandLine(cmd);
117
- const scopeArgv = [
118
- "--organization",
119
- azConfig.organization,
120
- "--project",
121
- projectName,
122
- ] as const;
123
-
124
- const argv = isStandaloneCommand
125
- ? cmdArgv
126
- : isDirectCommand
127
- ? [...cmdArgv, ...scopeArgv]
128
- : ["devops", ...cmdArgv, ...scopeArgv];
129
-
152
+ const { argv, azConfig } = yield* buildArgv(cmd, profile);
153
+ const timeoutMs = azConfig.timeoutMs ?? DEFAULT_TIMEOUT_MS;
130
154
  const fullCommand = renderCommandLine(["az", ...argv]);
131
- const resultOption = yield* runAzCommand(argv, azConfig.timeoutMs ?? 60000);
155
+
156
+ const resultOption = yield* spawnAz(argv, timeoutMs, fullCommand);
132
157
 
133
158
  if (Option.isNone(resultOption)) {
134
159
  return yield* new AzTimeoutError({
135
- message: `Command timed out after ${azConfig.timeoutMs ?? 60000}ms`,
160
+ message: `Command timed out after ${timeoutMs}ms`,
136
161
  command: fullCommand,
137
- timeoutMs: azConfig.timeoutMs ?? 60000,
162
+ timeoutMs,
138
163
  retryable: true,
139
- hint: "The command took too long. Retry or increase timeoutMs in azure config.",
164
+ hint: "The command took too long. Retry or increase timeoutMs in azurePlatform config.",
140
165
  });
141
166
  }
142
167
 
@@ -152,165 +177,35 @@ export class AzService extends Context.Service<
152
177
  }
153
178
 
154
179
  const output = result.stdout.trim();
155
- const transformed = transformCmdOutput(output);
156
-
157
- if (typeof transformed === "string") {
158
- return transformed;
159
- }
160
-
161
- return JSON.stringify(transformed);
162
- });
163
-
164
- const runInvoke = Effect.fn("AzService.runInvoke")(function* (params: InvokeParams) {
165
- const securityCheck = isInvokeAllowed(params);
166
- if (!securityCheck.allowed) {
167
- return yield* new AzSecurityError({
168
- message: securityCheck.reason ?? "Invoke not allowed",
169
- command: `invoke --area ${params.area} --resource ${params.resource}`,
170
- hint: "Only allowed invoke areas/resources can be used. Check az-tool security config.",
171
- });
172
- }
173
-
174
- const argv = ["devops", "invoke", "--area", params.area, "--resource", params.resource];
175
180
 
176
- const projectName = resolveProject(params.project);
177
-
178
- const routeParameters = {
179
- project: projectName,
180
- ...params.routeParameters,
181
- };
182
-
183
- if (Object.keys(routeParameters).length > 0) {
184
- argv.push(
185
- "--route-parameters",
186
- ...Object.entries(routeParameters).map(([k, v]) => `${k}=${v}`),
187
- );
188
- }
189
-
190
- if (params.queryParameters) {
191
- argv.push(
192
- "--query-parameters",
193
- ...Object.entries(params.queryParameters).map(([k, v]) => `${k}=${v}`),
194
- );
195
- }
196
-
197
- argv.push("--organization", azConfig.organization, "--output", "json");
198
-
199
- const fullCommand = renderCommandLine(["az", ...argv]);
200
- const resultOption = yield* runAzCommand(argv, azConfig.timeoutMs ?? 60000);
201
-
202
- if (Option.isNone(resultOption)) {
203
- return yield* new AzTimeoutError({
204
- message: `Invoke timed out after ${azConfig.timeoutMs ?? 60000}ms`,
181
+ if (output.length === 0) {
182
+ return {
205
183
  command: fullCommand,
206
- timeoutMs: azConfig.timeoutMs ?? 60000,
207
- retryable: true,
208
- hint: "The invoke took too long. Retry or increase timeoutMs in azure config.",
209
- });
184
+ subscription: azConfig.subscription,
185
+ data: null,
186
+ } satisfies AzCommandResult;
210
187
  }
211
188
 
212
- const result = resultOption.value;
213
-
214
- if (result.exitCode !== 0) {
215
- return yield* new AzCommandError({
216
- message: result.stderr || `Invoke failed with exit code ${result.exitCode}`,
217
- command: fullCommand,
218
- exitCode: result.exitCode,
219
- ...(result.stderr ? { stderr: result.stderr } : {}),
220
- });
221
- }
189
+ // --output json is injected unless the caller picked a format, so non-JSON
190
+ // output here is a deliberate table/tsv request and is passed through as text.
191
+ const parsed = ((): unknown => {
192
+ try {
193
+ return JSON.parse(output) as unknown;
194
+ } catch {
195
+ return output;
196
+ }
197
+ })();
222
198
 
223
- const jsonData = yield* Effect.try({
224
- try: () => JSON.parse(result.stdout) as unknown,
225
- catch: () =>
226
- new AzParseError({
227
- message: `Failed to parse JSON response from invoke`,
228
- rawOutput: result.stdout.slice(0, 500),
229
- hint: "The az CLI returned non-JSON output. Ensure --output json is used.",
230
- }),
231
- });
232
- return jsonData;
199
+ return {
200
+ command: fullCommand,
201
+ subscription: azConfig.subscription,
202
+ data: parsed,
203
+ } satisfies AzCommandResult;
233
204
  });
234
205
 
235
- return { runCommand, runInvoke };
206
+ return { runCommand, renderCommand };
236
207
  }),
237
208
  );
238
209
  }
239
210
 
240
211
  export const AzServiceLayer = AzService.layer;
241
-
242
- function parseInvokeFromCommand(cmd: string): InvokeParams | undefined {
243
- const words = cmd.trim().split(/\s+/);
244
- const loweredWords = words.map((word) => word.toLowerCase());
245
-
246
- if (!loweredWords.includes("invoke")) {
247
- return undefined;
248
- }
249
-
250
- const area = extractOptionValue(words, "--area");
251
- const resource = extractOptionValue(words, "--resource");
252
-
253
- if (!area || !resource) {
254
- return undefined;
255
- }
256
-
257
- const routeParameters = extractParametersOption(words, "--route-parameters");
258
- const queryParameters = extractParametersOption(words, "--query-parameters");
259
- const apiVersion = extractOptionValue(words, "--api-version");
260
-
261
- const mergedQueryParameters = apiVersion
262
- ? {
263
- ...queryParameters,
264
- "api-version": apiVersion,
265
- }
266
- : queryParameters;
267
-
268
- return {
269
- area,
270
- resource,
271
- ...(routeParameters ? { routeParameters } : {}),
272
- ...(mergedQueryParameters ? { queryParameters: mergedQueryParameters } : {}),
273
- };
274
- }
275
-
276
- function extractOptionValue(args: readonly string[], optionName: string): string | undefined {
277
- const optionIndex = args.findIndex((arg) => arg.toLowerCase() === optionName.toLowerCase());
278
-
279
- if (optionIndex === -1) {
280
- return undefined;
281
- }
282
-
283
- return args[optionIndex + 1];
284
- }
285
-
286
- function extractParametersOption(
287
- args: readonly string[],
288
- optionName: string,
289
- ): Record<string, string | number> | undefined {
290
- const optionIndex = args.findIndex((arg) => arg.toLowerCase() === optionName.toLowerCase());
291
-
292
- if (optionIndex === -1) {
293
- return undefined;
294
- }
295
-
296
- const result: Record<string, string | number> = {};
297
-
298
- for (let i = optionIndex + 1; i < args.length; i++) {
299
- const token = args[i];
300
- if (!token || token.startsWith("--")) {
301
- break;
302
- }
303
-
304
- const equalsIndex = token.indexOf("=");
305
- if (equalsIndex === -1) {
306
- continue;
307
- }
308
-
309
- const key = token.slice(0, equalsIndex);
310
- const rawValue = token.slice(equalsIndex + 1);
311
- const parsedNumber = Number(rawValue);
312
- result[key] = Number.isNaN(parsedNumber) ? rawValue : parsedNumber;
313
- }
314
-
315
- return Object.keys(result).length > 0 ? result : undefined;
316
- }
@@ -1,67 +1,14 @@
1
- export type SecurityCheckResult = {
2
- allowed: boolean;
3
- command?: string;
4
- reason?: string;
5
- };
6
-
7
- export type InvokeParams = {
8
- area: string;
9
- resource: string;
10
- project?: string;
11
- routeParameters?: Record<string, string | number>;
12
- queryParameters?: Record<string, string | number>;
13
- };
14
-
15
- export type BuildJob = {
16
- id: string;
17
- parentId?: string | null;
18
- type: "Job" | "Stage" | "Task" | "Phase" | "Checkpoint";
19
- name: string;
20
- state: "pending" | "inProgress" | "completed";
21
- result?: "succeeded" | "failed" | "canceled" | "skipped" | null;
22
- startTime?: string | null;
23
- finishTime?: string | null;
24
- errorCount?: number | null;
25
- warningCount?: number | null;
26
- log?: { id: number; url: string } | null;
27
- };
28
-
29
- export type BuildTimeline = {
30
- records: BuildJob[];
31
- id: string;
32
- changeId: number;
33
- lastChangedBy: string;
34
- lastChangedOn: string;
35
- url: string;
1
+ export type AzSecurityCheckOptions = {
2
+ /** Empty or absent allows every resource group in the subscription. */
3
+ allowedResourceGroups?: readonly string[];
36
4
  };
37
5
 
38
- export type BuildLog = {
39
- id: number;
40
- type: string;
41
- url: string;
42
- lineCount?: number;
43
- };
44
-
45
- export type BuildLogs = {
46
- count: number;
47
- value: BuildLog[];
48
- };
49
-
50
- export type JobSummary = {
51
- name: string;
52
- state: string;
53
- result?: string;
54
- stage?: string;
55
- duration?: string;
56
- logId?: number;
57
- };
58
-
59
- export type PipelineRun = {
60
- id: number;
61
- buildNumber: string;
62
- status: string;
63
- result?: string;
64
- sourceBranch: string;
65
- startTime?: string;
66
- finishTime?: string;
6
+ export type AzSecurityCheckResult = {
7
+ allowed: boolean;
8
+ command: string;
9
+ argv?: string[];
10
+ verb?: string;
11
+ resourceGroup?: string;
12
+ reason?: string;
13
+ hint?: string;
67
14
  };
@@ -2,15 +2,15 @@ import { Effect, Schema } from "effect";
2
2
 
3
3
  import type { BuildJob, BuildLogs, BuildTimeline, JobSummary, PipelineRun } from "./types";
4
4
 
5
- import { AzParseError } from "./errors";
6
- import { AzService } from "./service";
5
+ import { AzdoParseError } from "./errors";
6
+ import { AzdoService } from "./service";
7
7
  import { transformBuildLogContent, transformTimeline } from "./transformers";
8
8
 
9
9
  /**
10
10
  * Get build timeline with all records (jobs, stages, tasks, etc.)
11
11
  */
12
12
  export const getBuildTimeline = Effect.fn("Build.getBuildTimeline")(function* (buildId: number) {
13
- const az = yield* AzService;
13
+ const az = yield* AzdoService;
14
14
 
15
15
  const result = yield* az.runInvoke({
16
16
  area: "build",
@@ -52,7 +52,7 @@ export const getBuildTimeline = Effect.fn("Build.getBuildTimeline")(function* (b
52
52
  )(result).pipe(
53
53
  Effect.mapError(
54
54
  (e) =>
55
- new AzParseError({
55
+ new AzdoParseError({
56
56
  message: `Failed to parse build timeline: ${String(e)}`,
57
57
  rawOutput: JSON.stringify(result).slice(0, 500),
58
58
  hint: "The Azure DevOps API returned an unexpected response format for build timeline",
@@ -85,7 +85,7 @@ export const getBuildJobs = Effect.fn("Build.getBuildJobs")(function* (buildId:
85
85
  * Get list of build logs
86
86
  */
87
87
  export const getBuildLogs = Effect.fn("Build.getBuildLogs")(function* (buildId: number) {
88
- const az = yield* AzService;
88
+ const az = yield* AzdoService;
89
89
 
90
90
  const result = yield* az.runInvoke({
91
91
  area: "build",
@@ -109,7 +109,7 @@ export const getBuildLogs = Effect.fn("Build.getBuildLogs")(function* (buildId:
109
109
  )(result).pipe(
110
110
  Effect.mapError(
111
111
  (e) =>
112
- new AzParseError({
112
+ new AzdoParseError({
113
113
  message: `Failed to parse build logs: ${String(e)}`,
114
114
  rawOutput: JSON.stringify(result).slice(0, 500),
115
115
  hint: "The Azure DevOps API returned an unexpected response format for build logs",
@@ -127,7 +127,7 @@ export const getBuildLogContent = Effect.fn("Build.getBuildLogContent")(function
127
127
  buildId: number,
128
128
  logId: number,
129
129
  ) {
130
- const az = yield* AzService;
130
+ const az = yield* AzdoService;
131
131
 
132
132
  const result = yield* az.runInvoke({
133
133
  area: "build",
@@ -146,7 +146,7 @@ export const getBuildLogContent = Effect.fn("Build.getBuildLogContent")(function
146
146
  )(result).pipe(
147
147
  Effect.mapError(
148
148
  (e) =>
149
- new AzParseError({
149
+ new AzdoParseError({
150
150
  message: `Failed to parse log content: ${String(e)}`,
151
151
  rawOutput: String(result).slice(0, 500),
152
152
  hint: "The Azure DevOps API returned an unexpected format for log content",
@@ -228,7 +228,7 @@ export const listPipelineRuns = Effect.fn("Build.listPipelineRuns")(function* (o
228
228
  pipelineId?: number;
229
229
  top?: number;
230
230
  }) {
231
- const az = yield* AzService;
231
+ const az = yield* AzdoService;
232
232
 
233
233
  const parts = ["pipelines", "runs", "list", "--output", "json"];
234
234
 
@@ -249,7 +249,7 @@ export const listPipelineRuns = Effect.fn("Build.listPipelineRuns")(function* (o
249
249
  const jsonData = yield* Effect.try({
250
250
  try: () => JSON.parse(rawResult) as unknown,
251
251
  catch: () =>
252
- new AzParseError({
252
+ new AzdoParseError({
253
253
  message: "Failed to parse JSON from pipeline runs output",
254
254
  rawOutput: rawResult.slice(0, 500),
255
255
  hint: "The az CLI returned non-JSON output. Check that the command ran successfully.",
@@ -273,7 +273,7 @@ export const listPipelineRuns = Effect.fn("Build.listPipelineRuns")(function* (o
273
273
  )(jsonData).pipe(
274
274
  Effect.mapError(
275
275
  (e) =>
276
- new AzParseError({
276
+ new AzdoParseError({
277
277
  message: `Failed to parse pipeline runs: ${String(e)}`,
278
278
  rawOutput: rawResult.slice(0, 500),
279
279
  hint: "The Azure DevOps API returned an unexpected response format for pipeline runs",
@@ -0,0 +1,33 @@
1
+ export const ALLOWED_SUBCOMMANDS = ["list", "run", "show", "show-tags"] as const;
2
+
3
+ export const BLOCKED_SUBCOMMANDS = ["create", "delete", "update", "cancel", "queue"] as const;
4
+
5
+ export const DIRECT_AZ_COMMANDS = ["pipelines", "repos"] as const;
6
+
7
+ export const STANDALONE_AZ_COMMANDS = ["acr", "account"] as const;
8
+
9
+ export const ALLOWED_INVOKE_AREAS = ["build"] as const;
10
+
11
+ export const ALLOWED_INVOKE_RESOURCES: Record<string, readonly string[]> = {
12
+ build: ["timeline", "logs", "builds"],
13
+ } as const;
14
+
15
+ export const BLOCKED_INVOKE_AREAS = [
16
+ "git",
17
+ "policy",
18
+ "security",
19
+ "wiki",
20
+ "work",
21
+ "graph",
22
+ "audit",
23
+ "permissions",
24
+ ] as const;
25
+
26
+ export const BLOCKED_INVOKE_RESOURCES: Record<string, readonly string[]> = {
27
+ build: ["definitions", "folders", "tags", "retention"],
28
+ } as const;
29
+
30
+ export type AllowedSubcommand = (typeof ALLOWED_SUBCOMMANDS)[number];
31
+ export type BlockedSubcommand = (typeof BLOCKED_SUBCOMMANDS)[number];
32
+ export type AllowedInvokeArea = (typeof ALLOWED_INVOKE_AREAS)[number];
33
+ export type BlockedInvokeArea = (typeof BLOCKED_INVOKE_AREAS)[number];