mtech-ai-local-runner 0.1.32 → 0.1.33
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 +21 -0
- package/dist/src/daemon/execution-coordinator.js +1 -1
- package/dist/src/daemon/runtime.js +3 -1
- package/dist/src/gateway-client.js +1 -1
- package/dist/src/publication/installed-host.js +3 -1
- package/dist/src/publication/recovery.js +1 -1
- package/dist/src/publication/server-ci-host.js +48 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -458,3 +458,24 @@ query timeout, command and validation failures use distinct safe codes. The
|
|
|
458
458
|
inventory must still be fresh at Codex spawn. No automatic retry is performed.
|
|
459
459
|
|
|
460
460
|
The reviewed manual Codex path preserves the existing CLI and ChatGPT login. It is separate from unattended daemon readiness. After the exact CLI/native reader and Gateway release are installed, an employee-authorized foreground pilot uses `mtec-ai-local-runner manual-codex --job-id=<approved Job UUID>`. It refuses a running daemon, unresolved recovery state, unknown release/account or a different Job. It does not stop services or alter personal settings. A direct administrator policy inventory occurs just before the approved execution; historical inventory is not continuous MDM monitoring. The Gateway OAuth client refuses refresh in this manual command. Actual vendor account startup may still refresh shared credentials and requires disclosure in the exact launch approval.
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
### Local Fix with disclosed validation
|
|
464
|
+
|
|
465
|
+
The existing daemon can use a reviewed `gateway_publish_v1` installation for
|
|
466
|
+
approved local Codex fixes. It freezes the owned model and sends only sealed,
|
|
467
|
+
bounded changed text files to Gateway's existing publication service. Server
|
|
468
|
+
credentials create the generated branch and canonical development PR; no local
|
|
469
|
+
Git token, ECR, new CI installation or native PHP verifier is required.
|
|
470
|
+
|
|
471
|
+
Validation is explicitly **UNVERIFIED** in this mode, with CI **not run**.
|
|
472
|
+
Executor-reported checks, failures and checks not run stay attributed to the
|
|
473
|
+
executor in the PR, Work Item and Slack result. No new test/shell tool is granted.
|
|
474
|
+
A PR-created result is not a test-pass claim. Existing read-only Claude/Codex
|
|
475
|
+
investigation and the optional strict CI-required mode keep their prior behavior.
|
|
476
|
+
|
|
477
|
+
Only an exact reviewed v2 installation with matching mode/profile/runtime hashes
|
|
478
|
+
advertises `local_fix_gateway_publish_v1`. Publishing/installation/DEV activation
|
|
479
|
+
and an exact approved Fix contract/launch are still required before real use.
|
|
480
|
+
An unresolved canonical PR approval or publication receipt pauses delivery; do
|
|
481
|
+
not rerun the model or delete recovery state to force a PR.
|
|
@@ -453,7 +453,7 @@ export class ExecutionCoordinator {
|
|
|
453
453
|
await append({ eventType: 'checkpoint_ready', summary: 'final checkpoint ready' });
|
|
454
454
|
await publicationStep(() => this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: 'publication_pending', bindingId: binding.bindingId, eventCursor: sequence, workspaceManifest: workspace.manifest, publicationRecovery: { version: 1, job, checkoutPath: workspace.path, bindingId: binding.bindingId, completedEvent: event, gatewayContext: this.deps.gateway.publicationRecoveryContext?.(binding.bindingId), securityReceipt: adapter.securityReceipt?.(), freezeConfirmed: lifecycle.frozen } }));
|
|
455
455
|
publicationPending = true;
|
|
456
|
-
localFixPublication = await publicationStep(() => fixSession.finalize());
|
|
456
|
+
localFixPublication = await publicationStep(() => fixSession.finalize(event));
|
|
457
457
|
if (this.deps.gateway.syncPublicationCursor) {
|
|
458
458
|
try {
|
|
459
459
|
const cursor = await publicationStep(() => this.deps.gateway.syncPublicationCursor(binding.bindingId, sequence));
|
|
@@ -192,11 +192,13 @@ export async function reconcilePreparationState(previous, deps) {
|
|
|
192
192
|
export function localRunnerCapabilities(probes, trustedLocalFixHost = false, verificationMode) {
|
|
193
193
|
return probes.map(probe => {
|
|
194
194
|
const authenticated = probe.available && probe.authenticated;
|
|
195
|
-
const capabilities = probe.capabilities.filter(value => value !== 'local_fix_publication_v1' && value !== 'local_fix_gateway_ci_v1');
|
|
195
|
+
const capabilities = probe.capabilities.filter(value => value !== 'local_fix_publication_v1' && value !== 'local_fix_gateway_ci_v1' && value !== 'local_fix_gateway_publish_v1');
|
|
196
196
|
if (trustedLocalFixHost && authenticated && probe.id === 'codex_cli') {
|
|
197
197
|
capabilities.push('local_fix_publication_v1');
|
|
198
198
|
if (verificationMode === 'gateway_ci_v1')
|
|
199
199
|
capabilities.push('local_fix_gateway_ci_v1');
|
|
200
|
+
if (verificationMode === 'gateway_publish_v1')
|
|
201
|
+
capabilities.push('local_fix_gateway_publish_v1');
|
|
200
202
|
}
|
|
201
203
|
return { adapter: probe.id, version: probe.version, authenticated, capabilities };
|
|
202
204
|
});
|
|
@@ -551,7 +551,7 @@ export class McpGatewayClient {
|
|
|
551
551
|
delete receipt.preparation_timing;
|
|
552
552
|
delete receipt.launch_process;
|
|
553
553
|
}
|
|
554
|
-
Object.assign(body, { files: input.files, security_receipt: receipt, host_receipt: input.hostReceipt });
|
|
554
|
+
Object.assign(body, { files: input.files, security_receipt: receipt, host_receipt: input.hostReceipt, ...(input.validationReport ? { validation_report: input.validationReport } : {}) });
|
|
555
555
|
}
|
|
556
556
|
else
|
|
557
557
|
Object.assign(body, { publication_id: input.publicationId, ...(input.canonicalApprovalId ? { canonical_approval_id: input.canonicalApprovalId } : {}) });
|
|
@@ -49,9 +49,11 @@ export async function loadInstalledLocalFixHost(input) {
|
|
|
49
49
|
const raw = await readPrivateJson(file);
|
|
50
50
|
if (raw.schema === 'local-fix-host-installation-v2') {
|
|
51
51
|
const config = raw;
|
|
52
|
-
if (config.enabled !== true || config.verification_mode
|
|
52
|
+
if (config.enabled !== true || !['gateway_ci_v1', 'gateway_publish_v1'].includes(config.verification_mode) || config.runnerId !== input.runnerId)
|
|
53
53
|
throw denied();
|
|
54
54
|
assertServerCiRegistration(config.registration);
|
|
55
|
+
if (config.verification_mode !== (config.registration.verification_mode ?? 'gateway_ci_v1'))
|
|
56
|
+
throw denied();
|
|
55
57
|
const packageRoot = await realpath(fileURLToPath(new URL('../../../', import.meta.url)));
|
|
56
58
|
const assertReleaseCurrent = () => verifyLocalFixRuntime(packageRoot, config.runtimeFiles, config.registration.receipt);
|
|
57
59
|
await assertReleaseCurrent();
|
|
@@ -32,7 +32,7 @@ export async function recoverLocalFixPublication(input) {
|
|
|
32
32
|
let session;
|
|
33
33
|
try {
|
|
34
34
|
session = await input.host.prepare({ job, checkoutPath: r.checkoutPath, bindingId, lifecycle: { isFrozen: () => true, isCurrent: () => current, securityReceipt: () => r.securityReceipt } });
|
|
35
|
-
const receipt = await session.finalize();
|
|
35
|
+
const receipt = await session.finalize(event);
|
|
36
36
|
if (heartbeatError)
|
|
37
37
|
throw new PublicationOutcomeUnknownError();
|
|
38
38
|
const cursor = await input.gateway.syncLocalPublicationCursor(bindingId, s.eventCursor ?? 0);
|
|
@@ -5,6 +5,7 @@ import { constants } from 'node:fs';
|
|
|
5
5
|
import { lstat, open, realpath } from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { MAX_EVENT_SUMMARY_CHARS } from '../protocol.js';
|
|
8
9
|
import { protocolHash } from './candidate.js';
|
|
9
10
|
import { FilePublicationJournal } from './journal.js';
|
|
10
11
|
import { GitSnapshotHost } from './git-snapshot.js';
|
|
@@ -15,21 +16,59 @@ const matches = (p, s) => s.endsWith('/**') ? safePath(s.slice(0, -3)) && (p ===
|
|
|
15
16
|
const bindingFields = ['binding_id', 'actor_id', 'runner_id', 'job_id', 'generation', 'attempt_id', 'execution_contract_id', 'work_loop_id', 'repository_lease_id', 'repository_key', 'base_commit_sha', 'working_branch', 'contract_fingerprint', 'task_package_fingerprint', 'launch_fingerprint'];
|
|
16
17
|
const bindingProof = (v) => { const b = Object.fromEntries(bindingFields.map(k => [k, v[k]])); if (!Number.isSafeInteger(b.generation) || b.generation < 1 || bindingFields.some(k => k !== 'generation' && (typeof b[k] !== 'string' || !b[k])))
|
|
17
18
|
throw denied(); return b; };
|
|
18
|
-
export function assertServerCiRegistration(r) {
|
|
19
|
-
|
|
19
|
+
export function assertServerCiRegistration(r) {
|
|
20
|
+
const { review, ...manifest } = r;
|
|
21
|
+
if (r.verification_mode !== undefined && !['gateway_ci_v1', 'gateway_publish_v1'].includes(r.verification_mode))
|
|
22
|
+
throw denied();
|
|
23
|
+
const p = r.ci_profile;
|
|
24
|
+
if (r.verification_mode === 'gateway_publish_v1' && (!p || Object.keys(p).some(k => !['review_status', 'validation_policy', 'destination_branch', 'trusted_files'].includes(k)) || p.review_status !== 'reviewed' || p.validation_policy !== 'disclose_unverified' || typeof p.destination_branch !== 'string' || !p.destination_branch))
|
|
25
|
+
throw denied();
|
|
26
|
+
// Both server modes use the Gateway's ordered list of pinned file records.
|
|
27
|
+
// Older CI registrations without a local profile schema remain compatible.
|
|
28
|
+
if (r.verification_mode === 'gateway_publish_v1' || p?.trusted_files !== undefined) {
|
|
29
|
+
const files = p?.trusted_files, seen = new Set();
|
|
30
|
+
if (!Array.isArray(files) || !files.length || files.length > 50)
|
|
31
|
+
throw denied();
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
if (!file || typeof file !== 'object' || Array.isArray(file) || Object.keys(file).some(k => !['path', 'sha256'].includes(k)) || typeof file.path !== 'string' || !safePath(file.path) || !/^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(file.path) || /(^|\/)(vendor|node_modules|secrets)(\/|$)/.test(file.path) || seen.has(file.path) || typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256))
|
|
34
|
+
throw denied();
|
|
35
|
+
seen.add(file.path);
|
|
36
|
+
}
|
|
37
|
+
if (!seen.has('bitbucket-pipelines.yml'))
|
|
38
|
+
throw denied();
|
|
39
|
+
}
|
|
40
|
+
if (!review || review.status !== 'reviewed' || review.fingerprint !== protocolHash(manifest) || !r.project || r.ci_profile_fingerprint !== protocolHash(r.ci_profile) || Object.values(r.receipt).length !== 3 || Object.values(r.receipt).some(v => !/^[a-f0-9]{64}$/.test(v)))
|
|
41
|
+
throw denied();
|
|
42
|
+
}
|
|
43
|
+
/** Executor text is disclosure only; never derive trusted statuses from it. */
|
|
44
|
+
function validationReport(event) {
|
|
45
|
+
if (event === undefined)
|
|
46
|
+
return { summary: 'No executor validation report was provided.', evidence: [] };
|
|
47
|
+
if (event.eventType !== 'completed' || typeof event.summary !== 'string' || event.summary.length > MAX_EVENT_SUMMARY_CHARS || !event.summary.trim())
|
|
48
|
+
throw denied();
|
|
49
|
+
const evidence = event.payload?.completion_evidence === undefined ? [] : event.payload.completion_evidence;
|
|
50
|
+
if (!Array.isArray(evidence) || evidence.length > 20 || evidence.some(v => typeof v !== 'string' || v.length > 500 || !v.trim()))
|
|
51
|
+
throw denied();
|
|
52
|
+
const clean = (v) => v.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '').trim();
|
|
53
|
+
const summary = clean(event.summary), items = evidence.map(clean);
|
|
54
|
+
if (!summary || items.some(v => !v))
|
|
55
|
+
throw denied();
|
|
56
|
+
return { summary, evidence: items };
|
|
57
|
+
}
|
|
20
58
|
/** Only host-owned frozen bytes cross the Gateway boundary. Git is exclusively
|
|
21
59
|
* a pinned credential-free object reader; verification/publication run remotely. */
|
|
22
60
|
export function createServerCiLocalFixHost(options) {
|
|
23
61
|
const registration = structuredClone(options.registration);
|
|
24
62
|
assertServerCiRegistration(registration);
|
|
25
|
-
|
|
63
|
+
const mode = registration.verification_mode ?? 'gateway_ci_v1';
|
|
64
|
+
return { requiresOwnedFreeze: true, verificationMode: mode, prepare: async (input) => {
|
|
26
65
|
await options.assertReleaseCurrent?.();
|
|
27
66
|
const lifecycle = input.lifecycle;
|
|
28
67
|
if (!lifecycle)
|
|
29
68
|
throw denied();
|
|
30
69
|
const { job } = input, pkg = job.taskPackage, scope = pkg.local_fix_scope, repo = pkg.repository, limits = scope?.candidate_limits, allowed = scope?.allowed_paths;
|
|
31
70
|
const root = await realpath(options.privateRoot), st = await lstat(root);
|
|
32
|
-
if (!lifecycle.isCurrent() || pkg.runtime_adapter !== 'codex_cli' || !scope || scope.verification_mode !==
|
|
71
|
+
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) || !limits || ['max_changed_files', 'max_diff_bytes', 'max_file_bytes'].some(k => !Number.isSafeInteger(limits[k]) || limits[k] < 1) || 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))
|
|
33
72
|
throw denied();
|
|
34
73
|
const fence = { jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, launchGrant: job.launchGrant, bindingId: input.bindingId };
|
|
35
74
|
const binding = bindingProof((await options.gateway.localPublicationContext(fence)).context);
|
|
@@ -39,8 +78,9 @@ export function createServerCiLocalFixHost(options) {
|
|
|
39
78
|
const current = async () => { if (closed || !lifecycle.isCurrent() || !lifecycle.isFrozen())
|
|
40
79
|
throw denied(); await options.assertReleaseCurrent?.(); if (protocolHash(bindingProof((await options.gateway.localPublicationContext(fence)).context)) !== protocolHash(binding) || closed || !lifecycle.isCurrent() || !lifecycle.isFrozen())
|
|
41
80
|
throw denied(); await seal?.assertUnchanged(); };
|
|
42
|
-
return { close: async () => { closed = true; }, finalize() {
|
|
81
|
+
return { close: async () => { closed = true; }, finalize(completedEvent) {
|
|
43
82
|
return finalization ??= (async () => {
|
|
83
|
+
const report = mode === 'gateway_publish_v1' ? validationReport(completedEvent) : undefined;
|
|
44
84
|
await current();
|
|
45
85
|
const host = new GitSnapshotHost({ root: input.checkoutPath, privateRoot: root, git: options.git, baseCommitSha: binding.base_commit_sha, workingBranch: binding.working_branch, maxFileBytes: limits.max_file_bytes, maxTotalBytes: 128 * 1024 * 1024 });
|
|
46
86
|
const observed = await host.observe();
|
|
@@ -99,7 +139,7 @@ export function createServerCiLocalFixHost(options) {
|
|
|
99
139
|
await fd.close();
|
|
100
140
|
}
|
|
101
141
|
}
|
|
102
|
-
const journal = new FilePublicationJournal(root), key = protocolHash({ mode
|
|
142
|
+
const journal = new FilePublicationJournal(root), key = protocolHash({ mode, binding }), deltaFingerprint = protocolHash({ binding, files, profile: registration.ci_profile_fingerprint, ...(report ? { validation_report: report } : {}) });
|
|
103
143
|
return journal.exclusive(key, async () => {
|
|
104
144
|
const previous = await journal.load(key);
|
|
105
145
|
if (previous && previous.candidate_fingerprint !== deltaFingerprint)
|
|
@@ -114,7 +154,7 @@ export function createServerCiLocalFixHost(options) {
|
|
|
114
154
|
let result;
|
|
115
155
|
stageStarted = true;
|
|
116
156
|
try {
|
|
117
|
-
result = (await options.gateway.executeServerCiPublication({ ...fence, operation: 'stage', files, securityReceipt: { ...security }, hostReceipt: { ...registration.receipt } })).result;
|
|
157
|
+
result = (await options.gateway.executeServerCiPublication({ ...fence, operation: 'stage', files, ...(report ? { validationReport: report } : {}), securityReceipt: { ...security }, hostReceipt: { ...registration.receipt } })).result;
|
|
118
158
|
}
|
|
119
159
|
catch {
|
|
120
160
|
throw new PublicationOutcomeUnknownError();
|
|
@@ -135,7 +175,7 @@ export function createServerCiLocalFixHost(options) {
|
|
|
135
175
|
if (result.status === 'completed') {
|
|
136
176
|
await current();
|
|
137
177
|
const ci = result.ci;
|
|
138
|
-
if (result.verification_mode !== 'gateway_ci_v1'
|
|
178
|
+
if (result.verification_mode !== mode || (mode === 'gateway_ci_v1' ? ci?.status !== 'passed' || ci.commit_sha !== result.commit_sha : ci?.status !== 'not_run' || result.verification?.status !== 'unverified') || typeof result.commit_sha !== 'string' || !/^[a-f0-9]{40}$/.test(result.commit_sha) || result.working_branch !== binding.working_branch)
|
|
139
179
|
throw new PublicationOutcomeUnknownError();
|
|
140
180
|
return result;
|
|
141
181
|
}
|