neuron-inspector 1.2.1 → 1.3.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 (85) hide show
  1. package/dist/collections/assertions.js +11 -0
  2. package/dist/collections/assertions.js.map +1 -1
  3. package/dist/collections/dynamic-vars-core.d.ts +79 -0
  4. package/dist/collections/dynamic-vars-core.js +214 -0
  5. package/dist/collections/dynamic-vars-core.js.map +1 -0
  6. package/dist/collections/dynamic-vars.d.ts +2 -61
  7. package/dist/collections/dynamic-vars.js +6 -146
  8. package/dist/collections/dynamic-vars.js.map +1 -1
  9. package/dist/collections/encoding.d.ts +3 -0
  10. package/dist/collections/encoding.js +20 -0
  11. package/dist/collections/encoding.js.map +1 -0
  12. package/dist/collections/executor.d.ts +128 -5
  13. package/dist/collections/executor.js +330 -93
  14. package/dist/collections/executor.js.map +1 -1
  15. package/dist/collections/index.d.ts +11 -3
  16. package/dist/collections/index.js +7 -1
  17. package/dist/collections/index.js.map +1 -1
  18. package/dist/collections/masking.d.ts +42 -0
  19. package/dist/collections/masking.js +188 -0
  20. package/dist/collections/masking.js.map +1 -0
  21. package/dist/collections/resolver-core.d.ts +74 -0
  22. package/dist/collections/resolver-core.js +309 -0
  23. package/dist/collections/resolver-core.js.map +1 -0
  24. package/dist/collections/resolver.d.ts +9 -62
  25. package/dist/collections/resolver.js +21 -281
  26. package/dist/collections/resolver.js.map +1 -1
  27. package/dist/collections/scenario-run.d.ts +86 -0
  28. package/dist/collections/scenario-run.js +169 -0
  29. package/dist/collections/scenario-run.js.map +1 -0
  30. package/dist/collections/script-api.d.ts +18 -0
  31. package/dist/collections/script-api.js +208 -0
  32. package/dist/collections/script-api.js.map +1 -0
  33. package/dist/collections/scripts.d.ts +1 -8
  34. package/dist/collections/scripts.js +6 -196
  35. package/dist/collections/scripts.js.map +1 -1
  36. package/dist/collections/slug.d.ts +2 -0
  37. package/dist/collections/slug.js +8 -0
  38. package/dist/collections/slug.js.map +1 -0
  39. package/dist/collections/store.d.ts +5 -2
  40. package/dist/collections/store.js +6 -6
  41. package/dist/collections/store.js.map +1 -1
  42. package/dist/collections/tools.d.ts +8 -1
  43. package/dist/collections/tools.js +313 -89
  44. package/dist/collections/tools.js.map +1 -1
  45. package/dist/collections/types.d.ts +42 -3
  46. package/dist/correlation.js +10 -0
  47. package/dist/correlation.js.map +1 -1
  48. package/dist/extension-slot.d.ts +45 -0
  49. package/dist/extension-slot.js +81 -0
  50. package/dist/extension-slot.js.map +1 -0
  51. package/dist/monitor.d.ts +5 -0
  52. package/dist/monitor.js +7 -2
  53. package/dist/monitor.js.map +1 -1
  54. package/dist/server.js +113 -25
  55. package/dist/server.js.map +1 -1
  56. package/dist/tools.js +1 -1
  57. package/dist/tools.js.map +1 -1
  58. package/package.json +3 -2
  59. package/dist/collections/__tests__/assertions.test.d.ts +0 -1
  60. package/dist/collections/__tests__/assertions.test.js +0 -203
  61. package/dist/collections/__tests__/assertions.test.js.map +0 -1
  62. package/dist/collections/__tests__/dynamic-vars.test.d.ts +0 -1
  63. package/dist/collections/__tests__/dynamic-vars.test.js +0 -169
  64. package/dist/collections/__tests__/dynamic-vars.test.js.map +0 -1
  65. package/dist/collections/__tests__/execution.test.d.ts +0 -1
  66. package/dist/collections/__tests__/execution.test.js +0 -271
  67. package/dist/collections/__tests__/execution.test.js.map +0 -1
  68. package/dist/collections/__tests__/integration.test.d.ts +0 -1
  69. package/dist/collections/__tests__/integration.test.js +0 -478
  70. package/dist/collections/__tests__/integration.test.js.map +0 -1
  71. package/dist/collections/__tests__/merge.test.d.ts +0 -1
  72. package/dist/collections/__tests__/merge.test.js +0 -411
  73. package/dist/collections/__tests__/merge.test.js.map +0 -1
  74. package/dist/collections/__tests__/resolver.test.d.ts +0 -1
  75. package/dist/collections/__tests__/resolver.test.js +0 -282
  76. package/dist/collections/__tests__/resolver.test.js.map +0 -1
  77. package/dist/collections/__tests__/scenario.test.d.ts +0 -1
  78. package/dist/collections/__tests__/scenario.test.js +0 -367
  79. package/dist/collections/__tests__/scenario.test.js.map +0 -1
  80. package/dist/collections/__tests__/scripts.test.d.ts +0 -1
  81. package/dist/collections/__tests__/scripts.test.js +0 -245
  82. package/dist/collections/__tests__/scripts.test.js.map +0 -1
  83. package/dist/collections/__tests__/store.test.d.ts +0 -1
  84. package/dist/collections/__tests__/store.test.js +0 -313
  85. package/dist/collections/__tests__/store.test.js.map +0 -1
@@ -1,9 +1,105 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import * as YAML from "yaml";
3
3
  import { slugify } from "./store.js";
4
- import { executeEndpoint, executeFolder, executeCollection, executeScenario } from "./executor.js";
4
+ import { executeEndpoint, executeFolder, executeCollection, resolveStepRef, normalizePoll } from "./executor.js";
5
+ import { assertTimeZone } from "./dynamic-vars.js";
6
+ import { pickEnvironment, runInputsFor, runScenario, rerunScenario, RerunContexts } from "./scenario-run.js";
7
+ import { sanitizeInputDeclarations, mergeInputDeclarations, prepareInputs, describeInputs, SecretMasker } from "./masking.js";
5
8
  import { fromPostmanV21, toPostmanV21 } from "./postman.js";
6
9
  import { mergeCollections } from "./merge.js";
10
+ const INPUTS_SCHEMA = {
11
+ type: "array",
12
+ description: "Values supplied at run time, used as {{name}}. Declarations only: never put a value here. " +
13
+ "Secret inputs are kept in memory for one run and shown as ••• in every result.",
14
+ items: {
15
+ type: "object",
16
+ properties: {
17
+ name: { type: "string" },
18
+ label: { type: "string" },
19
+ description: { type: "string" },
20
+ secret: { type: "boolean", description: "Mask the value everywhere and never store it" },
21
+ required: { type: "boolean", description: "Default true" },
22
+ default: { type: "string", description: "Fallback for non-secret inputs only" },
23
+ },
24
+ required: ["name"],
25
+ },
26
+ };
27
+ const RUN_INPUT_VALUES_SCHEMA = {
28
+ type: "object",
29
+ description: "Values for the declared inputs, e.g. { client_id: '…', client_secret: '…' }. " +
30
+ "Held in memory for this run only; values of secret inputs are masked as ••• in the result.",
31
+ };
32
+ const STEP_SCHEMA = {
33
+ type: "object",
34
+ properties: {
35
+ ref: {
36
+ type: "string",
37
+ description: "Endpoint reference: 'folder/<endpoint name>' or 'folder/METHOD /path' (method case-insensitive)",
38
+ },
39
+ overrides: {
40
+ type: "object",
41
+ description: "Per-step overrides for headers, body, tests, path, variables",
42
+ properties: {
43
+ headers: { type: "object" },
44
+ body: {},
45
+ tests: { type: "array" },
46
+ path: { type: "string" },
47
+ variables: { type: "object" },
48
+ },
49
+ },
50
+ poll: {
51
+ type: "object",
52
+ description: "Repeat this request until every 'until' assertion passes. Each attempt is recorded. " +
53
+ "Fails the step (and halts the scenario) if max_attempts runs out.",
54
+ properties: {
55
+ until: { type: "array", description: "Assertions: [{ check, op, expect }]. op 'in' takes an array expect." },
56
+ interval_ms: { type: "number", description: "Wait between attempts (default 2000, 100–60000)" },
57
+ max_attempts: { type: "number", description: "Default 10, max 120" },
58
+ },
59
+ required: ["until"],
60
+ },
61
+ },
62
+ required: ["ref"],
63
+ };
64
+ const COLLECTION_DATA_SCHEMA = {
65
+ type: "object",
66
+ description: "Run this collection object instead of loading one from disk (used by the extension for its local collections)",
67
+ };
68
+ /** Step refs that won't resolve, so a scenario can be fixed before it runs. */
69
+ function checkStepRefs(collection, steps) {
70
+ const problems = [];
71
+ steps.forEach((step, i) => {
72
+ const r = resolveStepRef(collection, step.ref);
73
+ if ("error" in r)
74
+ problems.push(`step ${i + 1}: ${r.error}`);
75
+ if (step.poll) {
76
+ try {
77
+ normalizePoll(step.poll);
78
+ }
79
+ catch (err) {
80
+ problems.push(`step ${i + 1}: ${err.message}`);
81
+ }
82
+ }
83
+ });
84
+ return problems;
85
+ }
86
+ function validateSteps(raw) {
87
+ if (!Array.isArray(raw))
88
+ throw new Error("steps must be an array");
89
+ return raw.map((step, i) => {
90
+ if (!step || typeof step !== "object" || typeof step.ref !== "string") {
91
+ throw new Error(`step ${i + 1} needs a 'ref'`);
92
+ }
93
+ const s = step;
94
+ if (s.poll !== undefined)
95
+ normalizePoll(s.poll);
96
+ return s;
97
+ });
98
+ }
99
+ /** Cancellation flags for runs started with a run_id. */
100
+ const activeRuns = new Map();
101
+ /** In-memory context of recent runs (by run_id), for neuron_api_rerun_step. */
102
+ const rerunContexts = new RerunContexts();
7
103
  export function buildCollectionTools() {
8
104
  return [
9
105
  {
@@ -123,7 +219,8 @@ export function buildCollectionTools() {
123
219
  },
124
220
  extract: {
125
221
  type: "object",
126
- description: "Variables to extract from the response, e.g. { bot_id: 'body.id' }",
222
+ description: "Variables to extract from the response, e.g. { bot_id: 'body.id' }. " +
223
+ "Use { access_token: { path: 'body.access_token', mask: true } } to show the value as ••• in every result for the rest of the run.",
127
224
  },
128
225
  body_mode: {
129
226
  type: "string",
@@ -263,6 +360,8 @@ export function buildCollectionTools() {
263
360
  variables: { type: "object", description: "Environment variables (key-value pairs)" },
264
361
  headers: { type: "object", description: "Environment-specific header overrides" },
265
362
  timeout_ms: { type: "number", description: "Environment-specific timeout" },
363
+ inputs: INPUTS_SCHEMA,
364
+ timezone: { type: "string", description: "IANA zone for {{$today}} and formatted {{$now}}, e.g. 'Africa/Lagos' (default: defaults.timezone, else UTC)" },
266
365
  },
267
366
  required: ["collection", "name", "base_url"],
268
367
  },
@@ -321,6 +420,7 @@ export function buildCollectionTools() {
321
420
  {
322
421
  name: "neuron_api_set_defaults",
323
422
  description: "Set or update collection-level defaults. Merges with existing defaults. " +
423
+ "pre_request / post_response scripts here run before each endpoint's own script (same sandbox and nr API), so endpoints can override what they set. " +
324
424
  "Pass null for auth to clear it. Omit fields to leave them unchanged.",
325
425
  inputSchema: {
326
426
  type: "object",
@@ -328,6 +428,7 @@ export function buildCollectionTools() {
328
428
  collection: { type: "string", description: "Collection id" },
329
429
  headers: { type: "object", description: "Default headers (key-value pairs)" },
330
430
  variables: { type: "object", description: "Default variables (key-value pairs)" },
431
+ timezone: { type: "string", description: "IANA zone for {{$today}} and formatted {{$now}}, e.g. 'Africa/Lagos' (default UTC). An environment's timezone wins." },
331
432
  timeout_ms: { type: "number", description: "Default timeout (ms)" },
332
433
  auth: {
333
434
  type: "object",
@@ -444,34 +545,20 @@ export function buildCollectionTools() {
444
545
  // ── Scenario management ───────────────────────────────────
445
546
  {
446
547
  name: "neuron_api_create_scenario",
447
- description: "Create a new scenario in a collection. Steps reference endpoints as 'folder/method path' (e.g. 'auth/POST /login').",
548
+ description: "Create a new scenario in a collection. Steps reference endpoints as 'folder/<endpoint name>' or " +
549
+ "'folder/METHOD /path' (e.g. 'auth/POST /login'). A step can poll until assertions pass. " +
550
+ "Declare run-time values (client ids, secrets) in 'inputs' and pass them when you run.",
448
551
  inputSchema: {
449
552
  type: "object",
450
553
  properties: {
451
554
  collection: { type: "string", description: "Collection id" },
452
555
  name: { type: "string", description: "Scenario name" },
453
556
  description: { type: "string", description: "Scenario description" },
557
+ inputs: INPUTS_SCHEMA,
454
558
  steps: {
455
559
  type: "array",
456
- description: "Ordered steps. Each step has a 'ref' (folder/METHOD path) and optional overrides.",
457
- items: {
458
- type: "object",
459
- properties: {
460
- ref: { type: "string", description: "Endpoint reference: 'folder/METHOD path'" },
461
- overrides: {
462
- type: "object",
463
- description: "Per-step overrides for headers, body, tests, path, variables",
464
- properties: {
465
- headers: { type: "object" },
466
- body: {},
467
- tests: { type: "array" },
468
- path: { type: "string" },
469
- variables: { type: "object" },
470
- },
471
- },
472
- },
473
- required: ["ref"],
474
- },
560
+ description: "Ordered steps. Each step has a 'ref', optional overrides and an optional poll.",
561
+ items: STEP_SCHEMA,
475
562
  },
476
563
  },
477
564
  required: ["collection", "name", "steps"],
@@ -479,24 +566,18 @@ export function buildCollectionTools() {
479
566
  },
480
567
  {
481
568
  name: "neuron_api_update_scenario",
482
- description: "Update a scenario's description or steps.",
569
+ description: "Update a scenario's description, inputs or steps.",
483
570
  inputSchema: {
484
571
  type: "object",
485
572
  properties: {
486
573
  collection: { type: "string", description: "Collection id" },
487
574
  scenario: { type: "string", description: "Scenario name" },
488
575
  description: { type: "string", description: "New description" },
576
+ inputs: INPUTS_SCHEMA,
489
577
  steps: {
490
578
  type: "array",
491
579
  description: "New ordered steps (replaces existing)",
492
- items: {
493
- type: "object",
494
- properties: {
495
- ref: { type: "string" },
496
- overrides: { type: "object" },
497
- },
498
- required: ["ref"],
499
- },
580
+ items: STEP_SCHEMA,
500
581
  },
501
582
  },
502
583
  required: ["collection", "scenario"],
@@ -551,6 +632,7 @@ export function buildCollectionTools() {
551
632
  follow_authorization: { type: "boolean", description: "Override: retain Authorization on cross-host redirects" },
552
633
  remove_referer: { type: "boolean", description: "Override: remove Referer header on redirect" },
553
634
  encode_url: { type: "boolean", description: "Override: encode URL automatically" },
635
+ inputs: RUN_INPUT_VALUES_SCHEMA,
554
636
  },
555
637
  required: ["collection"],
556
638
  },
@@ -559,16 +641,63 @@ export function buildCollectionTools() {
559
641
  name: "neuron_api_run_scenario",
560
642
  description: "Run a named scenario: an ordered sequence of endpoint calls with variable chaining. " +
561
643
  "Extracted variables from each step are available to subsequent steps via {{variable}} references. " +
562
- "Execution halts on the first step that fails (HTTP error or assertion failure). " +
563
- "Steps can include overrides for headers, body, tests, path, and variables.",
644
+ "Execution halts on the first step that fails (HTTP error, assertion failure, or a poll that never succeeds). " +
645
+ "Steps can include overrides for headers, body, tests, path, and variables, and a poll. " +
646
+ "Pass values for the scenario's/environment's declared inputs in 'inputs'; secret ones are masked as ••• in the result. " +
647
+ "The environment you pass is the one used, whatever the collection's active environment is.",
564
648
  inputSchema: {
565
649
  type: "object",
566
650
  properties: {
567
651
  collection: { type: "string", description: "Collection id" },
568
652
  scenario: { type: "string", description: "Scenario name (key under 'scenarios' in the collection)" },
569
653
  environment: { type: "string", description: "Environment name (defaults to active_environment)" },
654
+ inputs: RUN_INPUT_VALUES_SCHEMA,
655
+ run_id: { type: "string", description: "Optional id so the run can be cancelled with neuron_api_cancel_run" },
656
+ collection_data: COLLECTION_DATA_SCHEMA,
570
657
  },
571
- required: ["collection", "scenario"],
658
+ required: ["scenario"],
659
+ },
660
+ },
661
+ {
662
+ name: "neuron_api_get_run_inputs",
663
+ description: "List the inputs a scenario run will ask for (environment inputs merged with the scenario's). " +
664
+ "Returns declarations only (name, label, secret, required), never values.",
665
+ inputSchema: {
666
+ type: "object",
667
+ properties: {
668
+ collection: { type: "string", description: "Collection id" },
669
+ scenario: { type: "string", description: "Scenario name (omit for environment inputs only)" },
670
+ environment: { type: "string", description: "Environment name (defaults to active_environment)" },
671
+ collection_data: COLLECTION_DATA_SCHEMA,
672
+ },
673
+ },
674
+ },
675
+ {
676
+ name: "neuron_api_rerun_step",
677
+ description: "Re-run one step of an earlier scenario run (mode 'step'), or that step and everything after it (mode 'from'), " +
678
+ "like re-running a job in GitHub Actions. It starts from the variables the original run had just before that step " +
679
+ "(tokens, ids, inputs), so earlier steps aren't called again. It uses the scenario as currently saved, so a fixed step can be re-run. " +
680
+ "Only runs started with a run_id in this bridge process within the last 30 minutes can be re-run; their context is kept in memory only.",
681
+ inputSchema: {
682
+ type: "object",
683
+ properties: {
684
+ run_id: { type: "string", description: "run_id of the original run_scenario call" },
685
+ step: { type: "number", description: "Step index as in the result's `step` field (0-based)" },
686
+ mode: { type: "string", enum: ["step", "from"], description: "'step' = only this step (default); 'from' = this step and all later ones" },
687
+ new_run_id: { type: "string", description: "Optional id for this re-run's live events / cancel" },
688
+ collection: { type: "string", description: "Collection id" },
689
+ collection_data: COLLECTION_DATA_SCHEMA,
690
+ },
691
+ required: ["run_id", "step"],
692
+ },
693
+ },
694
+ {
695
+ name: "neuron_api_cancel_run",
696
+ description: "Stop a scenario run started with a run_id. It halts before its next request or poll attempt.",
697
+ inputSchema: {
698
+ type: "object",
699
+ properties: { run_id: { type: "string" } },
700
+ required: ["run_id"],
572
701
  },
573
702
  },
574
703
  // ── Query and generation ─────────────────────────────────
@@ -693,7 +822,24 @@ export function buildCollectionTools() {
693
822
  }
694
823
  // ── Handler ─────────────────────────────────────────────────────
695
824
  export function buildCollectionHandler(store, httpExecutor, pipeline, assertionEvaluator, scriptRunner) {
696
- return async function handleCollectionTool(name, args) {
825
+ /** Load the collection a run targets: inline data from the caller, else the store. */
826
+ async function loadForRun(args) {
827
+ if (args.collection_data !== undefined) {
828
+ const data = args.collection_data;
829
+ if (!data || typeof data !== "object" || Array.isArray(data))
830
+ return { error: "collection_data must be a collection object" };
831
+ const c = data;
832
+ return { id: args.collection || slugify(c.name || "inline"), collection: c };
833
+ }
834
+ const id = args.collection;
835
+ if (!id)
836
+ return { error: "Provide 'collection' (id) or 'collection_data'" };
837
+ const result = await store.get(id);
838
+ if (!result)
839
+ return { error: `Collection '${id}' not found` };
840
+ return { id, collection: result.collection };
841
+ }
842
+ return async function handleCollectionTool(name, args, hooks) {
697
843
  switch (name) {
698
844
  case "neuron_api_list_collections": {
699
845
  return store.list();
@@ -785,6 +931,13 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
785
931
  env.headers = args.headers;
786
932
  if (args.timeout_ms)
787
933
  env.timeout_ms = args.timeout_ms;
934
+ if (typeof args.timezone === "string" && args.timezone) {
935
+ assertTimeZone(args.timezone);
936
+ env.timezone = args.timezone;
937
+ }
938
+ const envInputs = sanitizeInputDeclarations(args.inputs);
939
+ if (envInputs)
940
+ env.inputs = envInputs;
788
941
  await store.setEnvironment(id, envName, env);
789
942
  return { set: true, collection: id, environment: slugify(envName) };
790
943
  }
@@ -837,6 +990,8 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
837
990
  const { collection: _c, ...defaults } = args;
838
991
  if (Object.keys(defaults).length === 0)
839
992
  return { error: "No defaults provided" };
993
+ if (typeof defaults.timezone === "string" && defaults.timezone)
994
+ assertTimeZone(defaults.timezone);
840
995
  await store.setDefaults(id, defaults);
841
996
  return { set: true, collection: id, fields: Object.keys(defaults) };
842
997
  }
@@ -901,12 +1056,22 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
901
1056
  const id = args.collection;
902
1057
  const scenarioName = args.name;
903
1058
  const scenario = {
904
- steps: args.steps,
1059
+ steps: validateSteps(args.steps),
905
1060
  };
906
1061
  if (args.description)
907
1062
  scenario.description = args.description;
908
- await store.createScenario(id, scenarioName, scenario);
909
- return { created: true, collection: id, scenario: scenarioName, step_count: scenario.steps.length };
1063
+ const scenarioInputs = sanitizeInputDeclarations(args.inputs);
1064
+ if (scenarioInputs)
1065
+ scenario.inputs = scenarioInputs;
1066
+ const updated = await store.createScenario(id, scenarioName, scenario);
1067
+ const warnings = checkStepRefs(updated, scenario.steps);
1068
+ return {
1069
+ created: true,
1070
+ collection: id,
1071
+ scenario: scenarioName,
1072
+ step_count: scenario.steps.length,
1073
+ ...(warnings.length ? { warnings } : {}),
1074
+ };
910
1075
  }
911
1076
  case "neuron_api_update_scenario": {
912
1077
  const id = args.collection;
@@ -915,11 +1080,14 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
915
1080
  if (args.description !== undefined)
916
1081
  updates.description = args.description;
917
1082
  if (args.steps !== undefined)
918
- updates.steps = args.steps;
1083
+ updates.steps = validateSteps(args.steps);
1084
+ if (args.inputs !== undefined)
1085
+ updates.inputs = sanitizeInputDeclarations(args.inputs) ?? [];
919
1086
  if (Object.keys(updates).length === 0)
920
1087
  return { error: "No updates provided" };
921
- await store.updateScenario(id, scenarioName, updates);
922
- return { updated: true, collection: id, scenario: scenarioName };
1088
+ const updated = await store.updateScenario(id, scenarioName, updates);
1089
+ const warnings = checkStepRefs(updated, updated.scenarios?.[scenarioName]?.steps ?? []);
1090
+ return { updated: true, collection: id, scenario: scenarioName, ...(warnings.length ? { warnings } : {}) };
923
1091
  }
924
1092
  case "neuron_api_delete_scenario": {
925
1093
  const id = args.collection;
@@ -938,6 +1106,7 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
938
1106
  name: key,
939
1107
  description: s.description ?? null,
940
1108
  step_count: s.steps?.length ?? 0,
1109
+ inputs: describeInputs(s.inputs ?? []),
941
1110
  })),
942
1111
  };
943
1112
  }
@@ -946,25 +1115,26 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
946
1115
  if (!httpExecutor || !pipeline || !assertionEvaluator) {
947
1116
  return { error: "HTTP executor not configured" };
948
1117
  }
949
- const id = args.collection;
950
- const result = await store.get(id);
951
- if (!result)
952
- return { error: `Collection '${id}' not found` };
953
- const { collection } = result;
954
- // Resolve environment
955
- const envName = args.environment ?? collection.active_environment;
956
- if (!envName) {
957
- return { error: "No environment specified and no active_environment set on the collection" };
958
- }
959
- const environment = collection.environments?.[slugify(envName)];
960
- if (!environment) {
961
- return { error: `Environment '${envName}' not found in collection '${id}'` };
1118
+ const loaded = await loadForRun(args);
1119
+ if ("error" in loaded)
1120
+ return loaded;
1121
+ const { id, collection } = loaded;
1122
+ const picked = pickEnvironment(collection, id, args.environment);
1123
+ if ("error" in picked)
1124
+ return picked;
1125
+ const { key: envKey, environment } = picked;
1126
+ const declared = mergeInputDeclarations(environment.inputs);
1127
+ const prepared = prepareInputs(declared, args.inputs);
1128
+ if (prepared.missing.length) {
1129
+ return { error: `Missing required input(s): ${prepared.missing.join(", ")}`, inputs: describeInputs(declared) };
962
1130
  }
1131
+ const masker = new SecretMasker(prepared.secrets);
1132
+ const runCtx = { environmentName: envKey, inputs: prepared.values, onSecret: (v) => masker.add(v) };
963
1133
  const hasFolder = typeof args.folder === "string";
964
1134
  const hasEndpoint = typeof args.method === "string" && typeof args.path === "string";
965
1135
  // Mode 3: Collection-wide run (no folder specified)
966
1136
  if (!hasFolder) {
967
- return executeCollection({
1137
+ return masker.mask(await executeCollection({
968
1138
  collectionId: id,
969
1139
  collection,
970
1140
  environment,
@@ -972,16 +1142,17 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
972
1142
  pipeline,
973
1143
  assertionEvaluator,
974
1144
  scriptRunner,
975
- });
1145
+ ...runCtx,
1146
+ }));
976
1147
  }
977
1148
  const folderKey = slugify(args.folder);
978
- const folder = collection.folders?.[folderKey];
1149
+ const folder = collection.folders?.[folderKey] ?? collection.folders?.[args.folder];
979
1150
  if (!folder) {
980
1151
  return { error: `Folder '${args.folder}' not found in collection '${id}'` };
981
1152
  }
982
1153
  // Mode 2: Folder run (folder but no method/path)
983
1154
  if (!hasEndpoint) {
984
- return executeFolder({
1155
+ return masker.mask(await executeFolder({
985
1156
  folderKey,
986
1157
  collection,
987
1158
  environment,
@@ -990,7 +1161,8 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
990
1161
  pipeline,
991
1162
  assertionEvaluator,
992
1163
  scriptRunner,
993
- });
1164
+ ...runCtx,
1165
+ }));
994
1166
  }
995
1167
  // Mode 1: Single endpoint
996
1168
  const method = args.method.toUpperCase();
@@ -1023,7 +1195,7 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
1023
1195
  runSettings.encode_url = args.encode_url;
1024
1196
  endpoint.settings = runSettings;
1025
1197
  }
1026
- return executeEndpoint({
1198
+ return masker.mask(await executeEndpoint({
1027
1199
  collection,
1028
1200
  environment,
1029
1201
  folder,
@@ -1032,43 +1204,95 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
1032
1204
  pipeline,
1033
1205
  assertionEvaluator,
1034
1206
  scriptRunner,
1035
- });
1207
+ ...runCtx,
1208
+ }));
1036
1209
  }
1037
1210
  case "neuron_api_run_scenario": {
1038
1211
  if (!httpExecutor || !pipeline || !assertionEvaluator) {
1039
1212
  return { error: "HTTP executor not configured" };
1040
1213
  }
1041
- const id = args.collection;
1042
- const result = await store.get(id);
1043
- if (!result)
1044
- return { error: `Collection '${id}' not found` };
1045
- const { collection } = result;
1046
- // Resolve environment
1047
- const envName = args.environment ?? collection.active_environment;
1048
- if (!envName) {
1049
- return { error: "No environment specified and no active_environment set on the collection" };
1214
+ const loaded = await loadForRun(args);
1215
+ if ("error" in loaded)
1216
+ return loaded;
1217
+ const runId = typeof args.run_id === "string" && args.run_id ? args.run_id : undefined;
1218
+ const flag = { cancelled: false };
1219
+ if (runId)
1220
+ activeRuns.set(runId, flag);
1221
+ try {
1222
+ const rec = runId
1223
+ ? rerunContexts.recorder(runId, { scenario: args.scenario, environment: args.environment })
1224
+ : undefined;
1225
+ return await runScenario({
1226
+ ...(rec ?? {}),
1227
+ collection: loaded.collection,
1228
+ collectionId: loaded.id,
1229
+ scenario: args.scenario,
1230
+ environment: args.environment,
1231
+ inputs: args.inputs,
1232
+ httpExecutor,
1233
+ pipeline,
1234
+ assertionEvaluator,
1235
+ scriptRunner,
1236
+ isCancelled: () => flag.cancelled,
1237
+ onEvent: hooks?.onEvent && runId ? (event) => hooks.onEvent({ ...event, run_id: runId }) : undefined,
1238
+ });
1050
1239
  }
1051
- const environment = collection.environments?.[slugify(envName)];
1052
- if (!environment) {
1053
- return { error: `Environment '${envName}' not found in collection '${id}'` };
1240
+ finally {
1241
+ if (runId)
1242
+ activeRuns.delete(runId);
1054
1243
  }
1055
- // Find scenario
1056
- const scenarioKey = args.scenario;
1057
- const scenario = collection.scenarios?.[scenarioKey];
1058
- if (!scenario) {
1059
- return { error: `Scenario '${scenarioKey}' not found in collection '${id}'` };
1244
+ }
1245
+ case "neuron_api_get_run_inputs": {
1246
+ const loaded = await loadForRun(args);
1247
+ if ("error" in loaded)
1248
+ return loaded;
1249
+ const res = runInputsFor(loaded.collection, loaded.id, args.scenario, args.environment);
1250
+ if ("error" in res)
1251
+ return res;
1252
+ return { collection: loaded.id, environment: res.environment, scenario: args.scenario ?? null, inputs: res.inputs };
1253
+ }
1254
+ case "neuron_api_rerun_step": {
1255
+ if (!httpExecutor || !pipeline || !assertionEvaluator) {
1256
+ return { error: "HTTP executor not configured" };
1060
1257
  }
1061
- return executeScenario({
1062
- scenarioKey,
1063
- steps: scenario.steps,
1064
- collectionId: id,
1065
- collection,
1066
- environment,
1067
- httpExecutor,
1068
- pipeline,
1069
- assertionEvaluator,
1070
- scriptRunner,
1071
- });
1258
+ const loaded = await loadForRun(args);
1259
+ if ("error" in loaded)
1260
+ return loaded;
1261
+ const step = Number(args.step);
1262
+ if (!Number.isInteger(step) || step < 0)
1263
+ return { error: "step must be a 0-based step index" };
1264
+ const mode = args.mode === "from" ? "from" : "step";
1265
+ const newRunId = typeof args.new_run_id === "string" && args.new_run_id ? args.new_run_id : undefined;
1266
+ const flag = { cancelled: false };
1267
+ if (newRunId)
1268
+ activeRuns.set(newRunId, flag);
1269
+ try {
1270
+ return await rerunScenario({
1271
+ contexts: rerunContexts,
1272
+ runId: args.run_id,
1273
+ step,
1274
+ mode,
1275
+ collection: loaded.collection,
1276
+ collectionId: loaded.id,
1277
+ httpExecutor,
1278
+ pipeline,
1279
+ assertionEvaluator,
1280
+ scriptRunner,
1281
+ isCancelled: () => flag.cancelled,
1282
+ onEvent: hooks?.onEvent && newRunId ? (event) => hooks.onEvent({ ...event, run_id: newRunId }) : undefined,
1283
+ });
1284
+ }
1285
+ finally {
1286
+ if (newRunId)
1287
+ activeRuns.delete(newRunId);
1288
+ }
1289
+ }
1290
+ case "neuron_api_cancel_run": {
1291
+ const flag = activeRuns.get(args.run_id);
1292
+ if (!flag)
1293
+ return { cancelled: false, error: `No active run '${args.run_id}'` };
1294
+ flag.cancelled = true;
1295
+ return { cancelled: true, run_id: args.run_id };
1072
1296
  }
1073
1297
  // ── Query and generation ─────────────────────────────
1074
1298
  case "neuron_api_query": {