Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 7062d9c02d fix(test-e2e): report the abort instead of dying on it
An aborted Playwright run - watchdog SIGTERM, task timeout - returns a result dict with neither stdout nor rc. The reporting task dereferenced both, so it became the fatal error itself: the log ended on "object of type 'dict' has no attribute 'stdout'" while the real cause, a disk-exhaustion abort with rc 75, sat eight thousand lines earlier. Guard all three reads, fall back to msg for stderr because that is what Ansible fills on an abort, and treat an absent rc as a failure so the run is recorded and named by the aggregate check rather than vanishing behind a template error.

The distro sweep's summary also mislabelled the distros it never started: they kept the initial "not reached within the budget" note even when the sweep aborted on a failure with 12214 seconds of budget left, pointing at a budget problem that did not exist. Say what happened instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:08:51 +02:00
kevinveenbirkenbachandClaude Opus 5 1d1d47a335 fix(act): let the requested distro survive into the nested sweep
act merges its --env-file, which defaults to .env, after the workflow-level env block (pkg/runner/run_context.go:84 at v0.2.89) and interpolates only afterwards, so INFINITO_DISTROS: ${{ inputs.distros }} was replaced by the repository's own five-distro list before the expression was ever evaluated. Every workspace leg therefore swept all five distros instead of its own: 25 distro deploys where 5 were intended, which is what walked the five legs into their 345-minute cap. The drill step's name kept rendering the single distro because it is interpolated from the workflow model rather than from the env map, which is why the logs read as a contradiction.

Pass INFINITO_DISTROS as an --env pair instead, the layer act itself wins with, where --env beats the env-file because the file only fills keys that are absent (cmd/root.go:364-366). The expression was duplicated across ACT_ENV and ACT_INPUTS, so name it once.

Wrapping the assignment for readability required the splitter to tolerate whitespace: inside double quotes the shell removes the backslash-newline but keeps the continuation line's indentation, so every pair after the first arrived with a leading tab.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:08:51 +02:00
kevinveenbirkenbachandClaude Opus 5 914d90cd8d fix(swarm): make the forced recreation survive a paused update and prove it worked
The new handler had neither retries nor an assertion. update_config carries no failure_action, so Docker's default is pause: one flapping healthcheck inside the monitor window pauses the rolling update, docker service update exits non-zero, pipefail propagates it and any_errors_fatal kills the whole deploy - where the neighbouring converge handler would have retried. Give it five attempts fifteen seconds apart, matching how the registry-sync handler beside it already treats a transient action.

The registration probe only notified; nothing checked that the recreation actually registered the plugin. A plugin that can never register - corrupt pnpm tree, engine mismatch, invalid library code - therefore reported changed on every run, recreated the stack every time and still left the play green. Flush the recreation, re-resolve the container id the recreation invalidated, and assert the registration with a bounded wait, so that case fails loudly in the run where it happens instead of churning forever in silence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:08:51 +02:00
kevinveenbirkenbachandClaude Opus 5 36cd39533a fix(ci): stop reporting success for work that did not happen
The pipeline gate only listed twelve of seventeen jobs in its needs, leaving out wait-fork-prereq-run, fork-prereqs-ready, build-ci-images, images-mirror-missing and test-dns. A failure in any of them turns its dependents into skipped rather than failure, and the gate's check matches only failure and cancelled, so the run would have concluded green without images ever being built.

A caller-supplied whitelist was also passed through verbatim. Run 30492550883 was dispatched with web-app-prometheu; discovery dropped the unknown id without a warning, the empty resolution became [], the deploy workflows skipped their whole matrix on that condition and the parent job still concluded success. Validate every id against roles/<id> and exit naming the offenders, so a typo cannot quietly remove an application from a run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:08:51 +02:00
kevinveenbirkenbachandClaude Opus 5 cfd6f08795 fix(swarm): recreate tasks so in-container changes reach the process
docker stack deploy leaves tasks running when the service spec is unchanged, so a change made inside a container never reaches the process. PeerTube's CLI installs plugins with register:false, so the OIDC plugin sat installed and enabled in the database while the running server had loaded nothing: /login rendered only the local form and both OIDC specs timed out through all three attempts. Compose never showed it because its handler force-recreates.

Add a container recreate notify. In swarm it forces a task recreation per stack service and waits for convergence; in compose it maps onto the existing --force-recreate, so a role states the intent once and both modes honour it.

In the peertube role the install probe now tests node_modules/<npm-name>, where pnpm puts the plugin code; the old path was the data directory that registerPlugin creates at boot, so it never existed and the plugin was reinstalled on every run. The settings upsert derives changed from the rowcount via RETURNING, because database_query reports changed from the kind of statement rather than from what it touched. A registration probe notifies when the running process has not loaded the plugin, so an already-broken instance heals itself instead of walking past both earlier probes. The plugin:enable task is gone: npm has no such script, the call always failed and failed_when swallowed it.

The spec now polls the button lookup instead of counting once twenty milliseconds after goto, while still asserting on the URL so a failure keeps printing the address the browser actually sat on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:08:50 +02:00
kevinveenbirkenbachandClaude Opus 5 8aa251d0ac fix(worktree): derive a persistent base and slot the lab node IPs
The default base was /tmp, which is tmpfs on this host: a reboot took a whole worktree and its uncommitted work with it. Derive the base from the origin remote instead, as ~/.local/share/worktrees/<domain>/<account>/<repo>, and let the branch keep its slashes so it nests, which also makes the infinito-wt- prefix redundant. Without an origin remote it now fails loudly rather than inventing a location.

up.sh bumped only INFINITO_SWARM_LAB_SUBNET per slot while the five swarm lab node IPs stayed at default.env's 192.168.244.x, so every worktree from slot 1 on got a lab network in a different /24 than the nodes it places in it, and collided with the primary checkout's lab. Pin all six values from one octet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:08:50 +02:00
kevinveenbirkenbachandClaude Opus 5 5e1deb9ebb fix(ci): budget the distro sweep under the deploy step cap
The sweep's own governor was sized against the 6h platform kill (19800s) while the deploy step was capped at 300 minutes, so it could never fire: GitHub hard-killed the step mid-distro and reported a run that simply did not fit all distros as a job failure.

Size the ladder instead, 10 and 5 minutes apart: sweep budget 340min < deploy step 350 < job 355 < platform kill 360. The swarm per-leg ceiling moves to 350 so it stops matching the job cap, where it was dead code. The rescue-state dump drops from 10 to 5 minutes so it cannot alone consume the window between the job kill and the platform kill; measured teardown is 10-38 seconds.

A new lint asserts the budget stays under the governing step cap, the lower half of the invariant whose upper half the headroom lint already carried; that lint's ceiling moves from 345 to 355.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:08:50 +02:00
kevinveenbirkenbachandClaude Opus 5 f4bb6f0be6 fix(homeassistant): drop the stale long-lived token before minting a new one
MCP provisioning was idempotent against our own token store only. When a reinstall leaves the hub volume in place but resets the secrets store, the two disagree: the store is empty while Home Assistant still knows the client_name. It then refuses the second long-lived token with {"code": "unknown_error"}, which says nothing about the cause, and the task burned all 30 retries on it.

The mint step now lists the refresh tokens, deletes every entry carrying our client_name, and only then creates the new one. A token whose store entry is gone cannot be used by anything anyway; it just holds the name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:04:29 +02:00
kevinveenbirkenbachandClaude Opus 5 73e633a96d fix(dashboard/e2e): gate the logo spec on asset and wait for the decode
Without web-svc-asset the platform logo URL falls back to clearnet, which an
onion deploy's Tor-proxied browser cannot fetch — naturalWidth stayed 0 while
the URL assertion above it passed. The spec gated on cdn only, so it ran in a
scenario where it cannot hold. The added poll also separates 'never loaded'
from 'measured too early', since toBeVisible says nothing about the bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 13:04:56 +02:00
kevinveenbirkenbachandClaude Opus 5 e5894ee02d fix(web-svc-cdn): create the volumes dir before staging into it
The unconditional build-script copy relied on the gated manifest loop above
it creating the parent as a side effect. When every discovered role is absent
from the inventory that loop skips entirely, and the copy dies with
'Destination directory /opt/compose/cdn/volumes does not exist'. The canonical
creator runs later, via sys-stk-full, and cannot be hoisted because the
compose one-shot build consumes the staged files at stack-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 13:03:16 +02:00
kevinveenbirkenbachandClaude Opus 5 ff5ab69ef4 fix(bootstrap): keep the host-only loop driver out of the container bootstrap
environment-bootstrap runs inside the CI workspace containers, where a kernel
module can never be loaded: three distros lack modprobe entirely and two find
no /lib/modules for the host kernel. The DR drill preflight already names the
target, which is how it is meant to be invoked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 06:47:15 +02:00
kevinveenbirkenbachandClaude Opus 5 f09a05e69c fix(matomo/e2e): drop the unused resolveTimeout import
The spec navigates through gotoOnion and carries no timeout literals of its
own, so the import had no consumer and failed eslint's no-unused-vars, which
took the whole CI lint job down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 06:47:15 +02:00
kevinveenbirkenbachandClaude Opus 5 e308e18923 fix(container-dns): advertise the bridge resolver only where a listener exists
TOR_CONTAINER_DNS_HOST is empty in swarm mode by design, so no dnsmasq
listener appears on the docker bridge there. The lookup gated on the tor
group alone and wrote 172.17.0.1 into daemon.json anyway, handing every
nested container a resolver nothing answers on — nested image builds then
died resolving dl-cdn.alpinelinux.org.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 06:47:14 +02:00
kevinveenbirkenbach cf6e7f75e9 Merge remote-tracking branch 'fork/feature/swarm-nfs' into feature/ai-agent-employees 2026-07-29 23:29:03 +02:00
kevinveenbirkenbachandClaude Opus 5 b1d76f2ea5 fix(lookup): take the internal port for an MCP endpoint, always
The endpoint port resolver preferred ports.local and only reached for ports.internal when the block declared exposure: internal. Its single consumer builds a container-network URL (http://<service_key>:<port><path>), where a host-published port is always the wrong one. A server role that omits exposure would therefore have silently received the host port, which is the same class of silent-wrong-value the exposure handling was added to remove.

The order is now internal first, unconditionally, and the exposure argument is gone. All three MCP roles declare exposure: internal, so nothing rendered changes today. exposure stays part of the MCP schema, where it documents who may reach the endpoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:28:16 +02:00
kevinveenbirkenbachandClaude Opus 5 3cd3897091 fix(swarm): point swarm-clean at the renamed unmount helper
The merge of the swarm-nfs work moved utils/unmount_nfs_mounts.sh to utils/unmount/nfs_mounts.sh and updated teardown.sh and the bootstrap, but clean/all.sh kept the old path. make swarm-clean died at the unmount step with "No such file or directory" before it reached container removal, which is exactly the state that needs cleaning up. The host-state release is not added here because all.sh already unmounts DIR_VAR_LIB itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:27:31 +02:00
kevinveenbirkenbach 5dbbaca4a1 Merge branch 'feature/swarm-nfs' of github.com:kevinveenbirkenbach/infinito-nexus-core into feature/svc-net-tor 2026-07-29 23:23:56 +02:00
kevinveenbirkenbachandClaude Opus 5 a41283482c fix(ci-image): build node images locally under act instead of resolving ghcr
act fills the GITHUB_REPOSITORY* vars with its own placeholders, so the
existing owner guard passes and the resolver hands back ghcr.io/nektos/act/
<distro>. That path does not exist and the swarm node build died on a 403
three minutes into every local run. Returning nothing is what the callers
already treat as 'build it locally'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:55 +02:00
kevinveenbirkenbachandClaude Opus 5 c6133c157d docs(gitea): point the authentication link at its current location
Gitea moved the authentication docs from usage/ to administration/; the old path returns 404 and failed the URL reachability test. Probed the candidates: usage/authentication and usage/authentication/ both 404, administration/authentication 308s to administration/authentication/ which answers 200 -- use the 200 form so the result does not depend on whether the checker follows redirects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:55 +02:00
kevinveenbirkenbachandClaude Opus 5 367bb1c7bc fix(swarm): drop the shell-side default from the node-image log line
f660dd6bd wrote the placeholder into a parameter expansion, `${INFINITO_IMAGE:-<none, bootstrap will build it>}`, which tests/lint/filesystem/shell/test_no_infinito_defaults.py rejects: an INFINITO_* default in a shell file is a second source next to default.env that drifts silently, and only the empty form is allowed. It was the sole failure of test-lint.

Split the message into the resolved and the unresolved case instead of suppressing the lint; the output is also no longer self-contradictory, since 'published <none>' could not both be true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:55 +02:00
kevinveenbirkenbachandClaude Opus 5 97995c76df fix(ci-image): build node images locally under act instead of resolving ghcr
act fills the GITHUB_REPOSITORY* vars with its own placeholders, so the
existing owner guard passes and the resolver hands back ghcr.io/nektos/act/
<distro>. That path does not exist and the swarm node build died on a 403
three minutes into every local run. Returning nothing is what the callers
already treat as 'build it locally'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:55 +02:00
kevinveenbirkenbachandClaude Opus 5 bc98f16c07 docs(ci): realign the workflow table with the actual inputs
Three rows disagreed with their workflow files: ci-orchestrator still advertised a distros default that 0f7334b3d removed, test-deploy-swarm omitted distros entirely although it is its only required input, and test-instructions listed one input with a svc-db-postgres default where the workflow declares four on both triggers and picks a random role when role is empty. test-dns is left alone -- its 'required on call; default debian on manual' already describes the real split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:54 +02:00
kevinveenbirkenbachandClaude Opus 5 eac758f606 fix(swarm): declare the node-image tag as a pre-flight requirement
f660dd6bd introduced `${INFINITO_IMAGE_TAG:?}` in the image resolution, a hard requirement the replaced call deliberately did not have -- ci.sh exits 0 before its own assert when no registry owner is in scope. The check sat after the EXIT trap was armed and before the env layer is sourced, so a missing tag ran collect_and_teardown against a cluster that was never created, and the variable appeared in neither the # Param header nor the strict guard block that carries the other four required inputs.

Move it into that guard block, which runs before mktemp and before the trap, and drop the now-redundant inline assert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:54 +02:00
kevinveenbirkenbachandClaude Opus 5 ba31cd9b77 fix(worktree): prune only what is provably gone
841a5e0e1 claimed to keep entries whose checkout cannot be inspected, but the check resolved dirname(dirname(gitdir)) -- the checkout's PARENT, e.g. /tmp -- which always exists. The branch was dead code and every entry fell through to `rm -f gitdir HEAD`, including a live worktree whose metadata git can no longer read: that path drops it from the liveness list and it was then unregistered, which is the reported symptom.

Make the checkout directory itself the criterion, and refuse to conclude anything when its parent is not traversable -- `test -d` succeeds on a directory without +x, so a first attempt using -d alone reproduced the same blind spot. Reproduced in throwaway repos: a live worktree with a 0-byte commondir keeps its registration, a deleted checkout is pruned, and a metadata corpse without gitdir is cleaned.

Remaining limit: `git worktree prune`, which the script runs first, shares the blind spot and wins where it can unlink; this guard therefore only decides in the sandbox-pinned case that produced the bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:54 +02:00
kevinveenbirkenbachandClaude Opus 5 e2474c2732 fix(ci): let a failing distro resolver abort the workspace step
Both steps resolved their value as `echo "X=$(python3 ...)"`. A command substitution's exit status is discarded when it is only an argument, so echo's 0 won: the step stayed green and wrote an empty value. Measured against the real module -- old shape exit 0 with `INFINITO_DISTRO=`, assignment form exit 1 and nothing written. The empty matrix then surfaced as an unexplained fromJson error one job later, and the empty distro as `INFINITO_IMAGE=infinito-` deep inside the docker run, where set -u cannot help because GITHUB_ENV defined the variable as empty rather than unset.

The prepare step also had no `set` line of its own; it inherited -eo pipefail from the shell wrapper and never had -u.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:54 +02:00
kevinveenbirkenbachandClaude Opus 5 64142d0acb refactor(ci): keep the distro default at the entry workflow only
The distros input carried the full five-distro list as a default on ci-orchestrator and on both triggers of test-deploy-swarm, so a caller that forgot it silently inherited everything -- which is how a single workspace leg ended up drilling all five distros sequentially. test-deploy-compose already required the value; make swarm and the orchestrator match it. Every caller passes distros explicitly today, and entry-manual keeps the only default a human ever sees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:54 +02:00
kevinveenbirkenbachandClaude Opus 5 b3c3b6f5fd fix(ci): run each workspace leg against its own distro and image
Every workspace leg pinned INFINITO_DISTRO=debian and drilled all five distros sequentially, pulling its swarm nodes from ghcr <distro>:latest -- an image from 2026-03-21 whose nfs-ganesha 7.2 segfaults on any accepted TCP connection on 2049. The controller mount then failed with ECONNREFUSED for 26 minutes and took run 30446813110 down after 5h13m, with 3 of 5 distros never reached.

Derive the distro from the leg's own matrix entry, build that image, and drill only it: swarm-zombie takes an optional distros= that defaults to INFINITO_DISTRO, and 00_one.sh prefers the locally built image over the published one. 12_swarm.sh refuses to start when that image is missing, so a broken build fails loudly instead of being masked by a stale registry pull.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:54 +02:00
kevinveenbirkenbachandClaude Opus 5 be7e1eeeed refactor(ci): resolve the dev-runtime matrix through a named module
The workspace workflow asked the distro SPOT two questions through embedded python one-liners: the matrix image list, and the distro behind a matrix entry. Give both a name -- distro_of_dev_runtime_image() as the counterpart to dev_runtime_image(), and a cli.meta.ci.dev_runtime module shaped like its image_ref neighbour. The empty-SPOT guard that protected the matrix from silently skipping every job moves along with it. No caller yet; the workflow switches over in the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:53 +02:00
kevinveenbirkenbachandClaude Opus 5 2bdeebc9e5 fix(worktree): stop prune from unregistering live worktrees
Staleness was decided by reading gitdir inside the metadata dir. Under the agent sandbox that dir is pinned as read-only bind mounts, so a live worktree can read as having no gitdir at all -- prune then removed gitdir and HEAD and left the checkout as 'fatal: not a git repository' with no way back. Decide liveness from git's own worktree list plus each checkout's .git pointer instead, keep entries whose checkout base is not visible rather than guessing, drop the expected EBUSY noise, and report the pinned leftovers once with the command that clears them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:23:53 +02:00
kevinveenbirkenbachandClaude Opus 5 4a5a9e2c24 docs(gitea): point the authentication link at its current location
Gitea moved the authentication docs from usage/ to administration/; the old path returns 404 and failed the URL reachability test. Probed the candidates: usage/authentication and usage/authentication/ both 404, administration/authentication 308s to administration/authentication/ which answers 200 -- use the 200 form so the result does not depend on whether the checker follows redirects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:09:21 +02:00
kevinveenbirkenbachandClaude Opus 5 b62b09aece fix(swarm): drop the shell-side default from the node-image log line
f660dd6bd wrote the placeholder into a parameter expansion, `${INFINITO_IMAGE:-<none, bootstrap will build it>}`, which tests/lint/filesystem/shell/test_no_infinito_defaults.py rejects: an INFINITO_* default in a shell file is a second source next to default.env that drifts silently, and only the empty form is allowed. It was the sole failure of test-lint.

Split the message into the resolved and the unresolved case instead of suppressing the lint; the output is also no longer self-contradictory, since 'published <none>' could not both be true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:09:21 +02:00
kevinveenbirkenbachandClaude Opus 5 92ddeb0750 fix(ci-image): build node images locally under act instead of resolving ghcr
act fills the GITHUB_REPOSITORY* vars with its own placeholders, so the
existing owner guard passes and the resolver hands back ghcr.io/nektos/act/
<distro>. That path does not exist and the swarm node build died on a 403
three minutes into every local run. Returning nothing is what the callers
already treat as 'build it locally'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:31 +02:00
kevinveenbirkenbachandClaude Opus 5 f6c5f38760 docs(ci): realign the workflow table with the actual inputs
Three rows disagreed with their workflow files: ci-orchestrator still advertised a distros default that 0f7334b3d removed, test-deploy-swarm omitted distros entirely although it is its only required input, and test-instructions listed one input with a svc-db-postgres default where the workflow declares four on both triggers and picks a random role when role is empty. test-dns is left alone -- its 'required on call; default debian on manual' already describes the real split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:31 +02:00
kevinveenbirkenbachandClaude Opus 5 170efd1796 fix(swarm): declare the node-image tag as a pre-flight requirement
f660dd6bd introduced `${INFINITO_IMAGE_TAG:?}` in the image resolution, a hard requirement the replaced call deliberately did not have -- ci.sh exits 0 before its own assert when no registry owner is in scope. The check sat after the EXIT trap was armed and before the env layer is sourced, so a missing tag ran collect_and_teardown against a cluster that was never created, and the variable appeared in neither the # Param header nor the strict guard block that carries the other four required inputs.

Move it into that guard block, which runs before mktemp and before the trap, and drop the now-redundant inline assert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:30 +02:00
kevinveenbirkenbachandClaude Opus 5 005775a333 fix(worktree): prune only what is provably gone
841a5e0e1 claimed to keep entries whose checkout cannot be inspected, but the check resolved dirname(dirname(gitdir)) -- the checkout's PARENT, e.g. /tmp -- which always exists. The branch was dead code and every entry fell through to `rm -f gitdir HEAD`, including a live worktree whose metadata git can no longer read: that path drops it from the liveness list and it was then unregistered, which is the reported symptom.

Make the checkout directory itself the criterion, and refuse to conclude anything when its parent is not traversable -- `test -d` succeeds on a directory without +x, so a first attempt using -d alone reproduced the same blind spot. Reproduced in throwaway repos: a live worktree with a 0-byte commondir keeps its registration, a deleted checkout is pruned, and a metadata corpse without gitdir is cleaned.

Remaining limit: `git worktree prune`, which the script runs first, shares the blind spot and wins where it can unlink; this guard therefore only decides in the sandbox-pinned case that produced the bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:30 +02:00
kevinveenbirkenbachandClaude Opus 5 c991a3ac2a fix(ci): let a failing distro resolver abort the workspace step
Both steps resolved their value as `echo "X=$(python3 ...)"`. A command substitution's exit status is discarded when it is only an argument, so echo's 0 won: the step stayed green and wrote an empty value. Measured against the real module -- old shape exit 0 with `INFINITO_DISTRO=`, assignment form exit 1 and nothing written. The empty matrix then surfaced as an unexplained fromJson error one job later, and the empty distro as `INFINITO_IMAGE=infinito-` deep inside the docker run, where set -u cannot help because GITHUB_ENV defined the variable as empty rather than unset.

The prepare step also had no `set` line of its own; it inherited -eo pipefail from the shell wrapper and never had -u.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:30 +02:00
kevinveenbirkenbachandClaude Opus 5 9d72aee69c refactor(ci): keep the distro default at the entry workflow only
The distros input carried the full five-distro list as a default on ci-orchestrator and on both triggers of test-deploy-swarm, so a caller that forgot it silently inherited everything -- which is how a single workspace leg ended up drilling all five distros sequentially. test-deploy-compose already required the value; make swarm and the orchestrator match it. Every caller passes distros explicitly today, and entry-manual keeps the only default a human ever sees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:30 +02:00
kevinveenbirkenbachandClaude Opus 5 82f2cb8652 fix(ci): run each workspace leg against its own distro and image
Every workspace leg pinned INFINITO_DISTRO=debian and drilled all five distros sequentially, pulling its swarm nodes from ghcr <distro>:latest -- an image from 2026-03-21 whose nfs-ganesha 7.2 segfaults on any accepted TCP connection on 2049. The controller mount then failed with ECONNREFUSED for 26 minutes and took run 30446813110 down after 5h13m, with 3 of 5 distros never reached.

Derive the distro from the leg's own matrix entry, build that image, and drill only it: swarm-zombie takes an optional distros= that defaults to INFINITO_DISTRO, and 00_one.sh prefers the locally built image over the published one. 12_swarm.sh refuses to start when that image is missing, so a broken build fails loudly instead of being masked by a stale registry pull.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:30 +02:00
kevinveenbirkenbachandClaude Opus 5 3257376fce refactor(ci): resolve the dev-runtime matrix through a named module
The workspace workflow asked the distro SPOT two questions through embedded python one-liners: the matrix image list, and the distro behind a matrix entry. Give both a name -- distro_of_dev_runtime_image() as the counterpart to dev_runtime_image(), and a cli.meta.ci.dev_runtime module shaped like its image_ref neighbour. The empty-SPOT guard that protected the matrix from silently skipping every job moves along with it. No caller yet; the workflow switches over in the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:30 +02:00
kevinveenbirkenbachandClaude Opus 5 67ddb8c4b0 fix(worktree): stop prune from unregistering live worktrees
Staleness was decided by reading gitdir inside the metadata dir. Under the agent sandbox that dir is pinned as read-only bind mounts, so a live worktree can read as having no gitdir at all -- prune then removed gitdir and HEAD and left the checkout as 'fatal: not a git repository' with no way back. Decide liveness from git's own worktree list plus each checkout's .git pointer instead, keep entries whose checkout base is not visible rather than guessing, drop the expected EBUSY noise, and report the pinned leftovers once with the command that clears them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:47:29 +02:00
evangelostsak 93ea22b933 Merge remote-tracking branch 'origin/main' into feature/svc-net-wireguard 2026-07-29 22:44:32 +02:00
kevinveenbirkenbachandClaude Opus 5 accb7a33c5 style(swarm-ci): justify the trap-local '|| true' the merge brought in
The swarm branch predates the shell-or-true lint, so its EXIT-trap collectors
arrived unmarked. They are the legitimate case: best-effort diagnostics and
teardown that must not mask the original exit code, which the trailing
return "${rc}" preserves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:02:15 +02:00
kevinveenbirkenbachandClaude Opus 5 b486ebb608 fix(swarm-backup): release a stale device-mapper before opening the drill USB
The mapper name is host-global and nothing closes it once a drill succeeds:
the only teardowns sit inside the recover step and in the post-luksOpen
error path. A preserved cluster therefore hands the next round a live
'usbdrill' and cryptsetup refuses with 'Device usbdrill already exists'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 16:34:00 +02:00
kevinveenbirkenbach 3f84626883 Merge branch 'feature/swarm-nfs' of github.com:kevinveenbirkenbach/infinito-nexus-core into feature/svc-net-tor 2026-07-29 16:30:43 +02:00
kevinveenbirkenbach d7551061bf Merge remote-tracking branch 'fork/feature/swarm-nfs' into feature/ai-agent-employees
# Conflicts:
#	scripts/tests/deploy/swarm/routine/01_bootstrap.sh
#	scripts/tests/deploy/swarm/utils/clean/teardown.sh
2026-07-29 15:59:54 +02:00
kevinveenbirkenbachandClaude Opus 5 bc8f0ba30e docs(032): record gVisor as the delivered isolation and why Firecracker is not
The requirement named Kata with the Firecracker hypervisor. That cannot be delivered on Docker Engine: Firecracker needs the container rootfs as a block device through the containerd devmapper snapshotter, while dockerd always hands the runtime a directory. Kata Firecracker driver advertises no filesystem sharing, and Kata own documentation tests Docker with QEMU. Reaching a microVM means moving the agent tier off dockerd onto containerd with devmapper, which is a separate decision, so the microVM clauses are marked as intent rather than shipped mechanism.

What ships is gVisor: runtime runsc, a userspace guest kernel intercepting syscalls before the host sees them. Decision 4 turns from a claim into an enforced one, matching the implementation: compose reads the runtime the container actually received and refuses runc, swarm refuses the sandbox label while the node default is still runc, and a lint refuses privileged containers, host namespaces and the docker socket in any role declaring the sandbox. Decision 3 gains the inverse placement constraint, because a node-wide default runtime applies to everything scheduled there. Decision 5 states arm64 as coverage rather than exclusivity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:53:51 +02:00
kevinveenbirkenbachandClaude Opus 5 7f295ce959 fix(hermes): flush the stack handlers before reading the container runtime
include_role sys-stk-full only notifies the compose handler, so the container starts at the handler flush. The isolation assert reads HostConfig.Runtime through container inspect and ran before the container existed. openclaw and homeassistant carry the same assert and hit the same failure earlier in this branch.

The flush comes from the platform parameter rather than a standalone meta task, which also runs the post-deploy steps at the right point. openclaw and homeassistant cannot use the parameter as they stand: both render a config template between the include and the assert, so their container has to start after the render, not at the end of the include.

The MCP TODO records the current state instead of the original stub: discovery is done and openclaw consumes it, while Hermes registers servers through hermes mcp add into its own config store, for which upstream documents neither a file nor an environment contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:52:33 +02:00
kevinveenbirkenbachandClaude Opus 5 92c45e2471 refactor(openwebui): serve models through the gateway, not the model server
Open WebUI talked to Ollama directly. It now speaks the OpenAI-compatible surface of svc-ai-litellm with a per-consumer virtual key, so a second backend or an external provider becomes a gateway config change instead of a role change. A lint keeps future roles off the model servers.

The gateway advertised a hardcoded model list (ollama/llama3, ollama/nomic-embed-text) while the matrix variant preloads smollm2:135m, so it offered only models the backend did not have and hid the one it did. The list is derived from the ollama preload SPOT now.

The chat spec followed the rename: it gates on litellm instead of the removed ollama service flag, and picks the first non-embedding model from /api/models rather than pinning a variant-specific name, so it exercises the chain rather than a fixture.

Routing openwebui through the gateway pulls both model backends into the closure of every role that consumes it, which pushed nextcloud variant 2 over the memory budget. The budget stays at 32/64 GB; instead the matrix variants of svc-ai-ollama and svc-ai-lmstudio now declare the size their test model needs (1g/2g) while the base keeps the 6g/8g that llama3 and mistral require. The budget guard only saw half of that: it replaced the evaluated role with its variant but counted every dependency from meta/services.yml, a combination no matrix round deploys. It now evaluates every variant-carrying role at the same round index, falling back to variant 0, the way the deploy planner does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:49:59 +02:00
kevinveenbirkenbachandClaude Opus 5 e3481b7745 feat(openclaw): reach the declared MCP servers and prove it
MCP_DISCOVERED_SERVERS resolves every server-direction role to its internal URL, the bearer the server role minted, and its transport, dropping any server without a token so no client renders a connection it cannot authenticate.

OpenClaw moves from 2026.3.11 to 2026.7.1 because the pinned version has no configuration surface for outbound MCP servers at all: its validator rejects mcpServers, the shipped bundle carries no bundle-mcp, and the ACP path refuses per-session servers while pointing at a gateway config that does not exist there. 2026.7.1 reads mcp.servers, which the template now renders. The gateway command gains its run subcommand because the bare gateway no longer starts anything.

Reachability was the missing half: openclaw sat on its own networks only, so the hub name resolved through the DNS wildcard to the ingress instead of the container. The hub overlay now declares an mcp consumer key, which attaches every role carrying services.mcp.

A probe task runs openclaw mcp probe after the handlers flush and fails the deploy when a configured server does not answer. Without it the previous run reported failed=0 while the client could not reach anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:40:19 +02:00
kevinveenbirkenbachandClaude Opus 5 9164dcda4c feat(homeassistant): serve MCP and persist the token it mints
The role declared a server-direction MCP service but never provisioned one, and the declared endpoint pointed at /mcp_server/sse, the legacy path. Home Assistant serves MCP under /api/mcp, and its mcp_server integration has no YAML schema, so the config entry only exists through the config-entries flow API.

The provisioning script drives that path over the container loopback: onboard the administrator when the hub is fresh (falling back to the login flow when it is not), mint a long-lived token over the websocket API, then create the mcp_server config entry with it. The token is persisted through sys-token-store, which is why meta/users.yml registers the key; a second role registering a token for the same user only works since the per-key user merge.

The whole path is skipped when a token already exists, so a redeploy costs one lookup. Handlers flush before it runs because the hub container has to exist before anything talks to its API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:39:13 +02:00