mtech-ai-local-runner 0.1.52 → 0.1.53
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 -1
- package/dist/src/asks/commands.js +3 -1
- package/dist/src/asks/doctor.js +3 -1
- package/dist/src/publication/candidate-limits.js +89 -0
- package/dist/src/publication/failure.js +1 -1
- package/dist/src/publication/production-host.js +2 -0
- package/dist/src/publication/server-ci-host.js +18 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.1.53: separate candidate change, file and payload limits
|
|
2
|
+
|
|
3
|
+
New `gateway_publish_v1` scopes may explicitly opt into `candidate_limits.version: 2` with
|
|
4
|
+
`max_changed_files`, `max_change_bytes`, `max_file_bytes` and `max_payload_bytes`.
|
|
5
|
+
Change bytes are the exact minimum byte insertions plus deletions from the frozen base;
|
|
6
|
+
file bytes are each resulting decoded file; payload bytes are compact UTF-8 JSON for
|
|
7
|
+
`{files, validation_report?}`, including base64, paths, hashes and the report. HTTP/MCP
|
|
8
|
+
framing and provider multipart overhead remain separate infrastructure limits.
|
|
9
|
+
|
|
10
|
+
The Runner and Gateway compute these independently. Unknown measurement or unsupported
|
|
11
|
+
modes fail closed. The Gateway bounds frozen-base response streaming and rechecks
|
|
12
|
+
scope/authority before staging. Legacy unversioned `max_diff_bytes` keeps its existing
|
|
13
|
+
meaning. Existing candidates/contracts/Jobs/Loops are not upgraded or published by this
|
|
14
|
+
release. Match new package runtime receipts on both sides and approve a new v2 scope
|
|
15
|
+
before use; a publication-only recovery protocol for retained candidates is separate.
|
|
16
|
+
|
|
1
17
|
## 0.1.52: publication diagnostics and candidate cleanup
|
|
2
18
|
|
|
3
19
|
Publication failures report fixed `publication_step` and `LOCAL_FIX_*` codes;
|
|
@@ -15,6 +31,8 @@ model run. Pre-existing forensic candidates are not swept automatically.
|
|
|
15
31
|
|
|
16
32
|
## 0.1.51: open a session for an ask no session can show
|
|
17
33
|
|
|
34
|
+
0.1.51 was not published to npm; install 0.1.52 or later to get these changes.
|
|
35
|
+
|
|
18
36
|
Sometimes no live session can show an AI Gateway ask or answer soon: there is no live session, no human prompt for 30
|
|
19
37
|
minutes, an idle Codex session, or an idle Claude session that cannot be woken. The runner then opens a session in the
|
|
20
38
|
person's own AI app instead of only showing a notification (Work Item e1c08b33, Work Decision 3ea5c197).
|
|
@@ -118,7 +136,10 @@ npx mtech-ai-local-runner integrations remove claude
|
|
|
118
136
|
|
|
119
137
|
Claude Code gets the skills-directory plugin `~/.claude/skills/mtec-ai-gateway-asks/`, which loads as
|
|
120
138
|
`mtec-ai-gateway-asks@skills-dir` in a new session or after `/reload-plugins`; `settings.json` is not touched. Codex gets
|
|
121
|
-
four handlers appended to `~/.codex/hooks.json` after a backup of that file
|
|
139
|
+
four handlers appended to `~/.codex/hooks.json` after a backup of that file. Codex runs them only after you trust them
|
|
140
|
+
once. In the Codex app, trust them under Settings → Hooks or with the Review hooks button. In the Codex CLI in a
|
|
141
|
+
terminal, trust them with `/hooks`; the Codex app has no such command and sends `/hooks` to the model as text. Until
|
|
142
|
+
then the Slack message covers each ask. A
|
|
122
143
|
`hooks.json` that is not valid JSON, or whose hooks do not have the shape Codex reads, is left as it is and the install
|
|
123
144
|
reports `CODEX_HOOKS_JSON_INVALID`. `uninstall` removes both integrations, the shim, `asks.json` and any temporary
|
|
124
145
|
`asks.json.*.tmp` file a crash left behind, and it also works on a setup that never finished.
|
|
@@ -5,7 +5,9 @@ import { installIntegration, integrationStatus, removeIntegration, removeShimIfU
|
|
|
5
5
|
import { APPS, isApp } from "./types.js";
|
|
6
6
|
const NEXT_STEP = {
|
|
7
7
|
claude: "Start a new Claude Code session, or run /reload-plugins in an open one, to load mtec-ai-gateway-asks@skills-dir.",
|
|
8
|
-
|
|
8
|
+
// /hooks is a command of the Codex CLI in a terminal only: the Codex app sends it to the model as text.
|
|
9
|
+
codex: "Codex runs the four AI Gateway hooks only after you trust them once. In the Codex app, trust them under Settings → Hooks "
|
|
10
|
+
+ "or with the Review hooks button. In the Codex CLI in a terminal, trust them with /hooks. Until then the Slack message covers each ask.",
|
|
9
11
|
};
|
|
10
12
|
function errorCode(error) {
|
|
11
13
|
const code = error?.code;
|
package/dist/src/asks/doctor.js
CHANGED
|
@@ -10,7 +10,9 @@ const CLAUDE_NOT_LOADED = `Claude Code does not load ${PLUGIN_ID}. Managed setti
|
|
|
10
10
|
+ "(strictPluginOnlyCustomization, a strictKnownMarketplaces list without the skills-dir source, or blockedMarketplaces): "
|
|
11
11
|
+ "ask your administrator, or install the plugin from a directory marketplace your organization allows (claude plugin marketplace add <dir>). "
|
|
12
12
|
+ "A Claude Code release without skills-directory plugins does not load it either.";
|
|
13
|
-
|
|
13
|
+
// /hooks is a command of the Codex CLI in a terminal only: the Codex app sends it to the model as text.
|
|
14
|
+
const CODEX_TRUST = "Codex runs these hooks only after you trust them once. In the Codex app, trust them under Settings → Hooks "
|
|
15
|
+
+ "or with the Review hooks button. In the Codex CLI in a terminal, trust them with /hooks. Until then the Slack message covers each ask.";
|
|
14
16
|
/** Runs the Claude CLI with the environment doctor was given on top of the process's own (CLAUDE_CONFIG_DIR, for one). */
|
|
15
17
|
const defaultRun = (env) => (file, args) => new Promise((resolve, reject) => {
|
|
16
18
|
execFile(file, args, { encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024, env: { ...process.env, ...env } }, (error, stdout) => (error ? reject(error) : resolve({ stdout })));
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const issued = new WeakMap();
|
|
3
|
+
const reject = (code) => { const error = Error('CANDIDATE_LIMIT_REJECTED'); issued.set(error, code); throw error; };
|
|
4
|
+
export function trustedCandidateLimitCode(error) { return error && typeof error === 'object' ? issued.get(error) : undefined; }
|
|
5
|
+
export function candidateLimits(value, publish) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
+
return reject('LIMIT_SCHEMA_REJECTED');
|
|
8
|
+
const l = value;
|
|
9
|
+
if (l.version !== undefined) {
|
|
10
|
+
const keys = ['version', 'max_changed_files', 'max_change_bytes', 'max_file_bytes', 'max_payload_bytes'];
|
|
11
|
+
if (!publish || l.version !== 2 || Object.keys(l).sort().join() !== keys.sort().join() || keys.slice(0).filter(k => k !== 'version').some(k => !Number.isSafeInteger(l[k]) || Number(l[k]) < 1 || Number(l[k]) > (k === 'max_changed_files' ? 50 : 1000000)))
|
|
12
|
+
return reject('LIMIT_SCHEMA_REJECTED');
|
|
13
|
+
return { ...l };
|
|
14
|
+
}
|
|
15
|
+
if ('max_change_bytes' in l || 'max_payload_bytes' in l || ['max_changed_files', 'max_diff_bytes', 'max_file_bytes'].some(k => !Number.isSafeInteger(l[k]) || Number(l[k]) < 1))
|
|
16
|
+
return reject('LIMIT_SCHEMA_REJECTED');
|
|
17
|
+
return { ...l };
|
|
18
|
+
}
|
|
19
|
+
/** Exact byte insertion/deletion distance. Fixed work exhaustion never becomes a guessed count. */
|
|
20
|
+
export function changedBytes(before, after) {
|
|
21
|
+
if (before.length > 1000000 || after.length > 1000000)
|
|
22
|
+
return reject('CHANGE_MEASUREMENT_REJECTED');
|
|
23
|
+
let start = 0, n = before.length, m = after.length, work = 0;
|
|
24
|
+
const tick = () => { if (++work > 2000000)
|
|
25
|
+
reject('CHANGE_MEASUREMENT_REJECTED'); };
|
|
26
|
+
while (start < n && start < m && before[start] === after[start]) {
|
|
27
|
+
tick();
|
|
28
|
+
start++;
|
|
29
|
+
}
|
|
30
|
+
while (n > start && m > start && before[n - 1] === after[m - 1]) {
|
|
31
|
+
tick();
|
|
32
|
+
n--;
|
|
33
|
+
m--;
|
|
34
|
+
}
|
|
35
|
+
n -= start;
|
|
36
|
+
m -= start;
|
|
37
|
+
if (!n || !m)
|
|
38
|
+
return n + m;
|
|
39
|
+
let frontier = new Map([[1, 0]]);
|
|
40
|
+
for (let d = 0; d <= n + m; d++) {
|
|
41
|
+
const next = new Map();
|
|
42
|
+
for (let k = -d; k <= d; k += 2) {
|
|
43
|
+
tick();
|
|
44
|
+
const left = frontier.get(k - 1) ?? -1, down = frontier.get(k + 1) ?? -1;
|
|
45
|
+
let x = k === -d || (k !== d && left < down) ? down : left + 1;
|
|
46
|
+
if (x < 0)
|
|
47
|
+
continue;
|
|
48
|
+
let y = x - k;
|
|
49
|
+
while (x < n && y < m && y >= 0 && before[start + x] === after[start + y]) {
|
|
50
|
+
tick();
|
|
51
|
+
x++;
|
|
52
|
+
y++;
|
|
53
|
+
}
|
|
54
|
+
if (x >= n && y >= m)
|
|
55
|
+
return d;
|
|
56
|
+
next.set(k, x);
|
|
57
|
+
}
|
|
58
|
+
frontier = next;
|
|
59
|
+
}
|
|
60
|
+
return reject('CHANGE_MEASUREMENT_REJECTED');
|
|
61
|
+
}
|
|
62
|
+
export function candidatePayloadBytes(files, report) { return Buffer.byteLength(JSON.stringify({ files, ...(report === undefined ? {} : { validation_report: report }) }), 'utf8'); }
|
|
63
|
+
export function measureSplitDelta(diff, limits, report) {
|
|
64
|
+
const delta = JSON.parse(diff);
|
|
65
|
+
if (delta?.format !== 'local-candidate-binary-diff-v1' || !Array.isArray(delta.changes) || !delta.changes.length || delta.changes.length > limits.max_changed_files)
|
|
66
|
+
reject('FILE_COUNT_REJECTED');
|
|
67
|
+
let changes = 0, maxFile = 0;
|
|
68
|
+
const decode = (v) => { if (typeof v !== 'string')
|
|
69
|
+
return reject('CHANGE_MEASUREMENT_REJECTED'); const b = Buffer.from(v, 'base64'); if (b.toString('base64') !== v || b.length > 1000000 || b.includes(0) || !Buffer.from(b.toString('utf8')).equals(b))
|
|
70
|
+
return reject('CHANGE_MEASUREMENT_REJECTED'); return b; };
|
|
71
|
+
const files = delta.changes.map((c) => {
|
|
72
|
+
if (!c || typeof c.path !== 'string' || !['added', 'modified', 'deleted'].includes(c.status) || c.status === 'added' && c.before !== null || c.status === 'deleted' && c.after !== null)
|
|
73
|
+
reject('CHANGE_MEASUREMENT_REJECTED');
|
|
74
|
+
const before = c.status === 'added' ? Buffer.alloc(0) : decode(c.before), after = c.status === 'deleted' ? Buffer.alloc(0) : decode(c.after);
|
|
75
|
+
maxFile = Math.max(maxFile, after.length);
|
|
76
|
+
if (maxFile > limits.max_file_bytes)
|
|
77
|
+
reject('FILE_LIMIT_REJECTED');
|
|
78
|
+
changes += changedBytes(before, after);
|
|
79
|
+
if (changes > limits.max_change_bytes)
|
|
80
|
+
reject('CHANGE_LIMIT_REJECTED');
|
|
81
|
+
return { path: c.path, status: c.status, content_base64: c.status === 'deleted' ? null : after.toString('base64'), content_sha256: c.status === 'deleted' ? null : createHash('sha256').update(after).digest('hex') };
|
|
82
|
+
});
|
|
83
|
+
if (!changes)
|
|
84
|
+
reject('CHANGE_MEASUREMENT_REJECTED');
|
|
85
|
+
const payload = candidatePayloadBytes(files, report);
|
|
86
|
+
if (payload > limits.max_payload_bytes)
|
|
87
|
+
reject('PAYLOAD_LIMIT_REJECTED');
|
|
88
|
+
return { metrics: { version: 2, change_bytes: changes, max_file_bytes: maxFile, payload_bytes: payload }, files };
|
|
89
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PublicationOutcomeUnknownError } from './publisher.js';
|
|
2
2
|
export const PUBLICATION_STEPS = ['validation_report', 'current_fence', 'snapshot_observe', 'snapshot_materialize', 'candidate_limits', 'delta_format', 'delta_modes', 'delta_scope', 'candidate_files', 'journal', 'security_receipt', 'stage', 'advance', 'result', 'cleanup'];
|
|
3
|
-
export const PUBLICATION_CODES = ['LOCAL_FIX_CHECK_REJECTED', 'LOCAL_FIX_REPORT_REJECTED', 'LOCAL_FIX_DIFF_LIMIT_REJECTED', 'LOCAL_FIX_FILE_COUNT_REJECTED', 'LOCAL_FIX_FILE_LIMIT_REJECTED', 'LOCAL_FIX_FILE_ENCODING_REJECTED', 'LOCAL_FIX_PUBLICATION_OUTCOME_UNKNOWN', 'LOCAL_FIX_CLEANUP_FAILED'];
|
|
3
|
+
export const PUBLICATION_CODES = ['LOCAL_FIX_CHANGE_LIMIT_REJECTED', 'LOCAL_FIX_PAYLOAD_LIMIT_REJECTED', 'LOCAL_FIX_CHANGE_MEASUREMENT_REJECTED', 'LOCAL_FIX_CHECK_REJECTED', 'LOCAL_FIX_REPORT_REJECTED', 'LOCAL_FIX_DIFF_LIMIT_REJECTED', 'LOCAL_FIX_FILE_COUNT_REJECTED', 'LOCAL_FIX_FILE_LIMIT_REJECTED', 'LOCAL_FIX_FILE_ENCODING_REJECTED', 'LOCAL_FIX_PUBLICATION_OUTCOME_UNKNOWN', 'LOCAL_FIX_CLEANUP_FAILED'];
|
|
4
4
|
const issued = new WeakMap();
|
|
5
5
|
const cleanupFailures = new WeakSet();
|
|
6
6
|
/** Host vocabulary only; serialized fields/messages never carry diagnostic authority. */
|
|
@@ -55,6 +55,8 @@ export function createProductionLocalFixHost(options) {
|
|
|
55
55
|
} };
|
|
56
56
|
const frozenCurrent = async () => lifecycle.isFrozen() && await current();
|
|
57
57
|
const limits = scope.candidate_limits;
|
|
58
|
+
if (['version', 'max_change_bytes', 'max_payload_bytes'].some(k => k in limits))
|
|
59
|
+
throw denied();
|
|
58
60
|
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
61
|
const isolated = createSealedIsolatedVerifier({ host: snapshots, allowedPaths: verificationPaths, blockedPaths: ['.env', '.env.*', '.git/**', '.ssh/**', '.aws/**'], commands, current: frozenCurrent });
|
|
60
62
|
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)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { candidateLimits, measureSplitDelta, trustedCandidateLimitCode } from './candidate-limits.js';
|
|
1
2
|
import { publicationError, publicationCleanupFailure, trustedPublicationFailure } from './failure.js';
|
|
2
3
|
import { ServerCiPublicationError } from './server-ci-error.js';
|
|
3
4
|
export { ServerCiPublicationError } from './server-ci-error.js';
|
|
@@ -67,10 +68,11 @@ export function createServerCiLocalFixHost(options) {
|
|
|
67
68
|
const lifecycle = input.lifecycle;
|
|
68
69
|
if (!lifecycle)
|
|
69
70
|
throw denied();
|
|
70
|
-
const { job } = input, pkg = job.taskPackage, scope = pkg.local_fix_scope, repo = pkg.repository,
|
|
71
|
+
const { job } = input, pkg = job.taskPackage, scope = pkg.local_fix_scope, repo = pkg.repository, rawLimits = scope?.candidate_limits, allowed = scope?.allowed_paths;
|
|
71
72
|
const root = await realpath(options.privateRoot), st = await lstat(root);
|
|
72
|
-
if (!lifecycle.isCurrent() || pkg.runtime_adapter !== 'codex_cli' || !scope || scope.verification_mode !== mode || scope.ci_profile_fingerprint !== registration.ci_profile_fingerprint || protocolHash(scope.ci_profile) !== protocolHash(registration.ci_profile) || !Array.isArray(scope.required_commands) || scope.required_commands.length || !scope.registered_tests || typeof scope.registered_tests !== 'object' || Object.keys(scope.registered_tests).length || !Array.isArray(allowed) || !allowed.length || allowed.some(p => typeof p !== 'string' || !(safePath(p) || p.endsWith('/**') && safePath(p.slice(0, -3)))) || protocolHash(allowed) !== protocolHash(pkg.allowed_paths) || repo.key !== registration.project || job.gatewayTaskPackageFingerprint !== protocolHash(pkg) || Object.entries(registration.receipt).some(([k, v]) => scope[k] !== v) ||
|
|
73
|
+
if (!lifecycle.isCurrent() || pkg.runtime_adapter !== 'codex_cli' || !scope || scope.verification_mode !== mode || scope.ci_profile_fingerprint !== registration.ci_profile_fingerprint || protocolHash(scope.ci_profile) !== protocolHash(registration.ci_profile) || !Array.isArray(scope.required_commands) || scope.required_commands.length || !scope.registered_tests || typeof scope.registered_tests !== 'object' || Object.keys(scope.registered_tests).length || !Array.isArray(allowed) || !allowed.length || allowed.some(p => typeof p !== 'string' || !(safePath(p) || p.endsWith('/**') && safePath(p.slice(0, -3)))) || protocolHash(allowed) !== protocolHash(pkg.allowed_paths) || repo.key !== registration.project || job.gatewayTaskPackageFingerprint !== protocolHash(pkg) || Object.entries(registration.receipt).some(([k, v]) => scope[k] !== v) || root !== options.privateRoot || !st.isDirectory() || st.isSymbolicLink() || (st.mode & 0o077) !== 0 || (process.getuid && st.uid !== process.getuid()) || root === input.checkoutPath || root.startsWith(input.checkoutPath + path.sep))
|
|
73
74
|
throw denied();
|
|
75
|
+
const limits = candidateLimits(rawLimits, mode === 'gateway_publish_v1');
|
|
74
76
|
const fence = { jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, launchGrant: job.launchGrant, bindingId: input.bindingId };
|
|
75
77
|
const binding = bindingProof((await options.gateway.localPublicationContext(fence)).context);
|
|
76
78
|
if (binding.runner_id !== options.runnerId || binding.binding_id !== input.bindingId || binding.job_id !== job.id || binding.generation !== job.generation || binding.work_loop_id !== job.workLoopId || binding.repository_key !== repo.key || binding.base_commit_sha !== repo.base_commit_sha || binding.working_branch !== repo.working_branch || binding.working_branch !== `ai-work/local/${binding.attempt_id}` || binding.attempt_id !== scope.attempt_id || binding.contract_fingerprint !== job.gatewayContractFingerprint || binding.task_package_fingerprint !== job.gatewayTaskPackageFingerprint || binding.launch_fingerprint !== job.launchFingerprint)
|
|
@@ -99,7 +101,17 @@ export function createServerCiLocalFixHost(options) {
|
|
|
99
101
|
phase = 'snapshot_observe';
|
|
100
102
|
const observed = await host.observe();
|
|
101
103
|
phase = 'candidate_limits';
|
|
102
|
-
|
|
104
|
+
let split;
|
|
105
|
+
if (limits.version === 2) {
|
|
106
|
+
try {
|
|
107
|
+
split = measureSplitDelta(observed.diff, limits, report);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const code = trustedCandidateLimitCode(error);
|
|
111
|
+
throw publicationError(phase, code === 'CHANGE_LIMIT_REJECTED' ? 'LOCAL_FIX_CHANGE_LIMIT_REJECTED' : code === 'PAYLOAD_LIMIT_REJECTED' ? 'LOCAL_FIX_PAYLOAD_LIMIT_REJECTED' : code === 'FILE_LIMIT_REJECTED' ? 'LOCAL_FIX_FILE_LIMIT_REJECTED' : code === 'FILE_COUNT_REJECTED' ? 'LOCAL_FIX_FILE_COUNT_REJECTED' : 'LOCAL_FIX_CHANGE_MEASUREMENT_REJECTED');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (limits.version !== 2 && Buffer.byteLength(observed.diff) > limits.max_diff_bytes)
|
|
103
115
|
throw publicationError(phase, 'LOCAL_FIX_DIFF_LIMIT_REJECTED');
|
|
104
116
|
if (!observed.changed_files.length || observed.changed_files.length > limits.max_changed_files)
|
|
105
117
|
throw publicationError(phase, 'LOCAL_FIX_FILE_COUNT_REJECTED');
|
|
@@ -156,7 +168,7 @@ export function createServerCiLocalFixHost(options) {
|
|
|
156
168
|
if (!s.isFile() || s.nlink !== 1n || s.dev !== before.dev || s.ino !== before.ino || s.size !== before.size || s.mtimeNs !== before.mtimeNs || s.ctimeNs !== before.ctimeNs || s.mode !== before.mode)
|
|
157
169
|
throw denied();
|
|
158
170
|
bytes += content.length;
|
|
159
|
-
if (bytes > limits.max_diff_bytes)
|
|
171
|
+
if (limits.version !== 2 && bytes > limits.max_diff_bytes)
|
|
160
172
|
throw publicationError(phase, 'LOCAL_FIX_DIFF_LIMIT_REJECTED');
|
|
161
173
|
if (content.includes(0) || !Buffer.from(content.toString('utf8'), 'utf8').equals(content))
|
|
162
174
|
throw publicationError(phase, 'LOCAL_FIX_FILE_ENCODING_REJECTED');
|
|
@@ -166,6 +178,8 @@ export function createServerCiLocalFixHost(options) {
|
|
|
166
178
|
await fd.close();
|
|
167
179
|
}
|
|
168
180
|
}
|
|
181
|
+
if (split && protocolHash(split.files) !== protocolHash(files))
|
|
182
|
+
throw publicationError('candidate_files');
|
|
169
183
|
phase = 'journal';
|
|
170
184
|
const journal = new FilePublicationJournal(root), key = protocolHash({ mode, binding }), deltaFingerprint = protocolHash({ binding, files, profile: registration.ci_profile_fingerprint, ...(report ? { validation_report: report } : {}) });
|
|
171
185
|
return journal.exclusive(key, async () => {
|