@nuvlore/extension-workday-infra-access 0.1.1 → 0.1.2

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.
@@ -1,440 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { createReadStream, promises as fs } from "node:fs";
3
- import { isAbsolute, resolve } from "node:path";
4
- import { createInterface } from "node:readline/promises";
5
- import { parseAllDocuments } from "yaml";
6
- import { z } from "zod";
7
-
8
- export const DEFAULT_SCAN_LIMIT_BYTES = 256 * 1024 * 1024;
9
- const MAX_SCAN_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
10
- const OUTPUT_CHAR_LIMIT = 60_000;
11
- const SENSITIVE_FILE_PATTERN =
12
- /(^|[/\\])(\.env([./\\]|$)|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$|.*\.(pem|key|p12|pfx|crt)$|kubeconfig$|credentials?$|secrets?(\.|$))/i;
13
-
14
- export const filePathSchema = z
15
- .string()
16
- .min(1)
17
- .describe("Absolute path, or a path relative to the agent working directory.");
18
-
19
- export const scanLimitSchema = z
20
- .number()
21
- .int()
22
- .positive()
23
- .max(MAX_SCAN_LIMIT_BYTES)
24
- .default(DEFAULT_SCAN_LIMIT_BYTES)
25
- .describe("Maximum bytes to stream from the file.");
26
-
27
- export const allowSensitiveSchema = z
28
- .boolean()
29
- .default(false)
30
- .describe("Set true only after explicit user approval to inspect paths that look like secrets or credentials.");
31
-
32
- export function ok(text) {
33
- return { content: [{ type: "text", text: truncate(text) }] };
34
- }
35
-
36
- export function fail(error) {
37
- return {
38
- isError: true,
39
- content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }]
40
- };
41
- }
42
-
43
- export function defineReadOnlyTool(tool) {
44
- return {
45
- ...tool,
46
- annotations: {
47
- readOnlyHint: true,
48
- destructiveHint: false,
49
- ...(tool.annotations ?? {})
50
- }
51
- };
52
- }
53
-
54
- export function assertNonHtmlApiResponse(provider, text) {
55
- const body = String(text || "");
56
- const head = body.slice(0, 1000).toLowerCase();
57
- if (/<html\b|<!doctype html\b|<form\b|login|atlassian-token|ajs-/.test(head)) {
58
- throw new Error(`${provider} API returned an HTML login or browser page instead of API data. Authentication/session refresh must be handled outside this read-only tool with human-in-the-loop approval. HTML body was suppressed.`);
59
- }
60
- }
61
-
62
- function truncate(text) {
63
- if (text.length <= OUTPUT_CHAR_LIMIT) {
64
- return text;
65
- }
66
- return `${text.slice(0, OUTPUT_CHAR_LIMIT)}\n\n[output truncated at ${OUTPUT_CHAR_LIMIT} characters]`;
67
- }
68
-
69
- function resolveFilePath(filePath) {
70
- return isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
71
- }
72
-
73
- async function assertReadableFile(filePath, allowSensitive = false) {
74
- const resolvedPath = resolveFilePath(filePath);
75
- if (!allowSensitive && SENSITIVE_FILE_PATTERN.test(resolvedPath)) {
76
- throw new Error("Refusing to read a path that looks sensitive. Re-run only with explicit user approval and allowSensitive=true.");
77
- }
78
-
79
- const stat = await fs.stat(resolvedPath);
80
- if (!stat.isFile()) {
81
- throw new Error(`${resolvedPath} is not a regular file.`);
82
- }
83
-
84
- return { resolvedPath, stat };
85
- }
86
-
87
- async function streamLines(filePath, scanLimitBytes, onLine) {
88
- const stream = createReadStream(filePath, {
89
- encoding: "utf8",
90
- highWaterMark: 1024 * 1024
91
- });
92
- const reader = createInterface({ input: stream, crlfDelay: Infinity });
93
- let bytesScanned = 0;
94
- let lineNumber = 0;
95
- let truncatedByScanLimit = false;
96
-
97
- try {
98
- for await (const line of reader) {
99
- bytesScanned += Buffer.byteLength(`${line}\n`);
100
- if (bytesScanned > scanLimitBytes) {
101
- truncatedByScanLimit = true;
102
- reader.close();
103
- stream.destroy();
104
- break;
105
- }
106
-
107
- lineNumber += 1;
108
- await onLine(line, lineNumber);
109
- }
110
- } finally {
111
- reader.close();
112
- stream.destroy();
113
- }
114
-
115
- return { bytesScanned, lineNumber, truncatedByScanLimit };
116
- }
117
-
118
- function getPathValue(value, path) {
119
- return path.split(".").reduce((current, part) => {
120
- if (current === undefined || current === null) {
121
- return undefined;
122
- }
123
- return current[part];
124
- }, value);
125
- }
126
-
127
- function increment(map, key) {
128
- map.set(key, (map.get(key) ?? 0) + 1);
129
- }
130
-
131
- function renderTopMap(title, map, limit = 20) {
132
- const entries = [...map.entries()].sort((left, right) => right[1] - left[1]).slice(0, limit);
133
- if (entries.length === 0) {
134
- return `${title}\n- none`;
135
- }
136
- return [title, ...entries.map(([key, count]) => `- ${key}: ${count}`)].join("\n");
137
- }
138
-
139
- export async function logPatternSummary(input) {
140
- const {
141
- allowSensitive = false,
142
- filePath,
143
- maxSamplesPerPattern = 5,
144
- patterns = [
145
- "ERROR",
146
- "WARN",
147
- "Exception",
148
- "Traceback",
149
- "timeout",
150
- "connection refused",
151
- "5\\d\\d",
152
- "OOMKilled",
153
- "CrashLoopBackOff"
154
- ],
155
- scanLimitBytes = DEFAULT_SCAN_LIMIT_BYTES
156
- } = input;
157
- const { resolvedPath, stat } = await assertReadableFile(filePath, allowSensitive);
158
- const compiled = patterns.map((pattern) => ({ pattern, regex: new RegExp(pattern, "i"), count: 0, samples: [] }));
159
- let firstTimestamp;
160
- let lastTimestamp;
161
- const timestampRegex = /\b\d{4}-\d{2}-\d{2}[T ][0-9:.+-]+Z?\b/;
162
-
163
- const scan = await streamLines(resolvedPath, scanLimitBytes, (line, lineNumber) => {
164
- const timestamp = line.match(timestampRegex)?.[0];
165
- if (timestamp) {
166
- firstTimestamp ??= timestamp;
167
- lastTimestamp = timestamp;
168
- }
169
-
170
- for (const item of compiled) {
171
- if (item.regex.test(line)) {
172
- item.count += 1;
173
- if (item.samples.length < maxSamplesPerPattern) {
174
- item.samples.push(`${lineNumber}: ${line}`);
175
- }
176
- }
177
- }
178
- });
179
-
180
- return [
181
- `path: ${resolvedPath}`,
182
- `sizeBytes: ${stat.size}`,
183
- `bytesScanned: ${scan.bytesScanned}`,
184
- `linesScanned: ${scan.lineNumber}`,
185
- `truncatedByScanLimit: ${scan.truncatedByScanLimit}`,
186
- `firstTimestampSeen: ${firstTimestamp ?? "unknown"}`,
187
- `lastTimestampSeen: ${lastTimestamp ?? "unknown"}`,
188
- "",
189
- "patternCounts:",
190
- ...compiled.map((item) => `- /${item.pattern}/i: ${item.count}`),
191
- "",
192
- "samples:",
193
- ...compiled.flatMap((item) => [
194
- `## /${item.pattern}/i`,
195
- item.samples.length ? item.samples.join("\n") : "(no samples)"
196
- ])
197
- ].join("\n");
198
- }
199
-
200
- export async function jsonLogAnalyze(input) {
201
- const {
202
- allowSensitive = false,
203
- filePath,
204
- groupBy = ["level", "status", "service"],
205
- maxErrorSamples = 20,
206
- scanLimitBytes = DEFAULT_SCAN_LIMIT_BYTES
207
- } = input;
208
- const { resolvedPath, stat } = await assertReadableFile(filePath, allowSensitive);
209
- const fieldCounts = new Map(groupBy.map((field) => [field, new Map()]));
210
- const errorSamples = [];
211
- let parsedLines = 0;
212
- let invalidJsonLines = 0;
213
-
214
- const scan = await streamLines(resolvedPath, scanLimitBytes, (line, lineNumber) => {
215
- if (!line.trim()) {
216
- return;
217
- }
218
-
219
- let parsed;
220
- try {
221
- parsed = JSON.parse(line);
222
- parsedLines += 1;
223
- } catch {
224
- invalidJsonLines += 1;
225
- return;
226
- }
227
-
228
- for (const field of groupBy) {
229
- const value = getPathValue(parsed, field);
230
- if (value !== undefined) {
231
- increment(fieldCounts.get(field), String(value));
232
- }
233
- }
234
-
235
- const level = String(getPathValue(parsed, "level") ?? getPathValue(parsed, "severity") ?? "").toLowerCase();
236
- const status = Number(getPathValue(parsed, "status") ?? getPathValue(parsed, "statusCode") ?? 0);
237
- const message = String(getPathValue(parsed, "message") ?? getPathValue(parsed, "msg") ?? getPathValue(parsed, "error.message") ?? "");
238
- if ((level && ["error", "fatal", "warn", "warning"].includes(level)) || status >= 400 || /error|exception|timeout/i.test(message)) {
239
- if (errorSamples.length < maxErrorSamples) {
240
- errorSamples.push(`${lineNumber}: ${line}`);
241
- }
242
- }
243
- });
244
-
245
- return [
246
- `path: ${resolvedPath}`,
247
- `sizeBytes: ${stat.size}`,
248
- `bytesScanned: ${scan.bytesScanned}`,
249
- `linesScanned: ${scan.lineNumber}`,
250
- `parsedJsonLines: ${parsedLines}`,
251
- `invalidJsonLines: ${invalidJsonLines}`,
252
- `truncatedByScanLimit: ${scan.truncatedByScanLimit}`,
253
- "",
254
- ...[...fieldCounts.entries()].map(([field, map]) => renderTopMap(`top ${field}:`, map)),
255
- "",
256
- "errorSamples:",
257
- errorSamples.length ? errorSamples.join("\n") : "(none)"
258
- ].join("\n");
259
- }
260
-
261
- async function fetchWithTimeout(url, options = {}, timeoutMs = 15_000) {
262
- const controller = new AbortController();
263
- const timer = setTimeout(() => controller.abort(), timeoutMs);
264
- const startedAt = Date.now();
265
- try {
266
- const response = await fetch(url, { ...options, signal: controller.signal });
267
- const durationMs = Date.now() - startedAt;
268
- return { response, durationMs };
269
- } finally {
270
- clearTimeout(timer);
271
- }
272
- }
273
-
274
- function selectedHeaders(headers, names = []) {
275
- const selected = {};
276
- const wanted = names.length ? names : ["content-type", "cache-control", "location", "server", "x-request-id"];
277
- for (const name of wanted) {
278
- const value = headers.get(name);
279
- if (value !== null) {
280
- selected[name.toLowerCase()] = value;
281
- }
282
- }
283
- return selected;
284
- }
285
-
286
- async function responseSummary(url, input = {}) {
287
- const method = input.method ?? "GET";
288
- const { response, durationMs } = await fetchWithTimeout(url, { method }, input.timeoutMs ?? 15_000);
289
- const body = method === "HEAD" ? "" : await response.text();
290
- const bodyPreviewLimit = input.bodyPreviewChars ?? 2_000;
291
- const bodyHash = createHash("sha256").update(body).digest("hex");
292
- let json;
293
- try {
294
- json = body ? JSON.parse(body) : undefined;
295
- } catch {
296
- json = undefined;
297
- }
298
-
299
- return {
300
- bodyHash,
301
- bodyPreview: body.slice(0, bodyPreviewLimit),
302
- durationMs,
303
- headers: selectedHeaders(response.headers, input.headers),
304
- json,
305
- status: response.status,
306
- statusText: response.statusText,
307
- url: response.url
308
- };
309
- }
310
-
311
- export async function httpProbe(input) {
312
- const summary = await responseSummary(input.url, input);
313
- return JSON.stringify(summary, null, 2);
314
- }
315
-
316
- function walkManifests(value, visit, path = []) {
317
- if (!value || typeof value !== "object") {
318
- return;
319
- }
320
- visit(value, path);
321
- if (Array.isArray(value)) {
322
- value.forEach((item, index) => walkManifests(item, visit, [...path, String(index)]));
323
- } else {
324
- Object.entries(value).forEach(([key, item]) => walkManifests(item, visit, [...path, key]));
325
- }
326
- }
327
-
328
- export async function kubeManifestLintReadonly(input) {
329
- const { allowSensitive = false, filePath } = input;
330
- const { resolvedPath } = await assertReadableFile(filePath, allowSensitive);
331
- const text = await fs.readFile(resolvedPath, "utf8");
332
- const docs = parseAllDocuments(text).map((doc) => doc.toJSON()).filter(Boolean);
333
- const findings = [];
334
-
335
- for (const doc of docs) {
336
- const kind = doc.kind ?? "Unknown";
337
- const name = doc.metadata?.name ?? "unnamed";
338
- walkManifests(doc, (node, path) => {
339
- const pathText = path.join(".");
340
- if (node.image && typeof node.image === "string" && /:latest$|^[^:]+$/.test(node.image)) {
341
- findings.push(`[image] ${kind}/${name} ${pathText}.image uses an unpinned or latest tag: ${node.image}`);
342
- }
343
- if (node.privileged === true) {
344
- findings.push(`[security] ${kind}/${name} ${pathText}.privileged is true`);
345
- }
346
- if (node.hostPath) {
347
- findings.push(`[security] ${kind}/${name} ${pathText}.hostPath is used`);
348
- }
349
- if (node.containers && Array.isArray(node.containers)) {
350
- for (const container of node.containers) {
351
- const containerName = container.name ?? "unnamed-container";
352
- if (!container.resources) {
353
- findings.push(`[resources] ${kind}/${name} container ${containerName} has no resources block`);
354
- }
355
- if (!container.livenessProbe && !container.readinessProbe && kind !== "Job" && kind !== "CronJob") {
356
- findings.push(`[probes] ${kind}/${name} container ${containerName} has no liveness/readiness probe`);
357
- }
358
- if (!container.securityContext && !doc.spec?.template?.spec?.securityContext) {
359
- findings.push(`[security] ${kind}/${name} container ${containerName} has no securityContext`);
360
- }
361
- }
362
- }
363
- });
364
- }
365
-
366
- return [
367
- `path: ${resolvedPath}`,
368
- `documents: ${docs.length}`,
369
- `findings: ${findings.length}`,
370
- "",
371
- findings.length ? findings.join("\n") : "No common manifest hygiene findings."
372
- ].join("\n");
373
- }
374
-
375
- export async function ciLogSummary(input) {
376
- const { allowSensitive = false, filePath, scanLimitBytes = DEFAULT_SCAN_LIMIT_BYTES } = input;
377
- const { resolvedPath, stat } = await assertReadableFile(filePath, allowSensitive);
378
- const errorRegex = /\b(error|failed|failure|exception|traceback|exited with code|npm ERR!|fatal:)\b/i;
379
- const stepRegex = /(?:^|\s)(?:##\[group\]|##\[section\]|Step \d+\/\d+|Running step|Run )(.{1,160})/i;
380
- const errors = [];
381
- const stepCounts = new Map();
382
- let currentStep = "unknown";
383
-
384
- const scan = await streamLines(resolvedPath, scanLimitBytes, (line, lineNumber) => {
385
- const stepMatch = line.match(stepRegex);
386
- if (stepMatch?.[1]) {
387
- currentStep = stepMatch[1].trim();
388
- increment(stepCounts, currentStep);
389
- }
390
- if (errorRegex.test(line) && errors.length < 80) {
391
- errors.push(`${lineNumber} [${currentStep}]: ${line}`);
392
- }
393
- });
394
-
395
- return [
396
- `path: ${resolvedPath}`,
397
- `sizeBytes: ${stat.size}`,
398
- `bytesScanned: ${scan.bytesScanned}`,
399
- `linesScanned: ${scan.lineNumber}`,
400
- `truncatedByScanLimit: ${scan.truncatedByScanLimit}`,
401
- "",
402
- renderTopMap("topStepsSeen:", stepCounts, 15),
403
- "",
404
- "failureLines:",
405
- errors.length ? errors.join("\n") : "(none)"
406
- ].join("\n");
407
- }
408
-
409
- function diffObject(left, right) {
410
- const keys = [...new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})])].sort();
411
- return keys
412
- .filter((key) => left?.[key] !== right?.[key])
413
- .map((key) => `- ${key}: left=${JSON.stringify(left?.[key])} right=${JSON.stringify(right?.[key])}`);
414
- }
415
-
416
- export async function endpointDiff(input) {
417
- const left = await responseSummary(input.leftUrl, input);
418
- const right = await responseSummary(input.rightUrl, input);
419
- const jsonFields = input.jsonFields ?? [];
420
- const leftJson = Object.fromEntries(jsonFields.map((field) => [field, getPathValue(left.json, field)]));
421
- const rightJson = Object.fromEntries(jsonFields.map((field) => [field, getPathValue(right.json, field)]));
422
-
423
- return [
424
- `leftUrl: ${left.url}`,
425
- `rightUrl: ${right.url}`,
426
- `leftStatus: ${left.status} ${left.statusText}`,
427
- `rightStatus: ${right.status} ${right.statusText}`,
428
- `leftDurationMs: ${left.durationMs}`,
429
- `rightDurationMs: ${right.durationMs}`,
430
- `leftBodySha256: ${left.bodyHash}`,
431
- `rightBodySha256: ${right.bodyHash}`,
432
- `bodyHashesEqual: ${left.bodyHash === right.bodyHash}`,
433
- "",
434
- "headerDiffs:",
435
- diffObject(left.headers, right.headers).join("\n") || "(none)",
436
- "",
437
- "jsonFieldDiffs:",
438
- diffObject(leftJson, rightJson).join("\n") || "(none)"
439
- ].join("\n");
440
- }
@@ -1,180 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import { assertNonHtmlApiResponse } from "./devops-diagnostics.mjs";
4
-
5
- const execFileAsync = promisify(execFile);
6
-
7
- export function normalizeBaseUrl(value) {
8
- const raw = String(value || "").trim();
9
- if (!raw) return "";
10
- try {
11
- const url = new URL(raw);
12
- url.pathname = url.pathname.replace(/\/+$/, "");
13
- url.search = "";
14
- url.hash = "";
15
- return url.toString().replace(/\/+$/, "");
16
- } catch {
17
- return "";
18
- }
19
- }
20
-
21
- export function appendQueryValue(searchParams, key, value) {
22
- if (Array.isArray(value)) {
23
- for (const item of value) appendQueryValue(searchParams, key, item);
24
- return;
25
- }
26
- if (value === undefined || value === null) return;
27
- searchParams.append(key, String(value));
28
- }
29
-
30
- export function buildRequestUrl(baseUrl, requestPath, query) {
31
- const url = new URL(`${baseUrl}${requestPath}`);
32
- for (const [key, value] of Object.entries(query || {})) {
33
- if (key) appendQueryValue(url.searchParams, key, value);
34
- }
35
- return url;
36
- }
37
-
38
- export function extractTokenFromKeychainSecret(rawSecret = "") {
39
- const text = typeof rawSecret === "string" ? rawSecret.trim() : "";
40
- if (!text) return "";
41
- try {
42
- const parsed = JSON.parse(text);
43
- return typeof parsed?.token === "string" ? parsed.token.trim() : "";
44
- } catch {
45
- return text;
46
- }
47
- }
48
-
49
- export async function readTokenFromKeychain({ execFileImpl = execFileAsync, keychainService, keychainAccount }) {
50
- try {
51
- const result = await execFileImpl("/usr/bin/security", [
52
- "find-generic-password",
53
- "-s",
54
- keychainService,
55
- "-a",
56
- keychainAccount,
57
- "-w"
58
- ], { maxBuffer: 1024 * 1024 });
59
- return extractTokenFromKeychainSecret(String(result?.stdout || ""));
60
- } catch {
61
- return "";
62
- }
63
- }
64
-
65
- export function parseResponseBody(text) {
66
- try {
67
- return JSON.parse(text);
68
- } catch {
69
- return String(text || "");
70
- }
71
- }
72
-
73
- export class KeychainBearerAuthProvider {
74
- constructor(options = {}) {
75
- this.mode = "keychain";
76
- this.execFileImpl = options.execFileImpl || execFileAsync;
77
- this.keychainService = options.keychainService;
78
- this.keychainAccount = options.keychainAccount;
79
- this.authorizationHeader = options.authorizationHeader || ((token) => `Bearer ${token}`);
80
- }
81
-
82
- async buildHeaders() {
83
- const token = await readTokenFromKeychain({
84
- execFileImpl: this.execFileImpl,
85
- keychainService: this.keychainService,
86
- keychainAccount: this.keychainAccount
87
- });
88
- if (!token) {
89
- throw new Error(
90
- `Keychain token missing for ${this.keychainService}/${this.keychainAccount}. Save the personal access token in Keychain.`
91
- );
92
- }
93
- return { authorization: this.authorizationHeader(token) };
94
- }
95
- }
96
-
97
- /** Cookie SSO auth — inject `resolveCookieHeader(input)` from @nuvlore/extension-browser-sso. */
98
- export class CookieSsoAuthProvider {
99
- constructor(options = {}) {
100
- this.mode = "cookie-sso";
101
- this.resolveCookieHeader = options.resolveCookieHeader;
102
- if (typeof this.resolveCookieHeader !== "function") {
103
- throw new Error("CookieSsoAuthProvider requires resolveCookieHeader(input) async function.");
104
- }
105
- }
106
-
107
- async buildHeaders(input = {}) {
108
- const cookie = await this.resolveCookieHeader(input);
109
- if (!cookie) {
110
- throw new Error("Browser SSO cookie header is missing. Open the service in Chrome via Okta and retry.");
111
- }
112
- return { cookie };
113
- }
114
- }
115
-
116
- export class EnterpriseApiReadService {
117
- constructor(options = {}) {
118
- this.fetchImpl = options.fetchImpl || globalThis.fetch;
119
- this.execFileImpl = options.execFileImpl || execFileAsync;
120
- this.serviceName = options.serviceName;
121
- this.defaultBaseUrl = options.defaultBaseUrl;
122
- this.defaultUserAgent = options.defaultUserAgent;
123
- this.keychainService = options.keychainService;
124
- this.keychainAccount = options.keychainAccount;
125
- this.normalizePath = options.normalizePath;
126
- this.accept = options.accept || "application/json";
127
- this.extraHeaders = options.extraHeaders || {};
128
- this.authorizationHeader = options.authorizationHeader || ((token) => `Bearer ${token}`);
129
- this.formatErrorPayload = options.formatErrorPayload;
130
- this.includeMethodInPayload = options.includeMethodInPayload === true;
131
- this.transformPayload = options.transformPayload || ((payload) => payload);
132
- this.authProvider =
133
- options.authProvider ??
134
- new KeychainBearerAuthProvider({
135
- execFileImpl: this.execFileImpl,
136
- keychainService: this.keychainService,
137
- keychainAccount: this.keychainAccount,
138
- authorizationHeader: this.authorizationHeader
139
- });
140
- }
141
-
142
- async invoke(input = {}) {
143
- if (typeof this.fetchImpl !== "function") {
144
- throw new Error(`Fetch API is unavailable for ${this.serviceName} requests.`);
145
- }
146
- const baseUrl = normalizeBaseUrl(input.baseUrl || this.defaultBaseUrl);
147
- if (!baseUrl) {
148
- throw new Error(`${this.serviceName} base URL is missing or invalid. Expected something like ${this.defaultBaseUrl}.`);
149
- }
150
- const requestPath = this.normalizePath(input.restPath || input.path);
151
- const url = buildRequestUrl(baseUrl, requestPath, input.query);
152
- const authHeaders = await this.authProvider.buildHeaders(input);
153
- const response = await this.fetchImpl(url, {
154
- method: "GET",
155
- headers: {
156
- accept: this.accept,
157
- "user-agent": this.defaultUserAgent,
158
- ...this.extraHeaders,
159
- ...authHeaders,
160
- ...(input.headers ?? {})
161
- }
162
- });
163
- const text = await response.text();
164
- assertNonHtmlApiResponse(this.serviceName, text);
165
- const payload = parseResponseBody(text);
166
- if (!response.ok) {
167
- throw new Error(`${this.serviceName} request failed with status ${response.status}: ${this.formatErrorPayload(payload)}.`);
168
- }
169
- const output = {
170
- baseUrl,
171
- restPath: requestPath,
172
- path: requestPath,
173
- url: url.toString(),
174
- status: response.status,
175
- data: this.transformPayload(payload)
176
- };
177
- if (this.includeMethodInPayload) output.method = "GET";
178
- return JSON.stringify(output, null, 2);
179
- }
180
- }
@@ -1 +0,0 @@
1
- export * from "./read-only-plan.mjs";
@@ -1,45 +0,0 @@
1
- export function shellQuote(value) {
2
- return `'${String(value).replaceAll("'", "'\\''")}'`;
3
- }
4
-
5
- export function remoteCommandInput({ host, command, timeoutMs = 60_000 }) {
6
- return {
7
- targetType: "ssh",
8
- host,
9
- command,
10
- timeoutMs
11
- };
12
- }
13
-
14
- export class ReadOnlyStep {
15
- constructor({ id, purpose, command, expectedEvidence }) {
16
- this.id = id;
17
- this.purpose = purpose;
18
- this.command = command;
19
- this.expectedEvidence = expectedEvidence;
20
- }
21
-
22
- toMarkdown() {
23
- return [
24
- `### ${this.id}`,
25
- `Purpose: ${this.purpose}`,
26
- "",
27
- "```bash",
28
- this.command,
29
- "```",
30
- "",
31
- `Evidence to capture: ${this.expectedEvidence}`
32
- ].join("\n");
33
- }
34
-
35
- toJson() {
36
- return {
37
- id: this.id,
38
- purpose: this.purpose,
39
- command: this.command,
40
- expectedEvidence: this.expectedEvidence
41
- };
42
- }
43
- }
44
-
45
- export class ReadOnlyCommand extends ReadOnlyStep {}
@@ -1,26 +0,0 @@
1
- export type ReadOnlyValidation = {
2
- allowed: boolean;
3
- reason: string;
4
- };
5
-
6
- export type ReadOnlyPolicyConfig = {
7
- extraReadOnlyCommandPatterns?: RegExp[];
8
- };
9
-
10
- export const READ_ONLY_ALLOWED_TOOLS: readonly string[];
11
- export const READ_ONLY_DISALLOWED_TOOLS: readonly string[];
12
- export const MUTATING_SHELL_TOKENS: RegExp[];
13
- export const READ_ONLY_COMMAND_PATTERNS: RegExp[];
14
-
15
- export class ReadOnlyShellPolicy {
16
- constructor(config?: ReadOnlyPolicyConfig);
17
- validate(command: string): ReadOnlyValidation;
18
- readOnlyPatterns(): RegExp[];
19
- }
20
-
21
- export class ReadOnlyPolicyDescription {
22
- text(): string;
23
- }
24
-
25
- export function isReadOnlyShellCommand(command: string, config?: ReadOnlyPolicyConfig): ReadOnlyValidation;
26
- export function describeReadOnlyPolicy(): string;