{"record":{"id":"76dac083420a0bff","repo":"KeygraphHQ/shannon","slug":"variables-must-include-weburl-and-repopath","errorCode":null,"errorMessage":"Variables must include webUrl and repoPath","messagePattern":"Variables must include webUrl and repoPath","errorType":"validation","errorClass":"PentestError","httpStatus":null,"severity":"error","filePath":"apps/worker/src/services/prompt-manager.ts","lineNumber":325,"sourceCode":"\n// Pure function: Variable interpolation\nasync function interpolateVariables(\n  template: string,\n  variables: PromptVariables,\n  config: DistributedConfig | null = null,\n  logger: ActivityLogger,\n  promptsBaseDir: string = PROMPTS_DIR,\n): Promise<string> {\n  try {\n    if (!template || typeof template !== 'string') {\n      throw new PentestError('Template must be a non-empty string', 'validation', false, {\n        templateType: typeof template,\n        templateLength: template?.length,\n      });\n    }\n\n    if (!variables || !variables.webUrl || !variables.repoPath) {\n      throw new PentestError('Variables must include webUrl and repoPath', 'validation', false, {\n        variables: Object.keys(variables || {}),\n      });\n    }\n\n    // replaceLiteral is used for all value insertions so config values that\n    // contain `$&`/`$$`/`$1`/etc. aren't mangled as replacement patterns.\n    let result = template;\n    result = replaceLiteral(result, /{{WEB_URL}}/g, variables.webUrl);\n    result = replaceLiteral(result, /{{REPO_PATH}}/g, variables.repoPath);\n    result = replaceLiteral(result, /{{PLAYWRIGHT_SESSION}}/g, variables.PLAYWRIGHT_SESSION || 'agent1');\n    result = replaceLiteral(result, /{{AUTH_CONTEXT}}/g, buildAuthContext(config));\n    result = replaceLiteral(\n      result,\n      /{{DESCRIPTION}}/g,\n      config?.description ? `Description: ${config.description}` : '',\n    );\n\n    const avoidUrlRules = config?.avoid?.filter((r) => r.type !== 'code_path') ?? [];","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/KeygraphHQ/shannon/blob/1ae0a142f8525410a688f0309fd003cc5b1d92de/apps/worker/src/services/prompt-manager.ts#L307-L343","documentation":"Second validation guard in interpolateVariables: the variables object must be truthy and contain both webUrl and repoPath (the two placeholders {{WEB_URL}} and {{REPO_PATH}} are always substituted). Missing either throws with category 'validation', non-retryable, recording which keys were present in context.variables. This is an internal-contract check on the caller (loadPrompt builds these from CLI/pipeline input).","triggerScenarios":"loadPrompt (or a direct interpolateVariables caller) is invoked with a PromptVariables object where webUrl or repoPath is undefined/empty — e.g. the pipeline started without a target URL, the repo path was not mounted, or a resume path reconstructed variables incompletely.","commonSituations":"A pipeline-input bug omits repoPath when running a phase that does not need the repo but still interpolates prompts. Resume logic rebuilds variables from session.json but a field was never persisted. A test harness calls loadPrompt with a partial variables object.","solutions":["Inspect context.variables on the thrown PentestError to see which keys were supplied.","Ensure the scan start command passes both -u <url> and -r <repo> so the pipeline receives webUrl and repoPath.","If resuming, verify session.json contains session.webUrl and that the repo mount is present before the workflow restarts.","Patch the caller to default/validate these fields before calling loadPrompt."],"exampleFix":"// before: variables missing repoPath\n//   await loadPrompt('recon', { webUrl: 'https://t' }, config, false, logger);\n// after: supply both required fields\n//   await loadPrompt('recon', { webUrl: 'https://t', repoPath: '/workspace/repo' }, config, false, logger);","handlingStrategy":"validation","validationCode":"// Ensure both required variables are present before loading a prompt\nfunction hasRequiredVars(v: Partial<PromptVariables>): v is PromptVariables {\n  return typeof v.webUrl === 'string' && v.webUrl.length > 0 &&\n    typeof v.repoPath === 'string' && v.repoPath.length > 0;\n}\nif (!hasRequiredVars(variables)) {\n  throw new Error(`Missing required prompt variables: webUrl/repoPath`);\n}","typeGuard":"function hasWebUrlAndRepoPath(v: unknown): v is { webUrl: string; repoPath: string } {\n  return typeof v === 'object' && v !== null &&\n    typeof (v as any).webUrl === 'string' && (v as any).webUrl.length > 0 &&\n    typeof (v as any).repoPath === 'string' && (v as any).repoPath.length > 0;\n}","tryCatchPattern":"try {\n  await loadPrompt(name, variables, config, false, logger);\n} catch (e) {\n  if (e instanceof PentestError && /Variables must include webUrl and repoPath/.test(e.message)) {\n    // pipeline-input reconstruction is incomplete — rebuild vars from session.json\n    variables = { webUrl: session.session.webUrl, repoPath: expectedRepoPath, ...variables };\n  }\n  throw e;\n}","preventionTips":["Always start scans with both -u <url> and -r <repo>.","When reconstructing PromptVariables during resume, persist and reload webUrl/repoPath from session.json.","Type-narrow variables at the boundary with hasWebUrlAndRepoPath before passing to loadPrompt.","Unit-test prompt loading with a fully populated variables object."],"tags":["prompt","validation","typescript","pipeline-input"],"backgroundTag":null,"analyzedSha":"1ae0a142f8525410a688f0309fd003cc5b1d92de","analyzedAt":"2026-08-12T17:40:03.583Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}