amsd-pipeline-shared 0.5.28 → 0.5.29

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.
@@ -173,6 +173,35 @@ function codelineSets({ install, project, codelines = [], env = {} }) {
173
173
  } finally { if (prev === undefined) delete process.env.EPAM_BROWNFIELD; else process.env.EPAM_BROWNFIELD = prev; }
174
174
  }
175
175
 
176
+ // ── 3b. permissions ─────────────────────────────────────────────────────────────────────────────────────────────
177
+ /**
178
+ * THE RUN CAN WRITE WHAT IT COPIES (operator, 2026-10-23: "Add permissions check to pre-launch checks"; live run 13 died on EACCES — the
179
+ * install's codeline sets are locked read-only and the run's copies kept that mode). A trial copy of the install's project (as a run takes
180
+ * it: without runs/ and prompts/) is made with the launch's OWN copy function into a probe folder, and every copied path must be writable;
181
+ * and a run folder the launch reuses (a retry) must hold nothing the run cannot write over — every path owned by the user the launch runs as
182
+ * (the run makes its own paths writable as it clears them; one another user owns, it cannot). No copy function given: not checked — RED.
183
+ */
184
+ function permissions({ install, project, runDir, copy, uid = process.getuid() }) {
185
+ if (typeof copy !== 'function') return res('permissions', false, 'not checked: the launch gave no copy function — what the run copies cannot be proven writable');
186
+ const notWritable = (root) => { const bad = []; const walk = (p) => { let st; try { st = fs.lstatSync(p); } catch { return; } if (st.isSymbolicLink()) return;
187
+ try { fs.accessSync(p, fs.constants.W_OK); } catch { bad.push(p); } if (st.isDirectory()) { let es = []; try { es = fs.readdirSync(p); } catch { bad.push(p); } for (const n of es) walk(path.join(p, n)); } };
188
+ walk(root); return bad; };
189
+ const src = path.join(orchOf(install), 'projects', project);
190
+ const probe = fs.mkdtempSync(path.join(os.tmpdir(), 'prelaunch-permissions-'));
191
+ let copied = [];
192
+ try {
193
+ for (const n of fs.readdirSync(src)) if (n !== 'runs' && n !== 'prompts') copy(path.join(src, n), path.join(probe, n));
194
+ copied = notWritable(probe).map((p) => path.relative(probe, p));
195
+ } catch (e) { copied = [`the trial copy failed: ${e.message}`]; }
196
+ finally { spawnSync('chmod', ['-R', 'u+rwX', probe]); fs.rmSync(probe, { recursive: true, force: true }); }
197
+ const notOwned = (root) => { const bad = []; const walk = (p) => { let st; try { st = fs.lstatSync(p); } catch { return; } if (st.uid !== uid) bad.push(p);
198
+ if (st.isDirectory() && !st.isSymbolicLink()) { let es = []; try { es = fs.readdirSync(p); } catch { return; } for (const n of es) walk(path.join(p, n)); } }; walk(root); return bad; };
199
+ const left = runDir && fs.existsSync(runDir) ? notOwned(runDir).map((p) => path.relative(runDir, p) || '.') : [];
200
+ const why = [copied.length && `${copied.length} copied path(s) not writable: ${copied.slice(0, 5).join(', ')}`,
201
+ left.length && `the run folder holds ${left.length} path(s) another user owns — the run cannot write over them: ${left.slice(0, 5).join(', ')}`].filter(Boolean).join('; ');
202
+ return res('permissions', !why, why || `the launch's copy of ${project} is writable, and the run folder holds nothing another user owns`, { copied, left });
203
+ }
204
+
176
205
  // ── 4. cost ──────────────────────────────────────────────────────────────────────────────────────────────────────────
177
206
  /** Every model a provider set's ladders name (startModel, model, models, from/to), from its own declaration. */
178
207
  function ladderModels(install, set) {
@@ -295,6 +324,7 @@ function prelaunch(o) {
295
324
  // the variant (brownfield) is the run's, from the env the launch hands over — not this process's own environment
296
325
  if (o.codeline) results.push(cache({ install: o.install, project: o.project, codeline: o.codeline, variant: (o.env || {}).EPAM_BROWNFIELD === '1' ? 'brownfield' : '', maxRebuild: o.maxRebuild || 0 }));
297
326
  if (Array.isArray(o.codelines)) results.push(codelineSets({ install: o.install, project: o.project, codelines: o.codelines, env: o.env || {} }));
327
+ if (o.copy) results.push(permissions({ install: o.install, project: o.project, runDir: o.runDir, copy: o.copy }));
298
328
  results.push(cost({ install: o.install, set: o.set || o.env.EPAM_PROVIDER_SET || 'claude', tmpDir: o.outDir }));
299
329
  results.push(alone({ install: o.install }));
300
330
  results.push(memory({ needMb: Number(o.needMb || 4096) }));
@@ -307,7 +337,7 @@ function prelaunch(o) {
307
337
  return report;
308
338
  }
309
339
 
310
- module.exports = { prelaunch, prelaunchCli, preflight, langfuse, langfuseEnv, seed, cache, codelineSets, cost, ladderModels, alone, memory, noTimeout, checklist, ancestors };
340
+ module.exports = { prelaunch, prelaunchCli, preflight, langfuse, langfuseEnv, seed, cache, codelineSets, permissions, cost, ladderModels, alone, memory, noTimeout, checklist, ancestors };
311
341
 
312
342
  /**
313
343
  * THE COMMAND, for every pipeline:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-shared",
3
- "version": "0.5.28",
3
+ "version": "0.5.29",
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": {