@agentbridgehq/agentbridge 0.1.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.
- package/README.md +28 -0
- package/bin/agentbridge +38 -0
- package/install.js +125 -0
- package/package.json +17 -0
- package/platform.js +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# agentbridge
|
|
2
|
+
|
|
3
|
+
The supply chain for agent extensions — install any agent plugin into any agent
|
|
4
|
+
client.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install -g @agentbridgehq/agentbridge
|
|
8
|
+
agentbridge clients
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Node is a dependency of installing, never of running.** This package downloads
|
|
12
|
+
a static Go binary for your platform on install and verifies its SHA-256 against
|
|
13
|
+
the release's signed checksum file before writing anything. After that, Node is
|
|
14
|
+
not involved: the command you run is the binary.
|
|
15
|
+
|
|
16
|
+
That verification is not optional. `npm` postinstall scripts are a well-worn
|
|
17
|
+
supply-chain vector, and a tool whose whole argument is about knowing where your
|
|
18
|
+
plugins came from cannot have an installer that downloads a binary and trusts it.
|
|
19
|
+
|
|
20
|
+
Other ways to install, both of which also verify:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
brew install agentbridge/tap/agentbridge
|
|
24
|
+
curl -fsSL https://raw.githubusercontent.com/agentbridge/agentbridge/main/install.sh | sh
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Source, documentation and issues:
|
|
28
|
+
https://github.com/agentbridgehq/agentbridge
|
package/bin/agentbridge
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Thin shim: run the downloaded binary and pass everything through.
|
|
5
|
+
//
|
|
6
|
+
// Node is a dependency of *installing*, never of running. The binary is a
|
|
7
|
+
// static executable, so once it is in place this shim only forwards stdio,
|
|
8
|
+
// arguments and the exit code — which matters because agentbridge is itself
|
|
9
|
+
// launched by agent clients as an MCP server wrapper (`agentbridge run`), where
|
|
10
|
+
// anything that mangles signals or exit codes would be a real fault.
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
const { binaryPath } = require('../platform');
|
|
16
|
+
|
|
17
|
+
// Asked of platform.js rather than rebuilt here: this shim is itself called
|
|
18
|
+
// agentbridge and lives in bin/, so a path computed independently is one
|
|
19
|
+
// mistake away from resolving to this file and spawning itself.
|
|
20
|
+
const binary = binaryPath(path.join(__dirname, '..'), process.platform);
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(binary)) {
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
'agentbridge: the binary is missing. Reinstall with `npm install -g agentbridge`,\n' +
|
|
25
|
+
'or install another way: https://github.com/agentbridgehq/agentbridge\n'
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
31
|
+
|
|
32
|
+
if (result.error) {
|
|
33
|
+
process.stderr.write(`agentbridge: ${result.error.message}\n`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
// A process killed by a signal has no exit code; report it the way a shell
|
|
37
|
+
// would rather than exiting 0 and claiming success.
|
|
38
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/install.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Downloads the agentbridge binary for this platform.
|
|
4
|
+
//
|
|
5
|
+
// The package ships no binaries. Publishing six platforms' worth would make the
|
|
6
|
+
// tarball enormous for every user, and the alternative — optional
|
|
7
|
+
// platform-specific packages — multiplies the number of things that must be
|
|
8
|
+
// published in lockstep. Fetching one at install time keeps the package small
|
|
9
|
+
// and the release process single-artifact.
|
|
10
|
+
//
|
|
11
|
+
// The checksum is verified before anything is written. npm postinstall scripts
|
|
12
|
+
// are a well-worn supply-chain vector, and a tool that argues about the
|
|
13
|
+
// provenance of plugins cannot have an installer that downloads a binary and
|
|
14
|
+
// trusts it.
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const zlib = require('zlib');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const { execFileSync } = require('child_process');
|
|
22
|
+
const { artifactFor, binaryPath } = require('./platform');
|
|
23
|
+
|
|
24
|
+
const REPO = 'agentbridgehq/agentbridge';
|
|
25
|
+
const VERSION = require('./package.json').version;
|
|
26
|
+
const BASE =
|
|
27
|
+
process.env.AGENTBRIDGE_BASE_URL ||
|
|
28
|
+
`https://github.com/${REPO}/releases/download/v${VERSION}`;
|
|
29
|
+
|
|
30
|
+
async function fetchBuffer(url) {
|
|
31
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`${url}: HTTP ${res.status}`);
|
|
34
|
+
}
|
|
35
|
+
return Buffer.from(await res.arrayBuffer());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256(buf) {
|
|
39
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// verifyChecksum refuses anything the checksums file does not list, rather than
|
|
43
|
+
// skipping verification when the entry is absent — an unlisted artifact is the
|
|
44
|
+
// case this check exists to catch.
|
|
45
|
+
function verifyChecksum(name, buf, checksums) {
|
|
46
|
+
const line = checksums
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((l) => l.trim())
|
|
49
|
+
.find((l) => l.endsWith(` ${name}`) || l.endsWith(` ${name}`));
|
|
50
|
+
|
|
51
|
+
if (!line) {
|
|
52
|
+
throw new Error(`checksums.txt does not list ${name}; refusing to install`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const expected = line.split(/\s+/)[0];
|
|
56
|
+
const actual = sha256(buf);
|
|
57
|
+
if (expected !== actual) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`checksum mismatch for ${name}\n expected ${expected}\n actual ${actual}\nDo not use this download.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// extract pulls the single binary out of the archive.
|
|
65
|
+
//
|
|
66
|
+
// tar and unzip are invoked rather than depended on: adding an archive library
|
|
67
|
+
// would put third-party code in the install path of a security tool for the
|
|
68
|
+
// sake of one file.
|
|
69
|
+
function extract(archivePath, artifact, destDir) {
|
|
70
|
+
if (archivePath.endsWith('.zip')) {
|
|
71
|
+
execFileSync('unzip', ['-o', '-q', archivePath, artifact.binary, '-d', destDir], {
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
execFileSync('tar', ['-xzf', archivePath, '-C', destDir, artifact.binary], {
|
|
77
|
+
stdio: 'inherit',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function main() {
|
|
82
|
+
const artifact = artifactFor(VERSION, process.platform, process.arch);
|
|
83
|
+
const target = binaryPath(__dirname, process.platform);
|
|
84
|
+
const destDir = path.dirname(target);
|
|
85
|
+
|
|
86
|
+
if (fs.existsSync(target)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
process.stderr.write(`agentbridge v${VERSION} (${artifact.os}/${artifact.arch})\n`);
|
|
91
|
+
|
|
92
|
+
const [archive, checksums] = await Promise.all([
|
|
93
|
+
fetchBuffer(`${BASE}/${artifact.name}`),
|
|
94
|
+
fetchBuffer(`${BASE}/checksums.txt`).then((b) => b.toString('utf8')),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
verifyChecksum(artifact.name, archive, checksums);
|
|
98
|
+
process.stderr.write(' checksum ok\n');
|
|
99
|
+
|
|
100
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agentbridge-'));
|
|
101
|
+
try {
|
|
102
|
+
const archivePath = path.join(tmp, artifact.name);
|
|
103
|
+
fs.writeFileSync(archivePath, archive);
|
|
104
|
+
|
|
105
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
106
|
+
extract(archivePath, artifact, destDir);
|
|
107
|
+
fs.chmodSync(target, 0o755);
|
|
108
|
+
|
|
109
|
+
process.stderr.write(` installed ${target}\n`);
|
|
110
|
+
} finally {
|
|
111
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch((err) => {
|
|
116
|
+
process.stderr.write(`\nagentbridge install failed: ${err.message}\n\n`);
|
|
117
|
+
process.stderr.write(
|
|
118
|
+
'Install another way instead:\n' +
|
|
119
|
+
' brew install agentbridge/tap/agentbridge\n' +
|
|
120
|
+
` curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sh\n`
|
|
121
|
+
);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
module.exports = { verifyChecksum, sha256 };
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentbridgehq/agentbridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The supply chain for agent extensions — install any agent plugin into any agent client",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Masih Moloodian <masihmoloodian@gmail.com>",
|
|
7
|
+
"homepage": "https://github.com/agentbridgehq/agentbridge",
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/agentbridgehq/agentbridge.git" },
|
|
9
|
+
"keywords": ["agent", "plugins", "mcp", "agent-plugins", "skills", "cli"],
|
|
10
|
+
"bin": { "agentbridge": "bin/agentbridge" },
|
|
11
|
+
"files": ["bin/", "install.js", "platform.js", "README.md"],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"postinstall": "node install.js",
|
|
14
|
+
"test": "node --test"
|
|
15
|
+
},
|
|
16
|
+
"engines": { "node": ">=18" }
|
|
17
|
+
}
|
package/platform.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Mapping from Node's platform names to the release artifacts.
|
|
6
|
+
//
|
|
7
|
+
// Separated from the installer so it can be tested without a network, because
|
|
8
|
+
// this is where a distribution bug hides best: a wrong mapping produces a
|
|
9
|
+
// confident download of a binary for the wrong architecture, and the failure
|
|
10
|
+
// surfaces much later as "exec format error" with nothing pointing back here.
|
|
11
|
+
|
|
12
|
+
const PLATFORMS = {
|
|
13
|
+
darwin: 'darwin',
|
|
14
|
+
linux: 'linux',
|
|
15
|
+
win32: 'windows',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ARCHS = {
|
|
19
|
+
x64: 'amd64',
|
|
20
|
+
arm64: 'arm64',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the release artifact for a platform and architecture.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} version release version, with or without a leading "v"
|
|
27
|
+
* @param {string} platform Node's process.platform
|
|
28
|
+
* @param {string} arch Node's process.arch
|
|
29
|
+
*/
|
|
30
|
+
function artifactFor(version, platform, arch) {
|
|
31
|
+
const os = PLATFORMS[platform];
|
|
32
|
+
const goarch = ARCHS[arch];
|
|
33
|
+
|
|
34
|
+
if (!os || !goarch) {
|
|
35
|
+
// Naming both the unsupported pair and the supported set turns a dead end
|
|
36
|
+
// into something the reader can act on.
|
|
37
|
+
throw new Error(
|
|
38
|
+
`agentbridge does not publish a binary for ${platform}/${arch}.\n` +
|
|
39
|
+
`Supported: ${supportedPairs().join(', ')}.\n` +
|
|
40
|
+
`Build from source instead: https://github.com/agentbridgehq/agentbridge`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const stripped = String(version).replace(/^v/, '');
|
|
45
|
+
const ext = os === 'windows' ? 'zip' : 'tar.gz';
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
os,
|
|
49
|
+
arch: goarch,
|
|
50
|
+
// Must match archives.name_template in .goreleaser.yaml. A drift test in
|
|
51
|
+
// the Go suite keeps the two from separating.
|
|
52
|
+
name: `agentbridge_${stripped}_${os}_${goarch}.${ext}`,
|
|
53
|
+
binary: os === 'windows' ? 'agentbridge.exe' : 'agentbridge',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function supportedPairs() {
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const platform of Object.keys(PLATFORMS)) {
|
|
60
|
+
for (const arch of Object.keys(ARCHS)) {
|
|
61
|
+
out.push(`${platform}/${arch}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// binaryPath is where the downloaded executable lives, and it is deliberately
|
|
68
|
+
// not bin/.
|
|
69
|
+
//
|
|
70
|
+
// bin/agentbridge is the shim npm links onto the PATH, and it is shipped in the
|
|
71
|
+
// package. Downloading the real binary to that same name meant three things
|
|
72
|
+
// claimed one path: the installer's "already present?" check saw the shim and
|
|
73
|
+
// skipped the download, and the shim then found "the binary" — itself — and
|
|
74
|
+
// spawned it, recursing until something ran out. `npm i -g @agentbridgehq/agentbridge`
|
|
75
|
+
// produced a command that hung on first use.
|
|
76
|
+
//
|
|
77
|
+
// The shim and the installer both ask this function, so the two cannot drift
|
|
78
|
+
// apart again.
|
|
79
|
+
function binaryPath(packageRoot, platform) {
|
|
80
|
+
const name = platform === 'win32' ? 'agentbridge.exe' : 'agentbridge';
|
|
81
|
+
return path.join(packageRoot, 'vendor', name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { artifactFor, supportedPairs, binaryPath, PLATFORMS, ARCHS };
|