mtech-ai-local-runner 0.1.33 → 0.1.35
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 +34 -0
- package/dist/src/adapters/claude-code/adapter.js +7 -1
- package/dist/src/adapters/claude-code/stream-parser.js +22 -12
- package/dist/src/adapters/codex/prompt-compiler.js +24 -1
- package/dist/src/daemon/execution-coordinator.js +62 -3
- package/dist/src/daemon/runtime.js +1 -0
- package/dist/src/gateway-client.js +7 -2
- package/dist/src/publication/candidate.js +3 -2
- package/dist/src/publication/git-snapshot.js +87 -6
- package/dist/src/publication/git-transport.js +4 -30
- package/dist/src/publication/isolated-verifier.js +20 -16
- package/dist/src/publication/project-registration.js +1 -1
- package/dist/src/publication/raw-git-objects.js +42 -0
- package/dist/src/safety/codex-exec-production-host.js +13 -25
- package/dist/src/safety/job-tool-policy.js +9 -0
- package/dist/src/safety/local-tool-policy.js +17 -3
- package/dist/src/safety/pinned-git-head.js +113 -0
- package/dist/src/safety/subprocess-sandbox.js +72 -38
- package/dist/src/verification/p1-execution-host.js +78 -0
- package/dist/src/verification/runner-observed-verifier.js +147 -0
- package/dist/src/workspace/workspace-manager.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
## 0.1.35: exact approved source paths in Codex instructions
|
|
2
|
+
|
|
3
|
+
Codex receives the approved checkout-relative paths, revision, access mode and
|
|
4
|
+
blocked-path list from its binding bundle. A denial for an unlisted path does
|
|
5
|
+
not imply that another approved file is unavailable. Scope descriptions exclude
|
|
6
|
+
repository transport coordinates and write leases. Unregistered commands remain
|
|
7
|
+
unavailable, and missing validation stays not_run/unverified.
|
|
8
|
+
|
|
9
|
+
This patch does not expand source or tool permissions, change the Claude path,
|
|
10
|
+
or authorize a new execution. Package integrity, runtime pins and a separately
|
|
11
|
+
approved exact contract/launch remain required for the next Fix attempt.
|
|
12
|
+
|
|
13
|
+
## 0.1.34: pinned writable Fix HEAD preparation
|
|
14
|
+
|
|
15
|
+
Accept a linked worktree's symbolic HEAD only for the exact approved writable
|
|
16
|
+
Fix branch and commit. Detached read-only checkouts retain their existing policy.
|
|
17
|
+
Bounded, no-follow metadata reads reject ref chains, traversal, symlinks and
|
|
18
|
+
post-preparation path changes. Preparation errors retain a safe substage code.
|
|
19
|
+
This patch does not grant another execution: package/runtime pins, installation
|
|
20
|
+
review and exact Gateway contract/launch approval remain required. Model work
|
|
21
|
+
limits, personal login/settings and the Claude execution path are unchanged.
|
|
22
|
+
|
|
23
|
+
## Unreleased: P1 runner-observed evidence (disabled)
|
|
24
|
+
|
|
25
|
+
P1 adds candidate-bound command receipts and changed-path evidence for explicitly
|
|
26
|
+
reviewed Codex Node/file-only profiles. It verifies a private exact-base candidate
|
|
27
|
+
after the owned model stops; model test claims cannot satisfy the Loop criteria.
|
|
28
|
+
P1 defaults off, the Gateway profile registry is empty, and the runtime P1 host
|
|
29
|
+
is absent by default. Source package `0.1.33` is inherited from upstream; this is
|
|
30
|
+
not a P1 package release or live acceptance. See the [P1 implementation and
|
|
31
|
+
delivery record](../../docs/superpowers/plans/2026-10-17-local-runner-p1-runner-observed-evidence.md)
|
|
32
|
+
for local verification results and separate deployment, runner-update and
|
|
33
|
+
activation gates.
|
|
34
|
+
|
|
1
35
|
## Unreleased: P0 terminal safety baseline (local verification only)
|
|
2
36
|
|
|
3
37
|
The upstream source package is `0.1.31`; P0 does not bump or publish a version.
|
|
@@ -3,6 +3,7 @@ import { requireSecurityRequirements } from '../../safety/security-requirements.
|
|
|
3
3
|
import { createJobSecurityReceipt } from '../../safety/job-security-receipt.js';
|
|
4
4
|
import { isTerminalEventType } from "../../protocol.js";
|
|
5
5
|
import { parseClaudeLine } from "./stream-parser.js";
|
|
6
|
+
import { p1ModelEvent } from '../../verification/p1-execution-host.js';
|
|
6
7
|
import { compilePrompt } from "./prompt-compiler.js";
|
|
7
8
|
import { completionEvent } from "./completion-report.js";
|
|
8
9
|
import { requireJobSecurity } from "../../safety/job-security-policy.js";
|
|
@@ -44,6 +45,7 @@ export class ClaudeCodeAdapter {
|
|
|
44
45
|
tools;
|
|
45
46
|
cancelled = false;
|
|
46
47
|
started = false;
|
|
48
|
+
p1 = false;
|
|
47
49
|
startupAcquisition;
|
|
48
50
|
revokedTools;
|
|
49
51
|
revokeFailed = false;
|
|
@@ -71,6 +73,7 @@ export class ClaudeCodeAdapter {
|
|
|
71
73
|
if (this.started)
|
|
72
74
|
throw new Error("adapter has already been started");
|
|
73
75
|
this.started = true;
|
|
76
|
+
this.p1 = ctx.approvedTaskPackage?.verification?.mode === 'runner_observed_v1';
|
|
74
77
|
const generation = ctx.generation;
|
|
75
78
|
if (!Number.isSafeInteger(generation) || generation === undefined || generation < 1)
|
|
76
79
|
throw new Error("JOB_SECURITY_REJECTED");
|
|
@@ -207,11 +210,14 @@ export class ClaudeCodeAdapter {
|
|
|
207
210
|
// A nested agent message is not evidence that the executor has finished.
|
|
208
211
|
if (message.parent_tool_use_id != null)
|
|
209
212
|
continue;
|
|
210
|
-
let parsed = parseClaudeLine(line, this.options.worktreeRoot);
|
|
213
|
+
let parsed = parseClaudeLine(line, this.options.worktreeRoot, { p1: this.p1 });
|
|
211
214
|
if (!parsed)
|
|
212
215
|
continue;
|
|
213
216
|
if (parsed.eventType === "completed") {
|
|
217
|
+
const observedUsage = parsed.payload?.usage;
|
|
214
218
|
parsed = completionEvent(message.result, parsed.payload?.cost_usd);
|
|
219
|
+
if (this.p1)
|
|
220
|
+
parsed = p1ModelEvent({ ...parsed, payload: { ...parsed.payload, usage: observedUsage } });
|
|
215
221
|
}
|
|
216
222
|
if (isTerminalEventType(parsed.eventType))
|
|
217
223
|
sawTerminal = true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
2
|
import { MAX_EVENT_SUMMARY_CHARS } from "../../protocol.js";
|
|
3
|
+
import { p1ModelEvent } from '../../verification/p1-execution-host.js';
|
|
3
4
|
const EDIT_TOOL_NAMES = new Set(["Edit", "Write", "MultiEdit"]);
|
|
4
5
|
function bounded(summary) {
|
|
5
6
|
return summary.length > MAX_EVENT_SUMMARY_CHARS ? summary.slice(0, MAX_EVENT_SUMMARY_CHARS) : summary;
|
|
@@ -105,7 +106,7 @@ function parseResultMessage(message) {
|
|
|
105
106
|
const summary = subtype === "error_max_budget_exceeded" ? "execution failed: budget exceeded" : "execution failed";
|
|
106
107
|
return { eventType: "failed", summary, payload: costUsd !== undefined ? { cost_usd: costUsd } : undefined };
|
|
107
108
|
}
|
|
108
|
-
export function parseClaudeLine(line, worktreeRoot) {
|
|
109
|
+
export function parseClaudeLine(line, worktreeRoot, options = {}) {
|
|
109
110
|
let message;
|
|
110
111
|
try {
|
|
111
112
|
message = JSON.parse(line);
|
|
@@ -113,16 +114,25 @@ export function parseClaudeLine(line, worktreeRoot) {
|
|
|
113
114
|
catch {
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
const parsed = (() => {
|
|
118
|
+
switch (message.type) {
|
|
119
|
+
case "system":
|
|
120
|
+
return parseSystemMessage(message);
|
|
121
|
+
case "assistant":
|
|
122
|
+
return parseAssistantMessage(message.message ?? {}, worktreeRoot);
|
|
123
|
+
case "user":
|
|
124
|
+
return parseUserMessage(message.message ?? {});
|
|
125
|
+
case "result":
|
|
126
|
+
return parseResultMessage(message);
|
|
127
|
+
default:
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
if (!options.p1 || !parsed)
|
|
132
|
+
return parsed;
|
|
133
|
+
if (message.type === 'result') {
|
|
134
|
+
const usage = message.usage && typeof message.usage === 'object' ? message.usage : {};
|
|
135
|
+
parsed.payload = { ...parsed.payload, usage: { provider: 'anthropic', source: 'claude_code_cli.result', input_tokens: usage.input_tokens, cached_input_tokens: usage.cache_read_input_tokens, output_tokens: usage.output_tokens, duration_ms: message.duration_ms } };
|
|
127
136
|
}
|
|
137
|
+
return p1ModelEvent(parsed);
|
|
128
138
|
}
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { compilePrompt as compileBrief } from "../prompt-compiler.js";
|
|
2
|
+
function repositoryScope(taskPackage) {
|
|
3
|
+
const repositories = taskPackage.repositories;
|
|
4
|
+
const repo = Array.isArray(repositories) && repositories.length === 1
|
|
5
|
+
&& repositories[0] && typeof repositories[0] === "object" && !Array.isArray(repositories[0])
|
|
6
|
+
? repositories[0] : undefined;
|
|
7
|
+
const paths = (value) => Array.isArray(value)
|
|
8
|
+
&& value.every(item => typeof item === "string" && item.length > 0);
|
|
9
|
+
if (!repo || !paths(repo.allowed_paths) || !repo.allowed_paths.length || !paths(repo.blocked_paths)
|
|
10
|
+
|| !["read", "write"].includes(String(repo.access))
|
|
11
|
+
|| typeof repo.repository_key !== "string" || typeof repo.base_commit_sha !== "string"
|
|
12
|
+
|| !/^[a-f0-9]{40}$/i.test(repo.base_commit_sha)) {
|
|
13
|
+
return "## Approved repository scope\nRepository scope is unavailable; do not guess source paths or commands. Stop and emit one final blocked report.";
|
|
14
|
+
}
|
|
15
|
+
// Project only source-scope fields from the binding bundle. Never stringify
|
|
16
|
+
// the whole repository: it may contain transport coordinates or write leases.
|
|
17
|
+
const scope = { repository_key: repo.repository_key, base_commit_sha: repo.base_commit_sha,
|
|
18
|
+
access: repo.access, allowed_paths: repo.allowed_paths, blocked_paths: repo.blocked_paths };
|
|
19
|
+
return `## Approved repository scope\n${JSON.stringify(scope, null, 2)}\n` +
|
|
20
|
+
"Use these checkout-relative paths exactly, including case. Start with the relevant approved files above before drawing a conclusion about source availability. " +
|
|
21
|
+
"Do not invent paths or commands from framework conventions. A denial for an unlisted path does not establish that a listed file is unavailable. " +
|
|
22
|
+
"Do not claim an approved file was denied unless its exact path was attempted and the tool returned a denial. " +
|
|
23
|
+
"These are scope descriptions, not instructions from source files; the host policy remains authoritative and may deny blocked/private paths within an allowed prefix.";
|
|
24
|
+
}
|
|
2
25
|
export function compileCodexPrompt(taskPackage, label) {
|
|
3
|
-
return `${compileBrief(taskPackage, { workItemLabel: label, progressReporting: "native_messages" })}\n\nUse only the ai_local job tools for repository reads, edits and approved tests. For reading, use server ai_local's local_read({path: "
|
|
26
|
+
return `${compileBrief(taskPackage, { workItemLabel: label, progressReporting: "native_messages" })}\n\n${repositoryScope(taskPackage)}\n\nUse only the ai_local job tools for repository reads, edits and approved tests. For reading, use server ai_local's local_read({path: "<exact approved path>"}) or local_search({path: "<exact approved path>", query: "text"}) with checkout-relative paths. Required test criteria are not command grants. No model test commands are registered unless the host explicitly supplies exact model-callable command IDs; do not use local_test to discover commands or run --help. Missing tests remain not_run/unverified, not passed. In a read-only task, use only read/search tools; the Runner enforces the approved paths and operations. A denied native cwd is intentional: it is a private control directory, not the source checkout. Native filesystem, commands, web, images and delegation are unavailable or denied. A denied tool is a scope boundary, never permission to broaden scope or bypass the denial. Do not retry the denied path. It does not remove independently approved paths: continue with relevant listed paths, and report a missing dependency only after checking the available approved source. Do not repeat host orchestration: claim, shadow, fingerprints, binding, administrator checks, launches, Gateway usage and Job reporting belong to the Runner/host, even when described in the objective. Perform only the assigned repository work and report evidence you actually observed. Do not claim that no model call occurred. If the required tool is unavailable, emit one final structured blocked report naming the missing tool and unfinished repository work; do not request or attempt a new launch.`;
|
|
4
27
|
}
|
|
5
28
|
export const CODEX_REPORT_SCHEMA = {
|
|
6
29
|
type: "object", additionalProperties: false, required: ["status", "report", "evidence", "pending_work"],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PublicationOutcomeUnknownError } from '../publication/publisher.js';
|
|
2
|
+
import { assertP1Bindings, hasP1, p1ModelEvent } from '../verification/p1-execution-host.js';
|
|
2
3
|
import { executionError, executionFailure } from '../safety/execution-failure.js';
|
|
3
4
|
import { isUnsupportedSupervisorCommand } from '../safety/unsupported-supervisor-command.js';
|
|
4
5
|
import { requireSecurityRequirements } from '../safety/security-requirements.js';
|
|
@@ -71,6 +72,9 @@ export class ExecutionCoordinator {
|
|
|
71
72
|
let fixSession;
|
|
72
73
|
let localFixPublication;
|
|
73
74
|
const isLocalFix = job.taskPackage.local_fix_scope !== undefined;
|
|
75
|
+
const isP1 = hasP1(job.taskPackage);
|
|
76
|
+
let p1Result;
|
|
77
|
+
const p1ReceiptIds = [];
|
|
74
78
|
const lifecycle = { frozen: false, current: true };
|
|
75
79
|
const deadline = Date.now() + (this.deps.preparationTimeoutMs ?? 900_000);
|
|
76
80
|
let leaseExpiresAt = job.leaseExpiresAt;
|
|
@@ -80,6 +84,12 @@ export class ExecutionCoordinator {
|
|
|
80
84
|
try {
|
|
81
85
|
await preparation.start();
|
|
82
86
|
const pkg = job.taskPackage;
|
|
87
|
+
if (isP1) {
|
|
88
|
+
assertP1Bindings(pkg);
|
|
89
|
+
if (!this.deps.p1Host)
|
|
90
|
+
throw Error('P1_HOST_REQUIRED');
|
|
91
|
+
this.deps.p1Host.assertEligible(job);
|
|
92
|
+
}
|
|
83
93
|
if (isLocalFix && (!this.deps.localFixHost || pkg.repository?.access !== "write"))
|
|
84
94
|
throw new LocalFixHostRequired();
|
|
85
95
|
const repository = pkg.repository;
|
|
@@ -96,7 +106,8 @@ export class ExecutionCoordinator {
|
|
|
96
106
|
repositoryKey: text(repository?.key, "repository key"),
|
|
97
107
|
providerUrl: text(repository?.provider_url, "repository provider URL"),
|
|
98
108
|
baseCommitSha: text(repository?.base_commit_sha, "repository SHA"),
|
|
99
|
-
...(isLocalFix ? { workingBranch: text(repository?.working_branch, "approved fix branch") } : {})
|
|
109
|
+
...(isLocalFix || isP1 ? { workingBranch: text(repository?.working_branch, "approved fix branch") } : {}),
|
|
110
|
+
...(isP1 ? { p1WorkingBranch: text(repository?.working_branch, 'approved P1 branch') } : {})
|
|
100
111
|
}));
|
|
101
112
|
stage = "adapter";
|
|
102
113
|
adapter = await preparation.step(() => this.deps.registry.select(adapterId, adapterVersion, { worktreePath: workspace.path, jobId: job.id }), late => late.cancel());
|
|
@@ -107,11 +118,17 @@ export class ExecutionCoordinator {
|
|
|
107
118
|
if (contractFingerprint(gatewayShadow.contractPayload) !== expectedContractFingerprint || gatewayShadow.contractFingerprint !== expectedContractFingerprint || gatewayShadow.taskPackageFingerprint !== fingerprint(pkg)) {
|
|
108
119
|
throw new GatewayToolError("contract or task-package fingerprint mismatch before binding", 'work_execution_contract_show', 'JOB_SHADOW_FENCE_INVALID');
|
|
109
120
|
}
|
|
121
|
+
if (isP1)
|
|
122
|
+
assertP1Bindings(pkg, gatewayShadow.contractPayload);
|
|
123
|
+
else if (hasP1(gatewayShadow.contractPayload))
|
|
124
|
+
throw Error('P1_CONTRACT_REJECTED');
|
|
110
125
|
stage = "binding";
|
|
111
126
|
await step(() => this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "binding" }));
|
|
112
127
|
preparation.beginBinding();
|
|
113
128
|
bindingAttempted = true;
|
|
114
129
|
binding = await step(() => this.deps.gateway.createBinding({ ...job, checkoutPath: workspace.path }, preparation.signal));
|
|
130
|
+
if (isP1)
|
|
131
|
+
assertP1Bindings(pkg, binding.taskPackage, true);
|
|
115
132
|
await step(() => this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "running", bindingId: binding.bindingId, workspaceManifest: workspace.manifest }));
|
|
116
133
|
await preparation.stop();
|
|
117
134
|
preparation.assert();
|
|
@@ -164,6 +181,7 @@ export class ExecutionCoordinator {
|
|
|
164
181
|
const executor = adapter;
|
|
165
182
|
let heartbeatFailure;
|
|
166
183
|
const evidenceAbort = new AbortController();
|
|
184
|
+
const verificationAbort = new AbortController();
|
|
167
185
|
const controlAbort = new AbortController();
|
|
168
186
|
let leaseBoundary;
|
|
169
187
|
let rejectFence;
|
|
@@ -202,6 +220,7 @@ export class ExecutionCoordinator {
|
|
|
202
220
|
heartbeatFailure = executionError(error, 'heartbeat', 'EXECUTION_FAILED');
|
|
203
221
|
controlAbort.abort();
|
|
204
222
|
evidenceAbort.abort();
|
|
223
|
+
verificationAbort.abort();
|
|
205
224
|
rejectFence(heartbeatFailure);
|
|
206
225
|
// Cancellation cannot queue behind transport or generator cleanup.
|
|
207
226
|
void stopExecutor().catch(() => { });
|
|
@@ -239,6 +258,8 @@ export class ExecutionCoordinator {
|
|
|
239
258
|
let commandTerminal;
|
|
240
259
|
if (["cancel", "pause", "request_replan"].includes(command.command)) {
|
|
241
260
|
lifecycle.current = false;
|
|
261
|
+
if (isP1)
|
|
262
|
+
verificationAbort.abort();
|
|
242
263
|
commandTerminal = { eventType: "cancelled", summary: `supervisor ${command.command} acknowledged`,
|
|
243
264
|
payload: { reason: command.command === 'pause' ? 'paused' : command.command === 'request_replan' ? 'replan_requested' : 'cancelled' } };
|
|
244
265
|
await stopExecutor();
|
|
@@ -253,6 +274,8 @@ export class ExecutionCoordinator {
|
|
|
253
274
|
if (!isUnsupportedSupervisorCommand(error))
|
|
254
275
|
throw error;
|
|
255
276
|
lifecycle.current = false;
|
|
277
|
+
if (isP1)
|
|
278
|
+
verificationAbort.abort();
|
|
256
279
|
await stopExecutor();
|
|
257
280
|
requireControlFence();
|
|
258
281
|
commandTerminal = { eventType: 'failed', summary: 'supervisor command is unsupported for the approved launch',
|
|
@@ -329,7 +352,9 @@ export class ExecutionCoordinator {
|
|
|
329
352
|
if (!ignoreControlFailure && (controlFailure || heartbeatFailure))
|
|
330
353
|
throw controlFailure || heartbeatFailure;
|
|
331
354
|
};
|
|
332
|
-
const append = async (event) => {
|
|
355
|
+
const append = async (event, hostProof = false) => {
|
|
356
|
+
if (isP1 && !hostProof)
|
|
357
|
+
event = p1ModelEvent(event);
|
|
333
358
|
const previousStage = executionStage;
|
|
334
359
|
let payload = event.payload;
|
|
335
360
|
if (isTerminalEventType(event.eventType)) {
|
|
@@ -338,6 +363,8 @@ export class ExecutionCoordinator {
|
|
|
338
363
|
const evidence = localFixPublication && typeof localFixPublication.diff_hash === 'string' && typeof localFixPublication.changed_file_manifest_hash === 'string' ? { diffHash: localFixPublication.diff_hash, changedFileManifestHash: localFixPublication.changed_file_manifest_hash } : await fenced(this.deps.terminalEvidenceCollector.collect({ checkoutPath: workspace.path, baseSha: workspace.baseCommitSha, signal: evidenceAbort.signal }));
|
|
339
364
|
requireControlFence();
|
|
340
365
|
payload = { ...payload, diff_hash: evidence.diffHash, changed_file_manifest_hash: evidence.changedFileManifestHash };
|
|
366
|
+
if (isP1 && event.eventType === 'completed' && p1Result)
|
|
367
|
+
payload = { ...payload, candidate_commit_sha: p1Result.candidate_commit_sha, snapshot_fingerprint: p1Result.snapshot_fingerprint, changed_paths: p1Result.changed_paths, verification_receipt_ids: [...p1ReceiptIds] };
|
|
341
368
|
}
|
|
342
369
|
// A vendor report cannot supply host observations, even when its shape looks valid.
|
|
343
370
|
if (payload) {
|
|
@@ -373,8 +400,11 @@ export class ExecutionCoordinator {
|
|
|
373
400
|
}
|
|
374
401
|
requireControlFence();
|
|
375
402
|
eventUncertain = true;
|
|
376
|
-
await fenced(this.deps.gateway.appendEvent(binding.bindingId, normalized));
|
|
403
|
+
const receipt = await fenced(this.deps.gateway.appendEvent(binding.bindingId, normalized));
|
|
377
404
|
requireControlFence();
|
|
405
|
+
const eventId = receipt?.id;
|
|
406
|
+
if (isP1 && hostProof && (typeof eventId !== 'string' || !eventId.trim() || p1ReceiptIds.includes(eventId)))
|
|
407
|
+
throw Error('P1_EVENT_ID_UNCONFIRMED');
|
|
378
408
|
sequence = normalized.sequence;
|
|
379
409
|
executionStage = 'state_save';
|
|
380
410
|
if (event.eventType === "checkpoint_ready")
|
|
@@ -382,6 +412,7 @@ export class ExecutionCoordinator {
|
|
|
382
412
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "running", bindingId: binding.bindingId, eventCursor: sequence, ...(isTerminalEventType(event.eventType) ? { terminalEventType: event.eventType } : {}), workspaceManifest: workspace.manifest });
|
|
383
413
|
eventUncertain = false;
|
|
384
414
|
executionStage = previousStage;
|
|
415
|
+
return typeof eventId === 'string' ? eventId : undefined;
|
|
385
416
|
};
|
|
386
417
|
const publicationStep = async (operation) => {
|
|
387
418
|
requireControlFence();
|
|
@@ -434,6 +465,33 @@ export class ExecutionCoordinator {
|
|
|
434
465
|
if (requestedTerminal)
|
|
435
466
|
break;
|
|
436
467
|
if (isTerminalEventType(event.eventType)) {
|
|
468
|
+
if (event.eventType === 'completed' && isP1) {
|
|
469
|
+
if (!adapter.freezeForPublication)
|
|
470
|
+
throw Error('P1_OWNED_FREEZE_UNAVAILABLE');
|
|
471
|
+
const current = async () => { requireControlFence(); return lifecycle.current && !requestedTerminal && !verificationAbort.signal.aborted; };
|
|
472
|
+
const assertModelFrozen = async () => { if (!await current() || !lifecycle.frozen)
|
|
473
|
+
throw Error('P1_MODEL_NOT_FROZEN'); };
|
|
474
|
+
const freezeOwnedModel = async () => { if (!await current())
|
|
475
|
+
throw Error('P1_FENCE_LOST'); if (!lifecycle.frozen) {
|
|
476
|
+
await publicationStep(() => adapter.freezeForPublication());
|
|
477
|
+
lifecycle.frozen = true;
|
|
478
|
+
} await assertModelFrozen(); };
|
|
479
|
+
await freezeOwnedModel();
|
|
480
|
+
await readControls();
|
|
481
|
+
p1Result = await publicationStep(() => this.deps.p1Host.verify({ job, checkoutPath: workspace.path, bindingId: binding.bindingId, signal: verificationAbort.signal, current, freezeOwnedModel, assertModelFrozen }));
|
|
482
|
+
await readControls();
|
|
483
|
+
await assertModelFrozen();
|
|
484
|
+
const seen = new Set();
|
|
485
|
+
for (const receipt of p1Result.receipts) {
|
|
486
|
+
if (seen.has(receipt.receipt_id))
|
|
487
|
+
throw Error('P1_DUPLICATE_RECEIPT');
|
|
488
|
+
seen.add(receipt.receipt_id);
|
|
489
|
+
await readControls();
|
|
490
|
+
await assertModelFrozen();
|
|
491
|
+
const id = await publicationStep(() => append({ eventType: 'test_result', summary: 'runner command observed', payload: { observer: 'runner', runner_receipt: receipt } }, true));
|
|
492
|
+
p1ReceiptIds.push(id);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
437
495
|
if (event.eventType === "completed" && fixSession) {
|
|
438
496
|
// Stop only this owned model before running verified host publication.
|
|
439
497
|
// Controls/heartbeat remain active throughout the publication phase.
|
|
@@ -547,6 +605,7 @@ export class ExecutionCoordinator {
|
|
|
547
605
|
if (heartbeatTimer)
|
|
548
606
|
clearInterval(heartbeatTimer);
|
|
549
607
|
lifecycle.current = false;
|
|
608
|
+
verificationAbort.abort();
|
|
550
609
|
if (leaseBoundary)
|
|
551
610
|
clearTimeout(leaseBoundary);
|
|
552
611
|
await fixSession?.close();
|
|
@@ -58,6 +58,7 @@ export async function buildDaemon(input) {
|
|
|
58
58
|
const state = new FileDaemonStateStore(input.stateRoot);
|
|
59
59
|
const coordinator = new ExecutionCoordinator({
|
|
60
60
|
localFixHost,
|
|
61
|
+
p1Host: input.p1Host,
|
|
61
62
|
reportExecutionFailure: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "execution_failed", ...diagnostic })}\n`); },
|
|
62
63
|
reportPreparationFailure: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "preparation_failed", ...diagnostic })}\n`); },
|
|
63
64
|
terminalEvidenceCollector: buildTerminalEvidenceCollector(),
|
|
@@ -32,15 +32,17 @@ function epochToIso(value) {
|
|
|
32
32
|
function compactRecord(value) {
|
|
33
33
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && (!Array.isArray(item) || item.length > 0)));
|
|
34
34
|
}
|
|
35
|
-
function shadowTaskPackage(contract) {
|
|
35
|
+
export function shadowTaskPackage(contract) {
|
|
36
36
|
const payload = contract.contract_payload;
|
|
37
37
|
const execution = nested(payload, "agent_execution");
|
|
38
38
|
const brief = nested(execution, "implementation_brief");
|
|
39
|
+
const verification = nested(payload, 'verification.mode') === 'runner_observed_v1' ? payload.verification : undefined;
|
|
39
40
|
const repositories = (payload.repositories ?? []).map((repository) => ({
|
|
40
41
|
repository_key: requiredString(repository.repository_key, "repository key"),
|
|
41
42
|
access: repository.access === "write" ? "write" : "read",
|
|
42
43
|
responsibility: repository.responsibility === "primary" ? "primary" : "supporting",
|
|
43
44
|
base_commit_sha: requiredString(repository.base_commit_sha, "base commit SHA"),
|
|
45
|
+
...(verification ? { allowed_paths: repository.allowed_paths ?? [], blocked_paths: repository.blocked_paths ?? [], working_branch: repository.working_branch ?? null } : {}),
|
|
44
46
|
}));
|
|
45
47
|
const implementationBrief = compactRecord({
|
|
46
48
|
design_artifact_id: brief.design_artifact_id,
|
|
@@ -55,6 +57,7 @@ function shadowTaskPackage(contract) {
|
|
|
55
57
|
});
|
|
56
58
|
return {
|
|
57
59
|
bundle_version: "agent-execution-v1",
|
|
60
|
+
...(verification ? { verification, candidate_limits: nested(payload, 'policy.candidate_limits') ?? {} } : {}),
|
|
58
61
|
contract: compactRecord({
|
|
59
62
|
id: requiredString(contract.id, "execution contract ID"),
|
|
60
63
|
status: requiredString(contract.status, "execution contract status"),
|
|
@@ -354,6 +357,7 @@ export class McpGatewayClient {
|
|
|
354
357
|
const fingerprint = requiredString(nested(context.bundle, "contract.content_fingerprint"), "contract fingerprint");
|
|
355
358
|
const result = await this.callTool("agent_runtime_event_append", {
|
|
356
359
|
binding_id: bindingId,
|
|
360
|
+
...(nested(context.bundle, 'contract.verification.mode') === 'runner_observed_v1' || nested(context.bundle, 'verification.mode') === 'runner_observed_v1' ? { job_fence: context.jobFence } : {}),
|
|
357
361
|
...(isTerminalEventType(event.eventType) ? { lease_token: context.leaseToken } : {}),
|
|
358
362
|
event: {
|
|
359
363
|
protocol_version: event.protocolVersion,
|
|
@@ -368,7 +372,7 @@ export class McpGatewayClient {
|
|
|
368
372
|
contract_fingerprint: fingerprint,
|
|
369
373
|
},
|
|
370
374
|
});
|
|
371
|
-
return { sequence: result.event.sequence };
|
|
375
|
+
return { sequence: result.event.sequence, ...(typeof result.event.id === 'string' ? { id: result.event.id } : {}) };
|
|
372
376
|
}
|
|
373
377
|
async readBindingEvents(bindingId, afterSequence) {
|
|
374
378
|
const page = await this.callTool("agent_runtime_events_read", {
|
|
@@ -481,6 +485,7 @@ export class McpGatewayClient {
|
|
|
481
485
|
const iterationId = requiredString(result.binding.iteration_id, "iteration ID");
|
|
482
486
|
signal?.throwIfAborted();
|
|
483
487
|
this.context = {
|
|
488
|
+
jobFence: { job_id: input.jobId, generation: input.generation, lease_token: input.jobLeaseToken },
|
|
484
489
|
workLoopId: requiredString(result.work_loop_id, "Work Loop ID"),
|
|
485
490
|
leaseToken: requiredString(result.lease_token, "Work Loop lease token"),
|
|
486
491
|
leaseExpiresAt: epochToIso(result.binding.lease_expires_at),
|
|
@@ -2,10 +2,11 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { lstat, realpath } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { measureCommandFile } from '../safety/registered-command-integrity.js';
|
|
5
|
-
export function
|
|
5
|
+
export function protocolJson(value) {
|
|
6
6
|
const canonical = (v) => Array.isArray(v) ? v.map(canonical) : v && typeof v === 'object' ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, c]) => [k, canonical(c)])) : v;
|
|
7
|
-
return
|
|
7
|
+
return JSON.stringify(canonical(value)).replace(/[\u007f-\uffff]/g, c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
8
8
|
}
|
|
9
|
+
export function protocolHash(value) { return createHash('sha256').update(protocolJson(value)).digest('hex'); }
|
|
9
10
|
const denied = () => new Error('CANDIDATE_REJECTED');
|
|
10
11
|
function freeze(v) { if (v && typeof v === 'object') {
|
|
11
12
|
for (const c of Object.values(v))
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { constants } from 'node:fs';
|
|
4
|
-
import { lstat, mkdir, mkdtemp, open, realpath, readdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { lstat, mkdir, mkdtemp, open, realpath, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { deflateSync } from 'node:zlib';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { measureCommandFile } from '../safety/registered-command-integrity.js';
|
|
9
9
|
import { protocolHash } from './candidate.js';
|
|
10
|
+
import { RawGitObjects } from './raw-git-objects.js';
|
|
10
11
|
const execute = promisify(execFile), denied = () => Error('SNAPSHOT_REJECTED');
|
|
11
12
|
const digest = (data) => createHash('sha256').update(data).digest('hex');
|
|
12
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
|
+
/** P1 branch shape only. Authority still requires exact frozen-fence equality. */
|
|
15
|
+
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'));
|
|
13
16
|
/** Reads only immutable Git objects and metadata. Never runs diff against worktree
|
|
14
17
|
* content, where malicious clean filters can execute even with --no-ext-diff. */
|
|
15
18
|
export class GitSnapshotHost {
|
|
16
19
|
options;
|
|
17
20
|
seals = new WeakSet();
|
|
21
|
+
ownedRoots = new Set();
|
|
18
22
|
constructor(options) { this.options = structuredClone(options); }
|
|
19
23
|
async git(args) {
|
|
20
24
|
const o = this.options;
|
|
@@ -129,8 +133,8 @@ export class GitSnapshotHost {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
async capture() {
|
|
132
|
-
const o = this.options;
|
|
133
|
-
if (!/^[a-f0-9]{40}$/.test(o.baseCommitSha) ||
|
|
136
|
+
const o = this.options, branchAllowed = o.p1WorkingBranch === undefined ? /^ai-work\/local\/[A-Za-z0-9-]+$/.test(o.workingBranch) : o.p1WorkingBranch === o.workingBranch && isP1WorkingBranch(o.workingBranch);
|
|
137
|
+
if (!/^[a-f0-9]{40}$/.test(o.baseCommitSha) || !branchAllowed || await realpath(o.root) !== o.root || !Number.isSafeInteger(o.maxFileBytes) || o.maxFileBytes < 1 || !Number.isSafeInteger(o.maxTotalBytes) || o.maxTotalBytes < o.maxFileBytes)
|
|
134
138
|
throw denied();
|
|
135
139
|
const head = (await this.git(['rev-parse', '--verify', 'HEAD'])).toString().trim(), branch = (await this.git(['symbolic-ref', '--short', 'HEAD'])).toString().trim();
|
|
136
140
|
if (head !== o.baseCommitSha || branch !== o.workingBranch)
|
|
@@ -168,7 +172,7 @@ export class GitSnapshotHost {
|
|
|
168
172
|
const files = [], changes = [];
|
|
169
173
|
let total = 0;
|
|
170
174
|
for (const name of [...names].sort()) {
|
|
171
|
-
if ([...gitlinks.keys()].some(p => name === p || name.startsWith(p + '/')))
|
|
175
|
+
if ([...gitlinks.keys()].some(p => name === p || name.startsWith(p + '/') || p.startsWith(name + '/')))
|
|
172
176
|
throw denied();
|
|
173
177
|
const current = await this.currentFile(name), prior = base.get(name);
|
|
174
178
|
let before = null;
|
|
@@ -194,7 +198,7 @@ export class GitSnapshotHost {
|
|
|
194
198
|
/** Creates a separate local-only Git checkout with exact base parent objects.
|
|
195
199
|
* Raw object copying and raw file writes invoke no filters, templates or hooks;
|
|
196
200
|
* no local fetch/upload-pack, network or account credentials are involved. */
|
|
197
|
-
async
|
|
201
|
+
async privateCheckout(sealed, prefix = 'publisher-') {
|
|
198
202
|
try {
|
|
199
203
|
if (!this.seals.has(sealed))
|
|
200
204
|
throw denied();
|
|
@@ -202,7 +206,8 @@ export class GitSnapshotHost {
|
|
|
202
206
|
const o = this.options, parent = o.privateRoot;
|
|
203
207
|
if (await realpath(parent) !== parent || (await lstat(parent)).mode & 0o077)
|
|
204
208
|
throw denied();
|
|
205
|
-
const root = await mkdtemp(path.join(parent,
|
|
209
|
+
const root = await mkdtemp(path.join(parent, prefix)), gitdir = path.join(root, '.git');
|
|
210
|
+
this.ownedRoots.add(root);
|
|
206
211
|
await mkdir(path.join(gitdir, 'objects'), { recursive: true, mode: 0o700 });
|
|
207
212
|
const ref = path.join(gitdir, 'refs', 'heads', o.workingBranch);
|
|
208
213
|
await mkdir(path.dirname(ref), { recursive: true, mode: 0o700 });
|
|
@@ -261,6 +266,81 @@ export class GitSnapshotHost {
|
|
|
261
266
|
throw denied();
|
|
262
267
|
}
|
|
263
268
|
}
|
|
269
|
+
async publicationCheckout(sealed) { return this.privateCheckout(sealed); }
|
|
270
|
+
async assertSourceUnchanged(sealed) {
|
|
271
|
+
try {
|
|
272
|
+
if (!this.seals.has(sealed))
|
|
273
|
+
throw denied();
|
|
274
|
+
const capture = await this.capture();
|
|
275
|
+
if (protocolHash(capture.observation) !== protocolHash(sealed.observation) || protocolHash(capture.files.map(f => ({ path: f.path, mode: f.mode, sha256: digest(f.bytes) }))) !== sealed.treeFingerprint)
|
|
276
|
+
throw denied();
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
throw denied();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Verification only; creates no publication capability or remote operation. */
|
|
283
|
+
async verificationCandidate(sealed) {
|
|
284
|
+
try {
|
|
285
|
+
if (this.options.p1WorkingBranch === undefined)
|
|
286
|
+
throw denied();
|
|
287
|
+
const root = await this.privateCheckout(sealed, 'verification-'), entries = [];
|
|
288
|
+
const walk = async (dir, prefix = '') => { for (const name of (await readdir(dir)).sort()) {
|
|
289
|
+
if (!prefix && name === '.git')
|
|
290
|
+
continue;
|
|
291
|
+
const rel = prefix ? prefix + '/' + name : name, file = path.join(dir, name), s = await lstat(file);
|
|
292
|
+
if (s.isSymbolicLink() || (!s.isDirectory() && (!s.isFile() || s.nlink !== 1)))
|
|
293
|
+
throw denied();
|
|
294
|
+
if (s.isDirectory())
|
|
295
|
+
await walk(file, rel);
|
|
296
|
+
else
|
|
297
|
+
entries.push({ path: rel, mode: s.mode & 0o111 ? 0o100755 : 0o100644, bytes: await readFile(file) });
|
|
298
|
+
} };
|
|
299
|
+
await walk(root);
|
|
300
|
+
entries.sort((a, b) => a.path < b.path ? -1 : 1);
|
|
301
|
+
if (protocolHash(entries.map(e => ({ path: e.path, mode: e.mode, sha256: digest(e.bytes) }))) !== sealed.treeFingerprint)
|
|
302
|
+
throw denied();
|
|
303
|
+
for (const line of (await this.git(['ls-tree', '-r', '-z', this.options.baseCommitSha])).toString().split('\0')) {
|
|
304
|
+
const link = /^160000 commit ([a-f0-9]{40})\t(.+)$/s.exec(line);
|
|
305
|
+
if (link)
|
|
306
|
+
entries.push({ path: link[2], mode: 0o160000, bytes: Buffer.alloc(0), gitlink: link[1] });
|
|
307
|
+
}
|
|
308
|
+
const objects = new RawGitObjects(root), tree = await objects.tree(entries, true), commitSha = await objects.object('commit', Buffer.from(`tree ${tree}\nparent ${this.options.baseCommitSha}\nauthor Local Runner Verification <runner-verification@invalid> 1 +0000\ncommitter Local Runner Verification <runner-verification@invalid> 1 +0000\n\nLocal runner verification candidate v1\n`), true);
|
|
309
|
+
await writeFile(path.join(root, '.git', 'HEAD'), commitSha + '\n', { mode: 0o600 });
|
|
310
|
+
// Bind every private checkout byte, including raw Git metadata, before tests.
|
|
311
|
+
const fingerprint = async () => { const values = []; const visit = async (dir, prefix = '') => { for (const name of (await readdir(dir)).sort()) {
|
|
312
|
+
const relative = prefix ? prefix + '/' + name : name, file = path.join(dir, name), s = await lstat(file);
|
|
313
|
+
if (s.isSymbolicLink() || (!s.isDirectory() && (!s.isFile() || s.nlink !== 1)))
|
|
314
|
+
throw denied();
|
|
315
|
+
if (s.isDirectory())
|
|
316
|
+
await visit(file, relative);
|
|
317
|
+
else
|
|
318
|
+
values.push({ path: relative, mode: s.mode & 0o111 ? 0o100755 : 0o100644, sha256: await measureCommandFile(file) });
|
|
319
|
+
} }; await visit(root); return protocolHash(values); };
|
|
320
|
+
const expected = await fingerprint();
|
|
321
|
+
await sealed.assertUnchanged();
|
|
322
|
+
return Object.freeze({ root, commitSha, assertUnchanged: async () => { await sealed.assertUnchanged(); if (await fingerprint() !== expected)
|
|
323
|
+
throw denied(); } });
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
throw denied();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** Caller owns this lifecycle. Never returns success if private cleanup fails. */
|
|
330
|
+
async dispose() {
|
|
331
|
+
let failed = false;
|
|
332
|
+
for (const root of this.ownedRoots) {
|
|
333
|
+
try {
|
|
334
|
+
await rm(root, { recursive: true, force: true });
|
|
335
|
+
this.ownedRoots.delete(root);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
failed = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (failed)
|
|
342
|
+
throw denied();
|
|
343
|
+
}
|
|
264
344
|
async observe() { try {
|
|
265
345
|
const a = await this.capture(), b = await this.capture();
|
|
266
346
|
if (a.fingerprint !== b.fingerprint)
|
|
@@ -282,6 +362,7 @@ export class GitSnapshotHost {
|
|
|
282
362
|
if (!stat.isDirectory() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
|
|
283
363
|
throw denied();
|
|
284
364
|
const capture = await this.capture(), root = await mkdtemp(path.join(parent, 'candidate-'));
|
|
365
|
+
this.ownedRoots.add(root);
|
|
285
366
|
for (const f of capture.files) {
|
|
286
367
|
const file = path.join(root, f.path);
|
|
287
368
|
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import {
|
|
5
|
-
import { lstat, readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { lstat, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { protocolHash } from './candidate.js';
|
|
7
|
+
import { RawGitObjects } from './raw-git-objects.js';
|
|
8
8
|
import { assertPinnedGit, assertPrivateCheckout, publicationGitEnvironment, publicationGitArgs, strictPublicationOrigin } from './git-credential-broker.js';
|
|
9
9
|
const execute = promisify(execFile), denied = () => Error('GIT_PUBLICATION_REJECTED');
|
|
10
10
|
/** Only DeterministicPublisher may dispatch validated signed requests. This
|
|
@@ -79,34 +79,8 @@ export class GitPublicationTransport {
|
|
|
79
79
|
throw denied();
|
|
80
80
|
return entries;
|
|
81
81
|
}
|
|
82
|
-
async object(type, bytes, write) {
|
|
83
|
-
|
|
84
|
-
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
85
|
-
try {
|
|
86
|
-
await writeFile(file, deflateSync(raw), { flag: 'wx', mode: 0o400 });
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
if (e.code !== 'EEXIST' || !inflateSync(await readFile(file)).equals(raw))
|
|
90
|
-
throw denied();
|
|
91
|
-
}
|
|
92
|
-
} return oid; }
|
|
93
|
-
async tree(entries, write, prefix = '') {
|
|
94
|
-
const names = new Map();
|
|
95
|
-
const children = new Set();
|
|
96
|
-
for (const entry of entries) {
|
|
97
|
-
if (!entry.path.startsWith(prefix))
|
|
98
|
-
continue;
|
|
99
|
-
const rest = entry.path.slice(prefix.length), slash = rest.indexOf('/');
|
|
100
|
-
if (slash >= 0)
|
|
101
|
-
children.add(rest.slice(0, slash));
|
|
102
|
-
else
|
|
103
|
-
names.set(rest, { mode: entry.mode.toString(8), oid: entry.gitlink ?? await this.object('blob', entry.bytes, write) });
|
|
104
|
-
}
|
|
105
|
-
for (const child of children)
|
|
106
|
-
names.set(child, { mode: '40000', oid: await this.tree(entries, write, prefix + child + '/') });
|
|
107
|
-
const chunks = [...names].sort(([a, av], [b, bv]) => Buffer.compare(Buffer.from(a + (av.mode === '40000' ? '/' : '')), Buffer.from(b + (bv.mode === '40000' ? '/' : '')))).map(([name, v]) => Buffer.concat([Buffer.from(`${v.mode} ${name}\0`), Buffer.from(v.oid, 'hex')]));
|
|
108
|
-
return this.object('tree', Buffer.concat(chunks), write);
|
|
109
|
-
}
|
|
82
|
+
async object(type, bytes, write) { return new RawGitObjects(this.options.checkout).object(type, bytes, write); }
|
|
83
|
+
async tree(entries, write) { return new RawGitObjects(this.options.checkout).tree(entries, write); }
|
|
110
84
|
marker(r) { return `Local Fix ${r.attempt_id}\n\nLocal-Fix-Publication: ${r.publication_id}\nLocal-Fix-Candidate: ${r.candidate_fingerprint}\n`; }
|
|
111
85
|
async commit(r, entries, write) { const tree = await this.tree(entries, write); return this.object('commit', Buffer.from(`tree ${tree}\nparent ${r.base_commit_sha}\nauthor Local Fix <local-fix@invalid> 1 +0000\ncommitter Local Fix <local-fix@invalid> 1 +0000\n\n${this.marker(r)}Local-Fix-Request: ${r.request_fingerprint}\n`), write); }
|
|
112
86
|
scope(r) { return { origin: this.options.origin, checkout: this.options.checkout, ref: `refs/heads/${r.working_branch}`, sha: r.accepted_results.committed_sha }; }
|