external-subagent 0.4.0 → 0.5.0

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.
@@ -3,26 +3,47 @@
3
3
  // check-native-tarball.mjs [package.tgz] verify a packed tarball (entries,
4
4
  // payload manifest consistency,
5
5
  // forbidden development material)
6
- // check-native-tarball.mjs --staged verify only the staged payload
7
- // (npm/native/darwin-arm64) against
6
+ // check-native-tarball.mjs --staged verify only the staged payloads
7
+ // (npm/native/<platform>) against
8
8
  // the package version — the publish
9
9
  // gate, since `npm publish` packs
10
10
  // into a private temp directory no
11
11
  // lifecycle script can inspect.
12
+ // A released tarball carries every supported platform payload (darwin-arm64
13
+ // and linux-x64), so both entry sets and both manifests are required in either
14
+ // mode: a tarball missing one platform is rejected. Version agreement with the
15
+ // packed package is enforced for the running host's payload (prepack rebuilds
16
+ // only the host payload from source) and, in --staged mode, for every staged
17
+ // platform — the merged publish tree where the foreign payload comes from CI's
18
+ // other platform job. Image checks (Mach-O arm64 / ELF x86-64) and the 755
19
+ // mode check use the same probes and layout table as cli/install/payload.mjs,
20
+ // so this gate cannot drift from what an installed package verifies.
12
21
  // Without --staged the tarball is resolved from: the explicit argument, the
13
22
  // npm --pack-destination directory (set during `npm pack` lifecycles), or the
14
23
  // newest matching tgz in the working directory. A dry run (npm pack --dry-run)
15
24
  // or a publish (no local tarball) skips the tarball checks and still runs the
16
25
  // staged payload checks.
17
26
  import fs from 'node:fs';
18
- import os from 'node:os';
19
27
  import path from 'node:path';
20
28
  import { spawnSync } from 'node:child_process';
29
+ import { elfArch, machoArch } from '../../cli/install/payload.mjs';
30
+ import { nativePlatform } from '../../cli/install/layout.mjs';
21
31
 
22
32
  const stagedOnly = process.argv.includes('--staged');
23
33
  const explicit = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : null;
24
34
  const dryRun = process.env.npm_config_dry_run === 'true';
25
35
 
36
+ // Every platform the single release tarball ships, and the image identity each
37
+ // payload must carry. Keeping both lists here (rather than a fixed
38
+ // darwin-arm64 path) is what makes the gate dual-platform.
39
+ const RELEASE_PLATFORMS = Object.freeze(['darwin-arm64', 'linux-x64']);
40
+ const BINARIES = Object.freeze(['external-subagentd', 'external-subagent-mcp']);
41
+ const IMAGE_CHECKS = Object.freeze({
42
+ 'darwin-arm64': { describe: 'Mach-O arm64 executable', probe: machoArch, arch: 'arm64' },
43
+ 'linux-x64': { describe: 'ELF x86-64 executable', probe: elfArch, arch: 'x64' },
44
+ });
45
+ const hostPlatform = nativePlatform();
46
+
26
47
  function findTarball() {
27
48
  if (explicit) return explicit;
28
49
  const candidates = [];
@@ -57,14 +78,17 @@ if (!stagedOnly) {
57
78
  'package/bin/external-subagent-debug.mjs',
58
79
  'package/bin/external-subagent-debug-mcp.mjs',
59
80
  'package/cli/main.mjs',
60
- 'package/npm/native/darwin-arm64/external-subagentd',
61
- 'package/npm/native/darwin-arm64/external-subagent-mcp',
62
- 'package/npm/native/darwin-arm64/payload.json',
63
81
  'package/plugins/codex/external-subagent/.codex-plugin/plugin.json',
64
82
  'package/plugins/codex/external-subagent/.mcp.json',
65
83
  'package/launchd/com.external-subagent.daemon.plist.template',
66
84
  'package/LICENSE',
67
85
  ];
86
+ // Both platform payloads are mandatory: a tarball that ships only one is
87
+ // rejected here (and by the --staged publish gate) before it can be
88
+ // published.
89
+ for (const platform of RELEASE_PLATFORMS) {
90
+ for (const name of [...BINARIES, 'payload.json']) required.push(`package/npm/native/${platform}/${name}`);
91
+ }
68
92
 
69
93
  const listing = spawnSync('tar', ['-tzf', tgz], { encoding: 'utf8' });
70
94
  if (listing.status !== 0) {
@@ -85,38 +109,82 @@ if (!stagedOnly) {
85
109
  }
86
110
 
87
111
  const packedPackage = JSON.parse(spawnSync('tar', ['-xzf', tgz, '-O', 'package/package.json'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout);
88
- const manifestText = spawnSync('tar', ['-xzf', tgz, '-O', 'package/npm/native/darwin-arm64/payload.json'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout;
89
- const manifest = JSON.parse(manifestText);
90
- if (manifest.version !== packedPackage.version) {
91
- process.stderr.write(`payload version ${manifest.version} does not match package version ${packedPackage.version}\n`);
92
- failed = true;
112
+ for (const platform of RELEASE_PLATFORMS) {
113
+ const manifestText = spawnSync('tar', ['-xzf', tgz, '-O', `package/npm/native/${platform}/payload.json`], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout;
114
+ let manifest;
115
+ try {
116
+ manifest = JSON.parse(manifestText);
117
+ } catch (error) {
118
+ process.stderr.write(`payload manifest for ${platform} is not valid JSON: ${error.message}\n`);
119
+ failed = true;
120
+ continue;
121
+ }
122
+ if (manifest.product !== 'external-subagent' || manifest.platform !== platform) {
123
+ process.stderr.write(`payload manifest for ${platform} has an inconsistent identity (${manifest.product}/${manifest.platform})\n`);
124
+ failed = true;
125
+ }
126
+ if (platform === hostPlatform && manifest.version !== packedPackage.version) {
127
+ process.stderr.write(`payload version ${manifest.version} (${platform}) does not match package version ${packedPackage.version}\n`);
128
+ failed = true;
129
+ }
93
130
  }
94
131
  }
95
132
  }
96
133
 
97
- // Staged payload checks run in every mode (pack, publish, dry run): the
98
- // package version and the staged native binaries must agree right where the
99
- // tarball is built from.
134
+ // Staged payload checks: the staged native binaries and the package version
135
+ // must agree right where the tarball is built from. The publish gate
136
+ // (--staged) checks every platform because CI merges both artifacts into one
137
+ // tree before packing; a plain `npm pack` checks the running host's platform,
138
+ // whose payload prepack rebuilds from source (the foreign platform is required
139
+ // to be present by the tarball entry checks above).
100
140
  const repoPackage = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
101
- const stagedManifest = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'npm', 'native', 'darwin-arm64', 'payload.json'), 'utf8'));
102
- if (stagedManifest.version !== repoPackage.version) {
103
- process.stderr.write(`staged payload version ${stagedManifest.version} does not match package version ${repoPackage.version}\n`);
104
- failed = true;
105
- }
106
- if (stagedManifest.product !== 'external-subagent') {
107
- process.stderr.write(`staged payload product ${stagedManifest.product} is not the release payload\n`);
108
- failed = true;
109
- }
110
-
111
- for (const name of ['external-subagentd', 'external-subagent-mcp']) {
112
- const local = path.join(process.cwd(), 'npm', 'native', 'darwin-arm64', name);
113
- const file = spawnSync('file', ['-b', local], { encoding: 'utf8' });
114
- if (!/Mach-O 64-bit executable arm64/u.test(file.stdout)) {
115
- process.stderr.write(`${name} is not a Mach-O arm64 executable: ${file.stdout.trim()}\n`);
141
+ function checkStagedPlatform(platform) {
142
+ const dir = path.join(process.cwd(), 'npm', 'native', platform);
143
+ const manifestPath = path.join(dir, 'payload.json');
144
+ let manifest;
145
+ try {
146
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
147
+ } catch (error) {
148
+ process.stderr.write(`staged ${platform} payload manifest is unavailable: ${error.message}\n`);
149
+ failed = true;
150
+ return;
151
+ }
152
+ if (manifest.version !== repoPackage.version) {
153
+ process.stderr.write(`staged ${platform} payload version ${manifest.version} does not match package version ${repoPackage.version}\n`);
154
+ failed = true;
155
+ }
156
+ if (manifest.product !== 'external-subagent') {
157
+ process.stderr.write(`staged ${platform} payload product ${manifest.product} is not the release payload\n`);
116
158
  failed = true;
117
159
  }
118
- const mode = spawnSync('stat', ['-f', '%Lp', local], { encoding: 'utf8' }).stdout.trim();
119
- if (mode !== '755') { process.stderr.write(`${name} has release mode ${mode}, expected 755\n`); failed = true; }
160
+ if (manifest.platform !== platform) {
161
+ process.stderr.write(`staged ${platform} payload manifest declares ${manifest.platform}\n`);
162
+ failed = true;
163
+ }
164
+ const image = IMAGE_CHECKS[platform];
165
+ for (const name of BINARIES) {
166
+ const local = path.join(dir, name);
167
+ let bytes;
168
+ try {
169
+ bytes = fs.readFileSync(local);
170
+ } catch (error) {
171
+ process.stderr.write(`${platform}/${name} is missing from the staged payload: ${error.code}\n`);
172
+ failed = true;
173
+ continue;
174
+ }
175
+ if (image.probe(bytes) !== image.arch) {
176
+ process.stderr.write(`${platform}/${name} is not a ${image.describe}\n`);
177
+ failed = true;
178
+ }
179
+ const mode = (fs.statSync(local).mode & 0o777).toString(8);
180
+ if (mode !== '755') { process.stderr.write(`${platform}/${name} has release mode ${mode}, expected 755\n`); failed = true; }
181
+ }
182
+ }
183
+
184
+ const stagedPlatforms = stagedOnly ? RELEASE_PLATFORMS : (hostPlatform ? [hostPlatform] : []);
185
+ for (const platform of stagedPlatforms) checkStagedPlatform(platform);
186
+ if (stagedPlatforms.length === 0) {
187
+ note(`no staged release platform for ${process.platform}-${process.arch}; staged payload checks skipped\n`);
120
188
  }
121
189
 
122
190
  if (failed) process.exit(1);
@@ -10,6 +10,7 @@ import fs from 'node:fs';
10
10
  import os from 'node:os';
11
11
  import path from 'node:path';
12
12
  import { spawnSync } from 'node:child_process';
13
+ import { nativePlatform } from '../../cli/install/layout.mjs';
13
14
 
14
15
  function run(command, args, options = {}) {
15
16
  const result = spawnSync(command, args, { encoding: 'utf8', ...options });
@@ -38,7 +39,11 @@ try {
38
39
  run('npm', ['install', '--global', `--prefix=${prefix}`, '--no-audit', '--no-fund', tgz], { cwd: work, env });
39
40
  const cli = path.join(prefix, 'bin', 'external-subagent');
40
41
  const packageRoot = path.join(prefix, 'lib', 'node_modules', 'external-subagent');
41
- for (const target of [cli, path.join(prefix, 'bin', 'external-subagent-mcp'), path.join(packageRoot, 'npm', 'native', 'darwin-arm64', 'external-subagentd')]) {
42
+ // The packed payload directory is the running host's tuple (S01), so the
43
+ // smoke checks the platform it is actually installing rather than a fixed
44
+ // darwin-arm64 path.
45
+ const payloadPlatform = nativePlatform() ?? `${process.platform}-${process.arch}`;
46
+ for (const target of [cli, path.join(prefix, 'bin', 'external-subagent-mcp'), path.join(packageRoot, 'npm', 'native', payloadPlatform, 'external-subagentd')]) {
42
47
  fs.accessSync(target, fs.constants.X_OK);
43
48
  }
44
49
  run(cli, ['version'], { env });