Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Native Windows compatibility

Pullboard's private relay storage checks the actual NTFS owner and access-control
list on Windows. Synthetic Unix mode bits reported by Node are not used as NTFS
permissions. The current user must own the object and have read access; other
allowed principals are limited to SYSTEM and built-in Administrators. Reparse
points, null DACLs and failed permission queries are refused. New private files
also require a private parent directory before any bytes are written.

Configured gates, formatters and untimed shell commands use the Bash bundled with
Git for Windows, because Pullboard emits POSIX shell syntax. Commands and child
Node programs still run natively on Windows; WSL is not required. Missing Git Bash
is an explicit error.

The background view and other console children request hidden Windows consoles.
This prevents a new CMD/Git window for each view refresh while preserving output,
exit status, callbacks, environment, timeouts and existing child-process guards.

Run the focused compatibility checks from a native Windows checkout:

```powershell
node --test test/windows-storage.test.js test/windows-processes.test.js
```

This is a partial portability repair. Timed shell execution, POSIX process-group
cleanup and the complete test/landing workflow still need Windows-specific work.
These changes do not claim that the full gate passes on Windows.
2 changes: 1 addition & 1 deletion src/activity-inbox.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Local delivery state and real Git observations for the existing inbox [R2,N26]. */
import { randomUUID } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { execFileSync } from './windows-child-process.js';
import {
readFileSync,
renameSync,
Expand Down
2 changes: 1 addition & 1 deletion src/affected-tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Select committed Node test files through import edges; uncertainty requires the full gate [V4,C7]. */
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { posix } from 'node:path';
import { fileURLToPath } from 'node:url';
import { pathToFileURL } from 'node:url';
Expand Down
2 changes: 1 addition & 1 deletion src/agent-hook.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Native hooks provide bounded context and warnings, never a blocking decision [I6]. */
import { spawn } from 'node:child_process';
import { spawn } from './windows-child-process.js';
import { fileURLToPath } from 'node:url';
import { AGENT_HOOK_EVENTS } from './agent-hooks.js';

Expand Down
2 changes: 1 addition & 1 deletion src/approved-rows.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import {
parseSpec,
SIGNOFFS_FILE,
Expand Down
2 changes: 1 addition & 1 deletion src/background-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* left alone, a busy port is one line and never a failure, and the machine setting
* `view.background` (on unless set to false) turns it off.
*/
import { spawn } from 'node:child_process';
import { spawn } from './windows-child-process.js';
import {
closeSync,
mkdirSync,
Expand Down
2 changes: 1 addition & 1 deletion src/board-write-lock.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Advisory process-owned write records; never part of native board state or lock authority [C7,P4]. */
import { randomUUID } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { mkdirSync } from 'node:fs';
import { writeFileSync } from 'node:fs';
import { unlinkSync } from 'node:fs';
Expand Down
7 changes: 7 additions & 0 deletions src/browser-admission.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const SAFE_CHILD_PROCESS_MODULES = new Map([
['src/git.js', 'runs Git commands for repository inspection'],
['src/module-imports.js', 'runs the Node parser in an isolated child'],
['src/resources.js', 'reads process identity with /bin/ps'],
['src/windows-child-process.js', 'delegates child calls with hidden Windows consoles; importing it starts nothing'],
]);
/** Reviewed generic executors and fixed Git/Node calls retain prior import behavior; all other calls stay conservative. */
const REVIEWED_EXECUTORS = new Map([
Expand Down Expand Up @@ -592,6 +593,12 @@ async function testsNeedBrowserInTree(root, files, committed = null, onFallback,
? committedTarget(committed, paths[index], request)
: await localTarget(root, paths[index], request);
if (!target) return browserFallback(onFallback, paths[index], `import ${request} is missing or ambiguous`);
if (target === 'src/windows-child-process.js') {
if (byLaunch) starts[index] = true;
else if (!SAFE_CHILD_PROCESS_MODULES.has(paths[index])) {
return browserFallback(onFallback, paths[index], 'child_process use has no reviewed path-and-reason exception');
}
}
if (target === FIXTURE_CHILD && !safeFixtureChildImport(sources[index], request)) {
if (byLaunch) starts[index] = true;
else return browserFallback(onFallback, paths[index], 'fixture-child import exposes a generic caller-selected process target');
Expand Down
2 changes: 1 addition & 1 deletion src/check-baseline-worker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Detached baseline measurements and durable completion receipts use the ordered board engine [V2,H16]. */
import { spawn } from 'node:child_process';
import { spawn } from './windows-child-process.js';
import {
existsSync,
mkdirSync,
Expand Down
2 changes: 1 addition & 1 deletion src/check-baseline.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from './timing.js';
export { provingCheck, sayCheckBaseline } from './check-baseline-format.js';
/** Observe a proposed check on an isolated copy of main before recording its move [V2,H16]. */
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { randomUUID } from 'node:crypto';
import {
mkdtempSync,
Expand Down
4 changes: 2 additions & 2 deletions src/child-journal-preload.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** The SSH runner's spawn-journal preload, built from its directory alone so its content proves what it is [C7]. */
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

Expand Down Expand Up @@ -44,7 +44,7 @@ export function childJournalPreloadSource(directory) {
const overflowPath = join(journalDirectory, 'overflow');
return `
import diagnostics from 'node:diagnostics_channel';
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { closeSync, openSync, readFileSync, statSync, writeSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { createHash, randomUUID } from 'node:crypto';
Expand Down
2 changes: 1 addition & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
import {
spawn,
spawnSync,
} from 'node:child_process';
} from './windows-child-process.js';
import {
existsSync,
mkdirSync,
Expand Down
8 changes: 6 additions & 2 deletions src/code-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import {
execFileSync,
} from 'node:child_process';
} from './windows-child-process.js';
import { createHash } from 'node:crypto';
import {
existsSync,
Expand Down Expand Up @@ -227,7 +227,11 @@ export function scanSource(rel, src) {
const importMap = new Map(); for (const im of imports) for (const nm of im.names) if (nm.local) importMap.set(nm.local, { from: im.from, imported: nm.imported });
const topNames = new Set(fns.map((f) => f.name));
const childNames = new Set(); const fsNames = new Set();
for (const im of imports) { if (im.from === 'node:child_process' || im.from === 'child_process') im.names.forEach((x) => childNames.add(x.local)); if (/^(node:)?fs(\/promises)?$/.test(im.from)) im.names.forEach((x) => fsNames.add(x.local)); }
for (const im of imports) {
const windowsProcess = im.from.startsWith('.') && posix.normalize(posix.join(posix.dirname(rel), im.from)) === 'src/windows-child-process.js';
if (im.from === 'node:child_process' || im.from === 'child_process' || windowsProcess) im.names.forEach((x) => childNames.add(x.local));
if (/^(node:)?fs(\/promises)?$/.test(im.from)) im.names.forEach((x) => fsNames.add(x.local));
}
const netNames = new Set(); for (const im of imports) if (/^(node:)?(https?|http2|net|tls|dgram)$/.test(im.from)) im.names.forEach((x) => netNames.add(x.local));
const childExecImported = [...imports].some((im) => /child_process/.test(im.from) && im.names.some((x) => x.imported === 'exec'));

Expand Down
2 changes: 1 addition & 1 deletion src/commands/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
} from '../view-session.js';
import {
spawn,
} from 'node:child_process';
} from '../windows-child-process.js';
import {
existsSync,
} from 'node:fs';
Expand Down
2 changes: 1 addition & 1 deletion src/commit-signoff.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Check outside contributors' author-matching DCO trailers against committed policy [C1,C7]. */
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import {
existsSync,
readFileSync,
Expand Down
2 changes: 1 addition & 1 deletion src/committed-file-reader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Read pinned committed text files through one fresh Git batch, never a candidate's working bytes [N19,D1]. */
import { spawn } from 'node:child_process';
import { spawn } from './windows-child-process.js';
import { cleanGitEnvironment } from './git.js';
import { Refused } from './refused.js';

Expand Down
2 changes: 1 addition & 1 deletion src/doctrine-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* doctrine: accepted main's at submit, HEAD's for `pullboard doctrine check`, never a working-tree
* edit.
*/
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import {
mkdtempSync,
rmSync,
Expand Down
2 changes: 1 addition & 1 deletion src/evidence.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Read-only links between spec rows, their tests, board items and sign-offs. */
import { execFileSync } from 'node:child_process';
import { execFileSync } from './windows-child-process.js';
import { resolve } from 'node:path';

/**
Expand Down
5 changes: 3 additions & 2 deletions src/gate.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { commandShell } from './windows-shell.js';
/**
* The gate (V4, C3, Q1, Q2, V18): the repo's check command and shared queue for gates and item checks. A green
* run over a committed tree, with nothing untracked and nothing changed while it ran, leaves that
Expand All @@ -12,7 +13,7 @@ import {
import {
spawn,
spawnSync,
} from 'node:child_process';
} from './windows-child-process.js';
import {
createHash,
randomUUID,
Expand Down Expand Up @@ -362,7 +363,7 @@ export async function runProfiledShellAsync(root, command, { waitMs = 0, gateSlo
const runtimes = captureTestRuntimes({ ...gitChildEnv(root), ...inherited });
const env = runtimes.env;
const environment = executionEnvironment();
const child = spawn(`(\n${command}\n) 2>&1`, { cwd: root, env, shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
const child = spawn(`(\n${command}\n) 2>&1`, { cwd: root, env, shell: commandShell(), stdio: ['ignore', 'pipe', 'pipe'] });
const chunks = [];
let bytes = 0;
let overflow = false;
Expand Down
2 changes: 1 addition & 1 deletion src/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import {
execFileSync,
spawnSync,
} from 'node:child_process';
} from './windows-child-process.js';
import { AsyncLocalStorage } from 'node:async_hooks';
import { createHash } from 'node:crypto';
import {
Expand Down
2 changes: 1 addition & 1 deletion src/guard-revert.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Two forms: `guard: <path>:<from>-<to>` (lines of the submitted file, removed in the scratch tree) and
* `guard: patch <path>` (a patch file in the submitted commit, applied in reverse).
*/
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import {
existsSync,
readFileSync,
Expand Down
2 changes: 1 addition & 1 deletion src/gui-admission.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** Explicit test-file desktop requirements, independent of Chrome command-line flags [C7,V4]. */
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { explicitCheckFiles } from './browser-admission.js';

/** Only an explicit file annotation requests a GUI; --headless does not establish this capability. */
Expand Down
2 changes: 1 addition & 1 deletion src/gui-session.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** Read this execution user's Darwin GUI-domain availability without environment guesses. */
import {
spawnSync,
} from 'node:child_process';
} from './windows-child-process.js';

/**
* Return a fresh GUI-domain observation: true, known absent false, or unknown null.
Expand Down
2 changes: 1 addition & 1 deletion src/health.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { localDependencyIds } from './item-dependencies.js';
* It reads the board through a read-only SQLite connection and asks git only questions that leave
* every checkout alone: merge-tree writes a tree object and never an index or a working tree.
*/
import { execFileSync } from 'node:child_process';
import { execFileSync } from './windows-child-process.js';
import { existsSync } from 'node:fs';
import { missedJobFindings } from './recurring-jobs.js';
import {
Expand Down
2 changes: 1 addition & 1 deletion src/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* the spec, and every id a commit cites, must still be there, so a commit or an item that cites a
* row keeps its meaning however the spec changes.
*/
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { parseCommitMessage } from './commit-message.js';
import {
ID_RE,
Expand Down
5 changes: 3 additions & 2 deletions src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { commandShell } from './windows-shell.js';
import { BOARD_PROOF_GUIDANCE } from './agent-proof-guidance.js';
/**
* The checks git runs (C1–C3, L3, L4): the commit message cites the spec, a lane's worktree stays in
Expand All @@ -7,7 +8,7 @@ import { BOARD_PROOF_GUIDANCE } from './agent-proof-guidance.js';
import {
execFileSync,
spawnSync,
} from 'node:child_process';
} from './windows-child-process.js';
import { gitBoardDataPush } from './git-sync-format.js';
export { gitBoardDataPush };
import { hostname } from 'node:os';
Expand Down Expand Up @@ -706,7 +707,7 @@ export function applyFixers(root, fixers) {
const result = spawnSync(`${fixer.run} ${files.map(shellQuote).join(' ')}`, {
cwd: root,
env: gitChildEnv(root),
shell: true,
shell: commandShell(),
encoding: 'utf8',
timeout: 120_000,
});
Expand Down
2 changes: 1 addition & 1 deletion src/init-finish.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
createHash,
randomUUID,
} from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import {
lstatSync,
mkdirSync,
Expand Down
2 changes: 1 addition & 1 deletion src/landing-candidates.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Isolate speculative landing gates while their one parent retains the board lease [B3,V4]. */
import { spawn } from 'node:child_process';
import { spawn } from './windows-child-process.js';
import {
join,
resolve,
Expand Down
2 changes: 1 addition & 1 deletion src/landing-shards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import {
spawn,
spawnSync,
} from 'node:child_process';
} from './windows-child-process.js';
import {
existsSync,
lstatSync,
Expand Down
2 changes: 1 addition & 1 deletion src/landing-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import {
spawn,
spawnSync,
} from 'node:child_process';
} from './windows-child-process.js';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as store from './board.js';
Expand Down
2 changes: 1 addition & 1 deletion src/landing-worktree-prune.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Best-effort local pruning follows an ordered retirement receipt, never changes the board [R3,C7]. */
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { randomUUID } from 'node:crypto';
import {
existsSync,
Expand Down
7 changes: 4 additions & 3 deletions src/main-red.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { hasPrivatePermissions, writePrivateFileSync } from './windows-private-storage.js';
/** Fresh per-file main comparisons for inherited submit reds [V1]. */
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { randomUUID } from 'node:crypto';
import {
lstatSync,
Expand Down Expand Up @@ -99,7 +100,7 @@ function cacheFile(root) {
function readCache(file) {
try {
const stat = lstatSync(file);
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.size > 4 * 1024 * 1024) return { version: 1, entries: {} };
if (!stat.isFile() || stat.isSymbolicLink() || !hasPrivatePermissions(file, stat) || stat.size > 4 * 1024 * 1024) return { version: 1, entries: {} };
const value = JSON.parse(readFileSync(file, 'utf8'));
if (value.version !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)) return { version: 1, entries: {} };
const entries = {};
Expand Down Expand Up @@ -127,7 +128,7 @@ function writeCache(file, value) {
mkdirSync(directory, { recursive: true, mode: 0o700 });
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
try {
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
writePrivateFileSync(Refused, temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
renameSync(temporary, file);
} finally {
rmSync(temporary, { force: true });
Expand Down
2 changes: 1 addition & 1 deletion src/mechanical-merge.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from 'node:fs';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import { join } from 'node:path';
import { cleanGitEnvironment } from './git.js';

Expand Down
2 changes: 1 addition & 1 deletion src/merge-only-source.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Bounded Git proof that a resubmission adds only trunk and actual conflict resolutions [V1,V4]. */
import { createHash } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import {
cleanGitEnvironment,
mainCheckout,
Expand Down
2 changes: 1 addition & 1 deletion src/module-imports.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Parse import edges without evaluating repository code, using V8 rather than a JavaScript grammar guess [V4,C7]. */
import { spawnSync } from 'node:child_process';
import { spawnSync } from './windows-child-process.js';
import {
mkdtempSync,
rmSync,
Expand Down
2 changes: 1 addition & 1 deletion src/native-user-directory.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Read the current account's native macOS working directory without browser-specific policy [C7]. */
import { execFileSync } from 'node:child_process';
import { execFileSync } from './windows-child-process.js';

/** Keep the fixed getconf invocation separate from consumers' application-specific path names. */
export function nativeUserDirectory() {
Expand Down
Loading