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.
- package/CHANGELOG.md +22 -0
- package/README.md +2 -1
- package/UPGRADING.md +9 -1
- package/dist/bootstrap-from-env.d.ts +46 -0
- package/dist/bootstrap-from-env.d.ts.map +1 -0
- package/dist/bootstrap-from-env.js +130 -0
- package/dist/bootstrap-from-env.js.map +1 -0
- package/dist/bootstrap.d.ts +10 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +7 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.js +23 -164
- package/dist/cli.js.map +1 -1
- package/dist/connectors/registry.d.ts +9 -0
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +23 -0
- package/dist/connectors/registry.js.map +1 -1
- package/dist/connectors/skill-store.d.ts +1 -0
- package/dist/connectors/skill-store.d.ts.map +1 -1
- package/dist/connectors/skill-store.js +27 -0
- package/dist/connectors/skill-store.js.map +1 -1
- package/dist/connectors/sqlite-skill-store.d.ts +1 -0
- package/dist/connectors/sqlite-skill-store.d.ts.map +1 -1
- package/dist/connectors/sqlite-skill-store.js +10 -0
- package/dist/connectors/sqlite-skill-store.js.map +1 -1
- package/dist/connectors/types.d.ts +19 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/types.js.map +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +4 -0
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/spa/app.js +33 -5
- package/dist/help-content.d.ts.map +1 -1
- package/dist/help-content.js +3 -0
- package/dist/help-content.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +145 -0
- package/dist/lint.js.map +1 -1
- package/dist/mcp-server.d.ts +8 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +24 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +22 -6
- package/dist/parser.js.map +1 -1
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +147 -24
- package/dist/runtime.js.map +1 -1
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +3 -0
- package/dist/scheduler.js.map +1 -1
- package/dist/secrets.d.ts +119 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +153 -0
- package/dist/secrets.js.map +1 -0
- package/dist/skill-catalog.d.ts.map +1 -1
- package/dist/skill-catalog.js +2 -0
- package/dist/skill-catalog.js.map +1 -1
- package/docs/adopter-playbook.md +80 -4
- package/docs/configuration.md +2 -2
- package/docs/connector-contract-reference.md +4 -0
- package/docs/language-reference.md +82 -5
- package/docs/sqlite-skill-store.md +3 -2
- package/examples/connectors/SkillStoreTemplate/SkillStoreTemplate.ts +19 -0
- package/examples/custom-bootstrap.example.ts +17 -7
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/package.json +1 -1
- 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
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAuB,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAgB,eAAe,EAAE,WAAW,EAAY,MAAM,uBAAuB,CAAC;AAKlG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AA4BzD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG1D;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;OAeG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+HAA+H;IAC/H,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrF,qGAAqG;IACrG,qBAAqB,CAAC,EAAE,CACtB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC1B,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5B,kFAAkF;IAClF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxF;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,kGAAkG;IAClG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;
|
|
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
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
|
1555
|
-
|
|
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);
|