@the-open-engine/zeroshot 6.10.1 → 6.10.2
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/package.json
CHANGED
|
@@ -19,6 +19,7 @@ const { getTask, getTaskBySpawnOwnershipToken } = require('../../task-lib/store.
|
|
|
19
19
|
const { loadSettings } = require('../../lib/settings.js');
|
|
20
20
|
const { resolveClaudeAuth } = require('../../lib/settings/claude-auth.js');
|
|
21
21
|
const { prependWorktreeToolBinToEnv } = require('../worktree-tooling-env.js');
|
|
22
|
+
const { applyDarwinKeychainBoundaryToEnv } = require('../darwin-keychain-boundary.js');
|
|
22
23
|
const {
|
|
23
24
|
CLAUDE_SETTINGS_ENV,
|
|
24
25
|
cleanupClaudeSettingsOverlay,
|
|
@@ -689,7 +690,10 @@ function buildFinalContext({ agent, context, desiredOutputFormat, runOutputForma
|
|
|
689
690
|
}
|
|
690
691
|
|
|
691
692
|
function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
|
|
692
|
-
const {
|
|
693
|
+
const {
|
|
694
|
+
claudeSettingsPath = null,
|
|
695
|
+
applyDarwinKeychainBoundary = applyDarwinKeychainBoundaryToEnv,
|
|
696
|
+
} = options;
|
|
693
697
|
const spawnEnv = { ...process.env };
|
|
694
698
|
const agentCwd = agent.config?.cwd || agent.worktree?.path || process.cwd();
|
|
695
699
|
const clusterId = agent.cluster?.id || agent.cluster_id || process.env.ZEROSHOT_CLUSTER_ID;
|
|
@@ -724,6 +728,13 @@ function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
|
|
|
724
728
|
}
|
|
725
729
|
}
|
|
726
730
|
|
|
731
|
+
// KEYCHAIN BOUNDARY (darwin only): non-interactive local/worktree worker
|
|
732
|
+
// descendants must not reach the user's GUI Keychain session (issue #704).
|
|
733
|
+
// Docker isolation never reaches buildSpawnEnv (see spawnClaudeTaskIsolated).
|
|
734
|
+
// Applied before the worktree tool bins so repo-managed tool substitutes
|
|
735
|
+
// stay first on PATH.
|
|
736
|
+
applyDarwinKeychainBoundary(spawnEnv);
|
|
737
|
+
|
|
727
738
|
prependWorktreeToolBinToEnv(spawnEnv, {
|
|
728
739
|
cwd: agentCwd,
|
|
729
740
|
worktreePath: agent.worktree?.path || null,
|
|
@@ -822,14 +833,7 @@ function createPendingTaskLaunchHandle({
|
|
|
822
833
|
return handle;
|
|
823
834
|
}
|
|
824
835
|
|
|
825
|
-
function spawnTaskProcess({
|
|
826
|
-
agent,
|
|
827
|
-
ctPath,
|
|
828
|
-
args,
|
|
829
|
-
cwd,
|
|
830
|
-
spawnEnv,
|
|
831
|
-
spawnTimeoutMs = 30000,
|
|
832
|
-
}) {
|
|
836
|
+
function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv, spawnTimeoutMs = 30000 }) {
|
|
833
837
|
// Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it.
|
|
834
838
|
const SPAWN_TIMEOUT_MS = spawnTimeoutMs;
|
|
835
839
|
// spawn() throws on null bytes in argv; strip them before they get there.
|
|
@@ -878,10 +882,7 @@ function spawnTaskProcess({
|
|
|
878
882
|
const classifyCleanupOwnership = trackTaskWrapperCleanupOwnership(findPersistedTaskId);
|
|
879
883
|
const rejectWithOwnership = async (error) => {
|
|
880
884
|
const classifiedError = classifyCleanupOwnership(error);
|
|
881
|
-
if (
|
|
882
|
-
!callerOwnsCommandCleanup(classifiedError) &&
|
|
883
|
-
agent.currentTask === pendingLaunch
|
|
884
|
-
) {
|
|
885
|
+
if (!callerOwnsCommandCleanup(classifiedError) && agent.currentTask === pendingLaunch) {
|
|
885
886
|
let termination;
|
|
886
887
|
try {
|
|
887
888
|
termination = await pendingLaunch.kill(classifiedError.message);
|
|
@@ -2198,7 +2199,6 @@ function startIsolatedTail({ agent, manager, clusterId, logFilePath, state, onLi
|
|
|
2198
2199
|
});
|
|
2199
2200
|
}
|
|
2200
2201
|
|
|
2201
|
-
|
|
2202
2202
|
async function checkIsolatedStatus({
|
|
2203
2203
|
agent,
|
|
2204
2204
|
manager,
|
|
@@ -2628,6 +2628,7 @@ module.exports = {
|
|
|
2628
2628
|
ensureAskUserQuestionHook,
|
|
2629
2629
|
ensureDangerousGitHook,
|
|
2630
2630
|
resolveMcpConfigArgs,
|
|
2631
|
+
buildSpawnEnv,
|
|
2631
2632
|
spawnClaudeTask,
|
|
2632
2633
|
spawnTaskProcess,
|
|
2633
2634
|
followClaudeTaskLogs,
|
|
@@ -12,6 +12,7 @@ const { loadSettings } = require('../lib/settings');
|
|
|
12
12
|
const { normalizeProviderName } = require('../lib/provider-names');
|
|
13
13
|
const { getProvider } = require('./providers');
|
|
14
14
|
const { prependWorktreeToolBinToEnv } = require('./worktree-tooling-env');
|
|
15
|
+
const { applyDarwinKeychainBoundaryToEnv } = require('./darwin-keychain-boundary');
|
|
15
16
|
const { getTask, getTaskBySpawnOwnershipToken } = require('../task-lib/store.js');
|
|
16
17
|
const {
|
|
17
18
|
TASK_SPAWN_OWNERSHIP_TOKEN_ENV,
|
|
@@ -151,6 +152,7 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
151
152
|
* @param {boolean} [options.quiet] - Suppress console logging
|
|
152
153
|
* @param {number} [options.timeout] - Task timeout in ms (default: 1 hour)
|
|
153
154
|
* @param {Function} [options.onOutput] - Callback for output lines
|
|
155
|
+
* @param {Function} [options.applyDarwinKeychainBoundary] - Boundary injection seam for tests
|
|
154
156
|
*/
|
|
155
157
|
constructor(options = {}) {
|
|
156
158
|
super();
|
|
@@ -158,6 +160,8 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
158
160
|
this.quiet = options.quiet || false;
|
|
159
161
|
this.timeout = options.timeout || 60 * 60 * 1000;
|
|
160
162
|
this.onOutput = options.onOutput || null;
|
|
163
|
+
this.applyDarwinKeychainBoundary =
|
|
164
|
+
options.applyDarwinKeychainBoundary || applyDarwinKeychainBoundaryToEnv;
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
/**
|
|
@@ -376,19 +380,30 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
376
380
|
const spawnEnv = {
|
|
377
381
|
...process.env,
|
|
378
382
|
};
|
|
383
|
+
let claudeSettingsPath = null;
|
|
379
384
|
if (providerName === 'claude' && resolvedModelSpec?.model) {
|
|
380
385
|
spawnEnv.ANTHROPIC_MODEL = resolvedModelSpec.model;
|
|
381
386
|
}
|
|
382
387
|
if (providerName === 'claude') {
|
|
383
|
-
|
|
388
|
+
claudeSettingsPath = prepareClaudeSettingsOverlay({
|
|
384
389
|
includeDangerousGit: Boolean(worktreePath),
|
|
385
390
|
});
|
|
391
|
+
spawnEnv[CLAUDE_SETTINGS_ENV] = claudeSettingsPath;
|
|
386
392
|
const mcpConfigPath = resolveRepoMcpConfigPath({ cwd, worktreePath });
|
|
387
393
|
if (mcpConfigPath) {
|
|
388
394
|
spawnEnv[CLAUDE_MCP_CONFIG_ENV] = mcpConfigPath;
|
|
389
395
|
}
|
|
390
396
|
}
|
|
391
397
|
|
|
398
|
+
// KEYCHAIN BOUNDARY (darwin only): keep non-interactive worker descendants
|
|
399
|
+
// away from the user's GUI Keychain session (issue #704).
|
|
400
|
+
try {
|
|
401
|
+
this.applyDarwinKeychainBoundary(spawnEnv);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (claudeSettingsPath) cleanupClaudeSettingsOverlay(claudeSettingsPath);
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
|
|
392
407
|
prependWorktreeToolBinToEnv(spawnEnv, { cwd, worktreePath });
|
|
393
408
|
|
|
394
409
|
return spawnEnv;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Darwin worker Keychain boundary (issue #704).
|
|
3
|
+
*
|
|
4
|
+
* Non-interactive local/worktree workers are spawned with the host environment,
|
|
5
|
+
* so worker descendants (e.g. `claude doctor` probing Keychain writes through
|
|
6
|
+
* `security -i`) reach the logged-in user's GUI Keychain session and launch
|
|
7
|
+
* SecurityAgent dialogs from a supposedly non-interactive cluster.
|
|
8
|
+
*
|
|
9
|
+
* On darwin, worker spawn envs get a managed shim directory prepended to PATH
|
|
10
|
+
* containing a `security` wrapper that fails closed on interactive invocations
|
|
11
|
+
* (`-i`, `-p`, or no arguments) with a deterministic diagnostic, and execs the
|
|
12
|
+
* real /usr/bin/security for every other subcommand so provider authentication
|
|
13
|
+
* (e.g. `security find-generic-password`) keeps working.
|
|
14
|
+
*
|
|
15
|
+
* Docker isolation never reaches this code path, and non-darwin platforms are
|
|
16
|
+
* left untouched. Set ZEROSHOT_ALLOW_INTERACTIVE_KEYCHAIN=1 to opt out.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { randomUUID } = require('node:crypto');
|
|
23
|
+
|
|
24
|
+
const SHIM_DIR_RELATIVE_PATH = path.join('.zeroshot', 'keychain-shim');
|
|
25
|
+
const REAL_SECURITY_PATH = '/usr/bin/security';
|
|
26
|
+
const OPT_OUT_ENV_VAR = 'ZEROSHOT_ALLOW_INTERACTIVE_KEYCHAIN';
|
|
27
|
+
|
|
28
|
+
function shellQuote(value) {
|
|
29
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildSecurityShimScript(realSecurityPath) {
|
|
33
|
+
return `#!/bin/sh
|
|
34
|
+
# Managed by Zeroshot (src/darwin-keychain-boundary.js). Do not edit.
|
|
35
|
+
#
|
|
36
|
+
# Non-interactive Zeroshot workers must not open the logged-in user's GUI
|
|
37
|
+
# Keychain session (SecurityAgent). Interactive \`security\` invocations fail
|
|
38
|
+
# closed here; every other subcommand is passed through to the real binary so
|
|
39
|
+
# provider authentication keeps working.
|
|
40
|
+
|
|
41
|
+
REAL_SECURITY=${shellQuote(realSecurityPath)}
|
|
42
|
+
|
|
43
|
+
if [ "\${${OPT_OUT_ENV_VAR}:-0}" = "1" ]; then
|
|
44
|
+
exec "$REAL_SECURITY" "$@"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
fail_closed() {
|
|
48
|
+
echo "zeroshot: blocked interactive 'security' invocation from a non-interactive worker (argv: $*)." >&2
|
|
49
|
+
echo "zeroshot: this cluster has no interactive Keychain session, so SecurityAgent prompts are disabled." >&2
|
|
50
|
+
echo "zeroshot: run the cluster with Docker isolation or configure explicit credentials for the tool that attempted Keychain access." >&2
|
|
51
|
+
echo "zeroshot: set ${OPT_OUT_ENV_VAR}=1 to restore interactive Keychain access." >&2
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# \`security\` without arguments enters interactive mode.
|
|
56
|
+
[ "$#" -eq 0 ] && fail_closed
|
|
57
|
+
|
|
58
|
+
# Global options precede the subcommand. -i (interactive) and -p (prompt,
|
|
59
|
+
# implies -i) must not reach the real binary; option letters may be bundled
|
|
60
|
+
# (e.g. -qi). Scanning stops at the first non-option token (the subcommand).
|
|
61
|
+
for arg in "$@"; do
|
|
62
|
+
case "$arg" in
|
|
63
|
+
-*i*|-*p*) fail_closed "$@" ;;
|
|
64
|
+
-*) ;;
|
|
65
|
+
*) break ;;
|
|
66
|
+
esac
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
exec "$REAL_SECURITY" "$@"
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create (or refresh) the managed shim directory containing the `security`
|
|
75
|
+
* wrapper. Idempotent: the script is only rewritten when its content changes.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} [options]
|
|
78
|
+
* @param {string} [options.shimBaseDir] - Shim directory (tests only); defaults to ~/.zeroshot/keychain-shim.
|
|
79
|
+
* @param {string} [options.realSecurityPath] - Real binary to exec (tests only); defaults to /usr/bin/security.
|
|
80
|
+
* @returns {string} Absolute path of the shim directory.
|
|
81
|
+
*/
|
|
82
|
+
function ensureDarwinKeychainShimDir(options = {}) {
|
|
83
|
+
const shimDir = options.shimBaseDir || path.join(os.homedir(), SHIM_DIR_RELATIVE_PATH);
|
|
84
|
+
const script = buildSecurityShimScript(options.realSecurityPath || REAL_SECURITY_PATH);
|
|
85
|
+
const shimPath = path.join(shimDir, 'security');
|
|
86
|
+
|
|
87
|
+
fs.mkdirSync(shimDir, { recursive: true });
|
|
88
|
+
|
|
89
|
+
let existing = null;
|
|
90
|
+
try {
|
|
91
|
+
existing = fs.readFileSync(shimPath, 'utf8');
|
|
92
|
+
} catch {
|
|
93
|
+
// Missing or unreadable: (re)write below.
|
|
94
|
+
}
|
|
95
|
+
if (existing !== script) {
|
|
96
|
+
const tempPath = path.join(shimDir, `.security.${process.pid}.${randomUUID()}.tmp`);
|
|
97
|
+
try {
|
|
98
|
+
fs.writeFileSync(tempPath, script, { mode: 0o755, flag: 'wx' });
|
|
99
|
+
// The creation mode is subject to umask. Set the final mode before the
|
|
100
|
+
// rename so the live path is never observable as non-executable.
|
|
101
|
+
fs.chmodSync(tempPath, 0o755);
|
|
102
|
+
fs.renameSync(tempPath, shimPath);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
try {
|
|
105
|
+
// Remove any unpublished partial file without masking the publication
|
|
106
|
+
// failure that caused this cleanup path.
|
|
107
|
+
fs.rmSync(tempPath, { force: true });
|
|
108
|
+
} catch (cleanupError) {
|
|
109
|
+
error.message += ` Cleanup also failed: ${cleanupError.message}.`;
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
// An existing matching shim may have drifted permissions.
|
|
115
|
+
fs.chmodSync(shimPath, 0o755);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return shimDir;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Prepend the Keychain boundary shim to a worker spawn env's PATH.
|
|
123
|
+
*
|
|
124
|
+
* No-op off darwin and when the operator opted out via
|
|
125
|
+
* ZEROSHOT_ALLOW_INTERACTIVE_KEYCHAIN=1. Fails closed (throws) when the shim
|
|
126
|
+
* cannot be installed: spawning the worker anyway would silently re-expose the
|
|
127
|
+
* interactive Keychain session.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} env - Spawn env to mutate (also returned).
|
|
130
|
+
* @param {object} [options]
|
|
131
|
+
* @param {string} [options.platform] - Platform override (tests only).
|
|
132
|
+
* @param {string} [options.shimBaseDir] - See ensureDarwinKeychainShimDir.
|
|
133
|
+
* @param {string} [options.realSecurityPath] - See ensureDarwinKeychainShimDir.
|
|
134
|
+
* @returns {object} The same env object.
|
|
135
|
+
*/
|
|
136
|
+
function applyDarwinKeychainBoundaryToEnv(env, options = {}) {
|
|
137
|
+
const platform = options.platform || process.platform;
|
|
138
|
+
if (platform !== 'darwin') {
|
|
139
|
+
return env;
|
|
140
|
+
}
|
|
141
|
+
if (env[OPT_OUT_ENV_VAR] === '1' || process.env[OPT_OUT_ENV_VAR] === '1') {
|
|
142
|
+
return env;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let shimDir;
|
|
146
|
+
try {
|
|
147
|
+
shimDir = ensureDarwinKeychainShimDir(options);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Failed to install the darwin Keychain boundary shim: ${error.message}. ` +
|
|
151
|
+
`Non-interactive workers must not reach the interactive Keychain session; ` +
|
|
152
|
+
`use Docker isolation or set ${OPT_OUT_ENV_VAR}=1 to opt out.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Darwin environment keys are case-sensitive: descendants consult PATH,
|
|
157
|
+
// never a differently-cased key such as Path. Preserve empty components
|
|
158
|
+
// because POSIX interprets them as the current directory. An absent PATH
|
|
159
|
+
// retains Node/libuv's default Unix search path after the shim, while an
|
|
160
|
+
// explicitly empty PATH retains its empty component (`<shim>:`).
|
|
161
|
+
const existingEntries =
|
|
162
|
+
env.PATH === undefined
|
|
163
|
+
? ['/usr/bin', '/bin']
|
|
164
|
+
: String(env.PATH)
|
|
165
|
+
.split(path.delimiter)
|
|
166
|
+
.filter((entry) => entry !== shimDir);
|
|
167
|
+
env.PATH = [shimDir, ...existingEntries].join(path.delimiter);
|
|
168
|
+
return env;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
applyDarwinKeychainBoundaryToEnv,
|
|
173
|
+
ensureDarwinKeychainShimDir,
|
|
174
|
+
};
|
|
@@ -6,6 +6,9 @@ const DEFAULT_TOOL_BIN_RELATIVE_PATHS = ['.zeroshot/bin', '.worktree-tool-bin'];
|
|
|
6
6
|
const FALLBACK_BIN_PREFIX = '.worktree-tool-bin.';
|
|
7
7
|
|
|
8
8
|
function pathKeyForEnv(env) {
|
|
9
|
+
if (Object.prototype.hasOwnProperty.call(env, 'PATH')) {
|
|
10
|
+
return 'PATH';
|
|
11
|
+
}
|
|
9
12
|
return Object.keys(env).find((key) => key.toUpperCase() === 'PATH') || 'PATH';
|
|
10
13
|
}
|
|
11
14
|
|