-
Notifications
You must be signed in to change notification settings - Fork 0
feat(daemon): supervisor unit + chittyserv-vm bootstrap (stops before start) #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
774cd61
ac30e3e
b451622
076f0ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| [Unit] | ||
| Description=ChittyCommand cluster daemon (meta-orchestrator leader) | ||
| Documentation=chittycanon://docs/architecture/chittycommand/ADR-001 | ||
| Documentation=chittycanon://docs/runbooks/chittycommand/daemon-bring-up-vm | ||
| After=network-online.target | ||
| Wants=network-online.target | ||
|
|
||
| [Service] | ||
| Type=simple | ||
| User=chittycommand | ||
| Group=chittycommand | ||
| WorkingDirectory=/opt/chittycommand | ||
| EnvironmentFile=/etc/chittycommand/env | ||
| # NOTE: install-daemon-vm.sh substitutes @@NODE_BIN@@ with the detected | ||
| # node path (command -v node) before installing this unit, so deployments | ||
| # using nvm or /usr/local/bin/node still start. If editing this file by | ||
| # hand, replace @@NODE_BIN@@ with the absolute path to node. | ||
| # Codex P2 PR#105: previously hard-coded /usr/bin/node failed when node was | ||
| # installed elsewhere (nvm, /usr/local/bin). | ||
| ExecStart=@@NODE_BIN@@ /opt/chittycommand/dist/daemon/runtime/entrypoint.js | ||
| Restart=always | ||
| RestartSec=5 | ||
| KillSignal=SIGTERM | ||
| TimeoutStopSec=30 | ||
| StandardOutput=journal | ||
| StandardError=journal | ||
| SyslogIdentifier=chittycommand-daemon | ||
|
|
||
| # Hardening | ||
| NoNewPrivileges=true | ||
| ProtectSystem=strict | ||
| ProtectHome=true | ||
| PrivateTmp=true | ||
| PrivateDevices=true | ||
| ProtectKernelTunables=true | ||
| ProtectKernelModules=true | ||
| ProtectControlGroups=true | ||
| RestrictNamespaces=true | ||
| RestrictRealtime=true | ||
| LockPersonality=true | ||
| # MemoryDenyWriteExecute is intentionally OMITTED. | ||
| # Codex P2 PR#105: systemd documents MDWE as incompatible with JIT engines | ||
| # (V8 generates executable code pages at runtime). Enabling it would abort | ||
| # Node at startup. Source maps in NODE_OPTIONS do not affect this. | ||
| ReadWritePaths=/var/log/chittycommand | ||
|
|
||
| [Install] | ||
| WantedBy=multi-user.target | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| /** | ||
| * ChittyCommand cluster daemon — process entrypoint. | ||
| * | ||
| * Reads required environment, runs `runLeaderLoop`, and traps SIGTERM/SIGINT | ||
| * so the lease is released cleanly before the process exits. | ||
| * | ||
| * This is the file launchd/systemd invokes via `node dist/daemon/entrypoint.js`. | ||
| * It is intentionally thin: all logic lives in `daemon/loop.ts` and `daemon/leader.ts`. | ||
| * | ||
| * @canonical-uri chittycanon://docs/architecture/chittycommand/daemon-supervisor | ||
| */ | ||
|
|
||
| import { runLeaderLoop } from '../loop'; | ||
| import { releaseLeadership, META_LEADER_ROLE } from '../leader'; | ||
|
|
||
| interface RequiredEnv { | ||
| NODE_CHITTY_ID: string; | ||
| DATABASE_URL: string; | ||
| NODE_DESCRIPTOR: string; | ||
| } | ||
|
|
||
| function readEnv(): RequiredEnv { | ||
| const missing: string[] = []; | ||
| const nodeId = process.env.NODE_CHITTY_ID; | ||
| const dbUrl = process.env.DATABASE_URL; | ||
| const descriptor = process.env.NODE_DESCRIPTOR ?? process.env.HOSTNAME ?? ''; | ||
|
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a node is configured from Useful? React with 👍 / 👎. |
||
|
|
||
| if (!nodeId) missing.push('NODE_CHITTY_ID'); | ||
| if (!dbUrl) missing.push('DATABASE_URL'); | ||
| if (!descriptor) missing.push('NODE_DESCRIPTOR or HOSTNAME'); | ||
|
|
||
| if (missing.length > 0) { | ||
| process.stderr.write( | ||
| `[chittycommand-daemon] fatal: missing required env: ${missing.join(', ')}\n`, | ||
| ); | ||
| process.exit(2); | ||
| } | ||
|
|
||
| return { | ||
| NODE_CHITTY_ID: nodeId!, | ||
| DATABASE_URL: dbUrl!, | ||
| NODE_DESCRIPTOR: descriptor, | ||
| }; | ||
| } | ||
|
|
||
| function log(msg: string, meta?: Record<string, unknown>): void { | ||
| const line = { | ||
| ts: new Date().toISOString(), | ||
| svc: 'chittycommand-daemon', | ||
| msg, | ||
| ...(meta ?? {}), | ||
| }; | ||
| process.stdout.write(`${JSON.stringify(line)}\n`); | ||
| } | ||
|
|
||
| async function main(): Promise<void> { | ||
| const env = readEnv(); | ||
| const sessionId = `${process.pid}@${Date.now()}`; | ||
| const controller = new AbortController(); | ||
|
|
||
| log('daemon_start', { | ||
| nodeId: env.NODE_CHITTY_ID, | ||
| descriptor: env.NODE_DESCRIPTOR, | ||
| sessionId, | ||
| role: META_LEADER_ROLE, | ||
| }); | ||
|
|
||
| let shuttingDown = false; | ||
| const shutdown = (signal: string) => { | ||
| if (shuttingDown) return; | ||
| shuttingDown = true; | ||
| log('signal_received', { signal }); | ||
| controller.abort(); | ||
| // Belt-and-suspenders release in case the loop is wedged before the | ||
| // abort path reaches releaseLeadership. Pass sessionId — releaseLeadership | ||
| // gates on session ownership (codex-p2 PR#101 finding-2), so omitting it | ||
| // would no-op against a lease claimed with our sessionId. | ||
| releaseLeadership({ DATABASE_URL: env.DATABASE_URL }, env.NODE_CHITTY_ID, { | ||
| role: META_LEADER_ROLE, | ||
| sessionId, | ||
| }) | ||
|
chitcommit marked this conversation as resolved.
|
||
| .then((released) => log('release_on_signal', { released })) | ||
| .catch((err) => | ||
| log('release_on_signal_error', { | ||
| error: err instanceof Error ? err.message : String(err), | ||
| }), | ||
| ); | ||
| }; | ||
|
|
||
| process.on('SIGTERM', () => shutdown('SIGTERM')); | ||
| process.on('SIGINT', () => shutdown('SIGINT')); | ||
|
|
||
| // No executor callback is passed: as of PR #106 the leader loop dispatches | ||
| // through the canonical executor registry (`meta/executors/*`) via | ||
| // `executeIntent`, not an injected callback. This foundation entrypoint | ||
| // imports no executor modules, so the registry is empty — every claimed | ||
| // intent hits dispatch's "no executor registered" path and is routed to | ||
| // `failed` (never silently `done`). That preserves the PR #105 Codex P1 | ||
| // safety property without a stub callback. Real executors self-register | ||
| // once their modules are imported (mercury_payment lands in PR #108). | ||
| try { | ||
| const result = await runLeaderLoop( | ||
| { DATABASE_URL: env.DATABASE_URL }, | ||
| { | ||
| nodeId: env.NODE_CHITTY_ID, | ||
| nodeDescriptor: env.NODE_DESCRIPTOR, | ||
| sessionId, | ||
| signal: controller.signal, | ||
| log, | ||
| }, | ||
| ); | ||
| log('daemon_loop_returned', { ...result }); | ||
| } catch (err) { | ||
| log('daemon_fatal', { | ||
| error: err instanceof Error ? err.message : String(err), | ||
| }); | ||
| process.exitCode = 1; | ||
| } finally { | ||
| log('daemon_exit', { exitCode: process.exitCode ?? 0 }); | ||
| } | ||
| } | ||
|
|
||
| main().catch((err) => { | ||
| process.stderr.write( | ||
| `[chittycommand-daemon] unhandled: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`, | ||
| ); | ||
| process.exit(1); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # ChittyCommand cluster daemon environment template. | ||
| # | ||
| # Rendered at install time via: | ||
| # op inject -i daemon/runtime/env.tmpl -o /etc/chittycommand/env | ||
| # | ||
| # 1Password is the cold source of truth (operator-manifest policy). This file | ||
| # carries ONLY `op://` references — never real secret values. Do not commit | ||
| # a rendered copy. | ||
| # | ||
| # Vault layout assumed: | ||
| # - Vault: "ChittyOS-Core" | ||
| # - Items: CHITTYCOMMAND_DAEMON, CHITTYCOMMAND_NODES | ||
| # | ||
| # canonical-uri: chittycanon://docs/runbooks/chittycommand/daemon-bring-up-vm | ||
|
|
||
| # --- Node identity (Location-type ChittyID minted via chittyid.chitty.cc) --- | ||
| NODE_CHITTY_ID="op://ChittyOS-Core/CHITTYCOMMAND_NODES/chittyserv-vm/chitty_id" | ||
| NODE_DESCRIPTOR="chittyserv-vm" | ||
|
|
||
| # --- Neon connection for cc_node_leases + meta-orchestrator state --- | ||
| DATABASE_URL="op://ChittyOS-Core/CHITTYCOMMAND_DAEMON/database_url" | ||
|
|
||
| # --- Ecosystem URLs (overridable; defaults baked into the code) --- | ||
| REGISTRY_URL="https://registry.chitty.cc" | ||
| CHITTYAGENT_URL="https://agent.chitty.cc" | ||
| CHITTYTRUST_URL="https://trust.chitty.cc" | ||
| CHITTYCONNECT_URL="https://connect.chitty.cc" | ||
|
|
||
| # --- ChittyConnect token for context/sensitive-intent routing --- | ||
| CHITTYCONNECT_TOKEN="op://ChittyOS-Core/CHITTYCOMMAND_DAEMON/chittyconnect_token" | ||
|
|
||
| # --- Node runtime --- | ||
| NODE_ENV="production" | ||
| NODE_OPTIONS="--enable-source-maps" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # launchd-shim.sh — macOS env-loading shim for the ChittyCommand daemon. | ||
| # | ||
| # launchd has no native EnvironmentFile equivalent (unlike systemd), so this | ||
| # shim sources /etc/chittycommand/env before exec'ing node. The systemd unit | ||
| # uses EnvironmentFile=/etc/chittycommand/env directly; this shim keeps the | ||
| # macOS path consistent. | ||
| # | ||
| # Codex P2 PR#105: previously the launchd plist invoked node directly with | ||
| # only NODE_ENV/NODE_OPTIONS exported, which meant entrypoint.ts's readEnv() | ||
| # always tripped its fatal-missing-env branch on Mac Mini nodes. | ||
| # | ||
| # Install path: /opt/chittycommand/dist/daemon/runtime/launchd-shim.sh | ||
| # Mode: 0755, owned by chittycommand:chittycommand | ||
| # | ||
| # canonical-uri: chittycanon://docs/architecture/chittycommand/daemon-supervisor | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| ENV_FILE="${CHITTYCOMMAND_ENV_FILE:-/etc/chittycommand/env}" | ||
| NODE_BIN="${CHITTYCOMMAND_NODE_BIN:-/usr/local/bin/node}" | ||
| ENTRYPOINT="/opt/chittycommand/dist/daemon/runtime/entrypoint.js" | ||
|
|
||
| if [[ ! -r "${ENV_FILE}" ]]; then | ||
| echo "[chittycommand-daemon-shim] fatal: env file not readable: ${ENV_FILE}" >&2 | ||
| exit 7 | ||
| fi | ||
|
|
||
| # Source env file. The file is the same KEY=VALUE format the systemd | ||
| # EnvironmentFile expects, rendered by `op inject` at install time. | ||
| set -a | ||
| # shellcheck disable=SC1090 | ||
| . "${ENV_FILE}" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On macOS nodes using this shim, Useful? React with 👍 / 👎. |
||
| set +a | ||
|
|
||
| # Preserve NODE_ENV / NODE_OPTIONS if launchd set them. | ||
| export NODE_ENV="${NODE_ENV:-production}" | ||
| export NODE_OPTIONS="${NODE_OPTIONS:---enable-source-maps}" | ||
|
|
||
| if [[ ! -x "${NODE_BIN}" ]]; then | ||
| echo "[chittycommand-daemon-shim] fatal: node not executable at ${NODE_BIN}" >&2 | ||
| exit 8 | ||
| fi | ||
|
|
||
| exec "${NODE_BIN}" "${ENTRYPOINT}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!-- | ||
| ChittyCommand cluster daemon — macOS launchd unit. | ||
|
|
||
| Used on the Mac Mini cluster nodes (chittymini-01 stays macOS). The | ||
| chittyserv-vm bootstrap (PR target) uses systemd; this plist is included | ||
| so the next-node bring-up doesn't need a separate PR. | ||
|
|
||
| Install path: /Library/LaunchDaemons/com.chittyos.chittycommand-daemon.plist | ||
| Owned by root:wheel, mode 0644. | ||
| Load: sudo launchctl bootstrap system <path> | ||
| Unload: sudo launchctl bootout system/com.chittyos.chittycommand-daemon | ||
|
|
||
| canonical-uri: chittycanon://docs/architecture/chittycommand/daemon-supervisor | ||
| --> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>Label</key> | ||
| <string>com.chittyos.chittycommand-daemon</string> | ||
|
|
||
| <!-- | ||
| Codex P2 PR#105: launchd has no native EnvironmentFile equivalent and | ||
| entrypoint.ts requires NODE_CHITTY_ID, DATABASE_URL, and | ||
| NODE_DESCRIPTOR/HOSTNAME. Invoke a shim that sources | ||
| /etc/chittycommand/env before exec'ing node, so the daemon sees the | ||
| same env shape on macOS as on systemd Linux. | ||
| --> | ||
| <key>ProgramArguments</key> | ||
| <array> | ||
| <string>/opt/chittycommand/dist/daemon/runtime/launchd-shim.sh</string> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The launchd plist points directly at Useful? React with 👍 / 👎. |
||
| </array> | ||
|
|
||
| <key>WorkingDirectory</key> | ||
| <string>/opt/chittycommand</string> | ||
|
|
||
| <key>UserName</key> | ||
| <string>chittycommand</string> | ||
|
|
||
| <key>GroupName</key> | ||
| <string>chittycommand</string> | ||
|
|
||
| <key>RunAtLoad</key> | ||
| <true/> | ||
|
|
||
| <key>KeepAlive</key> | ||
| <dict> | ||
| <key>SuccessfulExit</key> | ||
| <false/> | ||
| <key>Crashed</key> | ||
| <true/> | ||
| </dict> | ||
|
|
||
| <key>ThrottleInterval</key> | ||
| <integer>5</integer> | ||
|
|
||
| <key>ExitTimeOut</key> | ||
| <integer>30</integer> | ||
|
|
||
| <!-- | ||
| Environment is loaded from /etc/chittycommand/env via a shim. launchd | ||
| has no native EnvironmentFile, so the ProgramArguments above should be | ||
| wrapped at install time by an entrypoint that sources the env file, or | ||
| EnvironmentVariables below is populated by `op inject` at install time. | ||
| See docs/runbooks/daemon-bring-up-vm.md note on macOS path. | ||
| --> | ||
| <key>EnvironmentVariables</key> | ||
| <dict> | ||
|
chitcommit marked this conversation as resolved.
|
||
| <key>NODE_ENV</key> | ||
| <string>production</string> | ||
| <key>NODE_OPTIONS</key> | ||
| <string>--enable-source-maps</string> | ||
| </dict> | ||
|
|
||
| <key>StandardOutPath</key> | ||
| <string>/var/log/chittycommand-daemon.out.log</string> | ||
|
|
||
| <key>StandardErrorPath</key> | ||
| <string>/var/log/chittycommand-daemon.err.log</string> | ||
|
|
||
| <key>ProcessType</key> | ||
| <string>Background</string> | ||
| </dict> | ||
| </plist> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "extends": "../../tsconfig.json", | ||
| "compilerOptions": { | ||
| "module": "NodeNext", | ||
| "moduleResolution": "NodeNext", | ||
| "target": "ES2022", | ||
| "lib": ["ES2022"], | ||
| "types": ["node"], | ||
| "noEmit": false, | ||
| "outDir": "../../dist", | ||
| "rootDir": "../..", | ||
| "declaration": false, | ||
| "sourceMap": true, | ||
| "isolatedModules": false, | ||
| "paths": {} | ||
| }, | ||
| "include": [ | ||
| "../../daemon/**/*.ts", | ||
| "../../meta/intent.ts", | ||
| "../../src/db/schema.ts" | ||
| ], | ||
| "exclude": [ | ||
| "../../node_modules", | ||
| "../../ui", | ||
| "../../tests", | ||
| "../../src/agents", | ||
| "../../src/index.ts" | ||
| ] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If Node.js 20+ is installed somewhere other than
/usr/bin(for example via nvm, a custom/usr/local/bininstall, or a packaged path the script finds inPATH), the installer passescommand -v nodebut the enabled service later fails at start becauseExecStartignores that detected path. Either render/install the unit with the discoveredNODE_BINor make/usr/bin/nodean explicit prerequisite.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 6d02b99. Unit ships with
ExecStart=@@NODE_BIN@@placeholder; install-daemon-vm.sh nowsed-substitutes the detectedcommand -v nodepath into a tempfile before installing the unit, so nvm / /usr/local/bin / packaged node paths all work.