@nuvlore/extension-bds 0.1.1

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 (109) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +3 -0
  3. package/agents/bds-airflow-job-status-readonly.yaml +123 -0
  4. package/agents/bds-disk-investigator.yaml +112 -0
  5. package/agents/bds-inventory-readonly.yaml +60 -0
  6. package/agents/bds-oncall-reporter.yaml +113 -0
  7. package/agents/bds-prism-service-proxy-timeout-readonly.yaml +105 -0
  8. package/agents/bds-scylla-tenant-bucket-readonly.yaml +104 -0
  9. package/agents/bds-tenant-data-operation-log-readonly.yaml +98 -0
  10. package/agents/bds-yarn-app-health-readonly.yaml +114 -0
  11. package/agents/pharos-alert-investigator.yaml +81 -0
  12. package/package.json +60 -0
  13. package/skills/bds-airflow-job-status-investigation/SKILL.md +104 -0
  14. package/skills/bds-airflow-job-status-investigation/references/airflow-access-runbook.md +35 -0
  15. package/skills/bds-airflow-job-status-investigation/references/airflow-bas-dr-runbook.md +42 -0
  16. package/skills/bds-airflow-job-status-investigation/references/airflow-copy-and-migration-runbook.md +63 -0
  17. package/skills/bds-airflow-job-status-investigation/references/airflow-job-status-runbook.md +15 -0
  18. package/skills/bds-airflow-job-status-investigation/references/airflow-output-runbook.md +28 -0
  19. package/skills/bds-airflow-job-status-investigation/references/airflow-snapshot-runbook.md +38 -0
  20. package/skills/bds-disk-io-alert-investigation/SKILL.md +116 -0
  21. package/skills/bds-hadoop-review/SKILL.md +66 -0
  22. package/skills/bds-machine-inventory/SKILL.md +48 -0
  23. package/skills/bds-oncall-followup/SKILL.md +71 -0
  24. package/skills/bds-oncall-followup/references/oncall-followup-runbook.md +58 -0
  25. package/skills/bds-prism-service-proxy-timeout-investigation/SKILL.md +73 -0
  26. package/skills/bds-prism-service-proxy-timeout-investigation/references/service-proxy-timeout-runbook.md +48 -0
  27. package/skills/bds-scylla-tenant-bucket-investigation/SKILL.md +73 -0
  28. package/skills/bds-scylla-tenant-bucket-investigation/references/tenant-bucket-runbook.md +91 -0
  29. package/skills/bds-tenant-data-operation-log-investigation/SKILL.md +89 -0
  30. package/skills/bds-yarn-app-health-investigation/SKILL.md +77 -0
  31. package/skills/bds-yarn-app-health-investigation/references/yarn-app-health-runbook.md +53 -0
  32. package/skills/pharos-alert-investigation/SKILL.md +80 -0
  33. package/src/bds-atomic-log-tools.mjs +300 -0
  34. package/src/bds-team-membership-claude-output.mjs +36 -0
  35. package/src/bds-team-membership-quality.mjs +90 -0
  36. package/tools/bds_airflow_dag_log_read.mjs +24 -0
  37. package/tools/bds_airflow_job_status_command_pack.mjs +587 -0
  38. package/tools/bds_bas_log_read.mjs +23 -0
  39. package/tools/bds_bdm_httpd_log_read.mjs +24 -0
  40. package/tools/bds_chef_client_log_read.mjs +23 -0
  41. package/tools/bds_dashboard_links.mjs +178 -0
  42. package/tools/bds_dashboard_query_knowledge.mjs +258 -0
  43. package/tools/bds_dashboard_query_read.mjs +120 -0
  44. package/tools/bds_disk_io_alert_command_pack.mjs +287 -0
  45. package/tools/bds_environment_host_lookup.mjs +741 -0
  46. package/tools/bds_hdfs_datanode_log_read.mjs +23 -0
  47. package/tools/bds_hdfs_journalnode_log_read.mjs +23 -0
  48. package/tools/bds_hdfs_namenode_log_read.mjs +23 -0
  49. package/tools/bds_hdfs_path_knowledge.mjs +324 -0
  50. package/tools/bds_information_source_knowledge.mjs +356 -0
  51. package/tools/bds_kms_log_read.mjs +23 -0
  52. package/tools/bds_log_path_knowledge.mjs +267 -0
  53. package/tools/bds_machine_inventory.mjs +434 -0
  54. package/tools/bds_mapreduce_historyserver_log_read.mjs +23 -0
  55. package/tools/bds_migration_service_operation_command_pack.mjs +233 -0
  56. package/tools/bds_oncall_followup_payload_draft.mjs +285 -0
  57. package/tools/bds_pharos_query_pack.mjs +218 -0
  58. package/tools/bds_portal_operation_knowledge.mjs +212 -0
  59. package/tools/bds_prism_clone_status_links.mjs +129 -0
  60. package/tools/bds_prism_service_proxy_timeout_query_pack.mjs +224 -0
  61. package/tools/bds_public_cloud_pod_log_read.mjs +18 -0
  62. package/tools/bds_scylla_tenant_bucket_command_pack.mjs +323 -0
  63. package/tools/bds_service_proxy_log_read.mjs +23 -0
  64. package/tools/bds_spark_historyserver_log_read.mjs +32 -0
  65. package/tools/bds_spas_mt_log_read.mjs +23 -0
  66. package/tools/bds_tenant_data_operation_log_command_pack.mjs +321 -0
  67. package/tools/bds_yarn_app_health_command_pack.mjs +529 -0
  68. package/tools/bds_yarn_nodemanager_log_read.mjs +23 -0
  69. package/tools/bds_yarn_resourcemanager_log_read.mjs +23 -0
  70. package/tools/bds_zookeeper_log_read.mjs +23 -0
  71. package/tools/confluence_handoff_page_draft.mjs +173 -0
  72. package/vendor/dashboard/src/grafana-dashboard-query.mjs +61 -0
  73. package/vendor/extension-read-only-toolkit/LICENSE +202 -0
  74. package/vendor/extension-read-only-toolkit/README.md +29 -0
  75. package/vendor/extension-read-only-toolkit/package.json +46 -0
  76. package/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
  77. package/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
  78. package/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
  79. package/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
  80. package/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
  81. package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
  82. package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
  83. package/vendor/extension-workday-infra-access/LICENSE +202 -0
  84. package/vendor/extension-workday-infra-access/README.md +3 -0
  85. package/vendor/extension-workday-infra-access/agents/workday-infra-access-readonly.yaml +183 -0
  86. package/vendor/extension-workday-infra-access/package.json +49 -0
  87. package/vendor/extension-workday-infra-access/skills/workday-infra-access/SKILL.md +102 -0
  88. package/vendor/extension-workday-infra-access/skills/workday-infra-access/references/access-boundary-runbook.md +43 -0
  89. package/vendor/extension-workday-infra-access/skills/workday-infra-access/references/access-runbook.md +15 -0
  90. package/vendor/extension-workday-infra-access/skills/workday-infra-access/references/access-scylla-cloud-runbook.md +39 -0
  91. package/vendor/extension-workday-infra-access/skills/workday-infra-access/references/access-spc-argo-runbook.md +99 -0
  92. package/vendor/extension-workday-infra-access/src/workday-access-profiles.mjs +194 -0
  93. package/vendor/extension-workday-infra-access/tools/aws_cust_federated_readonly_command_pack.mjs +234 -0
  94. package/vendor/extension-workday-infra-access/tools/gcp_readonly_command_pack.mjs +167 -0
  95. package/vendor/extension-workday-infra-access/tools/public_cloud_access_status.mjs +48 -0
  96. package/vendor/extension-workday-infra-access/tools/workday_boundary_login.mjs +460 -0
  97. package/vendor/extension-workday-infra-access/tools/workday_private_cloud_sudo_readonly_command.mjs +386 -0
  98. package/vendor/extension-workday-infra-access/tools/workday_public_cloud_environment_map.mjs +189 -0
  99. package/vendor/extension-workday-infra-access/tools/workday_public_cloud_kubernetes_read.mjs +297 -0
  100. package/vendor/extension-workday-infra-access/tools/workday_public_cloud_spc_login.mjs +505 -0
  101. package/vendor/extension-workday-infra-access/tools/workday_spc_argo_readonly_command_pack.mjs +215 -0
  102. package/vendor/extension-workday-infra-access/workflows/public-cloud-readonly-investigation.js +99 -0
  103. package/workflows/bds-airflow-job-status-investigation.js +168 -0
  104. package/workflows/bds-disk-io-alert-investigation.js +173 -0
  105. package/workflows/bds-oncall-followup.js +112 -0
  106. package/workflows/bds-prism-service-proxy-timeout-investigation.js +69 -0
  107. package/workflows/bds-scylla-tenant-bucket-investigation.js +139 -0
  108. package/workflows/bds-tenant-data-operation-log-investigation.js +189 -0
  109. package/workflows/bds-yarn-app-health-investigation.js +217 -0
@@ -0,0 +1,300 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { z } from "zod";
4
+ import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
5
+ import { WorkdayPrivateCloudSudoReadonlyCommandService } from "@nuvlore/extension-workday-infra-access/tools/workday_private_cloud_sudo_readonly_command.mjs";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ const DEFAULT_TIMEOUT_MS = 120_000;
10
+ const DEFAULT_KUBECONFIG = "/Users/boqiang.liang/.kube/spc-config";
11
+
12
+ function normalizeText(value) {
13
+ return typeof value === "string" ? value.trim() : "";
14
+ }
15
+
16
+ function normalizeLines(value) {
17
+ return Number.isInteger(value) ? Math.min(Math.max(value, 1), 5000) : 100;
18
+ }
19
+
20
+ function shellSingleQuote(value) {
21
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
22
+ }
23
+
24
+ function safeShellToken(value, label) {
25
+ const normalized = normalizeText(value);
26
+ if (!/^[A-Za-z0-9._:/=@%-]+$/u.test(normalized)) {
27
+ throw new Error(`Refusing unsafe ${label}: ${normalized}`);
28
+ }
29
+ return normalized;
30
+ }
31
+
32
+ function validatePrivatePath(path, allowedPrefixes) {
33
+ const normalized = normalizeText(path);
34
+ if (!normalized) return "";
35
+ if (!allowedPrefixes.some((prefix) => normalized.startsWith(prefix))) {
36
+ throw new Error(`Refusing log path outside allowed prefixes: ${normalized}`);
37
+ }
38
+ if (/[;&|`$<>]/u.test(normalized)) {
39
+ throw new Error(`Refusing unsafe log path: ${normalized}`);
40
+ }
41
+ return normalized;
42
+ }
43
+
44
+ function tailCommand(path, lines) {
45
+ return `tail -n ${normalizeLines(lines)} ${path}`;
46
+ }
47
+
48
+ function structuredApproval(input, toolName, host) {
49
+ return input.hitlApproval ?? {
50
+ approved: true,
51
+ approvedBy: "operator-requested",
52
+ action: toolName,
53
+ target: host,
54
+ reason: "Read BDS service log for investigation.",
55
+ expectedImpact: "Runs bauth init and a single read-only sudo log command.",
56
+ risk: "May reveal operational log content; no remote writes are requested."
57
+ };
58
+ }
59
+
60
+ export class BdsPrivateCloudAtomicLogReadService {
61
+ constructor(spec, options = {}) {
62
+ this.spec = spec;
63
+ this.privateSudoService = options.privateSudoService || new WorkdayPrivateCloudSudoReadonlyCommandService(options.privateSudoOptions);
64
+ }
65
+
66
+ buildCommand(input) {
67
+ const normalized = {
68
+ ...input,
69
+ lines: normalizeLines(input.lines)
70
+ };
71
+ return this.spec.buildCommand(normalized);
72
+ }
73
+
74
+ async invoke(input) {
75
+ const host = normalizeText(input.host);
76
+ if (!host) throw new Error(`${this.spec.toolName} requires host.`);
77
+ const action = normalizeText(input.action) || "plan";
78
+ const command = this.buildCommand(input);
79
+ return this.privateSudoService.invoke({
80
+ action,
81
+ host,
82
+ command,
83
+ proxyJumpHost: normalizeText(input.proxyJumpHost) || undefined,
84
+ connectTimeoutSeconds: input.connectTimeoutSeconds,
85
+ timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
86
+ humanApprovedSudoRead: input.humanApprovedSudoRead === true,
87
+ hitlApproval: action === "run" ? structuredApproval(input, this.spec.toolName, host) : input.hitlApproval
88
+ });
89
+ }
90
+ }
91
+
92
+ export function createPrivateCloudLogReadTool(spec) {
93
+ const defaultService = new BdsPrivateCloudAtomicLogReadService(spec);
94
+ return defineReadOnlyTool({
95
+ name: spec.toolName,
96
+ description: spec.description,
97
+ meta: {
98
+ permissions: { rule: "ask" },
99
+ delegatesSecretAccessTo: "workday_private_cloud_sudo_readonly_command"
100
+ },
101
+ inputSchema: {
102
+ action: z.enum(["plan", "run"]).default("plan"),
103
+ host: z.string().trim().min(1).max(253),
104
+ lines: z.number().int().positive().max(5000).default(100),
105
+ proxyJumpHost: z.string().trim().max(253).optional(),
106
+ connectTimeoutSeconds: z.number().int().positive().max(120).default(20),
107
+ humanApprovedSudoRead: z.boolean().default(false),
108
+ hitlApproval: z.object({
109
+ approved: z.literal(true),
110
+ approvedBy: z.string().trim().min(1).max(256),
111
+ action: z.string().trim().min(1).max(256),
112
+ target: z.string().trim().min(1).max(4096),
113
+ reason: z.string().trim().min(1).max(2000),
114
+ expectedImpact: z.string().trim().min(1).max(2000),
115
+ risk: z.string().trim().min(1).max(2000)
116
+ }).optional(),
117
+ timeoutMs: z.number().int().positive().max(600_000).default(DEFAULT_TIMEOUT_MS),
118
+ ...spec.extraSchema
119
+ },
120
+ async handler(input) {
121
+ try {
122
+ return ok(await defaultService.invoke(input));
123
+ } catch (error) {
124
+ return fail(error);
125
+ }
126
+ }
127
+ });
128
+ }
129
+
130
+ export function fixedPrivateLogSpec({ toolName, description, path }) {
131
+ return {
132
+ toolName,
133
+ description,
134
+ buildCommand: (input) => tailCommand(path, input.lines)
135
+ };
136
+ }
137
+
138
+ export function globPrivateLogSpec({ toolName, description, glob }) {
139
+ return fixedPrivateLogSpec({ toolName, description, path: glob });
140
+ }
141
+
142
+ export const BDM_HTTPD_EXTRA_SCHEMA = {
143
+ logName: z.enum(["error", "access"]).default("error")
144
+ };
145
+
146
+ export function bdmHttpdCommand(input) {
147
+ return tailCommand(`/var/log/httpd/${input.logName === "access" ? "access" : "error"}.log`, input.lines);
148
+ }
149
+
150
+ export const AIRFLOW_DAG_EXTRA_SCHEMA = {
151
+ logFile: z.string().trim().max(4096).default(""),
152
+ dagId: z.string().trim().max(256).default(""),
153
+ taskId: z.string().trim().max(256).default(""),
154
+ date: z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/u).optional()
155
+ };
156
+
157
+ export function airflowDagCommand(input) {
158
+ const allowed = ["/hadoop/disk2/workday-bds-airflow/logs/", "/data/workday-bds-airflow/logs/"];
159
+ const logFile = validatePrivatePath(input.logFile, allowed);
160
+ if (logFile) return tailCommand(shellSingleQuote(logFile), input.lines);
161
+ const dagPattern = normalizeText(input.dagId) || "dag_tenant_data";
162
+ const taskPattern = normalizeText(input.taskId);
163
+ const datePath = normalizeText(input.date) ? `/${normalizeText(input.date)}/` : "";
164
+ const pathPattern = taskPattern
165
+ ? `*${datePath}*${dagPattern}*${taskPattern}*`
166
+ : `*${datePath}*${dagPattern}*`;
167
+ return `find /hadoop/disk2/workday-bds-airflow/logs /data/workday-bds-airflow/logs -maxdepth 8 -type f -path ${shellSingleQuote(pathPattern)} -print`;
168
+ }
169
+
170
+ export const PRIVATE_K8S_LOG_EXTRA_SCHEMA = {
171
+ namespace: z.string().trim().min(1).max(253).default("bds-k8s-services"),
172
+ pod: z.string().trim().max(253).default(""),
173
+ deployment: z.string().trim().max(253).default(""),
174
+ statefulset: z.string().trim().max(253).default(""),
175
+ container: z.string().trim().max(253).default(""),
176
+ previous: z.boolean().default(false),
177
+ allPods: z.boolean().default(true)
178
+ };
179
+
180
+ function privateK8sTarget(input, defaultDeployment) {
181
+ if (normalizeText(input.pod)) return { target: safeShellToken(input.pod, "pod"), resourceTarget: false };
182
+ if (normalizeText(input.deployment)) return { target: `deploy/${safeShellToken(input.deployment, "deployment")}`, resourceTarget: true };
183
+ if (normalizeText(input.statefulset)) return { target: `statefulset/${safeShellToken(input.statefulset, "statefulset")}`, resourceTarget: true };
184
+ if (defaultDeployment) return { target: `deploy/${safeShellToken(defaultDeployment, "deployment")}`, resourceTarget: true };
185
+ throw new Error("private Kubernetes log read requires pod, deployment, or statefulset.");
186
+ }
187
+
188
+ export function privateK8sLogCommand(input, defaults = {}) {
189
+ const namespace = safeShellToken(normalizeText(input.namespace) || defaults.namespace || "bds-k8s-services", "namespace");
190
+ const { target, resourceTarget } = privateK8sTarget(input, defaults.deployment);
191
+ const args = ["kubectl", "logs", "-n", namespace, target, "--tail", String(normalizeLines(input.lines))];
192
+ const container = normalizeText(input.container) || defaults.container;
193
+ if (container) args.push("-c", container);
194
+ if (resourceTarget && input.allPods !== false) args.push("--all-pods=true");
195
+ if (input.previous === true) args.push("--previous");
196
+ return args.map((arg) => safeShellToken(arg, "kubectl argument")).join(" ");
197
+ }
198
+
199
+ export const PUBLIC_CLOUD_POD_EXTRA_SCHEMA = {
200
+ namespace: z.string().trim().min(1).max(253).default("bds"),
201
+ pod: z.string().trim().max(253).default(""),
202
+ deployment: z.string().trim().max(253).default(""),
203
+ statefulset: z.string().trim().max(253).default(""),
204
+ container: z.string().trim().max(253).default(""),
205
+ previous: z.boolean().default(false),
206
+ allPods: z.boolean().default(true),
207
+ kubeconfig: z.string().trim().max(4096).default(DEFAULT_KUBECONFIG)
208
+ };
209
+
210
+ function kubectlTarget(input) {
211
+ if (normalizeText(input.pod)) return { target: normalizeText(input.pod), resourceTarget: false };
212
+ if (normalizeText(input.deployment)) return { target: `deploy/${normalizeText(input.deployment)}`, resourceTarget: true };
213
+ if (normalizeText(input.statefulset)) return { target: `statefulset/${normalizeText(input.statefulset)}`, resourceTarget: true };
214
+ throw new Error("public cloud pod log read requires pod, deployment, or statefulset.");
215
+ }
216
+
217
+ export class BdsPublicCloudPodLogReadService {
218
+ constructor(options = {}) {
219
+ this.execFileImpl = options.execFileImpl || execFileAsync;
220
+ }
221
+
222
+ buildArgs(input) {
223
+ const { target, resourceTarget } = kubectlTarget(input);
224
+ const args = ["--kubeconfig", normalizeText(input.kubeconfig) || DEFAULT_KUBECONFIG, "logs", "-n", normalizeText(input.namespace) || "bds", target, "--tail", String(normalizeLines(input.lines))];
225
+ const container = normalizeText(input.container);
226
+ if (container) args.push("-c", container);
227
+ if (resourceTarget && input.allPods !== false) args.push("--all-pods=true");
228
+ if (input.previous === true) args.push("--previous");
229
+ return args;
230
+ }
231
+
232
+ async invoke(input) {
233
+ const action = normalizeText(input.action) || "plan";
234
+ const args = this.buildArgs(input);
235
+ const command = ["kubectl", ...args.map(shellSingleQuote)].join(" ");
236
+ if (action === "plan") {
237
+ return JSON.stringify({
238
+ mode: "public-cloud-pod-log-read",
239
+ accessPlane: "public-cloud",
240
+ prerequisite: "Run workday_public_cloud_spc_login first; do not use k9s.",
241
+ command
242
+ }, null, 2);
243
+ }
244
+ if (action !== "run") throw new Error(`Unsupported action: ${action}`);
245
+ const result = await this.execFileImpl("kubectl", args, {
246
+ timeout: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
247
+ maxBuffer: 1024 * 1024 * 10
248
+ });
249
+ return JSON.stringify({
250
+ mode: "public-cloud-pod-log-read",
251
+ action,
252
+ command,
253
+ result: {
254
+ stdout: result.stdout,
255
+ stderr: result.stderr,
256
+ exitCode: 0
257
+ }
258
+ }, null, 2);
259
+ }
260
+ }
261
+
262
+ export function createPublicCloudPodLogReadTool({ toolName, description }) {
263
+ const defaultService = new BdsPublicCloudPodLogReadService();
264
+ return defineReadOnlyTool({
265
+ name: toolName,
266
+ description,
267
+ inputSchema: {
268
+ action: z.enum(["plan", "run"]).default("plan"),
269
+ lines: z.number().int().positive().max(5000).default(100),
270
+ timeoutMs: z.number().int().positive().max(600_000).default(DEFAULT_TIMEOUT_MS),
271
+ ...PUBLIC_CLOUD_POD_EXTRA_SCHEMA
272
+ },
273
+ async handler(input) {
274
+ try {
275
+ return ok(await defaultService.invoke(input));
276
+ } catch (error) {
277
+ return fail(error);
278
+ }
279
+ }
280
+ });
281
+ }
282
+
283
+ export const __test = {
284
+ AIRFLOW_DAG_EXTRA_SCHEMA,
285
+ BDM_HTTPD_EXTRA_SCHEMA,
286
+ BdsPrivateCloudAtomicLogReadService,
287
+ BdsPublicCloudPodLogReadService,
288
+ PUBLIC_CLOUD_POD_EXTRA_SCHEMA,
289
+ PRIVATE_K8S_LOG_EXTRA_SCHEMA,
290
+ airflowDagCommand,
291
+ bdmHttpdCommand,
292
+ fixedPrivateLogSpec,
293
+ globPrivateLogSpec,
294
+ normalizeLines,
295
+ privateK8sLogCommand,
296
+ safeShellToken,
297
+ shellSingleQuote,
298
+ tailCommand,
299
+ validatePrivatePath
300
+ };
@@ -0,0 +1,36 @@
1
+ import { validateBdsTeamMembershipAnswer } from "./bds-team-membership-quality.mjs";
2
+
3
+ export function extractClaudeFinalAnswer(output = "") {
4
+ const text = String(output || "").trim();
5
+ if (!text) return "";
6
+ try {
7
+ const parsed = JSON.parse(text);
8
+ return String(parsed?.result ?? parsed?.message?.content?.[0]?.text ?? parsed?.content ?? text);
9
+ } catch {
10
+ return text;
11
+ }
12
+ }
13
+
14
+ export function extractBdsTeamMembershipQualityContract(output = "") {
15
+ const text = String(output || "").trim();
16
+ if (!text) return {};
17
+ try {
18
+ const parsed = JSON.parse(text);
19
+ return parsed?.devopsQuality
20
+ ?? parsed?.evidenceContract
21
+ ?? parsed?.metadata?.devopsQuality
22
+ ?? {};
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ export function validateBdsTeamMembershipClaudeOutput(output = "") {
29
+ const answer = extractClaudeFinalAnswer(output);
30
+ const failures = validateBdsTeamMembershipAnswer(answer, extractBdsTeamMembershipQualityContract(output));
31
+ return {
32
+ ok: failures.length === 0,
33
+ decision: failures.length === 0 ? "pass" : "fail",
34
+ failures,
35
+ };
36
+ }
@@ -0,0 +1,90 @@
1
+ function normalizeText(value) {
2
+ return typeof value === "string" ? value.trim() : "";
3
+ }
4
+
5
+ function normalizeArray(value) {
6
+ return Array.isArray(value) ? value : [];
7
+ }
8
+
9
+ function normalizeObject(value) {
10
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
11
+ }
12
+
13
+ function hasText(value) {
14
+ return normalizeText(value) !== "";
15
+ }
16
+
17
+ function hasConfluenceLocator(item) {
18
+ const entry = normalizeObject(item);
19
+ return entry.sourceType === "confluence"
20
+ && [entry.url, entry.pageId, entry.restPath, entry.cql, entry.title].some(hasText);
21
+ }
22
+
23
+ function hasMemberEvidence(item) {
24
+ const entry = normalizeObject(item);
25
+ return hasText(entry.name)
26
+ && (
27
+ normalizeArray(entry.sourceRefs).some(hasText)
28
+ || normalizeArray(entry.evidence).some(hasText)
29
+ || hasText(entry.sourceRef)
30
+ );
31
+ }
32
+
33
+ const DISALLOWED_SOURCE_TYPES = new Set([
34
+ "launcher_status",
35
+ "local_code",
36
+ "onboarding",
37
+ "pagerduty",
38
+ "slack"
39
+ ]);
40
+
41
+ function disallowedSourceTypes(contract) {
42
+ return normalizeArray(contract.excludedSources)
43
+ .map((source) => normalizeText(source.type ?? source.sourceType ?? source))
44
+ .filter((sourceType) => DISALLOWED_SOURCE_TYPES.has(sourceType));
45
+ }
46
+
47
+ export class BdsTeamMembershipQualityPolicy {
48
+ validate(answer = "", evidenceContract = {}) {
49
+ const text = normalizeText(answer);
50
+ if (!text) return ["answer is empty"];
51
+
52
+ const contract = normalizeObject(evidenceContract);
53
+ const failures = [];
54
+ const outcome = normalizeText(contract.outcome);
55
+ const searches = normalizeArray(contract.evidenceSearches);
56
+ const sources = normalizeArray(contract.sources);
57
+ const checkedLocations = normalizeArray(contract.checkedLocations);
58
+
59
+ if (!["confluence_roster", "confluence_evidence_insufficient"].includes(outcome)) {
60
+ failures.push("BDS team membership answer must include structured devopsQuality.outcome");
61
+ }
62
+
63
+ if (![...searches, ...sources, ...checkedLocations].some(hasConfluenceLocator)) {
64
+ failures.push("BDS team membership answer must include structured Confluence evidence locations");
65
+ }
66
+
67
+ if (outcome === "confluence_roster" && !normalizeArray(contract.members).some(hasMemberEvidence)) {
68
+ failures.push("BDS team membership roster must include structured members with evidence references");
69
+ }
70
+
71
+ if (outcome === "confluence_evidence_insufficient" && !hasText(contract.evidenceGap)) {
72
+ failures.push("BDS team membership insufficient-evidence answer must include a structured evidenceGap");
73
+ }
74
+
75
+ const blockedSources = disallowedSourceTypes(contract);
76
+ if (blockedSources.length > 0) {
77
+ failures.push(`BDS team membership answer used disallowed source types: ${blockedSources.join(", ")}`);
78
+ }
79
+
80
+ if (contract.includesContactNumbers === true) {
81
+ failures.push("BDS team membership answer must not include phone/contact numbers");
82
+ }
83
+
84
+ return failures;
85
+ }
86
+ }
87
+
88
+ export function validateBdsTeamMembershipAnswer(answer = "", evidenceContract = {}) {
89
+ return new BdsTeamMembershipQualityPolicy().validate(answer, evidenceContract);
90
+ }
@@ -0,0 +1,24 @@
1
+ export const description = "Find or read private-cloud BDS Airflow DAG logs from the shared Airflow log filesystem through the approved Boundary sudo read tool.";
2
+
3
+ import { AIRFLOW_DAG_EXTRA_SCHEMA, airflowDagCommand, createPrivateCloudLogReadTool } from "../src/bds-atomic-log-tools.mjs";
4
+
5
+ export const __test = {
6
+ buildCommand: airflowDagCommand
7
+ };
8
+
9
+ const sharedTool = createPrivateCloudLogReadTool({
10
+ toolName: "bds_airflow_dag_log_read",
11
+ description: "Find or read private-cloud BDS Airflow DAG logs from the shared Airflow log filesystem through the approved Boundary sudo read tool.",
12
+ extraSchema: AIRFLOW_DAG_EXTRA_SCHEMA,
13
+ buildCommand: airflowDagCommand
14
+ });
15
+
16
+ export default {
17
+ name: sharedTool.name,
18
+ description,
19
+ inputSchema: sharedTool.inputSchema,
20
+ meta: sharedTool.meta,
21
+ annotations: sharedTool.annotations,
22
+ invoke: sharedTool.invoke,
23
+ handler: sharedTool.handler
24
+ };