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 docs/concepts/work-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ POST /workitems/{id}/redrive-upstream

`GET` lists settled items (`Failed`, `MergeConflictResolutionFailed`, `Merged`) whose own branch has a stale or diverged remote: items with an open PR on their branch (matched by recorded PR number or, for legacy items with no recorded number, by head branch), plus `upstream_blocked` items whose branch has a live remote tip but no open PR. Each entry carries the PR head, the last-pushed SHA, and the reason. `POST` records the open PR's currently observed head as the operator-authorized lease base (invoking it asserts no third party pushed to the branch) and resumes the upstream phase under the normal lease rules — a concurrent third-party push still fails the lease and parks. Returns `202` when armed, `404` when the item is gone, `409` when the state/branch/PR preconditions do not hold or the retry is refused. A startup scan logs the same candidates once at boot; nothing is pushed without the explicit `POST`.

Two recovery paths cover items the recorded-number join would miss. When the item has no recorded PR number, the open PR is discovered by head branch (`codeybox/<id>`), recorded on the item, and used; when no open PR exists at all, the push is re-driven alone and the pipeline opens a fresh PR. When the branch has no recorded push, the remote tip must prove itself: the re-drive (and the push path itself) accepts the tip only when every commit exclusive of the base carries the configured CodeyBox trailers (`CodeyBox:UpstreamBranchOwnership`), otherwise it keeps refusing with a message that names this endpoint and the exact flag. Pass `{"confirmOwnership": true}` in the `POST` body to assert the tip is CodeyBox's own history explicitly — the assertion is logged and audited.
Two recovery paths cover items the recorded-number join would miss. When the item has no recorded PR number, the open PR is discovered by head branch (`codeybox/<id>`), recorded on the item, and used; when no open PR exists at all, the push is re-driven alone and the pipeline opens a fresh PR. When the branch has no recorded push, the remote tip must prove itself: the re-drive (and the push path itself) accepts the tip only when every commit exclusive of the current base tip carries any accepted CodeyBox trailer (`CodeyBox:UpstreamBranchOwnership`) — commits already reachable from the base never count against the branch — otherwise it keeps refusing with a message that names the offending commit SHAs, this endpoint, and the exact flag. Pass `{"confirmOwnership": true}` in the `POST` body to assert the tip is CodeyBox's own history explicitly — the assertion is logged and audited.

### AbandonedAfterRecoveryAttempts

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,14 +626,14 @@ Proves a stale CodeyBox-owned branch tip is CodeyBox's own history when no prior

```json
"UpstreamBranchOwnership": {
"RequiredTrailerKeys": ["CodeyBox-WorkItem", "Co-Authored-By"],
"RequiredTrailerKeys": ["Co-Authored-By", "CodeyBox-WorkItem", "CodeyBox-Agent", "CodeyBox-Prompt-Revision", "CodeyBox-Mechanical-Fixer"],
"MaxCommitsToVerify": 100
}
```

| Key | Default | Description |
|-----|---------|-------------|
| `RequiredTrailerKeys` | `["CodeyBox-WorkItem", "Co-Authored-By"]` | Trailer keys that must each appear in the trailer block of every commit exclusive of the base. `Co-Authored-By` additionally requires its value to name CodeyBox. An empty list never proves ownership. |
| `RequiredTrailerKeys` | `["Co-Authored-By", "CodeyBox-WorkItem", "CodeyBox-Agent", "CodeyBox-Prompt-Revision", "CodeyBox-Mechanical-Fixer"]` | Trailer keys accepted as ownership proof, any-of: each commit exclusive of the current base tip counts as CodeyBox's own history when its trailer block carries ANY one of these keys (matched exactly). Any-of because each writer stamps a different subset (agents write `CodeyBox-Prompt-Revision` + `Co-Authored-By`; the orchestrator adds `CodeyBox-WorkItem` + `CodeyBox-Agent` / `CodeyBox-Mechanical-Fixer`). `Co-Authored-By` additionally requires its value to equal the canonical CodeyBox identity exactly. Keys from a family disabled by `CodeyBox:CommitAttribution` cannot prove ownership; when attribution disables every family (or the list is empty), nothing proves ownership and the re-drive requires `{"confirmOwnership": true}`. A refusal names the offending commit SHAs. |
| `MaxCommitsToVerify` | `100` | Upper bound on the exclusive commits examined, enforced before buffering. A larger exclusive set is unverifiable and refused unless the operator passes `{"confirmOwnership": true}` on the re-drive. |

## `WorkerPoolHealthWatchdog`
Expand Down
3 changes: 2 additions & 1 deletion src/CodeyBox.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4875,7 +4875,8 @@ static Func<TestRunOptions> DotnetTestRunOptionsAccessor(IServiceProvider sp)
sp.GetRequiredService<IUpstreamRemoteFactory>(),
sp.GetRequiredService<WorkItemRetrier>(),
sp.GetRequiredService<ILogger<UpstreamRedriveService>>(),
sp.GetRequiredService<CodeyBox.Core.UpstreamBranchOwnershipSnapshot>()));
sp.GetRequiredService<CodeyBox.Core.UpstreamBranchOwnershipSnapshot>(),
sp.GetRequiredService<CodeyBox.Core.CommitAttributionPolicy>()));
builder.Services.AddHostedService(sp => new StaleUpstreamRedriveDetector(
sp.GetRequiredService<UpstreamRedriveService>(),
sp.GetRequiredService<ILogger<StaleUpstreamRedriveDetector>>()));
Expand Down
8 changes: 7 additions & 1 deletion src/CodeyBox.Core/CodeyBoxTrailers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ namespace CodeyBox.Core;
/// </summary>
public static class CodeyBoxTrailers
{
public const string CoAuthoredBy = "Co-Authored-By: CodeyBox <noreply@codeybox.invalid>";
/// <summary>
/// Canonical value of the terminal co-author trailer. Compared by exact
/// ordinal equality wherever ownership or attribution is decided — a
/// foreign <c>Co-Authored-By</c> value must never satisfy a CodeyBox check.
/// </summary>
public const string CoAuthoredByValue = "CodeyBox <noreply@codeybox.invalid>";
public const string CoAuthoredBy = "Co-Authored-By: " + CoAuthoredByValue;
public const string WorkItemTrailerKey = "CodeyBox-WorkItem";
public const string AgentTrailerKey = "CodeyBox-Agent";
public const string MechanicalFixerTrailerKey = "CodeyBox-Mechanical-Fixer";
Expand Down
29 changes: 29 additions & 0 deletions src/CodeyBox.Core/IUpstreamRemote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,27 @@ Task<IReadOnlyList<UpstreamPullRequest>> ListOpenPullRequestsAsync(
string baseBranch, string head, int maxCommits, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<string>?>(null);

/// <summary>
/// Lists the commits reachable from <paramref name="head"/> (a branch
/// name or commit sha) but not from <paramref name="baseRevision"/> (a
/// branch name or commit sha — pass the resolved current base tip sha to
/// pin the range to <c>&lt;base tip&gt;..&lt;remote tip&gt;</c>),
/// oldest first, with each commit's sha and full message. Same
/// ownership-proof use, bounds, and null-when-unsupported contract as
/// <see cref="ListBranchCommitMessagesAsync"/>, plus the shas so a
/// refusal can name exactly which commits blocked the proof. The default
/// derives from <see cref="ListBranchCommitMessagesAsync"/> with unknown
/// shas; forges with a real commit API override this instead.
/// </summary>
async Task<IReadOnlyList<OwnedBranchCommit>?> ListBranchCommitsAsync(
string baseRevision, string head, int maxCommits, CancellationToken ct = default)
{
var messages = await ListBranchCommitMessagesAsync(baseRevision, head, maxCommits, ct).ConfigureAwait(false);
return messages is null
? null
: messages.Select(m => new OwnedBranchCommit(string.Empty, m)).ToList();
}

/// <summary>
/// Forge capability seam for audit check-run publication. Returns whether
/// this remote can publish structured audit findings as forge check runs.
Expand Down Expand Up @@ -278,6 +299,14 @@ public sealed record UpstreamPullRequestState(
PullRequestStatus Status,
string? MergeCommitSha);

/// <summary>
/// One commit in a branch's exclusive range (<c>&lt;base&gt;..&lt;head&gt;</c>),
/// oldest first. <see cref="Sha"/> is the full commit sha (empty when the
/// forge did not report one); <see cref="Message"/> is the full commit
/// message, bounded by the listing implementation.
/// </summary>
public sealed record OwnedBranchCommit(string Sha, string Message);

/// <summary>
/// Snapshot of an open pull request as seen by an <see cref="IUpstreamRemote"/>
/// at a point in time. Fields mirror the subset of the forge PR object the
Expand Down
168 changes: 135 additions & 33 deletions src/CodeyBox.Core/UpstreamBranchOwnership.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,26 @@ namespace CodeyBox.Core;
public sealed class UpstreamBranchOwnershipOptions
{
/// <summary>
/// Trailer keys that must each appear on every commit reachable from the
/// remote tip but not from the base for the tip to count as CodeyBox's
/// own history. Matched as <c>Key: value</c> trailer lines (key compared
/// ordinal-ignore-case); <c>Co-Authored-By</c> additionally requires its
/// value to name CodeyBox so a foreign <c>Co-Authored-By: attacker</c>
/// line cannot pass. Defaults cover the trailers every CodeyBox-emitted
/// commit carries (see <see cref="CodeyBoxTrailers"/>).
/// Trailer keys accepted as ownership proof, any-of: a commit reachable
/// from the remote tip but not from the current base tip counts as
/// CodeyBox's own history when its trailer block carries ANY one of
/// these keys (matched exactly; <c>Co-Authored-By</c> additionally
/// requires its value to be exactly the canonical CodeyBox identity so
/// a foreign <c>Co-Authored-By: attacker</c> line cannot pass). Any-of
/// because the writers stamp different sets: agents write
/// <c>CodeyBox-Prompt-Revision</c> + <c>Co-Authored-By</c>, the
/// orchestrator writes <c>CodeyBox-WorkItem</c> + <c>CodeyBox-Agent</c> /
/// <c>CodeyBox-Mechanical-Fixer</c> + <c>Co-Authored-By</c> (see
/// <see cref="CodeyBoxTrailers"/>). Requiring every key on every commit
/// refused branches that are provably CodeyBox's own. Defaults cover
/// every trailer key this instance writes (see
/// <see cref="BranchOwnershipPolicy.DefaultRequiredTrailerKeys"/>); an
/// empty list never proves ownership. Attribution filtering applies on
/// top (see <see cref="BranchOwnershipPolicy.EffectiveAcceptedKeys"/>):
/// keys the instance no longer writes cannot prove anything it wrote.
/// </summary>
public string[] RequiredTrailerKeys { get; set; } =
[CodeyBoxTrailers.WorkItemTrailerKey, "Co-Authored-By"];
[.. BranchOwnershipPolicy.DefaultRequiredTrailerKeys];

/// <summary>
/// Upper bound on the exclusive commits examined for ownership proof.
Expand Down Expand Up @@ -62,68 +72,160 @@ public void Replace(UpstreamBranchOwnershipOptions next)
/// <summary>
/// Pure ownership proof over commit messages: a remote tip counts as
/// CodeyBox's own history only when EVERY commit reachable from it but not
/// from the base carries the CodeyBox trailers. A single unmarked commit
/// fails the whole tip — a third party's commit anywhere in the exclusive
/// range must never be rewritten.
/// from the current base tip carries ANY accepted CodeyBox trailer in its
/// trailer block. A single unmarked commit fails the whole tip — a third
/// party's commit anywhere in the exclusive range must never be rewritten.
/// </summary>
public static class BranchOwnershipPolicy
{
/// <summary>
/// Every trailer key this instance writes on CodeyBox-emitted commits
/// (see <see cref="CodeyBoxTrailers"/>). A commit carrying any one of
/// these (subject to attribution filtering) is CodeyBox's own.
/// </summary>
public static readonly string[] KnownCodeyBoxTrailerKeys =
[
"Co-Authored-By",
CodeyBoxTrailers.WorkItemTrailerKey,
CodeyBoxTrailers.AgentTrailerKey,
CodeyBoxTrailers.PromptRevisionTrailerKey,
CodeyBoxTrailers.MechanicalFixerTrailerKey,
];

/// <summary>
/// Default required trailer keys when no configured options are
/// available. Single source of truth for the fallback; the configured
/// <see cref="UpstreamBranchOwnershipOptions.RequiredTrailerKeys"/> wins
/// whenever options are present.
/// whenever options are present. Covers every trailer key this instance
/// writes (see <see cref="KnownCodeyBoxTrailerKeys"/>) because each
/// writer stamps a different subset.
/// </summary>
public static readonly string[] DefaultRequiredTrailerKeys =
[CodeyBoxTrailers.WorkItemTrailerKey, "Co-Authored-By"];
[.. KnownCodeyBoxTrailerKeys];

/// <summary>
/// True when <paramref name="commitMessage"/> carries every key in
/// <paramref name="requiredTrailerKeys"/> as a <c>Key: value</c> trailer
/// line. Keys compare ordinal-ignore-case; values must be non-empty, and
/// a <c>Co-Authored-By</c> value must name CodeyBox (ordinal) so a
/// True when <paramref name="commitMessage"/> carries ANY key in
/// <paramref name="acceptedTrailerKeys"/> as a <c>Key: value</c> trailer
/// line. Keys compare ordinally (exact); values must be non-empty, and a
/// <c>Co-Authored-By</c> value must exactly equal the canonical CodeyBox
/// identity (<see cref="CodeyBoxTrailers.CoAuthoredByValue"/>) so a
/// foreign co-author line cannot satisfy the check.
/// </summary>
public static bool IsOwnedCommit(string? commitMessage, IReadOnlyList<string> requiredTrailerKeys)
public static bool IsOwnedCommit(string? commitMessage, IReadOnlyList<string> acceptedTrailerKeys)
{
if (string.IsNullOrWhiteSpace(commitMessage) || requiredTrailerKeys.Count == 0)
if (string.IsNullOrWhiteSpace(commitMessage) || acceptedTrailerKeys.Count == 0)
return false;
foreach (var key in requiredTrailerKeys)
foreach (var key in acceptedTrailerKeys)
{
if (string.IsNullOrWhiteSpace(key))
return false;
if (!HasTrailer(commitMessage, key.Trim()))
return false;
continue;
if (HasTrailer(commitMessage, key.Trim()))
return true;
}
return true;
return false;
}

/// <summary>
/// True when <paramref name="exclusiveCommitMessages"/> is non-empty and
/// every entry satisfies <see cref="IsOwnedCommit"/>. An empty range is
/// NOT ownership (nothing proves CodeyBox wrote the tip); callers with a
/// True when <paramref name="exclusiveCommits"/> is non-empty and every
/// entry satisfies <see cref="IsOwnedCommit"/>. An empty range is NOT
/// ownership (nothing proves CodeyBox wrote the tip); callers with a
/// vacuous range decide separately.
/// </summary>
public static bool AreAllCommitsOwned(
IReadOnlyList<string> exclusiveCommitMessages,
IReadOnlyList<string> requiredTrailerKeys)
IReadOnlyList<string> acceptedTrailerKeys)
{
if (exclusiveCommitMessages.Count == 0)
return false;
foreach (var message in exclusiveCommitMessages)
{
if (!IsOwnedCommit(message, requiredTrailerKeys))
if (!IsOwnedCommit(message, acceptedTrailerKeys))
return false;
}
return true;
}

/// <summary>
/// Shas of the commits in <paramref name="exclusiveCommits"/> that carry
/// no accepted trailer, oldest first. Empty when the whole range is
/// CodeyBox's own. Callers name these shas in the refusal so the operator
/// can inspect exactly which commits blocked the proof.
/// </summary>
public static IReadOnlyList<string> FindUnownedCommitShas(
IReadOnlyList<OwnedBranchCommit> exclusiveCommits,
IReadOnlyList<string> acceptedTrailerKeys)
{
var unowned = new List<string>();
foreach (var commit in exclusiveCommits)
{
if (!IsOwnedCommit(commit.Message, acceptedTrailerKeys))
unowned.Add(commit.Sha);
}
return unowned;
}

/// <summary>
/// Human-readable summary of <paramref name="unownedShas"/> for refusal
/// messages: up to <paramref name="maxToName"/> shas named, the rest
/// counted. Entries are forge-reported shas already normalized to hex by
/// the listing implementation; blanks are never emitted, only counted.
/// </summary>
public static string DescribeUnownedCommits(IReadOnlyList<string> unownedShas, int maxToName = 10)
{
ArgumentNullException.ThrowIfNull(unownedShas);
if (maxToName < 1)
maxToName = 1;
var named = unownedShas.Where(s => !string.IsNullOrWhiteSpace(s)).Take(maxToName).ToList();
if (named.Count == 0)
return "unidentified commits";
var remaining = unownedShas.Count - named.Count;
var summary = string.Join(", ", named);
if (remaining > 0)
summary += $" (and {remaining} more)";
return summary;
}

/// <summary>
/// Narrows the configured ownership keys to what this instance can still
/// have written under <paramref name="attribution"/>: a disabled
/// <c>Co-Authored-By</c> family cannot prove anything (its line is
/// stripped from everything CodeyBox writes), and likewise for disabled
/// <c>CodeyBox-*</c> trailers. An empty result proves nothing — callers
/// must require explicit ownership confirmation. Configured keys compare
/// ordinal-ignore-case here (operator-written config); commit trailer
/// lines still match exactly (see <see cref="HasTrailer"/>).
/// </summary>
public static string[] EffectiveAcceptedKeys(
IReadOnlyList<string> configuredKeys,
CommitAttribution attribution)
{
ArgumentNullException.ThrowIfNull(configuredKeys);
ArgumentNullException.ThrowIfNull(attribution);
var accepted = new List<string>(configuredKeys.Count);
foreach (var key in configuredKeys)
{
if (string.IsNullOrWhiteSpace(key))
continue;
var trimmed = key.Trim();
if (!attribution.IncludeCoAuthoredBy
&& trimmed.Equals("Co-Authored-By", StringComparison.OrdinalIgnoreCase))
continue;
if (!attribution.IncludeCodeyBoxTrailers
&& trimmed.StartsWith("CodeyBox-", StringComparison.OrdinalIgnoreCase))
continue;
accepted.Add(trimmed);
}
return [.. accepted];
}

private static bool HasTrailer(string message, string key)
{
// Trailer block = contiguous trailing lines of `Token: value` form
// (git trailer semantics: only the end-of-message block counts, so a
// quoted trailer line in the body middle cannot satisfy the check).
// The walk stops at the first blank or non-trailer line.
// The walk stops at the first blank or non-trailer line. Keys match
// exactly (ordinal): CodeyBox always writes canonical-case keys, and
// a near-miss must not pass as proof of authorship.
var lines = message.Split('\n');
for (var i = lines.Length - 1; i >= 0; i--)
{
Expand All @@ -139,10 +241,10 @@ private static bool HasTrailer(string message, string key)
var value = line[(colon + 1)..].Trim();
if (value.Length == 0)
continue;
if (!name.Equals(key, StringComparison.OrdinalIgnoreCase))
if (!name.Equals(key, StringComparison.Ordinal))
continue;
if (string.Equals(key, "Co-Authored-By", StringComparison.OrdinalIgnoreCase)
&& !value.Contains("CodeyBox", StringComparison.Ordinal))
if (string.Equals(key, "Co-Authored-By", StringComparison.Ordinal)
&& !value.Equals(CodeyBoxTrailers.CoAuthoredByValue, StringComparison.Ordinal))
continue;
return true;
}
Expand Down
Loading
Loading