mtech-ai-local-runner 0.1.41 → 0.1.43
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/README.md +22 -0
- package/dist/src/process/owned-process-guardian.js +31 -26
- package/dist/src/publication/git-snapshot.js +3 -0
- package/dist/src/publication/production-host.js +3 -3
- package/dist/src/publication/server-ci-host.js +2 -2
- package/dist/src/verification/runner-observed-verifier.js +12 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 0.1.43: give whole-checkout snapshots whole-checkout bounds
|
|
2
|
+
|
|
3
|
+
The P1 verifier snapshots the whole model checkout before it runs the reviewed commands. It passed the candidate limits
|
|
4
|
+
(`max_file_bytes` and `max_diff_bytes`, 16 KiB in the pilot contract) as the snapshot's own per-file and whole-tree
|
|
5
|
+
bounds, so any real project failed with `SNAPSHOT_REJECTED`: the AI Gateway checkout has 3,781 files, 48.5 MB and 588
|
|
6
|
+
files above 16 KiB. The snapshot now takes the same 128 MiB whole-tree memory bound as the publication hosts. The
|
|
7
|
+
candidate limits still apply to the changed files: their count, the diff size and, newly explicit, each changed file's
|
|
8
|
+
before and after size. Local Fix had the same defect, and Local Fix → PR could not publish on the AI Gateway repository because
|
|
9
|
+
an unchanged 1.09 MB file exceeds the 1 MiB `max_file_bytes` cap. Its production and server-CI snapshots and its Git
|
|
10
|
+
publication transport now use the same shared `SNAPSHOT_TREE_MAX_BYTES`. Changed files are still bounded by
|
|
11
|
+
`max_file_bytes`.
|
|
12
|
+
|
|
13
|
+
## 0.1.42: keep the guardian's reports when a late terminate fails
|
|
14
|
+
|
|
15
|
+
The owned guardian now reports to the host on its own pipe (fd3) and takes the host's commands on another (fd5);
|
|
16
|
+
fd4 stays parent liveness, and none of them reaches the executor. Before, one control socket carried both. When
|
|
17
|
+
Codex exited by itself and the guardian confirmed and exited while the daemon was busy, the daemon's late terminate
|
|
18
|
+
write failed on that socket, Node destroyed it, and the unread `root_exit` and `cleaned` reports were lost. 0.1.41
|
|
19
|
+
recovered the cleanup confirmation from the exit code, but the lost root exit outcome still failed the P1 publication
|
|
20
|
+
freeze with `OWNED_PROCESS_EXIT_UNCONFIRMED`. A failed command write now leaves the report pipe intact, so the host reads
|
|
21
|
+
every report to its end. Cleanup is again confirmed only by `cleaned`; the exit-code fallback is removed.
|
|
22
|
+
|
|
1
23
|
## 0.1.41: confirm a cleanup the guardian finished before a late terminate
|
|
2
24
|
|
|
3
25
|
When Codex exits by itself, the owned guardian confirms the cleanup and exits. A busy daemon could then write its
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
// Trusted, packaged JavaScript, deliberately independent of TypeScript loaders.
|
|
3
|
-
// Only this helper creates and signals the new executor group. fd3
|
|
4
|
-
//
|
|
3
|
+
// Only this helper creates and signals the new executor group. fd3 carries its
|
|
4
|
+
// private reports to the host, fd4 parent liveness and fd5 the host's commands;
|
|
5
|
+
// none is passed to the executor. Commands have their own pipe: a host write
|
|
6
|
+
// that fails after the guardian exited cannot discard reports not yet read.
|
|
5
7
|
const GUARDIAN_SOURCE = String.raw `
|
|
6
8
|
const {spawn}=require('node:child_process');
|
|
7
9
|
const {Socket}=require('node:net');
|
|
8
|
-
const
|
|
9
|
-
control.setEncoding('utf8');
|
|
10
|
+
const reports=new Socket({fd:3,readable:false,writable:true});
|
|
10
11
|
const liveness=new Socket({fd:4,readable:true,writable:false});
|
|
12
|
+
const commands=new Socket({fd:5,readable:true,writable:false});
|
|
13
|
+
commands.setEncoding('utf8');
|
|
11
14
|
let child, ownedPid, ownedGone=false, cleaning, ready=Promise.resolve(), rootExited=false, closed=false, input='', configured=false, orphaned=false;
|
|
12
|
-
const send=value=>{if(!
|
|
15
|
+
const send=value=>{if(!reports.destroyed)reports.write(JSON.stringify(value)+'\n');};
|
|
13
16
|
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
|
14
17
|
const alive=()=>{if(!ownedPid||ownedGone)return false;try{process.kill(-ownedPid,0);return true;}catch(e){if(e.code==='ESRCH'){ownedGone=true;return false;}if(e.code==='EPERM')return true;throw Error('cleanup');}};
|
|
15
18
|
const signal=s=>{if(!ownedPid)throw Error('ownership');if(ownedGone)return 'esrch';try{process.kill(-ownedPid,s);return 'sent';}catch(e){if(e.code==='ESRCH'){ownedGone=true;return 'esrch';}if(e.code==='EPERM'){if(!rootExited)child.kill(s);return 'eperm';}throw Error('cleanup');}};
|
|
@@ -39,11 +42,11 @@ function cleanup(immediate=false,trigger='terminate'){
|
|
|
39
42
|
})();
|
|
40
43
|
return cleaning;
|
|
41
44
|
}
|
|
42
|
-
function finish(code){process.exitCode=code;process.stdin.destroy();liveness.destroy();
|
|
45
|
+
function finish(code){process.exitCode=code;process.stdin.destroy();liveness.destroy();commands.destroy();reports.end(()=>reports.destroy());process.stdout.end();process.stderr.end();}
|
|
43
46
|
function lost(){orphaned=true;if(configured)void cleanup(false,'lost');else finish(1);}
|
|
44
47
|
liveness.on('data',()=>{});liveness.once('end',lost);liveness.once('error',lost);
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
reports.on('error',lost);commands.on('error',lost);commands.on('end',lost);
|
|
49
|
+
commands.on('data',chunk=>{
|
|
47
50
|
input+=chunk.toString('utf8');if(input.length>2000000){lost();return;}
|
|
48
51
|
let end;while((end=input.indexOf('\n'))>=0){
|
|
49
52
|
let message;try{message=JSON.parse(input.slice(0,end));}catch{lost();return;}input=input.slice(end+1);
|
|
@@ -68,29 +71,31 @@ process.stderr.on('error',()=>{child?.stderr.resume();void cleanup(true,'output_
|
|
|
68
71
|
`;
|
|
69
72
|
export function spawnOwnedGuardian(command, args, options) {
|
|
70
73
|
const child = spawn(process.execPath, ['--input-type=commonjs', '-e', GUARDIAN_SOURCE], {
|
|
71
|
-
cwd: options.cwd, env: {}, shell: false, detached: true, stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
74
|
+
cwd: options.cwd, env: {}, shell: false, detached: true, stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
72
75
|
});
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
// Node types only the first five stdio entries; fd5 is the sixth 'pipe' above.
|
|
77
|
+
const stdio = child.stdio;
|
|
78
|
+
const reports = stdio[3], liveness = stdio[4], commands = stdio[5];
|
|
79
|
+
reports.setEncoding('utf8');
|
|
75
80
|
liveness.resume();
|
|
76
81
|
let resolveStarted, rejectStarted;
|
|
77
82
|
const started = new Promise((resolve, reject) => { resolveStarted = resolve; rejectStarted = reject; });
|
|
78
83
|
// Hosts can attach observers after spawn returns without unhandled rejection.
|
|
79
84
|
void started.catch(() => { });
|
|
80
85
|
let confirmed = false, spawnFailed = false, protocolFailed = false, buffer = '';
|
|
81
|
-
let terminateWritten = false, hostTimeout = false,
|
|
86
|
+
let terminateWritten = false, hostTimeout = false, commandFailed = false, detail;
|
|
82
87
|
let outcome;
|
|
83
88
|
let closePromise;
|
|
84
89
|
const closed = new Promise(resolve => child.once('close', () => { liveness.destroy(); resolve(); }));
|
|
85
|
-
// A guardian that has
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
// A guardian that has exited no longer reads commands, so a terminate written late by a busy host
|
|
91
|
+
// fails. That failure stays on the command pipe: the host still reads every report to its end.
|
|
92
|
+
commands.on('error', () => { if (!confirmed && !spawnFailed)
|
|
93
|
+
commandFailed = true; });
|
|
94
|
+
reports.on('error', () => { if (!confirmed && !spawnFailed)
|
|
95
|
+
protocolFailed = true; });
|
|
90
96
|
liveness.on('error', () => { if (!confirmed && !spawnFailed)
|
|
91
97
|
protocolFailed = true; });
|
|
92
|
-
|
|
93
|
-
control.on('data', (chunk) => {
|
|
98
|
+
reports.on('data', (chunk) => {
|
|
94
99
|
buffer += chunk;
|
|
95
100
|
if (buffer.length > 8192) {
|
|
96
101
|
protocolFailed = true;
|
|
@@ -132,10 +137,10 @@ export function spawnOwnedGuardian(command, args, options) {
|
|
|
132
137
|
});
|
|
133
138
|
child.once('error', () => { spawnFailed = true; rejectStarted(new Error('Provider process could not start')); });
|
|
134
139
|
child.once('close', () => { rejectStarted(new Error('Provider process could not start')); });
|
|
135
|
-
|
|
140
|
+
commands.write(JSON.stringify({ type: 'launch', command, args, ...options }) + '\n');
|
|
136
141
|
return { child, started, closed, outcome: () => outcome, spawnFailed: () => spawnFailed,
|
|
137
|
-
signal(immediate) { if (child.exitCode === null && child.signalCode === null && !confirmed && !spawnFailed && !
|
|
138
|
-
|
|
142
|
+
signal(immediate) { if (child.exitCode === null && child.signalCode === null && !confirmed && !spawnFailed && !commands.destroyed) {
|
|
143
|
+
commands.write(JSON.stringify({ type: 'terminate', immediate }) + '\n');
|
|
139
144
|
terminateWritten = true;
|
|
140
145
|
} },
|
|
141
146
|
terminate(immediate = false) {
|
|
@@ -144,7 +149,7 @@ export function spawnOwnedGuardian(command, args, options) {
|
|
|
144
149
|
this.signal(immediate);
|
|
145
150
|
closePromise = new Promise((resolve, reject) => {
|
|
146
151
|
const timer = setTimeout(() => { protocolFailed = true; hostTimeout = true; liveness.destroy(); reject(new Error('Provider process group cleanup could not be confirmed')); }, 5500);
|
|
147
|
-
void closed.then(() => { clearTimeout(timer); if (protocolFailed || !
|
|
152
|
+
void closed.then(() => { clearTimeout(timer); if (protocolFailed || !confirmed)
|
|
148
153
|
reject(new Error('Provider process group cleanup could not be confirmed'));
|
|
149
154
|
else
|
|
150
155
|
resolve(); });
|
|
@@ -153,14 +158,14 @@ export function spawnOwnedGuardian(command, args, options) {
|
|
|
153
158
|
},
|
|
154
159
|
/** Fixed cleanup observations after an unconfirmed cleanup; empty once cleanup is confirmed. */
|
|
155
160
|
cleanupObservations() {
|
|
156
|
-
if (
|
|
161
|
+
if (confirmed && !protocolFailed)
|
|
157
162
|
return [];
|
|
158
|
-
if (!protocolFailed && !hostTimeout && !
|
|
163
|
+
if (!protocolFailed && !hostTimeout && !commandFailed)
|
|
159
164
|
return [];
|
|
160
165
|
const observed = [terminateWritten ? 'cleanup_guardian_terminate_written' : 'cleanup_guardian_terminate_not_written'];
|
|
161
166
|
if (hostTimeout)
|
|
162
167
|
observed.push('cleanup_guardian_host_timeout');
|
|
163
|
-
if (
|
|
168
|
+
if (commandFailed)
|
|
164
169
|
observed.push('cleanup_guardian_control_failed');
|
|
165
170
|
if (!detail) {
|
|
166
171
|
if (!hostTimeout && protocolFailed)
|
|
@@ -12,6 +12,9 @@ const execute = promisify(execFile), denied = () => Error('SNAPSHOT_REJECTED');
|
|
|
12
12
|
const digest = (data) => createHash('sha256').update(data).digest('hex');
|
|
13
13
|
const safe = (p) => p.length > 0 && !path.isAbsolute(p) && !p.includes('\\') && !p.includes('\0') && p.split('/').every(s => s !== '' && s !== '.' && s !== '..' && !/^(?:\.git|\.ssh|\.aws|\.codex|\.claude|\.env|\.netrc|\.npmrc|credentials(?:\..*)?)$/i.test(s));
|
|
14
14
|
/** P1 branch shape only. Authority still requires exact frozen-fence equality. */
|
|
15
|
+
/** Whole-checkout memory bound for a snapshot, per file and in total. A snapshot reads every
|
|
16
|
+
* file of the checkout, so callers bound candidate size on the changed files instead. */
|
|
17
|
+
export const SNAPSHOT_TREE_MAX_BYTES = 128 * 1024 * 1024;
|
|
15
18
|
export const isP1WorkingBranch = (branch) => typeof branch === 'string' && branch.length <= 255 && /^ai-work\/[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(branch) && branch.split('/').every(p => !p.includes('..') && !p.endsWith('.') && !p.endsWith('.lock'));
|
|
16
19
|
/** Reads only immutable Git objects and metadata. Never runs diff against worktree
|
|
17
20
|
* content, where malicious clean filters can execute even with --no-ext-diff. */
|
|
@@ -4,7 +4,7 @@ import { lstat, realpath, open, rename, unlink } from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { CandidateVerifier, protocolHash } from './candidate.js';
|
|
6
6
|
import { createLocalFixExecutionHost } from './execution-host.js';
|
|
7
|
-
import { GitSnapshotHost } from './git-snapshot.js';
|
|
7
|
+
import { GitSnapshotHost, SNAPSHOT_TREE_MAX_BYTES } from './git-snapshot.js';
|
|
8
8
|
import { createSealedIsolatedVerifier } from './isolated-verifier.js';
|
|
9
9
|
import { FilePublicationJournal } from './journal.js';
|
|
10
10
|
import { DeterministicPublisher, PublicationOutcomeUnknownError } from './publisher.js';
|
|
@@ -55,7 +55,7 @@ export function createProductionLocalFixHost(options) {
|
|
|
55
55
|
} };
|
|
56
56
|
const frozenCurrent = async () => lifecycle.isFrozen() && await current();
|
|
57
57
|
const limits = scope.candidate_limits;
|
|
58
|
-
const snapshots = new GitSnapshotHost({ root: input.checkoutPath, privateRoot: root, git: options.git, baseCommitSha: binding.base_commit_sha, workingBranch: binding.working_branch, maxFileBytes:
|
|
58
|
+
const snapshots = new GitSnapshotHost({ root: input.checkoutPath, privateRoot: root, git: options.git, baseCommitSha: binding.base_commit_sha, workingBranch: binding.working_branch, maxFileBytes: SNAPSHOT_TREE_MAX_BYTES, maxTotalBytes: SNAPSHOT_TREE_MAX_BYTES });
|
|
59
59
|
const isolated = createSealedIsolatedVerifier({ host: snapshots, allowedPaths: verificationPaths, blockedPaths: ['.env', '.env.*', '.git/**', '.ssh/**', '.aws/**'], commands, current: frozenCurrent });
|
|
60
60
|
const verifier = new CandidateVerifier({ binding, root: input.checkoutPath, allowedPaths: allowed, blockedPaths: ['.env', '.env.*', '.git/**', '.ssh/**', '.aws/**'], maxFiles: limits.max_changed_files, maxDiffBytes: limits.max_diff_bytes, maxFileBytes: limits.max_file_bytes, commands, current: frozenCurrent, assertRetainedSeal: async () => { const seal = isolated.materialized(); if (!seal)
|
|
61
61
|
throw denied(); await seal.assertUnchanged(); }, observe: () => snapshots.observe(), verify: isolated.verify });
|
|
@@ -140,7 +140,7 @@ export function createProductionLocalFixHost(options) {
|
|
|
140
140
|
}
|
|
141
141
|
if (!await frozenCurrent())
|
|
142
142
|
throw denied();
|
|
143
|
-
git = new GitPublicationTransport({ checkout, privateRoot: root, git: options.git, sealedTreeFingerprint: seal.treeFingerprint, evidence: candidate.evidence, origin: options.origin, current: frozenCurrent, broker: typeof options.broker === 'function' ? options.broker({ branch: binding.working_branch, current: frozenCurrent }) : options.broker, maxFileBytes:
|
|
143
|
+
git = new GitPublicationTransport({ checkout, privateRoot: root, git: options.git, sealedTreeFingerprint: seal.treeFingerprint, evidence: candidate.evidence, origin: options.origin, current: frozenCurrent, broker: typeof options.broker === 'function' ? options.broker({ branch: binding.working_branch, current: frozenCurrent }) : options.broker, maxFileBytes: SNAPSHOT_TREE_MAX_BYTES, maxTotalBytes: SNAPSHOT_TREE_MAX_BYTES });
|
|
144
144
|
}
|
|
145
145
|
return git;
|
|
146
146
|
};
|
|
@@ -8,7 +8,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
8
8
|
import { MAX_EVENT_SUMMARY_CHARS } from '../protocol.js';
|
|
9
9
|
import { protocolHash } from './candidate.js';
|
|
10
10
|
import { FilePublicationJournal } from './journal.js';
|
|
11
|
-
import { GitSnapshotHost } from './git-snapshot.js';
|
|
11
|
+
import { GitSnapshotHost, SNAPSHOT_TREE_MAX_BYTES } from './git-snapshot.js';
|
|
12
12
|
import { PublicationOutcomeUnknownError } from './publisher.js';
|
|
13
13
|
const denied = () => Error('LOCAL_FIX_SERVER_CI_REJECTED');
|
|
14
14
|
const safePath = (p) => typeof p === 'string' && p.length > 0 && !p.includes('\\') && !p.includes('\0') && !path.isAbsolute(p) && p.split('/').every(s => s !== '' && s !== '.' && s !== '..' && !/^(?:\.git|\.mcp(?:\..*)?|\.npmrc|\.netrc|id_rsa|id_ed25519|\.local-runner|\.runner|\.env(?:\..*)?|\.codex|\.claude|\.ssh|\.aws|\.config|auth\.(?:json|toml|ya?ml)(?:\..*)?|credentials(?:\..*)?|AGENTS\.md|CLAUDE\.md|mcp\.(?:json|toml|ya?ml)(?:\..*)?)$/i.test(s));
|
|
@@ -82,7 +82,7 @@ export function createServerCiLocalFixHost(options) {
|
|
|
82
82
|
return finalization ??= (async () => {
|
|
83
83
|
const report = mode === 'gateway_publish_v1' ? validationReport(completedEvent) : undefined;
|
|
84
84
|
await current();
|
|
85
|
-
const host = new GitSnapshotHost({ root: input.checkoutPath, privateRoot: root, git: options.git, baseCommitSha: binding.base_commit_sha, workingBranch: binding.working_branch, maxFileBytes:
|
|
85
|
+
const host = new GitSnapshotHost({ root: input.checkoutPath, privateRoot: root, git: options.git, baseCommitSha: binding.base_commit_sha, workingBranch: binding.working_branch, maxFileBytes: SNAPSHOT_TREE_MAX_BYTES, maxTotalBytes: SNAPSHOT_TREE_MAX_BYTES });
|
|
86
86
|
const observed = await host.observe();
|
|
87
87
|
seal = await host.materialize();
|
|
88
88
|
if (protocolHash(observed) !== protocolHash(seal.observation) || Buffer.byteLength(observed.diff) > limits.max_diff_bytes || !observed.changed_files.length || observed.changed_files.length > limits.max_changed_files)
|
|
@@ -2,12 +2,21 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { realpath } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { protocolHash, protocolJson } from '../publication/candidate.js';
|
|
5
|
-
import { GitSnapshotHost, isP1WorkingBranch } from '../publication/git-snapshot.js';
|
|
5
|
+
import { GitSnapshotHost, isP1WorkingBranch, SNAPSHOT_TREE_MAX_BYTES } from '../publication/git-snapshot.js';
|
|
6
6
|
import { createIsolatedCommandPolicy } from '../publication/isolated-verifier.js';
|
|
7
7
|
import { buildReviewedProjectRegistration } from '../publication/project-registration.js';
|
|
8
8
|
const denied = () => Error('RUNNER_VERIFICATION_REJECTED');
|
|
9
9
|
const hex = (v, n) => typeof v === 'string' && new RegExp(`^[a-f0-9]{${n}}$`).test(v);
|
|
10
10
|
const same = (a, b) => protocolHash(a) === protocolHash(b);
|
|
11
|
+
/** Byte length of each changed file's before and after content in the sealed binary diff.
|
|
12
|
+
* The snapshot itself takes the whole-checkout bound; candidate limits apply to these files. */
|
|
13
|
+
function changedFileBytes(diff) {
|
|
14
|
+
const parsed = JSON.parse(diff);
|
|
15
|
+
if (parsed.format !== 'local-candidate-binary-diff-v1' || !Array.isArray(parsed.changes))
|
|
16
|
+
throw denied();
|
|
17
|
+
return parsed.changes.flatMap((change) => [change.before, change.after].filter(v => v !== null).map(v => { if (typeof v !== 'string')
|
|
18
|
+
throw denied(); return Buffer.from(v, 'base64').length; }));
|
|
19
|
+
}
|
|
11
20
|
const ceilings = { max_commands: 8, max_command_runtime_ms: 120000, max_output_bytes: 1048576, max_manifest_entries: 100, max_manifest_bytes: 32768, max_contract_bytes: 65536 };
|
|
12
21
|
const privatePart = /^(?:\.git|\.mcp(?:\..*)?|\.env(?:\..*)?|\.ssh|\.aws|\.config|\.codex|\.claude|\.runner|\.local-runner|\.npmrc|\.netrc|id_rsa|id_ed25519|credentials(?:\..*)?|auth\.(?:json|toml|ya?ml)(?:\..*)?|mcp\.(?:json|toml|ya?ml)(?:\..*)?|AGENTS\.md|CLAUDE\.md|vendor|node_modules)$/i;
|
|
13
22
|
const exactPath = (name) => typeof name === 'string' && /^[A-Za-z0-9_][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_.-]+)*$/.test(name) && name.split('/').every(p => p !== '.' && p !== '..' && !privatePart.test(p));
|
|
@@ -94,9 +103,9 @@ export class RunnerObservedVerifier {
|
|
|
94
103
|
await this.active();
|
|
95
104
|
if (!path.isAbsolute(o.root) || await realpath(o.root) !== o.root || !path.isAbsolute(o.privateRoot) || await realpath(o.privateRoot) !== o.privateRoot || o.privateRoot === o.root || o.privateRoot.startsWith(o.root + '/'))
|
|
96
105
|
throw denied();
|
|
97
|
-
host = o.host ?? new GitSnapshotHost({ root: o.root, privateRoot: o.privateRoot, git: o.registration.git, baseCommitSha: f.base_commit_sha, workingBranch: f.working_branch, p1WorkingBranch: f.working_branch, maxFileBytes:
|
|
106
|
+
host = o.host ?? new GitSnapshotHost({ root: o.root, privateRoot: o.privateRoot, git: o.registration.git, baseCommitSha: f.base_commit_sha, workingBranch: f.working_branch, p1WorkingBranch: f.working_branch, maxFileBytes: SNAPSHOT_TREE_MAX_BYTES, maxTotalBytes: SNAPSHOT_TREE_MAX_BYTES });
|
|
98
107
|
const sealed = await host.materialize(), observation = structuredClone(sealed.observation);
|
|
99
|
-
if (observation.base_commit_sha !== f.base_commit_sha || observation.head_commit_sha !== f.base_commit_sha || observation.working_branch !== f.working_branch || !observation.changed_files.length || observation.changed_files.length > o.candidateLimits.max_changed_files || Buffer.byteLength(observation.diff) > o.candidateLimits.max_diff_bytes || observation.changed_files.some(e => !o.allowedPaths.includes(e.path) || !exactPath(e.path) || isBlocked(e.path, o.blockedPaths)))
|
|
108
|
+
if (observation.base_commit_sha !== f.base_commit_sha || observation.head_commit_sha !== f.base_commit_sha || observation.working_branch !== f.working_branch || !observation.changed_files.length || observation.changed_files.length > o.candidateLimits.max_changed_files || Buffer.byteLength(observation.diff) > o.candidateLimits.max_diff_bytes || observation.changed_files.some(e => !o.allowedPaths.includes(e.path) || !exactPath(e.path) || isBlocked(e.path, o.blockedPaths)) || changedFileBytes(observation.diff).some(n => n > o.candidateLimits.max_file_bytes))
|
|
100
109
|
throw denied();
|
|
101
110
|
await this.active();
|
|
102
111
|
await sealed.assertUnchanged();
|