@cat-factory/executor-harness 1.50.10 → 1.50.12

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 (3) hide show
  1. package/dist/job.js +75 -58
  2. package/package.json +3 -3
  3. package/src/job.ts +109 -55
package/dist/job.js CHANGED
@@ -647,45 +647,82 @@ export function parseAgentJob(input) {
647
647
  // preview dispatch to send dummy values it has no reason to supply. Every other mode still
648
648
  // requires them (throws when missing/empty), exactly as before.
649
649
  const agentField = (value, path) => mode === 'preview' ? (typeof value === 'string' ? value : '') : str(value, path);
650
+ // Parse each field, then hand the pieces to `assembleAgentJob` for the (large) object literal —
651
+ // the parse/assemble split keeps both within the cyclomatic-complexity budget. Behaviour is
652
+ // byte-identical (the literal + host validation moved verbatim).
653
+ const job = assembleAgentJob(o, mode, agentField, {
654
+ output: parseAgentOutputSpec(o.output),
655
+ pr: parseAgentPrSpec(o.pr),
656
+ infra: parseAgentInfraSpec(o.infra),
657
+ peerRepos: parsePeerRepos(o.peerRepos),
658
+ referenceRepos: parseReferenceRepos(o.referenceRepos),
659
+ referenceBranches: parseReferenceBranches(o.referenceBranches),
660
+ bootstrap: parseAgentBootstrapSpec(o.bootstrap),
661
+ contextFiles: parseContextFiles(o.contextFiles),
662
+ packageRegistries: parsePackageRegistries(o.packageRegistries),
663
+ skill: parseSkillSpec(o.skill),
664
+ testSecrets: parseTestSecrets(o.testSecrets),
665
+ guardLimits: parseGuardLimits(o.guardLimits),
666
+ validation: parseValidationSpec(o.validation),
667
+ reviewPrNumber: posInt(o.reviewPrNumber),
668
+ });
669
+ assertAllowedHost(job.repo.cloneUrl, 'repo.cloneUrl');
670
+ if (job.githubApiBase)
671
+ assertAllowedHost(job.githubApiBase, 'githubApiBase');
672
+ // Bootstrap pushes the result to a SEPARATE target repo, so its clone URL must be an
673
+ // allowed GitHub host too (the installation token is sent to it on the force-push).
674
+ if (job.bootstrap)
675
+ assertAllowedHost(job.bootstrap.target.cloneUrl, 'bootstrap.target.cloneUrl');
676
+ // Each peer repo's clone URL receives the installation token on clone/push, so it must be
677
+ // an allowed GitHub host too — a body-supplied peer pointing at an attacker host would
678
+ // exfiltrate the token exactly like a rogue primary clone URL.
679
+ for (const [i, peer] of (job.peerRepos ?? []).entries()) {
680
+ assertAllowedHost(peer.repo.cloneUrl, `peerRepos[${i}].repo.cloneUrl`);
681
+ }
682
+ // Each reference repo's clone URL receives the installation/PAT token on clone (read-only,
683
+ // never pushed), so it must be an allowed host too — a body-supplied reference pointing at an
684
+ // attacker host would exfiltrate the token exactly like a rogue peer clone URL.
685
+ for (const [i, ref] of (job.referenceRepos ?? []).entries()) {
686
+ assertAllowedHost(ref.repo.cloneUrl, `referenceRepos[${i}].repo.cloneUrl`);
687
+ }
688
+ return job;
689
+ }
690
+ /** Parse the optional structured-output spec (`{ kind, shapeHint?, repair?, failOnUnusableFinal? }`). */
691
+ function parseAgentOutputSpec(raw) {
692
+ if (typeof raw !== 'object' || raw === null)
693
+ return undefined;
694
+ const so = raw;
695
+ const kind = so.kind === 'structured' ? 'structured' : 'prose';
696
+ const spec = { kind };
697
+ if (typeof so.shapeHint === 'string')
698
+ spec.shapeHint = so.shapeHint;
699
+ // Carry an explicit `repair: false` through — the handler defaults to repair-on
700
+ // when absent, so dropping `false` would silently re-enable the repair call for a
701
+ // kind that opted out (it keys off `output.repair === false`).
702
+ if (typeof so.repair === 'boolean')
703
+ spec.repair = so.repair;
704
+ // Carry the opt-in truncation gate through (document producers set it); dropping
705
+ // it would silently re-enable laundering a cut-off reply into a half-baked doc.
706
+ if (so.failOnUnusableFinal === true)
707
+ spec.failOnUnusableFinal = true;
708
+ return spec;
709
+ }
710
+ /** Parse the optional PR spec (`{ title, body }`). */
711
+ function parseAgentPrSpec(raw) {
712
+ if (typeof raw !== 'object' || raw === null)
713
+ return undefined;
714
+ const p = raw;
715
+ return { title: str(p.title, 'pr.title'), body: typeof p.body === 'string' ? p.body : '' };
716
+ }
717
+ /**
718
+ * Assemble the {@link AgentJob} object from the request `o` + the pre-parsed {@link
719
+ * ParsedAgentJobParts}. Extracted from {@link parseAgentJob} so the large conditional-spread
720
+ * literal doesn't blow the complexity budget; behaviour is byte-identical (spread order preserved).
721
+ */
722
+ function assembleAgentJob(o, mode, agentField, parts) {
723
+ const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, packageRegistries, skill, testSecrets, guardLimits, validation, reviewPrNumber, } = parts;
650
724
  const repo = (o.repo ?? {});
651
- const output = typeof o.output === 'object' && o.output !== null
652
- ? (() => {
653
- const so = o.output;
654
- const kind = so.kind === 'structured' ? 'structured' : 'prose';
655
- const spec = { kind };
656
- if (typeof so.shapeHint === 'string')
657
- spec.shapeHint = so.shapeHint;
658
- // Carry an explicit `repair: false` through — the handler defaults to repair-on
659
- // when absent, so dropping `false` would silently re-enable the repair call for a
660
- // kind that opted out (it keys off `output.repair === false`).
661
- if (typeof so.repair === 'boolean')
662
- spec.repair = so.repair;
663
- // Carry the opt-in truncation gate through (document producers set it); dropping
664
- // it would silently re-enable laundering a cut-off reply into a half-baked doc.
665
- if (so.failOnUnusableFinal === true)
666
- spec.failOnUnusableFinal = true;
667
- return spec;
668
- })()
669
- : undefined;
670
- const pr = typeof o.pr === 'object' && o.pr !== null
671
- ? (() => {
672
- const p = o.pr;
673
- return { title: str(p.title, 'pr.title'), body: typeof p.body === 'string' ? p.body : '' };
674
- })()
675
- : undefined;
676
- const infra = parseAgentInfraSpec(o.infra);
677
- const peerRepos = parsePeerRepos(o.peerRepos);
678
- const referenceRepos = parseReferenceRepos(o.referenceRepos);
679
- const referenceBranches = parseReferenceBranches(o.referenceBranches);
680
- const bootstrap = parseAgentBootstrapSpec(o.bootstrap);
681
- const contextFiles = parseContextFiles(o.contextFiles);
682
- const packageRegistries = parsePackageRegistries(o.packageRegistries);
683
- const skill = parseSkillSpec(o.skill);
684
- const testSecrets = parseTestSecrets(o.testSecrets);
685
- const guardLimits = parseGuardLimits(o.guardLimits);
686
- const validation = parseValidationSpec(o.validation);
687
- const reviewPrNumber = posInt(o.reviewPrNumber);
688
- const job = {
725
+ return {
689
726
  jobId: str(o.jobId, 'jobId'),
690
727
  mode,
691
728
  systemPrompt: agentField(o.systemPrompt, 'systemPrompt'),
@@ -723,24 +760,4 @@ export function parseAgentJob(input) {
723
760
  ...(guardLimits ? { guardLimits } : {}),
724
761
  ...(validation ? { validation } : {}),
725
762
  };
726
- assertAllowedHost(job.repo.cloneUrl, 'repo.cloneUrl');
727
- if (job.githubApiBase)
728
- assertAllowedHost(job.githubApiBase, 'githubApiBase');
729
- // Bootstrap pushes the result to a SEPARATE target repo, so its clone URL must be an
730
- // allowed GitHub host too (the installation token is sent to it on the force-push).
731
- if (job.bootstrap)
732
- assertAllowedHost(job.bootstrap.target.cloneUrl, 'bootstrap.target.cloneUrl');
733
- // Each peer repo's clone URL receives the installation token on clone/push, so it must be
734
- // an allowed GitHub host too — a body-supplied peer pointing at an attacker host would
735
- // exfiltrate the token exactly like a rogue primary clone URL.
736
- for (const [i, peer] of (job.peerRepos ?? []).entries()) {
737
- assertAllowedHost(peer.repo.cloneUrl, `peerRepos[${i}].repo.cloneUrl`);
738
- }
739
- // Each reference repo's clone URL receives the installation/PAT token on clone (read-only,
740
- // never pushed), so it must be an allowed host too — a body-supplied reference pointing at an
741
- // attacker host would exfiltrate the token exactly like a rogue peer clone URL.
742
- for (const [i, ref] of (job.referenceRepos ?? []).entries()) {
743
- assertAllowedHost(ref.repo.cloneUrl, `referenceRepos[${i}].repo.cloneUrl`);
744
- }
745
- return job;
746
763
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/executor-harness",
3
- "version": "1.50.10",
3
+ "version": "1.50.12",
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.30",
27
27
  "typescript": "7.0.2",
28
28
  "vitest": "^4.1.10",
29
- "@cat-factory/server": "0.140.4",
30
- "@cat-factory/spend": "0.12.69"
29
+ "@cat-factory/server": "0.141.2",
30
+ "@cat-factory/spend": "0.12.73"
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsc -p tsconfig.json",
package/src/job.ts CHANGED
@@ -1250,44 +1250,116 @@ export function parseAgentJob(input: unknown): AgentJob {
1250
1250
  // requires them (throws when missing/empty), exactly as before.
1251
1251
  const agentField = (value: unknown, path: string): string =>
1252
1252
  mode === 'preview' ? (typeof value === 'string' ? value : '') : str(value, path)
1253
+ // Parse each field, then hand the pieces to `assembleAgentJob` for the (large) object literal —
1254
+ // the parse/assemble split keeps both within the cyclomatic-complexity budget. Behaviour is
1255
+ // byte-identical (the literal + host validation moved verbatim).
1256
+ const job = assembleAgentJob(o, mode, agentField, {
1257
+ output: parseAgentOutputSpec(o.output),
1258
+ pr: parseAgentPrSpec(o.pr),
1259
+ infra: parseAgentInfraSpec(o.infra),
1260
+ peerRepos: parsePeerRepos(o.peerRepos),
1261
+ referenceRepos: parseReferenceRepos(o.referenceRepos),
1262
+ referenceBranches: parseReferenceBranches(o.referenceBranches),
1263
+ bootstrap: parseAgentBootstrapSpec(o.bootstrap),
1264
+ contextFiles: parseContextFiles(o.contextFiles),
1265
+ packageRegistries: parsePackageRegistries(o.packageRegistries),
1266
+ skill: parseSkillSpec(o.skill),
1267
+ testSecrets: parseTestSecrets(o.testSecrets),
1268
+ guardLimits: parseGuardLimits(o.guardLimits),
1269
+ validation: parseValidationSpec(o.validation),
1270
+ reviewPrNumber: posInt(o.reviewPrNumber),
1271
+ })
1272
+ assertAllowedHost(job.repo.cloneUrl, 'repo.cloneUrl')
1273
+ if (job.githubApiBase) assertAllowedHost(job.githubApiBase, 'githubApiBase')
1274
+ // Bootstrap pushes the result to a SEPARATE target repo, so its clone URL must be an
1275
+ // allowed GitHub host too (the installation token is sent to it on the force-push).
1276
+ if (job.bootstrap) assertAllowedHost(job.bootstrap.target.cloneUrl, 'bootstrap.target.cloneUrl')
1277
+ // Each peer repo's clone URL receives the installation token on clone/push, so it must be
1278
+ // an allowed GitHub host too — a body-supplied peer pointing at an attacker host would
1279
+ // exfiltrate the token exactly like a rogue primary clone URL.
1280
+ for (const [i, peer] of (job.peerRepos ?? []).entries()) {
1281
+ assertAllowedHost(peer.repo.cloneUrl, `peerRepos[${i}].repo.cloneUrl`)
1282
+ }
1283
+ // Each reference repo's clone URL receives the installation/PAT token on clone (read-only,
1284
+ // never pushed), so it must be an allowed host too — a body-supplied reference pointing at an
1285
+ // attacker host would exfiltrate the token exactly like a rogue peer clone URL.
1286
+ for (const [i, ref] of (job.referenceRepos ?? []).entries()) {
1287
+ assertAllowedHost(ref.repo.cloneUrl, `referenceRepos[${i}].repo.cloneUrl`)
1288
+ }
1289
+ return job
1290
+ }
1291
+
1292
+ /** The pre-parsed field bundle {@link parseAgentJob} hands to {@link assembleAgentJob}. */
1293
+ interface ParsedAgentJobParts {
1294
+ output: AgentOutputSpec | undefined
1295
+ pr: { title: string; body: string } | undefined
1296
+ infra: ReturnType<typeof parseAgentInfraSpec>
1297
+ peerRepos: ReturnType<typeof parsePeerRepos>
1298
+ referenceRepos: ReturnType<typeof parseReferenceRepos>
1299
+ referenceBranches: ReturnType<typeof parseReferenceBranches>
1300
+ bootstrap: ReturnType<typeof parseAgentBootstrapSpec>
1301
+ contextFiles: ReturnType<typeof parseContextFiles>
1302
+ packageRegistries: ReturnType<typeof parsePackageRegistries>
1303
+ skill: ReturnType<typeof parseSkillSpec>
1304
+ testSecrets: ReturnType<typeof parseTestSecrets>
1305
+ guardLimits: ReturnType<typeof parseGuardLimits>
1306
+ validation: ReturnType<typeof parseValidationSpec>
1307
+ reviewPrNumber: number | undefined
1308
+ }
1309
+
1310
+ /** Parse the optional structured-output spec (`{ kind, shapeHint?, repair?, failOnUnusableFinal? }`). */
1311
+ function parseAgentOutputSpec(raw: unknown): AgentOutputSpec | undefined {
1312
+ if (typeof raw !== 'object' || raw === null) return undefined
1313
+ const so = raw as Record<string, unknown>
1314
+ const kind = so.kind === 'structured' ? 'structured' : 'prose'
1315
+ const spec: AgentOutputSpec = { kind }
1316
+ if (typeof so.shapeHint === 'string') spec.shapeHint = so.shapeHint
1317
+ // Carry an explicit `repair: false` through — the handler defaults to repair-on
1318
+ // when absent, so dropping `false` would silently re-enable the repair call for a
1319
+ // kind that opted out (it keys off `output.repair === false`).
1320
+ if (typeof so.repair === 'boolean') spec.repair = so.repair
1321
+ // Carry the opt-in truncation gate through (document producers set it); dropping
1322
+ // it would silently re-enable laundering a cut-off reply into a half-baked doc.
1323
+ if (so.failOnUnusableFinal === true) spec.failOnUnusableFinal = true
1324
+ return spec
1325
+ }
1326
+
1327
+ /** Parse the optional PR spec (`{ title, body }`). */
1328
+ function parseAgentPrSpec(raw: unknown): { title: string; body: string } | undefined {
1329
+ if (typeof raw !== 'object' || raw === null) return undefined
1330
+ const p = raw as Record<string, unknown>
1331
+ return { title: str(p.title, 'pr.title'), body: typeof p.body === 'string' ? p.body : '' }
1332
+ }
1333
+
1334
+ /**
1335
+ * Assemble the {@link AgentJob} object from the request `o` + the pre-parsed {@link
1336
+ * ParsedAgentJobParts}. Extracted from {@link parseAgentJob} so the large conditional-spread
1337
+ * literal doesn't blow the complexity budget; behaviour is byte-identical (spread order preserved).
1338
+ */
1339
+ function assembleAgentJob(
1340
+ o: Record<string, unknown>,
1341
+ mode: AgentJob['mode'],
1342
+ agentField: (value: unknown, path: string) => string,
1343
+ parts: ParsedAgentJobParts,
1344
+ ): AgentJob {
1345
+ const {
1346
+ output,
1347
+ pr,
1348
+ infra,
1349
+ peerRepos,
1350
+ referenceRepos,
1351
+ referenceBranches,
1352
+ bootstrap,
1353
+ contextFiles,
1354
+ packageRegistries,
1355
+ skill,
1356
+ testSecrets,
1357
+ guardLimits,
1358
+ validation,
1359
+ reviewPrNumber,
1360
+ } = parts
1253
1361
  const repo = (o.repo ?? {}) as Record<string, unknown>
1254
- const output =
1255
- typeof o.output === 'object' && o.output !== null
1256
- ? (() => {
1257
- const so = o.output as Record<string, unknown>
1258
- const kind = so.kind === 'structured' ? 'structured' : 'prose'
1259
- const spec: AgentOutputSpec = { kind }
1260
- if (typeof so.shapeHint === 'string') spec.shapeHint = so.shapeHint
1261
- // Carry an explicit `repair: false` through — the handler defaults to repair-on
1262
- // when absent, so dropping `false` would silently re-enable the repair call for a
1263
- // kind that opted out (it keys off `output.repair === false`).
1264
- if (typeof so.repair === 'boolean') spec.repair = so.repair
1265
- // Carry the opt-in truncation gate through (document producers set it); dropping
1266
- // it would silently re-enable laundering a cut-off reply into a half-baked doc.
1267
- if (so.failOnUnusableFinal === true) spec.failOnUnusableFinal = true
1268
- return spec
1269
- })()
1270
- : undefined
1271
- const pr =
1272
- typeof o.pr === 'object' && o.pr !== null
1273
- ? (() => {
1274
- const p = o.pr as Record<string, unknown>
1275
- return { title: str(p.title, 'pr.title'), body: typeof p.body === 'string' ? p.body : '' }
1276
- })()
1277
- : undefined
1278
- const infra = parseAgentInfraSpec(o.infra)
1279
- const peerRepos = parsePeerRepos(o.peerRepos)
1280
- const referenceRepos = parseReferenceRepos(o.referenceRepos)
1281
- const referenceBranches = parseReferenceBranches(o.referenceBranches)
1282
- const bootstrap = parseAgentBootstrapSpec(o.bootstrap)
1283
- const contextFiles = parseContextFiles(o.contextFiles)
1284
- const packageRegistries = parsePackageRegistries(o.packageRegistries)
1285
- const skill = parseSkillSpec(o.skill)
1286
- const testSecrets = parseTestSecrets(o.testSecrets)
1287
- const guardLimits = parseGuardLimits(o.guardLimits)
1288
- const validation = parseValidationSpec(o.validation)
1289
- const reviewPrNumber = posInt(o.reviewPrNumber)
1290
- const job: AgentJob = {
1362
+ return {
1291
1363
  jobId: str(o.jobId, 'jobId'),
1292
1364
  mode,
1293
1365
  systemPrompt: agentField(o.systemPrompt, 'systemPrompt'),
@@ -1325,22 +1397,4 @@ export function parseAgentJob(input: unknown): AgentJob {
1325
1397
  ...(guardLimits ? { guardLimits } : {}),
1326
1398
  ...(validation ? { validation } : {}),
1327
1399
  }
1328
- assertAllowedHost(job.repo.cloneUrl, 'repo.cloneUrl')
1329
- if (job.githubApiBase) assertAllowedHost(job.githubApiBase, 'githubApiBase')
1330
- // Bootstrap pushes the result to a SEPARATE target repo, so its clone URL must be an
1331
- // allowed GitHub host too (the installation token is sent to it on the force-push).
1332
- if (job.bootstrap) assertAllowedHost(job.bootstrap.target.cloneUrl, 'bootstrap.target.cloneUrl')
1333
- // Each peer repo's clone URL receives the installation token on clone/push, so it must be
1334
- // an allowed GitHub host too — a body-supplied peer pointing at an attacker host would
1335
- // exfiltrate the token exactly like a rogue primary clone URL.
1336
- for (const [i, peer] of (job.peerRepos ?? []).entries()) {
1337
- assertAllowedHost(peer.repo.cloneUrl, `peerRepos[${i}].repo.cloneUrl`)
1338
- }
1339
- // Each reference repo's clone URL receives the installation/PAT token on clone (read-only,
1340
- // never pushed), so it must be an allowed host too — a body-supplied reference pointing at an
1341
- // attacker host would exfiltrate the token exactly like a rogue peer clone URL.
1342
- for (const [i, ref] of (job.referenceRepos ?? []).entries()) {
1343
- assertAllowedHost(ref.repo.cloneUrl, `referenceRepos[${i}].repo.cloneUrl`)
1344
- }
1345
- return job
1346
1400
  }