@whoz-oss/coday-server 0.236.0 → 0.238.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.
Files changed (3) hide show
  1. package/package.json +10 -1
  2. package/postinstall.js +215 -0
  3. package/server.js +0 -2308
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "@whoz-oss/coday-server",
3
- "version": "0.236.0",
3
+ "version": "0.238.0",
4
4
  "repository": "https://github.com/whoz-oss/coday",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
7
7
  "bin": {
8
8
  "coday-server": "./bin.js"
9
9
  },
10
+ "files": [
11
+ "bin.js",
12
+ "postinstall.js",
13
+ "server/",
14
+ "coday-client/"
15
+ ],
10
16
  "dependencies": {
11
17
  "@vscode/ripgrep": "1.15.9",
12
18
  "express": "5.1.0",
@@ -23,5 +29,8 @@
23
29
  },
24
30
  "publishConfig": {
25
31
  "access": "public"
32
+ },
33
+ "scripts": {
34
+ "postinstall": "node postinstall.js"
26
35
  }
27
36
  }
package/postinstall.js ADDED
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall.js — downloads AgentOS JARs from the matching GitHub Release.
4
+ *
5
+ * The JARs are too large to bundle in the npm tarball (they are already-compressed
6
+ * ZIP archives and do not benefit from gzip). Instead, they are uploaded as assets
7
+ * on the GitHub Release that corresponds to the version of this package, and this
8
+ * script fetches them on first install.
9
+ *
10
+ * The script is intentionally dependency-free (only Node built-ins) so it works
11
+ * in any environment without a prior `npm install`.
12
+ *
13
+ * Environment variables:
14
+ * AGENTOS_HOSTNAME if set, an external AgentOS instance is configured — skip download
15
+ * AGENTOS_PORT if set, an external AgentOS instance is configured — skip download
16
+ * CODAY_AGENTOS_VERSION override the version used to find the GitHub Release (e.g. for testing)
17
+ */
18
+ import { createHash } from 'crypto'
19
+ import { createWriteStream, existsSync, mkdirSync, readFileSync } from 'fs'
20
+ import { pipeline } from 'stream/promises'
21
+ import { dirname, resolve } from 'path'
22
+ import { fileURLToPath } from 'url'
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url))
25
+
26
+ const REPO = 'whoz-oss/coday'
27
+ const SEMVER_RE = /^\d+\.\d+\.\d+$/
28
+ const ALLOWED_DOWNLOAD_HOSTS = ['github.com', 'objects.githubusercontent.com']
29
+ // First release that includes a checksums.sha256 manifest — older releases are exempt
30
+ const CHECKSUM_MANDATORY_FROM = [0, 238, 0]
31
+
32
+ // JARs expected relative to this script's directory after download
33
+ const JARS = [
34
+ { asset: 'agentos-service.jar', dest: 'agentos/agentos-service.jar' },
35
+ // Plugin JARs are uploaded with versioned names (e.g. agentos-bash-plugin-1.2.3.jar).
36
+ // We match by prefix and rename to a stable filename on download.
37
+ { assetPrefix: 'agentos-bash-plugin-', dest: 'agentos/plugins/agentos-bash-plugin.jar' },
38
+ { assetPrefix: 'agentos-file-plugin-', dest: 'agentos/plugins/agentos-file-plugin.jar' },
39
+ { assetPrefix: 'agentos-mcp-plugin-', dest: 'agentos/plugins/agentos-mcp-plugin.jar' },
40
+ { assetPrefix: 'agentos-tmux-plugin-', dest: 'agentos/plugins/agentos-tmux-plugin.jar' },
41
+ ]
42
+
43
+ async function fetchJson(url) {
44
+ const res = await fetch(url, {
45
+ headers: { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' },
46
+ })
47
+ if (!res.ok) throw new Error(`GET ${url} → ${res.status} ${res.statusText}`)
48
+ return res.json()
49
+ }
50
+
51
+ function validateDownloadUrl(url) {
52
+ const parsed = new URL(url)
53
+ if (!ALLOWED_DOWNLOAD_HOSTS.includes(parsed.hostname)) {
54
+ throw new Error(`Refusing to download from unexpected host: ${parsed.hostname}`)
55
+ }
56
+ }
57
+
58
+ async function downloadFile(url, destPath) {
59
+ validateDownloadUrl(url)
60
+ mkdirSync(dirname(destPath), { recursive: true })
61
+ const res = await fetch(url, { headers: { Accept: 'application/octet-stream' }, redirect: 'follow' })
62
+ if (!res.ok) throw new Error(`Download ${url} → ${res.status} ${res.statusText}`)
63
+ await pipeline(res.body, createWriteStream(destPath))
64
+ }
65
+
66
+ function sha256File(filePath) {
67
+ const hash = createHash('sha256')
68
+ hash.update(readFileSync(filePath))
69
+ return hash.digest('hex')
70
+ }
71
+
72
+ function isChecksumMandatory(version) {
73
+ const [major, minor, patch] = version.split('.').map(Number)
74
+ const [mMajor, mMinor, mPatch] = CHECKSUM_MANDATORY_FROM
75
+ if (major !== mMajor) return major > mMajor
76
+ if (minor !== mMinor) return minor > mMinor
77
+ return patch >= mPatch
78
+ }
79
+
80
+ async function fetchChecksums(assets, tag, version) {
81
+ const checksumAsset = assets.find((a) => a.name === 'checksums.sha256')
82
+ if (!checksumAsset) {
83
+ if (isChecksumMandatory(version)) {
84
+ const err = new Error(`No checksums.sha256 found in release ${tag} — aborting download to prevent supply-chain attack`)
85
+ err.securityFailure = true
86
+ throw err
87
+ }
88
+ console.warn(`[coday-server] No checksums.sha256 in release ${tag} (pre-${CHECKSUM_MANDATORY_FROM.join('.')} release) — skipping integrity check`)
89
+ return null
90
+ }
91
+ validateDownloadUrl(checksumAsset.browser_download_url)
92
+ const res = await fetch(checksumAsset.browser_download_url, {
93
+ headers: { Accept: 'application/octet-stream' },
94
+ redirect: 'follow',
95
+ })
96
+ if (!res.ok) throw new Error(`Download checksums.sha256 → ${res.status} ${res.statusText}`)
97
+ const text = await res.text()
98
+ // Parse "<hash> <filename>" lines
99
+ const map = {}
100
+ for (const line of text.split('\n')) {
101
+ const parts = line.trim().split(/\s+/)
102
+ if (parts.length >= 2) map[parts[1]] = parts[0]
103
+ }
104
+ return map
105
+ }
106
+
107
+ async function main() {
108
+ if (process.env.AGENTOS_HOSTNAME || process.env.AGENTOS_PORT) {
109
+ console.log('[coday-server] External AgentOS configured — skipping JAR download')
110
+ return
111
+ }
112
+
113
+ // Read version from package.json sitting next to this script (or use override)
114
+ const { createRequire } = await import('module')
115
+ const require = createRequire(import.meta.url)
116
+ const { version: pkgVersion } = require('./package.json')
117
+ const rawVersion = process.env.CODAY_AGENTOS_VERSION ?? pkgVersion
118
+ if (!SEMVER_RE.test(rawVersion)) {
119
+ console.warn(`[coday-server] Invalid version format: "${rawVersion}" — skipping JAR download`)
120
+ return
121
+ }
122
+ const version = rawVersion
123
+
124
+ const tag = `release/${version}`
125
+ const allPresent = JARS.every(({ dest }) => existsSync(resolve(__dirname, dest)))
126
+ if (allPresent) {
127
+ console.log(`[coday-server] AgentOS JARs already present (v${version}) — skipping download`)
128
+ return
129
+ }
130
+
131
+ console.log(`[coday-server] Downloading AgentOS JARs v${version} from GitHub Release ${tag}...`)
132
+
133
+ let assets
134
+ try {
135
+ const release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`)
136
+ assets = release.assets
137
+ } catch (err) {
138
+ console.warn(`[coday-server] Could not fetch release ${tag}: ${err.message}`)
139
+ console.warn('[coday-server] AgentOS will not be available.')
140
+ return
141
+ }
142
+
143
+ const checksums = await fetchChecksums(assets, tag, version)
144
+
145
+ let failed = false
146
+ for (const { asset, assetPrefix, dest } of JARS) {
147
+ const destPath = resolve(__dirname, dest)
148
+ if (existsSync(destPath)) {
149
+ console.log(`[coday-server] ✓ ${dest} already present`)
150
+ continue
151
+ }
152
+
153
+ const found = asset
154
+ ? assets.find((a) => a.name === asset)
155
+ : assets.find((a) => a.name.startsWith(assetPrefix) && a.name.endsWith('.jar'))
156
+ const assetLabel = asset ?? assetPrefix + '*'
157
+ if (!found) {
158
+ console.warn(`[coday-server] ✗ asset ${assetLabel} not found in release ${tag}`)
159
+ failed = true
160
+ continue
161
+ }
162
+
163
+ process.stdout.write(`[coday-server] ↓ ${found.name}...`)
164
+ try {
165
+ await downloadFile(found.browser_download_url, destPath)
166
+
167
+ // Verify checksum if manifest is available
168
+ if (checksums) {
169
+ // The manifest uses the stable filename (basename of dest)
170
+ const stableFilename = dest.split('/').pop()
171
+ const expectedHash = checksums[stableFilename]
172
+ if (!expectedHash) {
173
+ process.stdout.write(` INTEGRITY FAILURE\n`)
174
+ console.error(`[coday-server] ✗ No checksum entry for ${stableFilename} in manifest — aborting`)
175
+ import('fs').then(({ unlinkSync }) => { try { unlinkSync(destPath) } catch {} })
176
+ failed = true
177
+ continue
178
+ }
179
+ const actualHash = sha256File(destPath)
180
+ if (actualHash !== expectedHash) {
181
+ process.stdout.write(` INTEGRITY FAILURE\n`)
182
+ console.error(`[coday-server] ✗ Checksum mismatch for ${stableFilename}:`)
183
+ console.error(`[coday-server] expected: ${expectedHash}`)
184
+ console.error(`[coday-server] actual: ${actualHash}`)
185
+ // Remove the corrupted file
186
+ import('fs').then(({ unlinkSync }) => { try { unlinkSync(destPath) } catch {} })
187
+ failed = true
188
+ continue
189
+ }
190
+ process.stdout.write(' ✓\n')
191
+ } else {
192
+ process.stdout.write(' done\n')
193
+ }
194
+ } catch (err) {
195
+ process.stdout.write(` FAILED: ${err.message}\n`)
196
+ failed = true
197
+ }
198
+ }
199
+
200
+ if (failed) {
201
+ console.warn('[coday-server] Some JARs could not be downloaded. AgentOS may not be available.')
202
+ } else {
203
+ console.log('[coday-server] AgentOS JARs downloaded successfully.')
204
+ }
205
+ }
206
+
207
+ main().catch((err) => {
208
+ if (err.securityFailure) {
209
+ // Hard fail on integrity/security errors — a missing checksum manifest is a red flag
210
+ console.error('[coday-server] SECURITY ERROR:', err.message)
211
+ process.exit(1)
212
+ }
213
+ // Never fail the install for network/availability issues — missing JARs just means AgentOS won't start
214
+ console.warn('[coday-server] postinstall error:', err.message)
215
+ })