unoverse 0.1.24 → 0.1.26

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/bin/unoverse.mjs CHANGED
@@ -60,7 +60,7 @@ const UNIVERSE = findUniverse();
60
60
  const OPERATOR_COMMANDS = new Set([
61
61
  "start", "stop", "check", "logs", "deploy",
62
62
  // kept working, not advertised
63
- "ground", "dev", "build", "publish", "init", "doctor", "db-setup", "db-verify", "open",
63
+ "ground", "dev", "build", "publish", "init",
64
64
  ]);
65
65
 
66
66
  const [, , cmd, ...args] = process.argv;
package/lib/create.mjs CHANGED
@@ -256,7 +256,7 @@ export async function create(nameArg) {
256
256
  env: { ...process.env, UNOVERSE_DOCR_TOKEN: token },
257
257
  });
258
258
  if (r.status !== 0) {
259
- fail(`setup did not finish. Run 'unoverse init'${name === "." ? "" : ` in ./${name}`} to pick it up`);
259
+ fail(`setup did not finish. Run 'unoverse start'${name === "." ? "" : ` in ./${name}`} to pick it back up`);
260
260
  process.exit(r.status ?? 1);
261
261
  }
262
262
  console.log(`
@@ -0,0 +1,66 @@
1
+ # Gravity Platform Ansible Automation
2
+
3
+ Ansible playbooks for deployment, upgrades, and operations.
4
+
5
+ ## Quick Start (Single VM)
6
+
7
+ There is no inventory to configure — deployments are single-VM by contract
8
+ (INFRASTRUCTURE.md) and `unoverse deploy` builds a temporary inventory from
9
+ `.env.production` each run.
10
+ Just use `.env.production` at the project root:
11
+
12
+ ```bash
13
+ # 1. Configure production environment (Terraform renders it, complete)
14
+ cd infra/digitalocean && terraform apply
15
+ terraform output -raw env_production > ../../.env.production
16
+
17
+ # 2. Deploy
18
+ gravity deploy
19
+
20
+ # 3. Verify
21
+ gravity deploy test
22
+ ```
23
+
24
+ The `gravity deploy` command reads `.env.production`, builds a temporary inventory, and runs the playbooks automatically.
25
+
26
+ ## Structure
27
+
28
+ ```
29
+ ansible/
30
+ ├── playbooks/ # All playbooks
31
+ ├── templates/
32
+ └── ansible.cfg
33
+ ```
34
+
35
+ ## Direct Ansible Usage
36
+
37
+ For advanced use or multi-VM enterprise deployments:
38
+
39
+ ```bash
40
+ cd ansible
41
+
42
+ # Single VM (reads .env.production from project root)
43
+
44
+ # Multi-VM (target specific groups)
45
+ ```
46
+
47
+ ## Playbooks
48
+
49
+ | Playbook | Purpose |
50
+ | ----------------------- | -------------------------------------------- |
51
+ | `install.yml` | Fresh install (Docker, images, services) |
52
+ | `deploy-packages.yml` | Deploy packages (rsync from local + build) |
53
+ | `db-setup.yml` | Database setup and migrations |
54
+ | `relocate-db.yml` | RELOCATE a database (dump/restore to another server) — NOT schema migrations (that is db-setup) |
55
+ | `rollback.yml` | Rollback to previous version |
56
+ | `health-check.yml` | Verify all services healthy |
57
+ | `backup.yml` | Backup UMAP models |
58
+ | `restore.yml` | Restore UMAP models from backup |
59
+ | `harden.yml` | Security hardening (SSH, firewall, fail2ban) |
60
+ | `test-connectivity.yml` | Test VM connectivity and ports |
61
+
62
+ ## Requirements
63
+
64
+ - Ansible 2.12+
65
+ - SSH access to target VMs
66
+ - DOCR token (for pulling Docker images from DigitalOcean Container Registry)
@@ -0,0 +1,19 @@
1
+ [defaults]
2
+ # inventory: none by default — `unoverse deploy` generates a temp inventory from
3
+ # .env.production (multi-VM inventory retired to _legacy/ansible 2026-07-28)
4
+ roles_path = roles
5
+ host_key_checking = False
6
+ retry_files_enabled = False
7
+ # Silence the noisy ansible-core module deprecation warnings (to_text/to_bytes imports in
8
+ # the synchronize module) — they're upstream, not our playbooks. Keeps the deploy log clean.
9
+ deprecation_warnings = False
10
+ gathering = smart
11
+ fact_caching = jsonfile
12
+ fact_caching_connection = /tmp/ansible_facts
13
+ fact_caching_timeout = 3600
14
+
15
+ [privilege_escalation]
16
+ become = True
17
+ become_method = sudo
18
+ become_user = root
19
+ become_ask_pass = False
@@ -0,0 +1,47 @@
1
+ ---
2
+ # Gravity Platform Backup Playbook
3
+ # Backs up UMAP models (database backup is customer responsibility)
4
+ #
5
+ # Usage:
6
+ # POC: ansible-playbook -i inventory/production.yml playbooks/backup.yml
7
+ # Enterprise: ansible-playbook -i inventory/production.yml playbooks/backup.yml -l ml_vms
8
+
9
+ - name: Backup Gravity Platform
10
+ hosts: all
11
+ become: yes
12
+ vars:
13
+ backup_dir: /opt/gravity/backups
14
+ backup_timestamp: "{{ ansible_date_time.iso8601_basic_short }}"
15
+
16
+ tasks:
17
+ - name: Create backup directory
18
+ file:
19
+ path: "{{ backup_dir }}"
20
+ state: directory
21
+ mode: "0755"
22
+
23
+ - name: Backup UMAP models
24
+ archive:
25
+ path: /opt/gravity/umap/models
26
+ dest: "{{ backup_dir }}/umap-models-{{ backup_timestamp }}.tar.gz"
27
+ format: gz
28
+ when: "'umap' in gravity_services"
29
+
30
+ - name: List backups
31
+ find:
32
+ paths: "{{ backup_dir }}"
33
+ patterns: "*.tar.gz,*.dump"
34
+ register: backup_files
35
+
36
+ - name: Display backup info
37
+ debug:
38
+ msg: |
39
+ Backup completed:
40
+ - UMAP Models: umap-models-{{ backup_timestamp }}.tar.gz
41
+ - Location: {{ backup_dir }}
42
+ - Total backups: {{ backup_files.files | length }}
43
+
44
+ - name: Clean old backups (keep last 7)
45
+ shell: |
46
+ ls -t {{ backup_dir }}/umap-models-*.tar.gz | tail -n +8 | xargs -r rm
47
+ when: backup_files.files | length > 7
@@ -0,0 +1,123 @@
1
+ ---
2
+ # Gravity Platform Database Setup Playbook
3
+ # Creates database tables - uses CREATE TABLE IF NOT EXISTS (safe to run multiple times)
4
+ #
5
+ # Usage:
6
+ # ansible-playbook -i inventory/production.yml playbooks/db-setup.yml
7
+ #
8
+ # Prerequisites:
9
+ # - Core services deployed (install.yml)
10
+ # - DATABASE_URL configured in /opt/gravity/.env
11
+ # - Required extensions enabled in your DB provider:
12
+ # vector, pg_stat_statements
13
+ # (DigitalOcean: Database → Settings → Allowed Extensions)
14
+
15
+ - name: Database Setup
16
+ hosts: all
17
+ become: yes
18
+
19
+ tasks:
20
+ - name: "[1/5] Verify .env exists"
21
+ stat:
22
+ path: /opt/gravity/.env
23
+ register: env_file
24
+
25
+ - name: "[1/5] Fail if .env missing"
26
+ fail:
27
+ msg: ".env file not found at /opt/gravity/.env - run install.yml first"
28
+ when: not env_file.stat.exists
29
+
30
+ - name: "[2/5] Check DATABASE_URL is configured"
31
+ shell: grep -q "^DATABASE_URL=" /opt/gravity/.env
32
+ register: db_url_check
33
+ ignore_errors: yes
34
+
35
+ - name: "[2/5] Fail if DATABASE_URL missing"
36
+ fail:
37
+ msg: "DATABASE_URL not found in .env - configure database connection first"
38
+ when: db_url_check.rc != 0
39
+
40
+ - name: "[3/5] Enable required PostgreSQL extensions"
41
+ command: >
42
+ docker compose exec -T -e NODE_TLS_REJECT_UNAUTHORIZED=0 unoverse node -e
43
+ "const{Pool}=require('pg');
44
+ const p=new Pool({connectionString:process.env.DATABASE_URL,ssl:{rejectUnauthorized:false}});
45
+ (async()=>{
46
+ const exts=['vector','pg_stat_statements'];
47
+ const results=[];
48
+ for(const ext of exts){
49
+ try{await p.query('CREATE EXTENSION IF NOT EXISTS '+ext);results.push(ext+': OK')}
50
+ catch(e){results.push(ext+': FAILED ('+e.message+')')}
51
+ }
52
+ console.log(results.join('\n'));
53
+ await p.end();
54
+ process.exit(results.some(r=>r.includes('FAILED'))?1:0);
55
+ })()"
56
+ args:
57
+ chdir: /opt/gravity
58
+ register: ext_result
59
+ ignore_errors: yes
60
+
61
+ - name: "[3/5] Extension status"
62
+ debug:
63
+ msg: "{{ ext_result.stdout_lines | default(['No output']) }}"
64
+
65
+ - name: "[3/5] Warn if extensions failed"
66
+ debug:
67
+ msg: |
68
+ ⚠️ Some extensions failed to enable. You must enable them in your
69
+ database provider's dashboard BEFORE running this playbook.
70
+
71
+ DigitalOcean: Database → Settings → Allowed Extensions
72
+ Enable: vector, pg_stat_statements
73
+
74
+ See docs/runbooks/02-database.md for details.
75
+ when: ext_result.rc != 0
76
+
77
+ # Apply the .sql migrations with node-pg-migrate — same as `./unoverse db-setup`
78
+ # (scripts/lib/db-setup.sh). The old programmatic `require('./dist/db')` table
79
+ # setup is retired: the engine no longer ships a dist, and .sql migrations under
80
+ # apps/unoverse/engine/migrations are the single source of truth.
81
+ - name: "[4/5] Apply database migrations (node-pg-migrate)"
82
+ shell: |
83
+ docker compose exec -T -e NODE_TLS_REJECT_UNAUTHORIZED=0 unoverse \
84
+ npx node-pg-migrate up \
85
+ --migrations-dir /app/apps/unoverse/engine/migrations \
86
+ --migration-file-language sql \
87
+ --no-lock
88
+ args:
89
+ chdir: /opt/gravity
90
+ register: migrate_result
91
+ ignore_errors: yes
92
+
93
+ - name: "[4/5] Migration output"
94
+ debug:
95
+ msg: "{{ migrate_result.stdout_lines | default(['No output']) }}"
96
+
97
+ - name: "[5/5] Verify database connectivity"
98
+ uri:
99
+ url: "http://localhost:4101/health"
100
+ status_code: 200
101
+ register: db_check
102
+ retries: 5
103
+ delay: 2
104
+ until: db_check.status == 200
105
+ ignore_errors: yes
106
+
107
+ post_tasks:
108
+ - name: "=== DATABASE MIGRATION SUMMARY ==="
109
+ debug:
110
+ msg: |
111
+ ============================================
112
+ DATABASE MIGRATION
113
+ ============================================
114
+ Host: {{ inventory_hostname }} ({{ ansible_host }})
115
+ Extensions: {{ 'OK' if ext_result.rc == 0 else 'FAILED — enable in DB provider dashboard' }}
116
+ Migration: {{ 'OK' if migrate_result.rc == 0 else 'FAILED — check logs above' }}
117
+
118
+ If migration failed, check:
119
+ - Extensions enabled in DB provider (vector, pg_stat_statements)
120
+ - DATABASE_URL in /opt/gravity/.env
121
+ - Database is accessible from VM
122
+ - See docs/runbooks/02-database.md for setup instructions
123
+ ============================================
@@ -0,0 +1,71 @@
1
+ ---
2
+ # DEPLOY IMAGES — the images half of the platform deploy.
3
+ #
4
+ # Pull the latest DOCR images and restart. That's the whole deployment:
5
+ # - platform code (incl. plugin-base, baked in the unoverse image) = the images
6
+ # - marketplace nodes CONVERGE at boot (keep-latest: the shared installed_plugins
7
+ # row is "the newest version anyone chose"; boot reinstalls anything missing
8
+ # or older — see runtime/plugins/startup.ts)
9
+ # - component nodes synthesize from rx/ definitions at boot (no build, ever)
10
+ #
11
+ # The CLI's `unoverse deploy` runs this first, then deploy-packages.yml (the
12
+ # local-source half). Use directly only when images alone changed.
13
+ #
14
+ # Usage (via CLI — recommended):
15
+ # unoverse deploy
16
+ #
17
+ # Usage (direct ansible):
18
+ # ansible-playbook -i inventory playbooks/deploy-a.yml
19
+
20
+ - name: Deploy platform images
21
+ hosts: all
22
+ become: yes
23
+
24
+ vars:
25
+ gravity_dir: /opt/gravity
26
+
27
+ tasks:
28
+ # Sync the compose FIRST so config changes (env vars, ports, new services) ship with
29
+ # a normal `deploy` — not only via `deploy init`. Previously deploy just re-ran the
30
+ # server's existing compose, so a new env var (e.g. MEMORY_SERVICE_URL) silently never
31
+ # reached prod even though the image had the code. `.env` (per-server values) is left
32
+ # untouched — only the canonical compose is refreshed from the repo.
33
+ - name: "[1/4] Sync docker-compose.yml (config travels with the deploy)"
34
+ copy:
35
+ src: "{{ playbook_dir }}/../../docker-compose.yml"
36
+ dest: "{{ gravity_dir }}/docker-compose.yml"
37
+
38
+ - name: "[2/4] Pull latest platform images"
39
+ shell: |
40
+ cd {{ gravity_dir }}
41
+ docker compose pull 2>&1 | tail -8
42
+ register: pull_result
43
+
44
+ - name: "[2/4] Pull output"
45
+ debug:
46
+ msg: "{{ pull_result.stdout_lines[-8:] | default(['done']) }}"
47
+
48
+ - name: "[3/4] Restart on the new images (marketplace converges at boot)"
49
+ shell: |
50
+ cd {{ gravity_dir }}
51
+ docker compose up -d 2>&1 | tail -6
52
+ register: up_result
53
+
54
+ - name: "[4/4] Service status"
55
+ shell: |
56
+ cd {{ gravity_dir }}
57
+ docker compose ps --format '{{ '{{' }}.Name{{ '}}' }}: {{ '{{' }}.Status{{ '}}' }}' 2>&1
58
+ register: status_result
59
+
60
+ - name: "=== IMAGES DEPLOYED ==="
61
+ debug:
62
+ msg: |
63
+ ============================================
64
+ PLATFORM IMAGES DEPLOYED
65
+ ============================================
66
+ Host: {{ inventory_hostname }} ({{ ansible_host }})
67
+ {{ status_result.stdout }}
68
+ Marketplace nodes converge to the recorded
69
+ versions at boot; component nodes synthesize
70
+ from rx/ definitions.
71
+ ============================================
@@ -0,0 +1,223 @@
1
+ ---
2
+ # Gravity Platform VM Hardening Playbook
3
+ # Enterprise security hardening for production VMs
4
+
5
+ - name: Harden VM for Enterprise Deployment
6
+ hosts: all
7
+ become: yes
8
+
9
+ vars:
10
+ # SSH hardening.
11
+ # PermitRootLogin MUST be prohibit-password, never "no": unoverse deploy and
12
+ # unoverse update SSH in as root (DEPLOY_USER=root on DO). "no" locks every
13
+ # future deploy out of the box after the first harden run; prohibit-password
14
+ # keeps key-based root working and blocks only password logins — which is
15
+ # the attack fail2ban and this hardening actually defend against.
16
+ ssh_port: 22
17
+ ssh_permit_root_login: "prohibit-password"
18
+ ssh_password_authentication: "no"
19
+
20
+ # NO firewall vars here: the firewall is the ground's (cloud firewall / SGs,
21
+ # owned by infra/ Terraform). This playbook must never touch app ports —
22
+ # 4105 (API/MCP: Canvas, Studio, Claude, ChatGPT) and 3001 stay governed by
23
+ # the load balancer and admin_cidr, so hardening can't cut clients off.
24
+
25
+ tasks:
26
+ # =========================================================================
27
+ # SYSTEM UPDATES
28
+ # =========================================================================
29
+ - name: Update all packages (Debian/Ubuntu)
30
+ apt:
31
+ upgrade: safe
32
+ update_cache: yes
33
+ cache_valid_time: 3600
34
+ when: ansible_facts['os_family'] == "Debian"
35
+
36
+ - name: Update all packages (RHEL/Amazon Linux)
37
+ dnf:
38
+ name: "*"
39
+ state: latest
40
+ security: yes
41
+ when: ansible_facts['os_family'] == "RedHat"
42
+
43
+ - name: Install security packages (Debian/Ubuntu)
44
+ apt:
45
+ name:
46
+ - fail2ban
47
+ - unattended-upgrades
48
+ - logrotate
49
+ state: present
50
+ when: ansible_facts['os_family'] == "Debian"
51
+
52
+ - name: Install security packages (RHEL/Amazon Linux)
53
+ dnf:
54
+ name:
55
+ - fail2ban
56
+ - dnf-automatic
57
+ - logrotate
58
+ - audit
59
+ state: present
60
+ when: ansible_facts['os_family'] == "RedHat"
61
+
62
+ # =========================================================================
63
+ # SSH HARDENING
64
+ # =========================================================================
65
+ - name: Configure SSH - Disable root login
66
+ lineinfile:
67
+ path: /etc/ssh/sshd_config
68
+ regexp: "^#?PermitRootLogin"
69
+ line: "PermitRootLogin {{ ssh_permit_root_login }}"
70
+ notify: Restart SSH
71
+
72
+ - name: Configure SSH - Disable password authentication
73
+ lineinfile:
74
+ path: /etc/ssh/sshd_config
75
+ regexp: "^#?PasswordAuthentication"
76
+ line: "PasswordAuthentication {{ ssh_password_authentication }}"
77
+ notify: Restart SSH
78
+
79
+ - name: Configure SSH - Use only SSH protocol 2
80
+ lineinfile:
81
+ path: /etc/ssh/sshd_config
82
+ regexp: "^#?Protocol"
83
+ line: "Protocol 2"
84
+ notify: Restart SSH
85
+
86
+ - name: Configure SSH - Set login grace time
87
+ lineinfile:
88
+ path: /etc/ssh/sshd_config
89
+ regexp: "^#?LoginGraceTime"
90
+ line: "LoginGraceTime 60"
91
+ notify: Restart SSH
92
+
93
+ - name: Configure SSH - Limit max auth tries
94
+ lineinfile:
95
+ path: /etc/ssh/sshd_config
96
+ regexp: "^#?MaxAuthTries"
97
+ line: "MaxAuthTries 3"
98
+ notify: Restart SSH
99
+
100
+ # =========================================================================
101
+ # FIREWALL: NONE HERE (moved to the ground, 2026-07-28)
102
+ # =========================================================================
103
+ # The cloud firewall (DO cloud firewall / AWS security groups, owned by
104
+ # infra/ Terraform) is the boundary. Host ufw/firewalld was removed: Docker's
105
+ # iptables rules BYPASS ufw for published ports, so it protected nothing —
106
+ # the ground firewall structurally can't be bypassed (traffic never reaches
107
+ # the box). On-prem: firewall at the customer's network layer, not here.
108
+
109
+ - name: Configure fail2ban for SSH
110
+ copy:
111
+ dest: /etc/fail2ban/jail.local
112
+ content: |
113
+ [DEFAULT]
114
+ bantime = 3600
115
+ findtime = 600
116
+ maxretry = 3
117
+
118
+ [sshd]
119
+ enabled = true
120
+ port = {{ ssh_port }}
121
+ filter = sshd
122
+ logpath = /var/log/auth.log
123
+ maxretry = 3
124
+ notify: Restart fail2ban
125
+
126
+ # =========================================================================
127
+ # AUTOMATIC SECURITY UPDATES
128
+ # =========================================================================
129
+ - name: Enable automatic security updates (Debian/Ubuntu)
130
+ copy:
131
+ dest: /etc/apt/apt.conf.d/20auto-upgrades
132
+ content: |
133
+ APT::Periodic::Update-Package-Lists "1";
134
+ APT::Periodic::Unattended-Upgrade "1";
135
+ APT::Periodic::AutocleanInterval "7";
136
+ when: ansible_facts['os_family'] == "Debian"
137
+
138
+ - name: Enable automatic security updates (RHEL/Amazon Linux)
139
+ service:
140
+ name: dnf-automatic.timer
141
+ state: started
142
+ enabled: yes
143
+ when: ansible_facts['os_family'] == "RedHat"
144
+
145
+ # =========================================================================
146
+ # SYSTEM LIMITS
147
+ # =========================================================================
148
+ - name: Set file descriptor limits
149
+ copy:
150
+ dest: /etc/security/limits.d/gravity.conf
151
+ content: |
152
+ # Gravity Platform limits
153
+ * soft nofile 65535
154
+ * hard nofile 65535
155
+ root soft nofile 65535
156
+ root hard nofile 65535
157
+
158
+ # =========================================================================
159
+ # DOCKER HARDENING
160
+ # =========================================================================
161
+ - name: Create Docker daemon config
162
+ copy:
163
+ dest: /etc/docker/daemon.json
164
+ content: |
165
+ {
166
+ "log-driver": "json-file",
167
+ "log-opts": {
168
+ "max-size": "100m",
169
+ "max-file": "3"
170
+ },
171
+ "live-restore": true,
172
+ "userland-proxy": false
173
+ }
174
+ notify: Restart Docker
175
+
176
+ # =========================================================================
177
+ # AUDIT LOGGING
178
+ # =========================================================================
179
+ - name: Install auditd (Debian/Ubuntu)
180
+ apt:
181
+ name: auditd
182
+ state: present
183
+ when: ansible_facts['os_family'] == "Debian"
184
+
185
+ - name: Ensure auditd running (RHEL/Amazon Linux)
186
+ service:
187
+ name: auditd
188
+ state: started
189
+ enabled: yes
190
+ when: ansible_facts['os_family'] == "RedHat"
191
+
192
+ - name: Configure audit rules for Docker
193
+ copy:
194
+ dest: /etc/audit/rules.d/docker.rules
195
+ content: |
196
+ # Docker daemon
197
+ -w /usr/bin/docker -p wa -k docker
198
+ -w /var/lib/docker -p wa -k docker
199
+ -w /etc/docker -p wa -k docker
200
+ -w /lib/systemd/system/docker.service -p wa -k docker
201
+ -w /etc/docker/daemon.json -p wa -k docker
202
+ notify: Restart auditd
203
+
204
+ handlers:
205
+ - name: Restart SSH
206
+ service:
207
+ name: ssh
208
+ state: restarted
209
+
210
+ - name: Restart fail2ban
211
+ service:
212
+ name: fail2ban
213
+ state: restarted
214
+
215
+ - name: Restart Docker
216
+ service:
217
+ name: docker
218
+ state: restarted
219
+
220
+ - name: Restart auditd
221
+ service:
222
+ name: auditd
223
+ state: restarted