@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.eeefdb0d19c1

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.
Files changed (118) hide show
  1. package/README.md +34 -0
  2. package/dist/bridge/bridge.base.d.ts +2 -1
  3. package/dist/bridge/bridge.service.d.ts +22 -3
  4. package/dist/bridge/bridge.service.js +416 -40
  5. package/dist/bridge/bridge.types.d.ts +59 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +37 -3
  14. package/dist/cli/cli.service.d.ts +30 -2
  15. package/dist/cli/cli.service.js +182 -23
  16. package/dist/cli/local-config.d.ts +4 -0
  17. package/dist/cli/local-config.js +41 -1
  18. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  19. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  20. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  22. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  23. package/dist/runtime/attempt-process-supervisor.js +343 -0
  24. package/dist/runtime/generated-output-reset.d.ts +10 -0
  25. package/dist/runtime/generated-output-reset.js +103 -0
  26. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  27. package/dist/runtime/host-listener-inspector.js +324 -0
  28. package/dist/runtime/host-port-broker.d.ts +76 -0
  29. package/dist/runtime/host-port-broker.js +333 -0
  30. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  31. package/dist/runtime/host-runtime-contract.js +700 -0
  32. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  33. package/dist/runtime/host-runtime-lease.js +1460 -0
  34. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  35. package/dist/runtime/host-runtime-promotion.js +159 -0
  36. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  37. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  38. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  40. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  41. package/dist/runtime/runtime-isolation-lease.js +158 -0
  42. package/dist/sandbox/preflight.d.ts +14 -0
  43. package/dist/sandbox/preflight.js +91 -12
  44. package/dist/sandbox/process.js +125 -40
  45. package/dist/sandbox/repository-lock.js +37 -1
  46. package/dist/sandbox/runtime.d.ts +66 -3
  47. package/dist/sandbox/runtime.js +670 -105
  48. package/dist/sandbox/sandbox.types.d.ts +3 -0
  49. package/dist/sandbox/smoke.d.ts +76 -0
  50. package/dist/sandbox/smoke.js +380 -0
  51. package/dist/sandbox/worker-supervisor.js +1 -1
  52. package/dist/sandbox/worker.js +2 -5
  53. package/dist/sandbox/workspace.service.d.ts +1 -0
  54. package/dist/sandbox/workspace.service.js +28 -5
  55. package/dist/session/managed-attempt-tools.d.ts +12 -0
  56. package/dist/session/managed-attempt-tools.js +350 -0
  57. package/dist/session/phase-completion.artifacts.js +9 -2
  58. package/dist/session/phase-completion.d.ts +4 -2
  59. package/dist/session/phase-completion.helpers.js +27 -3
  60. package/dist/session/phase-completion.js +12 -3
  61. package/dist/session/phase-completion.prompts.d.ts +3 -3
  62. package/dist/session/phase-completion.prompts.js +19 -5
  63. package/dist/session/phase-completion.runtime.d.ts +12 -1
  64. package/dist/session/phase-completion.runtime.js +127 -7
  65. package/dist/session/phase-completion.schemas.js +108 -3
  66. package/dist/session/phase-completion.validation.d.ts +4 -1
  67. package/dist/session/phase-completion.validation.js +106 -2
  68. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  69. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  70. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  71. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  72. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  73. package/dist/session/provider-cli/provider-cli.process.d.ts +16 -0
  74. package/dist/session/provider-cli/provider-cli.process.js +212 -25
  75. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  76. package/dist/session/provider-cli/provider-cli.prompts.js +84 -17
  77. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  78. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  79. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  80. package/dist/session/provider-cli/provider-cli.service.d.ts +3 -2
  81. package/dist/session/provider-cli/provider-cli.service.js +254 -71
  82. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  83. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  84. package/dist/session/provider-cli/provider-output-state.js +30 -2
  85. package/dist/session/session-redaction.d.ts +3 -0
  86. package/dist/session/session-redaction.js +23 -0
  87. package/dist/session/session.base.d.ts +6 -1
  88. package/dist/session/session.service.d.ts +8 -2
  89. package/dist/session/session.service.js +436 -41
  90. package/dist/session/session.types.d.ts +55 -0
  91. package/dist/session/session.types.js +69 -2
  92. package/dist/shared/path/path.js +3 -1
  93. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  94. package/dist/shared/process/host-process-recovery.js +78 -0
  95. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  96. package/dist/shared/process/host-process-supervisor.js +282 -0
  97. package/dist/shared/process/process.d.ts +1 -0
  98. package/dist/shared/process/process.js +1 -0
  99. package/dist/shared/redaction/redaction.js +6 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/dist/worktree/artifact-injector.d.ts +3 -2
  103. package/dist/worktree/artifact-injector.js +43 -4
  104. package/dist/worktree/dependency-warmer.d.ts +10 -1
  105. package/dist/worktree/dependency-warmer.js +94 -5
  106. package/dist/worktree/index.d.ts +1 -0
  107. package/dist/worktree/index.js +1 -0
  108. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  109. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  110. package/dist/worktree/worktree-paths.d.ts +4 -0
  111. package/dist/worktree/worktree-paths.js +30 -0
  112. package/dist/worktree/worktree.base.d.ts +3 -0
  113. package/dist/worktree/worktree.service.d.ts +7 -2
  114. package/dist/worktree/worktree.service.js +72 -44
  115. package/dist/worktree/worktree.types.d.ts +3 -1
  116. package/package.json +3 -2
  117. package/runtime/windows-job-supervisor.ps1 +356 -0
  118. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -14,12 +14,14 @@ export function parseRunOnceDispatchPacket(value) {
14
14
  session: parseSession(packet.session),
15
15
  lease: parseLease(packet.lease),
16
16
  execution: parseExecution(packet.execution),
17
+ runtimeIntent: parseRuntimeIntent(packet.runtimeIntent),
17
18
  serverLocal: parseServerLocal(packet.serverLocal),
18
19
  sandbox: parseSandbox(packet.sandbox, schemaVersion),
19
20
  pullRequest: parsePullRequest(packet.pullRequest),
20
21
  apiKeySecretGrant: parseApiKeySecretGrant(packet.apiKeySecretGrant),
21
22
  apiKeySecretGrantHandle: parseApiKeySecretGrantHandle(packet.apiKeySecretGrantHandle),
22
23
  artifacts: parseArtifacts(packet.artifacts),
24
+ priorPhaseArtifacts: parsePriorPhaseArtifacts(packet.priorPhaseArtifacts),
23
25
  warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
24
26
  qaBrowser: parseQaBrowser(packet.qaBrowser),
25
27
  qaScope: parseQaScope(packet.qaScope ?? packet.qa_scope),
@@ -32,6 +34,21 @@ export function parseRunOnceDispatchPacket(value) {
32
34
  draft: parseDraftPrompt(packet.draft)
33
35
  };
34
36
  }
37
+ function parseRuntimeIntent(value) {
38
+ if (value === undefined || value === null) {
39
+ return { executionClass: "managed_runtime", applicationRuntimeRequired: true };
40
+ }
41
+ const intent = asObject(value, "runtimeIntent");
42
+ const executionClass = requiredString(intent, "executionClass").trim().toLowerCase();
43
+ if (executionClass !== "analysis_only" && executionClass !== "managed_runtime") {
44
+ throw new Error("runtimeIntent.executionClass must be analysis_only or managed_runtime.");
45
+ }
46
+ const applicationRuntimeRequired = requiredBoolean(intent, "applicationRuntimeRequired");
47
+ if ((executionClass === "analysis_only") === applicationRuntimeRequired) {
48
+ throw new Error("runtimeIntent execution class and application runtime requirement are inconsistent.");
49
+ }
50
+ return { executionClass, applicationRuntimeRequired };
51
+ }
35
52
  function parseSandbox(value, schemaVersion) {
36
53
  if (value === undefined || value === null) {
37
54
  if (schemaVersion === 2) {
@@ -93,17 +110,38 @@ function parseSandboxResourceRequest(value) {
93
110
  return {
94
111
  sandboxSlots: 1,
95
112
  cpuMillis: 2000,
96
- memoryBytes: 2 * 1024 ** 3,
113
+ workerMemoryLimitBytes: 2 * 1024 ** 3,
114
+ serviceMemoryLimitBytes: 0,
115
+ runtimeOverheadBytes: 128 * 1024 ** 2,
116
+ memoryBytes: 2 * 1024 ** 3 + 128 * 1024 ** 2,
97
117
  workspaceBytes: 10 * 1024 ** 3,
98
118
  pids: 512,
99
119
  buildSlots: 1
100
120
  };
101
121
  }
102
122
  const resources = asObject(value, "sandbox.requestedResources");
123
+ const memoryBytes = boundedInteger(resources, "memoryBytes", 128 * 1024 ** 2, 512 * 1024 ** 3);
124
+ const hasMemoryBreakdown = ["workerMemoryLimitBytes", "serviceMemoryLimitBytes", "runtimeOverheadBytes"]
125
+ .some(key => resources[key] !== undefined && resources[key] !== null);
126
+ const workerMemoryLimitBytes = hasMemoryBreakdown
127
+ ? boundedInteger(resources, "workerMemoryLimitBytes", 128 * 1024 ** 2, 512 * 1024 ** 3)
128
+ : memoryBytes;
129
+ const serviceMemoryLimitBytes = hasMemoryBreakdown
130
+ ? boundedInteger(resources, "serviceMemoryLimitBytes", 0, 512 * 1024 ** 3)
131
+ : 0;
132
+ const runtimeOverheadBytes = hasMemoryBreakdown
133
+ ? boundedInteger(resources, "runtimeOverheadBytes", 0, 64 * 1024 ** 3)
134
+ : 0;
135
+ if (workerMemoryLimitBytes + serviceMemoryLimitBytes + runtimeOverheadBytes !== memoryBytes) {
136
+ throw new Error("sandbox.requestedResources memory breakdown must equal memoryBytes.");
137
+ }
103
138
  return {
104
139
  sandboxSlots: boundedInteger(resources, "sandboxSlots", 1, 4),
105
140
  cpuMillis: boundedInteger(resources, "cpuMillis", 100, 64_000),
106
- memoryBytes: boundedInteger(resources, "memoryBytes", 128 * 1024 ** 2, 512 * 1024 ** 3),
141
+ workerMemoryLimitBytes,
142
+ serviceMemoryLimitBytes,
143
+ runtimeOverheadBytes,
144
+ memoryBytes,
107
145
  workspaceBytes: boundedInteger(resources, "workspaceBytes", 1024 ** 3, 10 * 1024 ** 4),
108
146
  pids: boundedInteger(resources, "pids", 32, 65_536),
109
147
  buildSlots: boundedInteger(resources, "buildSlots", 0, 4)
@@ -384,6 +422,7 @@ function parseQaAppLaunchTarget(value) {
384
422
  command: optionalString(launchTarget, "command"),
385
423
  working_directory: optionalString(launchTarget, "working_directory"),
386
424
  ready_url: optionalString(launchTarget, "ready_url"),
425
+ managed_runtime_port: optionalString(launchTarget, "managed_runtime_port"),
387
426
  routes: Array.isArray(routes)
388
427
  ? routes.map((entry, index) => {
389
428
  if (typeof entry !== "string") {
@@ -662,6 +701,34 @@ function parseArtifacts(value) {
662
701
  };
663
702
  });
664
703
  }
704
+ function parsePriorPhaseArtifacts(value) {
705
+ if (value === undefined || value === null) {
706
+ return [];
707
+ }
708
+ if (!Array.isArray(value)) {
709
+ throw new Error("priorPhaseArtifacts must be an array when provided.");
710
+ }
711
+ return value.map((entry, index) => {
712
+ const artifact = asObject(entry, `priorPhaseArtifacts[${index}]`);
713
+ const phase = requiredString(artifact, "phase").trim().toLowerCase().replace(/-/g, "_");
714
+ if (phase !== "implementation" && phase !== "self_review" && phase !== "validation") {
715
+ throw new Error(`priorPhaseArtifacts[${index}].phase must be implementation, self_review, or validation.`);
716
+ }
717
+ return {
718
+ workItemId: requiredString(artifact, "workItemId"),
719
+ artifactId: requiredString(artifact, "artifactId"),
720
+ cycleRunId: requiredString(artifact, "cycleRunId"),
721
+ phase,
722
+ status: requiredString(artifact, "status"),
723
+ producedAt: requiredString(artifact, "producedAt"),
724
+ sourcePath: requiredString(artifact, "sourcePath"),
725
+ verdictJson: requiredString(artifact, "verdictJson"),
726
+ report: optionalString(artifact, "report") ?? "",
727
+ verdictContentHash: requiredString(artifact, "verdictContentHash").toLowerCase(),
728
+ reportContentHash: requiredString(artifact, "reportContentHash").toLowerCase()
729
+ };
730
+ });
731
+ }
665
732
  function asObject(value, label) {
666
733
  if (!value || typeof value !== "object" || Array.isArray(value)) {
667
734
  throw new Error(`${label} must be an object.`);
@@ -4,7 +4,9 @@ export function isSubPath(root, candidate) {
4
4
  const normalizedRoot = path.resolve(root);
5
5
  const normalizedCandidate = path.resolve(candidate);
6
6
  const relative = path.relative(normalizedRoot, normalizedCandidate);
7
- return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
7
+ return relative === "" || (relative !== ".."
8
+ && !relative.startsWith(`..${path.sep}`)
9
+ && !path.isAbsolute(relative));
8
10
  }
9
11
  export async function resolveExistingDirectoryPath(inputPath) {
10
12
  const stat = await fs.stat(inputPath);
@@ -0,0 +1,12 @@
1
+ import type { HostProcessSupervisorMetadata } from "./host-process-supervisor.js";
2
+ export type HostProcessRecoveryOutcome = "absent" | "terminated" | "pid_reused" | "unverifiable";
3
+ export interface HostProcessRecovery {
4
+ recover(metadata: HostProcessSupervisorMetadata): Promise<HostProcessRecoveryOutcome>;
5
+ }
6
+ /** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
7
+ export declare class ExactHostProcessRecovery implements HostProcessRecovery {
8
+ private readonly platform;
9
+ private readonly graceMs;
10
+ constructor(platform?: NodeJS.Platform, graceMs?: number);
11
+ recover(metadata: HostProcessSupervisorMetadata): Promise<HostProcessRecoveryOutcome>;
12
+ }
@@ -0,0 +1,78 @@
1
+ import { captureHostProcessIdentity } from "./host-process-supervisor.js";
2
+ /** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
3
+ export class ExactHostProcessRecovery {
4
+ platform;
5
+ graceMs;
6
+ constructor(platform = process.platform, graceMs = 2_000) {
7
+ this.platform = platform;
8
+ this.graceMs = graceMs;
9
+ }
10
+ async recover(metadata) {
11
+ if (!processExists(metadata.rootPid))
12
+ return "absent";
13
+ if (!metadata.rootIdentity)
14
+ return "unverifiable";
15
+ const currentIdentity = captureHostProcessIdentity(metadata.rootPid, this.platform);
16
+ if (!currentIdentity)
17
+ return processExists(metadata.rootPid) ? "unverifiable" : "absent";
18
+ if (currentIdentity !== metadata.rootIdentity)
19
+ return "pid_reused";
20
+ if (this.platform === "win32") {
21
+ signalExactProcess(metadata.rootPid, "SIGTERM");
22
+ }
23
+ else {
24
+ signalExactProcessGroup(metadata.rootPid, "SIGTERM");
25
+ }
26
+ await waitForExit(metadata.rootPid, this.graceMs);
27
+ if (processExists(metadata.rootPid)) {
28
+ if (captureHostProcessIdentity(metadata.rootPid, this.platform) !== metadata.rootIdentity) {
29
+ return "pid_reused";
30
+ }
31
+ if (this.platform === "win32") {
32
+ signalExactProcess(metadata.rootPid, "SIGKILL");
33
+ }
34
+ else {
35
+ signalExactProcessGroup(metadata.rootPid, "SIGKILL");
36
+ }
37
+ await waitForExit(metadata.rootPid, this.graceMs);
38
+ }
39
+ if (processExists(metadata.rootPid)
40
+ && captureHostProcessIdentity(metadata.rootPid, this.platform) === metadata.rootIdentity) {
41
+ throw new Error("host_process_recovery_degraded: exact supervised process tree did not stop.");
42
+ }
43
+ return "terminated";
44
+ }
45
+ }
46
+ function processExists(pid) {
47
+ try {
48
+ process.kill(pid, 0);
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ return error.code === "EPERM";
53
+ }
54
+ }
55
+ function signalExactProcess(pid, signal) {
56
+ try {
57
+ process.kill(pid, signal);
58
+ }
59
+ catch (error) {
60
+ if (error.code !== "ESRCH")
61
+ throw error;
62
+ }
63
+ }
64
+ function signalExactProcessGroup(pid, signal) {
65
+ try {
66
+ process.kill(-pid, signal);
67
+ }
68
+ catch (error) {
69
+ if (error.code !== "ESRCH")
70
+ throw error;
71
+ }
72
+ }
73
+ async function waitForExit(pid, timeoutMs) {
74
+ const deadline = Date.now() + timeoutMs;
75
+ while (Date.now() < deadline && processExists(pid)) {
76
+ await new Promise(resolve => setTimeout(resolve, 25));
77
+ }
78
+ }
@@ -0,0 +1,57 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ export type HostProcessSupervisorStrategy = "windows_job_object" | "unix_process_group";
3
+ export interface HostProcessSupervisionRequest {
4
+ readonly command: string;
5
+ readonly args: readonly string[];
6
+ readonly cwd: string;
7
+ readonly env?: Readonly<Record<string, string>>;
8
+ }
9
+ export interface HostProcessSupervisedLaunch {
10
+ readonly command: string;
11
+ readonly args: readonly string[];
12
+ readonly detached: boolean;
13
+ readonly supervisorId: string;
14
+ readonly strategy: HostProcessSupervisorStrategy;
15
+ readonly metadataPath: string;
16
+ readonly payloadPath?: string;
17
+ readonly readyPath?: string;
18
+ readonly startGrantPath?: string;
19
+ readonly commandDigest: string;
20
+ }
21
+ export interface HostProcessProviderReady {
22
+ readonly schemaVersion: 1;
23
+ readonly supervisorId: string;
24
+ readonly rootPid?: number;
25
+ readonly rootIdentity?: string | null;
26
+ readonly providerPid: number;
27
+ readonly providerCreatedAt: string;
28
+ readonly providerIdentity?: string | null;
29
+ readonly startAttempt: number;
30
+ }
31
+ export interface HostProcessSupervisorMetadata {
32
+ readonly schemaVersion: 1;
33
+ readonly supervisorId: string;
34
+ readonly strategy: HostProcessSupervisorStrategy;
35
+ readonly rootPid: number;
36
+ readonly rootCreatedAt: string;
37
+ readonly rootIdentity?: string | null;
38
+ readonly commandDigest: string;
39
+ readonly worktreePath: string;
40
+ readonly runtimeLeaseId: string;
41
+ readonly dispatchAttemptId: string;
42
+ readonly fencingToken: string;
43
+ readonly bridgePid: number;
44
+ readonly providerPid?: number;
45
+ readonly providerCreatedAt?: string;
46
+ readonly providerIdentity?: string | null;
47
+ readonly providerStartAttempt?: number;
48
+ }
49
+ export declare function resolveHostProcessSupervisedLaunch(request: HostProcessSupervisionRequest, resolvedCommand: string, resolvedArgs: readonly string[], platform?: NodeJS.Platform): HostProcessSupervisedLaunch | null;
50
+ export declare function persistHostProcessSupervisorMetadata(launch: HostProcessSupervisedLaunch, request: HostProcessSupervisionRequest, rootPid: number, rootCreatedAt: string, providerReady?: HostProcessProviderReady): HostProcessSupervisorMetadata;
51
+ /**
52
+ * Captures an OS-issued process birth marker. Recovery compares this marker
53
+ * before signalling a persisted PID so PID reuse can never redirect cleanup
54
+ * at an unrelated process.
55
+ */
56
+ export declare function captureHostProcessIdentity(pid: number, platform?: NodeJS.Platform): string | null;
57
+ export declare function terminateHostProcessTree(child: ChildProcess, launch: HostProcessSupervisedLaunch | null, graceMs?: number): Promise<void>;
@@ -0,0 +1,282 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const windowsJobSupervisorPath = fileURLToPath(new URL("../../../runtime/windows-job-supervisor.ps1", import.meta.url));
7
+ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, resolvedArgs, platform = process.platform) {
8
+ const env = request.env;
9
+ if (env?.FORGE_RUNTIME_MODE !== "host")
10
+ return null;
11
+ const supervisorId = requiredEnvironment(env, "FORGE_PROCESS_SUPERVISOR_ID");
12
+ const metadataPath = requiredEnvironment(env, "FORGE_PROCESS_SUPERVISOR_METADATA_PATH");
13
+ const logDirectory = requiredEnvironment(env, "FORGE_LOG_DIR");
14
+ assertMetadataPath(logDirectory, metadataPath);
15
+ const commandDigest = createHash("sha256")
16
+ .update(JSON.stringify([resolvedCommand, ...resolvedArgs]))
17
+ .digest("hex");
18
+ if (platform === "win32") {
19
+ if (!fs.existsSync(windowsJobSupervisorPath)) {
20
+ throw new Error(`host_process_supervisor_missing: ${windowsJobSupervisorPath}`);
21
+ }
22
+ const payloadPath = `${metadataPath}.launch.json`;
23
+ const readyPath = `${metadataPath}.provider-ready.json`;
24
+ const startGrantPath = `${metadataPath}.provider-start-granted`;
25
+ fs.writeFileSync(payloadPath, `${JSON.stringify({
26
+ command: resolvedCommand,
27
+ args: resolvedArgs,
28
+ cwd: request.cwd,
29
+ parentPid: process.pid,
30
+ supervisorId,
31
+ readyPath,
32
+ startGrantPath,
33
+ })}\n`, {
34
+ encoding: "utf8",
35
+ flag: "wx",
36
+ mode: 0o600,
37
+ });
38
+ return {
39
+ command: windowsPowerShellPath(),
40
+ args: [
41
+ "-NoLogo",
42
+ "-NoProfile",
43
+ "-NonInteractive",
44
+ "-ExecutionPolicy",
45
+ "Bypass",
46
+ "-File",
47
+ windowsJobSupervisorPath,
48
+ "-Mode",
49
+ "Outer",
50
+ "-PayloadPath",
51
+ payloadPath,
52
+ ],
53
+ detached: false,
54
+ supervisorId,
55
+ strategy: "windows_job_object",
56
+ metadataPath,
57
+ payloadPath,
58
+ readyPath,
59
+ startGrantPath,
60
+ commandDigest,
61
+ };
62
+ }
63
+ return {
64
+ command: resolvedCommand,
65
+ args: resolvedArgs,
66
+ detached: true,
67
+ supervisorId,
68
+ strategy: "unix_process_group",
69
+ metadataPath,
70
+ commandDigest,
71
+ };
72
+ }
73
+ export function persistHostProcessSupervisorMetadata(launch, request, rootPid, rootCreatedAt, providerReady) {
74
+ if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
75
+ throw new Error("host_process_supervisor_invalid: supervised root PID was not assigned.");
76
+ }
77
+ const env = request.env ?? {};
78
+ const metadata = {
79
+ schemaVersion: 1,
80
+ supervisorId: launch.supervisorId,
81
+ strategy: launch.strategy,
82
+ rootPid,
83
+ rootCreatedAt,
84
+ rootIdentity: providerReady?.rootPid === rootPid
85
+ ? providerReady.rootIdentity ?? null
86
+ : captureHostProcessIdentity(rootPid),
87
+ commandDigest: launch.commandDigest,
88
+ worktreePath: request.cwd,
89
+ runtimeLeaseId: requiredEnvironment(env, "FORGE_RESOURCE_LEASE_ID"),
90
+ dispatchAttemptId: requiredEnvironment(env, "FORGE_DISPATCH_ATTEMPT_ID"),
91
+ fencingToken: requiredEnvironment(env, "FORGE_FENCING_TOKEN"),
92
+ bridgePid: process.pid,
93
+ providerPid: providerReady?.providerPid,
94
+ providerCreatedAt: providerReady?.providerCreatedAt,
95
+ providerIdentity: providerReady?.providerIdentity
96
+ ?? (providerReady ? captureHostProcessIdentity(providerReady.providerPid) : undefined),
97
+ providerStartAttempt: providerReady?.startAttempt,
98
+ };
99
+ fs.writeFileSync(launch.metadataPath, `${JSON.stringify(metadata)}\n`, {
100
+ encoding: "utf8",
101
+ flag: "wx",
102
+ mode: 0o600,
103
+ });
104
+ return metadata;
105
+ }
106
+ /**
107
+ * Captures an OS-issued process birth marker. Recovery compares this marker
108
+ * before signalling a persisted PID so PID reuse can never redirect cleanup
109
+ * at an unrelated process.
110
+ */
111
+ export function captureHostProcessIdentity(pid, platform = process.platform) {
112
+ if (!Number.isSafeInteger(pid) || pid <= 0)
113
+ return null;
114
+ try {
115
+ if (platform === "linux") {
116
+ const bootId = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
117
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
118
+ const close = stat.lastIndexOf(")");
119
+ const fields = close >= 0 ? stat.slice(close + 2).trim().split(/\s+/u) : [];
120
+ const startTicks = fields[19];
121
+ return bootId && startTicks && /^\d+$/u.test(startTicks)
122
+ ? `linux:${bootId}:${startTicks}`
123
+ : null;
124
+ }
125
+ if (platform === "win32") {
126
+ const output = execFileSync(windowsPowerShellPath(), [
127
+ "-NoLogo",
128
+ "-NoProfile",
129
+ "-NonInteractive",
130
+ "-Command",
131
+ `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
132
+ ], {
133
+ encoding: "utf8",
134
+ windowsHide: true,
135
+ timeout: 10_000,
136
+ stdio: ["ignore", "pipe", "ignore"],
137
+ }).trim();
138
+ return /^\d+$/u.test(output) ? `win32:${output}` : null;
139
+ }
140
+ // Other Unix variants do not expose a sufficiently strong birth marker
141
+ // through Node. Fail closed rather than trust a second-resolution ps time.
142
+ return null;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
149
+ if (!launch) {
150
+ try {
151
+ child.kill("SIGTERM");
152
+ }
153
+ catch {
154
+ // The process already settled.
155
+ }
156
+ return;
157
+ }
158
+ if (launch.strategy === "windows_job_object") {
159
+ const rootPid = child.pid ?? 0;
160
+ const metadata = readSupervisorMetadata(launch.metadataPath);
161
+ const rootIdentity = metadata?.rootPid === rootPid
162
+ ? metadata.rootIdentity ?? null
163
+ : captureHostProcessIdentity(rootPid, "win32");
164
+ try {
165
+ child.kill("SIGTERM");
166
+ }
167
+ catch {
168
+ // Closing the launcher is best effort when the Job Object already settled.
169
+ }
170
+ await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
171
+ if (exactProcessExists(rootPid, rootIdentity, "win32")) {
172
+ forceWindowsProcessTree(rootPid);
173
+ await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
174
+ }
175
+ if (exactProcessExists(rootPid, rootIdentity, "win32")) {
176
+ throw new Error("host_process_supervisor_termination_failed: supervised Windows root did not stop.");
177
+ }
178
+ const providerPid = metadata?.providerPid ?? 0;
179
+ const providerIdentity = metadata?.providerIdentity ?? null;
180
+ if (exactProcessExists(providerPid, providerIdentity, "win32")) {
181
+ forceWindowsProcessTree(providerPid);
182
+ await waitForExactProcessExit(providerPid, providerIdentity, graceMs, "win32");
183
+ }
184
+ if (exactProcessExists(providerPid, providerIdentity, "win32")) {
185
+ throw new Error("host_process_supervisor_termination_failed: supervised Windows provider tree did not stop.");
186
+ }
187
+ return;
188
+ }
189
+ const processGroupId = child.pid;
190
+ if (!processGroupId || !signalUnixProcessGroup(processGroupId, "SIGTERM"))
191
+ return;
192
+ await new Promise(resolve => setTimeout(resolve, graceMs));
193
+ if (unixProcessGroupExists(processGroupId)) {
194
+ signalUnixProcessGroup(processGroupId, "SIGKILL");
195
+ }
196
+ }
197
+ function requiredEnvironment(env, name) {
198
+ const value = env[name]?.trim();
199
+ if (!value)
200
+ throw new Error(`host_process_supervisor_invalid: ${name} is required.`);
201
+ return value;
202
+ }
203
+ function assertMetadataPath(logDirectory, metadataPath) {
204
+ const root = path.resolve(logDirectory);
205
+ const target = path.resolve(metadataPath);
206
+ if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
207
+ throw new Error("host_process_supervisor_invalid: metadata path must be inside FORGE_LOG_DIR.");
208
+ }
209
+ }
210
+ function windowsPowerShellPath() {
211
+ const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
212
+ const candidate = path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
213
+ return fs.existsSync(candidate) ? candidate : "powershell.exe";
214
+ }
215
+ function signalUnixProcessGroup(processGroupId, signal) {
216
+ try {
217
+ process.kill(-processGroupId, signal);
218
+ return true;
219
+ }
220
+ catch (error) {
221
+ if (error.code === "ESRCH")
222
+ return false;
223
+ throw error;
224
+ }
225
+ }
226
+ function unixProcessGroupExists(processGroupId) {
227
+ try {
228
+ process.kill(-processGroupId, 0);
229
+ return true;
230
+ }
231
+ catch (error) {
232
+ if (error.code === "ESRCH")
233
+ return false;
234
+ throw error;
235
+ }
236
+ }
237
+ function readSupervisorMetadata(metadataPath) {
238
+ try {
239
+ return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
240
+ }
241
+ catch {
242
+ return null;
243
+ }
244
+ }
245
+ function exactProcessExists(pid, identity, platform) {
246
+ if (!identity || !processExists(pid))
247
+ return false;
248
+ return captureHostProcessIdentity(pid, platform) === identity;
249
+ }
250
+ function processExists(pid) {
251
+ if (!Number.isSafeInteger(pid) || pid <= 0)
252
+ return false;
253
+ try {
254
+ process.kill(pid, 0);
255
+ return true;
256
+ }
257
+ catch (error) {
258
+ return error.code === "EPERM";
259
+ }
260
+ }
261
+ async function waitForExactProcessExit(pid, identity, timeoutMs, platform) {
262
+ const deadline = Date.now() + timeoutMs;
263
+ while (Date.now() < deadline && exactProcessExists(pid, identity, platform)) {
264
+ await new Promise(resolve => setTimeout(resolve, 25));
265
+ }
266
+ }
267
+ function forceWindowsProcessTree(pid) {
268
+ if (!Number.isSafeInteger(pid) || pid <= 0)
269
+ return;
270
+ const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
271
+ const taskkill = path.win32.join(systemRoot, "System32", "taskkill.exe");
272
+ try {
273
+ execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], {
274
+ windowsHide: true,
275
+ timeout: 5_000,
276
+ stdio: "ignore",
277
+ });
278
+ }
279
+ catch {
280
+ // A racing process exit is verified by the caller before it returns.
281
+ }
282
+ }
@@ -2,6 +2,7 @@ export interface ProcessRunRequest {
2
2
  readonly command: string;
3
3
  readonly args: readonly string[];
4
4
  readonly cwd: string;
5
+ readonly env?: NodeJS.ProcessEnv;
5
6
  }
6
7
  export interface ProcessRunResult {
7
8
  readonly exitCode: number;
@@ -5,6 +5,7 @@ export class NodeProcessRunner {
5
5
  return await new Promise((resolve, reject) => {
6
6
  const child = spawn(request.command, request.args, {
7
7
  cwd: request.cwd,
8
+ env: request.env,
8
9
  stdio: ["ignore", "pipe", "pipe"]
9
10
  });
10
11
  let stdout = "";
@@ -1,6 +1,9 @@
1
1
  const secretKeyPattern = /(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)/i;
2
2
  const attachmentContentKeyPattern = /^(textContent|contentBase64)$/i;
3
3
  const serializedAttachmentContentPattern = /("(?:textContent|contentBase64)"\s*:\s*")(?:(?:\\.)|[^"\\])*(")/gi;
4
+ const authorizationCredentialPattern = /(\bauthorization\s*[:=]\s*(?:bearer|basic)?\s*)([^"',\s]+)/gi;
5
+ const bearerCredentialPattern = /(\bbearer\s+)([^"',\s]+)/gi;
6
+ const secretAssignmentPattern = /(api[_-]?key|client[_-]?secret|password|secret|token)(\s*["']?\s*[:=]\s*["']?\s*)([^"',\s]+)/gi;
4
7
  export function redactSecrets(value, exactSecrets = []) {
5
8
  return redactValue(value, exactSecrets);
6
9
  }
@@ -12,7 +15,9 @@ export function redactText(value, exactSecrets = []) {
12
15
  }
13
16
  }
14
17
  return redacted
15
- .replace(/(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)(["':=\s]+)([^"',\s]+)/gi, "$1$2[REDACTED]")
18
+ .replace(authorizationCredentialPattern, "$1[REDACTED]")
19
+ .replace(bearerCredentialPattern, "$1[REDACTED]")
20
+ .replace(secretAssignmentPattern, "$1$2[REDACTED]")
16
21
  .replace(/(sk-[a-zA-Z0-9_-]{12,})/g, "[REDACTED]");
17
22
  }
18
23
  function redactValue(value, exactSecrets) {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.3-dev.d46a8e73d03d";
1
+ export declare const forgeBridgeVersion = "0.1.3-dev.eeefdb0d19c1";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.3-dev.d46a8e73d03d";
1
+ export const forgeBridgeVersion = "0.1.3-dev.eeefdb0d19c1";
@@ -1,9 +1,10 @@
1
1
  import type { DispatchResolvedArtifact } from "../session/session.types.js";
2
- export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[]): Promise<void>;
2
+ export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[], protectedRelativePaths?: ReadonlySet<string>): Promise<void>;
3
3
  export declare function filterArtifactsForLane(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): readonly DispatchResolvedArtifact[];
4
- export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact): Promise<void>;
4
+ export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact, protectedRelativePaths?: ReadonlySet<string>): Promise<void>;
5
5
  export declare function selectLaneCommand(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): DispatchResolvedArtifact | undefined;
6
6
  export declare function resolveCommandSlug(lane: string, phase: string): string;
7
+ export declare function artifactExecutionBody(artifact: DispatchResolvedArtifact): string;
7
8
  export declare function buildSpecBody(laneCommand: DispatchResolvedArtifact, workItem: {
8
9
  readonly id: string;
9
10
  readonly title: string;