skillscript-runtime 0.23.1 → 0.25.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 (77) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +2 -1
  3. package/UPGRADING.md +9 -1
  4. package/dist/bootstrap-from-env.d.ts +46 -0
  5. package/dist/bootstrap-from-env.d.ts.map +1 -0
  6. package/dist/bootstrap-from-env.js +130 -0
  7. package/dist/bootstrap-from-env.js.map +1 -0
  8. package/dist/bootstrap.d.ts +10 -0
  9. package/dist/bootstrap.d.ts.map +1 -1
  10. package/dist/bootstrap.js +7 -0
  11. package/dist/bootstrap.js.map +1 -1
  12. package/dist/cli.js +23 -164
  13. package/dist/cli.js.map +1 -1
  14. package/dist/connectors/registry.d.ts +9 -0
  15. package/dist/connectors/registry.d.ts.map +1 -1
  16. package/dist/connectors/registry.js +23 -0
  17. package/dist/connectors/registry.js.map +1 -1
  18. package/dist/connectors/skill-store.d.ts +1 -0
  19. package/dist/connectors/skill-store.d.ts.map +1 -1
  20. package/dist/connectors/skill-store.js +27 -0
  21. package/dist/connectors/skill-store.js.map +1 -1
  22. package/dist/connectors/sqlite-skill-store.d.ts +1 -0
  23. package/dist/connectors/sqlite-skill-store.d.ts.map +1 -1
  24. package/dist/connectors/sqlite-skill-store.js +10 -0
  25. package/dist/connectors/sqlite-skill-store.js.map +1 -1
  26. package/dist/connectors/types.d.ts +19 -0
  27. package/dist/connectors/types.d.ts.map +1 -1
  28. package/dist/connectors/types.js.map +1 -1
  29. package/dist/dashboard/server.d.ts +2 -0
  30. package/dist/dashboard/server.d.ts.map +1 -1
  31. package/dist/dashboard/server.js +4 -0
  32. package/dist/dashboard/server.js.map +1 -1
  33. package/dist/dashboard/spa/app.js +33 -5
  34. package/dist/help-content.d.ts.map +1 -1
  35. package/dist/help-content.js +3 -0
  36. package/dist/help-content.js.map +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/lint.d.ts +1 -1
  42. package/dist/lint.d.ts.map +1 -1
  43. package/dist/lint.js +145 -0
  44. package/dist/lint.js.map +1 -1
  45. package/dist/mcp-server.d.ts +8 -0
  46. package/dist/mcp-server.d.ts.map +1 -1
  47. package/dist/mcp-server.js +24 -1
  48. package/dist/mcp-server.js.map +1 -1
  49. package/dist/parser.d.ts +9 -0
  50. package/dist/parser.d.ts.map +1 -1
  51. package/dist/parser.js +22 -6
  52. package/dist/parser.js.map +1 -1
  53. package/dist/runtime.d.ts +20 -0
  54. package/dist/runtime.d.ts.map +1 -1
  55. package/dist/runtime.js +147 -24
  56. package/dist/runtime.js.map +1 -1
  57. package/dist/scheduler.d.ts +8 -0
  58. package/dist/scheduler.d.ts.map +1 -1
  59. package/dist/scheduler.js +3 -0
  60. package/dist/scheduler.js.map +1 -1
  61. package/dist/secrets.d.ts +119 -0
  62. package/dist/secrets.d.ts.map +1 -0
  63. package/dist/secrets.js +153 -0
  64. package/dist/secrets.js.map +1 -0
  65. package/dist/skill-catalog.d.ts.map +1 -1
  66. package/dist/skill-catalog.js +2 -0
  67. package/dist/skill-catalog.js.map +1 -1
  68. package/docs/adopter-playbook.md +80 -4
  69. package/docs/configuration.md +2 -2
  70. package/docs/connector-contract-reference.md +4 -0
  71. package/docs/language-reference.md +82 -5
  72. package/docs/sqlite-skill-store.md +3 -2
  73. package/examples/connectors/SkillStoreTemplate/SkillStoreTemplate.ts +19 -0
  74. package/examples/custom-bootstrap.example.ts +17 -7
  75. package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
  76. package/package.json +1 -1
  77. package/scaffold/.env.example +13 -0
package/dist/runtime.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { ParsedSkill } from "./parser.js";
2
2
  import type { DeliveryReceipt, WakeReceipt } from "./connectors/agent.js";
3
3
  import type { Registry } from "./connectors/registry.js";
4
4
  import type { TraceConfig, TraceStore } from "./trace.js";
5
+ import type { SecretProvider } from "./secrets.js";
5
6
  /**
6
7
  * Runtime executor. Pure mechanical execution: walks the parsed skill
7
8
  * tree, dispatches each op to its handler, threads variable state.
@@ -143,6 +144,14 @@ export interface ExecuteContext {
143
144
  */
144
145
  _currentSkillName?: string;
145
146
  _currentSkillEventType?: string | null;
147
+ /**
148
+ * v0.25.0 — the running skill's declared `# Requires: secret.NAME` set.
149
+ * Stashed by execute() so the sink resolver can enforce declare-before-spend
150
+ * at RUNTIME, not just at lint: a `{{secret.X}}` whose X isn't declared is
151
+ * refused even when the marker was built dynamically (e.g. `{{secret.${VAR}}}`
152
+ * resolved by substituteRuntime), which lint can't see statically. Internal.
153
+ */
154
+ _currentSkillSecretRequires?: string[];
146
155
  /** Skill identity for trace records. Optional — falls back to parsed.name + version inference. */
147
156
  skillVersion?: string;
148
157
  /**
@@ -167,6 +176,17 @@ export interface ExecuteContext {
167
176
  * Undefined → TraceBuilder mints fresh (existing v1 behavior).
168
177
  */
169
178
  preMintedTraceId?: string;
179
+ /**
180
+ * v0.25.0 — resolver for `{{secret.NAME}}` markers. Wired at every
181
+ * ExecuteContext build site (bootstrap / scheduler / mcp-server). When a
182
+ * skill places a marker in a sink op (shell / `$`) the runtime resolves it
183
+ * here, AT the sink, and injects the value use-only — never binding,
184
+ * emitting, or tracing it. Undefined → markers are unresolvable: a sink op
185
+ * carrying one throws (fail-closed) so a misconfigured runtime can't send a
186
+ * literal `{{secret.X}}` to a credential-expecting sink. The env-backed
187
+ * provider ships today; a vault provider swaps in here with no other change.
188
+ */
189
+ secretProvider?: SecretProvider;
170
190
  }
171
191
  /**
172
192
  * Structured op-error record in `result.errors[]`. Per ERD §8: each entry
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
package/dist/runtime.js CHANGED
@@ -12,6 +12,7 @@ import { isSecuredMode } from "./approval.js";
12
12
  import { classifyMutation, authorizationGranted, buildAuthorizationSuggestion, } from "./mutation-gate.js";
13
13
  import { TraceBuilder, shouldTraceFire } from "./trace.js";
14
14
  import { describeValueShape, isShapeWorthRecording } from "./observed-shape.js";
15
+ import { expandSecretMarkers, expandSecretMarkersInList, hasSecretMarker, extractSecretRefs } from "./secrets.js";
15
16
  /**
16
17
  * v0.18.5 — address-routing predicate per Perry's design call (thread
17
18
  * `c453afa2`). The presence of `@session` on the agent_id encodes
@@ -148,7 +149,7 @@ export async function execute(parsed, initialVars, order, ctx) {
148
149
  // v0.9.6 — stash skill identity onto ctx so deep-stack op handlers
149
150
  // (notify()) can read for DeliveryMeta without threading `parsed`
150
151
  // through every call hop. Internal convention; do not set from outside.
151
- ctx = { ...ctx, _currentSkillName: skillName, _currentSkillEventType: parsed.eventType };
152
+ ctx = { ...ctx, _currentSkillName: skillName, _currentSkillEventType: parsed.eventType, _currentSkillSecretRequires: parsed.secretRequires };
152
153
  const traceBuilder = shouldTraceFire(ctx.trace, triggerId, skillName)
153
154
  ? new TraceBuilder(skillName, ctx.skillVersion ?? "unknown", triggerCtx, { agent_id: ctx.agentId }, ctx.preMintedTraceId)
154
155
  : null;
@@ -472,6 +473,77 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
472
473
  if (isEffectfulOpKind(op.kind) && isSecuredMode() && ctx.effectsAuthorized !== true) {
473
474
  throw new SecuredModeEffectError(op.kind, targetName);
474
475
  }
476
+ // v0.25.0 — sink-side secret resolution. Expand `{{secret.NAME}}` markers
477
+ // AFTER ${VAR} substitution, immediately before the value reaches the sink
478
+ // (spawn argv / connector arg). Resolved values are use-only: they never
479
+ // bind to a var, emit, or trace. Fail-closed: a marker with no provider
480
+ // wired throws rather than sending a literal `{{secret.X}}` to a sink. Only
481
+ // the sink handlers (shell / `$`) call these; lint keeps markers out of
482
+ // every other op. (No-op when the text has no marker, so call freely.)
483
+ const requireSecretProvider = () => {
484
+ if (ctx.secretProvider === undefined) {
485
+ throw new OpError(`\`${op.kind}\` op references a secret ({{secret.…}}) but this runtime has no secret provider wired.`, op.kind, "Run via the dashboard/serve runtime (it wires the env secret provider) and provision SKILLSCRIPT_SECRET_<NAME> in the operator's .env.", targetName);
486
+ }
487
+ return ctx.secretProvider;
488
+ };
489
+ // Runtime declare-before-spend enforcement. A marker can be built dynamically
490
+ // (`{{secret.${VAR}}}` → substituteRuntime makes `{{secret.FLAG}}`), which the
491
+ // static lint can't see — so the runtime independently refuses any secret name
492
+ // not in the skill's `# Requires: secret.NAME` set. Fail-closed; keeps the
493
+ // approver-visible declaration an enforced contract, not just a lint hint.
494
+ const enforceDeclared = (text) => {
495
+ const declared = ctx._currentSkillSecretRequires ?? [];
496
+ for (const name of extractSecretRefs(text)) {
497
+ if (!declared.includes(name)) {
498
+ throw new OpError(`\`${op.kind}\` op references secret '${name}' which is not declared in the skill's \`# Requires:\`.`, op.kind, `Add \`# Requires: secret.${name}\` to the frontmatter. Secrets must be declared up-front so an approver can see which credentials the skill reaches (dynamically-named markers are refused for the same reason).`, targetName);
499
+ }
500
+ }
501
+ };
502
+ // Per-op set of resolved secret VALUES. Populated as markers resolve; used by
503
+ // redactSecrets() to scrub a value from any error/fallback-reason that gets
504
+ // recorded or rethrown (Perry Bug B fix #2 — belt-and-suspenders: a secret
505
+ // value must never appear anywhere but the live sink call, even if a sink or
506
+ // connector echoes it in a thrown error). The shared invariant: outside the
507
+ // successful spawn, everything renders the marker, never the value.
508
+ const injectedSecretValues = [];
509
+ const collectSecret = (_name, value) => {
510
+ if (value !== "")
511
+ injectedSecretValues.push(value);
512
+ };
513
+ const redactSecrets = (text) => {
514
+ let out = text;
515
+ for (const v of injectedSecretValues) {
516
+ if (v !== "")
517
+ out = out.split(v).join("{{secret.REDACTED}}");
518
+ }
519
+ return out;
520
+ };
521
+ // Scrub resolved secret values from a thrown error's message/remediation in
522
+ // place (preserves the error class + `instanceof` checks). No-op when no
523
+ // secret resolved for this op (so pre-resolution errors — e.g. the binary
524
+ // allowlist gate — pass through untouched).
525
+ const redactErr = (err) => {
526
+ if (injectedSecretValues.length === 0 || !(err instanceof Error))
527
+ return err;
528
+ err.message = redactSecrets(err.message);
529
+ const withRem = err;
530
+ if (typeof withRem.remediation === "string")
531
+ withRem.remediation = redactSecrets(withRem.remediation);
532
+ return err;
533
+ };
534
+ const resolveSecretsAtSink = async (text) => {
535
+ if (!hasSecretMarker(text))
536
+ return text;
537
+ enforceDeclared(text);
538
+ return expandSecretMarkers(text, requireSecretProvider(), { skillName: ctx._currentSkillName }, collectSecret);
539
+ };
540
+ const resolveSecretsAtSinkList = async (items) => {
541
+ if (!items.some(hasSecretMarker))
542
+ return items;
543
+ for (const item of items)
544
+ enforceDeclared(item);
545
+ return expandSecretMarkersInList(items, requireSecretProvider(), { skillName: ctx._currentSkillName }, collectSecret);
546
+ };
475
547
  switch (op.kind) {
476
548
  case "$set": {
477
549
  // v0.5.0 item 3 — `$set X = "...$(REF)..."` now resolves $(REF) at
@@ -575,21 +647,30 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
575
647
  lastValue: placeholder,
576
648
  };
577
649
  }
578
- const [bin, ...args] = substArgv;
579
650
  let stdoutArgv;
580
651
  try {
581
- // v0.18.8 binary-scope gate. argv[0] IS the binary by construction;
582
- // the allowlist applies identically to argv mode and command= mode.
583
- if (!isBinaryAllowed(bin, ctx.shellAllowlist)) {
584
- throw new ShellBinaryNotAllowedError(bin, ctx.shellAllowlist, targetName);
652
+ // v0.18.8 binary-scope gate. argv[0] IS the binary by construction.
653
+ // v0.25.0 gate the binary BEFORE resolving secrets, so a refused
654
+ // op never resolves the credential and never lands it in the error
655
+ // message (which is built from the binary token). The binary itself
656
+ // is never a secret.
657
+ const bin0 = substArgv[0];
658
+ if (!isBinaryAllowed(bin0, ctx.shellAllowlist)) {
659
+ throw new ShellBinaryNotAllowedError(bin0, ctx.shellAllowlist, targetName);
585
660
  }
661
+ // Resolve `{{secret.NAME}}` markers per-element AFTER the gate so a
662
+ // secret value with spaces/quotes stays one argv element (the argv
663
+ // form never re-tokenizes). Use-only: spliced straight into spawn,
664
+ // never bound or traced.
665
+ const resolvedArgv = await resolveSecretsAtSinkList(substArgv);
666
+ const [bin, ...args] = resolvedArgv;
586
667
  stdoutArgv = await execShellCommand(bin, args, shellTimeoutMs);
587
668
  }
588
669
  catch (err) {
589
670
  if (shellFallback !== undefined) {
590
- return recordShellFallback(shellFallback, `shell argv failed: ${messageOf(err)}`);
671
+ return recordShellFallback(shellFallback, redactSecrets(`shell argv failed: ${messageOf(err)}`));
591
672
  }
592
- throw err;
673
+ throw redactErr(err);
593
674
  }
594
675
  // Empty-stdout fallback (matches $-op trailer empty-result semantic).
595
676
  if (shellFallback !== undefined && stdoutArgv.trim() === "") {
@@ -637,28 +718,41 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
637
718
  if (!isBinaryAllowed("bash", ctx.shellAllowlist)) {
638
719
  throw new ShellBinaryNotAllowedError("bash", ctx.shellAllowlist, targetName);
639
720
  }
640
- stdout = await execShellCommand("bash", ["-c", body], shellTimeoutMs);
721
+ // v0.25.0 resolve markers into the bash script. NOTE: the
722
+ // resolved value lands in the `bash -c <script>` argv, which is
723
+ // process-list visible (the documented argv/`ps` leak window);
724
+ // vault-era sink-aware injection closes it. Unsafe shell is
725
+ // already operator-gated, so a secret here is a deliberate choice.
726
+ const resolvedUnsafeBody = await resolveSecretsAtSink(body);
727
+ stdout = await execShellCommand("bash", ["-c", resolvedUnsafeBody], shellTimeoutMs);
641
728
  }
642
729
  else {
643
730
  const tokens = tokenizeShellArgs(body);
644
731
  if (tokens.length === 0) {
645
732
  throw new OpError(`Empty \`shell(...)\` op body in target '${targetName}'.`, "shell", "Provide a non-empty `command=\"...\"` kwarg.", targetName);
646
733
  }
647
- const [bin, ...args] = tokens;
648
- // v0.18.8 binary-scope gate. Safe-path grammar guarantees
649
- // one binary + no metacharacters, so the first token IS the
650
- // binary. Default-deny when allowlist unset (BREAKING).
651
- if (!isBinaryAllowed(bin, ctx.shellAllowlist)) {
652
- throw new ShellBinaryNotAllowedError(bin, ctx.shellAllowlist, targetName);
734
+ // v0.18.8 binary-scope gate. Safe-path grammar guarantees one
735
+ // binary + no metacharacters, so the first token IS the binary.
736
+ // Default-deny when allowlist unset (BREAKING). v0.25.0 gate
737
+ // BEFORE resolving secrets so a refused op never resolves the
738
+ // credential and the error message (built from the binary token)
739
+ // carries the marker, never the value. The binary is never a secret.
740
+ if (!isBinaryAllowed(tokens[0], ctx.shellAllowlist)) {
741
+ throw new ShellBinaryNotAllowedError(tokens[0], ctx.shellAllowlist, targetName);
653
742
  }
743
+ // Resolve `{{secret.NAME}}` markers AFTER tokenization + gate so a
744
+ // secret value with spaces/quotes stays one argv element and never
745
+ // reaches a shell for re-splitting. Use-only.
746
+ const resolvedTokens = await resolveSecretsAtSinkList(tokens);
747
+ const [bin, ...args] = resolvedTokens;
654
748
  stdout = await execShellCommand(bin, args, shellTimeoutMs);
655
749
  }
656
750
  }
657
751
  catch (err) {
658
752
  if (shellFallback !== undefined) {
659
- return recordShellFallback(shellFallback, `shell failed: ${messageOf(err)}`);
753
+ return recordShellFallback(shellFallback, redactSecrets(`shell failed: ${messageOf(err)}`));
660
754
  }
661
- throw err;
755
+ throw redactErr(err);
662
756
  }
663
757
  // Empty-stdout fallback (matches $-op trailer empty-result semantic).
664
758
  if (shellFallback !== undefined && stdout.trim() === "") {
@@ -1002,13 +1096,26 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
1002
1096
  // fallback present, ditto.
1003
1097
  const dollarFallback = op.fallback !== undefined ? coerceLiteralValue(op.fallback) : undefined;
1004
1098
  try {
1099
+ // v0.25.0 — resolve `{{secret.NAME}}` markers in string-valued args
1100
+ // just before the real connector dispatch (use-only). The built-in
1101
+ // execute_skill / json_parse intercepts ran earlier and never reach
1102
+ // here, so a secret value can't leak into a child skill's scope or a
1103
+ // parsed/bound var — it exists only for this one dispatch call.
1104
+ let dispatchArgs = args;
1105
+ if (Object.values(args).some((v) => typeof v === "string" && hasSecretMarker(v))) {
1106
+ dispatchArgs = { ...args };
1107
+ for (const [k, v] of Object.entries(args)) {
1108
+ if (typeof v === "string")
1109
+ dispatchArgs[k] = await resolveSecretsAtSink(v);
1110
+ }
1111
+ }
1005
1112
  if (ctx.registry.hasMcpConnector(connectorName)) {
1006
1113
  const connector = ctx.registry.getMcpConnector(connectorName);
1007
- rawResult = await dispatchWithTimeout(() => connector.call(toolName, args, ctx.agentId !== undefined ? { agentId: ctx.agentId } : undefined), timeoutMs, "$");
1114
+ rawResult = await dispatchWithTimeout(() => connector.call(toolName, dispatchArgs, ctx.agentId !== undefined ? { agentId: ctx.agentId } : undefined), timeoutMs, "$");
1008
1115
  dispatched = true;
1009
1116
  }
1010
1117
  else if (op.mcpConnector === undefined && ctx.toolDispatch) {
1011
- rawResult = await dispatchWithTimeout(() => ctx.toolDispatch(toolName, args), timeoutMs, "$");
1118
+ rawResult = await dispatchWithTimeout(() => ctx.toolDispatch(toolName, dispatchArgs), timeoutMs, "$");
1012
1119
  dispatched = true;
1013
1120
  }
1014
1121
  else {
@@ -1034,11 +1141,13 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
1034
1141
  target: targetName,
1035
1142
  opKind: "$",
1036
1143
  value: dollarFallback,
1037
- reason: `$ ${connectorLabel}${toolName} failed: ${messageOf(err)}`,
1144
+ // v0.25.0 a connector may echo a resolved secret arg in its
1145
+ // error; scrub it before this reason lands in the trace.
1146
+ reason: redactSecrets(`$ ${connectorLabel}${toolName} failed: ${messageOf(err)}`),
1038
1147
  });
1039
1148
  return { lastBoundVar: op.outputVar ?? flatKey, lastValue: dollarFallback };
1040
1149
  }
1041
- throw err;
1150
+ throw redactErr(err);
1042
1151
  }
1043
1152
  // c580de5: surface inner-tool `isError: true` as an op error. Otherwise
1044
1153
  // the error text gets bound silently to the output var and the skill
@@ -1048,7 +1157,9 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
1048
1157
  typeof rawResult === "object" &&
1049
1158
  rawResult.isError === true) {
1050
1159
  const innerText = extractToolErrorText(rawResult);
1051
- throw new OpError(`tool ${connectorLabel}${toolName} returned isError: ${innerText}`, "$", "The tool itself failed; inspect the inner error text. Add `(fallback: ...)` to the op for graceful failure.", targetName);
1160
+ throw new OpError(
1161
+ // v0.25.0 — the connector's error text may echo a resolved secret arg.
1162
+ redactSecrets(`tool ${connectorLabel}${toolName} returned isError: ${innerText}`), "$", "The tool itself failed; inspect the inner error text. Add `(fallback: ...)` to the op for graceful failure.", targetName);
1052
1163
  }
1053
1164
  const bindValue = unwrapToolResult(rawResult);
1054
1165
  // v0.23.0 — observed output-shape capture. For a qualified `$ conn.tool`
@@ -1550,9 +1661,21 @@ export function substituteRuntime(text, vars) {
1550
1661
  // through (the silent-blank case Perry hit with `gh pr list` writing
1551
1662
  // nothing to stdout). Now: same semantic across both fallback surfaces.
1552
1663
  return text.replace(
1664
+ // v0.25.0 (Perry red-team `d8a5ad0a` Bug A, fix #1): a `{{secret...}}`
1665
+ // marker is OPAQUE to var-substitution. Group 1 captures any double-brace
1666
+ // secret marker (incl. a dynamically-shaped interior like `{{secret.${NM}}}`)
1667
+ // and the callback returns it verbatim, so `${VAR}` substitution can NEVER
1668
+ // reach inside a marker to build a secret name at runtime. The secret name
1669
+ // therefore stays exactly as authored — a compile-time literal — which the
1670
+ // sink resolver + lint then validate against `# Requires`. `[^}]*` (not
1671
+ // `[^{}]*`) is deliberate so the interior `${...}` is consumed here, not by
1672
+ // the var-ref alternative below.
1553
1673
  // v0.7.0: alternation accepts both `$(REF|chain)` (legacy) and `${REF|chain}`
1554
- // (canonical). Capture groups 1+2 = paren form, 3+4 = brace form.
1555
- /\$(?:\(([^|)\s]+)\s*((?:\|\s*[A-Za-z_]\w*(?:\s*:\s*"[^"]*")?\s*)*)\)|\{([^|}\s]+)\s*((?:\|\s*[A-Za-z_]\w*(?:\s*:\s*"[^"]*")?\s*)*)\})/g, (_match, ref1, fc1, ref2, fc2) => {
1674
+ // (canonical). Capture groups 2+3 = paren form, 4+5 = brace form.
1675
+ /(\{\{\s*secret\.[^}]*\}\})|\$(?:\(([^|)\s]+)\s*((?:\|\s*[A-Za-z_]\w*(?:\s*:\s*"[^"]*")?\s*)*)\)|\{([^|}\s]+)\s*((?:\|\s*[A-Za-z_]\w*(?:\s*:\s*"[^"]*")?\s*)*)\})/g, (_match, secretMarker, ref1, fc1, ref2, fc2) => {
1676
+ // Opaque passthrough — never resolve inside a secret marker.
1677
+ if (secretMarker !== undefined)
1678
+ return secretMarker;
1556
1679
  const ref = (ref1 ?? ref2);
1557
1680
  const filterChain = fc1 ?? fc2 ?? "";
1558
1681
  let value = resolveRef(ref, vars);