Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/discussion-task-miner.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions pkg/actionpins/data/action_pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@
}
},
"containers": {
"ghcr.io/fabio-rovai/open-ontologies:latest": {
"image": "ghcr.io/fabio-rovai/open-ontologies:latest",
"digest": "sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530",
"pinned_image": "ghcr.io/fabio-rovai/open-ontologies:latest@sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530"
},
"ghcr.io/github/gh-aw-firewall/agent:0.27.43": {
"image": "ghcr.io/github/gh-aw-firewall/agent:0.27.43",
"digest": "sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6",
Expand Down Expand Up @@ -264,11 +269,6 @@
"image": "python:alpine",
"digest": "sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92",
"pinned_image": "python:alpine@sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92"
},
"ghcr.io/fabio-rovai/open-ontologies:latest": {
"image": "ghcr.io/fabio-rovai/open-ontologies:latest",
"digest": "sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530",
"pinned_image": "ghcr.io/fabio-rovai/open-ontologies:latest@sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530"
}
}
}
10 changes: 5 additions & 5 deletions pkg/workflow/data/action_pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@
}
},
"containers": {
"ghcr.io/fabio-rovai/open-ontologies:latest": {
"image": "ghcr.io/fabio-rovai/open-ontologies:latest",
"digest": "sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530",
"pinned_image": "ghcr.io/fabio-rovai/open-ontologies:latest@sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530"
},
"ghcr.io/github/gh-aw-firewall/agent:0.27.43": {
"image": "ghcr.io/github/gh-aw-firewall/agent:0.27.43",
"digest": "sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6",
Expand Down Expand Up @@ -264,11 +269,6 @@
"image": "python:alpine",
"digest": "sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92",
"pinned_image": "python:alpine@sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92"
},
"ghcr.io/fabio-rovai/open-ontologies:latest": {
"image": "ghcr.io/fabio-rovai/open-ontologies:latest",
"digest": "sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530",
"pinned_image": "ghcr.io/fabio-rovai/open-ontologies:latest@sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530"
}
}
}
31 changes: 31 additions & 0 deletions pkg/workflow/unified_prompt_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ func (c *Compiler) collectPromptSections(data *WorkflowData) []PromptSection {
// The checkout list may contain ${{ github.repository }} which must go through
// the expression extractor so the placeholder substitution step can resolve it.
combinedPromptText := githubContextPromptText
// In trial mode with a logical target repository, the workflow runs on the host
// repo (github.repository) but checkout and safe outputs are redirected to the
// logical target. Rewrite the reported repository so the agent's notion of "the
// repository" matches the logical target, and add a directive so GitHub MCP calls
// that omit owner/repo don't silently operate against the host repo.
if data.TrialMode && data.TrialLogicalRepo != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no test covering the case where trial-mode logical-repo injection and buildCheckoutsPromptContent both fire in the same run, even though both insert content at the same </github-context> anchor sequentially in this function.

💡 Add an interaction test

applyTrialLogicalRepoToGitHubContext inserts its directive before </github-context> first, then (a few lines later) the checkout-list injection does strings.LastIndex(combinedPromptText, closeTag) again and inserts its own content before the same tag. This ordering happens to work today (checkouts land after the trial directive, both still land before the closing tag), but there's no test asserting the combined output is well-formed (e.g. checkout content isn't inserted between the repository line and the trial directive, tags aren't duplicated, and the trial directive doesn't get needlessly re-scanned/altered by expression extraction due to ${{ ... }}-looking content it doesn't produce). Add a test with TrialMode: true, TrialLogicalRepo set, and non-empty CheckoutConfigs to lock in the expected ordering.

combinedPromptText = applyTrialLogicalRepoToGitHubContext(combinedPromptText, data.TrialLogicalRepo)
}
if checkoutsContent := buildCheckoutsPromptContent(data.CheckoutConfigs); checkoutsContent != "" {
unifiedPromptLog.Printf("Injecting checkout list into GitHub context (%d checkouts)", len(data.CheckoutConfigs))
const closeTag = "</github-context>"
Expand Down Expand Up @@ -780,3 +788,26 @@ func buildSafeOutputsSections(safeOutputs *SafeOutputsConfig) []PromptSection {

return sections
}

// applyTrialLogicalRepoToGitHubContext rewrites the github-context prompt so that
// the reported repository is the trial mode logical target rather than the host
// repository (github.repository). It replaces the unconditional repository line and
// appends a directive instructing the agent to target the logical repository for
// GitHub MCP calls that omit an explicit owner/repo.
func applyTrialLogicalRepoToGitHubContext(promptText, logicalRepo string) string {
const repoLine = "- **repository**: ${{ github.repository }}"
replacement := "- **repository**: " + logicalRepo
promptText = strings.Replace(promptText, repoLine, replacement, 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] Silent no-op if the repository line format ever drifts — strings.Replace returns the original string unchanged with no log or error.

💡 Suggestion

If the template changes (extra space, different casing, etc.) the replacement silently finds nothing, so the agent still sees the host repo with no indication anything failed.

Consider adding a guard:

replaced := strings.Replace(promptText, repoLine, replacement, 1)
if replaced == promptText {
    unifiedPromptLog.Printf("warning: repository line not found in github-context prompt; logical repo not applied")
}
promptText = replaced

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strings.Replace on repoLine silently no-ops if the literal template string doesn't match, which would silently reintroduce the exact host-repo leak this PR exists to fix.

💡 Fix: detect and surface a failed replacement

strings.Replace(promptText, repoLine, replacement, 1) gives no signal whether a match occurred. If pkg/workflow/prompts/github_context_prompt.md is later edited (reformatted, reindented, or the ${{ github.repository }} expression is changed), this function will silently return the unmodified text — still containing ${{ github.repository }} — while still appending the directive claiming "The repository above (%s) is the logical target repository." That's an internally inconsistent, wrong prompt with no compile-time or runtime signal, i.e. exactly the silent host/logical-repo mismatch class of bug this PR was written to eliminate.

Suggested change:

newText := strings.Replace(promptText, repoLine, replacement, 1)
if newText == promptText {
    unifiedPromptLog.Printf("WARNING: could not rewrite repository line for trial logical repo %q; github-context template may have changed", logicalRepo)
    return promptText // don't append a directive that references a substitution that never happened
}
promptText = newText

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If repoLine is not found in promptText (e.g., the template in github_context_prompt.md is later updated), strings.Replace silently no-ops and returns the original text unchanged. The host repo remains visible in the prompt, but the directive still says "The repository above (owner/target) is the logical target" — a direct contradiction that would mislead the agent with no observable failure.

Consider logging a warning when the replacement doesn't occur:

if !strings.Contains(promptText, repoLine) {
    unifiedPromptLog.Printf("WARNING: trial logical repo substitution skipped: repo line not found in github-context prompt")
}
promptText = strings.Replace(promptText, repoLine, replacement, 1)

@copilot please address this.


directive := fmt.Sprintf(
"\nThis workflow is running in trial mode. The repository above (%s) is the logical target repository. When calling GitHub tools without an explicit owner/repo, use this repository.\n",
logicalRepo,
)
const closeTag = "</github-context>"
if idx := strings.LastIndex(promptText, closeTag); idx >= 0 {
promptText = promptText[:idx] + directive + promptText[idx:]
} else {
promptText += directive
}
return promptText
}
60 changes: 60 additions & 0 deletions pkg/workflow/unified_prompt_step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,3 +718,63 @@ func TestGenerateUnifiedPromptCreationStep_BlankRunCapAdjacentToRuntimeImport(t
// No run of four or more newlines (i.e., 3+ consecutive blank lines) anywhere.
assert.NotContains(t, output, "\n\n\n\n", "blank run should be capped throughout the output")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test exercises the failure mode where repoLine doesn't match promptText, so a template-drift regression that silently reintroduces the host-repo leak would pass CI undetected.

💡 Add a regression test for template drift

Both new tests only check the "happy path" where githubContextPromptText contains the exact repoLine constant. Add a test that calls applyTrialLogicalRepoToGitHubContext directly with a prompt text that does not contain the literal - **repository**: ${{ github.repository }} line (e.g. simulating template drift), and assert on the desired behavior (e.g. no directive appended, or a specific fallback), once the fallback logic from the sibling comment is implemented. Without this, a future edit to github_context_prompt.md can silently break trial-mode's repo redirection with zero test signal — the exact "undetectable in a green run" failure class described in this PR's own description.

func TestCollectPromptSections_TrialLogicalRepoGitHubContext(t *testing.T) {
compiler := &Compiler{}

data := &WorkflowData{
ParsedTools: NewTools(map[string]any{
"github": true,
}),
Permissions: "contents: read",
On: "issue_comment",
TrialMode: true,
TrialLogicalRepo: "owner/target",
}

sections := compiler.collectPromptSections(data)

var githubContext string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The trial-mode test asserts the directive contains the string "trial mode" but not the logical repo slug inside the directive text itself — a future rephrasing could drop the slug from the directive while still passing this test.

💡 Suggestion

Add a tighter assertion on the directive content:

assert.Contains(t, githubContext, "owner/target",
    "trial mode directive should reference the logical repo slug")

This ensures both the repository line and the directive consistently reference the logical repo.

@copilot please address this.

for _, section := range sections {
if !section.IsFile && strings.Contains(section.Content, "github-context") {
githubContext = section.Content
break
}
}
require.NotEmpty(t, githubContext, "Should have a github-context section")

assert.Contains(t, githubContext, "- **repository**: owner/target",
"github-context should report the logical target repository")
assert.NotContains(t, githubContext, "**repository**: ${{ github.repository }}",
"github-context should not report the host repository in trial mode")
assert.Contains(t, githubContext, "trial mode",
"github-context should include a trial mode directive")
Comment on lines +750 to +751
}

func TestCollectPromptSections_NoTrialLogicalRepoKeepsHostRepo(t *testing.T) {
compiler := &Compiler{}

data := &WorkflowData{
ParsedTools: NewTools(map[string]any{
"github": true,
}),
Permissions: "contents: read",
On: "issue_comment",
}

sections := compiler.collectPromptSections(data)

var githubContext string
for _, section := range sections {
if !section.IsFile && strings.Contains(section.Content, "github-context") {
githubContext = section.Content
break
}
}
require.NotEmpty(t, githubContext, "Should have a github-context section")

assert.Contains(t, githubContext, "**repository**:",
"github-context should report the repository via the github.repository expression")
Comment on lines +767 to +777
assert.NotContains(t, githubContext, "owner/target",
"github-context should not include a logical target when trial mode is off")
}
Loading