Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
52f1a79
fix: reject Git-equivalent repair refs
LogicDuke Aug 16, 2026
c743b65
docs: clarify symbolic ref resolution boundary
LogicDuke Aug 19, 2026
fc49e9a
docs: complete execution boundary ref contract
LogicDuke Aug 19, 2026
afba343
docs: make protected parent role-aware
LogicDuke Aug 20, 2026
c8fde78
Merge pull request #37 from LogicDuke/repair/c1-a04-change-request-ta…
LogicDuke Aug 20, 2026
bf78cf6
docs: bind change requests at provider boundary
LogicDuke Aug 20, 2026
7810d6e
docs: allow identity-preserving provider resolution
LogicDuke Aug 20, 2026
0bef03c
Merge pull request #39 from LogicDuke/repair/c1-provider-resolution-i…
LogicDuke Aug 20, 2026
838d8ed
Merge pull request #38 from LogicDuke/repair/c1-change-request-provid…
LogicDuke Aug 20, 2026
077933d
C1: bind repair.push source to authorized ref
LogicDuke Aug 20, 2026
6812607
C1: distinguish repair.push source role
LogicDuke Aug 20, 2026
1fa4cf6
Merge pull request #41 from LogicDuke/repair/pr040-push-source-role-w…
LogicDuke Aug 20, 2026
6fa3e95
Merge pull request #40 from LogicDuke/repair/c1-push-source-binding
LogicDuke Aug 20, 2026
7444080
Merge pull request #36 from LogicDuke/repair/c1-a04-exec-boundary-con…
LogicDuke Aug 20, 2026
467ccc3
C1-A04 clarify effective repository ref identity
LogicDuke Aug 21, 2026
7a3e53b
Merge pull request #42 from LogicDuke/repair/c1-a04-repository-ref-id…
LogicDuke Aug 21, 2026
a466075
Merge pull request #35 from LogicDuke/repair/c1-a04-symref-claim
LogicDuke Aug 21, 2026
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
259 changes: 252 additions & 7 deletions docs/architecture/C1-repair-job-authority.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export {
findInvalidRepairJobFields,
isVerificationCommandClass,
JOB_BOUNDS,
readCanonicalBranchRef,
readRepairJobAuthorization,
readRepositoryRelativePath,
REPAIR_JOB_FIELD_ORDER,
Expand Down
18 changes: 18 additions & 0 deletions src/domain/job-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ export const JOB_AUTHORIZATION_REASON = objectFreeze({
WORKTREE_NOT_AUTHORIZED: 'WORKTREE_NOT_AUTHORIZED',
/** The verification class is unmodeled or not in the job's authorized set. */
COMMAND_CLASS_NOT_AUTHORIZED: 'COMMAND_CLASS_NOT_AUTHORIZED',
/** A ref operand was supplied but is not a canonical `refs/heads/<name>` ref. */
REF_MALFORMED: 'REF_MALFORMED',
/** The operation names the protected parent ref as a write target. */
PROTECTED_REF_MUTATION: 'PROTECTED_REF_MUTATION',
/** The ref operand is not the job's isolated repair branch. */
Expand Down Expand Up @@ -264,6 +266,12 @@ function checkOperands(
if (request.worktreeId !== job.repairWorktreeId) {
return JOB_AUTHORIZATION_REASON.WORKTREE_NOT_AUTHORIZED;
}
// A non-canonical spelling is refused for being unusable, before any
// comparison: `main` and `heads/main` may both denote `refs/heads/main`,
// so comparing either as a distinct string is exactly the bypass.
if (request.refMalformed) {
return JOB_AUTHORIZATION_REASON.REF_MALFORMED;
}
if (request.ref === null) {
return JOB_AUTHORIZATION_REASON.OPERAND_MISSING;
}
Expand All @@ -281,6 +289,9 @@ function checkOperands(
if (request.force) {
return JOB_AUTHORIZATION_REASON.FORCE_PUSH_FORBIDDEN;
}
if (request.refMalformed) {
return JOB_AUTHORIZATION_REASON.REF_MALFORMED;
}
if (request.ref === null) {
return JOB_AUTHORIZATION_REASON.OPERAND_MISSING;
}
Expand All @@ -293,6 +304,13 @@ function checkOperands(
return null;
}
case JOB_OPERATION.REPAIR_CHANGE_REQUEST: {
// Both ends are narrowed to the canonical spelling before either is
// compared, so source/target separation is separation of branches rather
// than of strings: an alias of the protected parent cannot be presented
// as the source, and an alias of the repair branch cannot be the target.
if (request.sourceRefMalformed || request.targetRefMalformed) {
return JOB_AUTHORIZATION_REASON.REF_MALFORMED;
}
if (request.sourceRef === null || request.targetRef === null) {
return JOB_AUTHORIZATION_REASON.OPERAND_MISSING;
}
Expand Down
42 changes: 36 additions & 6 deletions src/domain/job-operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import {
append,
containsValue,
readCanonicalBranchRef,
readExactIdentifier,
readOwnProperty,
readRepositoryRelativePath,
Expand Down Expand Up @@ -246,11 +247,17 @@ export interface JobOperationRequest {
readonly path?: string;
/** Verification class operand, for `verification.run`. */
readonly commandClass?: string;
/** Ref operand, for `repair.commit` and `repair.push`. */
/**
* Ref operand, for `repair.commit` and `repair.push`.
*
* Read through the same canonical branch-ref reader the job envelope uses, so
* an alternate spelling of a configured ref cannot be compared against it as
* if it were a different branch.
*/
readonly ref?: string;
/** Change-request source ref operand. */
/** Change-request source ref operand. Canonical branch ref. */
readonly sourceRef?: string;
/** Change-request target ref operand. */
/** Change-request target ref operand. Canonical branch ref. */
readonly targetRef?: string;
/** Force flag for a push. Anything that is not exactly absent or `false` is force. */
readonly force?: boolean;
Expand Down Expand Up @@ -279,8 +286,14 @@ export interface NormalizedJobOperation {
readonly pathMalformed: boolean;
readonly commandClass: string | null;
readonly ref: string | null;
/** True when a `ref` was supplied but is not a canonical branch ref. */
readonly refMalformed: boolean;
readonly sourceRef: string | null;
/** True when a `sourceRef` was supplied but is not a canonical branch ref. */
readonly sourceRefMalformed: boolean;
readonly targetRef: string | null;
/** True when a `targetRef` was supplied but is not a canonical branch ref. */
readonly targetRefMalformed: boolean;
/** Fails closed: only an absent or literally `false` value is not force. */
readonly force: boolean;
}
Expand All @@ -298,8 +311,11 @@ const UNREADABLE_OPERATION: NormalizedJobOperation = objectFreeze({
pathMalformed: false,
commandClass: null,
ref: null,
refMalformed: false,
sourceRef: null,
sourceRefMalformed: false,
targetRef: null,
targetRefMalformed: false,
force: true,
});

Expand Down Expand Up @@ -331,6 +347,17 @@ export function readJobOperation(request: JobOperationRequest): NormalizedJobOpe
const rawPath = readOwnProperty(record, 'path');
const path = readRepositoryRelativePath(rawPath);

// Each ref operand is read once, own-only, and narrowed to the one canonical
// branch-ref spelling. A supplied value that is not canonical becomes `null`
// and is flagged, so it is refused for being unusable rather than compared —
// as a distinct string — against a canonical value it may in fact alias.
const rawRef = readOwnProperty(record, 'ref');
const ref = readCanonicalBranchRef(rawRef);
const rawSourceRef = readOwnProperty(record, 'sourceRef');
const sourceRef = readCanonicalBranchRef(rawSourceRef);
const rawTargetRef = readOwnProperty(record, 'targetRef');
const targetRef = readCanonicalBranchRef(rawTargetRef);

return objectFreeze({
readable: true,
requestId: readExactIdentifier(readOwnProperty(record, 'requestId')),
Expand All @@ -343,9 +370,12 @@ export function readJobOperation(request: JobOperationRequest): NormalizedJobOpe
path,
pathMalformed: path === null && rawPath !== undefined,
commandClass: readExactIdentifier(readOwnProperty(record, 'commandClass')),
ref: readExactIdentifier(readOwnProperty(record, 'ref')),
sourceRef: readExactIdentifier(readOwnProperty(record, 'sourceRef')),
targetRef: readExactIdentifier(readOwnProperty(record, 'targetRef')),
ref,
refMalformed: ref === null && rawRef !== undefined,
sourceRef,
sourceRefMalformed: sourceRef === null && rawSourceRef !== undefined,
targetRef,
targetRefMalformed: targetRef === null && rawTargetRef !== undefined,
force: readForceFlag(readOwnProperty(record, 'force')),
});
}
Expand Down
Loading
Loading