@hivemagnus/magnus-server 2.0.0 → 2.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.
Files changed (73) hide show
  1. package/.env.example +32 -0
  2. package/README.md +32 -0
  3. package/agent-version.json +1 -1
  4. package/dist/ai/TOOLS.md +1 -1
  5. package/dist/cli/args.js +3 -1
  6. package/dist/cli/env-catalog.js +13 -0
  7. package/dist/cli/migrations.js +22 -0
  8. package/dist/cli/verify-initialization.js +12 -0
  9. package/dist/cli.js +28 -0
  10. package/dist/config/console-path.js +17 -0
  11. package/dist/config/env.js +28 -0
  12. package/dist/config/features.js +3 -0
  13. package/dist/config/oathweave.js +42 -0
  14. package/dist/config/rehearsal.js +22 -0
  15. package/dist/db/generated/prisma/internal/class.js +4 -4
  16. package/dist/db/generated/prisma/internal/prismaNamespace.js +5 -0
  17. package/dist/db/generated/prisma/internal/prismaNamespaceBrowser.js +5 -0
  18. package/dist/db/generated/prisma/models/ProductInitialization.js +2 -0
  19. package/dist/db/read-retry.js +5 -1
  20. package/dist/db/seed.js +8 -0
  21. package/dist/index.js +27 -21
  22. package/dist/middleware/auth.js +21 -0
  23. package/dist/middleware/error-handler.js +10 -10
  24. package/dist/middleware/oathweave-verifier.js +23 -0
  25. package/dist/middleware/oathweave.js +115 -0
  26. package/dist/middleware/rate-limit.js +12 -3
  27. package/dist/openapi/spec.js +38 -2
  28. package/dist/queue/names.js +9 -0
  29. package/dist/queue/workers.js +9 -3
  30. package/dist/routes/assistant.js +30 -12
  31. package/dist/routes/console.js +33 -0
  32. package/dist/routes/cron.js +8 -5
  33. package/dist/routes/events-stream.js +10 -0
  34. package/dist/routes/files.js +2 -1
  35. package/dist/routes/fleet-gateway.js +69 -0
  36. package/dist/routes/hermes.js +26 -6
  37. package/dist/routes/hiveskills.js +116 -0
  38. package/dist/routes/library.js +3 -2
  39. package/dist/routes/logs.js +5 -0
  40. package/dist/routes/metrics.js +4 -3
  41. package/dist/routes/nodes.js +45 -8
  42. package/dist/routes/ops.js +24 -6
  43. package/dist/routes/profiling.js +2 -1
  44. package/dist/routes/roles.js +17 -9
  45. package/dist/routes/stats.js +8 -5
  46. package/dist/routes/terminal-ticket.js +3 -3
  47. package/dist/routes/terminal.js +2 -2
  48. package/dist/routes/webhooks.js +15 -15
  49. package/dist/sdk/node-api-client.js +11 -1
  50. package/dist/security/native-completion-registry.js +3 -1
  51. package/dist/server.js +8 -0
  52. package/dist/services/ai-assistant-service.js +32 -6
  53. package/dist/services/ai-tools.js +15 -8
  54. package/dist/services/ardent-scope-enrollment.js +4 -2
  55. package/dist/services/forecast-service.js +23 -8
  56. package/dist/services/ops-service.js +20 -25
  57. package/dist/services/prediction-service.js +2 -1
  58. package/dist/services/resource-enrollment.js +57 -4
  59. package/dist/services/secret-views.js +72 -0
  60. package/dist/services/topology-service.js +2 -1
  61. package/dist/services/webhook-security.js +10 -1
  62. package/dist/services/webhook-service.js +14 -6
  63. package/dist/utils/node-url.js +2 -1
  64. package/dist/utils/safe-errors.js +130 -0
  65. package/node_modules/@hivemagnus/magnus-oathweave/LICENSE +202 -0
  66. package/node_modules/@hivemagnus/magnus-oathweave/README.md +20 -0
  67. package/node_modules/@hivemagnus/magnus-oathweave/dist/index.d.ts +61 -0
  68. package/node_modules/@hivemagnus/magnus-oathweave/dist/index.js +148 -0
  69. package/node_modules/@hivemagnus/magnus-oathweave/package.json +54 -0
  70. package/package.json +7 -3
  71. package/prisma/migrations/20261105010000_base_initialization/magnus-impact.json +1 -0
  72. package/prisma/migrations/20261105010000_base_initialization/migration.sql +5 -0
  73. package/prisma/schema.prisma +9 -0
package/.env.example CHANGED
@@ -8,6 +8,7 @@
8
8
  #
9
9
  # Generate each secret with: openssl rand -hex 32
10
10
  # Every secret must be distinct; there are no fallback values.
11
+ HIVE_REHEARSAL=false
11
12
 
12
13
  # ═══════════════════════════════════════════════════════════════════════════
13
14
  # Required
@@ -35,6 +36,8 @@ LOG_INGEST_SIGNING_SECRET=
35
36
  CREDENTIAL_ROTATION_KEY=
36
37
  METRICS_TOKEN=
37
38
  WEBHOOK_ENCRYPTION_KEY=
39
+ # Comma-separated exact HTTPS origins webhooks may target. Empty refuses every target.
40
+ # WEBHOOK_ALLOWED_ORIGINS=https://hooks.example.com
38
41
 
39
42
  # HTTPS listener. TLS is required in production. Relative paths resolve
40
43
  # against the working directory; absolute paths are recommended.
@@ -99,6 +102,32 @@ CORS_ORIGIN=https://magnus.example.com:3000
99
102
  # IAM_TOKEN_ISSUER=https://iam.example.com
100
103
  # IAM_TOKEN_AUDIENCE=magnus
101
104
 
105
+ # ── Oathweave: verify browser console resource tokens (audience "magnus") ──
106
+ # Cannot be combined with IAM_CORE_URL. Local HS256 and service credentials keep working.
107
+ # All four settings are required together. Endpoints must be HTTPS. A person needs an
108
+ # exact issuer + subject link, never inferred by email; `role` is a ceiling on the
109
+ # token's magnus:admin, magnus:operate and magnus:view permissions.
110
+ # OATHWEAVE_ISSUER=https://identity.example.com
111
+ # OATHWEAVE_JWKS_URL=https://identity.example.com/api/v1/oauth/jwks
112
+ # OATHWEAVE_USERINFO_URL=https://identity.example.com/api/v1/oauth/userinfo
113
+ # MAGNUS_OATHWEAVE_IDENTITIES_JSON=[{"issuer":"https://identity.example.com","subject":"example-subject","localUserId":"alice","role":"OPERATOR"}]
114
+ # Reported to the browser console by GET /api/v1/console/session.
115
+ # MAGNUS_HERMES_ENABLED=false
116
+
117
+ # ── Fleet gateway: the separate service that carries the web terminal ──
118
+ # This server never holds the SSH key. Both settings together offer the terminal to people with an
119
+ # ADMIN link and the magnus:terminal permission. The digest is of the gateway's
120
+ # FLEET_GATEWAY_API_TOKEN. The server must trust the gateway's certificate (NODE_EXTRA_CA_CERTS).
121
+ # FLEET_GATEWAY_URL=https://fleet-gateway.example.com:4310
122
+ # FLEET_GATEWAY_TOKEN_SHA256=
123
+ # Who answers console drain challenges during resource enrollment. gateway (the default) needs
124
+ # FLEET_GATEWAY_URL and RESOURCE_ENROLLMENT_DASHBOARD_SECRET. absent is for an installation with
125
+ # neither a gateway nor a console that ever did SSH work: it succeeds only while the node has no
126
+ # console work rows in any state, and otherwise fails closed.
127
+ # RESOURCE_ENROLLMENT_DASHBOARD_PARTICIPANT=gateway
128
+ # Path the console is served under, prefixed to links in notifications.
129
+ # MAGNUS_CONSOLE_BASE_PATH=/fleet
130
+
102
131
  # ── Hive Router (or any OpenAI-compatible router) for managed Hermes Agents ──
103
132
  # HIVE_ROUTER_URL / HIVE_ROUTER_MODEL / HIVE_ROUTER_API_KEY are accepted as aliases.
104
133
  # MAGNUS_ROUTER_URL=https://router.example.com/v1
@@ -205,3 +234,6 @@ CORS_ORIGIN=https://magnus.example.com:3000
205
234
  # CINDERTRACE_RESPONSE_SECRET=
206
235
  # CINDERTRACE_RESTART_ALLOWLIST=
207
236
  # CINDERTRACE_PROTECTED_TARGET_IDS=
237
+
238
+ # Optional fixed HiveSkills reconciliation; protected target credentials stay on hosts.
239
+ # MAGNUS_HIVESKILLS_DEPLOYMENTS_JSON={"fixture-registry":{"tokenSha256":"<sha256-of-dedicated-executor-token>","targets":{"fixture-target":{"nodeId":"<enrolled-node-id>","agentTarget":"fixture-native"}}}}
package/README.md CHANGED
@@ -51,6 +51,9 @@ chmod 600 ~/.magnus/server.env
51
51
  production `TLS_CERT_PATH` / `TLS_KEY_PATH`. The server refuses to start while
52
52
  any is missing; there are no fallback secret values.
53
53
 
54
+ Webhooks are refused until `WEBHOOK_ALLOWED_ORIGINS` lists the exact HTTPS origins
55
+ they may target, as a comma-separated list.
56
+
54
57
  Local data (audit logs, credential rotation state, uploaded backups) lives
55
58
  under `MAGNUS_HOME`, default `~/.magnus`.
56
59
 
@@ -110,6 +113,7 @@ without any of them. `magnus-server check` prints which are on.
110
113
  | Integration | What it adds | Environment |
111
114
  | --- | --- | --- |
112
115
  | Hive IAM | RS256 access tokens issued by Hive IAM replace locally issued JWTs | `IAM_CORE_URL`, `IAM_TOKEN_ISSUER`, `IAM_TOKEN_AUDIENCE` |
116
+ | Fleet gateway | The browser web terminal, through a [separate service](../fleet-gateway/README.md) that holds the SSH key; also answers console drain challenges | `FLEET_GATEWAY_URL`, `FLEET_GATEWAY_TOKEN_SHA256`, `RESOURCE_ENROLLMENT_DASHBOARD_PARTICIPANT` |
113
117
  | Hive Router | OpenAI-compatible model routing for managed Hermes Agents | `MAGNUS_ROUTER_URL` (alias `HIVE_ROUTER_URL`), `MAGNUS_ROUTER_MODEL`, `MAGNUS_ROUTER_API_KEY`, `HIVE_ROUTER_HEALTH_URL` |
114
118
  | HiveMem | Shared agent memory for Hermes Agents and operational lessons | `HIVEMEM_URL`, `HIVEMEM_TOKEN`, `MAGNUS_LESSONS_HIVEMEM_URL`, `MAGNUS_LESSONS_HIVEMEM_TOKEN` |
115
119
  | Security Fusion (local) | Security cases, evidence and signed receipts in this server | `SECURITY_FUSION_MODE=local`, `SECURITY_FUSION_RECEIPT_SIGNING_KEY`, optional `SECURITY_FUSION_EVIDENCE_DIR`, `SECURITY_FUSION_CONTAINMENT_COMMAND`, `SECURITY_FUSION_MATTERMOST_*` |
@@ -121,6 +125,15 @@ without any of them. `magnus-server check` prints which are on.
121
125
  | Agent discovery | Agents self-register | `ENABLE_AGENT_DISCOVERY=true`, `AGENT_DISCOVERY_SECRET` |
122
126
  | Resource leases | Durable node and shared-resource lease coordination | `MAGNUS_RESOURCE_LEASES_ENABLED=true` |
123
127
 
128
+ For an isolated restore, set `HIVE_REHEARSAL=true` and
129
+ `MAGNUS_AUTO_MIGRATE=false`. The API keeps read access but holds mutations,
130
+ terminal WebSockets, queue activation, host contact, collectors, retention,
131
+ reconciliation, updates and catalogue imports. Existing queued work remains
132
+ dormant. Run explicitly reviewed migrations separately before starting the
133
+ candidate. `magnus-server verify-migrations` verifies the exact packaged,
134
+ completed migration checksums using read-only queries; pending, failed, missing,
135
+ unknown or changed migrations fail. It does not initialize or repair the database.
136
+
124
137
  Credentials for services that call Magnus are stored only as SHA-256 digests.
125
138
 
126
139
  SSH helpers require `SSH_HOST_KEY_PINS_FILE` before they open a connection.
@@ -191,3 +204,22 @@ Apache-2.0. See [LICENSE](LICENSE).
191
204
  ## Cindertrace
192
205
 
193
206
  Embedded Security Fusion is retired. See [migration and response configuration](../../docs/CINDERTRACE_MIGRATION.md). CINDERTRACE_RESPONSE_SECRET, CINDERTRACE_RESTART_ALLOWLIST, and CINDERTRACE_PROTECTED_TARGET_IDS configure the optional managed response adapter.
207
+
208
+ ### HiveSkills execution bridge
209
+
210
+ `MAGNUS_HIVESKILLS_DEPLOYMENTS_JSON` maps each registry to a dedicated token SHA-256
211
+ and exact logical-target bindings (`nodeId`, `agentTarget`). This optional integration
212
+ accepts only POST/GET `/api/v1/hiveskills/jobs/:id`, with exact publication digests
213
+ and a durable registry-owned UUID. It cannot submit general commands. Nodes must
214
+ already have completed managed resource enrollment. Repeated requests inspect the
215
+ original signed receipt; they never dispatch an admitted job again. Unknown outcomes
216
+ retain their node ownership for explicit reconciliation.
217
+
218
+ Each node explicitly configures `MAGNUS_AGENT_HIVESKILLS_TARGETS_JSON`: registry ID,
219
+ registry target, owner, home, Hermes profile directory, protected environment-file
220
+ path, root-owned pinned sync script, Node executable and optional `caFile` trust bundle. The trust bundle is passed at Node startup; target bearer credentials remain in the protected environment file. The owner-scoped native
221
+ handler runs that fixed product client as the configured owner, with sanitized
222
+ environment and bounded output. No infrastructure or native target credential is
223
+ sent by the browser or included in a job body. The client receives the frozen
224
+ approved publication and independently checks archive origin, digest and local
225
+ Hermes compatibility. A non-root agent may synchronize its own explicit profile. Changing owner or running as root requires privileged mode. No SSH or Docker socket is used by this path.
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "2.0.0"
2
+ "version": "2.1.0"
3
3
  }
package/dist/ai/TOOLS.md CHANGED
@@ -65,7 +65,7 @@ Includes:
65
65
  #### `getFleetReadiness()`
66
66
 
67
67
  Returns stale-node, deploy-eligibility, destructive-action, agent-version, and
68
- credential-fingerprint posture from `/api/v1/ops/readiness`.
68
+ credential state from `/api/v1/ops/readiness`, without secret fingerprints.
69
69
 
70
70
  #### `getCodexFleetStatus()`
71
71
 
package/dist/cli/args.js CHANGED
@@ -6,6 +6,8 @@ Commands:
6
6
  start Start the API server (default)
7
7
  check Validate configuration and reach PostgreSQL and Redis; list enabled integrations
8
8
  migrate Apply pending migrations (prisma migrate deploy) and base data
9
+ verify-migrations Verify exact completed migration checksums without applying changes
10
+ verify-initialization Verify migrations and committed base initialization without changes
9
11
  migrations Show applied and pending migrations with their impact notes
10
12
  backup Dump PostgreSQL (pg_dump -Fc) into MAGNUS_BACKUP_DIR and print the restore command
11
13
  restore FILE Replace the database with a dump (stop the server first; asks unless --yes)
@@ -25,7 +27,7 @@ Options:
25
27
  -h, --help Show this help
26
28
  -v, --version Print the version
27
29
  `;
28
- const COMMANDS = new Set(['start', 'check', 'migrate', 'migrations', 'backup', 'restore', 'upgrade', 'rollback', 'agents', 'env-schema', 'help', 'version']);
30
+ const COMMANDS = new Set(['start', 'check', 'migrate', 'migrations', 'verify-migrations', 'verify-initialization', 'backup', 'restore', 'upgrade', 'rollback', 'agents', 'env-schema', 'help', 'version']);
29
31
  /** Commands whose remaining arguments belong to another tool. */
30
32
  const PASSTHROUGH = new Set(['upgrade', 'rollback', 'agents']);
31
33
  export function parseArgs(argv) {
@@ -1,10 +1,12 @@
1
1
  const secret32 = (name, description) => ({ name, group: 'secrets', required: true, secret: true, description: `${description} At least 32 characters; must differ from every other secret.` });
2
2
  export const ENV_CATALOG = [
3
3
  // ── Core ──
4
+ { name: 'HIVE_REHEARSAL', group: 'core', default: 'false', description: 'Isolated restore mode: no workers, timers, queue activation, mutations, host execution or terminal WebSockets. Explicit true/false only.' },
4
5
  { name: 'NODE_ENV', group: 'core', default: 'development', description: 'development, production or test. Production enforces TLS, DATABASE_URL and strict CORS.' },
5
6
  { name: 'LOG_LEVEL', group: 'core', default: 'info', description: 'debug, info, warn, error or fatal.' },
6
7
  { name: 'MAGNUS_ROUTE_ALIASES_JSON', group: 'core', description: 'Optional JSON object mapping legacy /api/v1 path prefixes to current ones, for clients built against older route names. Requests are rewritten before routing, so auth and audit see the current path. Unset means no aliases.' },
7
8
  { name: 'MAGNUS_WEBHOOK_TYPE_ALIASES', group: 'core', description: 'Optional comma-separated legacy=format pairs (format: generic, slack, discord or mattermost) so webhook endpoints stored or submitted under an older type name keep their delivery format.' },
9
+ { name: 'WEBHOOK_ALLOWED_ORIGINS', group: 'core', description: 'Comma-separated exact HTTPS origins that webhooks may target. Empty refuses every webhook target.' },
8
10
  { name: 'MAGNUS_HOME', group: 'paths', default: '~/.magnus', description: 'Base directory for server-owned data (audit logs, credential rotation state, backups).' },
9
11
  { name: 'MAGNUS_ENV_FILE', group: 'paths', description: 'Env file loaded at startup (set by `--env-file`). Variables already in the environment win.' },
10
12
  { name: 'AUDIT_LOG_DIR', group: 'paths', default: '$MAGNUS_HOME/audit-logs', description: 'Directory for the append-only audit log files.' },
@@ -24,6 +26,7 @@ export const ENV_CATALOG = [
24
26
  { name: 'ALLOW_INSECURE_HTTP', group: 'listener', default: 'false', description: 'Development only: serve cleartext HTTP on loopback when no certificate is configured.' },
25
27
  { name: 'CORS_ORIGIN', group: 'listener', default: 'https://localhost:3000', description: 'Comma-separated browser origins allowed to call the API (the dashboard).' },
26
28
  { name: 'DASHBOARD_PUBLIC_ORIGIN', group: 'listener', description: 'Public HTTPS origin of the dashboard, used for links in notifications.' },
29
+ { name: 'MAGNUS_CONSOLE_BASE_PATH', group: 'listener', description: 'Path the console is served under on DASHBOARD_PUBLIC_ORIGIN (for example /fleet), prefixed to links in notifications. Unset means the origin root.' },
27
30
  // ── Required secrets ──
28
31
  secret32('API_SECRET', 'Dashboard-to-server shared secret (x-api-secret).'),
29
32
  { ...secret32('JWT_SECRET', 'HS256 key for locally issued user tokens.'), required: false, requiredWhen: 'IAM_CORE_URL unset' },
@@ -49,6 +52,15 @@ export const ENV_CATALOG = [
49
52
  { name: 'IAM_CORE_URL', group: 'iam', description: 'Hive IAM base URL. When set, RS256 tokens from Hive IAM replace local JWTs.' },
50
53
  { name: 'IAM_TOKEN_ISSUER', group: 'iam', requiredWhen: 'IAM_CORE_URL set', description: 'Exact `iss` claim required on IAM access tokens.' },
51
54
  { name: 'IAM_TOKEN_AUDIENCE', group: 'iam', requiredWhen: 'IAM_CORE_URL set', description: 'Exact `aud` claim required on IAM access tokens.' },
55
+ // ── Oathweave browser console tokens ──
56
+ { name: 'OATHWEAVE_ISSUER', group: 'iam', description: 'Exact `iss` claim of Oathweave resource tokens (audience `magnus`). Cannot be combined with IAM_CORE_URL.' },
57
+ { name: 'OATHWEAVE_JWKS_URL', group: 'iam', requiredWhen: 'OATHWEAVE_ISSUER set', description: 'HTTPS JWKS URL that verifies Oathweave token signatures.' },
58
+ { name: 'OATHWEAVE_USERINFO_URL', group: 'iam', requiredWhen: 'OATHWEAVE_ISSUER set', description: 'HTTPS userinfo URL used for online session revocation (logout takes effect within 15 seconds, including on open streams).' },
59
+ { name: 'MAGNUS_OATHWEAVE_IDENTITIES_JSON', group: 'iam', requiredWhen: 'OATHWEAVE_ISSUER set', description: 'JSON array of {issuer,subject,localUserId,role}. Exact issuer and subject match, never by email; a missing or duplicate link is refused. `role` caps what the token permissions magnus:admin, magnus:operate and magnus:view can grant.' },
60
+ // ── Fleet gateway (web terminal) ──
61
+ { name: 'FLEET_GATEWAY_URL', group: 'iam', description: 'HTTPS origin of the fleet gateway, the separate service holding the restricted SSH key. Enables the console terminal capability with FLEET_GATEWAY_TOKEN_SHA256, and receives console drain challenges.' },
62
+ { name: 'FLEET_GATEWAY_TOKEN_SHA256', group: 'iam', requiredWhen: 'FLEET_GATEWAY_URL set', description: 'SHA-256 digest of the credential the fleet gateway presents to resolve nodes and record terminal audit events.' },
63
+ { name: 'RESOURCE_ENROLLMENT_DASHBOARD_PARTICIPANT', group: 'iam', description: '`gateway` (default) sends console drain challenges to FLEET_GATEWAY_URL. `absent` declares no console participant exists: drain evidence is accepted only while the node has no console work rows, in any state.' },
52
64
  // ── Hive Router ──
53
65
  { name: 'MAGNUS_ROUTER_URL', group: 'router', description: 'OpenAI-compatible model router (for example Hive Router) for managed Hermes Agents. HIVE_ROUTER_URL is accepted as an alias.' },
54
66
  { name: 'MAGNUS_ROUTER_MODEL', group: 'router', description: 'Default model served by the router. HIVE_ROUTER_MODEL is accepted as an alias.' },
@@ -80,6 +92,7 @@ export const ENV_CATALOG = [
80
92
  { name: 'SECURITY_FUSION_MATTERMOST_CHANNEL_ID', group: 'security', description: 'Mattermost channel ID.' },
81
93
  // ── Hermes Agent plugin API ──
82
94
  { name: 'MAGNUS_PLUGIN_API_ENABLED', group: 'plugins', default: 'false', description: 'Accept Hermes Agent plugin credentials on the plugin-scoped routes.' },
95
+ { name: 'MAGNUS_HIVESKILLS_DEPLOYMENTS_JSON', group: 'plugins', description: 'Optional endpoint-scoped HiveSkills executor grants: registry ID -> tokenSha256 and exact target -> nodeId/agentTarget mapping; no administrator credentials.' },
83
96
  { name: 'MAGNUS_PLUGIN_READ_TOKEN_DIGESTS_JSON', group: 'plugins', requiredWhen: 'MAGNUS_PLUGIN_API_ENABLED=true', description: 'JSON object {consumerId: sha256(token)} of 1-16 read-only plugin credentials.' },
84
97
  { name: 'MAGNUS_PLUGIN_OPERATOR_TOKEN_SHA256', group: 'plugins', requiredWhen: 'MAGNUS_PLUGIN_API_ENABLED=true', description: 'SHA-256 digest of the server-local plugin operator credential.' },
85
98
  { name: 'MAGNUS_APPROVAL_BRIDGE_CONSUMER', group: 'plugins', description: 'Plugin read consumer allowed to list approval plans and relay signed decisions.' },
@@ -3,6 +3,7 @@
3
3
  * `magnus-server migrate`, `check` and startup auto-migrate.
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
+ import { createHash } from 'node:crypto';
6
7
  import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
7
8
  import { createRequire } from 'node:module';
8
9
  import { basename, dirname, join } from 'node:path';
@@ -16,6 +17,27 @@ export function packagedMigrations(migrationsDir) {
16
17
  .map(entry => entry.name)
17
18
  .sort();
18
19
  }
20
+ /** Strict initialization receipt verification. SELECT only; never repairs history. */
21
+ export async function verifyCurrentMigrations(client, migrationsDir) {
22
+ const packaged = packagedMigrations(migrationsDir);
23
+ if (!packaged.length)
24
+ throw new Error('Packaged migrations are unavailable; select the owned migration artifact');
25
+ const table = await client.query(`SELECT to_regclass('public._prisma_migrations') IS NOT NULL AS exists`);
26
+ if (!table.rows[0]?.exists)
27
+ throw new Error('Migration history is absent; initialization has not completed');
28
+ const rows = (await client.query('SELECT migration_name, checksum, finished_at, rolled_back_at FROM _prisma_migrations ORDER BY migration_name')).rows;
29
+ const active = rows.filter(row => !row.rolled_back_at);
30
+ if (active.some(row => !row.finished_at))
31
+ throw new Error('An unfinished migration requires explicit recovery');
32
+ if (active.length !== packaged.length || active.some(row => !packaged.includes(row.migration_name)))
33
+ throw new Error('Database migration history differs from this exact release');
34
+ for (const name of packaged) {
35
+ const matching = active.filter(row => row.migration_name === name);
36
+ const expected = createHash('sha256').update(readFileSync(join(migrationsDir, name, 'migration.sql'))).digest('hex');
37
+ if (matching.length !== 1 || matching[0].checksum !== expected)
38
+ throw new Error('A completed migration checksum differs from its immutable packaged source');
39
+ }
40
+ }
19
41
  /**
20
42
  * Each migration may carry `magnus-impact.json` (`{"impact":"slow","note":"…"}`).
21
43
  * A migration without one is treated as fast and reversible by rollback.
@@ -0,0 +1,12 @@
1
+ /** No DDL, seeding or repairs: reconcile only an already committed owner receipt. */
2
+ export async function verifyInitialization(client) {
3
+ const result = await client.query(`SELECT
4
+ EXISTS (SELECT 1 FROM product_initializations WHERE id = 'base-data-v1') AS completed,
5
+ EXISTS (SELECT 1 FROM users WHERE (id = 'system' OR email = 'system@magnus.local') AND active = true AND role = 'ADMIN') AS actor,
6
+ (SELECT count(*) = 2 FROM nodes WHERE id IN ('__control-api__', '__dashboard__') AND os = 'internal' AND arch = 'internal') AS sentinels`);
7
+ const row = result.rows[0];
8
+ if (!row?.completed || !row.actor || !row.sentinels) {
9
+ throw Error('Base initialization is incomplete or its required system records changed; activation remains held. Use the product recovery procedure; no seeding or migration was replayed.');
10
+ }
11
+ }
12
+ //# sourceMappingURL=verify-initialization.js.map
package/dist/cli.js CHANGED
@@ -134,6 +134,25 @@ async function commandCheck(format) {
134
134
  return failures === 0 ? 0 : 1;
135
135
  }
136
136
  /** Applied and pending migrations, with impact notes (`--format json` for tooling). */
137
+ async function commandVerifyMigrations(initialization = false) {
138
+ const { resolveDatabaseUrl } = await import('./db/client.js');
139
+ const { verifyCurrentMigrations } = await import('./cli/migrations.js');
140
+ const pg = (await import('pg')).default;
141
+ const client = new pg.Client({ connectionString: process.env['MAGNUS_MIGRATE_DATABASE_URL']?.trim() || resolveDatabaseUrl(), connectionTimeoutMillis: 10_000 });
142
+ await client.connect();
143
+ try {
144
+ await verifyCurrentMigrations(client, MIGRATIONS_DIR);
145
+ if (initialization) {
146
+ const { verifyInitialization } = await import('./cli/verify-initialization.js');
147
+ await verifyInitialization(client);
148
+ }
149
+ console.log(JSON.stringify({ ok: true, migrations: 'current', ...(initialization ? { initialization: 'complete' } : {}) }));
150
+ return 0;
151
+ }
152
+ finally {
153
+ await client.end().catch(() => undefined);
154
+ }
155
+ }
137
156
  async function commandMigrations(format) {
138
157
  const { resolveDatabaseUrl } = await import('./db/client.js');
139
158
  const { readMigrationState } = await import('./cli/migrations.js');
@@ -284,6 +303,9 @@ async function commandStart(options) {
284
303
  process.loadEnvFile(packageEnv);
285
304
  dropEmptyValues();
286
305
  }
306
+ const { rehearsalEnabled } = await import('./config/rehearsal.js');
307
+ if (rehearsalEnabled() && (options.migrate || process.env['MAGNUS_AUTO_MIGRATE'] === 'true'))
308
+ fatal('Isolated rehearsal requires explicit migrations before startup; automatic migration must be disabled.');
287
309
  if (options.migrate)
288
310
  await migrate();
289
311
  else
@@ -309,6 +331,12 @@ async function run(argv) {
309
331
  case 'check':
310
332
  loadEnvFiles(options.envFiles);
311
333
  return commandCheck(options.formatGiven ? options.format : 'table');
334
+ case 'verify-migrations':
335
+ loadEnvFiles(options.envFiles);
336
+ return commandVerifyMigrations();
337
+ case 'verify-initialization':
338
+ loadEnvFiles(options.envFiles);
339
+ return commandVerifyMigrations(true);
312
340
  case 'migrations':
313
341
  loadEnvFiles(options.envFiles);
314
342
  return commandMigrations(options.formatGiven ? options.format : 'table');
@@ -0,0 +1,17 @@
1
+ /** One or more plain path segments with a leading slash and no trailing slash. */
2
+ const CONSOLE_BASE_PATH = /^(?:\/(?!\.{1,2}(?:\/|$))[A-Za-z0-9._~-]+)+$/;
3
+ export function isConsoleBasePath(value) {
4
+ return CONSOLE_BASE_PATH.test(value);
5
+ }
6
+ /**
7
+ * Absolute path of a console page, honouring MAGNUS_CONSOLE_BASE_PATH.
8
+ * Null when the configured base is invalid, so callers omit the link rather
9
+ * than emit one they cannot vouch for. Startup validation rejects that value.
10
+ */
11
+ export function consolePagePath(page, env = process.env) {
12
+ const base = env['MAGNUS_CONSOLE_BASE_PATH'];
13
+ if (!base)
14
+ return page;
15
+ return isConsoleBasePath(base) ? `${base}${page}` : null;
16
+ }
17
+ //# sourceMappingURL=console-path.js.map
@@ -4,6 +4,8 @@ import { parseRouteAliases } from './route-aliases.js';
4
4
  import { webhookTypeAliases } from '../services/webhook-types.js';
5
5
  // ─── Shared environment schema — validated with Zod ───
6
6
  const portSchema = z.coerce.number().int().min(1).max(65535);
7
+ import { isConsoleBasePath } from './console-path.js';
8
+ import { oathweaveConfigurationIssues } from './oathweave.js';
7
9
  const booleanEnvSchema = z.enum(['true', 'false']).default('false').transform((value) => value === 'true');
8
10
  function configuredDigestValues(raw) {
9
11
  if (!raw)
@@ -21,6 +23,7 @@ function configuredDigestValues(raw) {
21
23
  /** Default skills directory — `<package root>/skills`. */
22
24
  const DEFAULT_SKILLS_DIR = path.resolve(import.meta.dirname, '../../skills');
23
25
  export const envSchema = z.object({
26
+ HIVE_REHEARSAL: booleanEnvSchema,
24
27
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
25
28
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error', 'fatal']).default('info'),
26
29
  // PostgreSQL
@@ -43,6 +46,9 @@ export const envSchema = z.object({
43
46
  MAGNUS_COLLECTOR_TOKEN_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
44
47
  MAGNUS_RESOURCE_LEASES_ENABLED: booleanEnvSchema,
45
48
  RESOURCE_ENROLLMENT_DASHBOARD_SECRET: z.string().min(32).optional(),
49
+ RESOURCE_ENROLLMENT_DASHBOARD_PARTICIPANT: z.enum(['gateway', 'absent']).optional(),
50
+ FLEET_GATEWAY_URL: z.string().url().optional(),
51
+ FLEET_GATEWAY_TOKEN_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
46
52
  // Optional signed team-approval relay (Ed25519 public key + actor bindings).
47
53
  APPROVAL_SIGNER_PUBLIC_KEY_PEM: z.string().optional(),
48
54
  APPROVAL_SIGNER_PUBLIC_KEY_B64: z.string().min(32).optional(),
@@ -68,6 +74,10 @@ export const envSchema = z.object({
68
74
  SHARED_RESOURCE_MISSION_TRUST_JSON: z.string().optional(),
69
75
  SHARED_RESOURCE_RESULT_TRUST_JSON: z.string().optional(),
70
76
  DASHBOARD_PUBLIC_ORIGIN: z.string().url().optional(),
77
+ MAGNUS_CONSOLE_BASE_PATH: z.string()
78
+ .refine(isConsoleBasePath, 'must be an absolute path such as /fleet, without a trailing slash or dot segments')
79
+ .optional(),
80
+ MAGNUS_HERMES_ENABLED: booleanEnvSchema,
71
81
  MAGNUS_APPLICATION_PROBES_FILE: z.string().optional(),
72
82
  MAGNUS_OPERATIONS_CATALOG_FILE: z.string().optional(),
73
83
  MAGNUS_ENGINEERING_EVIDENCE_FILE: z.string().optional(),
@@ -104,6 +114,7 @@ export const envSchema = z.object({
104
114
  MAGNUS_PLUGIN_READ_TOKEN_DIGESTS_JSON: z.string().optional(),
105
115
  MAGNUS_PLUGIN_OPERATOR_TOKEN_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
106
116
  MAGNUS_SHARED_RESOURCE_TOKEN_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
117
+ MAGNUS_HIVESKILLS_DEPLOYMENTS_JSON: z.string().optional(),
107
118
  MAGNUS_SHARED_RESOURCE_TOKEN_DIGESTS_JSON: z.string().optional(),
108
119
  MAGNUS_FLEET_CHECK_TOKEN_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
109
120
  SECURITY_FUSION_RECEIPT_SIGNING_KEY: z.string().min(32).optional(),
@@ -114,6 +125,10 @@ export const envSchema = z.object({
114
125
  IAM_CORE_URL: z.string().url().optional(),
115
126
  IAM_TOKEN_ISSUER: z.string().min(1).optional(),
116
127
  IAM_TOKEN_AUDIENCE: z.string().min(1).optional(),
128
+ OATHWEAVE_ISSUER: z.string().url().optional(),
129
+ OATHWEAVE_JWKS_URL: z.string().url().optional(),
130
+ OATHWEAVE_USERINFO_URL: z.string().url().optional(),
131
+ MAGNUS_OATHWEAVE_IDENTITIES_JSON: z.string().optional(),
117
132
  // Skills
118
133
  SKILLS_DIR: z.string().default(DEFAULT_SKILLS_DIR),
119
134
  // Log retention
@@ -167,6 +182,15 @@ export const envSchema = z.object({
167
182
  .regex(/^\s*(?:[a-z][a-z0-9_-]{0,31}\s*=\s*(?:generic|slack|discord|mattermost)\s*(?:,\s*|$))*$/, 'must list legacy=format pairs, where format is generic, slack, discord or mattermost')
168
183
  .optional(),
169
184
  }).superRefine((env, ctx) => {
185
+ if (env.FLEET_GATEWAY_URL) {
186
+ const url = new URL(env.FLEET_GATEWAY_URL);
187
+ if (url.protocol !== 'https:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash)
188
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['FLEET_GATEWAY_URL'], message: 'FLEET_GATEWAY_URL must be a bare HTTPS origin' });
189
+ if (env.RESOURCE_ENROLLMENT_DASHBOARD_PARTICIPANT === 'absent')
190
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['RESOURCE_ENROLLMENT_DASHBOARD_PARTICIPANT'], message: 'A deployed fleet gateway cannot be declared absent; unset FLEET_GATEWAY_URL or RESOURCE_ENROLLMENT_DASHBOARD_PARTICIPANT' });
191
+ }
192
+ if (Boolean(env.FLEET_GATEWAY_URL) !== Boolean(env.FLEET_GATEWAY_TOKEN_SHA256))
193
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['FLEET_GATEWAY_TOKEN_SHA256'], message: 'FLEET_GATEWAY_URL and FLEET_GATEWAY_TOKEN_SHA256 must be configured together' });
170
194
  try {
171
195
  webhookTypeAliases(env.MAGNUS_WEBHOOK_TYPE_ALIASES);
172
196
  }
@@ -195,6 +219,9 @@ export const envSchema = z.object({
195
219
  message: 'NOTIFICATION_READ_RETENTION_DAYS cannot exceed NOTIFICATION_MAX_RETENTION_DAYS',
196
220
  });
197
221
  }
222
+ for (const issue of oathweaveConfigurationIssues(env)) {
223
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [issue.path], message: issue.message });
224
+ }
198
225
  if (env.IAM_CORE_URL) {
199
226
  for (const field of ['IAM_TOKEN_ISSUER', 'IAM_TOKEN_AUDIENCE']) {
200
227
  if (env[field])
@@ -335,6 +362,7 @@ export const envSchema = z.object({
335
362
  env.MAGNUS_PLUGIN_OPERATOR_TOKEN_SHA256,
336
363
  env.MAGNUS_FLEET_CHECK_TOKEN_SHA256,
337
364
  env.MAGNUS_COLLECTOR_TOKEN_SHA256,
365
+ env.FLEET_GATEWAY_TOKEN_SHA256,
338
366
  ...configuredDigestValues(env.MAGNUS_PLUGIN_READ_TOKEN_DIGESTS_JSON),
339
367
  ].filter((value) => Boolean(value)));
340
368
  if (env.MAGNUS_SHARED_RESOURCE_TOKEN_SHA256 && otherScopedDigests.has(env.MAGNUS_SHARED_RESOURCE_TOKEN_SHA256)) {
@@ -3,6 +3,8 @@ export function featureFlags(env = process.env) {
3
3
  const mode = env['SECURITY_FUSION_MODE'];
4
4
  return {
5
5
  iam: Boolean(env['IAM_CORE_URL']),
6
+ oathweave: Boolean(env['OATHWEAVE_ISSUER']),
7
+ hermes: truthy(env['MAGNUS_HERMES_ENABLED']),
6
8
  router: Boolean(env['MAGNUS_ROUTER_URL'] || env['HIVE_ROUTER_URL']),
7
9
  hivemem: Boolean(env['HIVEMEM_URL']),
8
10
  securityFusion: mode === 'remote' ? mode : 'off',
@@ -11,6 +13,7 @@ export function featureFlags(env = process.env) {
11
13
  hermesRegistry: truthy(env['MAGNUS_HERMES_REGISTRY_ENABLED']),
12
14
  resourceLeases: truthy(env['MAGNUS_RESOURCE_LEASES_ENABLED']),
13
15
  agentDiscovery: truthy(env['ENABLE_AGENT_DISCOVERY']),
16
+ fleetGateway: Boolean(env['FLEET_GATEWAY_URL'] && env['FLEET_GATEWAY_TOKEN_SHA256']),
14
17
  };
15
18
  }
16
19
  /** Boolean switch with an explicit default: only `true`/`false` (any case) override it. */
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ import { isOathweaveEndpoint, parseOathweaveLinks } from '@hivemagnus/magnus-oathweave';
3
+ export { isOathweaveEndpoint, OATHWEAVE_AUDIENCE, parseOathweaveLinks, } from '@hivemagnus/magnus-oathweave';
4
+ /** Every startup problem with the Oathweave settings, keyed by variable name. */
5
+ export function oathweaveConfigurationIssues(env) {
6
+ const issues = [];
7
+ const fields = ['OATHWEAVE_ISSUER', 'OATHWEAVE_JWKS_URL', 'OATHWEAVE_USERINFO_URL'];
8
+ if (!fields.some((field) => env[field]) && !env.MAGNUS_OATHWEAVE_IDENTITIES_JSON)
9
+ return issues;
10
+ if (env.IAM_CORE_URL && env.OATHWEAVE_ISSUER) {
11
+ issues.push({ path: 'OATHWEAVE_ISSUER', message: 'IAM_CORE_URL and OATHWEAVE_ISSUER cannot both be configured; choose one RS256 token verifier' });
12
+ }
13
+ for (const field of fields) {
14
+ const value = env[field];
15
+ if (!value)
16
+ issues.push({ path: field, message: `${field} is required when Oathweave authentication is configured` });
17
+ else if (!isOathweaveEndpoint(value))
18
+ issues.push({ path: field, message: `${field} must be an HTTPS URL without credentials or a fragment` });
19
+ }
20
+ if (!env.MAGNUS_OATHWEAVE_IDENTITIES_JSON) {
21
+ issues.push({ path: 'MAGNUS_OATHWEAVE_IDENTITIES_JSON', message: 'MAGNUS_OATHWEAVE_IDENTITIES_JSON is required when Oathweave authentication is configured' });
22
+ return issues;
23
+ }
24
+ try {
25
+ const links = parseOathweaveLinks(env.MAGNUS_OATHWEAVE_IDENTITIES_JSON);
26
+ const seen = new Set();
27
+ for (const link of links) {
28
+ if (env.OATHWEAVE_ISSUER && link.issuer !== env.OATHWEAVE_ISSUER)
29
+ throw new Error('every identity link issuer must exactly equal OATHWEAVE_ISSUER');
30
+ const key = JSON.stringify([link.issuer, link.subject]);
31
+ if (seen.has(key))
32
+ throw new Error('identity links must be unique per issuer and subject');
33
+ seen.add(key);
34
+ }
35
+ }
36
+ catch (error) {
37
+ issues.push({ path: 'MAGNUS_OATHWEAVE_IDENTITIES_JSON', message: error instanceof Error && !(error instanceof z.ZodError) && !(error instanceof SyntaxError)
38
+ ? error.message : 'MAGNUS_OATHWEAVE_IDENTITIES_JSON must be a JSON array of {issuer,subject,localUserId,role}' });
39
+ }
40
+ return issues;
41
+ }
42
+ //# sourceMappingURL=oathweave.js.map
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import { HttpError } from '../middleware/error-handler.js';
3
+ export function rehearsalEnabled(env = process.env) {
4
+ return z.enum(['true', 'false']).default('false').parse(env['HIVE_REHEARSAL']) === 'true';
5
+ }
6
+ /** Isolated candidates must not claim restored queues, run timers or import catalogs. */
7
+ export function startLiveBackgroundServices(start, env = process.env) {
8
+ if (rehearsalEnabled(env))
9
+ return false;
10
+ start();
11
+ return true;
12
+ }
13
+ export function assertLiveExecution(env = process.env) {
14
+ if (rehearsalEnabled(env))
15
+ throw new HttpError(503, 'REHEARSAL_READ_ONLY', 'Isolated rehearsal disables host execution and queue activation');
16
+ }
17
+ export const rehearsalReadOnly = async (context, next) => {
18
+ if (rehearsalEnabled() && !['GET', 'HEAD', 'OPTIONS'].includes(context.req.method))
19
+ return context.json({ success: false, error: { code: 'REHEARSAL_READ_ONLY', message: 'Isolated rehearsal disables mutations and dispatch' } }, 503);
20
+ return next();
21
+ };
22
+ //# sourceMappingURL=rehearsal.js.map