@hivemagnus/magnus-server 1.1.13 → 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.
- package/.env.example +37 -0
- package/README.md +36 -0
- package/agent-version.json +1 -1
- package/dist/ai/TOOLS.md +1 -1
- package/dist/cli/args.js +3 -1
- package/dist/cli/env-catalog.js +16 -0
- package/dist/cli/migrations.js +22 -0
- package/dist/cli/verify-initialization.js +12 -0
- package/dist/cli.js +28 -0
- package/dist/config/console-path.js +17 -0
- package/dist/config/env.js +32 -5
- package/dist/config/features.js +4 -1
- package/dist/config/oathweave.js +42 -0
- package/dist/config/rehearsal.js +22 -0
- package/dist/db/generated/prisma/internal/class.js +4 -4
- package/dist/db/generated/prisma/internal/prismaNamespace.js +17 -1
- package/dist/db/generated/prisma/internal/prismaNamespaceBrowser.js +17 -1
- package/dist/db/generated/prisma/models/CindertraceResponse.js +2 -0
- package/dist/db/generated/prisma/models/ProductInitialization.js +2 -0
- package/dist/db/read-retry.js +5 -1
- package/dist/db/seed.js +8 -0
- package/dist/index.js +28 -37
- package/dist/middleware/auth.js +21 -0
- package/dist/middleware/error-handler.js +10 -10
- package/dist/middleware/oathweave-verifier.js +23 -0
- package/dist/middleware/oathweave.js +115 -0
- package/dist/middleware/rate-limit.js +12 -3
- package/dist/openapi/spec.js +45 -4
- package/dist/queue/names.js +9 -0
- package/dist/queue/workers.js +9 -68
- package/dist/routes/assistant.js +30 -12
- package/dist/routes/cindertrace-response.js +183 -0
- package/dist/routes/console.js +33 -0
- package/dist/routes/cron.js +8 -5
- package/dist/routes/events-stream.js +10 -0
- package/dist/routes/files.js +2 -1
- package/dist/routes/fleet-gateway.js +69 -0
- package/dist/routes/heartbeat.js +1 -6
- package/dist/routes/hermes.js +26 -6
- package/dist/routes/hiveskills.js +116 -0
- package/dist/routes/library.js +3 -2
- package/dist/routes/logs.js +5 -0
- package/dist/routes/metrics.js +4 -3
- package/dist/routes/nodes.js +45 -8
- package/dist/routes/ops.js +24 -6
- package/dist/routes/profiling.js +2 -1
- package/dist/routes/roles.js +17 -9
- package/dist/routes/security-fusion.js +15 -600
- package/dist/routes/stats.js +8 -5
- package/dist/routes/terminal-ticket.js +3 -3
- package/dist/routes/terminal.js +2 -2
- package/dist/routes/webhooks.js +15 -15
- package/dist/sdk/node-api-client.js +23 -1
- package/dist/security/cindertrace-response.js +14 -0
- package/dist/security/native-completion-registry.js +14 -0
- package/dist/server.js +11 -14
- package/dist/services/ai-assistant-service.js +32 -6
- package/dist/services/ai-tools.js +15 -8
- package/dist/services/ardent-scope-enrollment.js +4 -2
- package/dist/services/command-resource-leases.js +2 -4
- package/dist/services/forecast-service.js +23 -8
- package/dist/services/ops-service.js +20 -25
- package/dist/services/prediction-service.js +2 -1
- package/dist/services/resource-enrollment.js +57 -4
- package/dist/services/secret-views.js +72 -0
- package/dist/services/security-export.js +4 -6
- package/dist/services/topology-service.js +2 -1
- package/dist/services/webhook-security.js +10 -1
- package/dist/services/webhook-service.js +14 -6
- package/dist/utils/node-url.js +2 -1
- package/dist/utils/safe-errors.js +130 -0
- package/node_modules/@hivemagnus/magnus-oathweave/LICENSE +202 -0
- package/node_modules/@hivemagnus/magnus-oathweave/README.md +20 -0
- package/node_modules/@hivemagnus/magnus-oathweave/dist/index.d.ts +61 -0
- package/node_modules/@hivemagnus/magnus-oathweave/dist/index.js +148 -0
- package/node_modules/@hivemagnus/magnus-oathweave/package.json +54 -0
- package/package.json +7 -3
- package/prisma/migrations/20261103000000_cindertrace_response_bridge/migration-impact.json +128 -0
- package/prisma/migrations/20261103000000_cindertrace_response_bridge/migration.sql +6 -0
- package/prisma/migrations/20261105010000_base_initialization/magnus-impact.json +1 -0
- package/prisma/migrations/20261105010000_base_initialization/migration.sql +5 -0
- package/prisma/schema.prisma +22 -0
- package/dist/services/security-fusion-actions.js +0 -22
- package/dist/services/security-fusion-evidence-integrity.js +0 -144
- package/dist/services/security-fusion-ingest.js +0 -101
- package/dist/services/security-fusion-outbox.js +0 -185
- package/dist/services/security-fusion-receipt-chain.js +0 -134
- package/dist/services/security-fusion-service.js +0 -751
- package/dist/services/security-fusion-verification.js +0 -50
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
|
|
@@ -200,3 +229,11 @@ CORS_ORIGIN=https://magnus.example.com:3000
|
|
|
200
229
|
# MAGNUS_AGENT_UPGRADE_MAX_FAILURES=2
|
|
201
230
|
# The retired git-checkout self-update endpoints (source installs only).
|
|
202
231
|
# MAGNUS_LEGACY_GIT_SELF_UPDATE=false
|
|
232
|
+
|
|
233
|
+
# Optional Cindertrace response bridge. Configure a new dedicated secret.
|
|
234
|
+
# CINDERTRACE_RESPONSE_SECRET=
|
|
235
|
+
# CINDERTRACE_RESTART_ALLOWLIST=
|
|
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.
|
|
@@ -187,3 +200,26 @@ migrations are never edited.
|
|
|
187
200
|
## License
|
|
188
201
|
|
|
189
202
|
Apache-2.0. See [LICENSE](LICENSE).
|
|
203
|
+
|
|
204
|
+
## Cindertrace
|
|
205
|
+
|
|
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.
|
package/agent-version.json
CHANGED
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
|
|
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) {
|
package/dist/cli/env-catalog.js
CHANGED
|
@@ -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.' },
|
|
@@ -60,6 +72,9 @@ export const ENV_CATALOG = [
|
|
|
60
72
|
{ name: 'MAGNUS_LESSONS_HIVEMEM_URL', group: 'hivemem', description: 'Optional HiveMem URL for operational lessons.' },
|
|
61
73
|
{ name: 'MAGNUS_LESSONS_HIVEMEM_TOKEN', group: 'hivemem', secret: true, description: 'Token for MAGNUS_LESSONS_HIVEMEM_URL.' },
|
|
62
74
|
// ── Security Fusion / external security console ──
|
|
75
|
+
{ name: 'CINDERTRACE_RESPONSE_SECRET', group: 'security', description: 'Dedicated HMAC credential for the optional Cindertrace managed response bridge.' },
|
|
76
|
+
{ name: 'CINDERTRACE_RESTART_ALLOWLIST', group: 'security', description: 'Comma-separated allowlisted response service names; empty by default.' },
|
|
77
|
+
{ name: 'CINDERTRACE_PROTECTED_TARGET_IDS', group: 'security', description: 'Comma-separated node IDs that Cindertrace must never isolate.' },
|
|
63
78
|
{ name: 'SECURITY_FUSION_MODE', group: 'security', default: 'off', description: 'off (feature disabled), local (cases and evidence in this server) or remote (export to an external security console).' },
|
|
64
79
|
{ name: 'SECURITY_FUSION_RECEIPT_SIGNING_KEY', group: 'security', secret: true, requiredWhen: 'SECURITY_FUSION_MODE=local', description: 'Dedicated key that signs Security Fusion deletion receipts (min 32 chars).' },
|
|
65
80
|
{ name: 'SECURITY_FUSION_EVIDENCE_DIR', group: 'security', description: 'Private evidence directory (local mode).' },
|
|
@@ -77,6 +92,7 @@ export const ENV_CATALOG = [
|
|
|
77
92
|
{ name: 'SECURITY_FUSION_MATTERMOST_CHANNEL_ID', group: 'security', description: 'Mattermost channel ID.' },
|
|
78
93
|
// ── Hermes Agent plugin API ──
|
|
79
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.' },
|
|
80
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.' },
|
|
81
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.' },
|
|
82
98
|
{ name: 'MAGNUS_APPROVAL_BRIDGE_CONSUMER', group: 'plugins', description: 'Plugin read consumer allowed to list approval plans and relay signed decisions.' },
|
package/dist/cli/migrations.js
CHANGED
|
@@ -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
|
package/dist/config/env.js
CHANGED
|
@@ -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(),
|
|
@@ -55,7 +61,10 @@ export const envSchema = z.object({
|
|
|
55
61
|
.regex(/^\s*[a-z0-9](?:[a-z0-9.-]{0,98}[a-z0-9])?\s*(?:,\s*[a-z0-9](?:[a-z0-9.-]{0,98}[a-z0-9])?\s*)*$/, 'must list domain prefixes made of a-z, 0-9, "." and "-", separated by commas')
|
|
56
62
|
.optional(),
|
|
57
63
|
// Security Fusion: off (default) | local (in-process) | remote (external security console).
|
|
58
|
-
SECURITY_FUSION_MODE: z.enum(['off', '
|
|
64
|
+
SECURITY_FUSION_MODE: z.enum(['off', 'remote'], { error: 'Embedded Security Fusion local mode is retired; export and import state into Cindertrace before selecting remote mode (docs/CINDERTRACE_MIGRATION.md)' }).default('off'),
|
|
65
|
+
CINDERTRACE_RESPONSE_SECRET: z.string().min(32).optional(),
|
|
66
|
+
CINDERTRACE_RESTART_ALLOWLIST: z.string().optional(),
|
|
67
|
+
CINDERTRACE_PROTECTED_TARGET_IDS: z.string().optional(),
|
|
59
68
|
SECURITY_FUSION_API_URL: z.string().url().optional(),
|
|
60
69
|
MAGNUS_SECURITY_CONSOLE_URL: z.string().url().optional(),
|
|
61
70
|
SECURITY_APPROVAL_BRIDGE_TOKEN: z.string().min(32).optional(),
|
|
@@ -65,6 +74,10 @@ export const envSchema = z.object({
|
|
|
65
74
|
SHARED_RESOURCE_MISSION_TRUST_JSON: z.string().optional(),
|
|
66
75
|
SHARED_RESOURCE_RESULT_TRUST_JSON: z.string().optional(),
|
|
67
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,
|
|
68
81
|
MAGNUS_APPLICATION_PROBES_FILE: z.string().optional(),
|
|
69
82
|
MAGNUS_OPERATIONS_CATALOG_FILE: z.string().optional(),
|
|
70
83
|
MAGNUS_ENGINEERING_EVIDENCE_FILE: z.string().optional(),
|
|
@@ -101,6 +114,7 @@ export const envSchema = z.object({
|
|
|
101
114
|
MAGNUS_PLUGIN_READ_TOKEN_DIGESTS_JSON: z.string().optional(),
|
|
102
115
|
MAGNUS_PLUGIN_OPERATOR_TOKEN_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
103
116
|
MAGNUS_SHARED_RESOURCE_TOKEN_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
117
|
+
MAGNUS_HIVESKILLS_DEPLOYMENTS_JSON: z.string().optional(),
|
|
104
118
|
MAGNUS_SHARED_RESOURCE_TOKEN_DIGESTS_JSON: z.string().optional(),
|
|
105
119
|
MAGNUS_FLEET_CHECK_TOKEN_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
106
120
|
SECURITY_FUSION_RECEIPT_SIGNING_KEY: z.string().min(32).optional(),
|
|
@@ -111,6 +125,10 @@ export const envSchema = z.object({
|
|
|
111
125
|
IAM_CORE_URL: z.string().url().optional(),
|
|
112
126
|
IAM_TOKEN_ISSUER: z.string().min(1).optional(),
|
|
113
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(),
|
|
114
132
|
// Skills
|
|
115
133
|
SKILLS_DIR: z.string().default(DEFAULT_SKILLS_DIR),
|
|
116
134
|
// Log retention
|
|
@@ -164,6 +182,15 @@ export const envSchema = z.object({
|
|
|
164
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')
|
|
165
183
|
.optional(),
|
|
166
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' });
|
|
167
194
|
try {
|
|
168
195
|
webhookTypeAliases(env.MAGNUS_WEBHOOK_TYPE_ALIASES);
|
|
169
196
|
}
|
|
@@ -192,6 +219,9 @@ export const envSchema = z.object({
|
|
|
192
219
|
message: 'NOTIFICATION_READ_RETENTION_DAYS cannot exceed NOTIFICATION_MAX_RETENTION_DAYS',
|
|
193
220
|
});
|
|
194
221
|
}
|
|
222
|
+
for (const issue of oathweaveConfigurationIssues(env)) {
|
|
223
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [issue.path], message: issue.message });
|
|
224
|
+
}
|
|
195
225
|
if (env.IAM_CORE_URL) {
|
|
196
226
|
for (const field of ['IAM_TOKEN_ISSUER', 'IAM_TOKEN_AUDIENCE']) {
|
|
197
227
|
if (env[field])
|
|
@@ -246,10 +276,6 @@ export const envSchema = z.object({
|
|
|
246
276
|
message: 'AGENT_DISCOVERY_SECRET is required when ENABLE_AGENT_DISCOVERY=true',
|
|
247
277
|
});
|
|
248
278
|
}
|
|
249
|
-
if (env.SECURITY_FUSION_MODE === 'local' && !env.SECURITY_FUSION_RECEIPT_SIGNING_KEY) {
|
|
250
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['SECURITY_FUSION_RECEIPT_SIGNING_KEY'],
|
|
251
|
-
message: 'SECURITY_FUSION_RECEIPT_SIGNING_KEY is required when SECURITY_FUSION_MODE=local' });
|
|
252
|
-
}
|
|
253
279
|
if (env.MAGNUS_PLUGIN_API_ENABLED) {
|
|
254
280
|
if (!env.MAGNUS_PLUGIN_READ_TOKEN_DIGESTS_JSON)
|
|
255
281
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['MAGNUS_PLUGIN_READ_TOKEN_DIGESTS_JSON'],
|
|
@@ -336,6 +362,7 @@ export const envSchema = z.object({
|
|
|
336
362
|
env.MAGNUS_PLUGIN_OPERATOR_TOKEN_SHA256,
|
|
337
363
|
env.MAGNUS_FLEET_CHECK_TOKEN_SHA256,
|
|
338
364
|
env.MAGNUS_COLLECTOR_TOKEN_SHA256,
|
|
365
|
+
env.FLEET_GATEWAY_TOKEN_SHA256,
|
|
339
366
|
...configuredDigestValues(env.MAGNUS_PLUGIN_READ_TOKEN_DIGESTS_JSON),
|
|
340
367
|
].filter((value) => Boolean(value)));
|
|
341
368
|
if (env.MAGNUS_SHARED_RESOURCE_TOKEN_SHA256 && otherScopedDigests.has(env.MAGNUS_SHARED_RESOURCE_TOKEN_SHA256)) {
|
package/dist/config/features.js
CHANGED
|
@@ -3,14 +3,17 @@ 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
|
-
securityFusion: mode === '
|
|
10
|
+
securityFusion: mode === 'remote' ? mode : 'off',
|
|
9
11
|
securityConsoleUrl: env['MAGNUS_SECURITY_CONSOLE_URL'] || null,
|
|
10
12
|
pluginApi: truthy(env['MAGNUS_PLUGIN_API_ENABLED']),
|
|
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
|