Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions daemon/runtime/chittycommand-daemon.service
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the detected Node path in the unit

If Node.js 20+ is installed somewhere other than /usr/bin (for example via nvm, a custom /usr/local/bin install, or a packaged path the script finds in PATH), the installer passes command -v node but the enabled service later fails at start because ExecStart ignores that detected path. Either render/install the unit with the discovered NODE_BIN or make /usr/bin/node an explicit prerequisite.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

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 now sed-substitutes the detected command -v node path into a tempfile before installing the unit, so nvm / /usr/local/bin / packaged node paths all work.

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
128 changes: 128 additions & 0 deletions daemon/runtime/entrypoint.ts
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept the documented node env names

When a node is configured from daemon/supervisor.md, the documented required variables are CHITTYCOMMAND_NODE_ID and CHITTYCOMMAND_NODE_DESCRIPTOR, but this entrypoint only reads NODE_CHITTY_ID/NODE_DESCRIPTOR and exits through the missing-env path. Either accept the documented names as aliases or update the supervisor/launchd environment contract everywhere; otherwise a daemon installed from the existing supervisor plan never reaches the leader loop.

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,
})
Comment thread
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);
});
34 changes: 34 additions & 0 deletions daemon/runtime/env.tmpl
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"
46 changes: 46 additions & 0 deletions daemon/runtime/launchd-shim.sh
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}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not shell-source rendered secrets

On macOS nodes using this shim, /etc/chittycommand/env is rendered from op inject in systemd EnvironmentFile format, but sourcing it as shell code means characters in DATABASE_URL or CHITTYCONNECT_TOKEN such as $ or backticks are expanded/executed instead of treated as literal secret bytes. A generated DB password containing these characters will make the daemon start with corrupted credentials; parse KEY=VALUE without shell evaluation or render a shell-escaped launchd-specific env file.

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}"
84 changes: 84 additions & 0 deletions daemon/runtime/launchd/com.chittyos.chittycommand-daemon.plist
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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install the launchd shim before referencing it

The launchd plist points directly at /opt/chittycommand/dist/daemon/runtime/launchd-shim.sh, but the daemon build only emits the TypeScript outputs under dist and rg shows no installer or copy step for this shell script. On a Mac Mini node that installs the documented plist path, launchd will fail before Node starts because the referenced ProgramArguments executable is absent unless operators manually copy it into dist.

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>
Comment thread
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>
29 changes: 29 additions & 0 deletions daemon/runtime/tsconfig.daemon.json
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"
]
}
2 changes: 2 additions & 0 deletions daemon/supervisor.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ related_adr: chittycanon://docs/architecture/chittycommand/ADR-001

# Cluster daemon — supervision plan

> **First-node target: `chittyserv-vm`.** Bring-up runbook + real systemd unit and bootstrap script live at [`docs/runbooks/daemon-bring-up-vm.md`](../docs/runbooks/daemon-bring-up-vm.md) (added in the stacked follow-on PR). The runtime artifacts are under [`daemon/runtime/`](./runtime/).

This document is doc-only. No runtime supervisor code ships in the foundation
PR. The targets below are the homelab cluster of 6 Mac Minis
(`chittymini-01..06`) plus `chittyserv-vm`. Each node runs **one** instance of
Expand Down
Loading
Loading