@cat-factory/executor-harness 1.37.2 → 1.39.2
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/dist/agent.js +41 -4
- package/dist/job.js +37 -0
- package/package.json +3 -3
- package/src/agent.ts +41 -4
- package/src/job.ts +63 -0
package/dist/agent.js
CHANGED
|
@@ -5,7 +5,7 @@ import { execFile } from 'node:child_process';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { standUpFrontend, tearDownFrontend } from './frontend-infra.js';
|
|
7
7
|
import { configurePackageRegistries } from './package-registries.js';
|
|
8
|
-
import { captureRedactedOutput, redactSecrets } from './redact.js';
|
|
8
|
+
import { captureRedactedOutput, redactSecrets, registerKnownSecrets } from './redact.js';
|
|
9
9
|
import { cloneRepo, commitAll, conflictDiff, hasAgentChanges, headCommit, mergeBranch, openPullRequest, prepareExistingCheckout, pushBranch, reinitAndPush, unmergedPaths, } from './git.js';
|
|
10
10
|
import { makeDirClaimer, noChangesReason, runCodingAgent, runMultiRepoCoding, } from './coding-agent.js';
|
|
11
11
|
import { acquireRepoCheckout, agentNeverActed, agentOutputTail, NEVER_ACTED_CAUSE, runAgentInWorkspace, unusableFinalAnswerCause, withWorkspace, } from './pi-workspace.js';
|
|
@@ -323,6 +323,32 @@ async function runPreviewMode(job, opts) {
|
|
|
323
323
|
throw err;
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Inject the tester's sensitive secrets into the PROCESS environment so the agent's shell tools
|
|
328
|
+
* (spawned as child processes that inherit this env) can read `$KEY` — the out-of-band delivery
|
|
329
|
+
* channel. Each value is registered for redaction so it can't leak into captured output/logs.
|
|
330
|
+
* Returns a restore closure that puts the environment back afterward (warm-pool hygiene, so a
|
|
331
|
+
* later job on a reused container never inherits a prior run's secrets). Reserved/toolchain env
|
|
332
|
+
* names were already dropped at parse. A no-op when there are no secrets.
|
|
333
|
+
*/
|
|
334
|
+
function applyTestSecrets(secrets) {
|
|
335
|
+
if (!secrets?.length)
|
|
336
|
+
return () => { };
|
|
337
|
+
registerKnownSecrets(secrets.map((s) => s.value));
|
|
338
|
+
const previous = new Map();
|
|
339
|
+
for (const { key, value } of secrets) {
|
|
340
|
+
previous.set(key, process.env[key]);
|
|
341
|
+
process.env[key] = value;
|
|
342
|
+
}
|
|
343
|
+
return () => {
|
|
344
|
+
for (const [key, prior] of previous) {
|
|
345
|
+
if (prior === undefined)
|
|
346
|
+
delete process.env[key];
|
|
347
|
+
else
|
|
348
|
+
process.env[key] = prior;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
326
352
|
/**
|
|
327
353
|
* Read-only exploration: clone `branch`, run the agent making no edits, and return its
|
|
328
354
|
* prose report — or, when `output.kind==='structured'`, the parsed JSON object as
|
|
@@ -386,6 +412,10 @@ async function runExploreMode(job, opts) {
|
|
|
386
412
|
const infraSetupFields = managed?.record
|
|
387
413
|
? { infraSetup: managed.record }
|
|
388
414
|
: {};
|
|
415
|
+
// Inject the tester's sensitive secrets into the environment (out of band) so the agent's
|
|
416
|
+
// shell can read them as `$KEY`; restore afterwards so a reused (warm-pool) container never
|
|
417
|
+
// leaks them to a later job. A no-op for non-tester runs (no `testSecrets`).
|
|
418
|
+
const restoreSecrets = applyTestSecrets(job.testSecrets);
|
|
389
419
|
try {
|
|
390
420
|
opts.onPhase?.('agent');
|
|
391
421
|
logger.info('agent(explore): running agent', { serviceDirectory });
|
|
@@ -412,6 +442,7 @@ async function runExploreMode(job, opts) {
|
|
|
412
442
|
return await finalizeExploreResult(job, { summary, stats, stderrTail, usage, callMetrics, runDiag }, { infra, infraSetupFields, logger, signal: opts.signal });
|
|
413
443
|
}
|
|
414
444
|
finally {
|
|
445
|
+
restoreSecrets();
|
|
415
446
|
if (managed)
|
|
416
447
|
await managed.cleanup();
|
|
417
448
|
}
|
|
@@ -529,14 +560,19 @@ async function runMultiRepoExplore(job, opts) {
|
|
|
529
560
|
{ repo: job.repo, cloneBranch: job.branch, ghToken: job.ghToken },
|
|
530
561
|
...peers.map((peer) => ({
|
|
531
562
|
repo: peer.repo,
|
|
532
|
-
|
|
563
|
+
// A read-only peer clones at its default branch (the bug-investigator) unless the job pins
|
|
564
|
+
// an explicit branch — the merger checks each peer out at its PR branch so the combined diff
|
|
565
|
+
// sees the PR change (`git diff origin/<base>...HEAD`).
|
|
566
|
+
cloneBranch: peer.cloneBranch ?? peer.repo.baseBranch,
|
|
533
567
|
ghToken: peer.ghToken ?? job.ghToken,
|
|
534
568
|
})),
|
|
535
569
|
].map((leg) => ({ ...leg, dirName: claimDir(leg.repo) }));
|
|
536
570
|
return withWorkspace('explore-multi', async (root) => {
|
|
537
571
|
// Clone phase: every repo (read-only) into its sibling dir under the workspace root. No
|
|
538
|
-
// work branch, no resume — the
|
|
539
|
-
//
|
|
572
|
+
// work branch, no resume — the agent only reads — so the legs are independent and clone in
|
|
573
|
+
// parallel (wall-clock is the slowest single clone, not the sum). `full` is honoured per the
|
|
574
|
+
// job (the merger needs full history so `git diff origin/<base>...HEAD` has the merge base;
|
|
575
|
+
// the bug-investigator leaves it shallow).
|
|
540
576
|
opts.onPhase?.('clone');
|
|
541
577
|
await Promise.all(legs.map(async (leg) => {
|
|
542
578
|
const dir = join(root, leg.dirName);
|
|
@@ -549,6 +585,7 @@ async function runMultiRepoExplore(job, opts) {
|
|
|
549
585
|
repo: { ...leg.repo, baseBranch: leg.cloneBranch },
|
|
550
586
|
ghToken: leg.ghToken,
|
|
551
587
|
dir,
|
|
588
|
+
full: job.full,
|
|
552
589
|
signal: opts.signal,
|
|
553
590
|
});
|
|
554
591
|
}));
|
package/dist/job.js
CHANGED
|
@@ -136,6 +136,10 @@ function parsePeerRepos(value) {
|
|
|
136
136
|
// read-only explore fan-out (bug-investigator) — validate it only when present.
|
|
137
137
|
if (e.newBranch !== undefined)
|
|
138
138
|
spec.newBranch = str(e.newBranch, `peerRepos[${i}].newBranch`);
|
|
139
|
+
// Read-only explore fan-out: the branch to check the peer out at (the merger's PR branch).
|
|
140
|
+
if (e.cloneBranch !== undefined) {
|
|
141
|
+
spec.cloneBranch = str(e.cloneBranch, `peerRepos[${i}].cloneBranch`);
|
|
142
|
+
}
|
|
139
143
|
if (typeof e.frameId === 'string' && e.frameId)
|
|
140
144
|
spec.frameId = e.frameId;
|
|
141
145
|
if (typeof e.ghToken === 'string' && e.ghToken)
|
|
@@ -282,6 +286,37 @@ export function parsePackageRegistries(value, env = process.env) {
|
|
|
282
286
|
}
|
|
283
287
|
return entries;
|
|
284
288
|
}
|
|
289
|
+
/** A valid POSIX shell variable name (letters, digits, underscore; not starting with a digit). */
|
|
290
|
+
const ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
291
|
+
/**
|
|
292
|
+
* Validate the optional tester `testSecrets` list — `{ key, value }` env pairs the harness
|
|
293
|
+
* injects into the run environment. Keys must be valid env-var names; toolchain-critical /
|
|
294
|
+
* reserved names ({@link isReservedEnvName}) and duplicates are dropped so a drifted body can't
|
|
295
|
+
* clobber PATH/NODE_OPTIONS/etc. Absent ⇒ no secrets injected.
|
|
296
|
+
*/
|
|
297
|
+
export function parseTestSecrets(value) {
|
|
298
|
+
if (value === undefined || value === null)
|
|
299
|
+
return [];
|
|
300
|
+
if (!Array.isArray(value))
|
|
301
|
+
throw new Error("Invalid job: 'testSecrets' must be an array");
|
|
302
|
+
const entries = [];
|
|
303
|
+
const seen = new Set();
|
|
304
|
+
for (const [i, raw] of value.entries()) {
|
|
305
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
306
|
+
throw new Error(`Invalid job: 'testSecrets[${i}]' must be an object`);
|
|
307
|
+
}
|
|
308
|
+
const entry = raw;
|
|
309
|
+
const key = str(entry.key, `testSecrets[${i}].key`).trim();
|
|
310
|
+
if (!ENV_VAR_NAME_PATTERN.test(key)) {
|
|
311
|
+
throw new Error(`Invalid job: 'testSecrets[${i}].key' must be a valid environment variable name`);
|
|
312
|
+
}
|
|
313
|
+
if (isReservedEnvName(key) || seen.has(key))
|
|
314
|
+
continue;
|
|
315
|
+
seen.add(key);
|
|
316
|
+
entries.push({ key, value: str(entry.value, `testSecrets[${i}].value`) });
|
|
317
|
+
}
|
|
318
|
+
return entries;
|
|
319
|
+
}
|
|
285
320
|
/** Parse the coding-mode bootstrap spec, or undefined when absent. Validates the target. */
|
|
286
321
|
function parseAgentBootstrapSpec(value) {
|
|
287
322
|
if (typeof value !== 'object' || value === null)
|
|
@@ -536,6 +571,7 @@ export function parseAgentJob(input) {
|
|
|
536
571
|
const bootstrap = parseAgentBootstrapSpec(o.bootstrap);
|
|
537
572
|
const contextFiles = parseContextFiles(o.contextFiles);
|
|
538
573
|
const packageRegistries = parsePackageRegistries(o.packageRegistries);
|
|
574
|
+
const testSecrets = parseTestSecrets(o.testSecrets);
|
|
539
575
|
const guardLimits = parseGuardLimits(o.guardLimits);
|
|
540
576
|
const job = {
|
|
541
577
|
jobId: str(o.jobId, 'jobId'),
|
|
@@ -556,6 +592,7 @@ export function parseAgentJob(input) {
|
|
|
556
592
|
...(output ? { output } : {}),
|
|
557
593
|
...(contextFiles.length ? { contextFiles } : {}),
|
|
558
594
|
...(packageRegistries.length ? { packageRegistries } : {}),
|
|
595
|
+
...(testSecrets.length ? { testSecrets } : {}),
|
|
559
596
|
...(infra ? { infra } : {}),
|
|
560
597
|
...(typeof o.newBranch === 'string' && o.newBranch ? { newBranch: o.newBranch } : {}),
|
|
561
598
|
...(typeof o.pushBranch === 'string' && o.pushBranch ? { pushBranch: o.pushBranch } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/executor-harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.2",
|
|
4
4
|
"description": "Container payload: a thin TypeScript wrapper that runs the Pi coding agent against a cloned repo and opens a PR. Runs in the Cloudflare Container (and, in local native mode, as a host process); carries no secrets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"hono": "^4.12.27",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"vitest": "^4.1.9",
|
|
29
|
-
"@cat-factory/server": "0.
|
|
30
|
-
"@cat-factory/spend": "0.11.
|
|
29
|
+
"@cat-factory/server": "0.105.0",
|
|
30
|
+
"@cat-factory/spend": "0.11.24"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc -p tsconfig.json",
|
package/src/agent.ts
CHANGED
|
@@ -9,10 +9,11 @@ import type {
|
|
|
9
9
|
AgentResult,
|
|
10
10
|
InfraSetupRecord,
|
|
11
11
|
ServiceInfraSpec,
|
|
12
|
+
TestSecretSpec,
|
|
12
13
|
} from './job.js'
|
|
13
14
|
import { standUpFrontend, tearDownFrontend } from './frontend-infra.js'
|
|
14
15
|
import { configurePackageRegistries } from './package-registries.js'
|
|
15
|
-
import { captureRedactedOutput, redactSecrets } from './redact.js'
|
|
16
|
+
import { captureRedactedOutput, redactSecrets, registerKnownSecrets } from './redact.js'
|
|
16
17
|
import {
|
|
17
18
|
cloneRepo,
|
|
18
19
|
commitAll,
|
|
@@ -408,6 +409,30 @@ async function runPreviewMode(job: AgentJob, opts: RunOptions): Promise<AgentRes
|
|
|
408
409
|
}
|
|
409
410
|
}
|
|
410
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Inject the tester's sensitive secrets into the PROCESS environment so the agent's shell tools
|
|
414
|
+
* (spawned as child processes that inherit this env) can read `$KEY` — the out-of-band delivery
|
|
415
|
+
* channel. Each value is registered for redaction so it can't leak into captured output/logs.
|
|
416
|
+
* Returns a restore closure that puts the environment back afterward (warm-pool hygiene, so a
|
|
417
|
+
* later job on a reused container never inherits a prior run's secrets). Reserved/toolchain env
|
|
418
|
+
* names were already dropped at parse. A no-op when there are no secrets.
|
|
419
|
+
*/
|
|
420
|
+
function applyTestSecrets(secrets: TestSecretSpec[] | undefined): () => void {
|
|
421
|
+
if (!secrets?.length) return () => {}
|
|
422
|
+
registerKnownSecrets(secrets.map((s) => s.value))
|
|
423
|
+
const previous = new Map<string, string | undefined>()
|
|
424
|
+
for (const { key, value } of secrets) {
|
|
425
|
+
previous.set(key, process.env[key])
|
|
426
|
+
process.env[key] = value
|
|
427
|
+
}
|
|
428
|
+
return () => {
|
|
429
|
+
for (const [key, prior] of previous) {
|
|
430
|
+
if (prior === undefined) delete process.env[key]
|
|
431
|
+
else process.env[key] = prior
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
411
436
|
/**
|
|
412
437
|
* Read-only exploration: clone `branch`, run the agent making no edits, and return its
|
|
413
438
|
* prose report — or, when `output.kind==='structured'`, the parsed JSON object as
|
|
@@ -473,6 +498,11 @@ async function runExploreMode(job: AgentJob, opts: RunOptions): Promise<AgentRes
|
|
|
473
498
|
? { infraSetup: managed.record }
|
|
474
499
|
: {}
|
|
475
500
|
|
|
501
|
+
// Inject the tester's sensitive secrets into the environment (out of band) so the agent's
|
|
502
|
+
// shell can read them as `$KEY`; restore afterwards so a reused (warm-pool) container never
|
|
503
|
+
// leaks them to a later job. A no-op for non-tester runs (no `testSecrets`).
|
|
504
|
+
const restoreSecrets = applyTestSecrets(job.testSecrets)
|
|
505
|
+
|
|
476
506
|
try {
|
|
477
507
|
opts.onPhase?.('agent')
|
|
478
508
|
logger.info('agent(explore): running agent', { serviceDirectory })
|
|
@@ -513,6 +543,7 @@ async function runExploreMode(job: AgentJob, opts: RunOptions): Promise<AgentRes
|
|
|
513
543
|
{ infra, infraSetupFields, logger, signal: opts.signal },
|
|
514
544
|
)
|
|
515
545
|
} finally {
|
|
546
|
+
restoreSecrets()
|
|
516
547
|
if (managed) await managed.cleanup()
|
|
517
548
|
}
|
|
518
549
|
},
|
|
@@ -655,15 +686,20 @@ async function runMultiRepoExplore(job: AgentJob, opts: RunOptions): Promise<Age
|
|
|
655
686
|
{ repo: job.repo, cloneBranch: job.branch, ghToken: job.ghToken },
|
|
656
687
|
...peers.map((peer) => ({
|
|
657
688
|
repo: peer.repo,
|
|
658
|
-
|
|
689
|
+
// A read-only peer clones at its default branch (the bug-investigator) unless the job pins
|
|
690
|
+
// an explicit branch — the merger checks each peer out at its PR branch so the combined diff
|
|
691
|
+
// sees the PR change (`git diff origin/<base>...HEAD`).
|
|
692
|
+
cloneBranch: peer.cloneBranch ?? peer.repo.baseBranch,
|
|
659
693
|
ghToken: peer.ghToken ?? job.ghToken,
|
|
660
694
|
})),
|
|
661
695
|
].map((leg) => ({ ...leg, dirName: claimDir(leg.repo) }))
|
|
662
696
|
|
|
663
697
|
return withWorkspace('explore-multi', async (root) => {
|
|
664
698
|
// Clone phase: every repo (read-only) into its sibling dir under the workspace root. No
|
|
665
|
-
// work branch, no resume — the
|
|
666
|
-
//
|
|
699
|
+
// work branch, no resume — the agent only reads — so the legs are independent and clone in
|
|
700
|
+
// parallel (wall-clock is the slowest single clone, not the sum). `full` is honoured per the
|
|
701
|
+
// job (the merger needs full history so `git diff origin/<base>...HEAD` has the merge base;
|
|
702
|
+
// the bug-investigator leaves it shallow).
|
|
667
703
|
opts.onPhase?.('clone')
|
|
668
704
|
await Promise.all(
|
|
669
705
|
legs.map(async (leg) => {
|
|
@@ -677,6 +713,7 @@ async function runMultiRepoExplore(job: AgentJob, opts: RunOptions): Promise<Age
|
|
|
677
713
|
repo: { ...leg.repo, baseBranch: leg.cloneBranch },
|
|
678
714
|
ghToken: leg.ghToken,
|
|
679
715
|
dir,
|
|
716
|
+
full: job.full,
|
|
680
717
|
signal: opts.signal,
|
|
681
718
|
})
|
|
682
719
|
}),
|
package/src/job.ts
CHANGED
|
@@ -78,6 +78,13 @@ export interface PeerRepoSpec {
|
|
|
78
78
|
* (the bug-investigator), which only clones the peer to read it and never pushes.
|
|
79
79
|
*/
|
|
80
80
|
newBranch?: string
|
|
81
|
+
/**
|
|
82
|
+
* The EXISTING branch to check the peer out at for a READ-ONLY explore fan-out (the `merger`
|
|
83
|
+
* scoring the combined diff clones each peer at its PR branch so the diff sees the PR change).
|
|
84
|
+
* Absent ⇒ the peer is cloned at its repo default branch (the bug-investigator). Ignored on the
|
|
85
|
+
* coding fan-out, which creates `newBranch` instead.
|
|
86
|
+
*/
|
|
87
|
+
cloneBranch?: string
|
|
81
88
|
/** Open a PR/MR in this peer when set AND the run changed the peer (skipped for a clean repo). */
|
|
82
89
|
pr?: PrSpec
|
|
83
90
|
/** Per-repo GitHub token; defaults to the job's `ghToken` (one installation per workspace today). */
|
|
@@ -236,6 +243,10 @@ function parsePeerRepos(value: unknown): PeerRepoSpec[] {
|
|
|
236
243
|
// `newBranch` is required for a coding fan-out (it pushes to it) but ABSENT for a
|
|
237
244
|
// read-only explore fan-out (bug-investigator) — validate it only when present.
|
|
238
245
|
if (e.newBranch !== undefined) spec.newBranch = str(e.newBranch, `peerRepos[${i}].newBranch`)
|
|
246
|
+
// Read-only explore fan-out: the branch to check the peer out at (the merger's PR branch).
|
|
247
|
+
if (e.cloneBranch !== undefined) {
|
|
248
|
+
spec.cloneBranch = str(e.cloneBranch, `peerRepos[${i}].cloneBranch`)
|
|
249
|
+
}
|
|
239
250
|
if (typeof e.frameId === 'string' && e.frameId) spec.frameId = e.frameId
|
|
240
251
|
if (typeof e.ghToken === 'string' && e.ghToken) spec.ghToken = e.ghToken
|
|
241
252
|
if (typeof e.pr === 'object' && e.pr !== null) {
|
|
@@ -407,6 +418,49 @@ export function parsePackageRegistries(
|
|
|
407
418
|
return entries
|
|
408
419
|
}
|
|
409
420
|
|
|
421
|
+
/**
|
|
422
|
+
* One sensitive test credential the tester receives: an env-var name + its (secret) value.
|
|
423
|
+
* The backend seals these at rest and decrypts them at dispatch; the harness injects each as an
|
|
424
|
+
* environment variable the tester's shell can read (out of band — the value is NEVER in the
|
|
425
|
+
* prompt/telemetry). See {@link parseTestSecrets}.
|
|
426
|
+
*/
|
|
427
|
+
export interface TestSecretSpec {
|
|
428
|
+
key: string
|
|
429
|
+
value: string
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** A valid POSIX shell variable name (letters, digits, underscore; not starting with a digit). */
|
|
433
|
+
const ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Validate the optional tester `testSecrets` list — `{ key, value }` env pairs the harness
|
|
437
|
+
* injects into the run environment. Keys must be valid env-var names; toolchain-critical /
|
|
438
|
+
* reserved names ({@link isReservedEnvName}) and duplicates are dropped so a drifted body can't
|
|
439
|
+
* clobber PATH/NODE_OPTIONS/etc. Absent ⇒ no secrets injected.
|
|
440
|
+
*/
|
|
441
|
+
export function parseTestSecrets(value: unknown): TestSecretSpec[] {
|
|
442
|
+
if (value === undefined || value === null) return []
|
|
443
|
+
if (!Array.isArray(value)) throw new Error("Invalid job: 'testSecrets' must be an array")
|
|
444
|
+
const entries: TestSecretSpec[] = []
|
|
445
|
+
const seen = new Set<string>()
|
|
446
|
+
for (const [i, raw] of value.entries()) {
|
|
447
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
448
|
+
throw new Error(`Invalid job: 'testSecrets[${i}]' must be an object`)
|
|
449
|
+
}
|
|
450
|
+
const entry = raw as Record<string, unknown>
|
|
451
|
+
const key = str(entry.key, `testSecrets[${i}].key`).trim()
|
|
452
|
+
if (!ENV_VAR_NAME_PATTERN.test(key)) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Invalid job: 'testSecrets[${i}].key' must be a valid environment variable name`,
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
if (isReservedEnvName(key) || seen.has(key)) continue
|
|
458
|
+
seen.add(key)
|
|
459
|
+
entries.push({ key, value: str(entry.value, `testSecrets[${i}].value`) })
|
|
460
|
+
}
|
|
461
|
+
return entries
|
|
462
|
+
}
|
|
463
|
+
|
|
410
464
|
// ---- Shared repo-bootstrap target ---------------------------------------
|
|
411
465
|
|
|
412
466
|
/** The new repository a repo-bootstrap run force-pushes its fresh history to. */
|
|
@@ -602,6 +656,13 @@ export interface AgentJob extends HarnessAuthFields {
|
|
|
602
656
|
* job on a reused container is removed.
|
|
603
657
|
*/
|
|
604
658
|
packageRegistries?: PackageRegistrySpec[]
|
|
659
|
+
/**
|
|
660
|
+
* Tester kinds only: sensitive test credentials injected into the run's ENVIRONMENT (out of
|
|
661
|
+
* band) as `{ key, value }` env pairs, so the tester's shell can read `$KEY` without the value
|
|
662
|
+
* ever appearing in the prompt or telemetry. Reserved/toolchain env names are dropped at parse.
|
|
663
|
+
* Absent ⇒ no secrets injected.
|
|
664
|
+
*/
|
|
665
|
+
testSecrets?: TestSecretSpec[]
|
|
605
666
|
/**
|
|
606
667
|
* Explore mode: stand the service's dependencies up before the agent runs (the
|
|
607
668
|
* tester). Brings the docker-compose infra up on localhost for the duration of the
|
|
@@ -1042,6 +1103,7 @@ export function parseAgentJob(input: unknown): AgentJob {
|
|
|
1042
1103
|
const bootstrap = parseAgentBootstrapSpec(o.bootstrap)
|
|
1043
1104
|
const contextFiles = parseContextFiles(o.contextFiles)
|
|
1044
1105
|
const packageRegistries = parsePackageRegistries(o.packageRegistries)
|
|
1106
|
+
const testSecrets = parseTestSecrets(o.testSecrets)
|
|
1045
1107
|
const guardLimits = parseGuardLimits(o.guardLimits)
|
|
1046
1108
|
const job: AgentJob = {
|
|
1047
1109
|
jobId: str(o.jobId, 'jobId'),
|
|
@@ -1062,6 +1124,7 @@ export function parseAgentJob(input: unknown): AgentJob {
|
|
|
1062
1124
|
...(output ? { output } : {}),
|
|
1063
1125
|
...(contextFiles.length ? { contextFiles } : {}),
|
|
1064
1126
|
...(packageRegistries.length ? { packageRegistries } : {}),
|
|
1127
|
+
...(testSecrets.length ? { testSecrets } : {}),
|
|
1065
1128
|
...(infra ? { infra } : {}),
|
|
1066
1129
|
...(typeof o.newBranch === 'string' && o.newBranch ? { newBranch: o.newBranch } : {}),
|
|
1067
1130
|
...(typeof o.pushBranch === 'string' && o.pushBranch ? { pushBranch: o.pushBranch } : {}),
|