@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.
- package/dist/job.js +75 -58
- package/package.json +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
|
-
|
|
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.
|
|
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.
|
|
30
|
-
"@cat-factory/spend": "0.12.
|
|
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
|
-
|
|
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
|
}
|