@hivemagnus/hive-skills 1.0.3 → 1.2.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 +24 -3
- package/dist/app.js +9 -2
- package/dist/check.js +67 -0
- package/dist/cli.js +53 -3
- package/dist/db.js +2 -52
- package/dist/deployment-worker.js +7 -3
- package/dist/env-schema.js +26 -0
- package/dist/migrations/managed-format-1.js +54 -0
- package/dist/release/ZOD_LICENSE +21 -0
- package/dist/release/runtime.js +23771 -0
- package/dist/schema.js +2 -0
- package/dist/seed.js +3 -1
- package/dist/server.js +15 -4
- package/dist/ui.js +15 -0
- package/dist/upgrade.js +61 -0
- package/package.json +253 -4
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
The registry is intentionally small and local-first. SQLite records skills, releases, sessions, audit events, and deployment targets; archives live under the configured data directory. The initial catalog entry is **HiveMem**, based on the existing HiveMem Hermes plugin/skill source supplied at seed time.
|
|
6
6
|
|
|
7
|
+
## Install with the Hive launcher
|
|
8
|
+
|
|
9
|
+
Run `npx @hivemagnus/hive@^1.1.0 init --services all` to configure all four services,
|
|
10
|
+
or select `--services skills` for this registry alone. The launcher generates private
|
|
11
|
+
configuration, a strong session secret and TLS, and wires the Router's Skills panel.
|
|
12
|
+
Skills OS/PAM administration remains separate from IAM sign-in. See the
|
|
13
|
+
[launcher guide](../../packages/hive/README.md#include-hive-skills).
|
|
14
|
+
|
|
7
15
|
## Install from npm
|
|
8
16
|
|
|
9
17
|
Requires Linux, Node.js 24.22.1+ or 26.13.0+, TLS certificates, and `pamtester` for
|
|
@@ -11,15 +19,18 @@ OS-backed sign-in. SQLite is provided by Node; there are no native npm add-ons.
|
|
|
11
19
|
Agent deployment also needs Bash, OpenSSH, and permission to enable the target's user timer.
|
|
12
20
|
|
|
13
21
|
```bash
|
|
14
|
-
npm install --prefix /opt/hive-skills/runtime @hivemagnus/hive-skills@1.0
|
|
22
|
+
npm install --prefix /opt/hive-skills/runtime @hivemagnus/hive-skills@1.2.0
|
|
15
23
|
/opt/hive-skills/runtime/node_modules/.bin/hive-skills --env-file /opt/hive-skills/skills.env
|
|
16
24
|
```
|
|
17
25
|
|
|
18
26
|
Or run `npx @hivemagnus/hive-skills --env-file skills.env`. The CLI accepts `start`
|
|
19
|
-
(the default), `seed`, `--help` and `--version`.
|
|
27
|
+
(the default), `seed`, `check`, `env-schema`, `migrate`, `upgrade`, `rollback`, `--help` and `--version`. `check` validates TLS,
|
|
28
|
+
configuration and an existing database without creating or migrating state. `env-schema` prints
|
|
29
|
+
configuration metadata without loading runtime configuration. Environment-file paths may be absolute;
|
|
20
30
|
existing environment variables win. Without `--env-file`, the historical `./.env` lookup
|
|
21
31
|
is retained. Help/version do not initialize storage. A fresh install starts with an empty
|
|
22
|
-
catalog; startup never seeds or replaces existing records.
|
|
32
|
+
catalog; startup never seeds or replaces existing records. Run `hive-skills migrate`
|
|
33
|
+
explicitly before first startup, or enable `HIVE_AUTO_MIGRATE=true` with backup access.
|
|
23
34
|
|
|
24
35
|
Configure these values in a private environment file (mode 0600):
|
|
25
36
|
|
|
@@ -145,3 +156,13 @@ version in `package.json`. CI also checks the package allowlist and npm 12 insta
|
|
|
145
156
|
The package publishes from `.github/workflows/publish.yml` alongside the other packages;
|
|
146
157
|
configure its npm trusted publisher for that workflow and the `npm-publish` environment after
|
|
147
158
|
the initial authenticated publication.
|
|
159
|
+
|
|
160
|
+
## Managed upgrades
|
|
161
|
+
|
|
162
|
+
Use `hive upgrade --dry-run`, then `hive upgrade` for the enrolled release bundle. This server's
|
|
163
|
+
`upgrade` and `rollback` commands delegate to the same owner-run launcher. `migrate --plan --json`
|
|
164
|
+
is read-only; migration requires a verified backup. Set `HIVE_AUTO_MIGRATE=true` explicitly for
|
|
165
|
+
startup migrations outside a managed install. Slow or one-way automatic migrations are refused.
|
|
166
|
+
`check --json` reports configuration without initializing state. No upgrade rotates signing keys
|
|
167
|
+
or reseeds existing accounts. See [UPGRADING.md](https://github.com/ChefTaco/hive/blob/main/UPGRADING.md)
|
|
168
|
+
for adoption, compatibility, recovery, update notices and container controls.
|
package/dist/app.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { maintenanceActive } from './release/runtime.js';
|
|
2
|
+
import { metadata } from './release/runtime.js';
|
|
3
|
+
import { packageRoot as releasePackageRoot } from './paths.js';
|
|
4
|
+
import { home as upgradeHome, updateStatus } from './release/runtime.js';
|
|
1
5
|
import { bodyLimit } from 'hono/body-limit';
|
|
2
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
7
|
import { Hono } from 'hono';
|
|
@@ -50,6 +54,8 @@ const archiveUrl = (skillSlug, release, channel) => {
|
|
|
50
54
|
const base = `${config.publicUrl}/v1/skills/${skillSlug}/releases/${release.version}/archive`;
|
|
51
55
|
return channel === 'stable' ? base : `${base}?channel=${channel}`;
|
|
52
56
|
};
|
|
57
|
+
app.use('*', async (c, next) => { if (maintenanceActive(new URL(c.req.url).pathname))
|
|
58
|
+
return c.json({ error: 'Hive is undergoing maintenance' }, 503); await next(); });
|
|
53
59
|
app.use('*', bodyLimit({ maxSize: 70 * 1024 * 1024 }));
|
|
54
60
|
for (const target of config.targets)
|
|
55
61
|
store.seedTarget(target);
|
|
@@ -60,11 +66,11 @@ app.use('*', async (c, next) => {
|
|
|
60
66
|
await next();
|
|
61
67
|
});
|
|
62
68
|
app.get('/', (c) => c.html(page));
|
|
63
|
-
app.get('/health/live', (c) => c.json({ status: 'ok', service: 'hive-skills' }));
|
|
69
|
+
app.get('/health/live', (c) => c.json({ ...metadata(releasePackageRoot), status: 'ok', service: 'hive-skills' }));
|
|
64
70
|
app.get('/healthz', (c) => {
|
|
65
71
|
try {
|
|
66
72
|
const health = store.readiness();
|
|
67
|
-
return c.json({ status: health.ready ? 'ok' : 'degraded', service: 'hive-skills', ...health }, health.ready ? 200 : 503);
|
|
73
|
+
return c.json({ ...metadata(releasePackageRoot), status: health.ready ? 'ok' : 'degraded', service: 'hive-skills', ...health }, health.ready ? 200 : 503);
|
|
68
74
|
}
|
|
69
75
|
catch {
|
|
70
76
|
return c.json({ status: 'degraded', service: 'hive-skills', reason: 'database_unavailable' }, 503);
|
|
@@ -179,6 +185,7 @@ app.post('/auth/logout', (c) => {
|
|
|
179
185
|
});
|
|
180
186
|
app.get('/auth/me', requireAuth(), (c) => c.json(c.get('auth')));
|
|
181
187
|
app.get('/admin/skills', requireAuth(true), (c) => c.json(store.listAllSkills()));
|
|
188
|
+
app.get('/admin/updates', requireAuth(true), async (c) => c.json(await updateStatus(metadata(releasePackageRoot).release, upgradeHome())));
|
|
182
189
|
app.post('/admin/skills', requireAuth(true), async (c) => {
|
|
183
190
|
const body = await requestJson(c);
|
|
184
191
|
const skill = store.createSkill({
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { X509Certificate, createPrivateKey } from 'node:crypto';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
/** Read-only preflight: never imports config/app/db, seeds, or creates state. */
|
|
7
|
+
export function checkConfiguration(env) {
|
|
8
|
+
let failures = 0;
|
|
9
|
+
const check = (ok, message) => { console.log(` ${ok ? 'ok ' : 'FAIL'} ${message}`); if (!ok)
|
|
10
|
+
failures++; };
|
|
11
|
+
const port = Number(env.HIVE_SKILLS_PORT ?? '4030');
|
|
12
|
+
check(Number.isInteger(port) && port > 0 && port <= 65535, 'HIVE_SKILLS_PORT is valid');
|
|
13
|
+
check((env.HIVE_SKILLS_SESSION_SECRET?.trim().length ?? 0) >= 32, 'HIVE_SKILLS_SESSION_SECRET has at least 32 characters');
|
|
14
|
+
let hostname;
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(env.HIVE_SKILLS_PUBLIC_URL ?? `https://localhost:${port}`);
|
|
17
|
+
hostname = url.hostname.replace(/^\[|\]$/g, '');
|
|
18
|
+
check(url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash, 'HIVE_SKILLS_PUBLIC_URL is an HTTPS URL without credentials');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
check(false, 'HIVE_SKILLS_PUBLIC_URL must be a valid HTTPS URL');
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const cert = new X509Certificate(readFileSync(env.HIVE_SKILLS_TLS_CERT_FILE ?? ''));
|
|
25
|
+
const key = createPrivateKey(readFileSync(env.HIVE_SKILLS_TLS_KEY_FILE ?? ''));
|
|
26
|
+
check(cert.checkPrivateKey(key), 'TLS certificate and key match');
|
|
27
|
+
check(Date.parse(cert.validTo) > Date.now() && Date.parse(cert.validFrom) <= Date.now(), 'TLS certificate is currently valid');
|
|
28
|
+
if (hostname)
|
|
29
|
+
check(Boolean(isIP(hostname) ? cert.checkIP(hostname) : cert.checkHost(hostname)), 'TLS certificate covers the public URL hostname');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
check(false, 'HIVE_SKILLS_TLS_CERT_FILE and HIVE_SKILLS_TLS_KEY_FILE must be readable valid PEM files');
|
|
33
|
+
}
|
|
34
|
+
const data = resolve(env.HIVE_SKILLS_DATA_DIR ?? './data');
|
|
35
|
+
try {
|
|
36
|
+
let ancestor = data;
|
|
37
|
+
while (!existsSync(ancestor) && dirname(ancestor) !== ancestor)
|
|
38
|
+
ancestor = dirname(ancestor);
|
|
39
|
+
accessSync(ancestor, constants.W_OK | constants.X_OK);
|
|
40
|
+
check(statSync(ancestor).isDirectory(), 'Skills data directory exists and is writable, or can be created');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
check(false, 'Skills data directory must be writable');
|
|
44
|
+
}
|
|
45
|
+
const database = resolve(data, 'registry.sqlite');
|
|
46
|
+
if (existsSync(database)) {
|
|
47
|
+
let db;
|
|
48
|
+
try {
|
|
49
|
+
db = new DatabaseSync(database, { readOnly: true });
|
|
50
|
+
check(db.prepare('PRAGMA quick_check').get()?.quick_check === 'ok', 'SQLite integrity');
|
|
51
|
+
check(db.prepare('PRAGMA foreign_key_check').all().length === 0, 'SQLite foreign keys');
|
|
52
|
+
db.prepare('SELECT slug FROM skills LIMIT 1').get();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
check(false, 'Existing database must be a readable Skills registry');
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
db?.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
console.log(' info Registry database will be created on first start; catalog starts empty');
|
|
63
|
+
if (!env.HIVE_SKILLS_ADMIN_USERS?.trim())
|
|
64
|
+
console.log(' ! HIVE_SKILLS_ADMIN_USERS is empty; OS administration is disabled');
|
|
65
|
+
console.log(failures ? `Result: ${failures} problem(s)` : 'Result: ready to start');
|
|
66
|
+
return failures ? 1 : 0;
|
|
67
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,16 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import { packageRoot } from './paths.js';
|
|
5
5
|
const usage = `Hive Skills: HTTPS skill registry and Hermes distribution bridge.
|
|
6
6
|
|
|
7
|
-
Usage: hive-skills [start|seed] [--env-file FILE]
|
|
7
|
+
Usage: hive-skills [start|seed|check|env-schema|migrate|upgrade|rollback] [--env-file FILE]
|
|
8
8
|
hive-skills --help | --version
|
|
9
9
|
|
|
10
10
|
start Start the registry (default); requires TLS key and certificate.
|
|
11
|
+
check Validate configuration, TLS and existing storage without writing.
|
|
12
|
+
--json prints a JSON report.
|
|
13
|
+
migrate --plan [--json] inspects; otherwise back up and migrate (--backup-dir DIR).
|
|
14
|
+
upgrade Delegate a complete managed bundle upgrade to the Hive launcher.
|
|
15
|
+
rollback Delegate managed code rollback or explicitly confirmed data restore.
|
|
16
|
+
env-schema Print configuration metadata as JSON without loading configuration.
|
|
11
17
|
seed Explicitly seed the bundled HiveMem skill; never runs on start.
|
|
12
18
|
--env-file Load a dotenv file; existing environment values win.
|
|
13
19
|
|
|
@@ -16,6 +22,27 @@ HIVE_SKILLS_ADMIN_USERS, HIVE_SKILLS_TLS_KEY_FILE and HIVE_SKILLS_TLS_CERT_FILE.
|
|
|
16
22
|
See the package README for the complete configuration and migration guide.`;
|
|
17
23
|
export async function main(args) {
|
|
18
24
|
try {
|
|
25
|
+
const special = args.find(arg => ['upgrade', 'rollback', 'migrate'].includes(arg));
|
|
26
|
+
if (special) {
|
|
27
|
+
const rest = [...args];
|
|
28
|
+
rest.splice(rest.indexOf(special), 1);
|
|
29
|
+
for (let i = 0; i < rest.length; i++) {
|
|
30
|
+
if (rest[i] === '--env-file' || rest[i]?.startsWith('--env-file=')) {
|
|
31
|
+
const inline = rest[i].startsWith('--env-file=');
|
|
32
|
+
const file = inline ? rest[i].slice(11) : rest[i + 1];
|
|
33
|
+
if (!file)
|
|
34
|
+
throw new Error('--env-file requires a file');
|
|
35
|
+
process.loadEnvFile(resolve(file));
|
|
36
|
+
rest.splice(i, inline ? 1 : 2);
|
|
37
|
+
i--;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (special === 'migrate') {
|
|
41
|
+
await (await import('./upgrade.js')).skillsMigration(rest);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
return (await import('./release/runtime.js')).delegate([special, ...rest]);
|
|
45
|
+
}
|
|
19
46
|
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
20
47
|
console.log(usage);
|
|
21
48
|
return 0;
|
|
@@ -25,6 +52,11 @@ export async function main(args) {
|
|
|
25
52
|
console.log(manifest.version);
|
|
26
53
|
return 0;
|
|
27
54
|
}
|
|
55
|
+
if (args.length === 1 && args[0] === 'env-schema') {
|
|
56
|
+
const { envSchemaDocument } = await import('./env-schema.js');
|
|
57
|
+
console.log(JSON.stringify(envSchemaDocument()));
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
28
60
|
let command;
|
|
29
61
|
const files = [];
|
|
30
62
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -35,15 +67,33 @@ export async function main(args) {
|
|
|
35
67
|
throw new Error('--env-file requires a file path');
|
|
36
68
|
files.push(file);
|
|
37
69
|
}
|
|
38
|
-
else if (!command && (arg === 'start' || arg === 'seed'))
|
|
70
|
+
else if (!command && (arg === 'start' || arg === 'seed' || arg === 'check'))
|
|
39
71
|
command = arg;
|
|
72
|
+
else if (arg === '--json' && command === 'check')
|
|
73
|
+
continue;
|
|
40
74
|
else
|
|
41
75
|
throw new Error('Invalid arguments. Run hive-skills --help.');
|
|
42
76
|
}
|
|
43
77
|
for (const file of files)
|
|
44
78
|
process.loadEnvFile(resolve(file));
|
|
45
|
-
if (command === '
|
|
79
|
+
if (command === 'check') {
|
|
80
|
+
const { checkConfiguration } = await import('./check.js');
|
|
81
|
+
// Preserve the historical cwd env lookup without importing the writable config module.
|
|
82
|
+
if (files.length === 0) {
|
|
83
|
+
const { existsSync } = await import('node:fs');
|
|
84
|
+
if (existsSync('.env'))
|
|
85
|
+
process.loadEnvFile('.env');
|
|
86
|
+
}
|
|
87
|
+
if (args.includes('--json'))
|
|
88
|
+
return (await import('./release/runtime.js')).captureCheck(() => checkConfiguration(process.env));
|
|
89
|
+
const code = checkConfiguration(process.env);
|
|
90
|
+
await (await import('./release/runtime.js')).printUpdateNotice(JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8')).version);
|
|
91
|
+
return code;
|
|
92
|
+
}
|
|
93
|
+
if (command === 'seed') {
|
|
94
|
+
await (await import('./upgrade.js')).skillsMigration([]);
|
|
46
95
|
await import('./seed.js');
|
|
96
|
+
}
|
|
47
97
|
else
|
|
48
98
|
await import('./server.js');
|
|
49
99
|
return 0;
|
package/dist/db.js
CHANGED
|
@@ -3,59 +3,9 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
4
|
import { config } from './config.js';
|
|
5
5
|
import { compatibleVersion } from './compatibility.js';
|
|
6
|
+
if (!existsSync(config.databasePath))
|
|
7
|
+
throw new Error('Skills database is missing; run hive-skills migrate before opening storage');
|
|
6
8
|
const db = new DatabaseSync(config.databasePath, { enableForeignKeyConstraints: true });
|
|
7
|
-
db.exec(`
|
|
8
|
-
PRAGMA journal_mode = WAL;
|
|
9
|
-
CREATE TABLE IF NOT EXISTS skills (
|
|
10
|
-
slug TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, homepage TEXT,
|
|
11
|
-
enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
12
|
-
) STRICT;
|
|
13
|
-
CREATE TABLE IF NOT EXISTS releases (
|
|
14
|
-
skill_slug TEXT NOT NULL REFERENCES skills(slug) ON DELETE CASCADE, version TEXT NOT NULL,
|
|
15
|
-
sha256 TEXT NOT NULL, archive_path TEXT NOT NULL, notes TEXT, created_at TEXT NOT NULL,
|
|
16
|
-
min_hermes_version TEXT, max_hermes_version TEXT,
|
|
17
|
-
PRIMARY KEY(skill_slug, version)
|
|
18
|
-
) STRICT;
|
|
19
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
20
|
-
id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL
|
|
21
|
-
) STRICT;
|
|
22
|
-
CREATE TABLE IF NOT EXISTS deployment_targets (
|
|
23
|
-
name TEXT PRIMARY KEY, host TEXT NOT NULL, username TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1))
|
|
24
|
-
) STRICT;
|
|
25
|
-
CREATE TABLE IF NOT EXISTS channel_releases (
|
|
26
|
-
channel TEXT NOT NULL CHECK(channel IN ('stable','canary')), skill_slug TEXT NOT NULL REFERENCES skills(slug) ON DELETE CASCADE,
|
|
27
|
-
version TEXT NOT NULL, PRIMARY KEY(channel,skill_slug), FOREIGN KEY(skill_slug,version) REFERENCES releases(skill_slug,version)
|
|
28
|
-
) STRICT;
|
|
29
|
-
CREATE TABLE IF NOT EXISTS target_policies (
|
|
30
|
-
target TEXT PRIMARY KEY REFERENCES deployment_targets(name), channel TEXT NOT NULL CHECK(channel IN ('stable','canary')),
|
|
31
|
-
token_hash TEXT NOT NULL, skills_json TEXT, receipt_json TEXT, updated_at TEXT NOT NULL
|
|
32
|
-
) STRICT;
|
|
33
|
-
CREATE TABLE IF NOT EXISTS registry_migrations (id TEXT PRIMARY KEY) STRICT;
|
|
34
|
-
CREATE TABLE IF NOT EXISTS deployment_jobs (
|
|
35
|
-
id TEXT PRIMARY KEY, target TEXT NOT NULL, actor TEXT NOT NULL, status TEXT NOT NULL,
|
|
36
|
-
result TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
37
|
-
) STRICT;
|
|
38
|
-
CREATE TABLE IF NOT EXISTS audit_log (
|
|
39
|
-
id INTEGER PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, subject TEXT NOT NULL, details TEXT NOT NULL, created_at TEXT NOT NULL
|
|
40
|
-
) STRICT;
|
|
41
|
-
`);
|
|
42
|
-
const releaseColumns = new Set(db.prepare('PRAGMA table_info(releases)').all().map((column) => column.name));
|
|
43
|
-
if (!releaseColumns.has('min_hermes_version'))
|
|
44
|
-
db.exec('ALTER TABLE releases ADD COLUMN min_hermes_version TEXT');
|
|
45
|
-
if (!releaseColumns.has('max_hermes_version'))
|
|
46
|
-
db.exec('ALTER TABLE releases ADD COLUMN max_hermes_version TEXT');
|
|
47
|
-
// Preserve the currently published version during the first channel migration.
|
|
48
|
-
if (!db.prepare("SELECT id FROM registry_migrations WHERE id='release-channels'").get()) {
|
|
49
|
-
db.exec("BEGIN IMMEDIATE");
|
|
50
|
-
try {
|
|
51
|
-
db.exec("INSERT OR IGNORE INTO channel_releases SELECT 'stable', skill_slug, version FROM releases r WHERE created_at=(SELECT MAX(created_at) FROM releases WHERE skill_slug=r.skill_slug)");
|
|
52
|
-
db.exec("INSERT INTO registry_migrations VALUES ('release-channels'); COMMIT");
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
db.exec('ROLLBACK');
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
9
|
const tokenHash = (token) => createHash('sha256').update(token).digest('hex');
|
|
60
10
|
const now = () => new Date().toISOString();
|
|
61
11
|
const mapSkill = (row) => ({ slug: row.slug, name: row.name, description: row.description, homepage: row.homepage, enabled: row.enabled === 1, createdAt: row.created_at, updatedAt: row.updated_at });
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { config } from './config.js';
|
|
3
3
|
import { store } from './db.js';
|
|
4
|
+
import { maintenanceActive } from './release/runtime.js';
|
|
4
5
|
/** One local deployment at a time. Output may contain credentials: retain only exit codes. */
|
|
5
6
|
export function startDeploymentWorker() {
|
|
6
7
|
let busy = false;
|
|
8
|
+
let active = Promise.resolve();
|
|
7
9
|
store.recoverDeployments();
|
|
8
10
|
const timer = setInterval(() => {
|
|
9
|
-
if (busy)
|
|
11
|
+
if (busy || maintenanceActive('/worker'))
|
|
10
12
|
return;
|
|
11
13
|
const job = store.claimDeployment();
|
|
12
14
|
if (!job)
|
|
@@ -26,8 +28,10 @@ export function startDeploymentWorker() {
|
|
|
26
28
|
HIVE_SKILLS_TARGET_USER: target.username, HIVE_SKILLS_KEY_ONLY: 'true' },
|
|
27
29
|
});
|
|
28
30
|
let finished = false;
|
|
31
|
+
let complete = () => { };
|
|
32
|
+
active = new Promise((done) => { complete = done; });
|
|
29
33
|
const finish = (ok, code) => { if (finished)
|
|
30
|
-
return; finished = true; store.finishDeployment(job.id, ok, code); busy = false; };
|
|
34
|
+
return; finished = true; store.finishDeployment(job.id, ok, code); busy = false; complete(); };
|
|
31
35
|
child.once('error', () => finish(false, 'launch_failed'));
|
|
32
36
|
child.once('close', (code) => finish(code === 0, code === 0 ? 'timer_installed' : `exit_${code ?? 'signal'}`));
|
|
33
37
|
}
|
|
@@ -37,5 +41,5 @@ export function startDeploymentWorker() {
|
|
|
37
41
|
}
|
|
38
42
|
}, 1000);
|
|
39
43
|
timer.unref();
|
|
40
|
-
return () => clearInterval(timer);
|
|
44
|
+
return async () => { clearInterval(timer); await active; };
|
|
41
45
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { commonEnv } from './release/runtime.js';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { packageRoot } from './paths.js';
|
|
5
|
+
/** Safe to call before configuration is loaded; contains no runtime values. */
|
|
6
|
+
export function envSchemaDocument() {
|
|
7
|
+
const { version } = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
|
|
8
|
+
return {
|
|
9
|
+
service: 'skills', package: '@hivemagnus/hive-skills', version, defaultPort: 4030,
|
|
10
|
+
healthPath: '/healthz', consolePath: '/', dependencies: [],
|
|
11
|
+
env: [...commonEnv(),
|
|
12
|
+
{ name: 'NODE_ENV', default: 'production', description: 'Runtime environment.' },
|
|
13
|
+
{ name: 'HIVE_SKILLS_HOST', default: '127.0.0.1', description: 'Listener bind address.' },
|
|
14
|
+
{ name: 'HIVE_SKILLS_PORT', default: '4030', description: 'HTTPS listener port.' },
|
|
15
|
+
{ name: 'HIVE_SKILLS_PUBLIC_URL', default: 'https://localhost:4030', description: 'Public HTTPS registry URL used in archive links.' },
|
|
16
|
+
{ name: 'HIVE_SKILLS_DATA_DIR', default: './data', description: 'SQLite database and immutable skill archives.' },
|
|
17
|
+
{ name: 'HIVE_SKILLS_SESSION_SECRET', required: true, secret: true, generate: 'hex32', description: 'Session signing secret; at least 32 characters.' },
|
|
18
|
+
{ name: 'HIVE_SKILLS_ADMIN_USERS', description: 'Comma-separated OS accounts allowed to administer the registry; independent of IAM.' },
|
|
19
|
+
{ name: 'HIVE_SKILLS_PAM_SERVICE', default: 'hive-skills', description: 'Installed PAM policy for OS authentication; use login for a standard Linux policy.' },
|
|
20
|
+
{ name: 'HIVE_SKILLS_TLS_KEY_FILE', required: true, description: 'TLS private-key PEM file.' },
|
|
21
|
+
{ name: 'HIVE_SKILLS_TLS_CERT_FILE', required: true, description: 'TLS certificate PEM file.' },
|
|
22
|
+
{ name: 'HIVE_SKILLS_DEPLOY_SCRIPT', description: 'Optional override for the bundled agent deployment helper.' },
|
|
23
|
+
{ name: 'HIVE_SKILLS_TARGETS', description: 'Optional initial deployment targets: name=user@host, comma-separated.' },
|
|
24
|
+
].map((entry) => ({ required: false, secret: false, ...entry })),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function initializeSchema(db) {
|
|
2
|
+
db.exec(`
|
|
3
|
+
PRAGMA journal_mode = WAL;
|
|
4
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
5
|
+
slug TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, homepage TEXT,
|
|
6
|
+
enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
7
|
+
) STRICT;
|
|
8
|
+
CREATE TABLE IF NOT EXISTS releases (
|
|
9
|
+
skill_slug TEXT NOT NULL REFERENCES skills(slug) ON DELETE CASCADE, version TEXT NOT NULL,
|
|
10
|
+
sha256 TEXT NOT NULL, archive_path TEXT NOT NULL, notes TEXT, created_at TEXT NOT NULL,
|
|
11
|
+
min_hermes_version TEXT, max_hermes_version TEXT,
|
|
12
|
+
PRIMARY KEY(skill_slug, version)
|
|
13
|
+
) STRICT;
|
|
14
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
15
|
+
id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL
|
|
16
|
+
) STRICT;
|
|
17
|
+
CREATE TABLE IF NOT EXISTS deployment_targets (
|
|
18
|
+
name TEXT PRIMARY KEY, host TEXT NOT NULL, username TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1))
|
|
19
|
+
) STRICT;
|
|
20
|
+
CREATE TABLE IF NOT EXISTS channel_releases (
|
|
21
|
+
channel TEXT NOT NULL CHECK(channel IN ('stable','canary')), skill_slug TEXT NOT NULL REFERENCES skills(slug) ON DELETE CASCADE,
|
|
22
|
+
version TEXT NOT NULL, PRIMARY KEY(channel,skill_slug), FOREIGN KEY(skill_slug,version) REFERENCES releases(skill_slug,version)
|
|
23
|
+
) STRICT;
|
|
24
|
+
CREATE TABLE IF NOT EXISTS target_policies (
|
|
25
|
+
target TEXT PRIMARY KEY REFERENCES deployment_targets(name), channel TEXT NOT NULL CHECK(channel IN ('stable','canary')),
|
|
26
|
+
token_hash TEXT NOT NULL, skills_json TEXT, receipt_json TEXT, updated_at TEXT NOT NULL
|
|
27
|
+
) STRICT;
|
|
28
|
+
CREATE TABLE IF NOT EXISTS registry_migrations (id TEXT PRIMARY KEY) STRICT;
|
|
29
|
+
CREATE TABLE IF NOT EXISTS deployment_jobs (
|
|
30
|
+
id TEXT PRIMARY KEY, target TEXT NOT NULL, actor TEXT NOT NULL, status TEXT NOT NULL,
|
|
31
|
+
result TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
32
|
+
) STRICT;
|
|
33
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
34
|
+
id INTEGER PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, subject TEXT NOT NULL, details TEXT NOT NULL, created_at TEXT NOT NULL
|
|
35
|
+
) STRICT;
|
|
36
|
+
`);
|
|
37
|
+
const releaseColumns = new Set(db.prepare('PRAGMA table_info(releases)').all().map((column) => column.name));
|
|
38
|
+
if (!releaseColumns.has('min_hermes_version'))
|
|
39
|
+
db.exec('ALTER TABLE releases ADD COLUMN min_hermes_version TEXT');
|
|
40
|
+
if (!releaseColumns.has('max_hermes_version'))
|
|
41
|
+
db.exec('ALTER TABLE releases ADD COLUMN max_hermes_version TEXT');
|
|
42
|
+
// Preserve the currently published version during the first channel migration.
|
|
43
|
+
if (!db.prepare("SELECT id FROM registry_migrations WHERE id='release-channels'").get()) {
|
|
44
|
+
db.exec('BEGIN IMMEDIATE');
|
|
45
|
+
try {
|
|
46
|
+
db.exec("INSERT OR IGNORE INTO channel_releases SELECT 'stable', skill_slug, version FROM releases r WHERE created_at=(SELECT MAX(created_at) FROM releases WHERE skill_slug=r.skill_slug)");
|
|
47
|
+
db.exec("INSERT INTO registry_migrations VALUES ('release-channels'); COMMIT");
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
db.exec('ROLLBACK');
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Colin McDonnell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|