Author SHA1 Message Date
Hermes Agent cf81405a3f fix(n8n-sandbox): publish Runtipi update version
Test / test (pull_request) Canceled after 0s
2026-09-02 18:04:57 +02:00
Hermes Agent 95dd642243 feat(n8n-sandbox): expose shared API key in install form
Test / test (pull_request) Canceled after 0s
2026-09-02 17:58:07 +02:00
Hermes Agent 7df02cccdf fix(n8n-sandbox): secure runner HTTP endpoint 2026-09-02 17:46:56 +02:00
Guillaume 05e1056a36 chore(apps): remove macOS metadata (#2)
Test / test (push) Canceled after 0s
Remove tracked apps metadata and prevent it from returning.
2026-09-02 15:34:20 +00:00
Hermes Agent e91b993cad chore(apps): remove macOS metadata
Test / test (pull_request) Canceled after 0s
2026-09-02 17:32:07 +02:00
Guillaume cab772403d fix(apps): place n8n sandbox in app directory (#1)
Test / test (push) Canceled after 0s
Preserve the modern Runtipi YAML compose format and validate both modern and legacy app definitions.
2026-09-02 15:31:38 +00:00
Hermes Agent 0ded9c3a4d fix(apps): preserve modern n8n compose format
Test / test (pull_request) Canceled after 0s
2026-09-02 17:27:56 +02:00
Hermes Agent d24d0c194b fix(apps): normalize environment mappings
Test / test (pull_request) Canceled after 0s
2026-09-02 17:23:48 +02:00
Hermes Agent 8f3b7b3b2b fix(apps): place n8n sandbox in app directory
Test / test (pull_request) Canceled after 0s
2026-09-02 17:22:03 +02:00
12 changed files with 134 additions and 250 deletions
+1
View File
@@ -1 +1,2 @@
node_modules/
/apps/.DS_Store
+49 -6
View File
@@ -3,6 +3,7 @@ import { appInfoSchema, dynamicComposeSchema } from '@runtipi/common/schemas'
import { fromError } from 'zod-validation-error';
import fs from 'node:fs'
import path from 'node:path'
import YAML from 'yaml'
const getApps = async () => {
const appsDir = await fs.promises.readdir(path.join(process.cwd(), 'apps'))
@@ -29,7 +30,7 @@ describe("each app should have the required files", async () => {
const apps = await getApps()
for (const app of apps) {
const files = ['config.json', 'docker-compose.json', 'metadata/logo.jpg', 'metadata/description.md']
const files = ['config.json', 'metadata/logo.jpg', 'metadata/description.md']
for (const file of files) {
test(`app ${app} should have ${file}`, async () => {
@@ -37,6 +38,12 @@ describe("each app should have the required files", async () => {
expect(fileContent).not.toBeNull()
})
}
test(`app ${app} should have a compose file`, async () => {
const legacyCompose = await getFile(app, 'docker-compose.json')
const modernCompose = await getFile(app, 'docker-compose.yml')
expect(legacyCompose || modernCompose).not.toBeNull()
})
}
})
@@ -58,17 +65,53 @@ describe("each app should have a valid config.json", async () => {
}
})
describe("each app should have a valid docker-compose.json", async () => {
describe("n8n-sandbox installation secrets", () => {
test("exposes the shared API key as a password field", async () => {
const fileContent = await getFile('n8n-sandbox', 'config.json')
const config = JSON.parse(fileContent || '{}')
const apiKey = config.form_fields?.find((field: { env_variable?: string }) => field.env_variable === 'SANDBOX_API_KEYS')
expect(apiKey?.type).toBe('password')
expect(apiKey?.required).toBe(true)
expect(apiKey?.min).toBeGreaterThanOrEqual(48)
expect(config.tipi_version).toBeGreaterThanOrEqual(2)
})
})
describe("modern compose files preserve runtime semantics", () => {
test("n8n-sandbox keeps its one-shot certificate service", async () => {
const fileContent = await getFile('n8n-sandbox', 'docker-compose.yml')
expect(fileContent).not.toBeNull()
const parsed = YAML.parse(fileContent || '')
expect(parsed['x-runtipi']?.schema_version).toBe(2)
expect(parsed.services?.['sandbox-certs']?.restart).toBe('no')
expect(parsed.services?.['sandbox-api']?.['x-runtipi']?.is_main).toBe(true)
expect(parsed.services?.['sandbox-runner-1']?.environment?.SANDBOX_RUNNER_HTTP_BASE_URL).toBe('https://sandbox-runner-1:8080')
expect(parsed.services?.['sandbox-runner-1']?.healthcheck?.test).toContain('https://localhost:8080/readyz')
})
})
describe("each app should have a valid compose file", async () => {
const apps = await getApps()
for (const app of apps) {
test(`app ${app} should have a valid docker-compose.json`, async () => {
const fileContent = await getFile(app, 'docker-compose.json')
const parsed = dynamicComposeSchema.safeParse(JSON.parse(fileContent || '{}'))
test(`app ${app} should have a valid compose file`, async () => {
const legacyCompose = await getFile(app, 'docker-compose.json')
const modernCompose = await getFile(app, 'docker-compose.yml')
if (modernCompose) {
const parsed = YAML.parse(modernCompose)
expect(parsed['x-runtipi']?.schema_version).toBeTypeOf('number')
expect(parsed.services).toBeTypeOf('object')
return
}
const parsed = dynamicComposeSchema.safeParse(JSON.parse(legacyCompose || '{}'))
if (!parsed.success) {
const validationError = fromError(parsed.error);
console.error(`Error parsing docker-compose.json for app ${app}:`, validationError.toString());
console.error(`Error parsing compose file for app ${app}:`, validationError.toString());
}
expect(parsed.success).toBe(true)
BIN
View File
Binary file not shown.
+16 -58
View File
@@ -7,64 +7,22 @@
"hostname": "mediamtx",
"isMain": true,
"internalPort": "8889",
"environment": [
{
"key": "MTX_PROTOCOLS",
"value": "tcp"
},
{
"key": "MTX_LOGLEVEL",
"value": "info"
},
{
"key": "MTX_LOGDESTINATIONS",
"value": "stdout"
},
{
"key": "MTX_RTSPADDRESS",
"value": ":8554"
},
{
"key": "MTX_RTMPADDRESS",
"value": ":1935"
},
{
"key": "MTX_HLSADDRESS",
"value": ":8888"
},
{
"key": "MTX_WEBRTCADDRESS",
"value": ":8889"
},
{
"key": "MTX_SRTADDRESS",
"value": ":8890"
},
{
"key": "MTX_APIADDRESS",
"value": ":9997"
},
{
"key": "MTX_METRICSADDRESS",
"value": ":9998"
},
{
"key": "MTX_AUTHINTERNALUSERS",
"value": "${MTX_API_USERNAME:+${MTX_API_USERNAME}:${MTX_API_PASSWORD}}"
},
{
"key": "MTX_PATHDEFAULTS_RECORD",
"value": "${MTX_RECORD_ENABLED:-false}"
},
{
"key": "MTX_PATHDEFAULTS_RECORDPATH",
"value": "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f"
},
{
"key": "MTX_PATHDEFAULTS_RECORDFORMAT",
"value": "fmp4"
}
],
"environment": {
"MTX_PROTOCOLS": "tcp",
"MTX_LOGLEVEL": "info",
"MTX_LOGDESTINATIONS": "stdout",
"MTX_RTSPADDRESS": ":8554",
"MTX_RTMPADDRESS": ":1935",
"MTX_HLSADDRESS": ":8888",
"MTX_WEBRTCADDRESS": ":8889",
"MTX_SRTADDRESS": ":8890",
"MTX_APIADDRESS": ":9997",
"MTX_METRICSADDRESS": ":9998",
"MTX_AUTHINTERNALUSERS": "${MTX_API_USERNAME:+${MTX_API_USERNAME}:${MTX_API_PASSWORD}}",
"MTX_PATHDEFAULTS_RECORD": "${MTX_RECORD_ENABLED:-false}",
"MTX_PATHDEFAULTS_RECORDPATH": "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
"MTX_PATHDEFAULTS_RECORDFORMAT": "fmp4"
},
"addPorts": [
{
"containerPort": 8554,
@@ -6,7 +6,7 @@
"no_gui": true,
"dynamic_config": true,
"port": 8080,
"tipi_version": 1,
"tipi_version": 2,
"min_tipi_version": "4.7.0",
"version": "1.3.0",
"author": "n8n",
@@ -18,11 +18,11 @@
"supported_architectures": ["amd64", "arm64"],
"form_fields": [
{
"type": "random",
"encoding": "hex",
"type": "password",
"min": 48,
"label": "Cle API sandbox (SANDBOX_API_KEYS)",
"hint": "A recopier dans l'app n8n : N8N_SANDBOX_SERVICE_API_KEY. Plusieurs cles possibles, separees par des virgules.",
"max": 128,
"label": "Clé API partagée avec n8n (SANDBOX_API_KEYS)",
"hint": "Choisir une clé forte, puis recopier cette même valeur dans N8N_SANDBOX_SERVICE_API_KEY dans les paramètres de l'app n8n officielle.",
"required": true,
"env_variable": "SANDBOX_API_KEYS"
},
@@ -115,11 +115,10 @@ services:
SANDBOX_RUNNER_REGISTRATION_TOKEN: "${SANDBOX_REGISTRATION_TOKEN}"
SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090
# http:// et non https:// : le mTLS ne couvre que le gRPC
# (enregistrement + SandboxControl). Le trafic proxy exec/files de
# l'API vers le runner reste en HTTP clair authentifie par X-Api-Key
# (docs/configuration.md du depot et compose officiel n8n).
SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080
# Le listener HTTP du runner sert obligatoirement TLS avec le certificat
# SandboxControl. Son SAN sandbox-runner-1 est genere par sandbox-certs.
# HTTP est refuse depuis la version 1.3.0 pour ne pas exposer X-Api-Key.
SANDBOX_RUNNER_HTTP_BASE_URL: https://sandbox-runner-1:8080
SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ":9091"
SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091
@@ -148,8 +147,9 @@ services:
healthcheck:
# /readyz passe au vert une fois le runner enregistre aupres de l'API
# (meme check que le compose du depot upstream).
test: "wget -qO- http://localhost:8080/readyz"
# Le probe local ignore uniquement la verification du certificat ; le
# trafic API -> runner reste verifie avec la CA et le SAN partages.
test: "wget -qO- --no-check-certificate https://localhost:8080/readyz"
interval: 5s
timeout: 5s
retries: 24
@@ -10,7 +10,7 @@ Sandbox auto-hébergé pour l'**AI Assistant** de n8n (setup « Self-host the sa
## Après l'installation
Dans l'app **n8n** (paramètres ou `app.env`), ajouter :
Lors de l'installation, choisir une clé forte dans le champ **Clé API partagée avec n8n**. Dans l'app officielle **n8n** (paramètres ou `app.env`), ajouter le bloc suivant en recopiant cette même clé :
```
N8N_ENABLED_MODULES=instance-ai
@@ -18,7 +18,8 @@ N8N_INSTANCE_AI_SANDBOX_ENABLED=true
N8N_INSTANCE_AI_SANDBOX_PROVIDER=n8n-sandbox
N8N_INSTANCE_AI_SANDBOX_IMAGE=n8nio/n8n-sandbox-service-sandbox:1.3.0
N8N_SANDBOX_SERVICE_URL=http://sandbox-api:8080
N8N_SANDBOX_SERVICE_API_KEY=<valeur du champ « Clé API sandbox »>
N8N_SANDBOX_SERVICE_API_KEY=<même valeur que « Clé API partagée avec n8n »>
N8N_PROXY_HOPS=1
```
Puis redémarrer n8n et vérifier depuis son conteneur :

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+4 -10
View File
@@ -29,16 +29,10 @@
"readOnly": true
}
],
"environment": [
{
"key": "NGINX_HOST",
"value": "${NGINX_SERVER_NAME:-localhost}"
},
{
"key": "TZ",
"value": "${TZ:-Europe/Paris}"
}
],
"environment": {
"NGINX_HOST": "${NGINX_SERVER_NAME:-localhost}",
"TZ": "${TZ:-Europe/Paris}"
},
"healthCheck": {
"test": "curl --fail http://localhost:80 || exit 1",
"interval": "30s",
+46 -160
View File
@@ -9,12 +9,9 @@
"sh",
"/scripts/init-certs.sh"
],
"environment": [
{
"key": "CERT_TOOL_VERSION",
"value": "4.14"
}
],
"environment": {
"CERT_TOOL_VERSION": "4.14"
},
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/config",
@@ -74,48 +71,18 @@
"condition": "service_healthy"
}
},
"environment": [
{
"key": "OPENSEARCH_JAVA_OPTS",
"value": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true"
},
{
"key": "DISABLE_INSTALL_DEMO_CONFIG",
"value": "true"
},
{
"key": "bootstrap.memory_lock",
"value": "true"
},
{
"key": "network.host",
"value": "wazuh.indexer"
},
{
"key": "node.name",
"value": "wazuh.indexer"
},
{
"key": "cluster.initial_cluster_manager_nodes",
"value": "wazuh.indexer"
},
{
"key": "node.max_local_storage_nodes",
"value": "1"
},
{
"key": "plugins.security.allow_default_init_securityindex",
"value": "true"
},
{
"key": "NODES_DN",
"value": "CN=wazuh.indexer,OU=Wazuh,O=Wazuh,L=California,C=US"
},
{
"key": "plugins.security.ssl.http.clientauth_mode",
"value": "OPTIONAL"
}
],
"environment": {
"OPENSEARCH_JAVA_OPTS": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true",
"DISABLE_INSTALL_DEMO_CONFIG": "true",
"bootstrap.memory_lock": "true",
"network.host": "wazuh.indexer",
"node.name": "wazuh.indexer",
"cluster.initial_cluster_manager_nodes": "wazuh.indexer",
"node.max_local_storage_nodes": "1",
"plugins.security.allow_default_init_securityindex": "true",
"NODES_DN": "CN=wazuh.indexer,OU=Wazuh,O=Wazuh,L=California,C=US",
"plugins.security.ssl.http.clientauth_mode": "OPTIONAL"
},
"ulimits": {
"memlock": {
"soft": -1,
@@ -209,64 +176,22 @@
"condition": "service_healthy"
}
},
"environment": [
{
"key": "WAZUH_INDEXER_HOSTS",
"value": "wazuh.indexer:9200"
},
{
"key": "WAZUH_NODE_NAME",
"value": "manager"
},
{
"key": "WAZUH_NODE_TYPE",
"value": "master"
},
{
"key": "WAZUH_CLUSTER_NODES",
"value": "wazuh.manager"
},
{
"key": "WAZUH_CLUSTER_BIND_ADDR",
"value": "wazuh.manager"
},
{
"key": "INDEXER_URL",
"value": "https://wazuh.indexer:9200"
},
{
"key": "INDEXER_USERNAME",
"value": "${INDEXER_USERNAME:-admin}"
},
{
"key": "INDEXER_PASSWORD",
"value": "${INDEXER_PASSWORD:-admin}"
},
{
"key": "FILEBEAT_SSL_VERIFICATION_MODE",
"value": "full"
},
{
"key": "SSL_CERTIFICATE_AUTHORITIES",
"value": "/var/ossec/etc/certs/root-ca.pem"
},
{
"key": "SSL_CERTIFICATE",
"value": "/var/ossec/etc/certs/server.pem"
},
{
"key": "SSL_KEY",
"value": "/var/ossec/etc/certs/server-key.pem"
},
{
"key": "API_USERNAME",
"value": "wazuh-wui"
},
{
"key": "API_PASSWORD",
"value": "${API_PASSWORD:-MyS3cr37P450r.*-}"
}
],
"environment": {
"WAZUH_INDEXER_HOSTS": "wazuh.indexer:9200",
"WAZUH_NODE_NAME": "manager",
"WAZUH_NODE_TYPE": "master",
"WAZUH_CLUSTER_NODES": "wazuh.manager",
"WAZUH_CLUSTER_BIND_ADDR": "wazuh.manager",
"INDEXER_URL": "https://wazuh.indexer:9200",
"INDEXER_USERNAME": "${INDEXER_USERNAME:-admin}",
"INDEXER_PASSWORD": "${INDEXER_PASSWORD:-admin}",
"FILEBEAT_SSL_VERIFICATION_MODE": "full",
"SSL_CERTIFICATE_AUTHORITIES": "/var/ossec/etc/certs/root-ca.pem",
"SSL_CERTIFICATE": "/var/ossec/etc/certs/server.pem",
"SSL_KEY": "/var/ossec/etc/certs/server-key.pem",
"API_USERNAME": "wazuh-wui",
"API_PASSWORD": "${API_PASSWORD:-MyS3cr37P450r.*-}"
},
"ulimits": {
"memlock": {
"soft": -1,
@@ -368,60 +293,21 @@
"condition": "service_healthy"
}
},
"environment": [
{
"key": "SERVER_HOST",
"value": "0.0.0.0"
},
{
"key": "OPENSEARCH_HOSTS",
"value": "https://wazuh.indexer:9200"
},
{
"key": "SERVER_SSL_ENABLED",
"value": "true"
},
{
"key": "INDEXER_USERNAME",
"value": "${INDEXER_USERNAME:-admin}"
},
{
"key": "INDEXER_PASSWORD",
"value": "${INDEXER_PASSWORD:-admin}"
},
{
"key": "WAZUH_API_URL",
"value": "https://wazuh.manager"
},
{
"key": "DASHBOARD_USERNAME",
"value": "${DASHBOARD_USERNAME:-kibanaserver}"
},
{
"key": "DASHBOARD_PASSWORD",
"value": "${DASHBOARD_PASSWORD:-kibanaserver}"
},
{
"key": "API_USERNAME",
"value": "wazuh-wui"
},
{
"key": "API_PASSWORD",
"value": "${API_PASSWORD:-MyS3cr37P450r.*-}"
},
{
"key": "SERVER_SSL_CERTIFICATE",
"value": "/usr/share/wazuh-dashboard/config/certs/dashboard.pem"
},
{
"key": "SERVER_SSL_KEY",
"value": "/usr/share/wazuh-dashboard/config/certs/dashboard-key.pem"
},
{
"key": "OPENSEARCH_SSL_CERTIFICATE_AUTHORITIES",
"value": "/usr/share/wazuh-dashboard/config/certs/root-ca.pem"
}
],
"environment": {
"SERVER_HOST": "0.0.0.0",
"OPENSEARCH_HOSTS": "https://wazuh.indexer:9200",
"SERVER_SSL_ENABLED": "true",
"INDEXER_USERNAME": "${INDEXER_USERNAME:-admin}",
"INDEXER_PASSWORD": "${INDEXER_PASSWORD:-admin}",
"WAZUH_API_URL": "https://wazuh.manager",
"DASHBOARD_USERNAME": "${DASHBOARD_USERNAME:-kibanaserver}",
"DASHBOARD_PASSWORD": "${DASHBOARD_PASSWORD:-kibanaserver}",
"API_USERNAME": "wazuh-wui",
"API_PASSWORD": "${API_PASSWORD:-MyS3cr37P450r.*-}",
"SERVER_SSL_CERTIFICATE": "/usr/share/wazuh-dashboard/config/certs/dashboard.pem",
"SERVER_SSL_KEY": "/usr/share/wazuh-dashboard/config/certs/dashboard-key.pem",
"OPENSEARCH_SSL_CERTIFICATE_AUTHORITIES": "/usr/share/wazuh-dashboard/config/certs/root-ca.pem"
},
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs",
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -12,7 +12,8 @@
"license": "ISC",
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^22.14.1"
"@types/node": "^22.14.1",
"yaml": "2"
},
"dependencies": {
"@runtipi/common": "^0.8.0",