software-defence-factory 0.11.1 → 0.11.2
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/docs/npm.md +8 -2
- package/docs/recovery.md +21 -0
- package/factory/candidate-patch.mjs +73 -0
- package/factory/delivery.mjs +6 -60
- package/factory/execution-profile.mjs +4 -2
- package/factory/executor.mjs +8 -6
- package/factory/git-environment.mjs +12 -2
- package/factory/lib.mjs +1 -1
- package/package.json +1 -1
package/docs/npm.md
CHANGED
|
@@ -104,8 +104,8 @@ installation or retained attempt needs them.
|
|
|
104
104
|
|
|
105
105
|
## Protected evidence compatibility
|
|
106
106
|
|
|
107
|
-
Factory 0.11.
|
|
108
|
-
**0.8.0, 0.9.0, 0.9.1, 0.10.0, 0.11.0 and 0.11.
|
|
107
|
+
Factory 0.11.2 recognizes version-1 execution profiles emitted by native
|
|
108
|
+
**0.8.0, 0.9.0, 0.9.1, 0.10.0, 0.11.0, 0.11.1 and 0.11.2**. This is an exact allowlist in
|
|
109
109
|
`factory/execution-profile.mjs`, independent of the installed package version;
|
|
110
110
|
it is not a semver range or an automatic promise for later releases. Unknown
|
|
111
111
|
runtime strings, unknown profile formats and incomplete legacy acceptance
|
|
@@ -143,6 +143,12 @@ a checkpoint usable. Browser-disabled 0.8.0 records cannot satisfy a newly
|
|
|
143
143
|
enabled browser policy. Missing or unsupported records stay unchanged and
|
|
144
144
|
unavailable; no schema migration, profile relabeling or policy repair occurs.
|
|
145
145
|
|
|
146
|
+
Version 0.11.2 retains the v1 profile/evidence schema and current provenance
|
|
147
|
+
rules while preserving exact patch bytes and requiring reconstruction before
|
|
148
|
+
new acceptance. Older writers remain compatible only when their retained patch
|
|
149
|
+
passes the current digest and tree reconstruction checks. A digest-matching
|
|
150
|
+
malformed patch remains blocked and unchanged; follow the [recovery guidance](recovery.md#already-accepted-malformed-patches-91).
|
|
151
|
+
|
|
146
152
|
New publication still requires the current remote target to equal the original
|
|
147
153
|
accepted base; reviewed-candidate continuation preserves that baseline, while
|
|
148
154
|
target refresh remains #72. See [the verification map](https://github.com/arcitai/software-and-defence-factory/blob/main/docs/proof.md) for delivered
|
package/docs/recovery.md
CHANGED
|
@@ -332,6 +332,27 @@ approval; do not edit SQLite or acceptance evidence to bypass the guard. A stale
|
|
|
332
332
|
target after branch creation leaves that unique branch for inspection and does
|
|
333
333
|
not open a PR. A PR does not imply integration or deployment.
|
|
334
334
|
|
|
335
|
+
## Already accepted malformed patches (#91)
|
|
336
|
+
|
|
337
|
+
A matching digest does not prove a patch can reproduce its candidate. Before
|
|
338
|
+
0.11.2, trimming Git diff output could remove trailing blank context lines.
|
|
339
|
+
Native publication correctly refuses such a patch before provider writes.
|
|
340
|
+
Preserve the accepted record, patch, digests, retained Git objects and prior
|
|
341
|
+
attempts. Restarting or reading state does not repair or migrate this evidence.
|
|
342
|
+
|
|
343
|
+
For recovery, use a separately reviewed, normal protected maintainer PR from
|
|
344
|
+
the exact retained candidate tree, checking its tree identity and original base,
|
|
345
|
+
or run a new native attempt with fresh Verify, independent Review and approval.
|
|
346
|
+
The maintainer route is not successful native publication. Never replace an old
|
|
347
|
+
patch or acceptance hash, fabricate approval, or disable reconstruction guards.
|
|
348
|
+
|
|
349
|
+
New handoffs in 0.11.2 retain raw Git bytes and reconstruct the protected patch
|
|
350
|
+
against the original retained base in private bare storage before acceptance.
|
|
351
|
+
Failure retains evidence and creates no accepted record or provider writes.
|
|
352
|
+
Publication still repeats reconstruction and all current provenance, source,
|
|
353
|
+
workflow and target checks. Operator installed/native qualification on Z13 is
|
|
354
|
+
required before further general unattended delivery.
|
|
355
|
+
|
|
335
356
|
## Continue a reviewed candidate (first #42 slice)
|
|
336
357
|
|
|
337
358
|
Use one explicit starting point for a current software review with a returned
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { assertPrivateDirectory } from './execution-evidence.mjs';
|
|
2
|
+
import { lstatSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { digest } from './lib.mjs';
|
|
6
|
+
|
|
7
|
+
const MAX_PATCH_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
const MAX_TREE_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
function readPrivateFile(path, limit) {
|
|
11
|
+
const stat = lstatSync(path);
|
|
12
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.size > limit)
|
|
13
|
+
throw new Error('Protected candidate patch is missing, unsafe or too large.');
|
|
14
|
+
return readFileSync(path);
|
|
15
|
+
}
|
|
16
|
+
function runGitCommand(args) {
|
|
17
|
+
const env = {
|
|
18
|
+
PATH: process.env.PATH || '/usr/bin:/bin', HOME: '/nonexistent',
|
|
19
|
+
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null',
|
|
20
|
+
GIT_TERMINAL_PROMPT: '0', GIT_OPTIONAL_LOCKS: '0',
|
|
21
|
+
};
|
|
22
|
+
const result = spawnSync('git', args, { env, encoding: 'utf8', maxBuffer: MAX_TREE_BYTES + MAX_PATCH_BYTES + 1024 * 1024 });
|
|
23
|
+
if (result.error || result.status !== 0) throw new Error('Could not prepare private candidate reconstruction storage.');
|
|
24
|
+
return result.stdout.trim();
|
|
25
|
+
}
|
|
26
|
+
function runGit(repo, args, { input, binary = false } = {}) {
|
|
27
|
+
// Delivery reconstruction runs only trusted Git operations in a newly made
|
|
28
|
+
// bare repository. Candidate hooks, filters, global config and credentials
|
|
29
|
+
// are unavailable to these subprocesses.
|
|
30
|
+
const env = {
|
|
31
|
+
PATH: process.env.PATH || '/usr/bin:/bin',
|
|
32
|
+
HOME: '/nonexistent',
|
|
33
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
34
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
35
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
36
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
37
|
+
};
|
|
38
|
+
const result = spawnSync('git', ['--git-dir', repo, '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', ...args], {
|
|
39
|
+
env, input, encoding: binary ? null : 'utf8', maxBuffer: MAX_TREE_BYTES + MAX_PATCH_BYTES + 1024 * 1024,
|
|
40
|
+
});
|
|
41
|
+
if (result.error || result.status !== 0) throw new Error('Could not reconstruct the accepted candidate from retained Git objects.');
|
|
42
|
+
return binary ? result.stdout : result.stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
// Shared acceptance/publication boundary. Only the original retained base and
|
|
45
|
+
// exact protected patch bytes enter new, private bare storage. The callback may
|
|
46
|
+
// inspect reconstructed objects; cleanup removes only this invocation's scratch.
|
|
47
|
+
export function withReconstructedCandidate({ state, jobId, sourcePath, objectFormat, candidate }, inspect = () => {}) {
|
|
48
|
+
const oid = new RegExp(`^[a-f0-9]{${objectFormat === 'sha256' ? 64 : 40}}$`);
|
|
49
|
+
if (!['sha1', 'sha256'].includes(objectFormat) || !oid.test(candidate.base || '') || !oid.test(candidate.tree || ''))
|
|
50
|
+
throw new Error('Candidate has an unsupported Git object identity.');
|
|
51
|
+
const folder = join(state, 'jobs', jobId);
|
|
52
|
+
const patchPath = join(folder, 'delivery-input', 'candidate.patch');
|
|
53
|
+
assertPrivateDirectory(join(state, 'jobs'));
|
|
54
|
+
assertPrivateDirectory(folder);
|
|
55
|
+
assertPrivateDirectory(join(folder, 'delivery-input'));
|
|
56
|
+
const patch = readPrivateFile(patchPath, MAX_PATCH_BYTES);
|
|
57
|
+
if (!patch.length || digest(patch) !== candidate.patch_sha256) throw new Error('Accepted patch digest does not match the approval record.');
|
|
58
|
+
const scratchRoot = mkdtempSync(join(folder, 'delivery-work-'));
|
|
59
|
+
const repo = join(scratchRoot, 'candidate.git');
|
|
60
|
+
try {
|
|
61
|
+
runGitCommand(['init', '--bare', '--quiet', '--template=', `--object-format=${objectFormat}`, repo]);
|
|
62
|
+
runGit(repo, ['fetch', '--quiet', '--no-tags', '--', sourcePath, `${candidate.base}:refs/heads/factory-base`]);
|
|
63
|
+
if (runGit(repo, ['rev-parse', '--verify', `${candidate.base}^{commit}`]) !== candidate.base)
|
|
64
|
+
throw new Error('Accepted base differs from the retained source commit.');
|
|
65
|
+
runGit(repo, ['read-tree', candidate.base]);
|
|
66
|
+
runGit(repo, ['apply', '--cached', '--binary', '--whitespace=nowarn', '-'], { input: patch });
|
|
67
|
+
const tree = runGit(repo, ['write-tree']);
|
|
68
|
+
if (tree !== candidate.tree) throw new Error('Protected patch does not reproduce the accepted candidate tree.');
|
|
69
|
+
return inspect({ repo, patch, tree, git: runGit });
|
|
70
|
+
} finally {
|
|
71
|
+
rmSync(scratchRoot, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
package/factory/delivery.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { withReconstructedCandidate } from './candidate-patch.mjs';
|
|
2
|
+
import { readPrivateJson, trustedExecutionProfile } from './execution-evidence.mjs';
|
|
2
3
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
-
import { existsSync,
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import { join, resolve } from 'node:path';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { configAt, digest } from './lib.mjs';
|
|
7
7
|
import { effectiveExecutionConfig, isSupportedExecutionProfile } from './execution-profile.mjs';
|
|
8
8
|
import { publicSourceAdmission } from './source-admission.mjs';
|
|
@@ -11,7 +11,6 @@ import { readProjectLinks } from './project-links.mjs';
|
|
|
11
11
|
import { qualifyGitHubActions } from './workflow-qualification.mjs';
|
|
12
12
|
import { assertCurrentWebEvidence, assertCurrentWebArtifacts } from './web-verification.mjs';
|
|
13
13
|
|
|
14
|
-
const MAX_PATCH_BYTES = 8 * 1024 * 1024;
|
|
15
14
|
const MAX_CHANGED_FILES = 500;
|
|
16
15
|
const MAX_TREE_BYTES = 8 * 1024 * 1024;
|
|
17
16
|
const SHA1 = /^[a-f0-9]{40}$/;
|
|
@@ -39,7 +38,6 @@ class BranchCollisionError extends QueueError {
|
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
function sha256(value) { return createHash('sha256').update(value).digest('hex'); }
|
|
43
41
|
function utcGitDate(value) {
|
|
44
42
|
const parsed = new Date(value);
|
|
45
43
|
if (!Number.isFinite(parsed.getTime())) throw new Error('Accepted timestamp is invalid.');
|
|
@@ -54,40 +52,6 @@ function commitObjectSha({ tree, parents, message, author, committer }) {
|
|
|
54
52
|
const raw = Buffer.from(`tree ${tree}\nparent ${parents[0]}\nauthor ${author.name} <${author.email}> ${date.epoch} +0000\ncommitter ${committer.name} <${committer.email}> ${date.epoch} +0000\n\n${message}`, 'utf8');
|
|
55
53
|
return createHash('sha1').update(`commit ${raw.length}\0`).update(raw).digest('hex');
|
|
56
54
|
}
|
|
57
|
-
function readPrivateFile(path, limit) {
|
|
58
|
-
const stat = lstatSync(path);
|
|
59
|
-
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.size > limit)
|
|
60
|
-
throw new Error('Protected candidate patch is missing, unsafe or too large.');
|
|
61
|
-
return readFileSync(path);
|
|
62
|
-
}
|
|
63
|
-
function runGitCommand(args) {
|
|
64
|
-
const env = {
|
|
65
|
-
PATH: process.env.PATH || '/usr/bin:/bin', HOME: '/nonexistent',
|
|
66
|
-
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null',
|
|
67
|
-
GIT_TERMINAL_PROMPT: '0', GIT_OPTIONAL_LOCKS: '0',
|
|
68
|
-
};
|
|
69
|
-
const result = spawnSync('git', args, { env, encoding: 'utf8', maxBuffer: MAX_TREE_BYTES + MAX_PATCH_BYTES + 1024 * 1024 });
|
|
70
|
-
if (result.error || result.status !== 0) throw new Error('Could not prepare private candidate reconstruction storage.');
|
|
71
|
-
return result.stdout.trim();
|
|
72
|
-
}
|
|
73
|
-
function runGit(repo, args, { input, binary = false } = {}) {
|
|
74
|
-
// Delivery reconstruction runs only trusted Git operations in a newly made
|
|
75
|
-
// bare repository. Candidate hooks, filters, global config and credentials
|
|
76
|
-
// are unavailable to these subprocesses.
|
|
77
|
-
const env = {
|
|
78
|
-
PATH: process.env.PATH || '/usr/bin:/bin',
|
|
79
|
-
HOME: '/nonexistent',
|
|
80
|
-
GIT_CONFIG_NOSYSTEM: '1',
|
|
81
|
-
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
82
|
-
GIT_TERMINAL_PROMPT: '0',
|
|
83
|
-
GIT_OPTIONAL_LOCKS: '0',
|
|
84
|
-
};
|
|
85
|
-
const result = spawnSync('git', ['--git-dir', repo, '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', ...args], {
|
|
86
|
-
env, input, encoding: binary ? null : 'utf8', maxBuffer: MAX_TREE_BYTES + MAX_PATCH_BYTES + 1024 * 1024,
|
|
87
|
-
});
|
|
88
|
-
if (result.error || result.status !== 0) throw new Error('Could not reconstruct the accepted candidate from retained Git objects.');
|
|
89
|
-
return binary ? result.stdout : result.stdout.trim();
|
|
90
|
-
}
|
|
91
55
|
function parseTree(bytes) {
|
|
92
56
|
const entries = new Map();
|
|
93
57
|
let start = 0;
|
|
@@ -112,24 +76,8 @@ function parseTree(bytes) {
|
|
|
112
76
|
function candidateTree(state, job, sourcePath, sourceAdmission, accepted, { branch, target }) {
|
|
113
77
|
if (sourceAdmission.object_format !== 'sha1' || !SHA1.test(accepted.base || '') || !SHA1.test(accepted.tree || ''))
|
|
114
78
|
throw new Error('GitHub delivery currently requires a SHA-1 candidate from the retained source repository.');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
assertPrivateDirectory(join(state, 'jobs', job.id));
|
|
118
|
-
assertPrivateDirectory(join(state, 'jobs', job.id, 'delivery-input'));
|
|
119
|
-
const patch = readPrivateFile(patchPath, MAX_PATCH_BYTES);
|
|
120
|
-
if (!patch.length || sha256(patch) !== accepted.patch_sha256) throw new Error('Accepted patch digest does not match the approval record.');
|
|
121
|
-
const folder = join(state, 'jobs', job.id);
|
|
122
|
-
const scratchRoot = mkdtempSync(join(folder, 'delivery-work-'));
|
|
123
|
-
const scratchRepo = join(scratchRoot, 'candidate.git');
|
|
124
|
-
try {
|
|
125
|
-
runGitCommand(['init', '--bare', '--quiet', '--object-format=sha1', scratchRepo]);
|
|
126
|
-
runGit(scratchRepo, ['fetch', '--quiet', '--no-tags', '--', sourcePath, `${accepted.base}:refs/heads/factory-base`]);
|
|
127
|
-
const baseCommit = runGit(scratchRepo, ['rev-parse', '--verify', `${accepted.base}^{commit}`]);
|
|
128
|
-
if (baseCommit !== accepted.base) throw new Error('Accepted base differs from the retained source commit.');
|
|
129
|
-
runGit(scratchRepo, ['read-tree', accepted.base]);
|
|
130
|
-
runGit(scratchRepo, ['apply', '--cached', '--binary', '--whitespace=nowarn', patchPath]);
|
|
131
|
-
const tree = runGit(scratchRepo, ['write-tree']);
|
|
132
|
-
if (tree !== accepted.tree) throw new Error('Protected patch does not reproduce the accepted candidate tree.');
|
|
79
|
+
return withReconstructedCandidate({ state, jobId: job.id, sourcePath,
|
|
80
|
+
objectFormat: sourceAdmission.object_format, candidate: accepted }, ({ repo: scratchRepo, patch, tree, git: runGit }) => {
|
|
133
81
|
const baseTree = runGit(scratchRepo, ['rev-parse', `${accepted.base}^{tree}`]);
|
|
134
82
|
const before = parseTree(runGit(scratchRepo, ['ls-tree', '-r', '-z', accepted.base], { binary: true }));
|
|
135
83
|
const after = parseTree(runGit(scratchRepo, ['ls-tree', '-r', '-z', tree], { binary: true }));
|
|
@@ -159,9 +107,7 @@ function candidateTree(state, job, sourcePath, sourceAdmission, accepted, { bran
|
|
|
159
107
|
entries.push({ path, mode: next.mode, type: 'blob', content });
|
|
160
108
|
}
|
|
161
109
|
return { patch, patch_sha256: accepted.patch_sha256, base_tree: baseTree, tree, entries, workflowQualification };
|
|
162
|
-
}
|
|
163
|
-
rmSync(scratchRoot, { recursive: true, force: true });
|
|
164
|
-
}
|
|
110
|
+
});
|
|
165
111
|
}
|
|
166
112
|
|
|
167
113
|
function sourceIssue(job, repository) {
|
|
@@ -7,12 +7,14 @@ import { effectiveInferenceProvider } from './model-environment.mjs';
|
|
|
7
7
|
import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
|
|
8
8
|
|
|
9
9
|
// Audited v1 protected evidence writers: 0.8.0 (380f749), 0.9.0 (cdaadef),
|
|
10
|
-
// 0.9.1, 0.10.0, 0.11.0 and 0.11.1 (unchanged protected evidence writers)
|
|
10
|
+
// 0.9.1, 0.10.0, 0.11.0 and 0.11.1 (unchanged protected evidence writers),
|
|
11
|
+
// 0.11.2 (exact patch bytes and pre-acceptance reconstruction; same v1 schema).
|
|
12
|
+
// Older writers still require exact patch/tree reconstruction before publication.
|
|
11
13
|
// Deliberately independent of VERSION: a release bump is not
|
|
12
14
|
// evidence compatibility. Re-audit this list for every trust-relevant writer,
|
|
13
15
|
// isolation or validation change; remove versions whose guarantees no longer
|
|
14
16
|
// satisfy current policy. See docs/npm.md. This predicate alone grants no trust.
|
|
15
|
-
const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0', '0.11.1']);
|
|
17
|
+
const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0', '0.11.1', '0.11.2']);
|
|
16
18
|
|
|
17
19
|
export function isSupportedExecutionProfile(profile) {
|
|
18
20
|
return profile?.version === 1 && SUPPORTED_EXECUTION_RUNTIMES_V1.has(profile.runtimeVersion);
|
package/factory/executor.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withReconstructedCandidate } from './candidate-patch.mjs';
|
|
1
2
|
import { harnessOf } from './lib.mjs';
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, lstatSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -7,7 +8,7 @@ import { incidentFor, validateReport } from './incident.mjs';
|
|
|
7
8
|
import { BoundedLog } from './bounded-log.mjs';
|
|
8
9
|
import { CodexUsageParser, emptyUsage, usageFields } from './usage.mjs';
|
|
9
10
|
import { assertRetainedSource, publicSourceAdmission, publicContinuation, restoreBuildCheckout } from './source-admission.mjs';
|
|
10
|
-
import { runCandidateGit } from './git-environment.mjs';
|
|
11
|
+
import { runCandidateGit, runCandidateGitRaw } from './git-environment.mjs';
|
|
11
12
|
import { selectedInferenceSecrets, writeSelectedModelEnvironment } from './model-environment.mjs';
|
|
12
13
|
import { redactInferenceText, redactRetainedPhaseOutputs } from './inference-redaction.mjs';
|
|
13
14
|
import { assertCurrentHandoffEvidence } from './execution-evidence.mjs';
|
|
@@ -293,8 +294,7 @@ try {
|
|
|
293
294
|
source_admission: publicSourceAdmission(sourceAdmission) };
|
|
294
295
|
save(join(folder,'candidate.json'),meta);
|
|
295
296
|
save(join(output,'candidate.json'),meta);
|
|
296
|
-
const
|
|
297
|
-
const patch = patchText ? `${patchText}\n` : '';
|
|
297
|
+
const patch = runCandidateGitRaw(workspace, '--no-pager', 'diff', '--binary', '--full-index', '--no-ext-diff', '--no-textconv', meta.base, meta.head);
|
|
298
298
|
writeFileSync(join(output,'change.patch'), patch);
|
|
299
299
|
writeFileSync(join(output,'implementation.md'),safeRead(join(reports,'agent-report.md')));
|
|
300
300
|
} else if (phase === 'verify') {
|
|
@@ -343,9 +343,8 @@ try {
|
|
|
343
343
|
if (config.webVerification?.enabled) assertCurrentWebArtifacts(config.webVerification, checks.web_verification,
|
|
344
344
|
join(folder, 'artifacts', checks.run_id));
|
|
345
345
|
assertCurrentHandoffEvidence(meta, checks, review, policyHash, config, job);
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
if (!patch) throw new Error('Candidate has no content changes to accept');
|
|
346
|
+
const patch = runCandidateGitRaw(workspace, '--no-pager', 'diff', '--binary', '--full-index', '--no-ext-diff', '--no-textconv', meta.base, meta.head);
|
|
347
|
+
if (!patch.length) throw new Error('Candidate has no content changes to accept');
|
|
349
348
|
const patchHash = digest(patch);
|
|
350
349
|
const deliveryInput = join(folder, 'delivery-input');
|
|
351
350
|
mkdirSync(deliveryInput, { recursive: true, mode: 0o700 });
|
|
@@ -355,6 +354,9 @@ try {
|
|
|
355
354
|
if (!stat.isFile() || stat.isSymbolicLink() || digest(readFileSync(deliveryPatch)) !== patchHash)
|
|
356
355
|
throw new Error('A different protected candidate patch already exists');
|
|
357
356
|
} else writeFileSync(deliveryPatch, patch, { mode: 0o600, flag: 'wx' });
|
|
357
|
+
const retained = assertRetainedSource(state, job, sourceAdmission);
|
|
358
|
+
withReconstructedCandidate({ state, jobId: job, sourcePath: retained.path,
|
|
359
|
+
objectFormat: sourceAdmission.object_format, candidate: { ...meta, patch_sha256: patchHash } });
|
|
358
360
|
const sourceSha = meta.source_admission?.resolved_sha || 'Not recorded (legacy/unknown)';
|
|
359
361
|
writeFileSync(join(output,'handoff.md'), `Accepted candidate ${meta.head} (tree ${meta.tree}), based on source ${sourceSha}.\nPatch handoff remains available. Optional PR delivery is a separate explicit operator action when configured.\nNo integration, merge, release or deployment performed.\nSee docs/quickstart.md for applying the reviewed change.patch to your own branch.\n`);
|
|
360
362
|
save(join(folder,'accepted.json'), { base: meta.base, head: meta.head, tree: meta.tree, patch_sha256: patchHash,
|
|
@@ -16,11 +16,21 @@ export function runHostGit(args, options = {}) {
|
|
|
16
16
|
return run('git', args, { ...options, env: hostGitEnvironment() });
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
function candidateGit(workspace, args, options) {
|
|
20
20
|
return runHostGit([
|
|
21
21
|
'-c', 'core.hooksPath=/dev/null',
|
|
22
22
|
'-c', 'core.fsmonitor=false',
|
|
23
23
|
'-C', workspace,
|
|
24
24
|
...args,
|
|
25
|
-
]);
|
|
25
|
+
], options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Scalar queries retain their historical trimming. Byte-bearing output must
|
|
29
|
+
// explicitly opt into Buffer stdout, with identical environment and limits.
|
|
30
|
+
export function runCandidateGit(workspace, ...args) {
|
|
31
|
+
return candidateGit(workspace, args);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runCandidateGitRaw(workspace, ...args) {
|
|
35
|
+
return candidateGit(workspace, args, { encoding: null });
|
|
26
36
|
}
|
package/factory/lib.mjs
CHANGED
|
@@ -19,7 +19,7 @@ export function run(command, args, options = {}) {
|
|
|
19
19
|
const result = spawnSync(command, args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, ...options });
|
|
20
20
|
if (result.error) throw result.error;
|
|
21
21
|
if (result.status !== 0) throw new Error(`${command} failed (${result.status}): ${result.stderr || result.stdout || ''}`);
|
|
22
|
-
return result.stdout?.trim() ?? '';
|
|
22
|
+
return Buffer.isBuffer(result.stdout) ? result.stdout : result.stdout?.trim() ?? '';
|
|
23
23
|
}
|
|
24
24
|
export function stream(command, args, options = {}) {
|
|
25
25
|
return new Promise((ok, fail) => {
|
package/package.json
CHANGED