@blacksandscyber/mcp-server-bursar 0.5.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 +230 -0
  2. package/build/config.d.ts +45 -0
  3. package/build/config.js +177 -0
  4. package/build/http-transport.d.ts +16 -0
  5. package/build/http-transport.js +191 -0
  6. package/build/index.d.ts +16 -0
  7. package/build/index.js +31 -0
  8. package/build/server.d.ts +41 -0
  9. package/build/server.js +902 -0
  10. package/build/shared/errors.d.ts +50 -0
  11. package/build/shared/errors.js +69 -0
  12. package/build/shared/linkBuilder.d.ts +93 -0
  13. package/build/shared/linkBuilder.js +148 -0
  14. package/build/shared/logger.d.ts +10 -0
  15. package/build/shared/logger.js +28 -0
  16. package/build/shield/bootRole.d.ts +60 -0
  17. package/build/shield/bootRole.js +145 -0
  18. package/build/shield/client.d.ts +265 -0
  19. package/build/shield/client.js +656 -0
  20. package/build/shield/deploy/index.d.ts +69 -0
  21. package/build/shield/deploy/index.js +569 -0
  22. package/build/shield/discovery/dataStoreDetector.d.ts +3 -0
  23. package/build/shield/discovery/dataStoreDetector.js +125 -0
  24. package/build/shield/discovery/dockerScanner.d.ts +34 -0
  25. package/build/shield/discovery/dockerScanner.js +543 -0
  26. package/build/shield/discovery/endpointScanner.d.ts +3 -0
  27. package/build/shield/discovery/endpointScanner.js +306 -0
  28. package/build/shield/discovery/environmentScanner.d.ts +86 -0
  29. package/build/shield/discovery/environmentScanner.js +545 -0
  30. package/build/shield/discovery/externalServiceDetector.d.ts +3 -0
  31. package/build/shield/discovery/externalServiceDetector.js +98 -0
  32. package/build/shield/discovery/frameworkDetector.d.ts +3 -0
  33. package/build/shield/discovery/frameworkDetector.js +114 -0
  34. package/build/shield/discovery/manifestGenerator.d.ts +12 -0
  35. package/build/shield/discovery/manifestGenerator.js +124 -0
  36. package/build/shield/discovery/piiDetector.d.ts +5 -0
  37. package/build/shield/discovery/piiDetector.js +203 -0
  38. package/build/shield/discovery/severity.d.ts +47 -0
  39. package/build/shield/discovery/severity.js +138 -0
  40. package/build/shield/discovery/topologyNormalizer.d.ts +109 -0
  41. package/build/shield/discovery/topologyNormalizer.js +416 -0
  42. package/build/shield/identity.d.ts +53 -0
  43. package/build/shield/identity.js +70 -0
  44. package/build/shield/install/configMerge.d.ts +91 -0
  45. package/build/shield/install/configMerge.js +324 -0
  46. package/build/shield/install/keystore.d.ts +25 -0
  47. package/build/shield/install/keystore.js +156 -0
  48. package/build/shield/install/orchestrator.d.ts +33 -0
  49. package/build/shield/install/orchestrator.js +404 -0
  50. package/build/shield/install/transports/awsSsm.d.ts +43 -0
  51. package/build/shield/install/transports/awsSsm.js +378 -0
  52. package/build/shield/install/transports/bootstrapToken.d.ts +39 -0
  53. package/build/shield/install/transports/bootstrapToken.js +117 -0
  54. package/build/shield/install/transports/ssh.d.ts +50 -0
  55. package/build/shield/install/transports/ssh.js +569 -0
  56. package/build/shield/install/types.d.ts +139 -0
  57. package/build/shield/install/types.js +10 -0
  58. package/build/shield/protocol-walkthrough.d.ts +65 -0
  59. package/build/shield/protocol-walkthrough.js +392 -0
  60. package/build/shield/provision/appProvisioner.d.ts +15 -0
  61. package/build/shield/provision/appProvisioner.js +25 -0
  62. package/build/shield/types.d.ts +261 -0
  63. package/build/shield/types.js +4 -0
  64. package/build/shield/verify/postureReporter.d.ts +4 -0
  65. package/build/shield/verify/postureReporter.js +31 -0
  66. package/dxt/blacksands-ca.crt +67 -0
  67. package/dxt/scripts/setup.js +520 -0
  68. package/package.json +76 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * MCP config-file merge for `bursar_install_agent_remotely`.
3
+ *
4
+ * Responsibilities (from SHIELD-INSTALL-AGENT-REMOTELY-REQUIREMENTS.md §FR-5):
5
+ * - Resolve the default config path for an agentType on a given platform.
6
+ * - Merge a "blacksands-shield" MCP server stanza into an existing JSON
7
+ * config, never clobbering unrelated keys.
8
+ * - Abort when the target file parses as something we don't recognize.
9
+ * - Never silently set SHIELD_API_KEY; explicitly remove it if present.
10
+ * - Produce an atomic write plan: compute the new content and a .bak of
11
+ * the old content so the orchestrator can roll back.
12
+ *
13
+ * This module is PURE: it does not touch the filesystem. It takes the
14
+ * current file content (string | null) as input and returns the new
15
+ * content, the backup, and a structured diff. The orchestrator handles
16
+ * the actual reads/writes — keeps this unit-testable without mocks.
17
+ */
18
+ export type AgentType = "claude-desktop" | "mcp-server" | "openclaw" | "custom";
19
+ export type Platform = "darwin" | "win32" | "linux";
20
+ export interface ConfigEnv {
21
+ SHIELD_CONNECTION_MODE: "broker";
22
+ SHIELD_AUTHORIZER_URL: string;
23
+ SHIELD_SERVICE_ID: string;
24
+ SHIELD_AUTH_PASSWORD: string;
25
+ SHIELD_CLIENT_CERT: string;
26
+ SHIELD_CLIENT_KEY: string;
27
+ SHIELD_CA_CERT?: string;
28
+ }
29
+ export interface MergeInput {
30
+ agentType: AgentType;
31
+ /** Current file content; null if the file does not exist. */
32
+ current: string | null;
33
+ /** Name to use as the JSON key under mcpServers. */
34
+ serverKey: string;
35
+ /** Env vars to inject into the server stanza. */
36
+ env: ConfigEnv;
37
+ /** Absolute path to the MCP server binary on the target (or command invocation). */
38
+ command: string;
39
+ args?: string[];
40
+ }
41
+ export interface MergeResult {
42
+ /** The new file content that should be written atomically. */
43
+ nextContent: string;
44
+ /** The previous content (null if file didn't exist) — used for .bak. */
45
+ previousContent: string | null;
46
+ /** Per-key summary of what changed, for human-readable dry-run output. */
47
+ diff: {
48
+ action: "create-file" | "insert-server" | "overwrite-server" | "noop";
49
+ serverKey: string;
50
+ removedKeys: string[];
51
+ notes: string[];
52
+ };
53
+ }
54
+ /**
55
+ * Resolves the default config-file path for an (agentType, platform) pair.
56
+ * Uses a provided home directory so this function is pure and testable.
57
+ * The `custom` agentType has no default — callers MUST pass an explicit path.
58
+ */
59
+ export declare function defaultConfigPath(agentType: AgentType, platform: Platform, homeDir: string, appDataDir?: string): string | null;
60
+ /** Convenience: resolve config path from runtime context with current-process fallbacks. */
61
+ export declare function resolveConfigPath(agentType: AgentType, explicit?: string): string;
62
+ /**
63
+ * Merge the blacksands-shield server stanza into a JSON config file.
64
+ *
65
+ * Merge semantics:
66
+ * - If file doesn't exist → create `{ "mcpServers": { [serverKey]: <stanza> } }`.
67
+ * - If file exists and parses as JSON object → preserve every existing key
68
+ * except `mcpServers[serverKey]`, which we replace. If the existing stanza
69
+ * had a SHIELD_API_KEY env var, we drop it (reported in diff.removedKeys).
70
+ * - If file exists but isn't parseable JSON → abort with InstallError(phase: apply-config).
71
+ * - If `mcpServers` exists but isn't an object → abort; same reasoning.
72
+ */
73
+ export declare function mergeJsonConfig(input: MergeInput, filePath: string): MergeResult;
74
+ /**
75
+ * YAML merge for OpenClaw's config.yaml. Same invariants as JSON:
76
+ * - preserve every existing key
77
+ * - strip legacy SHIELD_API_KEY if present
78
+ * - abort on unparseable YAML or non-object root
79
+ *
80
+ * OpenClaw's schema expects an `mcp_servers:` map (snake_case) at the root.
81
+ * We write/replace the "blacksands-shield" entry inside it; all sibling
82
+ * entries are left untouched.
83
+ */
84
+ export declare function mergeYamlConfig(input: MergeInput, filePath: string): MergeResult;
85
+ /**
86
+ * Merge dispatcher: picks the right format based on agentType.
87
+ * - openclaw → YAML (mcp_servers: …)
88
+ * - everything else → JSON (mcpServers: …)
89
+ */
90
+ export declare function mergeConfig(input: MergeInput, filePath: string): MergeResult;
91
+ //# sourceMappingURL=configMerge.d.ts.map
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ /**
3
+ * MCP config-file merge for `bursar_install_agent_remotely`.
4
+ *
5
+ * Responsibilities (from SHIELD-INSTALL-AGENT-REMOTELY-REQUIREMENTS.md §FR-5):
6
+ * - Resolve the default config path for an agentType on a given platform.
7
+ * - Merge a "blacksands-shield" MCP server stanza into an existing JSON
8
+ * config, never clobbering unrelated keys.
9
+ * - Abort when the target file parses as something we don't recognize.
10
+ * - Never silently set SHIELD_API_KEY; explicitly remove it if present.
11
+ * - Produce an atomic write plan: compute the new content and a .bak of
12
+ * the old content so the orchestrator can roll back.
13
+ *
14
+ * This module is PURE: it does not touch the filesystem. It takes the
15
+ * current file content (string | null) as input and returns the new
16
+ * content, the backup, and a structured diff. The orchestrator handles
17
+ * the actual reads/writes — keeps this unit-testable without mocks.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.defaultConfigPath = defaultConfigPath;
54
+ exports.resolveConfigPath = resolveConfigPath;
55
+ exports.mergeJsonConfig = mergeJsonConfig;
56
+ exports.mergeYamlConfig = mergeYamlConfig;
57
+ exports.mergeConfig = mergeConfig;
58
+ const path = __importStar(require("path"));
59
+ const os = __importStar(require("os"));
60
+ const yaml = __importStar(require("js-yaml"));
61
+ const errors_1 = require("../../shared/errors");
62
+ // ──────────────────────────────────────────────────────────────────────────
63
+ // Default config paths per agentType + platform
64
+ // ──────────────────────────────────────────────────────────────────────────
65
+ /**
66
+ * Resolves the default config-file path for an (agentType, platform) pair.
67
+ * Uses a provided home directory so this function is pure and testable.
68
+ * The `custom` agentType has no default — callers MUST pass an explicit path.
69
+ */
70
+ function defaultConfigPath(agentType, platform, homeDir, appDataDir) {
71
+ if (agentType === "claude-desktop") {
72
+ if (platform === "darwin") {
73
+ return path.join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
74
+ }
75
+ if (platform === "win32") {
76
+ const appData = appDataDir || path.join(homeDir, "AppData", "Roaming");
77
+ return path.join(appData, "Claude", "claude_desktop_config.json");
78
+ }
79
+ // Linux — Anthropic doesn't officially ship Claude Desktop for Linux, but
80
+ // the documented XDG path is the one third-party builds use.
81
+ return path.join(homeDir, ".config", "Claude", "claude_desktop_config.json");
82
+ }
83
+ if (agentType === "mcp-server") {
84
+ return path.join(homeDir, ".config", "blacksands", ".mcp.json");
85
+ }
86
+ if (agentType === "openclaw") {
87
+ return path.join(homeDir, ".openclaw", "config.yaml");
88
+ }
89
+ return null;
90
+ }
91
+ /** Convenience: resolve config path from runtime context with current-process fallbacks. */
92
+ function resolveConfigPath(agentType, explicit) {
93
+ if (explicit)
94
+ return explicit;
95
+ const platform = process.platform;
96
+ const home = os.homedir();
97
+ const appData = process.env.APPDATA;
98
+ const resolved = defaultConfigPath(agentType, platform, home, appData);
99
+ if (!resolved) {
100
+ throw new errors_1.InstallError(`agentType "${agentType}" has no default config path; pass configPath explicitly`, { phase: "apply-config", next_steps: ['Pass a `configPath` value in the tool input.'] }, 400);
101
+ }
102
+ return resolved;
103
+ }
104
+ // ──────────────────────────────────────────────────────────────────────────
105
+ // JSON config merge (claude-desktop, mcp-server)
106
+ // ──────────────────────────────────────────────────────────────────────────
107
+ /**
108
+ * Build the MCP server stanza that gets inserted under `mcpServers[serverKey]`.
109
+ * Returns a plain object; the caller serializes it as part of the full file.
110
+ *
111
+ * The env block explicitly OMITS SHIELD_API_KEY. If the existing stanza had
112
+ * one, we strip it (reported back in diff.removedKeys).
113
+ */
114
+ function buildServerStanza(command, args, env) {
115
+ const envOut = {
116
+ SHIELD_CONNECTION_MODE: env.SHIELD_CONNECTION_MODE,
117
+ SHIELD_AUTHORIZER_URL: env.SHIELD_AUTHORIZER_URL,
118
+ SHIELD_SERVICE_ID: env.SHIELD_SERVICE_ID,
119
+ SHIELD_AUTH_PASSWORD: env.SHIELD_AUTH_PASSWORD,
120
+ SHIELD_CLIENT_CERT: env.SHIELD_CLIENT_CERT,
121
+ SHIELD_CLIENT_KEY: env.SHIELD_CLIENT_KEY,
122
+ };
123
+ if (env.SHIELD_CA_CERT)
124
+ envOut.SHIELD_CA_CERT = env.SHIELD_CA_CERT;
125
+ return {
126
+ command,
127
+ args: args || [],
128
+ env: envOut,
129
+ };
130
+ }
131
+ /**
132
+ * Parse JSON with a precise error — we want to tell the operator which file
133
+ * failed to parse, not just hand them "Unexpected token {".
134
+ */
135
+ function safeParseJson(raw, filePath) {
136
+ try {
137
+ const parsed = JSON.parse(raw);
138
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
139
+ throw new errors_1.InstallError(`Config file at ${filePath} is not a JSON object — refusing to modify`, {
140
+ phase: "apply-config",
141
+ next_steps: [
142
+ `Inspect ${filePath}. If it was intentionally customized, back it up and delete it, then retry.`,
143
+ ],
144
+ }, 409);
145
+ }
146
+ return parsed;
147
+ }
148
+ catch (err) {
149
+ if (err instanceof errors_1.InstallError)
150
+ throw err;
151
+ throw new errors_1.InstallError(`Config file at ${filePath} is not valid JSON: ${err.message}`, {
152
+ phase: "apply-config",
153
+ next_steps: [
154
+ `Fix or remove ${filePath} before retrying. The tool refuses to overwrite unparseable files to avoid data loss.`,
155
+ ],
156
+ }, 409);
157
+ }
158
+ }
159
+ /**
160
+ * Merge the blacksands-shield server stanza into a JSON config file.
161
+ *
162
+ * Merge semantics:
163
+ * - If file doesn't exist → create `{ "mcpServers": { [serverKey]: <stanza> } }`.
164
+ * - If file exists and parses as JSON object → preserve every existing key
165
+ * except `mcpServers[serverKey]`, which we replace. If the existing stanza
166
+ * had a SHIELD_API_KEY env var, we drop it (reported in diff.removedKeys).
167
+ * - If file exists but isn't parseable JSON → abort with InstallError(phase: apply-config).
168
+ * - If `mcpServers` exists but isn't an object → abort; same reasoning.
169
+ */
170
+ function mergeJsonConfig(input, filePath) {
171
+ const notes = [];
172
+ const removedKeys = [];
173
+ const stanza = buildServerStanza(input.command, input.args, input.env);
174
+ if (input.current === null || input.current.trim() === "") {
175
+ const nextObj = { mcpServers: { [input.serverKey]: stanza } };
176
+ notes.push(`Created new config file at ${filePath}.`);
177
+ return {
178
+ nextContent: JSON.stringify(nextObj, null, 2) + "\n",
179
+ previousContent: null,
180
+ diff: { action: "create-file", serverKey: input.serverKey, removedKeys, notes },
181
+ };
182
+ }
183
+ const parsed = safeParseJson(input.current, filePath);
184
+ let action = "insert-server";
185
+ // mcpServers must be an object (or absent). Arrays/primitives → abort.
186
+ let mcpServers = parsed.mcpServers;
187
+ if (mcpServers === undefined) {
188
+ mcpServers = {};
189
+ }
190
+ else if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
191
+ throw new errors_1.InstallError(`Config file at ${filePath} has a non-object "mcpServers" key — refusing to modify`, {
192
+ phase: "apply-config",
193
+ next_steps: [`Fix the "mcpServers" key in ${filePath} to be a JSON object, then retry.`],
194
+ }, 409);
195
+ }
196
+ const mcpServersObj = mcpServers;
197
+ const existing = mcpServersObj[input.serverKey];
198
+ if (existing !== undefined) {
199
+ action = "overwrite-server";
200
+ notes.push(`Overwriting existing "${input.serverKey}" server stanza.`);
201
+ // Detect stripped SHIELD_API_KEY for reporting
202
+ if (existing !== null &&
203
+ typeof existing === "object" &&
204
+ !Array.isArray(existing) &&
205
+ "env" in existing &&
206
+ existing.env !== null &&
207
+ typeof existing.env === "object") {
208
+ const existingEnv = existing.env;
209
+ if ("SHIELD_API_KEY" in existingEnv) {
210
+ removedKeys.push(`mcpServers.${input.serverKey}.env.SHIELD_API_KEY`);
211
+ notes.push("Removed legacy SHIELD_API_KEY from existing stanza — broker mode requires mTLS + auth password only.");
212
+ }
213
+ }
214
+ }
215
+ const nextObj = {
216
+ ...parsed,
217
+ mcpServers: {
218
+ ...mcpServersObj,
219
+ [input.serverKey]: stanza,
220
+ },
221
+ };
222
+ // Preserve insertion order as closely as possible: if mcpServers wasn't
223
+ // originally in the parsed object, it will show up at the end, which
224
+ // matches what `JSON.stringify` does by default on spread.
225
+ return {
226
+ nextContent: JSON.stringify(nextObj, null, 2) + "\n",
227
+ previousContent: input.current,
228
+ diff: { action, serverKey: input.serverKey, removedKeys, notes },
229
+ };
230
+ }
231
+ // ──────────────────────────────────────────────────────────────────────────
232
+ // YAML config merge (openclaw)
233
+ // ──────────────────────────────────────────────────────────────────────────
234
+ /**
235
+ * YAML merge for OpenClaw's config.yaml. Same invariants as JSON:
236
+ * - preserve every existing key
237
+ * - strip legacy SHIELD_API_KEY if present
238
+ * - abort on unparseable YAML or non-object root
239
+ *
240
+ * OpenClaw's schema expects an `mcp_servers:` map (snake_case) at the root.
241
+ * We write/replace the "blacksands-shield" entry inside it; all sibling
242
+ * entries are left untouched.
243
+ */
244
+ function mergeYamlConfig(input, filePath) {
245
+ const notes = [];
246
+ const removedKeys = [];
247
+ const stanza = buildServerStanza(input.command, input.args, input.env);
248
+ if (input.current === null || input.current.trim() === "") {
249
+ const nextObj = { mcp_servers: { [input.serverKey]: stanza } };
250
+ notes.push(`Created new YAML config at ${filePath}.`);
251
+ return {
252
+ nextContent: yaml.dump(nextObj, { lineWidth: 120, noRefs: true }),
253
+ previousContent: null,
254
+ diff: { action: "create-file", serverKey: input.serverKey, removedKeys, notes },
255
+ };
256
+ }
257
+ let parsed;
258
+ try {
259
+ parsed = yaml.load(input.current);
260
+ }
261
+ catch (err) {
262
+ throw new errors_1.InstallError(`Config file at ${filePath} is not valid YAML: ${err.message}`, {
263
+ phase: "apply-config",
264
+ next_steps: [`Fix or remove ${filePath}. The tool refuses to overwrite unparseable files.`],
265
+ }, 409);
266
+ }
267
+ if (parsed === null || parsed === undefined) {
268
+ parsed = {};
269
+ }
270
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
271
+ throw new errors_1.InstallError(`Config file at ${filePath} root is not a YAML mapping — refusing to modify`, { phase: "apply-config" }, 409);
272
+ }
273
+ const root = parsed;
274
+ let action = "insert-server";
275
+ let mcpServers = root.mcp_servers;
276
+ if (mcpServers === undefined) {
277
+ mcpServers = {};
278
+ }
279
+ else if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
280
+ throw new errors_1.InstallError(`Config file at ${filePath} has a non-mapping "mcp_servers" key — refusing to modify`, { phase: "apply-config" }, 409);
281
+ }
282
+ const mcpServersObj = mcpServers;
283
+ const existing = mcpServersObj[input.serverKey];
284
+ if (existing !== undefined) {
285
+ action = "overwrite-server";
286
+ notes.push(`Overwriting existing "${input.serverKey}" entry.`);
287
+ if (existing !== null &&
288
+ typeof existing === "object" &&
289
+ !Array.isArray(existing) &&
290
+ "env" in existing &&
291
+ typeof existing.env === "object" &&
292
+ existing.env !== null) {
293
+ const existingEnv = existing.env;
294
+ if ("SHIELD_API_KEY" in existingEnv) {
295
+ removedKeys.push(`mcp_servers.${input.serverKey}.env.SHIELD_API_KEY`);
296
+ notes.push("Removed legacy SHIELD_API_KEY from existing entry — broker mode uses mTLS + auth password only.");
297
+ }
298
+ }
299
+ }
300
+ const nextObj = {
301
+ ...root,
302
+ mcp_servers: {
303
+ ...mcpServersObj,
304
+ [input.serverKey]: stanza,
305
+ },
306
+ };
307
+ return {
308
+ nextContent: yaml.dump(nextObj, { lineWidth: 120, noRefs: true }),
309
+ previousContent: input.current,
310
+ diff: { action, serverKey: input.serverKey, removedKeys, notes },
311
+ };
312
+ }
313
+ /**
314
+ * Merge dispatcher: picks the right format based on agentType.
315
+ * - openclaw → YAML (mcp_servers: …)
316
+ * - everything else → JSON (mcpServers: …)
317
+ */
318
+ function mergeConfig(input, filePath) {
319
+ if (input.agentType === "openclaw") {
320
+ return mergeYamlConfig(input, filePath);
321
+ }
322
+ return mergeJsonConfig(input, filePath);
323
+ }
324
+ //# sourceMappingURL=configMerge.js.map
@@ -0,0 +1,25 @@
1
+ export type KeystoreBackend = "keychain" | "libsecret" | "wincred" | "none";
2
+ export type KeystoreMode = "off" | "auto" | "on";
3
+ /** Service/label namespace under which secrets are filed in the OS store. */
4
+ export declare const KEYSTORE_SERVICE = "blacksands-shield-mcp";
5
+ /** The secret kinds we manage. */
6
+ export type SecretKind = "key" | "password";
7
+ /** Account name for a secret: "<clientName>.<kind>". */
8
+ export declare function secretAccount(clientName: string, kind: SecretKind): string;
9
+ /** Parse the BS_MCP_KEYSTORE gate. Default "off" (disk-only, no behavior change). */
10
+ export declare function keystoreMode(env?: NodeJS.ProcessEnv): KeystoreMode;
11
+ /** Which backend this platform can use (ignores the mode gate). */
12
+ export declare function detectBackend(platform?: NodeJS.Platform): KeystoreBackend;
13
+ /**
14
+ * Whether the keystore should be used right now: the mode is not "off" AND a
15
+ * backend is available. With mode "on" and no backend, we still report false
16
+ * (callers fall back to disk) but should surface a warning — see storeSecret.
17
+ */
18
+ export declare function isKeystoreAvailable(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
19
+ /** Store a secret. Returns true on success, false on any failure (caller falls back to disk). */
20
+ export declare function storeSecret(account: string, secret: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
21
+ /** Retrieve a secret. Returns the value, or null if absent / unavailable / on error. */
22
+ export declare function retrieveSecret(account: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | null;
23
+ /** Delete a secret. Best-effort; returns true if the backend reported success. */
24
+ export declare function removeSecret(account: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
25
+ //# sourceMappingURL=keystore.d.ts.map
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KEYSTORE_SERVICE = void 0;
4
+ exports.secretAccount = secretAccount;
5
+ exports.keystoreMode = keystoreMode;
6
+ exports.detectBackend = detectBackend;
7
+ exports.isKeystoreAvailable = isKeystoreAvailable;
8
+ exports.storeSecret = storeSecret;
9
+ exports.retrieveSecret = retrieveSecret;
10
+ exports.removeSecret = removeSecret;
11
+ /** OS keystore storage for MCP installation secrets (F4.11, Gap #9 PR-G9-5).
12
+ *
13
+ * The installation-bound MCP private key and broker auth-password are the two
14
+ * sensitive items in ~/.blacksands/mcp-certs/. This module stores them in the
15
+ * platform secret store instead of (or in addition to) plain disk files:
16
+ * - macOS → Keychain (`security` generic password)
17
+ * - Linux → libsecret (`secret-tool`)
18
+ * - Windows → not yet wired (falls back to disk; see GAP9 design doc)
19
+ *
20
+ * Design constraints:
21
+ * - Every operation is best-effort and NEVER throws. A keystore miss or a
22
+ * command failure returns false/null so the caller transparently falls
23
+ * back to the existing disk path — credential handling must never make an
24
+ * install worse than it is today.
25
+ * - Gated by BS_MCP_KEYSTORE (off | auto | on); default "off" preserves the
26
+ * current disk-only behavior byte-for-byte until a deployment opts in.
27
+ * - The cert and CA are public and stay on disk; only the key + password are
28
+ * candidates for the keystore.
29
+ *
30
+ * All shelling-out uses execFileSync (argv array, no shell) so a secret can
31
+ * never be interpreted by a shell.
32
+ */
33
+ const child_process_1 = require("child_process");
34
+ /** Service/label namespace under which secrets are filed in the OS store. */
35
+ exports.KEYSTORE_SERVICE = "blacksands-shield-mcp";
36
+ /** Account name for a secret: "<clientName>.<kind>". */
37
+ function secretAccount(clientName, kind) {
38
+ return `${clientName}.${kind}`;
39
+ }
40
+ /** Parse the BS_MCP_KEYSTORE gate. Default "off" (disk-only, no behavior change). */
41
+ function keystoreMode(env = process.env) {
42
+ const v = (env.BS_MCP_KEYSTORE || "").trim().toLowerCase();
43
+ if (v === "on" || v === "1" || v === "true" || v === "require")
44
+ return "on";
45
+ if (v === "auto")
46
+ return "auto";
47
+ return "off";
48
+ }
49
+ /** True if a command resolves on PATH (no output, no throw on success). */
50
+ function commandExists(cmd, platform = process.platform) {
51
+ try {
52
+ (0, child_process_1.execFileSync)(platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** Which backend this platform can use (ignores the mode gate). */
60
+ function detectBackend(platform = process.platform) {
61
+ if (platform === "darwin")
62
+ return commandExists("security", platform) ? "keychain" : "none";
63
+ if (platform === "linux")
64
+ return commandExists("secret-tool", platform) ? "libsecret" : "none";
65
+ // win32: Credential Manager wiring is a follow-up (PR-G9-5b). Fall back to disk.
66
+ return "none";
67
+ }
68
+ /**
69
+ * Whether the keystore should be used right now: the mode is not "off" AND a
70
+ * backend is available. With mode "on" and no backend, we still report false
71
+ * (callers fall back to disk) but should surface a warning — see storeSecret.
72
+ */
73
+ function isKeystoreAvailable(env = process.env, platform = process.platform) {
74
+ if (keystoreMode(env) === "off")
75
+ return false;
76
+ return detectBackend(platform) !== "none";
77
+ }
78
+ /** Store a secret. Returns true on success, false on any failure (caller falls back to disk). */
79
+ function storeSecret(account, secret, env = process.env, platform = process.platform) {
80
+ if (keystoreMode(env) === "off")
81
+ return false;
82
+ const backend = detectBackend(platform);
83
+ try {
84
+ if (backend === "keychain") {
85
+ // -U updates an existing item without prompting; -w takes the secret value.
86
+ (0, child_process_1.execFileSync)("security", [
87
+ "add-generic-password", "-U",
88
+ "-a", account, "-s", exports.KEYSTORE_SERVICE, "-w", secret,
89
+ ], { stdio: "ignore" });
90
+ return true;
91
+ }
92
+ if (backend === "libsecret") {
93
+ // secret-tool reads the secret from stdin (not argv) — preferred.
94
+ (0, child_process_1.execFileSync)("secret-tool", [
95
+ "store", "--label", `${exports.KEYSTORE_SERVICE}:${account}`,
96
+ "service", exports.KEYSTORE_SERVICE, "account", account,
97
+ ], { input: secret, stdio: ["pipe", "ignore", "ignore"] });
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ /** Retrieve a secret. Returns the value, or null if absent / unavailable / on error. */
107
+ function retrieveSecret(account, env = process.env, platform = process.platform) {
108
+ if (keystoreMode(env) === "off")
109
+ return null;
110
+ const backend = detectBackend(platform);
111
+ try {
112
+ if (backend === "keychain") {
113
+ const out = (0, child_process_1.execFileSync)("security", [
114
+ "find-generic-password", "-a", account, "-s", exports.KEYSTORE_SERVICE, "-w",
115
+ ], { encoding: "utf8" });
116
+ // `security -w` emits the password followed by a trailing newline.
117
+ return out.length > 0 ? out.replace(/\r?\n$/, "") : null;
118
+ }
119
+ if (backend === "libsecret") {
120
+ const out = (0, child_process_1.execFileSync)("secret-tool", [
121
+ "lookup", "service", exports.KEYSTORE_SERVICE, "account", account,
122
+ ], { encoding: "utf8" });
123
+ return out.length > 0 ? out.replace(/\r?\n$/, "") : null;
124
+ }
125
+ return null;
126
+ }
127
+ catch {
128
+ // Non-zero exit (item not found) or command missing → caller uses disk.
129
+ return null;
130
+ }
131
+ }
132
+ /** Delete a secret. Best-effort; returns true if the backend reported success. */
133
+ function removeSecret(account, env = process.env, platform = process.platform) {
134
+ if (keystoreMode(env) === "off")
135
+ return false;
136
+ const backend = detectBackend(platform);
137
+ try {
138
+ if (backend === "keychain") {
139
+ (0, child_process_1.execFileSync)("security", [
140
+ "delete-generic-password", "-a", account, "-s", exports.KEYSTORE_SERVICE,
141
+ ], { stdio: "ignore" });
142
+ return true;
143
+ }
144
+ if (backend === "libsecret") {
145
+ (0, child_process_1.execFileSync)("secret-tool", [
146
+ "clear", "service", exports.KEYSTORE_SERVICE, "account", account,
147
+ ], { stdio: "ignore" });
148
+ return true;
149
+ }
150
+ return false;
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ //# sourceMappingURL=keystore.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Orchestrator for bursar_install_agent_remotely.
3
+ *
4
+ * Coordinates: precheck → transport.deliver → (config merge + restart are
5
+ * the target's job for bootstrap-token; handled by the transport itself
6
+ * for SSH/SSM in later phases) → handshake verification → rollback on
7
+ * failure.
8
+ *
9
+ * The orchestrator is intentionally thin. Transport-specific behavior
10
+ * lives in the Transport classes; config-file manipulation lives in
11
+ * configMerge. This file is just the state machine.
12
+ *
13
+ * See SHIELD-INSTALL-AGENT-REMOTELY-REQUIREMENTS.md §FR-1 through §FR-12.
14
+ */
15
+ import type { ShieldClient } from "../client";
16
+ import type { InstallInput, InstallResult } from "./types";
17
+ export interface RunInstallDeps {
18
+ shield: ShieldClient;
19
+ /** Authorizer URL to fall back to when input.authorizerUrl is omitted. */
20
+ defaultAuthorizerUrl?: string;
21
+ }
22
+ export declare function runInstall(input: InstallInput, deps: RunInstallDeps): Promise<InstallResult>;
23
+ /**
24
+ * Run install with full lifecycle: advisory lock → audit start →
25
+ * precheck → transport.deliver → handshake poll → audit finalize →
26
+ * lock release. On failure after precheck, the transport's rollback
27
+ * hook runs (SSH rolls back files + revokes the cert; bootstrap-token
28
+ * deletes the minted token).
29
+ *
30
+ * Exposed as the public entry point used by the tool handler in server.ts.
31
+ */
32
+ export declare function runInstallWithRollback(input: InstallInput, deps: RunInstallDeps): Promise<InstallResult>;
33
+ //# sourceMappingURL=orchestrator.d.ts.map