amsd-pipeline-shared 0.5.45 → 0.5.46

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.
@@ -44,13 +44,32 @@ function renderPrompt(vars, profile) {
44
44
  }
45
45
 
46
46
  /** The command, prepared and not started: the CLI in print mode on the profile's family, in the work tree, memory-bounded when the host has the wrapper. */
47
+ /** THE RUNNER'S OWN ISOLATION FOR THE DOCTOR'S CALL (2026-10-25: the doctor's session loaded the host's MCP servers and skills — it is an
48
+ * agent like every other and gets the same declaration). The claude runner's alwaysFlags and env from the stack's declaration the
49
+ * profile names (config/llm-defaults.<set>.json runners.claude), read as the hub's resolver reads them; none when the file has none. */
50
+ function runnerIsolation(install, profile) {
51
+ try {
52
+ const file = String(profile.model.config || '').replace('{install}', install);
53
+ const settings = JSON.parse(fs.readFileSync(file, 'utf8'));
54
+ const r = settings.runners || {};
55
+ const d = r.claude || Object.values(r)[0] || {};
56
+ const flags = Array.isArray(d.alwaysFlags) ? d.alwaysFlags.filter((f) => f !== '--safe-mode') : [];
57
+ const env = {};
58
+ for (const [k, name] of Object.entries(d.env || {})) { const v = settings[name]; if (v !== undefined && v !== null && String(v) !== '') env[k] = String(v); }
59
+ return { flags, env };
60
+ } catch { return { flags: [], env: {} }; }
61
+ }
62
+
47
63
  function buildCommand({ install, profile, prompt, cwd, claudeBin, budgetUsd, model: rung }) {
48
64
  // a rung the failed-call step moved to is taken only within the profile's family; otherwise the family's own model
49
65
  const model = rung && rung.toLowerCase().includes(profile.model.family.toLowerCase()) ? rung : modelFor(install, profile);
66
+ const iso = runnerIsolation(install, profile);
67
+ // the declaration's switches (--setting-sources "", --strict-mcp-config, …) replace the former `--setting-sources project`; the
68
+ // doctor edits files, so its permission mode stays bypass
50
69
  const argv = [...hostWrap(), claudeBin || process.env.CLAUDE_BIN || 'claude', '-p', prompt, '--model', model, '--output-format', 'json',
51
- '--setting-sources', 'project', '--permission-mode', 'bypassPermissions',
70
+ ...(iso.flags.length ? iso.flags : ['--setting-sources', 'project']), '--permission-mode', 'bypassPermissions',
52
71
  ...(budgetUsd ? ['--max-budget-usd', String(budgetUsd)] : [])];
53
- return { cwd, argv, model };
72
+ return { cwd, argv, model, env: iso.env };
54
73
  }
55
74
 
56
75
  /** The answer's shape: repaired / cannot with the fields — anything else is no answer. */
@@ -80,7 +99,7 @@ function readAnswer(text) {
80
99
  /** Runs the session (the only model call the doctor makes). Returns the answer, the session's cost record and any model of another family it used. */
81
100
  function runAgent({ install, profile, prompt, cwd, sessionDir, env, claudeBin, budgetUsd, model }) {
82
101
  const cmd = buildCommand({ install, profile, prompt, cwd, claudeBin, budgetUsd, model });
83
- const r = spawnSync(cmd.argv[0], cmd.argv.slice(1), { cwd: cmd.cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: '0', ...process.env, ...(env || {}) }, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
102
+ const r = spawnSync(cmd.argv[0], cmd.argv.slice(1), { cwd: cmd.cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: '0', ...process.env, ...cmd.env, ...(env || {}) }, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
84
103
  try { fs.mkdirSync(sessionDir, { recursive: true }); fs.writeFileSync(path.join(sessionDir, 'agent.stderr'), String(r.stderr || '')); fs.writeFileSync(path.join(sessionDir, 'agent.stdout'), String(r.stdout || '')); } catch { /* evidence only */ }
85
104
  let out = null; try { out = JSON.parse(r.stdout); } catch { out = null; }
86
105
  const session = out && out.session_id ? { sessionId: out.session_id, modelUsage: out.modelUsage || {}, totalCostUsd: out.total_cost_usd || 0 } : null;
@@ -9,8 +9,12 @@
9
9
  // agent is granted them by name (--tools mcp__<server>__<tool>), and a tool whose permission is 'safe' is annotated read-only,
10
10
  // which is what lets it run in plan mode (proven 2026-10-24 on Claude Code 2.1.310: without the annotation plan mode refuses it).
11
11
  //
12
+ // node pipeline-tools-mcp.js --codeline <dir> serve the plugins THAT CODELINE provisions (.epam/settings.json tools — the
13
+ // pipeline's own grant rule, lib/agent-tools.js pluginToolsFor), run inside it
12
14
  // node pipeline-tools-mcp.js <runner-name> serve the runner's hostedToolPlugins (declaration read from the active set)
13
15
  // node pipeline-tools-mcp.js --modules a.js,b.js serve these plugin modules (tests)
16
+ // ONE SERVER FOR BOTH RUNTIMES (2026-10-25): the Claude workflows had their own (claude-workflows/mcp/pipeline-tools.js, per
17
+ // codeline) and the hub got this one (per runner) — duplicated code; the workflows now start this file in --codeline mode.
14
18
  //
15
19
  // Protocol: JSON-RPC 2.0, one message per line, MCP 2024-11-05 — initialize, tools/list, tools/call; nothing else.
16
20
  const fs = require('fs');
@@ -27,6 +31,13 @@ function hostedModulesFor(runnerName) {
27
31
  return ((runner && runner.hostedToolPlugins) || []).map((m) => path.resolve(ORCH, m));
28
32
  }
29
33
 
34
+ /** The plugin modules a codeline provisions (.epam/settings.json tools), as the pipeline's grant rule reads them. */
35
+ function codelineModulesOf(codeline) {
36
+ let settings;
37
+ try { settings = JSON.parse(fs.readFileSync(path.join(codeline, '.epam', 'settings.json'), 'utf8')); } catch { return []; }
38
+ return (Array.isArray(settings.tools) ? settings.tools : []).filter((e) => typeof e === 'string' && e);
39
+ }
40
+
30
41
  /** Every tool the modules export, as MCP lists them: safe → read-only; the plugin's own input schema and description. */
31
42
  function toolsOf(modules) {
32
43
  const out = [];
@@ -35,7 +46,7 @@ function toolsOf(modules) {
35
46
  const mod = require(m);
36
47
  for (const t of ((mod && mod.tools) || [])) {
37
48
  const name = t.name || (t.definition && t.definition.name);
38
- if (!name) continue;
49
+ if (!name || typeof t.execute !== 'function') continue;
39
50
  const def = t.definition || {};
40
51
  out.push({
41
52
  name,
@@ -94,9 +105,12 @@ function mcpConfig(runnerName, nodeBin = process.execPath) {
94
105
 
95
106
  if (require.main === module) {
96
107
  const a = process.argv.slice(2);
97
- const modules = a[0] === '--modules' ? String(a[1] || '').split(',').filter(Boolean).map((m) => path.resolve(m)) : hostedModulesFor(a[0]);
98
- if (!modules.length) { process.stderr.write(`[pipeline-tools-mcp] no hosted tool plugins for runner '${a[0] || ''}'\n`); process.exit(2); }
108
+ let modules;
109
+ if (a[0] === '--modules') modules = String(a[1] || '').split(',').filter(Boolean).map((m) => path.resolve(m));
110
+ else if (a[0] === '--codeline') { const cl = path.resolve(a[1] || process.cwd()); process.chdir(cl); modules = codelineModulesOf(cl); } // plugins work on the folder they run in
111
+ else modules = hostedModulesFor(a[0]);
112
+ if (!modules.length) { process.stderr.write(`[pipeline-tools-mcp] no tool plugins for ${a[0] === '--codeline' ? `codeline ${a[1]}` : `runner '${a[0] || ''}'`}\n`); process.exit(2); }
99
113
  serve(modules);
100
114
  }
101
115
 
102
- module.exports = { hostedModulesFor, toolsOf, serve, mcpConfig, asMcpResult };
116
+ module.exports = { hostedModulesFor, codelineModulesOf, toolsOf, serve, mcpConfig, asMcpResult };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-shared",
3
- "version": "0.5.45",
3
+ "version": "0.5.46",
4
4
  "description": "The AMSD pipeline's shared components, one source for the bash pipeline and its Claude workflows: the model-call gateway (llm-handler.sh — retry, ladder, self-heal, cost) and the libraries it runs.",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {