@llm4ts/shell 2.26.0 → 2.28.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.
@@ -11,14 +11,20 @@
11
11
  // llm4ts run soap-sample --repo . "call cercaMovimenti/happy-path"
12
12
  // llm4ts run soap-sample --repo . "call cercaMovimenti" every request of the operation
13
13
  // llm4ts run soap-sample --repo . -- --allow-mutating revocaBonifico "call revocaBonifico/gia-eseguito"
14
+ // llm4ts run soap-sample --repo . "check" the selected profile as decisions, no network
15
+ // llm4ts run soap-sample --repo . "trust call" capture and pin the chain the endpoint presents
16
+ // llm4ts run soap-sample --repo . "sts init" write the STS request template
17
+ // llm4ts run soap-sample --repo . -- --env uat "call all" with auth.uat.json (or LLM4TS_SOAP_ENV)
14
18
  //
15
19
  // Requests live in .llm4ts/soap/<service>/samples/<operation>/<name>.request.yaml
16
20
  // (or .request.xml). A call validates the request (an invalid one is never
17
21
  // sent), checks the confirmed class in operations.md and the environment's
18
22
  // mutating policy, sends it with auth.json's call side (Basic/Bearer, mTLS,
19
- // WS-Security), and records a masked, auth-stripped exchange beside it.
20
- // Flags: --allow-mutating <operation> (exactly one), --keep-raw (unmasked
21
- // response under the gitignored raw/). LLM4TS_SOAP_SERVICE picks the service
23
+ // WS-Security, an STS token), and records a masked, auth-stripped exchange
24
+ // beside it. Flags: --allow-mutating <operation> (exactly one), --keep-raw
25
+ // (unmasked response under the gitignored raw/), --env <name> (the
26
+ // environment's auth.<name>.json), --yes (pin a chain without asking).
27
+ // LLM4TS_SOAP_SERVICE picks the service
22
28
  // when several were discovered; LLM4TS_SOAP_STUB=<dir> answers calls from
23
29
  // <dir>/<operation>.xml instead of the network (rehearsals); LLM4TS_REASONER
24
30
  // picks the seat that proposes scenarios (default claude).
@@ -32,29 +38,40 @@ import { nodeSecretSource } from "./lib/soap/Auth.js";
32
38
  import { callOperation } from "./lib/soap/Call.js";
33
39
  import { operationByName } from "./lib/soap/Catalog.js";
34
40
  import { effectiveClass } from "./lib/soap/Classification.js";
35
- import { loadAuthProfile, loadCatalog, loadOperationsFile, readIfPresent } from "./lib/soap/Discover.js";
41
+ import { loadAuthProfile, loadCatalog, loadOperationsFile, profilePath, readIfPresent } from "./lib/soap/Discover.js";
36
42
  import { isSampleName, listRequests, loadMaskingKey, loadMaskingOverrides, readExchanges, renderRequestFile, samplePaths, seedsForService } from "./lib/soap/Samples.js";
37
43
  import { proposeScenarios } from "./lib/soap/Scenarios.js";
44
+ import { checkReport, initStsTemplate, trustSide } from "./lib/soap/Setup.js";
45
+ import { makeTokenSource } from "./lib/soap/Sts.js";
38
46
  import { importSoapUiProject } from "./lib/soap/SoapUiImport.js";
39
47
  import { makeDirectoryStubTransport } from "./lib/soap/Stub.js";
40
48
  import { makeNodeSoapTransport } from "./lib/soap/Transport.js";
41
49
  const usage = [
42
- 'usage: soap-sample [--allow-mutating <operation>] [--keep-raw] "<command>"',
50
+ 'usage: soap-sample [--allow-mutating <operation>] [--keep-raw] [--env <name>] [--yes] "<command>"',
43
51
  " list | init <operation> [name] | propose <operation> | import <soapui.xml>",
44
- " call <operation>[/<name>] | call all"
52
+ " call <operation>[/<name>] | call all",
53
+ " check | trust call|fetch|sts | sts init"
45
54
  ].join("\n");
46
- const parseSampleFlags = (argv) => Effect.gen(function* () {
55
+ export const parseSampleFlags = (argv) => Effect.gen(function* () {
47
56
  let allowMutating;
48
57
  let keepRaw = false;
58
+ let env;
59
+ let yes = false;
49
60
  const rest = [];
61
+ const valued = (name, index) => {
62
+ const argument = argv[index] ?? "";
63
+ return argument.includes("=") ? argument.slice(name.length + 1) : argv[index + 1];
64
+ };
50
65
  for (let index = 0; index < argv.length; index++) {
51
66
  const argument = argv[index] ?? "";
52
67
  if (argument === "--keep-raw")
53
68
  keepRaw = true;
69
+ else if (argument === "--yes")
70
+ yes = true;
54
71
  else if (argument === "--allow-mutating" || argument.startsWith("--allow-mutating=")) {
55
- const value = argument.includes("=")
56
- ? argument.slice("--allow-mutating=".length)
57
- : argv[++index];
72
+ const value = valued("--allow-mutating", index);
73
+ if (!argument.includes("="))
74
+ index++;
58
75
  if (value === undefined || value.trim() === "") {
59
76
  return yield* ScriptUsage.make({
60
77
  message: `--allow-mutating needs an operation name\n${usage}`
@@ -62,10 +79,19 @@ const parseSampleFlags = (argv) => Effect.gen(function* () {
62
79
  }
63
80
  allowMutating = value.trim();
64
81
  }
82
+ else if (argument === "--env" || argument.startsWith("--env=")) {
83
+ const value = valued("--env", index);
84
+ if (!argument.includes("="))
85
+ index++;
86
+ if (value === undefined || value.trim() === "" || value.startsWith("--")) {
87
+ return yield* ScriptUsage.make({ message: `--env needs an environment name\n${usage}` });
88
+ }
89
+ env = value.trim();
90
+ }
65
91
  else
66
92
  rest.push(argument);
67
93
  }
68
- return { allowMutating, keepRaw, rest };
94
+ return { allowMutating, keepRaw, env, yes, rest };
69
95
  });
70
96
  /** The service directory: LLM4TS_SOAP_SERVICE, or the only one discovered. */
71
97
  const pickService = (workspace, requested) => Effect.gen(function* () {
@@ -91,6 +117,7 @@ const program = Effect.gen(function* () {
91
117
  const input = yield* resolveFlowInput("list", flags.rest);
92
118
  const [command = "list", ...args] = input.prompt.trim().split(/\s+/);
93
119
  const environment = process.env;
120
+ const env = flags.env ?? (environment.LLM4TS_SOAP_ENV?.trim() || undefined);
94
121
  const reasoner = command === "propose"
95
122
  ? coderFor((environment.LLM4TS_REASONER ?? "claude").trim() || "claude")
96
123
  : undefined;
@@ -197,9 +224,107 @@ const program = Effect.gen(function* () {
197
224
  yield* say(`masked ${report.masking.entries.reduce((sum, entry) => sum + entry.count, 0)} values`);
198
225
  return;
199
226
  }
227
+ case "check": {
228
+ const profile = yield* loadAuthProfile(workspace, service, env).pipe(Effect.catch((error) => error._tag === "AuthProfileError"
229
+ ? say(error.message).pipe(Effect.as(undefined))
230
+ : Effect.fail(error)));
231
+ for (const line of yield* checkReport({
232
+ workspace,
233
+ service,
234
+ env,
235
+ profile,
236
+ environment
237
+ })) {
238
+ yield* say(line);
239
+ }
240
+ return;
241
+ }
242
+ case "trust": {
243
+ const side = args[0];
244
+ if (side !== "call" && side !== "fetch" && side !== "sts") {
245
+ return yield* FlowAborted.make({
246
+ message: `trust needs a side: call, fetch or sts\n${usage}`
247
+ });
248
+ }
249
+ const profile = yield* loadAuthProfile(workspace, service, env);
250
+ if (profile === undefined) {
251
+ return yield* FlowAborted.make({
252
+ message: `no profile at ${profilePath(service, env)}; write it first`
253
+ });
254
+ }
255
+ const stub = environment.LLM4TS_SOAP_STUB?.trim();
256
+ if (stub !== undefined && stub !== "") {
257
+ return yield* FlowAborted.make({
258
+ message: "trust reads a real server; unset LLM4TS_SOAP_STUB"
259
+ });
260
+ }
261
+ const fallback = catalog.endpoints[0]?.address;
262
+ const outcome = yield* trustSide({
263
+ workspace,
264
+ service,
265
+ env,
266
+ profile,
267
+ side,
268
+ ...(fallback === undefined ? {} : { fallbackEndpoint: fallback }),
269
+ secrets: nodeSecretSource(environment),
270
+ transport: makeNodeSoapTransport(),
271
+ confirm: (lines) => Effect.gen(function* () {
272
+ for (const line of lines)
273
+ yield* say(line);
274
+ if (flags.yes)
275
+ return true;
276
+ if (process.stdin.isTTY !== true) {
277
+ yield* say("not a terminal: pass --yes to pin this chain");
278
+ return false;
279
+ }
280
+ return yield* terminalInteraction.ask("trust this chain? [y/N] ").pipe(Effect.map((answer) => /^(y|yes|s|si|sì)$/i.test(answer.trim())), Effect.orElseSucceed(() => false));
281
+ })
282
+ });
283
+ switch (outcome._tag) {
284
+ case "AlreadyTrusted":
285
+ for (const line of outcome.lines)
286
+ yield* say(line);
287
+ yield* say("already trusted by the system roots; nothing to pin");
288
+ return;
289
+ case "Declined":
290
+ yield* say("not pinned");
291
+ return;
292
+ case "Pinned":
293
+ yield* say(`pinned ${outcome.pem}${outcome.replaced ? " (replaced)" : ""}; ${outcome.profile} now sets ${side}.tls.ca to it`);
294
+ return;
295
+ }
296
+ return;
297
+ }
298
+ case "sts": {
299
+ if (args[0] !== "init") {
300
+ return yield* FlowAborted.make({ message: `sts takes "init"\n${usage}` });
301
+ }
302
+ const profile = yield* loadAuthProfile(workspace, service, env);
303
+ if (profile === undefined) {
304
+ return yield* FlowAborted.make({
305
+ message: `no profile at ${profilePath(service, env)}; write it with an "sts" section first`
306
+ });
307
+ }
308
+ const result = yield* initStsTemplate(workspace, service, profile);
309
+ yield* say(result.written
310
+ ? `wrote ${result.path}; edit it if the STS differs from WS-Trust 1.3, then: soap-sample "call …"`
311
+ : `${result.path} exists; edit it, or delete it and run sts init again`);
312
+ return;
313
+ }
200
314
  case "call": {
201
315
  const target = args[0] ?? "";
202
- const profile = yield* loadAuthProfile(workspace, service);
316
+ const profile = yield* loadAuthProfile(workspace, service, env);
317
+ if (profile !== undefined)
318
+ yield* say(`profile: ${profilePath(service, env)}`);
319
+ const tokenSource = profile?.sts === undefined
320
+ ? undefined
321
+ : yield* makeTokenSource({
322
+ workspace,
323
+ service,
324
+ config: profile.sts,
325
+ secrets: nodeSecretSource(environment),
326
+ transport: makeNodeSoapTransport()
327
+ });
203
328
  const stub = environment.LLM4TS_SOAP_STUB?.trim();
204
329
  const transport = stub === undefined || stub === ""
205
330
  ? makeNodeSoapTransport()
@@ -236,6 +361,7 @@ const program = Effect.gen(function* () {
236
361
  operations,
237
362
  secrets: nodeSecretSource(environment),
238
363
  transport,
364
+ ...(tokenSource === undefined ? {} : { token: tokenSource }),
239
365
  allowMutating: flags.allowMutating,
240
366
  confirm,
241
367
  keepRaw: flags.keepRaw
@@ -251,6 +377,11 @@ const program = Effect.gen(function* () {
251
377
  : `HTTP ${exchange.response?.status ?? "?"}`;
252
378
  yield* say(`${operation}/${name}: ${status}, ${exchange.response?.elapsedMs ?? 0} ms, ${exchange.responseIssues.length} schema findings, ${exchange.masking.entries.reduce((sum, entry) => sum + entry.count, 0)} values masked → ${outcome.success.path}`);
253
379
  }
380
+ if (tokenSource !== undefined) {
381
+ const fetches = yield* tokenSource.fetches;
382
+ if (fetches > 0)
383
+ yield* say(`sts: ${fetches} token call(s) this run`);
384
+ }
254
385
  if (failures > 0)
255
386
  return yield* FlowAborted.make({
256
387
  message: `${failures} of ${targets.length} calls did not complete`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "2.26.0",
3
+ "version": "2.28.0",
4
4
  "description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,9 +52,9 @@
52
52
  "dependencies": {
53
53
  "@effect/platform-node": "4.0.0",
54
54
  "@effect/platform-node-shared": "4.0.0",
55
- "@llm4ts/core": "2.26.0",
56
- "@llm4ts/flow": "2.26.0",
57
- "@llm4ts/runner": "2.26.0"
55
+ "@llm4ts/core": "2.28.0",
56
+ "@llm4ts/flow": "2.28.0",
57
+ "@llm4ts/runner": "2.28.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "effect": "4.0.0"