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/ 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 { fromError } from 'zod-validation-error';
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import YAML from 'yaml'
const getApps = async () => { const getApps = async () => {
const appsDir = await fs.promises.readdir(path.join(process.cwd(), 'apps')) 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() const apps = await getApps()
for (const app of apps) { 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) { for (const file of files) {
test(`app ${app} should have ${file}`, async () => { test(`app ${app} should have ${file}`, async () => {
@@ -37,6 +38,12 @@ describe("each app should have the required files", async () => {
expect(fileContent).not.toBeNull() 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() const apps = await getApps()
for (const app of apps) { for (const app of apps) {
test(`app ${app} should have a valid docker-compose.json`, async () => { test(`app ${app} should have a valid compose file`, async () => {
const fileContent = await getFile(app, 'docker-compose.json') const legacyCompose = await getFile(app, 'docker-compose.json')
const parsed = dynamicComposeSchema.safeParse(JSON.parse(fileContent || '{}')) 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) { if (!parsed.success) {
const validationError = fromError(parsed.error); 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) expect(parsed.success).toBe(true)
BIN
View File
Binary file not shown.
+16 -58
View File
@@ -7,64 +7,22 @@
"hostname": "mediamtx", "hostname": "mediamtx",
"isMain": true, "isMain": true,
"internalPort": "8889", "internalPort": "8889",
"environment": [ "environment": {
{ "MTX_PROTOCOLS": "tcp",
"key": "MTX_PROTOCOLS", "MTX_LOGLEVEL": "info",
"value": "tcp" "MTX_LOGDESTINATIONS": "stdout",
}, "MTX_RTSPADDRESS": ":8554",
{ "MTX_RTMPADDRESS": ":1935",
"key": "MTX_LOGLEVEL", "MTX_HLSADDRESS": ":8888",
"value": "info" "MTX_WEBRTCADDRESS": ":8889",
}, "MTX_SRTADDRESS": ":8890",
{ "MTX_APIADDRESS": ":9997",
"key": "MTX_LOGDESTINATIONS", "MTX_METRICSADDRESS": ":9998",
"value": "stdout" "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",
"key": "MTX_RTSPADDRESS", "MTX_PATHDEFAULTS_RECORDFORMAT": "fmp4"
"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"
}
],
"addPorts": [ "addPorts": [
{ {
"containerPort": 8554, "containerPort": 8554,
@@ -6,7 +6,7 @@
"no_gui": true, "no_gui": true,
"dynamic_config": true, "dynamic_config": true,
"port": 8080, "port": 8080,
"tipi_version": 1, "tipi_version": 2,
"min_tipi_version": "4.7.0", "min_tipi_version": "4.7.0",
"version": "1.3.0", "version": "1.3.0",
"author": "n8n", "author": "n8n",
@@ -18,11 +18,11 @@
"supported_architectures": ["amd64", "arm64"], "supported_architectures": ["amd64", "arm64"],
"form_fields": [ "form_fields": [
{ {
"type": "random", "type": "password",
"encoding": "hex",
"min": 48, "min": 48,
"label": "Cle API sandbox (SANDBOX_API_KEYS)", "max": 128,
"hint": "A recopier dans l'app n8n : N8N_SANDBOX_SERVICE_API_KEY. Plusieurs cles possibles, separees par des virgules.", "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, "required": true,
"env_variable": "SANDBOX_API_KEYS" "env_variable": "SANDBOX_API_KEYS"
}, },
@@ -115,11 +115,10 @@ services:
SANDBOX_RUNNER_REGISTRATION_TOKEN: "${SANDBOX_REGISTRATION_TOKEN}" SANDBOX_RUNNER_REGISTRATION_TOKEN: "${SANDBOX_REGISTRATION_TOKEN}"
SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090 SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090
# http:// et non https:// : le mTLS ne couvre que le gRPC # Le listener HTTP du runner sert obligatoirement TLS avec le certificat
# (enregistrement + SandboxControl). Le trafic proxy exec/files de # SandboxControl. Son SAN sandbox-runner-1 est genere par sandbox-certs.
# l'API vers le runner reste en HTTP clair authentifie par X-Api-Key # HTTP est refuse depuis la version 1.3.0 pour ne pas exposer X-Api-Key.
# (docs/configuration.md du depot et compose officiel n8n). SANDBOX_RUNNER_HTTP_BASE_URL: https://sandbox-runner-1:8080
SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080
SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ":9091" SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ":9091"
SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091 SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091
@@ -148,8 +147,9 @@ services:
healthcheck: healthcheck:
# /readyz passe au vert une fois le runner enregistre aupres de l'API # /readyz passe au vert une fois le runner enregistre aupres de l'API
# (meme check que le compose du depot upstream). # Le probe local ignore uniquement la verification du certificat ; le
test: "wget -qO- http://localhost:8080/readyz" # trafic API -> runner reste verifie avec la CA et le SAN partages.
test: "wget -qO- --no-check-certificate https://localhost:8080/readyz"
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 24 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 ## 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 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_PROVIDER=n8n-sandbox
N8N_INSTANCE_AI_SANDBOX_IMAGE=n8nio/n8n-sandbox-service-sandbox:1.3.0 N8N_INSTANCE_AI_SANDBOX_IMAGE=n8nio/n8n-sandbox-service-sandbox:1.3.0
N8N_SANDBOX_SERVICE_URL=http://sandbox-api:8080 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 : 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 "readOnly": true
} }
], ],
"environment": [ "environment": {
{ "NGINX_HOST": "${NGINX_SERVER_NAME:-localhost}",
"key": "NGINX_HOST", "TZ": "${TZ:-Europe/Paris}"
"value": "${NGINX_SERVER_NAME:-localhost}" },
},
{
"key": "TZ",
"value": "${TZ:-Europe/Paris}"
}
],
"healthCheck": { "healthCheck": {
"test": "curl --fail http://localhost:80 || exit 1", "test": "curl --fail http://localhost:80 || exit 1",
"interval": "30s", "interval": "30s",
+47 -161
View File
@@ -9,12 +9,9 @@
"sh", "sh",
"/scripts/init-certs.sh" "/scripts/init-certs.sh"
], ],
"environment": [ "environment": {
{ "CERT_TOOL_VERSION": "4.14"
"key": "CERT_TOOL_VERSION", },
"value": "4.14"
}
],
"volumes": [ "volumes": [
{ {
"hostPath": "${APP_DATA_DIR}/data/config", "hostPath": "${APP_DATA_DIR}/data/config",
@@ -74,48 +71,18 @@
"condition": "service_healthy" "condition": "service_healthy"
} }
}, },
"environment": [ "environment": {
{ "OPENSEARCH_JAVA_OPTS": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true",
"key": "OPENSEARCH_JAVA_OPTS", "DISABLE_INSTALL_DEMO_CONFIG": "true",
"value": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true" "bootstrap.memory_lock": "true",
}, "network.host": "wazuh.indexer",
{ "node.name": "wazuh.indexer",
"key": "DISABLE_INSTALL_DEMO_CONFIG", "cluster.initial_cluster_manager_nodes": "wazuh.indexer",
"value": "true" "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",
"key": "bootstrap.memory_lock", "plugins.security.ssl.http.clientauth_mode": "OPTIONAL"
"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"
}
],
"ulimits": { "ulimits": {
"memlock": { "memlock": {
"soft": -1, "soft": -1,
@@ -209,64 +176,22 @@
"condition": "service_healthy" "condition": "service_healthy"
} }
}, },
"environment": [ "environment": {
{ "WAZUH_INDEXER_HOSTS": "wazuh.indexer:9200",
"key": "WAZUH_INDEXER_HOSTS", "WAZUH_NODE_NAME": "manager",
"value": "wazuh.indexer:9200" "WAZUH_NODE_TYPE": "master",
}, "WAZUH_CLUSTER_NODES": "wazuh.manager",
{ "WAZUH_CLUSTER_BIND_ADDR": "wazuh.manager",
"key": "WAZUH_NODE_NAME", "INDEXER_URL": "https://wazuh.indexer:9200",
"value": "manager" "INDEXER_USERNAME": "${INDEXER_USERNAME:-admin}",
}, "INDEXER_PASSWORD": "${INDEXER_PASSWORD:-admin}",
{ "FILEBEAT_SSL_VERIFICATION_MODE": "full",
"key": "WAZUH_NODE_TYPE", "SSL_CERTIFICATE_AUTHORITIES": "/var/ossec/etc/certs/root-ca.pem",
"value": "master" "SSL_CERTIFICATE": "/var/ossec/etc/certs/server.pem",
}, "SSL_KEY": "/var/ossec/etc/certs/server-key.pem",
{ "API_USERNAME": "wazuh-wui",
"key": "WAZUH_CLUSTER_NODES", "API_PASSWORD": "${API_PASSWORD:-MyS3cr37P450r.*-}"
"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.*-}"
}
],
"ulimits": { "ulimits": {
"memlock": { "memlock": {
"soft": -1, "soft": -1,
@@ -368,60 +293,21 @@
"condition": "service_healthy" "condition": "service_healthy"
} }
}, },
"environment": [ "environment": {
{ "SERVER_HOST": "0.0.0.0",
"key": "SERVER_HOST", "OPENSEARCH_HOSTS": "https://wazuh.indexer:9200",
"value": "0.0.0.0" "SERVER_SSL_ENABLED": "true",
}, "INDEXER_USERNAME": "${INDEXER_USERNAME:-admin}",
{ "INDEXER_PASSWORD": "${INDEXER_PASSWORD:-admin}",
"key": "OPENSEARCH_HOSTS", "WAZUH_API_URL": "https://wazuh.manager",
"value": "https://wazuh.indexer:9200" "DASHBOARD_USERNAME": "${DASHBOARD_USERNAME:-kibanaserver}",
}, "DASHBOARD_PASSWORD": "${DASHBOARD_PASSWORD:-kibanaserver}",
{ "API_USERNAME": "wazuh-wui",
"key": "SERVER_SSL_ENABLED", "API_PASSWORD": "${API_PASSWORD:-MyS3cr37P450r.*-}",
"value": "true" "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"
"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"
}
],
"volumes": [ "volumes": [
{ {
"hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs", "hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs",
@@ -462,4 +348,4 @@
} }
} }
] ]
} }
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -12,7 +12,8 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/node": "^22.14.1" "@types/node": "^22.14.1",
"yaml": "2"
}, },
"dependencies": { "dependencies": {
"@runtipi/common": "^0.8.0", "@runtipi/common": "^0.8.0",