mtech-ai-local-runner 0.1.33 → 0.1.34

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 CHANGED
@@ -1,3 +1,25 @@
1
+ ## 0.1.34: pinned writable Fix HEAD preparation
2
+
3
+ Accept a linked worktree's symbolic HEAD only for the exact approved writable
4
+ Fix branch and commit. Detached read-only checkouts retain their existing policy.
5
+ Bounded, no-follow metadata reads reject ref chains, traversal, symlinks and
6
+ post-preparation path changes. Preparation errors retain a safe substage code.
7
+ This patch does not grant another execution: package/runtime pins, installation
8
+ review and exact Gateway contract/launch approval remain required. Model work
9
+ limits, personal login/settings and the Claude execution path are unchanged.
10
+
11
+ ## Unreleased: P1 runner-observed evidence (disabled)
12
+
13
+ P1 adds candidate-bound command receipts and changed-path evidence for explicitly
14
+ reviewed Codex Node/file-only profiles. It verifies a private exact-base candidate
15
+ after the owned model stops; model test claims cannot satisfy the Loop criteria.
16
+ P1 defaults off, the Gateway profile registry is empty, and the runtime P1 host
17
+ is absent by default. Source package `0.1.33` is inherited from upstream; this is
18
+ not a P1 package release or live acceptance. See the [P1 implementation and
19
+ delivery record](../../docs/superpowers/plans/2026-10-17-local-runner-p1-runner-observed-evidence.md)
20
+ for local verification results and separate deployment, runner-update and
21
+ activation gates.
22
+
1
23
  ## Unreleased: P0 terminal safety baseline (local verification only)
2
24
 
3
25
  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
- switch (message.type) {
117
- case "system":
118
- return parseSystemMessage(message);
119
- case "assistant":
120
- return parseAssistantMessage(message.message ?? {}, worktreeRoot);
121
- case "user":
122
- return parseUserMessage(message.message ?? {});
123
- case "result":
124
- return parseResultMessage(message);
125
- default:
126
- return null;
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,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 protocolHash(value) {
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 createHash('sha256').update(JSON.stringify(canonical(value)).replace(/[\u007f-\uffff]/g, c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`)).digest('hex');
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) || !/^ai-work\/local\/[A-Za-z0-9-]+$/.test(o.workingBranch) || await realpath(o.root) !== o.root || !Number.isSafeInteger(o.maxFileBytes) || o.maxFileBytes < 1 || !Number.isSafeInteger(o.maxTotalBytes) || o.maxTotalBytes < o.maxFileBytes)
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 publicationCheckout(sealed) {
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, 'publisher-')), gitdir = path.join(root, '.git');
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 { deflateSync, inflateSync } from 'node:zlib';
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) { const raw = Buffer.concat([Buffer.from(`${type} ${bytes.length}\0`), bytes]), oid = createHash('sha1').update(raw).digest('hex'); if (write) {
83
- const dir = path.join(this.options.checkout, '.git', 'objects', oid.slice(0, 2)), file = path.join(dir, oid.slice(2));
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 }; }
@@ -6,25 +6,29 @@ import { protocolHash } from './candidate.js';
6
6
  /** Reuses the exact registered-command sandbox: no ambient credentials, network,
7
7
  * subprocess permissions, repository writes, or additional dependency grants.
8
8
  * Unsupported PHP/Node project commands remain denied by existing confinement. */
9
- function buildIsolatedVerifier(input, allowCandidateReads = false) {
9
+ export async function createIsolatedCommandPolicy(input, allowCandidateReads = false) {
10
10
  const options = { ...input, commands: structuredClone(input.commands), allowedPaths: [...input.allowedPaths], blockedPaths: [...input.blockedPaths] };
11
- return async (snapshot) => {
12
- const executionCommands = [];
13
- for (const command of options.commands) {
14
- const derived = structuredClone(command), names = command.candidateReadPaths ?? [];
15
- if (!Array.isArray(names) || new Set(names).size !== names.length || (!allowCandidateReads && names.length))
11
+ const executionCommands = [];
12
+ for (const command of options.commands) {
13
+ const derived = structuredClone(command), names = command.candidateReadPaths ?? [];
14
+ if (!Array.isArray(names) || new Set(names).size !== names.length || (!allowCandidateReads && names.length))
15
+ throw Error('ISOLATED_VERIFICATION_REJECTED');
16
+ for (const name of names) {
17
+ if (typeof name !== 'string' || !name || path.isAbsolute(name) || name.includes('\\') || name.split('/').some(p => !p || p === '.' || p === '..' || /^(?:\.git|\.env(?:\..*)?|\.ssh|\.aws|\.codex|\.claude|\.npmrc|\.netrc|credentials(?:\..*)?|auth\.(?:json|toml|ya?ml)|AGENTS\.md|CLAUDE\.md)$/i.test(p)) || !options.allowedPaths.includes(name) || Object.hasOwn(command.integrity.readFiles, name) || options.blockedPaths.some(p => name === p || name.startsWith(p.replace(/\/\*\*$/, '') + '/')))
16
18
  throw Error('ISOLATED_VERIFICATION_REJECTED');
17
- for (const name of names) {
18
- if (typeof name !== 'string' || !name || path.isAbsolute(name) || name.includes('\\') || name.split('/').some(p => !p || p === '.' || p === '..' || /^(?:\.git|\.env(?:\..*)?|\.ssh|\.aws|\.codex|\.claude|\.npmrc|\.netrc|credentials(?:\..*)?|auth\.(?:json|toml|ya?ml)|AGENTS\.md|CLAUDE\.md)$/i.test(p)) || !options.allowedPaths.includes(name) || Object.hasOwn(command.integrity.readFiles, name) || options.blockedPaths.some(p => name === p || name.startsWith(p.replace(/\/\*\*$/, '') + '/')))
19
- throw Error('ISOLATED_VERIFICATION_REJECTED');
20
- const file = path.join(options.root, name);
21
- if (await realpath(file) !== file)
22
- throw Error('ISOLATED_VERIFICATION_REJECTED');
23
- derived.integrity.readFiles[name] = await measureCommandFile(file);
24
- }
25
- executionCommands.push(derived);
19
+ const file = path.join(options.root, name);
20
+ if (await realpath(file) !== file)
21
+ throw Error('ISOLATED_VERIFICATION_REJECTED');
22
+ derived.integrity.readFiles[name] = await measureCommandFile(file);
26
23
  }
27
- const policy = new LocalToolPolicy({ worktreeRoot: options.root, access: 'read', allowedPaths: options.allowedPaths, blockedPaths: options.blockedPaths, requiredCommands: executionCommands.map(c => ({ ...c, readPaths: Object.keys(c.integrity.readFiles) })) });
24
+ executionCommands.push(derived);
25
+ }
26
+ return new LocalToolPolicy({ worktreeRoot: options.root, access: 'read', allowedPaths: options.allowedPaths, blockedPaths: options.blockedPaths, requiredCommands: executionCommands.map(c => ({ ...c, readPaths: Object.keys(c.integrity.readFiles) })) });
27
+ }
28
+ function buildIsolatedVerifier(input, allowCandidateReads = false) {
29
+ const options = { ...input, commands: structuredClone(input.commands), allowedPaths: [...input.allowedPaths], blockedPaths: [...input.blockedPaths] };
30
+ return async (snapshot) => {
31
+ const policy = await createIsolatedCommandPolicy(options, allowCandidateReads);
28
32
  try {
29
33
  if (snapshot.commands.length !== options.commands.length)
30
34
  throw Error('ISOLATED_VERIFICATION_REJECTED');
@@ -5,7 +5,7 @@ import { evaluateCommand } from '../safety/command-policy.js';
5
5
  import { protocolHash } from './candidate.js';
6
6
  const denied = () => new Error('PROJECT_REGISTRATION_REJECTED');
7
7
  const privateSegment = /^(?:\.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;
8
- function exactName(name) { return typeof name === 'string' && !!name && !path.isAbsolute(name) && !/[\\\0*?\[\]{}]/.test(name) && name.split('/').every(s => !!s && s !== '.' && s !== '..' && !privateSegment.test(s)); }
8
+ function exactName(name) { return typeof name === 'string' && !!name && !name.startsWith('-') && !path.isAbsolute(name) && !/[\\\0*?\[\]{}]/.test(name) && name.split('/').every(s => !!s && s !== '.' && s !== '..' && !privateSegment.test(s)); }
9
9
  function blocked(name, scopes) { return scopes.some(scope => scope === '.env.*' ? name.startsWith('.env.') : name === scope || name.startsWith(scope.replace(/\/\*\*$/, '') + '/')); }
10
10
  /** Validates reviewed exact registrations against the current host and scope.
11
11
  * Receipt authenticity/ownership belongs to host release attestation; this API