codex-orchestrator 0.1.47 → 0.1.48
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/CHANGELOG.md +20 -0
- package/dist/src/config/schema.d.ts +5 -1
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/config/schema.js +16 -0
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/runner/command-utils.d.ts.map +1 -1
- package/dist/src/runner/command-utils.js +1 -0
- package/dist/src/runner/command-utils.js.map +1 -1
- package/dist/src/runner/local-execution-session.d.ts.map +1 -1
- package/dist/src/runner/local-execution-session.js +5 -2
- package/dist/src/runner/local-execution-session.js.map +1 -1
- package/dist/src/runner/mission-activation.d.ts +29 -0
- package/dist/src/runner/mission-activation.d.ts.map +1 -0
- package/dist/src/runner/mission-activation.js +54 -0
- package/dist/src/runner/mission-activation.js.map +1 -0
- package/dist/src/runner/mission-agent-broker.d.ts +53 -0
- package/dist/src/runner/mission-agent-broker.d.ts.map +1 -0
- package/dist/src/runner/mission-agent-broker.js +182 -0
- package/dist/src/runner/mission-agent-broker.js.map +1 -0
- package/dist/src/runner/mission-cancellation.d.ts +77 -0
- package/dist/src/runner/mission-cancellation.d.ts.map +1 -0
- package/dist/src/runner/mission-cancellation.js +180 -0
- package/dist/src/runner/mission-cancellation.js.map +1 -0
- package/dist/src/runner/mission-canonical-path.d.ts +34 -0
- package/dist/src/runner/mission-canonical-path.d.ts.map +1 -0
- package/dist/src/runner/mission-canonical-path.js +121 -0
- package/dist/src/runner/mission-canonical-path.js.map +1 -0
- package/dist/src/runner/mission-capability-kernel.d.ts +34 -0
- package/dist/src/runner/mission-capability-kernel.d.ts.map +1 -0
- package/dist/src/runner/mission-capability-kernel.js +158 -0
- package/dist/src/runner/mission-capability-kernel.js.map +1 -0
- package/dist/src/runner/mission-check-policy.d.ts +17 -0
- package/dist/src/runner/mission-check-policy.d.ts.map +1 -0
- package/dist/src/runner/mission-check-policy.js +31 -0
- package/dist/src/runner/mission-check-policy.js.map +1 -0
- package/dist/src/runner/mission-coordinator-lock.d.ts +16 -0
- package/dist/src/runner/mission-coordinator-lock.d.ts.map +1 -0
- package/dist/src/runner/mission-coordinator-lock.js +170 -0
- package/dist/src/runner/mission-coordinator-lock.js.map +1 -0
- package/dist/src/runner/mission-deployment.d.ts +21 -0
- package/dist/src/runner/mission-deployment.d.ts.map +1 -0
- package/dist/src/runner/mission-deployment.js +108 -0
- package/dist/src/runner/mission-deployment.js.map +1 -0
- package/dist/src/runner/mission-evaluation.d.ts +61 -0
- package/dist/src/runner/mission-evaluation.d.ts.map +1 -0
- package/dist/src/runner/mission-evaluation.js +183 -0
- package/dist/src/runner/mission-evaluation.js.map +1 -0
- package/dist/src/runner/mission-executor-probe.d.ts +10 -0
- package/dist/src/runner/mission-executor-probe.d.ts.map +1 -0
- package/dist/src/runner/mission-executor-probe.js +180 -0
- package/dist/src/runner/mission-executor-probe.js.map +1 -0
- package/dist/src/runner/mission-git-contracts.d.ts +75 -0
- package/dist/src/runner/mission-git-contracts.d.ts.map +1 -0
- package/dist/src/runner/mission-git-contracts.js +201 -0
- package/dist/src/runner/mission-git-contracts.js.map +1 -0
- package/dist/src/runner/mission-git-runtime-owner-ref.d.ts +13 -0
- package/dist/src/runner/mission-git-runtime-owner-ref.d.ts.map +1 -0
- package/dist/src/runner/mission-git-runtime-owner-ref.js +111 -0
- package/dist/src/runner/mission-git-runtime-owner-ref.js.map +1 -0
- package/dist/src/runner/mission-git-transaction.d.ts +97 -0
- package/dist/src/runner/mission-git-transaction.d.ts.map +1 -0
- package/dist/src/runner/mission-git-transaction.js +1009 -0
- package/dist/src/runner/mission-git-transaction.js.map +1 -0
- package/dist/src/runner/mission-identifiers.d.ts +31 -0
- package/dist/src/runner/mission-identifiers.d.ts.map +1 -0
- package/dist/src/runner/mission-identifiers.js +75 -0
- package/dist/src/runner/mission-identifiers.js.map +1 -0
- package/dist/src/runner/mission-input-snapshot.d.ts +21 -0
- package/dist/src/runner/mission-input-snapshot.d.ts.map +1 -0
- package/dist/src/runner/mission-input-snapshot.js +63 -0
- package/dist/src/runner/mission-input-snapshot.js.map +1 -0
- package/dist/src/runner/mission-ownership.d.ts +38 -0
- package/dist/src/runner/mission-ownership.d.ts.map +1 -0
- package/dist/src/runner/mission-ownership.js +88 -0
- package/dist/src/runner/mission-ownership.js.map +1 -0
- package/dist/src/runner/mission-patch-audit.d.ts +21 -0
- package/dist/src/runner/mission-patch-audit.d.ts.map +1 -0
- package/dist/src/runner/mission-patch-audit.js +186 -0
- package/dist/src/runner/mission-patch-audit.js.map +1 -0
- package/dist/src/runner/mission-path-language.d.ts +6 -0
- package/dist/src/runner/mission-path-language.d.ts.map +1 -0
- package/dist/src/runner/mission-path-language.js +106 -0
- package/dist/src/runner/mission-path-language.js.map +1 -0
- package/dist/src/runner/mission-permit-authority.d.ts +26 -0
- package/dist/src/runner/mission-permit-authority.d.ts.map +1 -0
- package/dist/src/runner/mission-permit-authority.js +145 -0
- package/dist/src/runner/mission-permit-authority.js.map +1 -0
- package/dist/src/runner/mission-plan-parent-coordinator.d.ts +87 -0
- package/dist/src/runner/mission-plan-parent-coordinator.d.ts.map +1 -0
- package/dist/src/runner/mission-plan-parent-coordinator.js +412 -0
- package/dist/src/runner/mission-plan-parent-coordinator.js.map +1 -0
- package/dist/src/runner/mission-plan-parent.d.ts +154 -0
- package/dist/src/runner/mission-plan-parent.d.ts.map +1 -0
- package/dist/src/runner/mission-plan-parent.js +583 -0
- package/dist/src/runner/mission-plan-parent.js.map +1 -0
- package/dist/src/runner/mission-process-executor.d.ts +25 -0
- package/dist/src/runner/mission-process-executor.d.ts.map +1 -0
- package/dist/src/runner/mission-process-executor.js +182 -0
- package/dist/src/runner/mission-process-executor.js.map +1 -0
- package/dist/src/runner/mission-quarantine-patch-executor.d.ts +29 -0
- package/dist/src/runner/mission-quarantine-patch-executor.d.ts.map +1 -0
- package/dist/src/runner/mission-quarantine-patch-executor.js +121 -0
- package/dist/src/runner/mission-quarantine-patch-executor.js.map +1 -0
- package/dist/src/runner/mission-repository-executor.d.ts +54 -0
- package/dist/src/runner/mission-repository-executor.d.ts.map +1 -0
- package/dist/src/runner/mission-repository-executor.js +192 -0
- package/dist/src/runner/mission-repository-executor.js.map +1 -0
- package/dist/src/runner/mission-repository-observer.d.ts +8 -0
- package/dist/src/runner/mission-repository-observer.d.ts.map +1 -0
- package/dist/src/runner/mission-repository-observer.js +24 -0
- package/dist/src/runner/mission-repository-observer.js.map +1 -0
- package/dist/src/runner/mission-reservations.d.ts +15 -0
- package/dist/src/runner/mission-reservations.d.ts.map +1 -0
- package/dist/src/runner/mission-reservations.js +116 -0
- package/dist/src/runner/mission-reservations.js.map +1 -0
- package/dist/src/runner/mission-resolution-loop.d.ts +172 -0
- package/dist/src/runner/mission-resolution-loop.d.ts.map +1 -0
- package/dist/src/runner/mission-resolution-loop.js +318 -0
- package/dist/src/runner/mission-resolution-loop.js.map +1 -0
- package/dist/src/runner/mission-runtime-owner.d.ts +36 -0
- package/dist/src/runner/mission-runtime-owner.d.ts.map +1 -0
- package/dist/src/runner/mission-runtime-owner.js +174 -0
- package/dist/src/runner/mission-runtime-owner.js.map +1 -0
- package/dist/src/runner/mission-safe-executor.d.ts +96 -0
- package/dist/src/runner/mission-safe-executor.d.ts.map +1 -0
- package/dist/src/runner/mission-safe-executor.js +288 -0
- package/dist/src/runner/mission-safe-executor.js.map +1 -0
- package/dist/src/runner/mission-sandbox.d.ts +16 -0
- package/dist/src/runner/mission-sandbox.d.ts.map +1 -0
- package/dist/src/runner/mission-sandbox.js +91 -0
- package/dist/src/runner/mission-sandbox.js.map +1 -0
- package/dist/src/runner/mission-scheduler.d.ts +57 -0
- package/dist/src/runner/mission-scheduler.d.ts.map +1 -0
- package/dist/src/runner/mission-scheduler.js +205 -0
- package/dist/src/runner/mission-scheduler.js.map +1 -0
- package/dist/src/runner/mission-scope-expansion.d.ts +47 -0
- package/dist/src/runner/mission-scope-expansion.d.ts.map +1 -0
- package/dist/src/runner/mission-scope-expansion.js +147 -0
- package/dist/src/runner/mission-scope-expansion.js.map +1 -0
- package/dist/src/runner/mission-state-machine.d.ts +118 -0
- package/dist/src/runner/mission-state-machine.d.ts.map +1 -0
- package/dist/src/runner/mission-state-machine.js +316 -0
- package/dist/src/runner/mission-state-machine.js.map +1 -0
- package/dist/src/runner/mission-state-store.d.ts +70 -0
- package/dist/src/runner/mission-state-store.d.ts.map +1 -0
- package/dist/src/runner/mission-state-store.js +856 -0
- package/dist/src/runner/mission-state-store.js.map +1 -0
- package/dist/src/runner/rework-policy.d.ts +2 -1
- package/dist/src/runner/rework-policy.d.ts.map +1 -1
- package/dist/src/runner/rework-policy.js +3 -0
- package/dist/src/runner/rework-policy.js.map +1 -1
- package/dist/src/setup/project-config.d.ts.map +1 -1
- package/dist/src/setup/project-config.js +5 -0
- package/dist/src/setup/project-config.js.map +1 -1
- package/docs/deep-dive.md +25 -16
- package/package.json +1 -1
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { link, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { hostname, tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
7
|
+
import { defaultProcessExecutor } from '../process/command.js';
|
|
8
|
+
import { auditMissionPatch } from './mission-patch-audit.js';
|
|
9
|
+
import { missionApplyPermitFingerprint, validateMissionGitCommitIdentity, validateMissionApplyPermit, } from './mission-git-contracts.js';
|
|
10
|
+
import { transitionMission } from './mission-state-machine.js';
|
|
11
|
+
import { acquireMissionCoordinatorLock } from './mission-coordinator-lock.js';
|
|
12
|
+
export { createMissionApplyPermit, missionApplyPermitFingerprint, } from './mission-git-contracts.js';
|
|
13
|
+
export const missionGitBoundaries = [
|
|
14
|
+
'base-verified',
|
|
15
|
+
'isolated-index-created',
|
|
16
|
+
'patch-applied',
|
|
17
|
+
'tree-written',
|
|
18
|
+
'manifest-proved',
|
|
19
|
+
'commit-created',
|
|
20
|
+
'intent-persisted',
|
|
21
|
+
'ref-updated',
|
|
22
|
+
'receipt-persisted',
|
|
23
|
+
];
|
|
24
|
+
const missionGitProcessBootNonce = randomUUID();
|
|
25
|
+
const missionDurableGitConfig = [
|
|
26
|
+
'-c', 'core.fsync=all',
|
|
27
|
+
'-c', 'core.fsyncMethod=fsync',
|
|
28
|
+
];
|
|
29
|
+
export const defaultMissionBinaryProcessExecutor = (file, args, options) => new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(file, args, {
|
|
31
|
+
cwd: options.cwd,
|
|
32
|
+
env: options.env,
|
|
33
|
+
shell: false,
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
});
|
|
36
|
+
const stdout = [];
|
|
37
|
+
const stderr = [];
|
|
38
|
+
let outputBytes = 0;
|
|
39
|
+
let terminationError;
|
|
40
|
+
let settled = false;
|
|
41
|
+
const timeout = setTimeout(() => {
|
|
42
|
+
terminationError ??= new Error('Mission binary process timed out.');
|
|
43
|
+
child.kill('SIGKILL');
|
|
44
|
+
}, options.timeoutMs);
|
|
45
|
+
timeout.unref();
|
|
46
|
+
const collect = (target, chunk) => {
|
|
47
|
+
if (terminationError)
|
|
48
|
+
return;
|
|
49
|
+
outputBytes += chunk.byteLength;
|
|
50
|
+
if (outputBytes > options.maxOutputBytes) {
|
|
51
|
+
terminationError = new Error('Mission binary process exceeded its output limit.');
|
|
52
|
+
child.kill('SIGKILL');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
target.push(chunk);
|
|
56
|
+
};
|
|
57
|
+
child.stdout.on('data', (chunk) => collect(stdout, chunk));
|
|
58
|
+
child.stderr.on('data', (chunk) => collect(stderr, chunk));
|
|
59
|
+
child.on('error', (error) => {
|
|
60
|
+
if (settled)
|
|
61
|
+
return;
|
|
62
|
+
settled = true;
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
reject(error);
|
|
65
|
+
});
|
|
66
|
+
child.on('close', (exitCode) => {
|
|
67
|
+
if (settled)
|
|
68
|
+
return;
|
|
69
|
+
settled = true;
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
if (terminationError) {
|
|
72
|
+
reject(terminationError);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
resolve({
|
|
76
|
+
stdout: Buffer.concat(stdout),
|
|
77
|
+
stderr: Buffer.concat(stderr),
|
|
78
|
+
exitCode: exitCode ?? 1,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
export class MissionGitSafetyStopError extends Error {
|
|
83
|
+
code;
|
|
84
|
+
constructor(code, message) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.code = code;
|
|
87
|
+
this.name = 'MissionGitSafetyStopError';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export class MissionGitIntegrationConflictError extends Error {
|
|
91
|
+
paths;
|
|
92
|
+
constructor(paths) {
|
|
93
|
+
super(`Mission tree integration has conflicts: ${paths.join(', ') || 'unknown paths'}.`);
|
|
94
|
+
this.paths = paths;
|
|
95
|
+
this.name = 'MissionGitIntegrationConflictError';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class MissionGitReauthorizationRequiredError extends Error {
|
|
99
|
+
code;
|
|
100
|
+
constructor(code, message) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.code = code;
|
|
103
|
+
this.name = 'MissionGitReauthorizationRequiredError';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function buildMissionPatchCandidate(input) {
|
|
107
|
+
const execute = input.execute ?? defaultProcessExecutor;
|
|
108
|
+
const commit = validateMissionGitCommitIdentity(input.commit);
|
|
109
|
+
assertAuditedPatch(input.patch, input.auditedFiles);
|
|
110
|
+
const baseCommit = await resolveObject(execute, input.targetRoot, `${input.baseCommit}^{commit}`, 'base commit');
|
|
111
|
+
if (baseCommit !== input.baseCommit) {
|
|
112
|
+
throw new Error('Mission Git base commit must be the full canonical object ID.');
|
|
113
|
+
}
|
|
114
|
+
const baseTree = await resolveObject(execute, input.targetRoot, `${baseCommit}^{tree}`, 'base tree');
|
|
115
|
+
await emit(input.onBoundary, 'base-verified');
|
|
116
|
+
const transactionRoot = input.temporaryRoot
|
|
117
|
+
? await createMissionGitTemporaryDirectory(input.temporaryRoot)
|
|
118
|
+
: await mkdtemp(join(tmpdir(), 'codex-mission-git-index-'));
|
|
119
|
+
try {
|
|
120
|
+
const indexPath = join(transactionRoot, 'index');
|
|
121
|
+
const worktreePath = join(transactionRoot, 'worktree');
|
|
122
|
+
await mkdir(worktreePath);
|
|
123
|
+
const environment = gitEnvironment(indexPath, worktreePath, transactionRoot, commit);
|
|
124
|
+
await git(execute, input.targetRoot, ['read-tree', baseTree], { environment });
|
|
125
|
+
await emit(input.onBoundary, 'isolated-index-created');
|
|
126
|
+
await git(execute, input.targetRoot, [
|
|
127
|
+
'apply', '--cached', '--whitespace=nowarn', '-p1', '-',
|
|
128
|
+
], { environment, stdin: input.patch });
|
|
129
|
+
await emit(input.onBoundary, 'patch-applied');
|
|
130
|
+
const treeSha = objectId((await git(execute, input.targetRoot, ['write-tree'], { environment })).stdout, 'candidate tree');
|
|
131
|
+
await emit(input.onBoundary, 'tree-written');
|
|
132
|
+
const manifest = await readManifest(execute, input.executeBinary ?? defaultMissionBinaryProcessExecutor, input.targetRoot, baseTree, treeSha, environment);
|
|
133
|
+
assertExactManifestShape(manifest, input.auditedFiles);
|
|
134
|
+
await emit(input.onBoundary, 'manifest-proved');
|
|
135
|
+
const commitSha = objectId((await git(execute, input.targetRoot, [
|
|
136
|
+
'commit-tree', treeSha, '-p', baseCommit, '-F', '-',
|
|
137
|
+
], { environment, stdin: commit.message })).stdout, 'candidate commit');
|
|
138
|
+
await emit(input.onBoundary, 'commit-created');
|
|
139
|
+
const committedTree = await resolveObject(execute, input.targetRoot, `${commitSha}^{tree}`, 'committed tree');
|
|
140
|
+
if (committedTree !== treeSha) {
|
|
141
|
+
throw new MissionGitSafetyStopError('candidate-tree-mismatch', 'Candidate commit tree differs from the proved tree.');
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
baseCommit,
|
|
145
|
+
baseTree,
|
|
146
|
+
patchSha256: sha256(input.patch),
|
|
147
|
+
treeSha,
|
|
148
|
+
commitSha,
|
|
149
|
+
manifest,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await rm(transactionRoot, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export async function buildMissionTreeIntegrationCandidate(input) {
|
|
157
|
+
const execute = input.execute ?? defaultProcessExecutor;
|
|
158
|
+
const commit = validateMissionGitCommitIdentity(input.commit);
|
|
159
|
+
const baseCommit = await resolveObject(execute, input.targetRoot, `${input.baseCommit}^{commit}`, 'integration base commit');
|
|
160
|
+
const parentCommit = await resolveObject(execute, input.targetRoot, `${input.parentCommit}^{commit}`, 'integration parent commit');
|
|
161
|
+
const childCommit = await resolveObject(execute, input.targetRoot, `${input.childCommit}^{commit}`, 'integration child commit');
|
|
162
|
+
if (baseCommit !== input.baseCommit || parentCommit !== input.parentCommit || childCommit !== input.childCommit) {
|
|
163
|
+
throw new Error('Mission integration commits must be full canonical object IDs.');
|
|
164
|
+
}
|
|
165
|
+
const parentTree = await resolveObject(execute, input.targetRoot, `${parentCommit}^{tree}`, 'integration parent tree');
|
|
166
|
+
const environment = gitEnvironment('', '', tmpdir(), commit);
|
|
167
|
+
delete environment.GIT_INDEX_FILE;
|
|
168
|
+
delete environment.GIT_WORK_TREE;
|
|
169
|
+
const objectPathValue = (await git(execute, input.targetRoot, [
|
|
170
|
+
'rev-parse', '--git-path', 'objects',
|
|
171
|
+
], { environment })).stdout.trim();
|
|
172
|
+
const objectDirectory = isAbsolute(objectPathValue)
|
|
173
|
+
? objectPathValue
|
|
174
|
+
: join(input.targetRoot, objectPathValue);
|
|
175
|
+
const isolatedRoot = await mkdtemp(join(tmpdir(), 'codex-mission-integration-'));
|
|
176
|
+
try {
|
|
177
|
+
await git(execute, input.targetRoot, ['init', '--bare', isolatedRoot], { environment });
|
|
178
|
+
await writeFile(join(isolatedRoot, 'objects', 'info', 'alternates'), `${objectDirectory}\n`, {
|
|
179
|
+
encoding: 'utf8',
|
|
180
|
+
mode: 0o600,
|
|
181
|
+
});
|
|
182
|
+
const isolatedEnvironment = {
|
|
183
|
+
...environment,
|
|
184
|
+
GIT_DIR: isolatedRoot,
|
|
185
|
+
GIT_OBJECT_DIRECTORY: join(isolatedRoot, 'objects'),
|
|
186
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES: objectDirectory,
|
|
187
|
+
};
|
|
188
|
+
await assertNoExternalMergeDrivers(execute, input.targetRoot, [
|
|
189
|
+
baseCommit, parentCommit, childCommit,
|
|
190
|
+
], isolatedEnvironment);
|
|
191
|
+
const merged = await execute('git', durableGitArguments([
|
|
192
|
+
'-c', 'core.attributesFile=/dev/null',
|
|
193
|
+
'merge-tree', '--write-tree', '--name-only', '--no-messages',
|
|
194
|
+
'--merge-base', baseCommit, parentCommit, childCommit,
|
|
195
|
+
]), { cwd: input.targetRoot, env: isolatedEnvironment, timeoutMs: 60_000 });
|
|
196
|
+
const lines = merged.stdout.split(/\r?\n/u).filter((line) => line.length > 0);
|
|
197
|
+
if (merged.exitCode !== 0) {
|
|
198
|
+
if (merged.exitCode === 1 && /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(lines[0] ?? '')) {
|
|
199
|
+
throw new MissionGitIntegrationConflictError(lines.slice(1));
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Mission merge-tree command failed: ${merged.stderr.trim() || `exit ${merged.exitCode}`}`);
|
|
202
|
+
}
|
|
203
|
+
const treeSha = objectId(lines[0] ?? '', 'integration tree');
|
|
204
|
+
if (lines.length !== 1) {
|
|
205
|
+
throw new MissionGitSafetyStopError('integration-output-mismatch', 'Mission merge-tree returned unexpected non-conflict output.');
|
|
206
|
+
}
|
|
207
|
+
const commitSha = objectId((await git(execute, input.targetRoot, [
|
|
208
|
+
'commit-tree', treeSha, '-p', parentCommit, '-p', childCommit, '-F', '-',
|
|
209
|
+
], { environment: isolatedEnvironment, stdin: commit.message })).stdout, 'integration commit');
|
|
210
|
+
await git(execute, input.targetRoot, [
|
|
211
|
+
'fetch', '--no-tags', '--no-write-fetch-head', isolatedRoot, commitSha,
|
|
212
|
+
], { environment });
|
|
213
|
+
const manifest = await readManifest(execute, input.executeBinary ?? defaultMissionBinaryProcessExecutor, input.targetRoot, parentTree, treeSha, environment);
|
|
214
|
+
return {
|
|
215
|
+
baseCommit,
|
|
216
|
+
parentCommit,
|
|
217
|
+
childCommit,
|
|
218
|
+
parentTree,
|
|
219
|
+
treeSha,
|
|
220
|
+
commitSha,
|
|
221
|
+
manifest,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
await rm(isolatedRoot, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export async function applyMissionTreeIntegration(input) {
|
|
229
|
+
if (!/^refs\/(?:heads|codex-orchestrator)\/[A-Za-z0-9._\/-]+$/u.test(input.targetRef)) {
|
|
230
|
+
throw new Error('Mission integration target ref is invalid.');
|
|
231
|
+
}
|
|
232
|
+
const execute = input.execute ?? defaultProcessExecutor;
|
|
233
|
+
const candidate = await buildMissionTreeIntegrationCandidate(input);
|
|
234
|
+
if (await isSymbolicRef(execute, input.targetRoot, input.targetRef)) {
|
|
235
|
+
throw new MissionGitSafetyStopError('symbolic-target-ref-forbidden', 'Mission integration target ref must be a direct ref.');
|
|
236
|
+
}
|
|
237
|
+
const current = await resolveObject(execute, input.targetRoot, input.targetRef, 'integration target ref');
|
|
238
|
+
const committedTree = await resolveObject(execute, input.targetRoot, `${candidate.commitSha}^{tree}`, 'integration committed tree');
|
|
239
|
+
if (committedTree !== candidate.treeSha) {
|
|
240
|
+
throw new MissionGitSafetyStopError('integration-tree-mismatch', 'Mission integration candidate exposes an unexpected tree.');
|
|
241
|
+
}
|
|
242
|
+
const parentLine = (await git(execute, input.targetRoot, [
|
|
243
|
+
'rev-list', '--parents', '-n', '1', candidate.commitSha,
|
|
244
|
+
], { environment: readOnlyGitEnvironment() })).stdout.trim();
|
|
245
|
+
if (parentLine !== `${candidate.commitSha} ${candidate.parentCommit} ${candidate.childCommit}`) {
|
|
246
|
+
throw new MissionGitSafetyStopError('integration-parent-mismatch', 'Mission integration candidate has unexpected parents.');
|
|
247
|
+
}
|
|
248
|
+
if (current === candidate.commitSha) {
|
|
249
|
+
return withConfirmedDirectRefDurability(execute, input.targetRoot, input.targetRef, candidate.commitSha, async () => candidate);
|
|
250
|
+
}
|
|
251
|
+
if (current !== candidate.parentCommit) {
|
|
252
|
+
throw new MissionGitSafetyStopError('integration-target-third-identity', 'Mission integration target ref moved away from its parent commit.');
|
|
253
|
+
}
|
|
254
|
+
await input.assertOwnership(input.fencingEpoch);
|
|
255
|
+
const result = await executeGuardedRefUpdate(execute, input.targetRoot, input.targetRef, candidate.commitSha, candidate.parentCommit);
|
|
256
|
+
if (result.exitCode !== 0) {
|
|
257
|
+
if (await isSymbolicRef(execute, input.targetRoot, input.targetRef)) {
|
|
258
|
+
throw new MissionGitSafetyStopError('symbolic-target-ref-forbidden', 'Mission integration target became symbolic during apply.');
|
|
259
|
+
}
|
|
260
|
+
const reconciled = await resolveObject(execute, input.targetRoot, input.targetRef, 'integration target ref');
|
|
261
|
+
if (reconciled === candidate.parentCommit) {
|
|
262
|
+
throw new MissionGitReauthorizationRequiredError('integration-cas-retry-required', 'Mission integration ref remains at the old identity after CAS contention.');
|
|
263
|
+
}
|
|
264
|
+
if (reconciled !== candidate.commitSha) {
|
|
265
|
+
throw new MissionGitSafetyStopError('integration-ref-compare-and-swap-failed', 'Mission integration ref compare-and-swap failed.');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (await isSymbolicRef(execute, input.targetRoot, input.targetRef)) {
|
|
269
|
+
throw new MissionGitSafetyStopError('symbolic-target-ref-forbidden', 'Mission integration target became symbolic during apply.');
|
|
270
|
+
}
|
|
271
|
+
const exposedTree = await resolveObject(execute, input.targetRoot, `${input.targetRef}^{tree}`, 'integration exposed tree');
|
|
272
|
+
if (exposedTree !== candidate.treeSha) {
|
|
273
|
+
throw new MissionGitSafetyStopError('integration-tree-mismatch', 'Mission integration ref exposes an unexpected tree.');
|
|
274
|
+
}
|
|
275
|
+
return withConfirmedDirectRefDurability(execute, input.targetRoot, input.targetRef, candidate.commitSha, async () => candidate);
|
|
276
|
+
}
|
|
277
|
+
export class MissionGitTransaction {
|
|
278
|
+
options;
|
|
279
|
+
execute;
|
|
280
|
+
now;
|
|
281
|
+
constructor(options) {
|
|
282
|
+
this.options = options;
|
|
283
|
+
this.execute = options.execute ?? defaultProcessExecutor;
|
|
284
|
+
this.now = options.now ?? (() => new Date());
|
|
285
|
+
}
|
|
286
|
+
async apply(permitValue, patch) {
|
|
287
|
+
const permit = validateMissionApplyPermit(permitValue);
|
|
288
|
+
if (sha256(patch) !== permit.patchSha256) {
|
|
289
|
+
throw new MissionGitSafetyStopError('patch-digest-mismatch', 'Mission apply patch digest does not match the permit.');
|
|
290
|
+
}
|
|
291
|
+
const temporaryRoot = await this.options.stateStore.temporaryDirectory('mission-git-tmp');
|
|
292
|
+
const candidate = await buildMissionPatchCandidate({
|
|
293
|
+
targetRoot: this.options.targetRoot,
|
|
294
|
+
baseCommit: permit.expectedOldCommit,
|
|
295
|
+
patch,
|
|
296
|
+
auditedFiles: permit.manifest.map(({ path, operation, oldMode, newMode }) => ({
|
|
297
|
+
path, operation, oldMode, newMode,
|
|
298
|
+
})),
|
|
299
|
+
commit: permit.commit,
|
|
300
|
+
execute: this.execute,
|
|
301
|
+
executeBinary: this.options.executeBinary,
|
|
302
|
+
onBoundary: this.options.onBoundary,
|
|
303
|
+
temporaryRoot,
|
|
304
|
+
});
|
|
305
|
+
assertCandidateMatchesPermit(candidate, permit);
|
|
306
|
+
return this.options.stateStore.exclusive(async (session) => {
|
|
307
|
+
const prepared = await this.prepareIntent(session, permit);
|
|
308
|
+
if (await isSymbolicRef(this.execute, this.options.targetRoot, permit.targetRef)) {
|
|
309
|
+
return this.persistSafetyStop(session, permit, 'symbolic-target-ref-forbidden', 'Mission apply target ref must be a direct ref.');
|
|
310
|
+
}
|
|
311
|
+
const current = await this.readTargetIdentity(permit);
|
|
312
|
+
if (prepared.receipt) {
|
|
313
|
+
if (current?.commit === permit.expectedNewCommit && current.tree === permit.expectedNewTree) {
|
|
314
|
+
return prepared.receipt;
|
|
315
|
+
}
|
|
316
|
+
return this.persistSafetyStop(session, permit, 'receipt-ref-mismatch', 'Mission stored receipt no longer matches the target ref and tree.');
|
|
317
|
+
}
|
|
318
|
+
if (current?.commit === permit.expectedNewCommit && current.tree === permit.expectedNewTree) {
|
|
319
|
+
return withConfirmedDirectRefDurability(this.execute, this.options.targetRoot, permit.targetRef, permit.expectedNewCommit, () => this.persistReceipt(session, permit, prepared.generation, true));
|
|
320
|
+
}
|
|
321
|
+
if (!current || !this.isOldIdentity(current, permit)) {
|
|
322
|
+
return this.persistSafetyStop(session, permit, 'target-ref-third-identity', 'Mission target ref is neither the permitted old identity nor the expected new identity.');
|
|
323
|
+
}
|
|
324
|
+
if (this.now().getTime() >= Date.parse(permit.expiresAt)) {
|
|
325
|
+
return this.persistReauthorization(session, permit, 'permit-expired', 'Mission apply permit expired before ref mutation.');
|
|
326
|
+
}
|
|
327
|
+
await this.options.assertOwnership(permit.fencingEpoch);
|
|
328
|
+
const updated = await executeGuardedRefUpdate(this.execute, this.options.targetRoot, permit.targetRef, permit.expectedNewCommit, permit.expectedOldCommit);
|
|
329
|
+
if (updated.exitCode !== 0) {
|
|
330
|
+
if (await isSymbolicRef(this.execute, this.options.targetRoot, permit.targetRef)) {
|
|
331
|
+
return this.persistSafetyStop(session, permit, 'symbolic-target-ref-forbidden', 'Mission apply target became symbolic during compare-and-swap.');
|
|
332
|
+
}
|
|
333
|
+
const reconciled = await this.readTargetIdentity(permit);
|
|
334
|
+
if (reconciled && this.isOldIdentity(reconciled, permit)) {
|
|
335
|
+
return this.persistReauthorization(session, permit, 'cas-retry-required', 'Mission target ref remains at the old identity after CAS contention.');
|
|
336
|
+
}
|
|
337
|
+
if (reconciled?.commit !== permit.expectedNewCommit || reconciled.tree !== permit.expectedNewTree) {
|
|
338
|
+
return this.persistSafetyStop(session, permit, 'target-ref-compare-and-swap-failed', `Mission target ref compare-and-swap failed: ${updated.stderr.trim()}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
await emit(this.options.onBoundary, 'ref-updated');
|
|
342
|
+
if (await isSymbolicRef(this.execute, this.options.targetRoot, permit.targetRef)) {
|
|
343
|
+
return this.persistSafetyStop(session, permit, 'symbolic-target-ref-forbidden', 'Mission apply target became symbolic after compare-and-swap.');
|
|
344
|
+
}
|
|
345
|
+
const verified = await this.readTargetIdentity(permit);
|
|
346
|
+
if (verified?.commit !== permit.expectedNewCommit || verified.tree !== permit.expectedNewTree) {
|
|
347
|
+
return this.persistSafetyStop(session, permit, 'post-update-ref-mismatch', 'Mission target ref does not expose the permitted commit and tree.');
|
|
348
|
+
}
|
|
349
|
+
return withConfirmedDirectRefDurability(this.execute, this.options.targetRoot, permit.targetRef, permit.expectedNewCommit, () => this.persistReceipt(session, permit, prepared.generation, false));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async recover(permitValue) {
|
|
353
|
+
const permit = validateMissionApplyPermit(permitValue);
|
|
354
|
+
return this.options.stateStore.exclusive(async (session) => {
|
|
355
|
+
const snapshot = await session.load();
|
|
356
|
+
const mission = snapshot.missions[permit.missionId];
|
|
357
|
+
const fingerprint = missionApplyPermitFingerprint(permit);
|
|
358
|
+
if (!mission?.applyIntent || mission.applyIntent.permitFingerprint !== fingerprint) {
|
|
359
|
+
throw new MissionGitSafetyStopError('apply-intent-mismatch', 'Mission recovery requires the exact durable apply intent.');
|
|
360
|
+
}
|
|
361
|
+
if (await isSymbolicRef(this.execute, this.options.targetRoot, permit.targetRef)) {
|
|
362
|
+
return this.persistSafetyStop(session, permit, 'symbolic-target-ref-forbidden', 'Mission apply target ref must be a direct ref.');
|
|
363
|
+
}
|
|
364
|
+
const current = await this.readTargetIdentity(permit);
|
|
365
|
+
if (mission.applyReceipt) {
|
|
366
|
+
if (current?.commit === permit.expectedNewCommit && current.tree === permit.expectedNewTree) {
|
|
367
|
+
return mission.applyReceipt;
|
|
368
|
+
}
|
|
369
|
+
return this.persistSafetyStop(session, permit, 'receipt-ref-mismatch', 'Mission stored receipt no longer matches the target ref and tree.');
|
|
370
|
+
}
|
|
371
|
+
if (current?.commit === permit.expectedNewCommit && current.tree === permit.expectedNewTree) {
|
|
372
|
+
return withConfirmedDirectRefDurability(this.execute, this.options.targetRoot, permit.targetRef, permit.expectedNewCommit, () => this.persistReceipt(session, permit, snapshot.generation, true));
|
|
373
|
+
}
|
|
374
|
+
if (current && this.isOldIdentity(current, permit)) {
|
|
375
|
+
return this.persistReauthorization(session, permit, 'old-identity', 'Mission apply ref remains at the old identity; a fresh apply authorization is required.');
|
|
376
|
+
}
|
|
377
|
+
return this.persistSafetyStop(session, permit, 'target-ref-third-identity', 'Mission target ref is neither the permitted old identity nor the expected new identity.');
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async prepareIntent(session, permit) {
|
|
381
|
+
let snapshot = await session.load();
|
|
382
|
+
const mission = snapshot.missions[permit.missionId];
|
|
383
|
+
const fingerprint = missionApplyPermitFingerprint(permit);
|
|
384
|
+
if (!mission || !['apply-prepared', 'applying', 'reconciling'].includes(mission.state)) {
|
|
385
|
+
throw new MissionGitSafetyStopError('mission-state-mismatch', 'Mission is not in an apply state.');
|
|
386
|
+
}
|
|
387
|
+
if (!mission.applyPermit
|
|
388
|
+
|| missionApplyPermitFingerprint(mission.applyPermit) !== fingerprint
|
|
389
|
+
|| mission.actionKey !== permit.actionKey
|
|
390
|
+
|| mission.fencingEpoch !== permit.fencingEpoch) {
|
|
391
|
+
throw new MissionGitSafetyStopError('apply-permit-mismatch', 'Mission candidate manifest or durable apply permit does not match.');
|
|
392
|
+
}
|
|
393
|
+
if (mission.applyReceipt)
|
|
394
|
+
return { generation: snapshot.generation, receipt: mission.applyReceipt };
|
|
395
|
+
if (mission.applyIntent) {
|
|
396
|
+
if (mission.applyIntent.permitFingerprint !== fingerprint) {
|
|
397
|
+
throw new MissionGitSafetyStopError('apply-intent-mismatch', 'Mission has a different durable apply intent.');
|
|
398
|
+
}
|
|
399
|
+
return { generation: snapshot.generation };
|
|
400
|
+
}
|
|
401
|
+
snapshot = await session.mutate(snapshot.generation, (draft) => {
|
|
402
|
+
const current = draft.missions[permit.missionId];
|
|
403
|
+
if (!current)
|
|
404
|
+
throw new Error('Mission disappeared while persisting apply intent.');
|
|
405
|
+
const intent = {
|
|
406
|
+
version: 1,
|
|
407
|
+
permitFingerprint: fingerprint,
|
|
408
|
+
permit,
|
|
409
|
+
preparedAt: this.now().toISOString(),
|
|
410
|
+
};
|
|
411
|
+
draft.missions[permit.missionId] = transitionMission(current, {
|
|
412
|
+
type: 'apply-started',
|
|
413
|
+
intent,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
await emit(this.options.onBoundary, 'intent-persisted');
|
|
417
|
+
return { generation: snapshot.generation };
|
|
418
|
+
}
|
|
419
|
+
async persistReceipt(session, permit, expectedGeneration, recovered) {
|
|
420
|
+
const current = await session.load();
|
|
421
|
+
const mission = current.missions[permit.missionId];
|
|
422
|
+
if (mission?.applyReceipt)
|
|
423
|
+
return mission.applyReceipt;
|
|
424
|
+
if (current.generation !== expectedGeneration) {
|
|
425
|
+
throw new MissionGitSafetyStopError('state-generation-moved', 'Mission state generation moved while the apply lock was held.');
|
|
426
|
+
}
|
|
427
|
+
const receipt = {
|
|
428
|
+
version: 1,
|
|
429
|
+
permitFingerprint: missionApplyPermitFingerprint(permit),
|
|
430
|
+
targetRef: permit.targetRef,
|
|
431
|
+
oldCommitSha: permit.expectedOldCommit,
|
|
432
|
+
commitSha: permit.expectedNewCommit,
|
|
433
|
+
treeSha: permit.expectedNewTree,
|
|
434
|
+
recovered,
|
|
435
|
+
appliedAt: this.now().toISOString(),
|
|
436
|
+
};
|
|
437
|
+
await session.mutate(expectedGeneration, (draft) => {
|
|
438
|
+
const aggregate = draft.missions[permit.missionId];
|
|
439
|
+
if (!aggregate?.applyIntent
|
|
440
|
+
|| aggregate.applyIntent.permitFingerprint !== receipt.permitFingerprint) {
|
|
441
|
+
throw new MissionGitSafetyStopError('apply-intent-mismatch', 'Mission apply intent changed before receipt persistence.');
|
|
442
|
+
}
|
|
443
|
+
draft.missions[permit.missionId] = transitionMission(aggregate, {
|
|
444
|
+
type: 'apply-reconciled-new-identity',
|
|
445
|
+
intent: aggregate.applyIntent,
|
|
446
|
+
receipt,
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
await emit(this.options.onBoundary, 'receipt-persisted');
|
|
450
|
+
return receipt;
|
|
451
|
+
}
|
|
452
|
+
async readTargetIdentity(permit) {
|
|
453
|
+
const observed = await this.execute('git', ['rev-parse', '--verify', '--quiet', permit.targetRef], {
|
|
454
|
+
cwd: this.options.targetRoot,
|
|
455
|
+
env: readOnlyGitEnvironment(),
|
|
456
|
+
timeoutMs: 60_000,
|
|
457
|
+
});
|
|
458
|
+
if (observed.exitCode === 1)
|
|
459
|
+
return undefined;
|
|
460
|
+
if (observed.exitCode !== 0) {
|
|
461
|
+
throw new Error(`Mission Git target ref lookup failed: ${observed.stderr.trim()}`);
|
|
462
|
+
}
|
|
463
|
+
const commit = objectId(observed.stdout, 'target ref');
|
|
464
|
+
if (commit !== permit.expectedOldCommit && commit !== permit.expectedNewCommit) {
|
|
465
|
+
return { commit, tree: '' };
|
|
466
|
+
}
|
|
467
|
+
const observedTree = await this.execute('git', ['rev-parse', '--verify', '--quiet', `${commit}^{tree}`], {
|
|
468
|
+
cwd: this.options.targetRoot,
|
|
469
|
+
env: readOnlyGitEnvironment(),
|
|
470
|
+
timeoutMs: 60_000,
|
|
471
|
+
});
|
|
472
|
+
if (observedTree.exitCode !== 0)
|
|
473
|
+
return { commit, tree: '' };
|
|
474
|
+
const tree = objectId(observedTree.stdout, 'target tree');
|
|
475
|
+
return { commit, tree };
|
|
476
|
+
}
|
|
477
|
+
isOldIdentity(current, permit) {
|
|
478
|
+
return current.commit === permit.expectedOldCommit && current.tree === permit.expectedOldTree;
|
|
479
|
+
}
|
|
480
|
+
async persistReauthorization(session, permit, code, message) {
|
|
481
|
+
const snapshot = await session.load();
|
|
482
|
+
await session.mutate(snapshot.generation, (draft) => {
|
|
483
|
+
const mission = draft.missions[permit.missionId];
|
|
484
|
+
if (!mission)
|
|
485
|
+
throw new Error('Mission disappeared during apply reconciliation.');
|
|
486
|
+
draft.missions[permit.missionId] = transitionMission(mission, {
|
|
487
|
+
type: 'apply-reconciled-old-identity',
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
throw new MissionGitReauthorizationRequiredError(code, message);
|
|
491
|
+
}
|
|
492
|
+
async persistSafetyStop(session, permit, code, message) {
|
|
493
|
+
const snapshot = await session.load();
|
|
494
|
+
await session.mutate(snapshot.generation, (draft) => {
|
|
495
|
+
const mission = draft.missions[permit.missionId];
|
|
496
|
+
if (!mission)
|
|
497
|
+
throw new Error('Mission disappeared during apply safety reconciliation.');
|
|
498
|
+
draft.missions[permit.missionId] = transitionMission(mission, {
|
|
499
|
+
type: 'apply-reconciled-third-identity',
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
throw new MissionGitSafetyStopError(code, message);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function isSymbolicRef(execute, targetRoot, targetRef) {
|
|
506
|
+
const result = await execute('git', ['symbolic-ref', '--quiet', targetRef], {
|
|
507
|
+
cwd: targetRoot,
|
|
508
|
+
env: readOnlyGitEnvironment(),
|
|
509
|
+
timeoutMs: 60_000,
|
|
510
|
+
});
|
|
511
|
+
if (result.exitCode === 0)
|
|
512
|
+
return true;
|
|
513
|
+
if (result.exitCode === 1)
|
|
514
|
+
return false;
|
|
515
|
+
throw new Error(`Mission Git symbolic-ref inspection failed: ${result.stderr.trim()}`);
|
|
516
|
+
}
|
|
517
|
+
async function readManifest(execute, executeBinary, targetRoot, baseTree, treeSha, environment) {
|
|
518
|
+
const raw = (await git(execute, targetRoot, [
|
|
519
|
+
'diff-tree', '--no-commit-id', '--raw', '-r', '-z', '--no-renames', baseTree, treeSha,
|
|
520
|
+
], { environment })).stdout;
|
|
521
|
+
const fields = raw.split('\0');
|
|
522
|
+
if (fields.at(-1) === '')
|
|
523
|
+
fields.pop();
|
|
524
|
+
if (fields.length % 2 !== 0)
|
|
525
|
+
throw new Error('Mission Git raw diff output is malformed.');
|
|
526
|
+
const manifest = [];
|
|
527
|
+
for (let index = 0; index < fields.length; index += 2) {
|
|
528
|
+
const metadata = fields[index] ?? '';
|
|
529
|
+
const path = fields[index + 1] ?? '';
|
|
530
|
+
const match = /^:(\d{6}) (\d{6}) ([a-f0-9]+) ([a-f0-9]+) ([AMD])$/u.exec(metadata);
|
|
531
|
+
if (!match || path.length === 0)
|
|
532
|
+
throw new Error('Mission Git raw diff record is malformed.');
|
|
533
|
+
const operation = match[5] === 'A' ? 'add' : match[5] === 'D' ? 'delete' : 'modify';
|
|
534
|
+
const beforeBlob = operation === 'add' ? null : match[3];
|
|
535
|
+
const afterBlob = operation === 'delete' ? null : match[4];
|
|
536
|
+
manifest.push({
|
|
537
|
+
path,
|
|
538
|
+
operation,
|
|
539
|
+
oldMode: operation === 'add' ? null : match[1],
|
|
540
|
+
newMode: operation === 'delete' ? null : match[2],
|
|
541
|
+
beforeBlob,
|
|
542
|
+
afterBlob,
|
|
543
|
+
beforeSha256: beforeBlob ? await hashGitBlob(executeBinary, targetRoot, beforeBlob, environment) : null,
|
|
544
|
+
afterSha256: afterBlob ? await hashGitBlob(executeBinary, targetRoot, afterBlob, environment) : null,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
return manifest.sort((left, right) => left.path.localeCompare(right.path));
|
|
548
|
+
}
|
|
549
|
+
function hashGitBlob(executeBinary, targetRoot, object, environment) {
|
|
550
|
+
return executeBinary('git', ['cat-file', 'blob', object], {
|
|
551
|
+
cwd: targetRoot,
|
|
552
|
+
env: environment,
|
|
553
|
+
timeoutMs: 60_000,
|
|
554
|
+
maxOutputBytes: 256 * 1024 * 1024,
|
|
555
|
+
}).then((result) => {
|
|
556
|
+
if (result.exitCode !== 0) {
|
|
557
|
+
throw new Error(`Mission Git blob hashing failed: ${result.stderr.toString('utf8').trim()}`);
|
|
558
|
+
}
|
|
559
|
+
return `sha256:${createHash('sha256').update(result.stdout).digest('hex')}`;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
async function createMissionGitTemporaryDirectory(parent) {
|
|
563
|
+
await mkdir(parent, { recursive: true });
|
|
564
|
+
const currentBootNonce = missionGitProcessBootNonce.replaceAll('-', '');
|
|
565
|
+
for (const entry of await readdir(parent, { withFileTypes: true })) {
|
|
566
|
+
if (!entry.isDirectory())
|
|
567
|
+
continue;
|
|
568
|
+
const match = /^candidate-(\d+)-([a-f0-9]{32})-[a-f0-9-]{36}$/u.exec(entry.name);
|
|
569
|
+
if (!match)
|
|
570
|
+
continue;
|
|
571
|
+
const pid = Number(match[1]);
|
|
572
|
+
const bootNonce = match[2];
|
|
573
|
+
const ownedByThisProcess = pid === process.pid && bootNonce === currentBootNonce;
|
|
574
|
+
if (ownedByThisProcess || (pid !== process.pid && processAlive(pid)))
|
|
575
|
+
continue;
|
|
576
|
+
await rm(join(parent, entry.name), { recursive: true, force: true });
|
|
577
|
+
}
|
|
578
|
+
const path = join(parent, `candidate-${process.pid}-${currentBootNonce}-${randomUUID()}`);
|
|
579
|
+
await mkdir(path);
|
|
580
|
+
await writeFile(join(path, 'owner.json'), `${JSON.stringify({
|
|
581
|
+
version: 1,
|
|
582
|
+
pid: process.pid,
|
|
583
|
+
bootNonce: missionGitProcessBootNonce,
|
|
584
|
+
})}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
585
|
+
return path;
|
|
586
|
+
}
|
|
587
|
+
function processAlive(pid) {
|
|
588
|
+
try {
|
|
589
|
+
process.kill(pid, 0);
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
return error instanceof Error && 'code' in error && error.code === 'EPERM';
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function assertNoExternalMergeDrivers(execute, targetRoot, commits, environment) {
|
|
597
|
+
for (const commit of commits) {
|
|
598
|
+
const listing = (await git(execute, targetRoot, [
|
|
599
|
+
'ls-tree', '-r', '-z', '--full-tree', commit,
|
|
600
|
+
], { environment })).stdout;
|
|
601
|
+
for (const record of listing.split('\0').filter(Boolean)) {
|
|
602
|
+
const match = /^(\d{6}) blob ([a-f0-9]+)\t(.+)$/u.exec(record);
|
|
603
|
+
if (!match || (match[3] !== '.gitattributes' && !match[3].endsWith('/.gitattributes')))
|
|
604
|
+
continue;
|
|
605
|
+
const attributes = (await git(execute, targetRoot, ['cat-file', 'blob', match[2]], { environment })).stdout;
|
|
606
|
+
assertNoCustomMergeAttribute(attributes, `${commit}:${match[3]}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function assertNoCustomMergeAttribute(content, source) {
|
|
611
|
+
for (const rawLine of content.split(/\r?\n/u)) {
|
|
612
|
+
const line = rawLine.replace(/(^|[^\\])#.*/u, '$1').trim();
|
|
613
|
+
if (line.length === 0)
|
|
614
|
+
continue;
|
|
615
|
+
for (const token of line.split(/\s+/u).slice(1)) {
|
|
616
|
+
const match = /^merge=(.+)$/u.exec(token);
|
|
617
|
+
if (match && !new Set(['text', 'binary', 'union']).has(match[1])) {
|
|
618
|
+
throw new MissionGitSafetyStopError('external-merge-driver-forbidden', `Mission tree integration forbids custom merge attribute ${token} from ${source}.`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function assertAuditedPatch(patch, auditedFiles) {
|
|
624
|
+
const audit = auditMissionPatch({
|
|
625
|
+
patch,
|
|
626
|
+
grantedPaths: auditedFiles.map((file) => file.path),
|
|
627
|
+
deniedPaths: [],
|
|
628
|
+
maxBytes: Math.max(1, Buffer.byteLength(patch, 'utf8')),
|
|
629
|
+
});
|
|
630
|
+
if (!audit.accepted)
|
|
631
|
+
throw new Error(`Mission Git patch is not auditable: ${audit.reason}.`);
|
|
632
|
+
const expected = canonicalManifestShape(auditedFiles);
|
|
633
|
+
const actual = canonicalManifestShape(audit.files);
|
|
634
|
+
if (actual !== expected)
|
|
635
|
+
throw new Error('Mission Git patch audit does not match the supplied audited files.');
|
|
636
|
+
}
|
|
637
|
+
function assertExactManifestShape(manifest, auditedFiles) {
|
|
638
|
+
const actual = canonicalManifestShape(manifest);
|
|
639
|
+
const expected = canonicalManifestShape(auditedFiles);
|
|
640
|
+
if (actual !== expected) {
|
|
641
|
+
throw new MissionGitSafetyStopError('candidate-manifest-mismatch', 'Mission candidate manifest differs from the audited patch manifest.');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function assertCandidateMatchesPermit(candidate, permit) {
|
|
645
|
+
const candidateIdentity = canonicalJson({
|
|
646
|
+
baseCommit: candidate.baseCommit,
|
|
647
|
+
baseTree: candidate.baseTree,
|
|
648
|
+
patchSha256: candidate.patchSha256,
|
|
649
|
+
commitSha: candidate.commitSha,
|
|
650
|
+
treeSha: candidate.treeSha,
|
|
651
|
+
manifest: candidate.manifest,
|
|
652
|
+
});
|
|
653
|
+
const permitIdentity = canonicalJson({
|
|
654
|
+
baseCommit: permit.expectedOldCommit,
|
|
655
|
+
baseTree: permit.expectedOldTree,
|
|
656
|
+
patchSha256: permit.patchSha256,
|
|
657
|
+
commitSha: permit.expectedNewCommit,
|
|
658
|
+
treeSha: permit.expectedNewTree,
|
|
659
|
+
manifest: permit.manifest,
|
|
660
|
+
});
|
|
661
|
+
if (candidateIdentity !== permitIdentity) {
|
|
662
|
+
throw new MissionGitSafetyStopError('candidate-permit-mismatch', 'Mission candidate manifest or object identity differs from the apply permit.');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function canonicalManifestShape(files) {
|
|
666
|
+
return JSON.stringify(files.map(({ path, operation, oldMode, newMode }) => ({
|
|
667
|
+
path, operation, oldMode, newMode,
|
|
668
|
+
})).sort((left, right) => left.path.localeCompare(right.path)));
|
|
669
|
+
}
|
|
670
|
+
async function resolveObject(execute, targetRoot, expression, label) {
|
|
671
|
+
return objectId((await git(execute, targetRoot, ['rev-parse', '--verify', expression], {
|
|
672
|
+
environment: readOnlyGitEnvironment(),
|
|
673
|
+
})).stdout, label);
|
|
674
|
+
}
|
|
675
|
+
function objectId(stdout, label) {
|
|
676
|
+
const value = stdout.trim();
|
|
677
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value))
|
|
678
|
+
throw new Error(`Mission Git ${label} is not an object ID.`);
|
|
679
|
+
return value;
|
|
680
|
+
}
|
|
681
|
+
async function git(execute, targetRoot, args, options) {
|
|
682
|
+
const result = await execute('git', durableGitArguments(args), {
|
|
683
|
+
cwd: targetRoot,
|
|
684
|
+
env: options.environment,
|
|
685
|
+
timeoutMs: 60_000,
|
|
686
|
+
...(options.stdin === undefined ? {} : { stdin: options.stdin }),
|
|
687
|
+
});
|
|
688
|
+
if (result.exitCode !== 0) {
|
|
689
|
+
throw new Error(`Mission Git command failed: git ${args.join(' ')}: ${result.stderr.trim()}`);
|
|
690
|
+
}
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
function durableGitArguments(args, hooksPath = '/dev/null') {
|
|
694
|
+
return [...missionDurableGitConfig, '-c', `core.hooksPath=${hooksPath}`, ...args];
|
|
695
|
+
}
|
|
696
|
+
async function executeGuardedRefUpdate(execute, targetRoot, targetRef, newCommit, oldCommit) {
|
|
697
|
+
const rawPathValue = (await git(execute, targetRoot, [
|
|
698
|
+
'rev-parse', '--git-path', targetRef,
|
|
699
|
+
], { environment: readOnlyGitEnvironment() })).stdout.trim();
|
|
700
|
+
const rawPath = isAbsolute(rawPathValue) ? rawPathValue : join(targetRoot, rawPathValue);
|
|
701
|
+
const recoveryGuard = await acquireMissionRefRecoveryGuard(rawPath);
|
|
702
|
+
try {
|
|
703
|
+
return await executeGuardedRefUpdateUnderGuard(rawPath, newCommit, oldCommit);
|
|
704
|
+
}
|
|
705
|
+
finally {
|
|
706
|
+
await recoveryGuard.release();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function executeGuardedRefUpdateUnderGuard(rawPath, newCommit, oldCommit) {
|
|
710
|
+
const lockPath = `${rawPath}.lock`;
|
|
711
|
+
const ownerPath = `${rawPath}.mission-owner`;
|
|
712
|
+
await reclaimMissionRefLock(rawPath, lockPath, ownerPath);
|
|
713
|
+
const token = randomUUID();
|
|
714
|
+
const provisionalOwner = {
|
|
715
|
+
version: 1,
|
|
716
|
+
pid: process.pid,
|
|
717
|
+
bootNonce: missionGitProcessBootNonce,
|
|
718
|
+
token,
|
|
719
|
+
};
|
|
720
|
+
const temporaryLockPath = `${lockPath}.mission.${token}.tmp`;
|
|
721
|
+
let lock = await open(temporaryLockPath, 'wx', 0o600);
|
|
722
|
+
await lock.writeFile(`${JSON.stringify(provisionalOwner)}\n`, 'utf8');
|
|
723
|
+
await lock.sync();
|
|
724
|
+
const identity = await lock.stat();
|
|
725
|
+
let published = false;
|
|
726
|
+
try {
|
|
727
|
+
await link(temporaryLockPath, lockPath);
|
|
728
|
+
await unlink(temporaryLockPath);
|
|
729
|
+
const owner = { ...provisionalOwner, dev: identity.dev, ino: identity.ino };
|
|
730
|
+
await writeDurableRefOwner(ownerPath, owner);
|
|
731
|
+
const current = await open(rawPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
732
|
+
try {
|
|
733
|
+
const metadata = await current.stat();
|
|
734
|
+
const content = await current.readFile('utf8');
|
|
735
|
+
if (!metadata.isFile() || metadata.nlink !== 1 || content !== `${oldCommit}\n`) {
|
|
736
|
+
return { stdout: '', stderr: 'Mission target ref is not the expected loose direct ref.', exitCode: 1 };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
finally {
|
|
740
|
+
await current.close();
|
|
741
|
+
}
|
|
742
|
+
await lock.close();
|
|
743
|
+
lock = await open(lockPath, 'r+');
|
|
744
|
+
await lock.truncate(0);
|
|
745
|
+
await lock.writeFile(`${newCommit}\n`, 'utf8');
|
|
746
|
+
const written = await readFile(lockPath, 'utf8');
|
|
747
|
+
if (written !== `${newCommit}\n`)
|
|
748
|
+
throw new Error('Mission ref lock write was incomplete.');
|
|
749
|
+
await lock.sync();
|
|
750
|
+
await lock.close();
|
|
751
|
+
lock = undefined;
|
|
752
|
+
await rename(lockPath, rawPath);
|
|
753
|
+
published = true;
|
|
754
|
+
const parent = await open(dirname(rawPath), 'r');
|
|
755
|
+
try {
|
|
756
|
+
await parent.sync();
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
await parent.close();
|
|
760
|
+
}
|
|
761
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
762
|
+
}
|
|
763
|
+
catch (error) {
|
|
764
|
+
return {
|
|
765
|
+
stdout: '',
|
|
766
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
767
|
+
exitCode: 1,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
if (lock)
|
|
772
|
+
await lock.close().catch(() => undefined);
|
|
773
|
+
await unlink(temporaryLockPath).catch(() => undefined);
|
|
774
|
+
if (!published)
|
|
775
|
+
await unlinkRefIfIdentity(lockPath, identity.dev, identity.ino);
|
|
776
|
+
await unlinkRefOwnerIfToken(ownerPath, token);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function acquireMissionRefRecoveryGuard(rawPath) {
|
|
780
|
+
const key = createHash('sha256').update(rawPath, 'utf8').digest('hex');
|
|
781
|
+
return acquireMissionCoordinatorLock({
|
|
782
|
+
targetRoot: tmpdir(),
|
|
783
|
+
stateDir: join('codex-mission-ref-recovery', key),
|
|
784
|
+
hostId: hostname(),
|
|
785
|
+
bootNonce: missionGitProcessBootNonce,
|
|
786
|
+
waitTimeoutMs: 5_000,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async function withConfirmedDirectRefDurability(execute, targetRoot, targetRef, expectedCommit, operation) {
|
|
790
|
+
const rawPathValue = (await git(execute, targetRoot, [
|
|
791
|
+
'rev-parse', '--git-path', targetRef,
|
|
792
|
+
], { environment: readOnlyGitEnvironment() })).stdout.trim();
|
|
793
|
+
const rawPath = isAbsolute(rawPathValue) ? rawPathValue : join(targetRoot, rawPathValue);
|
|
794
|
+
const recoveryGuard = await acquireMissionRefRecoveryGuard(rawPath);
|
|
795
|
+
const lockPath = `${rawPath}.lock`;
|
|
796
|
+
const ownerPath = `${rawPath}.mission-owner`;
|
|
797
|
+
const token = randomUUID();
|
|
798
|
+
const temporary = `${lockPath}.confirm.${token}.tmp`;
|
|
799
|
+
let identity;
|
|
800
|
+
try {
|
|
801
|
+
await reclaimMissionRefLock(rawPath, lockPath, ownerPath);
|
|
802
|
+
const owner = {
|
|
803
|
+
version: 1, pid: process.pid, bootNonce: missionGitProcessBootNonce, token,
|
|
804
|
+
};
|
|
805
|
+
const guard = await open(temporary, 'wx', 0o600);
|
|
806
|
+
try {
|
|
807
|
+
await guard.writeFile(`${JSON.stringify(owner)}\n`, 'utf8');
|
|
808
|
+
await guard.sync();
|
|
809
|
+
identity = await guard.stat();
|
|
810
|
+
}
|
|
811
|
+
finally {
|
|
812
|
+
await guard.close();
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
await link(temporary, lockPath);
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
if (error instanceof Error && 'code' in error && error.code === 'EEXIST') {
|
|
819
|
+
throw new MissionGitReauthorizationRequiredError('ref-confirmation-lock-contended', 'Mission ref durability confirmation is contended by another writer.');
|
|
820
|
+
}
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
const ref = await open(rawPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
824
|
+
try {
|
|
825
|
+
const before = await ref.stat();
|
|
826
|
+
const content = await ref.readFile('utf8');
|
|
827
|
+
const after = await ref.stat();
|
|
828
|
+
if (!before.isFile() || before.nlink !== 1 || content !== `${expectedCommit}\n`
|
|
829
|
+
|| before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size) {
|
|
830
|
+
throw new MissionGitSafetyStopError('target-ref-not-loose-direct', 'Mission target ref is not the expected loose direct ref.');
|
|
831
|
+
}
|
|
832
|
+
await ref.sync();
|
|
833
|
+
}
|
|
834
|
+
finally {
|
|
835
|
+
await ref.close();
|
|
836
|
+
}
|
|
837
|
+
await syncDirectory(dirname(rawPath));
|
|
838
|
+
return await operation();
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
await unlink(temporary).catch(() => undefined);
|
|
842
|
+
if (identity)
|
|
843
|
+
await unlinkRefIfIdentity(lockPath, identity.dev, identity.ino);
|
|
844
|
+
await recoveryGuard.release();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function reclaimMissionRefLock(rawPath, lockPath, ownerPath) {
|
|
848
|
+
const owner = await readMissionRefOwner(ownerPath);
|
|
849
|
+
if (owner?.dev !== undefined && owner.ino !== undefined) {
|
|
850
|
+
const alive = owner.pid === process.pid
|
|
851
|
+
? owner.bootNonce === missionGitProcessBootNonce
|
|
852
|
+
: processAlive(owner.pid);
|
|
853
|
+
if (await pathHasIdentity(lockPath, owner.dev, owner.ino)) {
|
|
854
|
+
if (alive)
|
|
855
|
+
throw new Error('Mission ref lock is owned by a live process.');
|
|
856
|
+
await unlinkRefIfIdentity(lockPath, owner.dev, owner.ino);
|
|
857
|
+
await unlinkRefOwnerIfToken(ownerPath, owner.token);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (await pathHasIdentity(rawPath, owner.dev, owner.ino)) {
|
|
861
|
+
await syncDirectory(dirname(rawPath));
|
|
862
|
+
}
|
|
863
|
+
if (alive)
|
|
864
|
+
throw new Error('Mission ref owner is live but its lock identity moved.');
|
|
865
|
+
await unlinkRefOwnerIfToken(ownerPath, owner.token);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (owner) {
|
|
869
|
+
const alive = owner.pid === process.pid
|
|
870
|
+
? owner.bootNonce === missionGitProcessBootNonce
|
|
871
|
+
: processAlive(owner.pid);
|
|
872
|
+
if (alive)
|
|
873
|
+
throw new Error('Mission ref owner is live before lock publication.');
|
|
874
|
+
await unlinkRefOwnerIfToken(ownerPath, owner.token);
|
|
875
|
+
}
|
|
876
|
+
let embedded;
|
|
877
|
+
try {
|
|
878
|
+
const content = await readFile(lockPath, 'utf8');
|
|
879
|
+
try {
|
|
880
|
+
embedded = parseMissionRefOwner(content);
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
embedded = undefined;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
890
|
+
if (embedded) {
|
|
891
|
+
const alive = embedded.pid === process.pid
|
|
892
|
+
? embedded.bootNonce === missionGitProcessBootNonce
|
|
893
|
+
: processAlive(embedded.pid);
|
|
894
|
+
if (alive)
|
|
895
|
+
throw new Error('Mission ref lock is owned by a live process.');
|
|
896
|
+
await unlink(lockPath);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function writeDurableRefOwner(path, owner) {
|
|
900
|
+
const temporary = `${path}.${owner.token}.tmp`;
|
|
901
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
902
|
+
try {
|
|
903
|
+
await file.writeFile(`${JSON.stringify(owner)}\n`, 'utf8');
|
|
904
|
+
await file.sync();
|
|
905
|
+
}
|
|
906
|
+
finally {
|
|
907
|
+
await file.close();
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
await link(temporary, path);
|
|
911
|
+
}
|
|
912
|
+
finally {
|
|
913
|
+
await unlink(temporary).catch(() => undefined);
|
|
914
|
+
}
|
|
915
|
+
await syncDirectory(dirname(path));
|
|
916
|
+
}
|
|
917
|
+
async function readMissionRefOwner(path) {
|
|
918
|
+
try {
|
|
919
|
+
return parseMissionRefOwner(await readFile(path, 'utf8'));
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
923
|
+
return undefined;
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function parseMissionRefOwner(content) {
|
|
928
|
+
const value = JSON.parse(content);
|
|
929
|
+
if (value.version !== 1 || !Number.isSafeInteger(value.pid)
|
|
930
|
+
|| typeof value.bootNonce !== 'string' || typeof value.token !== 'string'
|
|
931
|
+
|| (value.dev !== undefined && !Number.isSafeInteger(value.dev))
|
|
932
|
+
|| (value.ino !== undefined && !Number.isSafeInteger(value.ino))) {
|
|
933
|
+
throw new Error('Mission ref lock owner metadata is invalid.');
|
|
934
|
+
}
|
|
935
|
+
return value;
|
|
936
|
+
}
|
|
937
|
+
async function pathHasIdentity(path, dev, ino) {
|
|
938
|
+
try {
|
|
939
|
+
const value = await lstat(path);
|
|
940
|
+
return value.dev === dev && value.ino === ino;
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
944
|
+
return false;
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async function unlinkRefIfIdentity(path, dev, ino) {
|
|
949
|
+
if (await pathHasIdentity(path, dev, ino))
|
|
950
|
+
await unlink(path);
|
|
951
|
+
}
|
|
952
|
+
async function unlinkRefOwnerIfToken(path, token) {
|
|
953
|
+
const owner = await readMissionRefOwner(path);
|
|
954
|
+
if (owner?.token === token)
|
|
955
|
+
await unlink(path);
|
|
956
|
+
}
|
|
957
|
+
async function syncDirectory(path) {
|
|
958
|
+
const directory = await open(path, 'r');
|
|
959
|
+
try {
|
|
960
|
+
await directory.sync();
|
|
961
|
+
}
|
|
962
|
+
finally {
|
|
963
|
+
await directory.close();
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function gitEnvironment(indexPath, worktreePath, home, identity) {
|
|
967
|
+
return {
|
|
968
|
+
...readOnlyGitEnvironment(),
|
|
969
|
+
HOME: home,
|
|
970
|
+
GIT_INDEX_FILE: indexPath,
|
|
971
|
+
GIT_WORK_TREE: worktreePath,
|
|
972
|
+
GIT_AUTHOR_NAME: identity.authorName,
|
|
973
|
+
GIT_AUTHOR_EMAIL: identity.authorEmail,
|
|
974
|
+
GIT_AUTHOR_DATE: identity.authoredAt,
|
|
975
|
+
GIT_COMMITTER_NAME: identity.committerName,
|
|
976
|
+
GIT_COMMITTER_EMAIL: identity.committerEmail,
|
|
977
|
+
GIT_COMMITTER_DATE: identity.committedAt,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function readOnlyGitEnvironment() {
|
|
981
|
+
return {
|
|
982
|
+
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
|
|
983
|
+
LC_ALL: 'C',
|
|
984
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
985
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
986
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
987
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function sha256(value) {
|
|
991
|
+
return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
992
|
+
}
|
|
993
|
+
function canonicalJson(value) {
|
|
994
|
+
return JSON.stringify(canonicalValue(value));
|
|
995
|
+
}
|
|
996
|
+
function canonicalValue(value) {
|
|
997
|
+
if (Array.isArray(value))
|
|
998
|
+
return value.map(canonicalValue);
|
|
999
|
+
if (typeof value === 'object' && value !== null) {
|
|
1000
|
+
return Object.fromEntries(Object.entries(value)
|
|
1001
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1002
|
+
.map(([key, child]) => [key, canonicalValue(child)]));
|
|
1003
|
+
}
|
|
1004
|
+
return value;
|
|
1005
|
+
}
|
|
1006
|
+
async function emit(callback, boundary) {
|
|
1007
|
+
await callback?.(boundary);
|
|
1008
|
+
}
|
|
1009
|
+
//# sourceMappingURL=mission-git-transaction.js.map
|