sim: merge PR #17191

This commit is contained in:
Brennan Benson
2026-08-30 15:53:51 -07:00
9 changed files with 360 additions and 38 deletions
+141 -30
View File
@@ -1,5 +1,5 @@
diff --git a/binding.gyp b/binding.gyp
index 5f63978b07ab50aaf7523219a2170ec737a6b5db..bbd9e06136e8922f40b5779e35d4fc835f1479ab 100644
index 5f63978..837360b 100644
--- a/binding.gyp
+++ b/binding.gyp
@@ -1,13 +1,18 @@
@@ -8,7 +8,7 @@ index 5f63978b07ab50aaf7523219a2170ec737a6b5db..bbd9e06136e8922f40b5779e35d4fc83
'dependencies': [
- "<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
+ "<!(node -p \"require.resolve('node-addon-api/node_addon_api.gyp')\"):node_addon_api_except",
],
+ ],
+ # Orca: GCC 9 (Ubuntu 20.04 floor) accepts C++20 as gnu++2a, but rejects
+ # the newer gnu++20 spelling emitted by Node 24's gyp flags.
+ 'cflags_cc!': [
@@ -16,7 +16,7 @@ index 5f63978b07ab50aaf7523219a2170ec737a6b5db..bbd9e06136e8922f40b5779e35d4fc83
+ ],
+ 'cflags_cc': [
+ '-std=gnu++2a'
+ ],
],
'conditions': [
['OS=="win"', {
- 'msvs_configuration_attributes': {
@@ -25,7 +25,7 @@ index 5f63978b07ab50aaf7523219a2170ec737a6b5db..bbd9e06136e8922f40b5779e35d4fc83
'msvs_settings': {
'VCCLCompilerTool': {
'AdditionalOptions': [
@@ -42,32 +39,6 @@
@@ -42,32 +47,6 @@
'-lshlwapi'
],
},
@@ -58,7 +58,7 @@ index 5f63978b07ab50aaf7523219a2170ec737a6b5db..bbd9e06136e8922f40b5779e35d4fc83
]
}, { # OS!="win"
'targets': [
@@ -88,6 +85,16 @@
@@ -88,6 +67,16 @@
'libraries!': [
'-lutil'
]
@@ -76,7 +76,7 @@ index 5f63978b07ab50aaf7523219a2170ec737a6b5db..bbd9e06136e8922f40b5779e35d4fc83
]
}
diff --git a/deps/winpty/src/winpty.gyp b/deps/winpty/src/winpty.gyp
index 1ac5758bedd8cf54f32280dea4e4aeb5afdee30d..e619813759c6f14694838bdfbd0ea5f8360130ef 100644
index 1ac5758..e619813 100644
--- a/deps/winpty/src/winpty.gyp
+++ b/deps/winpty/src/winpty.gyp
@@ -10,7 +10,7 @@
@@ -118,7 +118,7 @@ index 1ac5758bedd8cf54f32280dea4e4aeb5afdee30d..e619813759c6f14694838bdfbd0ea5f8
# Specify this setting here to override a setting from somewhere
# else, such as node's common.gypi.
diff --git a/lib/conpty_console_list_agent.js b/lib/conpty_console_list_agent.js
index 8c4fca9022a6d6f015bca87f61625cde2278f428..0a01730616488119aa21ef441cf3c441e02a974c 100644
index 8c4fca9..0a01730 100644
--- a/lib/conpty_console_list_agent.js
+++ b/lib/conpty_console_list_agent.js
@@ -10,7 +10,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
@@ -139,7 +139,7 @@ index 8c4fca9022a6d6f015bca87f61625cde2278f428..0a01730616488119aa21ef441cf3c441
//# sourceMappingURL=conpty_console_list_agent.js.map
\ No newline at end of file
diff --git a/lib/unixTerminal.js b/lib/unixTerminal.js
index 1ec12f796a822c78fba9ad7f6448c3987e325c23..cec8b67aef02f8199e5606a0d257088bf1865877 100644
index 1ec12f7..526a2c3 100644
--- a/lib/unixTerminal.js
+++ b/lib/unixTerminal.js
@@ -28,8 +28,12 @@ var native = utils_1.loadNativeModule('pty');
@@ -157,8 +157,29 @@ index 1ec12f796a822c78fba9ad7f6448c3987e325c23..cec8b67aef02f8199e5606a0d257088b
var DEFAULT_FILE = 'sh';
var DEFAULT_NAME = 'xterm';
var DESTROY_SOCKET_TIMEOUT_MS = 200;
@@ -127,6 +131,8 @@ var UnixTerminal = /** @class */ (function (_super) {
_this._pid = term.pid;
_this._fd = term.fd;
_this._pty = term.pty;
+ // Orca (STA-3631): absent on unpatched builds, so readers treat undefined as "unknown".
+ _this._tccDisclaim = term.tccDisclaim;
_this._file = file;
_this._name = name;
_this._readable = true;
@@ -166,6 +172,11 @@ var UnixTerminal = /** @class */ (function (_super) {
enumerable: false,
configurable: true
});
+ Object.defineProperty(UnixTerminal.prototype, "tccDisclaim", {
+ get: function () { return this._tccDisclaim; },
+ enumerable: false,
+ configurable: true
+ });
/**
* openpty
*/
diff --git a/src/conpty_console_list_agent.ts b/src/conpty_console_list_agent.ts
index 181ccabbbe9c4948a9725fb1db907a68e9de01fc..67f31facf85562b67adbfbd04ce28ddd8eeb4a79 100644
index 181ccab..67f31fa 100644
--- a/src/conpty_console_list_agent.ts
+++ b/src/conpty_console_list_agent.ts
@@ -10,6 +10,12 @@ import { loadNativeModule } from './utils';
@@ -176,7 +197,7 @@ index 181ccabbbe9c4948a9725fb1db907a68e9de01fc..67f31facf85562b67adbfbd04ce28ddd
process.send!({ consoleProcessList });
process.exit(0);
diff --git a/src/unix/pty.cc b/src/unix/pty.cc
index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d15c4dd44 100644
index 7b4b9e1..9193e6a 100644
--- a/src/unix/pty.cc
+++ b/src/unix/pty.cc
@@ -23,7 +23,9 @@
@@ -189,7 +210,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d
#include <thread>
#include <sys/types.h>
@@ -47,6 +49,25 @@
@@ -47,6 +49,68 @@
#include <termios.h>
#endif
@@ -211,11 +232,54 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d
+__asm__(".symver pthread_sigmask,pthread_sigmask@" ORCA_GLIBC_COMPAT_VERSION);
+# endif
+#endif
+
+/* Orca (STA-3631): macOS 26 no longer breaks TCC responsibility inheritance at
+ * an intermediate login(1) session, so every terminal child is attributed to the
+ * app bundle. Disclaim responsibility on the spawn attributes instead — the same
+ * mechanism Terminal.app and iTerm2 rely on. Resolved at runtime because the
+ * symbol is SPI and absent from the public SDK headers. */
+#if defined(__APPLE__)
+#include <dlfcn.h>
+#include <spawn.h>
+
+/* Verdict codes surfaced to JS; keep in sync with lib/unixTerminal.js. */
+#define ORCA_TCC_DISCLAIM_UNKNOWN 0
+#define ORCA_TCC_DISCLAIM_APPLIED 1
+#define ORCA_TCC_DISCLAIM_UNSUPPORTED 2
+#define ORCA_TCC_DISCLAIM_FAILED 3
+
+typedef int (*orca_tcc_setdisclaim_fn)(posix_spawnattr_t*, int);
+
+static orca_tcc_setdisclaim_fn
+orca_tcc_resolve_setdisclaim(void) {
+ static orca_tcc_setdisclaim_fn resolved = NULL;
+ static bool attempted = false;
+ if (!attempted) {
+ attempted = true;
+ resolved = (orca_tcc_setdisclaim_fn)dlsym(
+ RTLD_DEFAULT, "responsibility_spawnattrs_setdisclaim");
+ }
+ return resolved;
+}
+
+/* Returns one of the ORCA_TCC_DISCLAIM_* verdicts. Never reports APPLIED unless
+ * the SPI both resolved and returned success, so callers can say "unknown"
+ * rather than assume isolation that may not exist. */
+static int
+orca_tcc_apply_disclaim(posix_spawnattr_t* attrs) {
+ orca_tcc_setdisclaim_fn setdisclaim = orca_tcc_resolve_setdisclaim();
+ if (setdisclaim == NULL) {
+ return ORCA_TCC_DISCLAIM_UNSUPPORTED;
+ }
+ return setdisclaim(attrs, 1) == 0 ? ORCA_TCC_DISCLAIM_APPLIED
+ : ORCA_TCC_DISCLAIM_FAILED;
+}
+#endif
+
/* Some platforms name VWERASE and VDISCARD differently */
#if !defined(VWERASE) && defined(VWERSE)
#define VWERASE VWERSE
@@ -237,13 +258,23 @@ pty_getproc(int, char *);
@@ -237,13 +301,24 @@ pty_getproc(int, char *);
#endif
#if defined(__APPLE__) || defined(__OpenBSD__)
@@ -236,11 +300,12 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d
int* master,
pid_t* pid,
- int* err);
+ pty_spawn_error* err);
+ pty_spawn_error* err,
+ int* tcc_disclaim);
#endif
struct DelBuf {
@@ -367,10 +398,11 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
@@ -367,10 +442,13 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
argv[i + 3] = strdup(arg.c_str());
}
@@ -249,14 +314,26 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d
- if (err != 0) {
- throw Napi::Error::New(napiEnv, "posix_spawnp failed.");
+ pty_spawn_error spawn_error = { NULL, 0, "", "" };
+ pty_posix_spawn(argv, env, term, &winp, &master, &pid, &spawn_error);
+ int tcc_disclaim = ORCA_TCC_DISCLAIM_UNKNOWN;
+ pty_posix_spawn(argv, env, term, &winp, &master, &pid, &spawn_error,
+ &tcc_disclaim);
+ if (spawn_error.errnum != 0) {
+ std::string spawn_message = pty_format_spawn_error(spawn_error);
+ throw Napi::Error::New(napiEnv, spawn_message);
}
if (pty_nonblock(master) == -1) {
throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
@@ -684,15 +716,73 @@ pty_getproc(int fd, char *tty) {
@@ -452,6 +530,9 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
obj.Set("fd", Napi::Number::New(napiEnv, master));
obj.Set("pid", Napi::Number::New(napiEnv, pid));
obj.Set("pty", Napi::String::New(napiEnv, ptsname(master)));
+#if defined(__APPLE__)
+ obj.Set("tccDisclaim", Napi::Number::New(napiEnv, tcc_disclaim));
+#endif
// Set up process exit callback.
Napi::Function cb = info[10].As<Napi::Function>();
@@ -684,15 +765,74 @@ pty_getproc(int fd, char *tty) {
#endif
#if defined(__APPLE__)
@@ -320,7 +397,8 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d
pid_t* pid,
- int* err) {
- int low_fds[3];
+ pty_spawn_error* err) {
+ pty_spawn_error* err,
+ int* tcc_disclaim) {
+ int low_fds[3] = {-1, -1, -1};
size_t count = 0;
+ int res = -1;
@@ -332,25 +410,25 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d
for (; count < 3; count++) {
low_fds[count] = posix_openpt(O_RDWR);
@@ -706,80 +796,118 @@ pty_posix_spawn(char** argv, char** env,
@@ -706,80 +846,122 @@ pty_posix_spawn(char** argv, char** env,
POSIX_SPAWN_SETSID;
*master = posix_openpt(O_RDWR);
if (*master == -1) {
- return;
+ pty_set_spawn_error(err, "posix_openpt", errno);
+ goto done;
+ }
+
+ res = grantpt(*master);
+ if (res == -1) {
+ pty_set_spawn_error(err, "grantpt", errno);
+ goto done;
}
- int res = grantpt(*master) || unlockpt(*master);
+ res = grantpt(*master);
+ res = unlockpt(*master);
if (res == -1) {
- return;
+ pty_set_spawn_error(err, "grantpt", errno);
+ goto done;
+ }
+
+ res = unlockpt(*master);
+ if (res == -1) {
+ pty_set_spawn_error(err, "unlockpt", errno);
+ goto done;
}
@@ -411,14 +489,18 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d
+ res = posix_spawnattr_init(&attrs);
+ if (res != 0) {
+ pty_set_spawn_error(err, "posix_spawnattr_init", res);
+ goto done;
+ }
goto done;
}
+ attrs_initialized = true;
+ res = posix_spawnattr_setflags(&attrs, flags);
+ if (res != 0) {
+ pty_set_spawn_error(err, "posix_spawnattr_setflags", res);
goto done;
}
+ goto done;
+ }
+
+ /* Orca (STA-3631): a failed disclaim is reported, never fatal — a shell with
+ * collapsed attribution still beats no shell. */
+ *tcc_disclaim = orca_tcc_apply_disclaim(&attrs);
sigset_t signal_set;
/* Reset all signal the child to their default behavior */
@@ -475,8 +557,37 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d
}
}
#endif
diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts
index 98733dc..6077b46 100644
--- a/src/unixTerminal.ts
+++ b/src/unixTerminal.ts
@@ -26,6 +26,7 @@ const DESTROY_SOCKET_TIMEOUT_MS = 200;
export class UnixTerminal extends Terminal {
protected _fd: number;
protected _pty: string;
+ protected _tccDisclaim: number | undefined;
protected _file: string;
protected _name: string;
@@ -147,6 +148,8 @@ export class UnixTerminal extends Terminal {
this._pid = term.pid;
this._fd = term.fd;
this._pty = term.pty;
+ // Orca (STA-3631): absent on unpatched builds, so readers treat undefined as "unknown".
+ this._tccDisclaim = term.tccDisclaim;
this._file = file;
this._name = name;
@@ -172,6 +175,7 @@ export class UnixTerminal extends Terminal {
/* Accessors */
get fd(): number { return this._fd; }
+ get tccDisclaim(): number | undefined { return this._tccDisclaim; }
get ptsName(): string { return this._pty; }
/**
diff --git a/src/win/conpty.cc b/src/win/conpty.cc
index 7b286d3d644c26141df516929703aa6e129df4b2..ec6bf3932c65b89c013ff133dc6bf46a6a4082ce 100644
index 7b286d3..ec6bf39 100644
--- a/src/win/conpty.cc
+++ b/src/win/conpty.cc
@@ -18,6 +18,7 @@
+3 -3
View File
@@ -115,7 +115,7 @@ patchedDependencies:
'@xterm/addon-webgl@0.20.0-beta.286': 2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91
'@xterm/xterm@6.1.0-beta.287': 0139c8795bd015a024045e24fa5dfb6fdadb17912c495f9aa9594e50061a0356
lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
node-pty@1.1.0: 9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17
node-pty@1.1.0: e09d7ecc093abdce946aab18c025faa1ae4ab4b329be1d6be4829090740cd4ba
importers:
@@ -156,7 +156,7 @@ importers:
version: 3.3.1
node-pty:
specifier: ^1.1.0
version: 1.1.0(patch_hash=9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17)
version: 1.1.0(patch_hash=e09d7ecc093abdce946aab18c025faa1ae4ab4b329be1d6be4829090740cd4ba)
posthog-node:
specifier: ^5.33.3
version: 5.33.3
@@ -12194,7 +12194,7 @@ snapshots:
node-int64@0.4.0: {}
node-pty@1.1.0(patch_hash=9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17):
node-pty@1.1.0(patch_hash=e09d7ecc093abdce946aab18c025faa1ae4ab4b329be1d6be4829090740cd4ba):
dependencies:
node-addon-api: 7.1.1
+4 -1
View File
@@ -298,7 +298,10 @@ async function main(): Promise<void> {
...(process.platform === 'darwin'
? {
onMacosTccSpawnStrategy: (strategy) =>
daemonLog.log('macos-tcc-pty-spawn', { strategy })
daemonLog.log('macos-tcc-pty-spawn', {
wrapper: strategy.wrapper,
attribution: strategy.attribution
})
}
: {})
}),
+4 -1
View File
@@ -130,7 +130,10 @@ describe('createPtySubprocess', () => {
name: 'xterm-256color'
})
)
expect(onMacosTccSpawnStrategy).toHaveBeenCalledWith('direct')
expect(onMacosTccSpawnStrategy).toHaveBeenCalledWith({
wrapper: 'direct',
attribution: 'unknown'
})
})
it('does not spawn after cancellation wins during async cwd validation', async () => {
+2 -1
View File
@@ -4,6 +4,7 @@ import { TerminalAttachCanceledError } from './daemon-errors'
import { createDaemonPtyEnvironment } from './pty-subprocess/spawn-environment'
import { createPtyShellLaunchPlan } from './pty-subprocess/shell-launch-plan'
import { spawnNativeDaemonPty, type SpawnedDaemonPty } from './pty-subprocess/native-pty-spawn'
import type { MacosTccSpawnStrategy } from '../providers/macos-tcc-spawn-attribution'
import {
formatPtySpawnError,
preflightPtySpawn,
@@ -33,7 +34,7 @@ export type PtySubprocessOptions = {
isCanceled?: () => boolean
/** Aborts in-progress cwd validation; `isCanceled` is only polled between steps. */
cancelSignal?: AbortSignal
onMacosTccSpawnStrategy?: (strategy: 'wrapped' | 'direct') => void
onMacosTccSpawnStrategy?: (strategy: MacosTccSpawnStrategy) => void
}
export async function checkPtySpawnHealth(): Promise<void> {
@@ -0,0 +1,84 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
const spawnMock = vi.fn()
vi.mock('node-pty', () => ({ spawn: (...a: unknown[]) => spawnMock(...a) }))
vi.mock('../../windows/windows-pty-job', () => ({ assignHostProcessToKillOnCloseJob: vi.fn() }))
import { spawnNativeDaemonPty } from './native-pty-spawn'
import type { MacosTccSpawnStrategy } from '../../providers/macos-tcc-spawn-attribution'
const realPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
function setPlatform(value: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { value, configurable: true })
}
function spawnedPty(extra: Record<string, unknown> = {}) {
return { pid: 4242, onData: vi.fn(), onExit: vi.fn(), write: vi.fn(), kill: vi.fn(), ...extra }
}
function run(onMacosTccSpawnStrategy: (strategy: MacosTccSpawnStrategy) => void) {
spawnNativeDaemonPty({
shellPath: '/bin/zsh',
shellArgs: ['-l'],
spawnCwd: '/tmp',
env: { SHELL: '/bin/zsh' },
cols: 80,
rows: 24,
windowsFallbackAttempts: [],
onMacosTccSpawnStrategy
})
}
beforeEach(() => spawnMock.mockReset())
afterEach(() => {
if (realPlatform) {
Object.defineProperty(process, 'platform', realPlatform)
}
})
describe('spawnNativeDaemonPty macOS TCC attribution reporting', () => {
// Why: the verdict must come off the process node-pty actually returned, not
// from the argv we asked for — `wrapped` never implied isolation (STA-3631).
it('reports the disclaim verdict carried by the spawned process', () => {
setPlatform('darwin')
spawnMock.mockReturnValue(spawnedPty({ tccDisclaim: 1 }))
const onMacosTccSpawnStrategy = vi.fn()
run(onMacosTccSpawnStrategy)
expect(onMacosTccSpawnStrategy).toHaveBeenCalledWith(
expect.objectContaining({ attribution: 'disclaimed' })
)
})
it('reports unknown when the spawned process carries no verdict', () => {
setPlatform('darwin')
spawnMock.mockReturnValue(spawnedPty())
const onMacosTccSpawnStrategy = vi.fn()
run(onMacosTccSpawnStrategy)
expect(onMacosTccSpawnStrategy).toHaveBeenCalledWith(
expect.objectContaining({ attribution: 'unknown' })
)
})
it('reports not-disclaimed when the native spawn could not apply the attribute', () => {
setPlatform('darwin')
spawnMock.mockReturnValue(spawnedPty({ tccDisclaim: 2 }))
const onMacosTccSpawnStrategy = vi.fn()
run(onMacosTccSpawnStrategy)
expect(onMacosTccSpawnStrategy).toHaveBeenCalledWith(
expect.objectContaining({ attribution: 'not-disclaimed' })
)
})
// Why: the wrapper decision and the disclaim verdict are independent facts.
it('keeps the wrapper verdict separate from the attribution verdict', () => {
setPlatform('darwin')
spawnMock.mockReturnValue(spawnedPty({ tccDisclaim: 1 }))
const onMacosTccSpawnStrategy = vi.fn()
run(onMacosTccSpawnStrategy)
expect(onMacosTccSpawnStrategy).toHaveBeenCalledWith({
wrapper: 'direct',
attribution: 'disclaimed'
})
})
})
@@ -3,6 +3,10 @@ import {
hostReportsChildExitStatus,
wrapShellSpawnForMacosTccAttribution
} from '../../providers/macos-tcc-login-shell'
import {
readMacosTccAttribution,
type MacosTccSpawnStrategy
} from '../../providers/macos-tcc-spawn-attribution'
import type { WindowsShellSpawnAttempt } from '../../providers/windows-shell-fallback-chain'
import { assignHostProcessToKillOnCloseJob } from '../../windows/windows-pty-job'
@@ -24,7 +28,7 @@ export function spawnNativeDaemonPty(args: {
cols: number
rows: number
windowsFallbackAttempts: WindowsShellSpawnAttempt[]
onMacosTccSpawnStrategy?: (strategy: 'wrapped' | 'direct') => void
onMacosTccSpawnStrategy?: (strategy: MacosTccSpawnStrategy) => void
}): SpawnedDaemonPty {
let reportsChildExitStatus = true
const spawnAt = (shellPath: string, shellArgs: string[], cwd: string): pty.IPty => {
@@ -43,7 +47,12 @@ export function spawnNativeDaemonPty(args: {
...(process.platform === 'win32' ? { useConptyDll: true } : {})
})
reportsChildExitStatus = hostReportsChildExitStatus(wrapped.file)
args.onMacosTccSpawnStrategy?.(wrapped.file === shellPath ? 'direct' : 'wrapped')
// Why: the wrapper and the disclaim are independent — login(1) stopped isolating
// attribution on macOS 26, so report what each one actually achieved (STA-3631).
args.onMacosTccSpawnStrategy?.({
wrapper: wrapped.file === shellPath ? 'direct' : 'wrapped',
attribution: readMacosTccAttribution(proc)
})
return proc
}
@@ -0,0 +1,57 @@
import { describe, it, expect, afterEach } from 'vitest'
import { readMacosTccAttribution } from './macos-tcc-spawn-attribution'
const realPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
function withPlatform<T>(value: NodeJS.Platform, run: () => T): T {
Object.defineProperty(process, 'platform', { value, configurable: true })
return run()
}
afterEach(() => {
if (realPlatform) {
Object.defineProperty(process, 'platform', realPlatform)
}
})
describe('readMacosTccAttribution', () => {
it('reports disclaimed only when the native spawn says the attribute applied', () => {
expect(withPlatform('darwin', () => readMacosTccAttribution({ tccDisclaim: 1 }))).toBe(
'disclaimed'
)
})
it('reports not-disclaimed when the SPI was missing or the call failed', () => {
expect(withPlatform('darwin', () => readMacosTccAttribution({ tccDisclaim: 2 }))).toBe(
'not-disclaimed'
)
expect(withPlatform('darwin', () => readMacosTccAttribution({ tccDisclaim: 3 }))).toBe(
'not-disclaimed'
)
})
// Why: an unpatched node-pty reports nothing; silence must never read as success (STA-3631).
it('reports unknown when node-pty reports no verdict at all', () => {
expect(withPlatform('darwin', () => readMacosTccAttribution({}))).toBe('unknown')
expect(withPlatform('darwin', () => readMacosTccAttribution(undefined))).toBe('unknown')
expect(withPlatform('darwin', () => readMacosTccAttribution(null))).toBe('unknown')
})
it('reports unknown for a non-numeric or unrecognized verdict', () => {
expect(withPlatform('darwin', () => readMacosTccAttribution({ tccDisclaim: '1' }))).toBe(
'unknown'
)
expect(withPlatform('darwin', () => readMacosTccAttribution({ tccDisclaim: 0 }))).toBe(
'unknown'
)
expect(withPlatform('darwin', () => readMacosTccAttribution({ tccDisclaim: 99 }))).toBe(
'unknown'
)
})
// Why: disclaiming is a darwin spawn attribute; other hosts have nothing to claim either way.
it('never claims disclaimed off macOS even if a verdict is present', () => {
expect(withPlatform('linux', () => readMacosTccAttribution({ tccDisclaim: 1 }))).toBe('unknown')
expect(withPlatform('win32', () => readMacosTccAttribution({ tccDisclaim: 1 }))).toBe('unknown')
})
})
@@ -0,0 +1,54 @@
/**
* Spawn-time TCC responsibility disclaiming (STA-3631).
*
* macOS 26 no longer breaks TCC responsibility inheritance at an intermediate
* `login(1)` session, so every child of a wrapped pane is still attributed to
* Orca's bundle. The patched node-pty darwin spawn path calls
* `responsibility_spawnattrs_setdisclaim` and reports whether it took effect;
* this module turns that report into a verdict the spawn site can log honestly.
*/
/** Verdict codes emitted by the patched node-pty darwin spawn path. Keep in sync
* with ORCA_TCC_DISCLAIM_* in config/patches/node-pty@1.1.0.patch. */
const DISCLAIM_APPLIED = 1
const DISCLAIM_UNSUPPORTED = 2
const DISCLAIM_FAILED = 3
/**
* Whether this spawn's children get their own TCC identity.
*
* `unknown` is the honest default: an unpatched node-pty reports nothing, and
* `wrapped` never implied isolation on macOS 26 — neither may this.
*/
export type MacosTccAttribution = 'disclaimed' | 'not-disclaimed' | 'unknown'
/** How a pane shell's argv was built, paired with what that actually bought. */
export type MacosTccSpawnStrategy = {
wrapper: 'wrapped' | 'direct'
attribution: MacosTccAttribution
}
/**
* Read the disclaim verdict off a spawned node-pty process.
*
* Reports `disclaimed` only on a positive report from the native spawn; a
* missing, non-numeric, or unrecognized value stays `unknown` so a node-pty
* without the patch can never be mistaken for an isolated one.
*/
export function readMacosTccAttribution(ptyProcess: unknown): MacosTccAttribution {
if (process.platform !== 'darwin') {
return 'unknown'
}
// Why: every non-verdict — absent, wrong type, unrecognized code — funnels through
// the same default, so an unpatched node-pty can never read as isolated.
const reported = (ptyProcess as { tccDisclaim?: unknown } | null | undefined)?.tccDisclaim
switch (reported) {
case DISCLAIM_APPLIED:
return 'disclaimed'
case DISCLAIM_UNSUPPORTED:
case DISCLAIM_FAILED:
return 'not-disclaimed'
default:
return 'unknown'
}
}