agent-takkub 1.6.21 → 1.6.23

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.
@@ -4,7 +4,7 @@
4
4
  const { spawnSync } = require('child_process');
5
5
  const {
6
6
  venvPythonIfExists,
7
- pythonLooksExecutable,
7
+ pythonExecutableProblem,
8
8
  brokenInterpreterMessage,
9
9
  agentTakkubHome,
10
10
  } = require('../scripts/lib');
@@ -14,8 +14,9 @@ if (!py) {
14
14
  console.error('agent-takkub is not provisioned. Run: npm install -g agent-takkub');
15
15
  process.exit(1);
16
16
  }
17
- if (!pythonLooksExecutable(py)) {
18
- console.error(brokenInterpreterMessage(py, agentTakkubHome()));
17
+ const problem = pythonExecutableProblem(py);
18
+ if (problem) {
19
+ console.error(brokenInterpreterMessage(py, agentTakkubHome(), problem));
19
20
  process.exit(1);
20
21
  }
21
22
  const r = spawnSync(py, ['-m', 'agent_takkub', ...process.argv.slice(2)], { stdio: 'inherit' });
package/npm/bin/takkub.js CHANGED
@@ -4,7 +4,7 @@
4
4
  const { spawnSync } = require('child_process');
5
5
  const {
6
6
  venvPythonIfExists,
7
- pythonLooksExecutable,
7
+ pythonExecutableProblem,
8
8
  brokenInterpreterMessage,
9
9
  agentTakkubHome,
10
10
  } = require('../scripts/lib');
@@ -14,8 +14,9 @@ if (!py) {
14
14
  console.error('takkub is not provisioned. Run: npm install -g agent-takkub');
15
15
  process.exit(1);
16
16
  }
17
- if (!pythonLooksExecutable(py)) {
18
- console.error(brokenInterpreterMessage(py, agentTakkubHome()));
17
+ const problem = pythonExecutableProblem(py);
18
+ if (problem) {
19
+ console.error(brokenInterpreterMessage(py, agentTakkubHome(), problem));
19
20
  process.exit(1);
20
21
  }
21
22
  const r = spawnSync(py, ['-m', 'agent_takkub.cli', ...process.argv.slice(2)], { stdio: 'inherit' });
@@ -53,41 +53,84 @@ function findWheelForVersion(distDir, version) {
53
53
  // header bytes, no attempt to execute the file) so a broken interpreter is
54
54
  // never spawned at all — spawning it is exactly the risky step that can
55
55
  // leave the file locked/hard to overwrite during recovery.
56
- const MIN_PYTHON_EXE_BYTES = 40 * 1024; // a real venv python.exe/python is comfortably >90KB
56
+ //
57
+ // The floor only needs to catch truncation (#341's 29-byte case), not judge
58
+ // what a "normal-sized" interpreter looks like — a pyenv/python.org
59
+ // framework build's `bin/pythonX.Y` is a tiny Mach-O stub that re-execs into
60
+ // Python.framework and can be well under 40KB while still being completely
61
+ // runnable (#446). Magic-byte sniffing is what actually distinguishes it
62
+ // from garbage; size only rules out an empty/near-empty file.
63
+ const MIN_PYTHON_EXE_BYTES = 1024;
64
+
65
+ const ELF_MAGIC = 0x7f454c46; // '\x7fELF', big-endian read of the 4 header bytes
66
+ const MACHO_MAGICS = new Set([
67
+ 0xfeedface, // MH_MAGIC (32-bit)
68
+ 0xcefaedfe, // MH_CIGAM (32-bit, byte-swapped)
69
+ 0xfeedfacf, // MH_MAGIC_64
70
+ 0xcffaedfe, // MH_CIGAM_64 (byte-swapped)
71
+ 0xcafebabe, // FAT_MAGIC (universal/fat binary)
72
+ 0xbebafeca, // FAT_CIGAM (universal/fat binary, byte-swapped)
73
+ ]);
57
74
 
58
75
  // Opens the path exactly once and stats/reads that same descriptor for the
59
76
  // rest of the check — never stat a path and then separately open it, which
60
77
  // leaves a window for the file at that name to change between the two
61
78
  // syscalls (CodeQL js/file-system-race, alert #29).
62
- function pythonLooksExecutable(py) {
79
+ //
80
+ // Returns null when `py` looks like a real, runnable interpreter, otherwise
81
+ // a short machine-readable reason so callers can explain what specifically
82
+ // failed instead of a single opaque "not a real executable".
83
+ function pythonExecutableProblem(py) {
63
84
  let fd;
64
85
  try {
65
86
  fd = fs.openSync(py, 'r');
66
87
  } catch (_e) {
67
- return false;
88
+ return 'missing';
68
89
  }
69
90
  try {
70
91
  const stat = fs.fstatSync(fd);
71
- if (!stat.isFile() || stat.size < MIN_PYTHON_EXE_BYTES) return false;
92
+ if (!stat.isFile()) return 'not-a-file';
93
+ if (stat.size < MIN_PYTHON_EXE_BYTES) return 'too-small';
94
+ const buf = Buffer.alloc(4);
95
+ const n = fs.readSync(fd, buf, 0, 4, 0);
72
96
  if (process.platform === 'win32') {
73
97
  // Windows PE executables always start with the 'MZ' DOS-header magic.
74
- const buf = Buffer.alloc(2);
75
- fs.readSync(fd, buf, 0, 2, 0);
76
- return buf[0] === 0x4d && buf[1] === 0x5a; // 'M', 'Z'
98
+ return n >= 2 && buf[0] === 0x4d && buf[1] === 0x5a ? null : 'bad-magic';
77
99
  }
78
- return true;
100
+ // POSIX: accept a real ELF or Mach-O binary, or a shebang script — a
101
+ // pyenv shim / venv `python` wrapper is a text file starting with `#!`,
102
+ // not a compiled executable, and is just as valid an interpreter path.
103
+ if (n >= 2 && buf[0] === 0x23 && buf[1] === 0x21) return null; // '#!'
104
+ if (n < 4) return 'bad-magic';
105
+ const magic = buf.readUInt32BE(0);
106
+ if (magic === ELF_MAGIC || MACHO_MAGICS.has(magic)) return null;
107
+ return 'bad-magic';
79
108
  } catch (_e) {
80
- return false;
109
+ return 'read-error';
81
110
  } finally {
82
111
  fs.closeSync(fd);
83
112
  }
84
113
  }
85
114
 
86
- function brokenInterpreterMessage(py, home) {
115
+ function pythonLooksExecutable(py) {
116
+ return pythonExecutableProblem(py) === null;
117
+ }
118
+
119
+ const PROBLEM_DESCRIPTIONS = {
120
+ missing: "the file doesn't exist",
121
+ 'not-a-file': 'the path exists but is not a regular file (e.g. a directory)',
122
+ 'too-small': `the file is smaller than ${MIN_PYTHON_EXE_BYTES} bytes — too small to be a real interpreter`,
123
+ 'bad-magic': "the file's header does not match a known executable format (PE/ELF/Mach-O) or a shebang script",
124
+ 'read-error': 'the file could not be read to check its header',
125
+ };
126
+
127
+ function brokenInterpreterMessage(py, home, problem) {
87
128
  const venvDirPath = path.join(home, 'venv');
129
+ const why = PROBLEM_DESCRIPTIONS[problem] || PROBLEM_DESCRIPTIONS['bad-magic'];
88
130
  return (
89
131
  `[agent-takkub] the cockpit interpreter looks broken (exists but is not a real executable):\n` +
90
132
  ` ${py}\n` +
133
+ ` Reason: ${why}.\n` +
91
134
  ' Something outside agent-takkub overwrote or truncated this file — the cockpit\n' +
92
135
  ' did not do this to itself. Fix:\n' +
93
136
  ' 1) close every running cockpit/takkub window and process for this install\n' +
@@ -116,5 +159,6 @@ module.exports = {
116
159
  venvPythonw,
117
160
  findWheelForVersion,
118
161
  pythonLooksExecutable,
162
+ pythonExecutableProblem,
119
163
  brokenInterpreterMessage,
120
164
  };
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "agent-takkub",
3
- "version": "1.6.21",
4
- "description": "Desktop cockpit for orchestrating a team of Claude Code agents (Windows + macOS). Install globally: npm i -g agent-takkub",
5
- "keywords": [
6
- "claude",
7
- "claude-code",
8
- "ai-agents",
9
- "agent-orchestration",
10
- "cockpit",
11
- "desktop",
12
- "cli",
13
- "developer-tools"
14
- ],
15
- "author": "takkub",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/takkub/agent-takkub.git"
19
- },
20
- "homepage": "https://github.com/takkub/agent-takkub#readme",
21
- "bugs": {
22
- "url": "https://github.com/takkub/agent-takkub/issues"
23
- },
24
- "bin": {
25
- "agent-takkub": "npm/bin/agent-takkub.js",
26
- "takkub": "npm/bin/takkub.js"
27
- },
28
- "scripts": {
29
- "postinstall": "node npm/scripts/postinstall.js",
30
- "prepublishOnly": "node npm/scripts/preflight-publish.js",
31
- "test": "node --test npm/scripts/*.test.js"
32
- },
33
- "files": [
34
- "npm/",
35
- "!npm/scripts/*.test.js",
36
- "dist/*.whl",
37
- "assets/icon.ico",
38
- "assets/icon.png",
39
- "assets/cockpit-main.png"
40
- ],
41
- "os": [
42
- "win32",
43
- "darwin"
44
- ],
45
- "engines": {
46
- "node": ">=18"
47
- },
48
- "engines-note": "This floor is for agent-takkub itself. The OPTIONAL graft code-intelligence feature (installed separately via `takkub doctor --fix`, not an npm dependency of this package) needs Node >=20 — see README's Requirements section. Not raised here because it would force the >=20 floor onto every user even though graft stays off until explicitly enabled.",
49
- "license": "MIT"
50
- }
1
+ {
2
+ "name": "agent-takkub",
3
+ "version": "1.6.23",
4
+ "description": "Desktop cockpit for orchestrating a team of Claude Code agents (Windows + macOS). Install globally: npm i -g agent-takkub",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "ai-agents",
9
+ "agent-orchestration",
10
+ "cockpit",
11
+ "desktop",
12
+ "cli",
13
+ "developer-tools"
14
+ ],
15
+ "author": "takkub",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/takkub/agent-takkub.git"
19
+ },
20
+ "homepage": "https://github.com/takkub/agent-takkub#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/takkub/agent-takkub/issues"
23
+ },
24
+ "bin": {
25
+ "agent-takkub": "npm/bin/agent-takkub.js",
26
+ "takkub": "npm/bin/takkub.js"
27
+ },
28
+ "scripts": {
29
+ "postinstall": "node npm/scripts/postinstall.js",
30
+ "prepublishOnly": "node npm/scripts/preflight-publish.js",
31
+ "test": "node --test npm/scripts/*.test.js"
32
+ },
33
+ "files": [
34
+ "npm/",
35
+ "!npm/scripts/*.test.js",
36
+ "dist/*.whl",
37
+ "assets/icon.ico",
38
+ "assets/icon.png",
39
+ "assets/cockpit-main.png"
40
+ ],
41
+ "os": [
42
+ "win32",
43
+ "darwin"
44
+ ],
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "engines-note": "This floor is for agent-takkub itself. The OPTIONAL graft code-intelligence feature (installed separately via `takkub doctor --fix`, not an npm dependency of this package) needs Node >=20 — see README's Requirements section. Not raised here because it would force the >=20 floor onto every user even though graft stays off until explicitly enabled.",
49
+ "license": "MIT"
50
+ }