@checkstack/healthcheck-script-backend 0.5.0 → 0.6.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 CHANGED
@@ -1,5 +1,123 @@
1
1
  # @checkstack/healthcheck-script-backend
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 270ef29: Extend in-UI script testing to health-check collectors, and add
8
+ load-from-run replay for automation script tests.
9
+
10
+ - Health-check collectors: a new `testCollectorScript` RPC runs the
11
+ inline-script (TypeScript) collector and the shell `script` collector
12
+ against an editable, auto-seeded sample context using the same
13
+ sandboxed runner the real collector uses. Surfaces beneath the
14
+ collector script fields in the collector editor (both marked
15
+ `x-script-testable`). Gated by `healthcheck.configuration.manage`.
16
+ - Automation replay: a new `getRunScopeForReplay` RPC reconstructs an
17
+ editable test context from a real run (trigger + persisted artifacts,
18
+ plus the durable scope snapshot when the run is still in-flight), and
19
+ the script-test panel gains a "Load from run" picker that seeds the
20
+ sample context from a past run.
21
+
22
+ Note: health-check executions do not persist the script / config /
23
+ check / system that produced a result, so there is no health-check
24
+ replay - auto-seed is the only context source for collector tests. This
25
+ is by design; see the feature plan.
26
+
27
+ - 270ef29: Activate npm packages in script execution: thread the managed
28
+ `resolutionRoot` into every user-script call site so an allowlisted package
29
+ can actually be `import`ed.
30
+
31
+ - `@checkstack/backend-api`: the ESM runner now always writes a per-run
32
+ `bunfig.toml` with `[install] auto = "disable"` and runs with that dir as
33
+ CWD. Without this Bun silently auto-installs any imported package from the
34
+ registry (verified), defeating the allowlist; with it, imports resolve
35
+ only against the reconciled `current/node_modules` (when a `resolutionRoot`
36
+ is set) and otherwise fail fast.
37
+ - `@checkstack/script-packages-backend`: `resolveResolutionRoot` /
38
+ `resolveResolutionRootFromStore` / `resolveResolutionRootForHost` decide a
39
+ host's resolution-root status (`none` / `ready` / `notReady`) from the
40
+ local `<store>/current`.
41
+ - `run_script` (integration-script-backend), the inline-script collector
42
+ (healthcheck-script-backend, core + satellite), and the in-UI `testScript`
43
+ / `testCollectorScript` endpoints all resolve the root per run and pass it
44
+ to the runner; `run_script` surfaces a clear "npm packages not ready"
45
+ error when configured-but-unsynced. Shell paths are unaffected (no module
46
+ resolution).
47
+
48
+ An opt-in end-to-end test (`CHECKSTACK_E2E_NETWORK=1`) proves an allowlisted
49
+ package imports successfully through the real `run_script` action execute
50
+ path, with non-network degradation tests running always.
51
+
52
+ BREAKING CHANGES: `@checkstack/backend-api`'s `defaultEsmScriptRunner` now
53
+ always disables Bun auto-install for the user subprocess. A script that
54
+ previously relied on Bun silently fetching an un-vendored package from the
55
+ registry at import time will now fail to resolve it. This is intentional -
56
+ package availability is governed by the admin allowlist - but any caller
57
+ depending on the old implicit auto-install behavior must add the package to
58
+ the allowlist instead. The new `EsmScriptRunOptions.resolutionRoot` field is
59
+ optional and additive (defaults to today's `os.tmpdir()` behavior when
60
+ unset), so the runner API itself is source-compatible.
61
+
62
+ - 270ef29: Secrets platform Phase 3: just-in-time secret delivery to satellites + source-side masking, and central-execution injection for healthcheck collectors.
63
+
64
+ - New satellite WS messages `request_run_secrets` / `run_secrets`: just
65
+ before a satellite runs a collector that declares a `secretEnv`, it asks
66
+ core for that collector's resolved env; core resolves ONLY the secrets the
67
+ collector's OWN persisted assignment declares (least-privilege — the
68
+ satellite cannot choose) and replies with the env map (or a clear error).
69
+ The satellite injects it memory-only for the run and drops it on
70
+ completion. Secrets never ride the persisted assignment and never touch
71
+ disk.
72
+ - Source-side masking: the satellite runs `maskSecrets` over the collector's
73
+ stdout/stderr/result/error using the run's delivered values BEFORE the
74
+ result leaves the satellite (defense in depth).
75
+ - `CollectorStrategy.execute` gains an optional `secretEnv`. The
76
+ inline-script and shell collectors inject it into the runner
77
+ (`process.env` / `$VAR`) and mask the values out of their output.
78
+ - Healthcheck collectors running centrally (the queue executor) also resolve
79
+ - inject `secretEnv` via `secretResolverRef`, closing the gap where a
80
+ centrally-run secretEnv collector got no secrets. A missing required
81
+ secret fails the run clearly in all paths.
82
+
83
+ ### Patch Changes
84
+
85
+ - Updated dependencies [270ef29]
86
+ - Updated dependencies [270ef29]
87
+ - Updated dependencies [270ef29]
88
+ - Updated dependencies [270ef29]
89
+ - Updated dependencies [270ef29]
90
+ - Updated dependencies [270ef29]
91
+ - Updated dependencies [b995afb]
92
+ - Updated dependencies [b995afb]
93
+ - Updated dependencies [b995afb]
94
+ - Updated dependencies [270ef29]
95
+ - Updated dependencies [270ef29]
96
+ - Updated dependencies [270ef29]
97
+ - Updated dependencies [270ef29]
98
+ - Updated dependencies [270ef29]
99
+ - Updated dependencies [b995afb]
100
+ - Updated dependencies [b995afb]
101
+ - Updated dependencies [270ef29]
102
+ - Updated dependencies [270ef29]
103
+ - Updated dependencies [270ef29]
104
+ - Updated dependencies [b995afb]
105
+ - Updated dependencies [270ef29]
106
+ - Updated dependencies [b995afb]
107
+ - Updated dependencies [270ef29]
108
+ - Updated dependencies [270ef29]
109
+ - Updated dependencies [b995afb]
110
+ - Updated dependencies [270ef29]
111
+ - Updated dependencies [270ef29]
112
+ - Updated dependencies [270ef29]
113
+ - Updated dependencies [270ef29]
114
+ - Updated dependencies [270ef29]
115
+ - Updated dependencies [b995afb]
116
+ - @checkstack/backend-api@0.19.0
117
+ - @checkstack/healthcheck-common@1.4.0
118
+ - @checkstack/script-packages-backend@0.2.0
119
+ - @checkstack/secrets-common@0.1.0
120
+
3
121
  ## 0.5.0
4
122
 
5
123
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-script-backend",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "checkstack": {
@@ -14,15 +14,17 @@
14
14
  "pack": "bunx @checkstack/scripts plugin-pack"
15
15
  },
16
16
  "dependencies": {
17
- "@checkstack/backend-api": "0.17.1",
18
- "@checkstack/common": "0.11.0",
19
- "@checkstack/healthcheck-common": "1.2.0"
17
+ "@checkstack/backend-api": "0.18.0",
18
+ "@checkstack/common": "0.12.0",
19
+ "@checkstack/healthcheck-common": "1.3.0",
20
+ "@checkstack/script-packages-backend": "0.1.0",
21
+ "@checkstack/secrets-common": "0.0.1"
20
22
  },
21
23
  "devDependencies": {
22
24
  "@types/bun": "^1.0.0",
23
25
  "typescript": "^5.0.0",
24
26
  "@checkstack/tsconfig": "0.0.7",
25
- "@checkstack/scripts": "0.3.3"
27
+ "@checkstack/scripts": "0.3.4"
26
28
  },
27
29
  "description": "Checkstack healthcheck-script-backend plugin",
28
30
  "author": {
@@ -294,3 +294,71 @@ describe("ExecuteCollector", () => {
294
294
  });
295
295
  });
296
296
  });
297
+
298
+ describe("ExecuteCollector secret env injection + source-side masking", () => {
299
+ // Capture the env the collector passes to the transport client. Typed
300
+ // so we can assert without indexing the loosely-typed mock.calls tuple.
301
+ const makeRecordingClient = (
302
+ response: { stdout: string; stderr: string },
303
+ ): {
304
+ client: ScriptTransportClient;
305
+ getEnv: () => Record<string, string> | undefined;
306
+ } => {
307
+ let capturedEnv: Record<string, string> | undefined;
308
+ return {
309
+ getEnv: () => capturedEnv,
310
+ client: {
311
+ exec: async (input) => {
312
+ capturedEnv = input.env;
313
+ return {
314
+ exitCode: 0,
315
+ stdout: response.stdout,
316
+ stderr: response.stderr,
317
+ timedOut: false,
318
+ };
319
+ },
320
+ },
321
+ };
322
+ };
323
+
324
+ it("injects the delivered secretEnv into the shell exec and masks it from output", async () => {
325
+ const { client, getEnv } = makeRecordingClient({
326
+ stdout: "value=sh-secret-987",
327
+ stderr: "warn sh-secret-987",
328
+ });
329
+ const collector = new ExecuteCollector();
330
+
331
+ const result = await collector.execute({
332
+ config: {
333
+ script: "echo value=$TOKEN",
334
+ timeout: 5000,
335
+ secretEnv: { TOKEN: "${{ secrets.tok }}" },
336
+ },
337
+ client,
338
+ pluginId: "test",
339
+ secretEnv: { TOKEN: "sh-secret-987" },
340
+ });
341
+
342
+ // The exec received the injected env.
343
+ expect(getEnv()?.TOKEN).toBe("sh-secret-987");
344
+ // The secret is redacted from stdout/stderr (source-side).
345
+ expect(result.result.stdout).toBe("value=****");
346
+ expect(result.result.stderr).toBe("warn ****");
347
+ expect(JSON.stringify(result)).not.toContain("sh-secret-987");
348
+ });
349
+
350
+ it("does not inject or mask when no secretEnv is delivered", async () => {
351
+ const { client, getEnv } = makeRecordingClient({
352
+ stdout: "no secrets",
353
+ stderr: "",
354
+ });
355
+ const collector = new ExecuteCollector();
356
+ const result = await collector.execute({
357
+ config: { script: "echo hi", timeout: 5000 },
358
+ client,
359
+ pluginId: "test",
360
+ });
361
+ expect(getEnv()?.TOKEN).toBeUndefined();
362
+ expect(result.result.stdout).toBe("no secrets");
363
+ });
364
+ });
@@ -2,6 +2,7 @@ import {
2
2
  Versioned,
3
3
  z,
4
4
  configString,
5
+ withConfigMeta,
5
6
  type HealthCheckRunForAggregation,
6
7
  type CollectorResult,
7
8
  type CollectorStrategy,
@@ -21,6 +22,10 @@ import {
21
22
  } from "@checkstack/healthcheck-common";
22
23
  import { pluginMetadata } from "./plugin-metadata";
23
24
  import type { ScriptTransportClient } from "./transport-client";
25
+ import {
26
+ secretEnvMappingSchema,
27
+ maskScriptRunOutput,
28
+ } from "@checkstack/secrets-common";
24
29
 
25
30
  // ============================================================================
26
31
  // RUN-CONTEXT ENV INJECTION
@@ -82,6 +87,7 @@ function shellQuote(arg: string): string {
82
87
  const executeConfigSchemaV2 = z.object({
83
88
  script: configString({
84
89
  "x-editor-types": ["shell"],
90
+ "x-script-testable": true,
85
91
  }).describe(
86
92
  "Shell script source. Executed via `sh -c`, so pipes, redirects, `if`/`for`/`while`, variable expansion, command substitution etc. all work. Exit non-zero to fail the check.",
87
93
  ),
@@ -95,6 +101,11 @@ const executeConfigSchemaV2 = z.object({
95
101
  .describe(
96
102
  "Extra environment variables to expose to the script. Merged on top of the safe-vars whitelist (PATH, HOME, ...).",
97
103
  ),
104
+ secretEnv: withConfigMeta(secretEnvMappingSchema, { "x-secret-env": true })
105
+ .optional()
106
+ .describe(
107
+ 'Secret → env mapping, e.g. { "API_TOKEN": "${{ secrets.token }}" }. Only the named secrets are resolved and injected for this run (read via process.env.API_TOKEN / $API_TOKEN); on a satellite they are delivered just-in-time over the encrypted channel, never persisted. Values are masked out of the collector output.',
108
+ ),
98
109
  timeout: z
99
110
  .number()
100
111
  .int()
@@ -243,20 +254,25 @@ export class ExecuteCollector implements CollectorStrategy<
243
254
  config,
244
255
  client,
245
256
  runContext,
257
+ secretEnv,
246
258
  }: {
247
259
  config: ExecuteConfig;
248
260
  client: ScriptTransportClient;
249
261
  pluginId: string;
250
262
  runContext?: CollectorRunContext;
263
+ secretEnv?: Record<string, string>;
251
264
  }): Promise<CollectorResult<ExecuteResult>> {
252
265
  const startTime = Date.now();
253
266
 
254
- // Merge run-context metadata under the user-supplied env so a user
255
- // `config.env` key always wins on collision (matches the runner's
256
- // existing merge order against the safe-vars whitelist).
257
- const env = runContext
258
- ? { ...runContextEnv(runContext), ...config.env }
259
- : config.env;
267
+ // Merge run-context metadata under the user-supplied env, then the
268
+ // resolved secret env on top (a declared secret env name wins, matching
269
+ // the central action's layering). User `config.env` still wins over the
270
+ // run-context vars.
271
+ const env = {
272
+ ...(runContext ? runContextEnv(runContext) : {}),
273
+ ...config.env,
274
+ ...secretEnv,
275
+ };
260
276
 
261
277
  const response = await client.exec({
262
278
  script: config.script,
@@ -265,20 +281,33 @@ export class ExecuteCollector implements CollectorStrategy<
265
281
  timeout: config.timeout,
266
282
  });
267
283
 
284
+ // Source-side masking: redact the delivered secret values from the
285
+ // captured output BEFORE it leaves the satellite (core masks again on
286
+ // receipt). A script echoing a secret it was given is masked here.
287
+ const maskValues = Object.values(secretEnv ?? {});
288
+ const masked = maskScriptRunOutput({
289
+ output: {
290
+ stdout: response.stdout,
291
+ stderr: response.stderr,
292
+ error: response.error,
293
+ },
294
+ values: maskValues,
295
+ });
296
+
268
297
  const executionTimeMs = Date.now() - startTime;
269
298
  const success = response.exitCode === 0 && !response.timedOut;
270
299
 
271
300
  return {
272
301
  result: {
273
302
  exitCode: response.exitCode,
274
- stdout: response.stdout,
275
- stderr: response.stderr,
303
+ stdout: masked.stdout,
304
+ stderr: masked.stderr,
276
305
  executionTimeMs,
277
306
  success,
278
307
  timedOut: response.timedOut,
279
308
  },
280
309
  error:
281
- response.error ??
310
+ masked.error ??
282
311
  (success ? undefined : `Exit code: ${response.exitCode}`),
283
312
  };
284
313
  }
package/src/index.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
2
+ import {
3
+ resolveResolutionRootFromStore,
4
+ resolveScriptPackagesDir,
5
+ } from "@checkstack/script-packages-backend";
2
6
  import { ScriptHealthCheckStrategy } from "./strategy";
3
7
  import { pluginMetadata } from "./plugin-metadata";
4
8
  import { ExecuteCollector } from "./execute-collector";
5
- import { InlineScriptCollector } from "./inline-script-collector";
9
+ import {
10
+ InlineScriptCollector,
11
+ defaultInlineScriptExecutor,
12
+ } from "./inline-script-collector";
6
13
 
7
14
  export default createBackendPlugin({
8
15
  metadata: pluginMetadata,
@@ -18,7 +25,17 @@ export default createBackendPlugin({
18
25
  const strategy = new ScriptHealthCheckStrategy();
19
26
  healthCheckRegistry.register(strategy);
20
27
  collectorRegistry.register(new ExecuteCollector());
21
- collectorRegistry.register(new InlineScriptCollector());
28
+ // The inline TS collector resolves the managed npm-package tree from
29
+ // the local store - identical on core and satellites (same
30
+ // `<dataDir>/script-packages/current` convention). No RPC needed, so
31
+ // it works in the satellite runtime too. Execution safety is
32
+ // guaranteed by the runner (auto-install disabled); this just points
33
+ // it at the synced tree when present.
34
+ collectorRegistry.register(
35
+ new InlineScriptCollector(defaultInlineScriptExecutor, () =>
36
+ resolveResolutionRootFromStore(resolveScriptPackagesDir()),
37
+ ),
38
+ );
22
39
  },
23
40
  });
24
41
  },
@@ -2,6 +2,7 @@ import { describe, it, expect } from "bun:test";
2
2
  import {
3
3
  InlineScriptCollector,
4
4
  type InlineScriptConfig,
5
+ type InlineScriptExecutor,
5
6
  } from "./inline-script-collector";
6
7
  import type { ScriptTransportClient } from "./transport-client";
7
8
 
@@ -404,3 +405,55 @@ describe("InlineScriptCollector", () => {
404
405
  });
405
406
  });
406
407
  });
408
+
409
+ describe("InlineScriptCollector secret env injection + source-side masking", () => {
410
+ it("injects the delivered secretEnv into the executor and masks it from output", async () => {
411
+ const calls: Array<{ secretEnv?: Record<string, string> }> = [];
412
+ const mockExecutor: InlineScriptExecutor = {
413
+ execute: async (input) => {
414
+ calls.push({ secretEnv: input.secretEnv });
415
+ // Simulate a script that echoes its injected secret.
416
+ return {
417
+ result: { success: true, message: "token is gh_realSecret123" },
418
+ stdout: "logged gh_realSecret123",
419
+ stderr: "",
420
+ timedOut: false,
421
+ };
422
+ },
423
+ };
424
+ const collector = new InlineScriptCollector(mockExecutor);
425
+
426
+ const result = await collector.execute({
427
+ config: createConfig({
428
+ secretEnv: { API_TOKEN: "${{ secrets.tok }}" },
429
+ }),
430
+ client: mockClient,
431
+ pluginId: "script",
432
+ secretEnv: { API_TOKEN: "gh_realSecret123" },
433
+ });
434
+
435
+ // The executor received the injected env.
436
+ expect(calls[0]?.secretEnv).toEqual({ API_TOKEN: "gh_realSecret123" });
437
+ // The secret is redacted from the result message (source-side).
438
+ expect(result.result.message).toBe("token is ****");
439
+ expect(JSON.stringify(result)).not.toContain("gh_realSecret123");
440
+ });
441
+
442
+ it("does not mask anything when no secretEnv is delivered", async () => {
443
+ const mockExecutor: InlineScriptExecutor = {
444
+ execute: async () => ({
445
+ result: { success: true, message: "nothing secret" },
446
+ stdout: "plain output",
447
+ stderr: "",
448
+ timedOut: false,
449
+ }),
450
+ };
451
+ const collector = new InlineScriptCollector(mockExecutor);
452
+ const result = await collector.execute({
453
+ config: createConfig(),
454
+ client: mockClient,
455
+ pluginId: "script",
456
+ });
457
+ expect(result.result.message).toBe("nothing secret");
458
+ });
459
+ });
@@ -2,6 +2,7 @@ import {
2
2
  Versioned,
3
3
  z,
4
4
  configString,
5
+ withConfigMeta,
5
6
  requestTimeoutMs,
6
7
  defaultEsmScriptRunner,
7
8
  type EsmScriptRunResult,
@@ -25,6 +26,11 @@ import {
25
26
  import { pluginMetadata } from "./plugin-metadata";
26
27
  import type { ScriptTransportClient } from "./transport-client";
27
28
  import { extractErrorMessage } from "@checkstack/common";
29
+ import {
30
+ secretEnvMappingSchema,
31
+ maskScriptRunOutput,
32
+ } from "@checkstack/secrets-common";
33
+ import type { ResolutionRootStatus } from "@checkstack/script-packages-backend";
28
34
 
29
35
  // ============================================================================
30
36
  // EXECUTOR ADAPTER
@@ -58,6 +64,10 @@ export interface InlineScriptExecutor {
58
64
  config: Record<string, unknown>;
59
65
  timeoutMs: number;
60
66
  runContext?: CollectorRunContext;
67
+ /** Managed npm-package resolution root for this run, if ready. */
68
+ resolutionRoot?: string;
69
+ /** Resolved secret env injected into the runner for this run only. */
70
+ secretEnv?: Record<string, string>;
61
71
  }): Promise<InlineScriptExecutionResult>;
62
72
  }
63
73
 
@@ -67,8 +77,15 @@ export interface InlineScriptExecutor {
67
77
  * health-check runtime surface) and the virtual `@checkstack/healthcheck`
68
78
  * module / global `defineHealthCheck` helper.
69
79
  */
70
- const defaultInlineScriptExecutor: InlineScriptExecutor = {
71
- async execute({ script, config, timeoutMs, runContext }) {
80
+ export const defaultInlineScriptExecutor: InlineScriptExecutor = {
81
+ async execute({
82
+ script,
83
+ config,
84
+ timeoutMs,
85
+ runContext,
86
+ resolutionRoot,
87
+ secretEnv,
88
+ }) {
72
89
  const res: EsmScriptRunResult = await defaultEsmScriptRunner.run({
73
90
  script,
74
91
  context: {
@@ -80,6 +97,11 @@ const defaultInlineScriptExecutor: InlineScriptExecutor = {
80
97
  timeoutMs,
81
98
  helperModuleName: "@checkstack/healthcheck",
82
99
  helperFunctionName: "defineHealthCheck",
100
+ ...(resolutionRoot ? { resolutionRoot } : {}),
101
+ // Inject the resolved secrets as process.env for THIS run only.
102
+ ...(secretEnv && Object.keys(secretEnv).length > 0
103
+ ? { env: secretEnv }
104
+ : {}),
83
105
  });
84
106
  return res;
85
107
  },
@@ -92,9 +114,15 @@ const defaultInlineScriptExecutor: InlineScriptExecutor = {
92
114
  const inlineScriptConfigSchema = z.object({
93
115
  script: configString({
94
116
  "x-editor-types": ["typescript"],
117
+ "x-script-testable": true,
95
118
  }).describe(
96
119
  "TypeScript/JavaScript module. Use `import { ... } from \"node:os\"` to pull in Node built-ins. The recommended pattern is `export default defineHealthCheck({ success, message?, value? })` — `defineHealthCheck` is provided by `@checkstack/healthcheck` and asserts the return shape at the type level. Throwing also signals failure.",
97
120
  ),
121
+ secretEnv: withConfigMeta(secretEnvMappingSchema, { "x-secret-env": true })
122
+ .optional()
123
+ .describe(
124
+ 'Secret → env mapping, e.g. { "API_TOKEN": "${{ secrets.token }}" }. Only the named secrets are resolved and injected for this run (read via process.env.API_TOKEN / $API_TOKEN); on a satellite they are delivered just-in-time over the encrypted channel, never persisted. Values are masked out of the collector output.',
125
+ ),
98
126
  timeout: requestTimeoutMs().describe("Maximum execution time in milliseconds"),
99
127
  });
100
128
 
@@ -254,30 +282,65 @@ export class InlineScriptCollector implements CollectorStrategy<
254
282
  });
255
283
 
256
284
  private executor: InlineScriptExecutor;
285
+ private getResolutionRoot?: () => Promise<ResolutionRootStatus>;
257
286
 
258
- constructor(executor: InlineScriptExecutor = defaultInlineScriptExecutor) {
287
+ constructor(
288
+ executor: InlineScriptExecutor = defaultInlineScriptExecutor,
289
+ getResolutionRoot?: () => Promise<ResolutionRootStatus>,
290
+ ) {
259
291
  this.executor = executor;
292
+ this.getResolutionRoot = getResolutionRoot;
260
293
  }
261
294
 
262
295
  async execute({
263
296
  config,
264
297
  runContext,
298
+ secretEnv,
265
299
  }: {
266
300
  config: InlineScriptConfig;
267
301
  client: ScriptTransportClient;
268
302
  pluginId: string;
269
303
  runContext?: CollectorRunContext;
304
+ secretEnv?: Record<string, string>;
270
305
  }): Promise<CollectorResult<InlineScriptResult>> {
271
306
  const startTime = Date.now();
272
307
 
308
+ // Resolve the managed npm-package root. `notReady` -> fail clearly;
309
+ // `none` -> unset (no packages); `ready` -> point the runner at it.
310
+ const rootStatus = await this.getResolutionRoot?.();
311
+ if (rootStatus?.mode === "notReady") {
312
+ return {
313
+ result: {
314
+ success: false,
315
+ message: rootStatus.reason,
316
+ executionTimeMs: Date.now() - startTime,
317
+ timedOut: false,
318
+ },
319
+ error: rootStatus.reason,
320
+ };
321
+ }
322
+ const resolutionRoot =
323
+ rootStatus?.mode === "ready" ? rootStatus.root : undefined;
324
+
325
+ // Source-side masking values: the run's delivered secret values.
326
+ const maskValues = Object.values(secretEnv ?? {});
327
+
273
328
  let exec: InlineScriptExecutionResult;
274
329
  try {
275
- exec = await this.executor.execute({
330
+ const raw = await this.executor.execute({
276
331
  script: config.script,
277
332
  config: config as unknown as Record<string, unknown>,
278
333
  timeoutMs: config.timeout,
279
334
  runContext,
335
+ ...(resolutionRoot ? { resolutionRoot } : {}),
336
+ ...(secretEnv ? { secretEnv } : {}),
280
337
  });
338
+ // Redact the delivered secret values from the captured output BEFORE
339
+ // any of it leaves the satellite (defense in depth: core masks again
340
+ // on receipt). A script echoing a secret it was given is masked here.
341
+ // `raw` carries `stdout`/`stderr` (required by ScriptRunOutput) plus
342
+ // `timedOut`, so the masked result keeps the full shape.
343
+ exec = maskScriptRunOutput({ output: raw, values: maskValues });
281
344
  } catch (error) {
282
345
  const executionTimeMs = Date.now() - startTime;
283
346
  const message = extractErrorMessage(error);
package/tsconfig.json CHANGED
@@ -12,6 +12,12 @@
12
12
  },
13
13
  {
14
14
  "path": "../../core/healthcheck-common"
15
+ },
16
+ {
17
+ "path": "../../core/script-packages-backend"
18
+ },
19
+ {
20
+ "path": "../../core/secrets-common"
15
21
  }
16
22
  ]
17
23
  }