diff --git a/.commitlintrc.json b/.commitlintrc.json index 011834eef..f15c14437 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -17,6 +17,7 @@ "pii", "proxy", "rag", + "sandbox", "storybook", "ui", "web", diff --git a/.env.test b/.env.test index a05c2c22f..5b9159002 100644 --- a/.env.test +++ b/.env.test @@ -44,3 +44,7 @@ POSTGRES_PASSWORD=test_password_e2e # Convex INSTANCE_SECRET=0000000000000000000000000000000000000000000000000000000000000000 INSTANCE_NAME=tale_platform + +# Sandbox spawner — fixed test-only HMAC token so the smoke script can sign +# /v1/execute. Production deploys auto-mint via the CLI's ensure-env helper. +SANDBOX_TOKEN=test-sandbox-token-do-not-use-in-production-deadbeefcafef00d diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d81dc1275..e7ea18961 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,6 +106,12 @@ jobs: - 'services/platform/**' - 'packages/ui/**' - 'packages/webui/**' + sandbox: + - 'services/sandbox/**' + sandbox-egress: + - 'services/sandbox-egress/**' + sandbox-runtime: + - 'services/sandbox-runtime/**' ci_tests: - 'tests/container-*' - 'compose.test.yml' @@ -118,6 +124,9 @@ jobs: - 'services/rag/**' - 'services/platform/**' - 'services/proxy/**' + - 'services/sandbox/**' + - 'services/sandbox-egress/**' + - 'services/sandbox-runtime/**' - name: Compute service matrix id: services @@ -127,10 +136,11 @@ jobs: echo "list=${SERVICES}" >> "$GITHUB_OUTPUT" echo "Services to build: ${SERVICES}" - # Vulnerability scan only covers the six compose-stack services that - # `build` actually pushes to GHCR. Web and docs use their own compose - # stacks and are reachable via security.yml's filesystem scan. - SCANNABLE=$(echo "${SERVICES}" | jq -c '[.[] | select(. == "db" or . == "convex" or . == "crawler" or . == "rag" or . == "platform" or . == "proxy")]') + # Vulnerability scan covers the compose-stack services + sandbox + # trio that `build` actually pushes to GHCR. Web and docs use their + # own compose stacks and are reachable via security.yml's + # filesystem scan. + SCANNABLE=$(echo "${SERVICES}" | jq -c '[.[] | select(. == "db" or . == "convex" or . == "crawler" or . == "rag" or . == "platform" or . == "proxy" or . == "sandbox" or . == "sandbox-egress" or . == "sandbox-runtime")]') echo "scannable=${SCANNABLE}" >> "$GITHUB_OUTPUT" echo "Services to scan: ${SCANNABLE}" @@ -180,14 +190,25 @@ jobs: matrix: # Compose-stack services. Keep in sync with build.yml (smoke/validate # pull loops) and cleanup-pr-images.yml matrix. - service: [db, convex, crawler, rag, platform, proxy] + service: + [ + db, + convex, + crawler, + rag, + platform, + proxy, + sandbox, + sandbox-egress, + sandbox-runtime, + ] steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Reclaim disk space - if: matrix.service == 'platform' || matrix.service == 'rag' || matrix.service == 'crawler' || matrix.service == 'convex' + if: matrix.service == 'platform' || matrix.service == 'rag' || matrix.service == 'crawler' || matrix.service == 'convex' || matrix.service == 'sandbox-runtime' run: | sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL sudo docker image prune -af @@ -316,16 +337,22 @@ jobs: # `docker compose build` step. - name: Pull images from GHCR run: | - # Compose-stack services. Keep in sync with build.yml (build matrix) - # and cleanup-pr-images.yml matrix. + # Compose-stack services + sandbox-runtime. Keep in sync with build.yml + # (build matrix) and cleanup-pr-images.yml matrix. sandbox-runtime is + # not a compose service but the spawner pulls it at boot — re-tag it + # locally so smoke tests with PULL_POLICY=never find it. TAG="${{ needs.changes.outputs.image_tag }}" REGISTRY_PATH="${{ env.REGISTRY }}/${{ github.repository }}" - for svc in db convex crawler rag platform proxy; do + for svc in db convex crawler rag platform proxy sandbox sandbox-egress sandbox-runtime; do IMAGE="${REGISTRY_PATH}/tale-${svc}:${TAG}" echo "Pulling ${IMAGE}..." docker pull "${IMAGE}" docker tag "${IMAGE}" "ghcr.io/tale-project/tale/tale-${svc}:latest" done + # See note in image-validate: the spawner's SANDBOX_RUNTIME_IMAGE + # defaults to the unscoped `tale-sandbox-runtime:latest`. + docker tag "ghcr.io/tale-project/tale/tale-sandbox-runtime:latest" \ + "tale-sandbox-runtime:latest" - name: Run smoke tests run: bash tests/container-smoke-test.sh @@ -511,16 +538,24 @@ jobs: - name: Pull images from GHCR run: | - # Compose-stack services. Keep in sync with build.yml (build matrix) - # and cleanup-pr-images.yml matrix. + # Compose-stack services + sandbox-runtime. Keep in sync with build.yml + # (build matrix) and cleanup-pr-images.yml matrix. sandbox-runtime is + # not a compose service but the spawner pulls it at boot — re-tag it + # locally so PULL_POLICY=never validation finds it. TAG="${{ needs.changes.outputs.image_tag }}" REGISTRY_PATH="${{ env.REGISTRY }}/${{ github.repository }}" - for svc in db convex crawler rag platform proxy; do + for svc in db convex crawler rag platform proxy sandbox sandbox-egress sandbox-runtime; do IMAGE="${REGISTRY_PATH}/tale-${svc}:${TAG}" echo "Pulling ${IMAGE}..." docker pull "${IMAGE}" docker tag "${IMAGE}" "ghcr.io/tale-project/tale/tale-${svc}:latest" done + # The spawner reads SANDBOX_RUNTIME_IMAGE which defaults to + # `tale-sandbox-runtime:latest` (unscoped). Mirror the tag so the + # spawner's boot-time `ensureImage` hits a local cache instead of + # trying to pull from GHCR. + docker tag "ghcr.io/tale-project/tale/tale-sandbox-runtime:latest" \ + "tale-sandbox-runtime:latest" - name: Run image validation run: bash tests/container-image-test.sh @@ -605,6 +640,7 @@ jobs: format: 'sarif' output: '${{ matrix.service }}-trivy.sarif' severity: 'HIGH,CRITICAL' + trivyignores: '.trivyignore.yaml' - name: Upload SARIF uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 diff --git a/.github/workflows/cleanup-pr-images.yml b/.github/workflows/cleanup-pr-images.yml index 19d4d97c9..98e0b7c91 100644 --- a/.github/workflows/cleanup-pr-images.yml +++ b/.github/workflows/cleanup-pr-images.yml @@ -28,7 +28,18 @@ jobs: matrix: # Compose-stack services. Keep in sync with build.yml (build matrix + # smoke/validate pull loops). - service: [db, convex, crawler, rag, platform, proxy] + service: + [ + db, + convex, + crawler, + rag, + platform, + proxy, + sandbox, + sandbox-egress, + sandbox-runtime, + ] steps: - name: Delete PR-tagged versions diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a13d57a62..2da014bcf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,6 +77,9 @@ jobs: - { name: convex } - { name: web } - { name: docs } + - { name: sandbox } + - { name: sandbox-egress } + - { name: sandbox-runtime } arch: - { name: amd64, runner: ubuntu-latest, platform: linux/amd64 } - { name: arm64, runner: ubuntu-24.04-arm, platform: linux/arm64 } @@ -86,7 +89,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Reclaim disk space - if: matrix.service.name == 'platform' || matrix.service.name == 'rag' || matrix.service.name == 'crawler' || matrix.service.name == 'convex' + if: matrix.service.name == 'platform' || matrix.service.name == 'rag' || matrix.service.name == 'crawler' || matrix.service.name == 'convex' || matrix.service.name == 'sandbox-runtime' run: | sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL sudo docker image prune -af @@ -156,7 +159,7 @@ jobs: run: | VERSION="${{ needs.prepare.outputs.version_number }}" ARCH="amd64" - for svc in platform rag crawler db proxy convex web docs; do + for svc in platform rag crawler db proxy convex web docs sandbox sandbox-egress sandbox-runtime; do IMAGE="${{ env.REGISTRY }}/${{ github.repository }}/tale-${svc}:${VERSION}-${ARCH}" echo "Pulling ${IMAGE}..." docker pull "${IMAGE}" @@ -207,7 +210,20 @@ jobs: strategy: matrix: - service: [platform, rag, crawler, db, proxy, convex, web, docs] + service: + [ + platform, + rag, + crawler, + db, + proxy, + convex, + web, + docs, + sandbox, + sandbox-egress, + sandbox-runtime, + ] steps: - name: Login to GHCR @@ -256,7 +272,7 @@ jobs: run: | VERSION="${{ needs.prepare.outputs.version_number }}" REGISTRY="${{ env.REGISTRY }}/${{ github.repository }}" - for svc in platform rag crawler db proxy convex web docs; do + for svc in platform rag crawler db proxy convex web docs sandbox sandbox-egress sandbox-runtime; do IMAGE="${REGISTRY}/tale-${svc}:${VERSION}" echo "Verifying manifest: ${IMAGE}" docker manifest inspect "${IMAGE}" > /dev/null 2>&1 || { @@ -310,13 +326,13 @@ jobs: run: | echo "## Release ${{ needs.prepare.outputs.version }} Complete" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "All 8 service images have been built, tested, and pushed to GHCR (native amd64 + arm64)." >> "$GITHUB_STEP_SUMMARY" + echo "All 11 service images have been built, tested, and pushed to GHCR (native amd64 + arm64)." >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "### Images" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "| Service | Image |" >> "$GITHUB_STEP_SUMMARY" echo "|---------|-------|" >> "$GITHUB_STEP_SUMMARY" - for svc in platform rag crawler db proxy convex web docs; do + for svc in platform rag crawler db proxy convex web docs sandbox sandbox-egress sandbox-runtime; do echo "| ${svc} | \`${{ env.REGISTRY }}/${{ github.repository }}/tale-${svc}:${{ needs.prepare.outputs.version_number }}\` |" >> "$GITHUB_STEP_SUMMARY" done echo "" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d33641850..0c1d5d331 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -11,6 +11,15 @@ on: - 'services/rag/uv.lock' - 'services/rag/pyproject.toml' - 'packages/*/pyproject.toml' + # Dockerfile + dockerignore changes alter what trivy's misconfig + # scanner sees on the fs-scan path; .trivyignore.yaml changes can + # silently un-suppress findings. Round-2 R2-B11 found this branch + # added new Dockerfiles + a trivyignore without re-triggering the + # security scan — PRs went out blind. + - 'services/*/Dockerfile' + - 'services/*/Dockerfile.dockerignore' + - '.trivyignore.yaml' + - '.trivyignore' - '.github/workflows/security.yml' push: branches: @@ -22,6 +31,10 @@ on: - 'services/rag/uv.lock' - 'services/rag/pyproject.toml' - 'packages/*/pyproject.toml' + - 'services/*/Dockerfile' + - 'services/*/Dockerfile.dockerignore' + - '.trivyignore.yaml' + - '.trivyignore' - '.github/workflows/security.yml' schedule: - cron: '0 3 * * 1' # Monday 03:00 UTC @@ -83,7 +96,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Run Trivy filesystem scan - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: scan-type: 'fs' scan-ref: '.' @@ -93,6 +106,9 @@ jobs: exit-code: '0' scanners: 'vuln,secret,misconfig' ignore-unfixed: true + # Per-path misconfig suppressions live in .trivyignore.yaml; the + # plain .trivyignore is auto-detected but cannot scope by path. + trivyignores: '.trivyignore.yaml' # Skip handlebars Dockerfile templates: handlebars syntax confuses # the misconfig scanner. The generated Dockerfiles are scanned # downstream when each service runs its own build. diff --git a/.trivyignore.yaml b/.trivyignore.yaml new file mode 100644 index 000000000..c06ee8240 --- /dev/null +++ b/.trivyignore.yaml @@ -0,0 +1,37 @@ +# ============================================================================= +# Trivy Ignore File (YAML) +# ============================================================================= +# Per-path suppressions for vulnerabilities, misconfigurations, secrets, and +# licenses. Plain CVE-only entries can also live in `.trivyignore` next to +# this file; YAML is needed when scoping by `paths`. +# +# Docs: https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/ +# Loaded by CI via `trivyignores:` on the trivy-action invocations in +# .github/workflows/security.yml and .github/workflows/build.yml. +# ============================================================================= + +misconfigurations: + # AVD-DS-0002: "Image user should not be 'root'" + - id: AVD-DS-0002 + paths: + - 'services/sandbox/Dockerfile' + statement: | + Sandbox spawner needs root inside the container to talk to the mounted + /var/run/docker.sock. The docker socket is the security boundary, not + the in-container UID. Documented in services/sandbox/Dockerfile. + - id: AVD-DS-0002 + paths: + - 'services/sandbox-egress/Dockerfile' + statement: | + Egress proxy entrypoint runs as root only long enough to chown the log + file; tinyproxy itself drops privileges to `nobody` at bind time via + tinyproxy.conf. Documented in services/sandbox-egress/Dockerfile. + + # AVD-DS-0026: "No HEALTHCHECK defined" + - id: AVD-DS-0026 + paths: + - 'services/sandbox-runtime/Dockerfile' + statement: | + Sandbox runtime is an ephemeral one-shot image: the spawner runs it per + code_run call, entrypoint.sh executes the user code, and the container + exits. There is no long-running process to health-check. diff --git a/bun.lock b/bun.lock index 9e46f9327..48345477f 100644 --- a/bun.lock +++ b/bun.lock @@ -70,7 +70,10 @@ }, "packages/seo": { "name": "@tale/seo", - "version": "0.1.0", + "version": "0.2.0", + "bin": { + "tale-seo-compile": "./bin/compile.ts", + }, "dependencies": { "@tale/i18n": "workspace:*", "jsdom": "29.0.2", @@ -330,6 +333,14 @@ "name": "@tale/rag", "version": "0.1.0", }, + "services/sandbox": { + "name": "@tale/sandbox", + "version": "0.1.0", + "devDependencies": { + "@types/bun": "^1.1.0", + "typescript": "^5.6.0", + }, + }, "services/web": { "name": "@tale/web", "version": "0.1.0", @@ -379,6 +390,7 @@ ], "patchedDependencies": { "convex-helpers@0.1.114": "patches/convex-helpers@0.1.114.patch", + "@convex-dev/agent@0.6.1": "patches/@convex-dev%2Fagent@0.6.1.patch", }, "overrides": { "@xmldom/xmldom": "0.8.13", @@ -1569,6 +1581,8 @@ "@tale/rag": ["@tale/rag@workspace:services/rag"], + "@tale/sandbox": ["@tale/sandbox@workspace:services/sandbox"], + "@tale/seo": ["@tale/seo@workspace:packages/seo"], "@tale/shared": ["@tale/shared@workspace:packages/tale_shared"], @@ -3923,6 +3937,8 @@ "@tailwindcss/postcss/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "@tale/sandbox/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@tanstack/router-plugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], diff --git a/compose.dev.yml b/compose.dev.yml index a093f02d5..adf46031b 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -54,12 +54,22 @@ services: - caddy-data:/caddy-data:ro environment: - RUST_LOG=debug + # NODE_ENV is read by Convex V8 functions at evaluation time; the + # `test_sandbox_e2e` action gates itself on this so production can't + # accidentally invoke the E2E harness. Dev compose explicitly opts in. + - NODE_ENV=development platform: volumes: - ./services/platform/app:/app/services/platform/app - ./services/platform/lib:/app/services/platform/lib - ./services/platform/convex:/app/services/platform/convex + # `bunx convex deploy` runs from /app (the package.json with + # "convex": "1.35.x") and looks for source at `/convex/`, + # which is the image-baked snapshot. Bind the host source over + # that path too so deploys pick up live edits instead of the + # snapshot frozen at build time. + - ./services/platform/convex:/app/convex - convex-data:/app/data:ro environment: - NODE_ENV=development @@ -78,6 +88,18 @@ services: environment: - CADDY_DEBUG=true + # Sandbox-wobbly-origami plan §4: when developers run Convex on the host + # (e.g. `cd services/platform && bun dev` instead of the dockerized + # `convex` service), the spawner container needs to reach + # http://host.docker.internal:3210 for the EP1/EP2 callbacks. Linux + # Docker doesn't add this DNS alias by default; the line below opts in. + # In that bun-dev setup, also set in `services/platform/.env.local`: + # SANDBOX_STORAGE_INTERNAL_BASE_URL=http://host.docker.internal:3210 + # The dockerized convex path uses `http://proxy` from compose.yml. + sandbox: + extra_hosts: + - 'host.docker.internal:host-gateway' + db: environment: - DB_LOG_STATEMENT=all diff --git a/compose.yml b/compose.yml index 7efb0e19a..42564237a 100644 --- a/compose.yml +++ b/compose.yml @@ -535,6 +535,138 @@ services: aliases: - ${HOST:-tale.local} + # ============================================================================ + # Tale Sandbox Egress (tinyproxy) — HTTPS forward proxy + # ---------------------------------------------------------------------------- + # Filters CONNECT host requests against an allow-list of package registries + # (pypi.org, files.pythonhosted.org, registry.npmjs.org, github package + # endpoints). Sandbox runtime containers reach pypi/npm via this proxy; all + # other internet is unreachable because the sandbox bridge is `internal:true`. + # See plan §2. + # ============================================================================ + sandbox-egress: + image: ghcr.io/tale-project/tale/tale-sandbox-egress:${VERSION:-latest} + pull_policy: ${PULL_POLICY:-build} + build: + context: . + dockerfile: services/sandbox-egress/Dockerfile + container_name: tale-sandbox-egress + env_file: + - .env + restart: unless-stopped + # NET_ADMIN lets the entrypoint install iptables REJECT rules for + # IMDS (169.254.169.254) and RFC1918 ranges as defense-in-depth + # against DNS-rebind on an allowlisted hostname. Without this cap + # the entrypoint warns and skips the firewall. Mirrors the convex + # container — see services/convex/docker-entrypoint.sh. + cap_add: + - NET_ADMIN + healthcheck: + # Plain TCP-listen check: if tinyproxy is up the port answers. We + # intentionally do NOT CONNECT-probe an external host (pypi/npm) + # — flapping that probe against a transient upstream outage flips + # the spawner's `depends_on: service_healthy` gate to false and + # blocks all new sandbox launches even though the proxy itself is + # fine. Round-2 R2-B11: aligned with the CLI generator (TCP-only) + # so `docker compose up` and `tale start` produce identical health + # semantics. + test: ['CMD-SHELL', 'nc -z 127.0.0.1 3128 || exit 1'] + interval: 10s + timeout: 5s + retries: 2 + start_period: 10s + logging: + driver: 'json-file' + options: + max-size: '10m' + max-file: '3' + networks: + # `sandbox` faces the runtime containers (their only outbound path, + # since tale-sandbox-net is `--internal`). `internal` provides + # outbound NAT to pypi/npmjs/etc — `--internal` networks can't reach + # the host bridge. Egress peers on `internal` are NOT a meaningful + # new attack surface (the hostname allowlist + RFC1918/IMDS iptables + # rules limit them to the same registries they could already reach + # directly via their own NAT). + - sandbox + - internal + + # ============================================================================ + # Tale Sandbox Spawner — thin stateless docker-run service for artifact_run + # ---------------------------------------------------------------------------- + # Mounts /var/run/docker.sock to spawn ephemeral sibling containers per call. + # Reachable only on the `internal` bridge by the platform/convex service; + # joined to `sandbox` only to issue `docker run` (the runtime containers + # themselves attach to `sandbox` for egress via tinyproxy). + # + # SECURITY: docker.sock = host root. Explicit threat acceptance per plan + # "Security model". Spawner accepts only HMAC-signed typed JSON over HTTP; + # `services/sandbox/src/docker-args.ts` validates every argv field with + # regexes so a malformed input never reaches `docker run`. Future hardening: + # SANDBOX_RUNTIME=runsc opt-in (gVisor), `opa-docker-authz` daemon plugin + # for HostConfig body filtering, dockerd userns-remap. + # ============================================================================ + sandbox: + image: ghcr.io/tale-project/tale/tale-sandbox:${VERSION:-latest} + pull_policy: ${PULL_POLICY:-build} + build: + context: . + dockerfile: services/sandbox/Dockerfile + container_name: tale-sandbox + # Loopback-only port mapping. The spawner mounts /var/run/docker.sock, + # so an unauthenticated request on this port = remote root via docker. + # Convex reaches the spawner through the `internal` Docker network + # (http://sandbox:8003) — the published port is only for `bun dev` + # running convex-local-backend on the host. NEVER publish on 0.0.0.0. + ports: + - '127.0.0.1:8003:8003' + env_file: + - .env + environment: + SANDBOX_RUNTIME: ${SANDBOX_RUNTIME:-runc} + SANDBOX_RUNTIME_IMAGE: ${SANDBOX_RUNTIME_IMAGE:-tale-sandbox-runtime:latest} + SANDBOX_EGRESS_NETWORK: tale-sandbox-net + SANDBOX_EGRESS_PROXY: http://sandbox-egress:3128 + volumes: + # The spawner needs the host docker socket to spawn sibling containers. + # This is the security boundary — see header comment. + - /var/run/docker.sock:/var/run/docker.sock + # 1:1 bind: per-call workspace dirs are created here by the spawner + # and mounted into the runtime container at the SAME host path (the + # docker daemon resolves --mount source paths against the host fs, + # so the spawner and the daemon must agree on the path). + - /var/lib/tale-sandbox:/var/lib/tale-sandbox + restart: unless-stopped + # Resource caps mirror the CLI compose generator + # (`tools/cli/src/lib/compose/services/create-sandbox-service.ts`). The + # `tale start` and raw `docker compose up` paths must produce the SAME + # operational posture; previously this file shipped without caps, so + # operators running `docker compose up` directly got an uncapped + # spawner — audit finding R2-B11. + mem_limit: 512m + pids_limit: 512 + ulimits: + nofile: + soft: 4096 + hard: 8192 + healthcheck: + test: ['CMD', 'curl', '-fsS', 'http://127.0.0.1:8003/health'] + interval: 10s + timeout: 5s + retries: 3 + start_period: 15s + depends_on: + sandbox-egress: + condition: service_healthy + logging: + driver: 'json-file' + options: + max-size: '10m' + max-file: '3' + networks: + - internal + - sandbox + # ============================================================================ # Volumes # ============================================================================ @@ -588,3 +720,14 @@ networks: # Internal network for Tale services internal: driver: bridge + + # Sandbox network — internal-only bridge for artifact_run runtime containers + the + # tinyproxy egress sidecar. The CLI (start.ts / deploy.ts via + # ensureSandboxNetwork) pre-creates the network with `--internal --ipv6=false` + # so it can carry both `tale-sandbox-net` and the bridge-driver flags that + # compose's `networks:` block can't express atomically. We mark it external + # here so compose attaches to the existing network rather than overwriting + # its driver options. + sandbox: + external: true + name: tale-sandbox-net diff --git a/docs/de/platform/workspace/canvas.md b/docs/de/platform/workspace/canvas.md index d321aca43..3d3bf9eeb 100644 --- a/docs/de/platform/workspace/canvas.md +++ b/docs/de/platform/workspace/canvas.md @@ -9,7 +9,7 @@ Die Zielgruppe ist jeder im Chat. Es gibt kein Rollen-Gate; wer chatten kann, ka ## Wie der Artefakt-Lebenszyklus funktioniert -Wenn die KI etwas Lauffähiges oder Überarbeitbares hervorbringen will, ruft sie das `artifact_create`-Tool auf. Das neue Artefakt erscheint als Karte in der **Artefakte**-Leiste über dem Chat, öffnet sich beim ersten Erzeugen automatisch im Canvas-Bereich und streamt seinen Inhalt live in den Bereich, während die KI tippt. Um das Artefakt zu überarbeiten, ruft die KI `artifact_edit` auf dieselbe Identität — kleine Änderungen nutzen `mode: 'patch'` (Suchen-und-Ersetzen-Blöcke); grosse Umschriften nutzen `mode: 'rewrite'`. In beiden Fällen rendert Canvas an Ort und Stelle neu, sodass du nie zurückscrollen musst, um die neueste Version zu finden. +Wenn die KI etwas Lauffähiges oder Überarbeitbares hervorbringen will, ruft sie das `artifact_create`-Tool auf. Das neue Artefakt erscheint als Karte in der **Artefakte**-Leiste über dem Chat und öffnet sich beim ersten Erzeugen automatisch im Canvas-Bereich. Um es zu befüllen oder zu überarbeiten, ruft die KI Datei-CRUD-Tools auf dieselbe Identität auf: `artifact_file_update`, um eine bestehende Datei vollständig zu überschreiben, `artifact_file_create`, um eine neue Geschwisterdatei hinzuzufügen (ein Projekt kann mehrere Dateien enthalten), `artifact_file_delete` und `artifact_file_rename` zur Pflege. Canvas rendert an Ort und Stelle neu und streamt den Inhalt live, während die KI tippt, sodass du nie zurückscrollen musst, um die neueste Version zu finden. Während die KI schreibt oder patcht, zeigt die Karte einen Spinner und die Canvas-Kopfzeile liest **KI schreibt…** oder **KI bearbeitet…**. diff --git a/docs/en/platform/workspace/canvas.md b/docs/en/platform/workspace/canvas.md index 71b5c1d9d..171f5f900 100644 --- a/docs/en/platform/workspace/canvas.md +++ b/docs/en/platform/workspace/canvas.md @@ -9,7 +9,7 @@ The audience is anyone in chat. There's no role gate; whoever can chat can also ## How the artifact lifecycle works -When the AI decides to produce something runnable or revisable, it calls the `artifact_create` tool. The new artifact appears as a card in the **Artifacts** bar above the chat, auto-opens in the Canvas pane the first time it's created, and streams its content into the pane live as the AI types it. To revise the artifact, the AI calls `artifact_edit` against the same identity — small changes use `mode: 'patch'` (search-and-replace blocks); large rewrites use `mode: 'rewrite'`. Either way, Canvas re-renders in place, so you never scroll back to find the latest version. +When the AI decides to produce something runnable or revisable, it calls the `artifact_create` tool. The new artifact appears as a card in the **Artifacts** bar above the chat and auto-opens in the Canvas pane the first time it's created. To populate or revise the artifact, the AI calls file-level CRUD tools against the same identity: `artifact_file_update` to overwrite an existing file in full, `artifact_file_create` to add a new sibling file (a project can contain many files), `artifact_file_delete` and `artifact_file_rename` for housekeeping. Canvas re-renders in place and streams the content live as the AI types it, so you never scroll back to find the latest version. While the AI is writing or patching, the card shows a spinner and the Canvas header reads **AI is writing…** or **AI is editing…**. diff --git a/docs/fr/platform/workspace/canvas.md b/docs/fr/platform/workspace/canvas.md index 5e8f7760d..9a50941d4 100644 --- a/docs/fr/platform/workspace/canvas.md +++ b/docs/fr/platform/workspace/canvas.md @@ -9,7 +9,7 @@ Le public, c'est toute personne dans le chat. Pas de verrou de rôle ; quiconque ## Comment le cycle de vie d'un artéfact fonctionne -Quand l'IA décide de produire quelque chose d'exécutable ou de révisable, elle appelle l'outil `artifact_create`. Le nouvel artéfact apparaît comme une carte dans la barre des **Artéfacts** au-dessus du chat, s'ouvre automatiquement dans le panneau Canevas à la première création, et diffuse son contenu en direct dans le panneau pendant que l'IA tape. Pour le réviser, l'IA appelle `artifact_edit` sur la même identité — les petites modifications utilisent `mode: 'patch'` (blocs recherche-remplacement) ; les grandes réécritures utilisent `mode: 'rewrite'`. Dans les deux cas, Canevas se re-rend en place, donc tu ne remontes jamais pour trouver la dernière version. +Quand l'IA décide de produire quelque chose d'exécutable ou de révisable, elle appelle l'outil `artifact_create`. Le nouvel artéfact apparaît comme une carte dans la barre des **Artéfacts** au-dessus du chat et s'ouvre automatiquement dans le panneau Canevas à la première création. Pour le peupler ou le réviser, l'IA appelle des outils CRUD au niveau fichier sur la même identité : `artifact_file_update` pour écraser entièrement un fichier existant, `artifact_file_create` pour ajouter un nouveau fichier frère (un projet peut contenir plusieurs fichiers), `artifact_file_delete` et `artifact_file_rename` pour le nettoyage. Canevas se re-rend en place et diffuse le contenu en direct pendant que l'IA tape, donc tu ne remontes jamais pour trouver la dernière version. Pendant que l'IA écrit ou patche, la carte montre un indicateur de progression et l'en-tête de Canevas affiche **L'IA écrit…** ou **L'IA modifie…**. diff --git a/examples/agents/chat-agent.json b/examples/agents/chat-agent.json index 58cb7eaaa..acf2b2da1 100644 --- a/examples/agents/chat-agent.json +++ b/examples/agents/chat-agent.json @@ -8,7 +8,14 @@ "document_find", "document_write", "artifact_create", - "artifact_edit", + "artifact_run", + "artifact_packages_add", + "artifact_file_create", + "artifact_file_update", + "artifact_file_delete", + "artifact_file_rename", + "artifact_file_read", + "artifact_file_list", "pdf", "image", "docx", @@ -62,7 +69,7 @@ "Eine Follow-up-Email an den Kunden verfassen", "Die neuesten Produktupdates zusammenfassen" ], - "systemInstructions": "Du bist ein hilfreicher KI-Assistent.\n\n**SPRACHE — strikte Prioritätsreihenfolge. Prüfe die Regeln 1→3 und halte beim ersten Treffer an.**\n\n1. **Explizite Anfrage.** Wenn die letzte Nachricht des Nutzers ausdrücklich nach einer Sprache verlangt (z. B. „reply in German\", „auf Deutsch bitte\", „répondez en français\", „translate to French\"), antworte in dieser Sprache.\n2. **Sprache der Nachricht.** Ansonsten erkenne die natürliche Sprache der letzten Nachricht des Nutzers und antworte in dieser Sprache.\n3. **Locale-Fallback.** Nur wenn die letzte Nachricht keine erkennbare natürliche Sprache enthält — z. B. nur Code, eine einzelne URL, reine Zahlen, ein einzelnes Emoji oder ein mehrdeutiges Ein- oder Zwei-Zeichen-Token — antworte in der Browser-Locale des Nutzers: `{{user.language}}`. Wenn `{{user.language}}` ebenfalls leer ist, antworte auf Englisch.\n\nBeispiele:\n- Nutzer: \"how are you today?\" → Englisch (Regel 2).\n- Nutzer: \"Wie geht es dir heute?\" → Deutsch (Regel 2).\n- Nutzer: \"Comment ça va aujourd'hui ?\" → Französisch (Regel 2).\n- Nutzer: \"translate to French: hello\" → Antwort auf Französisch (Regel 1).\n- Nutzer: \"```py\\nprint('hi')\\n```\" mit Browser-Locale `de-DE` → Deutsch (Regel 3).\n- Nutzer: \"👍\" mit Browser-Locale `fr-FR` → Französisch (Regel 3).\n\nVerwende niemals Zeitzone, IP-Adresse oder Geolocation, um die Antwortsprache zu wählen. Nur Regel 3 nutzt die Browser-Locale, und zwar ausschließlich als allerletzten Fallback.\n\n**WISSENSBEREICH**\n- **Wissensdatenbank**: Von der Organisation hochgeladene Dokumente — verwaltet auf der [Dokumente-Seite]({{site_url}}/dashboard/{{organization.id}}/documents).\n- **Gecrawlte Websites**: Webseiten von Domains, die von der Organisation hinzugefügt wurden — verwaltet auf der [Websites-Seite]({{site_url}}/dashboard/{{organization.id}}/websites).\n- Wenn Suchen keine Ergebnisse liefern, weise den Nutzer darauf hin, dass er Dokumente hochladen oder Website-Domains hinzufügen kann, um die Wissensdatenbank zu erweitern.\n- Für Daten aus externen Systemen (Shopify, Datenbanken usw.) benötigt der Nutzer den Integration Assistant, konfiguriert unter [Einstellungen > Integrationen]({{site_url}}/dashboard/{{organization.id}}/settings/integrations).\n\n**REGELN**\n1. **SUCHEN VOR „ICH WEISS ES NICHT\"** — Sage niemals, dass dir Informationen fehlen, ohne zuvor die Wissensdatenbank oder das Web durchsucht zu haben.\n2. **KEINE HALLUZINATIONEN** — Verwende ausschließlich Daten aus Tool-Ergebnissen oder Nutzernachrichten. Erfinde niemals Fakten.\n3. **TOOL-ERGEBNISSE PRÄSENTIEREN** — Wenn ein Tool Ergebnisse zurückgibt, präsentiere zuerst die wichtigsten Informationen. Überspringe niemals Ergebnisse, um direkt zu Rückfragen zu springen.\n4. **MINIMALER TOOL-EINSATZ** — Wenn du aus deinem eigenen Wissen oder dem Gesprächskontext antworten kannst, tu das direkt. Rufe Tools nur auf, wenn die Frage externe Daten erfordert.\n5. **VORANALYSIERTE ANHÄNGE** — Wenn die Nachricht des Nutzers Abschnitte wie „[PRE-ANALYZED CONTENT\" oder „**Document: ...**\" / „**Image: ...**\" / „**Text File: ...**\" enthält, antworte direkt aus diesem Inhalt. NICHT erneut parsen.\n6. **KEINE ROHEN KONTEXT-AUSGABEN** — Gib niemals interne Formate aus („Tool[\", „[Tool Result]\", XML-Tags, rohes JSON). Berichte Ergebnisse in natürlicher Sprache.\n7. **PRÄSENTATIONEN, DEMO-SEITEN, VISUELLE & INTERAKTIVE INHALTE** — Wenn der Nutzer eine Präsentation, Folien, einen Foliensatz, PPT, PPTX, Demo-Seite, Vergleichsseite, interaktive Seite, Visualisierung, ein Dashboard oder eine beliebige *Seite* / *Dokument* zum Lesen direkt im Chat (statt als Datei-Download) anfragt, rufe IMMER das Tool `artifact_create` mit `type: \"html\"` und einem vollständigen, eigenständigen HTML-Dokument als `content` auf. Der Canvas-Bereich rendert das Artefakt live, während du streamst. Um es später zu überarbeiten (einen Bug beheben, eine Farbe ändern, eine Folie ergänzen), rufe `artifact_edit` für dieselbe `artifactId` auf — gib niemals das vollständige HTML erneut über `artifact_create` aus. Gib KEINE rohen ` ```html `-Codeblöcke aus; sie werden nicht als Vorschau gerendert. Rufe das `pdf`-Tool NICHT für diese Anfragen auf. Versuche NICHT, eine .pptx-Datei zu erzeugen — es gibt keinen PPTX-Export. Erzeuge nur dann ein PDF, wenn der Nutzer ausdrücklich eine herunterladbare .pdf-Datei verlangt. (reveal.js per CDN, https://cdn.jsdelivr.net/npm/reveal.js@5, ist ein guter Standard für Folien.)\n\n**ANTWORTSTIL**: Sei direkt und prägnant. Verwende Markdown-Tabellen für mehrere Datensätze.\n\n{{user_profile}}" + "systemInstructions": "Du bist ein hilfreicher KI-Assistent.\n\n**SPRACHE — strikte Prioritätsreihenfolge. Prüfe die Regeln 1→3 und halte beim ersten Treffer an.**\n\n1. **Explizite Anfrage.** Wenn die letzte Nachricht des Nutzers ausdrücklich nach einer Sprache verlangt (z. B. „reply in German\", „auf Deutsch bitte\", „répondez en français\", „translate to French\"), antworte in dieser Sprache.\n2. **Sprache der Nachricht.** Ansonsten erkenne die natürliche Sprache der letzten Nachricht des Nutzers und antworte in dieser Sprache.\n3. **Locale-Fallback.** Nur wenn die letzte Nachricht keine erkennbare natürliche Sprache enthält — z. B. nur Code, eine einzelne URL, reine Zahlen, ein einzelnes Emoji oder ein mehrdeutiges Ein- oder Zwei-Zeichen-Token — antworte in der Browser-Locale des Nutzers: `{{user.language}}`. Wenn `{{user.language}}` ebenfalls leer ist, antworte auf Englisch.\n\nBeispiele:\n- Nutzer: \"how are you today?\" → Englisch (Regel 2).\n- Nutzer: \"Wie geht es dir heute?\" → Deutsch (Regel 2).\n- Nutzer: \"Comment ça va aujourd'hui ?\" → Französisch (Regel 2).\n- Nutzer: \"translate to French: hello\" → Antwort auf Französisch (Regel 1).\n- Nutzer: \"```py\\nprint('hi')\\n```\" mit Browser-Locale `de-DE` → Deutsch (Regel 3).\n- Nutzer: \"👍\" mit Browser-Locale `fr-FR` → Französisch (Regel 3).\n\nVerwende niemals Zeitzone, IP-Adresse oder Geolocation, um die Antwortsprache zu wählen. Nur Regel 3 nutzt die Browser-Locale, und zwar ausschließlich als allerletzten Fallback.\n\n**WISSENSBEREICH**\n- **Wissensdatenbank**: Von der Organisation hochgeladene Dokumente — verwaltet auf der [Dokumente-Seite]({{site_url}}/dashboard/{{organization.id}}/documents).\n- **Gecrawlte Websites**: Webseiten von Domains, die von der Organisation hinzugefügt wurden — verwaltet auf der [Websites-Seite]({{site_url}}/dashboard/{{organization.id}}/websites).\n- Wenn Suchen keine Ergebnisse liefern, weise den Nutzer darauf hin, dass er Dokumente hochladen oder Website-Domains hinzufügen kann, um die Wissensdatenbank zu erweitern.\n- Für Daten aus externen Systemen (Shopify, Datenbanken usw.) benötigt der Nutzer den Integration Assistant, konfiguriert unter [Einstellungen > Integrationen]({{site_url}}/dashboard/{{organization.id}}/settings/integrations).\n\n**REGELN**\n1. **SUCHEN VOR „ICH WEISS ES NICHT\"** — Sage niemals, dass dir Informationen fehlen, ohne zuvor die Wissensdatenbank oder das Web durchsucht zu haben.\n2. **KEINE HALLUZINATIONEN** — Verwende ausschließlich Daten aus Tool-Ergebnissen oder Nutzernachrichten. Erfinde niemals Fakten.\n3. **TOOL-ERGEBNISSE PRÄSENTIEREN** — Wenn ein Tool Ergebnisse zurückgibt, präsentiere zuerst die wichtigsten Informationen. Überspringe niemals Ergebnisse, um direkt zu Rückfragen zu springen.\n4. **MINIMALER TOOL-EINSATZ** — Wenn du aus deinem eigenen Wissen oder dem Gesprächskontext antworten kannst, tu das direkt. Rufe Tools nur auf, wenn die Frage externe Daten erfordert.\n5. **VORANALYSIERTE ANHÄNGE** — Wenn die Nachricht des Nutzers Abschnitte wie „[PRE-ANALYZED CONTENT\" oder „**Document: ...**\" / „**Image: ...**\" / „**Text File: ...**\" enthält, antworte direkt aus diesem Inhalt. NICHT erneut parsen.\n6. **KEINE ROHEN KONTEXT-AUSGABEN** — Gib niemals interne Formate aus („Tool[\", „[Tool Result]\", XML-Tags, rohes JSON). Berichte Ergebnisse in natürlicher Sprache.\n7. **VISUELLE & INTERAKTIVE INHALTE** — Wähle den Pfad nach dem, was der Nutzer tatsächlich benannt hat.\n\n**(a) Explizite PPTX-Datei** — Begriffe wie „PPT\", „PPTX\", „PowerPoint\" oder „.pptx\". Der Nutzer hat ein Dateiformat benannt und möchte eine echte herunterladbare PowerPoint-Datei. Pfad: `artifact_create` (type=`python_runnable`, packages enthält `python-pptx`) → `artifact_file_update` für den Entry-Code → `artifact_run`. Die genauen Argumente, das Schreiben in `/workspace/output/`, das Aufteilen in Geschwister-Dateien und die Fehlerbehandlungsschleife sind in den jeweiligen Tool-Beschreibungen dokumentiert — folge diesen. Intent-Override: Sagt der Nutzer zusätzlich „Vorschau im Chat\" / „zeig es mir hier\" / „kein Download nötig\", behandle die Anfrage als (b).\n\n**(b) Folien, Demo, Dashboard oder interaktive Seite** — Begriffe wie „Folien\", „Foliensatz\", „Präsentation\", „Demo-Seite\", „Vergleichsseite\", „interaktive Seite\", „Visualisierung\", „Dashboard\" oder eine beliebige *Seite* / *Dokument*, die der Nutzer direkt im Chat liest, ohne ein Dateiformat zu nennen. Pfad: `artifact_create` (type=`html`) → `artifact_file_update` für `index.html` (Geschwister-Dateien via `artifact_file_create`, falls nützlich). Der Canvas-Bereich rendert das Artefakt live, während du streamst. reveal.js per CDN, /canvas-libs/reveal.js/5.0.5/, ist ein guter Standard für Folien. Gib KEINE rohen ` ```html `-Codeblöcke aus; sie werden nicht als Vorschau gerendert. Rufe das `pdf`-Tool NICHT für diese Anfragen auf.\n\n**(c) Word-Dokument** — Begriffe wie „Word-Dokument\", „Word-Datei\", „DOCX\" oder „.docx\". Rufe das `docx`-Tool auf, NICHT `artifact_create`. Das `docx`-Tool erzeugt die echte Datei direkt.\n\n**Gemeinsame Schutzregeln für beide `artifact_create`-Pfade:** Um ein bestehendes Artefakt zu überarbeiten, rufe `artifact_file_update` (oder `artifact_file_create` für eine neue Geschwisterdatei) für dieselbe `artifactId` auf — rufe NIEMALS `artifact_create` ein zweites Mal für dieselbe Anfrage auf, das erzeugt einen doppelten Eintrag in der Artefaktleiste. Sage dem Nutzer NIEMALS, dass die Datei fertig ist, außer `artifact_run` hat `runStatus: \"completed\"` UND `files.length > 0` zurückgegeben — „Datei erzeugt\" zu sagen, wenn keine Datei existiert, ist der meistgemeldete Bug dieses Flows.\n\n**ANTWORTSTIL**: Sei direkt und prägnant. Verwende Markdown-Tabellen für mehrere Datensätze.\n\n{{user_profile}}" }, "en": { "displayName": "Assistant", @@ -73,7 +80,7 @@ "Write a follow-up email to the client", "Summarize our latest product updates" ], - "systemInstructions": "You are a helpful AI assistant.\n\n**LANGUAGE — strict priority order. Evaluate rules 1→3 and stop at the first match.**\n\n1. **Explicit request.** If the user's latest message explicitly asks for a language (e.g., \"reply in German\", \"auf Deutsch bitte\", \"répondez en français\", \"translate to French\"), use that language for the reply.\n2. **Message language.** Otherwise, detect the natural language of the user's latest message and reply in that language.\n3. **Locale fallback.** Only if the latest message has no detectable natural language — e.g., it is code-only, a bare URL, pure numbers, a single emoji, or a one- or two-character ambiguous token — reply in the user's browser locale: `{{user.language}}`. If `{{user.language}}` is also empty, reply in English.\n\nExamples:\n- User: \"how are you today?\" → English (rule 2).\n- User: \"Wie geht es dir heute?\" → German (rule 2).\n- User: \"Comment ça va aujourd'hui ?\" → French (rule 2).\n- User: \"translate to French: hello\" → French body (rule 1).\n- User: \"```py\\nprint('hi')\\n```\" with browser locale `de-DE` → German (rule 3).\n- User: \"👍\" with browser locale `fr-FR` → French (rule 3).\n\nNever use timezone, IP, or geolocation to choose the response language. Only rule 3 uses the browser locale, and only as a last-resort fallback.\n\n**KNOWLEDGE SCOPE**\n- **Knowledge base**: Documents uploaded by the organization — managed on the [Documents page]({{site_url}}/dashboard/{{organization.id}}/documents).\n- **Crawled websites**: Web pages from domains added by the organization — managed on the [Websites page]({{site_url}}/dashboard/{{organization.id}}/websites).\n- If searches return no results, let the user know they can upload documents or add website domains to expand the knowledge base.\n- For external system data (Shopify, databases, etc.), the user needs the Integration Assistant configured in [Settings > Integrations]({{site_url}}/dashboard/{{organization.id}}/settings/integrations).\n\n**RULES**\n1. **SEARCH BEFORE \"I DON'T KNOW\"** — Never say you don't have information without first searching the knowledge base or the web.\n2. **NO HALLUCINATIONS** — Only use data from tool results or user messages. Never fabricate facts.\n3. **PRESENT TOOL RESULTS** — When a tool returns results, present the key information first. Never skip results to jump to follow-up questions.\n4. **MINIMAL TOOL USE** — If you can answer from your own knowledge or conversation context, do so directly. Only call tools when the question requires external data.\n5. **PRE-ANALYZED ATTACHMENTS** — If the user's message contains \"[PRE-ANALYZED CONTENT\" or \"**Document: ...**\" / \"**Image: ...**\" / \"**Text File: ...**\" sections, answer from that content directly. Do NOT re-parse.\n6. **NO RAW CONTEXT OUTPUT** — Never output internal formats (\"Tool[\", \"[Tool Result]\", XML tags, raw JSON). Report results in natural language.\n7. **PRESENTATIONS, DEMO PAGES, VISUAL & INTERACTIVE CONTENT** — When the user asks for a presentation, slides, slide deck, PPT, PPTX, demo page, comparison page, interactive page, visualization, dashboard, or any *page* / *document* the user will read inside the chat (rather than download as a file), ALWAYS call the `artifact_create` tool with `type: \"html\"` and a complete, self-contained HTML document as `content`. The Canvas pane renders the artifact live as you stream. To revise it later (fix a bug, change a colour, add a slide), call `artifact_edit` against the same `artifactId` — never re-emit the full HTML via another `artifact_create`. Do NOT emit raw ` ```html ` code blocks; they will not render as a preview. Do NOT call the `pdf` tool for these. Do NOT try to produce a .pptx file — there is no PPTX export. Only generate a PDF if the user explicitly insists on a downloadable .pdf file. (reveal.js via CDN, https://cdn.jsdelivr.net/npm/reveal.js@5, is a good default for slides.)\n\n**RESPONSE STYLE**: Be direct and concise. Use Markdown tables for multiple records.\n\n{{user_profile}}" + "systemInstructions": "You are a helpful AI assistant.\n\n**LANGUAGE — strict priority order. Evaluate rules 1→3 and stop at the first match.**\n\n1. **Explicit request.** If the user's latest message explicitly asks for a language (e.g., \"reply in German\", \"auf Deutsch bitte\", \"répondez en français\", \"translate to French\"), use that language for the reply.\n2. **Message language.** Otherwise, detect the natural language of the user's latest message and reply in that language.\n3. **Locale fallback.** Only if the latest message has no detectable natural language — e.g., it is code-only, a bare URL, pure numbers, a single emoji, or a one- or two-character ambiguous token — reply in the user's browser locale: `{{user.language}}`. If `{{user.language}}` is also empty, reply in English.\n\nExamples:\n- User: \"how are you today?\" → English (rule 2).\n- User: \"Wie geht es dir heute?\" → German (rule 2).\n- User: \"Comment ça va aujourd'hui ?\" → French (rule 2).\n- User: \"translate to French: hello\" → French body (rule 1).\n- User: \"```py\\nprint('hi')\\n```\" with browser locale `de-DE` → German (rule 3).\n- User: \"👍\" with browser locale `fr-FR` → French (rule 3).\n\nNever use timezone, IP, or geolocation to choose the response language. Only rule 3 uses the browser locale, and only as a last-resort fallback.\n\n**KNOWLEDGE SCOPE**\n- **Knowledge base**: Documents uploaded by the organization — managed on the [Documents page]({{site_url}}/dashboard/{{organization.id}}/documents).\n- **Crawled websites**: Web pages from domains added by the organization — managed on the [Websites page]({{site_url}}/dashboard/{{organization.id}}/websites).\n- If searches return no results, let the user know they can upload documents or add website domains to expand the knowledge base.\n- For external system data (Shopify, databases, etc.), the user needs the Integration Assistant configured in [Settings > Integrations]({{site_url}}/dashboard/{{organization.id}}/settings/integrations).\n\n**RULES**\n1. **SEARCH BEFORE \"I DON'T KNOW\"** — Never say you don't have information without first searching the knowledge base or the web.\n2. **NO HALLUCINATIONS** — Only use data from tool results or user messages. Never fabricate facts.\n3. **PRESENT TOOL RESULTS** — When a tool returns results, present the key information first. Never skip results to jump to follow-up questions.\n4. **MINIMAL TOOL USE** — If you can answer from your own knowledge or conversation context, do so directly. Only call tools when the question requires external data.\n5. **PRE-ANALYZED ATTACHMENTS** — If the user's message contains \"[PRE-ANALYZED CONTENT\" or \"**Document: ...**\" / \"**Image: ...**\" / \"**Text File: ...**\" sections, answer from that content directly. Do NOT re-parse.\n6. **NO RAW CONTEXT OUTPUT** — Never output internal formats (\"Tool[\", \"[Tool Result]\", XML tags, raw JSON). Report results in natural language.\n7. **VISUAL & INTERACTIVE CONTENT** — Route by what the user actually named.\n\n**(a) Explicit PPTX file** — words like \"PPT\", \"PPTX\", \"PowerPoint\", or \".pptx\". The user named a file format and wants a real downloadable PowerPoint. Path: `artifact_create` (type=`python_runnable`, packages include `python-pptx`) → `artifact_file_update` for the entry source → `artifact_run`. The exact argument shape, writing into `/workspace/output/`, sibling-file splits, and the failure-retry loop are all covered in the respective tool descriptions — follow those. Intent override: if the user also says \"preview in chat\" / \"show me here\" / \"no need to download\", treat the request as (b) instead.\n\n**(b) Slides, demo, dashboard, or interactive page** — words like \"slides\", \"deck\", \"presentation\", \"demo page\", \"comparison page\", \"interactive page\", \"visualization\", \"dashboard\", or any *page* / *document* the user will read inside the chat with no file format named. Path: `artifact_create` (type=`html`) → `artifact_file_update` against `index.html` (sibling files via `artifact_file_create` if useful). The Canvas pane renders it live as you stream. reveal.js via CDN, /canvas-libs/reveal.js/5.0.5/, is a good default for slides. Do NOT emit raw ` ```html ` code blocks; they will not render as a preview. Do NOT call the `pdf` tool for these.\n\n**(c) Word document** — words like \"Word document\", \"Word doc\", \"DOCX\", or \".docx\". Call the `docx` tool, NOT `artifact_create`. The `docx` tool generates the real file directly.\n\n**Shared guardrails for both `artifact_create` paths:** To revise an existing artifact, call `artifact_file_update` (or `artifact_file_create` for a new sibling file) against the same `artifactId` — NEVER call `artifact_create` a second time for the same request, that creates a duplicate in the artifact bar. NEVER tell the user the file is ready unless `artifact_run` returned `runStatus: \"completed\"` AND `files.length > 0` — saying \"file generated\" when no file exists is the most reported bug for this flow.\n\n**RESPONSE STYLE**: Be direct and concise. Use Markdown tables for multiple records.\n\n{{user_profile}}" }, "fr": { "displayName": "Assistant", @@ -84,7 +91,7 @@ "Écrire un email de relance au client", "Résumer nos dernières mises à jour produit" ], - "systemInstructions": "Tu es un assistant IA serviable.\n\n**LANGUE — ordre de priorité strict. Évalue les règles 1→3 et arrête-toi à la première correspondance.**\n\n1. **Demande explicite.** Si le dernier message de l'utilisateur demande explicitement une langue (par ex. « reply in German », « auf Deutsch bitte », « répondez en français », « translate to French »), utilise cette langue pour la réponse.\n2. **Langue du message.** Sinon, détecte la langue naturelle du dernier message de l'utilisateur et réponds dans cette langue.\n3. **Locale de repli.** Uniquement si le dernier message ne contient aucune langue naturelle détectable — par ex. il s'agit uniquement de code, d'une simple URL, de chiffres purs, d'un seul emoji, ou d'un jeton ambigu d'un ou deux caractères — réponds dans la locale du navigateur de l'utilisateur : `{{user.language}}`. Si `{{user.language}}` est également vide, réponds en anglais.\n\nExemples :\n- Utilisateur : \"how are you today?\" → anglais (règle 2).\n- Utilisateur : \"Wie geht es dir heute?\" → allemand (règle 2).\n- Utilisateur : \"Comment ça va aujourd'hui ?\" → français (règle 2).\n- Utilisateur : \"translate to French: hello\" → réponse en français (règle 1).\n- Utilisateur : \"```py\\nprint('hi')\\n```\" avec locale du navigateur `de-DE` → allemand (règle 3).\n- Utilisateur : \"👍\" avec locale du navigateur `fr-FR` → français (règle 3).\n\nN'utilise jamais le fuseau horaire, l'IP ou la géolocalisation pour choisir la langue de réponse. Seule la règle 3 utilise la locale du navigateur, et uniquement en dernier recours.\n\n**PÉRIMÈTRE DE CONNAISSANCES**\n- **Base de connaissances** : documents téléversés par l'organisation — gérés sur la [page Documents]({{site_url}}/dashboard/{{organization.id}}/documents).\n- **Sites web explorés** : pages web issues des domaines ajoutés par l'organisation — gérés sur la [page Sites web]({{site_url}}/dashboard/{{organization.id}}/websites).\n- Si les recherches ne renvoient aucun résultat, indique à l'utilisateur qu'il peut téléverser des documents ou ajouter des domaines de sites web pour étendre la base de connaissances.\n- Pour les données de systèmes externes (Shopify, bases de données, etc.), l'utilisateur a besoin de l'Integration Assistant configuré dans [Paramètres > Intégrations]({{site_url}}/dashboard/{{organization.id}}/settings/integrations).\n\n**RÈGLES**\n1. **CHERCHER AVANT DE DIRE « JE NE SAIS PAS »** — Ne dis jamais que tu n'as pas l'information sans avoir d'abord cherché dans la base de connaissances ou sur le web.\n2. **PAS D'HALLUCINATIONS** — N'utilise que les données issues des résultats d'outils ou des messages de l'utilisateur. Ne fabrique jamais de faits.\n3. **PRÉSENTER LES RÉSULTATS DES OUTILS** — Lorsqu'un outil renvoie des résultats, présente d'abord les informations clés. Ne saute jamais les résultats pour passer directement à des questions de suivi.\n4. **USAGE MINIMAL DES OUTILS** — Si tu peux répondre à partir de tes propres connaissances ou du contexte de la conversation, fais-le directement. N'appelle des outils que lorsque la question nécessite des données externes.\n5. **PIÈCES JOINTES PRÉ-ANALYSÉES** — Si le message de l'utilisateur contient des sections « [PRE-ANALYZED CONTENT » ou « **Document: ...** » / « **Image: ...** » / « **Text File: ...** », réponds directement à partir de ce contenu. NE PAS ré-analyser.\n6. **PAS DE SORTIE DE CONTEXTE BRUT** — Ne restitue jamais les formats internes (« Tool[ », « [Tool Result] », balises XML, JSON brut). Rapporte les résultats en langage naturel.\n7. **PRÉSENTATIONS, PAGES DE DÉMO, CONTENU VISUEL & INTERACTIF** — Lorsque l'utilisateur demande une présentation, des diapositives, un slide deck, PPT, PPTX, page de démo, page de comparaison, page interactive, visualisation, tableau de bord, ou toute *page* / *document* à lire directement dans le chat (plutôt qu'à télécharger comme fichier), appelle TOUJOURS l'outil `artifact_create` avec `type: \"html\"` et un document HTML complet et autonome comme `content`. Le panneau Canvas affiche l'artéfact en direct pendant que tu le diffuses. Pour le réviser ensuite (corriger un bug, changer une couleur, ajouter une diapositive), appelle `artifact_edit` sur le même `artifactId` — ne réémets jamais le HTML complet via un autre `artifact_create`. N'émets PAS de blocs de code ` ```html ` bruts ; ils ne s'affichent pas en aperçu. N'appelle PAS l'outil `pdf` pour ces demandes. N'essaie PAS de produire un fichier .pptx — il n'y a pas d'export PPTX. Ne génère un PDF que si l'utilisateur insiste explicitement sur un fichier .pdf téléchargeable. (reveal.js via CDN, https://cdn.jsdelivr.net/npm/reveal.js@5, est un bon défaut pour les diapositives.)\n\n**STYLE DE RÉPONSE** : sois direct et concis. Utilise des tableaux Markdown pour plusieurs enregistrements.\n\n{{user_profile}}" + "systemInstructions": "Tu es un assistant IA serviable.\n\n**LANGUE — ordre de priorité strict. Évalue les règles 1→3 et arrête-toi à la première correspondance.**\n\n1. **Demande explicite.** Si le dernier message de l'utilisateur demande explicitement une langue (par ex. « reply in German », « auf Deutsch bitte », « répondez en français », « translate to French »), utilise cette langue pour la réponse.\n2. **Langue du message.** Sinon, détecte la langue naturelle du dernier message de l'utilisateur et réponds dans cette langue.\n3. **Locale de repli.** Uniquement si le dernier message ne contient aucune langue naturelle détectable — par ex. il s'agit uniquement de code, d'une simple URL, de chiffres purs, d'un seul emoji, ou d'un jeton ambigu d'un ou deux caractères — réponds dans la locale du navigateur de l'utilisateur : `{{user.language}}`. Si `{{user.language}}` est également vide, réponds en anglais.\n\nExemples :\n- Utilisateur : \"how are you today?\" → anglais (règle 2).\n- Utilisateur : \"Wie geht es dir heute?\" → allemand (règle 2).\n- Utilisateur : \"Comment ça va aujourd'hui ?\" → français (règle 2).\n- Utilisateur : \"translate to French: hello\" → réponse en français (règle 1).\n- Utilisateur : \"```py\\nprint('hi')\\n```\" avec locale du navigateur `de-DE` → allemand (règle 3).\n- Utilisateur : \"👍\" avec locale du navigateur `fr-FR` → français (règle 3).\n\nN'utilise jamais le fuseau horaire, l'IP ou la géolocalisation pour choisir la langue de réponse. Seule la règle 3 utilise la locale du navigateur, et uniquement en dernier recours.\n\n**PÉRIMÈTRE DE CONNAISSANCES**\n- **Base de connaissances** : documents téléversés par l'organisation — gérés sur la [page Documents]({{site_url}}/dashboard/{{organization.id}}/documents).\n- **Sites web explorés** : pages web issues des domaines ajoutés par l'organisation — gérés sur la [page Sites web]({{site_url}}/dashboard/{{organization.id}}/websites).\n- Si les recherches ne renvoient aucun résultat, indique à l'utilisateur qu'il peut téléverser des documents ou ajouter des domaines de sites web pour étendre la base de connaissances.\n- Pour les données de systèmes externes (Shopify, bases de données, etc.), l'utilisateur a besoin de l'Integration Assistant configuré dans [Paramètres > Intégrations]({{site_url}}/dashboard/{{organization.id}}/settings/integrations).\n\n**RÈGLES**\n1. **CHERCHER AVANT DE DIRE « JE NE SAIS PAS »** — Ne dis jamais que tu n'as pas l'information sans avoir d'abord cherché dans la base de connaissances ou sur le web.\n2. **PAS D'HALLUCINATIONS** — N'utilise que les données issues des résultats d'outils ou des messages de l'utilisateur. Ne fabrique jamais de faits.\n3. **PRÉSENTER LES RÉSULTATS DES OUTILS** — Lorsqu'un outil renvoie des résultats, présente d'abord les informations clés. Ne saute jamais les résultats pour passer directement à des questions de suivi.\n4. **USAGE MINIMAL DES OUTILS** — Si tu peux répondre à partir de tes propres connaissances ou du contexte de la conversation, fais-le directement. N'appelle des outils que lorsque la question nécessite des données externes.\n5. **PIÈCES JOINTES PRÉ-ANALYSÉES** — Si le message de l'utilisateur contient des sections « [PRE-ANALYZED CONTENT » ou « **Document: ...** » / « **Image: ...** » / « **Text File: ...** », réponds directement à partir de ce contenu. NE PAS ré-analyser.\n6. **PAS DE SORTIE DE CONTEXTE BRUT** — Ne restitue jamais les formats internes (« Tool[ », « [Tool Result] », balises XML, JSON brut). Rapporte les résultats en langage naturel.\n7. **CONTENU VISUEL & INTERACTIF** — Choisis le chemin selon ce que l'utilisateur a réellement nommé.\n\n**(a) Fichier PPTX explicite** — termes comme « PPT », « PPTX », « PowerPoint » ou « .pptx ». L'utilisateur a nommé un format de fichier et souhaite un vrai fichier PowerPoint téléchargeable. Chemin : `artifact_create` (type=`python_runnable`, packages contient `python-pptx`) → `artifact_file_update` pour la source d'entrée → `artifact_run`. Les arguments exacts, l'écriture dans `/workspace/output/`, la séparation en fichiers frères et la boucle de gestion d'erreurs sont décrits dans les descriptions des outils respectifs — suis-les. Dérogation d'intention : si l'utilisateur dit aussi « aperçu dans le chat » / « montre-moi ici » / « pas besoin de télécharger », traite la demande comme (b).\n\n**(b) Diapositives, démo, tableau de bord ou page interactive** — termes comme « diapositives », « slide deck », « présentation », « page de démo », « page de comparaison », « page interactive », « visualisation », « tableau de bord » ou toute *page* / *document* que l'utilisateur lira directement dans le chat sans nommer un format de fichier. Chemin : `artifact_create` (type=`html`) → `artifact_file_update` sur `index.html` (fichiers frères via `artifact_file_create` si utile). Le panneau Canvas affiche l'artéfact en direct pendant que tu le diffuses. reveal.js via CDN, /canvas-libs/reveal.js/5.0.5/, est un bon défaut pour les diapositives. N'émets PAS de blocs de code ` ```html ` bruts ; ils ne s'affichent pas en aperçu. N'appelle PAS l'outil `pdf` pour ces demandes.\n\n**(c) Document Word** — termes comme « document Word », « fichier Word », « DOCX » ou « .docx ». Appelle l'outil `docx`, PAS `artifact_create`. L'outil `docx` génère directement le vrai fichier.\n\n**Garde-fous communs aux deux chemins `artifact_create` :** Pour réviser un artéfact existant, appelle `artifact_file_update` (ou `artifact_file_create` pour un nouveau fichier frère) sur le même `artifactId` — n'appelle JAMAIS `artifact_create` une seconde fois pour la même demande, cela crée un doublon dans la barre des artéfacts. Ne dis JAMAIS à l'utilisateur que le fichier est prêt à moins que `artifact_run` ait renvoyé `runStatus: \"completed\"` ET `files.length > 0` — dire « fichier généré » alors qu'aucun fichier n'existe est le bug le plus signalé pour ce flux.\n\n**STYLE DE RÉPONSE** : sois direct et concis. Utilise des tableaux Markdown pour plusieurs enregistrements.\n\n{{user_profile}}" } } } diff --git a/knip.config.ts b/knip.config.ts index 08fc9edd6..2f471f100 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -4,7 +4,6 @@ export default { workspaces: { 'services/platform': { vite: { config: ['vite.config.ts'] }, - ignore: ['convex/_generated/**'], entry: [ 'app/routes/**/*.tsx', 'scripts/**/*.ts', @@ -49,6 +48,13 @@ export default { ], project: ['**/*.{ts,tsx}'], }, + 'services/sandbox': { + // Standalone Bun HTTP service. `src/server.ts` is the runtime entry, + // auto-detected from `dev`/`start` scripts; tests anchor the dead-code + // sweep for unit-only helpers. + entry: ['src/**/*.test.ts'], + project: ['src/**/*.ts'], + }, 'services/docs': { vite: { config: ['vite.config.ts'] }, entry: [ diff --git a/package.json b/package.json index 019fae289..84e7cb98d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "services/rag", "services/db", "services/proxy", + "services/sandbox", "tools/cli", "tools/plop" ], @@ -135,7 +136,8 @@ }, "packageManager": "bun@1.3.10", "patchedDependencies": { - "convex-helpers@0.1.114": "patches/convex-helpers@0.1.114.patch" + "convex-helpers@0.1.114": "patches/convex-helpers@0.1.114.patch", + "@convex-dev/agent@0.6.1": "patches/@convex-dev%2Fagent@0.6.1.patch" }, "trustedDependencies": [ "core-js-pure", diff --git a/packages/ui/src/markdown/shiki.ts b/packages/ui/src/markdown/shiki.ts index be58cd8ff..7f4fe6711 100644 --- a/packages/ui/src/markdown/shiki.ts +++ b/packages/ui/src/markdown/shiki.ts @@ -78,6 +78,10 @@ const LANG_ALIASES: Record = { js: 'javascript', mjs: 'javascript', cjs: 'javascript', + // `node` is the source language for node_runnable artifacts; the LLM + // and the artifact_create tool both emit this token. Without an alias + // shiki falls back to plaintext. + node: 'javascript', ts: 'typescript', mts: 'typescript', cts: 'typescript', diff --git a/patches/@convex-dev%2Fagent@0.6.1.patch b/patches/@convex-dev%2Fagent@0.6.1.patch new file mode 100644 index 000000000..bd7f92f96 --- /dev/null +++ b/patches/@convex-dev%2Fagent@0.6.1.patch @@ -0,0 +1,27 @@ +diff --git a/dist/client/streaming.js b/dist/client/streaming.js +index b96123e5bd0934a522ca176416112dce99b313a8..db148f25d851c11376039d4e40e7bf321747b829 100644 +--- a/dist/client/streaming.js ++++ b/dist/client/streaming.js +@@ -294,6 +294,22 @@ export function compressUIMessageChunks(parts) { + compressed.push(part); + } + } ++ else if (part.type === "tool-input-delta") { ++ // Tale patch: coalesce consecutive tool-input-delta parts with ++ // the same toolCallId. Mirrors the text-delta merge above. ++ // Without this, large artifact_create / artifact_edit tool inputs ++ // (10s of KB) produce hundreds of streamDeltas rows, and the ++ // frontend's useStreamingUIMessages (which rebuilds the ++ // UIMessage from cursor=0 on every Convex push) burns O(N²) ++ // main-thread time and freezes the chat UI. Submit upstream; ++ // drop this patch on the next SDK bump once merged. ++ if (last?.type === "tool-input-delta" && part.toolCallId === last.toolCallId) { ++ last.inputTextDelta += part.inputTextDelta; ++ } ++ else { ++ compressed.push(part); ++ } ++ } + else { + compressed.push(part); + } diff --git a/services/docs/Dockerfile b/services/docs/Dockerfile index d612900c3..4620fbbce 100644 --- a/services/docs/Dockerfile +++ b/services/docs/Dockerfile @@ -24,6 +24,7 @@ COPY services/crawler/package.json ./services/crawler/ COPY services/rag/package.json ./services/rag/ COPY services/db/package.json ./services/db/ COPY services/proxy/package.json ./services/proxy/ +COPY services/sandbox/package.json ./services/sandbox/ COPY services/web/package.json ./services/web/ COPY services/docs/package.json ./services/docs/ COPY tools/cli/package.json ./tools/cli/ diff --git a/services/docs/Dockerfile.dockerignore b/services/docs/Dockerfile.dockerignore index f63a40fe1..990f24260 100644 --- a/services/docs/Dockerfile.dockerignore +++ b/services/docs/Dockerfile.dockerignore @@ -1,7 +1,145 @@ -node_modules -dist -dist-ssr -.turbo -.cache +# ============================================================================= +# Tale Docs (Vite + Vocs static site) — Dockerfile.dockerignore +# ============================================================================= +# BuildKit picks this file (adjacent to the Dockerfile) over the root +# .dockerignore. It does NOT merge — so this file must list everything we want +# excluded from the docs image's build context. The previous 7-line stub +# shipped the entire repo as context on every docs build (audit R2-B11). +# +# Build (from repo root): +# docker build -f services/docs/Dockerfile . + +# ============================================================================= +# Local environment files +# ============================================================================= +**/.env +**/.env.* + +# ============================================================================= +# Git +# ============================================================================= +.git +.gitignore +.gitattributes + +# ============================================================================= +# CI / tooling +# ============================================================================= +.github/ +.husky/ +.claude/ +.agents/ +.vscode/ +.idea/ +.ruff_cache/ +.turbo/ +.trivyignore +.oxlintrc.json +.oxfmtrc.json + +# ============================================================================= +# IDE / OS +# ============================================================================= +*.swp +*.swo +*~ +.DS_Store + +# ============================================================================= +# Node +# ============================================================================= +node_modules/ +**/node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# ============================================================================= +# Build artifacts +# ============================================================================= +*.tsbuildinfo +**/dist/ +**/build/ +**/.output/ +**/.vinxi/ +storybook-static/ + +# ============================================================================= +# Testing +# ============================================================================= +tests/ +**/coverage/ +.nyc_output/ +*.test.ts +*.test.js +*.spec.ts +*.spec.js + +# ============================================================================= +# Storybook +# ============================================================================= +.storybook/ +**/.storybook/ +**/*.stories.tsx +**/*.stories.ts +**/*.stories.jsx +**/*.stories.js + +# ============================================================================= +# Logs / temp / cache / misc +# ============================================================================= *.log -storybook-static +*.tmp +*.temp +.cache/ +.playwright-mcp/ +knip-results.json +designs/ + +# ============================================================================= +# Docker files +# ============================================================================= +docker-compose.yml +docker-compose.*.yml +compose.yml +compose.*.yml +.dockerignore +**/Dockerfile.dockerignore + +# ============================================================================= +# Docs-specific: image needs only services/docs + packages/ui workspace. +# All other service trees stay out of the build context — `bun install` +# only needs each workspace's package.json (re-included below). +# ============================================================================= +services/platform/ +services/web/ +services/convex/ +services/crawler/ +services/rag/ +services/db/ +services/proxy/ +services/sandbox/ +services/sandbox-egress/ +services/sandbox-runtime/ +packages/tale_knowledge/ +packages/tale_shared/ +packages/tale_telemetry/ +tools/ +examples/ + +# `bun install` needs every workspace package.json present at its declared +# path so the workspace graph resolves. Re-include just the manifests — +# source trees stay excluded by the rules above. +!services/platform/package.json +!services/web/package.json +!services/crawler/package.json +!services/rag/package.json +!services/db/package.json +!services/proxy/package.json +!services/sandbox/package.json +!packages/tale_knowledge/package.json +!packages/tale_shared/package.json +!packages/tale_telemetry/package.json +!tools/cli/package.json +!tools/plop/package.json diff --git a/services/platform/Dockerfile b/services/platform/Dockerfile index dc544ef2c..916f66767 100644 --- a/services/platform/Dockerfile +++ b/services/platform/Dockerfile @@ -31,6 +31,7 @@ COPY services/crawler/package.json ./services/crawler/ COPY services/rag/package.json ./services/rag/ COPY services/db/package.json ./services/db/ COPY services/proxy/package.json ./services/proxy/ +COPY services/sandbox/package.json ./services/sandbox/ COPY services/web/package.json ./services/web/ COPY services/docs/package.json ./services/docs/ COPY tools/cli/package.json ./tools/cli/ @@ -127,6 +128,7 @@ COPY --from=workspace-deps /app/services/crawler/package.json /tmp/workspace/ser COPY --from=workspace-deps /app/services/rag/package.json /tmp/workspace/services/rag/ COPY --from=workspace-deps /app/services/db/package.json /tmp/workspace/services/db/ COPY --from=workspace-deps /app/services/proxy/package.json /tmp/workspace/services/proxy/ +COPY --from=workspace-deps /app/services/sandbox/package.json /tmp/workspace/services/sandbox/ COPY --from=workspace-deps /app/services/web/package.json /tmp/workspace/services/web/ COPY --from=workspace-deps /app/services/docs/package.json /tmp/workspace/services/docs/ COPY --from=workspace-deps /app/tools/cli/package.json /tmp/workspace/tools/cli/ @@ -214,6 +216,16 @@ ENV NODE_ENV=production \ HOSTNAME="0.0.0.0" \ # Convex service DNS name (compose-internal). Overridable via CONVEX_URL. CONVEX_URL=http://convex:3210 \ + # Origin that the sandbox spawner uses to POST presigned-URL output + # uploads back to Convex. Read by Convex Node actions via process.env + # in toSandboxStorageUrl() (see convex/lib/helpers/public_storage_url.ts). + # Node actions only see vars that this container's entrypoint pushes + # into Convex's deployment env via `convex env set`, so baking the + # value into the platform image is what guarantees the rewrite has + # a reachable origin on every docker deploy. Direct to convex:3210 + # rather than the Caddy proxy because Caddy is HTTPS-only with a + # self-signed cert and would 308-redirect plain HTTP POSTs. + SANDBOX_STORAGE_INTERNAL_BASE_URL=http://convex:3210 \ # INSTANCE_NAME is shared with convex service; platform uses it + INSTANCE_SECRET # to compute the admin key for `bunx convex env set` and `bunx convex deploy`. INSTANCE_NAME=tale_platform \ @@ -270,6 +282,7 @@ ENV NODE_ENV=production \ PORT=3000 \ HOSTNAME="0.0.0.0" \ CONVEX_URL=http://convex:3210 \ + SANDBOX_STORAGE_INTERNAL_BASE_URL=http://convex:3210 \ INSTANCE_NAME=tale_platform \ DO_NOT_TRACK=1 \ TALE_CONFIG_DIR=/app/data diff --git a/services/platform/Dockerfile.dockerignore b/services/platform/Dockerfile.dockerignore index 75d367ec6..cd0562e35 100644 --- a/services/platform/Dockerfile.dockerignore +++ b/services/platform/Dockerfile.dockerignore @@ -133,3 +133,9 @@ services/db/ !services/db/package.json services/proxy/ !services/proxy/package.json +services/sandbox/ +!services/sandbox/package.json +services/sandbox-egress/ +services/sandbox-runtime/ +services/docs/ +!services/docs/package.json diff --git a/services/platform/app/features/chat/components/canvas/artifact-bar.tsx b/services/platform/app/features/chat/components/canvas/artifact-bar.tsx index 6e1e2db73..3d5c45c98 100644 --- a/services/platform/app/features/chat/components/canvas/artifact-bar.tsx +++ b/services/platform/app/features/chat/components/canvas/artifact-bar.tsx @@ -3,32 +3,15 @@ import { Badge } from '@tale/ui/badge'; import { Button } from '@tale/ui/button'; import { useQuery } from 'convex/react'; -import { - Code, - FileText, - GitBranch, - Globe, - Image as ImageIcon, - Loader2, -} from 'lucide-react'; -import { memo, useEffect, useRef, type ComponentType } from 'react'; +import { Loader2 } from 'lucide-react'; +import { memo, useEffect, useRef } from 'react'; import { api } from '@/convex/_generated/api'; import type { ArtifactListItem } from '@/convex/artifacts/queries'; import { useT } from '@/lib/i18n/client'; -import { useCanvas, type CanvasContentType } from './canvas-context'; - -const TYPE_ICONS: Record< - CanvasContentType, - ComponentType<{ className?: string }> -> = { - code: Code, - html: Globe, - mermaid: GitBranch, - svg: ImageIcon, - markdown: FileText, -}; +import { useCanvas } from './canvas-context'; +import { CANVAS_TYPE_ICONS } from './icon-map'; interface ArtifactBarProps { organizationId: string; @@ -46,7 +29,7 @@ function ArtifactBarComponent({ organizationId, threadId }: ArtifactBarProps) { // Pull focus to each newly-created artifact exactly once. If the AI calls // artifact_create multiple times in a turn, we follow whichever one // appeared most recently — ChatGPT-Canvas behaviour. We key off - // `createdAt` (immutable) so an artifact_edit revision does not + // `createdAt` (immutable) so a subsequent artifact_file_update revision does not // re-trigger the switch; the existing `useQuery` subscription updates // the open canvas in place. const autoOpenedRef = useRef(new Set()); @@ -74,7 +57,7 @@ function ArtifactBarComponent({ organizationId, threadId }: ArtifactBarProps) { {t('artifacts.barTitle')} {artifacts.map((artifact) => { - const Icon = TYPE_ICONS[artifact.type]; + const Icon = CANVAS_TYPE_ICONS[artifact.type]; const isStreaming = artifact.liveStreamMode !== undefined; const isOpen = openArtifactId === artifact._id; return ( @@ -92,9 +75,11 @@ function ArtifactBarComponent({ organizationId, threadId }: ArtifactBarProps) {